@effected/markdown 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/Frontmatter.js +272 -0
  2. package/FrontmatterResolver.js +273 -0
  3. package/JsonFrontmatter.js +46 -0
  4. package/LICENSE +21 -0
  5. package/Markdown.js +251 -0
  6. package/MarkdownDiagnostic.js +85 -0
  7. package/MarkdownDocument.js +248 -0
  8. package/MarkdownEdit.js +52 -0
  9. package/MarkdownFormat.js +380 -0
  10. package/MarkdownNode.js +641 -0
  11. package/MarkdownVisitor.js +88 -0
  12. package/Mdast.js +384 -0
  13. package/README.md +255 -0
  14. package/TomlFrontmatter.js +44 -0
  15. package/YamlFrontmatter.js +45 -0
  16. package/index.d.ts +2190 -0
  17. package/index.js +15 -0
  18. package/internal/blockParser.js +419 -0
  19. package/internal/blockRegistry.js +90 -0
  20. package/internal/blockTypes.js +27 -0
  21. package/internal/blocks/atxHeading.js +54 -0
  22. package/internal/blocks/blockquote.js +40 -0
  23. package/internal/blocks/code.js +90 -0
  24. package/internal/blocks/document.js +17 -0
  25. package/internal/blocks/fencedCode.js +26 -0
  26. package/internal/blocks/footnoteDefinition.js +84 -0
  27. package/internal/blocks/frontmatter.js +56 -0
  28. package/internal/blocks/htmlBlock.js +72 -0
  29. package/internal/blocks/indentedCode.js +16 -0
  30. package/internal/blocks/linkReferenceDefinition.js +72 -0
  31. package/internal/blocks/list.js +159 -0
  32. package/internal/blocks/paragraph.js +27 -0
  33. package/internal/blocks/setextHeading.js +24 -0
  34. package/internal/blocks/table.js +378 -0
  35. package/internal/blocks/taskListItem.js +36 -0
  36. package/internal/blocks/thematicBreak.js +35 -0
  37. package/internal/carriers.js +42 -0
  38. package/internal/entities.js +26 -0
  39. package/internal/entityMap.js +7 -0
  40. package/internal/htmlTags.js +18 -0
  41. package/internal/inlineNode.js +54 -0
  42. package/internal/inlineParser.js +403 -0
  43. package/internal/inlineRegistry.js +68 -0
  44. package/internal/inlines/autolink.js +36 -0
  45. package/internal/inlines/autolinkLiteral.js +374 -0
  46. package/internal/inlines/codeSpan.js +32 -0
  47. package/internal/inlines/emphasis.js +79 -0
  48. package/internal/inlines/entity.js +21 -0
  49. package/internal/inlines/escape.js +34 -0
  50. package/internal/inlines/footnoteReference.js +63 -0
  51. package/internal/inlines/lineBreak.js +25 -0
  52. package/internal/inlines/link.js +212 -0
  53. package/internal/inlines/rawHtml.js +27 -0
  54. package/internal/inlines/strikethrough.js +81 -0
  55. package/internal/inlines/text.js +22 -0
  56. package/internal/lineIndex.js +76 -0
  57. package/internal/patterns.js +26 -0
  58. package/internal/preprocess.js +58 -0
  59. package/internal/rawInline.js +57 -0
  60. package/internal/references.js +160 -0
  61. package/internal/segments.js +36 -0
  62. package/internal/stringify.js +519 -0
  63. package/internal/unescape.js +18 -0
  64. package/package.json +61 -0
  65. package/tsdoc-metadata.json +11 -0
package/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # @effected/markdown
2
+
3
+ [![npm](https://img.shields.io/npm/v/@effected%2Fmarkdown?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/markdown)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
+ [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](https://www.typescriptlang.org/)
7
+
8
+ Zero-dependency CommonMark 0.31.2 and GFM parsing, editing and transformation expressed as Effect schemas and pure functions. Parse markdown into mdast-shaped nodes carrying byte offsets, navigate headings, sections and links, compute surgical offset-splice edits, normalize markers, decode frontmatter into a validated domain schema, project to and from plain mdast for the remark ecosystem, and walk a document as a `Stream`.
9
+
10
+ > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
+ > development against a single pinned Effect v4 beta. Packages graduate to
12
+ > `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
13
+ > exactly the ones the kit is built and tested against, install
14
+ > [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
15
+ >
16
+ > **Stability: unstable.** This package's API surface is not yet considered
17
+ > complete and may change across `0.x` releases. Pin an exact version — even a
18
+ > package marked *stable* before `1.0.0` can introduce a breaking change by
19
+ > accident, and an exact pin turns that into a type-check error rather than a
20
+ > runtime surprise. Full policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
21
+
22
+ ## Why @effected/markdown
23
+
24
+ Markdown is where documentation, changelogs, knowledge bases and AI-agent context files actually live — files people edit by hand and expect to survive a tool touching them. The remark ecosystem is the JavaScript default, but its serializer reformats by design: read a document and write it back and you get remark's idea of markdown, not yours. Nobody in the ecosystem ships a lossless markdown CST, and the remark maintainers themselves point at positional splicing instead.
25
+
26
+ This package takes that advice as the architecture. Every node carries byte offsets alongside unist line/column positions, so an edit is a splice against the original source rather than a re-serialization of a tree: change a heading and the blank lines, HTML comments and hand-tuned spacing everywhere else come through byte-identical. `MarkdownEdit` is field-identical to `JsoncEdit`, `YamlEdit` and `TomlEdit`, so the same editing vocabulary spans every format in the kit.
27
+
28
+ The engine is a vendored, hardened port of commonmark.js — the reference parser maintained by the spec author — restructured as one module per construct behind dialect-keyed registries. That means no runtime parser dependency and no plugin surface to reason about: `commonmark` and `gfm` are the two dialects, `gfm` is the default, and both are pinned by the upstream conformance corpora. Parse is near-total, because CommonMark has no syntax errors: the typed error channel carries hardening-guard trips only, never "malformed markdown".
29
+
30
+ Nodes are shaped to mdast's exact type names and field shapes, so the tree is already the shape the remark ecosystem speaks, and `Mdast.toMdast` strips this package's fidelity fields to hand you plain spec-valid mdast JSON. Frontmatter — which mdast has no parsing story for at all — is captured behind a parse toggle and decoded through free-standing per-format codecs, giving typed gray-matter parity without dragging three format engines into a bundle that needs one.
31
+
32
+ Markdown to HTML is deliberately out of scope. This package parses, edits and transforms markdown; rendering belongs to whatever renderer you already have, reached through the mdast projection.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ npm install @effected/markdown effect
38
+ ```
39
+
40
+ ```bash
41
+ pnpm add @effected/markdown effect
42
+ ```
43
+
44
+ Requires Node.js >=24.11.0. `effect` v4 is a peer dependency; the package itself adds no runtime dependencies.
45
+
46
+ `@effected/yaml`, `@effected/toml` and `@effected/jsonc` are **optional** peers, required only by the frontmatter codec module you import. A consumer decoding yaml frontmatter installs `@effected/yaml` and nothing else; a consumer who never touches frontmatter installs none of them.
47
+
48
+ ```bash
49
+ npm install @effected/yaml
50
+ ```
51
+
52
+ All `@effected/*` packages are ESM-only: the exports maps publish only `import` conditions, so `require()` — including tools that resolve in CJS mode — fails with Node's `ERR_PACKAGE_PATH_NOT_EXPORTED` rather than loading a CJS build that does not exist. Import from an ES module.
53
+
54
+ ## Quick start
55
+
56
+ `MarkdownDocument.parse` gives you the source, the tree, the definition index and the navigation accessors in one value:
57
+
58
+ ```ts
59
+ import { MarkdownDocument } from "@effected/markdown";
60
+ import { Effect } from "effect";
61
+
62
+ const source = `# Release notes
63
+
64
+ ## Fixed
65
+
66
+ - Tables no longer ~~drop~~ trailing cells.
67
+ `;
68
+
69
+ const program = Effect.gen(function* () {
70
+ const doc = yield* MarkdownDocument.parse(source);
71
+ return doc.headings.map((heading) => `${"#".repeat(heading.depth)} ${heading.text}`);
72
+ });
73
+
74
+ Effect.runPromise(program).then(console.log);
75
+ // [ "# Release notes", "## Fixed" ]
76
+ ```
77
+
78
+ The dialect defaults to `gfm`, so the strikethrough above parses as a `delete` node with no configuration. Pass `MarkdownParseOptions.make({ dialect: "commonmark" })` for strict CommonMark.
79
+
80
+ Every parse has a synchronous twin — `MarkdownDocument.parseResult` and `Markdown.parseResult` return a `Result` — so a build script, a Vite plugin or a language-server tick can call in without an Effect runtime.
81
+
82
+ ## Editing without reformatting the document
83
+
84
+ `MarkdownFormat.modify` computes a `MarkdownEdit` array against the parsed document; `modifyToString` applies it in one step. The target is a node from the document's own tree, matched by identity, and everything the edit does not cover survives byte-for-byte:
85
+
86
+ ```ts
87
+ import { MarkdownDocument, MarkdownFormat } from "@effected/markdown";
88
+ import { Effect } from "effect";
89
+
90
+ const source = `# Release notes
91
+
92
+ See the [changelog](./CHANGELOG.md).
93
+ `;
94
+
95
+ const program = Effect.gen(function* () {
96
+ const doc = yield* MarkdownDocument.parse(source);
97
+ const label = doc.find("text")!;
98
+ return yield* MarkdownFormat.modifyToString(doc, label, "Release notes (2026)");
99
+ });
100
+
101
+ Effect.runPromise(program).then(console.log);
102
+ // # Release notes (2026)
103
+ //
104
+ // See the [changelog](./CHANGELOG.md).
105
+ ```
106
+
107
+ `find` walks the tree in document pre-order and returns the document's own node, so the match feeds `modify` by identity. Replacements are literal strings or node fragments, and both render through the canonical stringifier, so a modified document re-parses cleanly by construction — you cannot splice in raw markdown that reopens a fence or breaks a table.
108
+
109
+ `MarkdownFormat.format` handles the other half: conservative marker normalization, never content rewriting. It converts heading style, bullet character, emphasis marker, fence character and thematic-break character, and skips any conversion that would not be safe rather than attempting it cleverly:
110
+
111
+ ```ts
112
+ import { MarkdownFormat, MarkdownFormattingOptions } from "@effected/markdown";
113
+
114
+ const source = `Setext heading
115
+ ==============
116
+
117
+ * one
118
+ * two
119
+ `;
120
+
121
+ const options = MarkdownFormattingOptions.make({ headingStyle: "atx", bulletChar: "-" });
122
+
123
+ console.log(MarkdownFormat.formatToString(source, undefined, options));
124
+ // # Setext heading
125
+ //
126
+ // - one
127
+ // - two
128
+
129
+ console.log(MarkdownFormat.format(source, undefined, options));
130
+ // [
131
+ // { offset: 0, length: 29, content: "# Setext heading" },
132
+ // { offset: 31, length: 1, content: "-" },
133
+ // { offset: 37, length: 1, content: "-" }
134
+ // ]
135
+ ```
136
+
137
+ `format` is pure and total, and the edits are non-mutating data — hand them to `MarkdownEdit.applyAll`, or send them to an editor as a text-edit payload.
138
+
139
+ ## Frontmatter
140
+
141
+ Frontmatter capture is opt-in, because enabling it changes how a document opening with `---` parses: CommonMark reads `---\ntitle: x\n---` as a thematic break and a setext heading, and that spec-conformant reading holds unless you ask for something else. Turn it on and compose your schema with a codec for typed gray-matter parity:
142
+
143
+ ```ts
144
+ import { MarkdownDocument, MarkdownFrontmatter, MarkdownParseOptions, YamlFrontmatter } from "@effected/markdown";
145
+ import { Effect, Schema } from "effect";
146
+
147
+ const source = `---
148
+ title: Release notes
149
+ draft: false
150
+ ---
151
+
152
+ # Release notes
153
+ `;
154
+
155
+ const Post = Schema.Struct({ title: Schema.String, draft: Schema.Boolean });
156
+ const decodePost = MarkdownFrontmatter.schema(Post, YamlFrontmatter);
157
+
158
+ const program = Effect.gen(function* () {
159
+ const doc = yield* MarkdownDocument.parse(source, MarkdownParseOptions.make({ frontmatter: true }));
160
+ return yield* decodePost(doc);
161
+ });
162
+
163
+ Effect.runPromise(program).then(console.log);
164
+ // { title: "Release notes", draft: false }
165
+ ```
166
+
167
+ Each stage fails typed and separately: no capture is a `FrontmatterMissingError` (catch the tag for optional semantics), a codec handed the wrong fence is a `FrontmatterFormatMismatchError`, unparseable content is a `FrontmatterDecodeError` carrying the format package's own positioned failure structurally, and schema-invalid data is a `FrontmatterValidationError` carrying the structured issue tree rather than a stringified rendering.
168
+
169
+ The three codecs — `YamlFrontmatter` (`---`), `TomlFrontmatter` (`+++`) and `JsonFrontmatter` (`---json`) — are free-standing named exports, one module each, deliberately never collected into a namespace object. Naming one codec is what pulls in its format engine, so a JSON-frontmatter consumer never pays for the yaml parser.
170
+
171
+ 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.
172
+
173
+ ## Working with the tree
174
+
175
+ `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:
176
+
177
+ ```ts
178
+ import { MarkdownDocument } from "@effected/markdown";
179
+ import { Effect } from "effect";
180
+
181
+ const source = `# Release notes
182
+
183
+ See the [changelog](./CHANGELOG.md) and the [docs](https://example.com/docs).
184
+ `;
185
+
186
+ const program = Effect.gen(function* () {
187
+ const doc = yield* MarkdownDocument.parse(source);
188
+ return doc.links.map((link) => link.url);
189
+ });
190
+
191
+ Effect.runPromise(program).then(console.log);
192
+ // [ "./CHANGELOG.md", "https://example.com/docs" ]
193
+ ```
194
+
195
+ `headings` lists every heading wherever it sits, including inside blockquotes and list items. `sections` are delimited by root-level headings only, and each section's range spans its subsections, so the edit layer can splice a whole section out in one edit.
196
+
197
+ For anything the accessors do not cover, `find` and `findAll` walk the whole tree in document pre-order. A string selector matches the node's `type` and narrows the result — `doc.findAll("heading")` is `ReadonlyArray<Heading>`, `doc.find("table")` is `Table | undefined` — and a type-guard predicate narrows the same way. The nodes come back by identity, so `doc.findAll("heading")[1]` addresses the second heading for `MarkdownFormat.modify` without raw child indexing.
198
+
199
+ `MarkdownVisitor.visit` streams the same walk as `Enter`/`Exit` events carrying the node, its child-index path and its depth:
200
+
201
+ ```ts
202
+ import { MarkdownDocument, MarkdownVisitor } from "@effected/markdown";
203
+ import { Effect, Stream } from "effect";
204
+
205
+ const program = Effect.gen(function* () {
206
+ const doc = yield* MarkdownDocument.parse("# Title\n\nA *b* c\n");
207
+ const events = yield* Stream.runCollect(MarkdownVisitor.visit(doc.root));
208
+ return Array.from(events).map((event) => `${event._tag}:${"node" in event ? event.node.type : "?"}`);
209
+ });
210
+
211
+ Effect.runPromise(program).then(console.log);
212
+ // [ "Enter:root", "Enter:heading", "Enter:text", "Exit:text", "Exit:heading",
213
+ // "Enter:paragraph", "Enter:text", "Exit:text", "Enter:emphasis", "Enter:text",
214
+ // "Exit:text", "Exit:emphasis", "Enter:text", "Exit:text", "Exit:paragraph", "Exit:root" ]
215
+ ```
216
+
217
+ ## mdast interop
218
+
219
+ The node classes already use mdast's type names and field shapes; `Mdast.toMdast` strips the fidelity fields this package adds — bullet characters, fence style, ATX-versus-setext spelling — and emits plain spec-valid mdast JSON that the remark ecosystem consumes directly, including `mdast-util-to-hast` if you want hast:
220
+
221
+ ```ts
222
+ import { Markdown, Mdast } from "@effected/markdown";
223
+ import { Result } from "effect";
224
+
225
+ const parsed = Markdown.parseResult("A *b* c\n");
226
+ if (Result.isSuccess(parsed)) {
227
+ console.log(JSON.stringify(Mdast.toMdast(parsed.success)).slice(0, 62));
228
+ // {"type":"root","children":[{"type":"paragraph","children":[{"t
229
+ }
230
+ ```
231
+
232
+ `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.
233
+
234
+ ## Features
235
+
236
+ - `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.
237
+ - `MarkdownDocument` — source, tree, diagnostics and the link-definition index, plus the derived `headings`, `sections` and `links` accessors, the `find`/`findAll` tree queries over type-narrowed selectors, and the `frontmatter` capture.
238
+ - 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" })`.
239
+ - `MarkdownEdit` / `MarkdownRange` (with `applyAll`) — the non-mutating text-edit vocabulary, field-identical to `@effected/jsonc`'s, `@effected/yaml`'s and `@effected/toml`'s.
240
+ - `MarkdownFormat` — `format`/`formatToString` compute conservative marker-normalization edits; `modify`/`modifyToString` replace a node by identity through the canonical stringifier.
241
+ - `MarkdownVisitor` — walk a parsed tree as a lazy `Stream` of `Enter`/`Exit` events with child-index paths and depth.
242
+ - `Mdast` — `toMdast` projects to plain mdast JSON; `fromMdast`/`fromMdastResult` admit a foreign mdast tree with validation.
243
+ - `MarkdownFrontmatter.schema` plus the `YamlFrontmatter`, `TomlFrontmatter` and `JsonFrontmatter` codecs — typed frontmatter decoding over optional per-format peers, with four separately catchable failure modes.
244
+ - `SchemaResolver` — classify a frontmatter `$schema` declaration into `ByUrl`/`ByPath`/`Inline`/`ByName` and resolve names against a registry, with no IO and no dependencies.
245
+ - `MarkdownDiagnostic` — the structured diagnostic (`code`, `message`, `offset`, `length`, `line`, `character`) every typed error carries, shaped identically to the sibling packages'.
246
+
247
+ ## Conformance
248
+
249
+ All 652 CommonMark 0.31.2 spec examples run with an empty skip map, and the whole corpus runs again under both dialects with an explicitly asserted divergence list. The GFM extension corpora from cmark-gfm — the spec extension sections and `extensions.txt`, the only official footnote corpus — run complete, as does the 27-fixture `mdast-util-from-markdown` corpus, which asserts AST **and** position equality through the `Mdast` projection rather than just matching rendered output.
250
+
251
+ Two independent checks back that up. A differential property suite cross-checks the parser against the `commonmark` npm package across the corpus plus tens of thousands of generated documents, and cmark's pathological suite pins the linear-time guarantee with calibrated budgets — markdown's DoS vector is quadratic emphasis and link blowup, and the delimiter-stack algorithm is what defeats it. Recursive surfaces carry a 256-deep nesting cap, so a nesting bomb fails through the typed error channel instead of overflowing the stack. Every oracle and corpus is devDependency-only; none reaches your runtime.
252
+
253
+ ## License
254
+
255
+ [MIT](LICENSE)
@@ -0,0 +1,44 @@
1
+ import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError } from "./Frontmatter.js";
2
+ import { Effect } from "effect";
3
+ import { Toml } from "@effected/toml";
4
+
5
+ //#region src/TomlFrontmatter.ts
6
+ /**
7
+ * The toml frontmatter codec, over `@effected/toml`.
8
+ *
9
+ * @remarks
10
+ * Decodes a `+++`-fenced capture's raw value with `Toml.parse`, so the toml
11
+ * engine's nesting depth cap fails through the typed channel: a hostile
12
+ * frontmatter block surfaces as a {@link FrontmatterDecodeError} carrying the
13
+ * `TomlParseError` structurally, never a defect. An empty capture decodes to
14
+ * an empty table (`{}`), toml's empty-document value.
15
+ *
16
+ * Encodes with `Toml.stringify`; a value toml cannot represent fails as a
17
+ * {@link FrontmatterEncodeError} carrying the `TomlStringifyError`
18
+ * structurally. An empty object encodes to the **empty body** — the exact
19
+ * mirror of the decode ruling above, rendering as adjacent `+++` fences —
20
+ * so `set`-then-decode recovers `{}` exactly.
21
+ *
22
+ * `@effected/toml` is an optional peer — importing this module is what
23
+ * requires it; a consumer who never touches toml frontmatter never loads the
24
+ * toml engine.
25
+ *
26
+ * @public
27
+ */
28
+ const TomlFrontmatter = {
29
+ format: "toml",
30
+ decode: (node) => node.format !== "toml" ? Effect.fail(new FrontmatterFormatMismatchError({
31
+ expected: "toml",
32
+ actual: node.format
33
+ })) : Toml.parse(node.value).pipe(Effect.mapError((cause) => new FrontmatterDecodeError({
34
+ format: "toml",
35
+ cause
36
+ }))),
37
+ encode: (data) => Toml.stringify(data).pipe(Effect.mapError((cause) => new FrontmatterEncodeError({
38
+ format: "toml",
39
+ cause
40
+ })))
41
+ };
42
+
43
+ //#endregion
44
+ export { TomlFrontmatter };
@@ -0,0 +1,45 @@
1
+ import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError } from "./Frontmatter.js";
2
+ import { Effect } from "effect";
3
+ import { Yaml } from "@effected/yaml";
4
+
5
+ //#region src/YamlFrontmatter.ts
6
+ /**
7
+ * The yaml frontmatter codec, over `@effected/yaml`.
8
+ *
9
+ * @remarks
10
+ * Decodes a `---`-fenced capture's raw value with `Yaml.parse`, so the yaml
11
+ * engine's input hardening — the alias-expansion budget and the nesting depth
12
+ * cap — fails through the typed channel: a hostile frontmatter block surfaces
13
+ * as a {@link FrontmatterDecodeError} carrying the `YamlParseError`
14
+ * structurally, never a defect. An empty capture decodes to `null`, yaml's
15
+ * empty-document value.
16
+ *
17
+ * Encodes with `Yaml.stringify`; an unserializable value fails as a
18
+ * {@link FrontmatterEncodeError} carrying the `YamlStringifyError`
19
+ * structurally. An empty object encodes to the flow mapping `{}` —
20
+ * deliberately not an empty body, which would round-trip as `null` — so
21
+ * `set`-then-decode recovers `{}` exactly.
22
+ *
23
+ * `@effected/yaml` is an optional peer — importing this module is what
24
+ * requires it; a consumer who never touches yaml frontmatter never loads the
25
+ * yaml engine.
26
+ *
27
+ * @public
28
+ */
29
+ const YamlFrontmatter = {
30
+ format: "yaml",
31
+ decode: (node) => node.format !== "yaml" ? Effect.fail(new FrontmatterFormatMismatchError({
32
+ expected: "yaml",
33
+ actual: node.format
34
+ })) : Yaml.parse(node.value).pipe(Effect.mapError((cause) => new FrontmatterDecodeError({
35
+ format: "yaml",
36
+ cause
37
+ }))),
38
+ encode: (data) => Yaml.stringify(data).pipe(Effect.mapError((cause) => new FrontmatterEncodeError({
39
+ format: "yaml",
40
+ cause
41
+ })))
42
+ };
43
+
44
+ //#endregion
45
+ export { YamlFrontmatter };