@vectojs/markdown 0.13.0 → 0.15.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/README.md CHANGED
@@ -4,12 +4,11 @@ Canvas-native Markdown (with TeX math) rendering for [VectoJS](https://github.co
4
4
 
5
5
  `Markdown` is a high-level entity that parses Markdown with
6
6
  [`marked`](https://marked.js.org/), renders TeX math to SVG with
7
- [MathJax](https://www.mathjax.org/), and lays the result out using
8
- `@vectojs/ui` components (`RichText`, `Stack`, `Table`, `Text`, `Image`). It also
9
- exports `CodeBlock`.
7
+ `@vectojs/tex`, and lays the result out using `@vectojs/ui` components
8
+ (`RichText`, `Stack`, `Table`, `Text`, `Image`). It also exports `CodeBlock`.
10
9
 
11
10
  This package was split out of `@vectojs/ui` so that the heavy `marked` +
12
- `mathjax-full` dependencies are only pulled in by apps that actually render
11
+ `@vectojs/tex` dependencies are only pulled in by apps that actually render
13
12
  Markdown. Because it depends on `@vectojs/ui` components, it sits **above** `ui`
14
13
  in the dependency graph — install it alongside `@vectojs/ui` and `@vectojs/core`.
15
14
 
@@ -135,26 +134,32 @@ converted once no matter how many documents or instances render it.
135
134
  Inline `$...$` math is a separate path: it is currently shown as styled source
136
135
  text, not typeset.
137
136
 
138
- #### MathJax is loaded on demand
137
+ #### The math engine is loaded on demand
139
138
 
140
- MathJax is imported dynamically, the first time a document actually has a formula
141
- to typeset. It is by far the heaviest thing this package can pull in — measured
142
- against a browser bundle of a consumer that renders only prose:
139
+ TeX math is typeset by `@vectojs/tex`, which is imported dynamically the first
140
+ time a document actually has a formula. It is by far the heaviest thing this
141
+ package can pull in — measured against a browser bundle of a consumer that renders
142
+ only prose, built with code splitting and minification:
143
143
 
144
- | prose-only consumer | raw | gzip |
145
- | ------------------------- | --------: | ------: |
146
- | eagerly imported (before) | 2,157,295 | 725,012 |
147
- | lazily imported (now) | 339,767 | 106,095 |
144
+ | prose-only consumer | raw | gzip | chunks |
145
+ | ---------------------- | --------: | ------: | -----: |
146
+ | `mathjax-full` | 2,199,869 | 748,713 | 19 |
147
+ | `@vectojs/tex` (now) | 758,249 | 273,754 | 3 |
148
+ | no math at all (floor) | 379,224 | 118,670 | 3 |
148
149
 
149
- That is 85% of the bundle a document with no formulas used to carry, plus roughly
150
- 150 ms of module evaluation at startup. Your bundler needs code splitting enabled
151
- to see this; without it the bytes are still in the output, just not evaluated
152
- until first use.
150
+ Against that floor the math path itself is 630,043 gzip under `mathjax-full` and
151
+ 155,033 under `@vectojs/tex` **4.06x smaller**. The eagerly-downloaded entry
152
+ chunk a prose-only consumer actually pays for is 117,889 gzip, within 1 KB of the
153
+ no-math floor.
154
+
155
+ Your bundler needs code splitting enabled to see this; without it the bytes are
156
+ still in the output, just not evaluated until first use.
153
157
 
154
158
  The tradeoff is that **the first formula on a page cannot be typeset
155
159
  synchronously.** It renders as a code block of TeX source — the same state an
156
160
  unclosed fence already shows — and is replaced once the module resolves. Every
157
- formula after that is synchronous again.
161
+ formula after that is synchronous again. (The engine itself is synchronous; the
162
+ lazy import is what defers it, and it is kept for the bundle size above.)
158
163
 
159
164
  While streaming this is invisible: the load starts as soon as an _opening_ math
160
165
  fence appears, several chunks before the closing one, so the formula is typeset on
@@ -176,6 +181,63 @@ from several places starts one load. `isMathJaxReady()` reports whether formulas
176
181
  currently typeset without waiting. If the load fails, formulas keep rendering as
177
182
  TeX source rather than throwing.
178
183
 
184
+ Both names are historical: they date from when `mathjax-full` was the engine and
185
+ mean "the math engine", whichever one that is. They keep those names because they
186
+ are public API and a rename would break every consumer for cosmetics.
187
+
188
+ A formula containing a symbol outside the engine's shipped glyph corpus also
189
+ renders as TeX source rather than being drawn with that symbol missing.
190
+
191
+ ## Images
192
+
193
+ An image renders in one of two ways, decided by where it is written.
194
+
195
+ **On its own, or in a paragraph, blockquote or list item**, the paragraph splits
196
+ into blocks and the image becomes an `Image` entity at its natural size, capped
197
+ to the available width. This is the ordinary `![alt](url)` case.
198
+
199
+ **On a line it shares with text** — in a heading, or in a table cell — it renders
200
+ as an inline box in the text run, so the prose flows around it and selection and
201
+ the accessible name still work. Its height is a multiple of the run's font size
202
+ (`theme.inlineImageScale`, default `1.15`) and its width follows the image's
203
+ natural aspect ratio, so a badge stays wide and a square icon stays square:
204
+
205
+ ```ts
206
+ const md = new Markdown("# Build ![passing](https://img.example/badge.svg)");
207
+ ```
208
+
209
+ This is a deliberate departure from HTML, which would render an inline image at
210
+ its intrinsic size. A 512px logo written into an `h1` would otherwise tower over
211
+ its own heading, and an inline box has to be sized before the image has decoded.
212
+ The height is fixed up front for the same reason: the line box never moves, and
213
+ only the width settles once the aspect ratio is known.
214
+
215
+ The `alt` text is the accessible name and the copied text — never painted as
216
+ visible prose. If the image fails to load, the box is replaced by the alt text
217
+ rather than left as an invisible gap.
218
+
219
+ ## Syntax coverage
220
+
221
+ Everything in [CommonMark](https://spec.commonmark.org/) plus the
222
+ [GFM](https://github.github.com/gfm/) extensions this renderer draws: tables,
223
+ strikethrough, task lists, autolinks, plus `$…$` / `$$…$$` TeX math and
224
+ ` ```math ` fences.
225
+
226
+ Two constructs are deliberately **not** supported, and both are pinned by tests
227
+ so the behaviour cannot drift silently:
228
+
229
+ - **Definition lists.** `Term` then `: definition` renders as the two literal
230
+ lines the source contains, colon included.
231
+ - **Raw HTML blocks.** `<details>`, `<div>`, `<iframe>` and HTML comments render
232
+ nothing at all.
233
+
234
+ Definition lists are neither CommonMark nor GFM; when they arrive it will be
235
+ through the same syntax-extension mechanism footnotes need. Raw HTML blocks
236
+ cannot work in a zero-DOM renderer — there is no DOM to hand markup to. `<svg>`
237
+ is the one exception, because a self-contained SVG document can be rasterized.
238
+
239
+ Footnotes (`[^1]`) are **not yet parsed** and currently render as literal source.
240
+
179
241
  > Migrating from `@vectojs/ui` ≤ 1.x? `Markdown` and `CodeBlock` used to be
180
242
  > exported from `@vectojs/ui`. As of `@vectojs/ui@2.0.0` they live here — change
181
243
  > `import { Markdown } from '@vectojs/ui'` to `from '@vectojs/markdown'`.
@@ -1,167 +1,47 @@
1
- import { type ContentProjectionHint, Entity, type DevtoolsDescriptor, GlyphRasterAtlas, type GlyphRasterAtlasStats, IRenderer, type ContentProjection } from '@vectojs/core';
1
+ import { Entity, type DevtoolsDescriptor, IRenderer } from '@vectojs/core';
2
2
  import { type Token } from 'marked';
3
3
  import { type StreamController, type StreamControllerOptions } from './StreamController';
4
+ export { isMathJaxReady, MathBlock, preloadMathJax } from './markdown-math';
5
+ export { CodeBlock, codeAtlas, codeAtlasStats } from './markdown-code';
6
+ import { type MarkdownTheme } from './theme';
7
+ export type { MarkdownTheme } from './theme';
4
8
  import { Stack, UIComponent } from '@vectojs/ui';
5
- export declare function preloadMathJax(): Promise<void>;
6
- /** Whether formulas can be typeset without waiting. Exposed for tests. */
7
- export declare function isMathJaxReady(): boolean;
8
- /** Color and typography theme for Markdown rendering. */
9
- export interface MarkdownTheme {
10
- /** Body text color. */
11
- textColor?: string;
12
- /** Heading text color. */
13
- headingColor?: string;
14
- /** Code text color (inline + block). */
15
- codeColor?: string;
16
- /** Code block background color. */
17
- codeBgColor?: string;
18
- /** Blockquote border/accent color. */
19
- quoteBorderColor?: string;
20
- /** Blockquote text color. */
21
- quoteTextColor?: string;
22
- /** Horizontal-rule color. */
23
- hrColor?: string;
24
- /** Table background color. */
25
- tableBgColor?: string;
26
- /** Table header background color. */
27
- tableHeaderBgColor?: string;
28
- /** Body font. */
29
- bodyFont?: string;
30
- /** Monospace font for code. */
31
- codeFont?: string;
32
- /** Base font size in px. */
33
- fontSize?: number;
34
- }
35
- /** A simple concrete container entity for nested layouts. */
36
- declare class MarkdownContainer extends Entity {
37
- isPointInside(_globalX: number, _globalY: number): boolean;
38
- render(_r: any): void;
39
- }
40
- /**
41
- * One display formula: a `$$..$$` block or a closed ```` ```math ```` fence.
42
- *
43
- * A named class rather than a bare {@link MarkdownContainer} because the formula
44
- * needs a stable handle, and after the switch to an inline object it has none:
45
- * the typeset raster lives in a `paint` closure captured by the span, so removing
46
- * the `Image` entity left nothing exposing either the source or the SVG bytes.
47
- * Devtools, tests, and anything auditing what a formula actually rendered all
48
- * want that. `markstream-vue` reaches the same conclusion from the DOM side and
49
- * publishes `data-markstream-mode` on its math node for the same reason.
50
- *
51
- * Deliberately carries no typeset-vs-source flag. A formula MathJax has not
52
- * converted yet renders as a bare {@link CodeBlock} of its TeX, which this class
53
- * does not wrap — wrapping it would put a container between `content` and a
54
- * `CodeBlock` that the streamed `setCode` path locates by type. So a flag would
55
- * have exactly one reachable value, which is the dead-API trap that cost CTX-0208
56
- * a debugging pass. Add it together with wrapping the fallback, or not at all.
57
- */
58
- export declare class MathBlock extends MarkdownContainer {
9
+ export interface MarkdownOptions {
10
+ maxWidth?: number;
11
+ theme?: MarkdownTheme;
12
+ onLinkClick?: (url: string) => void;
13
+ /** Allow browser-native drag selection and copy for rendered text. Default `true`. */
14
+ selectable?: boolean;
15
+ /** Emit a `vecto:markdown:parse` User Timing measure. Default `false`. */
16
+ userTiming?: boolean;
59
17
  /**
60
- * The TeX source, exactly as written between the delimiters.
18
+ * Draw copy / download controls in the top-right corner of code blocks and
19
+ * tables. Default `false`.
61
20
  *
62
- * Also the projected text and the accessible name, so this is the one string a
63
- * reader can find, select, and copy.
21
+ * Opt-in rather than on by default because it adds two focusable stops per such
22
+ * block to the tab order, which a document with many code fences would make
23
+ * tedious to navigate past, and because a reader who cannot act on a control
24
+ * (no clipboard permission, no filesystem) is better served by not being
25
+ * offered one.
64
26
  */
65
- readonly formula: string;
66
- /** The `data:image/svg+xml` URI of the typeset glyphs. */
67
- readonly svgUri: string;
68
- constructor(formula: string, svgUri: string);
69
- getDevtoolsDescriptor(): DevtoolsDescriptor;
70
- }
71
- /**
72
- * A single self-rendering entity for fenced code blocks.
73
- *
74
- * Replaces the old N×M child-entity explosion (Container → Stack → Text per
75
- * segment per line) with a flat leaf that draws its own background + text.
76
- */
77
- export declare class CodeBlock extends UIComponent {
78
- private lines;
79
- private grid;
80
- /** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
81
- private rawLines;
82
- private cellWidth;
83
- private source;
84
- /** Bumped by {@link buildLines} and {@link setSelectable}; read by `Scene`. */
85
- private contentEpoch;
86
- private lang;
87
- private theme;
88
- private lineH;
89
- private pad;
90
- private codeFont;
91
- selectable: boolean;
92
- constructor(code: string, lang: string, maxWidth: number, theme: Required<MarkdownTheme>, selectable?: boolean);
93
- /** Re-parse code content (e.g. for live editing). */
94
- setCode(code: string, lang?: string): this;
95
- /** Enable or disable browser-native selection for this code block. */
96
- setSelectable(selectable: boolean): this;
97
- getContentEpoch(): number;
27
+ blockAffordances?: boolean;
98
28
  /**
99
- * Change the block's box width.
29
+ * Writes text to the clipboard for the copy controls.
100
30
  *
101
- * Deliberately does **not** rebuild the grid or the highlight, because code does
102
- * not reflow: lines are placed on a fixed monospace grid at `col × cellWidth` and
103
- * a long line overflows rather than wrapping, so `height` is a function of line
104
- * *count* alone. The width only sizes the rounded background. Anything that would
105
- * change the glyph geometry — the source, the language, the font — goes through
106
- * {@link setCode} and invalidates the grid there.
107
- *
108
- * @returns `this` for chaining.
31
+ * Injectable because the real path (`navigator.clipboard.writeText`) is absent
32
+ * in jsdom and, in a browser, rejects a write that did not originate in a user
33
+ * gesture so a test can only assert the payload, never the platform call.
34
+ * Defaults to `navigator.clipboard.writeText` when available.
109
35
  */
110
- setWidth(width: number): this;
111
- getContentProjection(hint?: ContentProjectionHint): ContentProjection | null;
36
+ writeClipboard?: (text: string) => void;
112
37
  /**
113
- * Re-highlight the code, reusing the highlight of any unchanged line prefix.
38
+ * Saves a generated file for the download controls.
114
39
  *
115
- * Streaming appends to the END of a block, so all but the last line or two are
116
- * byte-identical to the previous call yet this used to re-highlight every
117
- * line on every chunk, making a streamed block O(N) per append and O(N^2)
118
- * overall. Reusing the stable prefix makes an append proportional to what
119
- * actually changed.
120
- *
121
- * The last previously-seen line is deliberately NOT reused: a chunk usually
122
- * lands mid-line, so that line's text (and therefore its tokenization) changes.
40
+ * Defaults to an anchor-click download that revokes its object URL. Injectable
41
+ * for the same reason as {@link MarkdownOptions.writeClipboard}: jsdom has no
42
+ * download behaviour to observe.
123
43
  */
124
- private buildLines;
125
- private ensureGrid;
126
- /** Code blocks are decorative — not interactive. */
127
- isPointInside(): boolean;
128
- render(r: IRenderer): void;
129
- }
130
- /**
131
- * Instrumentation for the code-block glyph atlas in use, or `null` before first
132
- * use.
133
- *
134
- * Exposed so an app or benchmark can confirm the atlas is actually active and
135
- * reusing slots. Watch `resets`: a steadily climbing count means the glyph set is
136
- * unbounded for the atlas size, so every reset re-rasterizes everything and the
137
- * atlas is doing net harm rather than saving work.
138
- *
139
- * Reports the *most recently used* atlas, which after a zoom is the one now being
140
- * blitted — see {@link codeAtlas}.
141
- */
142
- export declare function codeAtlasStats(): GlyphRasterAtlasStats | null;
143
- /**
144
- * The code-block atlas most recently blitted from, or `null` before first use.
145
- *
146
- * For instrumentation that must map a traced `drawImage` back to the glyph it
147
- * painted — a blit carries only a source rect, so `slotAt()` is the only way to
148
- * recover the cluster and its metrics. Used by `e2e/text-projection.e2e.ts` to
149
- * keep the code-grid positioning assertions working on the blit path.
150
- *
151
- * "Most recently used" rather than "the one" because atlases are pooled per DPR:
152
- * a caller resolving a traced blit wants the atlas that produced it, which is the
153
- * one the last render selected. Compare its {@link GlyphRasterAtlas.pixelRatio}
154
- * against {@link IRenderer.pixelRatio} to assert the blit is 1:1.
155
- */
156
- export declare function codeAtlas(): GlyphRasterAtlas | null;
157
- export interface MarkdownOptions {
158
- maxWidth?: number;
159
- theme?: MarkdownTheme;
160
- onLinkClick?: (url: string) => void;
161
- /** Allow browser-native drag selection and copy for rendered text. Default `true`. */
162
- selectable?: boolean;
163
- /** Emit a `vecto:markdown:parse` User Timing measure. Default `false`. */
164
- userTiming?: boolean;
44
+ saveFile?: (filename: string, content: string, mimeType: string) => void;
165
45
  }
166
46
  /**
167
47
  * Renders Markdown content into a VectoJS entity tree using {@link marked}.
@@ -185,6 +65,18 @@ export declare class Markdown extends UIComponent {
185
65
  theme: Required<MarkdownTheme>;
186
66
  onLinkClick?: (url: string) => void;
187
67
  selectable: boolean;
68
+ /**
69
+ * Whether code blocks and tables carry copy / download controls.
70
+ *
71
+ * Read when a block entity is built, so it affects blocks rendered from here on
72
+ * rather than retroactively; a document does not rebuild to gain or lose an
73
+ * affordance.
74
+ */
75
+ blockAffordances: boolean;
76
+ /** Clipboard writer used by the copy controls. */
77
+ writeClipboard: (text: string) => void;
78
+ /** File saver used by the download controls. */
79
+ saveFile: (filename: string, content: string, mimeType: string) => void;
188
80
  private activeBlockMetrics;
189
81
  /**
190
82
  * Called after a streamed append has re-laid-out the document.
@@ -260,6 +152,20 @@ export declare class Markdown extends UIComponent {
260
152
  * field only so {@link destroy} can remove the exact closure it added.
261
153
  */
262
154
  private inlineMathRepaint?;
155
+ /**
156
+ * This instance's entry in the inline-image decode waiters, or `undefined` if it
157
+ * has never rendered an image. Held as a field only so {@link destroy} can remove
158
+ * the exact closure it added.
159
+ */
160
+ private inlineImageRemeasure?;
161
+ /**
162
+ * URLs whose decoded aspect ratio this document has already reserved a box for.
163
+ *
164
+ * The guard that makes the re-measure fire once per image rather than once per
165
+ * decode-notification-per-image: the waiter set is module-level, so a page of
166
+ * many documents tells all of them about all decodes.
167
+ */
168
+ private readonly inlineImagesMeasured;
263
169
  /**
264
170
  * True while this document is waiting on the lazy MathJax load.
265
171
  *
@@ -447,6 +353,45 @@ export declare class Markdown extends UIComponent {
447
353
  * one closure per instance.
448
354
  */
449
355
  private subscribeInlineMathRepaint;
356
+ /**
357
+ * Re-measure this document when an inline image's raster finishes decoding.
358
+ *
359
+ * Inline images differ from inline formulas in one way that matters: a formula's
360
+ * box is known synchronously the moment it typesets, while an image's aspect
361
+ * ratio arrives only with the decode. The span reserved a square until then, so a
362
+ * decode that reports anything else has invalidated a WIDTH, and a repaint into
363
+ * the old box would letterbox or stretch the picture.
364
+ *
365
+ * So this rebuilds through {@link retypesetFromTokens} — the same late-arrival
366
+ * path MathJax uses — but only when a reserved width actually changed. Every live
367
+ * document is notified for every decode, including images it does not contain, so
368
+ * an unconditional rebuild here would be O(documents x images) full re-renders
369
+ * for a page of many blocks.
370
+ *
371
+ * Subscribed lazily and held as a field for the same two reasons as its math
372
+ * counterpart: a document with no images costs nothing, and `destroy` must remove
373
+ * the exact closure it added.
374
+ */
375
+ private subscribeInlineImageRemeasure;
376
+ /**
377
+ * Whether any inline image in this document has just learned it is not square.
378
+ *
379
+ * An inline image's span reserves a square box before its raster decodes, because
380
+ * that is the only shape available without a natural size. The decode supplies the
381
+ * real aspect ratio, so a non-square image needs one rebuild to reserve the right
382
+ * width — and exactly one. Every live document is notified of every decode on the
383
+ * page, including images it does not contain, so this has to answer "did MY
384
+ * geometry just change" and not merely "did something decode".
385
+ *
386
+ * Walks the tokens rather than the entity tree: the reserved box is a function of
387
+ * the raster's aspect ratio, which is available here, and a token walk cannot be
388
+ * confused by an entity a previous rebuild already corrected.
389
+ *
390
+ * Only headings and table cells are inspected. Every other context splits an image
391
+ * into its own block whose `Image` entity resizes itself in `onLoad`, so a rebuild
392
+ * for one of those would be pure cost.
393
+ */
394
+ private inlineImageBoxesStale;
450
395
  destroy(): void;
451
396
  /**
452
397
  * Streaming and parse state — the markdown streaming inspector.
@@ -578,6 +523,28 @@ export declare class Markdown extends UIComponent {
578
523
  * policy for a zero-dimension source is a separate decision from notifying
579
524
  * the scene, which is the actual defect here.
580
525
  */
526
+ /**
527
+ * Wraps a block in its copy / download controls, or returns it untouched.
528
+ *
529
+ * The controls are built lazily through `make` so a document with
530
+ * `blockAffordances` off pays nothing — not the closures, not the measurement
531
+ * `BlockAffordanceButton` does in its constructor.
532
+ */
533
+ private withBlockAffordances;
534
+ /** Copy and download controls for one fenced code block. */
535
+ private codeBlockAffordances;
536
+ /** Copy (as Markdown) and download (as CSV) controls for one table. */
537
+ private tableAffordances;
538
+ /**
539
+ * Button styling for the affordances, derived from the document theme.
540
+ *
541
+ * Themed rather than hardcoded so a light-theme document does not get the dark
542
+ * default palette. `focusColor` is set explicitly from the theme's accent
543
+ * because `Button`'s default cyan is tuned for the dark palette and reads as
544
+ * off-brand elsewhere — while a focus ring is the one affordance a keyboard
545
+ * user cannot do without.
546
+ */
547
+ private affordanceButtonOptions;
581
548
  private paragraphImage;
582
549
  /** One table cell entity, shared by the render arm and the streamed-table path. */
583
550
  private tableCellRichText;
@@ -887,4 +854,3 @@ export declare class Markdown extends UIComponent {
887
854
  /** Structural — children draw themselves. */
888
855
  render(_r: IRenderer): void;
889
856
  }
890
- export {};
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Take-away affordances for block content: copy to the clipboard, download as a
3
+ * file.
4
+ *
5
+ * Split out of `Markdown.ts` rather than added to it because none of this is
6
+ * parsing or rendering — it is serialization plus two platform primitives — and
7
+ * that file is already 4.8k lines.
8
+ *
9
+ * The reference implementation is `streamdown` (clone `e5deed3`,
10
+ * `packages/streamdown/lib/`), read and recorded in
11
+ * `vectojs-docs/forge/findings/upstream/README.md`. Two details of its `save()`
12
+ * (`lib/utils.ts:35`) are borrowed deliberately: revoking the object URL, and the
13
+ * UTF-8 BOM for CSV. Both are commonly omitted and each is a real defect when
14
+ * missing — the first leaks a blob for the document's lifetime, the second makes
15
+ * Excel on Windows read the file in the system ANSI codepage and corrupt every
16
+ * non-ASCII cell.
17
+ */
18
+ import type { A11yAttributes, Entity, LayoutControlledProperty } from '@vectojs/core';
19
+ import { Button, type ButtonOptions, UIComponent } from '@vectojs/ui';
20
+ /** Extension for a fence info string, without the dot. `txt` when unrecognised. */
21
+ export declare function extensionForLanguage(lang: string): string;
22
+ /** MIME type for a downloaded code block. */
23
+ export declare function mimeForLanguage(lang: string): string;
24
+ /**
25
+ * Escapes one CSV field per RFC 4180.
26
+ *
27
+ * A field containing a comma, a quote or a newline is wrapped in quotes, and an
28
+ * internal quote is doubled. Anything else is returned unchanged, so the common
29
+ * case allocates nothing.
30
+ */
31
+ export declare function escapeCsvField(value: string): string;
32
+ /**
33
+ * Escapes one Markdown table cell.
34
+ *
35
+ * Backslash first, then pipe: doing it the other way escapes the backslash that
36
+ * the pipe escape just introduced, which is why `streamdown` documents the order
37
+ * at `lib/table/utils.ts:137`.
38
+ */
39
+ export declare function escapeMarkdownTableCell(cell: string): string;
40
+ /** Column alignment as `marked` reports it on a table token. */
41
+ export type TableAlign = 'left' | 'center' | 'right' | null;
42
+ /** Plain-text table content, extracted from the token before entities are built. */
43
+ export interface TableContent {
44
+ headers: string[];
45
+ rows: string[][];
46
+ align: readonly TableAlign[];
47
+ }
48
+ /**
49
+ * Serializes a table as CSV, prefixed with a UTF-8 BOM.
50
+ *
51
+ * Rows are `\r\n`-separated per RFC 4180.
52
+ *
53
+ * The BOM lives here rather than in {@link defaultSaveFile} — where `streamdown`
54
+ * puts it — so that the guarantee survives a caller supplying its own
55
+ * `saveFile`. Excel on Windows reads a BOM-less file in the system ANSI codepage
56
+ * and corrupts every non-ASCII cell, and that would be a silent,
57
+ * locale-dependent defect for anyone who replaced only the platform primitive.
58
+ * A property of the CSV belongs to the CSV.
59
+ */
60
+ export declare function tableToCsv(table: TableContent): string;
61
+ /**
62
+ * Serializes a table back to GitHub-flavoured Markdown.
63
+ *
64
+ * The alignment row is reproduced from the token's own `align`, so a copied table
65
+ * re-lexes to the same alignment rather than silently becoming left-aligned.
66
+ */
67
+ export declare function tableToMarkdown(table: TableContent): string;
68
+ /** Writes text to the clipboard, where the platform offers one. */
69
+ export declare function defaultWriteClipboard(text: string): void;
70
+ /**
71
+ * Downloads generated content as a file.
72
+ *
73
+ * Mirrors `streamdown`'s `save()` in revoking the object URL once the click has
74
+ * been dispatched. The other borrowed detail, the UTF-8 BOM for CSV, lives in
75
+ * {@link tableToCsv} instead — see there for why.
76
+ */
77
+ export declare function defaultSaveFile(filename: string, content: string, mimeType: string): void;
78
+ /**
79
+ * A copy or download control drawn in a block's top-right corner.
80
+ *
81
+ * Extends `@vectojs/ui`'s `Button` rather than hand-rolling an a11y hotspot: that
82
+ * class already projects `tag: 'button'` with a label, drives its focus ring from
83
+ * real DOM focus/blur, and handles hover and the disabled state. The repo rule
84
+ * against reimplementing what a `@vectojs/*` package provides applies to
85
+ * affordances too, and a bespoke hotspot would have to re-earn focus-ring
86
+ * behaviour `Button` already has.
87
+ *
88
+ * What this adds is transient success feedback. The label changes to a
89
+ * confirmation for `FEEDBACK_MS` and then reverts, which is the only signal a copy
90
+ * gives — nothing else about the document changes, so without it a reader cannot
91
+ * tell a working control from a broken one.
92
+ */
93
+ export declare class BlockAffordanceButton extends Button {
94
+ private readonly act;
95
+ /** How long the confirmation label stays up, in ms. */
96
+ static readonly FEEDBACK_MS = 1600;
97
+ private readonly restingLabel;
98
+ private readonly successLabel;
99
+ private feedbackTimer;
100
+ constructor(label: string, successLabel: string, act: () => void, opts?: ButtonOptions);
101
+ /**
102
+ * Runs the action, then shows the confirmation.
103
+ *
104
+ * The action runs first and a throw propagates: a clipboard write the browser
105
+ * rejected must not be reported as a success.
106
+ */
107
+ private run;
108
+ private setTransientLabel;
109
+ /**
110
+ * The label a reader hears is the one they see, transient confirmation
111
+ * included, so an AT user gets the same feedback a sighted user does.
112
+ */
113
+ getA11yAttributes(): A11yAttributes;
114
+ /** Clears the pending revert so a destroyed block leaves no timer behind. */
115
+ destroy(): void;
116
+ }
117
+ /**
118
+ * Wraps one block and positions its affordances in the top-right corner.
119
+ *
120
+ * A wrapper is necessary rather than adding the buttons to the block directly,
121
+ * because both candidate parents already own their children's geometry: `Stack`
122
+ * positions each child in flow, and `Table` recomputes `x`/`y`/`width`/`height`
123
+ * for every child from its column widths (`Table.ts:66`). A button added to
124
+ * either would be moved on the next layout. This owns only its own children's
125
+ * placement and delegates its size to the block, so the surrounding document
126
+ * lays out exactly as it did before.
127
+ */
128
+ export declare class BlockWithAffordances extends UIComponent {
129
+ readonly block: Entity;
130
+ private readonly controls;
131
+ /** Gap between the block's edges and the controls, in px. */
132
+ private static readonly INSET;
133
+ /** Gap between adjacent controls, in px. */
134
+ private static readonly GAP;
135
+ constructor(block: Entity, controls: readonly BlockAffordanceButton[]);
136
+ /**
137
+ * Places the controls right-aligned along the block's top edge.
138
+ *
139
+ * Laid out right-to-left from the block's right edge so the first control in
140
+ * the list ends up leftmost, which keeps DOM order (and therefore tab order and
141
+ * the a11y reading order) matching the visual order.
142
+ */
143
+ private layoutAffordances;
144
+ /**
145
+ * Re-places the controls after the block's own box changed.
146
+ *
147
+ * Called by the owner when a block is resized or its content grew; the controls
148
+ * are anchored to the right edge, so a width change moves them.
149
+ */
150
+ refreshAffordances(): void;
151
+ /** The wrapper is a pass-through: its size is the block's size. */
152
+ getLayoutControlledProperties(): ReadonlyArray<LayoutControlledProperty>;
153
+ /**
154
+ * Projected as a group so assistive technology reports one labelled region
155
+ * containing the block and its controls, rather than two unrelated siblings.
156
+ */
157
+ getA11yAttributes(): A11yAttributes;
158
+ render(): void;
159
+ }
160
+ /**
161
+ * Extracts plain-text table content from a `marked` table token.
162
+ *
163
+ * Reads the token rather than the built `Table` entity because the entity holds
164
+ * `RichText` children, not strings — reconstructing cell text from spans would
165
+ * have to reverse the inline formatting, and the token still has the source.
166
+ *
167
+ * `cell.text` is the cell's raw inline Markdown, which is what a copy should
168
+ * preserve: `**bold**` copied out of a table and pasted into another Markdown
169
+ * document should still be bold.
170
+ */
171
+ export declare function tableContentOf(token: {
172
+ header: ReadonlyArray<{
173
+ text: string;
174
+ }>;
175
+ rows: ReadonlyArray<ReadonlyArray<{
176
+ text: string;
177
+ }>>;
178
+ align: readonly TableAlign[];
179
+ }): TableContent;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './Markdown';
2
+ export { BlockAffordanceButton, BlockWithAffordances, escapeCsvField, escapeMarkdownTableCell, extensionForLanguage, mimeForLanguage, tableContentOf, tableToCsv, tableToMarkdown, } from './blockAffordances';
3
+ export type { TableAlign, TableContent } from './blockAffordances';
2
4
  export { parseFrontMatterFields, scanFrontMatter } from './frontMatter';
3
5
  export type { FrontMatterScan } from './frontMatter';
4
6
  export type { IncompleteMarkdownMode, StreamController, StreamControllerOptions, StreamControllerState, StreamPacingOptions, } from './StreamController';