comark 0.0.1 → 0.1.0
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/README.md +104 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/internal/frontmatter.d.ts +16 -0
- package/dist/internal/frontmatter.js +43 -0
- package/dist/internal/parse/auto-close/index.d.ts +12 -0
- package/dist/internal/parse/auto-close/index.js +457 -0
- package/dist/internal/parse/auto-close/table.d.ts +4 -0
- package/dist/internal/parse/auto-close/table.js +161 -0
- package/dist/internal/parse/auto-unwrap.d.ts +20 -0
- package/dist/internal/parse/auto-unwrap.js +42 -0
- package/dist/internal/parse/html/html_block_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_block_rule.js +60 -0
- package/dist/internal/parse/html/html_blocks.d.ts +2 -0
- package/dist/internal/parse/html/html_blocks.js +66 -0
- package/dist/internal/parse/html/html_inline_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_inline_rule.js +43 -0
- package/dist/internal/parse/html/html_re.d.ts +3 -0
- package/dist/internal/parse/html/html_re.js +18 -0
- package/dist/internal/parse/html/index.d.ts +18 -0
- package/dist/internal/parse/html/index.js +122 -0
- package/dist/internal/parse/incremental.d.ts +12 -0
- package/dist/internal/parse/incremental.js +39 -0
- package/dist/internal/parse/token-processor.d.ts +9 -0
- package/dist/internal/parse/token-processor.js +803 -0
- package/dist/internal/props-validation.d.ts +12 -0
- package/dist/internal/props-validation.js +112 -0
- package/dist/internal/stringify/attributes.d.ts +21 -0
- package/dist/internal/stringify/attributes.js +67 -0
- package/dist/internal/stringify/handlers/a.d.ts +3 -0
- package/dist/internal/stringify/handlers/a.js +11 -0
- package/dist/internal/stringify/handlers/blockquote.d.ts +3 -0
- package/dist/internal/stringify/handlers/blockquote.js +18 -0
- package/dist/internal/stringify/handlers/br.d.ts +3 -0
- package/dist/internal/stringify/handlers/br.js +3 -0
- package/dist/internal/stringify/handlers/code.d.ts +3 -0
- package/dist/internal/stringify/handlers/code.js +11 -0
- package/dist/internal/stringify/handlers/comment.d.ts +3 -0
- package/dist/internal/stringify/handlers/comment.js +6 -0
- package/dist/internal/stringify/handlers/del.d.ts +3 -0
- package/dist/internal/stringify/handlers/del.js +4 -0
- package/dist/internal/stringify/handlers/emphesis.d.ts +3 -0
- package/dist/internal/stringify/handlers/emphesis.js +13 -0
- package/dist/internal/stringify/handlers/heading.d.ts +3 -0
- package/dist/internal/stringify/handlers/heading.js +7 -0
- package/dist/internal/stringify/handlers/hr.d.ts +3 -0
- package/dist/internal/stringify/handlers/hr.js +3 -0
- package/dist/internal/stringify/handlers/html.d.ts +3 -0
- package/dist/internal/stringify/handlers/html.js +73 -0
- package/dist/internal/stringify/handlers/img.d.ts +3 -0
- package/dist/internal/stringify/handlers/img.js +9 -0
- package/dist/internal/stringify/handlers/index.d.ts +2 -0
- package/dist/internal/stringify/handlers/index.js +56 -0
- package/dist/internal/stringify/handlers/li.d.ts +3 -0
- package/dist/internal/stringify/handlers/li.js +43 -0
- package/dist/internal/stringify/handlers/math.d.ts +3 -0
- package/dist/internal/stringify/handlers/math.js +8 -0
- package/dist/internal/stringify/handlers/mdc.d.ts +3 -0
- package/dist/internal/stringify/handlers/mdc.js +47 -0
- package/dist/internal/stringify/handlers/mermaid.d.ts +3 -0
- package/dist/internal/stringify/handlers/mermaid.js +8 -0
- package/dist/internal/stringify/handlers/ol.d.ts +3 -0
- package/dist/internal/stringify/handlers/ol.js +18 -0
- package/dist/internal/stringify/handlers/p.d.ts +3 -0
- package/dist/internal/stringify/handlers/p.js +8 -0
- package/dist/internal/stringify/handlers/pre.d.ts +3 -0
- package/dist/internal/stringify/handlers/pre.js +60 -0
- package/dist/internal/stringify/handlers/strong.d.ts +3 -0
- package/dist/internal/stringify/handlers/strong.js +13 -0
- package/dist/internal/stringify/handlers/table.d.ts +8 -0
- package/dist/internal/stringify/handlers/table.js +180 -0
- package/dist/internal/stringify/handlers/template.d.ts +3 -0
- package/dist/internal/stringify/handlers/template.js +14 -0
- package/dist/internal/stringify/handlers/ul.d.ts +3 -0
- package/dist/internal/stringify/handlers/ul.js +18 -0
- package/dist/internal/stringify/indent.d.ts +4 -0
- package/dist/internal/stringify/indent.js +8 -0
- package/dist/internal/stringify/state.d.ts +13 -0
- package/dist/internal/stringify/state.js +121 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.js +51 -0
- package/dist/parse.d.ts +66 -0
- package/dist/parse.js +163 -0
- package/dist/plugins/alert.d.ts +2 -0
- package/dist/plugins/alert.js +66 -0
- package/dist/plugins/emoji.d.ts +3 -0
- package/dist/plugins/emoji.js +438 -0
- package/dist/plugins/headings.d.ts +48 -0
- package/dist/plugins/headings.js +85 -0
- package/dist/plugins/highlight.d.ts +63 -0
- package/dist/plugins/highlight.js +235 -0
- package/dist/plugins/math.d.ts +59 -0
- package/dist/plugins/math.js +263 -0
- package/dist/plugins/mermaid.d.ts +38 -0
- package/dist/plugins/mermaid.js +185 -0
- package/dist/plugins/security.d.ts +11 -0
- package/dist/plugins/security.js +32 -0
- package/dist/plugins/summary.d.ts +2 -0
- package/dist/plugins/summary.js +22 -0
- package/dist/plugins/task-list.d.ts +8 -0
- package/dist/plugins/task-list.js +117 -0
- package/dist/plugins/toc.d.ts +15 -0
- package/dist/plugins/toc.js +118 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +29 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.js +1 -0
- package/dist/utils/caret.d.ts +7 -0
- package/dist/utils/caret.js +36 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +149 -0
- package/package.json +73 -9
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ComarkPlugin } from 'comark';
|
|
2
|
+
export interface HeadingsOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Tag to extract as title and set to `tree.meta.title`.
|
|
5
|
+
* @default 'h1'
|
|
6
|
+
*/
|
|
7
|
+
titleTag?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Tag to extract as description and set to `tree.meta.description`.
|
|
10
|
+
* Useful alternatives: `'blockquote'`
|
|
11
|
+
* @default 'p'
|
|
12
|
+
*/
|
|
13
|
+
descriptionTag?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether to remove the extracted nodes from the tree.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
remove?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extracts the title and description from the top of the document and sets
|
|
22
|
+
* them on `tree.meta.title` and `tree.meta.description`.
|
|
23
|
+
*
|
|
24
|
+
* The plugin scans the top-level nodes (ignoring `<hr>` and bare text nodes)
|
|
25
|
+
* and checks the first two content nodes in order:
|
|
26
|
+
*
|
|
27
|
+
* 1. If the first node matches `titleTag` (default `h1`), its text content is
|
|
28
|
+
* written to `tree.meta.title`.
|
|
29
|
+
* 2. If the next content node matches `descriptionTag` (default `p`), its text
|
|
30
|
+
* content is written to `tree.meta.description`. When no title was found,
|
|
31
|
+
* this check starts from the very first content node.
|
|
32
|
+
*
|
|
33
|
+
* Both nodes are removed from the tree by default so they are not rendered
|
|
34
|
+
* twice. Set `remove: false` to keep them in place.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* // Default — h1 as title, first paragraph as description
|
|
39
|
+
* headings()
|
|
40
|
+
*
|
|
41
|
+
* // Use a blockquote as the description instead of a paragraph
|
|
42
|
+
* headings({ descriptionTag: 'blockquote' })
|
|
43
|
+
*
|
|
44
|
+
* // Extract metadata without removing the nodes from the tree
|
|
45
|
+
* headings({ remove: false })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export default function headings(options?: HeadingsOptions): ComarkPlugin;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function getTag(node) {
|
|
2
|
+
if (Array.isArray(node) && node.length >= 1) {
|
|
3
|
+
return node[0];
|
|
4
|
+
}
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
function getChildren(node) {
|
|
8
|
+
if (Array.isArray(node) && node.length > 2) {
|
|
9
|
+
return node.slice(2);
|
|
10
|
+
}
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
function flattenNodeText(node) {
|
|
14
|
+
if (typeof node === 'string') {
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(node)) {
|
|
18
|
+
return getChildren(node).reduce((text, child) => {
|
|
19
|
+
return text + flattenNodeText(child);
|
|
20
|
+
}, '');
|
|
21
|
+
}
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extracts the title and description from the top of the document and sets
|
|
26
|
+
* them on `tree.meta.title` and `tree.meta.description`.
|
|
27
|
+
*
|
|
28
|
+
* The plugin scans the top-level nodes (ignoring `<hr>` and bare text nodes)
|
|
29
|
+
* and checks the first two content nodes in order:
|
|
30
|
+
*
|
|
31
|
+
* 1. If the first node matches `titleTag` (default `h1`), its text content is
|
|
32
|
+
* written to `tree.meta.title`.
|
|
33
|
+
* 2. If the next content node matches `descriptionTag` (default `p`), its text
|
|
34
|
+
* content is written to `tree.meta.description`. When no title was found,
|
|
35
|
+
* this check starts from the very first content node.
|
|
36
|
+
*
|
|
37
|
+
* Both nodes are removed from the tree by default so they are not rendered
|
|
38
|
+
* twice. Set `remove: false` to keep them in place.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // Default — h1 as title, first paragraph as description
|
|
43
|
+
* headings()
|
|
44
|
+
*
|
|
45
|
+
* // Use a blockquote as the description instead of a paragraph
|
|
46
|
+
* headings({ descriptionTag: 'blockquote' })
|
|
47
|
+
*
|
|
48
|
+
* // Extract metadata without removing the nodes from the tree
|
|
49
|
+
* headings({ remove: false })
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export default function headings(options = {}) {
|
|
53
|
+
const { titleTag = 'h1', descriptionTag = 'p', remove = true } = options;
|
|
54
|
+
return {
|
|
55
|
+
name: 'headings',
|
|
56
|
+
post(state) {
|
|
57
|
+
const nodes = state.tree.nodes;
|
|
58
|
+
// Top-level content nodes — skip raw text nodes and <hr>
|
|
59
|
+
const contentNodes = nodes.filter(node => Array.isArray(node) && getTag(node) !== 'hr');
|
|
60
|
+
let titleNodeIndex = -1;
|
|
61
|
+
let descriptionNodeIndex = -1;
|
|
62
|
+
const first = contentNodes[0];
|
|
63
|
+
if (first && getTag(first) === titleTag) {
|
|
64
|
+
titleNodeIndex = nodes.indexOf(first);
|
|
65
|
+
state.tree.meta.title = flattenNodeText(first);
|
|
66
|
+
}
|
|
67
|
+
// Description is the first content node after the (optional) title
|
|
68
|
+
const afterTitle = titleNodeIndex !== -1 ? contentNodes.slice(1) : contentNodes;
|
|
69
|
+
const second = afterTitle[0];
|
|
70
|
+
if (second && getTag(second) === descriptionTag) {
|
|
71
|
+
descriptionNodeIndex = nodes.indexOf(second);
|
|
72
|
+
state.tree.meta.description = flattenNodeText(second);
|
|
73
|
+
}
|
|
74
|
+
if (remove) {
|
|
75
|
+
// Remove in reverse order to preserve indices
|
|
76
|
+
const toRemove = [titleNodeIndex, descriptionNodeIndex]
|
|
77
|
+
.filter(i => i !== -1)
|
|
78
|
+
.sort((a, b) => b - a);
|
|
79
|
+
for (const i of toRemove) {
|
|
80
|
+
nodes.splice(i, 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LanguageRegistration } from 'shiki';
|
|
2
|
+
import type { ComarkNode, ComarkTree, ComarkPlugin } from 'comark';
|
|
3
|
+
import type { ShikiPrimitive, ThemeRegistration } from '@shikijs/primitive';
|
|
4
|
+
export interface HighlightOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether to use the default language definitions
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
registerDefaultLanguages?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether to use the default theme definitions
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
registerDefaultThemes?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Languages to preload. If not specified, languages will be loaded on demand.
|
|
17
|
+
* @default undefined (load on demand)
|
|
18
|
+
*/
|
|
19
|
+
languages?: Array<LanguageRegistration | LanguageRegistration[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Additional themes to preload
|
|
22
|
+
* @default { light: 'material-theme-lighter', dark: 'material-theme-palenight' }
|
|
23
|
+
*/
|
|
24
|
+
themes?: {
|
|
25
|
+
light?: ThemeRegistration;
|
|
26
|
+
dark?: ThemeRegistration;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Whether to add pre styles to the code blocks
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
preStyles?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get or create the Shiki highlighter instance
|
|
36
|
+
* Uses a singleton pattern to avoid creating multiple highlighters
|
|
37
|
+
*/
|
|
38
|
+
export declare function getHighlighter(options?: HighlightOptions): Promise<ShikiPrimitive>;
|
|
39
|
+
/**
|
|
40
|
+
* Highlight code using Shiki with codeToTokens
|
|
41
|
+
* Returns comark nodes built from tokens
|
|
42
|
+
*/
|
|
43
|
+
export declare function highlightCode(code: string, attrs: {
|
|
44
|
+
language?: string;
|
|
45
|
+
class?: string;
|
|
46
|
+
highlights?: number[];
|
|
47
|
+
}, options?: HighlightOptions): Promise<{
|
|
48
|
+
nodes: ComarkNode[];
|
|
49
|
+
language: string;
|
|
50
|
+
bgColor?: string;
|
|
51
|
+
fgColor?: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Apply syntax highlighting to all code blocks in a Comark tree
|
|
55
|
+
* Uses codeToTokens API with batched async operations
|
|
56
|
+
*/
|
|
57
|
+
export declare function highlightCodeBlocks(tree: ComarkTree, options?: HighlightOptions): Promise<ComarkTree>;
|
|
58
|
+
/**
|
|
59
|
+
* Reset the highlighter instance
|
|
60
|
+
* Useful for testing or when you want to reconfigure
|
|
61
|
+
*/
|
|
62
|
+
export declare function resetHighlighter(): void;
|
|
63
|
+
export default function highlight(options?: HighlightOptions): ComarkPlugin;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { createShikiPrimitive, codeToTokensWithThemes } from '@shikijs/primitive';
|
|
2
|
+
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
|
|
3
|
+
let highlighter = null;
|
|
4
|
+
let highlighterPromise = null;
|
|
5
|
+
const loadedThemes = new Set();
|
|
6
|
+
const loadedLanguages = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Get or create the Shiki highlighter instance
|
|
9
|
+
* Uses a singleton pattern to avoid creating multiple highlighters
|
|
10
|
+
*/
|
|
11
|
+
export async function getHighlighter(options = {}) {
|
|
12
|
+
// If highlighter exists, load any new themes that aren't loaded yet
|
|
13
|
+
if (highlighter) {
|
|
14
|
+
const { themes, languages } = await registerDefaults(options);
|
|
15
|
+
await Promise.all(themes.map(theme => loadTheme(highlighter, theme)));
|
|
16
|
+
await Promise.all(languages.map(language => loadLanguage(highlighter, language)));
|
|
17
|
+
return highlighter;
|
|
18
|
+
}
|
|
19
|
+
if (highlighterPromise) {
|
|
20
|
+
return highlighterPromise;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
highlighterPromise = (async () => {
|
|
24
|
+
const { themes, languages } = await registerDefaults(options);
|
|
25
|
+
const hl = createShikiPrimitive({
|
|
26
|
+
themes: themes,
|
|
27
|
+
langs: languages,
|
|
28
|
+
langAlias: {
|
|
29
|
+
md: 'mdc',
|
|
30
|
+
markdown: 'mdc',
|
|
31
|
+
comark: 'mdc',
|
|
32
|
+
},
|
|
33
|
+
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
|
34
|
+
});
|
|
35
|
+
await Promise.all(themes.map(theme => loadTheme(hl, theme)));
|
|
36
|
+
await Promise.all(languages.map(language => loadLanguage(hl, language)));
|
|
37
|
+
return hl;
|
|
38
|
+
})();
|
|
39
|
+
highlighter = await highlighterPromise;
|
|
40
|
+
highlighterPromise = null;
|
|
41
|
+
return highlighter;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('Failed to create highlighter: make sure `shiki` is installed', error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convert color to inline style
|
|
50
|
+
*/
|
|
51
|
+
function colorToStyle(token) {
|
|
52
|
+
if (!token)
|
|
53
|
+
return undefined;
|
|
54
|
+
const variants = token.variants;
|
|
55
|
+
if (!variants)
|
|
56
|
+
return `color:${token.color}`;
|
|
57
|
+
const { light: lc, dark: dc } = variants;
|
|
58
|
+
if (!lc?.color || !dc?.color)
|
|
59
|
+
return undefined;
|
|
60
|
+
return lc.color === dc.color ? `color:${lc.color}` : `color:${lc.color};--shiki-dark:${dc.color}`;
|
|
61
|
+
}
|
|
62
|
+
async function registerDefaults(options) {
|
|
63
|
+
const themes = Object.values(options.themes || {});
|
|
64
|
+
const languages = options.languages || [];
|
|
65
|
+
const promises = [];
|
|
66
|
+
if (options.registerDefaultThemes !== false) {
|
|
67
|
+
promises.push(import('@shikijs/themes/material-theme-lighter').then(m => ({ type: 'theme', value: m.default })), import('@shikijs/themes/material-theme-palenight').then(m => ({ type: 'theme', value: m.default })));
|
|
68
|
+
}
|
|
69
|
+
if (options.registerDefaultLanguages !== false) {
|
|
70
|
+
promises.push(import('@shikijs/langs/vue').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/tsx').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/svelte').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/typescript').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/javascript').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/mdc').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/bash').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/json').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/yaml').then(m => ({ type: 'lang', value: m.default })), import('@shikijs/langs/astro').then(m => ({ type: 'lang', value: m.default })));
|
|
71
|
+
}
|
|
72
|
+
const results = await Promise.all(promises);
|
|
73
|
+
for (const result of results) {
|
|
74
|
+
if (result.type === 'theme')
|
|
75
|
+
themes.push(result.value);
|
|
76
|
+
else
|
|
77
|
+
languages.push(result.value);
|
|
78
|
+
}
|
|
79
|
+
return { themes, languages };
|
|
80
|
+
}
|
|
81
|
+
async function loadTheme(hl, theme) {
|
|
82
|
+
if (loadedThemes.has(theme.name || '')) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await hl.loadTheme(theme);
|
|
86
|
+
loadedThemes.add(theme.name || '');
|
|
87
|
+
}
|
|
88
|
+
async function loadLanguage(hl, language) {
|
|
89
|
+
if (loadedLanguages.has(Array.isArray(language) ? language.map(l => l.name || '').join(',') : language.name || '')) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await hl.loadLanguage(language);
|
|
93
|
+
loadedLanguages.add(Array.isArray(language) ? language.map(l => l.name || '').join(',') : language.name || '');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Highlight code using Shiki with codeToTokens
|
|
97
|
+
* Returns comark nodes built from tokens
|
|
98
|
+
*/
|
|
99
|
+
export async function highlightCode(code, attrs, options = {}) {
|
|
100
|
+
// Extract language from attributes
|
|
101
|
+
const language = attrs?.language;
|
|
102
|
+
try {
|
|
103
|
+
const hl = await getHighlighter(options);
|
|
104
|
+
const { themes = { light: 'material-theme-lighter', dark: 'material-theme-palenight' } } = options;
|
|
105
|
+
// Use codeToTokens to get raw tokens
|
|
106
|
+
const result = codeToTokensWithThemes(hl, code, {
|
|
107
|
+
lang: language,
|
|
108
|
+
themes: {
|
|
109
|
+
light: themes.light || themes.dark || 'material-theme-lighter',
|
|
110
|
+
dark: themes.dark || themes.light || 'material-theme-palenight',
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
// Build comark nodes from tokens (flatten all lines)
|
|
114
|
+
const allTokens = Array.from({ length: result.length });
|
|
115
|
+
const highlights = attrs.highlights;
|
|
116
|
+
for (let i = 0; i < result.length; i++) {
|
|
117
|
+
const lineTokens = result[i];
|
|
118
|
+
const children = Array.from({ length: lineTokens.length });
|
|
119
|
+
for (let j = 0; j < lineTokens.length; j++) {
|
|
120
|
+
const token = lineTokens[j];
|
|
121
|
+
const style = colorToStyle(token);
|
|
122
|
+
children[j] = style ? ['span', { style }, token.content] : token.content;
|
|
123
|
+
}
|
|
124
|
+
const lineClass = 'line' + (highlights?.includes(i + 1) ? ' highlight' : '');
|
|
125
|
+
allTokens[i] = ['span', { class: lineClass }, ...children];
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
nodes: allTokens,
|
|
129
|
+
language,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// If highlighting fails, return the original code
|
|
134
|
+
console.error('Shiki highlighting error:', error);
|
|
135
|
+
return {
|
|
136
|
+
nodes: [code],
|
|
137
|
+
language,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Apply syntax highlighting to all code blocks in a Comark tree
|
|
143
|
+
* Uses codeToTokens API with batched async operations
|
|
144
|
+
*/
|
|
145
|
+
export async function highlightCodeBlocks(tree, options = {}) {
|
|
146
|
+
const codeBlocks = [];
|
|
147
|
+
const findCodeBlocks = (nodes, path) => {
|
|
148
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
149
|
+
const node = nodes[i];
|
|
150
|
+
if (typeof node === 'string')
|
|
151
|
+
continue;
|
|
152
|
+
if (!Array.isArray(node) || node.length < 3)
|
|
153
|
+
continue;
|
|
154
|
+
if (node[0] === 'pre' && Array.isArray(node[2]) && node[2][0] === 'code') {
|
|
155
|
+
const codeContent = node[2][2];
|
|
156
|
+
if (typeof codeContent === 'string') {
|
|
157
|
+
codeBlocks.push({ node, path: [...path, i] });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
findCodeBlocks(node.slice(2), [...path, i]);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
findCodeBlocks(tree.nodes, []);
|
|
164
|
+
if (codeBlocks.length === 0)
|
|
165
|
+
return tree;
|
|
166
|
+
const highlightedResults = await Promise.all(codeBlocks.map(({ node }) => {
|
|
167
|
+
const attrs = node[1];
|
|
168
|
+
const codeContent = node[2][2];
|
|
169
|
+
return highlightCode(codeContent, attrs, options);
|
|
170
|
+
}));
|
|
171
|
+
const newNodes = JSON.parse(JSON.stringify(tree.nodes));
|
|
172
|
+
for (let i = 0; i < codeBlocks.length; i++) {
|
|
173
|
+
const { node, path } = codeBlocks[i];
|
|
174
|
+
const result = highlightedResults[i];
|
|
175
|
+
const { nodes } = result;
|
|
176
|
+
const preAttrs = node[1];
|
|
177
|
+
const newPreAttrs = {
|
|
178
|
+
...preAttrs,
|
|
179
|
+
class: ['shiki', options.themes?.light?.name, options.themes?.dark?.name ? `dark:${options.themes?.dark?.name}` : ''].filter(Boolean).join(' '),
|
|
180
|
+
tabindex: '0',
|
|
181
|
+
};
|
|
182
|
+
if (options.preStyles) {
|
|
183
|
+
const lightTheme = options.themes?.light;
|
|
184
|
+
const darkTheme = options.themes?.dark;
|
|
185
|
+
const styles = [];
|
|
186
|
+
if (lightTheme?.colors?.['editor.background']) {
|
|
187
|
+
styles.push(`background-color:${lightTheme.colors['editor.background']}`);
|
|
188
|
+
}
|
|
189
|
+
if (lightTheme?.colors?.['editor.foreground']) {
|
|
190
|
+
styles.push(`color:${lightTheme.colors['editor.foreground']}`);
|
|
191
|
+
}
|
|
192
|
+
if (lightTheme?.name !== darkTheme?.name) {
|
|
193
|
+
if (darkTheme?.colors?.['editor.background']) {
|
|
194
|
+
styles.push(`--shiki-dark-bg:${darkTheme.colors['editor.background']}`);
|
|
195
|
+
}
|
|
196
|
+
if (darkTheme?.colors?.['editor.foreground']) {
|
|
197
|
+
styles.push(`--shiki-dark:${darkTheme.colors['editor.foreground']}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
newPreAttrs.style = styles.join(';');
|
|
201
|
+
}
|
|
202
|
+
const codeEl = node[2];
|
|
203
|
+
const codeAttrs = codeEl[1] || {};
|
|
204
|
+
const newPreNode = ['pre', newPreAttrs, ['code', codeAttrs, ...nodes]];
|
|
205
|
+
if (path.length === 1) {
|
|
206
|
+
newNodes[path[0]] = newPreNode;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
let current = newNodes[path[0]];
|
|
210
|
+
for (let j = 1; j < path.length - 1; j++) {
|
|
211
|
+
current = current[path[j] + 2];
|
|
212
|
+
}
|
|
213
|
+
const childSlot = path[path.length - 1] + 2;
|
|
214
|
+
current[childSlot] = newPreNode;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { ...tree, nodes: newNodes };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Reset the highlighter instance
|
|
221
|
+
* Useful for testing or when you want to reconfigure
|
|
222
|
+
*/
|
|
223
|
+
export function resetHighlighter() {
|
|
224
|
+
highlighter = null;
|
|
225
|
+
highlighterPromise = null;
|
|
226
|
+
loadedThemes.clear();
|
|
227
|
+
}
|
|
228
|
+
export default function highlight(options = {}) {
|
|
229
|
+
return {
|
|
230
|
+
name: 'highlight',
|
|
231
|
+
async post(state) {
|
|
232
|
+
state.tree = await highlightCodeBlocks(state.tree, options);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ComarkPlugin } from 'comark';
|
|
2
|
+
export interface MathConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Throw on parse errors or return error message
|
|
5
|
+
* @default false
|
|
6
|
+
*/
|
|
7
|
+
throwOnError?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Additional katex options
|
|
10
|
+
*/
|
|
11
|
+
options?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Render LaTeX math expression to HTML using KaTeX
|
|
15
|
+
*
|
|
16
|
+
* @param code LaTeX math expression
|
|
17
|
+
* @param displayMode Whether to render in display mode
|
|
18
|
+
* @param config KaTeX configuration
|
|
19
|
+
* @returns HTML string
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const html = renderMath('E = mc^2', false)
|
|
24
|
+
* const display = renderMath('x^2', true)
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderMath(code: string, displayMode: boolean, config?: MathConfig): string;
|
|
28
|
+
/**
|
|
29
|
+
* Check if code is valid LaTeX math syntax
|
|
30
|
+
*
|
|
31
|
+
* @param code LaTeX math expression
|
|
32
|
+
* @returns true if valid, false otherwise
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* validateMath('E = mc^2') // true
|
|
37
|
+
* validateMath('\\invalid') // false
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateMath(code: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Create math plugin for comark
|
|
43
|
+
*
|
|
44
|
+
* This plugin processes markdown-it tokens and converts math expressions
|
|
45
|
+
* with $ and $$ delimiters to HTML using KaTeX.
|
|
46
|
+
*
|
|
47
|
+
* @param config Math rendering configuration
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { parse } from 'comark'
|
|
52
|
+
* import math from 'comark/plugins/math'
|
|
53
|
+
*
|
|
54
|
+
* const result = await parse('Inline $x^2$ and display $$E = mc^2$$', {
|
|
55
|
+
* plugins: [math({ throwOnError: false })]
|
|
56
|
+
* })
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export default function math(config?: MathConfig): ComarkPlugin;
|