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,344 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
const VOID = /* @__PURE__ */ new Set([
|
|
3
|
+
"area",
|
|
4
|
+
"base",
|
|
5
|
+
"br",
|
|
6
|
+
"col",
|
|
7
|
+
"embed",
|
|
8
|
+
"hr",
|
|
9
|
+
"img",
|
|
10
|
+
"input",
|
|
11
|
+
"link",
|
|
12
|
+
"meta",
|
|
13
|
+
"param",
|
|
14
|
+
"source",
|
|
15
|
+
"track",
|
|
16
|
+
"wbr"
|
|
17
|
+
]);
|
|
18
|
+
const ATTR_MAP = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
19
|
+
class: "className",
|
|
20
|
+
for: "htmlFor",
|
|
21
|
+
colspan: "colSpan",
|
|
22
|
+
rowspan: "rowSpan",
|
|
23
|
+
tabindex: "tabIndex",
|
|
24
|
+
maxlength: "maxLength",
|
|
25
|
+
minlength: "minLength",
|
|
26
|
+
readonly: "readOnly",
|
|
27
|
+
autocomplete: "autoComplete",
|
|
28
|
+
autofocus: "autoFocus",
|
|
29
|
+
spellcheck: "spellCheck",
|
|
30
|
+
contenteditable: "contentEditable",
|
|
31
|
+
crossorigin: "crossOrigin",
|
|
32
|
+
enterkeyhint: "enterKeyHint",
|
|
33
|
+
inputmode: "inputMode"
|
|
34
|
+
});
|
|
35
|
+
const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href", "formaction", "action", "poster", "data"]);
|
|
36
|
+
const PROP_DENY = /* @__PURE__ */ new Set([
|
|
37
|
+
"dangerouslysetinnerhtml",
|
|
38
|
+
"ref",
|
|
39
|
+
"key",
|
|
40
|
+
"defaultvalue",
|
|
41
|
+
"defaultchecked",
|
|
42
|
+
"suppresshydrationwarning",
|
|
43
|
+
"suppresscontenteditablewarning"
|
|
44
|
+
]);
|
|
45
|
+
const SAFE_ATTR_NAME = /^[a-z][a-z0-9-]*$/i;
|
|
46
|
+
function safeUrl(value) {
|
|
47
|
+
let decoded = value;
|
|
48
|
+
for (let i = 0, prev = ""; i < 8 && decoded !== prev; i++) {
|
|
49
|
+
prev = decoded;
|
|
50
|
+
decoded = decodeEntities(decoded);
|
|
51
|
+
}
|
|
52
|
+
const probe = decoded.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").replace(/^\s+/, "").toLowerCase();
|
|
53
|
+
if (probe.startsWith("javascript:") || probe.startsWith("vbscript:") || probe.startsWith("data:text/html") || probe.startsWith("data:text/javascript")) {
|
|
54
|
+
return "#";
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
const NAMED_ENTITIES = {
|
|
59
|
+
amp: "&",
|
|
60
|
+
lt: "<",
|
|
61
|
+
gt: ">",
|
|
62
|
+
quot: '"',
|
|
63
|
+
apos: "'",
|
|
64
|
+
nbsp: "\xA0",
|
|
65
|
+
copy: "\xA9",
|
|
66
|
+
reg: "\xAE",
|
|
67
|
+
hellip: "\u2026",
|
|
68
|
+
mdash: "\u2014",
|
|
69
|
+
ndash: "\u2013"
|
|
70
|
+
};
|
|
71
|
+
function decodeEntities(s) {
|
|
72
|
+
if (s.indexOf("&") === -1) return s;
|
|
73
|
+
return s.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]*);/g, (m, body) => {
|
|
74
|
+
if (body[0] === "#") {
|
|
75
|
+
const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
76
|
+
if (Number.isNaN(code) || code < 0 || code > 1114111) return m;
|
|
77
|
+
try {
|
|
78
|
+
return String.fromCodePoint(code);
|
|
79
|
+
} catch {
|
|
80
|
+
return m;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const named = NAMED_ENTITIES[body];
|
|
84
|
+
return named === void 0 ? m : named;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function parseStyle(css) {
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const decl of css.split(";")) {
|
|
90
|
+
const c = decl.indexOf(":");
|
|
91
|
+
if (c === -1) continue;
|
|
92
|
+
const rawName = decl.slice(0, c).trim();
|
|
93
|
+
const value = decl.slice(c + 1).trim();
|
|
94
|
+
if (!rawName || !value) continue;
|
|
95
|
+
const name = rawName.startsWith("--") ? rawName : rawName.toLowerCase().replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
96
|
+
out[name] = value;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
const DANGEROUS_CSS_VALUE = /url\(|expression\(|image-set\(|-moz-binding|@import|behavior\s*:/i;
|
|
101
|
+
function safeStyle(style) {
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const k in style) {
|
|
104
|
+
const v = style[k];
|
|
105
|
+
if (DANGEROUS_CSS_VALUE.test(v)) continue;
|
|
106
|
+
if (k.toLowerCase() === "position" && /\b(?:fixed|sticky)\b/i.test(v)) continue;
|
|
107
|
+
out[k] = v;
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
const TAG_CHAR = /[a-zA-Z0-9-]/;
|
|
112
|
+
const WS_CHAR = /\s/;
|
|
113
|
+
const ATTR_NAME_END = /[\s=>/]/;
|
|
114
|
+
const UNQUOTED_VALUE_END = /[\s>]/;
|
|
115
|
+
function parseOpenTag(html, start) {
|
|
116
|
+
let i = start + 1;
|
|
117
|
+
let j = i;
|
|
118
|
+
while (j < html.length && TAG_CHAR.test(html[j])) j++;
|
|
119
|
+
const tag = html.slice(i, j);
|
|
120
|
+
i = j;
|
|
121
|
+
const attrs = {};
|
|
122
|
+
while (i < html.length) {
|
|
123
|
+
const loopStart = i;
|
|
124
|
+
while (i < html.length && WS_CHAR.test(html[i])) i++;
|
|
125
|
+
if (html[i] === ">") return { tag, attrs, selfClose: false, next: i + 1 };
|
|
126
|
+
if (html[i] === "/" && html[i + 1] === ">") return { tag, attrs, selfClose: true, next: i + 2 };
|
|
127
|
+
if (i >= html.length) break;
|
|
128
|
+
let k = i;
|
|
129
|
+
while (k < html.length && !ATTR_NAME_END.test(html[k])) k++;
|
|
130
|
+
const name = html.slice(i, k);
|
|
131
|
+
i = k;
|
|
132
|
+
while (i < html.length && WS_CHAR.test(html[i])) i++;
|
|
133
|
+
if (html[i] === "=") {
|
|
134
|
+
i++;
|
|
135
|
+
while (i < html.length && WS_CHAR.test(html[i])) i++;
|
|
136
|
+
let value = "";
|
|
137
|
+
const q = html[i];
|
|
138
|
+
if (q === '"' || q === "'") {
|
|
139
|
+
i++;
|
|
140
|
+
const e = html.indexOf(q, i);
|
|
141
|
+
value = html.slice(i, e === -1 ? html.length : e);
|
|
142
|
+
i = e === -1 ? html.length : e + 1;
|
|
143
|
+
} else {
|
|
144
|
+
let v = i;
|
|
145
|
+
while (v < html.length && !UNQUOTED_VALUE_END.test(html[v])) v++;
|
|
146
|
+
value = html.slice(i, v);
|
|
147
|
+
i = v;
|
|
148
|
+
}
|
|
149
|
+
if (name) attrs[name] = decodeEntities(value);
|
|
150
|
+
} else if (name) {
|
|
151
|
+
attrs[name] = true;
|
|
152
|
+
}
|
|
153
|
+
if (i <= loopStart) i = loopStart + 1;
|
|
154
|
+
}
|
|
155
|
+
return { tag, attrs, selfClose: false, next: i };
|
|
156
|
+
}
|
|
157
|
+
let parseCount = 0;
|
|
158
|
+
function getParseCount() {
|
|
159
|
+
return parseCount;
|
|
160
|
+
}
|
|
161
|
+
function resetParseCount() {
|
|
162
|
+
parseCount = 0;
|
|
163
|
+
}
|
|
164
|
+
function parseTrustedHtml(html) {
|
|
165
|
+
parseCount++;
|
|
166
|
+
const root = [];
|
|
167
|
+
const stack = [];
|
|
168
|
+
let i = 0;
|
|
169
|
+
const push = (n) => {
|
|
170
|
+
if (stack.length) stack[stack.length - 1].children.push(n);
|
|
171
|
+
else root.push(n);
|
|
172
|
+
};
|
|
173
|
+
while (i < html.length) {
|
|
174
|
+
const lt = html.indexOf("<", i);
|
|
175
|
+
if (lt === -1) {
|
|
176
|
+
const t = html.slice(i);
|
|
177
|
+
if (t) push({ kind: "text", text: decodeEntities(t) });
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (lt > i) push({ kind: "text", text: decodeEntities(html.slice(i, lt)) });
|
|
181
|
+
if (html.startsWith("<!--", lt)) {
|
|
182
|
+
const end = html.indexOf("-->", lt + 4);
|
|
183
|
+
i = end === -1 ? html.length : end + 3;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (html[lt + 1] === "!") {
|
|
187
|
+
const end = html.indexOf(">", lt);
|
|
188
|
+
i = end === -1 ? html.length : end + 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (html[lt + 1] === "/") {
|
|
192
|
+
const end = html.indexOf(">", lt);
|
|
193
|
+
const closeLower = html.slice(lt + 2, end === -1 ? html.length : end).trim().toLowerCase();
|
|
194
|
+
for (let s = stack.length - 1; s >= 0; s--) {
|
|
195
|
+
if (stack[s].tag.toLowerCase() === closeLower) {
|
|
196
|
+
stack.length = s;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
i = end === -1 ? html.length : end + 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const c1 = html[lt + 1];
|
|
204
|
+
const isName = c1 >= "a" && c1 <= "z" || c1 >= "A" && c1 <= "Z";
|
|
205
|
+
if (!isName) {
|
|
206
|
+
push({ kind: "text", text: "<" });
|
|
207
|
+
i = lt + 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const { tag, attrs, selfClose, next } = parseOpenTag(html, lt);
|
|
211
|
+
const el = { kind: "el", tag, attrs, children: [] };
|
|
212
|
+
push(el);
|
|
213
|
+
if (!selfClose && !VOID.has(tag.toLowerCase())) stack.push(el);
|
|
214
|
+
i = next;
|
|
215
|
+
}
|
|
216
|
+
return root;
|
|
217
|
+
}
|
|
218
|
+
function attrsToProps(tag, attrs, key) {
|
|
219
|
+
const props = { key };
|
|
220
|
+
for (const name in attrs) {
|
|
221
|
+
const value = attrs[name];
|
|
222
|
+
const lower = name.toLowerCase();
|
|
223
|
+
if (lower.startsWith("on")) continue;
|
|
224
|
+
if (PROP_DENY.has(lower)) continue;
|
|
225
|
+
if (lower === "style" && typeof value === "string") {
|
|
226
|
+
props.style = safeStyle(parseStyle(value));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (URL_ATTRS.has(lower) && typeof value === "string") {
|
|
230
|
+
props[ATTR_MAP[lower] ?? name] = safeUrl(value);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (tag.toLowerCase() === "input" && lower === "checked") {
|
|
234
|
+
props.defaultChecked = value === true ? true : value;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!(lower in ATTR_MAP) && !SAFE_ATTR_NAME.test(name)) continue;
|
|
238
|
+
props[ATTR_MAP[lower] ?? name] = value;
|
|
239
|
+
}
|
|
240
|
+
return props;
|
|
241
|
+
}
|
|
242
|
+
function nodesToReact(nodes, components, keyPrefix) {
|
|
243
|
+
const out = [];
|
|
244
|
+
for (let idx = 0; idx < nodes.length; idx++) {
|
|
245
|
+
const n = nodes[idx];
|
|
246
|
+
if (n.kind === "text") {
|
|
247
|
+
out.push(n.text);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const key = keyPrefix + idx;
|
|
251
|
+
const type = components[n.tag] ?? n.tag;
|
|
252
|
+
const props = attrsToProps(n.tag, n.attrs, key);
|
|
253
|
+
if (VOID.has(n.tag.toLowerCase())) {
|
|
254
|
+
out.push(createElement(type, props));
|
|
255
|
+
} else {
|
|
256
|
+
out.push(createElement(type, props, nodesToReact(n.children, components, key + ".")));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return out.length === 0 ? null : out.length === 1 ? out[0] : out;
|
|
260
|
+
}
|
|
261
|
+
function topLevelSegments(html) {
|
|
262
|
+
const segs = [];
|
|
263
|
+
let depth = 0;
|
|
264
|
+
let segStart = 0;
|
|
265
|
+
let i = 0;
|
|
266
|
+
while (i < html.length) {
|
|
267
|
+
const lt = html.indexOf("<", i);
|
|
268
|
+
if (lt === -1) break;
|
|
269
|
+
if (html.startsWith("<!--", lt)) {
|
|
270
|
+
const end = html.indexOf("-->", lt + 4);
|
|
271
|
+
i = end === -1 ? html.length : end + 3;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (html[lt + 1] === "!") {
|
|
275
|
+
const end = html.indexOf(">", lt);
|
|
276
|
+
i = end === -1 ? html.length : end + 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (html[lt + 1] === "/") {
|
|
280
|
+
const end = html.indexOf(">", lt);
|
|
281
|
+
if (depth > 0) {
|
|
282
|
+
depth--;
|
|
283
|
+
if (depth === 0) {
|
|
284
|
+
const stop = end === -1 ? html.length : end + 1;
|
|
285
|
+
segs.push(html.slice(segStart, stop));
|
|
286
|
+
segStart = stop;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
i = end === -1 ? html.length : end + 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const c1 = html[lt + 1];
|
|
293
|
+
const isName = c1 >= "a" && c1 <= "z" || c1 >= "A" && c1 <= "Z";
|
|
294
|
+
if (!isName) {
|
|
295
|
+
i = lt + 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const { tag, selfClose, next } = parseOpenTag(html, lt);
|
|
299
|
+
if (depth === 0) {
|
|
300
|
+
if (lt > segStart) {
|
|
301
|
+
segs.push(html.slice(segStart, lt));
|
|
302
|
+
segStart = lt;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const isVoid = selfClose || VOID.has(tag.toLowerCase());
|
|
306
|
+
if (isVoid) {
|
|
307
|
+
if (depth === 0) {
|
|
308
|
+
segs.push(html.slice(segStart, next));
|
|
309
|
+
segStart = next;
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
depth++;
|
|
313
|
+
}
|
|
314
|
+
i = next;
|
|
315
|
+
}
|
|
316
|
+
if (segStart < html.length) segs.push(html.slice(segStart));
|
|
317
|
+
return segs;
|
|
318
|
+
}
|
|
319
|
+
function htmlToReact(html, components, childMemoMap) {
|
|
320
|
+
if (!childMemoMap) return nodesToReact(parseTrustedHtml(html), components, "");
|
|
321
|
+
const segs = topLevelSegments(html);
|
|
322
|
+
const out = [];
|
|
323
|
+
for (let idx = 0; idx < segs.length; idx++) {
|
|
324
|
+
const seg = segs[idx];
|
|
325
|
+
const cacheKey = idx + "\0" + seg;
|
|
326
|
+
const hit = childMemoMap.get(cacheKey);
|
|
327
|
+
if (hit !== void 0) {
|
|
328
|
+
out.push(hit);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const node = nodesToReact(parseTrustedHtml(seg), components, idx + ".");
|
|
332
|
+
childMemoMap.set(cacheKey, node);
|
|
333
|
+
out.push(node);
|
|
334
|
+
}
|
|
335
|
+
return out.length === 0 ? null : out.length === 1 ? out[0] : out;
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
decodeEntities,
|
|
339
|
+
getParseCount,
|
|
340
|
+
htmlToReact,
|
|
341
|
+
parseStyle,
|
|
342
|
+
parseTrustedHtml,
|
|
343
|
+
resetParseCount
|
|
344
|
+
};
|
|
@@ -15,28 +15,8 @@
|
|
|
15
15
|
* // ... wherever your tokens land: client.append(deltaText);
|
|
16
16
|
* client.finalize();
|
|
17
17
|
*/
|
|
18
|
-
export { FluxClient, FluxPool, getDefaultPool } from "./client";
|
|
19
|
-
export { FluxMarkdown, useFluxStream, useFluxMarkdownString } from "./react";
|
|
20
|
-
export { highlight, supportedLangs } from "./hi";
|
|
21
|
-
export { htmlToReact, parseTrustedHtml } from "./html-to-react";
|
|
22
|
-
export type {
|
|
23
|
-
Block,
|
|
24
|
-
BlockKind,
|
|
25
|
-
BlockKindTag,
|
|
26
|
-
BlockComponentProps,
|
|
27
|
-
Components,
|
|
28
|
-
Patch,
|
|
29
|
-
FromWorker,
|
|
30
|
-
ToWorker,
|
|
31
|
-
WorkerLike,
|
|
32
|
-
ParserConfig,
|
|
33
|
-
Align,
|
|
34
|
-
TableCell,
|
|
35
|
-
TableData,
|
|
36
|
-
HeadingData,
|
|
37
|
-
CodeBlockData,
|
|
38
|
-
MathBlockData,
|
|
39
|
-
ListData,
|
|
40
|
-
NestedBlock,
|
|
41
|
-
ContainerData,
|
|
42
|
-
} from "./types";
|
|
18
|
+
export { FluxClient, FluxPool, getDefaultPool } from "./client.js";
|
|
19
|
+
export { FluxMarkdown, useFluxStream, useFluxMarkdownString } from "./react.js";
|
|
20
|
+
export { highlight, supportedLangs } from "./hi.js";
|
|
21
|
+
export { htmlToReact, parseTrustedHtml } from "./html-to-react.js";
|
|
22
|
+
export type { Block, BlockKind, BlockKindTag, BlockComponentProps, Components, Patch, FromWorker, ToWorker, WorkerLike, ParserConfig, Align, TableCell, TableData, HeadingData, CodeBlockData, MathBlockData, ListData, NestedBlock, ContainerData, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FluxClient, FluxPool, getDefaultPool } from "./client.js";
|
|
2
|
+
import { FluxMarkdown, useFluxStream, useFluxMarkdownString } from "./react.js";
|
|
3
|
+
import { highlight, supportedLangs } from "./hi.js";
|
|
4
|
+
import { htmlToReact, parseTrustedHtml } from "./html-to-react.js";
|
|
5
|
+
export {
|
|
6
|
+
FluxClient,
|
|
7
|
+
FluxMarkdown,
|
|
8
|
+
FluxPool,
|
|
9
|
+
getDefaultPool,
|
|
10
|
+
highlight,
|
|
11
|
+
htmlToReact,
|
|
12
|
+
parseTrustedHtml,
|
|
13
|
+
supportedLangs,
|
|
14
|
+
useFluxMarkdownString,
|
|
15
|
+
useFluxStream
|
|
16
|
+
};
|
package/dist/morph.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny, self-contained idiomorph-style DOM morph (zero npm dependency, to keep
|
|
3
|
+
* the security/supply-chain posture). It mutates a live element's children to
|
|
4
|
+
* match a desired tree **in place** instead of replacing them wholesale, so the
|
|
5
|
+
* browser only repaints/relayouts the parts that actually changed and focus +
|
|
6
|
+
* text-selection survive a streaming update.
|
|
7
|
+
*
|
|
8
|
+
* Scope: this is intentionally minimal — it handles the cases the streaming
|
|
9
|
+
* open-block tail produces (text growth, appended trailing nodes, attribute
|
|
10
|
+
* tweaks). It is OPT-IN (see `morphOpenBlocks` in dom.ts) and never runs on the
|
|
11
|
+
* default path.
|
|
12
|
+
*
|
|
13
|
+
* Matching strategy (per child level):
|
|
14
|
+
* - Build an id-set over the *new* subtree so a node carrying an `id` that the
|
|
15
|
+
* incoming tree also has can be matched across reorders/insertions.
|
|
16
|
+
* - Walk old/new children with two cursors. When the heads are "soft-equal"
|
|
17
|
+
* (same nodeType, same tagName for elements) we morph them in place and
|
|
18
|
+
* recurse; otherwise we id-match, insert, or remove the minimum.
|
|
19
|
+
* - Mutate matched element attributes/text in place; append trailing new nodes;
|
|
20
|
+
* remove old nodes with no match.
|
|
21
|
+
*/
|
|
22
|
+
/** Parse an HTML string into a detached `<template>`'s content (a DocumentFragment). */
|
|
23
|
+
export declare function htmlToFragment(html: string): DocumentFragment;
|
|
24
|
+
/**
|
|
25
|
+
* Morph `from`'s children to match `to` (a fragment, element, or HTML string).
|
|
26
|
+
* `from` itself is never replaced — only its subtree is reconciled in place.
|
|
27
|
+
*/
|
|
28
|
+
export declare function morph(from: Element, to: string | DocumentFragment | Element): void;
|
package/dist/morph.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
function htmlToFragment(html) {
|
|
2
|
+
const tpl = document.createElement("template");
|
|
3
|
+
tpl.innerHTML = html;
|
|
4
|
+
return tpl.content;
|
|
5
|
+
}
|
|
6
|
+
function saveFocus(root) {
|
|
7
|
+
if (typeof document === "undefined") return null;
|
|
8
|
+
const active = document.activeElement;
|
|
9
|
+
if (!active || active === document.body) return null;
|
|
10
|
+
if (!root.contains(active)) return null;
|
|
11
|
+
let start = null;
|
|
12
|
+
let end = null;
|
|
13
|
+
const el = active;
|
|
14
|
+
try {
|
|
15
|
+
if (typeof el.selectionStart === "number") {
|
|
16
|
+
start = el.selectionStart;
|
|
17
|
+
end = el.selectionEnd;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
return { node: active, start, end };
|
|
22
|
+
}
|
|
23
|
+
function restoreFocus(saved) {
|
|
24
|
+
if (!saved) return;
|
|
25
|
+
const node = saved.node;
|
|
26
|
+
if (typeof document !== "undefined" && !document.contains(node)) return;
|
|
27
|
+
if (document.activeElement === node) {
|
|
28
|
+
} else if (typeof node.focus === "function") {
|
|
29
|
+
try {
|
|
30
|
+
node.focus({ preventScroll: true });
|
|
31
|
+
} catch {
|
|
32
|
+
try {
|
|
33
|
+
node.focus();
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (saved.start !== null && typeof node.setSelectionRange === "function") {
|
|
39
|
+
try {
|
|
40
|
+
node.setSelectionRange(saved.start, saved.end ?? saved.start);
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function morph(from, to) {
|
|
46
|
+
const fragment = typeof to === "string" ? htmlToFragment(to) : to;
|
|
47
|
+
const saved = saveFocus(from);
|
|
48
|
+
morphChildren(from, fragment);
|
|
49
|
+
restoreFocus(saved);
|
|
50
|
+
}
|
|
51
|
+
function isSoftMatch(a, b) {
|
|
52
|
+
if (a.nodeType !== b.nodeType) return false;
|
|
53
|
+
if (a.nodeType === 1) {
|
|
54
|
+
return a.tagName === b.tagName;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
function nodeId(n) {
|
|
59
|
+
if (n.nodeType !== 1) return null;
|
|
60
|
+
const id = n.getAttribute("id");
|
|
61
|
+
return id && id.length > 0 ? id : null;
|
|
62
|
+
}
|
|
63
|
+
function childIdSet(parent) {
|
|
64
|
+
const ids = /* @__PURE__ */ new Set();
|
|
65
|
+
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
|
66
|
+
const id = nodeId(c);
|
|
67
|
+
if (id) ids.add(id);
|
|
68
|
+
}
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
function morphChildren(oldParent, newParent) {
|
|
72
|
+
const newIds = childIdSet(newParent);
|
|
73
|
+
let oldChild = oldParent.firstChild;
|
|
74
|
+
let newChild = newParent.firstChild;
|
|
75
|
+
while (newChild) {
|
|
76
|
+
const nextNew = newChild.nextSibling;
|
|
77
|
+
if (!oldChild) {
|
|
78
|
+
oldParent.appendChild(importNode(oldParent, newChild));
|
|
79
|
+
newChild = nextNew;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const nextOld = oldChild.nextSibling;
|
|
83
|
+
const newKey = nodeId(newChild);
|
|
84
|
+
const oldKey = nodeId(oldChild);
|
|
85
|
+
if (isSoftMatch(oldChild, newChild) && newKey === oldKey) {
|
|
86
|
+
morphNode(oldChild, newChild);
|
|
87
|
+
oldChild = nextOld;
|
|
88
|
+
newChild = nextNew;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (oldKey && !newIds.has(oldKey)) {
|
|
92
|
+
const toRemove = oldChild;
|
|
93
|
+
oldChild = nextOld;
|
|
94
|
+
oldParent.removeChild(toRemove);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (newKey) {
|
|
98
|
+
const match = findChildById(oldParent, newKey);
|
|
99
|
+
if (match) {
|
|
100
|
+
if (match !== oldChild) oldParent.insertBefore(match, oldChild);
|
|
101
|
+
morphNode(match, newChild);
|
|
102
|
+
oldChild = match.nextSibling;
|
|
103
|
+
newChild = nextNew;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
oldParent.insertBefore(importNode(oldParent, newChild), oldChild);
|
|
107
|
+
newChild = nextNew;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (isSoftMatch(oldChild, newChild)) {
|
|
111
|
+
morphNode(oldChild, newChild);
|
|
112
|
+
oldChild = nextOld;
|
|
113
|
+
newChild = nextNew;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
oldParent.insertBefore(importNode(oldParent, newChild), oldChild);
|
|
117
|
+
oldParent.removeChild(oldChild);
|
|
118
|
+
oldChild = nextOld;
|
|
119
|
+
newChild = nextNew;
|
|
120
|
+
}
|
|
121
|
+
while (oldChild) {
|
|
122
|
+
const next = oldChild.nextSibling;
|
|
123
|
+
oldParent.removeChild(oldChild);
|
|
124
|
+
oldChild = next;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function findChildById(parent, id) {
|
|
128
|
+
for (let c = parent.firstChild; c; c = c.nextSibling) {
|
|
129
|
+
if (nodeId(c) === id) return c;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function importNode(target, source) {
|
|
134
|
+
const doc = target.ownerDocument ?? document;
|
|
135
|
+
return doc.importNode(source, true);
|
|
136
|
+
}
|
|
137
|
+
function morphNode(oldNode, newNode) {
|
|
138
|
+
if (oldNode.nodeType === 1) {
|
|
139
|
+
morphAttributes(oldNode, newNode);
|
|
140
|
+
morphChildren(oldNode, newNode);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
144
|
+
oldNode.nodeValue = newNode.nodeValue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function morphAttributes(oldEl, newEl) {
|
|
148
|
+
const newAttrs = newEl.attributes;
|
|
149
|
+
for (let i = 0; i < newAttrs.length; i++) {
|
|
150
|
+
const attr = newAttrs[i];
|
|
151
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
152
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const oldAttrs = oldEl.attributes;
|
|
156
|
+
const stale = [];
|
|
157
|
+
for (let i = 0; i < oldAttrs.length; i++) {
|
|
158
|
+
const name = oldAttrs[i].name;
|
|
159
|
+
if (!newEl.hasAttribute(name)) stale.push(name);
|
|
160
|
+
}
|
|
161
|
+
for (const name of stale) oldEl.removeAttribute(name);
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
htmlToFragment,
|
|
165
|
+
morph
|
|
166
|
+
};
|