brookmd 0.26.1 → 0.28.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/CHANGELOG.md +100 -0
- package/README.md +32 -4
- package/dist/client.js +4 -1
- package/dist/dom.d.ts +28 -0
- package/dist/dom.js +214 -20
- package/dist/hi-defer.d.ts +3 -2
- package/dist/hi-defer.js +7 -4
- package/dist/hi-inc.d.ts +127 -0
- package/dist/hi-inc.js +323 -0
- package/dist/hi.d.ts +17 -1
- package/dist/hi.js +5 -1
- package/dist/html-to-react.d.ts +2 -0
- package/dist/html-to-react.js +7 -0
- package/dist/react-splice.d.ts +32 -0
- package/dist/react-splice.js +33 -0
- package/dist/react.d.ts +28 -0
- package/dist/react.js +33 -8
- package/dist/renderers/CodeBlock.d.ts +15 -1
- package/dist/renderers/CodeBlock.js +76 -6
- package/dist/splice.d.ts +126 -0
- package/dist/splice.js +212 -0
- package/package.json +1 -1
package/dist/hi-defer.js
CHANGED
|
@@ -8,12 +8,15 @@ function __setSliceMs(ms) {
|
|
|
8
8
|
}
|
|
9
9
|
const noop = () => {
|
|
10
10
|
};
|
|
11
|
-
function
|
|
12
|
-
|
|
11
|
+
function seeded(seed) {
|
|
12
|
+
return seed === void 0 ? { pos: 0, out: "" } : { pos: seed.pos, out: seed.out };
|
|
13
|
+
}
|
|
14
|
+
function highlightWithin(code, lang, seed) {
|
|
15
|
+
const state = seeded(seed);
|
|
13
16
|
return runSlice(code, lang, state) ? state.out : null;
|
|
14
17
|
}
|
|
15
|
-
function highlightDeferred(code, lang) {
|
|
16
|
-
const state =
|
|
18
|
+
function highlightDeferred(code, lang, seed) {
|
|
19
|
+
const state = seeded(seed);
|
|
17
20
|
if (runSlice(code, lang, state)) {
|
|
18
21
|
return { html: state.out, rest: null, cancel: noop };
|
|
19
22
|
}
|
package/dist/hi-inc.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type HighlightState } from "./hi.js";
|
|
2
|
+
/**
|
|
3
|
+
* How an unbounded opener recognizes its terminator, with the O(1) state each
|
|
4
|
+
* one carries between patches:
|
|
5
|
+
*
|
|
6
|
+
* - `char` — the first occurrence of `ch` closes (`[^ch]*ch` forms: Go raw
|
|
7
|
+
* strings, HTML/CSS attribute strings, Rust `#[…]`, Bash `'…'` and `${…}`).
|
|
8
|
+
* - `starSlash` — `prevStar`, for `/* … *\/`.
|
|
9
|
+
* - `dashGt` — `dashRun`, for `<!-- … -->`.
|
|
10
|
+
* - `esc` — `esc`, for backslash-escaped strings and template literals. A
|
|
11
|
+
* backslash immediately before a newline is {@link DEAD}, not an escape: the
|
|
12
|
+
* pattern's `\\.` does not match a newline, so the form can never close.
|
|
13
|
+
* - `triple` — `runLen`, for Python `"""` / `'''`.
|
|
14
|
+
* - `dbl` — `pendingQuote`, for SQL's doubled-quote escaping (`'a''b'`).
|
|
15
|
+
*/
|
|
16
|
+
type PredKind = {
|
|
17
|
+
k: "char";
|
|
18
|
+
ch: string;
|
|
19
|
+
} | {
|
|
20
|
+
k: "starSlash";
|
|
21
|
+
} | {
|
|
22
|
+
k: "dashGt";
|
|
23
|
+
} | {
|
|
24
|
+
k: "esc";
|
|
25
|
+
ch: string;
|
|
26
|
+
} | {
|
|
27
|
+
k: "triple";
|
|
28
|
+
ch: string;
|
|
29
|
+
} | {
|
|
30
|
+
k: "dbl";
|
|
31
|
+
ch: string;
|
|
32
|
+
};
|
|
33
|
+
/** @internal */
|
|
34
|
+
export interface Opener {
|
|
35
|
+
/** Sticky regex for the OPENING delimiter only — bounded, O(1) to test. */
|
|
36
|
+
re: RegExp;
|
|
37
|
+
/** The pattern class the tokenizer emits when this form IS terminated. */
|
|
38
|
+
cls: string;
|
|
39
|
+
/** The literal terminator. */
|
|
40
|
+
end: string;
|
|
41
|
+
pred: PredKind;
|
|
42
|
+
}
|
|
43
|
+
/** @internal The carried state of one opener's continuation predicate. */
|
|
44
|
+
export interface OpenerScanState {
|
|
45
|
+
n: number;
|
|
46
|
+
f: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** Per-block incremental state. Create with {@link createInc}, feed {@link incHighlight}. */
|
|
49
|
+
export interface IncState {
|
|
50
|
+
/** The language key this state's tables were chosen for. */
|
|
51
|
+
readonly lang: string;
|
|
52
|
+
/** Settled through here: `[0, c)` of the source will never re-tokenize. */
|
|
53
|
+
c: number;
|
|
54
|
+
/** Markup for `[0, c)`. Always a byte-prefix of the block's final markup. */
|
|
55
|
+
frozenHtml: string;
|
|
56
|
+
/**
|
|
57
|
+
* Bumped whenever `frozenHtml` is TRUNCATED or cleared — the one checkpoint of
|
|
58
|
+
* rewind {@link adopt} performs, or a restart. It never moves while the prefix
|
|
59
|
+
* merely GROWS.
|
|
60
|
+
*
|
|
61
|
+
* A renderer that mirrors the frozen prefix into the DOM append-only reads it
|
|
62
|
+
* as the "may I splice?" token: same rev ⇒ the prefix only grew, so appending
|
|
63
|
+
* `frozenHtml.slice(alreadyWritten)` is exact; a changed rev means the prefix
|
|
64
|
+
* was rewritten underneath and the mirror must be re-seeded. Length alone is
|
|
65
|
+
* NOT enough — one call can rewind to `c0` and then re-freeze past the old
|
|
66
|
+
* length, which looks like a plain append but is not one.
|
|
67
|
+
*/
|
|
68
|
+
frozenRev: number;
|
|
69
|
+
/**
|
|
70
|
+
* The length `frozenHtml` was TRUNCATED to at the last {@link frozenRev} bump:
|
|
71
|
+
* `frozenLen0` for {@link adopt}'s one-checkpoint rewind, `0` for a restart.
|
|
72
|
+
*
|
|
73
|
+
* The rev alone says "rewritten"; this says *from where*, which is what lets a
|
|
74
|
+
* DOM mirror rewind to a boundary it already holds instead of re-seeding the
|
|
75
|
+
* whole run. It is load-bearing that this is reported rather than inferred:
|
|
76
|
+
* `adopt` frequently rewinds and then re-freezes PAST the old length within
|
|
77
|
+
* the same call, so the observable `frozenHtml.length` can come back unchanged
|
|
78
|
+
* while its bytes have moved.
|
|
79
|
+
*/
|
|
80
|
+
frozenCut: number;
|
|
81
|
+
/** The checkpoint BEFORE `c`, and the `frozenHtml` length that went with it —
|
|
82
|
+
* the one step of rewind a tail revision needs (see {@link adopt}). */
|
|
83
|
+
c0: number;
|
|
84
|
+
frozenLen0: number;
|
|
85
|
+
/** The unbounded opener live at the tail, or `null`. */
|
|
86
|
+
opener: Opener | null;
|
|
87
|
+
/** `opener`'s carried predicate state. */
|
|
88
|
+
scan: OpenerScanState;
|
|
89
|
+
/** `opener` can never close — stop re-scanning and render the tail plain. */
|
|
90
|
+
sealed: boolean;
|
|
91
|
+
/** The source last fed in, for the append/revision guard. */
|
|
92
|
+
text: string;
|
|
93
|
+
/** Escaped source for `[c, plainUpto)` — the plain tail, extended in place. */
|
|
94
|
+
plain: string;
|
|
95
|
+
plainFrom: number;
|
|
96
|
+
plainUpto: number;
|
|
97
|
+
/** The markup last handed out, so a repeated feed of the same text is free. */
|
|
98
|
+
html: string | null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* State for a block in `lang`, or `null` when the language has no table (the
|
|
102
|
+
* plain-escape fallback has no token boundaries to checkpoint on).
|
|
103
|
+
*/
|
|
104
|
+
export declare function createInc(lang: string): IncState | null;
|
|
105
|
+
/**
|
|
106
|
+
* Feed the block's CURRENT full source and get the markup for all of it, or
|
|
107
|
+
* `null` when the incremental path has bowed out (past {@link CLIFF}) and the
|
|
108
|
+
* caller should render the plain escaped body exactly as it does today.
|
|
109
|
+
*
|
|
110
|
+
* Calls must be append-only. Anything else — a `reset()`, a speculative tail
|
|
111
|
+
* revision, one block's id being reused for different content — is detected
|
|
112
|
+
* (`text.startsWith(prev)`, the same guard the DOM renderer's prefix-append fast
|
|
113
|
+
* path uses) and simply restarts the state from scratch.
|
|
114
|
+
*/
|
|
115
|
+
export declare function incHighlight(st: IncState, text: string): string | null;
|
|
116
|
+
/**
|
|
117
|
+
* The `{pos, out}` a close-time `highlightWithin`/`highlightDeferred` run should
|
|
118
|
+
* resume from, or `undefined` when nothing was frozen or the state does not
|
|
119
|
+
* belong to `text`/`lang` (a revised block, a different language, a block that
|
|
120
|
+
* crossed the cliff). Then the close-time run is the unseeded one it always was.
|
|
121
|
+
*/
|
|
122
|
+
export declare function incSeed(st: IncState, text: string, lang: string): HighlightState | undefined;
|
|
123
|
+
/** @internal Test-only: total source bytes re-tokenized since the last reset. */
|
|
124
|
+
export declare function __getIncScanned(): number;
|
|
125
|
+
/** @internal Test-only. */
|
|
126
|
+
export declare function __resetIncScanned(): void;
|
|
127
|
+
export {};
|
package/dist/hi-inc.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { escapeHtml, stepHighlight } from "./hi.js";
|
|
2
|
+
const GAP = 3;
|
|
3
|
+
const CAP = 8192;
|
|
4
|
+
const CLIFF = 5e4;
|
|
5
|
+
const OPEN = 0;
|
|
6
|
+
const CLOSED = 1;
|
|
7
|
+
const DEAD = 2;
|
|
8
|
+
const BLOCK_COMMENT = { re: /\/\*/y, cls: "com", end: "*/", pred: { k: "starSlash" } };
|
|
9
|
+
const JS_OPENERS = [
|
|
10
|
+
BLOCK_COMMENT,
|
|
11
|
+
{ re: /`/y, cls: "str", end: "`", pred: { k: "esc", ch: "`" } }
|
|
12
|
+
];
|
|
13
|
+
const RUST_OPENERS = [
|
|
14
|
+
BLOCK_COMMENT,
|
|
15
|
+
{ re: /b?"/y, cls: "str", end: '"', pred: { k: "esc", ch: '"' } },
|
|
16
|
+
{ re: /#!?\[/y, cls: "attr", end: "]", pred: { k: "char", ch: "]" } }
|
|
17
|
+
];
|
|
18
|
+
const PY_OPENERS = [
|
|
19
|
+
{ re: /[fFrRbB]{0,2}"""/y, cls: "str", end: '"""', pred: { k: "triple", ch: '"' } },
|
|
20
|
+
{ re: /[fFrRbB]{0,2}'''/y, cls: "str", end: "'''", pred: { k: "triple", ch: "'" } }
|
|
21
|
+
];
|
|
22
|
+
const GO_OPENERS = [
|
|
23
|
+
BLOCK_COMMENT,
|
|
24
|
+
{ re: /`/y, cls: "str", end: "`", pred: { k: "char", ch: "`" } }
|
|
25
|
+
];
|
|
26
|
+
const BASH_OPENERS = [
|
|
27
|
+
{ re: /"/y, cls: "str", end: '"', pred: { k: "esc", ch: '"' } },
|
|
28
|
+
{ re: /'/y, cls: "str", end: "'", pred: { k: "char", ch: "'" } },
|
|
29
|
+
{ re: /\$\{/y, cls: "var", end: "}", pred: { k: "char", ch: "}" } }
|
|
30
|
+
];
|
|
31
|
+
const SQL_OPENERS = [
|
|
32
|
+
BLOCK_COMMENT,
|
|
33
|
+
{ re: /'/y, cls: "str", end: "'", pred: { k: "dbl", ch: "'" } },
|
|
34
|
+
{ re: /"/y, cls: "str", end: '"', pred: { k: "dbl", ch: '"' } }
|
|
35
|
+
];
|
|
36
|
+
const HTML_OPENERS = [
|
|
37
|
+
{ re: /<!--/y, cls: "com", end: "-->", pred: { k: "dashGt" } },
|
|
38
|
+
{ re: /"/y, cls: "str", end: '"', pred: { k: "char", ch: '"' } },
|
|
39
|
+
{ re: /'/y, cls: "str", end: "'", pred: { k: "char", ch: "'" } }
|
|
40
|
+
];
|
|
41
|
+
const CSS_OPENERS = [
|
|
42
|
+
BLOCK_COMMENT,
|
|
43
|
+
{ re: /"/y, cls: "str", end: '"', pred: { k: "char", ch: '"' } },
|
|
44
|
+
{ re: /'/y, cls: "str", end: "'", pred: { k: "char", ch: "'" } }
|
|
45
|
+
];
|
|
46
|
+
const OPENERS = {
|
|
47
|
+
js: JS_OPENERS,
|
|
48
|
+
javascript: JS_OPENERS,
|
|
49
|
+
ts: JS_OPENERS,
|
|
50
|
+
tsx: JS_OPENERS,
|
|
51
|
+
jsx: JS_OPENERS,
|
|
52
|
+
typescript: JS_OPENERS,
|
|
53
|
+
rust: RUST_OPENERS,
|
|
54
|
+
rs: RUST_OPENERS,
|
|
55
|
+
py: PY_OPENERS,
|
|
56
|
+
python: PY_OPENERS,
|
|
57
|
+
go: GO_OPENERS,
|
|
58
|
+
bash: BASH_OPENERS,
|
|
59
|
+
sh: BASH_OPENERS,
|
|
60
|
+
shell: BASH_OPENERS,
|
|
61
|
+
json: [],
|
|
62
|
+
sql: SQL_OPENERS,
|
|
63
|
+
html: HTML_OPENERS,
|
|
64
|
+
xml: HTML_OPENERS,
|
|
65
|
+
css: CSS_OPENERS
|
|
66
|
+
};
|
|
67
|
+
const GT_CHECKPOINT = /* @__PURE__ */ new Set(["html", "xml"]);
|
|
68
|
+
function createInc(lang) {
|
|
69
|
+
const key = lang.toLowerCase();
|
|
70
|
+
if (!Object.prototype.hasOwnProperty.call(OPENERS, key)) return null;
|
|
71
|
+
return {
|
|
72
|
+
lang: key,
|
|
73
|
+
c: 0,
|
|
74
|
+
frozenHtml: "",
|
|
75
|
+
frozenRev: 0,
|
|
76
|
+
frozenCut: 0,
|
|
77
|
+
c0: 0,
|
|
78
|
+
frozenLen0: 0,
|
|
79
|
+
opener: null,
|
|
80
|
+
scan: { n: 0, f: false },
|
|
81
|
+
sealed: false,
|
|
82
|
+
text: "",
|
|
83
|
+
plain: "",
|
|
84
|
+
plainFrom: 0,
|
|
85
|
+
plainUpto: 0,
|
|
86
|
+
html: null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function divergence(a, b) {
|
|
90
|
+
const n = a.length < b.length ? a.length : b.length;
|
|
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) {
|
|
97
|
+
dropTail(st);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (st.c0 > 0 && d >= st.c0 + GAP) {
|
|
101
|
+
st.frozenHtml = st.frozenHtml.slice(0, st.frozenLen0);
|
|
102
|
+
st.frozenRev++;
|
|
103
|
+
st.frozenCut = st.frozenLen0;
|
|
104
|
+
st.c = st.c0;
|
|
105
|
+
st.c0 = 0;
|
|
106
|
+
st.frozenLen0 = 0;
|
|
107
|
+
dropTail(st);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function dropTail(st) {
|
|
113
|
+
st.opener = null;
|
|
114
|
+
st.scan = { n: 0, f: false };
|
|
115
|
+
st.sealed = false;
|
|
116
|
+
st.plain = "";
|
|
117
|
+
st.plainFrom = st.c;
|
|
118
|
+
st.plainUpto = st.c;
|
|
119
|
+
st.html = null;
|
|
120
|
+
}
|
|
121
|
+
function reset(st) {
|
|
122
|
+
st.c = 0;
|
|
123
|
+
st.frozenHtml = "";
|
|
124
|
+
st.frozenRev++;
|
|
125
|
+
st.frozenCut = 0;
|
|
126
|
+
st.c0 = 0;
|
|
127
|
+
st.frozenLen0 = 0;
|
|
128
|
+
st.opener = null;
|
|
129
|
+
st.scan = { n: 0, f: false };
|
|
130
|
+
st.sealed = false;
|
|
131
|
+
st.text = "";
|
|
132
|
+
st.plain = "";
|
|
133
|
+
st.plainFrom = 0;
|
|
134
|
+
st.plainUpto = 0;
|
|
135
|
+
st.html = null;
|
|
136
|
+
}
|
|
137
|
+
function incHighlight(st, text) {
|
|
138
|
+
if (st.text === text) return st.html;
|
|
139
|
+
const d = divergence(st.text, text);
|
|
140
|
+
const appended = d === st.text.length && text.length > st.text.length;
|
|
141
|
+
let from = st.text.length;
|
|
142
|
+
if (!appended) {
|
|
143
|
+
if (!adopt(st, d)) reset(st);
|
|
144
|
+
from = 0;
|
|
145
|
+
}
|
|
146
|
+
st.text = text;
|
|
147
|
+
if (text.length > CLIFF) {
|
|
148
|
+
reset(st);
|
|
149
|
+
st.text = text;
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const live = st.opener;
|
|
153
|
+
if (live !== null) {
|
|
154
|
+
const r = st.sealed ? OPEN : feed(live.pred, st.scan, text, from, text.length);
|
|
155
|
+
if (r === DEAD) st.sealed = true;
|
|
156
|
+
if (r !== CLOSED) {
|
|
157
|
+
st.html = st.frozenHtml + plainTail(st, text);
|
|
158
|
+
return st.html;
|
|
159
|
+
}
|
|
160
|
+
st.opener = null;
|
|
161
|
+
}
|
|
162
|
+
if (text.length - st.c > CAP) {
|
|
163
|
+
st.html = st.frozenHtml + plainTail(st, text);
|
|
164
|
+
return st.html;
|
|
165
|
+
}
|
|
166
|
+
st.html = rescan(st, text);
|
|
167
|
+
return st.html;
|
|
168
|
+
}
|
|
169
|
+
function incSeed(st, text, lang) {
|
|
170
|
+
if (st.c === 0 || st.lang !== lang.toLowerCase()) return void 0;
|
|
171
|
+
if (text.length > CLIFF || text.length < st.c) return void 0;
|
|
172
|
+
if (!text.startsWith(st.text.slice(0, st.c))) return void 0;
|
|
173
|
+
return { pos: st.c, out: st.frozenHtml };
|
|
174
|
+
}
|
|
175
|
+
let scanned = 0;
|
|
176
|
+
function __getIncScanned() {
|
|
177
|
+
return scanned;
|
|
178
|
+
}
|
|
179
|
+
function __resetIncScanned() {
|
|
180
|
+
scanned = 0;
|
|
181
|
+
}
|
|
182
|
+
function plainTail(st, text) {
|
|
183
|
+
if (st.plainFrom !== st.c || st.plainUpto > text.length) {
|
|
184
|
+
st.plainFrom = st.c;
|
|
185
|
+
st.plain = "";
|
|
186
|
+
st.plainUpto = st.c;
|
|
187
|
+
}
|
|
188
|
+
if (st.plainUpto < text.length) {
|
|
189
|
+
st.plain += escapeHtml(text.slice(st.plainUpto));
|
|
190
|
+
st.plainUpto = text.length;
|
|
191
|
+
}
|
|
192
|
+
return st.plain;
|
|
193
|
+
}
|
|
194
|
+
function rescan(st, text) {
|
|
195
|
+
const openers = OPENERS[st.lang];
|
|
196
|
+
const gt = GT_CHECKPOINT.has(st.lang);
|
|
197
|
+
const limit = text.length - GAP;
|
|
198
|
+
let cp = -1;
|
|
199
|
+
let cpOut = 0;
|
|
200
|
+
let liveAt = -1;
|
|
201
|
+
let liveOp = null;
|
|
202
|
+
let liveLen = 0;
|
|
203
|
+
const sink = (cls, start, end, outLen) => {
|
|
204
|
+
if (liveAt >= 0) return;
|
|
205
|
+
for (let i = 0; i < openers.length; i++) {
|
|
206
|
+
const op = openers[i];
|
|
207
|
+
op.re.lastIndex = start;
|
|
208
|
+
const m = op.re.exec(text);
|
|
209
|
+
if (m === null || m.index !== start) continue;
|
|
210
|
+
const terminated = cls === op.cls && end - start >= m[0].length + op.end.length && text.startsWith(op.end, end - op.end.length);
|
|
211
|
+
if (terminated) continue;
|
|
212
|
+
liveAt = start;
|
|
213
|
+
liveOp = op;
|
|
214
|
+
liveLen = m[0].length;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (gt) {
|
|
218
|
+
if (cls === "pun" && end - start === 1 && text.charCodeAt(start) === 62 && end <= limit) {
|
|
219
|
+
cp = end;
|
|
220
|
+
cpOut = outLen;
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (cls !== "ws") return;
|
|
225
|
+
const hi = end < limit ? end : limit;
|
|
226
|
+
if (hi <= start) return;
|
|
227
|
+
const nl = text.lastIndexOf("\n", hi - 1);
|
|
228
|
+
if (nl < start) return;
|
|
229
|
+
cp = nl + 1;
|
|
230
|
+
cpOut = outLen - (end - cp);
|
|
231
|
+
};
|
|
232
|
+
const state = { pos: st.c, out: "" };
|
|
233
|
+
scanned += text.length - st.c;
|
|
234
|
+
while (!stepHighlight(text, st.lang, state, text.length, sink)) {
|
|
235
|
+
}
|
|
236
|
+
const full = st.frozenHtml + state.out;
|
|
237
|
+
if (cp > st.c) {
|
|
238
|
+
st.c0 = st.c;
|
|
239
|
+
st.frozenLen0 = st.frozenHtml.length;
|
|
240
|
+
st.frozenHtml += state.out.slice(0, cpOut);
|
|
241
|
+
st.c = cp;
|
|
242
|
+
}
|
|
243
|
+
const found = liveOp;
|
|
244
|
+
if (found === null) {
|
|
245
|
+
st.opener = null;
|
|
246
|
+
} else {
|
|
247
|
+
st.scan = { n: 0, f: false };
|
|
248
|
+
const r = feed(found.pred, st.scan, text, liveAt + liveLen, text.length);
|
|
249
|
+
st.opener = r === CLOSED ? null : found;
|
|
250
|
+
st.sealed = r === DEAD;
|
|
251
|
+
}
|
|
252
|
+
return full;
|
|
253
|
+
}
|
|
254
|
+
function feed(p, st, s, from, to) {
|
|
255
|
+
switch (p.k) {
|
|
256
|
+
case "char": {
|
|
257
|
+
const i = s.indexOf(p.ch, from);
|
|
258
|
+
return i >= 0 && i < to ? CLOSED : OPEN;
|
|
259
|
+
}
|
|
260
|
+
case "starSlash": {
|
|
261
|
+
for (let i = from; i < to; i++) {
|
|
262
|
+
const ch = s[i];
|
|
263
|
+
if (st.f && ch === "/") return CLOSED;
|
|
264
|
+
st.f = ch === "*";
|
|
265
|
+
}
|
|
266
|
+
return OPEN;
|
|
267
|
+
}
|
|
268
|
+
case "dashGt": {
|
|
269
|
+
for (let i = from; i < to; i++) {
|
|
270
|
+
const ch = s[i];
|
|
271
|
+
if (ch === ">" && st.n >= 2) return CLOSED;
|
|
272
|
+
st.n = ch === "-" ? st.n + 1 : 0;
|
|
273
|
+
}
|
|
274
|
+
return OPEN;
|
|
275
|
+
}
|
|
276
|
+
case "esc": {
|
|
277
|
+
for (let i = from; i < to; i++) {
|
|
278
|
+
const ch = s[i];
|
|
279
|
+
if (st.f) {
|
|
280
|
+
st.f = false;
|
|
281
|
+
if (ch === "\n") return DEAD;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (ch === "\\") {
|
|
285
|
+
st.f = true;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (ch === p.ch) return CLOSED;
|
|
289
|
+
}
|
|
290
|
+
return OPEN;
|
|
291
|
+
}
|
|
292
|
+
case "triple": {
|
|
293
|
+
for (let i = from; i < to; i++) {
|
|
294
|
+
if (s[i] === p.ch) {
|
|
295
|
+
st.n++;
|
|
296
|
+
if (st.n === 3) return CLOSED;
|
|
297
|
+
} else {
|
|
298
|
+
st.n = 0;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return OPEN;
|
|
302
|
+
}
|
|
303
|
+
case "dbl": {
|
|
304
|
+
for (let i = from; i < to; i++) {
|
|
305
|
+
const ch = s[i];
|
|
306
|
+
if (st.f) {
|
|
307
|
+
st.f = false;
|
|
308
|
+
if (ch !== p.ch) return CLOSED;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (ch === p.ch) st.f = true;
|
|
312
|
+
}
|
|
313
|
+
return OPEN;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export {
|
|
318
|
+
__getIncScanned,
|
|
319
|
+
__resetIncScanned,
|
|
320
|
+
createInc,
|
|
321
|
+
incHighlight,
|
|
322
|
+
incSeed
|
|
323
|
+
};
|
package/dist/hi.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
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
|
+
export declare function escapeHtml(s: string): string;
|
|
11
12
|
/**
|
|
12
13
|
* The resumable tokenizer's cursor: `pos` is the next source index to consume,
|
|
13
14
|
* `out` the markup emitted so far. Start a run at `{ pos: 0, out: "" }`.
|
|
@@ -16,6 +17,21 @@ export interface HighlightState {
|
|
|
16
17
|
pos: number;
|
|
17
18
|
out: string;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Called once per token the tokenizer emits, AFTER its markup is appended:
|
|
22
|
+
* `(cls, start, end, outLen)` where `cls` is the PATTERN class (`ws`, `str`,
|
|
23
|
+
* `com`, `pun`, `ident`… — not the `kw`/`fn`/`ty` refinement), `[start, end)` is
|
|
24
|
+
* the source span, and `outLen` is `state.out.length` once the token has been
|
|
25
|
+
* written. The catch-all one-character fallback reports `cls === ""`.
|
|
26
|
+
*
|
|
27
|
+
* Passing no sink is the default and costs one `undefined` test per token; the
|
|
28
|
+
* escape-fallback path (unknown language / over the size guard) emits no tokens
|
|
29
|
+
* and so reports nothing.
|
|
30
|
+
*
|
|
31
|
+
* @internal The incremental streaming path (hi-inc.ts) is the only consumer —
|
|
32
|
+
* it needs token boundaries to pick a checkpoint that survives an append.
|
|
33
|
+
*/
|
|
34
|
+
export type TokenSink = (cls: string, start: number, end: number, outLen: number) => void;
|
|
19
35
|
/**
|
|
20
36
|
* One resumable slice of {@link highlight}. Consumes WHOLE tokens from
|
|
21
37
|
* `state.pos` until at least `chars` source characters have been taken (or the
|
|
@@ -32,6 +48,6 @@ export interface HighlightState {
|
|
|
32
48
|
*
|
|
33
49
|
* @internal Not part of the semver surface — use {@link highlight}.
|
|
34
50
|
*/
|
|
35
|
-
export declare function stepHighlight(code: string, lang: string, state: HighlightState, chars: number): boolean;
|
|
51
|
+
export declare function stepHighlight(code: string, lang: string, state: HighlightState, chars: number, sink?: TokenSink): boolean;
|
|
36
52
|
export declare function highlight(code: string, lang: string): string;
|
|
37
53
|
export declare function supportedLangs(): string[];
|
package/dist/hi.js
CHANGED
|
@@ -169,7 +169,7 @@ function escapeHtml(s) {
|
|
|
169
169
|
}
|
|
170
170
|
return out + s.slice(last);
|
|
171
171
|
}
|
|
172
|
-
function stepHighlight(code, lang, state, chars) {
|
|
172
|
+
function stepHighlight(code, lang, state, chars, sink) {
|
|
173
173
|
const conf = code.length > 5e4 ? void 0 : LANGS[lang.toLowerCase()];
|
|
174
174
|
const stop = state.pos + (chars > 0 ? chars : 1);
|
|
175
175
|
if (!conf) {
|
|
@@ -201,6 +201,7 @@ function stepHighlight(code, lang, state, chars) {
|
|
|
201
201
|
finalCls = "ty";
|
|
202
202
|
} else {
|
|
203
203
|
out += escapeHtml(text);
|
|
204
|
+
if (sink) sink(cls, pos, after, out.length);
|
|
204
205
|
pos = after;
|
|
205
206
|
matched = true;
|
|
206
207
|
break;
|
|
@@ -211,12 +212,14 @@ function stepHighlight(code, lang, state, chars) {
|
|
|
211
212
|
} else {
|
|
212
213
|
out += `<span class="t-${finalCls}">${escapeHtml(text)}</span>`;
|
|
213
214
|
}
|
|
215
|
+
if (sink) sink(cls, pos, after, out.length);
|
|
214
216
|
pos = after;
|
|
215
217
|
matched = true;
|
|
216
218
|
break;
|
|
217
219
|
}
|
|
218
220
|
if (!matched) {
|
|
219
221
|
out += escapeHtml(code[pos]);
|
|
222
|
+
if (sink) sink("", pos, pos + 1, out.length);
|
|
220
223
|
pos += 1;
|
|
221
224
|
}
|
|
222
225
|
}
|
|
@@ -234,6 +237,7 @@ function supportedLangs() {
|
|
|
234
237
|
return Object.keys(LANGS);
|
|
235
238
|
}
|
|
236
239
|
export {
|
|
240
|
+
escapeHtml,
|
|
237
241
|
highlight,
|
|
238
242
|
stepHighlight,
|
|
239
243
|
supportedLangs
|
package/dist/html-to-react.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ type HNode = {
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function parseStyle(css: string): Record<string, string>;
|
|
21
21
|
export declare function getParseCount(): number;
|
|
22
|
+
/** @internal Test-only. Characters of markup handed to the tokenizer so far. */
|
|
23
|
+
export declare function getParseChars(): number;
|
|
22
24
|
export declare function resetParseCount(): void;
|
|
23
25
|
export declare function parseTrustedHtml(html: string): HNode[];
|
|
24
26
|
/**
|
package/dist/html-to-react.js
CHANGED
|
@@ -131,14 +131,20 @@ function parseOpenTag(html, start) {
|
|
|
131
131
|
return { tag, attrs, selfClose: false, next: i };
|
|
132
132
|
}
|
|
133
133
|
let parseCount = 0;
|
|
134
|
+
let parseChars = 0;
|
|
134
135
|
function getParseCount() {
|
|
135
136
|
return parseCount;
|
|
136
137
|
}
|
|
138
|
+
function getParseChars() {
|
|
139
|
+
return parseChars;
|
|
140
|
+
}
|
|
137
141
|
function resetParseCount() {
|
|
138
142
|
parseCount = 0;
|
|
143
|
+
parseChars = 0;
|
|
139
144
|
}
|
|
140
145
|
function parseTrustedHtml(html) {
|
|
141
146
|
parseCount++;
|
|
147
|
+
parseChars += html.length;
|
|
142
148
|
const root = [];
|
|
143
149
|
const stack = [];
|
|
144
150
|
let i = 0;
|
|
@@ -357,6 +363,7 @@ function wrapLink(text, attrs) {
|
|
|
357
363
|
}
|
|
358
364
|
export {
|
|
359
365
|
decodeEntities,
|
|
366
|
+
getParseChars,
|
|
360
367
|
getParseCount,
|
|
361
368
|
htmlToReact,
|
|
362
369
|
parseStyle,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import type { Block } from "./types-core.js";
|
|
3
|
+
/**
|
|
4
|
+
* Let a layout effect own an element's children so an OPEN block's patch is
|
|
5
|
+
* applied incrementally (see splice.ts) instead of re-setting the whole
|
|
6
|
+
* `dangerouslySetInnerHTML` every time it grows.
|
|
7
|
+
*
|
|
8
|
+
* ## How React and the effect share the node
|
|
9
|
+
*
|
|
10
|
+
* The returned string is the html captured on the FIRST render and never
|
|
11
|
+
* changes again. Render it as the node's `__html` and React writes the element
|
|
12
|
+
* exactly once, at mount — its own `lastHtml !== nextHtml` check then keeps it
|
|
13
|
+
* from touching the children on any later commit, and the effect owns them from
|
|
14
|
+
* there.
|
|
15
|
+
*
|
|
16
|
+
* That is what makes this safe under concurrent rendering: a render that is
|
|
17
|
+
* thrown away commits nothing and runs no effect, and the effect's own
|
|
18
|
+
* bookkeeping makes a repeat run (StrictMode's double-invoke) a no-op. It also
|
|
19
|
+
* leaves SSR and hydration byte-identical, because the first markup React
|
|
20
|
+
* produces is still the block's full html.
|
|
21
|
+
*
|
|
22
|
+
* The caller hands the node BACK to React by rendering a different element
|
|
23
|
+
* (a closed block's plain `<div dangerouslySetInnerHTML>`), which remounts the
|
|
24
|
+
* subtree and re-renders the settled html in one pass.
|
|
25
|
+
*
|
|
26
|
+
* @param hostRef ref attached to the element whose children are managed
|
|
27
|
+
* @param block the block's CURRENT version (identity matters — `spliceKeep`
|
|
28
|
+
* is keyed on it)
|
|
29
|
+
* @param enabled false to stay entirely out of the way
|
|
30
|
+
* @returns the html to render as `__html`, or `null` when not managing
|
|
31
|
+
*/
|
|
32
|
+
export declare function useHtmlSplice(hostRef: MutableRefObject<HTMLElement | null>, block: Block | undefined, enabled: boolean): string | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
2
|
+
import { spliceHtml, spliceKeep } from "./splice.js";
|
|
3
|
+
const useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
4
|
+
function useHtmlSplice(hostRef, block, enabled) {
|
|
5
|
+
const seed = useRef(block);
|
|
6
|
+
const applied = useRef(null);
|
|
7
|
+
useIsoLayoutEffect(() => {
|
|
8
|
+
const base = seed.current;
|
|
9
|
+
if (!enabled || block === void 0 || base === void 0) {
|
|
10
|
+
applied.current = null;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const node = hostRef.current;
|
|
14
|
+
if (node === null) return;
|
|
15
|
+
let prev = applied.current;
|
|
16
|
+
if (prev === null || prev.node !== node) prev = { node, block: base };
|
|
17
|
+
if (prev.block === block) {
|
|
18
|
+
applied.current = prev;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const keep = spliceKeep(prev.block, block);
|
|
22
|
+
if (keep !== void 0 && spliceHtml(node, prev.block.html, block.html, keep)) {
|
|
23
|
+
applied.current = { node, block };
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
node.innerHTML = block.html;
|
|
27
|
+
applied.current = { node, block };
|
|
28
|
+
});
|
|
29
|
+
return enabled && seed.current !== void 0 ? seed.current.html : null;
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
useHtmlSplice
|
|
33
|
+
};
|
package/dist/react.d.ts
CHANGED
|
@@ -128,6 +128,31 @@ interface BrookMarkdownProps {
|
|
|
128
128
|
* applies only to the streaming tail.
|
|
129
129
|
*/
|
|
130
130
|
childMemo?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Highlight a code fence **while it is still streaming**, instead of showing
|
|
133
|
+
* plain escaped text until it closes. On by default.
|
|
134
|
+
*
|
|
135
|
+
* An open block keeps a frozen prefix and re-tokenizes only its tail on each
|
|
136
|
+
* patch, so this stays linear in the block's size (it does not re-highlight
|
|
137
|
+
* the whole fence per chunk). The settled markup is byte-identical either way
|
|
138
|
+
* — only the tail's colours are provisional, and they may shift as bytes
|
|
139
|
+
* arrive (`"hello` is a stray quote plus an identifier until its closing quote
|
|
140
|
+
* lands). Set `false` for the pre-0.27 behaviour: plain body until close.
|
|
141
|
+
*
|
|
142
|
+
* No effect on SSR (the server renders closed blocks only), and none at all
|
|
143
|
+
* when `components.CodeBlock` / `components.pre` / `components.code` take over
|
|
144
|
+
* the block — an override bypasses the built-in highlighter entirely.
|
|
145
|
+
*/
|
|
146
|
+
streamingHighlight?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* @internal TEST-ONLY. Turn off the incremental apply paths (the open code
|
|
149
|
+
* block's frozen/tail mirror and the open generic block's delta splice) so
|
|
150
|
+
* every patch re-renders the whole block through React, exactly as it did
|
|
151
|
+
* before they existed. The DOM-parity fuzz renders one tree with it on and one
|
|
152
|
+
* with it off and asserts their markup matches after every commit. Not part of
|
|
153
|
+
* the supported API.
|
|
154
|
+
*/
|
|
155
|
+
__fullRebuild?: boolean;
|
|
131
156
|
/** Appended to the root's `className` (the `brook-md` class is always present). */
|
|
132
157
|
className?: string;
|
|
133
158
|
/** Set on the root element. */
|
|
@@ -264,6 +289,9 @@ interface BlockViewProps {
|
|
|
264
289
|
virtualize?: boolean;
|
|
265
290
|
sanitize?: (html: string) => string;
|
|
266
291
|
childMemo?: boolean;
|
|
292
|
+
streamingHighlight?: boolean;
|
|
293
|
+
/** @internal TEST-ONLY — see BrookMarkdownProps.__fullRebuild. */
|
|
294
|
+
__fullRebuild?: boolean;
|
|
267
295
|
onRenderMetrics?: RenderMetricsHook;
|
|
268
296
|
decorators?: Decorator[];
|
|
269
297
|
urlTransform?: UrlTransform;
|