comark 0.3.1 → 0.4.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.
Files changed (93) hide show
  1. package/dist/internal/frontmatter.d.ts +1 -0
  2. package/dist/internal/frontmatter.js +4 -2
  3. package/dist/internal/parse/auto-close/index.js +69 -31
  4. package/dist/internal/parse/auto-close/table.js +12 -9
  5. package/dist/internal/parse/auto-unwrap.js +6 -10
  6. package/dist/internal/parse/html/html_block_rule.js +10 -16
  7. package/dist/internal/parse/html/html_inline_rule.js +3 -7
  8. package/dist/internal/parse/html/html_re.js +1 -1
  9. package/dist/internal/parse/html/index.d.ts +1 -0
  10. package/dist/internal/parse/html/index.js +15 -3
  11. package/dist/internal/parse/syntax/block-params.d.ts +9 -0
  12. package/dist/internal/parse/syntax/block-params.js +48 -0
  13. package/dist/internal/parse/syntax/brackets.d.ts +8 -0
  14. package/dist/internal/parse/syntax/brackets.js +20 -0
  15. package/dist/internal/parse/syntax/props.d.ts +5 -0
  16. package/dist/internal/parse/syntax/props.js +119 -0
  17. package/dist/internal/parse/token-processor.js +89 -50
  18. package/dist/internal/props-validation.js +4 -9
  19. package/dist/internal/stringify/attributes.d.ts +7 -0
  20. package/dist/internal/stringify/attributes.js +56 -1
  21. package/dist/internal/stringify/handlers/a.js +1 -3
  22. package/dist/internal/stringify/handlers/blockquote.js +19 -4
  23. package/dist/internal/stringify/handlers/code.js +1 -3
  24. package/dist/internal/stringify/handlers/emphesis.js +1 -3
  25. package/dist/internal/stringify/handlers/heading.js +6 -1
  26. package/dist/internal/stringify/handlers/html.js +34 -18
  27. package/dist/internal/stringify/handlers/img.js +1 -3
  28. package/dist/internal/stringify/handlers/li.js +18 -9
  29. package/dist/internal/stringify/handlers/mdc.js +3 -4
  30. package/dist/internal/stringify/handlers/ol.js +12 -2
  31. package/dist/internal/stringify/handlers/p.d.ts +1 -1
  32. package/dist/internal/stringify/handlers/p.js +8 -1
  33. package/dist/internal/stringify/handlers/pre.js +20 -14
  34. package/dist/internal/stringify/handlers/strong.js +1 -3
  35. package/dist/internal/stringify/handlers/table.js +14 -5
  36. package/dist/internal/stringify/handlers/template.js +5 -2
  37. package/dist/internal/stringify/handlers/ul.js +12 -2
  38. package/dist/internal/stringify/state.js +1 -1
  39. package/dist/internal/yaml.js +1 -1
  40. package/dist/parse.d.ts +4 -4
  41. package/dist/parse.js +20 -10
  42. package/dist/plugins/alert.d.ts +1 -1
  43. package/dist/plugins/alert.js +1 -1
  44. package/dist/plugins/binding.d.ts +1 -1
  45. package/dist/plugins/binding.js +1 -3
  46. package/dist/plugins/breaks.d.ts +1 -1
  47. package/dist/plugins/breaks.js +1 -1
  48. package/dist/plugins/emoji.d.ts +1 -1
  49. package/dist/plugins/emoji.js +8 -8
  50. package/dist/plugins/footnotes.d.ts +1 -1
  51. package/dist/plugins/footnotes.js +19 -13
  52. package/dist/plugins/headings.d.ts +19 -8
  53. package/dist/plugins/headings.js +27 -19
  54. package/dist/plugins/highlight.d.ts +2 -12
  55. package/dist/plugins/highlight.js +201 -103
  56. package/dist/plugins/json-render.d.ts +1 -1
  57. package/dist/plugins/json-render.js +5 -9
  58. package/dist/plugins/math.d.ts +1 -1
  59. package/dist/plugins/math.js +4 -6
  60. package/dist/plugins/mermaid.d.ts +1 -1
  61. package/dist/plugins/mermaid.js +6 -20
  62. package/dist/plugins/punctuation.d.ts +1 -1
  63. package/dist/plugins/punctuation.js +5 -6
  64. package/dist/plugins/security.d.ts +1 -1
  65. package/dist/plugins/security.js +2 -2
  66. package/dist/plugins/summary.d.ts +4 -1
  67. package/dist/plugins/syntax.d.ts +49 -0
  68. package/dist/plugins/syntax.js +558 -0
  69. package/dist/plugins/task-list.d.ts +2 -2
  70. package/dist/plugins/task-list.js +11 -8
  71. package/dist/plugins/toc.d.ts +3 -1
  72. package/dist/plugins/toc.js +1 -1
  73. package/dist/types.d.ts +57 -12
  74. package/dist/utils/comark.tmLanguage.d.ts +335 -0
  75. package/dist/utils/comark.tmLanguage.js +597 -0
  76. package/dist/utils/helpers.d.ts +16 -4
  77. package/dist/utils/helpers.js +16 -6
  78. package/dist/utils/index.d.ts +5 -0
  79. package/dist/utils/index.js +25 -3
  80. package/package.json +40 -40
  81. package/skills/comark/references/rendering-svelte.md +51 -7
  82. package/dist/internal/stringify/indent.d.ts +0 -5
  83. package/dist/internal/stringify/indent.js +0 -9
  84. package/dist/vite.d.ts +0 -1
  85. package/dist/vite.js +0 -1
  86. package/skills/skills/comark/AGENTS.md +0 -261
  87. package/skills/skills/comark/SKILL.md +0 -489
  88. package/skills/skills/comark/references/markdown-syntax.md +0 -599
  89. package/skills/skills/comark/references/parsing-ast.md +0 -378
  90. package/skills/skills/comark/references/rendering-react.md +0 -445
  91. package/skills/skills/comark/references/rendering-svelte.md +0 -453
  92. package/skills/skills/comark/references/rendering-vue.md +0 -462
  93. /package/skills/{skills/migrate-mdc-to-comark → migrate-mdc-to-comark}/SKILL.md +0 -0
@@ -35,19 +35,25 @@ function flattenNodeText(node) {
35
35
  * content is written to `tree.meta.description`. When no title was found,
36
36
  * this check starts from the very first content node.
37
37
  *
38
- * Both nodes are removed from the tree by default so they are not rendered
39
- * twice. Set `remove: false` to keep them in place.
38
+ * By default the extracted nodes are kept in the tree. Set `remove: true`
39
+ * to strip them so they are not rendered twice.
40
40
  *
41
41
  * @example
42
42
  * ```ts
43
- * // Default — h1 as title, first paragraph as description
43
+ * // Default — h1 as title, first paragraph as description, nodes kept in tree
44
44
  * headings()
45
45
  *
46
46
  * // Use a blockquote as the description instead of a paragraph
47
47
  * headings({ descriptionTag: 'blockquote' })
48
48
  *
49
- * // Extract metadata without removing the nodes from the tree
50
- * headings({ remove: false })
49
+ * // Extract metadata and remove the matched nodes from the tree
50
+ * headings({ remove: true })
51
+ *
52
+ * // Disable title extraction, only extract description
53
+ * headings({ titleTag: false })
54
+ *
55
+ * // Disable description extraction, only extract title
56
+ * headings({ descriptionTag: false })
51
57
  * ```
52
58
  */
53
59
  export default defineComarkPlugin((options = {}) => {
@@ -57,26 +63,28 @@ export default defineComarkPlugin((options = {}) => {
57
63
  post(state) {
58
64
  const nodes = state.tree.nodes;
59
65
  // Top-level content nodes — skip raw text nodes and <hr>
60
- const contentNodes = nodes.filter(node => Array.isArray(node) && getTag(node) !== 'hr');
66
+ const contentNodes = nodes.filter((node) => Array.isArray(node) && getTag(node) !== 'hr');
61
67
  let titleNodeIndex = -1;
62
68
  let descriptionNodeIndex = -1;
63
- const first = contentNodes[0];
64
- if (first && getTag(first) === titleTag) {
65
- titleNodeIndex = nodes.indexOf(first);
66
- state.tree.meta.title = flattenNodeText(first);
69
+ let nextContentIndex = 0;
70
+ if (titleTag !== false) {
71
+ const first = contentNodes[0];
72
+ if (first && getTag(first) === titleTag) {
73
+ titleNodeIndex = nodes.indexOf(first);
74
+ state.tree.meta.title = flattenNodeText(first);
75
+ nextContentIndex = 1;
76
+ }
67
77
  }
68
- // Description is the first content node after the (optional) title
69
- const afterTitle = titleNodeIndex !== -1 ? contentNodes.slice(1) : contentNodes;
70
- const second = afterTitle[0];
71
- if (second && getTag(second) === descriptionTag) {
72
- descriptionNodeIndex = nodes.indexOf(second);
73
- state.tree.meta.description = flattenNodeText(second);
78
+ if (descriptionTag !== false) {
79
+ const candidate = contentNodes[nextContentIndex];
80
+ if (candidate && getTag(candidate) === descriptionTag) {
81
+ descriptionNodeIndex = nodes.indexOf(candidate);
82
+ state.tree.meta.description = flattenNodeText(candidate);
83
+ }
74
84
  }
75
85
  if (remove) {
76
86
  // Remove in reverse order to preserve indices
77
- const toRemove = [titleNodeIndex, descriptionNodeIndex]
78
- .filter(i => i !== -1)
79
- .sort((a, b) => b - a);
87
+ const toRemove = [titleNodeIndex, descriptionNodeIndex].filter((i) => i !== -1).sort((a, b) => b - a);
80
88
  for (const i of toRemove) {
81
89
  nodes.splice(i, 1);
82
90
  }
@@ -1,5 +1,5 @@
1
1
  import type { LanguageRegistration, ShikiTransformer, ShikiPrimitive, ThemeRegistration } from 'shiki';
2
- import type { ComarkNode, ComarkTree } from 'comark';
2
+ import type { ComarkTree } from 'comark';
3
3
  export interface HighlightOptions {
4
4
  /**
5
5
  * Whether to use the default language definitions
@@ -46,16 +46,6 @@ export interface CodeBlockAttributes {
46
46
  * Uses a singleton pattern to avoid creating multiple highlighters
47
47
  */
48
48
  export declare function getHighlighter(options?: HighlightOptions): Promise<ShikiPrimitive>;
49
- /**
50
- * Highlight code using Shiki with codeToTokens
51
- * Returns comark nodes built from hast
52
- */
53
- export declare function highlightCode(code: string, attrs: CodeBlockAttributes, options?: HighlightOptions): Promise<{
54
- nodes: ComarkNode[];
55
- language: string;
56
- bgColor?: string;
57
- fgColor?: string;
58
- }>;
59
49
  /**
60
50
  * Apply syntax highlighting to all code blocks in a Comark tree
61
51
  * Uses codeToTokens API with batched async operations
@@ -66,5 +56,5 @@ export declare function highlightCodeBlocks(tree: ComarkTree, options?: Highligh
66
56
  * Useful for testing or when you want to reconfigure
67
57
  */
68
58
  export declare function resetHighlighter(): void;
69
- declare const _default: import("comark").ComarkPluginFactory<HighlightOptions>;
59
+ declare const _default: import("comark").ComarkPluginFactory<HighlightOptions, {}, {}>;
70
60
  export default _default;
@@ -1,7 +1,8 @@
1
1
  import { defineComarkPlugin } from "../utils/helpers.js";
2
2
  import { createShikiPrimitive } from 'shiki';
3
3
  import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
4
- import { codeToHast } from 'shiki/core';
4
+ import { codeToHast, codeToTokens, getTokenStyleObject, stringifyTokenStyle } from 'shiki/core';
5
+ import comakLanguage from "../utils/comark.tmLanguage.js";
5
6
  let highlighter = null;
6
7
  let highlighterPromise = null;
7
8
  const loadedThemes = new Set();
@@ -11,11 +12,14 @@ const loadedLanguages = new Set();
11
12
  * Uses a singleton pattern to avoid creating multiple highlighters
12
13
  */
13
14
  export async function getHighlighter(options = {}) {
14
- // If highlighter exists, load any new themes that aren't loaded yet
15
15
  if (highlighter) {
16
+ // Fast path: skip registerDefaults() when no custom themes/languages are requested
17
+ if (!options.themes && !options.languages) {
18
+ return highlighter;
19
+ }
16
20
  const { themes, languages } = await registerDefaults(options);
17
- await Promise.all(themes.map(theme => loadTheme(highlighter, theme)));
18
- await Promise.all(languages.map(language => loadLanguage(highlighter, language)));
21
+ await Promise.all(themes.map((theme) => loadTheme(highlighter, theme)));
22
+ await Promise.all(languages.map((language) => loadLanguage(highlighter, language)));
19
23
  return highlighter;
20
24
  }
21
25
  if (highlighterPromise) {
@@ -28,16 +32,16 @@ export async function getHighlighter(options = {}) {
28
32
  themes: themes,
29
33
  langs: languages,
30
34
  langAlias: {
31
- 'md': 'mdc',
32
- 'markdown': 'mdc',
33
- 'comark': 'mdc',
35
+ md: 'mdc',
36
+ markdown: 'mdc',
37
+ comark: 'mdc',
34
38
  'json-render': 'json',
35
39
  'yaml-render': 'yaml',
36
40
  },
37
41
  engine: createJavaScriptRegexEngine({ forgiving: true }),
38
42
  });
39
- await Promise.all(themes.map(theme => loadTheme(hl, theme)));
40
- await Promise.all(languages.map(language => loadLanguage(hl, language)));
43
+ await Promise.all(themes.map((theme) => loadTheme(hl, theme)));
44
+ await Promise.all(languages.map((language) => loadLanguage(hl, language)));
41
45
  return hl;
42
46
  })();
43
47
  highlighter = await highlighterPromise;
@@ -54,10 +58,18 @@ async function registerDefaults(options) {
54
58
  const languages = options.languages || [];
55
59
  const promises = [];
56
60
  if (options.registerDefaultThemes !== false) {
57
- promises.push(import('shiki/dist/themes/material-theme-lighter.mjs').then(m => ({ type: 'theme', value: m.default })), import('shiki/dist/themes/material-theme-palenight.mjs').then(m => ({ type: 'theme', value: m.default })));
61
+ promises.push(import('shiki/dist/themes/material-theme-lighter.mjs').then((m) => ({
62
+ type: 'theme',
63
+ value: m.default,
64
+ })), import('shiki/dist/themes/material-theme-palenight.mjs').then((m) => ({
65
+ type: 'theme',
66
+ value: m.default,
67
+ })));
58
68
  }
59
69
  if (options.registerDefaultLanguages !== false) {
60
- promises.push(import('shiki/dist/langs/vue.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/tsx.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/svelte.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/typescript.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/javascript.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/mdc.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/bash.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/json.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/yaml.mjs').then(m => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/astro.mjs').then(m => ({ type: 'lang', value: m.default })));
70
+ promises.push(import('shiki/dist/langs/vue.mjs').then((m) => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/tsx.mjs').then((m) => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/svelte.mjs').then((m) => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/typescript.mjs').then((m) => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/javascript.mjs').then((m) => ({ type: 'lang', value: m.default })),
71
+ // import('shiki/dist/langs/mdc.mjs').then(m => ({ type: 'lang' as const, value: m.default })),
72
+ import('shiki/dist/langs/bash.mjs').then((m) => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/json.mjs').then((m) => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/yaml.mjs').then((m) => ({ type: 'lang', value: m.default })), import('shiki/dist/langs/astro.mjs').then((m) => ({ type: 'lang', value: m.default })));
61
73
  }
62
74
  const results = await Promise.all(promises);
63
75
  for (const result of results) {
@@ -66,6 +78,8 @@ async function registerDefaults(options) {
66
78
  else
67
79
  languages.push(result.value);
68
80
  }
81
+ // Remove custom language after updating language in shiki core
82
+ languages.push(comakLanguage);
69
83
  return { themes, languages };
70
84
  }
71
85
  async function loadTheme(hl, theme) {
@@ -76,64 +90,37 @@ async function loadTheme(hl, theme) {
76
90
  loadedThemes.add(theme.name || '');
77
91
  }
78
92
  async function loadLanguage(hl, language) {
79
- if (loadedLanguages.has(Array.isArray(language) ? language.map(l => l.name || '').join(',') : language.name || '')) {
93
+ if (loadedLanguages.has(Array.isArray(language) ? language.map((l) => l.name || '').join(',') : language.name || '')) {
80
94
  return;
81
95
  }
82
96
  await hl.loadLanguage(language);
83
- loadedLanguages.add(Array.isArray(language) ? language.map(l => l.name || '').join(',') : language.name || '');
97
+ loadedLanguages.add(Array.isArray(language) ? language.map((l) => l.name || '').join(',') : language.name || '');
84
98
  }
85
99
  /**
86
- * Highlight code using Shiki with codeToTokens
87
- * Returns comark nodes built from hast
100
+ * Convert a hast (HTML AST) node into a ComarkNode.
101
+ * Uses pre-allocated arrays to avoid spread overhead.
88
102
  */
89
- export async function highlightCode(code, attrs, options = {}) {
90
- // Extract language from attributes
91
- const language = attrs?.language;
92
- try {
93
- const hl = await getHighlighter(options);
94
- const { themes = { light: 'material-theme-lighter', dark: 'material-theme-palenight' } } = options;
95
- const lightTheme = themes.light || themes.dark || 'material-theme-lighter';
96
- const darkTheme = themes.dark || themes.light || 'material-theme-palenight';
97
- // Use codeToTokens to get raw tokens
98
- const result = await codeToHast(hl, code, {
99
- lang: language,
100
- transformers: options.transformers,
101
- themes: {
102
- light: lightTheme,
103
- dark: lightTheme !== darkTheme ? darkTheme : undefined,
104
- },
105
- meta: {
106
- __raw: attrs.meta,
107
- },
108
- });
109
- const allTokens = result.children.map(hastToMinimarkNode);
110
- return {
111
- nodes: allTokens,
112
- language,
113
- };
114
- }
115
- catch (error) {
116
- // If highlighting fails, return the original code
117
- console.error('Shiki highlighting error:', error);
118
- return {
119
- nodes: [code],
120
- language,
121
- };
103
+ function hastToComarkNode(input) {
104
+ if (input.type === 'text')
105
+ return input.value;
106
+ if (input.type === 'comment')
107
+ return [null, {}, input.value];
108
+ const props = input.properties || {};
109
+ if (input.tag === 'code' && props?.className && props.className.length === 0) {
110
+ delete props.className;
122
111
  }
123
- function hastToMinimarkNode(input) {
124
- const props = input.properties || {};
125
- if (input.type === 'comment')
126
- return [null, {}, input.value];
127
- if (input.type === 'text')
128
- return input.value;
129
- if (input.tag === 'code' && props?.className && props.className.length === 0)
130
- delete props.className;
131
- return [
132
- input.tagName,
133
- props,
134
- ...(input.children || []).map(hastToMinimarkNode),
135
- ];
112
+ const children = input.children;
113
+ if (!children || children.length === 0)
114
+ return [input.tagName, props];
115
+ const len = children.length;
116
+ // eslint-disable-next-line unicorn/no-new-array -- pre-allocated for perf
117
+ const result = new Array(len + 2);
118
+ result[0] = input.tagName;
119
+ result[1] = props;
120
+ for (let i = 0; i < len; i++) {
121
+ result[i + 2] = hastToComarkNode(children[i]);
136
122
  }
123
+ return result;
137
124
  }
138
125
  /**
139
126
  * Apply syntax highlighting to all code blocks in a Comark tree
@@ -141,63 +128,162 @@ export async function highlightCode(code, attrs, options = {}) {
141
128
  */
142
129
  export async function highlightCodeBlocks(tree, options = {}) {
143
130
  const codeBlocks = [];
144
- const findCodeBlocks = (nodes, path) => {
145
- for (let i = 0; i < nodes.length; i++) {
146
- const node = nodes[i];
147
- if (typeof node === 'string')
131
+ const pathBuf = [];
132
+ // Recursively find <pre><code> blocks, tracking their path via push/pop on a shared buffer
133
+ const walkChildren = (element) => {
134
+ for (let i = 2; i < element.length; i++) {
135
+ const child = element[i];
136
+ if (typeof child === 'string')
148
137
  continue;
149
- if (!Array.isArray(node) || node.length < 3)
138
+ if (!Array.isArray(child) || child.length < 3)
150
139
  continue;
151
- if (node[0] === 'pre' && Array.isArray(node[2]) && node[2][0] === 'code') {
152
- const codeContent = node[2][2];
140
+ pathBuf.push(i - 2);
141
+ if (child[0] === 'pre' && Array.isArray(child[2]) && child[2][0] === 'code') {
142
+ const codeContent = child[2][2];
153
143
  if (typeof codeContent === 'string') {
154
- codeBlocks.push({ node, path: [...path, i] });
144
+ codeBlocks.push({ node: child, path: pathBuf.slice() });
155
145
  }
156
146
  }
157
- findCodeBlocks(node.slice(2), [...path, i]);
147
+ walkChildren(child);
148
+ pathBuf.pop();
158
149
  }
159
150
  };
160
- findCodeBlocks(tree.nodes, []);
151
+ for (let i = 0; i < tree.nodes.length; i++) {
152
+ const node = tree.nodes[i];
153
+ if (typeof node === 'string')
154
+ continue;
155
+ if (!Array.isArray(node) || node.length < 3)
156
+ continue;
157
+ if (node[0] === 'pre' && Array.isArray(node[2]) && node[2][0] === 'code') {
158
+ const codeContent = node[2][2];
159
+ if (typeof codeContent === 'string') {
160
+ codeBlocks.push({ node, path: [i] });
161
+ }
162
+ }
163
+ pathBuf.length = 1;
164
+ pathBuf[0] = i;
165
+ walkChildren(node);
166
+ }
161
167
  if (codeBlocks.length === 0)
162
168
  return tree;
163
- const highlightedResults = await Promise.all(codeBlocks.map(({ node }) => {
164
- return highlightCode(node[2][2], node[1], options);
165
- }));
166
- const newNodes = JSON.parse(JSON.stringify(tree.nodes));
169
+ const hl = await getHighlighter(options);
170
+ const { themes = { light: 'material-theme-lighter', dark: 'material-theme-palenight' } } = options;
171
+ const lightTheme = themes.light || themes.dark || 'material-theme-lighter';
172
+ const darkTheme = themes.dark || themes.light || 'material-theme-palenight';
173
+ const themeOptions = {
174
+ light: lightTheme,
175
+ dark: lightTheme !== darkTheme ? darkTheme : undefined,
176
+ };
177
+ const hasTransformers = options.transformers && options.transformers.length > 0;
178
+ const darkClassSuffix = options.themes?.dark?.name ? ` dark:${options.themes.dark.name}` : '';
179
+ // Build new nodes array, spine-copying only paths to modified <pre> nodes
180
+ const newNodes = [...tree.nodes];
167
181
  for (let i = 0; i < codeBlocks.length; i++) {
168
182
  const { node, path } = codeBlocks[i];
169
- const preAttrs = node[1];
170
- const result = highlightedResults[i];
171
- const preNode = result.nodes[0];
172
- const preNodeClasses = typeof preNode === 'string'
173
- ? ['shiki', options.themes?.light?.name]
174
- : (Array.isArray(preNode[1].class)
175
- ? preNode[1].class
176
- : String(preNode[1].class).split(' '));
177
- const codeChildren = preNode[2].slice(2);
178
- const children = typeof preNode === 'string'
179
- ? preNode
180
- : codeChildren;
181
- if (Array.isArray(children)) {
182
- let line = 1;
183
- for (const child of children) {
184
- if (Array.isArray(child)) {
185
- if (Array.isArray(preAttrs.highlights) && preAttrs.highlights.includes(line)) {
186
- child[1].class = `${child[1].class ?? ''} highlight`.trim();
187
- // TODO: (enforcing default style) once we unify all ecosystem styles we can remove this
188
- child[1].style = 'display: inline-block';
183
+ const code = node[2][2];
184
+ const attrs = node[1];
185
+ const preAttrs = attrs;
186
+ const language = attrs?.language;
187
+ let classStr;
188
+ let codeChildren;
189
+ try {
190
+ if (hasTransformers) {
191
+ // Transformers operate on hast, so we must go through codeToHast
192
+ const result = codeToHast(hl, code, {
193
+ lang: language,
194
+ transformers: options.transformers,
195
+ themes: themeOptions,
196
+ meta: { __raw: attrs.meta },
197
+ });
198
+ const preNode = result.children.map(hastToComarkNode)[0];
199
+ const cls = preNode[1].class;
200
+ classStr = Array.isArray(cls) ? cls.join(' ') : String(cls);
201
+ codeChildren = preNode[2].slice(2);
202
+ }
203
+ else {
204
+ // Fast path: build ComarkNodes directly from tokens, skipping hast
205
+ const result = codeToTokens(hl, code, {
206
+ lang: language,
207
+ themes: themeOptions,
208
+ });
209
+ classStr = `shiki ${result.themeName || ''}`;
210
+ // Replicate shiki's mergeWhitespaceTokens: merge pure-whitespace tokens
211
+ // into the following token (unless underline/strikethrough styled)
212
+ const tokenLines = result.tokens;
213
+ codeChildren = [];
214
+ for (let li = 0; li < tokenLines.length; li++) {
215
+ const line = tokenLines[li];
216
+ const spanCount = line.length;
217
+ // Merge whitespace tokens inline while building spans
218
+ let carry = '';
219
+ const spans = [];
220
+ for (let t = 0; t < spanCount; t++) {
221
+ const tk = line[t];
222
+ const canMerge = !((tk.fontStyle && (tk.fontStyle & 8 /* Strikethrough */ || tk.fontStyle & 4)) /* Underline */);
223
+ if (canMerge && /^\s+$/.test(tk.content) && t + 1 < spanCount) {
224
+ carry += tk.content;
225
+ }
226
+ else if (carry) {
227
+ const style = stringifyTokenStyle(tk.htmlStyle || getTokenStyleObject(tk));
228
+ if (canMerge) {
229
+ spans.push(style ? ['span', { style }, carry + tk.content] : ['span', {}, carry + tk.content]);
230
+ }
231
+ else {
232
+ spans.push(['span', {}, carry]);
233
+ spans.push(style ? ['span', { style }, tk.content] : ['span', {}, tk.content]);
234
+ }
235
+ carry = '';
236
+ }
237
+ else {
238
+ const style = stringifyTokenStyle(tk.htmlStyle || getTokenStyleObject(tk));
239
+ spans.push(style ? ['span', { style }, tk.content] : ['span', {}, tk.content]);
240
+ }
189
241
  }
190
- else {
191
- // TODO: (enforcing default style) once we unify all ecosystem styles we can remove this
192
- child[1].style = 'display: inline';
242
+ // If trailing whitespace wasn't merged, emit it
243
+ if (carry) {
244
+ spans.push(['span', {}, carry]);
193
245
  }
194
- line += 1;
246
+ // eslint-disable-next-line unicorn/no-new-array -- pre-allocated for perf
247
+ const lineNode = new Array(spans.length + 2);
248
+ lineNode[0] = 'span';
249
+ lineNode[1] = { class: 'line' };
250
+ for (let s = 0; s < spans.length; s++)
251
+ lineNode[s + 2] = spans[s];
252
+ codeChildren.push(lineNode);
253
+ if (li < tokenLines.length - 1)
254
+ codeChildren.push('\n');
255
+ }
256
+ }
257
+ }
258
+ catch {
259
+ classStr = 'shiki';
260
+ codeChildren = [code];
261
+ }
262
+ if (darkClassSuffix)
263
+ classStr += darkClassSuffix;
264
+ // Apply line highlights
265
+ const highlightSet = Array.isArray(preAttrs.highlights) ? new Set(preAttrs.highlights) : null;
266
+ let line = 1;
267
+ for (const child of codeChildren) {
268
+ if (Array.isArray(child)) {
269
+ if (highlightSet !== null && highlightSet.has(line)) {
270
+ child[1].class = `${child[1].class ?? ''} highlight`.trim();
271
+ // TODO: (enforcing default style) once we unify all ecosystem styles we can remove this
272
+ child[1].style = 'display: inline-block';
273
+ }
274
+ else {
275
+ // TODO: (enforcing default style) once we unify all ecosystem styles we can remove this
276
+ child[1].style = 'display: inline';
195
277
  }
278
+ line += 1;
196
279
  }
197
280
  }
281
+ // Merge highlighter class with any user-supplied class (e.g. from
282
+ // `::pre{.user-class}`) so the wrapper's class isn't lost.
283
+ const userClass = typeof preAttrs.class === 'string' ? preAttrs.class.trim() : '';
198
284
  const newPreAttrs = {
199
285
  ...preAttrs,
200
- class: [...preNodeClasses, options.themes?.dark?.name ? `dark:${options.themes?.dark?.name}` : ''].filter(Boolean).join(' '),
286
+ class: userClass ? `${classStr} . ${userClass}` : classStr,
201
287
  tabindex: '0',
202
288
  };
203
289
  if (options.preStyles) {
@@ -222,14 +308,26 @@ export async function highlightCodeBlocks(tree, options = {}) {
222
308
  }
223
309
  const codeEl = node[2];
224
310
  const codeAttrs = codeEl[1] || {};
225
- const newPreNode = ['pre', newPreAttrs, ['code', codeAttrs, ...children]];
311
+ // eslint-disable-next-line unicorn/no-new-array -- pre-allocated for perf
312
+ const codeNode = new Array(codeChildren.length + 2);
313
+ codeNode[0] = 'code';
314
+ codeNode[1] = codeAttrs;
315
+ for (let j = 0; j < codeChildren.length; j++)
316
+ codeNode[j + 2] = codeChildren[j];
317
+ const newPreNode = ['pre', newPreAttrs, codeNode];
226
318
  if (path.length === 1) {
227
319
  newNodes[path[0]] = newPreNode;
228
320
  }
229
321
  else {
230
- let current = newNodes[path[0]];
322
+ // Copy only the spine from root to this node to preserve immutability
323
+ const rootIdx = path[0];
324
+ let current = [...newNodes[rootIdx]];
325
+ newNodes[rootIdx] = current;
231
326
  for (let j = 1; j < path.length - 1; j++) {
232
- current = current[path[j] + 2];
327
+ const childSlot = path[j] + 2;
328
+ const next = [...current[childSlot]];
329
+ current[childSlot] = next;
330
+ current = next;
233
331
  }
234
332
  const childSlot = path[path.length - 1] + 2;
235
333
  current[childSlot] = newPreNode;
@@ -49,5 +49,5 @@ interface JsonRenderConfig {
49
49
  * </template>
50
50
  * ```
51
51
  */
52
- declare const _default: import("../types").ComarkPluginFactory<JsonRenderConfig>;
52
+ declare const _default: import("../types").ComarkPluginFactory<JsonRenderConfig, {}, {}>;
53
53
  export default _default;
@@ -16,13 +16,8 @@ function jsonRenderElementToAst(element, elements) {
16
16
  if (element.type === 'Text') {
17
17
  return String(element.props.content);
18
18
  }
19
- const children = element.children?.map(childName => elements[childName])
20
- .filter(Boolean) || [];
21
- return [
22
- element.type,
23
- element.props,
24
- ...children.map(child => jsonRenderElementToAst(child, elements)),
25
- ];
19
+ const children = element.children?.map((childName) => elements[childName]).filter(Boolean) || [];
20
+ return [element.type, element.props, ...children.map((child) => jsonRenderElementToAst(child, elements))];
26
21
  }
27
22
  /**
28
23
  * Plugin for rendering [JSON Render](https://json-render.dev/) specs as UI components.
@@ -76,8 +71,9 @@ function jsonRenderElementToAst(element, elements) {
76
71
  export default defineComarkPlugin((_config = {}) => ({
77
72
  name: 'json-render',
78
73
  post: async (state) => {
79
- visit(state.tree, node => node[0] === 'pre' && (node[1].language === 'json-render'
80
- || node[1].language === 'yaml-render'), (preNode) => {
74
+ visit(state.tree, (node) => node[0] === 'pre' &&
75
+ (node[1].language === 'json-render' ||
76
+ node[1].language === 'yaml-render'), (preNode) => {
81
77
  const language = preNode[1].language;
82
78
  try {
83
79
  let spec = undefined;
@@ -55,5 +55,5 @@ export declare function validateMath(code: string): boolean;
55
55
  * })
56
56
  * ```
57
57
  */
58
- declare const _default: import("comark").ComarkPluginFactory<MathConfig>;
58
+ declare const _default: import("comark").ComarkPluginFactory<MathConfig, {}, {}>;
59
59
  export default _default;
@@ -70,7 +70,7 @@ function mathInlineDisplayRule(state, silent, _config) {
70
70
  let pos = start + 2;
71
71
  while (pos + 1 < max) {
72
72
  // Stop at newline
73
- if (state.src.charCodeAt(pos) === 0x0A /* \n */) {
73
+ if (state.src.charCodeAt(pos) === 0x0a /* \n */) {
74
74
  return false;
75
75
  }
76
76
  // Check for $$
@@ -110,7 +110,7 @@ function mathInlineRule(state, silent, _config) {
110
110
  while (pos < max) {
111
111
  const char = state.src.charCodeAt(pos);
112
112
  // Stop at newline - $ must close on same line
113
- if (char === 0x0A /* \n */) {
113
+ if (char === 0x0a /* \n */) {
114
114
  return false;
115
115
  }
116
116
  if (char === 0x24 /* $ */) {
@@ -119,7 +119,7 @@ function mathInlineRule(state, silent, _config) {
119
119
  // it's not preceded by another $ (which would make it $$),
120
120
  // and it's not followed by another $ (which would make it $$)
121
121
  const hasContent = pos > start + 1;
122
- const notEscaped = pos === start + 1 || state.src.charCodeAt(pos - 1) !== 0x5C; /* \ */
122
+ const notEscaped = pos === start + 1 || state.src.charCodeAt(pos - 1) !== 0x5c; /* \ */
123
123
  const notPrecededByDollar = pos === start + 1 || state.src.charCodeAt(pos - 1) !== 0x24;
124
124
  const notFollowedByDollar = pos + 1 >= max || state.src.charCodeAt(pos + 1) !== 0x24;
125
125
  if (hasContent && notEscaped && notPrecededByDollar && notFollowedByDollar) {
@@ -256,7 +256,5 @@ function markdownItMath(md, config = {}) {
256
256
  */
257
257
  export default defineComarkPlugin((config = {}) => ({
258
258
  name: 'math',
259
- markdownItPlugins: [
260
- ((md) => markdownItMath(md, config)),
261
- ],
259
+ markdownItPlugins: [((md) => markdownItMath(md, config))],
262
260
  }));
@@ -34,5 +34,5 @@ export declare function searchProps(content: string, index?: number): {
34
34
  * })
35
35
  * ```
36
36
  */
37
- declare const _default: import("comark").ComarkPluginFactory<MermaidConfig>;
37
+ declare const _default: import("comark").ComarkPluginFactory<MermaidConfig, {}, {}>;
38
38
  export default _default;