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.
- package/dist/internal/frontmatter.d.ts +1 -0
- package/dist/internal/frontmatter.js +4 -2
- package/dist/internal/parse/auto-close/index.js +69 -31
- package/dist/internal/parse/auto-close/table.js +12 -9
- package/dist/internal/parse/auto-unwrap.js +6 -10
- package/dist/internal/parse/html/html_block_rule.js +10 -16
- package/dist/internal/parse/html/html_inline_rule.js +3 -7
- package/dist/internal/parse/html/html_re.js +1 -1
- package/dist/internal/parse/html/index.d.ts +1 -0
- package/dist/internal/parse/html/index.js +15 -3
- package/dist/internal/parse/syntax/block-params.d.ts +9 -0
- package/dist/internal/parse/syntax/block-params.js +48 -0
- package/dist/internal/parse/syntax/brackets.d.ts +8 -0
- package/dist/internal/parse/syntax/brackets.js +20 -0
- package/dist/internal/parse/syntax/props.d.ts +5 -0
- package/dist/internal/parse/syntax/props.js +119 -0
- package/dist/internal/parse/token-processor.js +89 -50
- package/dist/internal/props-validation.js +4 -9
- package/dist/internal/stringify/attributes.d.ts +7 -0
- package/dist/internal/stringify/attributes.js +56 -1
- package/dist/internal/stringify/handlers/a.js +1 -3
- package/dist/internal/stringify/handlers/blockquote.js +19 -4
- package/dist/internal/stringify/handlers/code.js +1 -3
- package/dist/internal/stringify/handlers/emphesis.js +1 -3
- package/dist/internal/stringify/handlers/heading.js +6 -1
- package/dist/internal/stringify/handlers/html.js +34 -18
- package/dist/internal/stringify/handlers/img.js +1 -3
- package/dist/internal/stringify/handlers/li.js +18 -9
- package/dist/internal/stringify/handlers/mdc.js +3 -4
- package/dist/internal/stringify/handlers/ol.js +12 -2
- package/dist/internal/stringify/handlers/p.d.ts +1 -1
- package/dist/internal/stringify/handlers/p.js +8 -1
- package/dist/internal/stringify/handlers/pre.js +20 -14
- package/dist/internal/stringify/handlers/strong.js +1 -3
- package/dist/internal/stringify/handlers/table.js +14 -5
- package/dist/internal/stringify/handlers/template.js +5 -2
- package/dist/internal/stringify/handlers/ul.js +12 -2
- package/dist/internal/stringify/state.js +1 -1
- package/dist/internal/yaml.js +1 -1
- package/dist/parse.d.ts +4 -4
- package/dist/parse.js +20 -10
- package/dist/plugins/alert.d.ts +1 -1
- package/dist/plugins/alert.js +1 -1
- package/dist/plugins/binding.d.ts +1 -1
- package/dist/plugins/binding.js +1 -3
- package/dist/plugins/breaks.d.ts +1 -1
- package/dist/plugins/breaks.js +1 -1
- package/dist/plugins/emoji.d.ts +1 -1
- package/dist/plugins/emoji.js +8 -8
- package/dist/plugins/footnotes.d.ts +1 -1
- package/dist/plugins/footnotes.js +19 -13
- package/dist/plugins/headings.d.ts +19 -8
- package/dist/plugins/headings.js +27 -19
- package/dist/plugins/highlight.d.ts +2 -12
- package/dist/plugins/highlight.js +201 -103
- package/dist/plugins/json-render.d.ts +1 -1
- package/dist/plugins/json-render.js +5 -9
- package/dist/plugins/math.d.ts +1 -1
- package/dist/plugins/math.js +4 -6
- package/dist/plugins/mermaid.d.ts +1 -1
- package/dist/plugins/mermaid.js +6 -20
- package/dist/plugins/punctuation.d.ts +1 -1
- package/dist/plugins/punctuation.js +5 -6
- package/dist/plugins/security.d.ts +1 -1
- package/dist/plugins/security.js +2 -2
- package/dist/plugins/summary.d.ts +4 -1
- package/dist/plugins/syntax.d.ts +49 -0
- package/dist/plugins/syntax.js +558 -0
- package/dist/plugins/task-list.d.ts +2 -2
- package/dist/plugins/task-list.js +11 -8
- package/dist/plugins/toc.d.ts +3 -1
- package/dist/plugins/toc.js +1 -1
- package/dist/types.d.ts +57 -12
- package/dist/utils/comark.tmLanguage.d.ts +335 -0
- package/dist/utils/comark.tmLanguage.js +597 -0
- package/dist/utils/helpers.d.ts +16 -4
- package/dist/utils/helpers.js +16 -6
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +25 -3
- package/package.json +40 -40
- package/skills/comark/references/rendering-svelte.md +51 -7
- package/dist/internal/stringify/indent.d.ts +0 -5
- package/dist/internal/stringify/indent.js +0 -9
- package/dist/vite.d.ts +0 -1
- package/dist/vite.js +0 -1
- package/skills/skills/comark/AGENTS.md +0 -261
- package/skills/skills/comark/SKILL.md +0 -489
- package/skills/skills/comark/references/markdown-syntax.md +0 -599
- package/skills/skills/comark/references/parsing-ast.md +0 -378
- package/skills/skills/comark/references/rendering-react.md +0 -445
- package/skills/skills/comark/references/rendering-svelte.md +0 -453
- package/skills/skills/comark/references/rendering-vue.md +0 -462
- /package/skills/{skills/migrate-mdc-to-comark → migrate-mdc-to-comark}/SKILL.md +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { htmlToComarkNodes, parseInlineHtmlTag } from "./html/index.js";
|
|
2
|
+
// `::tag` components that should fold into a single same-tagged child.
|
|
3
|
+
const WRAPPER_TAGS = new Set(['ul', 'ol', 'table', 'blockquote', 'pre']);
|
|
2
4
|
// Mapping from token types to tag names
|
|
3
5
|
const BLOCK_TAG_MAP = {
|
|
4
6
|
blockquote_open: 'blockquote',
|
|
@@ -17,7 +19,8 @@ const INLINE_TAG_MAP = {
|
|
|
17
19
|
strong_open: 'strong',
|
|
18
20
|
em_open: 'em',
|
|
19
21
|
s_open: 'del',
|
|
20
|
-
sub_open: '
|
|
22
|
+
sub_open: 'sub',
|
|
23
|
+
sup_open: 'sup',
|
|
21
24
|
};
|
|
22
25
|
// ─── main entry point ───────────────────────────────────────────────────────
|
|
23
26
|
/**
|
|
@@ -33,17 +36,23 @@ export function marmdownItTokensToComarkTree(tokens, options = { startLine: 0, p
|
|
|
33
36
|
let i = 0;
|
|
34
37
|
let endLine = options.startLine;
|
|
35
38
|
while (i < tokens.length) {
|
|
39
|
+
const token = tokens[i];
|
|
40
|
+
if (token.type === 'html_block') {
|
|
41
|
+
const result = processHtmlBlockTokens(tokens, i);
|
|
42
|
+
nodes.push(...result.nodes);
|
|
43
|
+
i = result.nextIndex;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
36
46
|
const result = processBlockToken(tokens, i, false, state);
|
|
37
47
|
if (result.node) {
|
|
38
48
|
if (options.preservePositions) {
|
|
39
49
|
for (let j = i; j < result.nextIndex; j++) {
|
|
40
50
|
if (tokens[j].map && tokens[j].map[1]) {
|
|
41
|
-
endLine = tokens[j].map[1]
|
|
42
|
-
+ options.startLine
|
|
43
|
-
+ (tokens[j].type?.endsWith('_close') ? 1 : 0);
|
|
51
|
+
endLine = tokens[j].map[1] + options.startLine + (tokens[j].type?.endsWith('_close') ? 1 : 0);
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
if (!result.node[1].$) {
|
|
55
|
+
;
|
|
47
56
|
result.node[1].$ = {};
|
|
48
57
|
}
|
|
49
58
|
;
|
|
@@ -55,6 +64,15 @@ export function marmdownItTokensToComarkTree(tokens, options = { startLine: 0, p
|
|
|
55
64
|
}
|
|
56
65
|
return nodes;
|
|
57
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Convert an html_block token into Comark nodes. The whole HTML payload is
|
|
69
|
+
* parsed once by htmlparser2; text inside is preserved verbatim (no markdown
|
|
70
|
+
* re-parsing — CommonMark default).
|
|
71
|
+
*/
|
|
72
|
+
function processHtmlBlockTokens(tokens, startIndex) {
|
|
73
|
+
const content = typeof tokens[startIndex]?.content === 'string' ? tokens[startIndex].content : '';
|
|
74
|
+
return { nodes: htmlToComarkNodes(content), nextIndex: startIndex + 1 };
|
|
75
|
+
}
|
|
58
76
|
/**
|
|
59
77
|
* Extract and process attributes from a token's attrs array
|
|
60
78
|
*/
|
|
@@ -73,7 +91,7 @@ function processAttributes(attrsArray, options = {}) {
|
|
|
73
91
|
continue;
|
|
74
92
|
}
|
|
75
93
|
// Handle boolean attributes: {bool} -> {":bool": "true"}
|
|
76
|
-
if (handleBoolean && !key.startsWith(':') && !key.startsWith('#') && !key.startsWith('.') &&
|
|
94
|
+
if (handleBoolean && !key.startsWith(':') && !key.startsWith('#') && !key.startsWith('.') && value === 'true') {
|
|
77
95
|
attrs[`:${key}`] = 'true';
|
|
78
96
|
continue;
|
|
79
97
|
}
|
|
@@ -140,7 +158,7 @@ function parseCodeblockInfo(info) {
|
|
|
140
158
|
const trimmed = part.trim();
|
|
141
159
|
if (trimmed.includes('-')) {
|
|
142
160
|
// Range like "1-3"
|
|
143
|
-
const [start, end] = trimmed.split('-').map(s => Number.parseInt(s.trim(), 10));
|
|
161
|
+
const [start, end] = trimmed.split('-').map((s) => Number.parseInt(s.trim(), 10));
|
|
144
162
|
if (!Number.isNaN(start) && !Number.isNaN(end)) {
|
|
145
163
|
for (let i = start; i <= end; i++) {
|
|
146
164
|
highlights.push(i);
|
|
@@ -219,22 +237,12 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
219
237
|
if (token.type === 'hr') {
|
|
220
238
|
return { node: ['hr', {}], nextIndex: startIndex + 1 };
|
|
221
239
|
}
|
|
222
|
-
// html_block is
|
|
240
|
+
// html_block is normally handled upstream (in marmdownItTokensToComarkTree /
|
|
223
241
|
// processBlockChildren / processBlockChildrenWithSlots) before reaching here.
|
|
224
|
-
//
|
|
242
|
+
// Safety fallback when it slips through.
|
|
225
243
|
if (token.type === 'html_block') {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
const inner = content.endsWith('-->') ? content.slice(4, -3) : content.slice(4);
|
|
229
|
-
return { node: [null, {}, inner], nextIndex: startIndex + 1 };
|
|
230
|
-
}
|
|
231
|
-
const children = processBlockChildren(tokens, startIndex + 1, 'html_block_close', false, false, false, state);
|
|
232
|
-
const [node1] = htmlToComarkNodes(content);
|
|
233
|
-
if (!node1) {
|
|
234
|
-
return { node: null, nextIndex: startIndex + 1 };
|
|
235
|
-
}
|
|
236
|
-
const node = [node1[0], node1[1], ...children.nodes];
|
|
237
|
-
return { node, nextIndex: children.nextIndex + 1 };
|
|
244
|
+
const result = processHtmlBlockTokens(tokens, startIndex);
|
|
245
|
+
return { node: result.nodes[0] ?? null, nextIndex: result.nextIndex };
|
|
238
246
|
}
|
|
239
247
|
// Handle Comark block components (e.g., ::component ... ::)
|
|
240
248
|
if (token.type === 'mdc_block_open') {
|
|
@@ -242,6 +250,20 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
242
250
|
const attrs = processAttributes(token.attrs);
|
|
243
251
|
// Process children until mdc_block_close, handling slots (#slotname)
|
|
244
252
|
const children = processBlockChildrenWithSlots(tokens, startIndex + 1, 'mdc_block_close', state);
|
|
253
|
+
// `::ul`/`::ol`/`::table`/`::blockquote`/`::pre` wrapping a single same-tag
|
|
254
|
+
// child collapses into a single element with the wrapper's attrs (outer wins).
|
|
255
|
+
if (WRAPPER_TAGS.has(componentName) &&
|
|
256
|
+
children.nodes.length === 1 &&
|
|
257
|
+
Array.isArray(children.nodes[0]) &&
|
|
258
|
+
children.nodes[0][0] === componentName) {
|
|
259
|
+
const inner = children.nodes[0];
|
|
260
|
+
const innerAttrs = inner[1];
|
|
261
|
+
const innerChildren = inner.slice(2);
|
|
262
|
+
return {
|
|
263
|
+
node: [componentName, { ...innerAttrs, ...attrs }, ...innerChildren],
|
|
264
|
+
nextIndex: children.nextIndex + 1,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
245
267
|
// Return the component even if it has no children (empty component like ::component\n::)
|
|
246
268
|
return { node: [componentName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
|
|
247
269
|
}
|
|
@@ -271,7 +293,10 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
271
293
|
return { node: [componentName, attrs, ...children], nextIndex: nextIndex };
|
|
272
294
|
}
|
|
273
295
|
if (token.type === 'math_block') {
|
|
274
|
-
return {
|
|
296
|
+
return {
|
|
297
|
+
node: ['math', { class: 'math block', content: token.content }, token.content],
|
|
298
|
+
nextIndex: startIndex + 1,
|
|
299
|
+
};
|
|
275
300
|
}
|
|
276
301
|
if (token.type === 'fence' || token.type === 'fenced_code_block' || token.type === 'code_block') {
|
|
277
302
|
const content = token.content || '';
|
|
@@ -305,14 +330,19 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
305
330
|
if (token.type === 'heading_open') {
|
|
306
331
|
const level = Number.parseInt(token.tag.replace('h', ''), 10);
|
|
307
332
|
const headingTag = `h${level}`;
|
|
333
|
+
const userAttrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
|
|
308
334
|
// Process heading children with inHeading flag for Comark component handling
|
|
309
335
|
const children = processBlockChildren(tokens, startIndex + 1, 'heading_close', true, true, insideNestedContext, state);
|
|
310
336
|
if (children.nodes.length > 0) {
|
|
311
337
|
// Always generate ID for all headings, no exceptions
|
|
312
338
|
const textContent = extractTextContent(children.nodes);
|
|
313
339
|
const headingId = uniqueSlug(slugify(textContent), level, state);
|
|
314
|
-
//
|
|
315
|
-
|
|
340
|
+
// Merge user-supplied attrs with the auto-generated id; user `id` wins.
|
|
341
|
+
const attrs = { id: headingId, ...userAttrs };
|
|
342
|
+
return {
|
|
343
|
+
node: [headingTag, attrs, ...children.nodes],
|
|
344
|
+
nextIndex: children.nextIndex + 1,
|
|
345
|
+
};
|
|
316
346
|
}
|
|
317
347
|
return { node: null, nextIndex: children.nextIndex + 1 };
|
|
318
348
|
}
|
|
@@ -320,19 +350,8 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
320
350
|
if (token.type === 'list_item_open') {
|
|
321
351
|
const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
|
|
322
352
|
const children = processBlockChildren(tokens, startIndex + 1, 'list_item_close', false, false, true, state);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
for (const child of children.nodes) {
|
|
326
|
-
if (Array.isArray(child) && child[0] === 'p') {
|
|
327
|
-
// Unwrap paragraph, add its children directly
|
|
328
|
-
unwrapped.push(...child.slice(2));
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
unwrapped.push(child);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (unwrapped.length > 0) {
|
|
335
|
-
return { node: ['li', attrs, ...unwrapped], nextIndex: children.nextIndex + 1 };
|
|
353
|
+
if (children.nodes.length > 0) {
|
|
354
|
+
return { node: ['li', attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
|
|
336
355
|
}
|
|
337
356
|
return { node: null, nextIndex: children.nextIndex + 1 };
|
|
338
357
|
}
|
|
@@ -353,25 +372,26 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType, state) {
|
|
|
353
372
|
const nodes = [];
|
|
354
373
|
let i = startIndex;
|
|
355
374
|
let currentSlotName = null;
|
|
375
|
+
let currentSlotAttrs = {};
|
|
356
376
|
let currentSlotChildren = [];
|
|
357
377
|
while (i < tokens.length && tokens[i].type !== closeType) {
|
|
358
378
|
const token = tokens[i];
|
|
359
379
|
// html_block can produce multiple nodes — handle before processBlockToken
|
|
360
380
|
if (token.type === 'html_block') {
|
|
361
|
-
const
|
|
381
|
+
const result = processHtmlBlockTokens(tokens, i);
|
|
362
382
|
if (currentSlotName !== null) {
|
|
363
|
-
currentSlotChildren.push(...
|
|
383
|
+
currentSlotChildren.push(...result.nodes);
|
|
364
384
|
}
|
|
365
385
|
else {
|
|
366
|
-
nodes.push(...
|
|
386
|
+
nodes.push(...result.nodes);
|
|
367
387
|
}
|
|
368
|
-
i
|
|
388
|
+
i = result.nextIndex;
|
|
369
389
|
continue;
|
|
370
390
|
}
|
|
371
391
|
// Check for slot marker: #slotname creates mdc_block_slot tokens
|
|
372
392
|
if (token.type === 'mdc_block_slot') {
|
|
373
393
|
// Extract slot name from token.attrs
|
|
374
|
-
// The attrs array contains [["#slotname", ""]] for open, and null/empty for close
|
|
394
|
+
// The attrs array contains [["#slotname", ""], ...props] for open, and null/empty for close
|
|
375
395
|
if (token.attrs && Array.isArray(token.attrs) && token.attrs.length > 0) {
|
|
376
396
|
const firstAttr = token.attrs[0];
|
|
377
397
|
if (Array.isArray(firstAttr) && firstAttr.length > 0) {
|
|
@@ -379,12 +399,21 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType, state) {
|
|
|
379
399
|
// Remove the # prefix to get the slot name
|
|
380
400
|
if (slotKey.startsWith('#')) {
|
|
381
401
|
const slotName = slotKey.substring(1);
|
|
402
|
+
const slotAttrs = processAttributes(token.attrs.slice(1));
|
|
382
403
|
// Save previous slot if any
|
|
383
404
|
if (currentSlotName !== null && currentSlotChildren.length > 0) {
|
|
384
|
-
nodes.push([
|
|
405
|
+
nodes.push([
|
|
406
|
+
'template',
|
|
407
|
+
{
|
|
408
|
+
name: currentSlotName,
|
|
409
|
+
...currentSlotAttrs,
|
|
410
|
+
},
|
|
411
|
+
...currentSlotChildren,
|
|
412
|
+
]);
|
|
385
413
|
currentSlotChildren = [];
|
|
386
414
|
}
|
|
387
415
|
currentSlotName = slotName;
|
|
416
|
+
currentSlotAttrs = slotAttrs;
|
|
388
417
|
i++;
|
|
389
418
|
continue;
|
|
390
419
|
}
|
|
@@ -411,7 +440,14 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType, state) {
|
|
|
411
440
|
}
|
|
412
441
|
// Save last slot if any
|
|
413
442
|
if (currentSlotName !== null && currentSlotChildren.length > 0) {
|
|
414
|
-
nodes.push([
|
|
443
|
+
nodes.push([
|
|
444
|
+
'template',
|
|
445
|
+
{
|
|
446
|
+
name: currentSlotName,
|
|
447
|
+
...currentSlotAttrs,
|
|
448
|
+
},
|
|
449
|
+
...currentSlotChildren,
|
|
450
|
+
]);
|
|
415
451
|
}
|
|
416
452
|
return { nodes, nextIndex: i };
|
|
417
453
|
}
|
|
@@ -420,10 +456,10 @@ function processBlockChildren(tokens, startIndex, closeType, inlineOnly, inHeadi
|
|
|
420
456
|
let i = startIndex;
|
|
421
457
|
while (i < tokens.length && tokens[i].type !== closeType) {
|
|
422
458
|
const token = tokens[i];
|
|
423
|
-
// html_block can produce multiple nodes — handle before processBlockToken
|
|
424
459
|
if (token.type === 'html_block') {
|
|
425
|
-
|
|
426
|
-
|
|
460
|
+
const result = processHtmlBlockTokens(tokens, i);
|
|
461
|
+
nodes.push(...result.nodes);
|
|
462
|
+
i = result.nextIndex;
|
|
427
463
|
continue;
|
|
428
464
|
}
|
|
429
465
|
if (token.type === 'inline') {
|
|
@@ -615,7 +651,7 @@ function processInlineToken(tokens, startIndex, inHeading = false) {
|
|
|
615
651
|
return { node, nextIndex: j };
|
|
616
652
|
}
|
|
617
653
|
// Handle Comark inline span (e.g., [text]{attr})
|
|
618
|
-
//
|
|
654
|
+
// The syntax plugin emits mdc_inline_span tokens, and props appear AFTER the close token
|
|
619
655
|
if (token.type === 'mdc_inline_span' && token.nesting === 1) {
|
|
620
656
|
const attrs = {};
|
|
621
657
|
let i = startIndex + 1;
|
|
@@ -698,11 +734,11 @@ function processInlineToken(tokens, startIndex, inHeading = false) {
|
|
|
698
734
|
else {
|
|
699
735
|
// Self-closing component (nesting === 0)
|
|
700
736
|
const attrs = {};
|
|
701
|
-
//
|
|
737
|
+
// The syntax plugin stores attributes in a separate mdc_inline_props token
|
|
702
738
|
// that appears right after the component token
|
|
703
739
|
const { attrs: componentAttrs, nextIndex: propsNextIndex } = extractAttributes(tokens, startIndex + 1, false);
|
|
704
740
|
Object.assign(attrs, componentAttrs);
|
|
705
|
-
// Extract attributes from token.attrs (fallback, though
|
|
741
|
+
// Extract attributes from token.attrs (fallback, though the syntax plugin uses mdc_inline_props)
|
|
706
742
|
const fallbackAttrs = processAttributes(token.attrs, { handleBoolean: false });
|
|
707
743
|
Object.assign(attrs, fallbackAttrs);
|
|
708
744
|
// Return the component without any text children
|
|
@@ -734,7 +770,10 @@ function processInlineToken(tokens, startIndex, inHeading = false) {
|
|
|
734
770
|
return { node: null, nextIndex };
|
|
735
771
|
}
|
|
736
772
|
if (token.type === 'math_inline') {
|
|
737
|
-
return {
|
|
773
|
+
return {
|
|
774
|
+
node: ['math', { class: 'math inline', content: token.content }, token.content],
|
|
775
|
+
nextIndex: startIndex + 1,
|
|
776
|
+
};
|
|
738
777
|
}
|
|
739
778
|
// Handle generic inline open/close pairs
|
|
740
779
|
const tagName = INLINE_TAG_MAP[token.type];
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
export const unsafeTags = [
|
|
2
|
-
|
|
3
|
-
];
|
|
4
|
-
export const unsafeAttributes = [
|
|
5
|
-
'srcdoc',
|
|
6
|
-
'formaction',
|
|
7
|
-
];
|
|
1
|
+
export const unsafeTags = ['object'];
|
|
2
|
+
export const unsafeAttributes = ['srcdoc', 'formaction'];
|
|
8
3
|
export const unsafeLinkPrefix = [
|
|
9
4
|
'javascript:',
|
|
10
5
|
'data:text/html',
|
|
@@ -44,7 +39,7 @@ function validateUrl(value, mode, options) {
|
|
|
44
39
|
return value;
|
|
45
40
|
}
|
|
46
41
|
// Block known-unsafe protocols — hard floor, not overrideable by options
|
|
47
|
-
if (unsafeLinkPrefix.some(prefix => url.href.toLowerCase().startsWith(prefix))) {
|
|
42
|
+
if (unsafeLinkPrefix.some((prefix) => url.href.toLowerCase().startsWith(prefix))) {
|
|
48
43
|
return false;
|
|
49
44
|
}
|
|
50
45
|
// Block data: images when allowDataImages is false
|
|
@@ -62,7 +57,7 @@ function validateUrl(value, mode, options) {
|
|
|
62
57
|
const allowedPrefixes = mode === 'link' ? allowedLinkPrefixes : allowedImagePrefixes;
|
|
63
58
|
if (!allowedPrefixes.includes('*')) {
|
|
64
59
|
const href = url.href.toLowerCase();
|
|
65
|
-
const matchesPrefix = allowedPrefixes.some(prefix => href.startsWith(prefix.toLowerCase()));
|
|
60
|
+
const matchesPrefix = allowedPrefixes.some((prefix) => href.startsWith(prefix.toLowerCase()));
|
|
66
61
|
if (!matchesPrefix) {
|
|
67
62
|
if (defaultOrigin) {
|
|
68
63
|
return rewriteToDefaultOrigin(urlSanitized, defaultOrigin);
|
|
@@ -34,6 +34,13 @@ export declare function resolveAttributes(attrs: Record<string, unknown>, render
|
|
|
34
34
|
* binding doesn't resolve.
|
|
35
35
|
*/
|
|
36
36
|
export declare function resolveAttribute(attrs: Record<string, unknown>, renderData: NodeRenderData, key: string): unknown;
|
|
37
|
+
/**
|
|
38
|
+
* Filter implicit/auto-generated attrs that are encoded by the native
|
|
39
|
+
* markdown syntax and shouldn't echo back as `{attr=...}`. Used by the
|
|
40
|
+
* block stringifiers to decide whether a node has *user* attrs that must
|
|
41
|
+
* be preserved via the `::tag{...}` wrapper form.
|
|
42
|
+
*/
|
|
43
|
+
export declare function userBlockAttrs(tag: string, attributes: Record<string, unknown>): Record<string, unknown>;
|
|
37
44
|
/**
|
|
38
45
|
* Convert attributes to a string of Comark attributes
|
|
39
46
|
*
|
|
@@ -67,6 +67,58 @@ export function resolveAttribute(attrs, renderData, key) {
|
|
|
67
67
|
}
|
|
68
68
|
return attrs[key];
|
|
69
69
|
}
|
|
70
|
+
// Implicit attributes the parser injects per tag — they're conveyed by the
|
|
71
|
+
// native markdown syntax (e.g. `as` becomes `> [!NOTE]`, `task-list-item`
|
|
72
|
+
// is implicit in `- [ ]`) so they should not echo back as user attrs.
|
|
73
|
+
const IMPLICIT_ATTRS = {
|
|
74
|
+
blockquote: { drop: ['as'] },
|
|
75
|
+
ul: { classBlocklist: ['contains-task-list'] },
|
|
76
|
+
li: { classBlocklist: ['task-list-item'] },
|
|
77
|
+
// `language`/`filename`/`highlights`/`meta` ride on the fence info string.
|
|
78
|
+
// `tabindex`/`style` come from render-time plugins (e.g. shiki) and have no
|
|
79
|
+
// markdown form. `class` is handled specially in userBlockAttrs because shiki
|
|
80
|
+
// merges its injected classes with the user's class — we need to strip just
|
|
81
|
+
// the highlighter portion.
|
|
82
|
+
pre: { drop: ['language', 'filename', 'highlights', 'meta', 'tabindex', 'style'] },
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Filter implicit/auto-generated attrs that are encoded by the native
|
|
86
|
+
* markdown syntax and shouldn't echo back as `{attr=...}`. Used by the
|
|
87
|
+
* block stringifiers to decide whether a node has *user* attrs that must
|
|
88
|
+
* be preserved via the `::tag{...}` wrapper form.
|
|
89
|
+
*/
|
|
90
|
+
export function userBlockAttrs(tag, attributes) {
|
|
91
|
+
const rule = IMPLICIT_ATTRS[tag];
|
|
92
|
+
if (!rule)
|
|
93
|
+
return { ...attributes };
|
|
94
|
+
const result = {};
|
|
95
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
96
|
+
if (rule.drop?.includes(key))
|
|
97
|
+
continue;
|
|
98
|
+
if (key === 'class' && rule.classBlocklist && typeof value === 'string') {
|
|
99
|
+
const remaining = value
|
|
100
|
+
.split(/\s+/)
|
|
101
|
+
.filter((c) => c && !rule.classBlocklist.includes(c))
|
|
102
|
+
.join(' ');
|
|
103
|
+
if (remaining)
|
|
104
|
+
result[key] = remaining;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (key === 'class' && tag === 'pre' && typeof value === 'string' && value.startsWith('shiki ')) {
|
|
108
|
+
// Shiki injects `shiki [shiki-themes] <themes…> dark:<theme>` and any
|
|
109
|
+
// user-supplied class is appended after it. Recover the user portion by
|
|
110
|
+
// dropping everything up to and including the first `dark:*` token.
|
|
111
|
+
const tokens = value.split(/\s+/);
|
|
112
|
+
let cutoff = tokens.findIndex((t) => t === '.');
|
|
113
|
+
const userClass = cutoff >= 0 ? tokens.slice(cutoff + 1).join(' ') : '';
|
|
114
|
+
if (userClass)
|
|
115
|
+
result[key] = userClass;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
result[key] = value;
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
70
122
|
/**
|
|
71
123
|
* Convert attributes to a string of Comark attributes
|
|
72
124
|
*
|
|
@@ -83,7 +135,10 @@ export function comarkAttributes(attributes) {
|
|
|
83
135
|
return `#${value}`;
|
|
84
136
|
}
|
|
85
137
|
if (key === 'class') {
|
|
86
|
-
return value
|
|
138
|
+
return value
|
|
139
|
+
.split(' ')
|
|
140
|
+
.map((c) => `.${c}`)
|
|
141
|
+
.join('');
|
|
87
142
|
}
|
|
88
143
|
if (typeof value === 'object') {
|
|
89
144
|
return `${key}="${JSON.stringify(value).replace(/"/g, '\\"')}"`;
|
|
@@ -3,9 +3,7 @@ import { comarkAttributes } from "../attributes.js";
|
|
|
3
3
|
export async function a(node, state) {
|
|
4
4
|
const [_, attrs] = node;
|
|
5
5
|
const { href, ...rest } = attrs;
|
|
6
|
-
const attrsString = Object.keys(rest).length > 0
|
|
7
|
-
? comarkAttributes(rest)
|
|
8
|
-
: '';
|
|
6
|
+
const attrsString = Object.keys(rest).length > 0 ? comarkAttributes(rest) : '';
|
|
9
7
|
const content = await state.flow(node, state);
|
|
10
8
|
if (content === href && !attrsString) {
|
|
11
9
|
return `<${href}>`;
|
|
@@ -1,18 +1,33 @@
|
|
|
1
|
+
import { comarkAttributes, userBlockAttrs } from "../attributes.js";
|
|
1
2
|
export async function blockquote(node, state) {
|
|
2
3
|
const children = node.slice(2);
|
|
3
4
|
let childResult = '';
|
|
4
5
|
for (const child of children) {
|
|
5
6
|
childResult += await state.one(child, state, node);
|
|
6
7
|
}
|
|
8
|
+
const userAttrs = userBlockAttrs('blockquote', node[1]);
|
|
9
|
+
const attrs = comarkAttributes(userAttrs);
|
|
10
|
+
const hasBlockChildren = children.some((c) => Array.isArray(c));
|
|
11
|
+
// Multi-block content with attrs has no unambiguous inline form — round-trip
|
|
12
|
+
// via `::blockquote{attrs}` so the attrs aren't visually attached to one
|
|
13
|
+
// paragraph and parsers can recover the same AST.
|
|
14
|
+
if (attrs && hasBlockChildren) {
|
|
15
|
+
const content = childResult
|
|
16
|
+
.trim()
|
|
17
|
+
.split('\n')
|
|
18
|
+
.map((line) => (line ? `> ${line}` : '>'))
|
|
19
|
+
.join('\n');
|
|
20
|
+
return `::blockquote${attrs}\n${content}\n::` + state.context.blockSeparator;
|
|
21
|
+
}
|
|
22
|
+
if (attrs)
|
|
23
|
+
childResult = `${childResult.replace(/[ \t]+$/, '')} ${attrs}`;
|
|
7
24
|
const content = childResult
|
|
8
25
|
.trim()
|
|
9
26
|
.split('\n')
|
|
10
|
-
.map(line => line ? `> ${line}` : '>')
|
|
27
|
+
.map((line) => (line ? `> ${line}` : '>'))
|
|
11
28
|
.join('\n');
|
|
12
29
|
if (node[1].as) {
|
|
13
|
-
return `> [!${String(node[1].as).toUpperCase()}]\n`
|
|
14
|
-
+ content
|
|
15
|
-
+ state.context.blockSeparator;
|
|
30
|
+
return `> [!${String(node[1].as).toUpperCase()}]\n` + content + state.context.blockSeparator;
|
|
16
31
|
}
|
|
17
32
|
return content + state.context.blockSeparator;
|
|
18
33
|
}
|
|
@@ -2,9 +2,7 @@ import { comarkAttributes } from "../attributes.js";
|
|
|
2
2
|
import { textContent } from "../../../utils/index.js";
|
|
3
3
|
export function code(node, _state) {
|
|
4
4
|
const [_, attrs] = node;
|
|
5
|
-
const attrsString = Object.keys(attrs).length > 0
|
|
6
|
-
? comarkAttributes(attrs)
|
|
7
|
-
: '';
|
|
5
|
+
const attrsString = Object.keys(attrs).length > 0 ? comarkAttributes(attrs) : '';
|
|
8
6
|
const content = textContent(node);
|
|
9
7
|
const fence = content.includes('`') ? '``' : '`';
|
|
10
8
|
return `${fence}${content}${fence}${attrsString}`;
|
|
@@ -6,8 +6,6 @@ export async function emphesis(node, state) {
|
|
|
6
6
|
content += await state.one(child, state, node);
|
|
7
7
|
}
|
|
8
8
|
content = content.trim();
|
|
9
|
-
const attrsString = Object.keys(attrs).length > 0
|
|
10
|
-
? comarkAttributes(attrs)
|
|
11
|
-
: '';
|
|
9
|
+
const attrsString = Object.keys(attrs).length > 0 ? comarkAttributes(attrs) : '';
|
|
12
10
|
return `*${content}*${attrsString}`;
|
|
13
11
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { comarkAttributes } from "../attributes.js";
|
|
1
2
|
// h1, h2, h3, h4, h5, h6
|
|
2
3
|
export async function heading(node, state) {
|
|
3
4
|
const [tag] = node;
|
|
4
5
|
const level = Number(tag.slice(1));
|
|
5
6
|
const content = await state.flow(node, state);
|
|
6
|
-
|
|
7
|
+
// The auto-generated id is implicit in `# Heading` markdown — don't echo it.
|
|
8
|
+
const { id: _id, ...rest } = node[1];
|
|
9
|
+
const attrs = comarkAttributes(rest);
|
|
10
|
+
const suffix = attrs ? ` ${attrs}` : '';
|
|
11
|
+
return '#'.repeat(level) + ' ' + content + suffix + state.context.blockSeparator;
|
|
7
12
|
}
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { htmlAttributes } from "../attributes.js";
|
|
2
|
-
import { indent } from "
|
|
2
|
+
import { indent } from "../../../utils/index.js";
|
|
3
3
|
const textBlocks = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'td', 'th']);
|
|
4
4
|
const selfCloseTags = new Set(['br', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
|
|
5
5
|
const inlineTags = new Set(['strong', 'em', 'code', 'a', 'br', 'span', 'img']);
|
|
6
|
-
const blockTags = new Set([
|
|
6
|
+
const blockTags = new Set([
|
|
7
|
+
'p',
|
|
8
|
+
'h1',
|
|
9
|
+
'h2',
|
|
10
|
+
'h3',
|
|
11
|
+
'h4',
|
|
12
|
+
'h5',
|
|
13
|
+
'h6',
|
|
14
|
+
'li',
|
|
15
|
+
'ul',
|
|
16
|
+
'ol',
|
|
17
|
+
'blockquote',
|
|
18
|
+
'hr',
|
|
19
|
+
'table',
|
|
20
|
+
'td',
|
|
21
|
+
'th',
|
|
22
|
+
]);
|
|
7
23
|
export async function html(node, state, parent) {
|
|
8
24
|
const [tag, attr, ...children] = node;
|
|
9
25
|
const { $ = {}, ...rawAttributes } = attr;
|
|
@@ -15,11 +31,9 @@ export async function html(node, state, parent) {
|
|
|
15
31
|
// back to the raw (empty) attrs to avoid leaking parent props onto native
|
|
16
32
|
// wrappers like `<p>` or `<ul>`.
|
|
17
33
|
const rawHasAttrs = Object.keys(rawAttributes).length > 0;
|
|
18
|
-
const attributes = state.context.html
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const hasOnlyTextChildren = children.every(child => typeof child === 'string' || inlineTags.has(String(child?.[0])));
|
|
22
|
-
const hasTextSibling = children.some(child => typeof child === 'string');
|
|
34
|
+
const attributes = state.context.html ? (rawHasAttrs ? state.renderData.props : rawAttributes) : rawAttributes;
|
|
35
|
+
const hasOnlyTextChildren = children.every((child) => typeof child === 'string' || inlineTags.has(String(child?.[0])));
|
|
36
|
+
const hasTextSibling = children.some((child) => typeof child === 'string');
|
|
23
37
|
const isBlock = textBlocks.has(String(tag));
|
|
24
38
|
const isInline = inlineTags.has(String(tag)) && $.block === 0;
|
|
25
39
|
let oneLiner = isBlock && hasOnlyTextChildren;
|
|
@@ -48,7 +62,8 @@ export async function html(node, state, parent) {
|
|
|
48
62
|
for (let i = 0; i < children.length; i++) {
|
|
49
63
|
const childContent = childrenContent[i];
|
|
50
64
|
const child = children[i];
|
|
51
|
-
const isBlock = typeof child !== 'string' &&
|
|
65
|
+
const isBlock = typeof child !== 'string' &&
|
|
66
|
+
(blockTags.has(String(child?.[0])) || (!inlineTags.has(String(child?.[0])) && !hasTextSibling));
|
|
52
67
|
if (i > 0 && !isPrevBlock && isBlock) {
|
|
53
68
|
content += state.context.blockSeparator;
|
|
54
69
|
}
|
|
@@ -62,23 +77,24 @@ export async function html(node, state, parent) {
|
|
|
62
77
|
if (revert) {
|
|
63
78
|
state.applyContext(revert);
|
|
64
79
|
}
|
|
65
|
-
const attrs = Object.keys(attributes).length > 0
|
|
66
|
-
? ` ${htmlAttributes(attributes)}`
|
|
67
|
-
: '';
|
|
80
|
+
const attrs = Object.keys(attributes).length > 0 ? ` ${htmlAttributes(attributes)}` : '';
|
|
68
81
|
if (isSelfClose) {
|
|
69
82
|
return `<${tag}${attrs} />` + (!parent && !isInline ? state.context.blockSeparator : '');
|
|
70
83
|
}
|
|
71
84
|
if (!oneLiner && content) {
|
|
72
|
-
content = '\n' + paddNoneHtmlContent(content, state).trimEnd() + '\n';
|
|
85
|
+
content = '\n' + paddNoneHtmlContent(content, state, String(tag)).trimEnd() + '\n';
|
|
73
86
|
}
|
|
74
|
-
return `<${tag}${attrs}>${content}</${tag}>`
|
|
75
|
-
+ (!parent && !isInline ? state.context.blockSeparator : '');
|
|
87
|
+
return `<${tag}${attrs}>${content}</${tag}>` + (!parent && !isInline ? state.context.blockSeparator : '');
|
|
76
88
|
}
|
|
77
|
-
|
|
89
|
+
// Literal-content tags whose body must be rendered verbatim (no indentation
|
|
90
|
+
// re-flow). Matches the parser-side set so `<style>` / `<script>` etc. stay
|
|
91
|
+
// flush-left in the output, the way they were authored.
|
|
92
|
+
const LITERAL_CONTENT_TAGS = new Set(['code', 'kbd', 'pre', 'samp', 'script', 'style', 'textarea', 'var']);
|
|
93
|
+
function paddNoneHtmlContent(content, state, tag) {
|
|
78
94
|
if (state.context.html) {
|
|
95
|
+
if (LITERAL_CONTENT_TAGS.has(tag.toLowerCase()))
|
|
96
|
+
return content;
|
|
79
97
|
return indent(content);
|
|
80
98
|
}
|
|
81
|
-
return (
|
|
82
|
-
+ content
|
|
83
|
-
+ (content.trim().endsWith('>') ? '' : ''));
|
|
99
|
+
return (content.trim().startsWith('<') ? '' : '') + content + (content.trim().endsWith('>') ? '' : '');
|
|
84
100
|
}
|
|
@@ -2,8 +2,6 @@ import { comarkAttributes } from "../attributes.js";
|
|
|
2
2
|
export function img(node, _state) {
|
|
3
3
|
const [_, attrs] = node;
|
|
4
4
|
const { title, src, alt = '', ...rest } = attrs;
|
|
5
|
-
const attrsString = Object.keys(rest).length > 0
|
|
6
|
-
? comarkAttributes(rest)
|
|
7
|
-
: '';
|
|
5
|
+
const attrsString = Object.keys(rest).length > 0 ? comarkAttributes(rest) : '';
|
|
8
6
|
return title ? `` : `${attrsString}`;
|
|
9
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { indent } from "
|
|
1
|
+
import { indent } from "../../../utils/index.js";
|
|
2
|
+
import { comarkAttributes, userBlockAttrs } from "../attributes.js";
|
|
2
3
|
// Block elements that need explicit indentation in list items.
|
|
3
4
|
// Note: ol/ul are handled by their own handlers which manage indentation via listIndent context.
|
|
4
5
|
const blockElements = new Set(['pre', 'blockquote', 'table']);
|
|
@@ -18,23 +19,31 @@ export async function li(node, state) {
|
|
|
18
19
|
let result = '';
|
|
19
20
|
for (const child of children) {
|
|
20
21
|
const rendered = await state.one(child, state, node);
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
if (result && Array.isArray(child)) {
|
|
23
|
+
if (blockElements.has(child[0])) {
|
|
24
|
+
// Block-level child: put on its own line and indent to align with list prefix
|
|
25
|
+
const indented = indent(rendered, { width: prefixWidth });
|
|
26
|
+
result = result.trimEnd() + '\n' + indented.trimEnd() + '\n';
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (child[0] === 'p') {
|
|
30
|
+
const indented = indent(rendered, { width: prefixWidth });
|
|
31
|
+
result = result.trimEnd() + '\n\n' + indented.trimEnd() + '\n';
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
28
34
|
}
|
|
35
|
+
result += rendered;
|
|
29
36
|
}
|
|
30
37
|
result = result.trim();
|
|
38
|
+
const attrs = comarkAttributes(userBlockAttrs('li', node[1]));
|
|
39
|
+
const suffix = attrs ? ` ${attrs}` : '';
|
|
31
40
|
if (!order) {
|
|
32
41
|
result = escapeLeadingNumberDot(result);
|
|
33
42
|
}
|
|
34
43
|
if (order) {
|
|
35
44
|
state.applyContext({ order: order + 1 });
|
|
36
45
|
}
|
|
37
|
-
return `${prefix}${result}\n`;
|
|
46
|
+
return `${prefix}${result}${suffix}\n`;
|
|
38
47
|
}
|
|
39
48
|
function escapeLeadingNumberDot(str) {
|
|
40
49
|
if (str.length === 0)
|