comark 0.3.2 → 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/parse/auto-close/index.js +44 -18
- package/dist/internal/parse/auto-unwrap.js +5 -1
- package/dist/internal/parse/html/html_block_rule.js +9 -15
- package/dist/internal/parse/html/index.d.ts +1 -0
- package/dist/internal/parse/html/index.js +1 -1
- package/dist/internal/parse/token-processor.js +70 -32
- package/dist/internal/stringify/attributes.d.ts +7 -0
- package/dist/internal/stringify/attributes.js +52 -0
- package/dist/internal/stringify/handlers/blockquote.js +17 -0
- package/dist/internal/stringify/handlers/heading.js +6 -1
- package/dist/internal/stringify/handlers/html.js +8 -2
- package/dist/internal/stringify/handlers/li.js +4 -1
- package/dist/internal/stringify/handlers/mdc.js +1 -1
- package/dist/internal/stringify/handlers/ol.js +11 -1
- package/dist/internal/stringify/handlers/p.js +4 -0
- package/dist/internal/stringify/handlers/pre.js +11 -2
- package/dist/internal/stringify/handlers/table.js +7 -0
- package/dist/internal/stringify/handlers/template.js +4 -1
- package/dist/internal/stringify/handlers/ul.js +11 -1
- package/dist/parse.d.ts +4 -4
- package/dist/parse.js +6 -2
- package/dist/plugins/alert.d.ts +1 -1
- package/dist/plugins/binding.d.ts +1 -1
- package/dist/plugins/breaks.d.ts +1 -1
- package/dist/plugins/emoji.d.ts +1 -1
- package/dist/plugins/footnotes.d.ts +1 -1
- package/dist/plugins/headings.d.ts +19 -8
- package/dist/plugins/headings.js +25 -15
- package/dist/plugins/highlight.d.ts +1 -1
- package/dist/plugins/highlight.js +4 -1
- package/dist/plugins/json-render.d.ts +1 -1
- package/dist/plugins/math.d.ts +1 -1
- package/dist/plugins/mermaid.d.ts +1 -1
- package/dist/plugins/punctuation.d.ts +1 -1
- package/dist/plugins/security.d.ts +1 -1
- package/dist/plugins/summary.d.ts +4 -1
- package/dist/plugins/syntax.d.ts +1 -1
- package/dist/plugins/syntax.js +70 -34
- package/dist/plugins/task-list.d.ts +1 -1
- package/dist/plugins/toc.d.ts +3 -1
- package/dist/types.d.ts +56 -12
- package/dist/utils/helpers.d.ts +16 -4
- package/dist/utils/helpers.js +15 -3
- package/package.json +3 -2
- package/skills/comark/references/rendering-svelte.md +51 -7
- package/dist/devtools/index.d.ts +0 -1
- package/dist/devtools/index.js +0 -1
- package/dist/devtools/register.d.ts +0 -1
- package/dist/devtools/register.js +0 -1
- package/dist/devtools/registry.d.ts +0 -1
- package/dist/devtools/registry.js +0 -1
- package/dist/devtools/vite.d.ts +0 -1
- package/dist/devtools/vite.js +0 -1
- package/dist/internal/stringify/indent.d.ts +0 -1
- package/dist/internal/stringify/indent.js +0 -1
- package/dist/vite.d.ts +0 -1
- package/dist/vite.js +0 -1
|
@@ -19,10 +19,33 @@ export function autoCloseMarkdown(markdown) {
|
|
|
19
19
|
let inFrontmatter = false;
|
|
20
20
|
let inBlockMath = false;
|
|
21
21
|
let tableStart = -1;
|
|
22
|
+
// Tag name when inside a raw-text HTML element (`<style>`, `<script>`,
|
|
23
|
+
// `<pre>`, `<textarea>`). Their bodies must be passed through verbatim —
|
|
24
|
+
// any `::root`/`**` markers there are CSS/JS/text, not Comark/markdown.
|
|
25
|
+
let inRawTextElement = null;
|
|
26
|
+
const RAW_TEXT_OPEN_RE = /^<(script|pre|style|textarea)(\s|>|$)/i;
|
|
22
27
|
const componentStack = [];
|
|
23
28
|
for (let idx = 0; idx < n; idx++) {
|
|
24
29
|
const line = lines[idx];
|
|
25
30
|
const trimmed = line.trim();
|
|
31
|
+
// Raw-text HTML element: skip all line-level processing inside its body,
|
|
32
|
+
// and update the open/close state. Open and close can sit on the same
|
|
33
|
+
// line (e.g. `<style>body { ... }</style>` inline).
|
|
34
|
+
if (inRawTextElement) {
|
|
35
|
+
const closeRe = new RegExp(`</${inRawTextElement}\\s*>`, 'i');
|
|
36
|
+
if (closeRe.test(line))
|
|
37
|
+
inRawTextElement = null;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const rawTextMatch = trimmed.match(RAW_TEXT_OPEN_RE);
|
|
41
|
+
if (rawTextMatch) {
|
|
42
|
+
const tag = rawTextMatch[1].toLowerCase();
|
|
43
|
+
// Stay "inside" only if the close tag isn't already on this line.
|
|
44
|
+
const closeRe = new RegExp(`</${tag}\\s*>`, 'i');
|
|
45
|
+
if (!closeRe.test(line))
|
|
46
|
+
inRawTextElement = tag;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
26
49
|
// Frontmatter: only starts at document line 0
|
|
27
50
|
if (idx === 0 && trimmed === '---') {
|
|
28
51
|
inFrontmatter = true;
|
|
@@ -190,7 +213,8 @@ function closeInlineMarkersLinear(line) {
|
|
|
190
213
|
// Count markers by scanning
|
|
191
214
|
let asteriskCount = 0;
|
|
192
215
|
let underscoreCount = 0;
|
|
193
|
-
let
|
|
216
|
+
let doubleTildeCount = 0; // Count ~~ occurrences (GFM strikethrough delimiter)
|
|
217
|
+
let singleTildeCount = 0; // Count standalone ~ (not part of ~~)
|
|
194
218
|
let backtickCount = 0;
|
|
195
219
|
let dollarCount = 0; // Count $ for math
|
|
196
220
|
let dollarPairCount = 0; // Count $$ pairs for block math
|
|
@@ -280,7 +304,13 @@ function closeInlineMarkersLinear(line) {
|
|
|
280
304
|
}
|
|
281
305
|
}
|
|
282
306
|
else if (ch === '~') {
|
|
283
|
-
|
|
307
|
+
if (i + 1 < len && line[i + 1] === '~') {
|
|
308
|
+
doubleTildeCount++;
|
|
309
|
+
i++; // Skip second tilde since we counted the pair
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
singleTildeCount++;
|
|
313
|
+
}
|
|
284
314
|
}
|
|
285
315
|
else if (ch === '`') {
|
|
286
316
|
backtickCount++;
|
|
@@ -450,22 +480,18 @@ function closeInlineMarkersLinear(line) {
|
|
|
450
480
|
}
|
|
451
481
|
}
|
|
452
482
|
}
|
|
453
|
-
// Check ~~ (strikethrough)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
closingSuffix = '~'.repeat(needed);
|
|
466
|
-
if (hasTrailingSpace)
|
|
467
|
-
shouldTrim = true;
|
|
468
|
-
}
|
|
483
|
+
// Check ~~ (strikethrough) and ~ (single-tilde) separately so that paired
|
|
484
|
+
// singles like ~Hello~ are left alone while ~~text and ~Hello both close.
|
|
485
|
+
if (!closingSuffix && doubleTildeCount % 2 === 1) {
|
|
486
|
+
// A trailing single ~ after an open ~~ is a partial closer (~~text~)
|
|
487
|
+
closingSuffix = singleTildeCount === 1 ? '~' : '~~';
|
|
488
|
+
if (hasTrailingSpace)
|
|
489
|
+
shouldTrim = true;
|
|
490
|
+
}
|
|
491
|
+
else if (!closingSuffix && singleTildeCount % 2 === 1) {
|
|
492
|
+
closingSuffix = '~';
|
|
493
|
+
if (hasTrailingSpace)
|
|
494
|
+
shouldTrim = true;
|
|
469
495
|
}
|
|
470
496
|
// Check ` (code)
|
|
471
497
|
if (!closingSuffix && backtickCount % 2 === 1) {
|
|
@@ -30,5 +30,9 @@ export function applyAutoUnwrap(node) {
|
|
|
30
30
|
if (nonEmptyChildren.length > 1 || typeof nonEmptyChildren[0] === 'string' || nonEmptyChildren[0][0] !== 'p') {
|
|
31
31
|
return [tag, props, ...children.map((child) => applyAutoUnwrap(child))];
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
// Lift the paragraph's attrs onto the parent so trailing `{attr}` survives the unwrap.
|
|
34
|
+
// Parent attrs take precedence so explicit component props aren't overridden.
|
|
35
|
+
const paragraphAttrs = nonEmptyChildren[0][1];
|
|
36
|
+
const mergedProps = paragraphAttrs && Object.keys(paragraphAttrs).length > 0 ? { ...paragraphAttrs, ...props } : props;
|
|
37
|
+
return [tag, mergedProps, ...nonEmptyChildren[0].slice(2)];
|
|
34
38
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Standard CommonMark html_block rule — see
|
|
2
|
+
// https://spec.commonmark.org/0.30/#html-blocks
|
|
3
|
+
//
|
|
4
|
+
// 7 sequences in priority order, each: [opener regex, closer regex, can-terminate-paragraph]
|
|
2
5
|
import block_names from "./html_blocks.js";
|
|
3
6
|
import { HTML_OPEN_CLOSE_TAG_RE } from "./html_re.js";
|
|
4
|
-
// An array of opening and corresponding closing sequences for html tags,
|
|
5
|
-
// last argument defines whether it can terminate a paragraph or not
|
|
6
|
-
//
|
|
7
7
|
const HTML_SEQUENCES = [
|
|
8
|
-
[new RegExp(`${HTML_OPEN_CLOSE_TAG_RE.source}\\s*$`), /^<\/[^>]+>$/, true],
|
|
9
8
|
[/^<(script|pre|style|textarea)(?=(\s|>|$))/i, /<\/(script|pre|style|textarea)>/i, true],
|
|
10
9
|
[/^<!--/, /-->/, true],
|
|
11
10
|
[/^<\?/, /\?>/, true],
|
|
@@ -17,7 +16,6 @@ const HTML_SEQUENCES = [
|
|
|
17
16
|
export default function html_block(state, startLine, endLine, silent) {
|
|
18
17
|
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
19
18
|
let max = state.eMarks[startLine];
|
|
20
|
-
// if it's indented more than 3 spaces, it should be a code block
|
|
21
19
|
if (state.sCount[startLine] - state.blkIndent >= 4)
|
|
22
20
|
return false;
|
|
23
21
|
if (state.src.charCodeAt(pos) !== 0x3c /* < */)
|
|
@@ -30,18 +28,14 @@ export default function html_block(state, startLine, endLine, silent) {
|
|
|
30
28
|
}
|
|
31
29
|
if (i === HTML_SEQUENCES.length)
|
|
32
30
|
return false;
|
|
33
|
-
if (silent)
|
|
34
|
-
// true if this sequence can be a terminator, false otherwise
|
|
31
|
+
if (silent)
|
|
35
32
|
return HTML_SEQUENCES[i][2];
|
|
36
|
-
}
|
|
37
33
|
let nextLine = startLine + 1;
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
if (i !== 0 && !HTML_SEQUENCES[i][1].test(lineText)) {
|
|
34
|
+
// Walk forward until the closer regex matches or we hit a blank line.
|
|
35
|
+
if (!HTML_SEQUENCES[i][1].test(lineText)) {
|
|
41
36
|
for (; nextLine < endLine; nextLine++) {
|
|
42
|
-
if (state.sCount[nextLine] < state.blkIndent)
|
|
37
|
+
if (state.sCount[nextLine] < state.blkIndent)
|
|
43
38
|
break;
|
|
44
|
-
}
|
|
45
39
|
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
46
40
|
max = state.eMarks[nextLine];
|
|
47
41
|
lineText = state.src.slice(pos, max);
|
|
@@ -53,7 +47,7 @@ export default function html_block(state, startLine, endLine, silent) {
|
|
|
53
47
|
}
|
|
54
48
|
}
|
|
55
49
|
state.line = nextLine;
|
|
56
|
-
const token =
|
|
50
|
+
const token = state.push('html_block', '', 1);
|
|
57
51
|
token.map = [startLine, nextLine];
|
|
58
52
|
token.content = state.getLines(startLine, nextLine, state.blkIndent, true);
|
|
59
53
|
return true;
|
|
@@ -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,6 +36,13 @@ 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) {
|
|
@@ -54,6 +64,15 @@ export function marmdownItTokensToComarkTree(tokens, options = { startLine: 0, p
|
|
|
54
64
|
}
|
|
55
65
|
return nodes;
|
|
56
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
|
+
}
|
|
57
76
|
/**
|
|
58
77
|
* Extract and process attributes from a token's attrs array
|
|
59
78
|
*/
|
|
@@ -72,11 +91,7 @@ function processAttributes(attrsArray, options = {}) {
|
|
|
72
91
|
continue;
|
|
73
92
|
}
|
|
74
93
|
// Handle boolean attributes: {bool} -> {":bool": "true"}
|
|
75
|
-
if (handleBoolean &&
|
|
76
|
-
!key.startsWith(':') &&
|
|
77
|
-
!key.startsWith('#') &&
|
|
78
|
-
!key.startsWith('.') &&
|
|
79
|
-
(!value || value === 'true' || value === '')) {
|
|
94
|
+
if (handleBoolean && !key.startsWith(':') && !key.startsWith('#') && !key.startsWith('.') && value === 'true') {
|
|
80
95
|
attrs[`:${key}`] = 'true';
|
|
81
96
|
continue;
|
|
82
97
|
}
|
|
@@ -222,22 +237,12 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
222
237
|
if (token.type === 'hr') {
|
|
223
238
|
return { node: ['hr', {}], nextIndex: startIndex + 1 };
|
|
224
239
|
}
|
|
225
|
-
// html_block is
|
|
240
|
+
// html_block is normally handled upstream (in marmdownItTokensToComarkTree /
|
|
226
241
|
// processBlockChildren / processBlockChildrenWithSlots) before reaching here.
|
|
227
|
-
//
|
|
242
|
+
// Safety fallback when it slips through.
|
|
228
243
|
if (token.type === 'html_block') {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const inner = content.endsWith('-->') ? content.slice(4, -3) : content.slice(4);
|
|
232
|
-
return { node: [null, {}, inner], nextIndex: startIndex + 1 };
|
|
233
|
-
}
|
|
234
|
-
const children = processBlockChildren(tokens, startIndex + 1, 'html_block_close', false, false, false, state);
|
|
235
|
-
const [node1] = htmlToComarkNodes(content);
|
|
236
|
-
if (!node1) {
|
|
237
|
-
return { node: null, nextIndex: startIndex + 1 };
|
|
238
|
-
}
|
|
239
|
-
const node = [node1[0], node1[1], ...children.nodes];
|
|
240
|
-
return { node, nextIndex: children.nextIndex + 1 };
|
|
244
|
+
const result = processHtmlBlockTokens(tokens, startIndex);
|
|
245
|
+
return { node: result.nodes[0] ?? null, nextIndex: result.nextIndex };
|
|
241
246
|
}
|
|
242
247
|
// Handle Comark block components (e.g., ::component ... ::)
|
|
243
248
|
if (token.type === 'mdc_block_open') {
|
|
@@ -245,6 +250,20 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
245
250
|
const attrs = processAttributes(token.attrs);
|
|
246
251
|
// Process children until mdc_block_close, handling slots (#slotname)
|
|
247
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
|
+
}
|
|
248
267
|
// Return the component even if it has no children (empty component like ::component\n::)
|
|
249
268
|
return { node: [componentName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
|
|
250
269
|
}
|
|
@@ -311,15 +330,17 @@ function processBlockToken(tokens, startIndex, insideNestedContext = false, stat
|
|
|
311
330
|
if (token.type === 'heading_open') {
|
|
312
331
|
const level = Number.parseInt(token.tag.replace('h', ''), 10);
|
|
313
332
|
const headingTag = `h${level}`;
|
|
333
|
+
const userAttrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
|
|
314
334
|
// Process heading children with inHeading flag for Comark component handling
|
|
315
335
|
const children = processBlockChildren(tokens, startIndex + 1, 'heading_close', true, true, insideNestedContext, state);
|
|
316
336
|
if (children.nodes.length > 0) {
|
|
317
337
|
// Always generate ID for all headings, no exceptions
|
|
318
338
|
const textContent = extractTextContent(children.nodes);
|
|
319
339
|
const headingId = uniqueSlug(slugify(textContent), level, state);
|
|
320
|
-
//
|
|
340
|
+
// Merge user-supplied attrs with the auto-generated id; user `id` wins.
|
|
341
|
+
const attrs = { id: headingId, ...userAttrs };
|
|
321
342
|
return {
|
|
322
|
-
node: [headingTag,
|
|
343
|
+
node: [headingTag, attrs, ...children.nodes],
|
|
323
344
|
nextIndex: children.nextIndex + 1,
|
|
324
345
|
};
|
|
325
346
|
}
|
|
@@ -351,25 +372,26 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType, state) {
|
|
|
351
372
|
const nodes = [];
|
|
352
373
|
let i = startIndex;
|
|
353
374
|
let currentSlotName = null;
|
|
375
|
+
let currentSlotAttrs = {};
|
|
354
376
|
let currentSlotChildren = [];
|
|
355
377
|
while (i < tokens.length && tokens[i].type !== closeType) {
|
|
356
378
|
const token = tokens[i];
|
|
357
379
|
// html_block can produce multiple nodes — handle before processBlockToken
|
|
358
380
|
if (token.type === 'html_block') {
|
|
359
|
-
const
|
|
381
|
+
const result = processHtmlBlockTokens(tokens, i);
|
|
360
382
|
if (currentSlotName !== null) {
|
|
361
|
-
currentSlotChildren.push(...
|
|
383
|
+
currentSlotChildren.push(...result.nodes);
|
|
362
384
|
}
|
|
363
385
|
else {
|
|
364
|
-
nodes.push(...
|
|
386
|
+
nodes.push(...result.nodes);
|
|
365
387
|
}
|
|
366
|
-
i
|
|
388
|
+
i = result.nextIndex;
|
|
367
389
|
continue;
|
|
368
390
|
}
|
|
369
391
|
// Check for slot marker: #slotname creates mdc_block_slot tokens
|
|
370
392
|
if (token.type === 'mdc_block_slot') {
|
|
371
393
|
// Extract slot name from token.attrs
|
|
372
|
-
// 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
|
|
373
395
|
if (token.attrs && Array.isArray(token.attrs) && token.attrs.length > 0) {
|
|
374
396
|
const firstAttr = token.attrs[0];
|
|
375
397
|
if (Array.isArray(firstAttr) && firstAttr.length > 0) {
|
|
@@ -377,12 +399,21 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType, state) {
|
|
|
377
399
|
// Remove the # prefix to get the slot name
|
|
378
400
|
if (slotKey.startsWith('#')) {
|
|
379
401
|
const slotName = slotKey.substring(1);
|
|
402
|
+
const slotAttrs = processAttributes(token.attrs.slice(1));
|
|
380
403
|
// Save previous slot if any
|
|
381
404
|
if (currentSlotName !== null && currentSlotChildren.length > 0) {
|
|
382
|
-
nodes.push([
|
|
405
|
+
nodes.push([
|
|
406
|
+
'template',
|
|
407
|
+
{
|
|
408
|
+
name: currentSlotName,
|
|
409
|
+
...currentSlotAttrs,
|
|
410
|
+
},
|
|
411
|
+
...currentSlotChildren,
|
|
412
|
+
]);
|
|
383
413
|
currentSlotChildren = [];
|
|
384
414
|
}
|
|
385
415
|
currentSlotName = slotName;
|
|
416
|
+
currentSlotAttrs = slotAttrs;
|
|
386
417
|
i++;
|
|
387
418
|
continue;
|
|
388
419
|
}
|
|
@@ -409,7 +440,14 @@ function processBlockChildrenWithSlots(tokens, startIndex, closeType, state) {
|
|
|
409
440
|
}
|
|
410
441
|
// Save last slot if any
|
|
411
442
|
if (currentSlotName !== null && currentSlotChildren.length > 0) {
|
|
412
|
-
nodes.push([
|
|
443
|
+
nodes.push([
|
|
444
|
+
'template',
|
|
445
|
+
{
|
|
446
|
+
name: currentSlotName,
|
|
447
|
+
...currentSlotAttrs,
|
|
448
|
+
},
|
|
449
|
+
...currentSlotChildren,
|
|
450
|
+
]);
|
|
413
451
|
}
|
|
414
452
|
return { nodes, nextIndex: i };
|
|
415
453
|
}
|
|
@@ -418,10 +456,10 @@ function processBlockChildren(tokens, startIndex, closeType, inlineOnly, inHeadi
|
|
|
418
456
|
let i = startIndex;
|
|
419
457
|
while (i < tokens.length && tokens[i].type !== closeType) {
|
|
420
458
|
const token = tokens[i];
|
|
421
|
-
// html_block can produce multiple nodes — handle before processBlockToken
|
|
422
459
|
if (token.type === 'html_block') {
|
|
423
|
-
|
|
424
|
-
|
|
460
|
+
const result = processHtmlBlockTokens(tokens, i);
|
|
461
|
+
nodes.push(...result.nodes);
|
|
462
|
+
i = result.nextIndex;
|
|
425
463
|
continue;
|
|
426
464
|
}
|
|
427
465
|
if (token.type === 'inline') {
|
|
@@ -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
|
*
|
|
@@ -1,9 +1,26 @@
|
|
|
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')
|
|
@@ -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
|
}
|
|
@@ -82,12 +82,18 @@ export async function html(node, state, parent) {
|
|
|
82
82
|
return `<${tag}${attrs} />` + (!parent && !isInline ? state.context.blockSeparator : '');
|
|
83
83
|
}
|
|
84
84
|
if (!oneLiner && content) {
|
|
85
|
-
content = '\n' + paddNoneHtmlContent(content, state).trimEnd() + '\n';
|
|
85
|
+
content = '\n' + paddNoneHtmlContent(content, state, String(tag)).trimEnd() + '\n';
|
|
86
86
|
}
|
|
87
87
|
return `<${tag}${attrs}>${content}</${tag}>` + (!parent && !isInline ? state.context.blockSeparator : '');
|
|
88
88
|
}
|
|
89
|
-
|
|
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) {
|
|
90
94
|
if (state.context.html) {
|
|
95
|
+
if (LITERAL_CONTENT_TAGS.has(tag.toLowerCase()))
|
|
96
|
+
return content;
|
|
91
97
|
return indent(content);
|
|
92
98
|
}
|
|
93
99
|
return (content.trim().startsWith('<') ? '' : '') + content + (content.trim().endsWith('>') ? '' : '');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
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']);
|
|
@@ -34,13 +35,15 @@ export async function li(node, state) {
|
|
|
34
35
|
result += rendered;
|
|
35
36
|
}
|
|
36
37
|
result = result.trim();
|
|
38
|
+
const attrs = comarkAttributes(userBlockAttrs('li', node[1]));
|
|
39
|
+
const suffix = attrs ? ` ${attrs}` : '';
|
|
37
40
|
if (!order) {
|
|
38
41
|
result = escapeLeadingNumberDot(result);
|
|
39
42
|
}
|
|
40
43
|
if (order) {
|
|
41
44
|
state.applyContext({ order: order + 1 });
|
|
42
45
|
}
|
|
43
|
-
return `${prefix}${result}\n`;
|
|
46
|
+
return `${prefix}${result}${suffix}\n`;
|
|
44
47
|
}
|
|
45
48
|
function escapeLeadingNumberDot(str) {
|
|
46
49
|
if (str.length === 0)
|
|
@@ -5,7 +5,7 @@ import { html } from "./html.js";
|
|
|
5
5
|
const INLINE_HTML_ELEMENTS = new Set(['a', 'strong', 'em', 'span']);
|
|
6
6
|
export async function mdc(node, state, parent) {
|
|
7
7
|
const [tag, attr, ...children] = node;
|
|
8
|
-
const {
|
|
8
|
+
const { $: _, ...attributes } = attr;
|
|
9
9
|
if (tag === 'table') {
|
|
10
10
|
return html(node, state);
|
|
11
11
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { indent } from "../../../utils/index.js";
|
|
2
|
+
import { comarkAttributes, userBlockAttrs } from "../attributes.js";
|
|
2
3
|
export async function ol(node, state) {
|
|
3
4
|
const children = node.slice(2);
|
|
4
5
|
const revert = state.applyContext({ list: true, order: 1, listIndent: 3 });
|
|
@@ -7,12 +8,21 @@ export async function ol(node, state) {
|
|
|
7
8
|
result += await state.one(child, state);
|
|
8
9
|
}
|
|
9
10
|
result = result.trim();
|
|
11
|
+
state.applyContext(revert);
|
|
12
|
+
// ol with user attrs round-trips via `::ol{attrs}\n1. …\n::` — the native
|
|
13
|
+
// markdown list syntax has no slot for list-level attrs.
|
|
14
|
+
const attrs = comarkAttributes(userBlockAttrs('ol', node[1]));
|
|
15
|
+
if (attrs) {
|
|
16
|
+
if (revert.list) {
|
|
17
|
+
return '\n' + indent(`::ol${attrs}\n${result}\n::`, { width: revert.listIndent || 2 });
|
|
18
|
+
}
|
|
19
|
+
return `::ol${attrs}\n${result}\n::` + state.context.blockSeparator;
|
|
20
|
+
}
|
|
10
21
|
if (revert.list) {
|
|
11
22
|
result = '\n' + indent(result, { width: revert.listIndent || 2 });
|
|
12
23
|
}
|
|
13
24
|
else {
|
|
14
25
|
result = result + state.context.blockSeparator;
|
|
15
26
|
}
|
|
16
|
-
state.applyContext(revert);
|
|
17
27
|
return result;
|
|
18
28
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { comarkAttributes } from "../attributes.js";
|
|
1
2
|
export async function p(node, state, parent) {
|
|
2
3
|
const children = node.slice(2);
|
|
3
4
|
let result = '';
|
|
4
5
|
for (const child of children) {
|
|
5
6
|
result += await state.one(child, state, node);
|
|
6
7
|
}
|
|
8
|
+
const attrs = comarkAttributes(node[1]);
|
|
9
|
+
if (attrs)
|
|
10
|
+
result = `${result.replace(/[ \t]+$/, '')} ${attrs}`;
|
|
7
11
|
if (parent?.[0] === 'li') {
|
|
8
12
|
return result;
|
|
9
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { textContent } from "../../../utils/index.js";
|
|
2
|
+
import { comarkAttributes, userBlockAttrs } from "../attributes.js";
|
|
2
3
|
export function pre(node, state) {
|
|
3
4
|
const [_, attributes, ...children] = node;
|
|
4
5
|
const codeClasses = children[0]?.[1]?.class;
|
|
@@ -13,8 +14,16 @@ export function pre(node, state) {
|
|
|
13
14
|
const highlights = attributes.highlights ? ' {' + formatHighlights(attributes.highlights) + '}' : '';
|
|
14
15
|
// Meta always has a leading space
|
|
15
16
|
const meta = attributes.meta ? ' ' + attributes.meta : '';
|
|
16
|
-
const
|
|
17
|
-
|
|
17
|
+
const code = String(node[1]?.code || textContent(node)).trim();
|
|
18
|
+
const fence = code.includes('```') ? '~~~' : '```';
|
|
19
|
+
const fenceBlock = fence + language + filename + highlights + meta + '\n' + code + '\n' + fence;
|
|
20
|
+
// Extra user attrs that can't ride on the fence info string round-trip via
|
|
21
|
+
// `::pre{attrs}\n```…```\n::` — mirrors the wrapper form for ul/ol/table/blockquote.
|
|
22
|
+
const attrs = comarkAttributes(userBlockAttrs('pre', attributes));
|
|
23
|
+
if (attrs) {
|
|
24
|
+
return `::pre${attrs}\n${fenceBlock}\n::` + state.context.blockSeparator;
|
|
25
|
+
}
|
|
26
|
+
return fenceBlock + state.context.blockSeparator;
|
|
18
27
|
}
|
|
19
28
|
function formatHighlights(highlights) {
|
|
20
29
|
if (highlights.length === 0)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { comarkAttributes, userBlockAttrs } from "../attributes.js";
|
|
1
2
|
// Helper function to extract alignment from style attribute
|
|
2
3
|
function getAlignment(attributes) {
|
|
3
4
|
const style = attributes.style;
|
|
@@ -158,6 +159,12 @@ export async function table(node, state) {
|
|
|
158
159
|
result += '| ' + cellContents.join(' | ') + ' |\n';
|
|
159
160
|
}
|
|
160
161
|
// result already ends with \n, so we only need to add one more \n
|
|
162
|
+
// table with user attrs round-trips via `::table{attrs}\n<table>\n::` —
|
|
163
|
+
// GFM table syntax has no slot for table-level attrs.
|
|
164
|
+
const attrs = comarkAttributes(userBlockAttrs('table', node[1]));
|
|
165
|
+
if (attrs) {
|
|
166
|
+
return `::table${attrs}\n${result.trimEnd()}\n::\n\n`;
|
|
167
|
+
}
|
|
161
168
|
return result + '\n';
|
|
162
169
|
}
|
|
163
170
|
export function thead(_node, _state) {
|