@effected/markdown 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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}.
@@ -235,6 +309,18 @@ var Markdown = class Markdown {
235
309
  * post-decode walk that reaches nested nodes. The choice then leaves the
236
310
  * emitter entirely and no output depends on a node's neighbours.
237
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
+ *
238
324
  * To *normalize* a document to different choices — a `-` list rewritten to
239
325
  * `*`, setext headings rewritten to ATX — use `MarkdownFormat` with
240
326
  * `MarkdownFormattingOptions`. That is the configurable surface; this one
@@ -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": out += phrasingText(node.children);
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([Frontmatter, FlowContent]))),
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 };
package/Mdast.js CHANGED
@@ -206,8 +206,40 @@ const projectNode = (node) => {
206
206
  value: node.value,
207
207
  position
208
208
  };
209
+ case "mdxJsxFlowElement":
210
+ case "mdxJsxTextElement": return {
211
+ type: node.type,
212
+ name: node.name,
213
+ attributes: node.attributes.map((attribute) => projectMdxAttribute(attribute)),
214
+ children: projectChildren(node.children),
215
+ position
216
+ };
217
+ case "mdxFlowExpression":
218
+ case "mdxTextExpression":
219
+ case "mdxjsEsm": return {
220
+ type: node.type,
221
+ value: node.value,
222
+ position
223
+ };
209
224
  }
210
225
  };
226
+ const projectMdxAttribute = (attribute) => {
227
+ if (attribute.type === "mdxJsxExpressionAttribute") return {
228
+ type: "mdxJsxExpressionAttribute",
229
+ value: attribute.value,
230
+ position: projectPosition(attribute.position)
231
+ };
232
+ const value = attribute.value;
233
+ return {
234
+ type: "mdxJsxAttribute",
235
+ name: attribute.name,
236
+ value: value === void 0 || value === null ? null : typeof value === "string" ? value : {
237
+ type: "mdxJsxAttributeValueExpression",
238
+ value: value.value
239
+ },
240
+ position: projectPosition(attribute.position)
241
+ };
242
+ };
211
243
  /** The frontmatter literal node types foreign mdast spells per format. */
212
244
  const frontmatterTypes = /* @__PURE__ */ new Map([
213
245
  ["yaml", "yaml"],
@@ -271,7 +303,10 @@ const admittedFields = {
271
303
  "spread"
272
304
  ],
273
305
  listItem: ["spread", "checked"],
274
- table: ["align"]
306
+ table: ["align"],
307
+ mdxFlowExpression: ["value"],
308
+ mdxTextExpression: ["value"],
309
+ mdxjsEsm: ["value"]
275
310
  };
276
311
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
277
312
  const completePoint = (value) => isRecord(value) && typeof value.line === "number" && typeof value.column === "number" && typeof value.offset === "number";
@@ -304,6 +339,13 @@ const normalizeNode = (value) => {
304
339
  value: value.value,
305
340
  position: normalizePosition(value.position)
306
341
  };
342
+ if (type === "mdxJsxFlowElement" || type === "mdxJsxTextElement") return {
343
+ type,
344
+ name: value.name,
345
+ attributes: Array.isArray(value.attributes) ? value.attributes.map(normalizeMdxAttribute) : value.attributes,
346
+ children: Array.isArray(value.children) ? value.children.map(normalizeNode) : value.children,
347
+ position: normalizePosition(value.position)
348
+ };
307
349
  const admitted = admittedFields[type];
308
350
  if (admitted === void 0) return value;
309
351
  const normalized = { type };
@@ -319,6 +361,29 @@ const normalizeNode = (value) => {
319
361
  normalized.position = normalizePosition(value.position);
320
362
  return normalized;
321
363
  };
364
+ const normalizeMdxAttribute = (value) => {
365
+ if (!isRecord(value) || typeof value.type !== "string") return value;
366
+ if (value.type === "mdxJsxExpressionAttribute") return {
367
+ type: "mdxJsxExpressionAttribute",
368
+ value: value.value,
369
+ position: normalizePosition(value.position)
370
+ };
371
+ if (value.type === "mdxJsxAttribute") {
372
+ const raw = value.value;
373
+ const normalized = {
374
+ type: "mdxJsxAttribute",
375
+ name: value.name,
376
+ position: normalizePosition(value.position)
377
+ };
378
+ if (raw !== void 0) normalized.value = isRecord(raw) && raw.type === "mdxJsxAttributeValueExpression" ? {
379
+ type: "mdxJsxAttributeValueExpression",
380
+ value: raw.value,
381
+ position: normalizePosition(raw.position)
382
+ } : raw;
383
+ return normalized;
384
+ }
385
+ return value;
386
+ };
322
387
  const decodeRoot = Schema.decodeUnknownResult(Root);
323
388
  /**
324
389
  * The mdast projection facade — the remark-ecosystem interop boundary.