Skip to content

Commit aecd161

Browse files
KazariEXantfu
andauthored
feat(transformers): render indent guides (#1060)
Co-authored-by: Anthony Fu <[email protected]>
1 parent 5cbb052 commit aecd161

File tree

5 files changed

+152
-0
lines changed

5 files changed

+152
-0
lines changed

docs/.vitepress/theme/transformers.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ pre.shiki .space::before {
1515
opacity: 0.3;
1616
}
1717

18+
pre.shiki .indent {
19+
display: inline-block;
20+
position: relative;
21+
left: var(--indent-offset);
22+
text-indent: 0;
23+
}
24+
25+
pre.shiki .indent:empty {
26+
height: 1lh;
27+
vertical-align: bottom;
28+
}
29+
30+
pre.shiki .indent::before {
31+
content: '';
32+
position: absolute;
33+
opacity: 0.15;
34+
width: 1px;
35+
height: 100%;
36+
background-color: currentColor;
37+
}
38+
1839
pre.shiki .highlighted-word {
1940
background-color: var(--vp-c-bg-soft);
2041
border: 1px solid var(--vp-c-border);

docs/packages/transformers.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,48 @@ pre.shiki .space::before {
358358

359359
---
360360

361+
### `transformerRenderIndentGuides`
362+
363+
Render indentations as individual spans, with class `indent`.
364+
365+
With some additional CSS rules, you can make it look like this:
366+
367+
<div class="language-js vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">js</span><pre v-pre class="shiki shiki-themes vitesse-light vitesse-dark" style="--shiki-light:#393a34;--shiki-dark:#dbd7caee;--shiki-light-bg:#ffffff;--shiki-dark-bg:#121212;" tabindex="0"><code><span class="line"><span style="color:#CB7676">function</span><span style="color:#80A665"> func</span><span style="color:#666666">()</span><span style="color:#666666"> {</span></span>
368+
<span class="line"><span class="indent"> </span><span style="color:#BD976A">console</span><span style="color:#666666">.</span><span style="color:#80A665">log</span><span style="color:#666666">(</span><span style="color:#4C9A91">1</span><span style="color:#666666">);</span></span>
369+
<span class="line"><span class="indent" style="--indent-offset: 0ch;"></span></span>
370+
<span class="line"><span class="indent"> </span><span style="color:#4D9375">for</span><span style="color:#666666"> (</span><span style="color:#CB7676">const </span><span style="color:#BD976A">i</span><span style="color:#CB7676"> of</span><span style="color:#666666"> [])</span><span style="color:#666666"> {</span></span>
371+
<span class="line"><span class="indent"> </span><span class="indent"> </span><span style="color:#BD976A">console</span><span style="color:#666666">.</span><span style="color:#80A665">log</span><span style="color:#666666">(</span><span style="color:#4C9A91">2</span><span style="color:#666666">);</span></span>
372+
<span class="line"><span class="indent"> </span><span style="color:#666666">}</span></span>
373+
<span class="line"><span style="color:#666666">}</span></span></code></pre></div>
374+
375+
::: details Example CSS
376+
377+
```css
378+
pre.shiki .indent {
379+
display: inline-block;
380+
position: relative;
381+
left: var(--indent-offset);
382+
}
383+
384+
pre.shiki .indent:empty {
385+
height: 1lh;
386+
vertical-align: bottom;
387+
}
388+
389+
pre.shiki .indent::before {
390+
content: '';
391+
position: absolute;
392+
opacity: 0.15;
393+
width: 1px;
394+
height: 100%;
395+
background-color: currentColor;
396+
}
397+
```
398+
399+
:::
400+
401+
---
402+
361403
### `transformerMetaHighlight`
362404

363405
Highlight lines based on the [meta string](/guide/transformers#meta) provided on the code snippet.

packages/transformers/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ export * from './transformers/notation-highlight-word'
1111
export * from './transformers/notation-map'
1212
export * from './transformers/remove-line-breaks'
1313
export * from './transformers/remove-notation-escape'
14+
export * from './transformers/render-indent-guides'
1415
export * from './transformers/render-whitespace'
1516
export * from './transformers/style-to-class'
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { ShikiTransformer } from '@shikijs/types'
2+
import type { Element } from 'hast'
3+
4+
export interface TransformerRenderIndentGuidesOptions {
5+
indent?: number
6+
}
7+
8+
/**
9+
* Render indentations as separate tokens.
10+
* Apply with CSS, it can be used to render indent guides visually.
11+
*/
12+
export function transformerRenderIndentGuides(
13+
options: TransformerRenderIndentGuidesOptions = {},
14+
): ShikiTransformer {
15+
return {
16+
name: '@shikijs/transformers:render-indent-guides',
17+
code(hast) {
18+
let { indent = 2 } = options
19+
20+
const match = this.options.meta?.__raw?.match(/\{indent:(\d+|false)\}/)
21+
if (match) {
22+
if (match[1] === 'false') {
23+
return hast
24+
}
25+
indent = Number(match[1])
26+
}
27+
const indentRegex = new RegExp(` {${indent}}| {0,${indent - 1}}\t| {1,}$`, 'g')
28+
29+
const emptyLines: [Element, number][] = []
30+
let level = 0
31+
32+
for (const line of hast.children) {
33+
if (line.type !== 'element') {
34+
continue
35+
}
36+
37+
const first = line.children[0]
38+
if (first?.type !== 'element' || first?.children[0]?.type !== 'text') {
39+
emptyLines.push([line, level])
40+
continue
41+
}
42+
43+
const text = first.children[0]
44+
const blanks = text.value.split(/[^ \t]/, 1)[0]
45+
46+
const ranges: [number, number][] = []
47+
for (const match of blanks.matchAll(indentRegex)) {
48+
const start = match.index
49+
const end = start + match[0].length
50+
ranges.push([start, end])
51+
}
52+
53+
for (const [line, level] of emptyLines) {
54+
line.children.unshift(...Array.from({ length: Math.min(ranges.length, level + 1) }, (_, i) => ({
55+
type: 'element',
56+
tagName: 'span',
57+
properties: {
58+
class: 'indent',
59+
style: `--indent-offset: ${i * indent}ch;`,
60+
},
61+
children: [],
62+
} satisfies Element)))
63+
}
64+
emptyLines.length = 0
65+
level = ranges.length
66+
67+
if (ranges.length) {
68+
line.children.unshift(
69+
...ranges.map(([start, end]) => ({
70+
type: 'element',
71+
tagName: 'span',
72+
properties: {
73+
class: 'indent',
74+
},
75+
children: [{
76+
type: 'text',
77+
value: text.value.slice(start, end),
78+
}],
79+
} satisfies Element)),
80+
)
81+
text.value = text.value.slice(ranges.at(-1)![1])
82+
}
83+
}
84+
return hast
85+
},
86+
}
87+
}

test/exports/@shikijs/transformers.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
transformerNotationWordHighlight: function
1515
transformerRemoveLineBreak: function
1616
transformerRemoveNotationEscape: function
17+
transformerRenderIndentGuides: function
1718
transformerRenderWhitespace: function
1819
transformerStyleToClass: function

0 commit comments

Comments
 (0)