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.
- package/README.md +104 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/internal/frontmatter.d.ts +16 -0
- package/dist/internal/frontmatter.js +43 -0
- package/dist/internal/parse/auto-close/index.d.ts +12 -0
- package/dist/internal/parse/auto-close/index.js +457 -0
- package/dist/internal/parse/auto-close/table.d.ts +4 -0
- package/dist/internal/parse/auto-close/table.js +161 -0
- package/dist/internal/parse/auto-unwrap.d.ts +20 -0
- package/dist/internal/parse/auto-unwrap.js +42 -0
- package/dist/internal/parse/html/html_block_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_block_rule.js +60 -0
- package/dist/internal/parse/html/html_blocks.d.ts +2 -0
- package/dist/internal/parse/html/html_blocks.js +66 -0
- package/dist/internal/parse/html/html_inline_rule.d.ts +2 -0
- package/dist/internal/parse/html/html_inline_rule.js +43 -0
- package/dist/internal/parse/html/html_re.d.ts +3 -0
- package/dist/internal/parse/html/html_re.js +18 -0
- package/dist/internal/parse/html/index.d.ts +18 -0
- package/dist/internal/parse/html/index.js +122 -0
- package/dist/internal/parse/incremental.d.ts +12 -0
- package/dist/internal/parse/incremental.js +39 -0
- package/dist/internal/parse/token-processor.d.ts +9 -0
- package/dist/internal/parse/token-processor.js +803 -0
- package/dist/internal/props-validation.d.ts +12 -0
- package/dist/internal/props-validation.js +112 -0
- package/dist/internal/stringify/attributes.d.ts +21 -0
- package/dist/internal/stringify/attributes.js +67 -0
- package/dist/internal/stringify/handlers/a.d.ts +3 -0
- package/dist/internal/stringify/handlers/a.js +11 -0
- package/dist/internal/stringify/handlers/blockquote.d.ts +3 -0
- package/dist/internal/stringify/handlers/blockquote.js +18 -0
- package/dist/internal/stringify/handlers/br.d.ts +3 -0
- package/dist/internal/stringify/handlers/br.js +3 -0
- package/dist/internal/stringify/handlers/code.d.ts +3 -0
- package/dist/internal/stringify/handlers/code.js +11 -0
- package/dist/internal/stringify/handlers/comment.d.ts +3 -0
- package/dist/internal/stringify/handlers/comment.js +6 -0
- package/dist/internal/stringify/handlers/del.d.ts +3 -0
- package/dist/internal/stringify/handlers/del.js +4 -0
- package/dist/internal/stringify/handlers/emphesis.d.ts +3 -0
- package/dist/internal/stringify/handlers/emphesis.js +13 -0
- package/dist/internal/stringify/handlers/heading.d.ts +3 -0
- package/dist/internal/stringify/handlers/heading.js +7 -0
- package/dist/internal/stringify/handlers/hr.d.ts +3 -0
- package/dist/internal/stringify/handlers/hr.js +3 -0
- package/dist/internal/stringify/handlers/html.d.ts +3 -0
- package/dist/internal/stringify/handlers/html.js +73 -0
- package/dist/internal/stringify/handlers/img.d.ts +3 -0
- package/dist/internal/stringify/handlers/img.js +9 -0
- package/dist/internal/stringify/handlers/index.d.ts +2 -0
- package/dist/internal/stringify/handlers/index.js +56 -0
- package/dist/internal/stringify/handlers/li.d.ts +3 -0
- package/dist/internal/stringify/handlers/li.js +43 -0
- package/dist/internal/stringify/handlers/math.d.ts +3 -0
- package/dist/internal/stringify/handlers/math.js +8 -0
- package/dist/internal/stringify/handlers/mdc.d.ts +3 -0
- package/dist/internal/stringify/handlers/mdc.js +47 -0
- package/dist/internal/stringify/handlers/mermaid.d.ts +3 -0
- package/dist/internal/stringify/handlers/mermaid.js +8 -0
- package/dist/internal/stringify/handlers/ol.d.ts +3 -0
- package/dist/internal/stringify/handlers/ol.js +18 -0
- package/dist/internal/stringify/handlers/p.d.ts +3 -0
- package/dist/internal/stringify/handlers/p.js +8 -0
- package/dist/internal/stringify/handlers/pre.d.ts +3 -0
- package/dist/internal/stringify/handlers/pre.js +60 -0
- package/dist/internal/stringify/handlers/strong.d.ts +3 -0
- package/dist/internal/stringify/handlers/strong.js +13 -0
- package/dist/internal/stringify/handlers/table.d.ts +8 -0
- package/dist/internal/stringify/handlers/table.js +180 -0
- package/dist/internal/stringify/handlers/template.d.ts +3 -0
- package/dist/internal/stringify/handlers/template.js +14 -0
- package/dist/internal/stringify/handlers/ul.d.ts +3 -0
- package/dist/internal/stringify/handlers/ul.js +18 -0
- package/dist/internal/stringify/indent.d.ts +4 -0
- package/dist/internal/stringify/indent.js +8 -0
- package/dist/internal/stringify/state.d.ts +13 -0
- package/dist/internal/stringify/state.js +121 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.js +51 -0
- package/dist/parse.d.ts +66 -0
- package/dist/parse.js +163 -0
- package/dist/plugins/alert.d.ts +2 -0
- package/dist/plugins/alert.js +66 -0
- package/dist/plugins/emoji.d.ts +3 -0
- package/dist/plugins/emoji.js +438 -0
- package/dist/plugins/headings.d.ts +48 -0
- package/dist/plugins/headings.js +85 -0
- package/dist/plugins/highlight.d.ts +63 -0
- package/dist/plugins/highlight.js +235 -0
- package/dist/plugins/math.d.ts +59 -0
- package/dist/plugins/math.js +263 -0
- package/dist/plugins/mermaid.d.ts +38 -0
- package/dist/plugins/mermaid.js +185 -0
- package/dist/plugins/security.d.ts +11 -0
- package/dist/plugins/security.js +32 -0
- package/dist/plugins/summary.d.ts +2 -0
- package/dist/plugins/summary.js +22 -0
- package/dist/plugins/task-list.d.ts +8 -0
- package/dist/plugins/task-list.js +117 -0
- package/dist/plugins/toc.d.ts +15 -0
- package/dist/plugins/toc.js +118 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +29 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.js +1 -0
- package/dist/utils/caret.d.ts +7 -0
- package/dist/utils/caret.js +36 -0
- package/dist/utils/index.d.ts +38 -0
- package/dist/utils/index.js +149 -0
- 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
|
+
}
|