@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.
- package/dist/Markdown.d.ts +26 -1
- package/dist/MarkdownWorkerSource.d.ts +1 -1
- package/dist/incrementalLex.d.ts +55 -17
- package/dist/index.js +1026 -75
- package/dist/index.mjs +1016 -74
- package/dist/markdown-abbr.d.ts +73 -0
- package/dist/markdown-code.d.ts +12 -8
- package/dist/markdown-container.d.ts +85 -0
- package/dist/markdown-emoji.d.ts +39 -0
- package/dist/markdown-entities.d.ts +18 -0
- package/dist/markdown-fenced-registry.d.ts +159 -0
- package/dist/markdown-footnote.d.ts +33 -3
- package/dist/markdown-inline.d.ts +40 -2
- package/dist/markdown-ins-mark.d.ts +33 -0
- package/dist/markdown-presets.d.ts +59 -0
- package/dist/markdown-superscript.d.ts +36 -0
- package/dist/markdown-typography.d.ts +72 -0
- package/dist/theme.d.ts +127 -0
- package/package.json +2 -2
|
@@ -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>;
|
package/dist/markdown-code.d.ts
CHANGED
|
@@ -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
|
|
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}
|
|
32
|
-
*
|
|
33
|
-
* written against an earlier, smaller
|
|
34
|
-
* is public API, and a hand-built
|
|
35
|
-
*
|
|
36
|
-
* size key
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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[];
|