@react-x11/components 0.4.0 → 0.6.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/README.md +143 -8
- package/dist/charts/components.d.ts +3 -1
- package/dist/charts/components.d.ts.map +1 -1
- package/dist/charts/components.js +8 -4
- package/dist/charts/components.js.map +1 -1
- package/dist/charts/node.d.ts +13 -6
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +75 -24
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +4 -0
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +11 -6
- package/dist/charts/render.js.map +1 -1
- package/dist/code-editor/node.d.ts +47 -16
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +121 -51
- package/dist/code-editor/node.js.map +1 -1
- package/dist/flow/draw.d.ts +19 -1
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +64 -35
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +24 -8
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/model.d.ts +4 -2
- package/dist/flow/model.d.ts.map +1 -1
- package/dist/flow/model.js +4 -2
- package/dist/flow/model.js.map +1 -1
- package/dist/flow/node.d.ts +40 -9
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +142 -53
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/types.d.ts +46 -21
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/formula/index.d.ts +4 -2
- package/dist/formula/index.d.ts.map +1 -1
- package/dist/formula/index.js.map +1 -1
- package/dist/formula/node.d.ts +5 -1
- package/dist/formula/node.d.ts.map +1 -1
- package/dist/formula/node.js +19 -3
- package/dist/formula/node.js.map +1 -1
- package/dist/html/controls.d.ts +4 -1
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +11 -5
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +18 -8
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +15 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +41 -16
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +3 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3 -1
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/values.d.ts +14 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +11 -8
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +1 -0
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +12 -5
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +7 -4
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +22 -1
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +22 -8
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +97 -15
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +3 -0
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +2 -2
- package/dist/html/paint.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/maps/clip.d.ts +43 -0
- package/dist/maps/clip.d.ts.map +1 -0
- package/dist/maps/clip.js +171 -0
- package/dist/maps/clip.js.map +1 -0
- package/dist/maps/gzip.d.ts +15 -0
- package/dist/maps/gzip.d.ts.map +1 -0
- package/dist/maps/gzip.js +56 -0
- package/dist/maps/gzip.js.map +1 -0
- package/dist/maps/index.d.ts +55 -0
- package/dist/maps/index.d.ts.map +1 -0
- package/dist/maps/index.js +141 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/labels.d.ts +123 -0
- package/dist/maps/labels.d.ts.map +1 -0
- package/dist/maps/labels.js +323 -0
- package/dist/maps/labels.js.map +1 -0
- package/dist/maps/mvt.d.ts +244 -0
- package/dist/maps/mvt.d.ts.map +1 -0
- package/dist/maps/mvt.js +820 -0
- package/dist/maps/mvt.js.map +1 -0
- package/dist/maps/node.d.ts +278 -0
- package/dist/maps/node.d.ts.map +1 -0
- package/dist/maps/node.js +1459 -0
- package/dist/maps/node.js.map +1 -0
- package/dist/maps/overlay.d.ts +157 -0
- package/dist/maps/overlay.d.ts.map +1 -0
- package/dist/maps/overlay.js +472 -0
- package/dist/maps/overlay.js.map +1 -0
- package/dist/maps/paint.d.ts +226 -0
- package/dist/maps/paint.d.ts.map +1 -0
- package/dist/maps/paint.js +705 -0
- package/dist/maps/paint.js.map +1 -0
- package/dist/maps/proj.d.ts +281 -0
- package/dist/maps/proj.d.ts.map +1 -0
- package/dist/maps/proj.js +451 -0
- package/dist/maps/proj.js.map +1 -0
- package/dist/maps/sources.d.ts +256 -0
- package/dist/maps/sources.d.ts.map +1 -0
- package/dist/maps/sources.js +210 -0
- package/dist/maps/sources.js.map +1 -0
- package/dist/maps/style.d.ts +141 -0
- package/dist/maps/style.d.ts.map +1 -0
- package/dist/maps/style.js +147 -0
- package/dist/maps/style.js.map +1 -0
- package/dist/maps/styles.d.ts +82 -0
- package/dist/maps/styles.d.ts.map +1 -0
- package/dist/maps/styles.js +898 -0
- package/dist/maps/styles.js.map +1 -0
- package/dist/maps/tiles.d.ts +283 -0
- package/dist/maps/tiles.d.ts.map +1 -0
- package/dist/maps/tiles.js +469 -0
- package/dist/maps/tiles.js.map +1 -0
- package/dist/maps/types.d.ts +244 -0
- package/dist/maps/types.d.ts.map +1 -0
- package/dist/maps/types.js +2 -0
- package/dist/maps/types.js.map +1 -0
- package/dist/markdown/ast.d.ts +80 -6
- package/dist/markdown/ast.d.ts.map +1 -1
- package/dist/markdown/ast.js +13 -4
- package/dist/markdown/ast.js.map +1 -1
- package/dist/markdown/expressions.d.ts +27 -0
- package/dist/markdown/expressions.d.ts.map +1 -0
- package/dist/markdown/expressions.js +203 -0
- package/dist/markdown/expressions.js.map +1 -0
- package/dist/markdown/index.d.ts +61 -2
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +101 -2
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/parse.d.ts +1 -1
- package/dist/markdown/parse.d.ts.map +1 -1
- package/dist/markdown/parse.js +163 -15
- package/dist/markdown/parse.js.map +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/markdown/spans.js +4 -0
- package/dist/markdown/spans.js.map +1 -1
- package/dist/markdown/tags.d.ts +35 -0
- package/dist/markdown/tags.d.ts.map +1 -0
- package/dist/markdown/tags.js +213 -0
- package/dist/markdown/tags.js.map +1 -0
- package/dist/reorder/hx.d.ts +18 -0
- package/dist/reorder/hx.d.ts.map +1 -0
- package/dist/reorder/hx.js +10 -0
- package/dist/reorder/hx.js.map +1 -0
- package/dist/reorder/index.d.ts +314 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +1539 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/model.d.ts +107 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +162 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/richtext/node.d.ts +15 -5
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +40 -11
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +35 -13
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +83 -27
- package/dist/richtext/runs.js.map +1 -1
- package/dist/tabs/index.d.ts +17 -0
- package/dist/tabs/index.d.ts.map +1 -1
- package/dist/tabs/index.js +849 -186
- package/dist/tabs/index.js.map +1 -1
- package/dist/terminal/vt/fonts.d.ts +45 -15
- package/dist/terminal/vt/fonts.d.ts.map +1 -1
- package/dist/terminal/vt/fonts.js +89 -19
- package/dist/terminal/vt/fonts.js.map +1 -1
- package/dist/terminal/vt/node.d.ts +42 -0
- package/dist/terminal/vt/node.d.ts.map +1 -1
- package/dist/terminal/vt/node.js +95 -11
- package/dist/terminal/vt/node.js.map +1 -1
- package/dist/terminal/vt/renderer.d.ts.map +1 -1
- package/dist/terminal/vt/renderer.js +13 -3
- package/dist/terminal/vt/renderer.js.map +1 -1
- package/package.json +14 -3
- package/src/charts/components.ts +14 -7
- package/src/charts/node.ts +81 -26
- package/src/charts/render.ts +16 -6
- package/src/code-editor/node.ts +147 -53
- package/src/flow/draw.ts +104 -35
- package/src/flow/index.ts +33 -13
- package/src/flow/model.ts +4 -2
- package/src/flow/node.ts +158 -59
- package/src/flow/types.ts +46 -21
- package/src/formula/index.ts +4 -2
- package/src/formula/node.ts +24 -5
- package/src/html/controls.ts +4 -1
- package/src/html/css/cascade.ts +18 -7
- package/src/html/css/parse.ts +46 -21
- package/src/html/css/style.ts +6 -1
- package/src/html/css/values.ts +25 -9
- package/src/html/index.ts +1 -0
- package/src/html/layout/boxes.ts +19 -9
- package/src/html/layout/inline.ts +23 -1
- package/src/html/node.ts +110 -17
- package/src/html/paint.ts +17 -2
- package/src/index.ts +102 -0
- package/src/maps/clip.ts +184 -0
- package/src/maps/gzip.ts +66 -0
- package/src/maps/index.ts +316 -0
- package/src/maps/labels.ts +442 -0
- package/src/maps/mvt.ts +857 -0
- package/src/maps/node.ts +1819 -0
- package/src/maps/overlay.ts +639 -0
- package/src/maps/paint.ts +978 -0
- package/src/maps/proj.ts +641 -0
- package/src/maps/sources.ts +425 -0
- package/src/maps/style.ts +309 -0
- package/src/maps/styles.ts +1040 -0
- package/src/maps/tiles.ts +676 -0
- package/src/maps/types.ts +260 -0
- package/src/markdown/ast.ts +85 -9
- package/src/markdown/expressions.ts +223 -0
- package/src/markdown/index.ts +190 -5
- package/src/markdown/parse.ts +230 -15
- package/src/markdown/spans.ts +4 -0
- package/src/markdown/tags.ts +240 -0
- package/src/reorder/hx.ts +31 -0
- package/src/reorder/index.ts +2223 -0
- package/src/reorder/model.ts +232 -0
- package/src/richtext/node.ts +49 -14
- package/src/richtext/runs.ts +115 -30
- package/src/tabs/index.ts +1040 -204
- package/src/terminal/vt/fonts.ts +128 -36
- package/src/terminal/vt/node.ts +105 -12
- package/src/terminal/vt/renderer.ts +13 -3
package/src/markdown/index.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// answering the four accessors in core's docs/extending.md, with nothing to
|
|
39
39
|
// register.
|
|
40
40
|
import React from 'react';
|
|
41
|
-
import type { ReactElement, ReactNode } from 'react';
|
|
41
|
+
import type { ComponentType, ReactElement, ReactNode } from 'react';
|
|
42
42
|
import { Icon, useApp, useTheme } from 'react-x11';
|
|
43
43
|
import type { DrawnNode, MouseEvent as X11MouseEvent } from 'react-x11';
|
|
44
44
|
import { tint } from 'react-x11/style';
|
|
@@ -46,6 +46,7 @@ import type { Style } from 'react-x11/style';
|
|
|
46
46
|
|
|
47
47
|
import type {
|
|
48
48
|
BlockNode,
|
|
49
|
+
ComponentBlock,
|
|
49
50
|
Document,
|
|
50
51
|
InlineNode,
|
|
51
52
|
ListBlock,
|
|
@@ -67,17 +68,27 @@ import type { CodeBlockLook } from '../codeblock/index.js';
|
|
|
67
68
|
import type { Language } from '../code-language/index.js';
|
|
68
69
|
|
|
69
70
|
import { parse } from './parse.js';
|
|
71
|
+
import { evaluator, resolveExpressions } from './expressions.js';
|
|
72
|
+
import type { Evaluate } from './expressions.js';
|
|
73
|
+
import { SPREAD_PREFIX } from './ast.js';
|
|
70
74
|
import { runsOf, plainTextOf } from './spans.js';
|
|
71
75
|
import type { InlineStyles } from './spans.js';
|
|
72
76
|
import { useLinkClicks } from '../richtext/index.js';
|
|
73
77
|
import { hx } from './hx.js';
|
|
74
78
|
|
|
75
79
|
export type {
|
|
80
|
+
AttributeValue,
|
|
76
81
|
BlockNode,
|
|
82
|
+
ComponentBlock,
|
|
83
|
+
ComponentInline,
|
|
84
|
+
ExpressionInline,
|
|
77
85
|
InlineNode,
|
|
78
86
|
Document as MarkdownDocument,
|
|
79
87
|
ParseOptions,
|
|
80
88
|
} from './ast.js';
|
|
89
|
+
export { SPREAD_PREFIX } from './ast.js';
|
|
90
|
+
export { scanTag } from './tags.js';
|
|
91
|
+
export type { ScannedTag, ScanResult } from './tags.js';
|
|
81
92
|
export { parse, parseInline } from './parse.js';
|
|
82
93
|
|
|
83
94
|
const h = React.createElement;
|
|
@@ -156,6 +167,62 @@ export interface MarkdownProps {
|
|
|
156
167
|
* stable identity — a new object per render defeats the block cache.
|
|
157
168
|
*/
|
|
158
169
|
fences?: Record<string, (fence: FenceInfo) => ReactNode>;
|
|
170
|
+
/**
|
|
171
|
+
* Components a document may name — the MDX gate (docs/prd-mdx.md).
|
|
172
|
+
*
|
|
173
|
+
* A tag is a component **iff its name is a key here**: there is no
|
|
174
|
+
* capitalisation rule and no HTML fallback, so `<Chart/>` in a document
|
|
175
|
+
* with no `Chart` key is the literal text it has always been, and a
|
|
176
|
+
* document that never passes this prop parses exactly as it did before
|
|
177
|
+
* the feature existed.
|
|
178
|
+
*
|
|
179
|
+
* ```tsx
|
|
180
|
+
* <Markdown source={doc} components={{ Chart, Callout }} />
|
|
181
|
+
* ```
|
|
182
|
+
*
|
|
183
|
+
* A dotted name resolves flat first (`components['Card.Header']`), then by
|
|
184
|
+
* walking (`components.Card.Header`), so compound components work.
|
|
185
|
+
*
|
|
186
|
+
* **Nothing is evaluated.** An attribute is a string, `true` for a bare
|
|
187
|
+
* name, or the `JSON.parse` of a `{…}`; a brace that is not JSON makes the
|
|
188
|
+
* tag unreadable and it stays text. That is what makes this safe to point
|
|
189
|
+
* at a document you did not write — which, for this component, is the
|
|
190
|
+
* usual case.
|
|
191
|
+
*
|
|
192
|
+
* Block position only: a tag on its own line(s) is a component, and one in
|
|
193
|
+
* the middle of a sentence is still text. See `ComponentInline` in ast.ts.
|
|
194
|
+
*
|
|
195
|
+
* Give the map a stable identity — a new object per render defeats the
|
|
196
|
+
* block cache, the same way a new `fences` map does.
|
|
197
|
+
*/
|
|
198
|
+
components?: Record<string, ComponentType<Record<string, unknown>>>;
|
|
199
|
+
/**
|
|
200
|
+
* Bindings that `{…}` in this document may read — and, by passing it, the
|
|
201
|
+
* statement that this document may **run code**.
|
|
202
|
+
*
|
|
203
|
+
* ```tsx
|
|
204
|
+
* <Markdown source={doc} components={{ Chart }} scope={{ quarters }} />
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* With it, an attribute `{…}` that is not JSON is compiled instead of
|
|
208
|
+
* making the tag text, `{...spread}` works, and `{count}` in the middle of
|
|
209
|
+
* a paragraph renders the value. Without it none of those exist and
|
|
210
|
+
* nothing is ever compiled.
|
|
211
|
+
*
|
|
212
|
+
* **There is no sandbox.** Expressions run through `new Function`, in this
|
|
213
|
+
* process, with this process's authority. `components` decides what a
|
|
214
|
+
* document may *reach*; this decides whether it may *compute* — so do not
|
|
215
|
+
* pass it alongside a document you did not write, which for this component
|
|
216
|
+
* usually means anything a model produced.
|
|
217
|
+
*
|
|
218
|
+
* An expression that throws, or does not compile, renders as nothing and
|
|
219
|
+
* warns once. A value that is not a primitive renders as nothing in prose:
|
|
220
|
+
* there is nowhere in a line of text to put an element.
|
|
221
|
+
*
|
|
222
|
+
* Stable identity, as with `components` — the keys are read once, and a new
|
|
223
|
+
* object per render re-parses the document.
|
|
224
|
+
*/
|
|
225
|
+
scope?: Record<string, unknown>;
|
|
159
226
|
/** The root `<box>`'s style — width, padding, margins, `overflow`. */
|
|
160
227
|
style?: Style | Style[];
|
|
161
228
|
'data-testname'?: string;
|
|
@@ -222,6 +289,33 @@ function deriveLook(
|
|
|
222
289
|
};
|
|
223
290
|
}
|
|
224
291
|
|
|
292
|
+
// --- components ------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The component a tag names, or undefined. Flat key first, so a map can spell
|
|
296
|
+
* a dotted name literally, then the walk, so `Card.Header` finds the property
|
|
297
|
+
* hanging off `Card`.
|
|
298
|
+
*/
|
|
299
|
+
function resolveComponent(
|
|
300
|
+
map: MarkdownProps['components'],
|
|
301
|
+
name: string,
|
|
302
|
+
): ComponentType<Record<string, unknown>> | undefined {
|
|
303
|
+
if (!map) return undefined;
|
|
304
|
+
const flat = map[name];
|
|
305
|
+
if (flat) return flat;
|
|
306
|
+
if (!name.includes('.')) return undefined;
|
|
307
|
+
let cur: unknown = map;
|
|
308
|
+
for (const part of name.split('.')) {
|
|
309
|
+
if (cur == null || (typeof cur !== 'object' && typeof cur !== 'function')) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
313
|
+
}
|
|
314
|
+
return cur == null
|
|
315
|
+
? undefined
|
|
316
|
+
: (cur as ComponentType<Record<string, unknown>>);
|
|
317
|
+
}
|
|
318
|
+
|
|
225
319
|
// --- rendering -------------------------------------------------------------
|
|
226
320
|
|
|
227
321
|
interface RenderCtx {
|
|
@@ -230,6 +324,9 @@ interface RenderCtx {
|
|
|
230
324
|
fonts: FontsMeasureLike | null;
|
|
231
325
|
fences?: MarkdownProps['fences'];
|
|
232
326
|
resolveLanguage?: MarkdownProps['resolveLanguage'];
|
|
327
|
+
components?: MarkdownProps['components'];
|
|
328
|
+
/** Present exactly when `scope` was given — the rung that compiles. */
|
|
329
|
+
evaluate?: Evaluate;
|
|
233
330
|
/** True while rendering the live tail block of a streaming document. */
|
|
234
331
|
live?: boolean;
|
|
235
332
|
}
|
|
@@ -325,7 +422,56 @@ function renderBlock(block: BlockNode, ctx: RenderCtx, key: number): ReactNode {
|
|
|
325
422
|
|
|
326
423
|
case 'table':
|
|
327
424
|
return renderTable(block, ctx, key);
|
|
425
|
+
|
|
426
|
+
case 'component':
|
|
427
|
+
return renderComponent(block, ctx, key);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* A component block. The node only exists because the parser was told this
|
|
433
|
+
* name resolves, so the lookup here agrees by construction — the `undefined`
|
|
434
|
+
* branch is for the window between a `components` prop changing and the
|
|
435
|
+
* re-parse that follows it.
|
|
436
|
+
*
|
|
437
|
+
* Children arrive as a laid-out column of blocks, so a component places one
|
|
438
|
+
* child and does not have to know what markdown is.
|
|
439
|
+
*/
|
|
440
|
+
function renderComponent(
|
|
441
|
+
block: ComponentBlock,
|
|
442
|
+
ctx: RenderCtx,
|
|
443
|
+
key: number,
|
|
444
|
+
): ReactNode {
|
|
445
|
+
const Component = resolveComponent(ctx.components, block.name);
|
|
446
|
+
if (!Component) return null;
|
|
447
|
+
const props: Record<string, unknown> = {};
|
|
448
|
+
// Insertion order is the order they were written, which is what decides
|
|
449
|
+
// whether a spread overrides a named attribute or the other way round.
|
|
450
|
+
for (const [name, value] of Object.entries(block.attributes)) {
|
|
451
|
+
if (name.startsWith(SPREAD_PREFIX)) {
|
|
452
|
+
const spread =
|
|
453
|
+
value.kind === 'expression' ? ctx.evaluate?.(value.src) : undefined;
|
|
454
|
+
if (spread && typeof spread === 'object') Object.assign(props, spread);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
props[name] =
|
|
458
|
+
value.kind === 'literal' ? value.value : ctx.evaluate?.(value.src);
|
|
328
459
|
}
|
|
460
|
+
const children =
|
|
461
|
+
block.children.length === 0
|
|
462
|
+
? undefined
|
|
463
|
+
: hx(
|
|
464
|
+
'box',
|
|
465
|
+
{
|
|
466
|
+
style: {
|
|
467
|
+
flexDirection: 'column',
|
|
468
|
+
gap: ctx.look.blockGap,
|
|
469
|
+
alignItems: 'stretch',
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
renderBlocks(block.children, ctx),
|
|
473
|
+
);
|
|
474
|
+
return h(React.Fragment, { key }, h(Component, props, children));
|
|
329
475
|
}
|
|
330
476
|
|
|
331
477
|
function renderCode(
|
|
@@ -591,11 +737,40 @@ export function Markdown(props: MarkdownProps): ReactElement {
|
|
|
591
737
|
],
|
|
592
738
|
);
|
|
593
739
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
740
|
+
// The gate the parser asks. Memoised on the map's identity so that a
|
|
741
|
+
// stable `components` prop keeps one function, and the parse memo below
|
|
742
|
+
// does not re-read the whole document on every render.
|
|
743
|
+
const componentMap = props.components;
|
|
744
|
+
const isComponent = React.useMemo(
|
|
745
|
+
() =>
|
|
746
|
+
componentMap
|
|
747
|
+
? (name: string) => resolveComponent(componentMap, name) !== undefined
|
|
748
|
+
: undefined,
|
|
749
|
+
[componentMap],
|
|
597
750
|
);
|
|
598
751
|
|
|
752
|
+
// `scope` is both the gate and the bindings: giving one turns expression
|
|
753
|
+
// parsing on, and is where the compiled functions get their arguments.
|
|
754
|
+
const scope = props.scope;
|
|
755
|
+
const evaluate = React.useMemo(
|
|
756
|
+
() => (scope ? evaluator(scope) : undefined),
|
|
757
|
+
[scope],
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const doc: Document = React.useMemo(() => {
|
|
761
|
+
const parsed = parse(source, {
|
|
762
|
+
partial,
|
|
763
|
+
...(isComponent ? { isComponent } : null),
|
|
764
|
+
...(evaluate ? { expressions: true } : null),
|
|
765
|
+
});
|
|
766
|
+
// Expressions resolve to text here rather than during the render, so
|
|
767
|
+
// every `runsOf` caller downstream keeps seeing an inline tree it
|
|
768
|
+
// already understands. `raws` is carried through, so the block cache
|
|
769
|
+
// still keys on the source text; a change of `scope` invalidates it
|
|
770
|
+
// through the seam epoch instead.
|
|
771
|
+
return evaluate ? resolveExpressions(parsed, evaluate) : parsed;
|
|
772
|
+
}, [source, partial, isComponent, evaluate]);
|
|
773
|
+
|
|
599
774
|
// Per-block element cache, keyed on the block's raw source (+ whether it
|
|
600
775
|
// is the live tail). Streaming appends re-render only the block that
|
|
601
776
|
// changed; everything else is the same ReactElement, so React bails out
|
|
@@ -608,19 +783,27 @@ export function Markdown(props: MarkdownProps): ReactElement {
|
|
|
608
783
|
const seamsRef = React.useRef<{
|
|
609
784
|
fences: MarkdownProps['fences'];
|
|
610
785
|
resolveLanguage: MarkdownProps['resolveLanguage'];
|
|
786
|
+
components: MarkdownProps['components'];
|
|
787
|
+
scope: MarkdownProps['scope'];
|
|
611
788
|
gen: number;
|
|
612
789
|
}>({
|
|
613
790
|
fences: props.fences,
|
|
614
791
|
resolveLanguage: props.resolveLanguage,
|
|
792
|
+
components: props.components,
|
|
793
|
+
scope: props.scope,
|
|
615
794
|
gen: 0,
|
|
616
795
|
});
|
|
617
796
|
if (
|
|
618
797
|
seamsRef.current.fences !== props.fences ||
|
|
619
|
-
seamsRef.current.resolveLanguage !== props.resolveLanguage
|
|
798
|
+
seamsRef.current.resolveLanguage !== props.resolveLanguage ||
|
|
799
|
+
seamsRef.current.components !== props.components ||
|
|
800
|
+
seamsRef.current.scope !== props.scope
|
|
620
801
|
) {
|
|
621
802
|
seamsRef.current = {
|
|
622
803
|
fences: props.fences,
|
|
623
804
|
resolveLanguage: props.resolveLanguage,
|
|
805
|
+
components: props.components,
|
|
806
|
+
scope: props.scope,
|
|
624
807
|
gen: seamsRef.current.gen + 1,
|
|
625
808
|
};
|
|
626
809
|
}
|
|
@@ -638,6 +821,8 @@ export function Markdown(props: MarkdownProps): ReactElement {
|
|
|
638
821
|
fonts,
|
|
639
822
|
fences: props.fences,
|
|
640
823
|
resolveLanguage: props.resolveLanguage,
|
|
824
|
+
components: props.components,
|
|
825
|
+
...(evaluate ? { evaluate } : null),
|
|
641
826
|
};
|
|
642
827
|
for (let i = 0; i < doc.blocks.length; i += 1) {
|
|
643
828
|
const live = partial && i === doc.blocks.length - 1;
|
package/src/markdown/parse.ts
CHANGED
|
@@ -22,10 +22,14 @@
|
|
|
22
22
|
// definitions pass over the whole document, and streamed model output
|
|
23
23
|
// essentially never uses them.
|
|
24
24
|
// - **Raw HTML is literal text.** There is no HTML pass anywhere in this
|
|
25
|
-
// component, by design
|
|
26
|
-
//
|
|
25
|
+
// component, by design. `<Component />` is a component only when
|
|
26
|
+
// `options.isComponent` claims the name — the MDX gate (docs/prd-mdx.md).
|
|
27
|
+
// Without it, every `<` here means exactly what it always did, which is
|
|
28
|
+
// how turning MDX on cannot change a document that never asked for it.
|
|
29
|
+
// Block position only in M1; see `ComponentInline` in ast.ts.
|
|
27
30
|
import type {
|
|
28
31
|
BlockNode,
|
|
32
|
+
ComponentBlock,
|
|
29
33
|
Document,
|
|
30
34
|
InlineNode,
|
|
31
35
|
ListBlock,
|
|
@@ -33,6 +37,11 @@ import type {
|
|
|
33
37
|
ParseOptions,
|
|
34
38
|
TableAlign,
|
|
35
39
|
} from './ast.js';
|
|
40
|
+
import { closeBrace, scanTag } from './tags.js';
|
|
41
|
+
import type { ScannedTag } from './tags.js';
|
|
42
|
+
|
|
43
|
+
/** Nothing is a component unless the caller says so. */
|
|
44
|
+
const NO_COMPONENTS = (): boolean => false;
|
|
36
45
|
|
|
37
46
|
// --- line-level regexes, compiled once -------------------------------------
|
|
38
47
|
|
|
@@ -54,16 +63,96 @@ const RE_TABLE_DELIM_PREFIX = /^ {0,3}\|[ \t:|-]*$/;
|
|
|
54
63
|
|
|
55
64
|
/** Would this line open something other than a paragraph? The test lazy
|
|
56
65
|
* continuation and list/quote termination share. */
|
|
57
|
-
function isBlockStart(
|
|
66
|
+
function isBlockStart(
|
|
67
|
+
line: string,
|
|
68
|
+
isComponent: (name: string) => boolean = NO_COMPONENTS,
|
|
69
|
+
expressions = false,
|
|
70
|
+
): boolean {
|
|
58
71
|
return (
|
|
59
72
|
RE_ATX.test(line) ||
|
|
60
73
|
RE_FENCE_OPEN.test(line) ||
|
|
61
74
|
RE_HR.test(line) ||
|
|
62
75
|
RE_QUOTE.test(line) ||
|
|
63
|
-
RE_LIST.test(line)
|
|
76
|
+
RE_LIST.test(line) ||
|
|
77
|
+
isComponentBlockLine(line, isComponent, expressions)
|
|
64
78
|
);
|
|
65
79
|
}
|
|
66
80
|
|
|
81
|
+
/** A line that opens a component block, resolved — speculation does not
|
|
82
|
+
* interrupt a paragraph. */
|
|
83
|
+
function isComponentBlockLine(
|
|
84
|
+
line: string,
|
|
85
|
+
isComponent: (name: string) => boolean,
|
|
86
|
+
expressions: boolean,
|
|
87
|
+
): boolean {
|
|
88
|
+
const found = componentBlockAt([line], 0, isComponent, expressions);
|
|
89
|
+
return found !== null && found !== 'incomplete';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The component block opening at `lines[at]`, if one does.
|
|
94
|
+
*
|
|
95
|
+
* A component block owns its whole line — `<Chart/>` with prose after it is
|
|
96
|
+
* a paragraph with a tag in it, which is the inline case and not this one —
|
|
97
|
+
* but it may spend several lines doing so, because a component with six
|
|
98
|
+
* props is written down the page and MDX authors expect that to work. Lines
|
|
99
|
+
* are joined until the tag closes; a blank line gives up, since a tag does
|
|
100
|
+
* not span a paragraph break.
|
|
101
|
+
*
|
|
102
|
+
* `'incomplete'` means "still arriving", which only the live tail acts on.
|
|
103
|
+
*/
|
|
104
|
+
function componentBlockAt(
|
|
105
|
+
lines: string[],
|
|
106
|
+
at: number,
|
|
107
|
+
isComponent: (name: string) => boolean,
|
|
108
|
+
expressions = false,
|
|
109
|
+
): { tag: ScannedTag; lastLine: number } | 'incomplete' | null {
|
|
110
|
+
// The identity check is not an optimisation: `scanTag` reports a bare
|
|
111
|
+
// `<Chart` as "still arriving" before it can know whether anyone claims
|
|
112
|
+
// the name, and a document that never opted in must not have its lazy
|
|
113
|
+
// continuation changed by a line that merely looks like a tag.
|
|
114
|
+
if (isComponent === NO_COMPONENTS) return null;
|
|
115
|
+
const indent = indentOf(lines[at]);
|
|
116
|
+
if (indent > 3 || lines[at][indent] !== '<') return null;
|
|
117
|
+
|
|
118
|
+
let text = lines[at];
|
|
119
|
+
for (let j = at; j < lines.length; j += 1) {
|
|
120
|
+
if (j > at) text += `\n${lines[j]}`;
|
|
121
|
+
const tag = scanTag(text, indent, isComponent, expressions);
|
|
122
|
+
if (tag === null) return null;
|
|
123
|
+
if (tag === 'incomplete') {
|
|
124
|
+
if (j + 1 < lines.length && RE_BLANK.test(lines[j + 1])) return null;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (tag.kind === 'close') return null;
|
|
128
|
+
return text.slice(tag.end).trim() === '' ? { tag, lastLine: j } : null;
|
|
129
|
+
}
|
|
130
|
+
return 'incomplete';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The line closing `name`, counting same-name nesting, or -1. */
|
|
134
|
+
function componentCloseLine(
|
|
135
|
+
lines: string[],
|
|
136
|
+
from: number,
|
|
137
|
+
name: string,
|
|
138
|
+
isComponent: (name: string) => boolean,
|
|
139
|
+
): number {
|
|
140
|
+
let depth = 0;
|
|
141
|
+
for (let i = from; i < lines.length; i += 1) {
|
|
142
|
+
const indent = indentOf(lines[i]);
|
|
143
|
+
if (indent > 3 || lines[i][indent] !== '<') continue;
|
|
144
|
+
const tag = scanTag(lines[i], indent, isComponent);
|
|
145
|
+
if (tag === null || tag === 'incomplete' || tag.name !== name) continue;
|
|
146
|
+
if (lines[i].slice(tag.end).trim() !== '') continue;
|
|
147
|
+
if (tag.kind === 'open') depth += 1;
|
|
148
|
+
else if (tag.kind === 'close') {
|
|
149
|
+
if (depth === 0) return i;
|
|
150
|
+
depth -= 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return -1;
|
|
154
|
+
}
|
|
155
|
+
|
|
67
156
|
/** Leading tabs advance to 4-column stops; content tabs are left alone. */
|
|
68
157
|
function expandLeadingTabs(line: string): string {
|
|
69
158
|
let i = 0;
|
|
@@ -93,7 +182,14 @@ export function parse(source: string, options: ParseOptions = {}): Document {
|
|
|
93
182
|
const lines = normalized.split('\n').map(expandLeadingTabs);
|
|
94
183
|
const blocks: BlockNode[] = [];
|
|
95
184
|
const ranges: Array<[number, number]> = [];
|
|
96
|
-
parseBlocks(
|
|
185
|
+
parseBlocks(
|
|
186
|
+
lines,
|
|
187
|
+
partial,
|
|
188
|
+
blocks,
|
|
189
|
+
ranges,
|
|
190
|
+
options.isComponent ?? NO_COMPONENTS,
|
|
191
|
+
options.expressions === true,
|
|
192
|
+
);
|
|
97
193
|
return {
|
|
98
194
|
blocks,
|
|
99
195
|
raws: ranges.map(([a, b]) => lines.slice(a, b).join('\n')),
|
|
@@ -113,6 +209,8 @@ function parseBlocks(
|
|
|
113
209
|
tailOpen: boolean,
|
|
114
210
|
out: BlockNode[],
|
|
115
211
|
ranges?: Array<[number, number]>,
|
|
212
|
+
isComponent: (name: string) => boolean = NO_COMPONENTS,
|
|
213
|
+
expressions = false,
|
|
116
214
|
): void {
|
|
117
215
|
let i = 0;
|
|
118
216
|
const n = lines.length;
|
|
@@ -132,7 +230,7 @@ function parseBlocks(
|
|
|
132
230
|
para.length = 0;
|
|
133
231
|
paraStart = -1;
|
|
134
232
|
// the paragraph owns the live tail iff its last line is the document's
|
|
135
|
-
const children = parseInline(text, tailOpen && end === n);
|
|
233
|
+
const children = parseInline(text, tailOpen && end === n, expressions);
|
|
136
234
|
if (children.length > 0)
|
|
137
235
|
commit({ type: 'paragraph', children }, start, end);
|
|
138
236
|
};
|
|
@@ -166,7 +264,7 @@ function parseBlocks(
|
|
|
166
264
|
{
|
|
167
265
|
type: 'heading',
|
|
168
266
|
depth: setext[1][0] === '=' ? 1 : 2,
|
|
169
|
-
children: parseInline(text, false),
|
|
267
|
+
children: parseInline(text, false, expressions),
|
|
170
268
|
},
|
|
171
269
|
start,
|
|
172
270
|
i + 1,
|
|
@@ -203,7 +301,7 @@ function parseBlocks(
|
|
|
203
301
|
{
|
|
204
302
|
type: 'heading',
|
|
205
303
|
depth: atx[1].length,
|
|
206
|
-
children: parseInline(atx[2] ?? '', held),
|
|
304
|
+
children: parseInline(atx[2] ?? '', held, expressions),
|
|
207
305
|
},
|
|
208
306
|
i,
|
|
209
307
|
i + 1,
|
|
@@ -248,6 +346,69 @@ function parseBlocks(
|
|
|
248
346
|
continue;
|
|
249
347
|
}
|
|
250
348
|
|
|
349
|
+
// A component standing where a paragraph would (docs/prd-mdx.md). Gated
|
|
350
|
+
// on `isComponent`, so a document that never opted in never gets here.
|
|
351
|
+
const component = componentBlockAt(lines, i, isComponent, expressions);
|
|
352
|
+
if (component === 'incomplete') {
|
|
353
|
+
// A tag still arriving: hold it back rather than flash `<Cha` on the
|
|
354
|
+
// screen, the way a half-arrived link is held. Only the live tail may
|
|
355
|
+
// do this — anywhere else the tag is simply never going to close.
|
|
356
|
+
if (tailOpen) {
|
|
357
|
+
flushPara(i);
|
|
358
|
+
i = n;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
} else if (component) {
|
|
362
|
+
const { tag, lastLine } = component;
|
|
363
|
+
flushPara(i);
|
|
364
|
+
const start = i;
|
|
365
|
+
const node = (children: BlockNode[]): ComponentBlock => ({
|
|
366
|
+
type: 'component',
|
|
367
|
+
name: tag.name,
|
|
368
|
+
attributes: tag.attributes,
|
|
369
|
+
children,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (tag.kind === 'self') {
|
|
373
|
+
i = lastLine + 1;
|
|
374
|
+
commit(node([]), start, i);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const closeAt = componentCloseLine(
|
|
379
|
+
lines,
|
|
380
|
+
lastLine + 1,
|
|
381
|
+
tag.name,
|
|
382
|
+
isComponent,
|
|
383
|
+
);
|
|
384
|
+
if (closeAt !== -1) {
|
|
385
|
+
const children: BlockNode[] = [];
|
|
386
|
+
parseBlocks(
|
|
387
|
+
lines.slice(lastLine + 1, closeAt),
|
|
388
|
+
tailOpen && closeAt >= n,
|
|
389
|
+
children,
|
|
390
|
+
undefined,
|
|
391
|
+
isComponent,
|
|
392
|
+
expressions,
|
|
393
|
+
);
|
|
394
|
+
i = closeAt + 1;
|
|
395
|
+
commit(node(children), start, i);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Open, with no close yet. While the document is still arriving that
|
|
400
|
+
// is the ordinary state of an element being typed: hold the tag and
|
|
401
|
+
// let the children render as the markdown they are, so the reader sees
|
|
402
|
+
// the prose rather than nothing. Mounting the component now and
|
|
403
|
+
// appending to its children instead would remount it — and lose its
|
|
404
|
+
// state — on the chunk that finally closes it.
|
|
405
|
+
if (tailOpen) {
|
|
406
|
+
i = lastLine + 1;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
// Final document, never closed: the tag is text, so fall through.
|
|
410
|
+
}
|
|
411
|
+
|
|
251
412
|
const quote = RE_QUOTE.exec(line);
|
|
252
413
|
if (quote) {
|
|
253
414
|
flushPara(i);
|
|
@@ -266,7 +427,7 @@ function parseBlocks(
|
|
|
266
427
|
}
|
|
267
428
|
if (
|
|
268
429
|
!RE_BLANK.test(l) &&
|
|
269
|
-
!isBlockStart(l) &&
|
|
430
|
+
!isBlockStart(l, isComponent, expressions) &&
|
|
270
431
|
inner.length > 0 &&
|
|
271
432
|
!RE_BLANK.test(inner[inner.length - 1])
|
|
272
433
|
) {
|
|
@@ -277,7 +438,14 @@ function parseBlocks(
|
|
|
277
438
|
break;
|
|
278
439
|
}
|
|
279
440
|
const children: BlockNode[] = [];
|
|
280
|
-
parseBlocks(
|
|
441
|
+
parseBlocks(
|
|
442
|
+
inner,
|
|
443
|
+
tailOpen && i === n,
|
|
444
|
+
children,
|
|
445
|
+
undefined,
|
|
446
|
+
isComponent,
|
|
447
|
+
expressions,
|
|
448
|
+
);
|
|
281
449
|
if (children.length > 0) commit({ type: 'quote', children }, start, i);
|
|
282
450
|
continue;
|
|
283
451
|
}
|
|
@@ -288,7 +456,7 @@ function parseBlocks(
|
|
|
288
456
|
if (list && !(para.length > 0 && !list[4]) && !(held && !list[4])) {
|
|
289
457
|
flushPara(i);
|
|
290
458
|
const start = i;
|
|
291
|
-
const block = parseList(lines, i, tailOpen);
|
|
459
|
+
const block = parseList(lines, i, tailOpen, isComponent, expressions);
|
|
292
460
|
i = block.end;
|
|
293
461
|
commit(block.list, start, i);
|
|
294
462
|
continue;
|
|
@@ -323,7 +491,12 @@ function parseBlocks(
|
|
|
323
491
|
const rows: InlineNode[][][] = [];
|
|
324
492
|
while (i < n) {
|
|
325
493
|
const l = lines[i];
|
|
326
|
-
if (
|
|
494
|
+
if (
|
|
495
|
+
RE_BLANK.test(l) ||
|
|
496
|
+
!l.includes('|') ||
|
|
497
|
+
isBlockStart(l, isComponent, expressions)
|
|
498
|
+
)
|
|
499
|
+
break;
|
|
327
500
|
rows.push(
|
|
328
501
|
normalizeRow(splitRow(l), align.length, tailOpen && i === n - 1),
|
|
329
502
|
);
|
|
@@ -395,6 +568,8 @@ function parseList(
|
|
|
395
568
|
lines: string[],
|
|
396
569
|
from: number,
|
|
397
570
|
tailOpen: boolean,
|
|
571
|
+
isComponent: (name: string) => boolean = NO_COMPONENTS,
|
|
572
|
+
expressions = false,
|
|
398
573
|
): ParsedList {
|
|
399
574
|
const n = lines.length;
|
|
400
575
|
const first = RE_LIST.exec(lines[from]);
|
|
@@ -452,7 +627,11 @@ function parseList(
|
|
|
452
627
|
i += 1;
|
|
453
628
|
continue;
|
|
454
629
|
}
|
|
455
|
-
if (
|
|
630
|
+
if (
|
|
631
|
+
pendingBlanks === 0 &&
|
|
632
|
+
!isBlockStart(l, isComponent, expressions) &&
|
|
633
|
+
!RE_BLANK.test(l)
|
|
634
|
+
) {
|
|
456
635
|
inner.push(l); // lazy paragraph continuation
|
|
457
636
|
i += 1;
|
|
458
637
|
continue;
|
|
@@ -470,7 +649,14 @@ function parseList(
|
|
|
470
649
|
}
|
|
471
650
|
|
|
472
651
|
const children: BlockNode[] = [];
|
|
473
|
-
parseBlocks(
|
|
652
|
+
parseBlocks(
|
|
653
|
+
inner,
|
|
654
|
+
tailOpen && i >= n,
|
|
655
|
+
children,
|
|
656
|
+
undefined,
|
|
657
|
+
isComponent,
|
|
658
|
+
expressions,
|
|
659
|
+
);
|
|
474
660
|
items.push({ checked, children });
|
|
475
661
|
}
|
|
476
662
|
|
|
@@ -603,7 +789,11 @@ function classify(ch: string | undefined): 'ws' | 'punct' | 'other' {
|
|
|
603
789
|
* Parse inline markdown. `atDocEnd` marks text whose end is the live end of
|
|
604
790
|
* a streaming document — only then do the implicit-close rules apply.
|
|
605
791
|
*/
|
|
606
|
-
export function parseInline(
|
|
792
|
+
export function parseInline(
|
|
793
|
+
text: string,
|
|
794
|
+
atDocEnd: boolean,
|
|
795
|
+
expressions = false,
|
|
796
|
+
): InlineNode[] {
|
|
607
797
|
const items: Item[] = [];
|
|
608
798
|
let buf = '';
|
|
609
799
|
|
|
@@ -700,6 +890,31 @@ export function parseInline(text: string, atDocEnd: boolean): InlineNode[] {
|
|
|
700
890
|
continue;
|
|
701
891
|
}
|
|
702
892
|
|
|
893
|
+
// `{expr}` in the prose — rung 2 only, so a brace in an ordinary
|
|
894
|
+
// document is the character it has always been.
|
|
895
|
+
if (expressions && ch === '{') {
|
|
896
|
+
const end = closeBrace(text, i);
|
|
897
|
+
if (end === -1) {
|
|
898
|
+
// Still arriving: hold the rest of the line back rather than show
|
|
899
|
+
// half an expression. In a final document it is just text.
|
|
900
|
+
if (atDocEnd) {
|
|
901
|
+
flushText();
|
|
902
|
+
i = len;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
buf += ch;
|
|
906
|
+
i += 1;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
flushText();
|
|
910
|
+
items.push({
|
|
911
|
+
kind: 'node',
|
|
912
|
+
node: { type: 'expression', src: text.slice(i + 1, end - 1).trim() },
|
|
913
|
+
});
|
|
914
|
+
i = end;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
|
|
703
918
|
if (ch === '<') {
|
|
704
919
|
const rest = text.slice(i);
|
|
705
920
|
const auto = RE_AUTOLINK.exec(rest) ?? RE_AUTOEMAIL.exec(rest);
|
package/src/markdown/spans.ts
CHANGED
|
@@ -135,6 +135,10 @@ export function plainTextOf(nodes: InlineNode[]): string {
|
|
|
135
135
|
case 'break':
|
|
136
136
|
out += '\n';
|
|
137
137
|
break;
|
|
138
|
+
case 'expression':
|
|
139
|
+
// Resolved to text before rendering (`<Markdown>`'s `scope`), so a
|
|
140
|
+
// node reaching here was never evaluated and has no text to give.
|
|
141
|
+
break;
|
|
138
142
|
default:
|
|
139
143
|
out += plainTextOf(node.children);
|
|
140
144
|
}
|