diff --git a/.github/highlight-4.png b/.github/highlight-4.png new file mode 100644 index 0000000..be9f1ce Binary files /dev/null and b/.github/highlight-4.png differ diff --git a/README.md b/README.md index 4084b4c..064f6ba 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ You can read about why I started this package [here](https://stitcher.io/blog/a- - [Themes](#themes) - [For the web](#for-the-web) - [For the terminal](#for-the-terminal) +- [Gutter](#gutter) - [Special highlighting tags](#special-highlighting-tags) - [Emphasize strong and blur](#emphasize-strong-and-blur) - [Additions and deletions](#additions-and-deletions) @@ -60,6 +61,26 @@ echo $highlighter->parse($code, 'php'); ![](./.github/terminal.png) +## Gutter + +This package can render an optional gutter if needed. + +```php +$highlighter = (new Highlighter())->withGutter(startAt: 10); +``` + +The gutter will show additions and deletions, and can start at any given line number: + +![](./.github/highlight-4.png) + +Finally, you can enable gutter rendering on the fly if you're using [commonmark code blocks](#commonmark-integration) by appending `{startAt}` to the language definition: + +```md +```php{1} +echo 'hi'! +``` +``` + ## Special highlighting tags This package offers a collection of special tags that you can use within your code snippets. These tags won't be shown in the final output, but rather adjust the highlighter's default styling. All these tags work multi-line, and will still properly render its wrapped content. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..2d34ccd --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,30 @@ +## 1.1.0 + +- Added Gutter support: + +```php +$highlighter = (new \Tempest\Highlight\Highlighter())->withGutter(); +``` + +**Note**: three new classes have been added for gutter support. If you copied over an existing theme, you'll need to add these: + +```css +.hl-gutter { + display: inline-block; + font-size: 0.9em; + color: #555; + padding: 0 1ch; +} + +.hl-gutter-addition { + background-color: #34A853; + color: #fff; +} + +.hl-gutter-deletion { + background-color: #EA4334; + color: #fff; +} +``` + +**Note**: This package doesn't account for `pre` tag styling. You might need to make adjustments to how you style `pre` tags if you enable gutter support. \ No newline at end of file diff --git a/src/After.php b/src/After.php new file mode 100644 index 0000000..b8e0694 --- /dev/null +++ b/src/After.php @@ -0,0 +1,15 @@ +getLiteral(); - $language = $node->getInfoWords()[0] ?? 'txt'; + preg_match('/^(?[\w]+)(\{(?[\d]+)\})?/', $node->getInfoWords()[0] ?? 'txt', $matches); + + if ($startAt = ($matches['startAt']) ?? null) { + $this->highlighter->withGutter((int)$startAt); + } return new HtmlElement( 'pre', [], - $highlight->parse($code, $language) + $this->highlighter->parse( + content: $node->getLiteral(), + language: $matches['language'], + ), ); } } diff --git a/src/CommonMark/InlineCodeBlockRenderer.php b/src/CommonMark/InlineCodeBlockRenderer.php index 7634d4b..d5007cc 100644 --- a/src/CommonMark/InlineCodeBlockRenderer.php +++ b/src/CommonMark/InlineCodeBlockRenderer.php @@ -11,8 +11,13 @@ use League\CommonMark\Renderer\NodeRendererInterface; use Tempest\Highlight\Highlighter; -class InlineCodeBlockRenderer implements NodeRendererInterface +final class InlineCodeBlockRenderer implements NodeRendererInterface { + public function __construct( + private Highlighter $highlighter = new Highlighter(), + ) { + } + public function render(Node $node, ChildNodeRendererInterface $childRenderer) { if (! $node instanceof Code) { @@ -21,10 +26,9 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer) preg_match('/^\{(?[\w]+)}(?.*)/', $node->getLiteral(), $match); - $highlighter = new Highlighter(); $language = $match['match'] ?? 'txt'; $code = $match['code'] ?? $node->getLiteral(); - return '' . $highlighter->parse($code, $language) . ''; + return '' . $this->highlighter->parse($code, $language) . ''; } } diff --git a/src/Escape.php b/src/Escape.php index d6b8026..3d13433 100644 --- a/src/Escape.php +++ b/src/Escape.php @@ -28,7 +28,11 @@ public static function injection(string $input): string public static function terminal(string $input): string { - return str_replace(self::INJECTION_TOKEN, '', $input); + return preg_replace( + ['/❷(.*?)❸/', '/❿/'], + '', + $input, + ); } public static function html(string $input): string diff --git a/src/Highlighter.php b/src/Highlighter.php index b9fbb2b..f4013e5 100644 --- a/src/Highlighter.php +++ b/src/Highlighter.php @@ -4,7 +4,10 @@ namespace Tempest\Highlight; +use Generator; +use ReflectionClass; use Tempest\Highlight\Languages\Base\BaseLanguage; +use Tempest\Highlight\Languages\Base\Injections\GutterInjection; use Tempest\Highlight\Languages\Blade\BladeLanguage; use Tempest\Highlight\Languages\Css\CssLanguage; use Tempest\Highlight\Languages\DocComment\DocCommentLanguage; @@ -25,8 +28,9 @@ final class Highlighter { private array $languages = []; + private ?GutterInjection $gutterInjection = null; private ?Language $currentLanguage = null; - private bool $shouldEscape = true; + private bool $isNested = false; public function __construct( private readonly Theme $theme = new CssTheme(), @@ -46,10 +50,18 @@ public function __construct( ->setLanguage('yaml', new YamlLanguage()) ->setLanguage('yml', new YamlLanguage()) ->setLanguage('twig', new TwigLanguage()); + } - if ($this->theme instanceof TerminalTheme) { - $this->shouldEscape = false; - } + public function withGutter(int $startAt = 1): self + { + $this->gutterInjection = new GutterInjection($startAt); + + return $this; + } + + public function getGutterInjection(): ?GutterInjection + { + return $this->gutterInjection; } public function setLanguage(string $name, Language $language): self @@ -85,11 +97,11 @@ public function setCurrentLanguage(Language $language): void $this->currentLanguage = $language; } - public function withoutEscaping(): self + public function nested(): self { $clone = clone $this; - $clone->shouldEscape = false; + $clone->isNested = true; return $clone; } @@ -98,45 +110,103 @@ private function parseContent(string $content, Language $language): string { $tokens = []; - // Injections - foreach ($language->getInjections() as $injection) { - $parsedInjection = $injection->parse($content, $this->withoutEscaping()); - - // Injections are allowed to return one of two things: - // 1. A string of content, which will be used to replace the existing content - // 2. a `ParsedInjection` object, which contains both the new content AND a list of tokens to be parsed - // - // One benefit of returning ParsedInjections is that the list of returned tokens will be added - // to all other tokens detected by patterns, and thus follow all token rules. - // They are grouped and checked on whether tokens can be contained by other tokens. - // This offers more flexibility from the injection's point of view, and in same cases lead to more accurate highlighting. - // - // The other benefit is that injections returning ParsedInjection objects don't need to worry about Escape::injection anymore. - // This escape only exists to prevent outside patterns from matching already highlighted content that's injected. - // If an injection doesn't highlight content anymore, then there also isn't any danger for these kinds of collisions. - // And so, Escape::injection becomes obsolete. - // - // TODO: a future version might only allow ParsedTokens and no more standalone strings, but for now we'll keep it as is. - if (is_string($parsedInjection)) { - $content = $parsedInjection; - } else { - $content = $parsedInjection->content; - $tokens = [...$tokens, ...$parsedInjection->tokens]; - } + // Before Injections + foreach ($this->getBeforeInjections($language) as $injection) { + $parsedInjection = $this->parseInjection($content, $injection); + $content = $parsedInjection->content; + $tokens = [...$tokens, ...$parsedInjection->tokens]; } // Patterns $tokens = [...$tokens, ...(new ParseTokens())($content, $language)]; - $groupedTokens = (new GroupTokens())($tokens); + $content = (new RenderTokens($this->theme))($content, $groupedTokens); - $output = (new RenderTokens($this->theme))($content, $groupedTokens); + // After Injections + foreach ($this->getAfterInjections($language) as $injection) { + $parsedInjection = $this->parseInjection($content, $injection); + $content = $parsedInjection->content; + } // Determine proper escaping - return match(true) { - $this->theme instanceof TerminalTheme => Escape::terminal($output), - $this->shouldEscape => Escape::html($output), - default => $output, + return match (true) { + $this->isNested => $content, + $this->theme instanceof TerminalTheme => Escape::terminal($content), + default => Escape::html($content), }; } + + /** + * @param Language $language + * @return \Tempest\Highlight\Injection[] + */ + private function getBeforeInjections(Language $language): Generator + { + foreach ($language->getInjections() as $injection) { + $after = (new ReflectionClass($injection))->getAttributes(After::class)[0] ?? null; + + if ($after) { + continue; + } + + // Only injections without the `After` attribute are allowed + yield $injection; + } + } + + /** + * @param Language $language + * @return \Tempest\Highlight\Injection[] + */ + private function getAfterInjections(Language $language): Generator + { + if ($this->isNested) { + // After injections are only parsed at the very end + return; + } + + foreach ($language->getInjections() as $injection) { + $after = (new ReflectionClass($injection))->getAttributes(After::class)[0] ?? null; + + if (! $after) { + continue; + } + + yield $injection; + } + + // The gutter is always the latest injection + if ($this->gutterInjection) { + yield $this->gutterInjection; + } + } + + private function parseInjection(string $content, Injection $injection): ParsedInjection + { + $parsedInjection = $injection->parse( + $content, + $this->nested(), + ); + + // Injections are allowed to return one of two things: + // 1. A string of content, which will be used to replace the existing content + // 2. a `ParsedInjection` object, which contains both the new content AND a list of tokens to be parsed + // + // One benefit of returning ParsedInjections is that the list of returned tokens will be added + // to all other tokens detected by patterns, and thus follow all token rules. + // They are grouped and checked on whether tokens can be contained by other tokens. + // This offers more flexibility from the injection's point of view, and in same cases lead to more accurate highlighting. + // + // The other benefit is that injections returning ParsedInjection objects don't need to worry about Escape::injection anymore. + // This escape only exists to prevent outside patterns from matching already highlighted content that's injected. + // If an injection doesn't highlight content anymore, then there also isn't any danger for these kinds of collisions. + // And so, Escape::injection becomes obsolete. + // + // TODO: a future version might only allow ParsedTokens and no more standalone strings, but for now we'll keep it as is. + if (is_string($parsedInjection)) { + return new ParsedInjection($parsedInjection); + } + + return $parsedInjection; + } } diff --git a/src/Languages/Base/BaseLanguage.php b/src/Languages/Base/BaseLanguage.php index 6c02547..a181c5c 100644 --- a/src/Languages/Base/BaseLanguage.php +++ b/src/Languages/Base/BaseLanguage.php @@ -21,9 +21,9 @@ public function getInjections(): array new BlurInjection(), new EmphasizeInjection(), new StrongInjection(), + new CustomClassInjection(), new AdditionInjection(), new DeletionInjection(), - new CustomClassInjection(), ]; } diff --git a/src/Languages/Base/Injections/AdditionInjection.php b/src/Languages/Base/Injections/AdditionInjection.php index 3a5788c..1ad62bf 100644 --- a/src/Languages/Base/Injections/AdditionInjection.php +++ b/src/Languages/Base/Injections/AdditionInjection.php @@ -4,20 +4,56 @@ namespace Tempest\Highlight\Languages\Base\Injections; +use Tempest\Highlight\After; +use Tempest\Highlight\Escape; +use Tempest\Highlight\Highlighter; use Tempest\Highlight\Injection; -use Tempest\Highlight\Languages\Base\IsHighlightInjection; +#[After] final readonly class AdditionInjection implements Injection { - use IsHighlightInjection; - - private function getToken(): string + public function parse(string $content, Highlighter $highlighter): string { - return '+'; - } + preg_match_all('/(\{\+)((.|\n)*?)(\+})/', $content, $matches, PREG_OFFSET_CAPTURE); - private function getClassname(): string - { - return 'hl-addition'; + foreach ($matches[0] as $match) { + $matchedContent = $match[0]; + $offset = $match[1]; + + $open = Escape::tokens(''); + $close = Escape::tokens(''); + + // Replace tags + EOLs with appropriate span tags + $parsedMatchedContent = str_replace( + ['{+', PHP_EOL, '+}'], + [$open, $close . PHP_EOL . $open, $close], + $matchedContent, + ); + + // Inject the parsed match into the content + $content = str_replace($matchedContent, $parsedMatchedContent, $content); + + // Configure the gutter, + if ($gutter = $highlighter->getGutterInjection()) { + $startingLineNumber = substr_count( + haystack: $content, + needle: PHP_EOL, + length: $offset, + ) + 1; + + $totalAmountOfLines = substr_count( + haystack: $parsedMatchedContent, + needle: PHP_EOL, + ) + 1; + + for ($lineNumber = $startingLineNumber; $lineNumber < $startingLineNumber + $totalAmountOfLines; $lineNumber++) { + $gutter + ->addIcon($lineNumber, '+') + ->addClass($lineNumber, 'hl-gutter-addition'); + } + } + } + + return $content; } } diff --git a/src/Languages/Base/Injections/DeletionInjection.php b/src/Languages/Base/Injections/DeletionInjection.php index be1df23..753427b 100644 --- a/src/Languages/Base/Injections/DeletionInjection.php +++ b/src/Languages/Base/Injections/DeletionInjection.php @@ -4,20 +4,56 @@ namespace Tempest\Highlight\Languages\Base\Injections; +use Tempest\Highlight\After; +use Tempest\Highlight\Escape; +use Tempest\Highlight\Highlighter; use Tempest\Highlight\Injection; -use Tempest\Highlight\Languages\Base\IsHighlightInjection; +#[After] final readonly class DeletionInjection implements Injection { - use IsHighlightInjection; - - private function getToken(): string + public function parse(string $content, Highlighter $highlighter): string { - return '-'; - } + preg_match_all('/(\{-)((.|\n)*?)(-})/', $content, $matches, PREG_OFFSET_CAPTURE); - private function getClassname(): string - { - return 'hl-deletion'; + foreach ($matches[0] as $match) { + $matchedContent = $match[0]; + $offset = $match[1]; + + $open = Escape::tokens(''); + $close = Escape::tokens(''); + + // Replace tags + EOLs with appropriate span tags + $parsedMatchedContent = str_replace( + ['{-', PHP_EOL, '-}'], + [$open, $close . PHP_EOL . $open, $close], + $matchedContent, + ); + + // Inject the parsed match into the content + $content = str_replace($matchedContent, $parsedMatchedContent, $content); + + // Configure the gutter, + if ($gutter = $highlighter->getGutterInjection()) { + $startingLineNumber = substr_count( + haystack: $content, + needle: PHP_EOL, + length: $offset, + ) + 1; + + $totalAmountOfLines = substr_count( + haystack: $parsedMatchedContent, + needle: PHP_EOL, + ) + 1; + + for ($lineNumber = $startingLineNumber; $lineNumber < $startingLineNumber + $totalAmountOfLines; $lineNumber++) { + $gutter + ->addIcon($lineNumber, '-') + ->addClass($lineNumber, 'hl-gutter-deletion'); + } + } + } + + return $content; } } diff --git a/src/Languages/Base/Injections/GutterInjection.php b/src/Languages/Base/Injections/GutterInjection.php new file mode 100644 index 0000000..e2b83d9 --- /dev/null +++ b/src/Languages/Base/Injections/GutterInjection.php @@ -0,0 +1,77 @@ +icons[$line + $this->startAt - 1] = $token; + + return $this; + } + + public function addClass(int $line, string $class): self + { + $this->classes[$line + $this->startAt - 1] = $class; + + return $this; + } + + public function parse(string $content, Highlighter $highlighter): string|ParsedInjection + { + $lines = explode(PHP_EOL, trim($content)); + + $gutterNumbers = []; + $longestGutterNumber = ''; + + foreach ($lines as $i => $line) { + $gutterNumber = $i + $this->startAt; + + if ($icon = ($this->icons[$i + $this->startAt] ?? null)) { + $gutterNumber .= ' ' . $icon; + } + + $gutterNumbers[$i] = $gutterNumber; + + if (strlen((string) $longestGutterNumber) < strlen((string) $gutterNumber)) { + $longestGutterNumber = (string) $gutterNumber; + } + } + + $gutterWidth = strlen($longestGutterNumber); + + foreach ($lines as $i => $line) { + $gutterNumber = $gutterNumbers[$i]; + + $gutterClass = 'hl-gutter ' . ($this->classes[$i + $this->startAt] ?? ''); + + $lines[$i] = sprintf( + Escape::tokens('%s %s'), + $gutterClass, + str_pad( + string: (string) $gutterNumber, + length: $gutterWidth, + pad_type: STR_PAD_LEFT, + ), + $line, + ); + } + + return implode(PHP_EOL, $lines); + } +} diff --git a/src/Themes/highlight-dark-lite.css b/src/Themes/highlight-dark-lite.css index a2a32fa..3c9dbd9 100644 --- a/src/Themes/highlight-dark-lite.css +++ b/src/Themes/highlight-dark-lite.css @@ -49,13 +49,29 @@ pre, code { } .hl-addition { - display: inline-block; min-width: 100%; background-color: #00FF0033; } .hl-deletion { - display: inline-block; min-width: 100%; background-color: #FF000022; +} + +.hl-gutter { + display: inline-block; + margin-right: 1ch; + font-size: 0.9em; + color: #555; + padding: 0 1ch; +} + +.hl-gutter-addition { + background-color: #34A853; + color: #fff; +} + +.hl-gutter-deletion { + background-color: #EA4334; + color: #fff; } \ No newline at end of file diff --git a/src/Themes/highlight-light-lite.css b/src/Themes/highlight-light-lite.css index 573a2bc..038b896 100644 --- a/src/Themes/highlight-light-lite.css +++ b/src/Themes/highlight-light-lite.css @@ -48,13 +48,28 @@ pre, code { } .hl-addition { - display: inline-block; min-width: 100%; - background-color: #00FF0033; + background-color: #00FF0022; } .hl-deletion { - display: inline-block; min-width: 100%; - background-color: #FF000022; + background-color: #FF000011; +} + +.hl-gutter { + display: inline-block; + font-size: 0.9em; + color: #555; + padding: 0 1ch; +} + +.hl-gutter-addition { + background-color: #34A853; + color: #fff; +} + +.hl-gutter-deletion { + background-color: #EA4334; + color: #fff; } \ No newline at end of file diff --git a/test-terminal.php b/test-terminal.php index 50e1590..18e1de1 100644 --- a/test-terminal.php +++ b/test-terminal.php @@ -5,7 +5,7 @@ require_once __DIR__ . '/vendor/autoload.php'; -$highlighter = new Highlighter(new LightTerminalTheme()); +$highlighter = (new Highlighter(new LightTerminalTheme()))->withGutter(); $target = $argc > 1 ? $argv[1] diff --git a/tests/CommonMark/HighlightCodeBlockRendererTest.php b/tests/CommonMark/CodeBlockRendererTest.php similarity index 57% rename from tests/CommonMark/HighlightCodeBlockRendererTest.php rename to tests/CommonMark/CodeBlockRendererTest.php index 866537b..a27a019 100644 --- a/tests/CommonMark/HighlightCodeBlockRendererTest.php +++ b/tests/CommonMark/CodeBlockRendererTest.php @@ -12,7 +12,7 @@ use PHPUnit\Framework\TestCase; use Tempest\Highlight\CommonMark\CodeBlockRenderer; -class HighlightCodeBlockRendererTest extends TestCase +class CodeBlockRendererTest extends TestCase { public function test_commonmark(): void { @@ -31,4 +31,29 @@ class Foo {} $this->assertStringContainsString('hl-keyword', $parsed->getContent()); } + + public function test_commonmark_with_gutter(): void + { + $environment = new Environment(); + + $environment + ->addExtension(new CommonMarkCoreExtension()) + ->addExtension(new FrontMatterExtension()) + ->addRenderer(FencedCode::class, new CodeBlockRenderer()); + + $markdown = new MarkdownConverter($environment); + + $input = <<<'TXT' +```php{10} +class Foo {} +``` +TXT; + + $expected = <<<'TXT' +
10 class Foo {}
+ +TXT; + + $this->assertSame($expected, $markdown->convert($input)->getContent()); + } } diff --git a/tests/Languages/Base/Injections/AdditionInjectionTest.php b/tests/Languages/Base/Injections/AdditionInjectionTest.php index d3f8dce..5ae5fb8 100644 --- a/tests/Languages/Base/Injections/AdditionInjectionTest.php +++ b/tests/Languages/Base/Injections/AdditionInjectionTest.php @@ -5,6 +5,7 @@ namespace Tempest\Highlight\Tests\Languages\Base\Injections; use PHPUnit\Framework\TestCase; +use Tempest\Highlight\Escape; use Tempest\Highlight\Highlighter; use Tempest\Highlight\Languages\Base\Injections\AdditionInjection; @@ -17,15 +18,11 @@ public function test_addition_injection() TXT; $expected = <<class Foo TXT; - $parsed = (new AdditionInjection())->parse($content, new Highlighter()); + $content = (new AdditionInjection())->parse($content, new Highlighter()); - $this->assertSame($expected, $parsed->content); - $this->assertCount(1, $parsed->tokens); - $this->assertSame(0, $parsed->tokens[0]->start); - $this->assertSame(9, $parsed->tokens[0]->end); - $this->assertSame('hl-addition', $parsed->tokens[0]->type->getValue()); + $this->assertSame($expected, Escape::html($content)); } } diff --git a/tests/Languages/Base/Injections/DeletionInjectionTest.php b/tests/Languages/Base/Injections/DeletionInjectionTest.php index 2ded3ea..b9c0204 100644 --- a/tests/Languages/Base/Injections/DeletionInjectionTest.php +++ b/tests/Languages/Base/Injections/DeletionInjectionTest.php @@ -5,6 +5,7 @@ namespace Tempest\Highlight\Tests\Languages\Base\Injections; use PHPUnit\Framework\TestCase; +use Tempest\Highlight\Escape; use Tempest\Highlight\Highlighter; use Tempest\Highlight\Languages\Base\Injections\DeletionInjection; @@ -17,15 +18,11 @@ public function test_deletion_injection() TXT; $expected = <<class Foo TXT; - $parsed = (new DeletionInjection())->parse($content, new Highlighter()); + $content = (new DeletionInjection())->parse($content, new Highlighter()); - $this->assertSame($expected, $parsed->content); - $this->assertCount(1, $parsed->tokens); - $this->assertSame(0, $parsed->tokens[0]->start); - $this->assertSame(9, $parsed->tokens[0]->end); - $this->assertSame('hl-deletion', $parsed->tokens[0]->type->getValue()); + $this->assertSame($expected, Escape::html($content)); } } diff --git a/tests/Languages/Base/Injections/GutterInjectionTest.php b/tests/Languages/Base/Injections/GutterInjectionTest.php new file mode 100644 index 0000000..19010e0 --- /dev/null +++ b/tests/Languages/Base/Injections/GutterInjectionTest.php @@ -0,0 +1,86 @@ + $line) { + $gutterNumber = $gutterNumbers[$i]; + + $gutterClass = 'hl-gutter ' . ($this->classes[$i + 1] ?? ''); +{+ + $lines[$i] = sprintf( + Escape::tokens('%s%s'),+} + $gutterClass, + str_pad( + string: {-$gutterNumber-}, + length: $gutterWidth, + pad_type: STR_PAD_LEFT, + ), + $line, + ); +} +TXT; + + $expected = <<<'TXT' + 10 foreach ($lines as $i => $line) { + 11 $gutterNumber = $gutterNumbers[$i]; + 12 + 13 $gutterClass = 'hl-gutter ' . ($this->classes[$i + 1] ?? ''); +14 + +15 + $lines[$i] = sprintf( +16 + Escape::tokens('<span class="%s">%s</span>%s'), + 17 $gutterClass, + 18 str_pad( +19 - string: $gutterNumber, + 20 length: $gutterWidth, + 21 pad_type: STR_PAD_LEFT, + 22 ), + 23 $line, + 24 ); + 25 } +TXT; + $highlighter = (new Highlighter())->withGutter(10); + + $this->assertSame($expected, $highlighter->parse($input, 'php')); + } + + public function test_gutter_injection_terminal(): void + { + $input = <<<'TXT' +foreach ($lines as $i => $line) { + $gutterNumber = $gutterNumbers[$i]; + + $gutterClass = 'hl-gutter ' . ($this->classes[$i + 1] ?? ''); +{+ + $lines[$i] = sprintf( + Escape::tokens('%s%s'),+} + $gutterClass, + str_pad( + string: {-$gutterNumber-}, + length: $gutterWidth, + pad_type: STR_PAD_LEFT, + ), + $line, + ); +} +TXT; + + $expected = <<<'TXT' +ICAxMCAbWzM0bWZvcmVhY2gbWzBtICgbWzBtJGxpbmVzG1swbSAbWzM0bWFzG1swbSAbWzBtJGkbWzBtID0+IBtbMG0kbGluZRtbMG0pIHsKICAxMSAgICAgG1swbSRndXR0ZXJOdW1iZXIbWzBtID0gG1swbSRndXR0ZXJOdW1iZXJzG1swbVsbWzBtJGkbWzBtXTsKICAxMiAKICAxMyAgICAgG1swbSRndXR0ZXJDbGFzcxtbMG0gPSAnG1szMG1obC1ndXR0ZXIgG1swbScgLiAoG1swbSR0aGlzG1swbS0+G1szMm1jbGFzc2VzG1swbVsbWzBtJGkbWzBtICsgMV0gPz8gJxtbMzBtG1swbScpOwoxNCArIAoxNSArICAgICAbWzBtJGxpbmVzG1swbVsbWzBtJGkbWzBtXSA9IBtbMzJtc3ByaW50ZhtbMG0oCjE2ICsgICAgICAgICAbWzMxbUVzY2FwZRtbMG06OhtbMzJtdG9rZW5zG1swbSgnG1szMG08c3BhbiBjbGFzcz0iJXMiPiVzPC9zcGFuPiVzG1swbScpLAogIDE3ICAgICAgICAgG1swbSRndXR0ZXJDbGFzcxtbMG0sCiAgMTggICAgICAgICAbWzMybXN0cl9wYWQbWzBtKAoxOSAtICAgICAgICAgICAgIBtbMzJtc3RyaW5nG1swbTogG1swbSRndXR0ZXJOdW1iZXIbWzBtLAogIDIwICAgICAgICAgICAgIBtbMzJtbGVuZ3RoG1swbTogG1swbSRndXR0ZXJXaWR0aBtbMG0sCiAgMjEgICAgICAgICAgICAgG1szMm1wYWRfdHlwZRtbMG06IBtbMzJtU1RSX1BBRF9MRUZUG1swbSwKICAyMiAgICAgICAgICksCiAgMjMgICAgICAgICAbWzBtJGxpbmUbWzBtLAogIDI0ICAgICApOwogIDI1IH0= +TXT; + + $highlighter = (new Highlighter(new LightTerminalTheme()))->withGutter(10); + + $this->assertSame(base64_decode($expected), $highlighter->parse($input, 'php')); + } +} diff --git a/tests/TestsInjections.php b/tests/TestsInjections.php index e7f2248..8076194 100644 --- a/tests/TestsInjections.php +++ b/tests/TestsInjections.php @@ -17,7 +17,7 @@ public function assertMatches( string|array|null $expectedContent, Language|null $currentLanguage = null, ) { - $highlighter = (new Highlighter())->withoutEscaping(); + $highlighter = (new Highlighter())->nested(); if ($currentLanguage) { $highlighter->setCurrentLanguage($currentLanguage); diff --git a/tests/index.php b/tests/index.php index 9d93906..16a1058 100644 --- a/tests/index.php +++ b/tests/index.php @@ -9,13 +9,16 @@ use League\CommonMark\MarkdownConverter; use Tempest\Highlight\CommonMark\CodeBlockRenderer; use Tempest\Highlight\CommonMark\InlineCodeBlockRenderer; +use Tempest\Highlight\Highlighter; $environment = new Environment(); +$highlighter = (new Highlighter())->withGutter(20); + $environment ->addExtension(new CommonMarkCoreExtension()) - ->addRenderer(FencedCode::class, new CodeBlockRenderer()) - ->addRenderer(Code::class, new InlineCodeBlockRenderer()) + ->addRenderer(FencedCode::class, new CodeBlockRenderer($highlighter)) + ->addRenderer(Code::class, new InlineCodeBlockRenderer($highlighter)) ; $markdown = new MarkdownConverter($environment); @@ -52,7 +55,7 @@ .hl { margin: 3em auto; box-shadow: 0 0 10px 0 #00000044; - padding: 1em 2em; + padding: 1em 2em 1em 1ch; /*background-color: #fafafa;*/ border-radius: 3px; color: #000; diff --git a/tests/targets/test.md b/tests/targets/test.md index cac72fc..5a28ff0 100644 --- a/tests/targets/test.md +++ b/tests/targets/test.md @@ -1,8 +1,18 @@ -```js -/** - * Class making something fun and easy. - * @param {string} arg1 An argument that makes this more interesting. - * @param {Array.} arg2 List of numbers to be processed. - * @constructor - */ +```php +foreach ($lines as $i => $line) { + $gutterNumber = $gutterNumbers[$i]; + + $gutterClass = 'hl-gutter ' . ($this->classes[$i + 1] ?? ''); +{+ + $lines[$i] = sprintf( + Escape::tokens('%s%s'),+} + $gutterClass, + str_pad( + string: {-$gutterNumber-}, + length: $gutterWidth, + pad_type: STR_PAD_LEFT, + ), + $line, + ); +} ``` \ No newline at end of file