comark 0.3.2 → 0.5.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/README.md +25 -1
- package/dist/context.d.ts +78 -0
- package/dist/context.js +127 -0
- package/dist/devtools/bridge.d.ts +1 -0
- package/dist/devtools/bridge.js +1 -0
- package/dist/devtools/constants.d.ts +1 -0
- package/dist/devtools/constants.js +1 -0
- package/dist/devtools/renderer/dom.d.ts +1 -0
- package/dist/devtools/renderer/dom.js +1 -0
- package/dist/devtools/renderer/index.d.ts +2 -0
- package/dist/devtools/renderer/index.js +2 -0
- package/dist/devtools/renderer/output.d.ts +1 -0
- package/dist/devtools/renderer/output.js +1 -0
- package/dist/devtools/renderer/panel.d.ts +1 -0
- package/dist/devtools/renderer/panel.js +1 -0
- package/dist/devtools/renderer/styles.d.ts +1 -0
- package/dist/devtools/renderer/styles.js +1 -0
- package/dist/devtools/renderer/theme.d.ts +1 -0
- package/dist/devtools/renderer/theme.js +1 -0
- package/dist/devtools/types.d.ts +1 -0
- package/dist/devtools/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/parse/auto-close/index.js +96 -31
- 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 +8 -1
- package/dist/internal/stringify/attributes.js +53 -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 +19 -9
- package/dist/internal/stringify/handlers/mdc.js +1 -1
- package/dist/internal/stringify/handlers/ol.js +15 -2
- 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/internal/stringify/state.js +13 -1
- package/dist/parse.d.ts +4 -4
- package/dist/parse.js +7 -3
- 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 -2
- 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 +12 -1
- package/dist/plugins/security.js +13 -6
- package/dist/plugins/summary.d.ts +4 -1
- package/dist/plugins/syntax.d.ts +1 -1
- package/dist/plugins/syntax.js +95 -36
- package/dist/plugins/task-list.d.ts +1 -1
- package/dist/plugins/toc.d.ts +3 -1
- package/dist/render.d.ts +6 -2
- package/dist/render.js +2 -2
- package/dist/types.d.ts +61 -12
- package/dist/utils/helpers.d.ts +16 -4
- package/dist/utils/helpers.js +15 -3
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +30 -14
- package/package.json +6 -3
- package/skills/comark/references/rendering-svelte.md +51 -7
- 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
|
@@ -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') {
|
|
@@ -5,7 +5,7 @@ export interface ResolveAttributesOptions {
|
|
|
5
5
|
* `:` prefix is always stripped. Non-JSON strings fall back to a dot-path
|
|
6
6
|
* lookup in `renderData`; unresolved paths yield `undefined`.
|
|
7
7
|
*
|
|
8
|
-
* This matches the Vue/React/Svelte renderer semantics, which always
|
|
8
|
+
* This matches the Vue/React/Svelte/Angular renderer semantics, which always
|
|
9
9
|
* normalize bindings into real JS values suitable for typed component props.
|
|
10
10
|
*
|
|
11
11
|
* When false (default) only dot-path lookups are applied — literals and
|
|
@@ -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,59 @@ 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
|
+
ol: { drop: ['start'] },
|
|
76
|
+
ul: { classBlocklist: ['contains-task-list'] },
|
|
77
|
+
li: { classBlocklist: ['task-list-item'] },
|
|
78
|
+
// `language`/`filename`/`highlights`/`meta` ride on the fence info string.
|
|
79
|
+
// `style` comes from render-time plugins (e.g. shiki) and has no markdown
|
|
80
|
+
// form. `class` is handled specially in userBlockAttrs because shiki merges
|
|
81
|
+
// its injected classes with the user's class — we need to strip just the
|
|
82
|
+
// highlighter portion.
|
|
83
|
+
pre: { drop: ['language', 'filename', 'highlights', 'meta', 'style'] },
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Filter implicit/auto-generated attrs that are encoded by the native
|
|
87
|
+
* markdown syntax and shouldn't echo back as `{attr=...}`. Used by the
|
|
88
|
+
* block stringifiers to decide whether a node has *user* attrs that must
|
|
89
|
+
* be preserved via the `::tag{...}` wrapper form.
|
|
90
|
+
*/
|
|
91
|
+
export function userBlockAttrs(tag, attributes) {
|
|
92
|
+
const rule = IMPLICIT_ATTRS[tag];
|
|
93
|
+
if (!rule)
|
|
94
|
+
return { ...attributes };
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
97
|
+
if (rule.drop?.includes(key))
|
|
98
|
+
continue;
|
|
99
|
+
if (key === 'class' && rule.classBlocklist && typeof value === 'string') {
|
|
100
|
+
const remaining = value
|
|
101
|
+
.split(/\s+/)
|
|
102
|
+
.filter((c) => c && !rule.classBlocklist.includes(c))
|
|
103
|
+
.join(' ');
|
|
104
|
+
if (remaining)
|
|
105
|
+
result[key] = remaining;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (key === 'class' && tag === 'pre' && typeof value === 'string' && value.startsWith('shiki')) {
|
|
109
|
+
// Shiki injects `shiki [shiki-themes] <themes…>` (or a bare `shiki`) and
|
|
110
|
+
// appends any user class after a `.` separator. Recover the user portion
|
|
111
|
+
// by dropping everything up to and including that separator.
|
|
112
|
+
const tokens = value.split(/\s+/);
|
|
113
|
+
let cutoff = tokens.findIndex((t) => t === '.');
|
|
114
|
+
const userClass = cutoff >= 0 ? tokens.slice(cutoff + 1).join(' ') : '';
|
|
115
|
+
if (userClass)
|
|
116
|
+
result[key] = userClass;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
result[key] = value;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
70
123
|
/**
|
|
71
124
|
* Convert attributes to a string of Comark attributes
|
|
72
125
|
*
|
|
@@ -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']);
|
|
@@ -15,32 +16,41 @@ export async function li(node, state) {
|
|
|
15
16
|
prefix += input[1].checked || input[1][':checked'] ? '[x] ' : '[ ] ';
|
|
16
17
|
}
|
|
17
18
|
const prefixWidth = prefix.length;
|
|
19
|
+
// Direct text children render sibling components inline.
|
|
20
|
+
const hasInlineContent = children.some((child) => typeof child === 'string');
|
|
18
21
|
let result = '';
|
|
19
22
|
for (const child of children) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (blockElements.has(
|
|
23
|
-
|
|
24
|
-
const indented = indent(rendered, { width: prefixWidth });
|
|
23
|
+
if (Array.isArray(child)) {
|
|
24
|
+
const tag = child[0];
|
|
25
|
+
if (result && blockElements.has(tag)) {
|
|
26
|
+
const indented = indent(await state.one(child, state, node), { width: prefixWidth });
|
|
25
27
|
result = result.trimEnd() + '\n' + indented.trimEnd() + '\n';
|
|
26
28
|
continue;
|
|
27
29
|
}
|
|
28
|
-
if (
|
|
29
|
-
const indented = indent(
|
|
30
|
+
if (result && tag === 'p') {
|
|
31
|
+
const indented = indent(await state.one(child, state, node), { width: prefixWidth });
|
|
30
32
|
result = result.trimEnd() + '\n\n' + indented.trimEnd() + '\n';
|
|
31
33
|
continue;
|
|
32
34
|
}
|
|
35
|
+
// No parent → mdc skips its own nesting indentation, so li owns it here.
|
|
36
|
+
if (!hasInlineContent && !(tag in state.handlers)) {
|
|
37
|
+
const indented = indent(await state.one(child, state), { width: prefixWidth, ignoreFirstLine: !result });
|
|
38
|
+
result = result ? result.trimEnd() + '\n' + indented.trimEnd() + '\n' : indented.trimEnd() + '\n';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
33
41
|
}
|
|
34
|
-
result +=
|
|
42
|
+
result += await state.one(child, state, node);
|
|
35
43
|
}
|
|
36
44
|
result = result.trim();
|
|
45
|
+
const attrs = comarkAttributes(userBlockAttrs('li', node[1]));
|
|
46
|
+
const suffix = attrs ? ` ${attrs}` : '';
|
|
37
47
|
if (!order) {
|
|
38
48
|
result = escapeLeadingNumberDot(result);
|
|
39
49
|
}
|
|
40
50
|
if (order) {
|
|
41
51
|
state.applyContext({ order: order + 1 });
|
|
42
52
|
}
|
|
43
|
-
return `${prefix}${result}\n`;
|
|
53
|
+
return `${prefix}${result}${suffix}\n`;
|
|
44
54
|
}
|
|
45
55
|
function escapeLeadingNumberDot(str) {
|
|
46
56
|
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,18 +1,31 @@
|
|
|
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
|
+
// `start` is carried by the native numbering; IMPLICIT_ATTRS drops it from user attrs.
|
|
6
|
+
const start = Number(node[1].start);
|
|
7
|
+
const order = Number.isInteger(start) && start >= 1 ? start : 1;
|
|
8
|
+
const revert = state.applyContext({ list: true, order, listIndent: 3 });
|
|
5
9
|
let result = '';
|
|
6
10
|
for (const child of children) {
|
|
7
11
|
result += await state.one(child, state);
|
|
8
12
|
}
|
|
9
13
|
result = result.trim();
|
|
14
|
+
state.applyContext(revert);
|
|
15
|
+
// ol with user attrs round-trips via `::ol{attrs}\n1. …\n::` — the native
|
|
16
|
+
// markdown list syntax has no slot for list-level attrs.
|
|
17
|
+
const attrs = comarkAttributes(userBlockAttrs('ol', node[1]));
|
|
18
|
+
if (attrs) {
|
|
19
|
+
if (revert.list) {
|
|
20
|
+
return '\n' + indent(`::ol${attrs}\n${result}\n::`, { width: revert.listIndent || 2 });
|
|
21
|
+
}
|
|
22
|
+
return `::ol${attrs}\n${result}\n::` + state.context.blockSeparator;
|
|
23
|
+
}
|
|
10
24
|
if (revert.list) {
|
|
11
25
|
result = '\n' + indent(result, { width: revert.listIndent || 2 });
|
|
12
26
|
}
|
|
13
27
|
else {
|
|
14
28
|
result = result + state.context.blockSeparator;
|
|
15
29
|
}
|
|
16
|
-
state.applyContext(revert);
|
|
17
30
|
return result;
|
|
18
31
|
}
|
|
@@ -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) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { comarkAttributes } from "../attributes.js";
|
|
1
2
|
// slot template
|
|
2
3
|
export async function template(node, state, parent) {
|
|
3
4
|
const [_, attrs] = node;
|
|
@@ -10,5 +11,7 @@ export async function template(node, state, parent) {
|
|
|
10
11
|
return content + state.context.blockSeparator;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
|
-
|
|
14
|
+
const { name: _name, $: _$, ...rest } = attrs;
|
|
15
|
+
const extraAttrs = comarkAttributes(rest);
|
|
16
|
+
return `#${attrs.name}${extraAttrs}\n${content}` + state.context.blockSeparator;
|
|
14
17
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { indent } from "../../../utils/index.js";
|
|
2
|
+
import { comarkAttributes, userBlockAttrs } from "../attributes.js";
|
|
2
3
|
export async function ul(node, state) {
|
|
3
4
|
const children = node.slice(2);
|
|
4
5
|
const revert = state.applyContext({ list: true, order: false, listIndent: 2 });
|
|
@@ -7,12 +8,21 @@ export async function ul(node, state) {
|
|
|
7
8
|
result += await state.one(child, state);
|
|
8
9
|
}
|
|
9
10
|
result = result.trim();
|
|
11
|
+
state.applyContext(revert);
|
|
12
|
+
// ul with user attrs round-trips via `::ul{attrs}\n- …\n::` — the native
|
|
13
|
+
// markdown list syntax has no slot for list-level attrs.
|
|
14
|
+
const attrs = comarkAttributes(userBlockAttrs('ul', node[1]));
|
|
15
|
+
if (attrs) {
|
|
16
|
+
if (revert.list) {
|
|
17
|
+
return '\n' + indent(`::ul${attrs}\n${result}\n::`, { width: revert.listIndent || 2 });
|
|
18
|
+
}
|
|
19
|
+
return `::ul${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
|
}
|
|
@@ -25,7 +25,7 @@ export async function one(node, state, parent) {
|
|
|
25
25
|
if (state.context.html) {
|
|
26
26
|
return escapeHtml(node);
|
|
27
27
|
}
|
|
28
|
-
return node;
|
|
28
|
+
return escapeMarkdownText(node);
|
|
29
29
|
}
|
|
30
30
|
if (node[0] === null) {
|
|
31
31
|
return await state.handlers.comment(node, state);
|
|
@@ -175,3 +175,15 @@ function escapeHtml(text) {
|
|
|
175
175
|
};
|
|
176
176
|
return text.replace(/[<>]/g, (char) => map[char]);
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Escape characters in a markdown text node that would otherwise be
|
|
180
|
+
* misinterpreted as markdown syntax on a subsequent parse.
|
|
181
|
+
*
|
|
182
|
+
* `[` opens link/image syntax; `]` closes it. Both must be escaped so that
|
|
183
|
+
* a text node like `[foo](bar)` round-trips as plain text, and a text node
|
|
184
|
+
* containing `]` inside a link (e.g. `dsd]dsd`) doesn't prematurely close
|
|
185
|
+
* the surrounding `[…]` brackets.
|
|
186
|
+
*/
|
|
187
|
+
function escapeMarkdownText(text) {
|
|
188
|
+
return text.replace(/[[\]]/g, (ch) => `\\${ch}`);
|
|
189
|
+
}
|
package/dist/parse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComarkParseFn, ParseOptions, ComarkTree } from './types.ts';
|
|
1
|
+
import type { ComarkParseFn, ComarkPlugin, MergePluginFrontmatter, MergePluginMeta, ParseOptions, ResolvedFrontmatter, ResolvedMeta, ComarkTree } from './types.ts';
|
|
2
2
|
export { parseFrontmatter } from './internal/frontmatter.ts';
|
|
3
3
|
export { defineComarkPlugin } from './utils/helpers.ts';
|
|
4
4
|
/**
|
|
@@ -30,7 +30,7 @@ export { defineComarkPlugin } from './utils/helpers.ts';
|
|
|
30
30
|
* const parseNoHtml = createParse({ html: false })
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
|
-
export declare function createParse(options?: ParseOptions): ComarkParseFn
|
|
33
|
+
export declare function createParse<const TPlugins extends readonly ComarkPlugin<any, any>[] = []>(options?: ParseOptions<TPlugins>): ComarkParseFn<ResolvedMeta<MergePluginMeta<TPlugins>>, ResolvedFrontmatter<MergePluginFrontmatter<TPlugins>>>;
|
|
34
34
|
/**
|
|
35
35
|
* Parse Comark content from a string
|
|
36
36
|
*
|
|
@@ -64,7 +64,7 @@ export declare function createParse(options?: ParseOptions): ComarkParseFn;
|
|
|
64
64
|
* const tree2 = await parse(content, { autoUnwrap: false })
|
|
65
65
|
* ```
|
|
66
66
|
*/
|
|
67
|
-
export declare function parse(markdown: string, options?: ParseOptions): Promise<ComarkTree
|
|
67
|
+
export declare function parse<const TPlugins extends readonly ComarkPlugin<any, any>[] = []>(markdown: string, options?: ParseOptions<TPlugins>): Promise<ComarkTree<ResolvedMeta<MergePluginMeta<TPlugins>>, ResolvedFrontmatter<MergePluginFrontmatter<TPlugins>>>>;
|
|
68
68
|
/**
|
|
69
69
|
* Creates a serialized parser function for Comark content.
|
|
70
70
|
* This is useful for parsing large files in a streaming manner.
|
|
@@ -80,4 +80,4 @@ export declare function parse(markdown: string, options?: ParseOptions): Promise
|
|
|
80
80
|
* const tree = await parse(content)
|
|
81
81
|
* console.log(tree.nodes)
|
|
82
82
|
*/
|
|
83
|
-
export declare function createSerializedParse(options?: ParseOptions): ComarkParseFn
|
|
83
|
+
export declare function createSerializedParse<const TPlugins extends readonly ComarkPlugin<any, any>[] = []>(options?: ParseOptions<TPlugins>): ComarkParseFn<ResolvedMeta<MergePluginMeta<TPlugins>>, ResolvedFrontmatter<MergePluginFrontmatter<TPlugins>>>;
|