Skip to content

Commit 4539602

Browse files
committed
feat: add command color transformer and color definitions for syntax highlighting
1 parent 5311a07 commit 4539602

File tree

3 files changed

+191
-4
lines changed

3 files changed

+191
-4
lines changed

source.config.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { defineConfig, defineDocs, frontmatterSchema, metaSchema } from "fumadocs-mdx/config"
1+
import { defineConfig, defineDocs, frontmatterSchema, metaSchema } from "fumadocs-mdx/config";
2+
import { transformerCommandColor } from "./src/lib/command-transformer";
23

34
// You can customise Zod schemas for frontmatter and `meta.json` here
45
// see https://fumadocs.vercel.app/docs/mdx/collections#define-docs
@@ -9,10 +10,21 @@ export const docs = defineDocs({
910
meta: {
1011
schema: metaSchema,
1112
},
12-
})
13+
});
1314

1415
export default defineConfig({
1516
mdxOptions: {
16-
// MDX options
17+
rehypeCodeOptions: {
18+
themes: {
19+
light: "github-light",
20+
dark: "github-dark",
21+
},
22+
langAlias: {
23+
command: 'text',
24+
},
25+
transformers: [
26+
transformerCommandColor(),
27+
],
28+
},
1729
},
18-
})
30+
});

src/lib/command-colors.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export interface CommandColor {
2+
hsl: string;
3+
}
4+
5+
/**
6+
* Represents a color used by the command highlighter.
7+
* Currently only HSL is used by the transformer for inline styles.
8+
*
9+
* @example
10+
* ```command
11+
* <green>content</green>
12+
* ```
13+
*/
14+
export const commandColors: Record<string, CommandColor> = {
15+
green: {
16+
hsl: "hsl(142, 76%, 36%)",
17+
},
18+
red: {
19+
hsl: "hsl(0, 84%, 60%)",
20+
},
21+
blue: {
22+
hsl: "hsl(221, 83%, 53%)",
23+
},
24+
yellow: {
25+
hsl: "hsl(48, 96%, 53%)",
26+
},
27+
purple: {
28+
hsl: "hsl(262, 83%, 58%)",
29+
},
30+
orange: {
31+
hsl: "hsl(25, 95%, 53%)",
32+
},
33+
cyan: {
34+
hsl: "hsl(187, 100%, 42%)",
35+
},
36+
pink: {
37+
hsl: "hsl(330, 81%, 60%)",
38+
},
39+
gray: {
40+
hsl: "hsl(215, 16%, 47%)",
41+
},
42+
white: {
43+
hsl: "hsl(0, 0%, 100%)",
44+
},
45+
black: {
46+
hsl: "hsl(0, 0%, 0%)",
47+
}
48+
};
49+
50+
export function getCommandColor(colorName: string): CommandColor | null {
51+
return commandColors[colorName.toLowerCase()] || null;
52+
}

src/lib/command-transformer.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { ShikiTransformer, ThemedToken } from '@shikijs/types'
2+
import { getCommandColor } from './command-colors'
3+
4+
interface ColorContentRange {
5+
start: number
6+
end: number
7+
color: string
8+
}
9+
10+
interface HideRange {
11+
start: number
12+
end: number
13+
}
14+
15+
interface MetaRanges {
16+
contents: ColorContentRange[]
17+
hides: HideRange[]
18+
}
19+
20+
function detectRanges(code: string): MetaRanges {
21+
const contents: ColorContentRange[] = []
22+
const hides: HideRange[] = []
23+
24+
const re = /<([A-Za-z]+)>([\s\S]*?)<\/\1>/g
25+
let match: RegExpExecArray | null
26+
while ((match = re.exec(code)) !== null) {
27+
const [full, colorName, inner] = match
28+
const openStart = match.index
29+
const openEnd = openStart + `<${colorName}>`.length
30+
const closeEnd = openStart + full.length
31+
const closeStart = closeEnd - `</${colorName}>`.length
32+
hides.push({ start: openStart, end: openEnd })
33+
hides.push({ start: closeStart, end: closeEnd })
34+
contents.push({ start: openEnd, end: closeStart, color: colorName })
35+
}
36+
37+
return { contents, hides }
38+
}
39+
40+
function splitTokenAtOffsets(token: ThemedToken, breakpoints: number[]): ThemedToken[] {
41+
if (!breakpoints.length) return [token]
42+
const local = Array.from(new Set(breakpoints.filter(bp => bp > token.offset && bp < token.offset + token.content.length)))
43+
.sort((a, b) => a - b)
44+
45+
if (!local.length) return [token]
46+
47+
const result: ThemedToken[] = []
48+
let last = 0
49+
for (const bp of local) {
50+
const idx = bp - token.offset
51+
if (idx > last) {
52+
result.push({ ...token, content: token.content.slice(last, idx), offset: token.offset + last })
53+
}
54+
last = idx
55+
}
56+
if (last < token.content.length) {
57+
result.push({ ...token, content: token.content.slice(last), offset: token.offset + last })
58+
}
59+
return result
60+
}
61+
62+
export function transformerCommandColor(): ShikiTransformer {
63+
const map = new WeakMap<object, MetaRanges>()
64+
65+
return {
66+
name: 'color-tags',
67+
preprocess(code) {
68+
const ranges = detectRanges(code)
69+
const metaKey = (this as unknown as { meta?: object }).meta ?? {}
70+
map.set(metaKey, ranges)
71+
return undefined
72+
},
73+
tokens(lines) {
74+
const metaKey = (this as unknown as { meta?: object }).meta ?? {}
75+
const ranges = map.get(metaKey)
76+
if (!ranges) return lines
77+
78+
return lines.map((line) => {
79+
if (!line.length) return line
80+
81+
const start = line[0]
82+
const end = line[line.length - 1]
83+
const lineStart = start.offset
84+
const lineEnd = end.offset + end.content.length
85+
86+
const bps: number[] = []
87+
for (const r of [...ranges.hides, ...ranges.contents]) {
88+
if (r.start > lineStart && r.start < lineEnd) bps.push(r.start)
89+
if (r.end > lineStart && r.end < lineEnd) bps.push(r.end)
90+
}
91+
92+
if (!bps.length) return line
93+
94+
const splitted = line.flatMap(t => splitTokenAtOffsets(t, bps))
95+
96+
const styled: ThemedToken[] = splitted.map(seg => {
97+
const inHide = ranges.hides.some(r => r.start <= seg.offset && seg.offset + seg.content.length <= r.end)
98+
if (inHide) {
99+
return {
100+
...seg,
101+
htmlStyle: { ...(seg.htmlStyle || {}), display: 'none' },
102+
}
103+
}
104+
105+
const contentRange = ranges.contents.find(r => r.start <= seg.offset && seg.offset + seg.content.length <= r.end)
106+
if (contentRange) {
107+
const cfg = getCommandColor(contentRange.color)
108+
if (cfg) {
109+
return {
110+
...seg,
111+
htmlStyle: { ...(seg.htmlStyle || {}), color: cfg.hsl },
112+
}
113+
}
114+
}
115+
116+
return seg
117+
})
118+
119+
return styled
120+
})
121+
},
122+
}
123+
}

0 commit comments

Comments
 (0)