@vectojs/markdown 0.16.1 → 0.18.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.
@@ -0,0 +1,73 @@
1
+ import type { TokenizerAndRendererExtension, Token } from 'marked';
2
+ /**
3
+ * `markdown-it-abbr`-style abbreviations: `*[HTML]: HyperText Markup Language`
4
+ * defines a term, and every later whole-word occurrence of `HTML` in the
5
+ * document's prose gets a dotted-underline visual treatment.
6
+ *
7
+ * This is architecturally unlike every other `PX-0524` span construct. Every
8
+ * other one (`sub`, `sup`, `ins`, `mark`, `emoji`) is a token-level
9
+ * substitution: the tokenizer sees the construct's own delimiters and emits a
10
+ * styled span right there. An abbreviation definition carries no delimiters at
11
+ * its use site at all — `HTML` in `The HTML spec` is indistinguishable from
12
+ * any other word until the WHOLE document's definitions are known. So this
13
+ * module only collects the dictionary; applying it to prose is
14
+ * `markdown-inline.ts`'s `emitProse` job, which every `collectSpans` leaf now
15
+ * routes through instead of pushing a bare span.
16
+ *
17
+ * ## Why this forces a full-document re-render on change
18
+ *
19
+ * A definition can be written anywhere — GFM allows it inline, but the
20
+ * convention (and this module's only shape) is one line, and streamed
21
+ * documents commonly put them at the end, same as footnotes. If a definition
22
+ * arrives in the incremental-lex TAIL while the term it defines already
23
+ * rendered in the STABLE PREFIX, the prefix's paragraph entities were built by
24
+ * `collectSpans` before the dictionary contained that term, and `Markdown.ts`'s
25
+ * `updateTokens` reuses an entity whenever its token's `raw` is byte-identical
26
+ * — which it is, since the term's OWN paragraph did not change, only a
27
+ * definition elsewhere did. This is exactly `hasLinkDefinitions`'s own
28
+ * problem (a reference definition arriving late must retroactively change
29
+ * already-rendered inline tokens) and gets the same fix at the same layer:
30
+ * `Markdown.ts` tracks the resolved dictionary across renders and forces a full
31
+ * rebuild — not merely a full re-LEX, which `incrementalLex.ts` already gets
32
+ * right regardless — whenever it changes. See `Markdown.ts`'s
33
+ * `abbreviationsChanged` check next to `updateTokens`.
34
+ */
35
+ /** `*[TERM]: definition` — collected metadata, not rendered content. */
36
+ export interface AbbrDefToken {
37
+ type: 'abbrDef';
38
+ raw: string;
39
+ /** The term exactly as written between `*[` and `]`, e.g. `HTML`. */
40
+ term: string;
41
+ /** The definition text, shown as the term's tooltip title in prose. */
42
+ definition: string;
43
+ }
44
+ /**
45
+ * The single `marked.use` block extension, shared between `Markdown.ts` and
46
+ * `MarkdownWorker.ts` — same lockstep requirement `markdown-footnote.ts`
47
+ * documents at length: the two lexers must agree exactly.
48
+ *
49
+ * No `start()`, for the same reason `footnoteDef` supplies none: a block
50
+ * `start()` retroactively re-groups paragraphs already emitted (see
51
+ * `markdown-footnote.ts`'s measured evidence). Its absence costs the same one
52
+ * spec-adjacent behaviour footnotes already accept — a definition on the line
53
+ * directly after a paragraph with no blank line between is absorbed into that
54
+ * paragraph rather than recognised.
55
+ */
56
+ export declare const ABBR_EXTENSIONS: TokenizerAndRendererExtension[];
57
+ /**
58
+ * Whether `text` contains an abbreviation-definition line at all.
59
+ *
60
+ * Cheap reject first (`*[` is rare outside this construct), matching every
61
+ * other opener-detector in this package (`hasContainerOpener`,
62
+ * `hasFootnoteDefOpener`).
63
+ */
64
+ export declare function hasAbbrDef(text: string): boolean;
65
+ /**
66
+ * Scan top-level tokens for `abbrDef` entries and build the term dictionary.
67
+ *
68
+ * Later definitions of the same term win — the same "last write wins" rule
69
+ * `marked` itself applies to duplicate link reference definitions — rather
70
+ * than throwing or silently keeping the first, since a streamed document has
71
+ * no way to reject a correction after the fact.
72
+ */
73
+ export declare function collectAbbreviations(tokens: readonly Token[]): ReadonlyMap<string, string>;
@@ -1,6 +1,7 @@
1
1
  import { type ContentProjection, type ContentProjectionHint, GlyphRasterAtlas, type GlyphRasterAtlasStats, IRenderer } from '@vectojs/core';
2
2
  import { UIComponent } from '@vectojs/ui';
3
- import { type MarkdownTheme } from './theme';
3
+ import { type MarkdownThemePresetName } from './markdown-presets';
4
+ import type { MarkdownTheme } from './theme';
4
5
  /**
5
6
  * A single self-rendering entity for fenced code blocks.
6
7
  *
@@ -28,14 +29,17 @@ export declare class CodeBlock extends UIComponent {
28
29
  private codeFont;
29
30
  selectable: boolean;
30
31
  /**
31
- * @param theme Any subset of {@link MarkdownTheme}; missing keys fall back to
32
- * `DEFAULT_THEME` in `./theme`. Accepting a partial theme keeps callers that were
33
- * written against an earlier, smaller `MarkdownTheme` working — this class
34
- * is public API, and a hand-built theme literal would otherwise start
35
- * throwing `lineHeight must be a positive finite number` the moment a new
36
- * size key was added.
32
+ * @param theme Any subset of {@link MarkdownTheme}, or the name of a built-in
33
+ * preset (see {@link MarkdownThemePresetName}). Accepting a partial theme
34
+ * keeps callers that were written against an earlier, smaller
35
+ * `MarkdownTheme` working — this class is public API, and a hand-built
36
+ * theme literal would otherwise start throwing
37
+ * `lineHeight must be a positive finite number` the moment a new size key
38
+ * was added. Resolved through {@link resolvePresetTheme} so `CodeBlock` can
39
+ * be constructed directly with a preset name without going through
40
+ * `Markdown`.
37
41
  */
38
- constructor(code: string, lang: string, maxWidth: number, theme: MarkdownTheme, selectable?: boolean);
42
+ constructor(code: string, lang: string, maxWidth: number, theme: MarkdownThemePresetName | MarkdownTheme, selectable?: boolean);
39
43
  /** Re-parse code content (e.g. for live editing). */
40
44
  setCode(code: string, lang?: string): this;
41
45
  /** Enable or disable browser-native selection for this code block. */
@@ -0,0 +1,85 @@
1
+ import type { Token, TokenizerAndRendererExtension } from 'marked';
2
+ /**
3
+ * `:::` fenced containers (`:::note … :::`, `:::warning … :::`), the
4
+ * `remark-directive`/`markdown-it-container` construct.
5
+ *
6
+ * One new `marked.use` **block** extension. Nothing in `marked`'s grammar,
7
+ * including GFM, tokenizes a `:::` fence at all — verified against
8
+ * marked@18.0.7: it falls through to plain `paragraph`/`text` (`PX-0524`).
9
+ *
10
+ * Registered in `Markdown.ts` and `MarkdownWorker.ts` from this single shared
11
+ * array, for the reason `markdown-footnote.ts` gives at length: the two
12
+ * lexers must agree exactly, or the worker emits tokens the renderer has no
13
+ * arm for.
14
+ *
15
+ * Deliberately holds no entity, theme or `@vectojs/*` import, so this module
16
+ * stays safe to inline into the worker bundle (`scripts/build-worker.js`,
17
+ * `bundle: true`).
18
+ */
19
+ /** A `:::kind\n…body…\n:::` block, its body block-lexed like a blockquote's. */
20
+ export interface ContainerToken {
21
+ type: 'container';
22
+ raw: string;
23
+ /**
24
+ * The word after `:::` on the opening line, e.g. `'note'`. `undefined` for
25
+ * a bare `:::` with nothing after it — a container with no declared kind
26
+ * still fences its content, it just has no theme-mapped colour or label.
27
+ */
28
+ kind?: string;
29
+ /** Nested block tokens, exactly as `blockquote`'s `tokens` field. */
30
+ tokens: Token[];
31
+ }
32
+ /**
33
+ * The two extensions, in the order they are registered.
34
+ *
35
+ * ## No `start()`, for the same reason `footnoteDef` has none
36
+ *
37
+ * A block `start()` clips the text handed to the paragraph tokenizer and
38
+ * retroactively re-groups an already-emitted paragraph the moment a `:::`
39
+ * appears anywhere ahead — `markdown-footnote.ts`'s doc comment measures this
40
+ * exhaustively for `[^`, and the same `Lexer.blockTokens` mechanism applies
41
+ * here verbatim. Omitting it costs the same one CommonMark-adjacent
42
+ * behaviour footnotes already give up: a fence written directly after a
43
+ * paragraph line, with no blank line between, is absorbed into that
44
+ * paragraph instead of opening a container. Verified empirically against
45
+ * marked@18.0.7.
46
+ *
47
+ * ## The forward-reach hazard this construct DOES have
48
+ *
49
+ * Unlike `footnoteDef` (single-line, cannot reach past its own token) but
50
+ * LIKE `blockMath`, an unterminated `:::` opener can absorb arbitrarily much
51
+ * of the document once its closing fence eventually arrives — the tokenizer
52
+ * below scans forward for a depth-balanced close with no bound on how far. Measured: a
53
+ * prefix ending in an open `:::note\nHello` block-lexes to
54
+ * `[paragraph, space, paragraph]` (an ordinary unterminated-fence-reads-as-
55
+ * text fallback), and appending `\n\nWorld\n:::\n` collapses ALL of it into a
56
+ * single `container` token whose nested tokens are `[paragraph('Hello'),
57
+ * space, paragraph('World')]` — the same forward-reach shape `blockMath`'s
58
+ * `$$` used to have. `incrementalLex.ts`'s `hasContainerOpener()` therefore
59
+ * degrades an instance outright, using the same `OPEN_RE` this module exports
60
+ * for that check to share the exact definition of "a fence is open" with the
61
+ * tokenizer.
62
+ *
63
+ * Note `blockMath` no longer degrades: its tokenizer now stops at a blank line,
64
+ * so its forward reach is bounded and its backward reach is handled by
65
+ * `paragraphPairCap`. A `:::` fence still spans blank lines by design — that is
66
+ * the whole point of a container — so the forward hazard is real here and the
67
+ * blanket degrade stays.
68
+ *
69
+ * ## Why an extension is enough
70
+ *
71
+ * `marked.use` extensions run BEFORE the built-in tokenizers (`Lexer.use`
72
+ * inserts with `unshift`), so `container` claims the `:::` line ahead of the
73
+ * built-in `paragraph`/`text` rules — the same ordering `footnoteRef`/
74
+ * `footnoteDef` rely on, contradicting the earlier (wrong) assumption
75
+ * recorded in `PX-0517`/`DEC-01KZDGBE`.
76
+ *
77
+ * ## `renderer` is required but unreachable
78
+ *
79
+ * `marked.use` demands one for a custom token, and `@vectojs/markdown` never
80
+ * calls `marked.parse` — it renders from the token tree. Returning `raw`
81
+ * matches every other extension in this package.
82
+ */
83
+ export declare const CONTAINER_EXTENSIONS: TokenizerAndRendererExtension[];
84
+ /** Whether `text` contains a `:::` opener that {@link OPEN_RE} would match. */
85
+ export declare function hasContainerOpener(text: string): boolean;
@@ -0,0 +1,39 @@
1
+ import type { TokenizerAndRendererExtension } from 'marked';
2
+ /**
3
+ * GitHub-style emoji shortcodes: `:wink:` -> 😉.
4
+ *
5
+ * A new `marked.use` inline extension, the same shape as
6
+ * `markdown-superscript.ts`'s `sup`: `:name:` is not tokenized by marked's
7
+ * built-in grammar at all (verified against marked@18.0.7 — falls through to
8
+ * plain `text`, `PX-0524`), so it needs its own tokenizer.
9
+ *
10
+ * Unlike `sup`/`ins`/`mark`, the token carries no NEW rendering behaviour —
11
+ * once resolved, a shortcode is exactly a run of plain text (the emoji
12
+ * character itself), which the browser's font stack already shapes and colors
13
+ * via `fillText` like any other codepoint. So this module's only real content
14
+ * is the lookup table; `collectSpans`' `emoji` arm just pushes the resolved
15
+ * character with the inherited style, unchanged.
16
+ *
17
+ * Registered in `Markdown.ts` and `MarkdownWorker.ts` from this single shared
18
+ * array, for the reason `markdown-footnote.ts` gives at length: the two lexers
19
+ * must agree exactly, or the worker emits tokens the renderer has no arm for.
20
+ *
21
+ * Deliberately holds no entity, theme or `@vectojs/*` import, so this module
22
+ * stays safe to inline into the worker bundle (`scripts/build-worker.js`,
23
+ * `bundle: true`).
24
+ */
25
+ /** `:name:`, as it appears mid-sentence, already resolved to its character. */
26
+ export interface EmojiToken {
27
+ type: 'emoji';
28
+ raw: string;
29
+ /** The resolved emoji character(s), e.g. `'😉'` — never the shortcode text. */
30
+ text: string;
31
+ }
32
+ /**
33
+ * Shortcode -> emoji character, a representative common subset of GitHub's
34
+ * table (github/gemoji), not the full ~1800-entry set. Extending this table
35
+ * only ever adds a lookup entry; it never touches the tokenizer or
36
+ * `collectSpans`, so growing it later is a one-line-per-emoji change.
37
+ */
38
+ export declare const EMOJI_MAP: Readonly<Record<string, string>>;
39
+ export declare const EMOJI_EXTENSIONS: TokenizerAndRendererExtension[];
@@ -31,3 +31,21 @@ export declare class MarkdownContainer extends Entity {
31
31
  isPointInside(_globalX: number, _globalY: number): boolean;
32
32
  render(_r: any): void;
33
33
  }
34
+ /**
35
+ * A rounded-rect background fill for a `:::` container's full content area.
36
+ *
37
+ * A separate leaf rather than folding the fill into {@link QuoteBorder}: a
38
+ * blockquote's accent bar is the ONLY visual besides its text, but a container
39
+ * additionally has a background — real callout components (Docusaurus
40
+ * admonitions, mdBook, GitHub's `[!NOTE]`) all paint one — and the two need
41
+ * independent geometry (the fill spans the full width, the bar is a narrow
42
+ * strip at `x=0`), so one entity cannot own both a `width`/`height` box and a
43
+ * bar `width`.
44
+ */
45
+ export declare class ContainerBackground extends Entity {
46
+ color: string;
47
+ radius: number;
48
+ constructor(w: number, h: number, color: string, radius: number);
49
+ isPointInside(): boolean;
50
+ render(r: IRenderer): void;
51
+ }
@@ -0,0 +1,159 @@
1
+ import type { Entity } from '@vectojs/core';
2
+ import type { MarkdownTheme } from './theme';
3
+ /**
4
+ * Fenced block renderer registry: pluggable rendering for code fences keyed by
5
+ * info string.
6
+ *
7
+ * Each language (code, math, mermaid, graphviz, …) is a plugin rather than a
8
+ * branch in `Markdown.renderToken`. Renderers lazy-load on demand and cache
9
+ * conversions, reusing the pattern math already established.
10
+ *
11
+ * ## Lifecycle
12
+ *
13
+ * A renderer may be in one of three states:
14
+ * - **incomplete**: The renderer is registered but its module/assets are not
15
+ * loaded yet. The first call to `render()` triggers the load.
16
+ * - **ready**: The renderer is loaded and can render synchronously.
17
+ * - **error**: The renderer failed to load or encountered an unrecoverable error.
18
+ * Falls back to default code block rendering.
19
+ *
20
+ * ## Fallback
21
+ *
22
+ * When no renderer is registered for a language, or when a renderer returns
23
+ * `null`, the registry falls back to the default code block renderer. This
24
+ * preserves backward compatibility and degrades gracefully.
25
+ *
26
+ * ## Example
27
+ *
28
+ * ```typescript
29
+ * import { FencedBlockRegistry } from '@vectojs/markdown';
30
+ *
31
+ * // Register a custom renderer
32
+ * FencedBlockRegistry.register('mermaid', {
33
+ * async load() {
34
+ * const mermaid = await import('mermaid');
35
+ * return (source, lang, options) => {
36
+ * // ... render logic
37
+ * return entity;
38
+ * };
39
+ * }
40
+ * });
41
+ *
42
+ * // Unregister (for testing or cleanup)
43
+ * FencedBlockRegistry.unregister('mermaid');
44
+ * ```
45
+ */
46
+ /**
47
+ * Options passed to a fenced block renderer.
48
+ *
49
+ * Includes the theme, available width, and whether text should be selectable.
50
+ * Renderers may ignore options that don't apply to their output format.
51
+ */
52
+ export interface FencedBlockRenderOptions {
53
+ /** The current Markdown theme (colors, fonts, sizes). */
54
+ theme: MarkdownTheme;
55
+ /** Available horizontal space in pixels. */
56
+ availableWidth: number;
57
+ /** Whether text content should be selectable. */
58
+ selectable: boolean;
59
+ }
60
+ /**
61
+ * A fenced block renderer: converts source code in a specific language to an Entity.
62
+ *
63
+ * Returns `null` when rendering fails or the source is invalid. The caller falls
64
+ * back to a default code block.
65
+ */
66
+ export type FencedBlockRenderer = (source: string, lang: string, options: FencedBlockRenderOptions) => Entity | null;
67
+ /**
68
+ * A lazy-loadable renderer specification.
69
+ *
70
+ * The `load()` method is called once, the first time a fence with this language
71
+ * appears. It should return a synchronous renderer function, or `null` if loading
72
+ * fails. Failures are logged but swallowed — the fence renders as a code block.
73
+ */
74
+ export interface FencedBlockRendererSpec {
75
+ /**
76
+ * Load the renderer asynchronously.
77
+ *
78
+ * Called exactly once, the first time a fence with this language is encountered.
79
+ * Returns a synchronous renderer, or `null` on failure. The promise rejection
80
+ * is caught and logged; renderers must not leave unhandled rejections.
81
+ */
82
+ load: () => Promise<FencedBlockRenderer | null>;
83
+ }
84
+ /**
85
+ * Register a lazy-loadable fenced block renderer for a language.
86
+ *
87
+ * @param lang - The language identifier (case-insensitive). Normalized to lowercase.
88
+ * @param spec - The renderer specification, with a `load()` method.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * FencedBlockRegistry.register('mermaid', {
93
+ * async load() {
94
+ * const mermaid = await import('mermaid');
95
+ * return (source, lang, options) => {
96
+ * // ... render Mermaid diagram
97
+ * return entity;
98
+ * };
99
+ * }
100
+ * });
101
+ * ```
102
+ */
103
+ export declare function registerFencedBlockRenderer(lang: string, spec: FencedBlockRendererSpec): void;
104
+ /**
105
+ * Unregister a fenced block renderer.
106
+ *
107
+ * Used for testing (sabotage tests) and cleanup. After unregistering, fences with
108
+ * this language fall back to the default code block renderer.
109
+ *
110
+ * @param lang - The language identifier (case-insensitive).
111
+ */
112
+ export declare function unregisterFencedBlockRenderer(lang: string): void;
113
+ /**
114
+ * Check if a renderer is registered for a language.
115
+ *
116
+ * Returns `true` if a renderer is registered, regardless of whether it has loaded yet.
117
+ *
118
+ * @param lang - The language identifier (case-insensitive).
119
+ */
120
+ export declare function hasFencedBlockRenderer(lang: string): boolean;
121
+ /**
122
+ * Check if a renderer is ready (loaded and available for synchronous rendering).
123
+ *
124
+ * Returns `false` if the renderer is not registered, not loaded yet, or failed to load.
125
+ *
126
+ * @param lang - The language identifier (case-insensitive).
127
+ */
128
+ export declare function isFencedBlockRendererReady(lang: string): boolean;
129
+ /**
130
+ * Begin (or join) loading a fenced block renderer.
131
+ *
132
+ * Idempotent: the load promise is cached, so multiple callers share one load.
133
+ * Failures are swallowed — the renderer simply stays unavailable.
134
+ *
135
+ * Call this when a fence with this language first appears, even while it is still
136
+ * open (incomplete). This prefetches the module so the closing fence can render
137
+ * synchronously, hiding the lazy load during a stream.
138
+ *
139
+ * @param lang - The language identifier (case-insensitive).
140
+ * @returns A promise that resolves when the renderer is ready, or immediately if
141
+ * already loaded.
142
+ */
143
+ export declare function ensureFencedBlockRenderer(lang: string): Promise<void>;
144
+ /**
145
+ * Render a fenced code block using the registered renderer for its language.
146
+ *
147
+ * Returns `null` if:
148
+ * - No renderer is registered for this language
149
+ * - The renderer is not loaded yet
150
+ * - The renderer returned `null` (rendering failed)
151
+ *
152
+ * The caller falls back to the default code block renderer in all three cases.
153
+ *
154
+ * @param source - The source code inside the fence.
155
+ * @param lang - The language identifier (case-insensitive).
156
+ * @param options - Rendering options (theme, width, selectable).
157
+ * @returns The rendered entity, or `null` to fall back to default code block.
158
+ */
159
+ export declare function renderFencedBlock(source: string, lang: string, options: FencedBlockRenderOptions): Entity | null;
@@ -1,4 +1,4 @@
1
- import type { TokenizerAndRendererExtension } from 'marked';
1
+ import type { Token, TokenizerAndRendererExtension } from 'marked';
2
2
  /**
3
3
  * GFM footnotes: the two `marked` tokenizer extensions, their token types, and
4
4
  * the marker text a reference renders as.
@@ -32,15 +32,45 @@ export interface FootnoteRefToken {
32
32
  /** The label exactly as written between `[^` and `]`, e.g. `1` or `note`. */
33
33
  label: string;
34
34
  }
35
- /** A footnote definition line — `[^1]: The note.` — as its own block. */
35
+ /**
36
+ * A footnote definition — `[^1]: The note.` — as its own block, optionally
37
+ * followed by indented continuation lines that extend the body across
38
+ * multiple paragraphs (`markdown-it`'s and GFM's own shape for this).
39
+ */
36
40
  export interface FootnoteDefToken {
37
41
  type: 'footnoteDef';
38
42
  raw: string;
39
43
  /** The label exactly as written, matching a {@link FootnoteRefToken}'s. */
40
44
  label: string;
41
- /** The note body, source text with inline markup left unparsed. */
45
+ /** The header line's body text, source text with inline markup unparsed. */
42
46
  body: string;
47
+ /**
48
+ * Block tokens for any indented continuation content after the header line
49
+ * (further paragraphs, lists, code, …), block-lexed exactly like a
50
+ * blockquote's `tokens`. Empty when the definition is single-line, which is
51
+ * the overwhelming majority — a definition is only ever multi-paragraph when
52
+ * the source actually indents a second block under it.
53
+ */
54
+ tokens: Token[];
43
55
  }
56
+ /**
57
+ * Whether `text` contains a `[^label]:` header that {@link HEADER_RE} would
58
+ * match, ANYWHERE in the document — the exact condition under which
59
+ * {@link consumeContinuation} can still be scanning forward (an open
60
+ * continuation) when more text arrives.
61
+ *
62
+ * Used by `incrementalLex.ts`'s degrade check, mirroring `hasContainerOpener`:
63
+ * a footnote definition's continuation-consuming tokenizer has the exact same
64
+ * forward-reach hazard a `:::` fence does, now that it can span a blank line.
65
+ * (`blockMath` used to be the third member of this set; its tokenizer now stops
66
+ * at a blank line, so it no longer degrades — see `paragraphPairCap`.) Deliberately does not try to determine
67
+ * whether a SPECIFIC header's continuation is still open — that would need to
68
+ * replicate the tokenizer's own scan — and instead degrades on the mere
69
+ * presence of any header, which is safe (if conservative) the same way
70
+ * `hasContainerOpener` accepts matching inside a fenced code block it would
71
+ * never actually reach.
72
+ */
73
+ export declare function hasFootnoteDefOpener(text: string): boolean;
44
74
  /**
45
75
  * The two extensions, in the order they are registered.
46
76
  *
@@ -13,6 +13,35 @@ import type { MarkdownTheme } from './theme';
13
13
  */
14
14
  /** Decode basic HTML entities that `marked` emits in token text. */
15
15
  export declare function decodeEntities(text: string): string;
16
+ /**
17
+ * `markdown-it`'s `typographer` substitutions: dashes, ellipsis, trademark
18
+ * symbols, and quote pairs contained within one run of prose.
19
+ *
20
+ * Off by default (`theme.typographer`), matching markdown-it's own default —
21
+ * these are characters the author did not literally type, so applying them
22
+ * unconditionally would silently rewrite a document's source.
23
+ *
24
+ * ## Quote pairing is INTRA-RUN only
25
+ *
26
+ * `"quoted"` becomes curly only when both its opening and closing `"` are in
27
+ * the SAME decoded text run. A quote that spans an inline-markup boundary
28
+ * (`"quoted *emphasis* text"` splits into three text tokens around the `em`)
29
+ * is not paired across that boundary and stays straight. `collectSpans`
30
+ * recurses per-token with no shared mutable state across siblings, and
31
+ * markdown-it's own quote rule needs exactly that — a per-paragraph
32
+ * open/close stack — to pair nested and cross-boundary quotes. Threading that
33
+ * state through this module's recursion is a materially larger change,
34
+ * deferred until a real document exercises the gap.
35
+ *
36
+ * ## Apostrophes vs. quote delimiters
37
+ *
38
+ * A `'` between two letters (`it's`, `y'all`) is a contraction, not a quote
39
+ * delimiter, and is replaced with a closing curly quote BEFORE quote pairing
40
+ * runs — otherwise `it's fine, 'nice' day` would pair the apostrophe in
41
+ * `it's` with the opening `'` of `'nice'`, consuming both instead of matching
42
+ * `'nice'` on its own.
43
+ */
44
+ export declare function applyTypography(text: string): string;
16
45
  /**
17
46
  * Recursively walk the inline token tree, accumulating {@link StyledSpan}s
18
47
  * with inherited style overrides (bold, italic, etc.).
@@ -26,7 +55,14 @@ export declare function collectSpans(tokens: Token[], inherited: TextStyle, them
26
55
  * its size in its `font` string rather than in any span style, so it cannot be
27
56
  * recovered from `inherited`.
28
57
  */
29
- blockFontSize?: number): void;
58
+ blockFontSize?: number,
59
+ /**
60
+ * The document's `*[TERM]: definition` dictionary, applied to every prose
61
+ * leaf via {@link emitProse}. Defaults to {@link NO_ABBREVIATIONS} so every
62
+ * call site that has none to thread — nested recursive calls, and callers
63
+ * predating this feature — costs nothing extra.
64
+ */
65
+ abbr?: ReadonlyMap<string, string>): void;
30
66
  /**
31
67
  * One trailing inline construct that has opened but not closed yet.
32
68
  *
@@ -55,4 +91,6 @@ export interface UnclosedInline {
55
91
  */
56
92
  export declare function findUnclosedInline(text: string): UnclosedInline | null;
57
93
  /** Parse inline markdown tokens and produce a {@link RichText} entity. */
58
- export declare function renderInlineToRichText(tokens: Token[] | undefined, fallbackText: string, font: string, color: string, maxWidth: number, theme: Required<MarkdownTheme>, selectable: boolean, onLinkClick?: (url: string) => void): RichText;
94
+ export declare function renderInlineToRichText(tokens: Token[] | undefined, fallbackText: string, font: string, color: string, maxWidth: number, theme: Required<MarkdownTheme>, selectable: boolean, onLinkClick?: (url: string) => void,
95
+ /** The document's `*[TERM]: definition` dictionary — see {@link emitProse}. */
96
+ abbr?: ReadonlyMap<string, string>): RichText;
@@ -0,0 +1,33 @@
1
+ import type { TokenizerAndRendererExtension } from 'marked';
2
+ /**
3
+ * `markdown-it`-style insert and highlight: `++inserted++`, `==marked==`.
4
+ *
5
+ * Two new `marked.use` inline extensions, the same shape as
6
+ * `markdown-superscript.ts`'s `sup`: neither `+` nor `==` is tokenized by
7
+ * marked's built-in grammar at all (verified against marked@18.0.7 — both fall
8
+ * through to plain `text`, `PX-0524`), so each needs its own tokenizer rather
9
+ * than reusing an existing token type the way single-tilde subscript reuses
10
+ * `del`.
11
+ *
12
+ * Registered in `Markdown.ts` and `MarkdownWorker.ts` from this single shared
13
+ * array, for the reason `markdown-footnote.ts` gives at length: the two lexers
14
+ * must agree exactly, or the worker emits tokens the renderer has no arm for.
15
+ *
16
+ * Deliberately holds no entity, theme or `@vectojs/*` import, so this module
17
+ * stays safe to inline into the worker bundle (`scripts/build-worker.js`,
18
+ * `bundle: true`).
19
+ */
20
+ /** `++content++`, as it appears mid-sentence. See `SuperscriptToken` for why
21
+ * this carries `text` rather than a distinct field name. */
22
+ export interface InsToken {
23
+ type: 'ins';
24
+ raw: string;
25
+ text: string;
26
+ }
27
+ /** `==content==`, as it appears mid-sentence. */
28
+ export interface MarkToken {
29
+ type: 'mark';
30
+ raw: string;
31
+ text: string;
32
+ }
33
+ export declare const INS_MARK_EXTENSIONS: TokenizerAndRendererExtension[];
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Named theme presets for `@vectojs/markdown`.
3
+ *
4
+ * Each preset is a full {@link MarkdownTheme} partial whose color tokens are
5
+ * sourced verbatim from the palette's upstream specification:
6
+ *
7
+ * - **`githubDark`** — GitHub Dark Default (Primer dark palette).
8
+ * Source: `primer/github-vscode-theme` / `rouge-ruby/rouge` Primer primitives.
9
+ * - **`githubLight`** — GitHub Light Default (Primer light palette).
10
+ * Source: same — P_RED_5, P_BLUE_6/P_BLUE_8, P_GRAY_5, P_GRAY_9, canvas tokens.
11
+ * - **`dracula`** — Dracula Classic.
12
+ * Source: https://draculatheme.com/spec and https://github.com/dracula/dracula-theme
13
+ * - **`solarizedDark`** — Solarized dark mode.
14
+ * Source: https://ethanschoonover.com/solarized/ (L*a*b canonical, sRGB values).
15
+ * - **`solarizedLight`** — Solarized light mode.
16
+ * Source: same palette, base-pair swapped per Schoonover's own CSS snippet
17
+ * (light: `base3:base0` background/foreground, `base2` background highlight,
18
+ * `base1` comments; dark: `base03:base0`, `base02`, `base01`).
19
+ *
20
+ * Spacing and typography keys are NOT set in presets: they remain at
21
+ * {@link DEFAULT_THEME} defaults so a caller who passes `theme: 'dracula'`
22
+ * gets Dracula colors on a correctly-spaced layout without having to restate
23
+ * every dimension.
24
+ *
25
+ * Light presets apply a CONTRAST PASS on translucent overlays
26
+ * (`codeBgColor`, `tableHeaderBgColor`, `containerBgColor`, `markHighlightColor`,
27
+ * `hrColor`): the stock theme is dark and its translucent values composite
28
+ * against a near-black surface. On a white/cream background the same RGBA
29
+ * values produce the wrong visual weight, so light presets use opaque or
30
+ * differently-composited colors derived from the palette instead.
31
+ */
32
+ import { type MarkdownTheme } from './theme';
33
+ /** Names of the built-in theme presets. */
34
+ export type MarkdownThemePresetName = 'githubDark' | 'githubLight' | 'dracula' | 'solarizedDark' | 'solarizedLight';
35
+ /** Look-up table from preset name to its partial {@link MarkdownTheme}. */
36
+ export declare const PRESET_THEMES: Readonly<Record<MarkdownThemePresetName, MarkdownTheme>>;
37
+ /**
38
+ * Return `true` if `value` is a recognised {@link MarkdownThemePresetName}.
39
+ *
40
+ * Use this to branch on `theme?: MarkdownThemePresetName | MarkdownTheme`
41
+ * before calling {@link resolvePresetTheme}.
42
+ */
43
+ export declare function isPresetName(value: unknown): value is MarkdownThemePresetName;
44
+ /**
45
+ * Resolve a `theme` option that may be a preset name, a full/partial
46
+ * {@link MarkdownTheme}, or `undefined`, into a `Required<MarkdownTheme>`.
47
+ *
48
+ * Always calls {@link resolveTheme} rather than spreading {@link PRESET_THEMES}
49
+ * or `DEFAULT_THEME` directly, so the derived keys (`tableFontSize` from
50
+ * `fontSize`, `quoteTextColor`/`footnoteColor` from `textColor`/`linkColor`)
51
+ * still apply on top of a preset exactly as they do for a hand-written theme —
52
+ * a preset that sets `linkColor` but not `footnoteColor` should still get a
53
+ * footnote marker in the preset's link color, not the stock accent.
54
+ *
55
+ * A plain `MarkdownTheme` object is passed straight to `resolveTheme` unchanged
56
+ * (this is the pre-existing, non-preset path); a preset name is looked up in
57
+ * {@link PRESET_THEMES} first.
58
+ */
59
+ export declare function resolvePresetTheme(theme?: MarkdownThemePresetName | MarkdownTheme): Required<MarkdownTheme>;
@@ -0,0 +1,36 @@
1
+ import type { TokenizerAndRendererExtension } from 'marked';
2
+ /**
3
+ * `markdown-it`-style superscript: `19^th^`, `x^2^`.
4
+ *
5
+ * A new `marked.use` inline extension, unlike subscript — single-tilde already
6
+ * lexed to a real token (`del`) that `collectSpans` merely had to recognise by
7
+ * `raw`. Nothing in `marked`'s built-in grammar produces a token for `^…^` at
8
+ * all: it falls through to plain `text`, verified against marked@18.0.7 in
9
+ * `PX-0524`. So superscript needs its own tokenizer, the same shape as
10
+ * `markdown-footnote.ts`'s `footnoteRef`.
11
+ *
12
+ * Registered in `Markdown.ts` and `MarkdownWorker.ts` from this single shared
13
+ * array, for the reason `markdown-footnote.ts` gives at length: the two lexers
14
+ * must agree exactly, or the worker emits tokens the renderer has no arm for.
15
+ *
16
+ * Deliberately holds no entity, theme or `@vectojs/*` import, so this module
17
+ * stays safe to inline into the worker bundle (`scripts/build-worker.js`,
18
+ * `bundle: true`).
19
+ */
20
+ /**
21
+ * `^content^`, as it appears mid-sentence.
22
+ *
23
+ * Carries `text` rather than a distinct field name — unlike
24
+ * {@link import('./markdown-footnote').FootnoteRefToken}, which avoids `text`
25
+ * so an unhandled token falls through to nothing rather than to a wrong
26
+ * render. A superscript token IS mostly text, just raised: `producesEntity`'s
27
+ * `'text' in token` fallback rendering it as an ordinary block if this arm were
28
+ * ever missing is a reasonable enough degradation (plain, unraised text) that
29
+ * the distinct-name defence footnotes need does not apply here.
30
+ */
31
+ export interface SuperscriptToken {
32
+ type: 'sup';
33
+ raw: string;
34
+ text: string;
35
+ }
36
+ export declare const SUPERSCRIPT_EXTENSIONS: TokenizerAndRendererExtension[];