@vectojs/markdown 0.18.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,32 @@ import { type ContentProjection, type ContentProjectionHint, GlyphRasterAtlas, t
2
2
  import { UIComponent } from '@vectojs/ui';
3
3
  import { type MarkdownThemePresetName } from './markdown-presets';
4
4
  import type { MarkdownTheme } from './theme';
5
+ /**
6
+ * Languages this build can highlight, for an app that wants to check before
7
+ * rendering. A language appears here when it has syntax, keywords, or both.
8
+ */
9
+ export declare function highlightedLanguages(): string[];
10
+ /** Optional behaviour for {@link CodeBlock}. */
11
+ export interface CodeBlockOptions {
12
+ /**
13
+ * Draw a header band across the top of the block showing the language name.
14
+ *
15
+ * Off by default, and the default is not merely conservative: the band costs
16
+ * vertical space in every block of a document, and a label is worth that only
17
+ * where a document actually mixes languages. A single-language page gets the
18
+ * same word repeated down its length.
19
+ *
20
+ * Turning it on also RESERVES that space, which is what makes the block's
21
+ * own affordance controls stop overlapping the first line of code — measured
22
+ * before this existed: the controls occupied y 8-32 while line one occupied
23
+ * y 18-42, a 14px overlap in the default theme.
24
+ *
25
+ * The reserved height keeps `height` a pure function of line count (the
26
+ * invariant {@link CodeBlock.setWidth} documents); it only changes the
27
+ * constant term.
28
+ */
29
+ showLanguage?: boolean;
30
+ }
5
31
  /**
6
32
  * A single self-rendering entity for fenced code blocks.
7
33
  *
@@ -10,6 +36,14 @@ import type { MarkdownTheme } from './theme';
10
36
  */
11
37
  export declare class CodeBlock extends UIComponent {
12
38
  private lines;
39
+ /**
40
+ * Lexical state ENTERING each line, index-aligned with {@link lines}.
41
+ *
42
+ * Entering rather than leaving, so a streamed append can resume tokenizing at
43
+ * the prefix-reuse boundary by reading one entry instead of re-scanning the
44
+ * document for an unclosed block comment.
45
+ */
46
+ private lineCarry;
13
47
  private grid;
14
48
  /** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
15
49
  private rawLines;
@@ -17,6 +51,19 @@ export declare class CodeBlock extends UIComponent {
17
51
  private source;
18
52
  /** Bumped by {@link buildLines} and {@link setSelectable}; read by `Scene`. */
19
53
  private contentEpoch;
54
+ /**
55
+ * Horizontal scroll offset in local px, always in `[0, maxScrollX]`.
56
+ *
57
+ * Code does not wrap, so a line wider than the box would otherwise have an
58
+ * unreachable tail. This offset is subtracted from BOTH the painted cell x and
59
+ * the projected line x in the same frame — never one without the other, or the
60
+ * DOM selection carriers detach from the glyphs they are supposed to cover
61
+ * (the defect class `5cf7119` and `ee1de6f` fixed on the vertical axis).
62
+ */
63
+ private scrollXValue;
64
+ /** Memoized widest prepared line, keyed by the grid identity it came from. */
65
+ private contentWidthGrid;
66
+ private contentWidthValue;
20
67
  private lang;
21
68
  private theme;
22
69
  /**
@@ -28,6 +75,10 @@ export declare class CodeBlock extends UIComponent {
28
75
  private pad;
29
76
  private codeFont;
30
77
  selectable: boolean;
78
+ /** Whether the language header band is drawn. See {@link CodeBlockOptions.showLanguage}. */
79
+ private showLanguage;
80
+ /** Font of the header label, resolved once from the theme. */
81
+ private langFont;
31
82
  /**
32
83
  * @param theme Any subset of {@link MarkdownTheme}, or the name of a built-in
33
84
  * preset (see {@link MarkdownThemePresetName}). Accepting a partial theme
@@ -39,7 +90,70 @@ export declare class CodeBlock extends UIComponent {
39
90
  * be constructed directly with a preset name without going through
40
91
  * `Markdown`.
41
92
  */
42
- constructor(code: string, lang: string, maxWidth: number, theme: MarkdownThemePresetName | MarkdownTheme, selectable?: boolean);
93
+ constructor(code: string, lang: string, maxWidth: number, theme: MarkdownThemePresetName | MarkdownTheme, selectable?: boolean, options?: CodeBlockOptions);
94
+ /**
95
+ * The language name shown in the header, or `''` when there is nothing to show.
96
+ *
97
+ * Normalized exactly as the highlighter normalizes its lookup key, so the label
98
+ * and the colouring can never disagree about which language this is: a fence
99
+ * may be written ` ```Bash ` or carry attributes (` ```ts title="a.ts" `), and
100
+ * the label has to be the language, not the raw info string.
101
+ *
102
+ * Lowercased for the same reason `streamdown` lowercases its own
103
+ * (`lib/code-block/header.tsx:15`): the fence's capitalization is incidental,
104
+ * and a document mixing ` ```JS ` with ` ```js ` should not render two
105
+ * different-looking labels for one language.
106
+ */
107
+ private languageLabel;
108
+ /**
109
+ * Height in px of the header band, or `0` when it is off.
110
+ *
111
+ * The label sits in a band of its own rather than floating over the code,
112
+ * because a translucent overlay above real glyphs is unreadable at small sizes
113
+ * and would fight the horizontal scroll: the code slides under it, so any text
114
+ * drawn on top would collide with a different token every frame.
115
+ */
116
+ private headerHeight;
117
+ /**
118
+ * Local y of the first line of code.
119
+ *
120
+ * Everything that positions a row — the painter, the projection, the grid's
121
+ * own origin — goes through this, so the header offset cannot be applied to
122
+ * one and forgotten on another. That class of mismatch is exactly what
123
+ * detaches selection carriers from the glyphs they cover.
124
+ */
125
+ private contentTop;
126
+ /**
127
+ * Current horizontal scroll offset in local px, clamped to what the content
128
+ * currently allows.
129
+ *
130
+ * Clamped on READ, not only on write, because `setWidth()` may shrink the box
131
+ * after a scroll and is contractually forbidden from rebuilding anything. Both
132
+ * the painter and the projection read through here, which is what keeps the
133
+ * glyphs and the selection carriers on the same offset within a frame.
134
+ */
135
+ get scrollX(): number;
136
+ /**
137
+ * Widest line's overflow past the padded box, i.e. the maximum useful
138
+ * {@link scrollX}. `0` when every line already fits.
139
+ */
140
+ get maxScrollX(): number;
141
+ /**
142
+ * Widest prepared line, memoized against the grid that produced it.
143
+ *
144
+ * Read by {@link scrollX}, which both `render()` and `getContentProjection()`
145
+ * call every synced frame, so an O(lines) scan here would be an O(document) cost
146
+ * per frame on a long block — the exact shape the per-line projection window
147
+ * exists to avoid. The grid is rebuilt only when the content changes, so the
148
+ * cache key is identity of the grid object.
149
+ */
150
+ private contentWidth;
151
+ /**
152
+ * Scroll horizontally to `x`, clamped to `[0, maxScrollX]`.
153
+ *
154
+ * @returns `this` for chaining.
155
+ */
156
+ setScrollX(x: number): this;
43
157
  /** Re-parse code content (e.g. for live editing). */
44
158
  setCode(code: string, lang?: string): this;
45
159
  /** Enable or disable browser-native selection for this code block. */
@@ -51,9 +165,13 @@ export declare class CodeBlock extends UIComponent {
51
165
  * Deliberately does **not** rebuild the grid or the highlight, because code does
52
166
  * not reflow: lines are placed on a fixed monospace grid at `col × cellWidth` and
53
167
  * a long line overflows rather than wrapping, so `height` is a function of line
54
- * *count* alone. The width only sizes the rounded background. Anything that would
55
- * change the glyph geometry — the source, the language, the font — goes through
56
- * {@link setCode} and invalidates the grid there.
168
+ * *count* alone. The width sizes the rounded background and the clip. Anything
169
+ * that would change the glyph geometry — the source, the language, the font —
170
+ * goes through {@link setCode} and invalidates the grid there.
171
+ *
172
+ * A narrower box can leave {@link scrollX} past the new end of travel. That is
173
+ * resolved by clamping on read rather than by adjusting anything here, so this
174
+ * method keeps costing nothing.
57
175
  *
58
176
  * @returns `this` for chaining.
59
177
  */
@@ -70,10 +188,23 @@ export declare class CodeBlock extends UIComponent {
70
188
  *
71
189
  * The last previously-seen line is deliberately NOT reused: a chunk usually
72
190
  * lands mid-line, so that line's text (and therefore its tokenization) changes.
191
+ *
192
+ * Prefix reuse survives multi-line constructs because {@link lineCarry} records
193
+ * the state ENTERING each line, so resuming at the reuse boundary needs no
194
+ * rescan: a carried state is a pure function of the preceding text, and that
195
+ * text is byte-identical over the reused prefix by construction.
73
196
  */
74
197
  private buildLines;
75
198
  private ensureGrid;
76
- /** Code blocks are decorative — not interactive. */
199
+ /**
200
+ * Not hit-testable, and deliberately still not `interactive`, even though the
201
+ * block now consumes wheel events to scroll.
202
+ *
203
+ * The wheel arrives from the content-projection div rather than from canvas
204
+ * hit-testing, so no a11y shadow node is needed. Creating one would place a
205
+ * `pointer-events: auto` element above the transparent text mirror and swallow
206
+ * the mousedown that starts a native drag-selection.
207
+ */
77
208
  isPointInside(): boolean;
78
209
  render(r: IRenderer): void;
79
210
  }
package/dist/theme.d.ts CHANGED
@@ -121,6 +121,17 @@ export interface MarkdownTheme {
121
121
  syntaxCommentColor?: string;
122
122
  /** Code-block numeric-literal color. */
123
123
  syntaxNumberColor?: string;
124
+ /**
125
+ * Color of the language name in a code block's header band.
126
+ *
127
+ * Derived from {@link syntaxCommentColor} in {@link resolveTheme} when the
128
+ * caller does not set it. A comment is the one token class already defined as
129
+ * "present but subordinate to the code", which is exactly the label's role, so
130
+ * a theme that tuned its comment color for a given background has already
131
+ * answered this question. A literal default would ignore that and read wrong on
132
+ * every light preset.
133
+ */
134
+ codeLangColor?: string;
124
135
  /**
125
136
  * Enable `markdown-it`-style typographic substitutions: `--`/`---` to en/em
126
137
  * dash, `...` to an ellipsis, `(c)`/`(r)`/`(tm)` to their symbols, `+-` to
@@ -147,6 +158,16 @@ export interface MarkdownTheme {
147
158
  headingSizes?: readonly number[];
148
159
  /** Code-block font size in px. */
149
160
  codeFontSize?: number;
161
+ /**
162
+ * Font size in px of the language name in a code block's header band.
163
+ *
164
+ * Left `undefined` by default and **derived** as `codeFontSize - 3` (clamped
165
+ * to at least 1), following {@link tableFontSize}'s precedent: the label is
166
+ * chrome around the code, so a caller who raises only `codeFontSize` should
167
+ * get a proportionally larger label rather than one that stays put and
168
+ * gradually looks detached from the block it belongs to.
169
+ */
170
+ codeLangFontSize?: number;
150
171
  /**
151
172
  * Table cell font size in px.
152
173
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/markdown",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -48,7 +48,7 @@
48
48
  "marked": "^18.0.7"
49
49
  },
50
50
  "peerDependencies": {
51
- "@vectojs/core": ">=1.25.0 <2.0.0",
51
+ "@vectojs/core": ">=1.34.0 <2.0.0",
52
52
  "@vectojs/ui": ">=2.6.0 <3.0.0"
53
53
  },
54
54
  "devDependencies": {