@vectojs/markdown 0.12.0 → 0.14.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.
@@ -32,6 +32,42 @@ export interface MarkdownTheme {
32
32
  /** Base font size in px. */
33
33
  fontSize?: number;
34
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 {
59
+ /**
60
+ * The TeX source, exactly as written between the delimiters.
61
+ *
62
+ * Also the projected text and the accessible name, so this is the one string a
63
+ * reader can find, select, and copy.
64
+ */
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
+ }
35
71
  /**
36
72
  * A single self-rendering entity for fenced code blocks.
37
73
  *
@@ -126,6 +162,34 @@ export interface MarkdownOptions {
126
162
  selectable?: boolean;
127
163
  /** Emit a `vecto:markdown:parse` User Timing measure. Default `false`. */
128
164
  userTiming?: boolean;
165
+ /**
166
+ * Draw copy / download controls in the top-right corner of code blocks and
167
+ * tables. Default `false`.
168
+ *
169
+ * Opt-in rather than on by default because it adds two focusable stops per such
170
+ * block to the tab order, which a document with many code fences would make
171
+ * tedious to navigate past, and because a reader who cannot act on a control
172
+ * (no clipboard permission, no filesystem) is better served by not being
173
+ * offered one.
174
+ */
175
+ blockAffordances?: boolean;
176
+ /**
177
+ * Writes text to the clipboard for the copy controls.
178
+ *
179
+ * Injectable because the real path (`navigator.clipboard.writeText`) is absent
180
+ * in jsdom and, in a browser, rejects a write that did not originate in a user
181
+ * gesture — so a test can only assert the payload, never the platform call.
182
+ * Defaults to `navigator.clipboard.writeText` when available.
183
+ */
184
+ writeClipboard?: (text: string) => void;
185
+ /**
186
+ * Saves a generated file for the download controls.
187
+ *
188
+ * Defaults to an anchor-click download that revokes its object URL. Injectable
189
+ * for the same reason as {@link MarkdownOptions.writeClipboard}: jsdom has no
190
+ * download behaviour to observe.
191
+ */
192
+ saveFile?: (filename: string, content: string, mimeType: string) => void;
129
193
  }
130
194
  /**
131
195
  * Renders Markdown content into a VectoJS entity tree using {@link marked}.
@@ -149,6 +213,18 @@ export declare class Markdown extends UIComponent {
149
213
  theme: Required<MarkdownTheme>;
150
214
  onLinkClick?: (url: string) => void;
151
215
  selectable: boolean;
216
+ /**
217
+ * Whether code blocks and tables carry copy / download controls.
218
+ *
219
+ * Read when a block entity is built, so it affects blocks rendered from here on
220
+ * rather than retroactively; a document does not rebuild to gain or lose an
221
+ * affordance.
222
+ */
223
+ blockAffordances: boolean;
224
+ /** Clipboard writer used by the copy controls. */
225
+ writeClipboard: (text: string) => void;
226
+ /** File saver used by the download controls. */
227
+ saveFile: (filename: string, content: string, mimeType: string) => void;
152
228
  private activeBlockMetrics;
153
229
  /**
154
230
  * Called after a streamed append has re-laid-out the document.
@@ -542,6 +618,28 @@ export declare class Markdown extends UIComponent {
542
618
  * policy for a zero-dimension source is a separate decision from notifying
543
619
  * the scene, which is the actual defect here.
544
620
  */
621
+ /**
622
+ * Wraps a block in its copy / download controls, or returns it untouched.
623
+ *
624
+ * The controls are built lazily through `make` so a document with
625
+ * `blockAffordances` off pays nothing — not the closures, not the measurement
626
+ * `BlockAffordanceButton` does in its constructor.
627
+ */
628
+ private withBlockAffordances;
629
+ /** Copy and download controls for one fenced code block. */
630
+ private codeBlockAffordances;
631
+ /** Copy (as Markdown) and download (as CSV) controls for one table. */
632
+ private tableAffordances;
633
+ /**
634
+ * Button styling for the affordances, derived from the document theme.
635
+ *
636
+ * Themed rather than hardcoded so a light-theme document does not get the dark
637
+ * default palette. `focusColor` is set explicitly from the theme's accent
638
+ * because `Button`'s default cyan is tuned for the dark palette and reads as
639
+ * off-brand elsewhere — while a focus ring is the one affordance a keyboard
640
+ * user cannot do without.
641
+ */
642
+ private affordanceButtonOptions;
545
643
  private paragraphImage;
546
644
  /** One table cell entity, shared by the render arm and the streamed-table path. */
547
645
  private tableCellRichText;
@@ -851,3 +949,4 @@ export declare class Markdown extends UIComponent {
851
949
  /** Structural — children draw themselves. */
852
950
  render(_r: IRenderer): void;
853
951
  }
952
+ 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';