From 693396fc79fa7beec1d1dd57b3c41bc6294f43d7 Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Sun, 13 Apr 2025 17:35:44 +0100 Subject: [PATCH 1/4] Add support for inline code rendering --- meta/sample.php | 25 +++++++++++ src/CommonMark/InlineCodeRenderer.php | 39 +++++++++++++++++ src/CommonMark/PhikiExtension.php | 4 +- src/Generators/HtmlGenerator.php | 44 +++++++++++++++++++- src/Phiki.php | 3 +- tests/Unit/CommonMark/PhikiExtensionTest.php | 15 +++++++ 6 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 src/CommonMark/InlineCodeRenderer.php diff --git a/meta/sample.php b/meta/sample.php index 7e5bacd..bd83fb8 100644 --- a/meta/sample.php +++ b/meta/sample.php @@ -2,6 +2,10 @@ set_time_limit(2); +use League\CommonMark\Environment\Environment as EnvironmentEnvironment; +use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; +use League\CommonMark\MarkdownConverter; +use Phiki\CommonMark\PhikiExtension; use Phiki\Environment\Environment; use Phiki\Grammar\DefaultGrammars; use Phiki\Phiki; @@ -17,6 +21,7 @@ $grammar = $_GET['grammar'] ?? 'php'; $withGutter = ($_GET['gutter'] ?? false) === 'on'; +$markdown = $_GET['markdown'] ?? null; $environment = Environment::default()->enableStrictMode(); /** @var \Phiki\Grammar\GrammarRepository $repository */ $repository = $environment->getGrammarRepository(); @@ -60,6 +65,14 @@ $tokenDiff = array_diff_multidimensional($tokens, $vscodeTextmateOutput, false); +$converter = new MarkdownConverter( + (new EnvironmentEnvironment()) + ->addExtension(new CommonMarkCoreExtension) + ->addExtension(new PhikiExtension('github-dark')) +); + +$generatedMarkdown = $markdown ? $converter->convert($markdown)->getContent() : null; + ?> @@ -154,6 +167,18 @@ class="flex items-center gap-x-4"> + +
+
+

markdown:

+
+ + +
+
+ +
+
diff --git a/src/CommonMark/InlineCodeRenderer.php b/src/CommonMark/InlineCodeRenderer.php new file mode 100644 index 0000000..1da4766 --- /dev/null +++ b/src/CommonMark/InlineCodeRenderer.php @@ -0,0 +1,39 @@ +[\w]+)}(?.*)/', $node->getLiteral(), $match); + + if (! isset($match['match'])) { + return $internal->render($node, $childRenderer); + } + + $grammar = $match['match'] ?? 'txt'; + $code = $match['code'] ?? $node->getLiteral(); + + return $this->phiki->codeToHtml($code, $grammar, $this->theme, inline: true); + } +} diff --git a/src/CommonMark/PhikiExtension.php b/src/CommonMark/PhikiExtension.php index a4edcd9..8bf1c1b 100644 --- a/src/CommonMark/PhikiExtension.php +++ b/src/CommonMark/PhikiExtension.php @@ -4,6 +4,7 @@ use League\CommonMark\Environment\EnvironmentBuilderInterface; use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; +use League\CommonMark\Extension\CommonMark\Node\Inline\Code; use League\CommonMark\Extension\ExtensionInterface; use Phiki\Phiki; use Phiki\Theme\Theme; @@ -24,6 +25,7 @@ public function __construct( public function register(EnvironmentBuilderInterface $environment): void { $environment - ->addRenderer(FencedCode::class, new CodeBlockRenderer($this->theme, $this->phiki, $this->withGutter, $this->withWrapper), 10); + ->addRenderer(FencedCode::class, new CodeBlockRenderer($this->theme, $this->phiki, $this->withGutter, $this->withWrapper), 10) + ->addRenderer(Code::class, new InlineCodeRenderer($this->theme, $this->phiki), 10); } } diff --git a/src/Generators/HtmlGenerator.php b/src/Generators/HtmlGenerator.php index ae7fd48..0975428 100644 --- a/src/Generators/HtmlGenerator.php +++ b/src/Generators/HtmlGenerator.php @@ -16,11 +16,20 @@ public function __construct( protected array $themes, protected bool $withGutter = false, protected bool $withWrapper = false, + protected bool $inline = false, ) {} public function generate(array $tokens): string { - return $this->withWrapper ? $this->buildWrapper($tokens) : $this->buildPre($tokens); + if ($this->inline) { + return $this->buildCode($tokens); + } + + if ($this->withWrapper) { + return $this->buildWrapper($tokens); + } + + return $this->buildPre($tokens); } private function buildWrapper($tokens): string @@ -81,7 +90,38 @@ private function buildCode(array $tokens): string $output[] = $this->buildLine($line, $i); } - return ''.implode($output).''; + if (! $this->inline) { + return '' . implode($output) . ''; + } + + $codeClasses = $this->inline ? array_filter([ + 'phiki-inline', + $this->grammarName ? "language-$this->grammarName" : null, + $this->getDefaultTheme()->name, + count($this->themes) > 1 ? 'phiki-themes' : null, + ]) : []; + + foreach ($this->themes as $theme) { + if ($theme !== $this->getDefaultTheme()) { + $codeClasses[] = $theme->name; + } + } + + $codeStyles = [$this->getDefaultTheme()->base()->toStyleString()]; + + foreach ($this->themes as $id => $theme) { + if ($id !== $this->getDefaultThemeId()) { + $codeStyles[] = $theme->base()->toCssVarString($id); + } + } + + return sprintf( + '%s', + implode(' ', $codeClasses), + $this->grammarName ? " data-language=\"$this->grammarName\"" : null, + implode(';', $codeStyles), + implode('', $output), + ); } private function buildLine(array $line, int $index): string diff --git a/src/Phiki.php b/src/Phiki.php index 9f7045c..5f1562d 100644 --- a/src/Phiki.php +++ b/src/Phiki.php @@ -58,7 +58,7 @@ public function codeToHighlightedTokens(string $code, string|Grammar $grammar, s * @param bool $withGutter Include a gutter in the generated HTML. The gutter typically contains line numbers and helps provide context for the code. * @param bool $withWrapper Wrap the generated HTML in an additional `
` so that it can be styled with CSS. Useful for avoiding overflow issues. */ - public function codeToHtml(string $code, string|Grammar $grammar, string|array|Theme $theme, bool $withGutter = false, bool $withWrapper = false): string + public function codeToHtml(string $code, string|Grammar $grammar, string|array|Theme $theme, bool $withGutter = false, bool $withWrapper = false, bool $inline = false): string { $tokens = $this->codeToHighlightedTokens($code, $grammar, $theme); $generator = new HtmlGenerator( @@ -69,6 +69,7 @@ public function codeToHtml(string $code, string|Grammar $grammar, string|array|T $this->wrapThemes($theme), $withGutter, $withWrapper, + $inline, ); return $generator->generate($tokens); diff --git a/tests/Unit/CommonMark/PhikiExtensionTest.php b/tests/Unit/CommonMark/PhikiExtensionTest.php index d467f53..eae5662 100644 --- a/tests/Unit/CommonMark/PhikiExtensionTest.php +++ b/tests/Unit/CommonMark/PhikiExtensionTest.php @@ -45,4 +45,19 @@ class A {} expect($generated) ->toContain('data-language="php"'); }); + + it('highlights inline code when grammar is present', function () { + $environment = new Environment; + + $environment + ->addExtension(new CommonMarkCoreExtension) + ->addExtension(new PhikiExtension('github-dark')); + + $markdown = new MarkdownConverter($environment); + $generated = $markdown->convert(<<<'MD' + `{php}echo "Hello, world!";` + MD)->getContent(); + + dd($generated); + })->only(); }); From 526a3ae35e0981e3d1bbf772311ff04483666a12 Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Sun, 13 Apr 2025 17:37:04 +0100 Subject: [PATCH 2/4] Update tests for inline HTML --- ...22_it_highlights_inline_code_when_grammar_is_present.snap" | 2 ++ tests/Unit/CommonMark/PhikiExtensionTest.php | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 "tests/.pest/snapshots/Unit/CommonMark/PhikiExtensionTest/_CommonMark___Extension__\342\206\222_it_highlights_inline_code_when_grammar_is_present.snap" diff --git "a/tests/.pest/snapshots/Unit/CommonMark/PhikiExtensionTest/_CommonMark___Extension__\342\206\222_it_highlights_inline_code_when_grammar_is_present.snap" "b/tests/.pest/snapshots/Unit/CommonMark/PhikiExtensionTest/_CommonMark___Extension__\342\206\222_it_highlights_inline_code_when_grammar_is_present.snap" new file mode 100644 index 0000000..a6eab8a --- /dev/null +++ "b/tests/.pest/snapshots/Unit/CommonMark/PhikiExtensionTest/_CommonMark___Extension__\342\206\222_it_highlights_inline_code_when_grammar_is_present.snap" @@ -0,0 +1,2 @@ +

echo "Hello, world!"; +

diff --git a/tests/Unit/CommonMark/PhikiExtensionTest.php b/tests/Unit/CommonMark/PhikiExtensionTest.php index eae5662..4e26712 100644 --- a/tests/Unit/CommonMark/PhikiExtensionTest.php +++ b/tests/Unit/CommonMark/PhikiExtensionTest.php @@ -58,6 +58,6 @@ class A {} `{php}echo "Hello, world!";` MD)->getContent(); - dd($generated); - })->only(); + expect($generated)->toMatchSnapshot(); + }); }); From 8bef7b538447eb628ae70655164adf7904a081e0 Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Sun, 13 Apr 2025 17:38:15 +0100 Subject: [PATCH 3/4] Add to README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index c36f424..d8ee71a 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,16 @@ $environment ->addExtension(new PhikiExtension('github-dark', withWrapper: true)); ``` +#### Inline code highlighting + +If you wish to highlight inline code snippets in your Markdown content, you can specify a grammar after the opening backtick. + +```md +`{php}echo "Hello, world!"` +``` + +This will produce a highlighted `` element using the configured theme and the chosen grammar, specified inside of the `{}` characters. + ### Laravel If you're using Laravel's `Str::markdown()` or `str()->markdown()` methods, you can use the same CommonMark extension by passing it through to the method. From 49296439648d122a39e6f254a0924a5dc6320a8c Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Sun, 13 Apr 2025 18:24:56 +0100 Subject: [PATCH 4/4] Simplify code and make PHPStan happy --- src/CommonMark/InlineCodeRenderer.php | 9 ++------- src/Generators/HtmlGenerator.php | 6 +++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/CommonMark/InlineCodeRenderer.php b/src/CommonMark/InlineCodeRenderer.php index 1da4766..22bb107 100644 --- a/src/CommonMark/InlineCodeRenderer.php +++ b/src/CommonMark/InlineCodeRenderer.php @@ -25,15 +25,10 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer) $internal = new CodeRenderer(); - preg_match('/^\{(?[\w]+)}(?.*)/', $node->getLiteral(), $match); - - if (! isset($match['match'])) { + if (preg_match('/^\{([\w]+)}(.*)/', $node->getLiteral(), $match, PREG_UNMATCHED_AS_NULL) !== 1) { return $internal->render($node, $childRenderer); } - $grammar = $match['match'] ?? 'txt'; - $code = $match['code'] ?? $node->getLiteral(); - - return $this->phiki->codeToHtml($code, $grammar, $this->theme, inline: true); + return $this->phiki->codeToHtml($match[2], $match[1], $this->theme, inline: true); } } diff --git a/src/Generators/HtmlGenerator.php b/src/Generators/HtmlGenerator.php index 0975428..c2d076f 100644 --- a/src/Generators/HtmlGenerator.php +++ b/src/Generators/HtmlGenerator.php @@ -94,13 +94,13 @@ private function buildCode(array $tokens): string return '' . implode($output) . ''; } - $codeClasses = $this->inline ? array_filter([ + $codeClasses = array_filter([ 'phiki-inline', $this->grammarName ? "language-$this->grammarName" : null, $this->getDefaultTheme()->name, count($this->themes) > 1 ? 'phiki-themes' : null, - ]) : []; - + ]); + foreach ($this->themes as $theme) { if ($theme !== $this->getDefaultTheme()) { $codeClasses[] = $theme->name;