brookmd 0.29.1 → 0.30.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 +143 -1
- package/README.md +643 -30
- package/dist/dom.d.ts +25 -3
- package/dist/dom.js +25 -5
- package/dist/element.d.ts +3 -0
- package/dist/element.js +22 -2
- package/dist/hi-inc.d.ts +13 -0
- package/dist/hi-inc.js +27 -4
- package/dist/hi.d.ts +62 -4
- package/dist/hi.js +243 -22
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/react.d.ts +30 -4
- package/dist/react.js +17 -1
- package/dist/renderers/CodeBlock.d.ts +3 -2
- package/dist/renderers/CodeBlock.js +21 -8
- package/dist/solid.js +1 -0
- package/dist/splice.d.ts +58 -11
- package/dist/splice.js +35 -6
- package/dist/styles.css +295 -0
- package/dist/svelte.d.ts +6 -2
- package/dist/svelte.js +6 -3
- package/dist/types-core.d.ts +21 -1
- package/dist/types-react.d.ts +1 -1
- package/dist/vue.d.ts +8 -6
- package/dist/vue.js +11 -3
- package/dist/wasm/brook_md_core.d.ts +1 -1
- package/dist/wasm/brook_md_core.js +1 -1
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/package.json +1 -1
package/dist/hi.js
CHANGED
|
@@ -32,6 +32,50 @@ const KEYWORDS_SQL = new Set(
|
|
|
32
32
|
" "
|
|
33
33
|
)
|
|
34
34
|
);
|
|
35
|
+
const KEYWORDS_JAVA = new Set(
|
|
36
|
+
"abstract assert boolean break byte case catch char class const continue default do double else enum extends false final finally float for goto if implements import instanceof int interface long native new null package private protected public record return sealed short static strictfp super switch synchronized this throw throws transient true try var void volatile while yield".split(
|
|
37
|
+
" "
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
const KEYWORDS_C = new Set(
|
|
41
|
+
"auto bool break case char const continue default do double else enum extern false float for goto if inline int long register restrict return short signed sizeof static struct switch true typedef union unsigned void volatile while NULL".split(
|
|
42
|
+
" "
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
const KEYWORDS_CPP = /* @__PURE__ */ new Set([
|
|
46
|
+
...KEYWORDS_C,
|
|
47
|
+
...["catch", "class", "concept", "constexpr", "const_cast", "decltype", "delete", "dynamic_cast", "explicit", "export", "final", "friend", "mutable", "namespace", "new", "noexcept", "nullptr", "operator", "override", "private", "protected", "public", "reinterpret_cast", "requires", "static_assert", "static_cast", "template", "this", "throw", "try", "typeid", "typename", "using", "virtual"]
|
|
48
|
+
]);
|
|
49
|
+
const KEYWORDS_CS = new Set(
|
|
50
|
+
"abstract as async await base bool break byte case catch char checked class const continue decimal default delegate do double dynamic else enum event explicit extern false finally fixed float for foreach get goto if implicit in int interface internal is lock long nameof namespace new null object operator out override params private protected public readonly record ref return sbyte sealed set short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using var virtual void volatile while yield".split(
|
|
51
|
+
" "
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
const KEYWORDS_PHP = new Set(
|
|
55
|
+
"abstract and array as bool break callable case catch class clone const continue declare default do echo else elseif empty enum extends final finally float fn for foreach function global goto if implements include include_once instanceof insteadof int interface isset list match namespace new null or parent print private protected public readonly require require_once return self static string switch throw trait true try unset use var void while xor yield false".split(
|
|
56
|
+
" "
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
const KEYWORDS_RUBY = new Set(
|
|
60
|
+
"BEGIN END alias and begin break case class def defined? do else elsif end ensure false for if in module next nil not or redo require require_relative rescue retry return self super then true undef unless until when while yield attr_accessor attr_reader attr_writer lambda proc".split(
|
|
61
|
+
" "
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
const KEYWORDS_SWIFT = new Set(
|
|
65
|
+
"actor any as associatedtype async await break case catch class continue default defer deinit do else enum extension fallthrough false fileprivate for func guard if import in indirect init inout internal is lazy let mutating nil open operator private protocol public repeat rethrows return self Self some static struct subscript super switch throw throws true try typealias var where while".split(
|
|
66
|
+
" "
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
const KEYWORDS_KOTLIN = new Set(
|
|
70
|
+
"abstract actual annotation as break by catch class companion const constructor continue crossinline data do dynamic else enum expect external false field final finally for fun get if import in infix init inline inner interface internal is it lateinit noinline null object open operator out override package private protected public reified return sealed set super suspend tailrec this throw true try typealias typeof val var vararg when where while".split(
|
|
71
|
+
" "
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
const KEYWORDS_DOCKER = new Set(
|
|
75
|
+
"ADD ARG AS CMD COPY ENTRYPOINT ENV EXPOSE FROM HEALTHCHECK LABEL MAINTAINER ONBUILD RUN SHELL STOPSIGNAL USER VOLUME WORKDIR as".split(
|
|
76
|
+
" "
|
|
77
|
+
)
|
|
78
|
+
);
|
|
35
79
|
const jsPats = [
|
|
36
80
|
["com", /\/\/[^\n]*/y],
|
|
37
81
|
["com", /\/\*[\s\S]*?\*\//y],
|
|
@@ -125,27 +169,137 @@ const cssPats = [
|
|
|
125
169
|
["pun", /[:;,{}()]/y],
|
|
126
170
|
["ws", /\s+/y]
|
|
127
171
|
];
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
const cFamilyPats = [
|
|
173
|
+
["com", /\/\/[^\n]*/y],
|
|
174
|
+
["com", /\/\*[\s\S]*?\*\//y],
|
|
175
|
+
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
176
|
+
["str", /'(?:\\.|[^'\\\n])*'/y],
|
|
177
|
+
// The suffix class after a hex literal must not overlap the hex digits:
|
|
178
|
+
// `[\da-fA-F_]+` followed by `[lLfFdDuU]*` shares `d D f F`, and a long hex
|
|
179
|
+
// run ending in a non-suffix word char then backtracks quadratically (a
|
|
180
|
+
// 16 KB run cost ~0.5 s). Hex/binary take only `[lLuU]`; decimal keeps the
|
|
181
|
+
// float suffixes, whose class is disjoint from digits.
|
|
182
|
+
["num", /\b(?:0x[\da-fA-F_]+[lLuU]*|0b[01_]+[lLuU]*|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?[lLfFdDuU]*)\b/y],
|
|
183
|
+
["dec", /@[A-Za-z_]\w*/y],
|
|
184
|
+
["ident", /[A-Za-z_]\w*/y],
|
|
185
|
+
["pun", /[+\-*/=<>!&|^~?:;,.[\](){}%]/y],
|
|
186
|
+
["ws", /\s+/y]
|
|
187
|
+
];
|
|
188
|
+
const cPats = [["mac", /#[ \t]*[A-Za-z_]+/y], ...cFamilyPats];
|
|
189
|
+
const phpPats = [
|
|
190
|
+
["com", /\/\/[^\n]*/y],
|
|
191
|
+
["com", /#[^\n]*/y],
|
|
192
|
+
["com", /\/\*[\s\S]*?\*\//y],
|
|
193
|
+
["tag", /<\?php\b|<\?=|\?>/y],
|
|
194
|
+
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
195
|
+
["str", /'(?:\\.|[^'\\\n])*'/y],
|
|
196
|
+
["var", /\$+[A-Za-z_]\w*/y],
|
|
197
|
+
["num", /\b(?:0x[\da-fA-F_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?)\b/y],
|
|
198
|
+
["ident", /[A-Za-z_]\w*/y],
|
|
199
|
+
["pun", /[+\-*/=<>!&|^~?:;,.[\](){}%@\\]/y],
|
|
200
|
+
["ws", /\s+/y]
|
|
201
|
+
];
|
|
202
|
+
const rubyPats = [
|
|
203
|
+
["com", /#[^\n]*/y],
|
|
204
|
+
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
205
|
+
["str", /'(?:\\.|[^'\\\n])*'/y],
|
|
206
|
+
["lt", /:[A-Za-z_]\w*[?!]?/y],
|
|
207
|
+
["var", /@@?[A-Za-z_]\w*|\$[A-Za-z_]\w*/y],
|
|
208
|
+
["num", /\b\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?\b/y],
|
|
209
|
+
["ident", /[A-Za-z_]\w*[?!]?/y],
|
|
210
|
+
["pun", /[+\-*/=<>!&|^~?:;,.[\](){}%]/y],
|
|
211
|
+
["ws", /\s+/y]
|
|
212
|
+
];
|
|
213
|
+
const yamlPats = [
|
|
214
|
+
["com", /#[^\n]*/y],
|
|
215
|
+
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
216
|
+
["str", /'(?:''|[^'\n])*'/y],
|
|
217
|
+
["attr", /[A-Za-z_][\w.\-]*(?=:(?:\s|$))/y],
|
|
218
|
+
["lt", /\b(?:true|false|null|True|False|Null|TRUE|FALSE|NULL|yes|no|on|off)\b/y],
|
|
219
|
+
["num", /-?\b\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?\b/y],
|
|
220
|
+
["var", /[&*][A-Za-z_][\w.\-]*/y],
|
|
221
|
+
["ident", /[A-Za-z_][\w.\-]*/y],
|
|
222
|
+
["pun", /[:\-?[\]{},>|!]/y],
|
|
223
|
+
["ws", /\s+/y]
|
|
224
|
+
];
|
|
225
|
+
const tomlPats = [
|
|
226
|
+
["com", /#[^\n]*/y],
|
|
227
|
+
["sel", /^\[\[?[^\]\n]*\]\]?/my],
|
|
228
|
+
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
229
|
+
["str", /'[^'\n]*'/y],
|
|
230
|
+
["lt", /\b(?:true|false)\b/y],
|
|
231
|
+
["attr", /[A-Za-z_][\w.\-]*(?=[ \t]{0,2}=)/y],
|
|
232
|
+
["num", /[+-]?\b\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?\b/y],
|
|
233
|
+
["ident", /[A-Za-z_][\w.\-]*/y],
|
|
234
|
+
["pun", /[=[\]{},.:+\-]/y],
|
|
235
|
+
["ws", /\s+/y]
|
|
236
|
+
];
|
|
237
|
+
const diffPats = [
|
|
238
|
+
["com", /^(?:diff |index |similarity |rename |new file|deleted file|old mode|new mode|Binary |@@|---|\+\+\+)[^\n]*/my],
|
|
239
|
+
["str", /^\+[^\n]*/my],
|
|
240
|
+
["kw", /^-[^\n]*/my],
|
|
241
|
+
// `ws` FIRST, ahead of the context-line catch-all: the streaming checkpoint
|
|
242
|
+
// (hi-inc.ts) cuts a whitespace run in two just after a newline and re-matches
|
|
243
|
+
// `\s+` from there, so no other pattern may be able to start on whitespace —
|
|
244
|
+
// `[^\n]+` would happily swallow a line's indentation and re-tokenize the cut
|
|
245
|
+
// differently from a run that never stopped.
|
|
246
|
+
["ws", /\s+/y],
|
|
247
|
+
["txt", /[^\n]+/y]
|
|
248
|
+
];
|
|
249
|
+
const dockerPats = [
|
|
250
|
+
["com", /#[^\n]*/y],
|
|
251
|
+
["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
252
|
+
["str", /'[^'\n]*'/y],
|
|
253
|
+
["var", /\$\{[^}\n]*\}|\$\w+/y],
|
|
254
|
+
["num", /\b\d+\b/y],
|
|
255
|
+
["ident", /[A-Za-z_][\w.\-]*/y],
|
|
256
|
+
["pun", /[|&;<>(){}[\]=,:@/\\*+]/y],
|
|
257
|
+
["ws", /\s+/y]
|
|
258
|
+
];
|
|
259
|
+
const LANGS = /* @__PURE__ */ Object.create(null);
|
|
260
|
+
const REGISTERED = /* @__PURE__ */ new Set();
|
|
261
|
+
function def(names, pats, kw) {
|
|
262
|
+
const entry = kw === void 0 ? { pats } : { pats, kw };
|
|
263
|
+
for (const name of names.split(" ")) LANGS[name] = entry;
|
|
264
|
+
}
|
|
265
|
+
def("js javascript jsx", jsPats, KEYWORDS_JS);
|
|
266
|
+
def("ts tsx typescript", jsPats, KEYWORDS_TS);
|
|
267
|
+
def("rust rs", rustPats, KEYWORDS_RUST);
|
|
268
|
+
def("py python", pyPats, KEYWORDS_PY);
|
|
269
|
+
def("go", goPats, KEYWORDS_GO);
|
|
270
|
+
def("bash sh shell", bashPats, KEYWORDS_BASH);
|
|
271
|
+
def("json", jsonPats);
|
|
272
|
+
def("sql", sqlPats, KEYWORDS_SQL);
|
|
273
|
+
def("html xml", htmlPats);
|
|
274
|
+
def("css", cssPats);
|
|
275
|
+
def("java", cFamilyPats, KEYWORDS_JAVA);
|
|
276
|
+
def("c", cPats, KEYWORDS_C);
|
|
277
|
+
def("cpp c++", cPats, KEYWORDS_CPP);
|
|
278
|
+
def("cs csharp", cFamilyPats, KEYWORDS_CS);
|
|
279
|
+
def("swift", cFamilyPats, KEYWORDS_SWIFT);
|
|
280
|
+
def("kt kotlin", cFamilyPats, KEYWORDS_KOTLIN);
|
|
281
|
+
def("php", phpPats, KEYWORDS_PHP);
|
|
282
|
+
def("rb ruby", rubyPats, KEYWORDS_RUBY);
|
|
283
|
+
def("yaml yml", yamlPats);
|
|
284
|
+
def("toml", tomlPats);
|
|
285
|
+
def("diff", diffPats);
|
|
286
|
+
def("dockerfile", dockerPats, KEYWORDS_DOCKER);
|
|
287
|
+
const BUILTIN = Object.keys(LANGS);
|
|
288
|
+
const BUILTIN_ENTRIES = new Map(Object.entries(LANGS));
|
|
289
|
+
function __builtinLangs() {
|
|
290
|
+
return BUILTIN.slice();
|
|
291
|
+
}
|
|
292
|
+
function isRegisteredLang(lang) {
|
|
293
|
+
return REGISTERED.has(lang.toLowerCase());
|
|
294
|
+
}
|
|
295
|
+
function __resetLanguages() {
|
|
296
|
+
for (const k of Object.keys(LANGS)) delete LANGS[k];
|
|
297
|
+
for (const [k, v] of BUILTIN_ENTRIES) LANGS[k] = v;
|
|
298
|
+
REGISTERED.clear();
|
|
299
|
+
}
|
|
300
|
+
function hasLang(lang) {
|
|
301
|
+
return LANGS[lang.toLowerCase()] !== void 0;
|
|
302
|
+
}
|
|
149
303
|
function escapeHtml(s) {
|
|
150
304
|
const n = s.length;
|
|
151
305
|
let i = 0;
|
|
@@ -188,7 +342,7 @@ function stepHighlight(code, lang, state, chars, sink) {
|
|
|
188
342
|
const [cls, re] = pats[i];
|
|
189
343
|
re.lastIndex = pos;
|
|
190
344
|
const m = re.exec(code);
|
|
191
|
-
if (!m || m.index !== pos) continue;
|
|
345
|
+
if (!m || m.index !== pos || m[0].length === 0) continue;
|
|
192
346
|
const text = m[0];
|
|
193
347
|
const after = pos + text.length;
|
|
194
348
|
let finalCls = cls;
|
|
@@ -233,12 +387,79 @@ function highlight(code, lang) {
|
|
|
233
387
|
}
|
|
234
388
|
return state.out;
|
|
235
389
|
}
|
|
390
|
+
const TOKEN_CLASSES = /* @__PURE__ */ new Set([
|
|
391
|
+
"kw",
|
|
392
|
+
"str",
|
|
393
|
+
"rx",
|
|
394
|
+
"num",
|
|
395
|
+
"lt",
|
|
396
|
+
"com",
|
|
397
|
+
"fn",
|
|
398
|
+
"ty",
|
|
399
|
+
"mac",
|
|
400
|
+
"dec",
|
|
401
|
+
"attr",
|
|
402
|
+
"sel",
|
|
403
|
+
"tag",
|
|
404
|
+
"var",
|
|
405
|
+
"pun",
|
|
406
|
+
"txt",
|
|
407
|
+
"ident",
|
|
408
|
+
"ws"
|
|
409
|
+
]);
|
|
410
|
+
function registerLanguage(names, def_) {
|
|
411
|
+
const list = typeof names === "string" ? [names] : names;
|
|
412
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
413
|
+
throw new TypeError("registerLanguage: expected a language name or a non-empty array of names");
|
|
414
|
+
}
|
|
415
|
+
for (const name of list) {
|
|
416
|
+
if (typeof name !== "string" || name === "") {
|
|
417
|
+
throw new TypeError("registerLanguage: every language name must be a non-empty string");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (def_ === null || typeof def_ !== "object" || !Array.isArray(def_.pats) || def_.pats.length === 0) {
|
|
421
|
+
throw new TypeError("registerLanguage: `pats` must be a non-empty array of [token, regexp] pairs");
|
|
422
|
+
}
|
|
423
|
+
const pats = [];
|
|
424
|
+
for (let i = 0; i < def_.pats.length; i++) {
|
|
425
|
+
const pair = def_.pats[i];
|
|
426
|
+
if (!Array.isArray(pair) || pair.length < 2) {
|
|
427
|
+
throw new TypeError(`registerLanguage: pattern ${i} must be a [token, regexp] pair`);
|
|
428
|
+
}
|
|
429
|
+
const [cls, re] = pair;
|
|
430
|
+
if (typeof cls !== "string" || !TOKEN_CLASSES.has(cls)) {
|
|
431
|
+
throw new TypeError(
|
|
432
|
+
`registerLanguage: pattern ${i} has unknown token class ${JSON.stringify(cls)} \u2014 expected one of ${[...TOKEN_CLASSES].join(", ")}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (!(re instanceof RegExp) || !re.sticky) {
|
|
436
|
+
throw new TypeError(`registerLanguage: pattern ${i} (${cls}) must be a sticky RegExp (the \`y\` flag)`);
|
|
437
|
+
}
|
|
438
|
+
re.lastIndex = 0;
|
|
439
|
+
if (re.exec("") !== null) {
|
|
440
|
+
throw new TypeError(`registerLanguage: pattern ${i} (${cls}) matches the empty string`);
|
|
441
|
+
}
|
|
442
|
+
pats.push([cls, re]);
|
|
443
|
+
}
|
|
444
|
+
const kw = def_.kw === void 0 ? void 0 : new Set(def_.kw);
|
|
445
|
+
const entry = kw === void 0 ? { pats } : { pats, kw };
|
|
446
|
+
for (const name of list) {
|
|
447
|
+
const key = name.toLowerCase();
|
|
448
|
+
LANGS[key] = entry;
|
|
449
|
+
REGISTERED.add(key);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
236
452
|
function supportedLangs() {
|
|
237
453
|
return Object.keys(LANGS);
|
|
238
454
|
}
|
|
239
455
|
export {
|
|
456
|
+
__builtinLangs,
|
|
457
|
+
__resetLanguages,
|
|
240
458
|
escapeHtml,
|
|
459
|
+
hasLang,
|
|
241
460
|
highlight,
|
|
461
|
+
isRegisteredLang,
|
|
462
|
+
registerLanguage,
|
|
242
463
|
stepHighlight,
|
|
243
464
|
supportedLangs
|
|
244
465
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - BrookClient: owns one Web Worker + Rust parser per stream
|
|
6
6
|
* - BrookMarkdown: React component that subscribes to a BrookClient
|
|
7
7
|
* - Block / Patch / BlockKind types
|
|
8
|
-
* - highlight: optional in-house syntax highlighter
|
|
8
|
+
* - highlight / registerLanguage: optional in-house syntax highlighter
|
|
9
9
|
*
|
|
10
10
|
* Typical use (React + a Vite-like bundler):
|
|
11
11
|
*
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
export { BrookClient, BrookPool, getDefaultPool, sourceFingerprint } from "./client.js";
|
|
19
19
|
export type { PersistableSnapshot } from "./client.js";
|
|
20
20
|
export { BrookMarkdown, useBrookStream, useBrookMarkdownString } from "./react.js";
|
|
21
|
-
export { highlight, supportedLangs } from "./hi.js";
|
|
21
|
+
export { highlight, registerLanguage, supportedLangs } from "./hi.js";
|
|
22
|
+
export type { LanguageDef } from "./hi.js";
|
|
22
23
|
export { htmlToReact, parseTrustedHtml, safeUrl, wrapLink } from "./html-to-react.js";
|
|
23
|
-
export type { Block, BlockKind, BlockKindTag, BlockComponentProps, Components, Patch, FromWorker, ToWorker, WorkerLike, ParserConfig, Align, TableCell, TableData, HeadingData, CodeBlockData, MathBlockData, ListData, NestedBlock, ContainerData, Decorator, UrlTransform, BrookNode, } from "./types.js";
|
|
24
|
+
export type { Block, BlockKind, BlockKindTag, BlockComponentProps, Components, Patch, FromWorker, ToWorker, WorkerLike, ParserConfig, Align, TableCell, TableData, HeadingData, CodeBlockData, MathBlockData, ListData, NestedBlock, ContainerData, Decorator, UrlTransform, LinkClickInfo, BrookNode, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BrookClient, BrookPool, getDefaultPool, sourceFingerprint } from "./client.js";
|
|
2
2
|
import { BrookMarkdown, useBrookStream, useBrookMarkdownString } from "./react.js";
|
|
3
|
-
import { highlight, supportedLangs } from "./hi.js";
|
|
3
|
+
import { highlight, registerLanguage, supportedLangs } from "./hi.js";
|
|
4
4
|
import { htmlToReact, parseTrustedHtml, safeUrl, wrapLink } from "./html-to-react.js";
|
|
5
5
|
export {
|
|
6
6
|
BrookClient,
|
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
highlight,
|
|
11
11
|
htmlToReact,
|
|
12
12
|
parseTrustedHtml,
|
|
13
|
+
registerLanguage,
|
|
13
14
|
safeUrl,
|
|
14
15
|
sourceFingerprint,
|
|
15
16
|
supportedLangs,
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type MouseEvent as ReactMouseEvent } from "react";
|
|
1
2
|
import type { Block, BlockComponentProps, Components } from "./types.js";
|
|
2
3
|
import { BrookClient } from "./client.js";
|
|
3
|
-
import type { Decorator, ParserConfig, RenderMetricsHook, UrlTransform } from "./types-core.js";
|
|
4
|
+
import type { Decorator, LinkClickInfo, ParserConfig, RenderMetricsHook, UrlTransform } from "./types-core.js";
|
|
4
5
|
/**
|
|
5
6
|
* Render a streaming markdown document from a BrookClient. Each block is its
|
|
6
7
|
* own memoized React node keyed by its stable parser-assigned ID, so React
|
|
@@ -137,13 +138,23 @@ interface BrookMarkdownProps {
|
|
|
137
138
|
* the whole fence per chunk). The settled markup is byte-identical either way
|
|
138
139
|
* — only the tail's colours are provisional, and they may shift as bytes
|
|
139
140
|
* arrive (`"hello` is a stray quote plus an identifier until its closing quote
|
|
140
|
-
* lands).
|
|
141
|
+
* lands).
|
|
142
|
+
*
|
|
143
|
+
* - `true` / omitted — `"wavefront"`.
|
|
144
|
+
* - `"wavefront"` — the frozen prefix is coloured; the speculative tail (in
|
|
145
|
+
* practice the line being typed) renders as plain text until its line
|
|
146
|
+
* completes. The tail is one text node updated through its character data,
|
|
147
|
+
* which is what keeps the option's cost at the DOM near zero.
|
|
148
|
+
* - `"eager"` — colour the tail on every patch too, by rebuilding its span
|
|
149
|
+
* markup each time. Sub-line colour latency, ~all of the option's
|
|
150
|
+
* style/layout cost.
|
|
151
|
+
* - `false` — the pre-0.27 behaviour: plain body until close.
|
|
141
152
|
*
|
|
142
153
|
* No effect on SSR (the server renders closed blocks only), and none at all
|
|
143
154
|
* when `components.CodeBlock` / `components.pre` / `components.code` take over
|
|
144
155
|
* the block — an override bypasses the built-in highlighter entirely.
|
|
145
156
|
*/
|
|
146
|
-
streamingHighlight?: boolean;
|
|
157
|
+
streamingHighlight?: boolean | "wavefront" | "eager";
|
|
147
158
|
/**
|
|
148
159
|
* @internal TEST-ONLY. Turn off the incremental apply paths (the open code
|
|
149
160
|
* block's frozen/tail mirror and the open generic block's delta splice) so
|
|
@@ -214,6 +225,21 @@ interface BrookMarkdownProps {
|
|
|
214
225
|
* supplied. Guard with `if (!block) return <>{children}</>`.
|
|
215
226
|
*/
|
|
216
227
|
onBlockError?: (error: Error, info: BlockErrorInfo) => void;
|
|
228
|
+
/**
|
|
229
|
+
* Called when a rendered link is clicked — for in-app routing, analytics, or
|
|
230
|
+
* an "open citations in a drawer" affordance.
|
|
231
|
+
*
|
|
232
|
+
* DELEGATED: exactly ONE `onClick` sits on the `.brook-md` root and the anchor
|
|
233
|
+
* is resolved from the event target. No per-anchor prop is ever added, so this
|
|
234
|
+
* is invisible to the per-block memo — unlike `components` / `decorators`, a
|
|
235
|
+
* fresh closure each render re-renders NO blocks (hoisting is tidy, but costs
|
|
236
|
+
* you nothing here).
|
|
237
|
+
*
|
|
238
|
+
* The handler gets React's synthetic event, so `event.preventDefault()`
|
|
239
|
+
* cancels the navigation. A still-streaming anchor (`data-brook-pending`:
|
|
240
|
+
* label rendered, URL not yet arrived) is never reported.
|
|
241
|
+
*/
|
|
242
|
+
onLinkClick?: (event: ReactMouseEvent<HTMLElement>, link: LinkClickInfo) => void;
|
|
217
243
|
}
|
|
218
244
|
/** Context handed to {@link BrookMarkdownProps.onBlockError}. */
|
|
219
245
|
export interface BlockErrorInfo {
|
|
@@ -289,7 +315,7 @@ interface BlockViewProps {
|
|
|
289
315
|
virtualize?: boolean;
|
|
290
316
|
sanitize?: (html: string) => string;
|
|
291
317
|
childMemo?: boolean;
|
|
292
|
-
streamingHighlight?: boolean;
|
|
318
|
+
streamingHighlight?: boolean | "wavefront" | "eager";
|
|
293
319
|
/** @internal TEST-ONLY — see BrookMarkdownProps.__fullRebuild. */
|
|
294
320
|
__fullRebuild?: boolean;
|
|
295
321
|
onRenderMetrics?: RenderMetricsHook;
|
package/dist/react.js
CHANGED
|
@@ -64,7 +64,8 @@ function BrookMarkdownFromClient({
|
|
|
64
64
|
deferTail,
|
|
65
65
|
decorators,
|
|
66
66
|
urlTransform,
|
|
67
|
-
onBlockError
|
|
67
|
+
onBlockError,
|
|
68
|
+
onLinkClick
|
|
68
69
|
}) {
|
|
69
70
|
const blocks = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
70
71
|
useUnstablePropWarning("decorators", decorators);
|
|
@@ -84,15 +85,30 @@ function BrookMarkdownFromClient({
|
|
|
84
85
|
} : void 0,
|
|
85
86
|
[client, onRenderMetrics]
|
|
86
87
|
);
|
|
88
|
+
const rootRef = useRef(null);
|
|
89
|
+
const handleLinkClick = onLinkClick ? (e) => {
|
|
90
|
+
const root = rootRef.current;
|
|
91
|
+
const el = e.target;
|
|
92
|
+
if (!root || !el || typeof el.closest !== "function") return;
|
|
93
|
+
const a = el.closest("a[href]");
|
|
94
|
+
if (!a || !root.contains(a) || a.hasAttribute("data-brook-pending")) return;
|
|
95
|
+
onLinkClick(e, {
|
|
96
|
+
href: a.getAttribute("href") ?? "",
|
|
97
|
+
text: a.textContent ?? "",
|
|
98
|
+
element: a
|
|
99
|
+
});
|
|
100
|
+
} : void 0;
|
|
87
101
|
const rootClass = isDeferring ? className ? `brook-md brook-deferred ${className}` : "brook-md brook-deferred" : className ? `brook-md ${className}` : "brook-md";
|
|
88
102
|
return /* @__PURE__ */ jsxs(
|
|
89
103
|
"div",
|
|
90
104
|
{
|
|
105
|
+
ref: rootRef,
|
|
91
106
|
className: rootClass,
|
|
92
107
|
id,
|
|
93
108
|
role,
|
|
94
109
|
"aria-live": ariaLive,
|
|
95
110
|
"aria-atomic": ariaAtomic,
|
|
111
|
+
onClick: handleLinkClick,
|
|
96
112
|
children: [
|
|
97
113
|
rendered.map(
|
|
98
114
|
(b, i) => (
|
|
@@ -9,8 +9,9 @@ interface Props {
|
|
|
9
9
|
* as the highlight itself. Absent (blockData off) the HTML is decoded here.
|
|
10
10
|
*/
|
|
11
11
|
code?: string;
|
|
12
|
-
/** Highlight the block while it is still open. Default true
|
|
13
|
-
|
|
12
|
+
/** Highlight the block while it is still open. Default true (`"wavefront"`);
|
|
13
|
+
* `"eager"` colours the speculative tail on every patch; `false` opts out. */
|
|
14
|
+
streamingHighlight?: boolean | "wavefront" | "eager";
|
|
14
15
|
/**
|
|
15
16
|
* The block this markup came from, when the renderer is driven by the stream.
|
|
16
17
|
* Only used to apply the wire's `html_delta` to the PLAIN escaped body of an
|
|
@@ -3,7 +3,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
|
|
|
3
3
|
import { highlight } from "../hi.js";
|
|
4
4
|
import { highlightDeferred, highlightWithin } from "../hi-defer.js";
|
|
5
5
|
import { createInc, incHighlight, incSeed } from "../hi-inc.js";
|
|
6
|
-
import { newIncCode, paintIncCode } from "../splice.js";
|
|
6
|
+
import { incView, 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) {
|
|
@@ -21,6 +21,7 @@ function CodeBlockImpl({ html, open, code, streamingHighlight, block, __fullRebu
|
|
|
21
21
|
const lang = extractLang(html) || "text";
|
|
22
22
|
const text = useMemo(() => open ? "" : code ?? decodeText(html), [html, open, code]);
|
|
23
23
|
const streaming = open && streamingHighlight !== false;
|
|
24
|
+
const tailMode = streamingHighlight === "eager" ? "eager" : "wavefront";
|
|
24
25
|
const openText = useMemo(
|
|
25
26
|
() => streaming ? code ?? decodeText(html) : "",
|
|
26
27
|
[streaming, code, html]
|
|
@@ -48,8 +49,18 @@ function CodeBlockImpl({ html, open, code, streamingHighlight, block, __fullRebu
|
|
|
48
49
|
incRef.current = st;
|
|
49
50
|
}
|
|
50
51
|
const markup = st === null ? null : incHighlight(st, openText);
|
|
51
|
-
setInc(
|
|
52
|
-
|
|
52
|
+
setInc(
|
|
53
|
+
markup === null ? null : {
|
|
54
|
+
lang,
|
|
55
|
+
html: markup,
|
|
56
|
+
// `view` is only ever WRITTEN by the full-rebuild reference, which
|
|
57
|
+
// has to show the same thing the mirror paints or the parity fuzz
|
|
58
|
+
// would be comparing two different visual contracts. The mirrored
|
|
59
|
+
// path derives the tail itself, so it does not pay for this.
|
|
60
|
+
view: __fullRebuild ? incView(st, markup, tailMode) : markup
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}, [streaming, openText, lang, tailMode, __fullRebuild]);
|
|
53
64
|
const [slow, setSlow] = useState(null);
|
|
54
65
|
useEffect(() => {
|
|
55
66
|
if (!text || sync !== null) {
|
|
@@ -75,7 +86,7 @@ function CodeBlockImpl({ html, open, code, streamingHighlight, block, __fullRebu
|
|
|
75
86
|
}, [text, lang, sync]);
|
|
76
87
|
const settled = sync ?? (slow !== null && slow.text === text && slow.lang === lang ? slow.html : null);
|
|
77
88
|
const streamed = streaming && inc !== null && inc.lang === lang ? inc.html : null;
|
|
78
|
-
const highlighted = settled ?? streamed;
|
|
89
|
+
const highlighted = settled ?? (streamed === null ? null : inc.view);
|
|
79
90
|
const mirrored = settled === null && streamed !== null && !__fullRebuild;
|
|
80
91
|
useIsoLayoutEffect(() => {
|
|
81
92
|
if (!mirrored) {
|
|
@@ -85,14 +96,16 @@ function CodeBlockImpl({ html, open, code, streamingHighlight, block, __fullRebu
|
|
|
85
96
|
const node = codeRef.current;
|
|
86
97
|
const st = incRef.current;
|
|
87
98
|
if (node === null || st === null || streamed === null) return;
|
|
99
|
+
const markup = st.html;
|
|
100
|
+
if (markup === null) return;
|
|
88
101
|
let m = mirrorRef.current;
|
|
89
|
-
if (m === null || m.code !== node || m.lang !== lang) {
|
|
102
|
+
if (m === null || m.code !== node || m.lang !== lang || m.mode !== tailMode) {
|
|
90
103
|
node.innerHTML = "";
|
|
91
|
-
m = newIncCode(node, lang, st);
|
|
104
|
+
m = newIncCode(node, lang, st, tailMode);
|
|
92
105
|
mirrorRef.current = m;
|
|
93
106
|
}
|
|
94
|
-
if (!paintIncCode(m, st,
|
|
95
|
-
node.innerHTML =
|
|
107
|
+
if (!paintIncCode(m, st, markup)) {
|
|
108
|
+
node.innerHTML = incView(st, markup, tailMode);
|
|
96
109
|
mirrorRef.current = null;
|
|
97
110
|
}
|
|
98
111
|
});
|
package/dist/solid.js
CHANGED
package/dist/splice.d.ts
CHANGED
|
@@ -63,6 +63,41 @@ export declare function noteSplice(next: Block, prev: Block, keep: number): void
|
|
|
63
63
|
* @internal Renderer-only; not part of the public API.
|
|
64
64
|
*/
|
|
65
65
|
export declare function spliceKeep(from: Block, to: Block): number | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* How an open fence's SPECULATIVE TAIL is painted. The frozen prefix is painted
|
|
68
|
+
* the same way either way — appended once, never touched again.
|
|
69
|
+
*
|
|
70
|
+
* - `"wavefront"` (the default) — the tail is ONE TEXT NODE holding the plain
|
|
71
|
+
* source, updated per patch through its character data. No elements are
|
|
72
|
+
* created, no markup is parsed, and nothing before it is disturbed, so a patch
|
|
73
|
+
* costs the browser a text measure instead of a style/layout pass over a
|
|
74
|
+
* freshly built span tree. Colour therefore arrives at the checkpoint — in
|
|
75
|
+
* practice one source line behind the stream head.
|
|
76
|
+
* - `"eager"` — the tail is its highlighted MARKUP, re-parsed on every patch.
|
|
77
|
+
* Colour is immediate, at the cost of rebuilding a span-dense subtree per
|
|
78
|
+
* frame. This is what the mirror always did.
|
|
79
|
+
*
|
|
80
|
+
* Both settle to the same bytes: the tail is thrown away and re-derived at every
|
|
81
|
+
* patch regardless, and the close-time markup comes from the frozen prefix plus
|
|
82
|
+
* one final tokenizer run (see hi-inc.ts).
|
|
83
|
+
*/
|
|
84
|
+
export type TailMode = "wavefront" | "eager";
|
|
85
|
+
/**
|
|
86
|
+
* The markup an open fence's `<code>` holds under `mode` — what the mirror
|
|
87
|
+
* paints incrementally, and what a caller that cannot mirror (a rebuild, the
|
|
88
|
+
* test-only full-rebuild reference) has to write in one go so the two agree.
|
|
89
|
+
*
|
|
90
|
+
* For `"eager"` that is hi-inc's markup verbatim. For `"wavefront"` the frozen
|
|
91
|
+
* prefix keeps its spans and the tail is the escaped source, which is exactly
|
|
92
|
+
* what the tail TEXT NODE serializes to.
|
|
93
|
+
*
|
|
94
|
+
* The wavefront tail comes from `st.text`/`st.c` rather than from `markup`,
|
|
95
|
+
* because the plain source is not recoverable from the tail's markup without
|
|
96
|
+
* re-parsing it. That is only sound while `markup` is the very string this state
|
|
97
|
+
* just produced, so a stale pair is handed back untouched rather than paired
|
|
98
|
+
* with a tail it does not belong to.
|
|
99
|
+
*/
|
|
100
|
+
export declare function incView(st: IncState, markup: string, mode: TailMode): string;
|
|
66
101
|
/**
|
|
67
102
|
* The live `<code>` of an OPEN code block, split the way hi-inc splits its
|
|
68
103
|
* markup: a **frozen** run of children (proven immutable — appended once and
|
|
@@ -71,15 +106,17 @@ export declare function spliceKeep(from: Block, to: Block): number | undefined;
|
|
|
71
106
|
*
|
|
72
107
|
* The two regions are NOT wrapped in elements — `frozenEnd` is simply the last
|
|
73
108
|
* child that belongs to the frozen run — so the resulting `innerHTML` is
|
|
74
|
-
* byte-identical to the `code.innerHTML =
|
|
75
|
-
* *count* differs (a splice can leave two adjacent text nodes where a
|
|
76
|
-
* parse would have made one), which serializes the same and is exactly
|
|
77
|
-
* browser does for any streamed append.
|
|
109
|
+
* byte-identical to the `code.innerHTML = incView(...)` this replaces. Only the
|
|
110
|
+
* node *count* differs (a splice can leave two adjacent text nodes where a
|
|
111
|
+
* one-shot parse would have made one), which serializes the same and is exactly
|
|
112
|
+
* what a browser does for any streamed append.
|
|
78
113
|
*/
|
|
79
114
|
export interface IncCode {
|
|
80
115
|
code: Element;
|
|
81
116
|
/** The language the mirror was built for; a change invalidates it. */
|
|
82
117
|
lang: string;
|
|
118
|
+
/** How the tail is painted; fixed for the mirror's whole life. */
|
|
119
|
+
mode: TailMode;
|
|
83
120
|
/** Last child of the frozen run — everything after it is the tail. */
|
|
84
121
|
frozenEnd: ChildNode | null;
|
|
85
122
|
/** Chars of `IncState.frozenHtml` already mirrored into the DOM. */
|
|
@@ -93,21 +130,31 @@ export interface IncCode {
|
|
|
93
130
|
* else on the streaming path combined. */
|
|
94
131
|
frozenEnd0: ChildNode | null;
|
|
95
132
|
frozenLen0: number;
|
|
96
|
-
/**
|
|
133
|
+
/** What is on screen after `frozenEnd`, so an unchanged tail is not rewritten:
|
|
134
|
+
* the tail MARKUP in `"eager"` mode, the tail SOURCE in `"wavefront"`. */
|
|
97
135
|
tail: string;
|
|
136
|
+
/** `"wavefront"` only: the single text node carrying the tail source. Created
|
|
137
|
+
* once per mirror and then only ever written through its character data —
|
|
138
|
+
* that is the whole cost saving, so it is never replaced while it lives. */
|
|
139
|
+
tailText: Text | null;
|
|
98
140
|
}
|
|
99
141
|
/** A fresh, empty mirror for a `<code>` that has nothing painted into it yet. */
|
|
100
|
-
export declare function newIncCode(code: Element, lang: string, st: IncState): IncCode;
|
|
142
|
+
export declare function newIncCode(code: Element, lang: string, st: IncState, mode: TailMode): IncCode;
|
|
101
143
|
/**
|
|
102
144
|
* Mirror hi-inc's frozen/tail split into a live `<code>`: append whatever the
|
|
103
|
-
* frozen prefix settled since the last patch, then
|
|
104
|
-
*
|
|
105
|
-
*
|
|
145
|
+
* frozen prefix settled since the last patch, then update the speculative tail.
|
|
146
|
+
* Returns false when the mirror cannot be trusted (see the invariants below) so
|
|
147
|
+
* the caller falls back to a full node rebuild.
|
|
106
148
|
*
|
|
107
149
|
* Cost per patch is |newly frozen| + |tail|. The frozen term sums, across the
|
|
108
150
|
* whole stream, to one pass over the final markup; the tail is bounded by
|
|
109
|
-
* hi-inc's CAP. That is what makes an open fence linear at the DOM, not just
|
|
110
|
-
*
|
|
151
|
+
* hi-inc's CAP. That is what makes an open fence linear at the DOM, not just at
|
|
152
|
+
* the tokenizer.
|
|
153
|
+
*
|
|
154
|
+
* In `"wavefront"` mode the tail term is also the CHEAPEST shape the DOM has:
|
|
155
|
+
* one character-data write, no elements created and no markup parsed. Only a
|
|
156
|
+
* checkpoint advance parses anything, and then only the newly frozen slice — so
|
|
157
|
+
* span creation drops from (tail spans × patches) to one pass over the block.
|
|
111
158
|
*/
|
|
112
159
|
export declare function paintIncCode(ic: IncCode, st: IncState, markup: string): boolean;
|
|
113
160
|
/**
|