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.
Files changed (93) hide show
  1. package/dist/internal/frontmatter.d.ts +1 -0
  2. package/dist/internal/frontmatter.js +4 -2
  3. package/dist/internal/parse/auto-close/index.js +69 -31
  4. package/dist/internal/parse/auto-close/table.js +12 -9
  5. package/dist/internal/parse/auto-unwrap.js +6 -10
  6. package/dist/internal/parse/html/html_block_rule.js +10 -16
  7. package/dist/internal/parse/html/html_inline_rule.js +3 -7
  8. package/dist/internal/parse/html/html_re.js +1 -1
  9. package/dist/internal/parse/html/index.d.ts +1 -0
  10. package/dist/internal/parse/html/index.js +15 -3
  11. package/dist/internal/parse/syntax/block-params.d.ts +9 -0
  12. package/dist/internal/parse/syntax/block-params.js +48 -0
  13. package/dist/internal/parse/syntax/brackets.d.ts +8 -0
  14. package/dist/internal/parse/syntax/brackets.js +20 -0
  15. package/dist/internal/parse/syntax/props.d.ts +5 -0
  16. package/dist/internal/parse/syntax/props.js +119 -0
  17. package/dist/internal/parse/token-processor.js +89 -50
  18. package/dist/internal/props-validation.js +4 -9
  19. package/dist/internal/stringify/attributes.d.ts +7 -0
  20. package/dist/internal/stringify/attributes.js +56 -1
  21. package/dist/internal/stringify/handlers/a.js +1 -3
  22. package/dist/internal/stringify/handlers/blockquote.js +19 -4
  23. package/dist/internal/stringify/handlers/code.js +1 -3
  24. package/dist/internal/stringify/handlers/emphesis.js +1 -3
  25. package/dist/internal/stringify/handlers/heading.js +6 -1
  26. package/dist/internal/stringify/handlers/html.js +34 -18
  27. package/dist/internal/stringify/handlers/img.js +1 -3
  28. package/dist/internal/stringify/handlers/li.js +18 -9
  29. package/dist/internal/stringify/handlers/mdc.js +3 -4
  30. package/dist/internal/stringify/handlers/ol.js +12 -2
  31. package/dist/internal/stringify/handlers/p.d.ts +1 -1
  32. package/dist/internal/stringify/handlers/p.js +8 -1
  33. package/dist/internal/stringify/handlers/pre.js +20 -14
  34. package/dist/internal/stringify/handlers/strong.js +1 -3
  35. package/dist/internal/stringify/handlers/table.js +14 -5
  36. package/dist/internal/stringify/handlers/template.js +5 -2
  37. package/dist/internal/stringify/handlers/ul.js +12 -2
  38. package/dist/internal/stringify/state.js +1 -1
  39. package/dist/internal/yaml.js +1 -1
  40. package/dist/parse.d.ts +4 -4
  41. package/dist/parse.js +20 -10
  42. package/dist/plugins/alert.d.ts +1 -1
  43. package/dist/plugins/alert.js +1 -1
  44. package/dist/plugins/binding.d.ts +1 -1
  45. package/dist/plugins/binding.js +1 -3
  46. package/dist/plugins/breaks.d.ts +1 -1
  47. package/dist/plugins/breaks.js +1 -1
  48. package/dist/plugins/emoji.d.ts +1 -1
  49. package/dist/plugins/emoji.js +8 -8
  50. package/dist/plugins/footnotes.d.ts +1 -1
  51. package/dist/plugins/footnotes.js +19 -13
  52. package/dist/plugins/headings.d.ts +19 -8
  53. package/dist/plugins/headings.js +27 -19
  54. package/dist/plugins/highlight.d.ts +2 -12
  55. package/dist/plugins/highlight.js +201 -103
  56. package/dist/plugins/json-render.d.ts +1 -1
  57. package/dist/plugins/json-render.js +5 -9
  58. package/dist/plugins/math.d.ts +1 -1
  59. package/dist/plugins/math.js +4 -6
  60. package/dist/plugins/mermaid.d.ts +1 -1
  61. package/dist/plugins/mermaid.js +6 -20
  62. package/dist/plugins/punctuation.d.ts +1 -1
  63. package/dist/plugins/punctuation.js +5 -6
  64. package/dist/plugins/security.d.ts +1 -1
  65. package/dist/plugins/security.js +2 -2
  66. package/dist/plugins/summary.d.ts +4 -1
  67. package/dist/plugins/syntax.d.ts +49 -0
  68. package/dist/plugins/syntax.js +558 -0
  69. package/dist/plugins/task-list.d.ts +2 -2
  70. package/dist/plugins/task-list.js +11 -8
  71. package/dist/plugins/toc.d.ts +3 -1
  72. package/dist/plugins/toc.js +1 -1
  73. package/dist/types.d.ts +57 -12
  74. package/dist/utils/comark.tmLanguage.d.ts +335 -0
  75. package/dist/utils/comark.tmLanguage.js +597 -0
  76. package/dist/utils/helpers.d.ts +16 -4
  77. package/dist/utils/helpers.js +16 -6
  78. package/dist/utils/index.d.ts +5 -0
  79. package/dist/utils/index.js +25 -3
  80. package/package.json +40 -40
  81. package/skills/comark/references/rendering-svelte.md +51 -7
  82. package/dist/internal/stringify/indent.d.ts +0 -5
  83. package/dist/internal/stringify/indent.js +0 -9
  84. package/dist/vite.d.ts +0 -1
  85. package/dist/vite.js +0 -1
  86. package/skills/skills/comark/AGENTS.md +0 -261
  87. package/skills/skills/comark/SKILL.md +0 -489
  88. package/skills/skills/comark/references/markdown-syntax.md +0 -599
  89. package/skills/skills/comark/references/parsing-ast.md +0 -378
  90. package/skills/skills/comark/references/rendering-react.md +0 -445
  91. package/skills/skills/comark/references/rendering-svelte.md +0 -453
  92. package/skills/skills/comark/references/rendering-vue.md +0 -462
  93. /package/skills/{skills/migrate-mdc-to-comark → migrate-mdc-to-comark}/SKILL.md +0 -0
@@ -7,6 +7,7 @@ import type { DumpOptions } from 'js-yaml';
7
7
  export declare function parseFrontmatter(content: string): {
8
8
  content: string;
9
9
  data: Record<string, any>;
10
+ frontmatterText: string;
10
11
  };
11
12
  /**
12
13
  * Render frontmatter to content
@@ -9,11 +9,12 @@ const CR = '\r';
9
9
  */
10
10
  export function parseFrontmatter(content) {
11
11
  let data = {};
12
+ let frontmatter = '';
12
13
  if (content.startsWith(FRONTMATTER_DELIMITER_DEFAULT)) {
13
14
  const idx = content.indexOf(LF + FRONTMATTER_DELIMITER_DEFAULT);
14
15
  if (idx !== -1) {
15
16
  const hasCarriageReturn = content[idx - 1] === CR;
16
- const frontmatter = content.slice(4, idx - (hasCarriageReturn ? 1 : 0));
17
+ frontmatter = content.slice(4, idx - (hasCarriageReturn ? 1 : 0));
17
18
  if (frontmatter) {
18
19
  data = parseYaml(frontmatter);
19
20
  content = content.slice(idx + 4 + (hasCarriageReturn ? 1 : 0));
@@ -23,6 +24,7 @@ export function parseFrontmatter(content) {
23
24
  return {
24
25
  content,
25
26
  data,
27
+ frontmatterText: frontmatter,
26
28
  };
27
29
  }
28
30
  /**
@@ -33,7 +35,7 @@ export function parseFrontmatter(content) {
33
35
  */
34
36
  export function renderFrontmatter(data, content, yamlOptions) {
35
37
  if (!data || Object.keys(data).length === 0) {
36
- return (content?.trim() || '');
38
+ return content?.trim() || '';
37
39
  }
38
40
  const fm = stringifyYaml(data, yamlOptions).trim();
39
41
  if (content) {
@@ -19,10 +19,33 @@ export function autoCloseMarkdown(markdown) {
19
19
  let inFrontmatter = false;
20
20
  let inBlockMath = false;
21
21
  let tableStart = -1;
22
+ // Tag name when inside a raw-text HTML element (`<style>`, `<script>`,
23
+ // `<pre>`, `<textarea>`). Their bodies must be passed through verbatim —
24
+ // any `::root`/`**` markers there are CSS/JS/text, not Comark/markdown.
25
+ let inRawTextElement = null;
26
+ const RAW_TEXT_OPEN_RE = /^<(script|pre|style|textarea)(\s|>|$)/i;
22
27
  const componentStack = [];
23
28
  for (let idx = 0; idx < n; idx++) {
24
29
  const line = lines[idx];
25
30
  const trimmed = line.trim();
31
+ // Raw-text HTML element: skip all line-level processing inside its body,
32
+ // and update the open/close state. Open and close can sit on the same
33
+ // line (e.g. `<style>body { ... }</style>` inline).
34
+ if (inRawTextElement) {
35
+ const closeRe = new RegExp(`</${inRawTextElement}\\s*>`, 'i');
36
+ if (closeRe.test(line))
37
+ inRawTextElement = null;
38
+ continue;
39
+ }
40
+ const rawTextMatch = trimmed.match(RAW_TEXT_OPEN_RE);
41
+ if (rawTextMatch) {
42
+ const tag = rawTextMatch[1].toLowerCase();
43
+ // Stay "inside" only if the close tag isn't already on this line.
44
+ const closeRe = new RegExp(`</${tag}\\s*>`, 'i');
45
+ if (!closeRe.test(line))
46
+ inRawTextElement = tag;
47
+ continue;
48
+ }
26
49
  // Frontmatter: only starts at document line 0
27
50
  if (idx === 0 && trimmed === '---') {
28
51
  inFrontmatter = true;
@@ -77,11 +100,22 @@ export function autoCloseMarkdown(markdown) {
77
100
  let nameEnd = colonCount;
78
101
  while (nameEnd < trimmed.length) {
79
102
  const c = trimmed[nameEnd];
80
- if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '$' || c === '.' || c === '-' || c === '_'))
103
+ if (!((c >= 'a' && c <= 'z') ||
104
+ (c >= 'A' && c <= 'Z') ||
105
+ (c >= '0' && c <= '9') ||
106
+ c === '$' ||
107
+ c === '.' ||
108
+ c === '-' ||
109
+ c === '_'))
81
110
  break;
82
111
  nameEnd++;
83
112
  }
84
- componentStack.push({ depth: colonCount, name: trimmed.slice(colonCount, nameEnd), indent, hasYamlProps: false });
113
+ componentStack.push({
114
+ depth: colonCount,
115
+ name: trimmed.slice(colonCount, nameEnd),
116
+ indent,
117
+ hasYamlProps: false,
118
+ });
85
119
  }
86
120
  else if (colonCount === trimmed.length && componentStack.length > 0) {
87
121
  const top = componentStack[componentStack.length - 1];
@@ -136,14 +170,14 @@ export function autoCloseMarkdown(markdown) {
136
170
  for (let i = 0; i < propsContent.length; i++) {
137
171
  if (propsContent[i] === '"')
138
172
  dq++;
139
- if (propsContent[i] === '\'')
173
+ if (propsContent[i] === "'")
140
174
  sq++;
141
175
  }
142
176
  let braceClose = '';
143
177
  if (dq % 2 === 1)
144
178
  braceClose += '"';
145
179
  if (sq % 2 === 1)
146
- braceClose += '\'';
180
+ braceClose += "'";
147
181
  result += braceClose + '}';
148
182
  }
149
183
  if (componentStack.length > 0) {
@@ -179,7 +213,8 @@ function closeInlineMarkersLinear(line) {
179
213
  // Count markers by scanning
180
214
  let asteriskCount = 0;
181
215
  let underscoreCount = 0;
182
- let tildeCount = 0; // Count individual tildes
216
+ let doubleTildeCount = 0; // Count ~~ occurrences (GFM strikethrough delimiter)
217
+ let singleTildeCount = 0; // Count standalone ~ (not part of ~~)
183
218
  let backtickCount = 0;
184
219
  let dollarCount = 0; // Count $ for math
185
220
  let dollarPairCount = 0; // Count $$ pairs for block math
@@ -269,7 +304,13 @@ function closeInlineMarkersLinear(line) {
269
304
  }
270
305
  }
271
306
  else if (ch === '~') {
272
- tildeCount++;
307
+ if (i + 1 < len && line[i + 1] === '~') {
308
+ doubleTildeCount++;
309
+ i++; // Skip second tilde since we counted the pair
310
+ }
311
+ else {
312
+ singleTildeCount++;
313
+ }
273
314
  }
274
315
  else if (ch === '`') {
275
316
  backtickCount++;
@@ -299,8 +340,11 @@ function closeInlineMarkersLinear(line) {
299
340
  // Check if line starts with more than 3 asterisks (e.g., ****)
300
341
  if (!(line[3] === '*')) {
301
342
  // Check if marker at end with no content
302
- if (!(contentEnd >= 3 && line[contentEnd - 1] === '*' && line[contentEnd - 2] === '*'
303
- && line[contentEnd - 3] === '*' && (contentEnd === 3 || line[contentEnd - 4] === ' '))) {
343
+ if (!(contentEnd >= 3 &&
344
+ line[contentEnd - 1] === '*' &&
345
+ line[contentEnd - 2] === '*' &&
346
+ line[contentEnd - 3] === '*' &&
347
+ (contentEnd === 3 || line[contentEnd - 4] === ' '))) {
304
348
  closingSuffix = '***';
305
349
  }
306
350
  }
@@ -333,9 +377,9 @@ function closeInlineMarkersLinear(line) {
333
377
  if (!allPaired) {
334
378
  // Check if line ends with word (not just a closing marker)
335
379
  const lastChar = line[contentEnd - 1];
336
- const endsWithWord = (lastChar >= 'a' && lastChar <= 'z')
337
- || (lastChar >= 'A' && lastChar <= 'Z')
338
- || (lastChar >= '0' && lastChar <= '9');
380
+ const endsWithWord = (lastChar >= 'a' && lastChar <= 'z') ||
381
+ (lastChar >= 'A' && lastChar <= 'Z') ||
382
+ (lastChar >= '0' && lastChar <= '9');
339
383
  if (!hasCompleteBoldPair || endsWithWord) {
340
384
  closingSuffix = '**';
341
385
  if (hasTrailingSpace && !endsWithMarker) {
@@ -375,8 +419,7 @@ function closeInlineMarkersLinear(line) {
375
419
  // Check marker at end with no content
376
420
  // Only skip if it's truly isolated (e.g., "input *")
377
421
  // Don't skip if there are complete pairs before it (e.g., "input **bold** *")
378
- const markerAtEnd = contentEnd >= 1 && line[contentEnd - 1] === '*'
379
- && (contentEnd === 1 || line[contentEnd - 2] === ' ');
422
+ const markerAtEnd = contentEnd >= 1 && line[contentEnd - 1] === '*' && (contentEnd === 1 || line[contentEnd - 2] === ' ');
380
423
  if (!markerAtEnd || asteriskCount > 1) {
381
424
  closingSuffix = '*';
382
425
  const endsWithMarker = line[contentEnd - 1] === '*';
@@ -427,8 +470,7 @@ function closeInlineMarkersLinear(line) {
427
470
  }
428
471
  if (validItalic) {
429
472
  // Check marker at end with no content
430
- const markerAtEnd = contentEnd >= 1 && line[contentEnd - 1] === '_'
431
- && (contentEnd === 1 || line[contentEnd - 2] === ' ');
473
+ const markerAtEnd = contentEnd >= 1 && line[contentEnd - 1] === '_' && (contentEnd === 1 || line[contentEnd - 2] === ' ');
432
474
  if (!markerAtEnd) {
433
475
  closingSuffix = '_';
434
476
  const endsWithMarker = line[contentEnd - 1] === '_';
@@ -438,22 +480,18 @@ function closeInlineMarkersLinear(line) {
438
480
  }
439
481
  }
440
482
  }
441
- // Check ~~ (strikethrough)
442
- if (!closingSuffix && tildeCount >= 2) {
443
- const remainder = tildeCount % 4;
444
- if (remainder === 2) {
445
- // Two tildes unclosed, close with ~~
446
- closingSuffix = '~~';
447
- if (hasTrailingSpace)
448
- shouldTrim = true;
449
- }
450
- else if (remainder > 2 && remainder < 4) {
451
- // Partial marker like ~~text~ (3 tildes), need 1 more
452
- const needed = 4 - remainder;
453
- closingSuffix = '~'.repeat(needed);
454
- if (hasTrailingSpace)
455
- shouldTrim = true;
456
- }
483
+ // Check ~~ (strikethrough) and ~ (single-tilde) separately so that paired
484
+ // singles like ~Hello~ are left alone while ~~text and ~Hello both close.
485
+ if (!closingSuffix && doubleTildeCount % 2 === 1) {
486
+ // A trailing single ~ after an open ~~ is a partial closer (~~text~)
487
+ closingSuffix = singleTildeCount === 1 ? '~' : '~~';
488
+ if (hasTrailingSpace)
489
+ shouldTrim = true;
490
+ }
491
+ else if (!closingSuffix && singleTildeCount % 2 === 1) {
492
+ closingSuffix = '~';
493
+ if (hasTrailingSpace)
494
+ shouldTrim = true;
457
495
  }
458
496
  // Check ` (code)
459
497
  if (!closingSuffix && backtickCount % 2 === 1) {
@@ -87,12 +87,10 @@ export function closeTables(markdown) {
87
87
  const generateSeparator = () => '| ' + Array(columnCount).fill('---').join(' | ') + ' |';
88
88
  // Check if separator exists (including incomplete ones with just :)
89
89
  const secondLine = end - start >= 1 ? lines[start + 1].trim() : '';
90
- const hasSeparator = secondLine.startsWith('|')
91
- && (secondLine.includes('-') || secondLine.includes(':'));
90
+ const hasSeparator = secondLine.startsWith('|') && (secondLine.includes('-') || secondLine.includes(':'));
92
91
  // Handle last line
93
92
  const lastLine = lines[end].trim();
94
- const isSeparator = lastLine.startsWith('|')
95
- && (lastLine.includes('-') || lastLine.includes(':'));
93
+ const isSeparator = lastLine.startsWith('|') && (lastLine.includes('-') || lastLine.includes(':'));
96
94
  if (isSeparator) {
97
95
  // Parse and complete separator cells
98
96
  const sepCells = parseCells(lastLine);
@@ -147,11 +145,16 @@ export function closeTables(markdown) {
147
145
  const refWidths = parseCellWidths(refRow);
148
146
  const cells = parseCells(lastLine);
149
147
  // Rebuild with padding
150
- lines[end] = '| ' + cells.map((cell, i) => {
151
- const targetWidth = refWidths[i] || cell.length + 2;
152
- const padding = ' '.repeat(Math.max(0, targetWidth - cell.length - 2));
153
- return cell + padding;
154
- }).join(' | ') + ' |';
148
+ lines[end] =
149
+ '| ' +
150
+ cells
151
+ .map((cell, i) => {
152
+ const targetWidth = refWidths[i] || cell.length + 2;
153
+ const padding = ' '.repeat(Math.max(0, targetWidth - cell.length - 2));
154
+ return cell + padding;
155
+ })
156
+ .join(' | ') +
157
+ ' |';
155
158
  }
156
159
  // Add separator if missing
157
160
  if (!hasSeparator) {
@@ -28,15 +28,11 @@ export function applyAutoUnwrap(node) {
28
28
  }
29
29
  // Check if we have exactly one paragraph child (and possibly empty text nodes)
30
30
  if (nonEmptyChildren.length > 1 || typeof nonEmptyChildren[0] === 'string' || nonEmptyChildren[0][0] !== 'p') {
31
- return [
32
- tag,
33
- props,
34
- ...children.map((child) => applyAutoUnwrap(child)),
35
- ];
31
+ return [tag, props, ...children.map((child) => applyAutoUnwrap(child))];
36
32
  }
37
- return [
38
- tag,
39
- props,
40
- ...nonEmptyChildren[0].slice(2),
41
- ];
33
+ // Lift the paragraph's attrs onto the parent so trailing `{attr}` survives the unwrap.
34
+ // Parent attrs take precedence so explicit component props aren't overridden.
35
+ const paragraphAttrs = nonEmptyChildren[0][1];
36
+ const mergedProps = paragraphAttrs && Object.keys(paragraphAttrs).length > 0 ? { ...paragraphAttrs, ...props } : props;
37
+ return [tag, mergedProps, ...nonEmptyChildren[0].slice(2)];
42
38
  }
@@ -1,11 +1,10 @@
1
- // BASED ON https://github.com/serkodev/markdown-exit/blob/fe1351070a5841426223ab4a0a5c7874ba2b1257/packages/markdown-exit/src/parser/block/rules/html_block.ts
1
+ // Standard CommonMark html_block rule — see
2
+ // https://spec.commonmark.org/0.30/#html-blocks
3
+ //
4
+ // 7 sequences in priority order, each: [opener regex, closer regex, can-terminate-paragraph]
2
5
  import block_names from "./html_blocks.js";
3
6
  import { HTML_OPEN_CLOSE_TAG_RE } from "./html_re.js";
4
- // An array of opening and corresponding closing sequences for html tags,
5
- // last argument defines whether it can terminate a paragraph or not
6
- //
7
7
  const HTML_SEQUENCES = [
8
- [new RegExp(`${HTML_OPEN_CLOSE_TAG_RE.source}\\s*$`), /^<\/[^>]+>$/, true],
9
8
  [/^<(script|pre|style|textarea)(?=(\s|>|$))/i, /<\/(script|pre|style|textarea)>/i, true],
10
9
  [/^<!--/, /-->/, true],
11
10
  [/^<\?/, /\?>/, true],
@@ -17,10 +16,9 @@ const HTML_SEQUENCES = [
17
16
  export default function html_block(state, startLine, endLine, silent) {
18
17
  let pos = state.bMarks[startLine] + state.tShift[startLine];
19
18
  let max = state.eMarks[startLine];
20
- // if it's indented more than 3 spaces, it should be a code block
21
19
  if (state.sCount[startLine] - state.blkIndent >= 4)
22
20
  return false;
23
- if (state.src.charCodeAt(pos) !== 0x3C /* < */)
21
+ if (state.src.charCodeAt(pos) !== 0x3c /* < */)
24
22
  return false;
25
23
  let lineText = state.src.slice(pos, max);
26
24
  let i = 0;
@@ -30,18 +28,14 @@ export default function html_block(state, startLine, endLine, silent) {
30
28
  }
31
29
  if (i === HTML_SEQUENCES.length)
32
30
  return false;
33
- if (silent) {
34
- // true if this sequence can be a terminator, false otherwise
31
+ if (silent)
35
32
  return HTML_SEQUENCES[i][2];
36
- }
37
33
  let nextLine = startLine + 1;
38
- // If we are here - we detected HTML block.
39
- // Let's roll down till block end.
40
- if (i !== 0 && !HTML_SEQUENCES[i][1].test(lineText)) {
34
+ // Walk forward until the closer regex matches or we hit a blank line.
35
+ if (!HTML_SEQUENCES[i][1].test(lineText)) {
41
36
  for (; nextLine < endLine; nextLine++) {
42
- if (state.sCount[nextLine] < state.blkIndent) {
37
+ if (state.sCount[nextLine] < state.blkIndent)
43
38
  break;
44
- }
45
39
  pos = state.bMarks[nextLine] + state.tShift[nextLine];
46
40
  max = state.eMarks[nextLine];
47
41
  lineText = state.src.slice(pos, max);
@@ -53,7 +47,7 @@ export default function html_block(state, startLine, endLine, silent) {
53
47
  }
54
48
  }
55
49
  state.line = nextLine;
56
- const token = lineText.startsWith('</') ? state.push('html_block_close', '', -1) : state.push('html_block', '', 1);
50
+ const token = state.push('html_block', '', 1);
57
51
  token.map = [startLine, nextLine];
58
52
  token.content = state.getLines(startLine, nextLine, state.blkIndent, true);
59
53
  return true;
@@ -9,22 +9,18 @@ function isLinkClose(str) {
9
9
  function isLetter(ch) {
10
10
  /* eslint no-bitwise:0 */
11
11
  const lc = ch | 0x20; // to lower case
12
- return (lc >= 0x61 /* a */) && (lc <= 0x7A /* z */);
12
+ return lc >= 0x61 /* a */ && lc <= 0x7a; /* z */
13
13
  }
14
14
  export default function html_inline(state, silent) {
15
15
  // Check start
16
16
  const max = state.posMax;
17
17
  const pos = state.pos;
18
- if (state.src.charCodeAt(pos) !== 0x3C
19
- || /* < */ pos + 2 >= max) {
18
+ if (state.src.charCodeAt(pos) !== 0x3c || /* < */ pos + 2 >= max) {
20
19
  return false;
21
20
  }
22
21
  // Quick fail on second char
23
22
  const ch = state.src.charCodeAt(pos + 1);
24
- if (ch !== 0x21
25
- && /* ! */ ch !== 0x3F
26
- && /* ? */ ch !== 0x2F
27
- && /* / */ !isLetter(ch)) {
23
+ if (ch !== 0x21 && /* ! */ ch !== 0x3f && /* ? */ ch !== 0x2f && /* / */ !isLetter(ch)) {
28
24
  return false;
29
25
  }
30
26
  const match = state.src.slice(pos).match(HTML_TAG_RE);
@@ -1,7 +1,7 @@
1
1
  // Regexps to match html elements
2
2
  const attr_name = '[a-zA-Z_:][a-zA-Z0-9:._-]*';
3
3
  const unquoted = '[^"\'=<>`\\x00-\\x20]+';
4
- const single_quoted = '\'[^\']*\'';
4
+ const single_quoted = "'[^']*'";
5
5
  const double_quoted = '"[^"]*"';
6
6
  const attr_value = `(?:${unquoted}|${single_quoted}|${double_quoted})`;
7
7
  const attribute = `(?:\\s+${attr_name}(?:\\s*=\\s*${attr_value})?)`;
@@ -1,4 +1,5 @@
1
1
  import type { ComarkNode } from 'comark';
2
+ export declare const VOID_ELEMENTS: Set<string>;
2
3
  interface HtmlTagInfo {
3
4
  tag: string;
4
5
  attrs: Record<string, unknown>;
@@ -1,7 +1,19 @@
1
1
  import { Parser } from 'htmlparser2';
2
- const VOID_ELEMENTS = new Set([
3
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
4
- 'link', 'meta', 'param', 'source', 'track', 'wbr',
2
+ export const VOID_ELEMENTS = new Set([
3
+ 'area',
4
+ 'base',
5
+ 'br',
6
+ 'col',
7
+ 'embed',
8
+ 'hr',
9
+ 'img',
10
+ 'input',
11
+ 'link',
12
+ 'meta',
13
+ 'param',
14
+ 'source',
15
+ 'track',
16
+ 'wbr',
5
17
  ]);
6
18
  function attribsToComarkAttrs(attribs, isInline = false) {
7
19
  const attrs = {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Parse `component-name [content] {.params}` from the trailing portion of a block fence line.
3
+ */
4
+ export declare function parseBlockParams(str: string): {
5
+ name: string;
6
+ content?: string;
7
+ props?: [string, string][];
8
+ remaining?: string;
9
+ };
@@ -0,0 +1,48 @@
1
+ import { kebabCase } from "../../../utils/index.js";
2
+ import { parseBracketContent } from "./brackets.js";
3
+ import { searchProps } from "./props.js";
4
+ const RE_BLOCK_NAME = /^[a-z$][$\w.-]*/i;
5
+ /**
6
+ * Parse `component-name [content] {.params}` from the trailing portion of a block fence line.
7
+ */
8
+ export function parseBlockParams(str) {
9
+ str = str.trim();
10
+ if (!str)
11
+ return { name: '' };
12
+ const name = str.match(RE_BLOCK_NAME)?.[0];
13
+ if (!name)
14
+ throw new Error(`Invalid block params: ${str}`);
15
+ let remaining = str.slice(name.length).trim();
16
+ let content;
17
+ let props;
18
+ let unparsedRemaining;
19
+ if (remaining.startsWith('[')) {
20
+ const result = parseBracketContent(remaining, 0);
21
+ if (result) {
22
+ content = result.content;
23
+ remaining = remaining.slice(result.endIndex).trim();
24
+ }
25
+ }
26
+ if (remaining.startsWith('{')) {
27
+ const propsResult = searchProps(remaining, 0);
28
+ if (propsResult) {
29
+ props = propsResult.props;
30
+ const afterProps = remaining.slice(propsResult.index).trim();
31
+ if (afterProps)
32
+ unparsedRemaining = afterProps;
33
+ }
34
+ }
35
+ else if (remaining) {
36
+ unparsedRemaining = remaining;
37
+ }
38
+ const result = {
39
+ name: kebabCase(name),
40
+ };
41
+ if (content !== undefined)
42
+ result.content = content;
43
+ if (props !== undefined)
44
+ result.props = props;
45
+ if (unparsedRemaining)
46
+ result.remaining = unparsedRemaining;
47
+ return result;
48
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Parse content within square brackets `[content]`.
3
+ * Returns the content (without the brackets) and the index just past the closing `]`.
4
+ */
5
+ export declare function parseBracketContent(str: string, startIndex: number): {
6
+ content: string;
7
+ endIndex: number;
8
+ } | null;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Parse content within square brackets `[content]`.
3
+ * Returns the content (without the brackets) and the index just past the closing `]`.
4
+ */
5
+ export function parseBracketContent(str, startIndex) {
6
+ if (str[startIndex] !== '[')
7
+ return null;
8
+ let index = startIndex + 1;
9
+ while (index < str.length) {
10
+ if (str[index] === '\\' && index + 1 < str.length) {
11
+ index += 2;
12
+ continue;
13
+ }
14
+ if (str[index] === ']') {
15
+ return { content: str.slice(startIndex + 1, index), endIndex: index + 1 };
16
+ }
17
+ index += 1;
18
+ }
19
+ return null;
20
+ }
@@ -0,0 +1,5 @@
1
+ export declare function parseProps(content: string): [string, string][] | undefined;
2
+ export declare function searchProps(content: string, index?: number): {
3
+ props: [string, string][];
4
+ index: number;
5
+ } | undefined;
@@ -0,0 +1,119 @@
1
+ const bracketPairs = {
2
+ '[': ']',
3
+ '{': '}',
4
+ '(': ')',
5
+ };
6
+ const quotePairs = {
7
+ "'": "'",
8
+ '"': '"',
9
+ '`': '`',
10
+ };
11
+ export function parseProps(content) {
12
+ content = content.trim();
13
+ if (!content)
14
+ return undefined;
15
+ const props = searchProps(content);
16
+ if (!props)
17
+ throw new Error(`Invalid props: \`${content}\``);
18
+ if (props.index !== content.length)
19
+ throw new Error(`Invalid props: \`${content}\`, expected end \`}\` but got \`${content.slice(props.index)}\``);
20
+ return props.props;
21
+ }
22
+ export function searchProps(content, index = 0) {
23
+ if (content[index] !== '{')
24
+ throw new Error(`Invalid props, expected \`{\` but got '${content[index]}'`);
25
+ const props = [];
26
+ // Skip Vue mustache `{{ }}` syntax
27
+ if (content[index + 1] === '{')
28
+ return undefined;
29
+ index += 1;
30
+ while (index < content.length) {
31
+ if (content[index] === '\\') {
32
+ index += 2;
33
+ }
34
+ else if (content[index] === '}') {
35
+ index += 1;
36
+ break;
37
+ }
38
+ else if (content[index] === ' ') {
39
+ index += 1;
40
+ }
41
+ else if (content[index] === '.') {
42
+ index += 1;
43
+ props.push(['class', searchUntil(' #.}')]);
44
+ }
45
+ else if (content[index] === '#') {
46
+ index += 1;
47
+ props.push(['id', searchUntil(' #.}')]);
48
+ }
49
+ else {
50
+ const start = index;
51
+ while (index < content.length) {
52
+ index += 1;
53
+ if (' }='.includes(content[index]))
54
+ break;
55
+ }
56
+ const char = content[index];
57
+ if (start !== index) {
58
+ const key = content.slice(start, index).trim();
59
+ if (char === '=') {
60
+ index += 1;
61
+ props.push([key, searchValue()]);
62
+ }
63
+ else {
64
+ props.push([key, 'true']);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ function searchUntil(str) {
70
+ const start = index;
71
+ while (index < content.length) {
72
+ index += 1;
73
+ if (content[index] === '\\')
74
+ index += 2;
75
+ if (str.includes(content[index]))
76
+ break;
77
+ }
78
+ return content.slice(start, index);
79
+ }
80
+ function searchValue() {
81
+ const start = index;
82
+ if (content[index] in bracketPairs) {
83
+ searchBracket(bracketPairs[content[index]]);
84
+ index += 1;
85
+ return content.slice(start, index);
86
+ }
87
+ else if (content[index] in quotePairs) {
88
+ searchString(quotePairs[content[index]]);
89
+ index += 1;
90
+ return content.slice(start, index);
91
+ }
92
+ else {
93
+ return searchUntil(' }');
94
+ }
95
+ }
96
+ function searchBracket(end) {
97
+ while (index < content.length) {
98
+ index++;
99
+ if (content[index] in quotePairs)
100
+ searchString(quotePairs[content[index]]);
101
+ else if (content[index] in bracketPairs)
102
+ searchBracket(bracketPairs[content[index]]);
103
+ else if (content[index] === end)
104
+ return;
105
+ }
106
+ }
107
+ function searchString(end) {
108
+ return searchUntil(end);
109
+ }
110
+ // Strip surrounding quotes from values
111
+ props.forEach((v) => {
112
+ if (/^(['"`]).*\1$/.test(v[1]))
113
+ v[1] = v[1].slice(1, -1);
114
+ });
115
+ return {
116
+ props,
117
+ index,
118
+ };
119
+ }