comark 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/frontmatter.d.ts +1 -0
- package/dist/internal/frontmatter.js +4 -2
- package/dist/internal/parse/auto-close/index.js +69 -31
- package/dist/internal/parse/auto-close/table.js +12 -9
- package/dist/internal/parse/auto-unwrap.js +6 -10
- package/dist/internal/parse/html/html_block_rule.js +10 -16
- package/dist/internal/parse/html/html_inline_rule.js +3 -7
- package/dist/internal/parse/html/html_re.js +1 -1
- package/dist/internal/parse/html/index.d.ts +1 -0
- package/dist/internal/parse/html/index.js +15 -3
- package/dist/internal/parse/syntax/block-params.d.ts +9 -0
- package/dist/internal/parse/syntax/block-params.js +48 -0
- package/dist/internal/parse/syntax/brackets.d.ts +8 -0
- package/dist/internal/parse/syntax/brackets.js +20 -0
- package/dist/internal/parse/syntax/props.d.ts +5 -0
- package/dist/internal/parse/syntax/props.js +119 -0
- package/dist/internal/parse/token-processor.js +89 -50
- package/dist/internal/props-validation.js +4 -9
- package/dist/internal/stringify/attributes.d.ts +7 -0
- package/dist/internal/stringify/attributes.js +56 -1
- package/dist/internal/stringify/handlers/a.js +1 -3
- package/dist/internal/stringify/handlers/blockquote.js +19 -4
- package/dist/internal/stringify/handlers/code.js +1 -3
- package/dist/internal/stringify/handlers/emphesis.js +1 -3
- package/dist/internal/stringify/handlers/heading.js +6 -1
- package/dist/internal/stringify/handlers/html.js +34 -18
- package/dist/internal/stringify/handlers/img.js +1 -3
- package/dist/internal/stringify/handlers/li.js +18 -9
- package/dist/internal/stringify/handlers/mdc.js +3 -4
- package/dist/internal/stringify/handlers/ol.js +12 -2
- package/dist/internal/stringify/handlers/p.d.ts +1 -1
- package/dist/internal/stringify/handlers/p.js +8 -1
- package/dist/internal/stringify/handlers/pre.js +20 -14
- package/dist/internal/stringify/handlers/strong.js +1 -3
- package/dist/internal/stringify/handlers/table.js +14 -5
- package/dist/internal/stringify/handlers/template.js +5 -2
- package/dist/internal/stringify/handlers/ul.js +12 -2
- package/dist/internal/stringify/state.js +1 -1
- package/dist/internal/yaml.js +1 -1
- package/dist/parse.d.ts +4 -4
- package/dist/parse.js +20 -10
- package/dist/plugins/alert.d.ts +1 -1
- package/dist/plugins/alert.js +1 -1
- package/dist/plugins/binding.d.ts +1 -1
- package/dist/plugins/binding.js +1 -3
- package/dist/plugins/breaks.d.ts +1 -1
- package/dist/plugins/breaks.js +1 -1
- package/dist/plugins/emoji.d.ts +1 -1
- package/dist/plugins/emoji.js +8 -8
- package/dist/plugins/footnotes.d.ts +1 -1
- package/dist/plugins/footnotes.js +19 -13
- package/dist/plugins/headings.d.ts +19 -8
- package/dist/plugins/headings.js +27 -19
- package/dist/plugins/highlight.d.ts +2 -12
- package/dist/plugins/highlight.js +201 -103
- package/dist/plugins/json-render.d.ts +1 -1
- package/dist/plugins/json-render.js +5 -9
- package/dist/plugins/math.d.ts +1 -1
- package/dist/plugins/math.js +4 -6
- package/dist/plugins/mermaid.d.ts +1 -1
- package/dist/plugins/mermaid.js +6 -20
- package/dist/plugins/punctuation.d.ts +1 -1
- package/dist/plugins/punctuation.js +5 -6
- package/dist/plugins/security.d.ts +1 -1
- package/dist/plugins/security.js +2 -2
- package/dist/plugins/summary.d.ts +4 -1
- package/dist/plugins/syntax.d.ts +49 -0
- package/dist/plugins/syntax.js +558 -0
- package/dist/plugins/task-list.d.ts +2 -2
- package/dist/plugins/task-list.js +11 -8
- package/dist/plugins/toc.d.ts +3 -1
- package/dist/plugins/toc.js +1 -1
- package/dist/types.d.ts +57 -12
- package/dist/utils/comark.tmLanguage.d.ts +335 -0
- package/dist/utils/comark.tmLanguage.js +597 -0
- package/dist/utils/helpers.d.ts +16 -4
- package/dist/utils/helpers.js +16 -6
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +25 -3
- package/package.json +40 -40
- package/skills/comark/references/rendering-svelte.md +51 -7
- package/dist/internal/stringify/indent.d.ts +0 -5
- package/dist/internal/stringify/indent.js +0 -9
- package/dist/vite.d.ts +0 -1
- package/dist/vite.js +0 -1
- package/skills/skills/comark/AGENTS.md +0 -261
- package/skills/skills/comark/SKILL.md +0 -489
- package/skills/skills/comark/references/markdown-syntax.md +0 -599
- package/skills/skills/comark/references/parsing-ast.md +0 -378
- package/skills/skills/comark/references/rendering-react.md +0 -445
- package/skills/skills/comark/references/rendering-svelte.md +0 -453
- package/skills/skills/comark/references/rendering-vue.md +0 -462
- /package/skills/{skills/migrate-mdc-to-comark → migrate-mdc-to-comark}/SKILL.md +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { Token } from 'markdown-exit';
|
|
2
|
+
import { defineComarkPlugin } from "../utils/helpers.js";
|
|
3
|
+
import { parseBracketContent } from "../internal/parse/syntax/brackets.js";
|
|
4
|
+
import { searchProps } from "../internal/parse/syntax/props.js";
|
|
5
|
+
import { parseBlockParams } from "../internal/parse/syntax/block-params.js";
|
|
6
|
+
import { parseYaml } from "../internal/yaml.js";
|
|
7
|
+
// #region Block component plugin (`::name` and `::name ... ::`)
|
|
8
|
+
const blockYamlLines = {
|
|
9
|
+
'---': '---',
|
|
10
|
+
'```yaml [props]': '```',
|
|
11
|
+
'~~~yaml [props]': '~~~',
|
|
12
|
+
'```yml [props]': '```',
|
|
13
|
+
'~~~yml [props]': '~~~',
|
|
14
|
+
};
|
|
15
|
+
const markdownItComarkBlock = (md) => {
|
|
16
|
+
const min_markers = 2;
|
|
17
|
+
const marker_str = ':';
|
|
18
|
+
const marker_char = marker_str.charCodeAt(0);
|
|
19
|
+
md.block.ruler.before('fence', 'comark_block_shorthand', function comark_block_shorthand(state, startLine, _endLine, silent) {
|
|
20
|
+
const line = state.src.slice(state.bMarks[startLine] + state.tShift[startLine], state.eMarks[startLine]);
|
|
21
|
+
if (!/^:\w/.test(line))
|
|
22
|
+
return false;
|
|
23
|
+
const { name, content, props, remaining } = parseBlockParams(line.slice(1));
|
|
24
|
+
// If there's unparsed remaining content, treat it as inline component in a paragraph
|
|
25
|
+
if (remaining)
|
|
26
|
+
return false;
|
|
27
|
+
state.lineMax = startLine + 1;
|
|
28
|
+
if (!silent) {
|
|
29
|
+
if (content !== undefined) {
|
|
30
|
+
const tokenOpen = state.push('mdc_block_shorthand', name, 1);
|
|
31
|
+
props?.forEach(([key, value]) => {
|
|
32
|
+
if (key === 'class')
|
|
33
|
+
tokenOpen.attrJoin(key, value);
|
|
34
|
+
else
|
|
35
|
+
tokenOpen.attrSet(key, value);
|
|
36
|
+
});
|
|
37
|
+
tokenOpen.map = [startLine, startLine + 1];
|
|
38
|
+
const inline = state.push('inline', '', 0);
|
|
39
|
+
inline.content = content;
|
|
40
|
+
inline.children = [];
|
|
41
|
+
const tokenClose = state.push('mdc_block_shorthand', name, -1);
|
|
42
|
+
tokenClose.map = [startLine, startLine + 1];
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const token = state.push('mdc_block_shorthand', name, 0);
|
|
46
|
+
token.map = [startLine, startLine + 1];
|
|
47
|
+
props?.forEach(([key, value]) => {
|
|
48
|
+
if (key === 'class')
|
|
49
|
+
token.attrJoin(key, value);
|
|
50
|
+
else
|
|
51
|
+
token.attrSet(key, value);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
state.line = startLine + 1;
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
md.block.ruler.before('fence', 'comark_block', function comark_block(state, startLine, endLine, silent) {
|
|
59
|
+
let pos;
|
|
60
|
+
let nextLine;
|
|
61
|
+
let auto_closed = false;
|
|
62
|
+
let start = state.bMarks[startLine] + state.tShift[startLine];
|
|
63
|
+
let max = state.eMarks[startLine];
|
|
64
|
+
const indent = state.sCount[startLine];
|
|
65
|
+
// Track code fences (``` or ~~~) so we don't match closing :: inside them
|
|
66
|
+
let inCodeFence = false;
|
|
67
|
+
let codeFenceCharCode = 0;
|
|
68
|
+
let codeFenceCount = 0;
|
|
69
|
+
// Track nesting depth for blocks with the same marker count
|
|
70
|
+
let nestingDepth = 0;
|
|
71
|
+
if (state.src[start] !== ':')
|
|
72
|
+
return false;
|
|
73
|
+
for (pos = start + 1; pos <= max; pos++) {
|
|
74
|
+
if (marker_str !== state.src[pos])
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
const marker_count = Math.floor(pos - start);
|
|
78
|
+
if (marker_count < min_markers)
|
|
79
|
+
return false;
|
|
80
|
+
const markup = state.src.slice(start, pos);
|
|
81
|
+
const params = parseBlockParams(state.src.slice(pos, max));
|
|
82
|
+
if (!params.name)
|
|
83
|
+
return false;
|
|
84
|
+
if (silent)
|
|
85
|
+
return true;
|
|
86
|
+
nextLine = startLine;
|
|
87
|
+
for (;;) {
|
|
88
|
+
nextLine++;
|
|
89
|
+
if (nextLine >= endLine)
|
|
90
|
+
break;
|
|
91
|
+
start = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
92
|
+
max = state.eMarks[nextLine];
|
|
93
|
+
if (start < max && state.sCount[nextLine] < state.blkIndent)
|
|
94
|
+
break;
|
|
95
|
+
const lineCharCode = state.src.charCodeAt(start);
|
|
96
|
+
// Detect closing code fence (``` or ~~~)
|
|
97
|
+
if (inCodeFence) {
|
|
98
|
+
if (lineCharCode === codeFenceCharCode) {
|
|
99
|
+
let fencePos = start + 1;
|
|
100
|
+
while (fencePos < max && state.src.charCodeAt(fencePos) === codeFenceCharCode)
|
|
101
|
+
fencePos++;
|
|
102
|
+
if (fencePos - start >= codeFenceCount) {
|
|
103
|
+
const afterFence = state.skipSpaces(fencePos);
|
|
104
|
+
if (afterFence >= max)
|
|
105
|
+
inCodeFence = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Detect opening code fence (``` or ~~~)
|
|
111
|
+
if (lineCharCode === 0x60 /* ` */ || lineCharCode === 0x7e /* ~ */) {
|
|
112
|
+
let fencePos = start + 1;
|
|
113
|
+
while (fencePos < max && state.src.charCodeAt(fencePos) === lineCharCode)
|
|
114
|
+
fencePos++;
|
|
115
|
+
if (fencePos - start >= 3) {
|
|
116
|
+
inCodeFence = true;
|
|
117
|
+
codeFenceCharCode = lineCharCode;
|
|
118
|
+
codeFenceCount = fencePos - start;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (marker_char !== lineCharCode)
|
|
123
|
+
continue;
|
|
124
|
+
for (pos = start + 1; pos <= max; pos++) {
|
|
125
|
+
if (marker_str !== state.src[pos])
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
// Closing fence must match the opening fence length
|
|
129
|
+
if (pos - start !== marker_count)
|
|
130
|
+
continue;
|
|
131
|
+
pos = state.skipSpaces(pos);
|
|
132
|
+
if (pos < max) {
|
|
133
|
+
// A new nested block opens with same marker count
|
|
134
|
+
nestingDepth++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (nestingDepth > 0) {
|
|
138
|
+
nestingDepth--;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
auto_closed = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const old_parent = state.parentType;
|
|
145
|
+
const old_line_max = state.lineMax;
|
|
146
|
+
state.parentType = 'comark_block';
|
|
147
|
+
// Prevent lazy continuations from going past our end marker
|
|
148
|
+
state.lineMax = nextLine;
|
|
149
|
+
const tokenOpen = state.push('mdc_block_open', params.name, 1);
|
|
150
|
+
tokenOpen.markup = markup;
|
|
151
|
+
tokenOpen.block = true;
|
|
152
|
+
tokenOpen.info = params.name;
|
|
153
|
+
tokenOpen.map = [startLine, nextLine];
|
|
154
|
+
params.props?.forEach(([key, value]) => {
|
|
155
|
+
if (key === 'class')
|
|
156
|
+
tokenOpen.attrJoin(key, value);
|
|
157
|
+
else
|
|
158
|
+
tokenOpen.attrSet(key, value);
|
|
159
|
+
});
|
|
160
|
+
// Render bracket content as the first paragraph: `::block[Content]\n::`
|
|
161
|
+
if (params.content !== undefined) {
|
|
162
|
+
const pOpen = state.push('paragraph_open', 'p', 1);
|
|
163
|
+
pOpen.map = [startLine, startLine + 1];
|
|
164
|
+
const inline = state.push('inline', '', 0);
|
|
165
|
+
inline.content = params.content;
|
|
166
|
+
inline.children = [];
|
|
167
|
+
state.push('paragraph_close', 'p', -1);
|
|
168
|
+
}
|
|
169
|
+
const blkIndent = state.blkIndent;
|
|
170
|
+
state.blkIndent = indent;
|
|
171
|
+
state.env.comarkBlockTokens ||= [];
|
|
172
|
+
state.env.comarkBlockTokens.unshift(tokenOpen);
|
|
173
|
+
state.md.block.tokenize(state, startLine + 1, nextLine);
|
|
174
|
+
state.blkIndent = blkIndent;
|
|
175
|
+
state.env.comarkBlockTokens.shift();
|
|
176
|
+
const tokenClose = state.push('mdc_block_close', params.name, -1);
|
|
177
|
+
tokenClose.map = [startLine, nextLine];
|
|
178
|
+
tokenClose.markup = state.src.slice(start, pos);
|
|
179
|
+
tokenClose.block = true;
|
|
180
|
+
// Hide the wrapper paragraph for single-paragraph blocks
|
|
181
|
+
state.tokens
|
|
182
|
+
.slice(state.tokens.indexOf(tokenOpen) + 1, state.tokens.indexOf(tokenClose))
|
|
183
|
+
.filter((i) => i.level === tokenOpen.level + 1)
|
|
184
|
+
.forEach((i, _, arr) => {
|
|
185
|
+
if (arr.length <= 2 && i.tag === 'p')
|
|
186
|
+
i.hidden = true;
|
|
187
|
+
});
|
|
188
|
+
state.parentType = old_parent;
|
|
189
|
+
state.lineMax = old_line_max;
|
|
190
|
+
state.line = nextLine + (auto_closed ? 1 : 0);
|
|
191
|
+
return true;
|
|
192
|
+
}, {
|
|
193
|
+
alt: ['paragraph', 'reference', 'blockquote', 'list'],
|
|
194
|
+
});
|
|
195
|
+
md.block.ruler.after('code', 'comark_block_yaml', function comark_block_yaml(state, startLine, endLine, silent) {
|
|
196
|
+
if (!state.env.comarkBlockTokens?.length)
|
|
197
|
+
return false;
|
|
198
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
199
|
+
const end = state.eMarks[startLine];
|
|
200
|
+
const line = state.src.slice(start, end);
|
|
201
|
+
const blockAttributesClosingFence = blockYamlLines[line] || '';
|
|
202
|
+
if (!blockAttributesClosingFence)
|
|
203
|
+
return false;
|
|
204
|
+
// The `---` fence is only valid on the line immediately after the component opener. Any other `---` is a thematic break.
|
|
205
|
+
if (line === '---') {
|
|
206
|
+
const parentOpenLine = state.env.comarkBlockTokens[0].map?.[0];
|
|
207
|
+
if (parentOpenLine === undefined || startLine !== parentOpenLine + 1)
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
let lineEnd = startLine + 1;
|
|
211
|
+
let found = false;
|
|
212
|
+
while (lineEnd < endLine) {
|
|
213
|
+
const inner = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
|
|
214
|
+
if (inner === blockAttributesClosingFence) {
|
|
215
|
+
found = true;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
lineEnd += 1;
|
|
219
|
+
}
|
|
220
|
+
if (!found)
|
|
221
|
+
return false;
|
|
222
|
+
if (!silent) {
|
|
223
|
+
const yaml = state.src.slice(state.bMarks[startLine + 1], state.eMarks[lineEnd - 1]);
|
|
224
|
+
const data = parseYaml(yaml);
|
|
225
|
+
const token = state.env.comarkBlockTokens[0];
|
|
226
|
+
Object.entries(data || {}).forEach(([key, value]) => {
|
|
227
|
+
if (key === 'class')
|
|
228
|
+
token.attrJoin(key, value);
|
|
229
|
+
else
|
|
230
|
+
token.attrSet(key, typeof value === 'string' ? value : JSON.stringify(value));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
state.line = lineEnd + 1;
|
|
234
|
+
state.lineMax = lineEnd + 1;
|
|
235
|
+
return true;
|
|
236
|
+
});
|
|
237
|
+
md.block.ruler.after('code', 'comark_block_slots', function comark_block_slots(state, startLine, endLine, silent) {
|
|
238
|
+
if (!state.env.comarkBlockTokens?.length)
|
|
239
|
+
return false;
|
|
240
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
241
|
+
if (!(state.src[start] === '#' && state.src[start + 1] !== ' ' && state.src[start + 1] !== '#'))
|
|
242
|
+
return false;
|
|
243
|
+
const line = state.src.slice(start, state.eMarks[startLine]);
|
|
244
|
+
const { name, props } = parseBlockParams(line.slice(1));
|
|
245
|
+
let lineEnd = startLine + 1;
|
|
246
|
+
let inCodeFence = false;
|
|
247
|
+
let codeFenceChar = '';
|
|
248
|
+
let codeFenceCount = 0;
|
|
249
|
+
while (lineEnd < endLine) {
|
|
250
|
+
const inner = state.src.slice(state.bMarks[lineEnd] + state.tShift[startLine], state.eMarks[lineEnd]);
|
|
251
|
+
if (inCodeFence) {
|
|
252
|
+
// Look for matching closing fence (same char, >= opening count, nothing but spaces after)
|
|
253
|
+
if (inner[0] === codeFenceChar) {
|
|
254
|
+
let fencePos = 1;
|
|
255
|
+
while (fencePos < inner.length && inner[fencePos] === codeFenceChar)
|
|
256
|
+
fencePos++;
|
|
257
|
+
if (fencePos >= codeFenceCount && inner.slice(fencePos).trim() === '') {
|
|
258
|
+
inCodeFence = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
lineEnd += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// Detect opening code fence (``` or ~~~, length >= 3)
|
|
265
|
+
if (inner[0] === '`' || inner[0] === '~') {
|
|
266
|
+
const ch = inner[0];
|
|
267
|
+
let fencePos = 1;
|
|
268
|
+
while (fencePos < inner.length && inner[fencePos] === ch)
|
|
269
|
+
fencePos++;
|
|
270
|
+
if (fencePos >= 3) {
|
|
271
|
+
inCodeFence = true;
|
|
272
|
+
codeFenceChar = ch;
|
|
273
|
+
codeFenceCount = fencePos;
|
|
274
|
+
lineEnd += 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (/^#\w+/.test(inner) || inner.startsWith('::'))
|
|
279
|
+
break;
|
|
280
|
+
lineEnd += 1;
|
|
281
|
+
}
|
|
282
|
+
if (silent) {
|
|
283
|
+
state.line = lineEnd;
|
|
284
|
+
state.lineMax = lineEnd;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
state.lineMax = startLine + 1;
|
|
288
|
+
const slot = state.push('mdc_block_slot', 'template', 1);
|
|
289
|
+
slot.attrSet(`#${name}`, '');
|
|
290
|
+
props?.forEach(([key, value]) => {
|
|
291
|
+
if (key === 'class')
|
|
292
|
+
slot.attrJoin(key, value);
|
|
293
|
+
else
|
|
294
|
+
slot.attrSet(key, value);
|
|
295
|
+
});
|
|
296
|
+
state.line = startLine + 1;
|
|
297
|
+
state.lineMax = lineEnd;
|
|
298
|
+
state.md.block.tokenize(state, startLine + 1, lineEnd);
|
|
299
|
+
state.push('mdc_block_slot', 'template', -1);
|
|
300
|
+
state.line = lineEnd;
|
|
301
|
+
state.lineMax = lineEnd;
|
|
302
|
+
return true;
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
// #endregion
|
|
306
|
+
// #region Inline span plugin (`[text]`)
|
|
307
|
+
const markdownItInlineSpan = (md) => {
|
|
308
|
+
md.inline.ruler.before('link', 'comark_inline_span', (state, silent) => {
|
|
309
|
+
const start = state.pos;
|
|
310
|
+
if (state.src[start] !== '[')
|
|
311
|
+
return false;
|
|
312
|
+
let index = start + 1;
|
|
313
|
+
let depth = 0;
|
|
314
|
+
while (index < state.src.length) {
|
|
315
|
+
if (state.src[index] === '\\') {
|
|
316
|
+
index += 2;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (state.src[index] === '[') {
|
|
320
|
+
depth++;
|
|
321
|
+
}
|
|
322
|
+
else if (state.src[index] === ']') {
|
|
323
|
+
if (depth === 0)
|
|
324
|
+
break;
|
|
325
|
+
depth--;
|
|
326
|
+
}
|
|
327
|
+
index += 1;
|
|
328
|
+
}
|
|
329
|
+
if (index === start)
|
|
330
|
+
return false;
|
|
331
|
+
// Don't match `[text](url)` or `[text][ref]` — let the link parser handle those
|
|
332
|
+
const nextChar = state.src[index + 1];
|
|
333
|
+
if (nextChar === '(' || nextChar === '[')
|
|
334
|
+
return false;
|
|
335
|
+
if (silent)
|
|
336
|
+
return true;
|
|
337
|
+
state.push('mdc_inline_span', 'span', 1);
|
|
338
|
+
const oldPos = state.pos;
|
|
339
|
+
const oldPosMax = state.posMax;
|
|
340
|
+
state.pos = start + 1;
|
|
341
|
+
state.posMax = index;
|
|
342
|
+
state.md.inline.tokenize(state);
|
|
343
|
+
state.pos = oldPos;
|
|
344
|
+
state.posMax = oldPosMax;
|
|
345
|
+
state.push('mdc_inline_span', 'span', -1);
|
|
346
|
+
state.pos = index + 1;
|
|
347
|
+
return true;
|
|
348
|
+
});
|
|
349
|
+
};
|
|
350
|
+
// #endregion
|
|
351
|
+
// #region Inline component plugin (`:name[content]{props}`)
|
|
352
|
+
const ALLOWED_PREV_CHARS = new Set([' ', '\t', '\n', '*', '_', '[']);
|
|
353
|
+
const markdownItInlineComponent = (md) => {
|
|
354
|
+
md.inline.ruler.after('entity', 'comark_inline_component', (state, silent) => {
|
|
355
|
+
const start = state.pos;
|
|
356
|
+
if (state.src[start] !== ':')
|
|
357
|
+
return false;
|
|
358
|
+
const prevChar = state.src[start - 1];
|
|
359
|
+
if (start > 0 && !ALLOWED_PREV_CHARS.has(prevChar))
|
|
360
|
+
return false;
|
|
361
|
+
let index = start + 1;
|
|
362
|
+
let nameEnd = -1;
|
|
363
|
+
let contentStart = -1;
|
|
364
|
+
let contentEnd = -1;
|
|
365
|
+
while (index < state.src.length) {
|
|
366
|
+
const char = state.src[index];
|
|
367
|
+
if (char === '[') {
|
|
368
|
+
nameEnd = index;
|
|
369
|
+
const result = parseBracketContent(state.src, index);
|
|
370
|
+
if (result) {
|
|
371
|
+
contentStart = index + 1;
|
|
372
|
+
contentEnd = result.endIndex - 1;
|
|
373
|
+
index = result.endIndex;
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
if (!/[\w$-]/.test(char))
|
|
378
|
+
break;
|
|
379
|
+
index += 1;
|
|
380
|
+
}
|
|
381
|
+
if (nameEnd === -1)
|
|
382
|
+
nameEnd = index;
|
|
383
|
+
// Empty name
|
|
384
|
+
if (nameEnd <= start + 1)
|
|
385
|
+
return false;
|
|
386
|
+
state.pos = index;
|
|
387
|
+
if (silent)
|
|
388
|
+
return true;
|
|
389
|
+
const name = state.src.slice(start + 1, nameEnd);
|
|
390
|
+
if (contentStart !== -1) {
|
|
391
|
+
state.push('mdc_inline_component', name, 1);
|
|
392
|
+
const oldPos = state.pos;
|
|
393
|
+
const oldPosMax = state.posMax;
|
|
394
|
+
state.pos = contentStart;
|
|
395
|
+
state.posMax = contentEnd;
|
|
396
|
+
state.md.inline.tokenize(state);
|
|
397
|
+
state.pos = oldPos;
|
|
398
|
+
state.posMax = oldPosMax;
|
|
399
|
+
state.push('mdc_inline_component', name, -1);
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
state.push('mdc_inline_component', name, 0);
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
// #endregion
|
|
408
|
+
// #region Inline props plugin (`{class="foo"}` after a token)
|
|
409
|
+
const markdownItInlineProps = (md) => {
|
|
410
|
+
md.inline.ruler.after('entity', 'comark_inline_props', (state, silent) => {
|
|
411
|
+
const start = state.pos;
|
|
412
|
+
if (state.src[start] !== '{')
|
|
413
|
+
return false;
|
|
414
|
+
// Skip Vue mustache `{{ }}` and template `${ }` syntax
|
|
415
|
+
if (state.src[start + 1] === '{' || state.src[start - 1] === '{' || state.src[start - 1] === '$')
|
|
416
|
+
return false;
|
|
417
|
+
const search = searchProps(state.src, start);
|
|
418
|
+
if (!search)
|
|
419
|
+
return false;
|
|
420
|
+
const { props, index: end } = search;
|
|
421
|
+
if (end === start)
|
|
422
|
+
return false;
|
|
423
|
+
state.pos = end;
|
|
424
|
+
if (silent)
|
|
425
|
+
return true;
|
|
426
|
+
// Hidden token holding the props; later applied to the previous token
|
|
427
|
+
const token = state.push('mdc_inline_props', 'span', 0);
|
|
428
|
+
token.attrs = props;
|
|
429
|
+
token.hidden = true;
|
|
430
|
+
return true;
|
|
431
|
+
});
|
|
432
|
+
md.renderer.rules.mdc_inline_props = () => '';
|
|
433
|
+
const _parse = md.parse;
|
|
434
|
+
md.parse = function (src, env) {
|
|
435
|
+
const tokens = _parse.call(this, src, env);
|
|
436
|
+
// When the trailing inline child is a props token directly after a text
|
|
437
|
+
// node, lift the props onto the surrounding heading/paragraph/list_item.
|
|
438
|
+
// (If the props follow a closing tag, they belong to that inline tag, not
|
|
439
|
+
// the parent — leave them alone.)
|
|
440
|
+
tokens.forEach((token, index) => {
|
|
441
|
+
const prev = tokens[index - 1];
|
|
442
|
+
const next = tokens[index + 1];
|
|
443
|
+
if (!prev || !['heading_open', 'paragraph_open', 'list_item_open'].includes(prev.type) || prev.hidden)
|
|
444
|
+
return;
|
|
445
|
+
// Tight-list paragraph: the inline lives one slot ahead
|
|
446
|
+
if (token.hidden && next?.type === 'inline')
|
|
447
|
+
token = next;
|
|
448
|
+
if (token.type !== 'inline' || !token.children?.length)
|
|
449
|
+
return;
|
|
450
|
+
const last = token.children[token.children.length - 1];
|
|
451
|
+
if (last.type !== 'mdc_inline_props')
|
|
452
|
+
return;
|
|
453
|
+
// Find the previous non-empty child. Markdown-it's emphasis tokenizer
|
|
454
|
+
// can leave an empty text token between the closing delimiter and the
|
|
455
|
+
// props — skipping it lets us distinguish "props on the parent" from
|
|
456
|
+
// "props on the preceding inline tag".
|
|
457
|
+
let beforeIdx = token.children.length - 2;
|
|
458
|
+
while (beforeIdx >= 0) {
|
|
459
|
+
const child = token.children[beforeIdx];
|
|
460
|
+
if (child.type === 'text' && !child.content) {
|
|
461
|
+
beforeIdx--;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
const beforeProps = beforeIdx >= 0 ? token.children[beforeIdx] : undefined;
|
|
467
|
+
if (!beforeProps || beforeProps.type !== 'text')
|
|
468
|
+
return;
|
|
469
|
+
// Strip the trailing space the text picked up before the `{...}` token.
|
|
470
|
+
if (typeof beforeProps.content === 'string') {
|
|
471
|
+
beforeProps.content = beforeProps.content.replace(/[ \t]+$/, '');
|
|
472
|
+
}
|
|
473
|
+
const props = last.attrs;
|
|
474
|
+
// Drop the props token (last) plus any empty text tokens it left behind.
|
|
475
|
+
token.children.length = beforeProps.content ? beforeIdx + 1 : beforeIdx;
|
|
476
|
+
props?.forEach(([key, value]) => {
|
|
477
|
+
if (key === 'class')
|
|
478
|
+
prev.attrJoin('class', value);
|
|
479
|
+
else
|
|
480
|
+
prev.attrSet(key, value);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
return tokens;
|
|
484
|
+
};
|
|
485
|
+
md.renderer.renderInline = wrapRenderInline(md.renderer.renderInline);
|
|
486
|
+
// Support markdown-exit's async inline renderer
|
|
487
|
+
if ('renderInlineAsync' in md.renderer) {
|
|
488
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirrors the sync wrapper for the async overload
|
|
489
|
+
;
|
|
490
|
+
md.renderer.renderInlineAsync = wrapRenderInline(md.renderer.renderInlineAsync);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
function wrapRenderInline(renderInline) {
|
|
494
|
+
return function (tokens, options, env) {
|
|
495
|
+
tokens = [...tokens];
|
|
496
|
+
tokens.forEach((token, index) => {
|
|
497
|
+
if (token.type !== 'mdc_inline_props')
|
|
498
|
+
return;
|
|
499
|
+
let prevIndex = index - 1;
|
|
500
|
+
let prev = tokens[prevIndex];
|
|
501
|
+
// Skip whitespace-only text tokens
|
|
502
|
+
while (prevIndex >= 0) {
|
|
503
|
+
if (prev.type === 'text' && !prev.content.trim()) {
|
|
504
|
+
prevIndex--;
|
|
505
|
+
prev = tokens[prevIndex];
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Wrap a bare text token in a span so we can attach attrs to it
|
|
512
|
+
if (!prev.tag && prev.type === 'text') {
|
|
513
|
+
prev = new Token('mdc_inline_span', 'span', 1);
|
|
514
|
+
tokens.splice(index - 1, 0, prev);
|
|
515
|
+
const close = new Token('mdc_inline_span', 'span', -1);
|
|
516
|
+
tokens.splice(index + 2, 0, close);
|
|
517
|
+
}
|
|
518
|
+
else if (prev.nesting === -1) {
|
|
519
|
+
// Resolve a closing tag back to its matching opening tag
|
|
520
|
+
let searchIndex = index - 1;
|
|
521
|
+
while (searchIndex >= 0) {
|
|
522
|
+
const searchToken = tokens[searchIndex];
|
|
523
|
+
if (searchToken.nesting === 1 && searchToken.tag === prev.tag && searchToken.level === prev.level) {
|
|
524
|
+
prev = searchToken;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
searchIndex--;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (prev.nesting === -1)
|
|
531
|
+
throw new Error(`No matching opening tag found for ${JSON.stringify(prev)}`);
|
|
532
|
+
token.attrs?.forEach(([key, value]) => {
|
|
533
|
+
if (key === 'class')
|
|
534
|
+
prev.attrJoin('class', value);
|
|
535
|
+
else
|
|
536
|
+
prev.attrSet(key, value);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
return renderInline.call(this, tokens, options, env);
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// #endregion
|
|
543
|
+
function applySyntax(md, options = {}) {
|
|
544
|
+
const { blockComponent = true, inlineProps = true, inlineSpan = true, inlineComponent = true } = options;
|
|
545
|
+
if (blockComponent)
|
|
546
|
+
md.use(markdownItComarkBlock);
|
|
547
|
+
if (inlineProps)
|
|
548
|
+
md.use(markdownItInlineProps);
|
|
549
|
+
if (inlineSpan)
|
|
550
|
+
md.use(markdownItInlineSpan);
|
|
551
|
+
if (inlineComponent)
|
|
552
|
+
md.use(markdownItInlineComponent);
|
|
553
|
+
}
|
|
554
|
+
export default defineComarkPlugin((options = {}) => ({
|
|
555
|
+
name: 'syntax',
|
|
556
|
+
markdownItPlugins: [((md) => applySyntax(md, options))],
|
|
557
|
+
}));
|
|
558
|
+
export const markdownItComark = applySyntax;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom task list plugin for markdown-it that
|
|
2
|
+
* Custom task list plugin for markdown-it that integrates with the Comark syntax plugin.
|
|
3
3
|
*
|
|
4
4
|
* This plugin runs before inline parsing to prevent Comark from interpreting
|
|
5
5
|
* task list markers [X] and [ ] as Comark inline span syntax.
|
|
6
6
|
*/
|
|
7
|
-
declare const _default: import("../types.ts").ComarkPluginFactory<unknown>;
|
|
7
|
+
declare const _default: import("../types.ts").ComarkPluginFactory<unknown, {}, {}>;
|
|
8
8
|
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom task list plugin for markdown-it that
|
|
2
|
+
* Custom task list plugin for markdown-it that integrates with the Comark syntax plugin.
|
|
3
3
|
*
|
|
4
4
|
* This plugin runs before inline parsing to prevent Comark from interpreting
|
|
5
5
|
* task list markers [X] and [ ] as Comark inline span syntax.
|
|
@@ -35,8 +35,8 @@ function findParentList(tokens, listItemIndex) {
|
|
|
35
35
|
const targetLevel = tokens[listItemIndex].level - 1;
|
|
36
36
|
// Look backwards for the list (ul/ol) that contains this list item
|
|
37
37
|
for (let i = listItemIndex - 1; i >= 0; i--) {
|
|
38
|
-
if (tokens[i].level === targetLevel
|
|
39
|
-
|
|
38
|
+
if (tokens[i].level === targetLevel &&
|
|
39
|
+
(tokens[i].type === 'bullet_list_open' || tokens[i].type === 'ordered_list_open')) {
|
|
40
40
|
return i;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -67,7 +67,7 @@ function markdownItTaskList(md, options) {
|
|
|
67
67
|
// Replace the task marker with a placeholder that won't be processed by Comark
|
|
68
68
|
// We use a special format that we can detect later
|
|
69
69
|
// Keep one space after the placeholder to match expected output
|
|
70
|
-
const checkboxPlaceholder = `
|
|
70
|
+
const checkboxPlaceholder = `TASK_CHECKBOX_${isChecked ? 'CHECKED' : 'UNCHECKED'} `;
|
|
71
71
|
token.content = token.content.replace(/^\[[ x]\]\s+/i, checkboxPlaceholder);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -85,13 +85,16 @@ function markdownItTaskList(md, options) {
|
|
|
85
85
|
const child = token.children[j];
|
|
86
86
|
if (child.type === 'text' && child.content) {
|
|
87
87
|
// Check for our checkbox placeholder
|
|
88
|
-
const checkedMatch = child.content.match(
|
|
89
|
-
const uncheckedMatch = child.content.match(
|
|
88
|
+
const checkedMatch = child.content.match(/^TASK_CHECKBOX_CHECKED/);
|
|
89
|
+
const uncheckedMatch = child.content.match(/^TASK_CHECKBOX_UNCHECKED/);
|
|
90
90
|
if (checkedMatch || uncheckedMatch) {
|
|
91
91
|
const isChecked = !!checkedMatch;
|
|
92
92
|
// Create checkbox token
|
|
93
93
|
const checkbox = new state.Token('mdc_inline_component', 'input', 0);
|
|
94
|
-
checkbox.attrs = [
|
|
94
|
+
checkbox.attrs = [
|
|
95
|
+
['class', 'task-list-item-checkbox'],
|
|
96
|
+
['type', 'checkbox'],
|
|
97
|
+
];
|
|
95
98
|
if (disableCheckboxes) {
|
|
96
99
|
checkbox.attrs.push([':disabled', 'true']);
|
|
97
100
|
}
|
|
@@ -99,7 +102,7 @@ function markdownItTaskList(md, options) {
|
|
|
99
102
|
checkbox.attrs.push([':checked', 'true']);
|
|
100
103
|
}
|
|
101
104
|
// Remove placeholder from text
|
|
102
|
-
child.content = child.content.replace(
|
|
105
|
+
child.content = child.content.replace(/^TASK_CHECKBOX_(CHECKED|UNCHECKED)/, '');
|
|
103
106
|
// Insert checkbox before the text
|
|
104
107
|
token.children.splice(j, 0, checkbox);
|
|
105
108
|
j++; // Skip the newly inserted checkbox
|
package/dist/plugins/toc.d.ts
CHANGED
|
@@ -12,5 +12,7 @@ export interface Toc {
|
|
|
12
12
|
links: TocLink[];
|
|
13
13
|
}
|
|
14
14
|
export declare function generateFlatToc(body: ComarkTree, options: Toc): Toc;
|
|
15
|
-
declare const _default: import("comark").ComarkPluginFactory<Partial<Toc
|
|
15
|
+
declare const _default: import("comark").ComarkPluginFactory<Partial<Toc>, {
|
|
16
|
+
toc: Toc;
|
|
17
|
+
}, {}>;
|
|
16
18
|
export default _default;
|
package/dist/plugins/toc.js
CHANGED
|
@@ -94,7 +94,7 @@ export function generateFlatToc(body, options) {
|
|
|
94
94
|
const tag = getTag(node);
|
|
95
95
|
return tag !== null && tags.includes(tag);
|
|
96
96
|
});
|
|
97
|
-
const links = headers.map(node => ({
|
|
97
|
+
const links = headers.map((node) => ({
|
|
98
98
|
id: getProps(node).id || '',
|
|
99
99
|
depth: getHeaderDepth(node),
|
|
100
100
|
text: flattenNodeText(node),
|