brookmd 0.29.0 → 0.29.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 +27 -0
- package/dist/block-props.js +8 -2
- package/dist/dom.js +8 -3
- package/dist/hi-inc.d.ts +15 -0
- package/dist/hi-inc.js +13 -12
- package/dist/react.js +8 -2
- package/dist/renderers/CodeBlock.js +8 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ Notable changes to brookmd (formerly `flux-md`). Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project aims to follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.29.1 — 2026-07-31
|
|
8
|
+
|
|
9
|
+
Performance only; rendered bytes unchanged (asserted: settled markup identical
|
|
10
|
+
with streaming highlight on and off, and mid-stream parity suites untouched).
|
|
11
|
+
|
|
12
|
+
### Performance
|
|
13
|
+
|
|
14
|
+
- **Two quadratic terms removed from the streaming-highlight hot path.** An
|
|
15
|
+
external benchmark of 0.27 showed streaming highlighting costing +36%
|
|
16
|
+
main-thread time on a code-heavy stream. Most of that was the 0.27 DOM
|
|
17
|
+
write pattern, already fixed in 0.28 (the on/off DOM-write delta is 151×
|
|
18
|
+
smaller at this release). Decomposing what remained found two genuinely
|
|
19
|
+
quadratic terms: the revision guard re-scanned the block character-by-
|
|
20
|
+
character on *every* patch to find a divergence point whose exact value
|
|
21
|
+
was never used (~2,070× the source in `charCodeAt` calls — replaced with
|
|
22
|
+
two `startsWith` questions against a cached prefix), and the fence body
|
|
23
|
+
was re-decoded out of its rendered HTML on every patch through a lazy
|
|
24
|
+
regex plus five unconditional entity passes (~5,169× the source — replaced
|
|
25
|
+
with landmark indexing and an `&`-gated entity chain). Streaming a 32 KB
|
|
26
|
+
code block, highlight-only JS cost drops **782 ms → 163 ms**, and both
|
|
27
|
+
terms now scale linearly. A scale-free regression gate
|
|
28
|
+
(`test/streaming-highlight-cost.test.ts`) pins ratios against source and
|
|
29
|
+
markup size — verified to fail on the pre-fix tree.
|
|
30
|
+
- Evaluated and rejected on measurement: computing the highlight in the
|
|
31
|
+
worker (the worker forwards patches as opaque strings; parsing them there
|
|
32
|
+
costs more than the work it would relocate).
|
|
33
|
+
|
|
7
34
|
## 0.29.0 — 2026-07-31
|
|
8
35
|
|
|
9
36
|
**Sublinear resources.** Total parse work has an Ω(n) floor and stays exactly
|
package/dist/block-props.js
CHANGED
|
@@ -2,8 +2,14 @@ function decodeEntities(s) {
|
|
|
2
2
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
3
3
|
}
|
|
4
4
|
function decodeCodeText(html) {
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const open = html.indexOf("<pre><code");
|
|
6
|
+
if (open < 0) return "";
|
|
7
|
+
const start = html.indexOf(">", open + 10);
|
|
8
|
+
if (start < 0) return "";
|
|
9
|
+
const end = html.indexOf("</code></pre>", start + 1);
|
|
10
|
+
if (end < 0) return "";
|
|
11
|
+
const body = html.slice(start + 1, end);
|
|
12
|
+
return body.indexOf("&") < 0 ? body : decodeEntities(body);
|
|
7
13
|
}
|
|
8
14
|
function decodeMathText(html) {
|
|
9
15
|
const d = html.match(/<div class="math math-display">([\s\S]*?)<\/div>/);
|
package/dist/dom.js
CHANGED
|
@@ -776,9 +776,14 @@ function codeText(b) {
|
|
|
776
776
|
return typeof data?.code === "string" ? data.code : decodeCodeText(b.html);
|
|
777
777
|
}
|
|
778
778
|
function decodeCodeText(html) {
|
|
779
|
-
const
|
|
780
|
-
if (
|
|
781
|
-
|
|
779
|
+
const open = html.indexOf("<pre><code");
|
|
780
|
+
if (open < 0) return "";
|
|
781
|
+
const start = html.indexOf(">", open + 10);
|
|
782
|
+
if (start < 0) return "";
|
|
783
|
+
const end = html.indexOf("</code></pre>", start + 1);
|
|
784
|
+
if (end < 0) return "";
|
|
785
|
+
const body = html.slice(start + 1, end);
|
|
786
|
+
return body.indexOf("&") < 0 ? body : body.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
782
787
|
}
|
|
783
788
|
const CONTAINER_ATTR_RE = /([a-zA-Z][a-zA-Z0-9-]*)="([^"]*)"/g;
|
|
784
789
|
function openTagOf(html) {
|
package/dist/hi-inc.d.ts
CHANGED
|
@@ -90,6 +90,21 @@ export interface IncState {
|
|
|
90
90
|
sealed: boolean;
|
|
91
91
|
/** The source last fed in, for the append/revision guard. */
|
|
92
92
|
text: string;
|
|
93
|
+
/**
|
|
94
|
+
* `text.slice(0, c + GAP)` — the settled source the frozen markup was derived
|
|
95
|
+
* from, and {@link adopt}'s whole question in one string: a revision keeps the
|
|
96
|
+
* prefix iff the new text still STARTS WITH this.
|
|
97
|
+
*
|
|
98
|
+
* Cached rather than re-sliced because it changes only when `c` does (once per
|
|
99
|
+
* checkpoint, so once per source line) while the question is asked on every
|
|
100
|
+
* patch. Asking it as `text.startsWith(pfx)` hands the compare to the engine's
|
|
101
|
+
* native string compare; the char-by-char loop it replaces was, on a streamed
|
|
102
|
+
* 32 KB fence, 373 ms of the 480 ms this module spent in total — ten times
|
|
103
|
+
* what the same question costs natively.
|
|
104
|
+
*/
|
|
105
|
+
pfx: string;
|
|
106
|
+
/** The same, for the checkpoint BEFORE `c` — {@link adopt}'s second question. */
|
|
107
|
+
pfx0: string;
|
|
93
108
|
/** Escaped source for `[c, plainUpto)` — the plain tail, extended in place. */
|
|
94
109
|
plain: string;
|
|
95
110
|
plainFrom: number;
|
package/dist/hi-inc.js
CHANGED
|
@@ -80,29 +80,27 @@ function createInc(lang) {
|
|
|
80
80
|
scan: { n: 0, f: false },
|
|
81
81
|
sealed: false,
|
|
82
82
|
text: "",
|
|
83
|
+
pfx: "",
|
|
84
|
+
pfx0: "",
|
|
83
85
|
plain: "",
|
|
84
86
|
plainFrom: 0,
|
|
85
87
|
plainUpto: 0,
|
|
86
88
|
html: null
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
let i = 0;
|
|
92
|
-
while (i < n && a.charCodeAt(i) === b.charCodeAt(i)) i++;
|
|
93
|
-
return i;
|
|
94
|
-
}
|
|
95
|
-
function adopt(st, d) {
|
|
96
|
-
if (st.c > 0 && d >= st.c + GAP) {
|
|
91
|
+
function adopt(st, text) {
|
|
92
|
+
if (st.c > 0 && text.startsWith(st.pfx)) {
|
|
97
93
|
dropTail(st);
|
|
98
94
|
return true;
|
|
99
95
|
}
|
|
100
|
-
if (st.c0 > 0 &&
|
|
96
|
+
if (st.c0 > 0 && text.startsWith(st.pfx0)) {
|
|
101
97
|
st.frozenHtml = st.frozenHtml.slice(0, st.frozenLen0);
|
|
102
98
|
st.frozenRev++;
|
|
103
99
|
st.frozenCut = st.frozenLen0;
|
|
104
100
|
st.c = st.c0;
|
|
101
|
+
st.pfx = st.pfx0;
|
|
105
102
|
st.c0 = 0;
|
|
103
|
+
st.pfx0 = "";
|
|
106
104
|
st.frozenLen0 = 0;
|
|
107
105
|
dropTail(st);
|
|
108
106
|
return true;
|
|
@@ -125,6 +123,8 @@ function reset(st) {
|
|
|
125
123
|
st.frozenCut = 0;
|
|
126
124
|
st.c0 = 0;
|
|
127
125
|
st.frozenLen0 = 0;
|
|
126
|
+
st.pfx = "";
|
|
127
|
+
st.pfx0 = "";
|
|
128
128
|
st.opener = null;
|
|
129
129
|
st.scan = { n: 0, f: false };
|
|
130
130
|
st.sealed = false;
|
|
@@ -136,11 +136,10 @@ function reset(st) {
|
|
|
136
136
|
}
|
|
137
137
|
function incHighlight(st, text) {
|
|
138
138
|
if (st.text === text) return st.html;
|
|
139
|
-
const
|
|
140
|
-
const appended = d === st.text.length && text.length > st.text.length;
|
|
139
|
+
const appended = text.length > st.text.length && text.startsWith(st.text);
|
|
141
140
|
let from = st.text.length;
|
|
142
141
|
if (!appended) {
|
|
143
|
-
if (!adopt(st,
|
|
142
|
+
if (!adopt(st, text)) reset(st);
|
|
144
143
|
from = 0;
|
|
145
144
|
}
|
|
146
145
|
st.text = text;
|
|
@@ -236,9 +235,11 @@ function rescan(st, text) {
|
|
|
236
235
|
const full = st.frozenHtml + state.out;
|
|
237
236
|
if (cp > st.c) {
|
|
238
237
|
st.c0 = st.c;
|
|
238
|
+
st.pfx0 = st.pfx;
|
|
239
239
|
st.frozenLen0 = st.frozenHtml.length;
|
|
240
240
|
st.frozenHtml += state.out.slice(0, cpOut);
|
|
241
241
|
st.c = cp;
|
|
242
|
+
st.pfx = text.slice(0, cp + GAP);
|
|
242
243
|
}
|
|
243
244
|
const found = liveOp;
|
|
244
245
|
if (found === null) {
|
package/dist/react.js
CHANGED
|
@@ -204,8 +204,14 @@ function decodeEntities(s) {
|
|
|
204
204
|
return s.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
205
205
|
}
|
|
206
206
|
function decodeCodeText(html) {
|
|
207
|
-
const
|
|
208
|
-
|
|
207
|
+
const open = html.indexOf("<pre><code");
|
|
208
|
+
if (open < 0) return "";
|
|
209
|
+
const start = html.indexOf(">", open + 10);
|
|
210
|
+
if (start < 0) return "";
|
|
211
|
+
const end = html.indexOf("</code></pre>", start + 1);
|
|
212
|
+
if (end < 0) return "";
|
|
213
|
+
const body = html.slice(start + 1, end);
|
|
214
|
+
return body.indexOf("&") < 0 ? body : decodeEntities(body);
|
|
209
215
|
}
|
|
210
216
|
function decodeMathText(html) {
|
|
211
217
|
const d = html.match(/<div class="math math-display">([\s\S]*?)<\/div>/);
|
|
@@ -7,9 +7,14 @@ import { newIncCode, paintIncCode } from "../splice.js";
|
|
|
7
7
|
import { useHtmlSplice } from "../react-splice.js";
|
|
8
8
|
import { extractLang } from "../block-props.js";
|
|
9
9
|
function decodeText(html) {
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
12
|
-
|
|
10
|
+
const open = html.indexOf("<pre><code");
|
|
11
|
+
if (open < 0) return "";
|
|
12
|
+
const start = html.indexOf(">", open + 10);
|
|
13
|
+
if (start < 0) return "";
|
|
14
|
+
const end = html.indexOf("</code></pre>", start + 1);
|
|
15
|
+
if (end < 0) return "";
|
|
16
|
+
const body = html.slice(start + 1, end);
|
|
17
|
+
return body.indexOf("&") < 0 ? body : body.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
13
18
|
}
|
|
14
19
|
const useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
15
20
|
function CodeBlockImpl({ html, open, code, streamingHighlight, block, __fullRebuild }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brookmd",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "Zero-dep streaming markdown for the browser. Rust→WASM core, Web Worker per stream, incremental parse with speculative closure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": ["./dist/worker.js", "./dist/styles.css"],
|