comark 0.0.1 → 0.1.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.
Files changed (112) hide show
  1. package/README.md +104 -0
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.js +6 -0
  4. package/dist/internal/frontmatter.d.ts +16 -0
  5. package/dist/internal/frontmatter.js +43 -0
  6. package/dist/internal/parse/auto-close/index.d.ts +12 -0
  7. package/dist/internal/parse/auto-close/index.js +457 -0
  8. package/dist/internal/parse/auto-close/table.d.ts +4 -0
  9. package/dist/internal/parse/auto-close/table.js +161 -0
  10. package/dist/internal/parse/auto-unwrap.d.ts +20 -0
  11. package/dist/internal/parse/auto-unwrap.js +42 -0
  12. package/dist/internal/parse/html/html_block_rule.d.ts +2 -0
  13. package/dist/internal/parse/html/html_block_rule.js +60 -0
  14. package/dist/internal/parse/html/html_blocks.d.ts +2 -0
  15. package/dist/internal/parse/html/html_blocks.js +66 -0
  16. package/dist/internal/parse/html/html_inline_rule.d.ts +2 -0
  17. package/dist/internal/parse/html/html_inline_rule.js +43 -0
  18. package/dist/internal/parse/html/html_re.d.ts +3 -0
  19. package/dist/internal/parse/html/html_re.js +18 -0
  20. package/dist/internal/parse/html/index.d.ts +18 -0
  21. package/dist/internal/parse/html/index.js +122 -0
  22. package/dist/internal/parse/incremental.d.ts +12 -0
  23. package/dist/internal/parse/incremental.js +39 -0
  24. package/dist/internal/parse/token-processor.d.ts +9 -0
  25. package/dist/internal/parse/token-processor.js +803 -0
  26. package/dist/internal/props-validation.d.ts +12 -0
  27. package/dist/internal/props-validation.js +112 -0
  28. package/dist/internal/stringify/attributes.d.ts +21 -0
  29. package/dist/internal/stringify/attributes.js +67 -0
  30. package/dist/internal/stringify/handlers/a.d.ts +3 -0
  31. package/dist/internal/stringify/handlers/a.js +11 -0
  32. package/dist/internal/stringify/handlers/blockquote.d.ts +3 -0
  33. package/dist/internal/stringify/handlers/blockquote.js +18 -0
  34. package/dist/internal/stringify/handlers/br.d.ts +3 -0
  35. package/dist/internal/stringify/handlers/br.js +3 -0
  36. package/dist/internal/stringify/handlers/code.d.ts +3 -0
  37. package/dist/internal/stringify/handlers/code.js +11 -0
  38. package/dist/internal/stringify/handlers/comment.d.ts +3 -0
  39. package/dist/internal/stringify/handlers/comment.js +6 -0
  40. package/dist/internal/stringify/handlers/del.d.ts +3 -0
  41. package/dist/internal/stringify/handlers/del.js +4 -0
  42. package/dist/internal/stringify/handlers/emphesis.d.ts +3 -0
  43. package/dist/internal/stringify/handlers/emphesis.js +13 -0
  44. package/dist/internal/stringify/handlers/heading.d.ts +3 -0
  45. package/dist/internal/stringify/handlers/heading.js +7 -0
  46. package/dist/internal/stringify/handlers/hr.d.ts +3 -0
  47. package/dist/internal/stringify/handlers/hr.js +3 -0
  48. package/dist/internal/stringify/handlers/html.d.ts +3 -0
  49. package/dist/internal/stringify/handlers/html.js +73 -0
  50. package/dist/internal/stringify/handlers/img.d.ts +3 -0
  51. package/dist/internal/stringify/handlers/img.js +9 -0
  52. package/dist/internal/stringify/handlers/index.d.ts +2 -0
  53. package/dist/internal/stringify/handlers/index.js +56 -0
  54. package/dist/internal/stringify/handlers/li.d.ts +3 -0
  55. package/dist/internal/stringify/handlers/li.js +43 -0
  56. package/dist/internal/stringify/handlers/math.d.ts +3 -0
  57. package/dist/internal/stringify/handlers/math.js +8 -0
  58. package/dist/internal/stringify/handlers/mdc.d.ts +3 -0
  59. package/dist/internal/stringify/handlers/mdc.js +47 -0
  60. package/dist/internal/stringify/handlers/mermaid.d.ts +3 -0
  61. package/dist/internal/stringify/handlers/mermaid.js +8 -0
  62. package/dist/internal/stringify/handlers/ol.d.ts +3 -0
  63. package/dist/internal/stringify/handlers/ol.js +18 -0
  64. package/dist/internal/stringify/handlers/p.d.ts +3 -0
  65. package/dist/internal/stringify/handlers/p.js +8 -0
  66. package/dist/internal/stringify/handlers/pre.d.ts +3 -0
  67. package/dist/internal/stringify/handlers/pre.js +60 -0
  68. package/dist/internal/stringify/handlers/strong.d.ts +3 -0
  69. package/dist/internal/stringify/handlers/strong.js +13 -0
  70. package/dist/internal/stringify/handlers/table.d.ts +8 -0
  71. package/dist/internal/stringify/handlers/table.js +180 -0
  72. package/dist/internal/stringify/handlers/template.d.ts +3 -0
  73. package/dist/internal/stringify/handlers/template.js +14 -0
  74. package/dist/internal/stringify/handlers/ul.d.ts +3 -0
  75. package/dist/internal/stringify/handlers/ul.js +18 -0
  76. package/dist/internal/stringify/indent.d.ts +4 -0
  77. package/dist/internal/stringify/indent.js +8 -0
  78. package/dist/internal/stringify/state.d.ts +13 -0
  79. package/dist/internal/stringify/state.js +121 -0
  80. package/dist/internal/yaml.d.ts +12 -0
  81. package/dist/internal/yaml.js +51 -0
  82. package/dist/parse.d.ts +66 -0
  83. package/dist/parse.js +163 -0
  84. package/dist/plugins/alert.d.ts +2 -0
  85. package/dist/plugins/alert.js +66 -0
  86. package/dist/plugins/emoji.d.ts +3 -0
  87. package/dist/plugins/emoji.js +438 -0
  88. package/dist/plugins/headings.d.ts +48 -0
  89. package/dist/plugins/headings.js +85 -0
  90. package/dist/plugins/highlight.d.ts +63 -0
  91. package/dist/plugins/highlight.js +235 -0
  92. package/dist/plugins/math.d.ts +59 -0
  93. package/dist/plugins/math.js +263 -0
  94. package/dist/plugins/mermaid.d.ts +38 -0
  95. package/dist/plugins/mermaid.js +185 -0
  96. package/dist/plugins/security.d.ts +11 -0
  97. package/dist/plugins/security.js +32 -0
  98. package/dist/plugins/summary.d.ts +2 -0
  99. package/dist/plugins/summary.js +22 -0
  100. package/dist/plugins/task-list.d.ts +8 -0
  101. package/dist/plugins/task-list.js +117 -0
  102. package/dist/plugins/toc.d.ts +15 -0
  103. package/dist/plugins/toc.js +118 -0
  104. package/dist/render.d.ts +18 -0
  105. package/dist/render.js +29 -0
  106. package/dist/types.d.ts +258 -0
  107. package/dist/types.js +1 -0
  108. package/dist/utils/caret.d.ts +7 -0
  109. package/dist/utils/caret.js +36 -0
  110. package/dist/utils/index.d.ts +38 -0
  111. package/dist/utils/index.js +149 -0
  112. package/package.json +73 -9
@@ -0,0 +1,803 @@
1
+ import { htmlToComarkNodes, parseInlineHtmlTag } from "./html/index.js";
2
+ // Mapping from token types to tag names
3
+ const BLOCK_TAG_MAP = {
4
+ blockquote_open: 'blockquote',
5
+ ordered_list_open: 'ol',
6
+ bullet_list_open: 'ul',
7
+ list_item_open: 'li',
8
+ paragraph_open: 'p',
9
+ table_open: 'table',
10
+ thead_open: 'thead',
11
+ tbody_open: 'tbody',
12
+ tr_open: 'tr',
13
+ th_open: 'th',
14
+ td_open: 'td',
15
+ };
16
+ const INLINE_TAG_MAP = {
17
+ strong_open: 'strong',
18
+ em_open: 'em',
19
+ s_open: 'del',
20
+ sub_open: 'del',
21
+ };
22
+ // ─── main entry point ───────────────────────────────────────────────────────
23
+ /**
24
+ * Convert Markdown-It tokens to a Comark tree
25
+ */
26
+ export function marmdownItTokensToComarkTree(tokens, options = { startLine: 0, preservePositions: false }) {
27
+ const nodes = [];
28
+ let i = 0;
29
+ let endLine = options.startLine;
30
+ while (i < tokens.length) {
31
+ const result = processBlockToken(tokens, i, false);
32
+ if (result.node) {
33
+ if (options.preservePositions) {
34
+ for (let j = i; j < result.nextIndex; j++) {
35
+ if (tokens[j].map && tokens[j].map[1]) {
36
+ endLine = tokens[j].map[1] + options.startLine;
37
+ }
38
+ }
39
+ if (!result.node[1].$) {
40
+ result.node[1].$ = {};
41
+ }
42
+ ;
43
+ result.node[1].$.line = endLine;
44
+ }
45
+ nodes.push(result.node);
46
+ }
47
+ i = result.nextIndex;
48
+ }
49
+ return nodes;
50
+ }
51
+ /**
52
+ * Extract and process attributes from a token's attrs array
53
+ */
54
+ function processAttributes(attrsArray, options = {}) {
55
+ const { handleBoolean = true, handleJSON = true, filterEmpty = false } = options;
56
+ const attrs = {};
57
+ if (!attrsArray || !Array.isArray(attrsArray)) {
58
+ return attrs;
59
+ }
60
+ for (const attr of attrsArray) {
61
+ if (Array.isArray(attr) && attr.length >= 2) {
62
+ const [key] = attr;
63
+ let value = attr[1];
64
+ // Filter empty values if requested
65
+ if (filterEmpty && (value === '' || value === null || value === undefined)) {
66
+ continue;
67
+ }
68
+ // Handle boolean attributes: {bool} -> {":bool": "true"}
69
+ if (handleBoolean && !key.startsWith(':') && !key.startsWith('#') && !key.startsWith('.') && (!value || value === 'true' || value === '')) {
70
+ attrs[`:${key}`] = 'true';
71
+ continue;
72
+ }
73
+ // Handle JSON values
74
+ if (handleJSON && typeof value === 'string') {
75
+ if (value.startsWith('{') && value.endsWith('}')) {
76
+ try {
77
+ value = JSON.parse(value);
78
+ }
79
+ catch {
80
+ // Keep original value if parsing fails
81
+ }
82
+ }
83
+ else if (value.startsWith('[') && value.endsWith(']')) {
84
+ try {
85
+ value = JSON.parse(value);
86
+ }
87
+ catch {
88
+ // Keep original value if parsing fails
89
+ }
90
+ }
91
+ }
92
+ // Handle class attribute (multiple classes)
93
+ if (key === 'class' && typeof attrs[key] === 'string') {
94
+ attrs[key] = `${attrs[key]} ${value}`;
95
+ }
96
+ else {
97
+ attrs[key] = value;
98
+ }
99
+ }
100
+ }
101
+ return attrs;
102
+ }
103
+ /**
104
+ * Parse codeblock info string to extract language, highlights, filename, and meta
105
+ * Example: "javascript {1-3} [filename.ts] meta=value"
106
+ * Example: "typescript[filename]{1,3-5}meta"
107
+ */
108
+ function parseCodeblockInfo(info) {
109
+ if (!info) {
110
+ return { language: '' };
111
+ }
112
+ const result = { language: '' };
113
+ let remaining = info.trim();
114
+ // Extract language (stops at [ or { or whitespace)
115
+ const languageMatch = remaining.match(/^([^\s[{]+)/);
116
+ if (languageMatch) {
117
+ result.language = languageMatch[1];
118
+ remaining = remaining.slice(languageMatch[1].length).trim();
119
+ }
120
+ // Extract highlights and filename in any order
121
+ // They can appear as: {highlights} [filename] or [filename] {highlights}
122
+ while (remaining && (remaining.startsWith('{') || remaining.startsWith('['))) {
123
+ if (remaining.startsWith('{')) {
124
+ // Extract highlights {1-3} or {1,2,3} or {1-3,5,9-11}
125
+ const highlightsMatch = remaining.match(/^\{([^}]+)\}/);
126
+ if (highlightsMatch) {
127
+ const highlightsStr = highlightsMatch[1];
128
+ remaining = remaining.slice(highlightsMatch[0].length).trim();
129
+ // Parse highlight ranges and individual numbers
130
+ const highlights = [];
131
+ const parts = highlightsStr.split(',');
132
+ for (const part of parts) {
133
+ const trimmed = part.trim();
134
+ if (trimmed.includes('-')) {
135
+ // Range like "1-3"
136
+ const [start, end] = trimmed.split('-').map(s => Number.parseInt(s.trim(), 10));
137
+ if (!Number.isNaN(start) && !Number.isNaN(end)) {
138
+ for (let i = start; i <= end; i++) {
139
+ highlights.push(i);
140
+ }
141
+ }
142
+ }
143
+ else {
144
+ // Single number
145
+ const num = Number.parseInt(trimmed, 10);
146
+ if (!Number.isNaN(num)) {
147
+ highlights.push(num);
148
+ }
149
+ }
150
+ }
151
+ if (highlights.length > 0) {
152
+ result.highlights = highlights;
153
+ }
154
+ }
155
+ else {
156
+ break;
157
+ }
158
+ }
159
+ else if (remaining.startsWith('[')) {
160
+ // Extract filename [filename.ts] - handle nested brackets and escaped backslashes
161
+ let depth = 0;
162
+ let i = 0;
163
+ for (; i < remaining.length; i++) {
164
+ if (remaining[i] === '[') {
165
+ depth++;
166
+ }
167
+ else if (remaining[i] === ']') {
168
+ depth--;
169
+ if (depth === 0) {
170
+ // Found the closing bracket
171
+ const filename = remaining.slice(1, i);
172
+ // Unescape backslashes: @[...slug\\\\].ts -> @[...slug].ts
173
+ result.filename = filename.replace(/\\\\/g, '');
174
+ remaining = remaining.slice(i + 1).trim();
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ if (depth !== 0) {
180
+ // Unclosed bracket, stop processing
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ // Remaining text is meta
186
+ if (remaining) {
187
+ result.meta = remaining;
188
+ }
189
+ return result;
190
+ }
191
+ /**
192
+ * Extract Comark attributes from mdc_inline_props token
193
+ */
194
+ function extractAttributes(tokens, startIndex, skipEmptyText = true) {
195
+ let propsIndex = startIndex;
196
+ // Skip empty text tokens if requested
197
+ if (skipEmptyText) {
198
+ while (propsIndex < tokens.length && tokens[propsIndex].type === 'text' && !tokens[propsIndex].content?.trim()) {
199
+ propsIndex++;
200
+ }
201
+ }
202
+ // Check for props token
203
+ if (propsIndex < tokens.length && tokens[propsIndex].type === 'mdc_inline_props') {
204
+ const propsToken = tokens[propsIndex];
205
+ const attrs = processAttributes(propsToken.attrs);
206
+ return { attrs, nextIndex: propsIndex + 1 };
207
+ }
208
+ return { attrs: {}, nextIndex: startIndex };
209
+ }
210
+ function processBlockToken(tokens, startIndex, insideNestedContext = false) {
211
+ const token = tokens[startIndex];
212
+ if (token.type === 'hr') {
213
+ return { node: ['hr', {}], nextIndex: startIndex + 1 };
214
+ }
215
+ // html_block is now handled upstream (in marmdownItTokensToComarkTree /
216
+ // processBlockChildren / processBlockChildrenWithSlots) before reaching here.
217
+ // This branch is kept as a safety fallback.
218
+ if (token.type === 'html_block') {
219
+ const content = token.content?.trim() || '';
220
+ if (content.startsWith('<!--')) {
221
+ const inner = content.endsWith('-->') ? content.slice(4, -3) : content.slice(4);
222
+ return { node: [null, {}, inner], nextIndex: startIndex + 1 };
223
+ }
224
+ const children = processBlockChildren(tokens, startIndex + 1, 'html_block_close', false, false, false);
225
+ const [node1] = htmlToComarkNodes(content);
226
+ if (!node1) {
227
+ return { node: null, nextIndex: startIndex + 1 };
228
+ }
229
+ const node = [node1[0], node1[1], ...children.nodes];
230
+ return { node, nextIndex: children.nextIndex + 1 };
231
+ }
232
+ // Handle Comark block components (e.g., ::component ... ::)
233
+ if (token.type === 'mdc_block_open') {
234
+ const componentName = token.tag || 'component';
235
+ const attrs = processAttributes(token.attrs);
236
+ // Process children until mdc_block_close, handling slots (#slotname)
237
+ const children = processBlockChildrenWithSlots(tokens, startIndex + 1, 'mdc_block_close');
238
+ // Return the component even if it has no children (empty component like ::component\n::)
239
+ return { node: [componentName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
240
+ }
241
+ // Handle Comark block shorthand components (e.g., standalone :inline-component, ::inline-component[content])
242
+ // These should be wrapped in a paragraph
243
+ if (token.type === 'mdc_block_shorthand') {
244
+ let nextIndex = startIndex + 1;
245
+ const componentName = token.tag || 'component';
246
+ const attrs = processAttributes(token.attrs, { handleJSON: false });
247
+ const children = [];
248
+ // Opening tag with content - process children until closing tag
249
+ if (token.nesting === 1) {
250
+ while (nextIndex < tokens.length) {
251
+ const childToken = tokens[nextIndex];
252
+ nextIndex++;
253
+ // Check for closing tag
254
+ if (childToken.type === 'mdc_block_shorthand' && childToken.nesting === -1) {
255
+ break;
256
+ }
257
+ // Process inline token
258
+ if (childToken.type === 'inline') {
259
+ const inlineNodes = processInlineTokens(childToken.children || [], false);
260
+ children.push(...inlineNodes);
261
+ }
262
+ }
263
+ }
264
+ return { node: [componentName, attrs, ...children], nextIndex: nextIndex };
265
+ }
266
+ if (token.type === 'math_block') {
267
+ return { node: ['math', { class: 'math block', content: token.content }, token.content], nextIndex: startIndex + 1 };
268
+ }
269
+ if (token.type === 'fence' || token.type === 'fenced_code_block' || token.type === 'code_block') {
270
+ const content = token.content || '';
271
+ const info = token.info || token.params || '';
272
+ // Parse the info string
273
+ const parsed = parseCodeblockInfo(info);
274
+ // Build pre attributes
275
+ const preAttrs = {};
276
+ if (parsed.language && parsed.language.trim()) {
277
+ preAttrs.language = parsed.language;
278
+ }
279
+ if (parsed.filename) {
280
+ preAttrs.filename = parsed.filename;
281
+ }
282
+ if (parsed.highlights) {
283
+ preAttrs.highlights = parsed.highlights;
284
+ }
285
+ if (parsed.meta) {
286
+ preAttrs.meta = parsed.meta;
287
+ }
288
+ // Build code attributes
289
+ const codeAttrs = {};
290
+ if (parsed.language && parsed.language.trim()) {
291
+ codeAttrs['class'] = `language-${parsed.language}`;
292
+ }
293
+ const codeContentWithoutLastNewline = content.endsWith('\n') ? content.slice(0, -1) : content;
294
+ const code = ['code', codeAttrs, codeContentWithoutLastNewline];
295
+ const pre = ['pre', preAttrs, code];
296
+ return { node: pre, nextIndex: startIndex + 1 };
297
+ }
298
+ if (token.type === 'heading_open') {
299
+ const level = token.tag.replace('h', '');
300
+ const headingTag = `h${level}`;
301
+ // Process heading children with inHeading flag for Comark component handling
302
+ const children = processBlockChildren(tokens, startIndex + 1, 'heading_close', true, true, insideNestedContext);
303
+ if (children.nodes.length > 0) {
304
+ // Always generate ID for all headings, no exceptions
305
+ const textContent = extractTextContent(children.nodes);
306
+ const headingId = slugify(textContent);
307
+ // Always attach ID to the heading element itself
308
+ return { node: [headingTag, { id: headingId }, ...children.nodes], nextIndex: children.nextIndex + 1 };
309
+ }
310
+ return { node: null, nextIndex: children.nextIndex + 1 };
311
+ }
312
+ // Handle list items - paragraphs should be unwrapped
313
+ if (token.type === 'list_item_open') {
314
+ const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
315
+ const children = processBlockChildren(tokens, startIndex + 1, 'list_item_close', false, false, true);
316
+ // Unwrap paragraphs in list items
317
+ const unwrapped = [];
318
+ for (const child of children.nodes) {
319
+ if (Array.isArray(child) && child[0] === 'p') {
320
+ // Unwrap paragraph, add its children directly
321
+ unwrapped.push(...child.slice(2));
322
+ }
323
+ else {
324
+ unwrapped.push(child);
325
+ }
326
+ }
327
+ if (unwrapped.length > 0) {
328
+ return { node: ['li', attrs, ...unwrapped], nextIndex: children.nextIndex + 1 };
329
+ }
330
+ return { node: null, nextIndex: children.nextIndex + 1 };
331
+ }
332
+ // Handle generic block-level open/close pairs (includes blockquote, lists, tables, etc.)
333
+ const tagName = BLOCK_TAG_MAP[token.type];
334
+ if (tagName) {
335
+ const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
336
+ const closeType = token.type.replace('_open', '_close');
337
+ // Special handling for blockquotes
338
+ if (tagName === 'blockquote') {
339
+ // First pass: get children
340
+ const children = processBlockChildren(tokens, startIndex + 1, closeType, false, false, false);
341
+ // Rule: If a heading is the FIRST child AND there are additional children after it,
342
+ // then the heading should NOT have an ID. Otherwise, headings should have IDs.
343
+ if (children.nodes.length > 1) {
344
+ const firstChild = children.nodes[0];
345
+ // Check if first child is a heading (h1-h6)
346
+ const isHeading = Array.isArray(firstChild)
347
+ && typeof firstChild[0] === 'string'
348
+ && /^h[1-6]$/.test(firstChild[0]);
349
+ if (isHeading) {
350
+ // Heading is first child with more siblings - reprocess without IDs
351
+ const childrenNoIds = processBlockChildren(tokens, startIndex + 1, closeType, false, false, true);
352
+ if (childrenNoIds.nodes.length > 0) {
353
+ return { node: [tagName, attrs, ...childrenNoIds.nodes], nextIndex: childrenNoIds.nextIndex + 1 };
354
+ }
355
+ return { node: null, nextIndex: childrenNoIds.nextIndex + 1 };
356
+ }
357
+ }
358
+ // All other cases: use original processing (allows IDs)
359
+ if (children.nodes.length > 0) {
360
+ return { node: [tagName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
361
+ }
362
+ return { node: null, nextIndex: children.nextIndex + 1 };
363
+ }
364
+ // For other elements (tables, etc.)
365
+ const isNestedContext = ['td', 'th'].includes(tagName);
366
+ const children = processBlockChildren(tokens, startIndex + 1, closeType, false, false, isNestedContext);
367
+ if (children.nodes.length > 0) {
368
+ return { node: [tagName, attrs, ...children.nodes], nextIndex: children.nextIndex + 1 };
369
+ }
370
+ return { node: null, nextIndex: children.nextIndex + 1 };
371
+ }
372
+ const componentName = token.tag || 'component';
373
+ const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
374
+ return { node: [componentName, attrs], nextIndex: startIndex + 1 };
375
+ }
376
+ function processBlockChildrenWithSlots(tokens, startIndex, closeType) {
377
+ const nodes = [];
378
+ let i = startIndex;
379
+ let currentSlotName = null;
380
+ let currentSlotChildren = [];
381
+ while (i < tokens.length && tokens[i].type !== closeType) {
382
+ const token = tokens[i];
383
+ // html_block can produce multiple nodes — handle before processBlockToken
384
+ if (token.type === 'html_block') {
385
+ const htmlNodes = htmlToComarkNodes(token.content);
386
+ if (currentSlotName !== null) {
387
+ currentSlotChildren.push(...htmlNodes);
388
+ }
389
+ else {
390
+ nodes.push(...htmlNodes);
391
+ }
392
+ i++;
393
+ continue;
394
+ }
395
+ // Check for slot marker: #slotname creates mdc_block_slot tokens
396
+ if (token.type === 'mdc_block_slot') {
397
+ // Extract slot name from token.attrs
398
+ // The attrs array contains [["#slotname", ""]] for open, and null/empty for close
399
+ if (token.attrs && Array.isArray(token.attrs) && token.attrs.length > 0) {
400
+ const firstAttr = token.attrs[0];
401
+ if (Array.isArray(firstAttr) && firstAttr.length > 0) {
402
+ const slotKey = firstAttr[0];
403
+ // Remove the # prefix to get the slot name
404
+ if (slotKey.startsWith('#')) {
405
+ const slotName = slotKey.substring(1);
406
+ // Save previous slot if any
407
+ if (currentSlotName !== null && currentSlotChildren.length > 0) {
408
+ nodes.push(['template', { name: currentSlotName }, ...currentSlotChildren]);
409
+ currentSlotChildren = [];
410
+ }
411
+ currentSlotName = slotName;
412
+ i++;
413
+ continue;
414
+ }
415
+ }
416
+ }
417
+ // If attrs is null/empty, this is a slot close token - just skip it
418
+ i++;
419
+ continue;
420
+ }
421
+ // Process other block tokens
422
+ // Comark components are not nested contexts - headings inside them should get IDs
423
+ const result = processBlockToken(tokens, i, false);
424
+ i = result.nextIndex;
425
+ if (result.node) {
426
+ if (currentSlotName !== null) {
427
+ // Add to current slot
428
+ currentSlotChildren.push(result.node);
429
+ }
430
+ else {
431
+ // Add directly to component
432
+ nodes.push(result.node);
433
+ }
434
+ }
435
+ }
436
+ // Save last slot if any
437
+ if (currentSlotName !== null && currentSlotChildren.length > 0) {
438
+ nodes.push(['template', { name: currentSlotName }, ...currentSlotChildren]);
439
+ }
440
+ return { nodes, nextIndex: i };
441
+ }
442
+ function processBlockChildren(tokens, startIndex, closeType, inlineOnly, inHeading = false, insideNestedContext = false) {
443
+ const nodes = [];
444
+ let i = startIndex;
445
+ while (i < tokens.length && tokens[i].type !== closeType) {
446
+ const token = tokens[i];
447
+ // html_block can produce multiple nodes — handle before processBlockToken
448
+ if (token.type === 'html_block') {
449
+ nodes.push(...htmlToComarkNodes(token.content));
450
+ i++;
451
+ continue;
452
+ }
453
+ if (token.type === 'inline') {
454
+ const inlineNodes = processInlineTokens(token.children || [], inHeading);
455
+ nodes.push(...inlineNodes);
456
+ i++;
457
+ }
458
+ else if (token.type === 'hardbreak' || token.type === 'hard_break') {
459
+ nodes.push(['br', {}]);
460
+ i++;
461
+ }
462
+ else if (token.type === 'softbreak') {
463
+ // Soft breaks are preserved as newlines in the text content
464
+ nodes.push('\n');
465
+ i++;
466
+ }
467
+ else if (inlineOnly && (token.type === 'text' || token.type === 'code_inline')) {
468
+ if (token.content) {
469
+ nodes.push(token.content);
470
+ }
471
+ i++;
472
+ }
473
+ else {
474
+ const result = processBlockToken(tokens, i, insideNestedContext);
475
+ i = result.nextIndex;
476
+ if (result.node) {
477
+ nodes.push(result.node);
478
+ }
479
+ }
480
+ }
481
+ // Merge adjacent text nodes
482
+ return { nodes: mergeAdjacentTextNodes(nodes), nextIndex: i };
483
+ }
484
+ /**
485
+ * Merge adjacent string nodes in an array of nodes
486
+ */
487
+ function mergeAdjacentTextNodes(nodes) {
488
+ const merged = [];
489
+ for (const node of nodes) {
490
+ const lastNode = merged[merged.length - 1];
491
+ // If both current and last nodes are strings, merge them
492
+ if (typeof node === 'string' && typeof lastNode === 'string') {
493
+ merged[merged.length - 1] = lastNode + node;
494
+ }
495
+ else {
496
+ merged.push(node);
497
+ }
498
+ }
499
+ return merged;
500
+ }
501
+ /**
502
+ * Extract text content from nodes for heading ID generation
503
+ */
504
+ function extractTextContent(nodes) {
505
+ let text = '';
506
+ for (const node of nodes) {
507
+ if (typeof node === 'string') {
508
+ text += node;
509
+ }
510
+ else if (Array.isArray(node)) {
511
+ // For array nodes (elements), include the tag name (for inline components)
512
+ const tag = node[0];
513
+ const children = node.slice(2);
514
+ // Skip 'br' and 'html_inline' tags
515
+ if (tag === 'br' || tag === 'html_inline') {
516
+ continue;
517
+ }
518
+ // Include the tag name (e.g., "inline" from :inline component)
519
+ text += ' ' + tag + ' ';
520
+ // Also include any text from children
521
+ if (children.length > 0) {
522
+ text += extractTextContent(children);
523
+ }
524
+ }
525
+ }
526
+ return text;
527
+ }
528
+ /**
529
+ * Convert text to a slug for heading IDs
530
+ * Example: "Hello World" -> "hello-world"
531
+ * Example: "1. Introduction" -> "_1-introduction"
532
+ */
533
+ function slugify(text) {
534
+ let slug = text
535
+ .toLowerCase()
536
+ .trim()
537
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
538
+ .replace(/[^\w-]+/g, '') // Remove non-word chars (except hyphens)
539
+ .replace(/-{2,}/g, '-') // Replace multiple hyphens with single hyphen
540
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
541
+ // Prefix with underscore if starts with a digit (HTML IDs can't start with numbers)
542
+ if (/^\d/.test(slug)) {
543
+ slug = '_' + slug;
544
+ }
545
+ return slug;
546
+ }
547
+ export function processInlineTokens(tokens, inHeading = false) {
548
+ const nodes = [];
549
+ let i = 0;
550
+ while (i < tokens.length) {
551
+ const token = tokens[i];
552
+ // Skip hidden mdc_inline_props tokens (they're handled by the parent element)
553
+ // These appear after elements like **strong**{attr} and should be attached to the parent
554
+ if (token.type === 'mdc_inline_props' && token.hidden) {
555
+ // Props tokens are handled by the parent element that processes them
556
+ // We should not process them here as separate nodes
557
+ i++;
558
+ continue;
559
+ }
560
+ const result = processInlineToken(tokens, i, inHeading);
561
+ i = result.nextIndex;
562
+ if (result.node) {
563
+ nodes.push(result.node);
564
+ }
565
+ }
566
+ // Merge adjacent text nodes (e.g., "text" + "\n" + "text" → "text\ntext")
567
+ return mergeAdjacentTextNodes(nodes);
568
+ }
569
+ function processInlineToken(tokens, startIndex, inHeading = false) {
570
+ const token = tokens[startIndex];
571
+ if (token.type === 'text') {
572
+ return { node: token.content || null, nextIndex: startIndex + 1 };
573
+ }
574
+ // Handle emoji tokens (e.g., :rocket: -> 🚀)
575
+ if (token.type === 'emoji') {
576
+ return { node: token.content || null, nextIndex: startIndex + 1 };
577
+ }
578
+ // Handle html_inline tokens using htmlparser2
579
+ if (token.type === 'html_inline') {
580
+ const content = token.content || '';
581
+ const tagInfo = parseInlineHtmlTag(content);
582
+ if (!tagInfo) {
583
+ // Not a recognisable tag — return as raw text
584
+ return { node: content || null, nextIndex: startIndex + 1 };
585
+ }
586
+ if (tagInfo.isClose) {
587
+ // Orphaned closing tag — skip (handled by the opener's lookahead)
588
+ return { node: null, nextIndex: startIndex + 1 };
589
+ }
590
+ if (tagInfo.isVoid) {
591
+ // Self-closing void element: <br>, <img>, <input>, …
592
+ return { node: [tagInfo.tag, tagInfo.attrs], nextIndex: startIndex + 1 };
593
+ }
594
+ // Non-void opening tag — look ahead for the matching closing tag
595
+ const children = [];
596
+ let j = startIndex + 1;
597
+ while (j < tokens.length) {
598
+ const nextToken = tokens[j];
599
+ if (nextToken.type === 'html_inline') {
600
+ const nextInfo = parseInlineHtmlTag(nextToken.content || '');
601
+ if (nextInfo?.isClose && nextInfo.tag === tagInfo.tag) {
602
+ j++; // consume the closing tag
603
+ break;
604
+ }
605
+ }
606
+ const result = processInlineToken(tokens, j, inHeading);
607
+ j = result.nextIndex;
608
+ if (result.node) {
609
+ children.push(result.node);
610
+ }
611
+ }
612
+ const node = children.length > 0
613
+ ? [tagInfo.tag, tagInfo.attrs, ...children]
614
+ : [tagInfo.tag, tagInfo.attrs];
615
+ return { node, nextIndex: j };
616
+ }
617
+ // Handle Comark inline span (e.g., [text]{attr})
618
+ // @comark/markdown-it creates mdc_inline_span tokens, and props appear AFTER the close token
619
+ if (token.type === 'mdc_inline_span' && token.nesting === 1) {
620
+ const attrs = {};
621
+ let i = startIndex + 1;
622
+ const nodes = [];
623
+ // Process children until span close
624
+ while (i < tokens.length) {
625
+ const childToken = tokens[i];
626
+ // Check for span close
627
+ if (childToken.type === 'mdc_inline_span' && childToken.nesting === -1) {
628
+ break;
629
+ }
630
+ // Skip empty text tokens
631
+ if (childToken.type === 'text' && !childToken.content?.trim()) {
632
+ i++;
633
+ continue;
634
+ }
635
+ // Process other tokens
636
+ const result = processInlineToken(tokens, i, inHeading);
637
+ i = result.nextIndex;
638
+ if (result.node) {
639
+ nodes.push(result.node);
640
+ }
641
+ }
642
+ // Skip the close token and check for props token after it
643
+ const { attrs: spanAttrs, nextIndex } = extractAttributes(tokens, i + 1);
644
+ Object.assign(attrs, spanAttrs);
645
+ if (nodes.length > 0 || Object.keys(attrs).length > 0) {
646
+ return { node: ['span', attrs, ...nodes], nextIndex };
647
+ }
648
+ return { node: null, nextIndex };
649
+ }
650
+ // Skip mdc_inline_span close tokens
651
+ if (token.type === 'mdc_inline_span' && token.nesting === -1) {
652
+ return { node: null, nextIndex: startIndex + 1 };
653
+ }
654
+ if (token.type === 'code_inline') {
655
+ const { attrs, nextIndex } = extractAttributes(tokens, startIndex + 1);
656
+ if (token.content) {
657
+ return { node: ['code', attrs, token.content], nextIndex };
658
+ }
659
+ return { node: null, nextIndex };
660
+ }
661
+ if (token.type === 'hardbreak' || token.type === 'hard_break') {
662
+ return { node: ['br', {}], nextIndex: startIndex + 1 };
663
+ }
664
+ if (token.type === 'softbreak') {
665
+ // Soft breaks are preserved as newlines in the text content
666
+ return { node: '\n', nextIndex: startIndex + 1 };
667
+ }
668
+ // Handle Comark inline components (e.g., :inline-component or :component[text]{attrs})
669
+ if (token.type === 'mdc_inline_component') {
670
+ const componentName = token.tag || 'component';
671
+ // Check if this is an opening tag (has children) or a self-closing tag
672
+ if (token.nesting === 1) {
673
+ // Opening tag - process children until closing tag
674
+ const children = [];
675
+ let i = startIndex + 1;
676
+ while (i < tokens.length) {
677
+ const childToken = tokens[i];
678
+ // Check for closing tag
679
+ if (childToken.type === 'mdc_inline_component' && childToken.nesting === -1) {
680
+ // Found closing tag, now check for props after it
681
+ const { attrs, nextIndex } = extractAttributes(tokens, i + 1, false);
682
+ return { node: [componentName, attrs, ...children], nextIndex };
683
+ }
684
+ // Process child token
685
+ const result = processInlineToken(tokens, i, inHeading);
686
+ i = result.nextIndex;
687
+ if (result.node) {
688
+ children.push(result.node);
689
+ }
690
+ }
691
+ // No closing tag found, return what we have
692
+ return { node: [componentName, {}, ...children], nextIndex: i };
693
+ }
694
+ else if (token.nesting === -1) {
695
+ // Closing tag - should be handled by the opening tag processing
696
+ return { node: null, nextIndex: startIndex + 1 };
697
+ }
698
+ else {
699
+ // Self-closing component (nesting === 0)
700
+ const attrs = {};
701
+ // @comark/markdown-it stores attributes in a separate mdc_inline_props token
702
+ // that appears right after the component token
703
+ const { attrs: componentAttrs, nextIndex: propsNextIndex } = extractAttributes(tokens, startIndex + 1, false);
704
+ Object.assign(attrs, componentAttrs);
705
+ // Extract attributes from token.attrs (fallback, though @comark/markdown-it uses mdc_inline_props)
706
+ const fallbackAttrs = processAttributes(token.attrs, { handleBoolean: false });
707
+ Object.assign(attrs, fallbackAttrs);
708
+ // Return the component without any text children
709
+ // Text after the component will be processed as siblings by processInlineChildren
710
+ const nextIndex = Object.keys(componentAttrs).length > 0 ? propsNextIndex : startIndex + 1;
711
+ return { node: [componentName, attrs], nextIndex };
712
+ }
713
+ }
714
+ if (token.type === 'image') {
715
+ const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false, filterEmpty: true });
716
+ // Override alt with token.content if available
717
+ if (token.content) {
718
+ attrs.alt = token.content;
719
+ }
720
+ // Check if there's a props token right after the image token
721
+ const { attrs: imageAttrs, nextIndex } = extractAttributes(tokens, startIndex + 1);
722
+ Object.assign(attrs, imageAttrs);
723
+ return { node: ['img', attrs], nextIndex };
724
+ }
725
+ if (token.type === 'link_open') {
726
+ const attrs = processAttributes(token.attrs, { handleBoolean: false, handleJSON: false });
727
+ const children = processInlineChildren(tokens, startIndex + 1, 'link_close', inHeading);
728
+ // Check if there's a props token right after the link_close token
729
+ const { attrs: linkAttrs, nextIndex } = extractAttributes(tokens, children.nextIndex + 1);
730
+ Object.assign(attrs, linkAttrs);
731
+ if (children.nodes.length > 0) {
732
+ return { node: ['a', attrs, ...children.nodes], nextIndex };
733
+ }
734
+ return { node: null, nextIndex };
735
+ }
736
+ if (token.type === 'math_inline') {
737
+ return { node: ['math', { class: 'math inline', content: token.content }, token.content], nextIndex: startIndex + 1 };
738
+ }
739
+ // Handle generic inline open/close pairs
740
+ const tagName = INLINE_TAG_MAP[token.type];
741
+ if (tagName) {
742
+ const closeType = token.type.replace('_open', '_close');
743
+ const children = processInlineChildren(tokens, startIndex + 1, closeType, inHeading);
744
+ // Check if there's a props token right after the close token
745
+ const { attrs, nextIndex } = extractAttributes(tokens, children.nextIndex + 1);
746
+ if (children.nodes.length > 0) {
747
+ return { node: [tagName, attrs, ...children.nodes], nextIndex };
748
+ }
749
+ return { node: null, nextIndex };
750
+ }
751
+ if (token.children) {
752
+ const nestedNodes = processInlineTokens(token.children, inHeading);
753
+ return { node: nestedNodes.length === 1 ? nestedNodes[0] : null, nextIndex: startIndex + 1 };
754
+ }
755
+ return { node: null, nextIndex: startIndex + 1 };
756
+ }
757
+ function processInlineChildren(tokens, startIndex, closeType, inHeading = false) {
758
+ const nodes = [];
759
+ let i = startIndex;
760
+ while (i < tokens.length) {
761
+ const token = tokens[i];
762
+ // Check for close token (either by type or by nesting for mdc_inline_span)
763
+ if (token.type === closeType) {
764
+ if (closeType === 'mdc_inline_span' && token.nesting === -1) {
765
+ break;
766
+ }
767
+ else if (closeType !== 'mdc_inline_span') {
768
+ break;
769
+ }
770
+ }
771
+ // Skip hidden mdc_inline_props tokens inside children
772
+ // These should not be processed here - they're handled by the parent
773
+ if (token.type === 'mdc_inline_props' && token.hidden) {
774
+ i++;
775
+ continue;
776
+ }
777
+ // Special handling for Comark inline components in headings
778
+ // In headings, text after components should be siblings, not children
779
+ if (token.type === 'mdc_inline_component' && inHeading) {
780
+ const componentName = token.tag || 'component';
781
+ const attrs = {};
782
+ // Check for mdc_inline_props token after the component
783
+ const { attrs: componentAttrs, nextIndex: componentNextIndex } = extractAttributes(tokens, i + 1, false);
784
+ Object.assign(attrs, componentAttrs);
785
+ if (Object.keys(componentAttrs).length > 0) {
786
+ i = componentNextIndex; // Skip both component and props tokens
787
+ }
788
+ else {
789
+ i++;
790
+ }
791
+ nodes.push([componentName, attrs]);
792
+ // Continue processing subsequent tokens as siblings
793
+ continue;
794
+ }
795
+ const result = processInlineToken(tokens, i, inHeading);
796
+ i = result.nextIndex;
797
+ if (result.node) {
798
+ nodes.push(result.node);
799
+ }
800
+ }
801
+ // Merge adjacent text nodes
802
+ return { nodes: mergeAdjacentTextNodes(nodes), nextIndex: i };
803
+ }