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.
@@ -2,8 +2,14 @@ function decodeEntities(s) {
2
2
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
3
3
  }
4
4
  function decodeCodeText(html) {
5
- const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
6
- return m ? decodeEntities(m[1]) : "";
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. Set `false` for the pre-0.27 behaviour.
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 = openMarkup ?? (run ? run.html : null);
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 m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
780
- if (!m) return "";
781
- return m[1].replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/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
- if (!Object.prototype.hasOwnProperty.call(OPENERS, key)) return null;
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 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) {
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 && d >= st.c0 + GAP) {
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 d = divergence(st.text, text);
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, d)) reset(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 = OPENERS[st.lang];
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 no Shiki, no Prism, no
3
- * Highlight.js. Covers the languages an LLM typically emits:
4
- * js/ts/tsx/jsx, rust, python, go, bash, json, html, css, sql. Unknown
5
- * languages fall through to plain escaped text. ~6KB minified.
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[];