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.
- package/README.md +25 -1
- package/dist/context.d.ts +78 -0
- package/dist/context.js +127 -0
- package/dist/devtools/bridge.d.ts +1 -0
- package/dist/devtools/bridge.js +1 -0
- package/dist/devtools/constants.d.ts +1 -0
- package/dist/devtools/constants.js +1 -0
- package/dist/devtools/renderer/dom.d.ts +1 -0
- package/dist/devtools/renderer/dom.js +1 -0
- package/dist/devtools/renderer/index.d.ts +2 -0
- package/dist/devtools/renderer/index.js +2 -0
- package/dist/devtools/renderer/output.d.ts +1 -0
- package/dist/devtools/renderer/output.js +1 -0
- package/dist/devtools/renderer/panel.d.ts +1 -0
- package/dist/devtools/renderer/panel.js +1 -0
- package/dist/devtools/renderer/styles.d.ts +1 -0
- package/dist/devtools/renderer/styles.js +1 -0
- package/dist/devtools/renderer/theme.d.ts +1 -0
- package/dist/devtools/renderer/theme.js +1 -0
- package/dist/devtools/types.d.ts +1 -0
- package/dist/devtools/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/parse/auto-close/index.js +96 -31
- package/dist/internal/parse/auto-unwrap.js +5 -1
- package/dist/internal/parse/html/html_block_rule.js +9 -15
- package/dist/internal/parse/html/index.d.ts +1 -0
- package/dist/internal/parse/html/index.js +1 -1
- package/dist/internal/parse/token-processor.js +70 -32
- package/dist/internal/stringify/attributes.d.ts +8 -1
- package/dist/internal/stringify/attributes.js +53 -0
- package/dist/internal/stringify/handlers/blockquote.js +17 -0
- package/dist/internal/stringify/handlers/heading.js +6 -1
- package/dist/internal/stringify/handlers/html.js +8 -2
- package/dist/internal/stringify/handlers/li.js +19 -9
- package/dist/internal/stringify/handlers/mdc.js +1 -1
- package/dist/internal/stringify/handlers/ol.js +15 -2
- package/dist/internal/stringify/handlers/p.js +4 -0
- package/dist/internal/stringify/handlers/pre.js +11 -2
- package/dist/internal/stringify/handlers/table.js +7 -0
- package/dist/internal/stringify/handlers/template.js +4 -1
- package/dist/internal/stringify/handlers/ul.js +11 -1
- package/dist/internal/stringify/state.js +13 -1
- package/dist/parse.d.ts +4 -4
- package/dist/parse.js +7 -3
- package/dist/plugins/alert.d.ts +1 -1
- package/dist/plugins/binding.d.ts +1 -1
- package/dist/plugins/breaks.d.ts +1 -1
- package/dist/plugins/emoji.d.ts +1 -1
- package/dist/plugins/footnotes.d.ts +1 -1
- package/dist/plugins/headings.d.ts +19 -8
- package/dist/plugins/headings.js +25 -15
- package/dist/plugins/highlight.d.ts +1 -1
- package/dist/plugins/highlight.js +4 -2
- package/dist/plugins/json-render.d.ts +1 -1
- package/dist/plugins/math.d.ts +1 -1
- package/dist/plugins/mermaid.d.ts +1 -1
- package/dist/plugins/punctuation.d.ts +1 -1
- package/dist/plugins/security.d.ts +12 -1
- package/dist/plugins/security.js +13 -6
- package/dist/plugins/summary.d.ts +4 -1
- package/dist/plugins/syntax.d.ts +1 -1
- package/dist/plugins/syntax.js +95 -36
- package/dist/plugins/task-list.d.ts +1 -1
- package/dist/plugins/toc.d.ts +3 -1
- package/dist/render.d.ts +6 -2
- package/dist/render.js +2 -2
- package/dist/types.d.ts +61 -12
- package/dist/utils/helpers.d.ts +16 -4
- package/dist/utils/helpers.js +15 -3
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +30 -14
- package/package.json +6 -3
- package/skills/comark/references/rendering-svelte.md +51 -7
- package/dist/internal/stringify/indent.d.ts +0 -1
- package/dist/internal/stringify/indent.js +0 -1
- package/dist/vite.d.ts +0 -1
- package/dist/vite.js +0 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://comark.dev)
|
|
9
9
|
[](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;
|
package/dist/context.js
ADDED
|
@@ -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 @@
|
|
|
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
|
|
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
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
39
|
-
|
|
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 =
|
|
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;
|