comark 0.4.0 → 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/index.d.ts +1 -0
- package/dist/devtools/index.js +1 -0
- package/dist/devtools/register.d.ts +1 -0
- package/dist/devtools/register.js +1 -0
- package/dist/devtools/registry.d.ts +1 -0
- package/dist/devtools/registry.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/devtools/vite.d.ts +1 -0
- package/dist/devtools/vite.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/parse/auto-close/index.js +55 -16
- package/dist/internal/stringify/attributes.d.ts +1 -1
- package/dist/internal/stringify/attributes.js +10 -9
- package/dist/internal/stringify/handlers/li.js +15 -8
- package/dist/internal/stringify/handlers/ol.js +4 -1
- package/dist/internal/stringify/state.js +13 -1
- package/dist/parse.js +1 -1
- package/dist/plugins/highlight.js +0 -1
- package/dist/plugins/security.d.ts +11 -0
- package/dist/plugins/security.js +13 -6
- package/dist/plugins/syntax.js +25 -2
- package/dist/render.d.ts +6 -2
- package/dist/render.js +2 -2
- package/dist/types.d.ts +5 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +30 -14
- package/package.json +4 -2
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/index'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../src/devtools/index.ts'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../src/devtools/register'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../src/devtools/register.ts'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../src/devtools/registry'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../src/devtools/registry.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'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../src/devtools/vite'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../src/devtools/vite.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";
|
|
@@ -235,9 +235,31 @@ function closeInlineMarkersLinear(line) {
|
|
|
235
235
|
let inAttributes = 0;
|
|
236
236
|
let inLinkText = 0;
|
|
237
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;
|
|
238
242
|
for (let i = 0; i < len; i++) {
|
|
239
243
|
const prevCh = i > 0 ? line[i - 1] : '';
|
|
240
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
|
+
}
|
|
241
263
|
if (ch === '{' && prevCh !== ' ') {
|
|
242
264
|
inAttributes++;
|
|
243
265
|
continue;
|
|
@@ -278,8 +300,30 @@ function closeInlineMarkersLinear(line) {
|
|
|
278
300
|
}
|
|
279
301
|
continue;
|
|
280
302
|
}
|
|
281
|
-
if (inLinkText > 0
|
|
303
|
+
if (inLinkText > 0) {
|
|
304
|
+
if (ch === '`')
|
|
305
|
+
linkTextBacktickCount++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (inLinkUrl > 0)
|
|
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
|
+
}
|
|
282
325
|
continue;
|
|
326
|
+
}
|
|
283
327
|
if (ch === '*') {
|
|
284
328
|
asteriskCount++;
|
|
285
329
|
// Track ** positions (not part of ***)
|
|
@@ -312,20 +356,13 @@ function closeInlineMarkersLinear(line) {
|
|
|
312
356
|
singleTildeCount++;
|
|
313
357
|
}
|
|
314
358
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
dollarCount += 2; // Count both dollars in the pair
|
|
323
|
-
i++; // Skip next $ since we counted the pair
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
dollarCount++; // Single $ for inline math
|
|
327
|
-
}
|
|
328
|
-
}
|
|
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 + '$';
|
|
329
366
|
}
|
|
330
367
|
// Check for complete ** pairs in O(1) - pairs are matched left to right
|
|
331
368
|
const hasCompleteBoldPair = doubleAsteriskPositions.length >= 2;
|
|
@@ -512,7 +549,9 @@ function closeInlineMarkersLinear(line) {
|
|
|
512
549
|
}
|
|
513
550
|
// Check [ ] (brackets)
|
|
514
551
|
if (!closingSuffix && bracketBalance > 0) {
|
|
515
|
-
|
|
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 ? '`]' : ']';
|
|
516
555
|
}
|
|
517
556
|
// Check ( ) (parens)
|
|
518
557
|
if (!closingSuffix && parenBalance > 0) {
|
|
@@ -5,7 +5,7 @@ export interface ResolveAttributesOptions {
|
|
|
5
5
|
* `:` prefix is always stripped. Non-JSON strings fall back to a dot-path
|
|
6
6
|
* lookup in `renderData`; unresolved paths yield `undefined`.
|
|
7
7
|
*
|
|
8
|
-
* This matches the Vue/React/Svelte renderer semantics, which always
|
|
8
|
+
* This matches the Vue/React/Svelte/Angular renderer semantics, which always
|
|
9
9
|
* normalize bindings into real JS values suitable for typed component props.
|
|
10
10
|
*
|
|
11
11
|
* When false (default) only dot-path lookups are applied — literals and
|
|
@@ -72,14 +72,15 @@ export function resolveAttribute(attrs, renderData, key) {
|
|
|
72
72
|
// is implicit in `- [ ]`) so they should not echo back as user attrs.
|
|
73
73
|
const IMPLICIT_ATTRS = {
|
|
74
74
|
blockquote: { drop: ['as'] },
|
|
75
|
+
ol: { drop: ['start'] },
|
|
75
76
|
ul: { classBlocklist: ['contains-task-list'] },
|
|
76
77
|
li: { classBlocklist: ['task-list-item'] },
|
|
77
78
|
// `language`/`filename`/`highlights`/`meta` ride on the fence info string.
|
|
78
|
-
// `
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
pre: { drop: ['language', 'filename', 'highlights', 'meta', '
|
|
79
|
+
// `style` comes from render-time plugins (e.g. shiki) and has no markdown
|
|
80
|
+
// form. `class` is handled specially in userBlockAttrs because shiki merges
|
|
81
|
+
// its injected classes with the user's class — we need to strip just the
|
|
82
|
+
// highlighter portion.
|
|
83
|
+
pre: { drop: ['language', 'filename', 'highlights', 'meta', 'style'] },
|
|
83
84
|
};
|
|
84
85
|
/**
|
|
85
86
|
* Filter implicit/auto-generated attrs that are encoded by the native
|
|
@@ -104,10 +105,10 @@ export function userBlockAttrs(tag, attributes) {
|
|
|
104
105
|
result[key] = remaining;
|
|
105
106
|
continue;
|
|
106
107
|
}
|
|
107
|
-
if (key === 'class' && tag === 'pre' && typeof value === 'string' && value.startsWith('shiki
|
|
108
|
-
// Shiki injects `shiki [shiki-themes] <themes
|
|
109
|
-
// user
|
|
110
|
-
// dropping everything up to and including
|
|
108
|
+
if (key === 'class' && tag === 'pre' && typeof value === 'string' && value.startsWith('shiki')) {
|
|
109
|
+
// Shiki injects `shiki [shiki-themes] <themes…>` (or a bare `shiki`) and
|
|
110
|
+
// appends any user class after a `.` separator. Recover the user portion
|
|
111
|
+
// by dropping everything up to and including that separator.
|
|
111
112
|
const tokens = value.split(/\s+/);
|
|
112
113
|
let cutoff = tokens.findIndex((t) => t === '.');
|
|
113
114
|
const userClass = cutoff >= 0 ? tokens.slice(cutoff + 1).join(' ') : '';
|
|
@@ -16,23 +16,30 @@ export async function li(node, state) {
|
|
|
16
16
|
prefix += input[1].checked || input[1][':checked'] ? '[x] ' : '[ ] ';
|
|
17
17
|
}
|
|
18
18
|
const prefixWidth = prefix.length;
|
|
19
|
+
// Direct text children render sibling components inline.
|
|
20
|
+
const hasInlineContent = children.some((child) => typeof child === 'string');
|
|
19
21
|
let result = '';
|
|
20
22
|
for (const child of children) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (blockElements.has(
|
|
24
|
-
|
|
25
|
-
const indented = indent(rendered, { width: prefixWidth });
|
|
23
|
+
if (Array.isArray(child)) {
|
|
24
|
+
const tag = child[0];
|
|
25
|
+
if (result && blockElements.has(tag)) {
|
|
26
|
+
const indented = indent(await state.one(child, state, node), { width: prefixWidth });
|
|
26
27
|
result = result.trimEnd() + '\n' + indented.trimEnd() + '\n';
|
|
27
28
|
continue;
|
|
28
29
|
}
|
|
29
|
-
if (
|
|
30
|
-
const indented = indent(
|
|
30
|
+
if (result && tag === 'p') {
|
|
31
|
+
const indented = indent(await state.one(child, state, node), { width: prefixWidth });
|
|
31
32
|
result = result.trimEnd() + '\n\n' + indented.trimEnd() + '\n';
|
|
32
33
|
continue;
|
|
33
34
|
}
|
|
35
|
+
// No parent → mdc skips its own nesting indentation, so li owns it here.
|
|
36
|
+
if (!hasInlineContent && !(tag in state.handlers)) {
|
|
37
|
+
const indented = indent(await state.one(child, state), { width: prefixWidth, ignoreFirstLine: !result });
|
|
38
|
+
result = result ? result.trimEnd() + '\n' + indented.trimEnd() + '\n' : indented.trimEnd() + '\n';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
34
41
|
}
|
|
35
|
-
result +=
|
|
42
|
+
result += await state.one(child, state, node);
|
|
36
43
|
}
|
|
37
44
|
result = result.trim();
|
|
38
45
|
const attrs = comarkAttributes(userBlockAttrs('li', node[1]));
|
|
@@ -2,7 +2,10 @@ import { indent } from "../../../utils/index.js";
|
|
|
2
2
|
import { comarkAttributes, userBlockAttrs } from "../attributes.js";
|
|
3
3
|
export async function ol(node, state) {
|
|
4
4
|
const children = node.slice(2);
|
|
5
|
-
|
|
5
|
+
// `start` is carried by the native numbering; IMPLICIT_ATTRS drops it from user attrs.
|
|
6
|
+
const start = Number(node[1].start);
|
|
7
|
+
const order = Number.isInteger(start) && start >= 1 ? start : 1;
|
|
8
|
+
const revert = state.applyContext({ list: true, order, listIndent: 3 });
|
|
6
9
|
let result = '';
|
|
7
10
|
for (const child of children) {
|
|
8
11
|
result += await state.one(child, state);
|
|
@@ -25,7 +25,7 @@ export async function one(node, state, parent) {
|
|
|
25
25
|
if (state.context.html) {
|
|
26
26
|
return escapeHtml(node);
|
|
27
27
|
}
|
|
28
|
-
return node;
|
|
28
|
+
return escapeMarkdownText(node);
|
|
29
29
|
}
|
|
30
30
|
if (node[0] === null) {
|
|
31
31
|
return await state.handlers.comment(node, state);
|
|
@@ -175,3 +175,15 @@ function escapeHtml(text) {
|
|
|
175
175
|
};
|
|
176
176
|
return text.replace(/[<>]/g, (char) => map[char]);
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Escape characters in a markdown text node that would otherwise be
|
|
180
|
+
* misinterpreted as markdown syntax on a subsequent parse.
|
|
181
|
+
*
|
|
182
|
+
* `[` opens link/image syntax; `]` closes it. Both must be escaped so that
|
|
183
|
+
* a text node like `[foo](bar)` round-trips as plain text, and a text node
|
|
184
|
+
* containing `]` inside a link (e.g. `dsd]dsd`) doesn't prematurely close
|
|
185
|
+
* the surrounding `[…]` brackets.
|
|
186
|
+
*/
|
|
187
|
+
function escapeMarkdownText(text) {
|
|
188
|
+
return text.replace(/[[\]]/g, (ch) => `\\${ch}`);
|
|
189
|
+
}
|
package/dist/parse.js
CHANGED
|
@@ -53,7 +53,7 @@ export function createParse(options = {}) {
|
|
|
53
53
|
plugins.unshift(alert());
|
|
54
54
|
const parser = new MarkdownExit({
|
|
55
55
|
html: false,
|
|
56
|
-
linkify: true,
|
|
56
|
+
linkify: options.linkify ?? true,
|
|
57
57
|
}).enable(['table', 'strikethrough']);
|
|
58
58
|
if (options.html !== false) {
|
|
59
59
|
parser.inline.ruler.before('text', 'comark_html_inline', html_inline);
|
|
@@ -284,7 +284,6 @@ export async function highlightCodeBlocks(tree, options = {}) {
|
|
|
284
284
|
const newPreAttrs = {
|
|
285
285
|
...preAttrs,
|
|
286
286
|
class: userClass ? `${classStr} . ${userClass}` : classStr,
|
|
287
|
-
tabindex: '0',
|
|
288
287
|
};
|
|
289
288
|
if (options.preStyles) {
|
|
290
289
|
const lightTheme = options.themes?.light;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ComarkElement, ComarkNode } from 'comark';
|
|
1
2
|
import type { PropsValidationOptions } from '../internal/props-validation.ts';
|
|
2
3
|
interface SecurityOptions extends PropsValidationOptions {
|
|
3
4
|
/**
|
|
@@ -5,6 +6,16 @@ interface SecurityOptions extends PropsValidationOptions {
|
|
|
5
6
|
* @default []
|
|
6
7
|
*/
|
|
7
8
|
blockedTags?: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Tags to allow only in the output tree.
|
|
11
|
+
* @default []
|
|
12
|
+
*/
|
|
13
|
+
allowedTags?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Behavior when encountering an unallowed or blocked tag.
|
|
16
|
+
* @default undefined
|
|
17
|
+
*/
|
|
18
|
+
tagFallback?: (element: ComarkElement) => false | ComarkNode | Promise<false | ComarkNode>;
|
|
8
19
|
}
|
|
9
20
|
declare const _default: import("comark").ComarkPluginFactory<SecurityOptions, {}, {}>;
|
|
10
21
|
export default _default;
|
package/dist/plugins/security.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineComarkPlugin } from "../utils/helpers.js";
|
|
2
|
-
import {
|
|
2
|
+
import { visitAsync } from "../utils/index.js";
|
|
3
3
|
import { validateProps } from "../internal/props-validation.js";
|
|
4
4
|
export default defineComarkPlugin((options = {}) => {
|
|
5
|
-
const { blockedTags = [], allowedLinkPrefixes, allowedImagePrefixes, allowedProtocols, defaultOrigin, allowDataImages, } = options;
|
|
5
|
+
const { blockedTags = [], allowedTags = [], tagFallback = undefined, allowedLinkPrefixes, allowedImagePrefixes, allowedProtocols, defaultOrigin, allowDataImages, } = options;
|
|
6
6
|
const dropSet = new Set(blockedTags.map((t) => t.toLowerCase()));
|
|
7
|
+
const allowSet = new Set(allowedTags.map((t) => t.toLowerCase()));
|
|
7
8
|
const propsOptions = {
|
|
8
9
|
allowedLinkPrefixes,
|
|
9
10
|
allowedImagePrefixes,
|
|
@@ -13,11 +14,17 @@ export default defineComarkPlugin((options = {}) => {
|
|
|
13
14
|
};
|
|
14
15
|
return {
|
|
15
16
|
name: 'security',
|
|
16
|
-
post(state) {
|
|
17
|
-
|
|
17
|
+
async post(state) {
|
|
18
|
+
await visitAsync(state.tree, (node) => typeof node !== 'string' && node[0] !== null, async (node) => {
|
|
18
19
|
const element = node;
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const tagName = element[0].toLowerCase();
|
|
21
|
+
const isBlocked = dropSet.has(tagName);
|
|
22
|
+
const isNotAllowed = allowSet.size > 0 && !allowSet.has(tagName);
|
|
23
|
+
if (isNotAllowed || isBlocked) {
|
|
24
|
+
if (typeof tagFallback === 'function') {
|
|
25
|
+
return await tagFallback(element);
|
|
26
|
+
}
|
|
27
|
+
// return false to remove the node from the tree
|
|
21
28
|
return false;
|
|
22
29
|
}
|
|
23
30
|
const keys = Object.keys(element[1]);
|
package/dist/plugins/syntax.js
CHANGED
|
@@ -4,6 +4,22 @@ import { parseBracketContent } from "../internal/parse/syntax/brackets.js";
|
|
|
4
4
|
import { searchProps } from "../internal/parse/syntax/props.js";
|
|
5
5
|
import { parseBlockParams } from "../internal/parse/syntax/block-params.js";
|
|
6
6
|
import { parseYaml } from "../internal/yaml.js";
|
|
7
|
+
/**
|
|
8
|
+
* A component name must start with a letter or `$`, followed by word chars,
|
|
9
|
+
* `$` or `-`. Mirrors the block name grammar (`RE_BLOCK_NAME = /^[a-z$]/i`).
|
|
10
|
+
*/
|
|
11
|
+
const RE_COMPONENT_NAME = /^[a-z$][\w$-]*/i;
|
|
12
|
+
/**
|
|
13
|
+
* Whether `name` begins with a syntactically valid component name.
|
|
14
|
+
*
|
|
15
|
+
* This prevents sequences such as `:8100` or `::30` from being treated as
|
|
16
|
+
* components — a purely numeric name is not a valid component and would
|
|
17
|
+
* otherwise produce invalid output like `createElement('8100')` (inline) or
|
|
18
|
+
* throw `Invalid block params` (block).
|
|
19
|
+
*/
|
|
20
|
+
function isValidComponentName(name) {
|
|
21
|
+
return RE_COMPONENT_NAME.test(name);
|
|
22
|
+
}
|
|
7
23
|
// #region Block component plugin (`::name` and `::name ... ::`)
|
|
8
24
|
const blockYamlLines = {
|
|
9
25
|
'---': '---',
|
|
@@ -18,7 +34,7 @@ const markdownItComarkBlock = (md) => {
|
|
|
18
34
|
const marker_char = marker_str.charCodeAt(0);
|
|
19
35
|
md.block.ruler.before('fence', 'comark_block_shorthand', function comark_block_shorthand(state, startLine, _endLine, silent) {
|
|
20
36
|
const line = state.src.slice(state.bMarks[startLine] + state.tShift[startLine], state.eMarks[startLine]);
|
|
21
|
-
if (
|
|
37
|
+
if (line[0] !== ':' || !isValidComponentName(line.slice(1)))
|
|
22
38
|
return false;
|
|
23
39
|
const { name, content, props, remaining } = parseBlockParams(line.slice(1));
|
|
24
40
|
// If there's unparsed remaining content, treat it as inline component in a paragraph
|
|
@@ -78,6 +94,11 @@ const markdownItComarkBlock = (md) => {
|
|
|
78
94
|
if (marker_count < min_markers)
|
|
79
95
|
return false;
|
|
80
96
|
const markup = state.src.slice(start, pos);
|
|
97
|
+
// Bail out (plain text) on an invalid name instead of letting
|
|
98
|
+
// parseBlockParams throw on e.g. `::8100`.
|
|
99
|
+
const nameStart = state.skipSpaces(pos);
|
|
100
|
+
if (nameStart < max && !isValidComponentName(state.src.slice(nameStart, max)))
|
|
101
|
+
return false;
|
|
81
102
|
const params = parseBlockParams(state.src.slice(pos, max));
|
|
82
103
|
if (!params.name)
|
|
83
104
|
return false;
|
|
@@ -383,10 +404,12 @@ const markdownItInlineComponent = (md) => {
|
|
|
383
404
|
// Empty name
|
|
384
405
|
if (nameEnd <= start + 1)
|
|
385
406
|
return false;
|
|
407
|
+
const name = state.src.slice(start + 1, nameEnd);
|
|
408
|
+
if (!isValidComponentName(name))
|
|
409
|
+
return false;
|
|
386
410
|
state.pos = index;
|
|
387
411
|
if (silent)
|
|
388
412
|
return true;
|
|
389
|
-
const name = state.src.slice(start + 1, nameEnd);
|
|
390
413
|
if (contentStart !== -1) {
|
|
391
414
|
state.push('mdc_inline_component', name, 1);
|
|
392
415
|
const oldPos = state.pos;
|
package/dist/render.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ export { resolveAttributes, resolveAttribute } from './internal/stringify/attrib
|
|
|
8
8
|
* @param context - The context of the renderer
|
|
9
9
|
* @returns The string representation of the Comark tree
|
|
10
10
|
*/
|
|
11
|
-
export declare function render(tree: ComarkTree
|
|
11
|
+
export declare function render(tree: ComarkTree | {
|
|
12
|
+
nodes: ComarkTree['nodes'];
|
|
13
|
+
}, context?: RenderOptions): Promise<string>;
|
|
12
14
|
/**
|
|
13
15
|
* Render Comark tree to markdown
|
|
14
16
|
*
|
|
@@ -16,4 +18,6 @@ export declare function render(tree: ComarkTree, context?: RenderOptions): Promi
|
|
|
16
18
|
* @param options - Optional rendering options
|
|
17
19
|
* @returns The markdown string with optional frontmatter
|
|
18
20
|
*/
|
|
19
|
-
export declare function renderMarkdown(tree: ComarkTree
|
|
21
|
+
export declare function renderMarkdown(tree: ComarkTree | {
|
|
22
|
+
nodes: ComarkTree['nodes'];
|
|
23
|
+
}, options?: RenderMarkdownOptions): Promise<string>;
|
package/dist/render.js
CHANGED
|
@@ -11,7 +11,7 @@ export { resolveAttributes, resolveAttribute } from "./internal/stringify/attrib
|
|
|
11
11
|
* @returns The string representation of the Comark tree
|
|
12
12
|
*/
|
|
13
13
|
export async function render(tree, context = {}) {
|
|
14
|
-
const state = createState({ ...context, tree, handlers: context.components });
|
|
14
|
+
const state = createState({ ...context, tree: tree, handlers: context.components });
|
|
15
15
|
let result = '';
|
|
16
16
|
for (const child of tree.nodes) {
|
|
17
17
|
result += await one(child, state);
|
|
@@ -27,5 +27,5 @@ export async function render(tree, context = {}) {
|
|
|
27
27
|
*/
|
|
28
28
|
export async function renderMarkdown(tree, options) {
|
|
29
29
|
const content = await render(tree, { format: 'markdown/comark', ...options });
|
|
30
|
-
return renderFrontmatter(tree.frontmatter, content, options?.frontmatterOptions);
|
|
30
|
+
return renderFrontmatter(tree.frontmatter || {}, content, options?.frontmatterOptions);
|
|
31
31
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -339,6 +339,11 @@ export interface ParseOptions<TPlugins extends readonly ComarkPlugin<any, any>[]
|
|
|
339
339
|
* // With html: false — HTML tags are left as raw text / ignored
|
|
340
340
|
*/
|
|
341
341
|
html?: boolean;
|
|
342
|
+
/**
|
|
343
|
+
* Set `false` to disable autoconvert URL-like text to links.
|
|
344
|
+
* @default true
|
|
345
|
+
*/
|
|
346
|
+
linkify?: boolean;
|
|
342
347
|
/**
|
|
343
348
|
* Additional plugins to use
|
|
344
349
|
* @default []
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComarkNode, ComarkTree } from 'comark';
|
|
2
|
+
type VisitResult = ComarkNode | false | undefined | void;
|
|
2
3
|
/**
|
|
3
4
|
* Get the text content of a Comark node
|
|
4
5
|
*
|
|
@@ -17,7 +18,8 @@ export declare function textContent(node: ComarkNode, options?: {
|
|
|
17
18
|
* @param checker - A function that checks if a node should be visited
|
|
18
19
|
* @param visitor - A function that visits a node
|
|
19
20
|
*/
|
|
20
|
-
export declare function visit(tree: ComarkTree, checker: (node: ComarkNode) => boolean, visitor: (node: ComarkNode) =>
|
|
21
|
+
export declare function visit(tree: ComarkTree, checker: (node: ComarkNode) => boolean, visitor: (node: ComarkNode) => VisitResult): void;
|
|
22
|
+
export declare function visitAsync(tree: ComarkTree, checker: (node: ComarkNode) => boolean, visitor: (node: ComarkNode) => Promise<VisitResult> | VisitResult): Promise<void>;
|
|
21
23
|
export declare function indent(text: string, { ignoreFirstLine, level, width }?: {
|
|
22
24
|
ignoreFirstLine?: boolean;
|
|
23
25
|
level?: number;
|
package/dist/utils/index.js
CHANGED
|
@@ -22,20 +22,11 @@ export function textContent(node, options = {}) {
|
|
|
22
22
|
}
|
|
23
23
|
return out;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
* @param tree - The Comark tree
|
|
29
|
-
* @param checker - A function that checks if a node should be visited
|
|
30
|
-
* @param visitor - A function that visits a node
|
|
31
|
-
*/
|
|
32
|
-
export function visit(tree, checker,
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
34
|
-
visitor) {
|
|
35
|
-
function walk(node, parent, index) {
|
|
25
|
+
function* walkGenerator(tree, checker) {
|
|
26
|
+
function* walk(node, parent, index) {
|
|
36
27
|
let currentNode = node;
|
|
37
28
|
if (checker(node)) {
|
|
38
|
-
const res =
|
|
29
|
+
const res = yield node;
|
|
39
30
|
if (res === false) {
|
|
40
31
|
// remove the node from the parent
|
|
41
32
|
;
|
|
@@ -52,7 +43,7 @@ visitor) {
|
|
|
52
43
|
// Use a while loop to handle removals correctly - don't increment if node was removed
|
|
53
44
|
let i = 2;
|
|
54
45
|
while (i < currentNode.length) {
|
|
55
|
-
const childRemoved = walk(currentNode[i], currentNode, i);
|
|
46
|
+
const childRemoved = yield* walk(currentNode[i], currentNode, i);
|
|
56
47
|
if (childRemoved) {
|
|
57
48
|
// If removed, i stays the same (next node is now at this index)
|
|
58
49
|
continue;
|
|
@@ -65,7 +56,7 @@ visitor) {
|
|
|
65
56
|
// Use a while loop to handle removals correctly - don't increment if node was removed
|
|
66
57
|
let i = 0;
|
|
67
58
|
while (i < tree.nodes.length) {
|
|
68
|
-
const removed = walk(tree.nodes[i], tree.nodes, i);
|
|
59
|
+
const removed = yield* walk(tree.nodes[i], tree.nodes, i);
|
|
69
60
|
if (removed) {
|
|
70
61
|
// If removed, i stays the same (next node is now at this index)
|
|
71
62
|
continue;
|
|
@@ -73,6 +64,31 @@ visitor) {
|
|
|
73
64
|
i += 1;
|
|
74
65
|
}
|
|
75
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Visit a Comark tree and apply a visitor function to each node
|
|
69
|
+
*
|
|
70
|
+
* @param tree - The Comark tree
|
|
71
|
+
* @param checker - A function that checks if a node should be visited
|
|
72
|
+
* @param visitor - A function that visits a node
|
|
73
|
+
*/
|
|
74
|
+
export function visit(tree, checker,
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
76
|
+
visitor) {
|
|
77
|
+
const iterator = walkGenerator(tree, checker);
|
|
78
|
+
let step = iterator.next();
|
|
79
|
+
while (!step.done) {
|
|
80
|
+
const res = visitor(step.value);
|
|
81
|
+
step = iterator.next(res);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function visitAsync(tree, checker, visitor) {
|
|
85
|
+
const iterator = walkGenerator(tree, checker);
|
|
86
|
+
let step = iterator.next();
|
|
87
|
+
while (!step.done) {
|
|
88
|
+
const res = await visitor(step.value);
|
|
89
|
+
step = iterator.next(res);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
76
92
|
// #region String Utils
|
|
77
93
|
export function indent(text, { ignoreFirstLine = false, level = 1, width } = {}) {
|
|
78
94
|
const pad = width ? ' '.repeat(width) : ' '.repeat(level);
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comark",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Components in Markdown (Comark) parser with streaming support for Vue, React, Svelte and HTML",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Components in Markdown (Comark) parser with streaming support for Vue, React, Svelte, Angular and HTML",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"angular",
|
|
6
7
|
"markdown",
|
|
7
8
|
"mdc",
|
|
8
9
|
"parser",
|
|
9
10
|
"react",
|
|
10
11
|
"streaming",
|
|
12
|
+
"svelte",
|
|
11
13
|
"vue"
|
|
12
14
|
],
|
|
13
15
|
"homepage": "https://comark.dev",
|