Skip to content

Commit 57ae10e

Browse files
[2.x] Add support for highlighting/focusing in CommonMark (#103)
* Add API for storing Meta in a Transformer * Pass the meta object to pending HTML output and transformers * Add test for meta object passing * Pass meta string from CommonMark extension * Add highlight and focus support * Add docs for highlight/focus * Update snapshots
1 parent 571c532 commit 57ae10e

File tree

14 files changed

+424
-4
lines changed

14 files changed

+424
-4
lines changed

docs/commonmark.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ description: "Learn how to use Phiki with The PHP League's CommonMark library."
55

66
If you're using `league/commonmark` to parse and render Markdown in your PHP project, you can easily integrate Phiki using our custom extension.
77

8+
## Usage
9+
810
```php
911
use League\CommonMark\Environment\Environment;
1012
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
@@ -57,3 +59,43 @@ $environment
5759
'dark' => Theme::GithubDark, // [!code ++]
5860
])); // [!code ++]
5961
```
62+
63+
### Highlighting and focusing lines
64+
65+
You can highlight and focus lines in your code blocks using special annotations in the code block's info string.
66+
67+
```md
68+
```php {2,4-8}{3}
69+
```
70+
71+
The first set of braces represents the lines to highlight, while the second set represents the lines to focus on.
72+
73+
In this example, line 2 will be highlighted, as well as lines 4 through 8. Line 3 will then be focused.
74+
75+
If you only want to focus lines, you can use an empty set of braces for the highlighted lines:
76+
77+
```md
78+
```php {}{3}
79+
```
80+
81+
#### Sample CSS
82+
83+
Phiki does not style the highlighted or focused lines by default, so you will need to add your own CSS.
84+
85+
You can use the following sample CSS to get started:
86+
87+
```css
88+
pre.phiki code .line.highlight {
89+
background-color: hsl(197, 88%, 94%);
90+
}
91+
92+
.shiki.focus .line:not(.focus) {
93+
transition: all 250ms;
94+
filter: blur(2px);
95+
}
96+
97+
.shiki.focus:hover .line {
98+
transition: all 250ms;
99+
filter: blur(0);
100+
}
101+
```

src/Adapters/CommonMark/CodeBlockRenderer.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
use League\CommonMark\Node\Node;
88
use League\CommonMark\Renderer\ChildNodeRendererInterface;
99
use League\CommonMark\Renderer\NodeRendererInterface;
10+
use Phiki\Adapters\CommonMark\Transformers\MetaTransformer;
1011
use Phiki\Grammar\Grammar;
1112
use Phiki\Phiki;
1213
use Phiki\Theme\Theme;
14+
use Phiki\Transformers\Meta;
1315

1416
class CodeBlockRenderer implements NodeRendererInterface
1517
{
@@ -27,8 +29,13 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer)
2729

2830
$code = rtrim($node->getLiteral(), "\n");
2931
$grammar = $this->detectGrammar($node);
32+
$meta = new Meta(markdownInfo: $node->getInfoWords()[1] ?? null);
3033

31-
return $this->phiki->codeToHtml($code, $grammar, $this->theme)->withGutter($this->withGutter)->toString();
34+
return $this->phiki->codeToHtml($code, $grammar, $this->theme)
35+
->withGutter($this->withGutter)
36+
->withMeta($meta)
37+
->transformer(new MetaTransformer)
38+
->toString();
3239
}
3340

3441
protected function detectGrammar(FencedCode $node): Grammar|string
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace Phiki\Adapters\CommonMark\Transformers;
4+
5+
use Phiki\Phast\Element;
6+
use Phiki\Support\Arr;
7+
use Phiki\Transformers\AbstractTransformer;
8+
9+
class MetaTransformer extends AbstractTransformer
10+
{
11+
protected array $highlights = [];
12+
13+
protected array $focuses = [];
14+
15+
public function preprocess(string $code): string
16+
{
17+
$this->parse();
18+
19+
return $code;
20+
}
21+
22+
public function line(Element $span, array $tokens, int $index): Element
23+
{
24+
if (in_array($index + 1, $this->highlights, true)) {
25+
$span->properties->get('class')->add('highlight');
26+
}
27+
28+
if (in_array($index + 1, $this->focuses, true)) {
29+
$span->properties->get('class')->add('focus');
30+
}
31+
32+
return $span;
33+
}
34+
35+
protected function parse(): void
36+
{
37+
if (! $this->meta->markdownInfo) {
38+
return;
39+
}
40+
41+
[$highlights, $focuses] = array_pad(
42+
explode(
43+
'}{',
44+
rtrim(ltrim($this->meta->markdownInfo, '{'), '}'),
45+
2
46+
),
47+
2,
48+
null
49+
);
50+
51+
if (! $highlights && ! $focuses) {
52+
return;
53+
}
54+
55+
$highlights = array_map(
56+
fn(array $part) => count($part) > 1 ? $part : $part[0],
57+
array_map(
58+
fn(string $part) => array_map(fn(string $number) => intval($number), explode('-', trim($part))),
59+
explode(',', $highlights)
60+
)
61+
);
62+
63+
foreach ($highlights as $part) {
64+
if (is_array($part)) {
65+
$this->highlights = array_merge($this->highlights, range($part[0], $part[1]));
66+
} else {
67+
$this->highlights[] = $part;
68+
}
69+
}
70+
71+
$this->highlights = array_unique($this->highlights);
72+
73+
if (! $focuses) {
74+
return;
75+
}
76+
77+
$focuses = array_map(
78+
fn(array $part) => count($part) > 1 ? $part : $part[0],
79+
array_map(
80+
fn(string $part) => array_map(fn(string $number) => intval($number), explode('-', trim($part))),
81+
explode(',', $focuses)
82+
)
83+
);
84+
85+
foreach ($focuses as $part) {
86+
if (is_array($part)) {
87+
$this->focuses = array_merge($this->focuses, range($part[0], $part[1]));
88+
} else {
89+
$this->focuses[] = $part;
90+
}
91+
}
92+
93+
$this->focuses = array_unique($this->focuses);
94+
}
95+
}

src/Contracts/TransformerInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Phiki\Phast\Root;
77
use Phiki\Token\HighlightedToken;
88
use Phiki\Token\Token;
9+
use Phiki\Transformers\Meta;
910

1011
interface TransformerInterface
1112
{
@@ -59,4 +60,9 @@ public function token(Element $span, HighlightedToken $token, int $index, int $l
5960
* Modify the HTML output after the AST has been converted.
6061
*/
6162
public function postprocess(string $html): string;
63+
64+
/**
65+
* Supply the meta object to the transformer.
66+
*/
67+
public function withMeta(Meta $meta): void;
6268
}

src/Output/Html/PendingHtmlOutput.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Phiki\Token\Token;
1616
use Phiki\Transformers\Decorations\DecorationTransformer;
1717
use Phiki\Transformers\Decorations\LineDecoration;
18+
use Phiki\Transformers\Meta;
1819
use Psr\SimpleCache\CacheInterface;
1920
use Stringable;
2021

@@ -34,6 +35,8 @@ class PendingHtmlOutput implements Stringable
3435

3536
protected int $startingLineNumber = 1;
3637

38+
protected Meta $meta;
39+
3740
/**
3841
* @param array<string, ParsedTheme> $themes
3942
*/
@@ -102,6 +105,13 @@ public function startingLine(int $lineNumber): self
102105
return $this;
103106
}
104107

108+
public function withMeta(Meta $meta): self
109+
{
110+
$this->meta = $meta;
111+
112+
return $this;
113+
}
114+
105115
public function toString(): string
106116
{
107117
return $this->__toString();
@@ -153,6 +163,14 @@ public function __toString(): string
153163
return $this->cache->get($cacheKey);
154164
}
155165

166+
if (! isset($this->meta)) {
167+
$this->meta = new Meta();
168+
}
169+
170+
foreach ($this->transformers as $transformer) {
171+
$transformer->withMeta($this->meta);
172+
}
173+
156174
[$code] = $this->callTransformerMethod('preprocess', $this->code);
157175
[$tokens] = $this->callTransformerMethod('tokens', call_user_func($this->generateTokensUsing, $code, $this->grammar));
158176
[$highlightedTokens] = $this->callTransformerMethod('highlighted', call_user_func($this->highlightTokensUsing, $tokens, $this->themes));

src/Transformers/AbstractTransformer.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010

1111
class AbstractTransformer implements TransformerInterface
1212
{
13+
/**
14+
* The meta information.
15+
*/
16+
protected Meta $meta;
17+
1318
/**
1419
* Modify the code before it is tokenized.
1520
*/
@@ -87,4 +92,12 @@ public function postprocess(string $html): string
8792
{
8893
return $html;
8994
}
95+
96+
/**
97+
* Store the meta object.
98+
*/
99+
public function withMeta(Meta $meta): void
100+
{
101+
$this->meta = $meta;
102+
}
90103
}

src/Transformers/Meta.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Phiki\Transformers;
4+
5+
readonly class Meta
6+
{
7+
public function __construct(
8+
public ?string $markdownInfo = null,
9+
) {}
10+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<pre class="phiki language-php github-dark" data-language="php" style="background-color: #24292e;color: #e1e4e8;"><code><span class="line highlight"><span class="token" style="color: #f97583;">class</span><span class="token"> </span><span class="token" style="color: #b392f0;">A</span><span class="token"> </span><span class="token">{</span><span class="token">}</span><span class="token">
2+
</span></span></code></pre>

tests/.pest/snapshots/Unit/CommonMark/PhikiExtensionTest/it_can_be_configured_using_environment_config_array.snap

Lines changed: 0 additions & 2 deletions
This file was deleted.

tests/Adapters/CommonMark/PhikiExtensionTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
55
use League\CommonMark\MarkdownConverter;
66
use Phiki\Adapters\CommonMark\PhikiExtension;
7+
use Phiki\Tests\Fixtures\UselessTransformer;
78
use Phiki\Theme\Theme;
89

910
it('registers renderers', function () {
@@ -47,3 +48,21 @@ class A {}
4748

4849
expect($generated)->toMatchSnapshot();
4950
});
51+
52+
it('understands the info string', function () {
53+
$environment = new Environment;
54+
55+
$environment
56+
->addExtension(new CommonMarkCoreExtension)
57+
->addExtension(new PhikiExtension('github-dark'));
58+
59+
$markdown = new MarkdownConverter($environment);
60+
61+
$generated = $markdown->convert(<<<'MD'
62+
```php {0-10}
63+
class A {}
64+
```
65+
MD);
66+
67+
expect($generated)->toMatchSnapshot();
68+
});

0 commit comments

Comments
 (0)