comark 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +25 -1
  2. package/dist/context.d.ts +78 -0
  3. package/dist/context.js +127 -0
  4. package/dist/devtools/bridge.d.ts +1 -0
  5. package/dist/devtools/bridge.js +1 -0
  6. package/dist/devtools/constants.d.ts +1 -0
  7. package/dist/devtools/constants.js +1 -0
  8. package/dist/devtools/renderer/dom.d.ts +1 -0
  9. package/dist/devtools/renderer/dom.js +1 -0
  10. package/dist/devtools/renderer/index.d.ts +2 -0
  11. package/dist/devtools/renderer/index.js +2 -0
  12. package/dist/devtools/renderer/output.d.ts +1 -0
  13. package/dist/devtools/renderer/output.js +1 -0
  14. package/dist/devtools/renderer/panel.d.ts +1 -0
  15. package/dist/devtools/renderer/panel.js +1 -0
  16. package/dist/devtools/renderer/styles.d.ts +1 -0
  17. package/dist/devtools/renderer/styles.js +1 -0
  18. package/dist/devtools/renderer/theme.d.ts +1 -0
  19. package/dist/devtools/renderer/theme.js +1 -0
  20. package/dist/devtools/types.d.ts +1 -0
  21. package/dist/devtools/types.js +1 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +2 -0
  24. package/dist/internal/parse/auto-close/index.js +96 -31
  25. package/dist/internal/parse/auto-unwrap.js +5 -1
  26. package/dist/internal/parse/html/html_block_rule.js +9 -15
  27. package/dist/internal/parse/html/index.d.ts +1 -0
  28. package/dist/internal/parse/html/index.js +1 -1
  29. package/dist/internal/parse/token-processor.js +70 -32
  30. package/dist/internal/stringify/attributes.d.ts +8 -1
  31. package/dist/internal/stringify/attributes.js +53 -0
  32. package/dist/internal/stringify/handlers/blockquote.js +17 -0
  33. package/dist/internal/stringify/handlers/heading.js +6 -1
  34. package/dist/internal/stringify/handlers/html.js +8 -2
  35. package/dist/internal/stringify/handlers/li.js +19 -9
  36. package/dist/internal/stringify/handlers/mdc.js +1 -1
  37. package/dist/internal/stringify/handlers/ol.js +15 -2
  38. package/dist/internal/stringify/handlers/p.js +4 -0
  39. package/dist/internal/stringify/handlers/pre.js +11 -2
  40. package/dist/internal/stringify/handlers/table.js +7 -0
  41. package/dist/internal/stringify/handlers/template.js +4 -1
  42. package/dist/internal/stringify/handlers/ul.js +11 -1
  43. package/dist/internal/stringify/state.js +13 -1
  44. package/dist/parse.d.ts +4 -4
  45. package/dist/parse.js +7 -3
  46. package/dist/plugins/alert.d.ts +1 -1
  47. package/dist/plugins/binding.d.ts +1 -1
  48. package/dist/plugins/breaks.d.ts +1 -1
  49. package/dist/plugins/emoji.d.ts +1 -1
  50. package/dist/plugins/footnotes.d.ts +1 -1
  51. package/dist/plugins/headings.d.ts +19 -8
  52. package/dist/plugins/headings.js +25 -15
  53. package/dist/plugins/highlight.d.ts +1 -1
  54. package/dist/plugins/highlight.js +4 -2
  55. package/dist/plugins/json-render.d.ts +1 -1
  56. package/dist/plugins/math.d.ts +1 -1
  57. package/dist/plugins/mermaid.d.ts +1 -1
  58. package/dist/plugins/punctuation.d.ts +1 -1
  59. package/dist/plugins/security.d.ts +12 -1
  60. package/dist/plugins/security.js +13 -6
  61. package/dist/plugins/summary.d.ts +4 -1
  62. package/dist/plugins/syntax.d.ts +1 -1
  63. package/dist/plugins/syntax.js +95 -36
  64. package/dist/plugins/task-list.d.ts +1 -1
  65. package/dist/plugins/toc.d.ts +3 -1
  66. package/dist/render.d.ts +6 -2
  67. package/dist/render.js +2 -2
  68. package/dist/types.d.ts +61 -12
  69. package/dist/utils/helpers.d.ts +16 -4
  70. package/dist/utils/helpers.js +15 -3
  71. package/dist/utils/index.d.ts +3 -1
  72. package/dist/utils/index.js +30 -14
  73. package/package.json +6 -3
  74. package/skills/comark/references/rendering-svelte.md +51 -7
  75. package/dist/internal/stringify/indent.d.ts +0 -1
  76. package/dist/internal/stringify/indent.js +0 -1
  77. package/dist/vite.d.ts +0 -1
  78. package/dist/vite.js +0 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![Documentation](https://img.shields.io/badge/Documentation-black?logo=readme&logoColor=white)](https://comark.dev)
9
9
  [![license](https://img.shields.io/github/license/comarkdown/comark?color=black)](https://github.com/comarkdown/comark/blob/main/LICENSE)
10
10
 
11
- A high-performance markdown parser and renderer with Vue, React, Svelte, HTML and ANSI terminal.
11
+ A high-performance markdown parser and renderer with Vue, React, Svelte, Angular, HTML and ANSI terminal.
12
12
 
13
13
  ## Features
14
14
 
@@ -80,6 +80,30 @@ pnpm add @comark/svelte katex
80
80
  <Comark markdown={chatMessage} components={{ math: Math }} plugins={[math()]} />
81
81
  ```
82
82
 
83
+ ### Angular
84
+
85
+ ```bash
86
+ npm install @comark/angular katex
87
+ # or
88
+ pnpm add @comark/angular katex
89
+ ```
90
+
91
+ ```typescript
92
+ import { Component } from '@angular/core'
93
+ import { ComarkComponent } from '@comark/angular'
94
+ import math, { Math } from '@comark/angular/plugins/math'
95
+
96
+ @Component({
97
+ selector: 'app-chat',
98
+ standalone: true,
99
+ imports: [ComarkComponent],
100
+ template: `<comark [markdown]="chatMessage" [components]="{ Math }" [plugins]="[math()]" />`,
101
+ })
102
+ export class ChatComponent {
103
+ chatMessage = ...
104
+ }
105
+ ```
106
+
83
107
  ### HTML (No Framework)
84
108
 
85
109
  ```bash
@@ -0,0 +1,78 @@
1
+ import type { ComarkNode, ComarkTree } from './types.ts';
2
+ /**
3
+ * A patch describes a surgical mutation of a {@link ComarkTree}.
4
+ *
5
+ * `path` is a node index path into `tree.nodes`: the first segment indexes
6
+ * into `tree.nodes`, each subsequent segment indexes into the *children* of
7
+ * the addressed element. Because `ComarkElement` is `[tag, attrs, ...children]`,
8
+ * child index `i` is resolved against array slot `i + 2` internally — callers
9
+ * always work in plain child indices.
10
+ *
11
+ * @example `{ op: 'replace', path: [2, 0], node: 'updated' }`
12
+ * replaces the first child of the third top-level node.
13
+ */
14
+ export type ComarkPatch = {
15
+ op: 'replace';
16
+ path: number[];
17
+ node: ComarkNode;
18
+ } | {
19
+ op: 'insert';
20
+ path: number[];
21
+ node: ComarkNode;
22
+ } | {
23
+ op: 'remove';
24
+ path: number[];
25
+ } | {
26
+ op: 'meta';
27
+ meta: Record<string, unknown>;
28
+ } | {
29
+ op: 'frontmatter';
30
+ frontmatter: Record<string, unknown>;
31
+ } | {
32
+ op: 'data';
33
+ data: Record<string, unknown>;
34
+ };
35
+ /** A single live document: the per-id handle returned by `context.get(id)`. */
36
+ export interface ComarkDocument {
37
+ /** The current tree. Replaced wholesale on `set`, structurally on `patch`. */
38
+ readonly tree: ComarkTree;
39
+ /** Replace the whole tree (e.g. an HMR re-parse or an agent rewrite). */
40
+ set(tree: ComarkTree): void;
41
+ /** Apply one or more patches against the current tree (structural sharing). */
42
+ patch(patch: ComarkPatch | ComarkPatch[]): void;
43
+ /** Subscribe to tree changes. Returns the cleanup function. */
44
+ listen(fn: (tree: ComarkTree) => void): (clear?: boolean) => void;
45
+ }
46
+ /**
47
+ * Ambient registry a `ComarkRenderer` subscribes to so external sources can
48
+ * drive a mounted document by `id`. A renderer calls `get(id).listen(fn)` on
49
+ * mount and the returned cleanup on unmount; drivers (HMR, devtools, collab,
50
+ * agents) call `get(id).set` / `.patch` to push new trees to every renderer
51
+ * with that id.
52
+ *
53
+ * The context is opt-in: a renderer only wires up when `globalThis.comarkContext`
54
+ * exists, so it costs one global lookup when absent and tree-shakes out of any
55
+ * build that never sets it.
56
+ */
57
+ /** Document lifecycle event emitted by the context. */
58
+ export interface ComarkContextEvent {
59
+ event: 'create' | 'remove';
60
+ id: string;
61
+ tree: ComarkTree;
62
+ }
63
+ export interface ComarkContext {
64
+ /** Get the document for `id`, creating it (with `initial`) on first access. */
65
+ get(id: string, initial?: ComarkTree): ComarkDocument;
66
+ /** Ids currently tracked — for devtools enumeration. */
67
+ keys(): string[];
68
+ /** Listen for documents being created or removed. Returns the cleanup. */
69
+ listen(fn: (e: ComarkContextEvent) => void): () => void;
70
+ }
71
+ declare global {
72
+ var comarkContext: ComarkContext | undefined;
73
+ }
74
+ /**
75
+ * Create a {@link ComarkContext} and (by default) install it on
76
+ * `globalThis.comarkContext`. Returns the context.
77
+ */
78
+ export declare function createComarkContext(install?: boolean): ComarkContext;
@@ -0,0 +1,127 @@
1
+ function emptyTree() {
2
+ return { nodes: [], frontmatter: {}, meta: {} };
3
+ }
4
+ /**
5
+ * Apply a node-level patch to a node list, returning a **new** list with
6
+ * structural sharing: only the arrays along `path` are cloned, untouched
7
+ * siblings and branches keep their references so framework identity checks
8
+ * only re-render what actually changed.
9
+ */
10
+ function patchNodeList(nodes, path, patch) {
11
+ const [index, ...rest] = path;
12
+ const next = nodes.slice();
13
+ if (rest.length === 0) {
14
+ if (patch.op === 'replace') {
15
+ if (index < 0 || index >= next.length) {
16
+ throw new Error(`Comark patch: cannot replace out-of-range index [${index}]`);
17
+ }
18
+ next[index] = patch.node;
19
+ }
20
+ else if (patch.op === 'insert') {
21
+ next.splice(index, 0, patch.node);
22
+ }
23
+ else if (patch.op === 'remove') {
24
+ if (index < 0 || index >= next.length) {
25
+ throw new Error(`Comark patch: cannot remove out-of-range index [${index}]`);
26
+ }
27
+ next.splice(index, 1);
28
+ }
29
+ return next;
30
+ }
31
+ const target = next[index];
32
+ if (!Array.isArray(target) || target[0] === null) {
33
+ throw new Error(`Comark patch: path segment [${index}] does not point to an element`);
34
+ }
35
+ const element = target;
36
+ const children = element.slice(2);
37
+ next[index] = [element[0], element[1], ...patchNodeList(children, rest, patch)];
38
+ return next;
39
+ }
40
+ function applyPatch(current, patch) {
41
+ switch (patch.op) {
42
+ case 'meta':
43
+ return { ...current, meta: { ...current.meta, ...patch.meta } };
44
+ case 'frontmatter':
45
+ return { ...current, frontmatter: { ...current.frontmatter, ...patch.frontmatter } };
46
+ case 'data':
47
+ // @ts-expect-error - patch.data is a plain object
48
+ return { ...current, data: { ...current.data, ...patch.data } };
49
+ default: {
50
+ if (!patch.path.length) {
51
+ throw new Error(`Comark patch "${patch.op}" requires a non-empty path`);
52
+ }
53
+ return { ...current, nodes: patchNodeList(current.nodes, patch.path, patch) };
54
+ }
55
+ }
56
+ }
57
+ function createDocument(initial, onEmpty) {
58
+ let tree = initial;
59
+ const listeners = new Set();
60
+ const emit = () => listeners.forEach((fn) => fn(tree));
61
+ return {
62
+ get tree() {
63
+ return tree;
64
+ },
65
+ set(next) {
66
+ tree = next;
67
+ emit();
68
+ },
69
+ patch(patch) {
70
+ const patches = Array.isArray(patch) ? patch : [patch];
71
+ for (const p of patches)
72
+ tree = applyPatch(tree, p);
73
+ emit();
74
+ },
75
+ listen(fn) {
76
+ listeners.add(fn);
77
+ return (clear = false) => {
78
+ if (!listeners.has(fn)) {
79
+ return;
80
+ }
81
+ if (clear) {
82
+ listeners.clear();
83
+ }
84
+ else {
85
+ listeners.delete(fn);
86
+ }
87
+ // Drop the document once nobody is listening — frees ids on unmount.
88
+ if (listeners.size === 0)
89
+ onEmpty(tree);
90
+ };
91
+ },
92
+ };
93
+ }
94
+ /**
95
+ * Create a {@link ComarkContext} and (by default) install it on
96
+ * `globalThis.comarkContext`. Returns the context.
97
+ */
98
+ export function createComarkContext(install = true) {
99
+ if (install && globalThis.comarkContext) {
100
+ return globalThis.comarkContext;
101
+ }
102
+ const docs = new Map();
103
+ const lifecycle = new Set();
104
+ const emit = (e) => lifecycle.forEach((fn) => fn(e));
105
+ const ctx = {
106
+ get(id, initial) {
107
+ let doc = docs.get(id);
108
+ if (!doc) {
109
+ docs.set(id, (doc = createDocument(initial ?? emptyTree(), (tree) => {
110
+ docs.delete(id);
111
+ emit({ event: 'remove', id, tree });
112
+ })));
113
+ emit({ event: 'create', id, tree: doc.tree });
114
+ }
115
+ return doc;
116
+ },
117
+ keys: () => [...docs.keys()],
118
+ listen(fn) {
119
+ lifecycle.add(fn);
120
+ return () => lifecycle.delete(fn);
121
+ },
122
+ };
123
+ if (install)
124
+ globalThis.comarkContext = ctx;
125
+ return ctx;
126
+ }
127
+ // #endregion
@@ -0,0 +1 @@
1
+ export * from '../../src/devtools/bridge'
@@ -0,0 +1 @@
1
+ export * from '../../src/devtools/bridge.ts'
@@ -0,0 +1 @@
1
+ export * from '../../src/devtools/constants'
@@ -0,0 +1 @@
1
+ export * from '../../src/devtools/constants.ts'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/dom'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/dom.ts'
@@ -0,0 +1,2 @@
1
+ export * from '../../../src/devtools/renderer/index'
2
+ export { default } from '../../../src/devtools/renderer/index'
@@ -0,0 +1,2 @@
1
+ export * from '../../../src/devtools/renderer/index.ts'
2
+ export { default } from '../../../src/devtools/renderer/index.ts'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/output'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/output.ts'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/panel'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/panel.ts'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/styles'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/styles.ts'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/theme'
@@ -0,0 +1 @@
1
+ export * from '../../../src/devtools/renderer/theme.ts'
@@ -0,0 +1 @@
1
+ export * from '../../src/devtools/types'
@@ -0,0 +1 @@
1
+ export * from '../../src/devtools/types.ts'
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { autoCloseMarkdown } from './internal/parse/auto-close/index.ts';
2
2
  export { applyAutoUnwrap } from './internal/parse/auto-unwrap.ts';
3
3
  export * from './parse.ts';
4
+ export { createComarkContext } from './context.ts';
5
+ export type { ComarkContext, ComarkDocument, ComarkPatch } from './context.ts';
4
6
  export type * from './types';
package/dist/index.js CHANGED
@@ -4,3 +4,5 @@ export { autoCloseMarkdown } from "./internal/parse/auto-close/index.js";
4
4
  export { applyAutoUnwrap } from "./internal/parse/auto-unwrap.js";
5
5
  // Re-export parse utilities
6
6
  export * from "./parse.js";
7
+ // Re-export the ambient renderer context for live updates
8
+ export { createComarkContext } from "./context.js";
@@ -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;
@@ -190,7 +213,8 @@ function closeInlineMarkersLinear(line) {
190
213
  // Count markers by scanning
191
214
  let asteriskCount = 0;
192
215
  let underscoreCount = 0;
193
- let tildeCount = 0; // Count individual tildes
216
+ let doubleTildeCount = 0; // Count ~~ occurrences (GFM strikethrough delimiter)
217
+ let singleTildeCount = 0; // Count standalone ~ (not part of ~~)
194
218
  let backtickCount = 0;
195
219
  let dollarCount = 0; // Count $ for math
196
220
  let dollarPairCount = 0; // Count $$ pairs for block math
@@ -211,9 +235,31 @@ function closeInlineMarkersLinear(line) {
211
235
  let inAttributes = 0;
212
236
  let inLinkText = 0;
213
237
  let inLinkUrl = 0;
238
+ let linkTextBacktickCount = 0;
239
+ // Markers inside an inline code span (`...`) or math ($...$) are literal text
240
+ let inCode = false;
241
+ let inMath = false;
214
242
  for (let i = 0; i < len; i++) {
215
243
  const prevCh = i > 0 ? line[i - 1] : '';
216
244
  const ch = line[i];
245
+ if (ch === '\\') {
246
+ i++; // Backslash escapes the next char, so neither is a delimiter
247
+ continue;
248
+ }
249
+ if (inCode) {
250
+ if (ch === '`') {
251
+ backtickCount++;
252
+ inCode = false;
253
+ }
254
+ continue;
255
+ }
256
+ if (inMath) {
257
+ if (ch === '$' && !(i + 1 < len && line[i + 1] === '$')) {
258
+ dollarCount++;
259
+ inMath = false;
260
+ }
261
+ continue;
262
+ }
217
263
  if (ch === '{' && prevCh !== ' ') {
218
264
  inAttributes++;
219
265
  continue;
@@ -254,8 +300,30 @@ function closeInlineMarkersLinear(line) {
254
300
  }
255
301
  continue;
256
302
  }
257
- if (inLinkText > 0 || inLinkUrl > 0)
303
+ if (inLinkText > 0) {
304
+ if (ch === '`')
305
+ linkTextBacktickCount++;
306
+ continue;
307
+ }
308
+ if (inLinkUrl > 0)
258
309
  continue;
310
+ if (ch === '`') {
311
+ backtickCount++;
312
+ inCode = true;
313
+ continue;
314
+ }
315
+ if (ch === '$') {
316
+ if (i + 1 < len && line[i + 1] === '$') {
317
+ dollarPairCount++; // `$$` is block math, counted as a pair
318
+ dollarCount += 2;
319
+ i++;
320
+ }
321
+ else {
322
+ dollarCount++;
323
+ inMath = true;
324
+ }
325
+ continue;
326
+ }
259
327
  if (ch === '*') {
260
328
  asteriskCount++;
261
329
  // Track ** positions (not part of ***)
@@ -280,23 +348,22 @@ function closeInlineMarkersLinear(line) {
280
348
  }
281
349
  }
282
350
  else if (ch === '~') {
283
- tildeCount++;
284
- }
285
- else if (ch === '`') {
286
- backtickCount++;
287
- }
288
- else if (ch === '$' && prevCh !== '\\') {
289
- // Count $$ pairs for block/display math
290
- if (i + 1 < len && line[i + 1] === '$') {
291
- dollarPairCount++;
292
- dollarCount += 2; // Count both dollars in the pair
293
- i++; // Skip next $ since we counted the pair
351
+ if (i + 1 < len && line[i + 1] === '~') {
352
+ doubleTildeCount++;
353
+ i++; // Skip second tilde since we counted the pair
294
354
  }
295
355
  else {
296
- dollarCount++; // Single $ for inline math
356
+ singleTildeCount++;
297
357
  }
298
358
  }
299
359
  }
360
+ // Open code/math region: close only it; everything after the opener is literal
361
+ if (inCode) {
362
+ return line + '`';
363
+ }
364
+ if (inMath) {
365
+ return line.trim() === '$$' ? line : line + '$';
366
+ }
300
367
  // Check for complete ** pairs in O(1) - pairs are matched left to right
301
368
  const hasCompleteBoldPair = doubleAsteriskPositions.length >= 2;
302
369
  let closingSuffix = '';
@@ -450,22 +517,18 @@ function closeInlineMarkersLinear(line) {
450
517
  }
451
518
  }
452
519
  }
453
- // Check ~~ (strikethrough)
454
- if (!closingSuffix && tildeCount >= 2) {
455
- const remainder = tildeCount % 4;
456
- if (remainder === 2) {
457
- // Two tildes unclosed, close with ~~
458
- closingSuffix = '~~';
459
- if (hasTrailingSpace)
460
- shouldTrim = true;
461
- }
462
- else if (remainder > 2 && remainder < 4) {
463
- // Partial marker like ~~text~ (3 tildes), need 1 more
464
- const needed = 4 - remainder;
465
- closingSuffix = '~'.repeat(needed);
466
- if (hasTrailingSpace)
467
- shouldTrim = true;
468
- }
520
+ // Check ~~ (strikethrough) and ~ (single-tilde) separately so that paired
521
+ // singles like ~Hello~ are left alone while ~~text and ~Hello both close.
522
+ if (!closingSuffix && doubleTildeCount % 2 === 1) {
523
+ // A trailing single ~ after an open ~~ is a partial closer (~~text~)
524
+ closingSuffix = singleTildeCount === 1 ? '~' : '~~';
525
+ if (hasTrailingSpace)
526
+ shouldTrim = true;
527
+ }
528
+ else if (!closingSuffix && singleTildeCount % 2 === 1) {
529
+ closingSuffix = '~';
530
+ if (hasTrailingSpace)
531
+ shouldTrim = true;
469
532
  }
470
533
  // Check ` (code)
471
534
  if (!closingSuffix && backtickCount % 2 === 1) {
@@ -486,7 +549,9 @@ function closeInlineMarkersLinear(line) {
486
549
  }
487
550
  // Check [ ] (brackets)
488
551
  if (!closingSuffix && bracketBalance > 0) {
489
- closingSuffix = ']';
552
+ // An odd backtick opened inside the unclosed link text is an unclosed
553
+ // inline code span; close it before the bracket so `]` stays outside it.
554
+ closingSuffix = linkTextBacktickCount % 2 === 1 ? '`]' : ']';
490
555
  }
491
556
  // Check ( ) (parens)
492
557
  if (!closingSuffix && parenBalance > 0) {
@@ -30,5 +30,9 @@ export function applyAutoUnwrap(node) {
30
30
  if (nonEmptyChildren.length > 1 || typeof nonEmptyChildren[0] === 'string' || nonEmptyChildren[0][0] !== 'p') {
31
31
  return [tag, props, ...children.map((child) => applyAutoUnwrap(child))];
32
32
  }
33
- return [tag, props, ...nonEmptyChildren[0].slice(2)];
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)];
34
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,7 +16,6 @@ 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
21
  if (state.src.charCodeAt(pos) !== 0x3c /* < */)
@@ -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;
@@ -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,5 +1,5 @@
1
1
  import { Parser } from 'htmlparser2';
2
- const VOID_ELEMENTS = new Set([
2
+ export const VOID_ELEMENTS = new Set([
3
3
  'area',
4
4
  'base',
5
5
  'br',