@effected/markdown 0.6.2 → 0.7.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/FrontmatterSource.js +188 -0
- package/Markdown.js +146 -0
- package/MarkdownDocument.js +4 -3
- package/MarkdownNode.js +154 -3
- package/Mdast.js +74 -1
- package/README.md +65 -0
- package/index.d.ts +443 -8
- package/index.js +3 -2
- package/internal/blockParser.js +1 -1
- package/internal/blocks/frontmatter.js +47 -1
- package/internal/phrasing.js +57 -0
- package/internal/rawInline.js +3 -1
- package/internal/stringify.js +143 -3
- package/package.json +3 -3
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { FrontmatterFormat } from "./MarkdownNode.js";
|
|
2
|
+
import { scanRawFrontmatter } from "./internal/blocks/frontmatter.js";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/FrontmatterSource.ts
|
|
6
|
+
/**
|
|
7
|
+
* String-level frontmatter split and join — the raw-source surface, usable
|
|
8
|
+
* without parsing the body at all.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* The `MarkdownFrontmatter` seam requires a parsed `MarkdownDocument`; this
|
|
12
|
+
* module serves the consumer whose body the CommonMark engine cannot or
|
|
13
|
+
* should not parse (an MDX page, a template) and whose contract is
|
|
14
|
+
* byte-exact boundaries (a snapshot hash over the body). It runs the SAME
|
|
15
|
+
* closed fence grammar as the parser's offset-0 pre-scan — `---` yaml,
|
|
16
|
+
* `+++` toml, `---json` json, fence lines exactly the fence, an unclosed
|
|
17
|
+
* fence is not frontmatter — over raw bytes, and never parses the
|
|
18
|
+
* frontmatter value or the body: string level only, decoding is the codec
|
|
19
|
+
* modules' business.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The three line-terminator spellings a fence line may end with. Recorded by
|
|
23
|
+
* {@link FrontmatterSource.split} as fidelity — what the source's fence
|
|
24
|
+
* lines actually used — and consumed by {@link FrontmatterSource.join}.
|
|
25
|
+
*
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
const FrontmatterNewline = Schema.Literals([
|
|
29
|
+
"\n",
|
|
30
|
+
"\r\n",
|
|
31
|
+
"\r"
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* A frontmatter block at string level: the format its fence declared, the
|
|
35
|
+
* exact bytes between the fence lines, and the fence lines' terminator.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* `value` is the exact source slice strictly between the opening fence line
|
|
39
|
+
* (its terminator included) and the closing fence line: every value line
|
|
40
|
+
* WITH its own terminator, verbatim — so a block whose value is one blank
|
|
41
|
+
* line (`"\n"`) stays distinct from a block with no value lines (`""`), and
|
|
42
|
+
* interior CRLF terminators survive untouched. This deliberately differs
|
|
43
|
+
* from the parsed `Frontmatter` NODE's `value`, which drops the final
|
|
44
|
+
* terminator; the node feeds codecs, this feeds byte-exact reconstruction.
|
|
45
|
+
* {@link FrontmatterSource.join} accepts a value without a trailing
|
|
46
|
+
* terminator too, appending `newline` so a hand-built block cannot fuse its
|
|
47
|
+
* last line into the closing fence.
|
|
48
|
+
*
|
|
49
|
+
* `newline` is a fidelity field: `split` always records the terminator
|
|
50
|
+
* following the opening fence; when absent (a hand-built block), `join`
|
|
51
|
+
* uses `"\n"`.
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
var FrontmatterSourceBlock = class extends Schema.Class("FrontmatterSourceBlock")({
|
|
56
|
+
format: FrontmatterFormat,
|
|
57
|
+
value: Schema.String,
|
|
58
|
+
newline: Schema.optionalKey(FrontmatterNewline)
|
|
59
|
+
}) {};
|
|
60
|
+
/**
|
|
61
|
+
* The result of {@link FrontmatterSource.split}: the frontmatter block when
|
|
62
|
+
* one exists, the exact body remainder, and the body's byte offset.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* `body` is exactly `source.slice(bodyOffset)` — the string and the offset
|
|
66
|
+
* can never disagree. With no block, `body` is the whole source and
|
|
67
|
+
* `bodyOffset` is `0`. With a block, `bodyOffset` sits immediately after
|
|
68
|
+
* the closing fence line and its line terminator (the terminator belongs to
|
|
69
|
+
* the fence line, as every line's terminator belongs to its line), which is
|
|
70
|
+
* gray-matter's body boundary; when the closing fence ends the document
|
|
71
|
+
* there is no terminator and the body is empty.
|
|
72
|
+
*
|
|
73
|
+
* `bodyOffset` is informational output of `split`; `join` never reads it,
|
|
74
|
+
* and it constructor-defaults to `0` so a hand-built value for `join` can
|
|
75
|
+
* omit it.
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
var FrontmatterSourceSplit = class extends Schema.Class("FrontmatterSourceSplit")({
|
|
80
|
+
frontmatter: Schema.optionalKey(FrontmatterSourceBlock),
|
|
81
|
+
body: Schema.String,
|
|
82
|
+
bodyOffset: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0)))
|
|
83
|
+
}) {};
|
|
84
|
+
/** The opening fence line per format (the closed grammar's spellings). */
|
|
85
|
+
const OPEN_FENCE = {
|
|
86
|
+
yaml: "---",
|
|
87
|
+
toml: "+++",
|
|
88
|
+
json: "---json"
|
|
89
|
+
};
|
|
90
|
+
/** The closing fence line per format. */
|
|
91
|
+
const CLOSE_FENCE = {
|
|
92
|
+
yaml: "---",
|
|
93
|
+
toml: "+++",
|
|
94
|
+
json: "---"
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The string-level frontmatter facade: split raw source at the fence
|
|
98
|
+
* boundaries and join it back, without parsing anything.
|
|
99
|
+
*
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
var FrontmatterSource = class {
|
|
103
|
+
constructor() {}
|
|
104
|
+
/**
|
|
105
|
+
* Split raw source into its frontmatter block and body, at string level.
|
|
106
|
+
* Total: never fails, never parses — absence of a block (including an
|
|
107
|
+
* opening fence with no closing fence, which per the grammar is not
|
|
108
|
+
* frontmatter) is a representable result, not an error.
|
|
109
|
+
*
|
|
110
|
+
* @remarks
|
|
111
|
+
* The grammar is the parser's own offset-0 pre-scan, over raw bytes:
|
|
112
|
+
* the fence set is closed (`---` yaml, `+++` toml, `---json` json), a
|
|
113
|
+
* fence line is exactly the fence with no surrounding whitespace, and a
|
|
114
|
+
* leading BOM means offset 0 is not a fence — strip one before calling
|
|
115
|
+
* if the source may carry it. Unlike the parse path there is no capture
|
|
116
|
+
* toggle here, so absence is one fact: the source has no closed block.
|
|
117
|
+
* (`MarkdownDocument`'s two-reason absence exists because parsing can be
|
|
118
|
+
* asked not to look; this surface always looks.)
|
|
119
|
+
*
|
|
120
|
+
* Byte exactness: `source.slice(0, result.bodyOffset) + result.body`
|
|
121
|
+
* reassembles `source` exactly, and {@link FrontmatterSource.join}
|
|
122
|
+
* reproduces the original bytes for an unmodified round-trip — see
|
|
123
|
+
* `join` for the two normalizations at the contract's edge.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { FrontmatterSource } from "@effected/markdown";
|
|
128
|
+
*
|
|
129
|
+
* const split = FrontmatterSource.split("---\ntitle: hi\n---\n\n# Body\n");
|
|
130
|
+
* console.log(split.frontmatter?.format); // => "yaml"
|
|
131
|
+
* console.log(split.frontmatter?.value); // => "title: hi\n"
|
|
132
|
+
* console.log(split.body); // => "\n# Body\n"
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* @param source - The raw document source, body format irrelevant.
|
|
136
|
+
* @returns A {@link FrontmatterSourceSplit}; `frontmatter` is absent when
|
|
137
|
+
* the source opens with no closed fence block.
|
|
138
|
+
*/
|
|
139
|
+
static split(source) {
|
|
140
|
+
const capture = scanRawFrontmatter(source);
|
|
141
|
+
if (capture === null) return FrontmatterSourceSplit.make({
|
|
142
|
+
body: source,
|
|
143
|
+
bodyOffset: 0
|
|
144
|
+
});
|
|
145
|
+
return FrontmatterSourceSplit.make({
|
|
146
|
+
frontmatter: FrontmatterSourceBlock.make({
|
|
147
|
+
format: capture.format,
|
|
148
|
+
value: capture.value,
|
|
149
|
+
newline: capture.newline
|
|
150
|
+
}),
|
|
151
|
+
body: source.slice(capture.bodyOffset),
|
|
152
|
+
bodyOffset: capture.bodyOffset
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Serialize a split back to one source string. Total and pure.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* With no block the body is returned verbatim. With a block the output
|
|
160
|
+
* is the opening fence, `newline` (default `"\n"`), the value — a
|
|
161
|
+
* non-empty value missing a final line terminator gets `newline`
|
|
162
|
+
* appended, so the closing fence always starts its own line — the
|
|
163
|
+
* closing fence, `newline`, then the body verbatim.
|
|
164
|
+
*
|
|
165
|
+
* **Round-trip contract:** `join(split(source)) === source`, byte for
|
|
166
|
+
* byte, for every source whose closing fence line ends with a line
|
|
167
|
+
* terminator matching the opening fence's. The two edges where join
|
|
168
|
+
* normalizes instead: a closing fence at end-of-document gains a final
|
|
169
|
+
* `newline` (the block's one unterminated spelling), and fence lines
|
|
170
|
+
* with MISMATCHED terminators (say `---\r\n … ---\n`) re-emit both with
|
|
171
|
+
* the opening one. Value and body bytes survive verbatim in every case.
|
|
172
|
+
*
|
|
173
|
+
* @param split - The parts to serialize, typically from
|
|
174
|
+
* {@link FrontmatterSource.split} or built with
|
|
175
|
+
* `FrontmatterSourceSplit.make`.
|
|
176
|
+
* @returns The combined source string.
|
|
177
|
+
*/
|
|
178
|
+
static join(split) {
|
|
179
|
+
const block = split.frontmatter;
|
|
180
|
+
if (block === void 0) return split.body;
|
|
181
|
+
const newline = block.newline ?? "\n";
|
|
182
|
+
const value = block.value === "" || block.value.endsWith("\n") || block.value.endsWith("\r") ? block.value : `${block.value}${newline}`;
|
|
183
|
+
return `${OPEN_FENCE[block.format]}${newline}${value}${CLOSE_FENCE[block.format]}${newline}${split.body}`;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
export { FrontmatterNewline, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit };
|
package/Markdown.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Root } from "./MarkdownNode.js";
|
|
2
2
|
import { isGuardExceeded, isRawMarkdownError } from "./internal/carriers.js";
|
|
3
3
|
import { parseBlocks } from "./internal/blockParser.js";
|
|
4
|
+
import { parsePhrasingText } from "./internal/phrasing.js";
|
|
4
5
|
import { stringifyTree } from "./internal/stringify.js";
|
|
5
6
|
import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
|
|
6
7
|
import { Effect, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
|
|
@@ -176,6 +177,79 @@ var Markdown = class Markdown {
|
|
|
176
177
|
*/
|
|
177
178
|
static parse = Effect.fn("Markdown.parse")((text, options) => Effect.fromResult(Markdown.parseResult(text, options)));
|
|
178
179
|
/**
|
|
180
|
+
* Parse a text fragment as a single paragraph's inline content,
|
|
181
|
+
* synchronously, as a `Result` — the phrasing-level twin of
|
|
182
|
+
* {@link Markdown.parseResult}, for callers holding already-markdown
|
|
183
|
+
* prose (a link-carrying sentence, a backtick span) who want its
|
|
184
|
+
* phrasing nodes without a full document parse and a paragraph splice.
|
|
185
|
+
*
|
|
186
|
+
* @remarks
|
|
187
|
+
* The whole input is prepared exactly the way the block pass prepares one
|
|
188
|
+
* paragraph's content — leading and trailing whitespace trimmed, line
|
|
189
|
+
* terminators normalized to `\n` — and handed to the inline pass, so the
|
|
190
|
+
* result matches the `children` of the paragraph a full parse of the same
|
|
191
|
+
* fragment would produce. Node positions are correct relative to the
|
|
192
|
+
* input string.
|
|
193
|
+
*
|
|
194
|
+
* Two consequences of the single-paragraph contract:
|
|
195
|
+
*
|
|
196
|
+
* - **Blank lines do not break blocks.** A `\n\n` in the input stays
|
|
197
|
+
* inline content (literal newlines in a text node); nothing at this
|
|
198
|
+
* level opens a heading, list or code block — `# foo` is the text
|
|
199
|
+
* `# foo`.
|
|
200
|
+
* - **No reference context exists.** The fragment carries no definitions,
|
|
201
|
+
* so `[foo]`, `![foo]` and `[^foo]` remain literal text — the same
|
|
202
|
+
* result a full parse of the isolated fragment produces.
|
|
203
|
+
*
|
|
204
|
+
* `options.dialect` selects the inline dialect (default `"gfm"`);
|
|
205
|
+
* `options.frontmatter` has no effect at phrasing level. Failure is rare
|
|
206
|
+
* by design, on {@link Markdown.parseResult}'s terms: only a
|
|
207
|
+
* hardening-guard trip (delimiter or bracket nesting past the cap) fails.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* import { Markdown } from "@effected/markdown";
|
|
212
|
+
* import { Result } from "effect";
|
|
213
|
+
*
|
|
214
|
+
* const ok = Markdown.parsePhrasingResult("see [the docs](./docs.md)");
|
|
215
|
+
* if (Result.isSuccess(ok)) {
|
|
216
|
+
* console.log(ok.success.map((node) => node.type)); // => ["text", "link"]
|
|
217
|
+
* }
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @param text - The prose fragment to parse.
|
|
221
|
+
* @param options - Optional {@link MarkdownParseOptions}; the dialect
|
|
222
|
+
* defaults to `"gfm"`.
|
|
223
|
+
* @returns A `Result` succeeding with the fragment's phrasing content, or
|
|
224
|
+
* failing with {@link MarkdownParseError}.
|
|
225
|
+
*/
|
|
226
|
+
static parsePhrasingResult(text, options) {
|
|
227
|
+
try {
|
|
228
|
+
return Result.succeed(parsePhrasingText(text, dialectOf(options)));
|
|
229
|
+
} catch (caught) {
|
|
230
|
+
if (isGuardExceeded(caught)) return Result.fail(new MarkdownParseError({ diagnostic: MarkdownDiagnostic.fromRaw(text, {
|
|
231
|
+
code: caught.reason,
|
|
232
|
+
message: caught.message,
|
|
233
|
+
offset: caught.offset,
|
|
234
|
+
length: 0
|
|
235
|
+
}) }));
|
|
236
|
+
throw caught;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Parse a text fragment as a single paragraph's inline content. Defined
|
|
241
|
+
* in terms of {@link Markdown.parsePhrasingResult} — synchronous callers
|
|
242
|
+
* can use that variant directly; the single-paragraph contract (blank
|
|
243
|
+
* lines stay inline, references never form) is documented there.
|
|
244
|
+
*
|
|
245
|
+
* @param text - The prose fragment to parse.
|
|
246
|
+
* @param options - Optional {@link MarkdownParseOptions}; the dialect
|
|
247
|
+
* defaults to `"gfm"`.
|
|
248
|
+
* @returns An `Effect` that succeeds with the fragment's phrasing
|
|
249
|
+
* content, or fails with {@link MarkdownParseError}.
|
|
250
|
+
*/
|
|
251
|
+
static parsePhrasing = Effect.fn("Markdown.parsePhrasing")((text, options) => Effect.fromResult(Markdown.parsePhrasingResult(text, options)));
|
|
252
|
+
/**
|
|
179
253
|
* Serialize a {@link Root} tree to canonical markdown, synchronously, as a
|
|
180
254
|
* `Result`. The pure primitive twin of {@link Markdown.stringify}, on the
|
|
181
255
|
* same terms as {@link Markdown.parseResult}.
|
|
@@ -192,6 +266,66 @@ var Markdown = class Markdown {
|
|
|
192
266
|
* contract — but is not a byte-level round-trip of any original source;
|
|
193
267
|
* surgical editing goes through the offset-splice edit layer instead.
|
|
194
268
|
*
|
|
269
|
+
* **The canonical form is a stability commitment, so a test may assert
|
|
270
|
+
* against these bytes.** There are no options to configure and therefore
|
|
271
|
+
* nothing to drift, the choices below are pinned by byte-level tests, and
|
|
272
|
+
* the engine is cross-checked against commonmark.js over the full
|
|
273
|
+
* CommonMark 0.31.2 corpus. Changing any of them is a **breaking** change
|
|
274
|
+
* to this package, never a patch. A consumer asserting on rendered
|
|
275
|
+
* markdown should serialize through here rather than through a third-party
|
|
276
|
+
* stringifier whose defaults are free to move.
|
|
277
|
+
*
|
|
278
|
+
* The canonical choices a byte-level assertion depends on, for a node
|
|
279
|
+
* carrying no fidelity field:
|
|
280
|
+
*
|
|
281
|
+
* | Construct | Canonical form |
|
|
282
|
+
* | --- | --- |
|
|
283
|
+
* | Heading | ATX (`## x`), at every depth |
|
|
284
|
+
* | Thematic break | `***` |
|
|
285
|
+
* | Bullet list marker | `-` |
|
|
286
|
+
* | Ordered list delimiter | `.`, flipping to `)` to separate an immediately adjacent sibling list |
|
|
287
|
+
* | Emphasis / strong | `*` / `**` |
|
|
288
|
+
* | Code block | fenced when the node carries a `lang` or a `fenceChar`, with the fence grown past any interior backtick run; otherwise indented — **except where indenting would not re-parse as a code block**, which forces a fence (see below) |
|
|
289
|
+
* | Block separation | exactly one blank line |
|
|
290
|
+
* | Document | a single trailing newline |
|
|
291
|
+
*
|
|
292
|
+
* A node carrying a fidelity field overrides the corresponding row —
|
|
293
|
+
* `headingStyle: "setext"`, `markerChar`, `fenceChar`, `delimiter` — which
|
|
294
|
+
* is how a parsed document re-serializes in its author's spelling. The
|
|
295
|
+
* rows describe a synthesized node, which is the case a test asserting on
|
|
296
|
+
* generated markdown actually has.
|
|
297
|
+
*
|
|
298
|
+
* **Representability wins over the table.** The canonical form never emits
|
|
299
|
+
* text that would re-parse as something else, so a row yields where the two
|
|
300
|
+
* conflict. The case that reaches a consumer is the indented code block: an
|
|
301
|
+
* indented block directly after a list is absorbed as list content, so a
|
|
302
|
+
* `Code` node with neither `lang` nor `fenceChar` emits **fenced** in that
|
|
303
|
+
* position and indented everywhere else. A byte-level assertion over
|
|
304
|
+
* synthesized code blocks therefore depends on the preceding sibling.
|
|
305
|
+
*
|
|
306
|
+
* **The posture that makes this a non-issue**, and the one a consumer
|
|
307
|
+
* building trees for byte-level assertion should adopt: set `fenceChar` on
|
|
308
|
+
* every `Code` node that carries neither a `lang` nor one already, in a
|
|
309
|
+
* post-decode walk that reaches nested nodes. The choice then leaves the
|
|
310
|
+
* emitter entirely and no output depends on a node's neighbours.
|
|
311
|
+
*
|
|
312
|
+
* **MDX nodes (constructed trees only — the parser reads no MDX syntax)**
|
|
313
|
+
* serialize to valid MDX with the mdast-util-mdx defaults, and these
|
|
314
|
+
* choices are part of the same stability commitment: attribute values
|
|
315
|
+
* quote with `"` (the quote escaped as `"`), an empty element
|
|
316
|
+
* self-closes spaced (`<a />`), a fragment is `<></>`, flow children take
|
|
317
|
+
* block layout indented two spaces per JSX ancestor, attributes move onto
|
|
318
|
+
* their own lines only when one carries a line ending, expressions emit
|
|
319
|
+
* `{expr}` with two-space continuation indent, and ESM values emit
|
|
320
|
+
* verbatim. A tree containing any MDX node additionally escapes `{` in
|
|
321
|
+
* text — MDX makes it significant — while a tree with none serializes
|
|
322
|
+
* byte-identically to the table above.
|
|
323
|
+
*
|
|
324
|
+
* To *normalize* a document to different choices — a `-` list rewritten to
|
|
325
|
+
* `*`, setext headings rewritten to ATX — use `MarkdownFormat` with
|
|
326
|
+
* `MarkdownFormattingOptions`. That is the configurable surface; this one
|
|
327
|
+
* deliberately is not.
|
|
328
|
+
*
|
|
195
329
|
* Failure is rare by design, symmetric with parse: only a hardening-guard
|
|
196
330
|
* trip on a tree nesting past the depth cap fails, and only synthesized
|
|
197
331
|
* or decoded trees can nest that far.
|
|
@@ -220,6 +354,18 @@ var Markdown = class Markdown {
|
|
|
220
354
|
* {@link Markdown.stringifyResult} — synchronous callers can use that
|
|
221
355
|
* variant directly.
|
|
222
356
|
*
|
|
357
|
+
* @remarks
|
|
358
|
+
* The canonical form is a **stability commitment** and safe to assert
|
|
359
|
+
* against byte-for-byte; the choices it makes, and the reason changing one
|
|
360
|
+
* is a breaking change, are documented on
|
|
361
|
+
* {@link Markdown.stringifyResult}.
|
|
362
|
+
*
|
|
363
|
+
* **Fidelity fields must be set on the DECODED tree.** `Mdast.fromMdast`
|
|
364
|
+
* admits spec mdast and strips everything outside it, so a `fenceChar`,
|
|
365
|
+
* `headingStyle`, `markerChar` or `delimiter` placed on a plain mdast tree
|
|
366
|
+
* before admission is silently dropped and the canonical default applies.
|
|
367
|
+
* Set them on the nodes that come back.
|
|
368
|
+
*
|
|
223
369
|
* @param root - The document tree to serialize.
|
|
224
370
|
* @returns An `Effect` that succeeds with markdown source, or fails with
|
|
225
371
|
* {@link MarkdownStringifyError}.
|
package/MarkdownDocument.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { MarkdownRange } from "./MarkdownEdit.js";
|
|
2
2
|
import { Definition, Root } from "./MarkdownNode.js";
|
|
3
|
+
import { scanFrontmatter } from "./internal/blocks/frontmatter.js";
|
|
3
4
|
import { preprocessLines } from "./internal/preprocess.js";
|
|
4
5
|
import { normalizeLabelText } from "./internal/references.js";
|
|
5
|
-
import { scanFrontmatter } from "./internal/blocks/frontmatter.js";
|
|
6
6
|
import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
|
|
7
7
|
import { parsePassResult } from "./Markdown.js";
|
|
8
8
|
import { Effect, Result, Schema } from "effect";
|
|
@@ -115,7 +115,8 @@ const phrasingText = (nodes) => {
|
|
|
115
115
|
case "strong":
|
|
116
116
|
case "delete":
|
|
117
117
|
case "link":
|
|
118
|
-
case "linkReference":
|
|
118
|
+
case "linkReference":
|
|
119
|
+
case "mdxJsxTextElement": out += phrasingText(node.children);
|
|
119
120
|
}
|
|
120
121
|
return out;
|
|
121
122
|
};
|
|
@@ -275,7 +276,7 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
|
|
|
275
276
|
offset: start,
|
|
276
277
|
length: boundary - start
|
|
277
278
|
}),
|
|
278
|
-
children: blocks.slice(current.index + 1, end).filter((child) => child.type !== "frontmatter"),
|
|
279
|
+
children: blocks.slice(current.index + 1, end).filter((child) => child.type !== "frontmatter" && child.type !== "mdxjsEsm"),
|
|
279
280
|
text: phrasingText(block.children),
|
|
280
281
|
bodyRange: MarkdownRange.make({
|
|
281
282
|
offset: bodyStart,
|
package/MarkdownNode.js
CHANGED
|
@@ -324,6 +324,8 @@ const PhrasingContent = Schema.suspend(() => Schema.Union([
|
|
|
324
324
|
InlineCode,
|
|
325
325
|
Link,
|
|
326
326
|
LinkReference,
|
|
327
|
+
MdxJsxTextElement,
|
|
328
|
+
MdxTextExpression,
|
|
327
329
|
Strong,
|
|
328
330
|
Text
|
|
329
331
|
]));
|
|
@@ -567,6 +569,8 @@ const FlowContent = Schema.suspend(() => Schema.Union([
|
|
|
567
569
|
Heading,
|
|
568
570
|
Html,
|
|
569
571
|
List,
|
|
572
|
+
MdxFlowExpression,
|
|
573
|
+
MdxJsxFlowElement,
|
|
570
574
|
Paragraph,
|
|
571
575
|
Table,
|
|
572
576
|
ThematicBreak
|
|
@@ -581,6 +585,147 @@ const FlowContent = Schema.suspend(() => Schema.Union([
|
|
|
581
585
|
*/
|
|
582
586
|
const ListContent = Schema.suspend(() => Schema.Union([ListItem]));
|
|
583
587
|
/**
|
|
588
|
+
* MdxJsxAttributeValueExpression — a JSX attribute value written as an
|
|
589
|
+
* expression (`<a b={c} />`); `value` holds the expression source text
|
|
590
|
+
* between the braces, never evaluated or parsed.
|
|
591
|
+
*
|
|
592
|
+
* The primary construction path is a JSON-encoded prop:
|
|
593
|
+
* `MdxJsxAttributeValueExpression.make({ value: JSON.stringify(props) })`.
|
|
594
|
+
*
|
|
595
|
+
* @public
|
|
596
|
+
*/
|
|
597
|
+
var MdxJsxAttributeValueExpression = class extends Schema.Class("MdxJsxAttributeValueExpression")({
|
|
598
|
+
type: Schema.tag("mdxJsxAttributeValueExpression"),
|
|
599
|
+
value: Schema.String,
|
|
600
|
+
position: NodePosition
|
|
601
|
+
}) {};
|
|
602
|
+
/**
|
|
603
|
+
* MdxJsxAttribute — a named JSX attribute (`<a b="c" />`). `value` is a
|
|
604
|
+
* string literal, a {@link MdxJsxAttributeValueExpression}, or — for a
|
|
605
|
+
* boolean attribute (`<a b />`) — absent or `null`, both of which the
|
|
606
|
+
* mdast-util-mdx-jsx contract spells (its parser writes `null`; absence is
|
|
607
|
+
* the constructed-tree spelling). The serializer treats the two identically.
|
|
608
|
+
*
|
|
609
|
+
* `name` must be non-empty — an attribute without a name has no MDX spelling,
|
|
610
|
+
* so the schema refuses it at construction and decode (the oracle's
|
|
611
|
+
* serialize-time crash, moved to the admission boundary).
|
|
612
|
+
*
|
|
613
|
+
* @public
|
|
614
|
+
*/
|
|
615
|
+
var MdxJsxAttribute = class extends Schema.Class("MdxJsxAttribute")(Schema.Struct({
|
|
616
|
+
type: Schema.tag("mdxJsxAttribute"),
|
|
617
|
+
name: Schema.String,
|
|
618
|
+
value: Schema.optionalKey(Schema.NullOr(Schema.Union([MdxJsxAttributeValueExpression, Schema.String]))),
|
|
619
|
+
position: NodePosition
|
|
620
|
+
}).pipe(Schema.check(Schema.makeFilter((attribute) => attribute.name.length === 0 ? "an MDX JSX attribute requires a non-empty name" : void 0)))) {};
|
|
621
|
+
/**
|
|
622
|
+
* MdxJsxExpressionAttribute — a JSX attribute written whole as an expression
|
|
623
|
+
* (`<a {...b} />`); `value` holds the expression source text between the
|
|
624
|
+
* braces.
|
|
625
|
+
*
|
|
626
|
+
* @public
|
|
627
|
+
*/
|
|
628
|
+
var MdxJsxExpressionAttribute = class extends Schema.Class("MdxJsxExpressionAttribute")({
|
|
629
|
+
type: Schema.tag("mdxJsxExpressionAttribute"),
|
|
630
|
+
value: Schema.String,
|
|
631
|
+
position: NodePosition
|
|
632
|
+
}) {};
|
|
633
|
+
/**
|
|
634
|
+
* The union of every node that may appear in a JSX element's `attributes`
|
|
635
|
+
* array. A real `Schema.Union` for the construction pass-through documented
|
|
636
|
+
* on `RowContent`.
|
|
637
|
+
*
|
|
638
|
+
* Attribute carriers are node-shaped values — they carry `type` and
|
|
639
|
+
* `position` per the mdast-util-mdx-jsx contract — but they are **not tree
|
|
640
|
+
* content**: they never appear in a `children` array, so they are excluded
|
|
641
|
+
* from `MarkdownNode` and invisible to the visitor and to
|
|
642
|
+
* `MarkdownDocument.find`.
|
|
643
|
+
*
|
|
644
|
+
* @public
|
|
645
|
+
*/
|
|
646
|
+
const MdxJsxAttributeContent = Schema.Union([MdxJsxAttribute, MdxJsxExpressionAttribute]);
|
|
647
|
+
/**
|
|
648
|
+
* MdxJsxFlowElement — a JSX element in flow (block) position (`<Component />`
|
|
649
|
+
* on its own lines). `name` is `null` for a fragment (`<></>`); children are
|
|
650
|
+
* flow content, per the oracle's `BlockContent | DefinitionContent` model.
|
|
651
|
+
*
|
|
652
|
+
* A fragment cannot carry attributes — that shape has no MDX spelling — so
|
|
653
|
+
* the schema refuses it at construction and decode. A **named** element's
|
|
654
|
+
* name must be non-empty on the same terms: `""` has no MDX spelling either
|
|
655
|
+
* (the oracle's parser only ever produces a real name or `null`, and its
|
|
656
|
+
* serializer treats a falsy name as the fragment), so `null` is the one
|
|
657
|
+
* fragment spelling and the empty string fails typed.
|
|
658
|
+
*
|
|
659
|
+
* @public
|
|
660
|
+
*/
|
|
661
|
+
var MdxJsxFlowElement = class extends Schema.Class("MdxJsxFlowElement")(Schema.Struct({
|
|
662
|
+
type: Schema.tag("mdxJsxFlowElement"),
|
|
663
|
+
name: Schema.NullOr(Schema.String),
|
|
664
|
+
attributes: Schema.Array(MdxJsxAttributeContent),
|
|
665
|
+
children: Schema.Array(Schema.suspend(() => FlowContent)),
|
|
666
|
+
position: NodePosition
|
|
667
|
+
}).pipe(Schema.check(Schema.makeFilter((element) => {
|
|
668
|
+
if (element.name !== null && element.name.length === 0) return "an MDX JSX element requires a non-empty name (`null` is the fragment spelling)";
|
|
669
|
+
return element.name === null && element.attributes.length > 0 ? "an MDX JSX fragment cannot carry attributes" : void 0;
|
|
670
|
+
})))) {};
|
|
671
|
+
/**
|
|
672
|
+
* MdxJsxTextElement — a JSX element in text (phrasing) position
|
|
673
|
+
* (`a <b>c</b> d`). `name` is `null` for a fragment; children are phrasing
|
|
674
|
+
* content. Refuses attributes on a fragment and an empty-string name, on the
|
|
675
|
+
* same terms as {@link MdxJsxFlowElement}.
|
|
676
|
+
*
|
|
677
|
+
* @public
|
|
678
|
+
*/
|
|
679
|
+
var MdxJsxTextElement = class extends Schema.Class("MdxJsxTextElement")(Schema.Struct({
|
|
680
|
+
type: Schema.tag("mdxJsxTextElement"),
|
|
681
|
+
name: Schema.NullOr(Schema.String),
|
|
682
|
+
attributes: Schema.Array(MdxJsxAttributeContent),
|
|
683
|
+
children: Schema.Array(Schema.suspend(() => PhrasingContent)),
|
|
684
|
+
position: NodePosition
|
|
685
|
+
}).pipe(Schema.check(Schema.makeFilter((element) => {
|
|
686
|
+
if (element.name !== null && element.name.length === 0) return "an MDX JSX element requires a non-empty name (`null` is the fragment spelling)";
|
|
687
|
+
return element.name === null && element.attributes.length > 0 ? "an MDX JSX fragment cannot carry attributes" : void 0;
|
|
688
|
+
})))) {};
|
|
689
|
+
/**
|
|
690
|
+
* MdxFlowExpression — an expression in flow (block) position (`{a + b}` on
|
|
691
|
+
* its own lines); `value` holds the expression source text between the
|
|
692
|
+
* braces.
|
|
693
|
+
*
|
|
694
|
+
* @public
|
|
695
|
+
*/
|
|
696
|
+
var MdxFlowExpression = class extends Schema.Class("MdxFlowExpression")({
|
|
697
|
+
type: Schema.tag("mdxFlowExpression"),
|
|
698
|
+
value: Schema.String,
|
|
699
|
+
position: NodePosition
|
|
700
|
+
}) {};
|
|
701
|
+
/**
|
|
702
|
+
* MdxTextExpression — an expression in text (phrasing) position
|
|
703
|
+
* (`a {b} c`); `value` holds the expression source text between the braces.
|
|
704
|
+
*
|
|
705
|
+
* @public
|
|
706
|
+
*/
|
|
707
|
+
var MdxTextExpression = class extends Schema.Class("MdxTextExpression")({
|
|
708
|
+
type: Schema.tag("mdxTextExpression"),
|
|
709
|
+
value: Schema.String,
|
|
710
|
+
position: NodePosition
|
|
711
|
+
}) {};
|
|
712
|
+
/**
|
|
713
|
+
* MdxjsEsm — an MDX ESM block (`import`/`export` statements); `value` holds
|
|
714
|
+
* the statement source verbatim.
|
|
715
|
+
*
|
|
716
|
+
* Only ever a child of {@link Root}, per the mdast-util-mdxjs-esm content
|
|
717
|
+
* registration — ESM cannot nest inside a JSX element or any other
|
|
718
|
+
* container. As with the frontmatter head node, the constraint is structural
|
|
719
|
+
* (the `Root` children union admits it, no other union does), not validated.
|
|
720
|
+
*
|
|
721
|
+
* @public
|
|
722
|
+
*/
|
|
723
|
+
var MdxjsEsm = class extends Schema.Class("MdxjsEsm")({
|
|
724
|
+
type: Schema.tag("mdxjsEsm"),
|
|
725
|
+
value: Schema.String,
|
|
726
|
+
position: NodePosition
|
|
727
|
+
}) {};
|
|
728
|
+
/**
|
|
584
729
|
* The frontmatter formats the capture recognizes, keyed by their opening
|
|
585
730
|
* fence: `---` is yaml, `+++` is toml and `---json` is json.
|
|
586
731
|
*
|
|
@@ -631,13 +776,18 @@ const FrontmatterContent = Schema.suspend(() => Frontmatter);
|
|
|
631
776
|
* mdast leaves a root's content model open; a parsed markdown document is
|
|
632
777
|
* flow content, optionally headed by one {@link Frontmatter} node — mdast's
|
|
633
778
|
* `FlowContentFrontmatter` merge, which admits frontmatter at the root and
|
|
634
|
-
* nowhere else.
|
|
779
|
+
* nowhere else. {@link MdxjsEsm} is likewise admitted at the root and nowhere
|
|
780
|
+
* else, per mdast-util-mdxjs-esm's `RootContentMap` registration.
|
|
635
781
|
*
|
|
636
782
|
* @public
|
|
637
783
|
*/
|
|
638
784
|
var Root = class extends Schema.Class("Root")({
|
|
639
785
|
type: Schema.tag("root"),
|
|
640
|
-
children: Schema.Array(Schema.suspend(() => Schema.Union([
|
|
786
|
+
children: Schema.Array(Schema.suspend(() => Schema.Union([
|
|
787
|
+
Frontmatter,
|
|
788
|
+
MdxjsEsm,
|
|
789
|
+
FlowContent
|
|
790
|
+
]))),
|
|
641
791
|
position: NodePosition
|
|
642
792
|
}) {};
|
|
643
793
|
/**
|
|
@@ -650,10 +800,11 @@ const MarkdownNode = Schema.suspend(() => Schema.Union([
|
|
|
650
800
|
FrontmatterContent,
|
|
651
801
|
FlowContent,
|
|
652
802
|
ListContent,
|
|
803
|
+
MdxjsEsm,
|
|
653
804
|
PhrasingContent,
|
|
654
805
|
RowContent,
|
|
655
806
|
TableContent
|
|
656
807
|
]));
|
|
657
808
|
|
|
658
809
|
//#endregion
|
|
659
|
-
export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterFormat, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, MarkdownNode, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar };
|
|
810
|
+
export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterFormat, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, MarkdownNode, MdxFlowExpression, MdxJsxAttribute, MdxJsxAttributeContent, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar };
|