Skip to content

Commit b39c018

Browse files
authored
Merge pull request #4 from adhocore/2-line-nos
Prepend line number to the highlighted code
2 parents 7377048 + 19afb8d commit b39c018

File tree

10 files changed

+176
-74
lines changed

10 files changed

+176
-74
lines changed

README.md

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,19 @@ clish -f file.php -o file.png
6363
6464
#### Options
6565

66-
Parameter options.
66+
Parameter options:
6767

6868
```
69-
[-e|--echo] Forces echo to STDOUT when --output is passed
70-
[-f|--file] Input PHP file to highlight and/or export
71-
(will read from piped input if file not given)
72-
[-F|--font] Font to use for export to png
73-
[-h|--help] Show help
74-
[-o|--output] Output filepath where PNG image is exported
75-
[-v|--verbosity] Verbosity level
76-
[-V|--version] Show version
69+
[-e|--echo] Forces echo to STDOUT when --output is passed
70+
[-f|--file] Input PHP file to highlight and/or export
71+
(will read from piped input if file not given)
72+
[-F|--font] Font to use for export to png
73+
[-l|--with-line-no] Highlight with line number
74+
[-o|--output] Output filepath where PNG image is exported
7775
```
7876

77+
> Run `clish -h` to show help.
78+
7979
##### Examples
8080

8181
```sh
@@ -84,6 +84,7 @@ Parameter options.
8484
bin/clish < file.php # from redirected stdin
8585
bin/clish --file file.php --output file.png # export
8686
bin/clish --file file.php --output file.png --echo # print + export
87+
bin/clish --file file.php --with-line-no # print with lineno
8788
bin/clish -f file.php -o file.png -F dejavu # export in dejavu font
8889
```
8990

@@ -99,10 +100,15 @@ use Ahc\CliSyntax\Highlighter;
99100
// PHP code
100101
echo new Highlighter('<?php echo "Hello world!";');
101102
// OR
102-
echo (new Highlighter)->highlight('<?php echo "Hello world!";');
103+
echo (new Highlighter)->highlight('<?php echo "Hello world!";', $options);
103104

104105
// PHP file
105-
echo Highlighter::for('/path/to/file.php');
106+
echo Highlighter::for('/path/to/file.php', $options);
107+
108+
// $options array is optional and can contain:
109+
[
110+
'lineNo' => true, // bool
111+
];
106112
```
107113

108114
#### Export
@@ -111,13 +117,24 @@ echo Highlighter::for('/path/to/file.php');
111117
use Ahc\CliSyntax\Exporter;
112118

113119
// PHP file
114-
Exporter::for('/path/to/file.php')->export('file.png');
120+
Exporter::for('/path/to/file.php')->export('file.png', $options);
121+
122+
// $options array is optional and can contain:
123+
[
124+
'lineNo' => true, // bool
125+
'font' => 'full/path/of/font.ttf', // str
126+
'size' => 'font size', // int
127+
];
115128
```
116129

117130
See [example usage](./example.php). Here's how the export looks like:
118131

119132
![adhocore/cli-syntax](./example.png)
120133

134+
---
135+
And with line numbers:
136+
137+
![Example with line numbers](https://imgur.com/Jqiydf8.png)
121138

122139
## Customisation
123140

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"php": ">=7.0.0",
2828
"ext-dom": "*",
2929
"ext-gd": "*",
30-
"adhocore/cli": "^0.7.0"
30+
"adhocore/cli": "^0.8.3"
3131
},
3232
"require-dev": {
3333
"phpunit/phpunit": "^6.5 || ^7.5"

example.png

24 Bytes
Loading

src/Console/ClishCommand.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public function __construct()
2727
$this
2828
->option('-o --output', 'Output filepath where PNG image is exported', null, '')
2929
->option('-e --echo', 'Forces echo to STDOUT when --output is passed', null, '')
30+
->option('-l --with-line-no', 'Highlight with line number')
3031
->option('-f --file', \implode("\n", [
3132
'Input PHP file to highlight and/or export',
3233
'(will read from piped input if file not given)',
@@ -38,6 +39,7 @@ public function __construct()
3839
. '<bold> $0</end> <comment>< file.php</end> ## from redirected stdin<eol/>'
3940
. '<bold> $0</end> <comment>--file file.php --output file.png</end> ## export<eol/>'
4041
. '<bold> $0</end> <comment>--file file.php --output file.png --echo</end> ## print + export<eol/>'
42+
. '<bold> $0</end> <comment>--file file.php --with-line-no</end> ## print with lineno<eol/>'
4143
. '<bold> $0</end> <comment>-f file.php -o file.png -F dejavu</end> ## export in dejavu font<eol/>'
4244
);
4345
}
@@ -78,7 +80,7 @@ public function execute()
7880
protected function doHighlight(string $code = null)
7981
{
8082
if (!$this->output || $this->echo) {
81-
$this->app()->io()->raw((string) new Highlighter($code));
83+
$this->app()->io()->raw((new Highlighter)->highlight($code, ['lineNo' => $this->lineNo]));
8284
}
8385
}
8486

@@ -92,7 +94,7 @@ protected function doExport(string $code = null)
9294
\mkdir(\dirname($this->output), 0755, true);
9395
}
9496

95-
$options = ['font' => $this->fixFont($this->font ?: 'ubuntu')];
97+
$options = ['font' => $this->fixFont($this->font ?: 'ubuntu'), 'lineNo' => $this->lineNo];
9698

9799
(new Exporter($code))->export($this->output, $options);
98100
}

src/Exporter.php

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ class Exporter extends Pretty
3030
/** @var array Colors cached for each types. */
3131
protected $colors = [];
3232

33-
/** @var array Lengths of each line */
34-
protected $lengths = [];
35-
3633
public function __destruct()
3734
{
3835
if (\is_resource($this->image)) {
@@ -48,8 +45,11 @@ public function export(string $output, array $options = [])
4845

4946
$this->setOptions($options);
5047

51-
$this->imgSize = $this->estimateSize($this->code);
52-
$this->image = \imagecreate($this->imgSize['x'] + 50, $this->imgSize['y'] + 25);
48+
$this->imgSize = $this->estimateSize($this->code);
49+
$this->lineCount = \substr_count($this->code, "\n") ?: 1;
50+
51+
$padLineNo = $this->withLineNo ? $this->estimateSize($this->formatLineNo())['x'] : 0;
52+
$this->image = \imagecreate($this->imgSize['x'] + $padLineNo + 50, $this->imgSize['y'] + 25);
5353

5454
\imagecolorallocate($this->image, 0, 0, 0);
5555

@@ -60,6 +60,8 @@ public function export(string $output, array $options = [])
6060

6161
protected function setOptions(array $options)
6262
{
63+
parent::setOptions($options);
64+
6365
if (isset($options['size'])) {
6466
$this->size = $options['size'];
6567
}
@@ -90,28 +92,32 @@ protected function estimateSize(string $for): array
9092
return ['x' => $box[2], 'y' => $box[1], 'y1' => \intval($box[1] / $eol)];
9193
}
9294

93-
protected function reset()
95+
protected function doReset()
9496
{
95-
$this->colors = $this->lengths = [];
97+
$this->colors = [];
9698
}
9799

98100
protected function visit(\DOMNode $el)
99101
{
100-
$lineNo = $el->getLineNo() - 2;
101102
$type = $el instanceof \DOMElement ? $el->getAttribute('data-type') : 'raw';
102103
$color = $this->colorCode($type);
104+
$ncolor = $this->colorCode('lineno');
103105
$text = \str_replace(['&nbsp;', '&lt;', '&gt;'], [' ', '<', '>'], $el->textContent);
104106

105107
foreach (\explode("\n", $text) as $line) {
106-
$lineNo++;
108+
$xlen = $this->lengths[$this->lineNo] ?? 0;
109+
$ypos = 12 + $this->imgSize['y1'] * $this->lineNo;
110+
111+
if ('' !== $lineNo = $this->formatLineNo()) {
112+
$xlen += $this->estimateSize($lineNo)['x'];
113+
\imagefttext($this->image, $this->size, 0, 12, $ypos, $ncolor, $this->font, $lineNo);
114+
}
107115

108-
$xlen = $this->lengths[$lineNo] ?? 0;
109-
$xpos = 12 + $xlen;
110-
$ypos = 12 + $this->imgSize['y1'] * $lineNo;
116+
\imagefttext($this->image, $this->size, 0, 12 + $xlen, $ypos, $color, $this->font, $line);
111117

112-
\imagefttext($this->image, $this->size, 0, $xpos, $ypos, $color, $this->font, $line);
118+
$this->lengths[$this->lineNo] = $xlen + $this->estimateSize($line)['x'];
113119

114-
$this->lengths[$lineNo] = $xlen + $this->estimateSize($line)['x'];
120+
$this->lineNo++;
115121
}
116122
}
117123

@@ -127,6 +133,7 @@ protected function colorCode(string $type): int
127133
'keyword' => [192, 0, 0],
128134
'string' => [192, 192, 0],
129135
'raw' => [128, 128, 128],
136+
'lineno' => [128, 224, 224],
130137
];
131138

132139
return $this->colors[$type] = \imagecolorallocate($this->image, ...$palette[$type]);

src/Highlighter.php

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,53 @@ public function __toString(): string
2323
return $this->highlight();
2424
}
2525

26-
public function highlight(string $code = null): string
26+
public function highlight(string $code = null, array $options = []): string
2727
{
28-
$this->parse($code);
28+
$this->setOptions($options);
29+
30+
$this->parse($code ?? $this->code);
2931

3032
return \trim($this->out, "\n") . "\n";
3133
}
3234

33-
protected function reset()
35+
protected function doReset()
3436
{
3537
$this->out = '';
3638
}
3739

3840
protected function visit(\DOMNode $el)
41+
{
42+
$type = $el instanceof \DOMElement ? $el->getAttribute('data-type') : 'raw';
43+
$text = \str_replace(['&nbsp;', '&lt;', '&gt;'], [' ', '<', '>'], $el->textContent);
44+
45+
$lastLine = 0;
46+
$lines = \explode("\n", $text);
47+
foreach ($lines as $i => $line) {
48+
$this->out .= $this->formatLine($type, $line);
49+
50+
if (isset($lines[$i + 1])) {
51+
$this->out .= "\n";
52+
}
53+
54+
$this->lengths[$this->lineNo++] = \strlen($line);
55+
}
56+
}
57+
58+
protected function formatLine(string $type, string $line)
3959
{
4060
static $formats = [
4161
'comment' => "\033[0;34;40m%s\033[0m",
4262
'default' => "\033[0;32;40m%s\033[0m",
4363
'keyword' => "\033[0;31;40m%s\033[0m",
4464
'string' => "\033[0;33;40m%s\033[0m",
65+
'lineno' => "\033[2;36;40m%s\033[0m",
66+
'raw' => '%s',
4567
];
4668

47-
$type = $el instanceof \DOMElement ? $el->getAttribute('data-type') : 'raw';
48-
$text = \str_replace(['&nbsp;', '&lt;', '&gt;'], [' ', '<', '>'], $el->textContent);
69+
if ('' !== $lineNo = $this->formatLineNo()) {
70+
$lineNo = \sprintf($formats['lineno'], $lineNo);
71+
}
4972

50-
$this->out .= \sprintf($formats[$type] ?? '%s', $text);
73+
return $lineNo . \sprintf($formats[$type], $line);
5174
}
5275
}

src/Pretty.php

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ abstract class Pretty
1818
/** @var string The PHP code. */
1919
protected $code;
2020

21+
/** @var int The current line number. */
22+
protected $lineNo = 0;
23+
24+
/** @var int The total lines count. */
25+
protected $lineCount = 0;
26+
27+
/** @var bool Show line numbers. */
28+
protected $withLineNo = false;
29+
30+
/** @var array Lengths of each line */
31+
protected $lengths = [];
32+
2133
/** @var bool Indicates if it has been already configured. */
2234
protected static $configured;
2335

@@ -48,28 +60,56 @@ public static function configure()
4860
static::$configured = true;
4961
}
5062

63+
protected function setOptions(array $options)
64+
{
65+
if ($options['lineNo'] ?? false) {
66+
$this->withLineNo = true;
67+
}
68+
}
69+
5170
protected function parse(string $code = null)
5271
{
5372
$this->reset();
5473

5574
$dom = new \DOMDocument;
56-
$dom->loadHTML($this->codeToHtml($code));
75+
$dom->loadHTML($this->codeToHtml($code ?? $this->code));
5776

58-
foreach ((new \DOMXPath($dom))->query('/html/body/span')[0]->childNodes as $el) {
77+
$adjust = -1;
78+
foreach ((new \DOMXPath($dom))->query('/html/body/code/span/*') as $el) {
79+
$this->lineNo = $el->getLineNo() + $adjust;
5980
$this->visit($el);
6081
}
6182
}
6283

63-
protected function codeToHtml(string $code = null): string
84+
protected function codeToHtml(string $code): string
6485
{
6586
static::configure();
6687

67-
$html = \highlight_string($code ?? $this->code, true);
88+
$this->lineCount = \substr_count($code, "\n") ?: 1;
89+
90+
$html = \highlight_string($code, true);
91+
92+
return \str_replace(['<br />'], ["\n"], $html);
93+
}
94+
95+
protected function formatLineNo(): string
96+
{
97+
if ($this->withLineNo && $this->lineNo <= $this->lineCount && !isset($this->lengths[$this->lineNo])) {
98+
return \str_pad("$this->lineNo", \strlen("$this->lineCount"), ' ', \STR_PAD_LEFT) . '. ';
99+
}
100+
101+
return '';
102+
}
103+
104+
protected function reset()
105+
{
106+
$this->doReset();
68107

69-
return \str_replace(['<br />', '<code>', '</code>'], ["\n", '', ''], $html);
108+
$this->lengths = [];
109+
$this->lineNo = $this->lineCount = 0;
70110
}
71111

72-
abstract protected function reset();
112+
abstract protected function doReset();
73113

74114
abstract protected function visit(\DOMNode $el);
75115
}

tests/ExporterTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ public function testSetOptions()
5353

5454
public function testSetOptionsFont()
5555
{
56-
Exporter::for(__DIR__ . '/../example.php')->export($this->out, ['font' => __DIR__ . '/../font/dejavu.ttf']);
56+
Exporter::for(__DIR__ . '/../example.php')->export(
57+
$this->out,
58+
['font' => __DIR__ . '/../font/dejavu.ttf', 'lineNo' => true]
59+
);
5760

5861
$this->assertFileExists($this->out, 'It should export with given font');
5962
// $this->assertSame(\file_get_contents($this->ref), \file_get_contents($this->out));

tests/HighlighterTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ public function testHighlightCode()
2626
);
2727
}
2828

29+
public function testHighlightCodeWithLineNo()
30+
{
31+
$code = (new Highlighter)->highlight('<?php echo "Hello world!";', ['lineNo' => true]);
32+
33+
$this->assertContains(
34+
'1. <?php echo "Hello world!";',
35+
$code
36+
);
37+
}
38+
2939
public function testHighlightFile()
3040
{
3141
$code = (string) Highlighter::for(__DIR__ . '/../example.php');

0 commit comments

Comments
 (0)