@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 +79 -17
- package/dist/Markdown.d.ts +119 -153
- package/dist/blockAffordances.d.ts +179 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1233 -566
- package/dist/index.mjs +1205 -547
- package/dist/markdown-code.d.ts +102 -0
- package/dist/markdown-entities.d.ts +33 -0
- package/dist/markdown-image.d.ts +126 -0
- package/dist/markdown-inline.d.ts +58 -0
- package/dist/markdown-math.d.ts +169 -0
- package/dist/theme.d.ts +153 -0
- package/package.json +8 -7
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
|
-
|
|
8
|
-
|
|
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
|
-
`
|
|
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
|
-
####
|
|
137
|
+
#### The math engine is loaded on demand
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
against a browser bundle of a consumer that renders
|
|
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
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 `` 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 ");
|
|
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'`.
|
package/dist/Markdown.d.ts
CHANGED
|
@@ -1,167 +1,47 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
*
|
|
18
|
+
* Draw copy / download controls in the top-right corner of code blocks and
|
|
19
|
+
* tables. Default `false`.
|
|
61
20
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
29
|
+
* Writes text to the clipboard for the copy controls.
|
|
100
30
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
-
|
|
111
|
-
getContentProjection(hint?: ContentProjectionHint): ContentProjection | null;
|
|
36
|
+
writeClipboard?: (text: string) => void;
|
|
112
37
|
/**
|
|
113
|
-
*
|
|
38
|
+
* Saves a generated file for the download controls.
|
|
114
39
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
|
|
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';
|