comark 0.3.0 → 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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/dist/devtools/index.d.ts +1 -0
  3. package/dist/devtools/index.js +1 -0
  4. package/dist/devtools/register.d.ts +1 -0
  5. package/dist/devtools/register.js +1 -0
  6. package/dist/devtools/registry.d.ts +1 -0
  7. package/dist/devtools/registry.js +1 -0
  8. package/dist/devtools/vite.d.ts +1 -0
  9. package/dist/devtools/vite.js +1 -0
  10. package/dist/internal/frontmatter.d.ts +1 -0
  11. package/dist/internal/frontmatter.js +4 -2
  12. package/dist/internal/parse/auto-close/index.js +25 -13
  13. package/dist/internal/parse/auto-close/table.js +12 -9
  14. package/dist/internal/parse/auto-unwrap.js +2 -10
  15. package/dist/internal/parse/html/html_block_rule.js +1 -1
  16. package/dist/internal/parse/html/html_inline_rule.js +3 -7
  17. package/dist/internal/parse/html/html_re.js +1 -1
  18. package/dist/internal/parse/html/index.js +14 -2
  19. package/dist/internal/parse/syntax/block-params.d.ts +9 -0
  20. package/dist/internal/parse/syntax/block-params.js +48 -0
  21. package/dist/internal/parse/syntax/brackets.d.ts +8 -0
  22. package/dist/internal/parse/syntax/brackets.js +20 -0
  23. package/dist/internal/parse/syntax/props.d.ts +5 -0
  24. package/dist/internal/parse/syntax/props.js +119 -0
  25. package/dist/internal/parse/token-processor.js +25 -24
  26. package/dist/internal/props-validation.js +4 -9
  27. package/dist/internal/stringify/attributes.js +4 -1
  28. package/dist/internal/stringify/handlers/a.js +1 -3
  29. package/dist/internal/stringify/handlers/blockquote.js +2 -4
  30. package/dist/internal/stringify/handlers/code.js +1 -3
  31. package/dist/internal/stringify/handlers/emphesis.js +1 -3
  32. package/dist/internal/stringify/handlers/html.js +26 -16
  33. package/dist/internal/stringify/handlers/img.js +1 -3
  34. package/dist/internal/stringify/handlers/li.js +14 -8
  35. package/dist/internal/stringify/handlers/mdc.js +2 -3
  36. package/dist/internal/stringify/handlers/ol.js +1 -1
  37. package/dist/internal/stringify/handlers/p.d.ts +1 -1
  38. package/dist/internal/stringify/handlers/p.js +4 -1
  39. package/dist/internal/stringify/handlers/pre.js +10 -13
  40. package/dist/internal/stringify/handlers/strong.js +1 -3
  41. package/dist/internal/stringify/handlers/table.js +7 -5
  42. package/dist/internal/stringify/handlers/template.js +1 -1
  43. package/dist/internal/stringify/handlers/ul.js +1 -1
  44. package/dist/internal/stringify/indent.d.ts +1 -5
  45. package/dist/internal/stringify/indent.js +1 -9
  46. package/dist/internal/stringify/state.js +1 -1
  47. package/dist/internal/yaml.js +1 -1
  48. package/dist/parse.js +14 -8
  49. package/dist/plugins/alert.js +1 -1
  50. package/dist/plugins/binding.js +1 -3
  51. package/dist/plugins/breaks.js +1 -1
  52. package/dist/plugins/emoji.js +8 -8
  53. package/dist/plugins/footnotes.js +19 -13
  54. package/dist/plugins/headings.js +2 -4
  55. package/dist/plugins/highlight.d.ts +1 -11
  56. package/dist/plugins/highlight.js +198 -103
  57. package/dist/plugins/json-render.js +5 -9
  58. package/dist/plugins/math.js +4 -6
  59. package/dist/plugins/mermaid.js +6 -20
  60. package/dist/plugins/punctuation.js +5 -6
  61. package/dist/plugins/security.js +2 -2
  62. package/dist/plugins/syntax.d.ts +49 -0
  63. package/dist/plugins/syntax.js +522 -0
  64. package/dist/plugins/task-list.d.ts +1 -1
  65. package/dist/plugins/task-list.js +11 -8
  66. package/dist/plugins/toc.js +1 -1
  67. package/dist/types.d.ts +1 -0
  68. package/dist/utils/comark.tmLanguage.d.ts +335 -0
  69. package/dist/utils/comark.tmLanguage.js +597 -0
  70. package/dist/utils/helpers.js +1 -3
  71. package/dist/utils/index.d.ts +5 -0
  72. package/dist/utils/index.js +25 -3
  73. package/package.json +49 -51
  74. package/skills/skills/comark/AGENTS.md +0 -261
  75. package/skills/skills/comark/SKILL.md +0 -489
  76. package/skills/skills/comark/references/markdown-syntax.md +0 -599
  77. package/skills/skills/comark/references/parsing-ast.md +0 -378
  78. package/skills/skills/comark/references/rendering-react.md +0 -445
  79. package/skills/skills/comark/references/rendering-svelte.md +0 -453
  80. package/skills/skills/comark/references/rendering-vue.md +0 -462
  81. /package/skills/{skills/migrate-mdc-to-comark → migrate-mdc-to-comark}/SKILL.md +0 -0
@@ -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>;
@@ -0,0 +1,522 @@
1
+ import { Token } from 'markdown-exit';
2
+ import { defineComarkPlugin } from "../utils/helpers.js";
3
+ import { parseBracketContent } from "../internal/parse/syntax/brackets.js";
4
+ import { searchProps } from "../internal/parse/syntax/props.js";
5
+ import { parseBlockParams } from "../internal/parse/syntax/block-params.js";
6
+ import { parseYaml } from "../internal/yaml.js";
7
+ // #region Block component plugin (`::name` and `::name ... ::`)
8
+ const blockYamlLines = {
9
+ '---': '---',
10
+ '```yaml [props]': '```',
11
+ '~~~yaml [props]': '~~~',
12
+ '```yml [props]': '```',
13
+ '~~~yml [props]': '~~~',
14
+ };
15
+ const markdownItComarkBlock = (md) => {
16
+ const min_markers = 2;
17
+ const marker_str = ':';
18
+ const marker_char = marker_str.charCodeAt(0);
19
+ md.block.ruler.before('fence', 'comark_block_shorthand', function comark_block_shorthand(state, startLine, _endLine, silent) {
20
+ const line = state.src.slice(state.bMarks[startLine] + state.tShift[startLine], state.eMarks[startLine]);
21
+ if (!/^:\w/.test(line))
22
+ return false;
23
+ const { name, content, props, remaining } = parseBlockParams(line.slice(1));
24
+ // If there's unparsed remaining content, treat it as inline component in a paragraph
25
+ if (remaining)
26
+ return false;
27
+ state.lineMax = startLine + 1;
28
+ if (!silent) {
29
+ if (content !== undefined) {
30
+ const tokenOpen = state.push('mdc_block_shorthand', name, 1);
31
+ props?.forEach(([key, value]) => {
32
+ if (key === 'class')
33
+ tokenOpen.attrJoin(key, value);
34
+ else
35
+ tokenOpen.attrSet(key, value);
36
+ });
37
+ tokenOpen.map = [startLine, startLine + 1];
38
+ const inline = state.push('inline', '', 0);
39
+ inline.content = content;
40
+ inline.children = [];
41
+ const tokenClose = state.push('mdc_block_shorthand', name, -1);
42
+ tokenClose.map = [startLine, startLine + 1];
43
+ }
44
+ else {
45
+ const token = state.push('mdc_block_shorthand', name, 0);
46
+ token.map = [startLine, startLine + 1];
47
+ props?.forEach(([key, value]) => {
48
+ if (key === 'class')
49
+ token.attrJoin(key, value);
50
+ else
51
+ token.attrSet(key, value);
52
+ });
53
+ }
54
+ }
55
+ state.line = startLine + 1;
56
+ return true;
57
+ });
58
+ md.block.ruler.before('fence', 'comark_block', function comark_block(state, startLine, endLine, silent) {
59
+ let pos;
60
+ let nextLine;
61
+ let auto_closed = false;
62
+ let start = state.bMarks[startLine] + state.tShift[startLine];
63
+ let max = state.eMarks[startLine];
64
+ const indent = state.sCount[startLine];
65
+ // Track code fences (``` or ~~~) so we don't match closing :: inside them
66
+ let inCodeFence = false;
67
+ let codeFenceCharCode = 0;
68
+ let codeFenceCount = 0;
69
+ // Track nesting depth for blocks with the same marker count
70
+ let nestingDepth = 0;
71
+ if (state.src[start] !== ':')
72
+ return false;
73
+ for (pos = start + 1; pos <= max; pos++) {
74
+ if (marker_str !== state.src[pos])
75
+ break;
76
+ }
77
+ const marker_count = Math.floor(pos - start);
78
+ if (marker_count < min_markers)
79
+ return false;
80
+ const markup = state.src.slice(start, pos);
81
+ const params = parseBlockParams(state.src.slice(pos, max));
82
+ if (!params.name)
83
+ return false;
84
+ if (silent)
85
+ return true;
86
+ nextLine = startLine;
87
+ for (;;) {
88
+ nextLine++;
89
+ if (nextLine >= endLine)
90
+ break;
91
+ start = state.bMarks[nextLine] + state.tShift[nextLine];
92
+ max = state.eMarks[nextLine];
93
+ if (start < max && state.sCount[nextLine] < state.blkIndent)
94
+ break;
95
+ const lineCharCode = state.src.charCodeAt(start);
96
+ // Detect closing code fence (``` or ~~~)
97
+ if (inCodeFence) {
98
+ if (lineCharCode === codeFenceCharCode) {
99
+ let fencePos = start + 1;
100
+ while (fencePos < max && state.src.charCodeAt(fencePos) === codeFenceCharCode)
101
+ fencePos++;
102
+ if (fencePos - start >= codeFenceCount) {
103
+ const afterFence = state.skipSpaces(fencePos);
104
+ if (afterFence >= max)
105
+ inCodeFence = false;
106
+ }
107
+ }
108
+ continue;
109
+ }
110
+ // Detect opening code fence (``` or ~~~)
111
+ if (lineCharCode === 0x60 /* ` */ || lineCharCode === 0x7e /* ~ */) {
112
+ let fencePos = start + 1;
113
+ while (fencePos < max && state.src.charCodeAt(fencePos) === lineCharCode)
114
+ fencePos++;
115
+ if (fencePos - start >= 3) {
116
+ inCodeFence = true;
117
+ codeFenceCharCode = lineCharCode;
118
+ codeFenceCount = fencePos - start;
119
+ continue;
120
+ }
121
+ }
122
+ if (marker_char !== lineCharCode)
123
+ continue;
124
+ for (pos = start + 1; pos <= max; pos++) {
125
+ if (marker_str !== state.src[pos])
126
+ break;
127
+ }
128
+ // Closing fence must match the opening fence length
129
+ if (pos - start !== marker_count)
130
+ continue;
131
+ pos = state.skipSpaces(pos);
132
+ if (pos < max) {
133
+ // A new nested block opens with same marker count
134
+ nestingDepth++;
135
+ continue;
136
+ }
137
+ if (nestingDepth > 0) {
138
+ nestingDepth--;
139
+ continue;
140
+ }
141
+ auto_closed = true;
142
+ break;
143
+ }
144
+ const old_parent = state.parentType;
145
+ const old_line_max = state.lineMax;
146
+ state.parentType = 'comark_block';
147
+ // Prevent lazy continuations from going past our end marker
148
+ state.lineMax = nextLine;
149
+ const tokenOpen = state.push('mdc_block_open', params.name, 1);
150
+ tokenOpen.markup = markup;
151
+ tokenOpen.block = true;
152
+ tokenOpen.info = params.name;
153
+ tokenOpen.map = [startLine, nextLine];
154
+ params.props?.forEach(([key, value]) => {
155
+ if (key === 'class')
156
+ tokenOpen.attrJoin(key, value);
157
+ else
158
+ tokenOpen.attrSet(key, value);
159
+ });
160
+ // Render bracket content as the first paragraph: `::block[Content]\n::`
161
+ if (params.content !== undefined) {
162
+ const pOpen = state.push('paragraph_open', 'p', 1);
163
+ pOpen.map = [startLine, startLine + 1];
164
+ const inline = state.push('inline', '', 0);
165
+ inline.content = params.content;
166
+ inline.children = [];
167
+ state.push('paragraph_close', 'p', -1);
168
+ }
169
+ const blkIndent = state.blkIndent;
170
+ state.blkIndent = indent;
171
+ state.env.comarkBlockTokens ||= [];
172
+ state.env.comarkBlockTokens.unshift(tokenOpen);
173
+ state.md.block.tokenize(state, startLine + 1, nextLine);
174
+ state.blkIndent = blkIndent;
175
+ state.env.comarkBlockTokens.shift();
176
+ const tokenClose = state.push('mdc_block_close', params.name, -1);
177
+ tokenClose.map = [startLine, nextLine];
178
+ tokenClose.markup = state.src.slice(start, pos);
179
+ tokenClose.block = true;
180
+ // Hide the wrapper paragraph for single-paragraph blocks
181
+ state.tokens
182
+ .slice(state.tokens.indexOf(tokenOpen) + 1, state.tokens.indexOf(tokenClose))
183
+ .filter((i) => i.level === tokenOpen.level + 1)
184
+ .forEach((i, _, arr) => {
185
+ if (arr.length <= 2 && i.tag === 'p')
186
+ i.hidden = true;
187
+ });
188
+ state.parentType = old_parent;
189
+ state.lineMax = old_line_max;
190
+ state.line = nextLine + (auto_closed ? 1 : 0);
191
+ return true;
192
+ }, {
193
+ alt: ['paragraph', 'reference', 'blockquote', 'list'],
194
+ });
195
+ md.block.ruler.after('code', 'comark_block_yaml', function comark_block_yaml(state, startLine, endLine, silent) {
196
+ if (!state.env.comarkBlockTokens?.length)
197
+ return false;
198
+ const start = state.bMarks[startLine] + state.tShift[startLine];
199
+ const end = state.eMarks[startLine];
200
+ const line = state.src.slice(start, end);
201
+ const blockAttributesClosingFence = blockYamlLines[line] || '';
202
+ if (!blockAttributesClosingFence)
203
+ return false;
204
+ let lineEnd = startLine + 1;
205
+ let found = false;
206
+ while (lineEnd < endLine) {
207
+ const inner = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
208
+ if (inner === blockAttributesClosingFence) {
209
+ found = true;
210
+ break;
211
+ }
212
+ lineEnd += 1;
213
+ }
214
+ if (!found)
215
+ return false;
216
+ if (!silent) {
217
+ const yaml = state.src.slice(state.bMarks[startLine + 1], state.eMarks[lineEnd - 1]);
218
+ const data = parseYaml(yaml);
219
+ const token = state.env.comarkBlockTokens[0];
220
+ Object.entries(data || {}).forEach(([key, value]) => {
221
+ if (key === 'class')
222
+ token.attrJoin(key, value);
223
+ else
224
+ token.attrSet(key, typeof value === 'string' ? value : JSON.stringify(value));
225
+ });
226
+ }
227
+ state.line = lineEnd + 1;
228
+ state.lineMax = lineEnd + 1;
229
+ return true;
230
+ });
231
+ md.block.ruler.after('code', 'comark_block_slots', function comark_block_slots(state, startLine, endLine, silent) {
232
+ if (!state.env.comarkBlockTokens?.length)
233
+ return false;
234
+ const start = state.bMarks[startLine] + state.tShift[startLine];
235
+ if (!(state.src[start] === '#' && state.src[start + 1] !== ' ' && state.src[start + 1] !== '#'))
236
+ return false;
237
+ const line = state.src.slice(start, state.eMarks[startLine]);
238
+ const { name, props } = parseBlockParams(line.slice(1));
239
+ let lineEnd = startLine + 1;
240
+ while (lineEnd < endLine) {
241
+ const inner = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
242
+ if (/^#\w+/.test(inner) || inner.startsWith('::'))
243
+ break;
244
+ lineEnd += 1;
245
+ }
246
+ if (silent) {
247
+ state.line = lineEnd;
248
+ state.lineMax = lineEnd;
249
+ return true;
250
+ }
251
+ state.lineMax = startLine + 1;
252
+ const slot = state.push('mdc_block_slot', 'template', 1);
253
+ slot.attrSet(`#${name}`, '');
254
+ props?.forEach(([key, value]) => {
255
+ if (key === 'class')
256
+ slot.attrJoin(key, value);
257
+ else
258
+ slot.attrSet(key, value);
259
+ });
260
+ state.line = startLine + 1;
261
+ state.lineMax = lineEnd;
262
+ state.md.block.tokenize(state, startLine + 1, lineEnd);
263
+ state.push('mdc_block_slot', 'template', -1);
264
+ state.line = lineEnd;
265
+ state.lineMax = lineEnd;
266
+ return true;
267
+ });
268
+ };
269
+ // #endregion
270
+ // #region Inline span plugin (`[text]`)
271
+ const markdownItInlineSpan = (md) => {
272
+ md.inline.ruler.before('link', 'comark_inline_span', (state, silent) => {
273
+ const start = state.pos;
274
+ if (state.src[start] !== '[')
275
+ return false;
276
+ let index = start + 1;
277
+ let depth = 0;
278
+ while (index < state.src.length) {
279
+ if (state.src[index] === '\\') {
280
+ index += 2;
281
+ continue;
282
+ }
283
+ if (state.src[index] === '[') {
284
+ depth++;
285
+ }
286
+ else if (state.src[index] === ']') {
287
+ if (depth === 0)
288
+ break;
289
+ depth--;
290
+ }
291
+ index += 1;
292
+ }
293
+ if (index === start)
294
+ return false;
295
+ // Don't match `[text](url)` or `[text][ref]` — let the link parser handle those
296
+ const nextChar = state.src[index + 1];
297
+ if (nextChar === '(' || nextChar === '[')
298
+ return false;
299
+ if (silent)
300
+ return true;
301
+ state.push('mdc_inline_span', 'span', 1);
302
+ const oldPos = state.pos;
303
+ const oldPosMax = state.posMax;
304
+ state.pos = start + 1;
305
+ state.posMax = index;
306
+ state.md.inline.tokenize(state);
307
+ state.pos = oldPos;
308
+ state.posMax = oldPosMax;
309
+ state.push('mdc_inline_span', 'span', -1);
310
+ state.pos = index + 1;
311
+ return true;
312
+ });
313
+ };
314
+ // #endregion
315
+ // #region Inline component plugin (`:name[content]{props}`)
316
+ const ALLOWED_PREV_CHARS = new Set([' ', '\t', '\n', '*', '_', '[']);
317
+ const markdownItInlineComponent = (md) => {
318
+ md.inline.ruler.after('entity', 'comark_inline_component', (state, silent) => {
319
+ const start = state.pos;
320
+ if (state.src[start] !== ':')
321
+ return false;
322
+ const prevChar = state.src[start - 1];
323
+ if (start > 0 && !ALLOWED_PREV_CHARS.has(prevChar))
324
+ return false;
325
+ let index = start + 1;
326
+ let nameEnd = -1;
327
+ let contentStart = -1;
328
+ let contentEnd = -1;
329
+ while (index < state.src.length) {
330
+ const char = state.src[index];
331
+ if (char === '[') {
332
+ nameEnd = index;
333
+ const result = parseBracketContent(state.src, index);
334
+ if (result) {
335
+ contentStart = index + 1;
336
+ contentEnd = result.endIndex - 1;
337
+ index = result.endIndex;
338
+ }
339
+ break;
340
+ }
341
+ if (!/[\w$-]/.test(char))
342
+ break;
343
+ index += 1;
344
+ }
345
+ if (nameEnd === -1)
346
+ nameEnd = index;
347
+ // Empty name
348
+ if (nameEnd <= start + 1)
349
+ return false;
350
+ state.pos = index;
351
+ if (silent)
352
+ return true;
353
+ const name = state.src.slice(start + 1, nameEnd);
354
+ if (contentStart !== -1) {
355
+ state.push('mdc_inline_component', name, 1);
356
+ const oldPos = state.pos;
357
+ const oldPosMax = state.posMax;
358
+ state.pos = contentStart;
359
+ state.posMax = contentEnd;
360
+ state.md.inline.tokenize(state);
361
+ state.pos = oldPos;
362
+ state.posMax = oldPosMax;
363
+ state.push('mdc_inline_component', name, -1);
364
+ }
365
+ else {
366
+ state.push('mdc_inline_component', name, 0);
367
+ }
368
+ return true;
369
+ });
370
+ };
371
+ // #endregion
372
+ // #region Inline props plugin (`{class="foo"}` after a token)
373
+ const markdownItInlineProps = (md) => {
374
+ md.inline.ruler.after('entity', 'comark_inline_props', (state, silent) => {
375
+ const start = state.pos;
376
+ if (state.src[start] !== '{')
377
+ return false;
378
+ // Skip Vue mustache `{{ }}` and template `${ }` syntax
379
+ if (state.src[start + 1] === '{' || state.src[start - 1] === '{' || state.src[start - 1] === '$')
380
+ return false;
381
+ const search = searchProps(state.src, start);
382
+ if (!search)
383
+ return false;
384
+ const { props, index: end } = search;
385
+ if (end === start)
386
+ return false;
387
+ state.pos = end;
388
+ if (silent)
389
+ return true;
390
+ // Hidden token holding the props; later applied to the previous token
391
+ const token = state.push('mdc_inline_props', 'span', 0);
392
+ token.attrs = props;
393
+ token.hidden = true;
394
+ return true;
395
+ });
396
+ md.renderer.rules.mdc_inline_props = () => '';
397
+ const _parse = md.parse;
398
+ md.parse = function (src, env) {
399
+ const tokens = _parse.call(this, src, env);
400
+ // When the props token is the only inline child of a heading/paragraph/list_item,
401
+ // apply it to the parent block tag instead of producing a `span`
402
+ tokens.forEach((token, index) => {
403
+ const prev = tokens[index - 1];
404
+ const next = tokens[index + 1];
405
+ if (!prev || !['heading_open', 'paragraph_open', 'list_item_open'].includes(prev.type) || prev.hidden)
406
+ return;
407
+ // list item handling
408
+ if (token.hidden && next?.type === 'inline')
409
+ token = next;
410
+ if (token.type === 'inline' &&
411
+ token.children?.length === 2 &&
412
+ token.children[0].type === 'text' &&
413
+ token.children[1].type === 'mdc_inline_props') {
414
+ const props = token.children[1].attrs;
415
+ token.children.splice(1, 1);
416
+ props?.forEach(([key, value]) => {
417
+ if (key === 'class')
418
+ prev.attrJoin('class', value);
419
+ else
420
+ prev.attrSet(key, value);
421
+ });
422
+ }
423
+ });
424
+ // Deduplicate `ul` wrapping when `::ul` is used and contains exactly one bullet list
425
+ tokens.forEach((tokenOpen, index) => {
426
+ if (tokenOpen.type !== 'bullet_list_open')
427
+ return;
428
+ const prev = tokens[index - 1];
429
+ if (!prev || prev.type !== 'mdc_block_open' || prev.tag !== 'ul')
430
+ return;
431
+ let closeIndex = index + 1;
432
+ while (closeIndex < tokens.length) {
433
+ const close = tokens[closeIndex];
434
+ if (close.type === 'bullet_list_close' && close.level === tokenOpen.level)
435
+ break;
436
+ closeIndex += 1;
437
+ }
438
+ const tokenClose = tokens[closeIndex];
439
+ if (tokenClose?.type !== 'bullet_list_close')
440
+ return;
441
+ const next = tokens[closeIndex + 1];
442
+ if (next?.type === 'mdc_block_close' && next.tag === 'ul') {
443
+ tokenOpen.hidden = true;
444
+ tokenClose.hidden = true;
445
+ }
446
+ });
447
+ return tokens;
448
+ };
449
+ md.renderer.renderInline = wrapRenderInline(md.renderer.renderInline);
450
+ // Support markdown-exit's async inline renderer
451
+ if ('renderInlineAsync' in md.renderer) {
452
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors the sync wrapper for the async overload
453
+ ;
454
+ md.renderer.renderInlineAsync = wrapRenderInline(md.renderer.renderInlineAsync);
455
+ }
456
+ };
457
+ function wrapRenderInline(renderInline) {
458
+ return function (tokens, options, env) {
459
+ tokens = [...tokens];
460
+ tokens.forEach((token, index) => {
461
+ if (token.type !== 'mdc_inline_props')
462
+ return;
463
+ let prevIndex = index - 1;
464
+ let prev = tokens[prevIndex];
465
+ // Skip whitespace-only text tokens
466
+ while (prevIndex >= 0) {
467
+ if (prev.type === 'text' && !prev.content.trim()) {
468
+ prevIndex--;
469
+ prev = tokens[prevIndex];
470
+ }
471
+ else {
472
+ break;
473
+ }
474
+ }
475
+ // Wrap a bare text token in a span so we can attach attrs to it
476
+ if (!prev.tag && prev.type === 'text') {
477
+ prev = new Token('mdc_inline_span', 'span', 1);
478
+ tokens.splice(index - 1, 0, prev);
479
+ const close = new Token('mdc_inline_span', 'span', -1);
480
+ tokens.splice(index + 2, 0, close);
481
+ }
482
+ else if (prev.nesting === -1) {
483
+ // Resolve a closing tag back to its matching opening tag
484
+ let searchIndex = index - 1;
485
+ while (searchIndex >= 0) {
486
+ const searchToken = tokens[searchIndex];
487
+ if (searchToken.nesting === 1 && searchToken.tag === prev.tag && searchToken.level === prev.level) {
488
+ prev = searchToken;
489
+ break;
490
+ }
491
+ searchIndex--;
492
+ }
493
+ }
494
+ if (prev.nesting === -1)
495
+ throw new Error(`No matching opening tag found for ${JSON.stringify(prev)}`);
496
+ token.attrs?.forEach(([key, value]) => {
497
+ if (key === 'class')
498
+ prev.attrJoin('class', value);
499
+ else
500
+ prev.attrSet(key, value);
501
+ });
502
+ });
503
+ return renderInline.call(this, tokens, options, env);
504
+ };
505
+ }
506
+ // #endregion
507
+ function applySyntax(md, options = {}) {
508
+ const { blockComponent = true, inlineProps = true, inlineSpan = true, inlineComponent = true } = options;
509
+ if (blockComponent)
510
+ md.use(markdownItComarkBlock);
511
+ if (inlineProps)
512
+ md.use(markdownItInlineProps);
513
+ if (inlineSpan)
514
+ md.use(markdownItInlineSpan);
515
+ if (inlineComponent)
516
+ md.use(markdownItInlineComponent);
517
+ }
518
+ export default defineComarkPlugin((options = {}) => ({
519
+ name: 'syntax',
520
+ markdownItPlugins: [((md) => applySyntax(md, options))],
521
+ }));
522
+ export const markdownItComark = applySyntax;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Custom task list plugin for markdown-it that works with @comark/markdown-it
2
+ * Custom task list plugin for markdown-it that integrates with the Comark syntax plugin.
3
3
  *
4
4
  * This plugin runs before inline parsing to prevent Comark from interpreting
5
5
  * task list markers [X] and [ ] as Comark inline span syntax.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Custom task list plugin for markdown-it that works with @comark/markdown-it
2
+ * Custom task list plugin for markdown-it that integrates with the Comark syntax plugin.
3
3
  *
4
4
  * This plugin runs before inline parsing to prevent Comark from interpreting
5
5
  * task list markers [X] and [ ] as Comark inline span syntax.
@@ -35,8 +35,8 @@ function findParentList(tokens, listItemIndex) {
35
35
  const targetLevel = tokens[listItemIndex].level - 1;
36
36
  // Look backwards for the list (ul/ol) that contains this list item
37
37
  for (let i = listItemIndex - 1; i >= 0; i--) {
38
- if (tokens[i].level === targetLevel
39
- && (tokens[i].type === 'bullet_list_open' || tokens[i].type === 'ordered_list_open')) {
38
+ if (tokens[i].level === targetLevel &&
39
+ (tokens[i].type === 'bullet_list_open' || tokens[i].type === 'ordered_list_open')) {
40
40
  return i;
41
41
  }
42
42
  }
@@ -67,7 +67,7 @@ function markdownItTaskList(md, options) {
67
67
  // Replace the task marker with a placeholder that won't be processed by Comark
68
68
  // We use a special format that we can detect later
69
69
  // Keep one space after the placeholder to match expected output
70
- const checkboxPlaceholder = `{{TASK_CHECKBOX_${isChecked ? 'CHECKED' : 'UNCHECKED'}}} `;
70
+ const checkboxPlaceholder = `TASK_CHECKBOX_${isChecked ? 'CHECKED' : 'UNCHECKED'} `;
71
71
  token.content = token.content.replace(/^\[[ x]\]\s+/i, checkboxPlaceholder);
72
72
  }
73
73
  }
@@ -85,13 +85,16 @@ function markdownItTaskList(md, options) {
85
85
  const child = token.children[j];
86
86
  if (child.type === 'text' && child.content) {
87
87
  // Check for our checkbox placeholder
88
- const checkedMatch = child.content.match(/^\{\{TASK_CHECKBOX_CHECKED\}\}/);
89
- const uncheckedMatch = child.content.match(/^\{\{TASK_CHECKBOX_UNCHECKED\}\}/);
88
+ const checkedMatch = child.content.match(/^TASK_CHECKBOX_CHECKED/);
89
+ const uncheckedMatch = child.content.match(/^TASK_CHECKBOX_UNCHECKED/);
90
90
  if (checkedMatch || uncheckedMatch) {
91
91
  const isChecked = !!checkedMatch;
92
92
  // Create checkbox token
93
93
  const checkbox = new state.Token('mdc_inline_component', 'input', 0);
94
- checkbox.attrs = [['class', 'task-list-item-checkbox'], ['type', 'checkbox']];
94
+ checkbox.attrs = [
95
+ ['class', 'task-list-item-checkbox'],
96
+ ['type', 'checkbox'],
97
+ ];
95
98
  if (disableCheckboxes) {
96
99
  checkbox.attrs.push([':disabled', 'true']);
97
100
  }
@@ -99,7 +102,7 @@ function markdownItTaskList(md, options) {
99
102
  checkbox.attrs.push([':checked', 'true']);
100
103
  }
101
104
  // Remove placeholder from text
102
- child.content = child.content.replace(/^\{\{TASK_CHECKBOX_(CHECKED|UNCHECKED)\}\}/, '');
105
+ child.content = child.content.replace(/^TASK_CHECKBOX_(CHECKED|UNCHECKED)/, '');
103
106
  // Insert checkbox before the text
104
107
  token.children.splice(j, 0, checkbox);
105
108
  j++; // Skip the newly inserted checkbox
@@ -94,7 +94,7 @@ export function generateFlatToc(body, options) {
94
94
  const tag = getTag(node);
95
95
  return tag !== null && tags.includes(tag);
96
96
  });
97
- const links = headers.map(node => ({
97
+ const links = headers.map((node) => ({
98
98
  id: getProps(node).id || '',
99
99
  depth: getHeaderDepth(node),
100
100
  text: flattenNodeText(node),
package/dist/types.d.ts CHANGED
@@ -228,6 +228,7 @@ export interface NodeRenderData {
228
228
  }
229
229
  export type MarkdownExitPlugin = (md: MarkdownExit) => void;
230
230
  export type MarkdownItPlugin = (md: MarkdownIt) => void;
231
+ export type MarkdownItPluginWithOptions<T> = (md: MarkdownIt, options: T) => void;
231
232
  export type ComarkParsePreState = {
232
233
  markdown: string;
233
234
  options: ParseOptions;