brookmd 0.29.0 → 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 +170 -1
- package/README.md +643 -30
- package/dist/block-props.js +8 -2
- package/dist/dom.d.ts +25 -3
- package/dist/dom.js +33 -8
- package/dist/element.d.ts +3 -0
- package/dist/element.js +22 -2
- package/dist/hi-inc.d.ts +28 -0
- package/dist/hi-inc.js +40 -16
- 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 +25 -3
- package/dist/renderers/CodeBlock.d.ts +3 -2
- package/dist/renderers/CodeBlock.js +29 -11
- 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/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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BrookClient } from "./client.js";
|
|
2
|
-
import type { Block, BlockComponentProps, Decorator, RenderMetricsHook, UrlTransform } from "./types-core.js";
|
|
2
|
+
import type { Block, BlockComponentProps, Decorator, LinkClickInfo, RenderMetricsHook, UrlTransform } from "./types-core.js";
|
|
3
3
|
/**
|
|
4
4
|
* Framework-neutral DOM renderer for a {@link BrookClient}. Mounts the streaming
|
|
5
5
|
* document into a container and keeps it in sync via direct DOM mutation,
|
|
@@ -37,6 +37,7 @@ export interface MountHandle {
|
|
|
37
37
|
*/
|
|
38
38
|
openBlockId(): number | null;
|
|
39
39
|
}
|
|
40
|
+
export type { LinkClickInfo };
|
|
40
41
|
export type DomBlockComponent = (props: BlockComponentProps) => HTMLElement | string;
|
|
41
42
|
/** Override map: capitalized block-kind / component-tag keys only. */
|
|
42
43
|
export type DomComponents = Record<string, DomBlockComponent>;
|
|
@@ -72,9 +73,19 @@ export interface MountOptions {
|
|
|
72
73
|
* An open block keeps a frozen prefix and re-tokenizes only its tail on each
|
|
73
74
|
* patch, so this stays linear in the block's size. The settled markup is
|
|
74
75
|
* byte-identical either way — only the tail's colours are provisional, and
|
|
75
|
-
* they may shift as bytes arrive.
|
|
76
|
+
* they may shift as bytes arrive.
|
|
77
|
+
*
|
|
78
|
+
* - `true` / omitted — `"wavefront"`.
|
|
79
|
+
* - `"wavefront"` — the frozen prefix is coloured; the speculative tail (in
|
|
80
|
+
* practice the line being typed) renders as plain text until its line
|
|
81
|
+
* completes. The tail is one text node updated through its character data,
|
|
82
|
+
* which is what keeps the option's cost at the DOM near zero.
|
|
83
|
+
* - `"eager"` — colour the tail on every patch too, by rebuilding its span
|
|
84
|
+
* markup each time. Sub-line colour latency, ~all of the option's
|
|
85
|
+
* style/layout cost.
|
|
86
|
+
* - `false` — the pre-0.27 behaviour: plain body until the fence closes.
|
|
76
87
|
*/
|
|
77
|
-
streamingHighlight?: boolean;
|
|
88
|
+
streamingHighlight?: boolean | "wavefront" | "eager";
|
|
78
89
|
/** Coalesce patches into one DOM write per animation frame. Default true. */
|
|
79
90
|
batch?: boolean;
|
|
80
91
|
/**
|
|
@@ -107,6 +118,17 @@ export interface MountOptions {
|
|
|
107
118
|
* scheme. O(1) per attribute.
|
|
108
119
|
*/
|
|
109
120
|
urlTransform?: UrlTransform;
|
|
121
|
+
/**
|
|
122
|
+
* Called when a rendered link is clicked (parity with the React `onLinkClick`
|
|
123
|
+
* prop). Exactly ONE `click` listener is added to the renderer root and the
|
|
124
|
+
* anchor is resolved from the event target — never a listener per anchor, so
|
|
125
|
+
* this costs nothing per block and the streaming path is untouched. The
|
|
126
|
+
* listener is removed by {@link MountHandle.destroy}.
|
|
127
|
+
*
|
|
128
|
+
* `event.preventDefault()` cancels the navigation. A still-streaming anchor
|
|
129
|
+
* (`data-brook-pending`, no href yet) is never reported.
|
|
130
|
+
*/
|
|
131
|
+
onLinkClick?: (event: MouseEvent, link: LinkClickInfo) => void;
|
|
110
132
|
/** Appended to the root's `className` (the `brook-md` class is always present). */
|
|
111
133
|
className?: string;
|
|
112
134
|
/** Set on the root element. */
|
package/dist/dom.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { highlightDeferred } from "./hi-defer.js";
|
|
2
2
|
import { createInc, incHighlight, incSeed } from "./hi-inc.js";
|
|
3
3
|
import { morph } from "./morph.js";
|
|
4
|
-
import { newIncCode, paintIncCode, spliceHtml, spliceKeep } from "./splice.js";
|
|
4
|
+
import { incView, newIncCode, paintIncCode, spliceHtml, spliceKeep } from "./splice.js";
|
|
5
5
|
import { blockProps, extractLang } from "./block-props.js";
|
|
6
6
|
import { decorateSegments } from "./decorate.js";
|
|
7
7
|
import { safeUrl } from "./url-safety.js";
|
|
@@ -28,18 +28,26 @@ const INTRINSIC_PX = {
|
|
|
28
28
|
Html: 80,
|
|
29
29
|
Component: 120
|
|
30
30
|
};
|
|
31
|
+
function linkFromEvent(target, root) {
|
|
32
|
+
const el = target;
|
|
33
|
+
if (!el || typeof el.closest !== "function") return null;
|
|
34
|
+
const a = el.closest("a[href]");
|
|
35
|
+
if (!a || !root.contains(a) || a.hasAttribute("data-brook-pending")) return null;
|
|
36
|
+
return a;
|
|
37
|
+
}
|
|
31
38
|
function mountBrookMarkdown(client, container, options = {}) {
|
|
32
39
|
if (typeof document === "undefined") {
|
|
33
40
|
throw new Error("mountBrookMarkdown is browser-only; call it after the DOM exists.");
|
|
34
41
|
}
|
|
35
42
|
const components = options.components && Object.keys(options.components).length > 0 ? options.components : void 0;
|
|
36
|
-
const { sanitize, virtualize, stickToBottom, onRenderMetrics } = options;
|
|
43
|
+
const { sanitize, virtualize, stickToBottom, onRenderMetrics, onLinkClick } = options;
|
|
37
44
|
const decorators = options.decorators && options.decorators.length > 0 ? options.decorators : void 0;
|
|
38
45
|
const urlTransform = options.urlTransform;
|
|
39
46
|
const hasInlineTransforms = !!decorators || !!urlTransform;
|
|
40
47
|
const hasPerf = typeof performance !== "undefined";
|
|
41
48
|
const highlightCode = options.highlightCode !== false && !components?.CodeBlock;
|
|
42
49
|
const streamingHighlight = options.streamingHighlight !== false;
|
|
50
|
+
const tailMode = options.streamingHighlight === "eager" ? "eager" : "wavefront";
|
|
43
51
|
const batch = options.batch !== false && typeof requestAnimationFrame === "function";
|
|
44
52
|
const morphOpenBlocks = options.morphOpenBlocks === true;
|
|
45
53
|
const fullRebuild = options.__fullRebuild === true;
|
|
@@ -58,6 +66,16 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
58
66
|
anchor.style.scrollSnapAlign = "end";
|
|
59
67
|
root.appendChild(anchor);
|
|
60
68
|
}
|
|
69
|
+
const linkClickListener = onLinkClick ? (ev) => {
|
|
70
|
+
const a = linkFromEvent(ev.target, root);
|
|
71
|
+
if (!a) return;
|
|
72
|
+
onLinkClick(ev, {
|
|
73
|
+
href: a.getAttribute("href") ?? "",
|
|
74
|
+
text: a.textContent ?? "",
|
|
75
|
+
element: a
|
|
76
|
+
});
|
|
77
|
+
} : null;
|
|
78
|
+
if (linkClickListener) root.addEventListener("click", linkClickListener);
|
|
61
79
|
const mounted = /* @__PURE__ */ new Map();
|
|
62
80
|
let order = [];
|
|
63
81
|
let dead = false;
|
|
@@ -510,10 +528,11 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
510
528
|
}
|
|
511
529
|
if (inc !== void 0) openMarkup = incHighlight(inc, codeText(b));
|
|
512
530
|
}
|
|
531
|
+
const openView = openMarkup === null || mb.inc === void 0 ? openMarkup : incView(mb.inc, openMarkup, tailMode);
|
|
513
532
|
const seed = !b.open && mb.inc !== void 0 ? incSeed(mb.inc, text, lang) : void 0;
|
|
514
533
|
const run = text ? highlightDeferred(text, lang, seed) : null;
|
|
515
534
|
if (!b.open) mb.inc = void 0;
|
|
516
|
-
const highlighted =
|
|
535
|
+
const highlighted = openView ?? (run ? run.html : null);
|
|
517
536
|
const block = document.createElement("div");
|
|
518
537
|
block.className = "brook-code-block" + (b.open ? " brook-streaming" : "");
|
|
519
538
|
const header = document.createElement("div");
|
|
@@ -572,9 +591,9 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
572
591
|
pre.setAttribute("role", "region");
|
|
573
592
|
pre.setAttribute("aria-label", `${lang} code`);
|
|
574
593
|
const code = document.createElement("code");
|
|
575
|
-
const ic = newIncCode(code, lang, st);
|
|
594
|
+
const ic = newIncCode(code, lang, st, tailMode);
|
|
576
595
|
if (paintIncCode(ic, st, markup)) mb.codeInc = ic;
|
|
577
|
-
else code.innerHTML = markup;
|
|
596
|
+
else code.innerHTML = incView(st, markup, tailMode);
|
|
578
597
|
pre.appendChild(code);
|
|
579
598
|
return pre;
|
|
580
599
|
}
|
|
@@ -680,6 +699,7 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
680
699
|
frame = 0;
|
|
681
700
|
}
|
|
682
701
|
unsubscribe();
|
|
702
|
+
if (linkClickListener) root.removeEventListener("click", linkClickListener);
|
|
683
703
|
for (const mb of mounted.values()) {
|
|
684
704
|
if (mb.highlight) {
|
|
685
705
|
mb.highlight.cancel();
|
|
@@ -776,9 +796,14 @@ function codeText(b) {
|
|
|
776
796
|
return typeof data?.code === "string" ? data.code : decodeCodeText(b.html);
|
|
777
797
|
}
|
|
778
798
|
function decodeCodeText(html) {
|
|
779
|
-
const
|
|
780
|
-
if (
|
|
781
|
-
|
|
799
|
+
const open = html.indexOf("<pre><code");
|
|
800
|
+
if (open < 0) return "";
|
|
801
|
+
const start = html.indexOf(">", open + 10);
|
|
802
|
+
if (start < 0) return "";
|
|
803
|
+
const end = html.indexOf("</code></pre>", start + 1);
|
|
804
|
+
if (end < 0) return "";
|
|
805
|
+
const body = html.slice(start + 1, end);
|
|
806
|
+
return body.indexOf("&") < 0 ? body : body.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
782
807
|
}
|
|
783
808
|
const CONTAINER_ATTR_RE = /([a-zA-Z][a-zA-Z0-9-]*)="([^"]*)"/g;
|
|
784
809
|
function openTagOf(html) {
|
package/dist/element.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LinkClickInfo } from "./dom.js";
|
|
1
2
|
/**
|
|
2
3
|
* `<brook-markdown>` custom element — thin lifecycle glue over
|
|
3
4
|
* {@link mountBrookMarkdown}. It owns no diffing: connect mounts the DOM
|
|
@@ -17,4 +18,6 @@
|
|
|
17
18
|
* browser code.
|
|
18
19
|
*/
|
|
19
20
|
export declare function parseTriBool(value: string | null): boolean | undefined;
|
|
21
|
+
/** The `onLinkClick` handler shape, mirroring `MountOptions.onLinkClick`. */
|
|
22
|
+
export type LinkClickHandler = (event: MouseEvent, link: LinkClickInfo) => void;
|
|
20
23
|
export declare function defineBrookMarkdown(tag?: string): void;
|
package/dist/element.js
CHANGED
|
@@ -23,17 +23,19 @@ const CONFIG_ATTRS = [
|
|
|
23
23
|
"block-html",
|
|
24
24
|
"retain-committed-html"
|
|
25
25
|
];
|
|
26
|
+
const MOUNT_ATTRS = ["stick-to-bottom", "virtualize"];
|
|
26
27
|
function defineBrookMarkdown(tag = "brook-markdown") {
|
|
27
28
|
if (typeof customElements === "undefined") return;
|
|
28
29
|
if (customElements.get(tag)) return;
|
|
29
30
|
class BrookMarkdownElement extends HTMLElement {
|
|
30
31
|
static get observedAttributes() {
|
|
31
|
-
return ["markdown", "src", "component-tags", "allow-schemes", ...CONFIG_ATTRS];
|
|
32
|
+
return ["markdown", "src", "component-tags", "allow-schemes", ...MOUNT_ATTRS, ...CONFIG_ATTRS];
|
|
32
33
|
}
|
|
33
34
|
#client = null;
|
|
34
35
|
#ownsClient = false;
|
|
35
36
|
#components = void 0;
|
|
36
37
|
#sanitize = void 0;
|
|
38
|
+
#onLinkClick = void 0;
|
|
37
39
|
#handle = null;
|
|
38
40
|
#connected = false;
|
|
39
41
|
// In-flight `src` fetch supersession. A self-owned client is REUSED across
|
|
@@ -71,6 +73,14 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
71
73
|
this.#sanitize = value;
|
|
72
74
|
if (this.#connected) this.#remount();
|
|
73
75
|
}
|
|
76
|
+
get onLinkClick() {
|
|
77
|
+
return this.#onLinkClick;
|
|
78
|
+
}
|
|
79
|
+
set onLinkClick(value) {
|
|
80
|
+
if (value === this.#onLinkClick) return;
|
|
81
|
+
this.#onLinkClick = value;
|
|
82
|
+
if (this.#connected) this.#remount();
|
|
83
|
+
}
|
|
74
84
|
// --- Self-owned-client methods -------------------------------------------
|
|
75
85
|
append(chunk) {
|
|
76
86
|
this.#cancelSrcStream();
|
|
@@ -95,6 +105,7 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
95
105
|
this.#upgradeProperty("client");
|
|
96
106
|
this.#upgradeProperty("components");
|
|
97
107
|
this.#upgradeProperty("sanitize");
|
|
108
|
+
this.#upgradeProperty("onLinkClick");
|
|
98
109
|
this.#mountIfReady();
|
|
99
110
|
if (!this.#client || this.#ownsClient) {
|
|
100
111
|
this.#resolveInitialContent();
|
|
@@ -109,6 +120,10 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
109
120
|
}
|
|
110
121
|
return;
|
|
111
122
|
}
|
|
123
|
+
if (MOUNT_ATTRS.includes(name)) {
|
|
124
|
+
this.#remount();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
112
127
|
if (this.#client && !this.#ownsClient) {
|
|
113
128
|
console.warn(
|
|
114
129
|
"<brook-markdown>: config attributes are ignored while a caller-owned `client` is set (ParserConfig is immutable per stream)."
|
|
@@ -194,7 +209,12 @@ function defineBrookMarkdown(tag = "brook-markdown") {
|
|
|
194
209
|
if (!this.#connected || !this.#client || this.#handle) return;
|
|
195
210
|
this.#handle = mountBrookMarkdown(this.#client, this, {
|
|
196
211
|
components: this.#components,
|
|
197
|
-
sanitize: this.#sanitize
|
|
212
|
+
sanitize: this.#sanitize,
|
|
213
|
+
onLinkClick: this.#onLinkClick,
|
|
214
|
+
// Renderer options read fresh on every mount (MOUNT_ATTRS remounts on a
|
|
215
|
+
// change); `undefined` = absent = the renderer's own default.
|
|
216
|
+
stickToBottom: parseTriBool(this.getAttribute("stick-to-bottom")),
|
|
217
|
+
virtualize: parseTriBool(this.getAttribute("virtualize"))
|
|
198
218
|
});
|
|
199
219
|
}
|
|
200
220
|
// Destroy the current mount and remount against the current client+options.
|
package/dist/hi-inc.d.ts
CHANGED
|
@@ -49,6 +49,19 @@ export interface OpenerScanState {
|
|
|
49
49
|
export interface IncState {
|
|
50
50
|
/** The language key this state's tables were chosen for. */
|
|
51
51
|
readonly lang: string;
|
|
52
|
+
/** The unbounded openers of `lang` — resolved once, at {@link createInc}. */
|
|
53
|
+
readonly openers: Opener[];
|
|
54
|
+
/**
|
|
55
|
+
* May this state freeze a prefix at all?
|
|
56
|
+
*
|
|
57
|
+
* False for a language added through `registerLanguage`: the checkpoint rule
|
|
58
|
+
* is derived from knowing which of a table's forms can run past a newline, and
|
|
59
|
+
* a caller-supplied table does not say. Such a block is re-tokenized from the
|
|
60
|
+
* top on each patch (the {@link CAP} bound still applies) and freezes nothing,
|
|
61
|
+
* which is the one setting that is safe for ANY pattern list — with `c` at 0
|
|
62
|
+
* there is no frozen byte to be wrong, and the close-time run is unseeded.
|
|
63
|
+
*/
|
|
64
|
+
readonly freeze: boolean;
|
|
52
65
|
/** Settled through here: `[0, c)` of the source will never re-tokenize. */
|
|
53
66
|
c: number;
|
|
54
67
|
/** Markup for `[0, c)`. Always a byte-prefix of the block's final markup. */
|
|
@@ -90,6 +103,21 @@ export interface IncState {
|
|
|
90
103
|
sealed: boolean;
|
|
91
104
|
/** The source last fed in, for the append/revision guard. */
|
|
92
105
|
text: string;
|
|
106
|
+
/**
|
|
107
|
+
* `text.slice(0, c + GAP)` — the settled source the frozen markup was derived
|
|
108
|
+
* from, and {@link adopt}'s whole question in one string: a revision keeps the
|
|
109
|
+
* prefix iff the new text still STARTS WITH this.
|
|
110
|
+
*
|
|
111
|
+
* Cached rather than re-sliced because it changes only when `c` does (once per
|
|
112
|
+
* checkpoint, so once per source line) while the question is asked on every
|
|
113
|
+
* patch. Asking it as `text.startsWith(pfx)` hands the compare to the engine's
|
|
114
|
+
* native string compare; the char-by-char loop it replaces was, on a streamed
|
|
115
|
+
* 32 KB fence, 373 ms of the 480 ms this module spent in total — ten times
|
|
116
|
+
* what the same question costs natively.
|
|
117
|
+
*/
|
|
118
|
+
pfx: string;
|
|
119
|
+
/** The same, for the checkpoint BEFORE `c` — {@link adopt}'s second question. */
|
|
120
|
+
pfx0: string;
|
|
93
121
|
/** Escaped source for `[c, plainUpto)` — the plain tail, extended in place. */
|
|
94
122
|
plain: string;
|
|
95
123
|
plainFrom: number;
|
package/dist/hi-inc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml, stepHighlight } from "./hi.js";
|
|
1
|
+
import { escapeHtml, hasLang, isRegisteredLang, stepHighlight } from "./hi.js";
|
|
2
2
|
const GAP = 3;
|
|
3
3
|
const CAP = 8192;
|
|
4
4
|
const CLIFF = 5e4;
|
|
@@ -43,6 +43,8 @@ const CSS_OPENERS = [
|
|
|
43
43
|
{ re: /"/y, cls: "str", end: '"', pred: { k: "char", ch: '"' } },
|
|
44
44
|
{ re: /'/y, cls: "str", end: "'", pred: { k: "char", ch: "'" } }
|
|
45
45
|
];
|
|
46
|
+
const C_OPENERS = [BLOCK_COMMENT];
|
|
47
|
+
const NO_OPENERS = [];
|
|
46
48
|
const OPENERS = {
|
|
47
49
|
js: JS_OPENERS,
|
|
48
50
|
javascript: JS_OPENERS,
|
|
@@ -62,14 +64,34 @@ const OPENERS = {
|
|
|
62
64
|
sql: SQL_OPENERS,
|
|
63
65
|
html: HTML_OPENERS,
|
|
64
66
|
xml: HTML_OPENERS,
|
|
65
|
-
css: CSS_OPENERS
|
|
67
|
+
css: CSS_OPENERS,
|
|
68
|
+
java: C_OPENERS,
|
|
69
|
+
c: C_OPENERS,
|
|
70
|
+
cpp: C_OPENERS,
|
|
71
|
+
"c++": C_OPENERS,
|
|
72
|
+
cs: C_OPENERS,
|
|
73
|
+
csharp: C_OPENERS,
|
|
74
|
+
swift: C_OPENERS,
|
|
75
|
+
kt: C_OPENERS,
|
|
76
|
+
kotlin: C_OPENERS,
|
|
77
|
+
php: C_OPENERS,
|
|
78
|
+
rb: NO_OPENERS,
|
|
79
|
+
ruby: NO_OPENERS,
|
|
80
|
+
yaml: NO_OPENERS,
|
|
81
|
+
yml: NO_OPENERS,
|
|
82
|
+
toml: NO_OPENERS,
|
|
83
|
+
diff: NO_OPENERS,
|
|
84
|
+
dockerfile: NO_OPENERS
|
|
66
85
|
};
|
|
67
86
|
const GT_CHECKPOINT = /* @__PURE__ */ new Set(["html", "xml"]);
|
|
68
87
|
function createInc(lang) {
|
|
69
88
|
const key = lang.toLowerCase();
|
|
70
|
-
|
|
89
|
+
const known = Object.prototype.hasOwnProperty.call(OPENERS, key) && !isRegisteredLang(key);
|
|
90
|
+
if (!known && !hasLang(key)) return null;
|
|
71
91
|
return {
|
|
72
92
|
lang: key,
|
|
93
|
+
openers: known ? OPENERS[key] : NO_OPENERS,
|
|
94
|
+
freeze: known,
|
|
73
95
|
c: 0,
|
|
74
96
|
frozenHtml: "",
|
|
75
97
|
frozenRev: 0,
|
|
@@ -80,29 +102,27 @@ function createInc(lang) {
|
|
|
80
102
|
scan: { n: 0, f: false },
|
|
81
103
|
sealed: false,
|
|
82
104
|
text: "",
|
|
105
|
+
pfx: "",
|
|
106
|
+
pfx0: "",
|
|
83
107
|
plain: "",
|
|
84
108
|
plainFrom: 0,
|
|
85
109
|
plainUpto: 0,
|
|
86
110
|
html: null
|
|
87
111
|
};
|
|
88
112
|
}
|
|
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) {
|
|
113
|
+
function adopt(st, text) {
|
|
114
|
+
if (st.c > 0 && text.startsWith(st.pfx)) {
|
|
97
115
|
dropTail(st);
|
|
98
116
|
return true;
|
|
99
117
|
}
|
|
100
|
-
if (st.c0 > 0 &&
|
|
118
|
+
if (st.c0 > 0 && text.startsWith(st.pfx0)) {
|
|
101
119
|
st.frozenHtml = st.frozenHtml.slice(0, st.frozenLen0);
|
|
102
120
|
st.frozenRev++;
|
|
103
121
|
st.frozenCut = st.frozenLen0;
|
|
104
122
|
st.c = st.c0;
|
|
123
|
+
st.pfx = st.pfx0;
|
|
105
124
|
st.c0 = 0;
|
|
125
|
+
st.pfx0 = "";
|
|
106
126
|
st.frozenLen0 = 0;
|
|
107
127
|
dropTail(st);
|
|
108
128
|
return true;
|
|
@@ -125,6 +145,8 @@ function reset(st) {
|
|
|
125
145
|
st.frozenCut = 0;
|
|
126
146
|
st.c0 = 0;
|
|
127
147
|
st.frozenLen0 = 0;
|
|
148
|
+
st.pfx = "";
|
|
149
|
+
st.pfx0 = "";
|
|
128
150
|
st.opener = null;
|
|
129
151
|
st.scan = { n: 0, f: false };
|
|
130
152
|
st.sealed = false;
|
|
@@ -136,11 +158,10 @@ function reset(st) {
|
|
|
136
158
|
}
|
|
137
159
|
function incHighlight(st, text) {
|
|
138
160
|
if (st.text === text) return st.html;
|
|
139
|
-
const
|
|
140
|
-
const appended = d === st.text.length && text.length > st.text.length;
|
|
161
|
+
const appended = text.length > st.text.length && text.startsWith(st.text);
|
|
141
162
|
let from = st.text.length;
|
|
142
163
|
if (!appended) {
|
|
143
|
-
if (!adopt(st,
|
|
164
|
+
if (!adopt(st, text)) reset(st);
|
|
144
165
|
from = 0;
|
|
145
166
|
}
|
|
146
167
|
st.text = text;
|
|
@@ -192,7 +213,7 @@ function plainTail(st, text) {
|
|
|
192
213
|
return st.plain;
|
|
193
214
|
}
|
|
194
215
|
function rescan(st, text) {
|
|
195
|
-
const openers =
|
|
216
|
+
const openers = st.openers;
|
|
196
217
|
const gt = GT_CHECKPOINT.has(st.lang);
|
|
197
218
|
const limit = text.length - GAP;
|
|
198
219
|
let cp = -1;
|
|
@@ -202,6 +223,7 @@ function rescan(st, text) {
|
|
|
202
223
|
let liveLen = 0;
|
|
203
224
|
const sink = (cls, start, end, outLen) => {
|
|
204
225
|
if (liveAt >= 0) return;
|
|
226
|
+
if (!st.freeze) return;
|
|
205
227
|
for (let i = 0; i < openers.length; i++) {
|
|
206
228
|
const op = openers[i];
|
|
207
229
|
op.re.lastIndex = start;
|
|
@@ -236,9 +258,11 @@ function rescan(st, text) {
|
|
|
236
258
|
const full = st.frozenHtml + state.out;
|
|
237
259
|
if (cp > st.c) {
|
|
238
260
|
st.c0 = st.c;
|
|
261
|
+
st.pfx0 = st.pfx;
|
|
239
262
|
st.frozenLen0 = st.frozenHtml.length;
|
|
240
263
|
st.frozenHtml += state.out.slice(0, cpOut);
|
|
241
264
|
st.c = cp;
|
|
265
|
+
st.pfx = text.slice(0, cp + GAP);
|
|
242
266
|
}
|
|
243
267
|
const found = liveOp;
|
|
244
268
|
if (found === null) {
|
package/dist/hi.d.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-house syntax highlighter. Native RegExp only
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* In-house syntax highlighter. Native RegExp only. Covers the languages an LLM
|
|
3
|
+
* typically emits: js/ts/tsx/jsx, rust, python, go, bash, json, html, css, sql,
|
|
4
|
+
* yaml, toml, diff, java, c/c++, c#, php, ruby, swift, kotlin and dockerfile.
|
|
5
|
+
* {@link registerLanguage} adds more at runtime; anything still unknown falls
|
|
6
|
+
* through to plain escaped text.
|
|
6
7
|
*
|
|
7
8
|
* Highlighting is per-block, runs once when the block closes. We never
|
|
8
9
|
* highlight an open (streaming) block, which avoids re-highlighting the same
|
|
9
10
|
* code on every chunk — the main perf win for streaming code.
|
|
10
11
|
*/
|
|
12
|
+
/** @internal Test-only: the built-in languages, without registered additions. */
|
|
13
|
+
export declare function __builtinLangs(): string[];
|
|
14
|
+
/** @internal Whether `lang` has a tokenizer table (built-in or registered). */
|
|
15
|
+
/**
|
|
16
|
+
* Whether `lang` currently resolves to a table supplied through
|
|
17
|
+
* {@link registerLanguage} — including a built-in name the caller replaced. The
|
|
18
|
+
* streaming highlighter asks this so a replaced built-in loses the frozen-prefix
|
|
19
|
+
* mode that was derived from the ORIGINAL table's forms.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isRegisteredLang(lang: string): boolean;
|
|
22
|
+
/** @internal Test-only: restore the built-in tables and forget registrations. */
|
|
23
|
+
export declare function __resetLanguages(): void;
|
|
24
|
+
export declare function hasLang(lang: string): boolean;
|
|
11
25
|
export declare function escapeHtml(s: string): string;
|
|
12
26
|
/**
|
|
13
27
|
* The resumable tokenizer's cursor: `pos` is the next source index to consume,
|
|
@@ -50,4 +64,48 @@ export type TokenSink = (cls: string, start: number, end: number, outLen: number
|
|
|
50
64
|
*/
|
|
51
65
|
export declare function stepHighlight(code: string, lang: string, state: HighlightState, chars: number, sink?: TokenSink): boolean;
|
|
52
66
|
export declare function highlight(code: string, lang: string): string;
|
|
67
|
+
/** A language table for {@link registerLanguage}. */
|
|
68
|
+
export interface LanguageDef {
|
|
69
|
+
/**
|
|
70
|
+
* Ordered `[token class, sticky regex]` pairs. At each cursor position the
|
|
71
|
+
* FIRST pattern that matches wins, so put the longer forms first. Every regex
|
|
72
|
+
* must carry the `y` flag; it is matched against the whole source with
|
|
73
|
+
* `lastIndex` at the cursor, so `^`/`$` (with `m`) and lookaheads work, and
|
|
74
|
+
* `\b` sees the real neighbouring characters.
|
|
75
|
+
*/
|
|
76
|
+
pats: Array<[token: string, re: RegExp]>;
|
|
77
|
+
/** Words an `ident` token is promoted to `kw` for. Stored as a `Set`. */
|
|
78
|
+
kw?: Iterable<string>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Teach {@link highlight} a language, under one name or several aliases.
|
|
82
|
+
*
|
|
83
|
+
* registerLanguage(["hcl", "tf"], {
|
|
84
|
+
* pats: [
|
|
85
|
+
* ["com", /#.+/y],
|
|
86
|
+
* ["str", /"(?:\\.|[^"\\\n])*"/y],
|
|
87
|
+
* ["num", /\b\d+(?:\.\d+)?/y],
|
|
88
|
+
* ["ident", /\w+/y],
|
|
89
|
+
* ["pun", /[=[\]{}(),.]/y],
|
|
90
|
+
* ["ws", /\s+/y],
|
|
91
|
+
* ],
|
|
92
|
+
* kw: ["resource", "variable", "module", "output", "true", "false"],
|
|
93
|
+
* });
|
|
94
|
+
*
|
|
95
|
+
* Names are lower-cased, and registering a name that already exists REPLACES
|
|
96
|
+
* its table — including a built-in one, which is how a caller retunes `yaml` or
|
|
97
|
+
* `json` rather than living with this module's taste.
|
|
98
|
+
*
|
|
99
|
+
* Throws a `TypeError`, naming the offending pattern index, for a regex that is
|
|
100
|
+
* not sticky or a token class that has no style.
|
|
101
|
+
*
|
|
102
|
+
* A registered language highlights exactly like a built-in one when its block
|
|
103
|
+
* closes. While the block is still STREAMING it is re-tokenized from the top on
|
|
104
|
+
* each patch instead of growing a frozen prefix: the frozen-prefix rule needs to
|
|
105
|
+
* know which of a table's forms can run past a newline (a block comment, a
|
|
106
|
+
* multi-line string), and a caller-supplied table does not say. The size cap in
|
|
107
|
+
* the streaming path bounds that work; nothing about the settled markup differs.
|
|
108
|
+
*/
|
|
109
|
+
export declare function registerLanguage(names: string | string[], def_: LanguageDef): void;
|
|
110
|
+
/** Every language {@link highlight} knows, built-in and registered alike. */
|
|
53
111
|
export declare function supportedLangs(): string[];
|