@effected/markdown 0.20.0 → 0.22.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/Frontmatter.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MarkdownEdit } from "./MarkdownEdit.js";
2
2
  import { FrontmatterFormat } from "./MarkdownNode.js";
3
- import { Effect, Schema } from "effect";
3
+ import { Effect, Option, Schema } from "effect";
4
4
 
5
5
  // src/Frontmatter.ts
6
6
  /**
@@ -212,7 +212,7 @@ var MarkdownFrontmatter = class MarkdownFrontmatter {
212
212
  */
213
213
  static schema(schema, codec) {
214
214
  return (document) => {
215
- const node = document.frontmatter;
215
+ const node = Option.getOrUndefined(document.frontmatter);
216
216
  return node === void 0 ? Effect.fail(new FrontmatterMissingError({ reason: document.hasFrontmatterBlock ? "captureDisabled" : "absent" })) : codec.decode(node).pipe(Effect.flatMap((data) => Schema.decodeUnknownEffect(schema)(data).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new FrontmatterValidationError({ issue: error.issue }))))));
217
217
  };
218
218
  }
@@ -258,7 +258,7 @@ var MarkdownFrontmatter = class MarkdownFrontmatter {
258
258
  */
259
259
  static set(schema, codec) {
260
260
  return (document, data) => {
261
- const node = document.frontmatter;
261
+ const node = Option.getOrUndefined(document.frontmatter);
262
262
  if (node !== void 0 && node.format !== codec.format) return Effect.fail(new FrontmatterFormatMismatchError({
263
263
  expected: codec.format,
264
264
  actual: node.format
@@ -1,4 +1,4 @@
1
- import { Effect, Result, Schema } from "effect";
1
+ import { Effect, Option, Predicate, Result, Schema } from "effect";
2
2
 
3
3
  // src/FrontmatterResolver.ts
4
4
  /**
@@ -140,7 +140,6 @@ const parseVersionSegments = (version) => {
140
140
  if (!/^\d+(\.\d+){0,2}$/.test(version)) return;
141
141
  return version.split(".").map((segment) => Number.parseInt(segment, 10));
142
142
  };
143
- const isMapping = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
144
143
  /**
145
144
  * The `$schema` declaration classifier and the package's one built-in
146
145
  * resolver implementation.
@@ -184,30 +183,17 @@ var SchemaResolver = class SchemaResolver {
184
183
  version
185
184
  }));
186
185
  }
187
- if (isMapping(value)) return Result.succeed(new SchemaDeclarationInline({ document: value }));
186
+ if (Predicate.isObject(value)) return Result.succeed(new SchemaDeclarationInline({ document: value }));
188
187
  return Result.fail(new SchemaDeclarationInvalidError({
189
188
  reason: "the declaration is neither a string nor a mapping",
190
189
  value
191
190
  }));
192
191
  }
193
- /**
194
- * Extract and classify the `$schema` declaration from decoded frontmatter
195
- * data.
196
- *
197
- * @remarks
198
- * Non-mapping data and a mapping without a `$schema` key both carry no
199
- * declaration: the result succeeds with `undefined` by default, or fails
200
- * with {@link SchemaDeclarationMissingError} under `requireDeclaration`.
201
- *
202
- * @param data - The decoded frontmatter data.
203
- * @param options - `requireDeclaration` makes a missing `$schema` a typed
204
- * error.
205
- * @returns The classified declaration, `undefined` when absent and
206
- * tolerated, or the typed error.
207
- */
208
192
  static declarationOf(data, options) {
209
- if (!isMapping(data) || !Object.hasOwn(data, "$schema")) return options?.requireDeclaration === true ? Result.fail(new SchemaDeclarationMissingError()) : Result.succeed(void 0);
210
- return SchemaResolver.classify(data.$schema);
193
+ const required = options?.requireDeclaration === true;
194
+ if (!Predicate.isObject(data) || !Object.hasOwn(data, "$schema")) return required ? Result.fail(new SchemaDeclarationMissingError()) : Result.succeed(Option.none());
195
+ const classified = SchemaResolver.classify(data.$schema);
196
+ return required ? classified : Result.map(classified, Option.some);
211
197
  }
212
198
  /**
213
199
  * The package's one built-in resolver: a name-keyed registry with
@@ -6,7 +6,7 @@ import { normalizeLabelText } from "./internal/references.js";
6
6
  import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
7
7
  import { parsePassResult } from "./Markdown.js";
8
8
  import { rootHeadingBounds } from "./internal/sectionBounds.js";
9
- import { Effect, Result, Schema } from "effect";
9
+ import { Effect, Option, Result, Schema } from "effect";
10
10
 
11
11
  // src/MarkdownDocument.ts
12
12
  /**
@@ -167,19 +167,26 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
167
167
  definitions: Schema.ReadonlyMap(Schema.String, Definition)
168
168
  }) {
169
169
  /**
170
- * The document's frontmatter capture, or `undefined` when there is none.
170
+ * The document's frontmatter capture, or `Option.none()` when there is
171
+ * none.
171
172
  *
172
173
  * @remarks
173
174
  * Derived from the tree rather than stored: a {@link Frontmatter} node can
174
175
  * only ever sit at the head of `root.children` (the capture fires at most
175
176
  * once, at offset 0), so the tree is the single source of truth and the
176
- * accessor can never disagree with it. `undefined` covers both a document
177
- * with no frontmatter block and one parsed with the capture toggle off —
178
- * {@link MarkdownDocument.hasFrontmatterBlock} tells the two apart.
177
+ * accessor can never disagree with it. `Option.none()` covers both a
178
+ * document with no frontmatter block and one parsed with the capture toggle
179
+ * off — {@link MarkdownDocument.hasFrontmatterBlock} tells the two apart.
180
+ *
181
+ * An `Option` rather than `Frontmatter | undefined` because this is a
182
+ * lookup that can miss, and the kit's lookups return `Option` — the same
183
+ * shape as the sibling format packages' tree lookups (`JsoncNode.find`,
184
+ * `YamlNode.find`). A derived getter is a lookup, not a data field: it
185
+ * recomputes from the tree on every access.
179
186
  */
180
187
  get frontmatter() {
181
188
  const head = this.root.children[0];
182
- return head !== void 0 && head.type === "frontmatter" ? head : void 0;
189
+ return head !== void 0 && head.type === "frontmatter" ? Option.some(head) : Option.none();
183
190
  }
184
191
  /**
185
192
  * Whether the source opens with a well-formed frontmatter block —
@@ -187,10 +194,10 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
187
194
  *
188
195
  * @remarks
189
196
  * The structural signal that disambiguates the `frontmatter` accessor's
190
- * `undefined`: capture is opt-in (`MarkdownParseOptions.frontmatter`
197
+ * `Option.none()`: capture is opt-in (`MarkdownParseOptions.frontmatter`
191
198
  * defaults off), so a document whose source visibly starts with `---` can
192
199
  * still carry no capture node — spec-correct CommonMark reads the fences as
193
- * a thematic break plus content. `frontmatter === undefined` with
200
+ * a thematic break plus content. An empty `frontmatter` with
194
201
  * `hasFrontmatterBlock === true` means exactly "parsed with capture off";
195
202
  * with `false` it means the source genuinely has no block.
196
203
  *
@@ -274,11 +281,12 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
274
281
  * document repeatedly should bind `document.sections` once instead.
275
282
  *
276
283
  * @param options - Depth restriction.
277
- * @returns The first matching section, or `undefined`.
284
+ * @returns The first matching section, or `Option.none()` when no section
285
+ * matches.
278
286
  */
279
287
  firstSection(options) {
280
288
  const depth = options?.depth;
281
- return this.sections.find((section) => depth === void 0 || section.depth === depth);
289
+ return Option.fromUndefinedOr(this.sections.find((section) => depth === void 0 || section.depth === depth));
282
290
  }
283
291
  /**
284
292
  * The first section whose heading matches, in document order.
@@ -295,7 +303,8 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
295
303
  *
296
304
  * @param match - A string, a RegExp, or a predicate.
297
305
  * @param options - Depth restriction, applied before matching.
298
- * @returns The first matching section, or `undefined`.
306
+ * @returns The first matching section, or `Option.none()` when no section
307
+ * matches.
299
308
  */
300
309
  sectionByHeading(match, options) {
301
310
  const depth = options?.depth;
@@ -303,7 +312,7 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
303
312
  match.lastIndex = 0;
304
313
  return match.test(section.text.trim());
305
314
  } : (section) => match(section.text, section);
306
- return this.sections.find((section) => (depth === void 0 || section.depth === depth) && matches(section));
315
+ return Option.fromUndefinedOr(this.sections.find((section) => (depth === void 0 || section.depth === depth) && matches(section)));
307
316
  }
308
317
  /**
309
318
  * Every link-bearing node in the document, in document order: links,
@@ -343,7 +352,7 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
343
352
  return entries;
344
353
  }
345
354
  find(selector) {
346
- return findInTree(this.root, 0, selectorPredicate(selector));
355
+ return Option.fromUndefinedOr(findInTree(this.root, 0, selectorPredicate(selector)));
347
356
  }
348
357
  findAll(selector) {
349
358
  const predicate = selectorPredicate(selector);
@@ -11,8 +11,9 @@ import { Data, Stream } from "effect";
11
11
  */
12
12
  const MarkdownVisitorEvent = Data.taggedEnum();
13
13
  /**
14
- * Walks a parsed markdown tree as a lazy `Stream` of `Enter`/`Exit` events in
15
- * document pre-order. Not instantiable.
14
+ * Walks a parsed markdown tree as a lazy sequence of `Enter`/`Exit` events in
15
+ * document pre-order — synchronously with {@link MarkdownVisitor.walk}, or as
16
+ * a `Stream` with {@link MarkdownVisitor.visit}. Not instantiable.
16
17
  *
17
18
  * @example
18
19
  * ```ts
@@ -32,9 +33,52 @@ const MarkdownVisitorEvent = Data.taggedEnum();
32
33
  *
33
34
  * @public
34
35
  */
35
- var MarkdownVisitor = class {
36
+ var MarkdownVisitor = class MarkdownVisitor {
36
37
  constructor() {}
37
38
  /**
39
+ * Walk `root` synchronously, as a lazy `Iterable` of `MarkdownVisitorEvent`
40
+ * in document pre-order: `Enter` and `Exit` for every node including the
41
+ * root and every leaf. The primitive {@link MarkdownVisitor.visit} is
42
+ * defined in terms of, so the two yield the same events in the same order.
43
+ *
44
+ * @remarks
45
+ * For callers that cannot suspend — a lint rule whose host calls it
46
+ * synchronously, a getter, a non-Effect adapter. Events are produced on
47
+ * demand: a `for…of` that `break`s stops the walk without visiting the
48
+ * rest of the tree. The iterable is re-iterable — every `[Symbol.iterator]()`
49
+ * call starts a fresh walk from the root, so binding the result once and
50
+ * iterating it twice walks the tree twice, independently.
51
+ *
52
+ * Not named `walkResult`: it returns no `Result`, and the walk has no
53
+ * failure channel to put in one. A hand-constructed tree nested past the
54
+ * depth cap surfaces as a terminal `MarkdownVisitorEvent` `Error` event
55
+ * carrying a `NestingDepthExceeded` {@link MarkdownDiagnostic}, after which
56
+ * nothing is yielded — the same posture as `visit`. A tree produced by
57
+ * `Markdown.parse` or decoded by `Mdast.fromMdast` can never trip it.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * import { Markdown, MarkdownVisitor } from "@effected/markdown";
62
+ * import { Result } from "effect";
63
+ *
64
+ * const root = Result.getOrThrow(Markdown.parseResult("# Hi\n\ntext\n"));
65
+ * const entered: Array<string> = [];
66
+ * for (const event of MarkdownVisitor.walk(root)) {
67
+ * if (event._tag === "Enter") entered.push(event.node.type);
68
+ * }
69
+ * console.log(entered);
70
+ * // => ["root", "heading", "text", "paragraph", "text"]
71
+ * ```
72
+ *
73
+ * @param root - The tree to walk.
74
+ * @returns A lazy, re-iterable `Iterable` of `MarkdownVisitorEvent`.
75
+ */
76
+ static walk(root) {
77
+ return { [Symbol.iterator]() {
78
+ return walkNode(root, [], 0);
79
+ } };
80
+ }
81
+ /**
38
82
  * Create a lazy `Stream` of `MarkdownVisitorEvent` walking `root` in
39
83
  * document pre-order: `Enter` and `Exit` for every node including the
40
84
  * root and every leaf. Events are produced on demand, so combining with
@@ -44,28 +88,24 @@ var MarkdownVisitor = class {
44
88
  * @remarks
45
89
  * Infallible at the type level. A tree produced by `Markdown.parse` can
46
90
  * never trip the walk's depth guard — the parser refuses deeper input —
47
- * but a decoded foreign tree (`Mdast.fromMdast`) can; the trip surfaces
48
- * as a terminal `MarkdownVisitorEvent` `Error` event carrying a
49
- * `NestingDepthExceeded` {@link MarkdownDiagnostic}, mirroring the typed
50
- * failure `Markdown.stringify` produces for the same tree.
91
+ * and neither can a decoded foreign tree (`Mdast.fromMdast`), because the
92
+ * decode boundary refuses it first; only a hand-constructed tree can trip
93
+ * the guard. The trip surfaces as a terminal `MarkdownVisitorEvent`
94
+ * `Error` event carrying a `NestingDepthExceeded` {@link MarkdownDiagnostic},
95
+ * mirroring the typed failure `Markdown.stringify` produces for the same
96
+ * tree.
97
+ *
98
+ * Defined as `Stream.fromIterable(MarkdownVisitor.walk(root))`: every run
99
+ * of the stream walks afresh, and the two forms never diverge.
51
100
  *
52
101
  * @param root - The tree to walk.
53
102
  * @returns A lazy `Stream` of `MarkdownVisitorEvent`, infallible at the type
54
103
  * level.
55
104
  */
56
105
  static visit(root) {
57
- return Stream.fromIterable(walkIterable(root));
106
+ return Stream.fromIterable(MarkdownVisitor.walk(root));
58
107
  }
59
108
  };
60
- /** Lazy per-subscription iterable so each run of the stream walks afresh. */
61
- function walkIterable(root) {
62
- return { [Symbol.iterator]() {
63
- return walk(root)[Symbol.iterator]();
64
- } };
65
- }
66
- function* walk(root) {
67
- yield* walkNode(root, [], 0);
68
- }
69
109
  function* walkNode(node, path, depth) {
70
110
  if (depth > 256) {
71
111
  yield MarkdownVisitorEvent.Error({
package/Mdast.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Root } from "./MarkdownNode.js";
2
2
  import { unescapeString } from "./internal/unescape.js";
3
- import { Effect, Result, Schema } from "effect";
3
+ import { GuardExceeded, isGuardExceeded } from "./internal/carriers.js";
4
+ import { Effect, Predicate, Result, Schema, SchemaIssue } from "effect";
4
5
 
5
6
  // src/Mdast.ts
6
7
  /**
@@ -286,10 +287,9 @@ const admittedFields = {
286
287
  mdxTextExpression: ["value"],
287
288
  mdxjsEsm: ["value"]
288
289
  };
289
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
290
- const completePoint = (value) => isRecord(value) && typeof value.line === "number" && typeof value.column === "number" && typeof value.offset === "number";
290
+ const completePoint = (value) => Predicate.isObject(value) && typeof value.line === "number" && typeof value.column === "number" && typeof value.offset === "number";
291
291
  const normalizePosition = (value) => {
292
- if (isRecord(value) && completePoint(value.start) && completePoint(value.end)) {
292
+ if (Predicate.isObject(value) && completePoint(value.start) && completePoint(value.end)) {
293
293
  const start = value.start;
294
294
  const end = value.end;
295
295
  return {
@@ -307,8 +307,9 @@ const normalizePosition = (value) => {
307
307
  }
308
308
  return sentinelPosition;
309
309
  };
310
- const normalizeNode = (value) => {
311
- if (!isRecord(value) || typeof value.type !== "string") return value;
310
+ const normalizeNode = (value, depth = 0) => {
311
+ if (!Predicate.isObject(value) || typeof value.type !== "string") return value;
312
+ if (depth > 256) throw new GuardExceeded("NestingDepthExceeded", 256, depth, 0);
312
313
  const type = value.type;
313
314
  const format = frontmatterTypes.get(type);
314
315
  if (format !== void 0) return {
@@ -321,7 +322,7 @@ const normalizeNode = (value) => {
321
322
  type,
322
323
  name: value.name,
323
324
  attributes: Array.isArray(value.attributes) ? value.attributes.map(normalizeMdxAttribute) : value.attributes,
324
- children: Array.isArray(value.children) ? value.children.map(normalizeNode) : value.children,
325
+ children: Array.isArray(value.children) ? value.children.map((child) => normalizeNode(child, depth + 1)) : value.children,
325
326
  position: normalizePosition(value.position)
326
327
  };
327
328
  const admitted = admittedFields[type];
@@ -335,12 +336,12 @@ const normalizeNode = (value) => {
335
336
  const code = normalized.value;
336
337
  normalized.value = code.endsWith("\n") ? code : `${code}\n`;
337
338
  }
338
- if (Array.isArray(value.children)) normalized.children = value.children.map(normalizeNode);
339
+ if (Array.isArray(value.children)) normalized.children = value.children.map((child) => normalizeNode(child, depth + 1));
339
340
  normalized.position = normalizePosition(value.position);
340
341
  return normalized;
341
342
  };
342
343
  const normalizeMdxAttribute = (value) => {
343
- if (!isRecord(value) || typeof value.type !== "string") return value;
344
+ if (!Predicate.isObject(value) || typeof value.type !== "string") return value;
344
345
  if (value.type === "mdxJsxExpressionAttribute") return {
345
346
  type: "mdxJsxExpressionAttribute",
346
347
  value: value.value,
@@ -353,7 +354,7 @@ const normalizeMdxAttribute = (value) => {
353
354
  name: value.name,
354
355
  position: normalizePosition(value.position)
355
356
  };
356
- if (raw !== void 0) normalized.value = isRecord(raw) && raw.type === "mdxJsxAttributeValueExpression" ? {
357
+ if (raw !== void 0) normalized.value = Predicate.isObject(raw) && raw.type === "mdxJsxAttributeValueExpression" ? {
357
358
  type: "mdxJsxAttributeValueExpression",
358
359
  value: raw.value,
359
360
  position: normalizePosition(raw.position)
@@ -452,12 +453,27 @@ var Mdast = class Mdast {
452
453
  * plain tree built for `Markdown.stringify` can carry it straight in. A
453
454
  * value outside `"canonical" | "literal"` fails the decode typed.
454
455
  *
456
+ * **Nesting is capped at 256 levels below the root**, the same cap the
457
+ * parser and `Markdown.stringify` enforce. A tree with any node deeper
458
+ * than that, leaves included, fails typed with a `SchemaIssue.InvalidValue`
459
+ * issue before the schema decode runs, so hostile depth is never a defect.
460
+ * Because leaves count, a document the parser accepts can still be
461
+ * refused here: `"> ".repeat(255) + "x"` parses, but its `text` leaf sits
462
+ * at depth 257, and `Markdown.stringify` refuses that tree too.
463
+ *
455
464
  * @param input - A plain mdast tree, typically a `root`.
456
465
  * @returns A `Result` succeeding with the decoded {@link Root}, or
457
466
  * failing with {@link MdastDecodeError} carrying the structured issue.
458
467
  */
459
468
  static fromMdastResult(input) {
460
- return Result.mapError(decodeRoot(normalizeNode(input)), (error) => new MdastDecodeError({ issue: error.issue }));
469
+ let normalized;
470
+ try {
471
+ normalized = normalizeNode(input);
472
+ } catch (error) {
473
+ if (!isGuardExceeded(error)) throw error;
474
+ return Result.fail(new MdastDecodeError({ issue: new SchemaIssue.InvalidValue({ message: error.message }) }));
475
+ }
476
+ return Result.mapError(decodeRoot(normalized), (error) => new MdastDecodeError({ issue: error.issue }));
461
477
  }
462
478
  /**
463
479
  * Decode foreign plain mdast into the package's node classes. Defined in
package/README.md CHANGED
@@ -85,7 +85,7 @@ Every parse has a synchronous twin — `MarkdownDocument.parseResult` and `Markd
85
85
 
86
86
  ```ts
87
87
  import { MarkdownDocument, MarkdownFormat } from "@effected/markdown";
88
- import { Effect } from "effect";
88
+ import { Effect, Option } from "effect";
89
89
 
90
90
  const source = `# Release notes
91
91
 
@@ -94,7 +94,7 @@ See the [changelog](./CHANGELOG.md).
94
94
 
95
95
  const program = Effect.gen(function* () {
96
96
  const doc = yield* MarkdownDocument.parse(source);
97
- const label = doc.find("text")!;
97
+ const label = Option.getOrThrow(doc.find("text"));
98
98
  return yield* MarkdownFormat.modifyToString(doc, label, "Release notes (2026)");
99
99
  });
100
100
 
@@ -104,7 +104,7 @@ Effect.runPromise(program).then(console.log);
104
104
  // See the [changelog](./CHANGELOG.md).
105
105
  ```
106
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.
107
+ `find` walks the tree in document pre-order and returns the document's own node inside an `Option` — `Option.none()` when nothing matches — 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
108
 
109
109
  `MarkdownFormat.format` handles the other half: conservative marker normalization, never content rewriting. It converts heading style, bullet character, emphasis marker, fence character, thematic-break character and code-block style, and skips any conversion that would not be safe rather than attempting it cleverly:
110
110
 
@@ -198,11 +198,11 @@ Effect.runPromise(program).then(console.log);
198
198
  // [ "./CHANGELOG.md", "https://example.com/docs" ]
199
199
  ```
200
200
 
201
- `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. `firstSection` and `sectionByHeading` find one `DocumentSection` without scanning `sections` yourself — the second matches on an exact trimmed heading string, a `RegExp`, or a predicate, and both accept `{ depth }` to restrict to one heading level:
201
+ `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. `firstSection` and `sectionByHeading` find one `DocumentSection` without scanning `sections` yourself — the second matches on an exact trimmed heading string, a `RegExp`, or a predicate, and both accept `{ depth }` to restrict to one heading level. Both return an `Option`, as every lookup in the kit does (the `frontmatter` accessor and `find` too):
202
202
 
203
203
  ```ts
204
204
  import { MarkdownDocument } from "@effected/markdown";
205
- import { Effect } from "effect";
205
+ import { Effect, Option } from "effect";
206
206
 
207
207
  const source = `# Changelog
208
208
 
@@ -217,7 +217,10 @@ Initial release.
217
217
 
218
218
  const program = Effect.gen(function* () {
219
219
  const doc = yield* MarkdownDocument.parse(source);
220
- return doc.sectionByHeading("1.2.3", { depth: 2 })?.body.trim();
220
+ return Option.match(doc.sectionByHeading("1.2.3", { depth: 2 }), {
221
+ onNone: () => "no such release",
222
+ onSome: (section) => section.body.trim(),
223
+ });
221
224
  });
222
225
 
223
226
  Effect.runPromise(program).then(console.log);
@@ -226,7 +229,7 @@ Effect.runPromise(program).then(console.log);
226
229
 
227
230
  `DocumentSection.body` is the section without its heading, and it is deliberately untrimmed — exactly the bytes `bodyRange` describes — so callers write `.body.trim()` when they want the tidy form.
228
231
 
229
- 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.
232
+ 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 `Option<Table>` — 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.
230
233
 
231
234
  `MarkdownVisitor.visit` streams the same walk as `Enter`/`Exit` events carrying the node, its child-index path and its depth:
232
235
 
@@ -246,6 +249,21 @@ Effect.runPromise(program).then(console.log);
246
249
  // "Exit:text", "Exit:emphasis", "Enter:text", "Exit:text", "Exit:paragraph", "Exit:root" ]
247
250
  ```
248
251
 
252
+ `MarkdownVisitor.walk` is the same walk as a synchronous, lazy `Iterable` — the primitive `visit` is defined in terms of — for code that cannot suspend, such as a lint rule a host calls synchronously. Each iteration walks afresh from the root, and a `for…of` that `break`s stops the walk:
253
+
254
+ ```ts
255
+ import { Markdown, MarkdownVisitor } from "@effected/markdown";
256
+ import { Result } from "effect";
257
+
258
+ const root = Result.getOrThrow(Markdown.parseResult("# Title\n\nA *b* c\n"));
259
+ const headings: Array<number> = [];
260
+ for (const event of MarkdownVisitor.walk(root)) {
261
+ if (event._tag === "Enter" && event.node.type === "heading") headings.push(event.node.depth);
262
+ }
263
+ console.log(headings);
264
+ // [ 1 ]
265
+ ```
266
+
249
267
  ## mdast interop
250
268
 
251
269
  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 — (keeping only a `Text` node's `escapeStyle` when set, see [Literal text](#literal-text)) and emits plain spec-valid mdast JSON that the remark ecosystem consumes directly, including `mdast-util-to-hast` if you want hast:
@@ -364,7 +382,7 @@ To *normalize* an existing document to different choices, use `MarkdownFormat` w
364
382
  - `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.
365
383
  - `MarkdownEdit` / `MarkdownRange` (with `applyAll`) — the non-mutating text-edit vocabulary, field-identical to `@effected/jsonc`'s, `@effected/yaml`'s and `@effected/toml`'s.
366
384
  - `MarkdownFormat` — `format`/`formatToString` compute conservative marker-normalization edits; `modify`/`modifyToString` replace a node by identity through the canonical stringifier.
367
- - `MarkdownVisitor` — walk a parsed tree as a lazy `Stream` of `Enter`/`Exit` events with child-index paths and depth.
385
+ - `MarkdownVisitor` — walk a parsed tree as `Enter`/`Exit` events with child-index paths and depth: synchronously as a lazy `Iterable` (`walk`), or as a `Stream` (`visit`).
368
386
  - `Mdast` — `toMdast` projects to plain mdast JSON; `fromMdast`/`fromMdastResult` admit a foreign mdast tree with validation.
369
387
  - `MarkdownFrontmatter.schema` plus the `YamlFrontmatter`, `TomlFrontmatter` and `JsonFrontmatter` codecs — typed frontmatter decoding over optional per-format peers, with four separately catchable failure modes and a `FrontmatterMissingReason` on the absent one.
370
388
  - `SchemaResolver` — classify a frontmatter `$schema` declaration into `ByUrl`/`ByPath`/`Inline`/`ByName` and resolve names against a registry, with no IO and no dependencies.
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Data, Effect, Result, Schema, Stream } from "effect";
1
+ import { Data, Effect, Option, Result, Schema, Stream } from "effect";
2
2
  declare const Point_base: Schema.Class<Point, Schema.Struct<{
3
3
  readonly line: Schema.Number;
4
4
  readonly column: Schema.Number;
@@ -1589,27 +1589,34 @@ declare const MarkdownDocument_base: Schema.Class<MarkdownDocument, Schema.Struc
1589
1589
  */
1590
1590
  export declare class MarkdownDocument extends MarkdownDocument_base {
1591
1591
  /**
1592
- * The document's frontmatter capture, or `undefined` when there is none.
1592
+ * The document's frontmatter capture, or `Option.none()` when there is
1593
+ * none.
1593
1594
  *
1594
1595
  * @remarks
1595
1596
  * Derived from the tree rather than stored: a {@link Frontmatter} node can
1596
1597
  * only ever sit at the head of `root.children` (the capture fires at most
1597
1598
  * once, at offset 0), so the tree is the single source of truth and the
1598
- * accessor can never disagree with it. `undefined` covers both a document
1599
- * with no frontmatter block and one parsed with the capture toggle off —
1600
- * {@link MarkdownDocument.hasFrontmatterBlock} tells the two apart.
1599
+ * accessor can never disagree with it. `Option.none()` covers both a
1600
+ * document with no frontmatter block and one parsed with the capture toggle
1601
+ * off — {@link MarkdownDocument.hasFrontmatterBlock} tells the two apart.
1602
+ *
1603
+ * An `Option` rather than `Frontmatter | undefined` because this is a
1604
+ * lookup that can miss, and the kit's lookups return `Option` — the same
1605
+ * shape as the sibling format packages' tree lookups (`JsoncNode.find`,
1606
+ * `YamlNode.find`). A derived getter is a lookup, not a data field: it
1607
+ * recomputes from the tree on every access.
1601
1608
  */
1602
- get frontmatter(): Frontmatter | undefined;
1609
+ get frontmatter(): Option.Option<Frontmatter>;
1603
1610
  /**
1604
1611
  * Whether the source opens with a well-formed frontmatter block —
1605
1612
  * regardless of how the document was parsed.
1606
1613
  *
1607
1614
  * @remarks
1608
1615
  * The structural signal that disambiguates the `frontmatter` accessor's
1609
- * `undefined`: capture is opt-in (`MarkdownParseOptions.frontmatter`
1616
+ * `Option.none()`: capture is opt-in (`MarkdownParseOptions.frontmatter`
1610
1617
  * defaults off), so a document whose source visibly starts with `---` can
1611
1618
  * still carry no capture node — spec-correct CommonMark reads the fences as
1612
- * a thematic break plus content. `frontmatter === undefined` with
1619
+ * a thematic break plus content. An empty `frontmatter` with
1613
1620
  * `hasFrontmatterBlock === true` means exactly "parsed with capture off";
1614
1621
  * with `false` it means the source genuinely has no block.
1615
1622
  *
@@ -1656,9 +1663,10 @@ export declare class MarkdownDocument extends MarkdownDocument_base {
1656
1663
  * document repeatedly should bind `document.sections` once instead.
1657
1664
  *
1658
1665
  * @param options - Depth restriction.
1659
- * @returns The first matching section, or `undefined`.
1666
+ * @returns The first matching section, or `Option.none()` when no section
1667
+ * matches.
1660
1668
  */
1661
- firstSection(options?: SectionQueryOptions): DocumentSection | undefined;
1669
+ firstSection(options?: SectionQueryOptions): Option.Option<DocumentSection>;
1662
1670
  /**
1663
1671
  * The first section whose heading matches, in document order.
1664
1672
  *
@@ -1674,9 +1682,10 @@ export declare class MarkdownDocument extends MarkdownDocument_base {
1674
1682
  *
1675
1683
  * @param match - A string, a RegExp, or a predicate.
1676
1684
  * @param options - Depth restriction, applied before matching.
1677
- * @returns The first matching section, or `undefined`.
1685
+ * @returns The first matching section, or `Option.none()` when no section
1686
+ * matches.
1678
1687
  */
1679
- sectionByHeading(match: SectionHeadingMatch, options?: SectionQueryOptions): DocumentSection | undefined;
1688
+ sectionByHeading(match: SectionHeadingMatch, options?: SectionQueryOptions): Option.Option<DocumentSection>;
1680
1689
  /**
1681
1690
  * Every link-bearing node in the document, in document order: links,
1682
1691
  * images, definitions, and the reference forms resolved through the
@@ -1697,7 +1706,7 @@ export declare class MarkdownDocument extends MarkdownDocument_base {
1697
1706
  *
1698
1707
  * @remarks
1699
1708
  * A string selector matches on the node's `type` tag and narrows the
1700
- * result (`find("heading")` is `Heading | undefined`); a type-guard
1709
+ * result (`find("heading")` is `Option<Heading>`); a type-guard
1701
1710
  * predicate narrows the same way, and a plain predicate returns the wide
1702
1711
  * `MarkdownNode` union. The returned node is the document's own — matched
1703
1712
  * and returnable by identity, so it feeds `MarkdownFormat.modify`
@@ -1706,11 +1715,12 @@ export declare class MarkdownDocument extends MarkdownDocument_base {
1706
1715
  * hand-built or foreign decoded tree) is a thrown defect.
1707
1716
  *
1708
1717
  * @param selector - A node `type` tag or a predicate over nodes.
1709
- * @returns The first matching node in document order, or `undefined`.
1718
+ * @returns The first matching node in document order, or `Option.none()`
1719
+ * when none matches.
1710
1720
  */
1711
- find<T extends MarkdownNodeType>(selector: T): MarkdownNodeOfType<T> | undefined;
1712
- find<T extends MarkdownNode>(selector: (node: MarkdownNode) => node is T): T | undefined;
1713
- find(selector: (node: MarkdownNode) => boolean): MarkdownNode | undefined;
1721
+ find<T extends MarkdownNodeType>(selector: T): Option.Option<MarkdownNodeOfType<T>>;
1722
+ find<T extends MarkdownNode>(selector: (node: MarkdownNode) => node is T): Option.Option<T>;
1723
+ find(selector: (node: MarkdownNode) => boolean): Option.Option<MarkdownNode>;
1714
1724
  /**
1715
1725
  * Find every node matching a selector, in document pre-order — the same
1716
1726
  * order {@link MarkdownVisitor} enters nodes, starting at the root itself.
@@ -2241,19 +2251,34 @@ export declare class SchemaResolver {
2241
2251
  * data.
2242
2252
  *
2243
2253
  * @remarks
2244
- * Non-mapping data and a mapping without a `$schema` key both carry no
2245
- * declaration: the result succeeds with `undefined` by default, or fails
2246
- * with {@link SchemaDeclarationMissingError} under `requireDeclaration`.
2254
+ * A lookup that can miss, so by default the success channel is an
2255
+ * `Option`: non-mapping data and a mapping without a `$schema` key both
2256
+ * carry no declaration and succeed with `Option.none()`, and the error
2257
+ * union is {@link SchemaDeclarationInvalidError} alone. Under
2258
+ * `requireDeclaration: true` the miss becomes a typed
2259
+ * {@link SchemaDeclarationMissingError} and the success is the bare
2260
+ * declaration, since it is statically always present. The two overloads
2261
+ * keep each caller's types honest: no `Option` to unwrap when the
2262
+ * declaration is required, no `SchemaDeclarationMissingError` to handle
2263
+ * when it is not. A present key is classified by
2264
+ * {@link SchemaResolver.classify}, and its
2265
+ * {@link SchemaDeclarationInvalidError} propagates unchanged. Hand an
2266
+ * optional declaration to a {@link FrontmatterSchemaResolver} with
2267
+ * `Option.getOrUndefined`, since the seam takes the absent case as
2268
+ * `undefined`; a required one passes straight through.
2247
2269
  *
2248
2270
  * @param data - The decoded frontmatter data.
2249
- * @param options - `requireDeclaration` makes a missing `$schema` a typed
2250
- * error.
2251
- * @returns The classified declaration, `undefined` when absent and
2252
- * tolerated, or the typed error.
2271
+ * @param options - `requireDeclaration: true` makes a missing `$schema` a
2272
+ * typed error and narrows the success to the declaration itself.
2273
+ * @returns The classified declaration under `requireDeclaration`;
2274
+ * otherwise `Option.some` of it, or `Option.none()` when absent.
2253
2275
  */
2276
+ static declarationOf(data: unknown, options: {
2277
+ readonly requireDeclaration: true;
2278
+ }): Result.Result<SchemaDeclaration, SchemaDeclarationInvalidError | SchemaDeclarationMissingError>;
2254
2279
  static declarationOf(data: unknown, options?: {
2255
- readonly requireDeclaration?: boolean;
2256
- }): Result.Result<SchemaDeclaration | undefined, SchemaDeclarationInvalidError | SchemaDeclarationMissingError>;
2280
+ readonly requireDeclaration?: false;
2281
+ }): Result.Result<Option.Option<SchemaDeclaration>, SchemaDeclarationInvalidError>;
2257
2282
  /**
2258
2283
  * The package's one built-in resolver: a name-keyed registry with
2259
2284
  * exact version-segment resolution.
@@ -2902,7 +2927,7 @@ export declare class MarkdownTransform {
2902
2927
  * leaf the two fire back to back. Every variant carries `path` (child
2903
2928
  * indexes from the root; the root's own path is empty) and `depth`
2904
2929
  * (zero-based; the root is 0). `Error` is terminal: it carries the
2905
- * depth-guard diagnostic for a decoded foreign tree nested past
2930
+ * depth-guard diagnostic for a hand-constructed tree nested past
2906
2931
  * `MAX_NESTING_DEPTH` and nothing follows it.
2907
2932
  *
2908
2933
  * @public
@@ -3046,8 +3071,9 @@ export declare const MarkdownVisitorEvent: {
3046
3071
  }, "_tag">;
3047
3072
  };
3048
3073
  /**
3049
- * Walks a parsed markdown tree as a lazy `Stream` of `Enter`/`Exit` events in
3050
- * document pre-order. Not instantiable.
3074
+ * Walks a parsed markdown tree as a lazy sequence of `Enter`/`Exit` events in
3075
+ * document pre-order — synchronously with {@link MarkdownVisitor.walk}, or as
3076
+ * a `Stream` with {@link MarkdownVisitor.visit}. Not instantiable.
3051
3077
  *
3052
3078
  * @example
3053
3079
  * ```ts
@@ -3069,6 +3095,45 @@ export declare const MarkdownVisitorEvent: {
3069
3095
  */
3070
3096
  export declare class MarkdownVisitor {
3071
3097
  private constructor();
3098
+ /**
3099
+ * Walk `root` synchronously, as a lazy `Iterable` of `MarkdownVisitorEvent`
3100
+ * in document pre-order: `Enter` and `Exit` for every node including the
3101
+ * root and every leaf. The primitive {@link MarkdownVisitor.visit} is
3102
+ * defined in terms of, so the two yield the same events in the same order.
3103
+ *
3104
+ * @remarks
3105
+ * For callers that cannot suspend — a lint rule whose host calls it
3106
+ * synchronously, a getter, a non-Effect adapter. Events are produced on
3107
+ * demand: a `for…of` that `break`s stops the walk without visiting the
3108
+ * rest of the tree. The iterable is re-iterable — every `[Symbol.iterator]()`
3109
+ * call starts a fresh walk from the root, so binding the result once and
3110
+ * iterating it twice walks the tree twice, independently.
3111
+ *
3112
+ * Not named `walkResult`: it returns no `Result`, and the walk has no
3113
+ * failure channel to put in one. A hand-constructed tree nested past the
3114
+ * depth cap surfaces as a terminal `MarkdownVisitorEvent` `Error` event
3115
+ * carrying a `NestingDepthExceeded` {@link MarkdownDiagnostic}, after which
3116
+ * nothing is yielded — the same posture as `visit`. A tree produced by
3117
+ * `Markdown.parse` or decoded by `Mdast.fromMdast` can never trip it.
3118
+ *
3119
+ * @example
3120
+ * ```ts
3121
+ * import { Markdown, MarkdownVisitor } from "@effected/markdown";
3122
+ * import { Result } from "effect";
3123
+ *
3124
+ * const root = Result.getOrThrow(Markdown.parseResult("# Hi\n\ntext\n"));
3125
+ * const entered: Array<string> = [];
3126
+ * for (const event of MarkdownVisitor.walk(root)) {
3127
+ * if (event._tag === "Enter") entered.push(event.node.type);
3128
+ * }
3129
+ * console.log(entered);
3130
+ * // => ["root", "heading", "text", "paragraph", "text"]
3131
+ * ```
3132
+ *
3133
+ * @param root - The tree to walk.
3134
+ * @returns A lazy, re-iterable `Iterable` of `MarkdownVisitorEvent`.
3135
+ */
3136
+ static walk(root: Root): Iterable<MarkdownVisitorEvent>;
3072
3137
  /**
3073
3138
  * Create a lazy `Stream` of `MarkdownVisitorEvent` walking `root` in
3074
3139
  * document pre-order: `Enter` and `Exit` for every node including the
@@ -3079,10 +3144,15 @@ export declare class MarkdownVisitor {
3079
3144
  * @remarks
3080
3145
  * Infallible at the type level. A tree produced by `Markdown.parse` can
3081
3146
  * never trip the walk's depth guard — the parser refuses deeper input —
3082
- * but a decoded foreign tree (`Mdast.fromMdast`) can; the trip surfaces
3083
- * as a terminal `MarkdownVisitorEvent` `Error` event carrying a
3084
- * `NestingDepthExceeded` {@link MarkdownDiagnostic}, mirroring the typed
3085
- * failure `Markdown.stringify` produces for the same tree.
3147
+ * and neither can a decoded foreign tree (`Mdast.fromMdast`), because the
3148
+ * decode boundary refuses it first; only a hand-constructed tree can trip
3149
+ * the guard. The trip surfaces as a terminal `MarkdownVisitorEvent`
3150
+ * `Error` event carrying a `NestingDepthExceeded` {@link MarkdownDiagnostic},
3151
+ * mirroring the typed failure `Markdown.stringify` produces for the same
3152
+ * tree.
3153
+ *
3154
+ * Defined as `Stream.fromIterable(MarkdownVisitor.walk(root))`: every run
3155
+ * of the stream walks afresh, and the two forms never diverge.
3086
3156
  *
3087
3157
  * @param root - The tree to walk.
3088
3158
  * @returns A lazy `Stream` of `MarkdownVisitorEvent`, infallible at the type
@@ -3221,6 +3291,14 @@ export declare class Mdast {
3221
3291
  * plain tree built for `Markdown.stringify` can carry it straight in. A
3222
3292
  * value outside `"canonical" | "literal"` fails the decode typed.
3223
3293
  *
3294
+ * **Nesting is capped at 256 levels below the root**, the same cap the
3295
+ * parser and `Markdown.stringify` enforce. A tree with any node deeper
3296
+ * than that, leaves included, fails typed with a `SchemaIssue.InvalidValue`
3297
+ * issue before the schema decode runs, so hostile depth is never a defect.
3298
+ * Because leaves count, a document the parser accepts can still be
3299
+ * refused here: `"> ".repeat(255) + "x"` parses, but its `text` leaf sits
3300
+ * at depth 257, and `Markdown.stringify` refuses that tree too.
3301
+ *
3224
3302
  * @param input - A plain mdast tree, typically a `root`.
3225
3303
  * @returns A `Result` succeeding with the decoded {@link Root}, or
3226
3304
  * failing with {@link MdastDecodeError} carrying the structured issue.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/markdown",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "private": false,
5
5
  "description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
6
6
  "keywords": [
@@ -39,9 +39,9 @@
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "peerDependencies": {
42
- "@effected/jsonc": "^0.19.0",
43
- "@effected/toml": "^0.15.0",
44
- "@effected/yaml": "^0.23.0",
42
+ "@effected/jsonc": "^0.20.0",
43
+ "@effected/toml": "^0.16.0",
44
+ "@effected/yaml": "^0.25.0",
45
45
  "effect": "^4.0.0"
46
46
  },
47
47
  "peerDependenciesMeta": {