@terrafirmagreg-team/mdx 0.0.0-stage → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +61 -3
- package/src/components/docs-image.tsx +62 -0
- package/src/components/mdx-components.tsx +158 -0
- package/src/components/story-fields.tsx +318 -0
- package/src/components/story-panel.tsx +181 -0
- package/src/lib/markdown/dom.ts +104 -0
- package/src/lib/markdown/index.ts +18 -0
- package/src/lib/markdown/providers.ts +26 -0
- package/src/lib/md-plugins.ts +113 -0
- package/src/lib/md.ts +20 -0
- package/src/lib/mdx/accordions.ts +102 -0
- package/src/lib/mdx/banner.ts +57 -0
- package/src/lib/mdx/directive-utils.ts +118 -0
- package/src/lib/mdx/directives.ts +261 -0
- package/src/lib/mdx/game-export.ts +110 -0
- package/src/lib/mdx/html-style.ts +136 -0
- package/src/lib/mdx/preview.ts +253 -0
- package/src/lib/mdx/references.ts +57 -0
- package/src/lib/mdx/tabs.ts +76 -0
- package/src/lib/pack-assets.ts +46 -0
- package/src/lib/remark-highlight.ts +82 -0
- package/src/lib/story-remark.ts +468 -0
- package/src/lib/story.ts +11 -0
- package/src/types/mdx.ts +68 -0
- package/README.md +0 -3
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { remarkDirectives } from './directives.ts';
|
|
2
|
+
import {
|
|
3
|
+
remarkCodeTab,
|
|
4
|
+
remarkHeading,
|
|
5
|
+
remarkImage,
|
|
6
|
+
remarkMdxFiles,
|
|
7
|
+
remarkMdxMermaid,
|
|
8
|
+
remarkNpm,
|
|
9
|
+
remarkSteps,
|
|
10
|
+
} from 'fumadocs-core/mdx-plugins';
|
|
11
|
+
import remarkDirective from 'remark-directive';
|
|
12
|
+
import remarkGfm from 'remark-gfm';
|
|
13
|
+
import remarkMath from 'remark-math';
|
|
14
|
+
import remarkParse from 'remark-parse';
|
|
15
|
+
import { unified } from 'unified';
|
|
16
|
+
import {
|
|
17
|
+
contentChildren,
|
|
18
|
+
extractDirectiveLabel,
|
|
19
|
+
jsxAttr,
|
|
20
|
+
type DirectiveNode,
|
|
21
|
+
type JsxElement,
|
|
22
|
+
type MdastNode,
|
|
23
|
+
} from './directive-utils.ts';
|
|
24
|
+
|
|
25
|
+
type VFileLike = {
|
|
26
|
+
path?: string;
|
|
27
|
+
cwd?: string;
|
|
28
|
+
value?: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type Positioned = {
|
|
32
|
+
position?: {
|
|
33
|
+
start?: { offset?: number };
|
|
34
|
+
end?: { offset?: number };
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const previewSeq = new WeakMap<object, number>();
|
|
39
|
+
|
|
40
|
+
function nextPreviewId(file: VFileLike): number {
|
|
41
|
+
const key = file as object;
|
|
42
|
+
const n = (previewSeq.get(key) ?? 0) + 1;
|
|
43
|
+
previewSeq.set(key, n);
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function slugify(text: string): string {
|
|
48
|
+
return (
|
|
49
|
+
text
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
52
|
+
.replace(/^-+|-+$/g, '') || 'section'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function walk(node: MdastNode, visit: (n: MdastNode) => void): void {
|
|
57
|
+
visit(node);
|
|
58
|
+
for (const child of node.children ?? []) walk(child, visit);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function markHeadingNoToc(heading: MdastNode): void {
|
|
62
|
+
const kids = heading.children ?? [];
|
|
63
|
+
const last = kids[kids.length - 1];
|
|
64
|
+
if (last?.type === 'text' && typeof last.value === 'string') {
|
|
65
|
+
if (!last.value.includes('[!toc]')) {
|
|
66
|
+
last.value = `${last.value.replace(/\s+$/, '')} [!toc]`;
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
kids.push({ type: 'text', value: ' [!toc]' });
|
|
71
|
+
heading.children = kids;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function uniquifyHeadingIds(tree: MdastNode): void {
|
|
75
|
+
const used = new Set<string>();
|
|
76
|
+
walk(tree, (node) => {
|
|
77
|
+
if (node.type !== 'heading') return;
|
|
78
|
+
const props = (node.data?.hProperties ?? {}) as { id?: string };
|
|
79
|
+
node.data ??= {};
|
|
80
|
+
node.data.hProperties = props;
|
|
81
|
+
if (typeof props.id !== 'string' || props.id.length === 0) return;
|
|
82
|
+
|
|
83
|
+
let id = props.id;
|
|
84
|
+
if (!used.has(id)) {
|
|
85
|
+
used.add(id);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let n = 1;
|
|
89
|
+
while (used.has(`${id}-${n}`)) n += 1;
|
|
90
|
+
props.id = `${id}-${n}`;
|
|
91
|
+
used.add(props.id);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function remarkPreview() {
|
|
96
|
+
return async (tree: MdastNode, file: VFileLike) => {
|
|
97
|
+
previewSeq.set(file as object, 0);
|
|
98
|
+
await expandPreviews(tree, file);
|
|
99
|
+
uniquifyHeadingIds(tree);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function expandPreviews(parent: MdastNode, file: VFileLike): Promise<void> {
|
|
104
|
+
const children = parent.children;
|
|
105
|
+
if (!children) return;
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < children.length; i++) {
|
|
108
|
+
const child = children[i]!;
|
|
109
|
+
if (child.type === 'containerDirective' && child.name === 'preview') {
|
|
110
|
+
const expanded = await expandPreview(child as DirectiveNode, file);
|
|
111
|
+
children.splice(i, 1, ...expanded);
|
|
112
|
+
i += expanded.length - 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
await expandPreviews(child, file);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isMdEscapeFence(node: MdastNode): boolean {
|
|
120
|
+
if (node.type !== 'code' || typeof node.value !== 'string') return false;
|
|
121
|
+
const lang = typeof node.lang === 'string' ? node.lang.toLowerCase() : '';
|
|
122
|
+
return lang === 'md' || lang === 'markdown';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function fileText(file: VFileLike): string {
|
|
126
|
+
if (typeof file.value === 'string') return file.value;
|
|
127
|
+
if (file.value == null) return '';
|
|
128
|
+
if (Buffer.isBuffer(file.value)) return file.value.toString('utf8');
|
|
129
|
+
return String(file.value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function offsetOf(node: MdastNode, side: 'start' | 'end'): number | undefined {
|
|
133
|
+
const pos = (node as Positioned).position?.[side]?.offset;
|
|
134
|
+
return typeof pos === 'number' ? pos : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sourceFromChildren(
|
|
138
|
+
kids: MdastNode[],
|
|
139
|
+
file: VFileLike,
|
|
140
|
+
): string | undefined {
|
|
141
|
+
if (kids.length === 0) return undefined;
|
|
142
|
+
|
|
143
|
+
if (kids.length === 1 && isMdEscapeFence(kids[0]!)) {
|
|
144
|
+
return kids[0]!.value as string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const raw = fileText(file);
|
|
148
|
+
const start = offsetOf(kids[0]!, 'start');
|
|
149
|
+
const end = offsetOf(kids[kids.length - 1]!, 'end');
|
|
150
|
+
if (raw && start != null && end != null && end >= start) {
|
|
151
|
+
return raw.slice(start, end).replace(/\r\n/g, '\n').trimEnd();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function expandPreview(
|
|
158
|
+
node: DirectiveNode,
|
|
159
|
+
file: VFileLike,
|
|
160
|
+
): Promise<MdastNode[]> {
|
|
161
|
+
const kids = contentChildren(node);
|
|
162
|
+
const source = sourceFromChildren(kids, file);
|
|
163
|
+
if (source === undefined) return kids;
|
|
164
|
+
|
|
165
|
+
const label = extractDirectiveLabel(node);
|
|
166
|
+
const previewNo = nextPreviewId(file);
|
|
167
|
+
|
|
168
|
+
const input: JsxElement = {
|
|
169
|
+
type: 'mdxJsxFlowElement',
|
|
170
|
+
name: 'Input',
|
|
171
|
+
attributes: label ? [jsxAttr('label', label)] : [],
|
|
172
|
+
children: [
|
|
173
|
+
{
|
|
174
|
+
type: 'code',
|
|
175
|
+
lang: 'md',
|
|
176
|
+
meta: 'title="example.md" lineNumbers',
|
|
177
|
+
value: source,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const parsed = await processOutputBody(source, file, previewNo);
|
|
183
|
+
|
|
184
|
+
const output: JsxElement = {
|
|
185
|
+
type: 'mdxJsxFlowElement',
|
|
186
|
+
name: 'Output',
|
|
187
|
+
attributes: [],
|
|
188
|
+
children: parsed.children ?? [],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const preview: JsxElement = {
|
|
192
|
+
type: 'mdxJsxFlowElement',
|
|
193
|
+
name: 'Preview',
|
|
194
|
+
attributes: [],
|
|
195
|
+
children: [input as unknown as MdastNode, output as unknown as MdastNode],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return [preview as unknown as MdastNode];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function processOutputBody(
|
|
202
|
+
source: string,
|
|
203
|
+
file: VFileLike,
|
|
204
|
+
previewNo: number,
|
|
205
|
+
): Promise<MdastNode> {
|
|
206
|
+
|
|
207
|
+
const { remarkInclude } = await import('fumadocs-core/mdx-plugins');
|
|
208
|
+
|
|
209
|
+
const used = new Map<string, number>();
|
|
210
|
+
|
|
211
|
+
const processor = unified()
|
|
212
|
+
.use(remarkParse)
|
|
213
|
+
.use(remarkGfm)
|
|
214
|
+
.use(remarkMath)
|
|
215
|
+
.use(remarkDirective)
|
|
216
|
+
.use(remarkDirectives)
|
|
217
|
+
.use(remarkInclude)
|
|
218
|
+
.use(remarkImage, { publicDir: 'public', useImport: false })
|
|
219
|
+
.use(remarkNpm)
|
|
220
|
+
.use(remarkCodeTab)
|
|
221
|
+
.use(remarkMdxFiles)
|
|
222
|
+
.use(remarkMdxMermaid)
|
|
223
|
+
.use(remarkSteps)
|
|
224
|
+
.use(remarkHeading, {
|
|
225
|
+
generateToc: false,
|
|
226
|
+
customId: false,
|
|
227
|
+
slug: (_root, _heading, text: string) => {
|
|
228
|
+
const base = slugify(text);
|
|
229
|
+
const n = used.get(base) ?? 0;
|
|
230
|
+
used.set(base, n + 1);
|
|
231
|
+
const local = n === 0 ? base : `${base}-${n}`;
|
|
232
|
+
return `preview-${previewNo}-${local}`;
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const tree = processor.parse({ value: source, path: file.path });
|
|
237
|
+
const subFile = Object.assign(
|
|
238
|
+
Object.create(Object.getPrototypeOf(file)) as VFileLike,
|
|
239
|
+
file,
|
|
240
|
+
{ value: source },
|
|
241
|
+
);
|
|
242
|
+
const result = (await processor.run(
|
|
243
|
+
tree,
|
|
244
|
+
subFile as never,
|
|
245
|
+
)) as unknown as MdastNode;
|
|
246
|
+
|
|
247
|
+
walk(result, (node) => {
|
|
248
|
+
if (node.type === 'heading') markHeadingNoToc(node);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type MdastNode = {
|
|
2
|
+
type: string;
|
|
3
|
+
value?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
children?: MdastNode[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const ISSUE_REF = /^#\d+$/;
|
|
9
|
+
const USER_REF = /^@[a-zA-Z\d](?:[a-zA-Z\d-]{0,37}[a-zA-Z\d])?$/;
|
|
10
|
+
const AUTO_REF = /(\B#\d+\b|\B@[a-zA-Z\d](?:[a-zA-Z\d-]{0,37}[a-zA-Z\d])?)/g;
|
|
11
|
+
const SKIP = new Set([
|
|
12
|
+
'link',
|
|
13
|
+
'linkReference',
|
|
14
|
+
'image',
|
|
15
|
+
'imageReference',
|
|
16
|
+
'code',
|
|
17
|
+
'inlineCode',
|
|
18
|
+
'html',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const CHANGELOG_PATH = /[/\\]changelog[/\\]([^/\\]+)[/\\]/;
|
|
22
|
+
|
|
23
|
+
export function remarkChangelogReferences(options: { datasetConfig?: Record<string, { repo?: string }> } = {}) {
|
|
24
|
+
const datasetConfig = options.datasetConfig ?? {};
|
|
25
|
+
return (tree: MdastNode, file: { path?: string }) => {
|
|
26
|
+
const pack = CHANGELOG_PATH.exec(file.path ?? '')?.[1];
|
|
27
|
+
const repo = pack ? datasetConfig[pack]?.repo : undefined;
|
|
28
|
+
if (!repo) return;
|
|
29
|
+
const repoUrl = `https://github.com/${repo}`;
|
|
30
|
+
const visit = (node: MdastNode) => {
|
|
31
|
+
if (SKIP.has(node.type) || !node.children) return;
|
|
32
|
+
node.children = node.children.flatMap((child): MdastNode[] => {
|
|
33
|
+
if (child.type !== 'text') {
|
|
34
|
+
visit(child);
|
|
35
|
+
return [child];
|
|
36
|
+
}
|
|
37
|
+
return (child.value ?? '').split(AUTO_REF).map((part) =>
|
|
38
|
+
ISSUE_REF.test(part)
|
|
39
|
+
? {
|
|
40
|
+
type: 'link',
|
|
41
|
+
url: `${repoUrl}/issues/${part.slice(1)}`,
|
|
42
|
+
children: [{ type: 'text', value: part }],
|
|
43
|
+
}
|
|
44
|
+
: USER_REF.test(part)
|
|
45
|
+
? {
|
|
46
|
+
type: 'link',
|
|
47
|
+
url: `https://github.com/${part.slice(1)}`,
|
|
48
|
+
children: [{ type: 'text', value: part }],
|
|
49
|
+
}
|
|
50
|
+
: { type: 'text', value: part },
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
visit(tree);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
estreeStringArray,
|
|
3
|
+
extractLeafLabel,
|
|
4
|
+
jsxExpression,
|
|
5
|
+
toJsxAttributes,
|
|
6
|
+
walkContainers,
|
|
7
|
+
type DirectiveNode,
|
|
8
|
+
type JsxElement,
|
|
9
|
+
type MdastNode,
|
|
10
|
+
} from './directive-utils.ts';
|
|
11
|
+
|
|
12
|
+
export function remarkTabs() {
|
|
13
|
+
return (tree: MdastNode) => {
|
|
14
|
+
walkContainers(tree, (directive) => {
|
|
15
|
+
if (directive.name !== 'tabs') return;
|
|
16
|
+
toTabs(directive);
|
|
17
|
+
return false;
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeValue(value: string): string {
|
|
23
|
+
return value.toLowerCase().replace(/\s/, '-');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toTabs(node: DirectiveNode): void {
|
|
27
|
+
const items: string[] = [];
|
|
28
|
+
const tabs: JsxElement[] = [];
|
|
29
|
+
let current: JsxElement | undefined;
|
|
30
|
+
|
|
31
|
+
for (const item of node.children ?? []) {
|
|
32
|
+
if (item.type === 'leafDirective' && (item as { name?: string }).name === 'tab') {
|
|
33
|
+
const label = extractLeafLabel(item);
|
|
34
|
+
items.push(label);
|
|
35
|
+
current = {
|
|
36
|
+
type: 'mdxJsxFlowElement',
|
|
37
|
+
name: 'Tab',
|
|
38
|
+
attributes: [
|
|
39
|
+
{
|
|
40
|
+
type: 'mdxJsxAttribute',
|
|
41
|
+
name: 'value',
|
|
42
|
+
value: escapeValue(label),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
children: [],
|
|
46
|
+
};
|
|
47
|
+
tabs.push(current);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
current?.children.push(item);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const attributes = toJsxAttributes(node).map((attribute) =>
|
|
55
|
+
attribute.name === 'default'
|
|
56
|
+
? {
|
|
57
|
+
type: 'mdxJsxAttribute' as const,
|
|
58
|
+
name: 'defaultValue',
|
|
59
|
+
value: escapeValue(String(attribute.value)),
|
|
60
|
+
}
|
|
61
|
+
: attribute,
|
|
62
|
+
);
|
|
63
|
+
attributes.unshift({
|
|
64
|
+
type: 'mdxJsxAttribute',
|
|
65
|
+
name: 'items',
|
|
66
|
+
value: jsxExpression(estreeStringArray(items)),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
Object.assign(node, {
|
|
70
|
+
type: 'mdxJsxFlowElement',
|
|
71
|
+
name: 'Tabs',
|
|
72
|
+
attributes,
|
|
73
|
+
children: tabs,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DocsImageProps } from '../types/mdx';
|
|
2
|
+
|
|
3
|
+
export function packPublicUrl(pack: string, ...segments: string[]): string {
|
|
4
|
+
const rel = segments
|
|
5
|
+
.map((s) => s.replace(/^\/+/, '').replaceAll('\\', '/'))
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.join('/');
|
|
8
|
+
return rel ? `/${pack}/${rel}` : `/${pack}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function assetUrl(
|
|
12
|
+
pack: string,
|
|
13
|
+
modId: string,
|
|
14
|
+
...pathSegments: string[]
|
|
15
|
+
): string {
|
|
16
|
+
return packPublicUrl(pack, 'assets', modId, ...pathSegments);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveSrc(
|
|
20
|
+
props: Pick<DocsImageProps, 'src' | 'pack' | 'mod' | 'path'>,
|
|
21
|
+
packFallback?: string,
|
|
22
|
+
): string | null {
|
|
23
|
+
if (props.src) {
|
|
24
|
+
const s = props.src.trim();
|
|
25
|
+
if (
|
|
26
|
+
s.startsWith('/') ||
|
|
27
|
+
s.startsWith('http://') ||
|
|
28
|
+
s.startsWith('https://')
|
|
29
|
+
) {
|
|
30
|
+
return s;
|
|
31
|
+
}
|
|
32
|
+
return `/${s.replace(/^\/+/, '')}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pack = props.pack ?? packFallback;
|
|
36
|
+
if (!pack) return null;
|
|
37
|
+
|
|
38
|
+
if (props.mod && props.path) {
|
|
39
|
+
return assetUrl(pack, props.mod, props.path);
|
|
40
|
+
}
|
|
41
|
+
if (props.path) {
|
|
42
|
+
return packPublicUrl(pack, props.path);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type RemarkToken = { text: string; cls?: string };
|
|
2
|
+
|
|
3
|
+
const NAME = 'text-fd-primary font-medium';
|
|
4
|
+
const STR = 'text-diff-add-symbol';
|
|
5
|
+
const MUTED = 'text-fd-muted-foreground';
|
|
6
|
+
|
|
7
|
+
function attrTokens(s: string): RemarkToken[] {
|
|
8
|
+
const out: RemarkToken[] = [];
|
|
9
|
+
const re = /"[^"]*"|[^"]+/g;
|
|
10
|
+
for (const m of s.matchAll(re)) {
|
|
11
|
+
out.push({ text: m[0], cls: m[0].startsWith('"') ? STR : undefined });
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function inlineTokens(s: string): RemarkToken[] {
|
|
17
|
+
const out: RemarkToken[] = [];
|
|
18
|
+
const re = /\[[^\]]*\]|\{[^}]*\}/g;
|
|
19
|
+
let last = 0;
|
|
20
|
+
for (const m of s.matchAll(re)) {
|
|
21
|
+
if (m.index > last) out.push({ text: s.slice(last, m.index) });
|
|
22
|
+
const seg = m[0];
|
|
23
|
+
if (seg.startsWith('[')) {
|
|
24
|
+
out.push({ text: '[', cls: MUTED });
|
|
25
|
+
out.push({ text: seg.slice(1, -1), cls: STR });
|
|
26
|
+
out.push({ text: ']', cls: MUTED });
|
|
27
|
+
} else {
|
|
28
|
+
out.push({ text: '{', cls: MUTED });
|
|
29
|
+
out.push(...attrTokens(seg.slice(1, -1)));
|
|
30
|
+
out.push({ text: '}', cls: MUTED });
|
|
31
|
+
}
|
|
32
|
+
last = m.index + seg.length;
|
|
33
|
+
}
|
|
34
|
+
if (last < s.length) out.push({ text: s.slice(last) });
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function lineTokens(line: string): RemarkToken[] {
|
|
39
|
+
const fence = line.match(/^(```+)(.*)$/);
|
|
40
|
+
if (fence) {
|
|
41
|
+
const rest = fence[2];
|
|
42
|
+
const sp = rest.search(/\s/);
|
|
43
|
+
const lang = sp === -1 ? rest : rest.slice(0, sp);
|
|
44
|
+
const meta = sp === -1 ? '' : rest.slice(sp);
|
|
45
|
+
const toks: RemarkToken[] = [
|
|
46
|
+
{ text: fence[1] + lang, cls: NAME },
|
|
47
|
+
];
|
|
48
|
+
if (meta) toks.push(...attrTokens(meta));
|
|
49
|
+
return toks;
|
|
50
|
+
}
|
|
51
|
+
if (/^:{2,3}$/.test(line)) return [{ text: line, cls: NAME }];
|
|
52
|
+
const dir = line.match(/^(:{1,3})([\w-]+)?(.*)$/);
|
|
53
|
+
if (dir && (dir[2] || dir[1].length >= 2)) {
|
|
54
|
+
return [
|
|
55
|
+
{ text: dir[1], cls: NAME },
|
|
56
|
+
...(dir[2] ? [{ text: dir[2], cls: NAME }] : []),
|
|
57
|
+
...inlineTokens(dir[3]),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
const math = line.match(/^(\${1,2})(.*?)(\${1,2})?$/);
|
|
61
|
+
if (math) {
|
|
62
|
+
const toks: RemarkToken[] = [{ text: math[1], cls: NAME }];
|
|
63
|
+
if (math[2]) toks.push({ text: math[2] });
|
|
64
|
+
if (math[3]) toks.push({ text: math[3], cls: NAME });
|
|
65
|
+
return toks;
|
|
66
|
+
}
|
|
67
|
+
const head = line.match(/^(#{1,6}\s)(.*)$/);
|
|
68
|
+
if (head) {
|
|
69
|
+
return [{ text: head[1], cls: NAME }, { text: head[2] }];
|
|
70
|
+
}
|
|
71
|
+
return [{ text: line }];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function highlightRemark(src: string): RemarkToken[] {
|
|
75
|
+
const out: RemarkToken[] = [];
|
|
76
|
+
src.split('\n').forEach((line, i) => {
|
|
77
|
+
if (i) out.push({ text: '\n' });
|
|
78
|
+
out.push(...lineTokens(line));
|
|
79
|
+
});
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|