@sigx/lynx-markdown 0.4.4 → 0.4.6
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 +114 -44
- package/dist/XMarkdown.d.ts +36 -0
- package/dist/{Markdown.js → XMarkdown.js} +9 -3
- package/dist/ast.d.ts +117 -0
- package/dist/ast.js +11 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +13 -1
- package/dist/parser/blocks.d.ts +21 -0
- package/dist/parser/blocks.js +274 -0
- package/dist/parser/incremental.d.ts +26 -0
- package/dist/parser/incremental.js +65 -0
- package/dist/parser/inline.d.ts +16 -0
- package/dist/parser/inline.js +451 -0
- package/dist/parser/scanner.d.ts +73 -0
- package/dist/parser/scanner.js +219 -0
- package/dist/render/MarkdownView.d.ts +29 -0
- package/dist/render/MarkdownView.js +43 -0
- package/dist/render/components.d.ts +140 -0
- package/dist/render/components.js +99 -0
- package/dist/render/engine.d.ts +21 -0
- package/dist/render/engine.js +145 -0
- package/dist/stream.d.ts +42 -0
- package/dist/stream.js +69 -0
- package/package.json +9 -4
- package/dist/Markdown.d.ts +0 -30
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render engine: walks a `BlockNode[]` AST and dispatches each node to a
|
|
3
|
+
* {@link MarkdownComponents} renderer.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities the engine keeps (so components stay simple and design
|
|
6
|
+
* systems can't accidentally break streaming):
|
|
7
|
+
* - AST recursion — children are fully rendered before a component is called.
|
|
8
|
+
* - Stable reconciliation keys — `VNode.key` is stamped from the AST node's
|
|
9
|
+
* streaming key after the component returns, so finalized blocks never
|
|
10
|
+
* remount regardless of which element a component produced.
|
|
11
|
+
*/
|
|
12
|
+
/** Stamp a stable reconciliation key onto a rendered element (no-op for strings). */
|
|
13
|
+
function stampKey(el, key) {
|
|
14
|
+
if (el && typeof el === 'object')
|
|
15
|
+
el.key = key;
|
|
16
|
+
}
|
|
17
|
+
/** Render top-level blocks and wrap them in the `root` container. */
|
|
18
|
+
export function renderDocument(blocks, ctx) {
|
|
19
|
+
const children = blocks.map((b) => renderBlock(b, ctx));
|
|
20
|
+
return ctx.components.root({ children });
|
|
21
|
+
}
|
|
22
|
+
function renderBlock(block, ctx) {
|
|
23
|
+
const C = ctx.components;
|
|
24
|
+
let el;
|
|
25
|
+
switch (block.type) {
|
|
26
|
+
case 'heading':
|
|
27
|
+
el = C.heading({ level: block.level, children: renderInline(block.children, ctx), node: block });
|
|
28
|
+
break;
|
|
29
|
+
case 'paragraph':
|
|
30
|
+
el = C.paragraph({ children: renderInline(block.children, ctx), node: block });
|
|
31
|
+
break;
|
|
32
|
+
case 'blockquote':
|
|
33
|
+
el = C.blockquote({ children: block.children.map((b) => renderBlock(b, ctx)), node: block });
|
|
34
|
+
break;
|
|
35
|
+
case 'list':
|
|
36
|
+
el = C.list({
|
|
37
|
+
ordered: block.ordered,
|
|
38
|
+
start: block.start,
|
|
39
|
+
tight: block.tight,
|
|
40
|
+
children: block.items.map((item, i) => {
|
|
41
|
+
const li = C.listItem({
|
|
42
|
+
ordered: block.ordered,
|
|
43
|
+
index: i,
|
|
44
|
+
number: block.start + i,
|
|
45
|
+
checked: item.checked,
|
|
46
|
+
children: item.children.map((b) => renderBlock(b, ctx)),
|
|
47
|
+
item,
|
|
48
|
+
});
|
|
49
|
+
stampKey(li, item.key);
|
|
50
|
+
return li;
|
|
51
|
+
}),
|
|
52
|
+
node: block,
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
case 'codeBlock':
|
|
56
|
+
el = C.code({
|
|
57
|
+
...(block.lang ? { lang: block.lang } : {}),
|
|
58
|
+
value: block.value,
|
|
59
|
+
closed: block.closed,
|
|
60
|
+
node: block,
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
case 'thematicBreak':
|
|
64
|
+
el = C.thematicBreak({ node: block });
|
|
65
|
+
break;
|
|
66
|
+
case 'table':
|
|
67
|
+
el = renderTable(block, ctx);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
stampKey(el, block.key);
|
|
71
|
+
return el;
|
|
72
|
+
}
|
|
73
|
+
function renderTable(block, ctx) {
|
|
74
|
+
const C = ctx.components;
|
|
75
|
+
const headerCells = block.header.map((cell, i) => {
|
|
76
|
+
const c = C.tableCell({
|
|
77
|
+
header: true,
|
|
78
|
+
align: block.align[i] ?? null,
|
|
79
|
+
children: renderInline(cell.children, ctx),
|
|
80
|
+
node: block,
|
|
81
|
+
});
|
|
82
|
+
stampKey(c, `c${i}`);
|
|
83
|
+
return c;
|
|
84
|
+
});
|
|
85
|
+
const headerRow = C.tableRow({ header: true, children: headerCells, node: block });
|
|
86
|
+
stampKey(headerRow, 'head');
|
|
87
|
+
const bodyRows = block.rows.map((row, ri) => {
|
|
88
|
+
const cells = row.map((cell, ci) => {
|
|
89
|
+
const c = C.tableCell({
|
|
90
|
+
header: false,
|
|
91
|
+
align: block.align[ci] ?? null,
|
|
92
|
+
children: renderInline(cell.children, ctx),
|
|
93
|
+
node: block,
|
|
94
|
+
});
|
|
95
|
+
stampKey(c, `c${ci}`);
|
|
96
|
+
return c;
|
|
97
|
+
});
|
|
98
|
+
const r = C.tableRow({ header: false, children: cells, node: block });
|
|
99
|
+
stampKey(r, `r${ri}`);
|
|
100
|
+
return r;
|
|
101
|
+
});
|
|
102
|
+
return C.table({ align: block.align, children: [headerRow, ...bodyRows], node: block });
|
|
103
|
+
}
|
|
104
|
+
function renderInline(nodes, ctx) {
|
|
105
|
+
return nodes.map((node, i) => {
|
|
106
|
+
const el = renderInlineNode(node, ctx);
|
|
107
|
+
stampKey(el, String(i));
|
|
108
|
+
return el;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function renderInlineNode(node, ctx) {
|
|
112
|
+
const C = ctx.components;
|
|
113
|
+
switch (node.type) {
|
|
114
|
+
case 'text':
|
|
115
|
+
return node.value;
|
|
116
|
+
case 'br':
|
|
117
|
+
return C.br();
|
|
118
|
+
case 'strong':
|
|
119
|
+
return C.strong({ children: renderInline(node.children, ctx), node });
|
|
120
|
+
case 'em':
|
|
121
|
+
return C.em({ children: renderInline(node.children, ctx), node });
|
|
122
|
+
case 'del':
|
|
123
|
+
return C.del({ children: renderInline(node.children, ctx), node });
|
|
124
|
+
case 'codeSpan':
|
|
125
|
+
return C.codeSpan({ value: node.value, node });
|
|
126
|
+
case 'link':
|
|
127
|
+
return C.link({
|
|
128
|
+
href: node.href,
|
|
129
|
+
...(node.title ? { title: node.title } : {}),
|
|
130
|
+
children: renderInline(node.children, ctx),
|
|
131
|
+
onLink: ctx.onLink,
|
|
132
|
+
node,
|
|
133
|
+
});
|
|
134
|
+
case 'autolink':
|
|
135
|
+
return C.autolink({ href: node.href, value: node.value, onLink: ctx.onLink, node });
|
|
136
|
+
case 'image':
|
|
137
|
+
return C.image({
|
|
138
|
+
src: node.src,
|
|
139
|
+
alt: node.alt,
|
|
140
|
+
...(node.title ? { title: node.title } : {}),
|
|
141
|
+
onImageTap: ctx.onImageTap,
|
|
142
|
+
node,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createMarkdownStream()` — a one-line bridge between an AI token loop and
|
|
3
|
+
* `<Markdown>`.
|
|
4
|
+
*
|
|
5
|
+
* It owns a reactive `value` signal and coalesces bursts of `append()` calls
|
|
6
|
+
* into a single signal write per `flushIntervalMs` window, so a fast token
|
|
7
|
+
* stream re-renders at a bounded rate instead of once per token.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const md = createMarkdownStream({ flushIntervalMs: 16 });
|
|
12
|
+
*
|
|
13
|
+
* // producer
|
|
14
|
+
* for await (const token of completion) md.append(token);
|
|
15
|
+
* md.done();
|
|
16
|
+
*
|
|
17
|
+
* // consumer
|
|
18
|
+
* <Markdown value={md.value.value} />
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { type PrimitiveSignal } from '@sigx/lynx';
|
|
22
|
+
export interface CreateMarkdownStreamOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Coalesce `append()` calls within this many milliseconds into a single
|
|
25
|
+
* `value` update. `0` (default) flushes synchronously on every append.
|
|
26
|
+
* A small value such as `16` caps re-renders to ~60fps under fast streams.
|
|
27
|
+
*/
|
|
28
|
+
flushIntervalMs?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface MarkdownStream {
|
|
31
|
+
/** Reactive accumulated source. Pass to `<Markdown value={stream.value.value} />`. */
|
|
32
|
+
readonly value: PrimitiveSignal<string>;
|
|
33
|
+
/** Reactive completion flag, set by {@link MarkdownStream.done}. */
|
|
34
|
+
readonly finished: PrimitiveSignal<boolean>;
|
|
35
|
+
/** Append a token/chunk; buffered and coalesced into `value`. */
|
|
36
|
+
append(chunk: string): void;
|
|
37
|
+
/** Flush any pending buffer and mark the stream complete. */
|
|
38
|
+
done(): void;
|
|
39
|
+
/** Clear the buffer, `value`, and `finished` (e.g. for a regenerate). */
|
|
40
|
+
reset(): void;
|
|
41
|
+
}
|
|
42
|
+
export declare function createMarkdownStream(opts?: CreateMarkdownStreamOptions): MarkdownStream;
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createMarkdownStream()` — a one-line bridge between an AI token loop and
|
|
3
|
+
* `<Markdown>`.
|
|
4
|
+
*
|
|
5
|
+
* It owns a reactive `value` signal and coalesces bursts of `append()` calls
|
|
6
|
+
* into a single signal write per `flushIntervalMs` window, so a fast token
|
|
7
|
+
* stream re-renders at a bounded rate instead of once per token.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const md = createMarkdownStream({ flushIntervalMs: 16 });
|
|
12
|
+
*
|
|
13
|
+
* // producer
|
|
14
|
+
* for await (const token of completion) md.append(token);
|
|
15
|
+
* md.done();
|
|
16
|
+
*
|
|
17
|
+
* // consumer
|
|
18
|
+
* <Markdown value={md.value.value} />
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { signal } from '@sigx/lynx';
|
|
22
|
+
export function createMarkdownStream(opts) {
|
|
23
|
+
const flushIntervalMs = opts?.flushIntervalMs ?? 0;
|
|
24
|
+
const value = signal('');
|
|
25
|
+
const finished = signal(false);
|
|
26
|
+
let buffer = '';
|
|
27
|
+
let timer = null;
|
|
28
|
+
const flush = () => {
|
|
29
|
+
if (timer !== null) {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
timer = null;
|
|
32
|
+
}
|
|
33
|
+
if (value.value !== buffer)
|
|
34
|
+
value.value = buffer;
|
|
35
|
+
};
|
|
36
|
+
const schedule = () => {
|
|
37
|
+
if (flushIntervalMs <= 0) {
|
|
38
|
+
flush();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (timer === null)
|
|
42
|
+
timer = setTimeout(flush, flushIntervalMs);
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
value,
|
|
46
|
+
finished,
|
|
47
|
+
append(chunk) {
|
|
48
|
+
if (!chunk)
|
|
49
|
+
return;
|
|
50
|
+
buffer += chunk;
|
|
51
|
+
if (finished.value)
|
|
52
|
+
finished.value = false;
|
|
53
|
+
schedule();
|
|
54
|
+
},
|
|
55
|
+
done() {
|
|
56
|
+
flush();
|
|
57
|
+
finished.value = true;
|
|
58
|
+
},
|
|
59
|
+
reset() {
|
|
60
|
+
if (timer !== null) {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
timer = null;
|
|
63
|
+
}
|
|
64
|
+
buffer = '';
|
|
65
|
+
value.value = '';
|
|
66
|
+
finished.value = false;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigx/lynx-markdown",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"description": "SignalX-native streaming markdown renderer for Lynx. Parses markdown in JS (zero deps) and renders to native <view>/<text> primitives, with incremental streaming for AI output. Also ships XMarkdown, the native <x-markdown> wrapper.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@sigx/lynx": "^0.4.
|
|
22
|
+
"@sigx/lynx": "^0.4.6"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@typescript/native-preview": "7.0.0-dev.20260521.1",
|
|
26
26
|
"typescript": "^6.0.3",
|
|
27
|
-
"@sigx/lynx": "^0.4.
|
|
27
|
+
"@sigx/lynx": "^0.4.6",
|
|
28
|
+
"@sigx/lynx-testing": "^0.4.6"
|
|
28
29
|
},
|
|
29
30
|
"author": "Andreas Ekdahl",
|
|
30
31
|
"license": "MIT",
|
|
@@ -45,11 +46,15 @@
|
|
|
45
46
|
"sigx",
|
|
46
47
|
"lynx",
|
|
47
48
|
"markdown",
|
|
49
|
+
"native",
|
|
50
|
+
"streaming",
|
|
51
|
+
"ai",
|
|
48
52
|
"x-markdown"
|
|
49
53
|
],
|
|
50
54
|
"scripts": {
|
|
51
55
|
"build": "node ../../scripts/clean.mjs dist && tsgo",
|
|
52
56
|
"dev": "tsgo --watch",
|
|
57
|
+
"test": "vitest run",
|
|
53
58
|
"clean": "node ../../scripts/clean.mjs dist .turbo"
|
|
54
59
|
}
|
|
55
60
|
}
|
package/dist/Markdown.d.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { type Define } from '@sigx/lynx';
|
|
2
|
-
import './jsx-augment.js';
|
|
3
|
-
import type { MarkdownLinkEvent, MarkdownImageTapEvent, MarkdownParseEndEvent } from './jsx-augment.js';
|
|
4
|
-
export type MarkdownEffect = 'typewriter' | 'none' | (string & {});
|
|
5
|
-
export type MarkdownProps = Define.Prop<'value', string, false> & Define.Prop<'effect', MarkdownEffect, false> & Define.Prop<'attachments', ReadonlyArray<unknown>, false> & Define.Prop<'class', string, false> & Define.Prop<'style', string | Record<string, string | number>, false> & Define.Prop<'onLink', (e: MarkdownLinkEvent) => void, false> & Define.Prop<'onImageTap', (e: MarkdownImageTapEvent) => void, false> & Define.Prop<'onParseEnd', (e: MarkdownParseEndEvent) => void, false>;
|
|
6
|
-
/**
|
|
7
|
-
* Render a markdown document using Lynx's `<x-markdown>` XElement.
|
|
8
|
-
*
|
|
9
|
-
* The markdown source is passed via the `value` prop; it is delivered to the
|
|
10
|
-
* native element as a raw-text child (per the 3.7.0 "raw-text node
|
|
11
|
-
* optimization" path). Event props use signalx's automatic
|
|
12
|
-
* `onLink`→`bindlink` mapping in `nodeOps.parseEventProp`, so handlers wire
|
|
13
|
-
* up without any per-event glue.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```tsx
|
|
17
|
-
* <Markdown
|
|
18
|
-
* value={"# Hello\n\nThis is **markdown**."}
|
|
19
|
-
* effect="typewriter"
|
|
20
|
-
* onLink={(e) => console.log('tapped', e.detail.url)}
|
|
21
|
-
* />
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* @remarks
|
|
25
|
-
* Availability of the `<x-markdown>` element is platform-dependent — see
|
|
26
|
-
* `jsx-augment.ts` for the per-platform schedule. On platforms where the
|
|
27
|
-
* native element is not registered, the engine logs a warning and renders
|
|
28
|
-
* no view; there is no JS-side feature gate.
|
|
29
|
-
*/
|
|
30
|
-
export declare const Markdown: import("@sigx/runtime-core").ComponentFactory<MarkdownProps, void, {}>;
|