flux-md 0.16.2 → 0.17.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 +70 -39
- package/dist/block-props.d.ts +18 -0
- package/dist/block-props.js +75 -0
- package/dist/client.d.ts +310 -0
- package/{src/client.ts → dist/client.js} +123 -319
- package/dist/dom.d.ts +110 -0
- package/dist/dom.js +576 -0
- package/dist/element.d.ts +20 -0
- package/dist/element.js +287 -0
- package/dist/hi.d.ts +12 -0
- package/{src/hi.ts → dist/hi.js} +42 -74
- package/dist/html-to-react.d.ts +40 -0
- package/dist/html-to-react.js +344 -0
- package/{src/index.ts → dist/index.d.ts} +5 -25
- package/dist/index.js +16 -0
- package/dist/morph.d.ts +28 -0
- package/dist/morph.js +166 -0
- package/dist/react.d.ts +204 -0
- package/dist/react.js +490 -0
- package/dist/renderers/CodeBlock.d.ts +7 -0
- package/dist/renderers/CodeBlock.js +75 -0
- package/dist/renderers/Math.d.ts +14 -0
- package/dist/renderers/Math.js +15 -0
- package/dist/renderers/Mermaid.d.ts +13 -0
- package/dist/renderers/Mermaid.js +15 -0
- package/dist/server.d.ts +61 -0
- package/dist/server.js +126 -0
- package/{src/solid.tsx → dist/solid.d.ts} +19 -95
- package/dist/solid.js +54 -0
- package/dist/svelte.d.ts +80 -0
- package/dist/svelte.js +59 -0
- package/dist/types-core.d.ts +377 -0
- package/dist/types-core.js +0 -0
- package/{src/types-react.ts → dist/types-react.d.ts} +0 -1
- package/dist/types-react.js +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/vue.d.ts +94 -0
- package/dist/vue.js +79 -0
- package/{src → dist}/wasm/flux_md_core.d.ts +18 -6
- package/{src → dist}/wasm/flux_md_core.js +50 -55
- package/dist/wasm/flux_md_core_bg.wasm +0 -0
- package/{src → dist}/wasm/flux_md_core_bg.wasm.d.ts +1 -0
- package/dist/worker-core.d.ts +60 -0
- package/dist/worker-core.js +121 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +48 -0
- package/package.json +22 -18
- package/src/block-props.ts +0 -141
- package/src/dom.ts +0 -946
- package/src/element.ts +0 -400
- package/src/html-to-react.ts +0 -455
- package/src/morph.ts +0 -253
- package/src/react.tsx +0 -1020
- package/src/renderers/CodeBlock.tsx +0 -116
- package/src/renderers/Math.tsx +0 -28
- package/src/renderers/Mermaid.tsx +0 -27
- package/src/server.tsx +0 -221
- package/src/svelte.ts +0 -179
- package/src/types-core.ts +0 -398
- package/src/types.ts +0 -7
- package/src/vue.ts +0 -184
- package/src/wasm/flux_md_core_bg.wasm +0 -0
- package/src/worker-core.ts +0 -174
- package/src/worker.ts +0 -72
- /package/{src → dist}/styles.css +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<flux-markdown>` custom element — thin lifecycle glue over
|
|
3
|
+
* {@link mountFluxMarkdown}. It owns no diffing: connect mounts the DOM
|
|
4
|
+
* renderer into the element itself (LIGHT DOM, so the host app's markdown CSS
|
|
5
|
+
* reaches the content), disconnect tears the mount down. It never reimplements
|
|
6
|
+
* subscribe/patch.
|
|
7
|
+
*
|
|
8
|
+
* Two usage modes:
|
|
9
|
+
* - **Caller-owned client** (`el.client = myClient`): the element subscribes
|
|
10
|
+
* and mounts but NEVER destroys the client — the caller owns the
|
|
11
|
+
* worker/stream lifecycle.
|
|
12
|
+
* - **Self-owned client** (`markdown`/`src`/`textContent` attrs, or
|
|
13
|
+
* `el.append()`): the element lazily creates an internal client from its
|
|
14
|
+
* config attributes and destroys it on disconnect.
|
|
15
|
+
*
|
|
16
|
+
* Not auto-registered (SSR-unsafe): call {@link defineFluxMarkdown} from
|
|
17
|
+
* browser code.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseTriBool(value: string | null): boolean | undefined;
|
|
20
|
+
export declare function defineFluxMarkdown(tag?: string): void;
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { FluxClient } from "./client.js";
|
|
2
|
+
import { mountFluxMarkdown } from "./dom.js";
|
|
3
|
+
function parseTriBool(value) {
|
|
4
|
+
if (value === null) return void 0;
|
|
5
|
+
if (value === "" || value === "true" || value === "1") return true;
|
|
6
|
+
if (value === "false" || value === "0") return false;
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
const CONFIG_ATTRS = [
|
|
10
|
+
"gfm-autolinks",
|
|
11
|
+
"gfm-alerts",
|
|
12
|
+
"gfm-footnotes",
|
|
13
|
+
"gfm-math",
|
|
14
|
+
"dir-auto",
|
|
15
|
+
"a11y",
|
|
16
|
+
"unsafe-html"
|
|
17
|
+
];
|
|
18
|
+
function defineFluxMarkdown(tag = "flux-markdown") {
|
|
19
|
+
if (typeof customElements === "undefined") return;
|
|
20
|
+
if (customElements.get(tag)) return;
|
|
21
|
+
class FluxMarkdownElement extends HTMLElement {
|
|
22
|
+
static get observedAttributes() {
|
|
23
|
+
return ["markdown", "src", "component-tags", ...CONFIG_ATTRS];
|
|
24
|
+
}
|
|
25
|
+
#client = null;
|
|
26
|
+
#ownsClient = false;
|
|
27
|
+
#components = void 0;
|
|
28
|
+
#sanitize = void 0;
|
|
29
|
+
#handle = null;
|
|
30
|
+
#connected = false;
|
|
31
|
+
// In-flight `src` fetch supersession. A self-owned client is REUSED across
|
|
32
|
+
// src changes (not torn down), so two concurrent #streamFromSrc runs would
|
|
33
|
+
// capture the same client and reset() even reuses the worker streamId — an
|
|
34
|
+
// identity guard alone can't separate them. Each run captures the current
|
|
35
|
+
// #srcSeq; a newer src (or teardown) bumps it and aborts the fetch, so a
|
|
36
|
+
// stale run bails before interleaving its chunks into the parser.
|
|
37
|
+
#srcSeq = 0;
|
|
38
|
+
#srcAbort = null;
|
|
39
|
+
// --- Accessor properties (objects/functions can't be attributes) ---------
|
|
40
|
+
get client() {
|
|
41
|
+
return this.#client;
|
|
42
|
+
}
|
|
43
|
+
set client(value) {
|
|
44
|
+
if (value === this.#client) return;
|
|
45
|
+
this.#teardownClient();
|
|
46
|
+
this.#client = value;
|
|
47
|
+
this.#ownsClient = false;
|
|
48
|
+
if (this.#connected) this.#remount();
|
|
49
|
+
}
|
|
50
|
+
get components() {
|
|
51
|
+
return this.#components;
|
|
52
|
+
}
|
|
53
|
+
set components(value) {
|
|
54
|
+
if (value === this.#components) return;
|
|
55
|
+
this.#components = value;
|
|
56
|
+
if (this.#connected) this.#remount();
|
|
57
|
+
}
|
|
58
|
+
get sanitize() {
|
|
59
|
+
return this.#sanitize;
|
|
60
|
+
}
|
|
61
|
+
set sanitize(value) {
|
|
62
|
+
if (value === this.#sanitize) return;
|
|
63
|
+
this.#sanitize = value;
|
|
64
|
+
if (this.#connected) this.#remount();
|
|
65
|
+
}
|
|
66
|
+
// --- Self-owned-client methods -------------------------------------------
|
|
67
|
+
append(chunk) {
|
|
68
|
+
this.#cancelSrcStream();
|
|
69
|
+
this.#ensureClient();
|
|
70
|
+
this.#client.append(chunk);
|
|
71
|
+
}
|
|
72
|
+
finalize() {
|
|
73
|
+
this.#cancelSrcStream();
|
|
74
|
+
this.#client?.finalize();
|
|
75
|
+
}
|
|
76
|
+
reset() {
|
|
77
|
+
this.#cancelSrcStream();
|
|
78
|
+
this.#client?.reset();
|
|
79
|
+
}
|
|
80
|
+
getClient() {
|
|
81
|
+
return this.#client;
|
|
82
|
+
}
|
|
83
|
+
// --- Lifecycle -----------------------------------------------------------
|
|
84
|
+
connectedCallback() {
|
|
85
|
+
if (this.#connected) return;
|
|
86
|
+
this.#connected = true;
|
|
87
|
+
this.#upgradeProperty("client");
|
|
88
|
+
this.#upgradeProperty("components");
|
|
89
|
+
this.#upgradeProperty("sanitize");
|
|
90
|
+
this.#mountIfReady();
|
|
91
|
+
if (!this.#client || this.#ownsClient) {
|
|
92
|
+
this.#resolveInitialContent();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
96
|
+
if (!this.#connected) return;
|
|
97
|
+
if (oldValue === newValue) return;
|
|
98
|
+
if (name === "markdown" || name === "src") {
|
|
99
|
+
if (!this.#client || this.#ownsClient) {
|
|
100
|
+
this.#resolveInitialContent();
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (this.#client && !this.#ownsClient) {
|
|
105
|
+
console.warn(
|
|
106
|
+
"<flux-markdown>: config attributes are ignored while a caller-owned `client` is set (ParserConfig is immutable per stream)."
|
|
107
|
+
);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (this.#ownsClient) {
|
|
111
|
+
this.#teardownClient();
|
|
112
|
+
this.#mountIfReady();
|
|
113
|
+
this.#resolveInitialContent();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
disconnectedCallback() {
|
|
117
|
+
this.#connected = false;
|
|
118
|
+
this.#cancelSrcStream();
|
|
119
|
+
this.#handle?.destroy();
|
|
120
|
+
this.#handle = null;
|
|
121
|
+
if (this.#ownsClient) {
|
|
122
|
+
this.#client?.destroy();
|
|
123
|
+
this.#client = null;
|
|
124
|
+
this.#ownsClient = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// --- Internals -----------------------------------------------------------
|
|
128
|
+
#upgradeProperty(prop) {
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(this, prop)) {
|
|
130
|
+
const value = this[prop];
|
|
131
|
+
delete this[prop];
|
|
132
|
+
this[prop] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Build a ParserConfig from the current config attributes. Read ONCE, at
|
|
136
|
+
// client creation — config is immutable per stream.
|
|
137
|
+
#readConfig() {
|
|
138
|
+
const cfg = {};
|
|
139
|
+
let any = false;
|
|
140
|
+
const set = (attr, key) => {
|
|
141
|
+
const v = parseTriBool(this.getAttribute(attr));
|
|
142
|
+
if (v !== void 0) {
|
|
143
|
+
cfg[key] = v;
|
|
144
|
+
any = true;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
set("gfm-autolinks", "gfmAutolinks");
|
|
148
|
+
set("gfm-alerts", "gfmAlerts");
|
|
149
|
+
set("gfm-footnotes", "gfmFootnotes");
|
|
150
|
+
set("gfm-math", "gfmMath");
|
|
151
|
+
set("dir-auto", "dirAuto");
|
|
152
|
+
set("a11y", "a11y");
|
|
153
|
+
set("unsafe-html", "unsafeHtml");
|
|
154
|
+
const tags = this.getAttribute("component-tags");
|
|
155
|
+
if (tags !== null) {
|
|
156
|
+
const list = tags.split(/[\s,]+/).filter(Boolean);
|
|
157
|
+
if (list.length > 0) {
|
|
158
|
+
cfg.componentTags = list;
|
|
159
|
+
any = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return any ? cfg : void 0;
|
|
163
|
+
}
|
|
164
|
+
// Lazily create the internal client from config attributes (self-owned).
|
|
165
|
+
#ensureClient() {
|
|
166
|
+
if (this.#client) return;
|
|
167
|
+
this.#client = new FluxClient({ config: this.#readConfig() });
|
|
168
|
+
this.#ownsClient = true;
|
|
169
|
+
this.#mountIfReady();
|
|
170
|
+
}
|
|
171
|
+
// Mount once a client exists and we're connected. Idempotent.
|
|
172
|
+
#mountIfReady() {
|
|
173
|
+
if (!this.#connected || !this.#client || this.#handle) return;
|
|
174
|
+
this.#handle = mountFluxMarkdown(this.#client, this, {
|
|
175
|
+
components: this.#components,
|
|
176
|
+
sanitize: this.#sanitize
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// Destroy the current mount and remount against the current client+options.
|
|
180
|
+
// Used when a property changes while connected.
|
|
181
|
+
#remount() {
|
|
182
|
+
this.#handle?.destroy();
|
|
183
|
+
this.#handle = null;
|
|
184
|
+
this.#mountIfReady();
|
|
185
|
+
}
|
|
186
|
+
// Tear down only the client side (mount stays / is handled by the caller).
|
|
187
|
+
// Destroys the client only if self-owned, then clears it and the mount so
|
|
188
|
+
// the next mount targets a fresh client.
|
|
189
|
+
#teardownClient() {
|
|
190
|
+
this.#cancelSrcStream();
|
|
191
|
+
this.#handle?.destroy();
|
|
192
|
+
this.#handle = null;
|
|
193
|
+
if (this.#ownsClient) this.#client?.destroy();
|
|
194
|
+
this.#client = null;
|
|
195
|
+
this.#ownsClient = false;
|
|
196
|
+
}
|
|
197
|
+
// Resolve the initial content of a self-owned stream from the attributes,
|
|
198
|
+
// in priority order: `src` (fetch+stream) > `markdown` (one-shot) >
|
|
199
|
+
// textContent (one-shot). A caller-owned client never reaches here.
|
|
200
|
+
#resolveInitialContent() {
|
|
201
|
+
this.#cancelSrcStream();
|
|
202
|
+
const src = this.getAttribute("src");
|
|
203
|
+
if (src) {
|
|
204
|
+
void this.#streamFromSrc(src);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const markdown = this.getAttribute("markdown");
|
|
208
|
+
if (markdown !== null) {
|
|
209
|
+
this.#oneShot(markdown);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const text = this.#captureSourceText();
|
|
213
|
+
if (text.trim().length > 0) this.#oneShot(text);
|
|
214
|
+
}
|
|
215
|
+
// Read the raw markdown the host put between the tags, ignoring the
|
|
216
|
+
// renderer's `.flux-md` root (and any other element children).
|
|
217
|
+
#captureSourceText() {
|
|
218
|
+
let text = "";
|
|
219
|
+
for (const node of Array.from(this.childNodes)) {
|
|
220
|
+
if (node.nodeType === 3) {
|
|
221
|
+
text += node.textContent ?? "";
|
|
222
|
+
node.parentNode?.removeChild(node);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return text;
|
|
226
|
+
}
|
|
227
|
+
// One-shot: reset the stream (in case content changed), feed it, finalize.
|
|
228
|
+
#oneShot(markdown) {
|
|
229
|
+
this.#ensureClient();
|
|
230
|
+
this.#client.reset();
|
|
231
|
+
this.#client.append(markdown);
|
|
232
|
+
this.#client.finalize();
|
|
233
|
+
}
|
|
234
|
+
// Abort any in-flight `src` fetch and invalidate its read loop, so it can
|
|
235
|
+
// no longer append into a client we're about to reuse, swap, or destroy.
|
|
236
|
+
#cancelSrcStream() {
|
|
237
|
+
this.#srcSeq++;
|
|
238
|
+
this.#srcAbort?.abort();
|
|
239
|
+
this.#srcAbort = null;
|
|
240
|
+
}
|
|
241
|
+
// Fetch a URL and stream its body. TextDecoder with {stream:true} carries a
|
|
242
|
+
// multibyte sequence that straddles a chunk boundary into the next decode.
|
|
243
|
+
async #streamFromSrc(src) {
|
|
244
|
+
this.#cancelSrcStream();
|
|
245
|
+
const token = this.#srcSeq;
|
|
246
|
+
const abort = new AbortController();
|
|
247
|
+
this.#srcAbort = abort;
|
|
248
|
+
this.#ensureClient();
|
|
249
|
+
this.#client.reset();
|
|
250
|
+
const owned = this.#client;
|
|
251
|
+
const current = () => this.#srcSeq === token && this.#client === owned;
|
|
252
|
+
try {
|
|
253
|
+
const res = await fetch(src, { signal: abort.signal });
|
|
254
|
+
if (!current()) return;
|
|
255
|
+
const body = res.body;
|
|
256
|
+
if (!body) {
|
|
257
|
+
const text = await res.text();
|
|
258
|
+
if (!current()) return;
|
|
259
|
+
owned.append(text);
|
|
260
|
+
owned.finalize();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const reader = body.getReader();
|
|
264
|
+
const decoder = new TextDecoder();
|
|
265
|
+
for (; ; ) {
|
|
266
|
+
const { done, value } = await reader.read();
|
|
267
|
+
if (!current()) return;
|
|
268
|
+
if (done) break;
|
|
269
|
+
if (value) owned.append(decoder.decode(value, { stream: true }));
|
|
270
|
+
}
|
|
271
|
+
owned.append(decoder.decode());
|
|
272
|
+
owned.finalize();
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (abort.signal.aborted || !current()) return;
|
|
275
|
+
console.error(
|
|
276
|
+
"<flux-markdown>: failed to stream src",
|
|
277
|
+
err instanceof Error ? err.message : String(err)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
customElements.define(tag, FluxMarkdownElement);
|
|
283
|
+
}
|
|
284
|
+
export {
|
|
285
|
+
defineFluxMarkdown,
|
|
286
|
+
parseTriBool
|
|
287
|
+
};
|
package/dist/hi.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-house syntax highlighter. Native RegExp only — no Shiki, no Prism, no
|
|
3
|
+
* Highlight.js. Covers the languages an LLM typically emits:
|
|
4
|
+
* js/ts/tsx/jsx, rust, python, go, bash, json, html, css, sql. Unknown
|
|
5
|
+
* languages fall through to plain escaped text. ~6KB minified.
|
|
6
|
+
*
|
|
7
|
+
* Highlighting is per-block, runs once when the block closes. We never
|
|
8
|
+
* highlight an open (streaming) block, which avoids re-highlighting the same
|
|
9
|
+
* code on every chunk — the main perf win for streaming code.
|
|
10
|
+
*/
|
|
11
|
+
export declare function highlight(code: string, lang: string): string;
|
|
12
|
+
export declare function supportedLangs(): string[];
|
package/{src/hi.ts → dist/hi.js}
RENAMED
|
@@ -1,55 +1,38 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-house syntax highlighter. Native RegExp only — no Shiki, no Prism, no
|
|
3
|
-
* Highlight.js. Covers the languages an LLM typically emits:
|
|
4
|
-
* js/ts/tsx/jsx, rust, python, go, bash, json, html, css, sql. Unknown
|
|
5
|
-
* languages fall through to plain escaped text. ~6KB minified.
|
|
6
|
-
*
|
|
7
|
-
* Highlighting is per-block, runs once when the block closes. We never
|
|
8
|
-
* highlight an open (streaming) block, which avoids re-highlighting the same
|
|
9
|
-
* code on every chunk — the main perf win for streaming code.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
1
|
const KEYWORDS_JS = new Set(
|
|
13
2
|
"async await break case catch class const continue debugger default delete do else export extends false finally for from function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while with yield".split(
|
|
14
|
-
" "
|
|
15
|
-
)
|
|
3
|
+
" "
|
|
4
|
+
)
|
|
16
5
|
);
|
|
17
|
-
const KEYWORDS_TS = new Set([
|
|
6
|
+
const KEYWORDS_TS = /* @__PURE__ */ new Set([
|
|
18
7
|
...KEYWORDS_JS,
|
|
19
|
-
...["any", "as", "boolean", "declare", "enum", "interface", "is", "keyof", "module", "namespace", "never", "number", "private", "protected", "public", "readonly", "string", "type", "unknown", "satisfies"]
|
|
8
|
+
...["any", "as", "boolean", "declare", "enum", "interface", "is", "keyof", "module", "namespace", "never", "number", "private", "protected", "public", "readonly", "string", "type", "unknown", "satisfies"]
|
|
20
9
|
]);
|
|
21
10
|
const KEYWORDS_RUST = new Set(
|
|
22
11
|
"as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return Self self static struct super trait true type unsafe use where while".split(
|
|
23
|
-
" "
|
|
24
|
-
)
|
|
12
|
+
" "
|
|
13
|
+
)
|
|
25
14
|
);
|
|
26
15
|
const KEYWORDS_PY = new Set(
|
|
27
16
|
"False None True and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield".split(
|
|
28
|
-
" "
|
|
29
|
-
)
|
|
17
|
+
" "
|
|
18
|
+
)
|
|
30
19
|
);
|
|
31
20
|
const KEYWORDS_GO = new Set(
|
|
32
21
|
"break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var nil true false".split(
|
|
33
|
-
" "
|
|
34
|
-
)
|
|
22
|
+
" "
|
|
23
|
+
)
|
|
35
24
|
);
|
|
36
25
|
const KEYWORDS_BASH = new Set(
|
|
37
26
|
"if then elif else fi case esac for select while until do done function in time coproc return break continue".split(
|
|
38
|
-
" "
|
|
39
|
-
)
|
|
27
|
+
" "
|
|
28
|
+
)
|
|
40
29
|
);
|
|
41
30
|
const KEYWORDS_SQL = new Set(
|
|
42
31
|
"SELECT FROM WHERE JOIN LEFT RIGHT INNER OUTER ON GROUP BY ORDER HAVING LIMIT OFFSET INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE DROP ALTER INDEX VIEW IF EXISTS NOT NULL DEFAULT PRIMARY KEY FOREIGN REFERENCES UNIQUE AS WITH UNION ALL DISTINCT IS BETWEEN LIKE IN AND OR".split(
|
|
43
|
-
" "
|
|
44
|
-
)
|
|
32
|
+
" "
|
|
33
|
+
)
|
|
45
34
|
);
|
|
46
|
-
|
|
47
|
-
// Each language is described by an ordered list of (token-class, regex) pairs.
|
|
48
|
-
// The regex must be sticky (y flag) so it only matches at the current cursor.
|
|
49
|
-
// First match wins.
|
|
50
|
-
type Pat = [string, RegExp];
|
|
51
|
-
|
|
52
|
-
const jsPats: Pat[] = [
|
|
35
|
+
const jsPats = [
|
|
53
36
|
["com", /\/\/[^\n]*/y],
|
|
54
37
|
["com", /\/\*[\s\S]*?\*\//y],
|
|
55
38
|
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
@@ -59,10 +42,9 @@ const jsPats: Pat[] = [
|
|
|
59
42
|
["num", /\b(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)\b/y],
|
|
60
43
|
["ident", /[A-Za-z_$][\w$]*/y],
|
|
61
44
|
["pun", /[+\-*/=<>!&|^~?:;,.[\](){}]/y],
|
|
62
|
-
["ws", /\s+/y]
|
|
45
|
+
["ws", /\s+/y]
|
|
63
46
|
];
|
|
64
|
-
|
|
65
|
-
const rustPats: Pat[] = [
|
|
47
|
+
const rustPats = [
|
|
66
48
|
["com", /\/\/[^\n]*/y],
|
|
67
49
|
["com", /\/\*[\s\S]*?\*\//y],
|
|
68
50
|
["str", /b?"(?:\\.|[^"\\])*"/y],
|
|
@@ -73,10 +55,9 @@ const rustPats: Pat[] = [
|
|
|
73
55
|
["attr", /#!?\[[^\]]*\]/y],
|
|
74
56
|
["ident", /[A-Za-z_]\w*/y],
|
|
75
57
|
["pun", /[+\-*/=<>!&|^~?:;,.\[\](){}@]/y],
|
|
76
|
-
["ws", /\s+/y]
|
|
58
|
+
["ws", /\s+/y]
|
|
77
59
|
];
|
|
78
|
-
|
|
79
|
-
const pyPats: Pat[] = [
|
|
60
|
+
const pyPats = [
|
|
80
61
|
["com", /#[^\n]*/y],
|
|
81
62
|
["str", /[fFrRbB]{0,2}"""[\s\S]*?"""/y],
|
|
82
63
|
["str", /[fFrRbB]{0,2}'''[\s\S]*?'''/y],
|
|
@@ -86,10 +67,9 @@ const pyPats: Pat[] = [
|
|
|
86
67
|
["dec", /@[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*/y],
|
|
87
68
|
["ident", /[A-Za-z_]\w*/y],
|
|
88
69
|
["pun", /[+\-*/=<>!&|^~?:;,.[\](){}@%]/y],
|
|
89
|
-
["ws", /\s+/y]
|
|
70
|
+
["ws", /\s+/y]
|
|
90
71
|
];
|
|
91
|
-
|
|
92
|
-
const goPats: Pat[] = [
|
|
72
|
+
const goPats = [
|
|
93
73
|
["com", /\/\/[^\n]*/y],
|
|
94
74
|
["com", /\/\*[\s\S]*?\*\//y],
|
|
95
75
|
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
@@ -98,10 +78,9 @@ const goPats: Pat[] = [
|
|
|
98
78
|
["num", /\b\d[\d_]*(?:\.\d[\d_]*)?\b/y],
|
|
99
79
|
["ident", /[A-Za-z_]\w*/y],
|
|
100
80
|
["pun", /[+\-*/=<>!&|^~?:;,.[\](){}]/y],
|
|
101
|
-
["ws", /\s+/y]
|
|
81
|
+
["ws", /\s+/y]
|
|
102
82
|
];
|
|
103
|
-
|
|
104
|
-
const bashPats: Pat[] = [
|
|
83
|
+
const bashPats = [
|
|
105
84
|
["com", /#[^\n]*/y],
|
|
106
85
|
["str", /"(?:\\.|[^"\\])*"/y],
|
|
107
86
|
["str", /'[^']*'/y],
|
|
@@ -109,18 +88,16 @@ const bashPats: Pat[] = [
|
|
|
109
88
|
["num", /\b\d+\b/y],
|
|
110
89
|
["ident", /[A-Za-z_][\w-]*/y],
|
|
111
90
|
["pun", /[|&;<>(){}[\]=]/y],
|
|
112
|
-
["ws", /\s+/y]
|
|
91
|
+
["ws", /\s+/y]
|
|
113
92
|
];
|
|
114
|
-
|
|
115
|
-
const jsonPats: Pat[] = [
|
|
93
|
+
const jsonPats = [
|
|
116
94
|
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
117
95
|
["num", /-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/y],
|
|
118
96
|
["kw", /\b(?:true|false|null)\b/y],
|
|
119
97
|
["pun", /[{}[\]:,]/y],
|
|
120
|
-
["ws", /\s+/y]
|
|
98
|
+
["ws", /\s+/y]
|
|
121
99
|
];
|
|
122
|
-
|
|
123
|
-
const sqlPats: Pat[] = [
|
|
100
|
+
const sqlPats = [
|
|
124
101
|
["com", /--[^\n]*/y],
|
|
125
102
|
["com", /\/\*[\s\S]*?\*\//y],
|
|
126
103
|
["str", /'(?:''|[^'])*'/y],
|
|
@@ -128,30 +105,27 @@ const sqlPats: Pat[] = [
|
|
|
128
105
|
["num", /\b\d+(?:\.\d+)?\b/y],
|
|
129
106
|
["ident", /[A-Za-z_][\w]*/y],
|
|
130
107
|
["pun", /[+\-*/=<>!,;.(){}]/y],
|
|
131
|
-
["ws", /\s+/y]
|
|
108
|
+
["ws", /\s+/y]
|
|
132
109
|
];
|
|
133
|
-
|
|
134
|
-
const htmlPats: Pat[] = [
|
|
110
|
+
const htmlPats = [
|
|
135
111
|
["com", /<!--[\s\S]*?-->/y],
|
|
136
112
|
["tag", /<\/?[A-Za-z][\w-]*/y],
|
|
137
113
|
["str", /"[^"]*"/y],
|
|
138
114
|
["str", /'[^']*'/y],
|
|
139
115
|
["attr", /[A-Za-z][\w-]*(?==)/y],
|
|
140
116
|
["pun", /[=/>]/y],
|
|
141
|
-
["txt", /[^<>"'=]+/y]
|
|
117
|
+
["txt", /[^<>"'=]+/y]
|
|
142
118
|
];
|
|
143
|
-
|
|
144
|
-
const cssPats: Pat[] = [
|
|
119
|
+
const cssPats = [
|
|
145
120
|
["com", /\/\*[\s\S]*?\*\//y],
|
|
146
121
|
["str", /"[^"]*"/y],
|
|
147
122
|
["str", /'[^']*'/y],
|
|
148
123
|
["num", /-?\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms|deg)?/y],
|
|
149
124
|
["sel", /[#.]?[A-Za-z][\w-]*/y],
|
|
150
125
|
["pun", /[:;,{}()]/y],
|
|
151
|
-
["ws", /\s+/y]
|
|
126
|
+
["ws", /\s+/y]
|
|
152
127
|
];
|
|
153
|
-
|
|
154
|
-
const LANGS: Record<string, { pats: Pat[]; kw?: Set<string> }> = {
|
|
128
|
+
const LANGS = {
|
|
155
129
|
js: { pats: jsPats, kw: KEYWORDS_JS },
|
|
156
130
|
javascript: { pats: jsPats, kw: KEYWORDS_JS },
|
|
157
131
|
ts: { pats: jsPats, kw: KEYWORDS_TS },
|
|
@@ -170,10 +144,9 @@ const LANGS: Record<string, { pats: Pat[]; kw?: Set<string> }> = {
|
|
|
170
144
|
sql: { pats: sqlPats, kw: KEYWORDS_SQL },
|
|
171
145
|
html: { pats: htmlPats },
|
|
172
146
|
xml: { pats: htmlPats },
|
|
173
|
-
css: { pats: cssPats }
|
|
147
|
+
css: { pats: cssPats }
|
|
174
148
|
};
|
|
175
|
-
|
|
176
|
-
function escapeHtml(s: string): string {
|
|
149
|
+
function escapeHtml(s) {
|
|
177
150
|
let out = "";
|
|
178
151
|
for (let i = 0; i < s.length; i++) {
|
|
179
152
|
const c = s[i];
|
|
@@ -185,19 +158,14 @@ function escapeHtml(s: string): string {
|
|
|
185
158
|
}
|
|
186
159
|
return out;
|
|
187
160
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Defense-in-depth: never tokenize a pathologically huge block on the main
|
|
191
|
-
// thread — fall back to plain escaped text.
|
|
192
|
-
if (code.length > 50_000) return escapeHtml(code);
|
|
161
|
+
function highlight(code, lang) {
|
|
162
|
+
if (code.length > 5e4) return escapeHtml(code);
|
|
193
163
|
const conf = LANGS[lang.toLowerCase()];
|
|
194
164
|
if (!conf) return escapeHtml(code);
|
|
195
|
-
|
|
196
165
|
let out = "";
|
|
197
166
|
let pos = 0;
|
|
198
167
|
const pats = conf.pats;
|
|
199
168
|
const kw = conf.kw;
|
|
200
|
-
// Linear pass with sticky regex tracking lastIndex.
|
|
201
169
|
while (pos < code.length) {
|
|
202
170
|
let matched = false;
|
|
203
171
|
for (let i = 0; i < pats.length; i++) {
|
|
@@ -216,7 +184,6 @@ export function highlight(code: string, lang: string): string {
|
|
|
216
184
|
} else if (text.length > 1 && text[0] >= "A" && text[0] <= "Z") {
|
|
217
185
|
finalCls = "ty";
|
|
218
186
|
} else {
|
|
219
|
-
// Plain identifier — no span needed.
|
|
220
187
|
out += escapeHtml(text);
|
|
221
188
|
pos = after;
|
|
222
189
|
matched = true;
|
|
@@ -233,15 +200,16 @@ export function highlight(code: string, lang: string): string {
|
|
|
233
200
|
break;
|
|
234
201
|
}
|
|
235
202
|
if (!matched) {
|
|
236
|
-
// No pattern matched (shouldn't happen with a catch-all ws/other) — emit
|
|
237
|
-
// one char as plain text to make progress.
|
|
238
203
|
out += escapeHtml(code[pos]);
|
|
239
204
|
pos += 1;
|
|
240
205
|
}
|
|
241
206
|
}
|
|
242
207
|
return out;
|
|
243
208
|
}
|
|
244
|
-
|
|
245
|
-
export function supportedLangs(): string[] {
|
|
209
|
+
function supportedLangs() {
|
|
246
210
|
return Object.keys(LANGS);
|
|
247
211
|
}
|
|
212
|
+
export {
|
|
213
|
+
highlight,
|
|
214
|
+
supportedLangs
|
|
215
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { Components } from "./types.js";
|
|
3
|
+
type HNode = {
|
|
4
|
+
kind: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "el";
|
|
8
|
+
tag: string;
|
|
9
|
+
attrs: Record<string, string | true>;
|
|
10
|
+
children: HNode[];
|
|
11
|
+
};
|
|
12
|
+
/** Decode the (small, known) set of entities the core emits, plus numeric refs. */
|
|
13
|
+
export declare function decodeEntities(s: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Parse an inline CSS string (`"text-align:left;color:red"`) into the object
|
|
16
|
+
* React's `style` prop requires, camelCasing property names. Custom properties
|
|
17
|
+
* (`--x`) keep their literal name.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseStyle(css: string): Record<string, string>;
|
|
20
|
+
export declare function getParseCount(): number;
|
|
21
|
+
export declare function resetParseCount(): void;
|
|
22
|
+
export declare function parseTrustedHtml(html: string): HNode[];
|
|
23
|
+
/**
|
|
24
|
+
* Convert a block's trusted HTML string into a React node tree, replacing any
|
|
25
|
+
* element whose tag name appears in `components`.
|
|
26
|
+
*
|
|
27
|
+
* With no `childMemoMap`, behavior is byte-identical to a single
|
|
28
|
+
* `parseTrustedHtml` + convert pass — the closed-block call site memoizes on
|
|
29
|
+
* `(html, components)`.
|
|
30
|
+
*
|
|
31
|
+
* Passing a `childMemoMap` opts into OPEN-block child reuse: the html is split
|
|
32
|
+
* into top-level node segments and each is keyed by its exact substring. On a
|
|
33
|
+
* hit the cached React node is reused (no re-parse, no re-serialize); only new /
|
|
34
|
+
* changed trailing segments are parsed. The caller owns the map's lifetime and
|
|
35
|
+
* must scope it per block.id and invalidate it when `components` changes (a hit
|
|
36
|
+
* carries the React node built under the previous components map). Segment keys
|
|
37
|
+
* carry their original document order via `keyOffset` so React keys stay stable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function htmlToReact(html: string, components: Components, childMemoMap?: Map<string, ReactNode>): ReactNode;
|
|
40
|
+
export {};
|