comark 0.3.1 → 0.3.2

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 (80) hide show
  1. package/dist/devtools/index.d.ts +1 -0
  2. package/dist/devtools/index.js +1 -0
  3. package/dist/devtools/register.d.ts +1 -0
  4. package/dist/devtools/register.js +1 -0
  5. package/dist/devtools/registry.d.ts +1 -0
  6. package/dist/devtools/registry.js +1 -0
  7. package/dist/devtools/vite.d.ts +1 -0
  8. package/dist/devtools/vite.js +1 -0
  9. package/dist/internal/frontmatter.d.ts +1 -0
  10. package/dist/internal/frontmatter.js +4 -2
  11. package/dist/internal/parse/auto-close/index.js +25 -13
  12. package/dist/internal/parse/auto-close/table.js +12 -9
  13. package/dist/internal/parse/auto-unwrap.js +2 -10
  14. package/dist/internal/parse/html/html_block_rule.js +1 -1
  15. package/dist/internal/parse/html/html_inline_rule.js +3 -7
  16. package/dist/internal/parse/html/html_re.js +1 -1
  17. package/dist/internal/parse/html/index.js +14 -2
  18. package/dist/internal/parse/syntax/block-params.d.ts +9 -0
  19. package/dist/internal/parse/syntax/block-params.js +48 -0
  20. package/dist/internal/parse/syntax/brackets.d.ts +8 -0
  21. package/dist/internal/parse/syntax/brackets.js +20 -0
  22. package/dist/internal/parse/syntax/props.d.ts +5 -0
  23. package/dist/internal/parse/syntax/props.js +119 -0
  24. package/dist/internal/parse/token-processor.js +25 -24
  25. package/dist/internal/props-validation.js +4 -9
  26. package/dist/internal/stringify/attributes.js +4 -1
  27. package/dist/internal/stringify/handlers/a.js +1 -3
  28. package/dist/internal/stringify/handlers/blockquote.js +2 -4
  29. package/dist/internal/stringify/handlers/code.js +1 -3
  30. package/dist/internal/stringify/handlers/emphesis.js +1 -3
  31. package/dist/internal/stringify/handlers/html.js +26 -16
  32. package/dist/internal/stringify/handlers/img.js +1 -3
  33. package/dist/internal/stringify/handlers/li.js +14 -8
  34. package/dist/internal/stringify/handlers/mdc.js +2 -3
  35. package/dist/internal/stringify/handlers/ol.js +1 -1
  36. package/dist/internal/stringify/handlers/p.d.ts +1 -1
  37. package/dist/internal/stringify/handlers/p.js +4 -1
  38. package/dist/internal/stringify/handlers/pre.js +10 -13
  39. package/dist/internal/stringify/handlers/strong.js +1 -3
  40. package/dist/internal/stringify/handlers/table.js +7 -5
  41. package/dist/internal/stringify/handlers/template.js +1 -1
  42. package/dist/internal/stringify/handlers/ul.js +1 -1
  43. package/dist/internal/stringify/indent.d.ts +1 -5
  44. package/dist/internal/stringify/indent.js +1 -9
  45. package/dist/internal/stringify/state.js +1 -1
  46. package/dist/internal/yaml.js +1 -1
  47. package/dist/parse.js +14 -8
  48. package/dist/plugins/alert.js +1 -1
  49. package/dist/plugins/binding.js +1 -3
  50. package/dist/plugins/breaks.js +1 -1
  51. package/dist/plugins/emoji.js +8 -8
  52. package/dist/plugins/footnotes.js +19 -13
  53. package/dist/plugins/headings.js +2 -4
  54. package/dist/plugins/highlight.d.ts +1 -11
  55. package/dist/plugins/highlight.js +198 -103
  56. package/dist/plugins/json-render.js +5 -9
  57. package/dist/plugins/math.js +4 -6
  58. package/dist/plugins/mermaid.js +6 -20
  59. package/dist/plugins/punctuation.js +5 -6
  60. package/dist/plugins/security.js +2 -2
  61. package/dist/plugins/syntax.d.ts +49 -0
  62. package/dist/plugins/syntax.js +522 -0
  63. package/dist/plugins/task-list.d.ts +1 -1
  64. package/dist/plugins/task-list.js +11 -8
  65. package/dist/plugins/toc.js +1 -1
  66. package/dist/types.d.ts +1 -0
  67. package/dist/utils/comark.tmLanguage.d.ts +335 -0
  68. package/dist/utils/comark.tmLanguage.js +597 -0
  69. package/dist/utils/helpers.js +1 -3
  70. package/dist/utils/index.d.ts +5 -0
  71. package/dist/utils/index.js +25 -3
  72. package/package.json +39 -40
  73. package/skills/skills/comark/AGENTS.md +0 -261
  74. package/skills/skills/comark/SKILL.md +0 -489
  75. package/skills/skills/comark/references/markdown-syntax.md +0 -599
  76. package/skills/skills/comark/references/parsing-ast.md +0 -378
  77. package/skills/skills/comark/references/rendering-react.md +0 -445
  78. package/skills/skills/comark/references/rendering-svelte.md +0 -453
  79. package/skills/skills/comark/references/rendering-vue.md +0 -462
  80. /package/skills/{skills/migrate-mdc-to-comark → migrate-mdc-to-comark}/SKILL.md +0 -0
@@ -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,159 @@ 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
  }
198
281
  const newPreAttrs = {
199
282
  ...preAttrs,
200
- class: [...preNodeClasses, options.themes?.dark?.name ? `dark:${options.themes?.dark?.name}` : ''].filter(Boolean).join(' '),
283
+ class: classStr,
201
284
  tabindex: '0',
202
285
  };
203
286
  if (options.preStyles) {
@@ -222,14 +305,26 @@ export async function highlightCodeBlocks(tree, options = {}) {
222
305
  }
223
306
  const codeEl = node[2];
224
307
  const codeAttrs = codeEl[1] || {};
225
- const newPreNode = ['pre', newPreAttrs, ['code', codeAttrs, ...children]];
308
+ // eslint-disable-next-line unicorn/no-new-array -- pre-allocated for perf
309
+ const codeNode = new Array(codeChildren.length + 2);
310
+ codeNode[0] = 'code';
311
+ codeNode[1] = codeAttrs;
312
+ for (let j = 0; j < codeChildren.length; j++)
313
+ codeNode[j + 2] = codeChildren[j];
314
+ const newPreNode = ['pre', newPreAttrs, codeNode];
226
315
  if (path.length === 1) {
227
316
  newNodes[path[0]] = newPreNode;
228
317
  }
229
318
  else {
230
- let current = newNodes[path[0]];
319
+ // Copy only the spine from root to this node to preserve immutability
320
+ const rootIdx = path[0];
321
+ let current = [...newNodes[rootIdx]];
322
+ newNodes[rootIdx] = current;
231
323
  for (let j = 1; j < path.length - 1; j++) {
232
- current = current[path[j] + 2];
324
+ const childSlot = path[j] + 2;
325
+ const next = [...current[childSlot]];
326
+ current[childSlot] = next;
327
+ current = next;
233
328
  }
234
329
  const childSlot = path[path.length - 1] + 2;
235
330
  current[childSlot] = newPreNode;
@@ -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;
@@ -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
  }));
@@ -43,7 +43,7 @@ export function searchProps(content, index = 0) {
43
43
  '(': ')',
44
44
  };
45
45
  const quotePairs = {
46
- '\'': '\'',
46
+ "'": "'",
47
47
  '"': '"',
48
48
  '`': '`',
49
49
  };
@@ -67,17 +67,11 @@ export function searchProps(content, index = 0) {
67
67
  }
68
68
  else if (content[index] === '.') {
69
69
  index += 1;
70
- props.push([
71
- 'class',
72
- searchUntil(' #.}'),
73
- ]);
70
+ props.push(['class', searchUntil(' #.}')]);
74
71
  }
75
72
  else if (content[index] === '#') {
76
73
  index += 1;
77
- props.push([
78
- 'id',
79
- searchUntil(' #.}'),
80
- ]);
74
+ props.push(['id', searchUntil(' #.}')]);
81
75
  }
82
76
  else {
83
77
  const start = index;
@@ -91,16 +85,10 @@ export function searchProps(content, index = 0) {
91
85
  const key = content.slice(start, index).trim();
92
86
  if (char === '=') {
93
87
  index += 1;
94
- props.push([
95
- key,
96
- searchValue(),
97
- ]);
88
+ props.push([key, searchValue()]);
98
89
  }
99
90
  else {
100
- props.push([
101
- key,
102
- 'true',
103
- ]);
91
+ props.push([key, 'true']);
104
92
  }
105
93
  }
106
94
  }
@@ -178,7 +166,5 @@ export function searchProps(content, index = 0) {
178
166
  */
179
167
  export default defineComarkPlugin((config = {}) => ({
180
168
  name: 'mermaid',
181
- markdownItPlugins: [
182
- ((md) => markdownItMermaid(md, config)),
183
- ],
169
+ markdownItPlugins: [((md) => markdownItMermaid(md, config))],
184
170
  }));
@@ -3,8 +3,7 @@ const DEFAULT_QUOTES = '\u201C\u201D\u2018\u2019'; // ""''
3
3
  /** Tags whose text content should not be transformed */
4
4
  const SKIP_TAGS = new Set(['code', 'pre', 'math', 'script', 'style', 'kbd']);
5
5
  function isWhitespaceOrOpener(ch) {
6
- return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r'
7
- || ch === '(' || ch === '[' || ch === '{';
6
+ return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '(' || ch === '[' || ch === '{';
8
7
  }
9
8
  function isLetter(ch) {
10
9
  return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
@@ -117,11 +116,11 @@ function applyPunctuation(text, enableQuotes, dashes, ellipsis, symbols, normali
117
116
  if (enableQuotes) {
118
117
  if (ch === '"') {
119
118
  const prev = i > 0 ? text[i - 1] : ' ';
120
- result += text.slice(last, i) + ((isWhitespaceOrOpener(prev) || i === 0) ? openDouble : closeDouble);
119
+ result += text.slice(last, i) + (isWhitespaceOrOpener(prev) || i === 0 ? openDouble : closeDouble);
121
120
  last = i + 1;
122
121
  continue;
123
122
  }
124
- if (ch === '\'') {
123
+ if (ch === "'") {
125
124
  const prev = i > 0 ? text[i - 1] : ' ';
126
125
  const next = i + 1 < len ? text[i + 1] : '';
127
126
  result += text.slice(last, i);
@@ -130,7 +129,7 @@ function applyPunctuation(text, enableQuotes, dashes, ellipsis, symbols, normali
130
129
  result += closeSingle;
131
130
  }
132
131
  else {
133
- result += (isWhitespaceOrOpener(prev) || i === 0) ? openSingle : closeSingle;
132
+ result += isWhitespaceOrOpener(prev) || i === 0 ? openSingle : closeSingle;
134
133
  }
135
134
  last = i + 1;
136
135
  continue;
@@ -181,7 +180,7 @@ function applyPunctuation(text, enableQuotes, dashes, ellipsis, symbols, normali
181
180
  * ```
182
181
  */
183
182
  export default defineComarkPlugin((options = {}) => {
184
- const { quotes = true, dashes = true, ellipsis = true, symbols = true, normalize = true, } = options;
183
+ const { quotes = true, dashes = true, ellipsis = true, symbols = true, normalize = true } = options;
185
184
  // Resolve quote characters
186
185
  let enableQuotes;
187
186
  let openDouble;
@@ -3,7 +3,7 @@ import { visit } from "../utils/index.js";
3
3
  import { validateProps } from "../internal/props-validation.js";
4
4
  export default defineComarkPlugin((options = {}) => {
5
5
  const { blockedTags = [], allowedLinkPrefixes, allowedImagePrefixes, allowedProtocols, defaultOrigin, allowDataImages, } = options;
6
- const dropSet = new Set(blockedTags.map(t => t.toLowerCase()));
6
+ const dropSet = new Set(blockedTags.map((t) => t.toLowerCase()));
7
7
  const propsOptions = {
8
8
  allowedLinkPrefixes,
9
9
  allowedImagePrefixes,
@@ -14,7 +14,7 @@ export default defineComarkPlugin((options = {}) => {
14
14
  return {
15
15
  name: 'security',
16
16
  post(state) {
17
- visit(state.tree, node => typeof node !== 'string' && node[0] !== null, (node) => {
17
+ visit(state.tree, (node) => typeof node !== 'string' && node[0] !== null, (node) => {
18
18
  const element = node;
19
19
  // return false to remove the node from the tree
20
20
  if (dropSet.has(element[0].toLowerCase())) {
@@ -0,0 +1,49 @@
1
+ import type { MarkdownItPluginWithOptions } from '../types.ts';
2
+ export interface SyntaxOptions {
3
+ /**
4
+ * Enable block component syntax.
5
+ *
6
+ * @see https://comark.dev/syntax/components#block
7
+ * @default true
8
+ */
9
+ blockComponent?: boolean;
10
+ /**
11
+ * Enable inline props syntax.
12
+ *
13
+ * @see https://comark.dev/syntax/attributes
14
+ * @default true
15
+ */
16
+ inlineProps?: boolean;
17
+ /**
18
+ * Enable inline span syntax.
19
+ *
20
+ * @see https://comark.dev/syntax/attributes#span-attributes
21
+ * @default true
22
+ */
23
+ inlineSpan?: boolean;
24
+ /**
25
+ * Enable inline component syntax.
26
+ *
27
+ * @see https://comark.dev/syntax/components#inline
28
+ * @default true
29
+ */
30
+ inlineComponent?: boolean;
31
+ /**
32
+ * Enable inline binding syntax (`{{ value }}` and `{{ value || default }}`).
33
+ *
34
+ * Off by default — opt in here, or use the standalone `plugins/binding.ts` plugin.
35
+ *
36
+ * @see https://comark.dev/syntax/components#data-binding
37
+ * @default false
38
+ */
39
+ inlineBinding?: boolean;
40
+ /**
41
+ * The tag name used to render an inline binding.
42
+ *
43
+ * @default 'binding'
44
+ */
45
+ bindingTag?: string;
46
+ }
47
+ declare const _default: import("../types.ts").ComarkPluginFactory<SyntaxOptions>;
48
+ export default _default;
49
+ export declare const markdownItComark: MarkdownItPluginWithOptions<SyntaxOptions>;