brookmd 0.22.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +1229 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1265 -0
  4. package/dist/block-props.d.ts +18 -0
  5. package/dist/block-props.js +75 -0
  6. package/dist/client.d.ts +370 -0
  7. package/dist/client.js +754 -0
  8. package/dist/decorate.d.ts +24 -0
  9. package/dist/decorate.js +71 -0
  10. package/dist/dom.d.ts +130 -0
  11. package/dist/dom.js +627 -0
  12. package/dist/element.d.ts +20 -0
  13. package/dist/element.js +288 -0
  14. package/dist/hi.d.ts +12 -0
  15. package/dist/hi.js +215 -0
  16. package/dist/html-to-react.d.ts +61 -0
  17. package/dist/html-to-react.js +338 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.js +18 -0
  20. package/dist/morph.d.ts +28 -0
  21. package/dist/morph.js +166 -0
  22. package/dist/react.d.ts +236 -0
  23. package/dist/react.js +539 -0
  24. package/dist/renderers/CodeBlock.d.ts +7 -0
  25. package/dist/renderers/CodeBlock.js +75 -0
  26. package/dist/renderers/Math.d.ts +14 -0
  27. package/dist/renderers/Math.js +15 -0
  28. package/dist/renderers/Mermaid.d.ts +13 -0
  29. package/dist/renderers/Mermaid.js +15 -0
  30. package/dist/server-react.d.ts +32 -0
  31. package/dist/server-react.js +48 -0
  32. package/dist/server.d.ts +31 -0
  33. package/dist/server.js +82 -0
  34. package/dist/solid.d.ts +104 -0
  35. package/dist/solid.js +54 -0
  36. package/dist/styles.css +188 -0
  37. package/dist/svelte.d.ts +80 -0
  38. package/dist/svelte.js +59 -0
  39. package/dist/types-core.d.ts +436 -0
  40. package/dist/types-core.js +0 -0
  41. package/dist/types-react.d.ts +13 -0
  42. package/dist/types-react.js +0 -0
  43. package/dist/types.d.ts +2 -0
  44. package/dist/types.js +2 -0
  45. package/dist/url-safety.d.ts +12 -0
  46. package/dist/url-safety.js +45 -0
  47. package/dist/vue.d.ts +94 -0
  48. package/dist/vue.js +79 -0
  49. package/dist/wasm/LICENSE +21 -0
  50. package/dist/wasm/README.md +71 -0
  51. package/dist/wasm/brook_md_core.d.ts +166 -0
  52. package/dist/wasm/brook_md_core.js +512 -0
  53. package/dist/wasm/brook_md_core_bg.wasm +0 -0
  54. package/dist/wasm/brook_md_core_bg.wasm.d.ts +26 -0
  55. package/dist/worker-core.d.ts +65 -0
  56. package/dist/worker-core.js +155 -0
  57. package/dist/worker.d.ts +1 -0
  58. package/dist/worker.js +49 -0
  59. package/package.json +87 -0
@@ -0,0 +1,288 @@
1
+ import { BrookClient } from "./client.js";
2
+ import { mountBrookMarkdown } 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 defineBrookMarkdown(tag = "brook-markdown") {
19
+ if (typeof customElements === "undefined") return;
20
+ if (customElements.get(tag)) return;
21
+ class BrookMarkdownElement 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
+ "<brook-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-tagfilter", "gfmTagfilter");
150
+ set("gfm-footnotes", "gfmFootnotes");
151
+ set("gfm-math", "gfmMath");
152
+ set("dir-auto", "dirAuto");
153
+ set("a11y", "a11y");
154
+ set("unsafe-html", "unsafeHtml");
155
+ const tags = this.getAttribute("component-tags");
156
+ if (tags !== null) {
157
+ const list = tags.split(/[\s,]+/).filter(Boolean);
158
+ if (list.length > 0) {
159
+ cfg.componentTags = list;
160
+ any = true;
161
+ }
162
+ }
163
+ return any ? cfg : void 0;
164
+ }
165
+ // Lazily create the internal client from config attributes (self-owned).
166
+ #ensureClient() {
167
+ if (this.#client) return;
168
+ this.#client = new BrookClient({ config: this.#readConfig() });
169
+ this.#ownsClient = true;
170
+ this.#mountIfReady();
171
+ }
172
+ // Mount once a client exists and we're connected. Idempotent.
173
+ #mountIfReady() {
174
+ if (!this.#connected || !this.#client || this.#handle) return;
175
+ this.#handle = mountBrookMarkdown(this.#client, this, {
176
+ components: this.#components,
177
+ sanitize: this.#sanitize
178
+ });
179
+ }
180
+ // Destroy the current mount and remount against the current client+options.
181
+ // Used when a property changes while connected.
182
+ #remount() {
183
+ this.#handle?.destroy();
184
+ this.#handle = null;
185
+ this.#mountIfReady();
186
+ }
187
+ // Tear down only the client side (mount stays / is handled by the caller).
188
+ // Destroys the client only if self-owned, then clears it and the mount so
189
+ // the next mount targets a fresh client.
190
+ #teardownClient() {
191
+ this.#cancelSrcStream();
192
+ this.#handle?.destroy();
193
+ this.#handle = null;
194
+ if (this.#ownsClient) this.#client?.destroy();
195
+ this.#client = null;
196
+ this.#ownsClient = false;
197
+ }
198
+ // Resolve the initial content of a self-owned stream from the attributes,
199
+ // in priority order: `src` (fetch+stream) > `markdown` (one-shot) >
200
+ // textContent (one-shot). A caller-owned client never reaches here.
201
+ #resolveInitialContent() {
202
+ this.#cancelSrcStream();
203
+ const src = this.getAttribute("src");
204
+ if (src) {
205
+ void this.#streamFromSrc(src);
206
+ return;
207
+ }
208
+ const markdown = this.getAttribute("markdown");
209
+ if (markdown !== null) {
210
+ this.#oneShot(markdown);
211
+ return;
212
+ }
213
+ const text = this.#captureSourceText();
214
+ if (text.trim().length > 0) this.#oneShot(text);
215
+ }
216
+ // Read the raw markdown the host put between the tags, ignoring the
217
+ // renderer's `.brook-md` root (and any other element children).
218
+ #captureSourceText() {
219
+ let text = "";
220
+ for (const node of Array.from(this.childNodes)) {
221
+ if (node.nodeType === 3) {
222
+ text += node.textContent ?? "";
223
+ node.parentNode?.removeChild(node);
224
+ }
225
+ }
226
+ return text;
227
+ }
228
+ // One-shot: reset the stream (in case content changed), feed it, finalize.
229
+ #oneShot(markdown) {
230
+ this.#ensureClient();
231
+ this.#client.reset();
232
+ this.#client.append(markdown);
233
+ this.#client.finalize();
234
+ }
235
+ // Abort any in-flight `src` fetch and invalidate its read loop, so it can
236
+ // no longer append into a client we're about to reuse, swap, or destroy.
237
+ #cancelSrcStream() {
238
+ this.#srcSeq++;
239
+ this.#srcAbort?.abort();
240
+ this.#srcAbort = null;
241
+ }
242
+ // Fetch a URL and stream its body. TextDecoder with {stream:true} carries a
243
+ // multibyte sequence that straddles a chunk boundary into the next decode.
244
+ async #streamFromSrc(src) {
245
+ this.#cancelSrcStream();
246
+ const token = this.#srcSeq;
247
+ const abort = new AbortController();
248
+ this.#srcAbort = abort;
249
+ this.#ensureClient();
250
+ this.#client.reset();
251
+ const owned = this.#client;
252
+ const current = () => this.#srcSeq === token && this.#client === owned;
253
+ try {
254
+ const res = await fetch(src, { signal: abort.signal });
255
+ if (!current()) return;
256
+ const body = res.body;
257
+ if (!body) {
258
+ const text = await res.text();
259
+ if (!current()) return;
260
+ owned.append(text);
261
+ owned.finalize();
262
+ return;
263
+ }
264
+ const reader = body.getReader();
265
+ const decoder = new TextDecoder();
266
+ for (; ; ) {
267
+ const { done, value } = await reader.read();
268
+ if (!current()) return;
269
+ if (done) break;
270
+ if (value) owned.append(decoder.decode(value, { stream: true }));
271
+ }
272
+ owned.append(decoder.decode());
273
+ owned.finalize();
274
+ } catch (err) {
275
+ if (abort.signal.aborted || !current()) return;
276
+ console.error(
277
+ "<brook-markdown>: failed to stream src",
278
+ err instanceof Error ? err.message : String(err)
279
+ );
280
+ }
281
+ }
282
+ }
283
+ customElements.define(tag, BrookMarkdownElement);
284
+ }
285
+ export {
286
+ defineBrookMarkdown,
287
+ parseTriBool
288
+ };
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/dist/hi.js ADDED
@@ -0,0 +1,215 @@
1
+ const KEYWORDS_JS = new Set(
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(
3
+ " "
4
+ )
5
+ );
6
+ const KEYWORDS_TS = /* @__PURE__ */ new Set([
7
+ ...KEYWORDS_JS,
8
+ ...["any", "as", "boolean", "declare", "enum", "interface", "is", "keyof", "module", "namespace", "never", "number", "private", "protected", "public", "readonly", "string", "type", "unknown", "satisfies"]
9
+ ]);
10
+ const KEYWORDS_RUST = new Set(
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(
12
+ " "
13
+ )
14
+ );
15
+ const KEYWORDS_PY = new Set(
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(
17
+ " "
18
+ )
19
+ );
20
+ const KEYWORDS_GO = new Set(
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(
22
+ " "
23
+ )
24
+ );
25
+ const KEYWORDS_BASH = new Set(
26
+ "if then elif else fi case esac for select while until do done function in time coproc return break continue".split(
27
+ " "
28
+ )
29
+ );
30
+ const KEYWORDS_SQL = new Set(
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(
32
+ " "
33
+ )
34
+ );
35
+ const jsPats = [
36
+ ["com", /\/\/[^\n]*/y],
37
+ ["com", /\/\*[\s\S]*?\*\//y],
38
+ ["str", /"(?:\\.|[^"\\\n])*"/y],
39
+ ["str", /'(?:\\.|[^'\\\n])*'/y],
40
+ ["str", /`(?:\\.|[^`\\])*`/y],
41
+ ["rx", /\/(?![*/])(?:\\.|[^/\\\n])+\/[gimsuy]*/y],
42
+ ["num", /\b(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)\b/y],
43
+ ["ident", /[A-Za-z_$][\w$]*/y],
44
+ ["pun", /[+\-*/=<>!&|^~?:;,.[\](){}]/y],
45
+ ["ws", /\s+/y]
46
+ ];
47
+ const rustPats = [
48
+ ["com", /\/\/[^\n]*/y],
49
+ ["com", /\/\*[\s\S]*?\*\//y],
50
+ ["str", /b?"(?:\\.|[^"\\])*"/y],
51
+ ["str", /b?'(?:\\.|[^'\\])'/y],
52
+ ["lt", /'[a-zA-Z_][\w]*/y],
53
+ ["num", /\b\d[\d_]*(?:\.\d[\d_]*)?(?:[ui](?:8|16|32|64|128|size)|f(?:32|64))?\b/y],
54
+ ["mac", /[A-Za-z_]\w*!/y],
55
+ ["attr", /#!?\[[^\]]*\]/y],
56
+ ["ident", /[A-Za-z_]\w*/y],
57
+ ["pun", /[+\-*/=<>!&|^~?:;,.\[\](){}@]/y],
58
+ ["ws", /\s+/y]
59
+ ];
60
+ const pyPats = [
61
+ ["com", /#[^\n]*/y],
62
+ ["str", /[fFrRbB]{0,2}"""[\s\S]*?"""/y],
63
+ ["str", /[fFrRbB]{0,2}'''[\s\S]*?'''/y],
64
+ ["str", /[fFrRbB]{0,2}"(?:\\.|[^"\\\n])*"/y],
65
+ ["str", /[fFrRbB]{0,2}'(?:\\.|[^'\\\n])*'/y],
66
+ ["num", /\b(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?[jJ]?)\b/y],
67
+ ["dec", /@[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*/y],
68
+ ["ident", /[A-Za-z_]\w*/y],
69
+ ["pun", /[+\-*/=<>!&|^~?:;,.[\](){}@%]/y],
70
+ ["ws", /\s+/y]
71
+ ];
72
+ const goPats = [
73
+ ["com", /\/\/[^\n]*/y],
74
+ ["com", /\/\*[\s\S]*?\*\//y],
75
+ ["str", /"(?:\\.|[^"\\\n])*"/y],
76
+ ["str", /`[^`]*`/y],
77
+ ["str", /'(?:\\.|[^'\\\n])'/y],
78
+ ["num", /\b\d[\d_]*(?:\.\d[\d_]*)?\b/y],
79
+ ["ident", /[A-Za-z_]\w*/y],
80
+ ["pun", /[+\-*/=<>!&|^~?:;,.[\](){}]/y],
81
+ ["ws", /\s+/y]
82
+ ];
83
+ const bashPats = [
84
+ ["com", /#[^\n]*/y],
85
+ ["str", /"(?:\\.|[^"\\])*"/y],
86
+ ["str", /'[^']*'/y],
87
+ ["var", /\$\{[^}]+\}|\$\w+|\$[*@#?!$0-9]/y],
88
+ ["num", /\b\d+\b/y],
89
+ ["ident", /[A-Za-z_][\w-]*/y],
90
+ ["pun", /[|&;<>(){}[\]=]/y],
91
+ ["ws", /\s+/y]
92
+ ];
93
+ const jsonPats = [
94
+ ["str", /"(?:\\.|[^"\\\n])*"/y],
95
+ ["num", /-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/y],
96
+ ["kw", /\b(?:true|false|null)\b/y],
97
+ ["pun", /[{}[\]:,]/y],
98
+ ["ws", /\s+/y]
99
+ ];
100
+ const sqlPats = [
101
+ ["com", /--[^\n]*/y],
102
+ ["com", /\/\*[\s\S]*?\*\//y],
103
+ ["str", /'(?:''|[^'])*'/y],
104
+ ["str", /"(?:""|[^"])*"/y],
105
+ ["num", /\b\d+(?:\.\d+)?\b/y],
106
+ ["ident", /[A-Za-z_][\w]*/y],
107
+ ["pun", /[+\-*/=<>!,;.(){}]/y],
108
+ ["ws", /\s+/y]
109
+ ];
110
+ const htmlPats = [
111
+ ["com", /<!--[\s\S]*?-->/y],
112
+ ["tag", /<\/?[A-Za-z][\w-]*/y],
113
+ ["str", /"[^"]*"/y],
114
+ ["str", /'[^']*'/y],
115
+ ["attr", /[A-Za-z][\w-]*(?==)/y],
116
+ ["pun", /[=/>]/y],
117
+ ["txt", /[^<>"'=]+/y]
118
+ ];
119
+ const cssPats = [
120
+ ["com", /\/\*[\s\S]*?\*\//y],
121
+ ["str", /"[^"]*"/y],
122
+ ["str", /'[^']*'/y],
123
+ ["num", /-?\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms|deg)?/y],
124
+ ["sel", /[#.]?[A-Za-z][\w-]*/y],
125
+ ["pun", /[:;,{}()]/y],
126
+ ["ws", /\s+/y]
127
+ ];
128
+ const LANGS = {
129
+ js: { pats: jsPats, kw: KEYWORDS_JS },
130
+ javascript: { pats: jsPats, kw: KEYWORDS_JS },
131
+ ts: { pats: jsPats, kw: KEYWORDS_TS },
132
+ tsx: { pats: jsPats, kw: KEYWORDS_TS },
133
+ jsx: { pats: jsPats, kw: KEYWORDS_JS },
134
+ typescript: { pats: jsPats, kw: KEYWORDS_TS },
135
+ rust: { pats: rustPats, kw: KEYWORDS_RUST },
136
+ rs: { pats: rustPats, kw: KEYWORDS_RUST },
137
+ py: { pats: pyPats, kw: KEYWORDS_PY },
138
+ python: { pats: pyPats, kw: KEYWORDS_PY },
139
+ go: { pats: goPats, kw: KEYWORDS_GO },
140
+ bash: { pats: bashPats, kw: KEYWORDS_BASH },
141
+ sh: { pats: bashPats, kw: KEYWORDS_BASH },
142
+ shell: { pats: bashPats, kw: KEYWORDS_BASH },
143
+ json: { pats: jsonPats },
144
+ sql: { pats: sqlPats, kw: KEYWORDS_SQL },
145
+ html: { pats: htmlPats },
146
+ xml: { pats: htmlPats },
147
+ css: { pats: cssPats }
148
+ };
149
+ function escapeHtml(s) {
150
+ let out = "";
151
+ for (let i = 0; i < s.length; i++) {
152
+ const c = s[i];
153
+ if (c === "<") out += "&lt;";
154
+ else if (c === ">") out += "&gt;";
155
+ else if (c === "&") out += "&amp;";
156
+ else if (c === '"') out += "&quot;";
157
+ else out += c;
158
+ }
159
+ return out;
160
+ }
161
+ function highlight(code, lang) {
162
+ if (code.length > 5e4) return escapeHtml(code);
163
+ const conf = LANGS[lang.toLowerCase()];
164
+ if (!conf) return escapeHtml(code);
165
+ let out = "";
166
+ let pos = 0;
167
+ const pats = conf.pats;
168
+ const kw = conf.kw;
169
+ while (pos < code.length) {
170
+ let matched = false;
171
+ for (let i = 0; i < pats.length; i++) {
172
+ const [cls, re] = pats[i];
173
+ re.lastIndex = pos;
174
+ const m = re.exec(code);
175
+ if (!m || m.index !== pos) continue;
176
+ const text = m[0];
177
+ const after = pos + text.length;
178
+ let finalCls = cls;
179
+ if (cls === "ident") {
180
+ if (kw && kw.has(text)) {
181
+ finalCls = "kw";
182
+ } else if (after < code.length && code[after] === "(") {
183
+ finalCls = "fn";
184
+ } else if (text.length > 1 && text[0] >= "A" && text[0] <= "Z") {
185
+ finalCls = "ty";
186
+ } else {
187
+ out += escapeHtml(text);
188
+ pos = after;
189
+ matched = true;
190
+ break;
191
+ }
192
+ }
193
+ if (cls === "ws") {
194
+ out += text;
195
+ } else {
196
+ out += `<span class="t-${finalCls}">${escapeHtml(text)}</span>`;
197
+ }
198
+ pos = after;
199
+ matched = true;
200
+ break;
201
+ }
202
+ if (!matched) {
203
+ out += escapeHtml(code[pos]);
204
+ pos += 1;
205
+ }
206
+ }
207
+ return out;
208
+ }
209
+ function supportedLangs() {
210
+ return Object.keys(LANGS);
211
+ }
212
+ export {
213
+ highlight,
214
+ supportedLangs
215
+ };
@@ -0,0 +1,61 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import type { Components } from "./types.js";
3
+ import type { Decorator, UrlTransform } from "./types-core.js";
4
+ import { decodeEntities, safeUrl } from "./url-safety.js";
5
+ export { decodeEntities, safeUrl };
6
+ type HNode = {
7
+ kind: "text";
8
+ text: string;
9
+ } | {
10
+ kind: "el";
11
+ tag: string;
12
+ attrs: Record<string, string | true>;
13
+ children: HNode[];
14
+ };
15
+ /**
16
+ * Parse an inline CSS string (`"text-align:left;color:red"`) into the object
17
+ * React's `style` prop requires, camelCasing property names. Custom properties
18
+ * (`--x`) keep their literal name.
19
+ */
20
+ export declare function parseStyle(css: string): Record<string, string>;
21
+ export declare function getParseCount(): number;
22
+ export declare function resetParseCount(): void;
23
+ export declare function parseTrustedHtml(html: string): HNode[];
24
+ /**
25
+ * Convert a block's trusted HTML string into a React node tree, replacing any
26
+ * element whose tag name appears in `components`.
27
+ *
28
+ * With no `childMemoMap`, behavior is byte-identical to a single
29
+ * `parseTrustedHtml` + convert pass — the closed-block call site memoizes on
30
+ * `(html, components)`.
31
+ *
32
+ * Passing a `childMemoMap` opts into OPEN-block child reuse: the html is split
33
+ * into top-level node segments and each is keyed by its exact substring. On a
34
+ * hit the cached React node is reused (no re-parse, no re-serialize); only new /
35
+ * changed trailing segments are parsed. The caller owns the map's lifetime and
36
+ * must scope it per block.id and invalidate it when `components` changes (a hit
37
+ * carries the React node built under the previous components map). Segment keys
38
+ * carry their original document order via `keyOffset` so React keys stay stable.
39
+ */
40
+ export declare function htmlToReact(html: string, components: Components, childMemoMap?: Map<string, ReactNode>, opts?: {
41
+ decorators?: Decorator[];
42
+ urlTransform?: UrlTransform;
43
+ }): ReactNode;
44
+ /**
45
+ * Build a SAFE `<a>` for use inside a {@link Decorator}'s `replace`. Decorator
46
+ * output is a TRUSTED surface that does NOT pass through brookmd's attribute
47
+ * sanitizer, and React renders a `javascript:` href without complaint — so this
48
+ * runs `href` through {@link safeUrl} (the same scheme filter the core uses) and
49
+ * spreads the remaining attributes verbatim. Prefer this over a hand-built
50
+ * `<a>` whenever the href can come from model output.
51
+ *
52
+ * ```tsx
53
+ * const decorators = [{
54
+ * match: /\$[\d.]+[BMK]/g,
55
+ * replace: (t) => wrapLink(t, { href: "/figures/" + t, className: "fig" }),
56
+ * }];
57
+ * ```
58
+ */
59
+ export declare function wrapLink(text: ReactNode, attrs: {
60
+ href: string;
61
+ } & Record<string, unknown>): ReactElement;