├── package.json ├── LICENSE.md ├── src ├── Renderers │ ├── IndentedCodeRenderer.php │ └── FencedCodeRenderer.php ├── HighlightCodeExtension.php └── ShikiHighlighter.php ├── .php_cs.dist.php ├── composer.json ├── CHANGELOG.md └── README.md /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "shiki": "^1.22.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Renderers/IndentedCodeRenderer.php: -------------------------------------------------------------------------------- 1 | highlighter = $codeBlockHighlighter; 20 | 21 | $this->baseRenderer = new BaseIndentedCodeRenderer(); 22 | } 23 | 24 | public function render( 25 | Node $node, 26 | ChildNodeRendererInterface $childRenderer 27 | ): string { 28 | $element = $this->baseRenderer->render($node, $childRenderer); 29 | 30 | $element->setContents( 31 | $this->highlighter->highlight($element->getContents()) 32 | ); 33 | 34 | return $element->getContents(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/HighlightCodeExtension.php: -------------------------------------------------------------------------------- 1 | $theme Can be a single theme or an array with a light and a dark theme. 19 | */ 20 | public function __construct(mixed $theme = 'nord', ?Shiki $shiki = null, bool $throw = false) 21 | { 22 | $this->shikiHighlighter = new ShikiHighlighter($shiki ?? new Shiki($theme), $throw); 23 | } 24 | 25 | public function register(EnvironmentBuilderInterface $environment): void 26 | { 27 | $environment 28 | ->addRenderer(FencedCode::class, new FencedCodeRenderer($this->shikiHighlighter), 10) 29 | ->addRenderer(IndentedCode::class, new IndentedCodeRenderer($this->shikiHighlighter), 10); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/commonmark-shiki-highlighter", 3 | "description": "Highlight code blocks with league/commonmark and Shiki", 4 | "type": "commonmark-extension", 5 | "keywords": [ 6 | "spatie", 7 | "commonmark-shiki-highlighter" 8 | ], 9 | "homepage": "https://github.com/spatie/commonmark-shiki-highlighter", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Freek Van der Herten", 14 | "email": "freek@spatie.be", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "league/commonmark": "^2.4.2", 21 | "spatie/shiki-php": "^2.2.2", 22 | "symfony/process": "^5.4|^6.4|^7.1|^8.0" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "^2.19|^v3.49.0", 26 | "phpunit/phpunit": "^9.5", 27 | "spatie/phpunit-snapshot-assertions": "^4.2.7", 28 | "spatie/ray": "^1.28" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Spatie\\CommonMarkShikiHighlighter\\": "src" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Spatie\\CommonMarkShikiHighlighter\\Tests\\": "tests" 38 | } 39 | }, 40 | "scripts": { 41 | "psalm": "vendor/bin/psalm", 42 | "test": "vendor/bin/phpunit", 43 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage", 44 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable": true 51 | } 52 | -------------------------------------------------------------------------------- /src/Renderers/FencedCodeRenderer.php: -------------------------------------------------------------------------------- 1 | highlighter = $codeBlockHighlighter; 22 | 23 | $this->baseRenderer = new BaseFencedCodeRenderer(); 24 | } 25 | 26 | public function render( 27 | Node $node, 28 | ChildNodeRendererInterface $childRenderer 29 | ): string { 30 | $element = $this->baseRenderer->render($node, $childRenderer); 31 | 32 | $element->setContents( 33 | $this->highlighter->highlight( 34 | $element->getContents(), 35 | $this->getSpecifiedLanguage($node) 36 | ) 37 | ); 38 | 39 | return $element->getContents(); 40 | } 41 | 42 | protected function getSpecifiedLanguage(FencedCode $block): ?string 43 | { 44 | $infoWords = $block->getInfoWords(); 45 | 46 | if (empty($infoWords) || empty($infoWords[0])) { 47 | return null; 48 | } 49 | 50 | return Xml::escape($infoWords[0]); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ShikiHighlighter.php: -------------------------------------------------------------------------------- 1 | parseAddedAndDeletedLines($contents); 23 | 24 | $definition = $this->parseLangAndLines($infoLine); 25 | 26 | $language = $definition['lang'] ?? 'php'; 27 | 28 | try { 29 | $highlightedContents = $this->shiki->highlightCode( 30 | code: $contents, 31 | language: $language, 32 | options: [ 33 | 'addLines' => $addLines, 34 | 'deleteLines' => $deleteLines, 35 | 'highlightLines' => $definition['highlightLines'], 36 | 'focusLines' => $definition['focusLines'], 37 | ], 38 | ); 39 | } catch (Exception $e) { 40 | if ($this->throw) { 41 | throw $e; 42 | } 43 | 44 | $highlightedContents = $codeBlock; 45 | } 46 | 47 | return $highlightedContents; 48 | } 49 | 50 | protected function parseLangAndLines(?string $language): array 51 | { 52 | $parsed = [ 53 | 'lang' => $language, 54 | 'highlightLines' => [], 55 | 'focusLines' => [], 56 | ]; 57 | 58 | if ($language === null) { 59 | return $parsed; 60 | } 61 | 62 | $bracePosition = strpos($language, '{'); 63 | 64 | if ($bracePosition === false) { 65 | return $parsed; 66 | } 67 | 68 | preg_match_all('/{([^}]*)}/', $language, $matches); 69 | 70 | $parsed['lang'] = substr($language, 0, $bracePosition); 71 | $parsed['highlightLines'] = array_map('trim', explode(',', $matches[1][0] ?? '')); 72 | $parsed['focusLines'] = array_map('trim', explode(',', $matches[1][1] ?? '')); 73 | 74 | return $parsed; 75 | } 76 | 77 | private function parseAddedAndDeletedLines(string $contents): array 78 | { 79 | $addLines = []; 80 | $deleteLines = []; 81 | 82 | $contentLines = explode("\n", $contents); 83 | $contentLines = array_map(function (string $line, int $index) use (&$addLines, &$deleteLines) { 84 | if (str_starts_with($line, '+ ')) { 85 | $addLines[] = $index + 1; 86 | $line = substr($line, 2); 87 | } 88 | 89 | if (str_starts_with($line, '- ')) { 90 | $deleteLines[] = $index + 1; 91 | $line = substr($line, 2); 92 | } 93 | 94 | return $line; 95 | }, $contentLines, array_keys($contentLines)); 96 | 97 | return [ 98 | implode("\n", $contentLines), 99 | $addLines, 100 | $deleteLines, 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `commonmark-shiki-highlighter` will be documented in this file. 4 | 5 | ## 2.5.2 - 2025-11-24 6 | 7 | ### What's Changed 8 | 9 | * Update issue template by @AlexVanderbist in https://github.com/spatie/commonmark-shiki-highlighter/pull/15 10 | * Added Symfony 8 support to all symfony/* packages. by @thecaliskan in https://github.com/spatie/commonmark-shiki-highlighter/pull/18 11 | 12 | ### New Contributors 13 | 14 | * @AlexVanderbist made their first contribution in https://github.com/spatie/commonmark-shiki-highlighter/pull/15 15 | 16 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.5.1...2.5.2 17 | 18 | ## 2.5.1 - 2025-01-13 19 | 20 | ### What's Changed 21 | 22 | * Fix: Make parameter nullable to fix PHP 8.4 deprecation notice by @nicoverbruggen in https://github.com/spatie/commonmark-shiki-highlighter/pull/14 23 | 24 | ### New Contributors 25 | 26 | * @nicoverbruggen made their first contribution in https://github.com/spatie/commonmark-shiki-highlighter/pull/14 27 | 28 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.5.0...2.5.1 29 | 30 | ## 2.5.0 - 2024-11-12 31 | 32 | ### What's Changed 33 | 34 | * feat: support (at least) dual themes by @Barbapapazes in https://github.com/spatie/commonmark-shiki-highlighter/pull/13 35 | 36 | ### New Contributors 37 | 38 | * @Barbapapazes made their first contribution in https://github.com/spatie/commonmark-shiki-highlighter/pull/13 39 | 40 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.4.0...2.5.0 41 | 42 | ## 2.4.0 - 2024-04-11 43 | 44 | * Add a way to throw on exceptions 45 | 46 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.3.0...2.4.0 47 | 48 | ## 2.3.0 - 2024-02-19 49 | 50 | * Change dependency to shiki ^2.0 51 | 52 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.2.0...2.3.0 53 | 54 | ## 2.2.0 - 2024-02-17 55 | 56 | ### What's Changed 57 | 58 | * Add line break by @maximal in https://github.com/spatie/commonmark-shiki-highlighter/pull/8 59 | * Update README.md by @joshbruce in https://github.com/spatie/commonmark-shiki-highlighter/pull/9 60 | * Allow Symfony 7.x by @thecaliskan in https://github.com/spatie/commonmark-shiki-highlighter/pull/11 61 | * feat: allow creating extension with instantiated `Shiki` object by @kbond in https://github.com/spatie/commonmark-shiki-highlighter/pull/12 62 | 63 | ### New Contributors 64 | 65 | * @maximal made their first contribution in https://github.com/spatie/commonmark-shiki-highlighter/pull/8 66 | * @joshbruce made their first contribution in https://github.com/spatie/commonmark-shiki-highlighter/pull/9 67 | * @thecaliskan made their first contribution in https://github.com/spatie/commonmark-shiki-highlighter/pull/11 68 | * @kbond made their first contribution in https://github.com/spatie/commonmark-shiki-highlighter/pull/12 69 | 70 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.1.1...2.2.0 71 | 72 | ## 2.1.1 - 2022-11-28 73 | 74 | - Fixes a potential XSS issue when highlighting fails 75 | 76 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.1.0...2.1.1 77 | 78 | ## 2.1.0 - 2022-01-13 79 | 80 | ## What's Changed 81 | 82 | - Allow symfony 6 83 | - Update README.md by @alexanderglueck in https://github.com/spatie/commonmark-shiki-highlighter/pull/6 84 | 85 | **Full Changelog**: https://github.com/spatie/commonmark-shiki-highlighter/compare/2.0.0...2.1.0 86 | 87 | ## 2.0.0 - 2021-08-04 88 | 89 | - upgrade to support commonmark 2.0 90 | 91 | No changes to our API were made 92 | 93 | ## 1.1.1 - 2021-07-11 94 | 95 | - improve deps 96 | 97 | ## 1.1.0 - 2021-07-10 98 | 99 | - You can now mark lines as `highlighted`, `added`, `deleted` or `focus` 100 | 101 | ## 1.0.0 - 2021-07-09 102 | 103 | - initial release 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Highlight code blocks with league/commonmark and Shiki 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/commonmark-shiki-highlighter.svg?style=flat-square)](https://packagist.org/packages/spatie/commonmark-shiki-highlighter) 4 | [![Tests](https://github.com/spatie/commonmark-shiki-highlighter/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/commonmark-shiki-highlighter/actions/workflows/run-tests.yml) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/spatie/commonmark-shiki-highlighter/Check%20&%20fix%20styling?label=code%20style)](https://github.com/spatie/commonmark-shiki-highlighter/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amaster) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/commonmark-shiki-highlighter.svg?style=flat-square)](https://packagist.org/packages/spatie/commonmark-shiki-highlighter) 7 | 8 | This package contains a block renderer for [league/commonmark](https://github.com/thephpleague/commonmark) to highlight code blocks using [Shiki PHP](https://github.com/spatie/shiki-php). 9 | 10 | This package also ships with the following extra languages, on top of the [100+ that Shiki supports](https://github.com/shikijs/shiki/tree/master/docs/languages.md) out of the box: 11 | 12 | - Antlers 13 | - Blade 14 | 15 | If you're using Laravel, make sure to look at our [spatie/laravel-markdown](https://github.com/spatie/laravel-markdown) package which offers easy integration with Shiki in laravel projects. 16 | 17 | ## Support us 18 | 19 | [](https://spatie.be/github-ad-click/commonmark-shiki-highlighter) 20 | 21 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 22 | 23 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 24 | 25 | ## Installation 26 | 27 | You can install the package via composer: 28 | 29 | ```bash 30 | composer require spatie/commonmark-shiki-highlighter 31 | ``` 32 | 33 | In your project, you must have v1 of the JavaScript package [`shiki`](https://github.com/shikijs/shiki) installed, otherwise the `
` element will not be present in the output. 
 34 | 
 35 | You can install it via npm
 36 | 
 37 | ```bash
 38 | npm install shiki@^1.3.0
 39 | ```
 40 | 
 41 | or Yarn
 42 | 
 43 | ```bash
 44 | yarn add shiki@^1.3.0
 45 | ```
 46 | 
 47 | ## Usage
 48 | 
 49 | Here's how we can create a function that can convert markdown to HTML with all code snippets highlighted. Inside the function will create a new `MarkdownConverter` that uses the `HighlightCodeExtension` provided by this package.
 50 | 
 51 | ```php
 52 | use League\CommonMark\Environment\Environment;
 53 | use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
 54 | use League\CommonMark\MarkdownConverter;
 55 | use Spatie\CommonMarkShikiHighlighter\HighlightCodeExtension;
 56 | 
 57 | function convertToHtml(string $markdown, string $theme): string
 58 | {
 59 |     $environment = (new Environment())
 60 |         ->addExtension(new CommonMarkCoreExtension())
 61 |         ->addExtension(new HighlightCodeExtension(theme: $theme));
 62 | 
 63 |     $markdownConverter = new MarkdownConverter(environment: $environment);
 64 | 
 65 |     return $markdownConverter->convertToHtml($markdown);
 66 | }
 67 | ```
 68 | 
 69 | Alternatively, you can inject an already instantiated `Shiki` instance into the `HighlightCodeExtension`:
 70 | 
 71 | ```php
 72 | use Spatie\ShikiPhp\Shiki;
 73 | use Spatie\CommonMarkShikiHighlighter\HighlightCodeExtension;
 74 | 
 75 | $environment->addExtension(new HighlightCodeExtension(shiki: new Shiki()));
 76 | ```
 77 | 
 78 | ## Using themes
 79 | 
 80 | The `$theme` argument on `HighlightCodeExtension` expects the name of [one of the many themes](https://github.com/shikijs/shiki/blob/master/docs/themes.md) that Shiki supports.
 81 | 
 82 | Alternatively, you can use a custom theme. Shiki [supports](https://github.com/shikijs/shiki/blob/master/docs/themes.md) any [VSCode themes](https://code.visualstudio.com/docs/getstarted/themes). You can load a theme simply by passing an absolute path of a theme file to the `$theme` argument.
 83 | 
 84 | ## Marking lines as highlighted, added, deleted and focus
 85 | 
 86 | You can mark lines using the Markdown info tag as highlighted or focused. You can prefix lines with `+ ` or `- ` to mark them as added or deleted.
 87 | In the first pair of brackets, you can specify line numbers that should be highlighted. In an optional second pair you can specify which lines should be focused on.
 88 | 
 89 | ```md
 90 | ```php{1,2}{3}
 91 | addExtension(new CommonMarkCoreExtension())
159 |     ->addExtension(new HighlightCodeExtension(theme: $theme, throw: true));
160 | ```
161 | 
162 | ## A word on performance
163 | 
164 | Highlighting with Shiki is a resource intensive process. We highly recommend using some form of caching.
165 | 
166 | ## Testing
167 | 
168 | ```bash
169 | composer test
170 | ```
171 | 
172 | ## Changelog
173 | 
174 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
175 | 
176 | ## Contributing
177 | 
178 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
179 | 
180 | ## Security Vulnerabilities
181 | 
182 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
183 | 
184 | ## Credits
185 | 
186 | - [Freek Van der Herten](https://github.com/freekmurze)
187 | - [All Contributors](../../contributors)
188 | 
189 | ## Alternatives
190 | 
191 | If you don't want to install and handle Shiki yourself, take a look at [Torchlight](https://torchlight.dev), which can highlight your code with minimal setup.
192 | 
193 | ## License
194 | 
195 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
196 | 


--------------------------------------------------------------------------------