brookmd 0.25.2 → 0.26.1
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/CHANGELOG.md +254 -0
- package/README.md +205 -22
- package/dist/block-props.js +2 -1
- package/dist/dom.js +42 -14
- package/dist/element.js +20 -2
- package/dist/hi-defer.d.ts +35 -0
- package/dist/hi-defer.js +90 -0
- package/dist/hi.d.ts +25 -0
- package/dist/hi.js +42 -17
- package/dist/react.js +13 -2
- package/dist/renderers/CodeBlock.d.ts +8 -1
- package/dist/renderers/CodeBlock.js +29 -4
- package/dist/server.d.ts +15 -4
- package/dist/server.js +11 -1
- package/dist/types-core.d.ts +121 -13
- package/dist/wasm/README.md +2 -2
- package/dist/wasm/brook_md_core.d.ts +44 -0
- package/dist/wasm/brook_md_core.js +54 -0
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/dist/wasm/brook_md_core_bg.wasm.d.ts +4 -0
- package/dist/worker.js +4 -0
- package/package.json +1 -1
package/dist/dom.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { highlightDeferred } from "./hi-defer.js";
|
|
2
2
|
import { morph } from "./morph.js";
|
|
3
3
|
import { blockProps, extractLang } from "./block-props.js";
|
|
4
4
|
import { decorateSegments } from "./decorate.js";
|
|
@@ -115,6 +115,10 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
117
|
existing.table = void 0;
|
|
118
|
+
if (existing.highlight) {
|
|
119
|
+
existing.highlight.cancel();
|
|
120
|
+
existing.highlight = void 0;
|
|
121
|
+
}
|
|
118
122
|
const node = renderBlock(b, existing);
|
|
119
123
|
existing.node.replaceWith(node);
|
|
120
124
|
existing.node = node;
|
|
@@ -128,6 +132,7 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
128
132
|
if (mounted.size > seen.size) {
|
|
129
133
|
for (const [id, mb] of mounted) {
|
|
130
134
|
if (!seen.has(id)) {
|
|
135
|
+
if (mb.highlight) mb.highlight.cancel();
|
|
131
136
|
mb.node.remove();
|
|
132
137
|
mounted.delete(id);
|
|
133
138
|
}
|
|
@@ -184,7 +189,7 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
184
189
|
}
|
|
185
190
|
switch (kind) {
|
|
186
191
|
case "CodeBlock":
|
|
187
|
-
if (highlightCode) return renderCodeBlock(b);
|
|
192
|
+
if (highlightCode) return renderCodeBlock(b, mb);
|
|
188
193
|
break;
|
|
189
194
|
// fall through to the generic path
|
|
190
195
|
case "MathBlock":
|
|
@@ -341,10 +346,11 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
341
346
|
}
|
|
342
347
|
return result;
|
|
343
348
|
}
|
|
344
|
-
function renderCodeBlock(b) {
|
|
349
|
+
function renderCodeBlock(b, mb) {
|
|
345
350
|
const lang = extractLang(b.html) || "text";
|
|
346
|
-
const text = b.open ? "" :
|
|
347
|
-
const
|
|
351
|
+
const text = b.open ? "" : codeText(b);
|
|
352
|
+
const run = text ? highlightDeferred(text, lang) : null;
|
|
353
|
+
const highlighted = run ? run.html : null;
|
|
348
354
|
const block = document.createElement("div");
|
|
349
355
|
block.className = "brook-code-block" + (b.open ? " brook-streaming" : "");
|
|
350
356
|
const header = document.createElement("div");
|
|
@@ -364,15 +370,8 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
364
370
|
block.appendChild(header);
|
|
365
371
|
const body = document.createElement("div");
|
|
366
372
|
body.className = "brook-code-body";
|
|
367
|
-
if (highlighted) {
|
|
368
|
-
|
|
369
|
-
pre.tabIndex = 0;
|
|
370
|
-
pre.setAttribute("role", "region");
|
|
371
|
-
pre.setAttribute("aria-label", `${lang} code`);
|
|
372
|
-
const code = document.createElement("code");
|
|
373
|
-
code.innerHTML = highlighted;
|
|
374
|
-
pre.appendChild(code);
|
|
375
|
-
body.appendChild(pre);
|
|
373
|
+
if (highlighted !== null) {
|
|
374
|
+
body.appendChild(highlightedPre(lang, highlighted));
|
|
376
375
|
} else {
|
|
377
376
|
const div = document.createElement("div");
|
|
378
377
|
div.tabIndex = 0;
|
|
@@ -380,10 +379,29 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
380
379
|
div.setAttribute("aria-label", `${lang} code`);
|
|
381
380
|
div.innerHTML = b.html;
|
|
382
381
|
body.appendChild(div);
|
|
382
|
+
if (run !== null && run.rest !== null) {
|
|
383
|
+
mb.highlight = run;
|
|
384
|
+
run.rest.then((markup) => {
|
|
385
|
+
if (markup === null || dead) return;
|
|
386
|
+
if (mb.highlight !== run || mb.node !== block) return;
|
|
387
|
+
mb.highlight = void 0;
|
|
388
|
+
body.replaceChild(highlightedPre(lang, markup), div);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
383
391
|
}
|
|
384
392
|
block.appendChild(body);
|
|
385
393
|
return block;
|
|
386
394
|
}
|
|
395
|
+
function highlightedPre(lang, markup) {
|
|
396
|
+
const pre = document.createElement("pre");
|
|
397
|
+
pre.tabIndex = 0;
|
|
398
|
+
pre.setAttribute("role", "region");
|
|
399
|
+
pre.setAttribute("aria-label", `${lang} code`);
|
|
400
|
+
const code = document.createElement("code");
|
|
401
|
+
code.innerHTML = markup;
|
|
402
|
+
pre.appendChild(code);
|
|
403
|
+
return pre;
|
|
404
|
+
}
|
|
387
405
|
function renderMathBlock(b) {
|
|
388
406
|
const block = document.createElement("div");
|
|
389
407
|
block.className = "brook-math-block" + (b.open ? " brook-streaming" : "");
|
|
@@ -476,6 +494,12 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
476
494
|
frame = 0;
|
|
477
495
|
}
|
|
478
496
|
unsubscribe();
|
|
497
|
+
for (const mb of mounted.values()) {
|
|
498
|
+
if (mb.highlight) {
|
|
499
|
+
mb.highlight.cancel();
|
|
500
|
+
mb.highlight = void 0;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
479
503
|
root.remove();
|
|
480
504
|
},
|
|
481
505
|
refresh() {
|
|
@@ -558,6 +582,10 @@ function tailOpenBlockId(snapshot) {
|
|
|
558
582
|
const tail = snapshot.length > 0 ? snapshot[snapshot.length - 1] : void 0;
|
|
559
583
|
return tail && tail.open ? tail.id : null;
|
|
560
584
|
}
|
|
585
|
+
function codeText(b) {
|
|
586
|
+
const data = b.kind.data;
|
|
587
|
+
return typeof data?.code === "string" ? data.code : decodeCodeText(b.html);
|
|
588
|
+
}
|
|
561
589
|
function decodeCodeText(html) {
|
|
562
590
|
const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
|
|
563
591
|
if (!m) return "";
|
package/dist/element.js
CHANGED
|
@@ -9,18 +9,25 @@ function parseTriBool(value) {
|
|
|
9
9
|
const CONFIG_ATTRS = [
|
|
10
10
|
"gfm-autolinks",
|
|
11
11
|
"gfm-alerts",
|
|
12
|
+
// Must mirror the `set(...)` map below: this list feeds `observedAttributes`,
|
|
13
|
+
// so an attribute missing here is read once at mount and then never reacts to
|
|
14
|
+
// a change. `gfm-tagfilter` was in the map but not in this list.
|
|
15
|
+
"gfm-tagfilter",
|
|
12
16
|
"gfm-footnotes",
|
|
13
17
|
"gfm-math",
|
|
14
18
|
"dir-auto",
|
|
19
|
+
"lenient-lists",
|
|
20
|
+
"soft-breaks",
|
|
15
21
|
"a11y",
|
|
16
|
-
"unsafe-html"
|
|
22
|
+
"unsafe-html",
|
|
23
|
+
"block-html"
|
|
17
24
|
];
|
|
18
25
|
function defineBrookMarkdown(tag = "brook-markdown") {
|
|
19
26
|
if (typeof customElements === "undefined") return;
|
|
20
27
|
if (customElements.get(tag)) return;
|
|
21
28
|
class BrookMarkdownElement extends HTMLElement {
|
|
22
29
|
static get observedAttributes() {
|
|
23
|
-
return ["markdown", "src", "component-tags", ...CONFIG_ATTRS];
|
|
30
|
+
return ["markdown", "src", "component-tags", "allow-schemes", ...CONFIG_ATTRS];
|
|
24
31
|
}
|
|
25
32
|
#client = null;
|
|
26
33
|
#ownsClient = false;
|
|
@@ -150,8 +157,11 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
150
157
|
set("gfm-footnotes", "gfmFootnotes");
|
|
151
158
|
set("gfm-math", "gfmMath");
|
|
152
159
|
set("dir-auto", "dirAuto");
|
|
160
|
+
set("lenient-lists", "lenientLists");
|
|
161
|
+
set("soft-breaks", "softBreaks");
|
|
153
162
|
set("a11y", "a11y");
|
|
154
163
|
set("unsafe-html", "unsafeHtml");
|
|
164
|
+
set("block-html", "blockHtml");
|
|
155
165
|
const tags = this.getAttribute("component-tags");
|
|
156
166
|
if (tags !== null) {
|
|
157
167
|
const list = tags.split(/[\s,]+/).filter(Boolean);
|
|
@@ -160,6 +170,14 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
160
170
|
any = true;
|
|
161
171
|
}
|
|
162
172
|
}
|
|
173
|
+
const schemes = this.getAttribute("allow-schemes");
|
|
174
|
+
if (schemes !== null) {
|
|
175
|
+
const list = schemes.split(/[\s,]+/).filter(Boolean);
|
|
176
|
+
if (list.length > 0) {
|
|
177
|
+
cfg.allowSchemes = list;
|
|
178
|
+
any = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
163
181
|
return any ? cfg : void 0;
|
|
164
182
|
}
|
|
165
183
|
// Lazily create the internal client from config attributes (self-owned).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only: shrink the per-slice budget so a suite can force the deferred path
|
|
3
|
+
* deterministically instead of betting on how fast the machine is. Call with no
|
|
4
|
+
* argument to restore the default. Not part of the public API.
|
|
5
|
+
*/
|
|
6
|
+
export declare function __setSliceMs(ms?: number): void;
|
|
7
|
+
export interface DeferredHighlight {
|
|
8
|
+
/**
|
|
9
|
+
* The finished markup when the whole block tokenized inside the first
|
|
10
|
+
* (synchronous) slice — the common case. Apply it in this same tick: no
|
|
11
|
+
* swap, no second paint, no flash.
|
|
12
|
+
*/
|
|
13
|
+
html: string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Resolves with the finished markup once the remaining slices have run, or
|
|
16
|
+
* with `null` if the run was {@link DeferredHighlight.cancel}led. `null` (the
|
|
17
|
+
* property, not the resolution) when `html` already holds the answer. Never
|
|
18
|
+
* rejects.
|
|
19
|
+
*/
|
|
20
|
+
rest: Promise<string | null> | null;
|
|
21
|
+
/** Abandon the remaining slices — the block was superseded or unmounted. */
|
|
22
|
+
cancel(): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Tokenize `code` for at most one slice and return the finished markup, or
|
|
26
|
+
* `null` when it did not fit. Pure and synchronous — it schedules nothing, so a
|
|
27
|
+
* renderer can call it from a render pass and only reach for
|
|
28
|
+
* {@link highlightDeferred} when this comes back empty.
|
|
29
|
+
*/
|
|
30
|
+
export declare function highlightWithin(code: string, lang: string): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Highlight `code` without blocking: the first slice runs here, synchronously,
|
|
33
|
+
* and the rest (if any) continues on later tasks. See {@link DeferredHighlight}.
|
|
34
|
+
*/
|
|
35
|
+
export declare function highlightDeferred(code: string, lang: string): DeferredHighlight;
|
package/dist/hi-defer.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { stepHighlight } from "./hi.js";
|
|
2
|
+
const SLICE_MS = 5;
|
|
3
|
+
const CHUNK = 1024;
|
|
4
|
+
const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
5
|
+
let sliceMs = SLICE_MS;
|
|
6
|
+
function __setSliceMs(ms) {
|
|
7
|
+
sliceMs = ms === void 0 ? SLICE_MS : ms;
|
|
8
|
+
}
|
|
9
|
+
const noop = () => {
|
|
10
|
+
};
|
|
11
|
+
function highlightWithin(code, lang) {
|
|
12
|
+
const state = { pos: 0, out: "" };
|
|
13
|
+
return runSlice(code, lang, state) ? state.out : null;
|
|
14
|
+
}
|
|
15
|
+
function highlightDeferred(code, lang) {
|
|
16
|
+
const state = { pos: 0, out: "" };
|
|
17
|
+
if (runSlice(code, lang, state)) {
|
|
18
|
+
return { html: state.out, rest: null, cancel: noop };
|
|
19
|
+
}
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
let channel = null;
|
|
22
|
+
let pending = null;
|
|
23
|
+
function closeChannel() {
|
|
24
|
+
if (channel !== null) {
|
|
25
|
+
channel.port1.close();
|
|
26
|
+
channel.port2.close();
|
|
27
|
+
channel = null;
|
|
28
|
+
}
|
|
29
|
+
pending = null;
|
|
30
|
+
}
|
|
31
|
+
function post() {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
if (typeof MessageChannel !== "function") {
|
|
34
|
+
setTimeout(resolve, 0);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (channel === null) {
|
|
38
|
+
channel = new MessageChannel();
|
|
39
|
+
channel.port1.onmessage = () => {
|
|
40
|
+
const next = pending;
|
|
41
|
+
pending = null;
|
|
42
|
+
if (next) next();
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
pending = resolve;
|
|
46
|
+
channel.port2.postMessage(0);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function nextTask() {
|
|
50
|
+
const scheduler = globalThis.scheduler;
|
|
51
|
+
if (scheduler && typeof scheduler.yield === "function") {
|
|
52
|
+
try {
|
|
53
|
+
const p = scheduler.yield();
|
|
54
|
+
if (p && typeof p.then === "function") return p.then(void 0, post);
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return post();
|
|
59
|
+
}
|
|
60
|
+
const rest = (async () => {
|
|
61
|
+
try {
|
|
62
|
+
for (; ; ) {
|
|
63
|
+
await nextTask();
|
|
64
|
+
if (cancelled) return null;
|
|
65
|
+
if (runSlice(code, lang, state)) return state.out;
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
closeChannel();
|
|
69
|
+
}
|
|
70
|
+
})();
|
|
71
|
+
return {
|
|
72
|
+
html: null,
|
|
73
|
+
rest,
|
|
74
|
+
cancel() {
|
|
75
|
+
cancelled = true;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function runSlice(code, lang, state) {
|
|
80
|
+
const started = now();
|
|
81
|
+
for (; ; ) {
|
|
82
|
+
if (stepHighlight(code, lang, state, CHUNK)) return true;
|
|
83
|
+
if (now() - started >= sliceMs) return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
__setSliceMs,
|
|
88
|
+
highlightDeferred,
|
|
89
|
+
highlightWithin
|
|
90
|
+
};
|
package/dist/hi.d.ts
CHANGED
|
@@ -8,5 +8,30 @@
|
|
|
8
8
|
* highlight an open (streaming) block, which avoids re-highlighting the same
|
|
9
9
|
* code on every chunk — the main perf win for streaming code.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* The resumable tokenizer's cursor: `pos` is the next source index to consume,
|
|
13
|
+
* `out` the markup emitted so far. Start a run at `{ pos: 0, out: "" }`.
|
|
14
|
+
*/
|
|
15
|
+
export interface HighlightState {
|
|
16
|
+
pos: number;
|
|
17
|
+
out: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One resumable slice of {@link highlight}. Consumes WHOLE tokens from
|
|
21
|
+
* `state.pos` until at least `chars` source characters have been taken (or the
|
|
22
|
+
* input ends), appending to `state.out`; returns true once the input is fully
|
|
23
|
+
* consumed.
|
|
24
|
+
*
|
|
25
|
+
* The pass carries NO state between tokens beyond `pos` — every pattern is
|
|
26
|
+
* sticky and matched against the immutable `code` — so stopping and resuming is
|
|
27
|
+
* invisible: for ANY sequence of chunk sizes the final `state.out` is
|
|
28
|
+
* byte-identical to `highlight(code, lang)` (test/hi-chunked.test.ts proves it
|
|
29
|
+
* over the language corpus, down to one token per slice). That is what lets a
|
|
30
|
+
* renderer spread a big block's highlight across several tasks without changing
|
|
31
|
+
* a byte of markup.
|
|
32
|
+
*
|
|
33
|
+
* @internal Not part of the semver surface — use {@link highlight}.
|
|
34
|
+
*/
|
|
35
|
+
export declare function stepHighlight(code: string, lang: string, state: HighlightState, chars: number): boolean;
|
|
11
36
|
export declare function highlight(code: string, lang: string): string;
|
|
12
37
|
export declare function supportedLangs(): string[];
|
package/dist/hi.js
CHANGED
|
@@ -147,26 +147,42 @@ const LANGS = {
|
|
|
147
147
|
css: { pats: cssPats }
|
|
148
148
|
};
|
|
149
149
|
function escapeHtml(s) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
else if (c === "&") out += "&";
|
|
156
|
-
else if (c === '"') out += """;
|
|
157
|
-
else out += c;
|
|
150
|
+
const n = s.length;
|
|
151
|
+
let i = 0;
|
|
152
|
+
for (; i < n; i++) {
|
|
153
|
+
const c = s.charCodeAt(i);
|
|
154
|
+
if (c === 60 || c === 62 || c === 38 || c === 34) break;
|
|
158
155
|
}
|
|
159
|
-
return
|
|
156
|
+
if (i === n) return s;
|
|
157
|
+
let out = s.slice(0, i);
|
|
158
|
+
let last = i;
|
|
159
|
+
for (; i < n; i++) {
|
|
160
|
+
const c = s.charCodeAt(i);
|
|
161
|
+
let esc;
|
|
162
|
+
if (c === 60) esc = "<";
|
|
163
|
+
else if (c === 62) esc = ">";
|
|
164
|
+
else if (c === 38) esc = "&";
|
|
165
|
+
else if (c === 34) esc = """;
|
|
166
|
+
else continue;
|
|
167
|
+
out += s.slice(last, i) + esc;
|
|
168
|
+
last = i + 1;
|
|
169
|
+
}
|
|
170
|
+
return out + s.slice(last);
|
|
160
171
|
}
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
if (!conf)
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
function stepHighlight(code, lang, state, chars) {
|
|
173
|
+
const conf = code.length > 5e4 ? void 0 : LANGS[lang.toLowerCase()];
|
|
174
|
+
const stop = state.pos + (chars > 0 ? chars : 1);
|
|
175
|
+
if (!conf) {
|
|
176
|
+
const end = stop < code.length ? stop : code.length;
|
|
177
|
+
state.out += escapeHtml(code.slice(state.pos, end));
|
|
178
|
+
state.pos = end;
|
|
179
|
+
return state.pos >= code.length;
|
|
180
|
+
}
|
|
181
|
+
let out = state.out;
|
|
182
|
+
let pos = state.pos;
|
|
167
183
|
const pats = conf.pats;
|
|
168
184
|
const kw = conf.kw;
|
|
169
|
-
while (pos < code.length) {
|
|
185
|
+
while (pos < code.length && pos < stop) {
|
|
170
186
|
let matched = false;
|
|
171
187
|
for (let i = 0; i < pats.length; i++) {
|
|
172
188
|
const [cls, re] = pats[i];
|
|
@@ -204,12 +220,21 @@ function highlight(code, lang) {
|
|
|
204
220
|
pos += 1;
|
|
205
221
|
}
|
|
206
222
|
}
|
|
207
|
-
|
|
223
|
+
state.out = out;
|
|
224
|
+
state.pos = pos;
|
|
225
|
+
return pos >= code.length;
|
|
226
|
+
}
|
|
227
|
+
function highlight(code, lang) {
|
|
228
|
+
const state = { pos: 0, out: "" };
|
|
229
|
+
while (!stepHighlight(code, lang, state, code.length)) {
|
|
230
|
+
}
|
|
231
|
+
return state.out;
|
|
208
232
|
}
|
|
209
233
|
function supportedLangs() {
|
|
210
234
|
return Object.keys(LANGS);
|
|
211
235
|
}
|
|
212
236
|
export {
|
|
213
237
|
highlight,
|
|
238
|
+
stepHighlight,
|
|
214
239
|
supportedLangs
|
|
215
240
|
};
|
package/dist/react.js
CHANGED
|
@@ -218,8 +218,9 @@ function blockKindProps(block, components) {
|
|
|
218
218
|
if (block.kind.type === "CodeBlock") {
|
|
219
219
|
props.text = data?.code ?? decodeCodeText(block.html);
|
|
220
220
|
props.language = data?.lang ?? "";
|
|
221
|
+
if (typeof data?.meta === "string") props.meta = data.meta;
|
|
221
222
|
if (typeof data?.code === "string") {
|
|
222
|
-
props.code = { lang: data.lang ?? null, code: data.code };
|
|
223
|
+
props.code = { lang: data.lang ?? null, meta: data.meta, code: data.code };
|
|
223
224
|
}
|
|
224
225
|
} else if (block.kind.type === "MathBlock") {
|
|
225
226
|
props.text = data?.latex ?? decodeMathText(block.html);
|
|
@@ -507,7 +508,17 @@ function renderBlockContent({
|
|
|
507
508
|
switch (kind) {
|
|
508
509
|
case "CodeBlock": {
|
|
509
510
|
const wantsCodeOverride = !!components && (!!components.pre || !!components.code);
|
|
510
|
-
if (!wantsCodeOverride)
|
|
511
|
+
if (!wantsCodeOverride) {
|
|
512
|
+
const source = block.kind.data?.code;
|
|
513
|
+
return /* @__PURE__ */ jsx(
|
|
514
|
+
CodeBlock,
|
|
515
|
+
{
|
|
516
|
+
html: block.html,
|
|
517
|
+
open: block.open,
|
|
518
|
+
code: typeof source === "string" ? source : void 0
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
}
|
|
511
522
|
break;
|
|
512
523
|
}
|
|
513
524
|
case "MathBlock":
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
html: string;
|
|
3
3
|
open: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* The block's DECODED source, carried by `kind.data.code` when `blockData` is
|
|
6
|
+
* on. Identical to `decodeText(html)` — supplying it skips that whole-body
|
|
7
|
+
* regex + five entity passes, which on a big fence is the same order of work
|
|
8
|
+
* as the highlight itself. Absent (blockData off) the HTML is decoded here.
|
|
9
|
+
*/
|
|
10
|
+
code?: string;
|
|
4
11
|
}
|
|
5
|
-
declare function CodeBlockImpl({ html, open }: Props): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
declare function CodeBlockImpl({ html, open, code }: Props): import("react/jsx-runtime").JSX.Element;
|
|
6
13
|
export declare const CodeBlock: import("react").MemoExoticComponent<typeof CodeBlockImpl>;
|
|
7
14
|
export {};
|
|
@@ -1,19 +1,44 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { highlight } from "../hi.js";
|
|
4
|
+
import { highlightDeferred, highlightWithin } from "../hi-defer.js";
|
|
4
5
|
import { extractLang } from "../block-props.js";
|
|
5
6
|
function decodeText(html) {
|
|
6
7
|
const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
|
|
7
8
|
if (!m) return "";
|
|
8
9
|
return m[1].replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
9
10
|
}
|
|
10
|
-
function CodeBlockImpl({ html, open }) {
|
|
11
|
+
function CodeBlockImpl({ html, open, code }) {
|
|
11
12
|
const lang = extractLang(html) || "text";
|
|
12
|
-
const text = useMemo(() => open ? "" : decodeText(html), [html, open]);
|
|
13
|
-
const
|
|
13
|
+
const text = useMemo(() => open ? "" : code ?? decodeText(html), [html, open, code]);
|
|
14
|
+
const sync = useMemo(() => {
|
|
14
15
|
if (!text) return null;
|
|
15
|
-
return highlight(text, lang);
|
|
16
|
+
return typeof window === "undefined" ? highlight(text, lang) : highlightWithin(text, lang);
|
|
16
17
|
}, [text, lang]);
|
|
18
|
+
const [slow, setSlow] = useState(null);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!text || sync !== null) {
|
|
21
|
+
setSlow((prev) => prev === null ? prev : null);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const run = highlightDeferred(text, lang);
|
|
25
|
+
if (run.html !== null) {
|
|
26
|
+
setSlow({ text, lang, html: run.html });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
let live = true;
|
|
30
|
+
const rest = run.rest;
|
|
31
|
+
if (rest) {
|
|
32
|
+
rest.then((out) => {
|
|
33
|
+
if (live && out !== null) setSlow({ text, lang, html: out });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return () => {
|
|
37
|
+
live = false;
|
|
38
|
+
run.cancel();
|
|
39
|
+
};
|
|
40
|
+
}, [text, lang, sync]);
|
|
41
|
+
const highlighted = sync ?? (slow !== null && slow.text === text && slow.lang === lang ? slow.html : null);
|
|
17
42
|
const [copied, setCopied] = useState(false);
|
|
18
43
|
const timerRef = useRef(null);
|
|
19
44
|
useEffect(() => {
|
package/dist/server.d.ts
CHANGED
|
@@ -21,10 +21,21 @@ export declare function parseToBlocks(markdown: string, opts?: {
|
|
|
21
21
|
}): Block[];
|
|
22
22
|
/**
|
|
23
23
|
* Render a complete markdown string to an HTML string synchronously — no worker,
|
|
24
|
-
* no React. The
|
|
25
|
-
* For component dispatch / a `<BrookMarkdown>`-matching React
|
|
26
|
-
* `BrookMarkdownStatic` from `brookmd/server/react` with your
|
|
27
|
-
* renderer instead.
|
|
24
|
+
* no React. The per-block HTML joined into one document (XSS-safe with
|
|
25
|
+
* `unsafeHtml` off). For component dispatch / a `<BrookMarkdown>`-matching React
|
|
26
|
+
* tree, use `BrookMarkdownStatic` from `brookmd/server/react` with your
|
|
27
|
+
* framework's server renderer instead.
|
|
28
|
+
*
|
|
29
|
+
* **Document assembly** (WIRE.md §12): a block's `html` never ends with a
|
|
30
|
+
* newline — that terminator belongs to the document, not the block — so joining
|
|
31
|
+
* blocks inserts one `\n` between them, `cr()`-style: only when the previous
|
|
32
|
+
* block does not ALREADY end with one. A raw HTML block serializes its own
|
|
33
|
+
* trailing newline, so an unconditional join would double it. The document ends
|
|
34
|
+
* with a final newline. This is exactly what a reference CommonMark/GFM renderer
|
|
35
|
+
* emits, and what the spec harnesses measure byte-for-byte against.
|
|
36
|
+
*
|
|
37
|
+
* If you assemble `Block.html` yourself (rather than calling this), use the same
|
|
38
|
+
* rule.
|
|
28
39
|
*/
|
|
29
40
|
export declare function renderToString(markdown: string, opts?: {
|
|
30
41
|
config?: ParserConfig;
|
package/dist/server.js
CHANGED
|
@@ -40,6 +40,8 @@ function makeParser(config) {
|
|
|
40
40
|
p.setGfmFootnotes(config?.gfmFootnotes ?? false);
|
|
41
41
|
p.setGfmMath(config?.gfmMath ?? false);
|
|
42
42
|
p.setDirAuto(config?.dirAuto ?? false);
|
|
43
|
+
p.setLenientLists(config?.lenientLists ?? false);
|
|
44
|
+
p.setSoftBreaks(config?.softBreaks ?? false);
|
|
43
45
|
p.setA11y(config?.a11y ?? false);
|
|
44
46
|
p.setUnsafeHtml(config?.unsafeHtml ?? false);
|
|
45
47
|
p.setComponentTags(config?.componentTags ?? []);
|
|
@@ -49,6 +51,8 @@ function makeParser(config) {
|
|
|
49
51
|
config?.htmlAllowlist ?? [],
|
|
50
52
|
config?.dropHtmlTags ?? []
|
|
51
53
|
);
|
|
54
|
+
p.setBlockHtml(config?.blockHtml ?? false);
|
|
55
|
+
p.setAllowSchemes(config?.allowSchemes ?? []);
|
|
52
56
|
p.setBlockData(config?.blockData ?? false);
|
|
53
57
|
return p;
|
|
54
58
|
}
|
|
@@ -71,7 +75,13 @@ function parseToBlocks(markdown, opts) {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
function renderToString(markdown, opts) {
|
|
74
|
-
|
|
78
|
+
let out = "";
|
|
79
|
+
for (const b of parseToBlocks(markdown, opts)) {
|
|
80
|
+
if (out.length > 0 && !out.endsWith("\n")) out += "\n";
|
|
81
|
+
out += b.html;
|
|
82
|
+
}
|
|
83
|
+
if (out.length > 0 && !out.endsWith("\n")) out += "\n";
|
|
84
|
+
return out;
|
|
75
85
|
}
|
|
76
86
|
export {
|
|
77
87
|
initBrook,
|