brookmd 0.25.2 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -89,15 +89,24 @@ export interface HeadingData {
89
89
  id: string;
90
90
  }
91
91
  /**
92
- * A CodeBlock's `kind.data` when {@link ParserConfig.blockData} is on. `lang` is
93
- * the always-on info-string language (`null` for none); `code` is the opt-in
94
- * DECODED source inside `<pre><code>…</code></pre>` (only present when `blockData`
95
- * is on). Build a copy-to-clipboard string / re-highlight from `code` alone — no
96
- * HTML re-parse, no entity-decode. When `blockData` is off, `code` is absent and
97
- * `kind.data` is just `{ lang }`, byte-identical to before.
92
+ * A CodeBlock's `kind.data`. `lang` is the always-on info-string language (`null`
93
+ * for none) — the info string's first word; `meta` is the always-on REMAINDER of
94
+ * that same info string, trimmed (```` ```ts title="src/main.ts" ```` ⇒
95
+ * `lang: "ts"`, `meta: 'title="src/main.ts"'`), absent when the fence carried
96
+ * none. `code` is the opt-in DECODED source inside `<pre><code>…</code></pre>`
97
+ * (only present when `blockData` is on). Build a copy-to-clipboard string /
98
+ * re-highlight from `code` alone — no HTML re-parse, no entity-decode. When
99
+ * `blockData` is off, `code` is absent and `kind.data` is just `{ lang }` (plus
100
+ * `meta` if the fence had one), byte-identical to before.
101
+ *
102
+ * Both halves are the RAW info-string text (backslash escapes / entity references
103
+ * left undecoded), and only `lang` appears in the rendered HTML
104
+ * (`class="language-…" data-lang="…"`) — there is deliberately no `data-meta`
105
+ * attribute, so a filename header needs a `components.CodeBlock` override.
98
106
  */
99
107
  export interface CodeBlockData {
100
108
  lang: string | null;
109
+ meta?: string;
101
110
  code?: string;
102
111
  }
103
112
  /**
@@ -117,6 +126,24 @@ export interface MathBlockData {
117
126
  */
118
127
  export interface ListItemData {
119
128
  html: string;
129
+ /**
130
+ * The item's DOCUMENT-ABSOLUTE source byte offset — the index, in the markdown
131
+ * fed so far, of the byte where this item's marker (`-`, `*`, `1.`, …) begins.
132
+ * Same origin as {@link Block.start}, and stable as the document grows (the
133
+ * parser's buffer is append-only), so `source.slice(item.start)` always begins
134
+ * at this item's marker. Use it to read or rewrite the item in place — e.g.
135
+ * find the task-list checkbox with your own `findTaskListMarkerOffset(source,
136
+ * item.start)` and flip `[ ]` ⇄ `[x]` in the original string.
137
+ *
138
+ * Present only when {@link ParserConfig.blockData} is on.
139
+ *
140
+ * KNOWN LIMITATION — absent for NESTED list items. A nested list is not a
141
+ * separate block: its items live inside the parent item's `html` and never
142
+ * reach the `items` channel, and the nested render runs against a synthesized
143
+ * de-indented string with no document offset. Nested items therefore carry no
144
+ * offset rather than a wrong one. Only top-level list items get a `start`.
145
+ */
146
+ start?: number;
120
147
  }
121
148
  /**
122
149
  * A List's `kind.data` when {@link ParserConfig.blockData} is on. `ordered` is the
@@ -243,6 +270,18 @@ export interface BlockComponentProps {
243
270
  text?: string;
244
271
  /** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
245
272
  language?: string;
273
+ /**
274
+ * Info-string META — everything after the language word, trimmed (from
275
+ * `kind.data.meta`), e.g. `title="src/main.ts"` or a bare `src/main.ts`.
276
+ * Always-on like `language` (no `blockData` needed); `undefined` when the
277
+ * fence carried none. Deliberately absent from the rendered HTML, so render a
278
+ * filename header from this prop.
279
+ *
280
+ * While streaming it appears once it can no longer change — when the opening
281
+ * fence line is terminated by a newline, or at finalize — so a header never
282
+ * flickers through a half-typed `title="src/ma`.
283
+ */
284
+ meta?: string;
246
285
  /** Component tag name — present for `Component` blocks (from `kind.data.tag`). */
247
286
  tag?: string;
248
287
  /**
@@ -274,11 +313,12 @@ export interface BlockComponentProps {
274
313
  heading?: HeadingData;
275
314
  /**
276
315
  * Structured code data — present for `CodeBlock` blocks when
277
- * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ lang, code }`
278
- * with `code` the DECODED source. Build a copy-to-clipboard string / re-highlight
279
- * from `code` — no HTML re-parse, no entity-decode. (`props.text` / `props.language`
280
- * carry the same source / lang and stay populated even when off, via the HTML
281
- * regex fallback.)
316
+ * {@link ParserConfig.blockData} is on (otherwise `undefined`). `{ lang, meta?,
317
+ * code }` with `code` the DECODED source. Build a copy-to-clipboard string /
318
+ * re-highlight from `code` — no HTML re-parse, no entity-decode. (`props.text` /
319
+ * `props.language` carry the same source / lang and stay populated even when off,
320
+ * via the HTML regex fallback; `props.meta` carries the same meta and is
321
+ * always-on, since it has no HTML form to fall back to.)
282
322
  */
283
323
  code?: CodeBlockData;
284
324
  /**
@@ -343,6 +383,28 @@ export interface ParserConfig {
343
383
  * apps that render RTL or mixed-direction content.
344
384
  */
345
385
  dirAuto?: boolean;
386
+ /**
387
+ * Lenient list indentation: a list marker followed by 6 or more columns of
388
+ * SPACE padding yields the item's text, where strict CommonMark (§5.2) keeps
389
+ * one column and renders the rest as an indented code block. Default false.
390
+ *
391
+ * Aimed at model output, which routinely over-indents after a bullet
392
+ * (`- const value = 1;`). Four cases stay strictly conformant: exactly
393
+ * 5 columns of padding, a fenced code block opened on the marker line itself,
394
+ * indented code that starts on a line AFTER the marker, and tab-padded
395
+ * markers (`-\t\tfoo`).
396
+ */
397
+ lenientLists?: boolean;
398
+ /**
399
+ * Render a CommonMark SOFT line break (a bare `\n` inside inline content) as
400
+ * a `<br>` — the `remark-breaks` / "GitHub comment" convention, where one
401
+ * Enter is one visual line. Default false (strict CommonMark: a soft break is
402
+ * whitespace). Hard breaks (two trailing spaces, or a trailing `\`) are `<br>`
403
+ * either way, so enabling this only ADDS breaks — it never removes one. Chat
404
+ * UIs streaming model output usually want this on, since models emit single
405
+ * newlines expecting a visible break.
406
+ */
407
+ softBreaks?: boolean;
346
408
  /**
347
409
  * Opt-in accessibility markup that deviates from strict GFM byte-output:
348
410
  * wraps a task-list checkbox + its text in a `<label>` (programmatic
@@ -385,8 +447,8 @@ export interface ParserConfig {
385
447
  * `input`, `svg`, …); a **non-empty** array renders only those tags (e.g.
386
448
  * `["br","sub","sup"]`) and escapes the rest. Every rendered tag's attributes
387
449
  * are sanitized (event handlers dropped, dangerous URL schemes → `#`), and HTML
388
- * comments are dropped. Block-level raw HTML stays escaped (sanitize is
389
- * inline-scoped for now). Unset/omitted = off (raw HTML handling unchanged).
450
+ * comments are dropped. Block-level raw HTML stays escaped unless you also set
451
+ * {@link blockHtml}. Unset/omitted = off (raw HTML handling unchanged).
390
452
  * Matching is case-insensitive. See also {@link dropHtmlTags}.
391
453
  */
392
454
  htmlAllowlist?: string[];
@@ -397,6 +459,52 @@ export interface ParserConfig {
397
459
  * raw-HTML sanitizer (see {@link htmlAllowlist}). Case-insensitive.
398
460
  */
399
461
  dropHtmlTags?: string[];
462
+ /**
463
+ * Extend the safe raw-HTML sanitizer to **block-level** raw HTML, so a model
464
+ * emitting `<details><summary>…</summary>…</details>` on its own lines renders
465
+ * as real elements instead of an escaped code block. Only takes effect when
466
+ * the sanitizer is engaged ({@link htmlAllowlist} / {@link dropHtmlTags}); on
467
+ * its own it does nothing. Default false — existing sanitizer users keep
468
+ * escaped block HTML until they opt in.
469
+ *
470
+ * Scope is CommonMark HTML block **types 6 and 7**: a known block-level tag
471
+ * (`<details>`, `<div>`, `<table>`, …) or any other complete tag alone on its
472
+ * line. Types 1–5 stay escaped/dropped: type 1 is the raw-text family
473
+ * (`<script>`, `<pre>`, `<style>`, `<textarea>`) — a browser reads everything
474
+ * after such a tag as unparsed text, so a speculative mid-stream close is
475
+ * mXSS-prone — and types 2–5 (comments, PIs, CDATA, declarations) carry no
476
+ * renderable element. The tag allow/drop/dangerous decision and the hardened
477
+ * attribute policy are exactly the inline sanitizer's.
478
+ *
479
+ * While the block streams, still-open elements get **speculative closers**, so
480
+ * what the reader has seen so far is a complete tree at every append; a
481
+ * half-arrived tag stays invisible until it completes. Markdown *inside* the
482
+ * HTML is not parsed (the body is text + tags).
483
+ */
484
+ blockHtml?: boolean;
485
+ /**
486
+ * Opt-in **un-blocklist** for URL schemes that brookmd blocks by default.
487
+ * Bare scheme names, **without** the colon (`["file"]`), matched
488
+ * case-insensitively. Empty/omitted = the built-in policy is unchanged.
489
+ *
490
+ * This never *restricts* anything — it is not a general allowlist. Schemes
491
+ * outside the built-in blocklist (`vscode:`, `ftp:`, `mailto:`, …) already
492
+ * render today and are unaffected. The only tier it can reach is the
493
+ * overridable-blocked one, currently just `file:`.
494
+ *
495
+ * The script-executing tier — `javascript:`, `vbscript:`, `data:text/html`,
496
+ * `data:text/javascript`, and the scriptable `data:` media types
497
+ * (`data:image/svg`, `data:application/xhtml`, …) — is **non-overridable**:
498
+ * listing one here is a silent no-op, exactly as allowlisting `<script>`
499
+ * cannot re-enable it via {@link htmlAllowlist}.
500
+ *
501
+ * Only enable `file:` in a host that intercepts link clicks instead of
502
+ * navigating (an Electron / extension UI that opens the path in an editor);
503
+ * local-resource disclosure then becomes the embedder's responsibility.
504
+ * Applies uniformly to links, URI autolinks, images, and sanitized URL
505
+ * attributes.
506
+ */
507
+ allowSchemes?: string[];
400
508
  /**
401
509
  * Opt-in structured table data. When on, a `Table` block's `kind.data` is
402
510
  * populated with `{ headers, rows, aligns }` (each cell `{ text, html }`) so a
@@ -52,13 +52,13 @@ entirely:
52
52
 
53
53
  ```toml
54
54
  [dependencies]
55
- brookmd-core = { version = "0.23", default-features = false }
55
+ brookmd-core = { version = "0.25", default-features = false }
56
56
  ```
57
57
 
58
58
  ## Wire format
59
59
 
60
60
  Blocks and patches serialize to a stable, language-agnostic JSON wire format —
61
- see [WIRE.md](WIRE.md) (wire contract v1.2.0). Native consumers can produce the
61
+ see [WIRE.md](WIRE.md) (wire contract v1.3.0). Native consumers can produce the
62
62
  same bytes as the WASM/JS boundary via `wire::patch_to_json` / `wire::blocks_to_json`.
63
63
 
64
64
  Contract v1.2.0 adds the opt-in **wire delta mode**
@@ -38,6 +38,18 @@ export class BrookParser {
38
38
  * to table header cells. Off by default (conformance output unchanged).
39
39
  */
40
40
  setA11y(on: boolean): void;
41
+ /**
42
+ * Un-block specific URL schemes that are blocked by DEFAULT — bare scheme
43
+ * names without the colon (`["file"]`), matched case-insensitively. Empty
44
+ * by default (built-in policy unchanged). This never RESTRICTS anything:
45
+ * schemes outside the built-in blocklist (`vscode:`, `ftp:`, …) already
46
+ * pass. The script-executing tier (`javascript:`, `vbscript:`,
47
+ * `data:text/html`, `data:text/javascript`, scriptable `data:` media types)
48
+ * is non-overridable — listing one here is a no-op. Only enable `file:` in
49
+ * a host that intercepts link clicks instead of navigating (Electron,
50
+ * extensions); local-resource disclosure is then the embedder's call.
51
+ */
52
+ setAllowSchemes(schemes: string[]): void;
41
53
  /**
42
54
  * Opt-in structured `kind.data` channel for Table blocks: a Table then
43
55
  * carries `{ headers, rows, aligns }` (per-cell `{ text, html }`) so a
@@ -46,6 +58,17 @@ export class BrookParser {
46
58
  * as `{"type":"Table"}` (no `data` key) and output is byte-identical.
47
59
  */
48
60
  setBlockData(on: boolean): void;
61
+ /**
62
+ * Extend the safe raw-HTML sanitizer to BLOCK-level raw HTML — a
63
+ * `<details><summary>…` block renders as real elements instead of escaping
64
+ * into a code block. Takes effect ONLY when the sanitizer is engaged
65
+ * (`setHtmlSanitize`), and only for CommonMark HTML block types 6 and 7.
66
+ * Types 1–5 (`<script>`/`<pre>`/`<style>`/`<textarea>`, comments, PIs,
67
+ * CDATA, declarations) stay escaped/dropped. Still-open elements get
68
+ * speculative closers while the block streams, so the emitted HTML is a
69
+ * complete tree at every prefix. Off by default (output unchanged).
70
+ */
71
+ setBlockHtml(on: boolean): void;
49
72
  /**
50
73
  * Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
51
74
  * A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
@@ -107,6 +130,23 @@ export class BrookParser {
107
130
  * cells, and list items. Empty by default (inline output unchanged).
108
131
  */
109
132
  setInlineComponentTags(tags: string[]): void;
133
+ /**
134
+ * Lenient list indentation: a list marker followed by 6+ columns of SPACE
135
+ * padding yields the item's text instead of an indented code block. Off by
136
+ * default (strict CommonMark §5.2). Useful for model output, which routinely
137
+ * over-indents after a bullet. Exactly-5-column padding, a fence opened on
138
+ * the marker line, indented code starting on a later line, and tab-padded
139
+ * markers all stay strictly conformant.
140
+ */
141
+ setLenientLists(on: boolean): void;
142
+ /**
143
+ * Render a CommonMark SOFT line break (a bare `\n` in inline content) as a
144
+ * `<br>` — the `remark-breaks` convention, where one Enter is one visual
145
+ * line. Off by default (strict CommonMark: a soft break is whitespace).
146
+ * Hard breaks (two trailing spaces / trailing `\`) are `<br>` either way,
147
+ * so turning this on only ADDS breaks; it never removes one.
148
+ */
149
+ setSoftBreaks(on: boolean): void;
110
150
  /**
111
151
  * Enable or disable raw-HTML pass-through. Default off. Do not enable
112
152
  * when rendering untrusted input — bypasses XSS protection.
@@ -135,7 +175,9 @@ export interface InitOutput {
135
175
  readonly brookparser_new: () => number;
136
176
  readonly brookparser_retainedBytes: (a: number) => number;
137
177
  readonly brookparser_setA11y: (a: number, b: number) => void;
178
+ readonly brookparser_setAllowSchemes: (a: number, b: number, c: number) => void;
138
179
  readonly brookparser_setBlockData: (a: number, b: number) => void;
180
+ readonly brookparser_setBlockHtml: (a: number, b: number) => void;
139
181
  readonly brookparser_setComponentTags: (a: number, b: number, c: number) => void;
140
182
  readonly brookparser_setDirAuto: (a: number, b: number) => void;
141
183
  readonly brookparser_setGfmAlerts: (a: number, b: number) => void;
@@ -145,6 +187,8 @@ export interface InitOutput {
145
187
  readonly brookparser_setGfmTagfilter: (a: number, b: number) => void;
146
188
  readonly brookparser_setHtmlSanitize: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
147
189
  readonly brookparser_setInlineComponentTags: (a: number, b: number, c: number) => void;
190
+ readonly brookparser_setLenientLists: (a: number, b: number) => void;
191
+ readonly brookparser_setSoftBreaks: (a: number, b: number) => void;
148
192
  readonly brookparser_setUnsafeHtml: (a: number, b: number) => void;
149
193
  readonly brookparser_setWireDelta: (a: number, b: number) => void;
150
194
  readonly __wbindgen_export: (a: number, b: number) => number;
@@ -138,6 +138,23 @@ export class BrookParser {
138
138
  setA11y(on) {
139
139
  wasm.brookparser_setA11y(this.__wbg_ptr, on);
140
140
  }
141
+ /**
142
+ * Un-block specific URL schemes that are blocked by DEFAULT — bare scheme
143
+ * names without the colon (`["file"]`), matched case-insensitively. Empty
144
+ * by default (built-in policy unchanged). This never RESTRICTS anything:
145
+ * schemes outside the built-in blocklist (`vscode:`, `ftp:`, …) already
146
+ * pass. The script-executing tier (`javascript:`, `vbscript:`,
147
+ * `data:text/html`, `data:text/javascript`, scriptable `data:` media types)
148
+ * is non-overridable — listing one here is a no-op. Only enable `file:` in
149
+ * a host that intercepts link clicks instead of navigating (Electron,
150
+ * extensions); local-resource disclosure is then the embedder's call.
151
+ * @param {string[]} schemes
152
+ */
153
+ setAllowSchemes(schemes) {
154
+ const ptr0 = passArrayJsValueToWasm0(schemes, wasm.__wbindgen_export);
155
+ const len0 = WASM_VECTOR_LEN;
156
+ wasm.brookparser_setAllowSchemes(this.__wbg_ptr, ptr0, len0);
157
+ }
141
158
  /**
142
159
  * Opt-in structured `kind.data` channel for Table blocks: a Table then
143
160
  * carries `{ headers, rows, aligns }` (per-cell `{ text, html }`) so a
@@ -149,6 +166,20 @@ export class BrookParser {
149
166
  setBlockData(on) {
150
167
  wasm.brookparser_setBlockData(this.__wbg_ptr, on);
151
168
  }
169
+ /**
170
+ * Extend the safe raw-HTML sanitizer to BLOCK-level raw HTML — a
171
+ * `<details><summary>…` block renders as real elements instead of escaping
172
+ * into a code block. Takes effect ONLY when the sanitizer is engaged
173
+ * (`setHtmlSanitize`), and only for CommonMark HTML block types 6 and 7.
174
+ * Types 1–5 (`<script>`/`<pre>`/`<style>`/`<textarea>`, comments, PIs,
175
+ * CDATA, declarations) stay escaped/dropped. Still-open elements get
176
+ * speculative closers while the block streams, so the emitted HTML is a
177
+ * complete tree at every prefix. Off by default (output unchanged).
178
+ * @param {boolean} on
179
+ */
180
+ setBlockHtml(on) {
181
+ wasm.brookparser_setBlockHtml(this.__wbg_ptr, on);
182
+ }
152
183
  /**
153
184
  * Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
154
185
  * A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
@@ -247,6 +278,29 @@ export class BrookParser {
247
278
  const len0 = WASM_VECTOR_LEN;
248
279
  wasm.brookparser_setInlineComponentTags(this.__wbg_ptr, ptr0, len0);
249
280
  }
281
+ /**
282
+ * Lenient list indentation: a list marker followed by 6+ columns of SPACE
283
+ * padding yields the item's text instead of an indented code block. Off by
284
+ * default (strict CommonMark §5.2). Useful for model output, which routinely
285
+ * over-indents after a bullet. Exactly-5-column padding, a fence opened on
286
+ * the marker line, indented code starting on a later line, and tab-padded
287
+ * markers all stay strictly conformant.
288
+ * @param {boolean} on
289
+ */
290
+ setLenientLists(on) {
291
+ wasm.brookparser_setLenientLists(this.__wbg_ptr, on);
292
+ }
293
+ /**
294
+ * Render a CommonMark SOFT line break (a bare `\n` in inline content) as a
295
+ * `<br>` — the `remark-breaks` convention, where one Enter is one visual
296
+ * line. Off by default (strict CommonMark: a soft break is whitespace).
297
+ * Hard breaks (two trailing spaces / trailing `\`) are `<br>` either way,
298
+ * so turning this on only ADDS breaks; it never removes one.
299
+ * @param {boolean} on
300
+ */
301
+ setSoftBreaks(on) {
302
+ wasm.brookparser_setSoftBreaks(this.__wbg_ptr, on);
303
+ }
250
304
  /**
251
305
  * Enable or disable raw-HTML pass-through. Default off. Do not enable
252
306
  * when rendering untrusted input — bypasses XSS protection.
Binary file
@@ -9,7 +9,9 @@ export const brookparser_finalize: (a: number, b: number) => void;
9
9
  export const brookparser_new: () => number;
10
10
  export const brookparser_retainedBytes: (a: number) => number;
11
11
  export const brookparser_setA11y: (a: number, b: number) => void;
12
+ export const brookparser_setAllowSchemes: (a: number, b: number, c: number) => void;
12
13
  export const brookparser_setBlockData: (a: number, b: number) => void;
14
+ export const brookparser_setBlockHtml: (a: number, b: number) => void;
13
15
  export const brookparser_setComponentTags: (a: number, b: number, c: number) => void;
14
16
  export const brookparser_setDirAuto: (a: number, b: number) => void;
15
17
  export const brookparser_setGfmAlerts: (a: number, b: number) => void;
@@ -19,6 +21,8 @@ export const brookparser_setGfmMath: (a: number, b: number) => void;
19
21
  export const brookparser_setGfmTagfilter: (a: number, b: number) => void;
20
22
  export const brookparser_setHtmlSanitize: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
21
23
  export const brookparser_setInlineComponentTags: (a: number, b: number, c: number) => void;
24
+ export const brookparser_setLenientLists: (a: number, b: number) => void;
25
+ export const brookparser_setSoftBreaks: (a: number, b: number) => void;
22
26
  export const brookparser_setUnsafeHtml: (a: number, b: number) => void;
23
27
  export const brookparser_setWireDelta: (a: number, b: number) => void;
24
28
  export const __wbindgen_export: (a: number, b: number) => number;
package/dist/worker.js CHANGED
@@ -15,6 +15,8 @@ const core = new WorkerCore({
15
15
  p.setGfmFootnotes(c?.gfmFootnotes ?? false);
16
16
  p.setGfmMath(c?.gfmMath ?? false);
17
17
  p.setDirAuto(c?.dirAuto ?? false);
18
+ p.setLenientLists(c?.lenientLists ?? false);
19
+ p.setSoftBreaks(c?.softBreaks ?? false);
18
20
  p.setA11y(c?.a11y ?? false);
19
21
  p.setUnsafeHtml(c?.unsafeHtml ?? false);
20
22
  p.setComponentTags(c?.componentTags ?? []);
@@ -24,6 +26,8 @@ const core = new WorkerCore({
24
26
  c?.htmlAllowlist ?? [],
25
27
  c?.dropHtmlTags ?? []
26
28
  );
29
+ p.setBlockHtml(c?.blockHtml ?? false);
30
+ p.setAllowSchemes(c?.allowSchemes ?? []);
27
31
  p.setBlockData(c?.blockData ?? false);
28
32
  p.setWireDelta(true);
29
33
  return p;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brookmd",
3
- "version": "0.25.2",
3
+ "version": "0.26.1",
4
4
  "description": "Zero-dep streaming markdown for the browser. Rust→WASM core, Web Worker per stream, incremental parse with speculative closure.",
5
5
  "type": "module",
6
6
  "sideEffects": ["./dist/worker.js", "./dist/styles.css"],