@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.
package/README.md CHANGED
@@ -174,6 +174,8 @@ The three codecs — `YamlFrontmatter` (`---`), `TomlFrontmatter` (`+++`) and `J
174
174
 
175
175
  Frontmatter blocks can also describe their own schema. `SchemaResolver.classify` sorts a `$schema` value into a tagged union — `ByUrl`, `ByPath`, `Inline` and `ByName` — and `SchemaResolver.fromRegistry` resolves `ByName` declarations like `skill@2.1.0` against schemas you register. URLs, paths and inline documents are carried as data and never fetched: this is a pure package and it performs no IO.
176
176
 
177
+ When the body should not go through the CommonMark engine at all — an MDX page, a template, a snapshot-hash contract that needs byte-exact boundaries — `FrontmatterSource.split` runs the same closed fence grammar over the raw string and nothing else. It is total: it returns the block's format, the exact bytes between the fence lines and the body remainder (`body` is always exactly `source.slice(bodyOffset)`), with absence a representable result rather than an error. `FrontmatterSource.join` serializes the parts back, reproducing the original bytes for an unmodified round-trip. Two edges normalize instead of reproducing: a closing fence that ends the document without a line terminator gains one, and fence lines with mismatched terminators (say `---\r\n … ---\n`) re-emit both with the opening fence's. The value and body bytes survive verbatim in every case, and neither is ever parsed at this level — decoding stays the codec modules' business.
178
+
177
179
  ## Working with the tree
178
180
 
179
181
  `MarkdownDocument` derives its navigation accessors from the tree, so they can never disagree with it. `links` collects every URL-bearing node and passes `url` through exactly as written — bundle-relative hrefs are never normalized:
@@ -261,6 +263,37 @@ if (Result.isSuccess(parsed)) {
261
263
 
262
264
  `Mdast.fromMdast` goes the other way as a checked admission boundary: it validates a foreign tree and synthesizes zero-width sentinel positions where one is absent or incomplete. Trees admitted that way serve tree-level workflows and canonical `stringify`; offset-splice editing needs the real positions only a parse produces.
263
265
 
266
+ ## MDX trees
267
+
268
+ The MDX node vocabulary — `MdxJsxFlowElement` and `MdxJsxTextElement` (with `MdxJsxAttribute`, `MdxJsxExpressionAttribute` and `MdxJsxAttributeValueExpression` as their attribute carriers), `MdxFlowExpression`, `MdxTextExpression` and `MdxjsEsm` — is shaped exactly to the `mdast-util-mdx` contracts and constructs like every other node class. The parser does not read MDX syntax; these nodes exist so a **synthesized** tree can carry JSX and serialize to valid MDX instead of joining strings:
269
+
270
+ ```ts
271
+ import { Markdown, MdxJsxAttribute, MdxJsxAttributeValueExpression, MdxJsxFlowElement, Root } from "@effected/markdown";
272
+ import { Result } from "effect";
273
+
274
+ const page = Root.make({
275
+ children: [
276
+ MdxJsxFlowElement.make({
277
+ name: "ApiSignature",
278
+ attributes: [
279
+ MdxJsxAttribute.make({
280
+ name: "code",
281
+ value: MdxJsxAttributeValueExpression.make({ value: JSON.stringify({ lang: "ts" }) }),
282
+ }),
283
+ ],
284
+ children: [],
285
+ }),
286
+ ],
287
+ });
288
+
289
+ const mdx = Markdown.stringifyResult(page);
290
+ if (Result.isSuccess(mdx)) {
291
+ console.log(mdx.success); // <ApiSignature code={{"lang":"ts"}} />
292
+ }
293
+ ```
294
+
295
+ Serialization matches `mdxJsxToMarkdown`'s defaults — `"` quotes, spaced self-closing `<a />`, `<></>` fragments, flow children indented two spaces per JSX ancestor, `{expr}` expressions, ESM values verbatim — and a tree containing any MDX node escapes `{` in text the way MDX requires, while a tree with none serializes byte-identically to the table below. The shapes that have no MDX spelling are unconstructible rather than serialization errors: a fragment cannot carry attributes and an attribute requires a non-empty name, refused at `make` and at decode.
296
+
264
297
  ## The canonical form is stable
265
298
 
266
299
  `Markdown.stringify` takes no options. It emits one canonical form, that form is pinned by byte-level tests, the engine behind it is cross-checked against commonmark.js over the full CommonMark 0.31.2 corpus, and **changing any of it is a breaking change to this package rather than a patch**. So a test may assert on these bytes, and a pipeline that needs stable rendered markdown should serialize through here instead of a third-party stringifier whose defaults are free to move between releases.
@@ -278,6 +311,8 @@ For a node carrying no fidelity field:
278
311
  | Block separation | exactly one blank line |
279
312
  | Document | a single trailing newline |
280
313
 
314
+ MDX nodes are part of the same commitment: the serialization choices listed under [MDX trees](#mdx-trees) are pinned the same way, and only trees that actually carry an MDX node pay the extra `{` escape.
315
+
281
316
  **Representability wins over the table.** The canonical form never emits text that would re-parse as something else, so a row yields where the two conflict. The case that reaches a consumer is the indented code block: an indented block directly after a list is absorbed as list content, so a `Code` node with neither `lang` nor `fenceChar` emits fenced in that position and indented everywhere else. A byte-level assertion over synthesized code blocks therefore depends on the preceding sibling.
282
317
 
283
318
  The posture that makes this a non-issue — and the one to adopt if you build trees and assert on their bytes — is to set `fenceChar` on every `Code` node carrying neither a `lang` nor one already, in a post-decode walk that reaches nested nodes. The choice then leaves the emitter entirely and no output depends on a node's neighbours.
@@ -291,8 +326,11 @@ To *normalize* an existing document to different choices, use `MarkdownFormat` w
291
326
  ## Features
292
327
 
293
328
  - `Markdown` — `parse`/`stringify` as `Effect`s with typed `MarkdownParseError`/`MarkdownStringifyError` channels, the pure `parseResult`/`stringifyResult` twins for synchronous callers, and the `MarkdownFromString` two-way codec.
329
+ - `Markdown.parsePhrasing`/`parsePhrasingResult` — parse a prose fragment as a single paragraph's inline content, without a full document parse and a paragraph splice: blank lines stay inline, references never form (no reference context), and positions are correct relative to the input string.
294
330
  - `MarkdownDocument` — source, tree, diagnostics and the link-definition index, plus the derived `headings`, `sections` and `links` accessors, the `firstSection`/`sectionByHeading` finders over a `DocumentSection` (heading, depth, range, body), the `find`/`findAll` tree queries over type-narrowed selectors, and the `frontmatter` capture with `hasFrontmatterBlock` to tell an absent block from an uncaptured one.
295
331
  - The mdast-shaped node classes — the CommonMark types plus GFM's `delete`, `table`, `tableRow`, `tableCell`, `footnoteDefinition`, `footnoteReference` and task-list `checked`, each carrying unist positions with byte offsets and this package's fidelity fields. `position` defaults to the zero-width synthetic sentinel, so a replacement fragment constructs in one line — `Text.make({ value: "shipped" })`.
332
+ - The MDX node vocabulary — `MdxJsxFlowElement`/`MdxJsxTextElement` with their attribute carriers, `MdxFlowExpression`/`MdxTextExpression` and `MdxjsEsm`, shaped to the `mdast-util-mdx` contracts for construction and serialization (the parser reads no MDX syntax).
333
+ - `FrontmatterSource` — string-level frontmatter `split`/`join` over the same closed fence grammar, byte-exact boundaries, no parsing of value or body; for bodies the CommonMark engine should not touch.
296
334
  - `MarkdownEdit` / `MarkdownRange` (with `applyAll`) — the non-mutating text-edit vocabulary, field-identical to `@effected/jsonc`'s, `@effected/yaml`'s and `@effected/toml`'s.
297
335
  - `MarkdownFormat` — `format`/`formatToString` compute conservative marker-normalization edits; `modify`/`modifyToString` replace a node by identity through the canonical stringifier.
298
336
  - `MarkdownVisitor` — walk a parsed tree as a lazy `Stream` of `Enter`/`Exit` events with child-index paths and depth.
package/index.d.ts CHANGED
@@ -353,11 +353,14 @@ declare class FootnoteReference extends FootnoteReference_base {}
353
353
  */
354
354
  declare const PhrasingContent: Schema.Codec<PhrasingContent>;
355
355
  /**
356
- * The union of all phrasing-content node types.
356
+ * The union of all phrasing-content node types. Widened for MDX with
357
+ * {@link MdxJsxTextElement} and {@link MdxTextExpression}, per
358
+ * mdast-util-mdx's `PhrasingContentMap` registrations — the parser never
359
+ * produces either; they serve constructed trees.
357
360
  *
358
361
  * @public
359
362
  */
360
- type PhrasingContent = Break | Delete | Emphasis | FootnoteReference | Html | Image | ImageReference | InlineCode | Link | LinkReference | Strong | Text;
363
+ type PhrasingContent = Break | Delete | Emphasis | FootnoteReference | Html | Image | ImageReference | InlineCode | Link | LinkReference | MdxJsxTextElement | MdxTextExpression | Strong | Text;
361
364
  declare const ThematicBreak_base: Schema.Class<ThematicBreak, Schema.Struct<{
362
365
  readonly type: Schema.tag<"thematicBreak">;
363
366
  readonly position: Schema.withConstructorDefault<typeof Position>;
@@ -618,11 +621,14 @@ declare const FlowContent: Schema.Codec<FlowContent>;
618
621
  /**
619
622
  * The union of all flow-content node types. Includes mdast's `Content`
620
623
  * category (`Definition | Paragraph`) inline, as the spec's `FlowContent`
621
- * definition does, and the GFM extras `FootnoteDefinition` and `Table`.
624
+ * definition does, the GFM extras `FootnoteDefinition` and `Table`, and the
625
+ * MDX extras {@link MdxJsxFlowElement} and {@link MdxFlowExpression} per
626
+ * mdast-util-mdx's `BlockContentMap` registrations — the parser never
627
+ * produces the MDX members; they serve constructed trees.
622
628
  *
623
629
  * @public
624
630
  */
625
- type FlowContent = Blockquote | Code | Definition | FootnoteDefinition | Heading | Html | List | Paragraph | Table | ThematicBreak;
631
+ type FlowContent = Blockquote | Code | Definition | FootnoteDefinition | Heading | Html | List | MdxFlowExpression | MdxJsxFlowElement | Paragraph | Table | ThematicBreak;
626
632
  /**
627
633
  * The union of every node that may appear where mdast expects **list**
628
634
  * content. A one-member union, kept because mdast names the category and
@@ -638,6 +644,155 @@ declare const ListContent: Schema.Codec<ListContent>;
638
644
  * @public
639
645
  */
640
646
  type ListContent = ListItem;
647
+ declare const MdxJsxAttributeValueExpression_base: Schema.Class<MdxJsxAttributeValueExpression, Schema.Struct<{
648
+ readonly type: Schema.tag<"mdxJsxAttributeValueExpression">;
649
+ readonly value: Schema.String;
650
+ readonly position: Schema.withConstructorDefault<typeof Position>;
651
+ }>, {}>;
652
+ /**
653
+ * MdxJsxAttributeValueExpression — a JSX attribute value written as an
654
+ * expression (`<a b={c} />`); `value` holds the expression source text
655
+ * between the braces, never evaluated or parsed.
656
+ *
657
+ * The primary construction path is a JSON-encoded prop:
658
+ * `MdxJsxAttributeValueExpression.make({ value: JSON.stringify(props) })`.
659
+ *
660
+ * @public
661
+ */
662
+ declare class MdxJsxAttributeValueExpression extends MdxJsxAttributeValueExpression_base {}
663
+ declare const MdxJsxAttribute_base: Schema.Class<MdxJsxAttribute, Schema.Struct<{
664
+ readonly type: Schema.tag<"mdxJsxAttribute">;
665
+ readonly name: Schema.String;
666
+ readonly value: Schema.optionalKey<Schema.NullOr<Schema.Union<readonly [typeof MdxJsxAttributeValueExpression, Schema.String]>>>;
667
+ readonly position: Schema.withConstructorDefault<typeof Position>;
668
+ }>, {}>;
669
+ /**
670
+ * MdxJsxAttribute — a named JSX attribute (`<a b="c" />`). `value` is a
671
+ * string literal, a {@link MdxJsxAttributeValueExpression}, or — for a
672
+ * boolean attribute (`<a b />`) — absent or `null`, both of which the
673
+ * mdast-util-mdx-jsx contract spells (its parser writes `null`; absence is
674
+ * the constructed-tree spelling). The serializer treats the two identically.
675
+ *
676
+ * `name` must be non-empty — an attribute without a name has no MDX spelling,
677
+ * so the schema refuses it at construction and decode (the oracle's
678
+ * serialize-time crash, moved to the admission boundary).
679
+ *
680
+ * @public
681
+ */
682
+ declare class MdxJsxAttribute extends MdxJsxAttribute_base {}
683
+ declare const MdxJsxExpressionAttribute_base: Schema.Class<MdxJsxExpressionAttribute, Schema.Struct<{
684
+ readonly type: Schema.tag<"mdxJsxExpressionAttribute">;
685
+ readonly value: Schema.String;
686
+ readonly position: Schema.withConstructorDefault<typeof Position>;
687
+ }>, {}>;
688
+ /**
689
+ * MdxJsxExpressionAttribute — a JSX attribute written whole as an expression
690
+ * (`<a {...b} />`); `value` holds the expression source text between the
691
+ * braces.
692
+ *
693
+ * @public
694
+ */
695
+ declare class MdxJsxExpressionAttribute extends MdxJsxExpressionAttribute_base {}
696
+ /**
697
+ * The union of every node that may appear in a JSX element's `attributes`
698
+ * array. A real `Schema.Union` for the construction pass-through documented
699
+ * on `RowContent`.
700
+ *
701
+ * Attribute carriers are node-shaped values — they carry `type` and
702
+ * `position` per the mdast-util-mdx-jsx contract — but they are **not tree
703
+ * content**: they never appear in a `children` array, so they are excluded
704
+ * from `MarkdownNode` and invisible to the visitor and to
705
+ * `MarkdownDocument.find`.
706
+ *
707
+ * @public
708
+ */
709
+ declare const MdxJsxAttributeContent: Schema.Codec<MdxJsxAttributeContent>;
710
+ /**
711
+ * The union of all JSX attribute node types.
712
+ *
713
+ * @public
714
+ */
715
+ type MdxJsxAttributeContent = MdxJsxAttribute | MdxJsxExpressionAttribute;
716
+ declare const MdxJsxFlowElement_base: Schema.Class<MdxJsxFlowElement, Schema.Struct<{
717
+ readonly type: Schema.tag<"mdxJsxFlowElement">;
718
+ readonly name: Schema.NullOr<Schema.String>;
719
+ readonly attributes: Schema.$Array<Schema.Codec<MdxJsxAttributeContent, MdxJsxAttributeContent, never, never>>;
720
+ readonly children: Schema.$Array<Schema.suspend<Schema.Codec<FlowContent, FlowContent, never, never>>>;
721
+ readonly position: Schema.withConstructorDefault<typeof Position>;
722
+ }>, {}>;
723
+ /**
724
+ * MdxJsxFlowElement — a JSX element in flow (block) position (`<Component />`
725
+ * on its own lines). `name` is `null` for a fragment (`<></>`); children are
726
+ * flow content, per the oracle's `BlockContent | DefinitionContent` model.
727
+ *
728
+ * A fragment cannot carry attributes — that shape has no MDX spelling — so
729
+ * the schema refuses it at construction and decode. A **named** element's
730
+ * name must be non-empty on the same terms: `""` has no MDX spelling either
731
+ * (the oracle's parser only ever produces a real name or `null`, and its
732
+ * serializer treats a falsy name as the fragment), so `null` is the one
733
+ * fragment spelling and the empty string fails typed.
734
+ *
735
+ * @public
736
+ */
737
+ declare class MdxJsxFlowElement extends MdxJsxFlowElement_base {}
738
+ declare const MdxJsxTextElement_base: Schema.Class<MdxJsxTextElement, Schema.Struct<{
739
+ readonly type: Schema.tag<"mdxJsxTextElement">;
740
+ readonly name: Schema.NullOr<Schema.String>;
741
+ readonly attributes: Schema.$Array<Schema.Codec<MdxJsxAttributeContent, MdxJsxAttributeContent, never, never>>;
742
+ readonly children: Schema.$Array<Schema.suspend<Schema.Codec<PhrasingContent, PhrasingContent, never, never>>>;
743
+ readonly position: Schema.withConstructorDefault<typeof Position>;
744
+ }>, {}>;
745
+ /**
746
+ * MdxJsxTextElement — a JSX element in text (phrasing) position
747
+ * (`a <b>c</b> d`). `name` is `null` for a fragment; children are phrasing
748
+ * content. Refuses attributes on a fragment and an empty-string name, on the
749
+ * same terms as {@link MdxJsxFlowElement}.
750
+ *
751
+ * @public
752
+ */
753
+ declare class MdxJsxTextElement extends MdxJsxTextElement_base {}
754
+ declare const MdxFlowExpression_base: Schema.Class<MdxFlowExpression, Schema.Struct<{
755
+ readonly type: Schema.tag<"mdxFlowExpression">;
756
+ readonly value: Schema.String;
757
+ readonly position: Schema.withConstructorDefault<typeof Position>;
758
+ }>, {}>;
759
+ /**
760
+ * MdxFlowExpression — an expression in flow (block) position (`{a + b}` on
761
+ * its own lines); `value` holds the expression source text between the
762
+ * braces.
763
+ *
764
+ * @public
765
+ */
766
+ declare class MdxFlowExpression extends MdxFlowExpression_base {}
767
+ declare const MdxTextExpression_base: Schema.Class<MdxTextExpression, Schema.Struct<{
768
+ readonly type: Schema.tag<"mdxTextExpression">;
769
+ readonly value: Schema.String;
770
+ readonly position: Schema.withConstructorDefault<typeof Position>;
771
+ }>, {}>;
772
+ /**
773
+ * MdxTextExpression — an expression in text (phrasing) position
774
+ * (`a {b} c`); `value` holds the expression source text between the braces.
775
+ *
776
+ * @public
777
+ */
778
+ declare class MdxTextExpression extends MdxTextExpression_base {}
779
+ declare const MdxjsEsm_base: Schema.Class<MdxjsEsm, Schema.Struct<{
780
+ readonly type: Schema.tag<"mdxjsEsm">;
781
+ readonly value: Schema.String;
782
+ readonly position: Schema.withConstructorDefault<typeof Position>;
783
+ }>, {}>;
784
+ /**
785
+ * MdxjsEsm — an MDX ESM block (`import`/`export` statements); `value` holds
786
+ * the statement source verbatim.
787
+ *
788
+ * Only ever a child of {@link Root}, per the mdast-util-mdxjs-esm content
789
+ * registration — ESM cannot nest inside a JSX element or any other
790
+ * container. As with the frontmatter head node, the constraint is structural
791
+ * (the `Root` children union admits it, no other union does), not validated.
792
+ *
793
+ * @public
794
+ */
795
+ declare class MdxjsEsm extends MdxjsEsm_base {}
641
796
  /**
642
797
  * The frontmatter formats the capture recognizes, keyed by their opening
643
798
  * fence: `---` is yaml, `+++` is toml and `---json` is json.
@@ -694,7 +849,7 @@ declare const FrontmatterContent: Schema.Codec<FrontmatterContent>;
694
849
  type FrontmatterContent = Frontmatter;
695
850
  declare const Root_base: Schema.Class<Root, Schema.Struct<{
696
851
  readonly type: Schema.tag<"root">;
697
- readonly children: Schema.$Array<Schema.suspend<Schema.Codec<Frontmatter | FlowContent, Frontmatter | FlowContent, never, never>>>;
852
+ readonly children: Schema.$Array<Schema.suspend<Schema.Codec<Frontmatter | MdxjsEsm | FlowContent, Frontmatter | MdxjsEsm | FlowContent, never, never>>>;
698
853
  readonly position: Schema.withConstructorDefault<typeof Position>;
699
854
  }>, {}>;
700
855
  /**
@@ -703,7 +858,8 @@ declare const Root_base: Schema.Class<Root, Schema.Struct<{
703
858
  * mdast leaves a root's content model open; a parsed markdown document is
704
859
  * flow content, optionally headed by one {@link Frontmatter} node — mdast's
705
860
  * `FlowContentFrontmatter` merge, which admits frontmatter at the root and
706
- * nowhere else.
861
+ * nowhere else. {@link MdxjsEsm} is likewise admitted at the root and nowhere
862
+ * else, per mdast-util-mdxjs-esm's `RootContentMap` registration.
707
863
  *
708
864
  * @public
709
865
  */
@@ -714,7 +870,7 @@ declare class Root extends Root_base {}
714
870
  *
715
871
  * @public
716
872
  */
717
- type MarkdownNode = Root | FrontmatterContent | FlowContent | ListContent | PhrasingContent | RowContent | TableContent;
873
+ type MarkdownNode = Root | FrontmatterContent | FlowContent | ListContent | MdxjsEsm | PhrasingContent | RowContent | TableContent;
718
874
  /**
719
875
  * A schema matching any node in the tree.
720
876
  *
@@ -929,6 +1085,67 @@ declare class Markdown {
929
1085
  * fails with {@link MarkdownParseError}.
930
1086
  */
931
1087
  static readonly parse: (text: string, options?: MarkdownParseOptions | undefined) => Effect.Effect<Root, MarkdownParseError, never>;
1088
+ /**
1089
+ * Parse a text fragment as a single paragraph's inline content,
1090
+ * synchronously, as a `Result` — the phrasing-level twin of
1091
+ * {@link Markdown.parseResult}, for callers holding already-markdown
1092
+ * prose (a link-carrying sentence, a backtick span) who want its
1093
+ * phrasing nodes without a full document parse and a paragraph splice.
1094
+ *
1095
+ * @remarks
1096
+ * The whole input is prepared exactly the way the block pass prepares one
1097
+ * paragraph's content — leading and trailing whitespace trimmed, line
1098
+ * terminators normalized to `\n` — and handed to the inline pass, so the
1099
+ * result matches the `children` of the paragraph a full parse of the same
1100
+ * fragment would produce. Node positions are correct relative to the
1101
+ * input string.
1102
+ *
1103
+ * Two consequences of the single-paragraph contract:
1104
+ *
1105
+ * - **Blank lines do not break blocks.** A `\n\n` in the input stays
1106
+ * inline content (literal newlines in a text node); nothing at this
1107
+ * level opens a heading, list or code block — `# foo` is the text
1108
+ * `# foo`.
1109
+ * - **No reference context exists.** The fragment carries no definitions,
1110
+ * so `[foo]`, `![foo]` and `[^foo]` remain literal text — the same
1111
+ * result a full parse of the isolated fragment produces.
1112
+ *
1113
+ * `options.dialect` selects the inline dialect (default `"gfm"`);
1114
+ * `options.frontmatter` has no effect at phrasing level. Failure is rare
1115
+ * by design, on {@link Markdown.parseResult}'s terms: only a
1116
+ * hardening-guard trip (delimiter or bracket nesting past the cap) fails.
1117
+ *
1118
+ * @example
1119
+ * ```ts
1120
+ * import { Markdown } from "@effected/markdown";
1121
+ * import { Result } from "effect";
1122
+ *
1123
+ * const ok = Markdown.parsePhrasingResult("see [the docs](./docs.md)");
1124
+ * if (Result.isSuccess(ok)) {
1125
+ * console.log(ok.success.map((node) => node.type)); // => ["text", "link"]
1126
+ * }
1127
+ * ```
1128
+ *
1129
+ * @param text - The prose fragment to parse.
1130
+ * @param options - Optional {@link MarkdownParseOptions}; the dialect
1131
+ * defaults to `"gfm"`.
1132
+ * @returns A `Result` succeeding with the fragment's phrasing content, or
1133
+ * failing with {@link MarkdownParseError}.
1134
+ */
1135
+ static parsePhrasingResult(text: string, options?: MarkdownParseOptions): Result.Result<ReadonlyArray<PhrasingContent>, MarkdownParseError>;
1136
+ /**
1137
+ * Parse a text fragment as a single paragraph's inline content. Defined
1138
+ * in terms of {@link Markdown.parsePhrasingResult} — synchronous callers
1139
+ * can use that variant directly; the single-paragraph contract (blank
1140
+ * lines stay inline, references never form) is documented there.
1141
+ *
1142
+ * @param text - The prose fragment to parse.
1143
+ * @param options - Optional {@link MarkdownParseOptions}; the dialect
1144
+ * defaults to `"gfm"`.
1145
+ * @returns An `Effect` that succeeds with the fragment's phrasing
1146
+ * content, or fails with {@link MarkdownParseError}.
1147
+ */
1148
+ static readonly parsePhrasing: (text: string, options?: MarkdownParseOptions | undefined) => Effect.Effect<readonly PhrasingContent[], MarkdownParseError, never>;
932
1149
  /**
933
1150
  * Serialize a {@link Root} tree to canonical markdown, synchronously, as a
934
1151
  * `Result`. The pure primitive twin of {@link Markdown.stringify}, on the
@@ -989,6 +1206,18 @@ declare class Markdown {
989
1206
  * post-decode walk that reaches nested nodes. The choice then leaves the
990
1207
  * emitter entirely and no output depends on a node's neighbours.
991
1208
  *
1209
+ * **MDX nodes (constructed trees only — the parser reads no MDX syntax)**
1210
+ * serialize to valid MDX with the mdast-util-mdx defaults, and these
1211
+ * choices are part of the same stability commitment: attribute values
1212
+ * quote with `"` (the quote escaped as `&#x22;`), an empty element
1213
+ * self-closes spaced (`<a />`), a fragment is `<></>`, flow children take
1214
+ * block layout indented two spaces per JSX ancestor, attributes move onto
1215
+ * their own lines only when one carries a line ending, expressions emit
1216
+ * `{expr}` with two-space continuation indent, and ESM values emit
1217
+ * verbatim. A tree containing any MDX node additionally escapes `{` in
1218
+ * text — MDX makes it significant — while a tree with none serializes
1219
+ * byte-identically to the table above.
1220
+ *
992
1221
  * To *normalize* a document to different choices — a `-` list rewritten to
993
1222
  * `*`, setext headings rewritten to ATX — use `MarkdownFormat` with
994
1223
  * `MarkdownFormattingOptions`. That is the configurable surface; this one
@@ -1963,6 +2192,144 @@ declare class SchemaResolver {
1963
2192
  static fromRegistry(registrations: Readonly<Record<string, Schema.Top>>): FrontmatterSchemaResolver;
1964
2193
  }
1965
2194
  //#endregion
2195
+ //#region src/FrontmatterSource.d.ts
2196
+ /**
2197
+ * The three line-terminator spellings a fence line may end with. Recorded by
2198
+ * {@link FrontmatterSource.split} as fidelity — what the source's fence
2199
+ * lines actually used — and consumed by {@link FrontmatterSource.join}.
2200
+ *
2201
+ * @public
2202
+ */
2203
+ declare const FrontmatterNewline: Schema.Literals<readonly ["\n", "\r\n", "\r"]>;
2204
+ /**
2205
+ * The union of all fence line-terminator literals.
2206
+ *
2207
+ * @public
2208
+ */
2209
+ type FrontmatterNewline = typeof FrontmatterNewline.Type;
2210
+ declare const FrontmatterSourceBlock_base: Schema.Class<FrontmatterSourceBlock, Schema.Struct<{
2211
+ readonly format: Schema.Literals<readonly ["yaml", "toml", "json"]>;
2212
+ readonly value: Schema.String;
2213
+ readonly newline: Schema.optionalKey<Schema.Literals<readonly ["\n", "\r\n", "\r"]>>;
2214
+ }>, {}>;
2215
+ /**
2216
+ * A frontmatter block at string level: the format its fence declared, the
2217
+ * exact bytes between the fence lines, and the fence lines' terminator.
2218
+ *
2219
+ * @remarks
2220
+ * `value` is the exact source slice strictly between the opening fence line
2221
+ * (its terminator included) and the closing fence line: every value line
2222
+ * WITH its own terminator, verbatim — so a block whose value is one blank
2223
+ * line (`"\n"`) stays distinct from a block with no value lines (`""`), and
2224
+ * interior CRLF terminators survive untouched. This deliberately differs
2225
+ * from the parsed `Frontmatter` NODE's `value`, which drops the final
2226
+ * terminator; the node feeds codecs, this feeds byte-exact reconstruction.
2227
+ * {@link FrontmatterSource.join} accepts a value without a trailing
2228
+ * terminator too, appending `newline` so a hand-built block cannot fuse its
2229
+ * last line into the closing fence.
2230
+ *
2231
+ * `newline` is a fidelity field: `split` always records the terminator
2232
+ * following the opening fence; when absent (a hand-built block), `join`
2233
+ * uses `"\n"`.
2234
+ *
2235
+ * @public
2236
+ */
2237
+ declare class FrontmatterSourceBlock extends FrontmatterSourceBlock_base {}
2238
+ declare const FrontmatterSourceSplit_base: Schema.Class<FrontmatterSourceSplit, Schema.Struct<{
2239
+ readonly frontmatter: Schema.optionalKey<typeof FrontmatterSourceBlock>;
2240
+ readonly body: Schema.String;
2241
+ readonly bodyOffset: Schema.withConstructorDefault<Schema.Number>;
2242
+ }>, {}>;
2243
+ /**
2244
+ * The result of {@link FrontmatterSource.split}: the frontmatter block when
2245
+ * one exists, the exact body remainder, and the body's byte offset.
2246
+ *
2247
+ * @remarks
2248
+ * `body` is exactly `source.slice(bodyOffset)` — the string and the offset
2249
+ * can never disagree. With no block, `body` is the whole source and
2250
+ * `bodyOffset` is `0`. With a block, `bodyOffset` sits immediately after
2251
+ * the closing fence line and its line terminator (the terminator belongs to
2252
+ * the fence line, as every line's terminator belongs to its line), which is
2253
+ * gray-matter's body boundary; when the closing fence ends the document
2254
+ * there is no terminator and the body is empty.
2255
+ *
2256
+ * `bodyOffset` is informational output of `split`; `join` never reads it,
2257
+ * and it constructor-defaults to `0` so a hand-built value for `join` can
2258
+ * omit it.
2259
+ *
2260
+ * @public
2261
+ */
2262
+ declare class FrontmatterSourceSplit extends FrontmatterSourceSplit_base {}
2263
+ /**
2264
+ * The string-level frontmatter facade: split raw source at the fence
2265
+ * boundaries and join it back, without parsing anything.
2266
+ *
2267
+ * @public
2268
+ */
2269
+ declare class FrontmatterSource {
2270
+ private constructor();
2271
+ /**
2272
+ * Split raw source into its frontmatter block and body, at string level.
2273
+ * Total: never fails, never parses — absence of a block (including an
2274
+ * opening fence with no closing fence, which per the grammar is not
2275
+ * frontmatter) is a representable result, not an error.
2276
+ *
2277
+ * @remarks
2278
+ * The grammar is the parser's own offset-0 pre-scan, over raw bytes:
2279
+ * the fence set is closed (`---` yaml, `+++` toml, `---json` json), a
2280
+ * fence line is exactly the fence with no surrounding whitespace, and a
2281
+ * leading BOM means offset 0 is not a fence — strip one before calling
2282
+ * if the source may carry it. Unlike the parse path there is no capture
2283
+ * toggle here, so absence is one fact: the source has no closed block.
2284
+ * (`MarkdownDocument`'s two-reason absence exists because parsing can be
2285
+ * asked not to look; this surface always looks.)
2286
+ *
2287
+ * Byte exactness: `source.slice(0, result.bodyOffset) + result.body`
2288
+ * reassembles `source` exactly, and {@link FrontmatterSource.join}
2289
+ * reproduces the original bytes for an unmodified round-trip — see
2290
+ * `join` for the two normalizations at the contract's edge.
2291
+ *
2292
+ * @example
2293
+ * ```ts
2294
+ * import { FrontmatterSource } from "@effected/markdown";
2295
+ *
2296
+ * const split = FrontmatterSource.split("---\ntitle: hi\n---\n\n# Body\n");
2297
+ * console.log(split.frontmatter?.format); // => "yaml"
2298
+ * console.log(split.frontmatter?.value); // => "title: hi\n"
2299
+ * console.log(split.body); // => "\n# Body\n"
2300
+ * ```
2301
+ *
2302
+ * @param source - The raw document source, body format irrelevant.
2303
+ * @returns A {@link FrontmatterSourceSplit}; `frontmatter` is absent when
2304
+ * the source opens with no closed fence block.
2305
+ */
2306
+ static split(source: string): FrontmatterSourceSplit;
2307
+ /**
2308
+ * Serialize a split back to one source string. Total and pure.
2309
+ *
2310
+ * @remarks
2311
+ * With no block the body is returned verbatim. With a block the output
2312
+ * is the opening fence, `newline` (default `"\n"`), the value — a
2313
+ * non-empty value missing a final line terminator gets `newline`
2314
+ * appended, so the closing fence always starts its own line — the
2315
+ * closing fence, `newline`, then the body verbatim.
2316
+ *
2317
+ * **Round-trip contract:** `join(split(source)) === source`, byte for
2318
+ * byte, for every source whose closing fence line ends with a line
2319
+ * terminator matching the opening fence's. The two edges where join
2320
+ * normalizes instead: a closing fence at end-of-document gains a final
2321
+ * `newline` (the block's one unterminated spelling), and fence lines
2322
+ * with MISMATCHED terminators (say `---\r\n … ---\n`) re-emit both with
2323
+ * the opening one. Value and body bytes survive verbatim in every case.
2324
+ *
2325
+ * @param split - The parts to serialize, typically from
2326
+ * {@link FrontmatterSource.split} or built with
2327
+ * `FrontmatterSourceSplit.make`.
2328
+ * @returns The combined source string.
2329
+ */
2330
+ static join(split: FrontmatterSourceSplit): string;
2331
+ }
2332
+ //#endregion
1966
2333
  //#region src/JsonFrontmatter.d.ts
1967
2334
  /**
1968
2335
  * The json frontmatter codec, over `@effected/jsonc`.
@@ -2480,5 +2847,5 @@ declare const TomlFrontmatter: FrontmatterCodec;
2480
2847
  */
2481
2848
  declare const YamlFrontmatter: FrontmatterCodec;
2482
2849
  //#endregion
2483
- export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, type DocumentHeading, type DocumentLink, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, type FrontmatterCodec, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, type FrontmatterResolveError, type FrontmatterSchemaError, type FrontmatterSchemaResolver, FrontmatterValidationError, type FrontmatterWriteError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, type LinkBearingNode, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, type MarkdownNodeOfType, type MarkdownNodeType, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, type MarkdownPath, MarkdownRange, type MarkdownRangeLike, type MarkdownSegment, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, type MdastNode, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, type SectionHeadingMatch, type SectionQueryOptions, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
2850
+ export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, type DocumentHeading, type DocumentLink, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, type FrontmatterCodec, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterNewline, type FrontmatterResolveError, type FrontmatterSchemaError, type FrontmatterSchemaResolver, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit, FrontmatterValidationError, type FrontmatterWriteError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, type LinkBearingNode, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, type MarkdownNodeOfType, type MarkdownNodeType, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, type MarkdownPath, MarkdownRange, type MarkdownRangeLike, type MarkdownSegment, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, type MdastNode, MdxFlowExpression, MdxJsxAttribute, MdxJsxAttributeContent, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, type SectionHeadingMatch, type SectionQueryOptions, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
2484
2851
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { MarkdownEdit, MarkdownRange } from "./MarkdownEdit.js";
2
- import { 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 } from "./MarkdownNode.js";
2
+ import { 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 } from "./MarkdownNode.js";
3
3
  import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterValidationError, MarkdownFrontmatter } from "./Frontmatter.js";
4
4
  import { SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError } from "./FrontmatterResolver.js";
5
+ import { FrontmatterNewline, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit } from "./FrontmatterSource.js";
5
6
  import { JsonFrontmatter } from "./JsonFrontmatter.js";
6
7
  import { MarkdownDiagnostic, MarkdownParseErrorCode } from "./MarkdownDiagnostic.js";
7
8
  import { Markdown, MarkdownDialect, MarkdownParseError, MarkdownParseOptions, MarkdownStringifyError } from "./Markdown.js";
@@ -12,4 +13,4 @@ import { Mdast, MdastDecodeError } from "./Mdast.js";
12
13
  import { TomlFrontmatter } from "./TomlFrontmatter.js";
13
14
  import { YamlFrontmatter } from "./YamlFrontmatter.js";
14
15
 
15
- export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
16
+ export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterNewline, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, MdxFlowExpression, MdxJsxAttribute, MdxJsxAttributeContent, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
@@ -1,9 +1,9 @@
1
1
  import { Frontmatter, Point, Position, Root } from "../MarkdownNode.js";
2
+ import { scanFrontmatter } from "./blocks/frontmatter.js";
2
3
  import { makeBlockNode } from "./blockTypes.js";
3
4
  import { columnsToNextTabStop, preprocessLines } from "./preprocess.js";
4
5
  import { isHtmlBlockEnd } from "./blocks/htmlBlock.js";
5
6
  import { blockDialect } from "./blockRegistry.js";
6
- import { scanFrontmatter } from "./blocks/frontmatter.js";
7
7
  import { GuardExceeded } from "./carriers.js";
8
8
  import { LineIndex } from "./lineIndex.js";
9
9
  import { prepareInline } from "./rawInline.js";
@@ -51,6 +51,52 @@ const scanFrontmatter = (lines, text) => {
51
51
  }
52
52
  return null;
53
53
  };
54
+ /** The terminator starting at `offset`, or null when none does. */
55
+ const terminatorAt = (source, offset) => {
56
+ const code = source.charCodeAt(offset);
57
+ if (code === 10) return "\n";
58
+ if (code === 13) return source.charCodeAt(offset + 1) === 10 ? "\r\n" : "\r";
59
+ return null;
60
+ };
61
+ /**
62
+ * Scan raw source for a frontmatter block — the same closed fence grammar as
63
+ * {@link scanFrontmatter}, over unpreprocessed bytes, returning exact
64
+ * offsets. The two scans agree on every input about WHETHER a block exists
65
+ * (the preprocessor's substitutions are length-preserving and can only break
66
+ * a fence match both scans require); this one exists for the string-level
67
+ * split/join surface, whose contract is byte exactness.
68
+ *
69
+ * Returns `null` when the document has no frontmatter — no fence at offset
70
+ * 0, or an opening fence with no closing fence, which per the grammar is not
71
+ * frontmatter at all.
72
+ */
73
+ const scanRawFrontmatter = (source) => {
74
+ let index = 0;
75
+ while (index < source.length && terminatorAt(source, index) === null) index += 1;
76
+ const openNewline = index < source.length ? terminatorAt(source, index) : null;
77
+ const rule = FENCES.get(source.slice(0, index));
78
+ if (rule === void 0 || openNewline === null) return null;
79
+ const valueStart = index + openNewline.length;
80
+ let lineStart = valueStart;
81
+ index = valueStart;
82
+ while (index <= source.length) {
83
+ const terminator = index === source.length ? null : terminatorAt(source, index);
84
+ if (terminator === null && index < source.length) {
85
+ index += 1;
86
+ continue;
87
+ }
88
+ if (source.slice(lineStart, index) === rule.close) return {
89
+ format: rule.format,
90
+ value: source.slice(valueStart, lineStart),
91
+ newline: openNewline,
92
+ bodyOffset: terminator === null ? index : index + terminator.length
93
+ };
94
+ if (terminator === null) break;
95
+ index += terminator.length;
96
+ lineStart = index;
97
+ }
98
+ return null;
99
+ };
54
100
 
55
101
  //#endregion
56
- export { scanFrontmatter };
102
+ export { scanFrontmatter, scanRawFrontmatter };