@fuzdev/mdz 0.1.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/Mdz.svelte +39 -0
  4. package/dist/Mdz.svelte.d.ts +17 -0
  5. package/dist/Mdz.svelte.d.ts.map +1 -0
  6. package/dist/MdzNodeView.svelte +140 -0
  7. package/dist/MdzNodeView.svelte.d.ts +9 -0
  8. package/dist/MdzNodeView.svelte.d.ts.map +1 -0
  9. package/dist/MdzPrecompiled.svelte +33 -0
  10. package/dist/MdzPrecompiled.svelte.d.ts +17 -0
  11. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -0
  12. package/dist/MdzRoot.svelte +40 -0
  13. package/dist/MdzRoot.svelte.d.ts +14 -0
  14. package/dist/MdzRoot.svelte.d.ts.map +1 -0
  15. package/dist/MdzStream.svelte +37 -0
  16. package/dist/MdzStream.svelte.d.ts +18 -0
  17. package/dist/MdzStream.svelte.d.ts.map +1 -0
  18. package/dist/MdzStreamNodeView.svelte +150 -0
  19. package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
  20. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
  21. package/dist/context_helpers.d.ts +23 -0
  22. package/dist/context_helpers.d.ts.map +1 -0
  23. package/dist/context_helpers.js +19 -0
  24. package/dist/mdz.d.ts +152 -0
  25. package/dist/mdz.d.ts.map +1 -0
  26. package/dist/mdz.js +61 -0
  27. package/dist/mdz_contexts.d.ts +106 -0
  28. package/dist/mdz_contexts.d.ts.map +1 -0
  29. package/dist/mdz_contexts.js +42 -0
  30. package/dist/mdz_helpers.d.ts +321 -0
  31. package/dist/mdz_helpers.d.ts.map +1 -0
  32. package/dist/mdz_helpers.js +668 -0
  33. package/dist/mdz_lexer.d.ts +123 -0
  34. package/dist/mdz_lexer.d.ts.map +1 -0
  35. package/dist/mdz_lexer.js +1405 -0
  36. package/dist/mdz_opcodes.d.ts +193 -0
  37. package/dist/mdz_opcodes.d.ts.map +1 -0
  38. package/dist/mdz_opcodes.js +14 -0
  39. package/dist/mdz_opcodes_to_nodes.d.ts +26 -0
  40. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
  41. package/dist/mdz_opcodes_to_nodes.js +410 -0
  42. package/dist/mdz_stream_parser.d.ts +109 -0
  43. package/dist/mdz_stream_parser.d.ts.map +1 -0
  44. package/dist/mdz_stream_parser.js +699 -0
  45. package/dist/mdz_stream_parser_block.d.ts +99 -0
  46. package/dist/mdz_stream_parser_block.d.ts.map +1 -0
  47. package/dist/mdz_stream_parser_block.js +437 -0
  48. package/dist/mdz_stream_parser_inline.d.ts +98 -0
  49. package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
  50. package/dist/mdz_stream_parser_inline.js +481 -0
  51. package/dist/mdz_stream_parser_link.d.ts +29 -0
  52. package/dist/mdz_stream_parser_link.d.ts.map +1 -0
  53. package/dist/mdz_stream_parser_link.js +227 -0
  54. package/dist/mdz_stream_parser_list.d.ts +106 -0
  55. package/dist/mdz_stream_parser_list.d.ts.map +1 -0
  56. package/dist/mdz_stream_parser_list.js +438 -0
  57. package/dist/mdz_stream_parser_state.d.ts +476 -0
  58. package/dist/mdz_stream_parser_state.d.ts.map +1 -0
  59. package/dist/mdz_stream_parser_state.js +651 -0
  60. package/dist/mdz_stream_parser_text.d.ts +25 -0
  61. package/dist/mdz_stream_parser_text.d.ts.map +1 -0
  62. package/dist/mdz_stream_parser_text.js +67 -0
  63. package/dist/mdz_stream_parser_url.d.ts +76 -0
  64. package/dist/mdz_stream_parser_url.d.ts.map +1 -0
  65. package/dist/mdz_stream_parser_url.js +328 -0
  66. package/dist/mdz_stream_state.svelte.d.ts +53 -0
  67. package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
  68. package/dist/mdz_stream_state.svelte.js +360 -0
  69. package/dist/mdz_to_svelte.d.ts +71 -0
  70. package/dist/mdz_to_svelte.d.ts.map +1 -0
  71. package/dist/mdz_to_svelte.js +127 -0
  72. package/dist/mdz_token_parser.d.ts +20 -0
  73. package/dist/mdz_token_parser.d.ts.map +1 -0
  74. package/dist/mdz_token_parser.js +449 -0
  75. package/dist/svelte_preprocess_mdz.d.ts +80 -0
  76. package/dist/svelte_preprocess_mdz.d.ts.map +1 -0
  77. package/dist/svelte_preprocess_mdz.js +543 -0
  78. package/dist/tsdoc_mdz.d.ts +45 -0
  79. package/dist/tsdoc_mdz.d.ts.map +1 -0
  80. package/dist/tsdoc_mdz.js +88 -0
  81. package/package.json +131 -0
  82. package/src/lib/context_helpers.ts +52 -0
  83. package/src/lib/mdz.ts +190 -0
  84. package/src/lib/mdz_contexts.ts +99 -0
  85. package/src/lib/mdz_helpers.ts +727 -0
  86. package/src/lib/mdz_lexer.ts +1761 -0
  87. package/src/lib/mdz_opcodes.ts +227 -0
  88. package/src/lib/mdz_opcodes_to_nodes.ts +470 -0
  89. package/src/lib/mdz_stream_parser.ts +766 -0
  90. package/src/lib/mdz_stream_parser_block.ts +490 -0
  91. package/src/lib/mdz_stream_parser_inline.ts +527 -0
  92. package/src/lib/mdz_stream_parser_link.ts +283 -0
  93. package/src/lib/mdz_stream_parser_list.ts +525 -0
  94. package/src/lib/mdz_stream_parser_state.ts +957 -0
  95. package/src/lib/mdz_stream_parser_text.ts +95 -0
  96. package/src/lib/mdz_stream_parser_url.ts +386 -0
  97. package/src/lib/mdz_stream_state.svelte.ts +383 -0
  98. package/src/lib/mdz_to_svelte.ts +213 -0
  99. package/src/lib/mdz_token_parser.ts +544 -0
  100. package/src/lib/svelte_preprocess_mdz.ts +777 -0
  101. package/src/lib/tsdoc_mdz.ts +97 -0
package/package.json ADDED
@@ -0,0 +1,131 @@
1
+ {
2
+ "name": "@fuzdev/mdz",
3
+ "version": "0.1.0",
4
+ "description": "a strict markdown dialect built for streaming, Svelte authoring, docs websites, and untrusted content",
5
+ "tagline": "markdown for fuz",
6
+ "glyph": "📝",
7
+ "logo": "logo.svg",
8
+ "logo_alt": "a friendly pixelated spider facing you",
9
+ "license": "MIT",
10
+ "homepage": "https://mdz.fuz.dev/",
11
+ "author": {
12
+ "name": "Ryan Atkinson",
13
+ "email": "mail@ryanatkn.com",
14
+ "url": "https://www.ryanatkn.com/"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/fuzdev/mdz.git"
19
+ },
20
+ "bugs": "https://github.com/fuzdev/mdz/issues",
21
+ "funding": "https://www.ryanatkn.com/funding",
22
+ "scripts": {
23
+ "start": "gro dev",
24
+ "dev": "gro dev",
25
+ "build": "gro build",
26
+ "check": "gro check",
27
+ "test": "gro test",
28
+ "fixtures:update": "gro src/test/fixtures/update",
29
+ "preview": "vite preview",
30
+ "deploy": "gro deploy",
31
+ "benchmark": "gro run src/benchmarks/mdz.benchmark.ts",
32
+ "benchmark:save": "gro run src/benchmarks/mdz.benchmark.ts --save",
33
+ "benchmark:clean": "rm -f src/benchmarks/baseline.json"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=24.14"
38
+ },
39
+ "peerDependencies": {
40
+ "@fuzdev/fuz_util": ">=0.65.1",
41
+ "@sveltejs/kit": "^2.47.3",
42
+ "@types/estree": "^1",
43
+ "esm-env": "^1",
44
+ "svelte": "^5"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@types/estree": {
48
+ "optional": true
49
+ },
50
+ "esm-env": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "magic-string": "^0.30.21",
56
+ "zimmerframe": "^1.1.4"
57
+ },
58
+ "devDependencies": {
59
+ "@fuzdev/blake3_wasm": "^0.1.1",
60
+ "@fuzdev/fuz_code": "^0.46.0",
61
+ "@fuzdev/fuz_css": "^0.62.0",
62
+ "@fuzdev/fuz_ui": "^0.204.0",
63
+ "@fuzdev/fuz_util": "^0.65.1",
64
+ "@fuzdev/gro": "^0.204.0",
65
+ "@ryanatkn/eslint-config": "^0.12.1",
66
+ "@sveltejs/acorn-typescript": "^1.0.10",
67
+ "@sveltejs/adapter-static": "^3.0.10",
68
+ "@sveltejs/kit": "^2.65.0",
69
+ "@sveltejs/package": "^2.5.7",
70
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
71
+ "@types/estree": "^1.0.8",
72
+ "@types/node": "^24.12.4",
73
+ "@webref/css": "^8.2.0",
74
+ "eslint": "^9.39.1",
75
+ "eslint-plugin-svelte": "^3.13.1",
76
+ "esm-env": "^1.2.2",
77
+ "prettier": "^3.7.4",
78
+ "prettier-plugin-svelte": "^3.5.1",
79
+ "svelte": "^5.56.3",
80
+ "svelte-check": "^4.6.0",
81
+ "svelte-docinfo": "^0.5.3",
82
+ "svelte2tsx": "^0.7.56",
83
+ "tslib": "^2.8.1",
84
+ "typescript": "^5.9.3",
85
+ "typescript-eslint": "^8.48.1",
86
+ "vite": "^7.3.1",
87
+ "vitest": "^4.0.15"
88
+ },
89
+ "prettier": {
90
+ "plugins": [
91
+ "prettier-plugin-svelte"
92
+ ],
93
+ "useTabs": true,
94
+ "printWidth": 100,
95
+ "singleQuote": true,
96
+ "bracketSpacing": false,
97
+ "overrides": [
98
+ {
99
+ "files": "package.json",
100
+ "options": {
101
+ "useTabs": false
102
+ }
103
+ }
104
+ ]
105
+ },
106
+ "sideEffects": [
107
+ "**/*.css"
108
+ ],
109
+ "files": [
110
+ "dist",
111
+ "src/lib/**/*.ts",
112
+ "!src/lib/**/*.test.*",
113
+ "!dist/**/*.test.*"
114
+ ],
115
+ "exports": {
116
+ "./package.json": "./package.json",
117
+ "./*.js": {
118
+ "types": "./dist/*.d.ts",
119
+ "default": "./dist/*.js"
120
+ },
121
+ "./*.ts": {
122
+ "types": "./dist/*.d.ts",
123
+ "default": "./dist/*.js"
124
+ },
125
+ "./*.svelte": {
126
+ "types": "./dist/*.svelte.d.ts",
127
+ "svelte": "./dist/*.svelte",
128
+ "default": "./dist/*.svelte"
129
+ }
130
+ }
131
+ }
@@ -0,0 +1,52 @@
1
+ import {getContext, setContext} from 'svelte';
2
+
3
+ // This uses a function instead of a class because of the overloaded type signatures.
4
+ // It could be implemented internally with a class like `SvelteContext`
5
+ // but this is less code and seems a bit simpler.
6
+ // The memory gains of a class appear minimal given the expected usage patterns.
7
+
8
+ // TODO maybe remove `error_message`?
9
+
10
+ /**
11
+ * Wraps Svelte's `setContext` and `getContext` for better ergonomics.
12
+ * When no value is set in the context,
13
+ * `get` throws an error and `get_maybe` returns `undefined`.
14
+ * If a `fallback` is provided, the `value` argument to `set` is optional
15
+ * and `get_maybe` is omitted from the type.
16
+ *
17
+ * Vendored from `@fuzdev/fuz_ui/context_helpers.js` — fuz_ui sits above mdz
18
+ * in the dependency graph, so mdz keeps its own copy for `mdz_contexts.ts`.
19
+ * Consumers should import fuz_ui's.
20
+ *
21
+ * @nodocs
22
+ */
23
+ export function create_context<T>(fallback: () => T): {
24
+ get: () => T;
25
+ set: (value?: T) => T;
26
+ };
27
+ export function create_context<T>(): {
28
+ get: (error_message?: string) => T;
29
+ get_maybe: () => T | undefined;
30
+ set: (value: T) => T;
31
+ };
32
+ export function create_context<T>(fallback?: () => T): {
33
+ get: (error_message?: string) => T;
34
+ get_maybe: () => T | undefined;
35
+ set: (value?: T) => T;
36
+ } {
37
+ const key = Symbol();
38
+ const get_maybe = () => {
39
+ const value: T | undefined = getContext(key); // treat `null` as a valid value - the `typescript-eslint` rule below is bugged because `??` would clobber nulls, see issue https://github.com/typescript-eslint/typescript-eslint/issues/7842
40
+ return value === undefined ? fallback?.() : value;
41
+ };
42
+ return {
43
+ get: (error_message?: string) => {
44
+ const value = get_maybe();
45
+ if (value === undefined) throw Error(error_message ?? 'context value is not set');
46
+ return value;
47
+ },
48
+ get_maybe,
49
+ // this is typesafe, so no runtime check:
50
+ set: (value: T | undefined = fallback?.()) => setContext(key, value)!,
51
+ };
52
+ }
package/src/lib/mdz.ts ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * mdz — strict markdown dialect built for streaming, Svelte authoring, docs
3
+ * sites, and untrusted content.
4
+ *
5
+ * Created for two motivating use cases: rendering TSDoc/JSDoc comments on
6
+ * docs websites (with domain-specific behavior like linkifying backticked
7
+ * identifiers) and authoring content with Svelte components. It also has a
8
+ * streaming parser that extends the same grammar to LLM output.
9
+ *
10
+ * This module is the synchronous entry: `mdz_parse` parses a complete string
11
+ * to an `MdzNode` tree. It is the canonical reference parser — the streaming
12
+ * pipeline (`mdz_stream_parser.ts`) emits opcodes that converge on the same
13
+ * tree, asserted by parity tests.
14
+ *
15
+ * The dialect supports:
16
+ * - inline formatting: `code`, **bold**, _italic_, ~~strikethrough~~
17
+ * - auto-detected links: external URLs (`https://...`) and internal paths (`/path`)
18
+ * - markdown links: `[text](url)` with custom display text
19
+ * - inline code in backticks (creates `Code` nodes; auto-linking to identifiers/modules
20
+ * is handled by the rendering layer via `MdzNodeView.svelte`)
21
+ * - paragraph breaks (blank lines)
22
+ * - block elements: headings, horizontal rules, lists, blockquotes, code blocks
23
+ * - HTML elements and Svelte components (opt-in via context)
24
+ *
25
+ * Whitespace: the parser preserves whitespace in text node content (single
26
+ * newlines stay literal `\n`, no `<br>` nodes), while structural whitespace is
27
+ * interpreted — blank lines separate paragraphs, extra blank lines collapse,
28
+ * and a line of only whitespace (spaces, tabs, `\r`) counts as blank, so an
29
+ * invisible trailing space never changes document structure.
30
+ * Presentation is the renderer's choice: by default whitespace collapses like
31
+ * standard markdown (single newlines are soft breaks), and the `whitespace`
32
+ * prop on `Mdz`/`MdzStream` opts into `pre-line` or `pre-wrap` rendering.
33
+ *
34
+ * ## Design philosophy
35
+ *
36
+ * - **False negatives over false positives**: When in doubt, treat as plain text.
37
+ * Block elements can interrupt paragraphs without blank lines; inline formatting is strict.
38
+ * - **One way to do things**: Single unambiguous syntax per feature. No alternatives.
39
+ * - **Explicit over implicit**: Clear delimiters and column-0 requirements avoid ambiguity.
40
+ * - **Simple over complete**: Prefer simple parsing rules over complex edge case handling.
41
+ * - **No implicit retroactivity**: the grammar admits no construct where later input changes
42
+ * the rendering of unboundedly-earlier output, so the streaming pipeline corrects only via
43
+ * bounded, explicit opcodes and never re-parses.
44
+ *
45
+ * ## Status
46
+ *
47
+ * Pre-stable — breaking changes are expected as the dialect matures.
48
+ * Next: attributes, editing.
49
+ *
50
+ * @module
51
+ */
52
+
53
+ import {MdzLexer} from './mdz_lexer.js';
54
+ import {MdzTokenParser} from './mdz_token_parser.js';
55
+
56
+ // TODO design incremental parsing or some system that preserves Svelte components across re-renders when possible
57
+
58
+ /**
59
+ * Parses text to an array of `MdzNode`.
60
+ *
61
+ * Two phases: `MdzLexer` tokenizes the input into a flat `MdzToken[]` stream,
62
+ * then `MdzTokenParser` builds the `MdzNode[]` tree from the tokens.
63
+ */
64
+ export const mdz_parse = (text: string): Array<MdzNode> =>
65
+ new MdzTokenParser(new MdzLexer(text).tokenize()).parse();
66
+
67
+ export type MdzNode =
68
+ | MdzNodeText
69
+ | MdzNodeCode
70
+ | MdzNodeCodeblock
71
+ | MdzNodeBold
72
+ | MdzNodeItalic
73
+ | MdzNodeStrikethrough
74
+ | MdzNodeLink
75
+ | MdzNodeParagraph
76
+ | MdzNodeHr
77
+ | MdzNodeHeading
78
+ | MdzNodeList
79
+ | MdzNodeListItem
80
+ | MdzNodeBlockquote
81
+ | MdzNodeElement
82
+ | MdzNodeComponent;
83
+
84
+ export interface MdzNodeBase {
85
+ type: string;
86
+ start: number;
87
+ end: number;
88
+ }
89
+
90
+ export interface MdzNodeText extends MdzNodeBase {
91
+ type: 'Text';
92
+ content: string;
93
+ }
94
+
95
+ export interface MdzNodeCode extends MdzNodeBase {
96
+ type: 'Code';
97
+ /** The code content (identifier/module name). */
98
+ content: string;
99
+ }
100
+
101
+ export interface MdzNodeCodeblock extends MdzNodeBase {
102
+ type: 'Codeblock';
103
+ /** Language hint, if provided. */
104
+ lang: string | null;
105
+ /** Raw code content. */
106
+ content: string;
107
+ }
108
+
109
+ export interface MdzNodeBold extends MdzNodeBase {
110
+ type: 'Bold';
111
+ children: Array<MdzNode>;
112
+ }
113
+
114
+ export interface MdzNodeItalic extends MdzNodeBase {
115
+ type: 'Italic';
116
+ children: Array<MdzNode>;
117
+ }
118
+
119
+ export interface MdzNodeStrikethrough extends MdzNodeBase {
120
+ type: 'Strikethrough';
121
+ children: Array<MdzNode>;
122
+ }
123
+
124
+ export interface MdzNodeLink extends MdzNodeBase {
125
+ type: 'Link';
126
+ /** URL or path. */
127
+ reference: string;
128
+ /** Display content (can include inline formatting). */
129
+ children: Array<MdzNode>;
130
+ /** `external` is `https`/`http`; `internal` is `/path`, `./path`, or `../path`. */
131
+ link_type: 'external' | 'internal';
132
+ }
133
+
134
+ export interface MdzNodeParagraph extends MdzNodeBase {
135
+ type: 'Paragraph';
136
+ children: Array<MdzNode>;
137
+ }
138
+
139
+ export interface MdzNodeHr extends MdzNodeBase {
140
+ type: 'Hr';
141
+ }
142
+
143
+ export interface MdzNodeHeading extends MdzNodeBase {
144
+ type: 'Heading';
145
+ level: 1 | 2 | 3 | 4 | 5 | 6;
146
+ /** Slugified heading text for fragment links. */
147
+ id: string;
148
+ /** Inline formatting allowed. */
149
+ children: Array<MdzNode>;
150
+ }
151
+
152
+ export interface MdzNodeList extends MdzNodeBase {
153
+ type: 'List';
154
+ ordered: boolean;
155
+ /** First item's authored number (ordered only) — the `<ol start>` attribute. */
156
+ start_number?: number;
157
+ children: Array<MdzNodeListItem>;
158
+ }
159
+
160
+ export interface MdzNodeListItem extends MdzNodeBase {
161
+ type: 'ListItem';
162
+ /** Authored number (ordered only); the renderer ignores it, tooling and the future formatter use it. */
163
+ number?: number;
164
+ /** Inline run first, then block children (`Paragraph` | `List` | `Codeblock` | `Blockquote`). */
165
+ children: Array<MdzNode>;
166
+ }
167
+
168
+ export interface MdzNodeBlockquote extends MdzNodeBase {
169
+ type: 'Blockquote';
170
+ /**
171
+ * Block children — a quote's content is a mini-document, so anything that
172
+ * can appear at the top level can appear here (`Paragraph` | `Heading` |
173
+ * `Hr` | `List` | `Codeblock` | `Blockquote`).
174
+ */
175
+ children: Array<MdzNode>;
176
+ }
177
+
178
+ export interface MdzNodeElement extends MdzNodeBase {
179
+ type: 'Element';
180
+ /** HTML element name (e.g. `div`, `span`, `aside`). */
181
+ name: string;
182
+ children: Array<MdzNode>;
183
+ }
184
+
185
+ export interface MdzNodeComponent extends MdzNodeBase {
186
+ type: 'Component';
187
+ /** Svelte component name (e.g. `Alert`, `Card`). */
188
+ name: string;
189
+ children: Array<MdzNode>;
190
+ }
@@ -0,0 +1,99 @@
1
+ import type {Component} from 'svelte';
2
+
3
+ import {create_context} from './context_helpers.js';
4
+
5
+ /**
6
+ * Component registry for custom Svelte components that can be used in mdz content.
7
+ *
8
+ * For example, registering 'Alert' allows using `<Alert>...</Alert>` in mdz content.
9
+ *
10
+ * The Map values are Svelte components.
11
+ */
12
+ export type MdzComponents = Map<string, Component<any, any>>; // TODO support params
13
+
14
+ /**
15
+ * Element registry for HTML elements that can be used in mdz content.
16
+ *
17
+ * For example, registering 'div' allows using `<div>...</div>` in mdz content.
18
+ *
19
+ * The Map values are boolean placeholders for future configuration options.
20
+ */
21
+ export type MdzElements = Map<string, boolean>;
22
+
23
+ /**
24
+ * CSS `white-space` values for the `whitespace` prop on `Mdz`, `MdzStream`,
25
+ * and `MdzPrecompiled`. By default no style is applied, so whitespace
26
+ * collapses like standard markdown — single newlines are soft breaks.
27
+ * `pre-line` renders every newline as a line break (chat-style input);
28
+ * `pre-wrap` additionally preserves spaces and tabs.
29
+ */
30
+ export type MdzWhitespace = 'normal' | 'nowrap' | 'pre' | 'pre-wrap' | 'pre-line' | 'break-spaces';
31
+
32
+ /**
33
+ * Context for providing custom mdz components via a getter function.
34
+ * Set to a getter (e.g., `() => components`) so changes are reflected reactively.
35
+ */
36
+ export const mdz_components_context = create_context<() => MdzComponents | undefined>();
37
+
38
+ /**
39
+ * Context for providing allowed HTML elements via a getter function.
40
+ * Set to a getter (e.g., `() => elements`) so changes are reflected reactively.
41
+ * By default, no HTML elements are allowed.
42
+ */
43
+ export const mdz_elements_context = create_context<() => MdzElements | undefined>();
44
+
45
+ /**
46
+ * Context for providing a base path getter for resolving relative links in mdz content.
47
+ * Set to a getter (e.g., `() => base`) so changes to the base prop are reflected
48
+ * without needing an effect. When the getter returns a path like `'/docs/usage/'`,
49
+ * relative paths like `./grammar` resolve to `/docs/usage/grammar` before rendering.
50
+ * When not set, relative paths use raw hrefs (browser resolves them).
51
+ */
52
+ export const mdz_base_context = create_context<() => string | undefined>();
53
+
54
+ /**
55
+ * Component that renders an inline `` `code` `` span. Receives the raw span
56
+ * content as `reference` — the prop name matches `fuz_ui`'s `DocsLink`, so it
57
+ * can be injected directly to auto-link backticked API identifiers.
58
+ */
59
+ export type MdzCodeComponent = Component<{reference: string}>;
60
+
61
+ /**
62
+ * Context for overriding how inline `` `code` `` spans render, via a getter
63
+ * function. When not set, inline code renders as a plain `<code>` element.
64
+ */
65
+ export const mdz_code_context = create_context<() => MdzCodeComponent | undefined>();
66
+
67
+ /**
68
+ * Component that renders a fenced code block. Receives `lang` and `content`,
69
+ * matching `@fuzdev/fuz_code`'s `Code`, so it can be injected directly for
70
+ * syntax highlighting.
71
+ */
72
+ export type MdzCodeblockComponent = Component<{lang: string | null; content: string}>;
73
+
74
+ /**
75
+ * Context for overriding how fenced code blocks render, via a getter function.
76
+ * When not set, code blocks render as a plain `<pre><code>` element.
77
+ */
78
+ export const mdz_codeblock_context = create_context<() => MdzCodeblockComponent | undefined>();
79
+
80
+ interface MdzGetterContext<T> {
81
+ get_maybe(): (() => T | undefined) | undefined;
82
+ set(value: () => T | undefined): unknown;
83
+ }
84
+
85
+ /**
86
+ * Set an mdz context to a getter that prefers the local `value_fn` and falls
87
+ * back to the ancestor's value when `value_fn()` returns `undefined`.
88
+ *
89
+ * Pass `value_fn` as a getter (not a snapshot) so prop changes remain reactive.
90
+ * The ancestor lookup happens once at component init — that's intentional, it
91
+ * captures the surrounding scope's context at mount time.
92
+ */
93
+ export const mdz_set_context_with_fallback = <T>(
94
+ context: MdzGetterContext<T>,
95
+ value_fn: () => T | undefined,
96
+ ): void => {
97
+ const ancestor = context.get_maybe();
98
+ context.set(() => value_fn() ?? ancestor?.());
99
+ };