@effected/markdown 0.19.0 → 0.21.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 (66) hide show
  1. package/Frontmatter.js +4 -5
  2. package/FrontmatterResolver.js +4 -6
  3. package/FrontmatterSource.js +1 -2
  4. package/JsonFrontmatter.js +1 -2
  5. package/Markdown.js +1 -2
  6. package/MarkdownDiagnostic.js +1 -2
  7. package/MarkdownDocument.js +23 -15
  8. package/MarkdownEdit.js +1 -2
  9. package/MarkdownFormat.js +1 -2
  10. package/MarkdownNode.js +1 -2
  11. package/MarkdownTransform.js +1 -2
  12. package/MarkdownVisitor.js +58 -19
  13. package/Mdast.js +28 -13
  14. package/README.md +26 -8
  15. package/TomlFrontmatter.js +1 -2
  16. package/YamlFrontmatter.js +1 -2
  17. package/index.d.ts +87 -54
  18. package/internal/blockParser.js +1 -2
  19. package/internal/blockRegistry.js +1 -2
  20. package/internal/blockTypes.js +1 -2
  21. package/internal/blocks/atxHeading.js +1 -2
  22. package/internal/blocks/blockquote.js +1 -2
  23. package/internal/blocks/code.js +1 -2
  24. package/internal/blocks/document.js +1 -2
  25. package/internal/blocks/fencedCode.js +1 -2
  26. package/internal/blocks/footnoteDefinition.js +1 -2
  27. package/internal/blocks/frontmatter.js +1 -2
  28. package/internal/blocks/htmlBlock.js +1 -2
  29. package/internal/blocks/indentedCode.js +1 -2
  30. package/internal/blocks/linkReferenceDefinition.js +1 -2
  31. package/internal/blocks/list.js +1 -2
  32. package/internal/blocks/paragraph.js +1 -2
  33. package/internal/blocks/setextHeading.js +1 -2
  34. package/internal/blocks/table.js +1 -2
  35. package/internal/blocks/taskListItem.js +1 -2
  36. package/internal/blocks/thematicBreak.js +1 -2
  37. package/internal/carriers.js +1 -2
  38. package/internal/entities.js +1 -2
  39. package/internal/entityMap.js +1 -2
  40. package/internal/htmlTags.js +1 -2
  41. package/internal/inlineNode.js +1 -2
  42. package/internal/inlineParser.js +1 -2
  43. package/internal/inlineRegistry.js +1 -2
  44. package/internal/inlines/autolink.js +1 -2
  45. package/internal/inlines/autolinkLiteral.js +1 -2
  46. package/internal/inlines/codeSpan.js +1 -2
  47. package/internal/inlines/emphasis.js +1 -2
  48. package/internal/inlines/entity.js +1 -2
  49. package/internal/inlines/escape.js +1 -2
  50. package/internal/inlines/footnoteReference.js +1 -2
  51. package/internal/inlines/lineBreak.js +1 -2
  52. package/internal/inlines/link.js +1 -2
  53. package/internal/inlines/rawHtml.js +1 -2
  54. package/internal/inlines/strikethrough.js +1 -2
  55. package/internal/inlines/text.js +1 -2
  56. package/internal/lineIndex.js +1 -2
  57. package/internal/patterns.js +1 -2
  58. package/internal/phrasing.js +1 -2
  59. package/internal/preprocess.js +1 -2
  60. package/internal/rawInline.js +1 -2
  61. package/internal/references.js +1 -2
  62. package/internal/sectionBounds.js +1 -2
  63. package/internal/segments.js +1 -2
  64. package/internal/stringify.js +1 -2
  65. package/internal/unescape.js +1 -2
  66. package/package.json +4 -4
package/Frontmatter.js CHANGED
@@ -1,8 +1,8 @@
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
- //#region src/Frontmatter.ts
5
+ // src/Frontmatter.ts
6
6
  /**
7
7
  * Indicates that a frontmatter codec was handed a capture of a different
8
8
  * format — a yaml codec applied to a `+++` toml capture, for example.
@@ -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
@@ -303,5 +303,4 @@ var MarkdownFrontmatter = class MarkdownFrontmatter {
303
303
  }
304
304
  };
305
305
 
306
- //#endregion
307
306
  export { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterValidationError, MarkdownFrontmatter };
@@ -1,6 +1,6 @@
1
- import { Effect, Result, Schema } from "effect";
1
+ import { Effect, Predicate, Result, Schema } from "effect";
2
2
 
3
- //#region src/FrontmatterResolver.ts
3
+ // src/FrontmatterResolver.ts
4
4
  /**
5
5
  * A `$schema` declaration referencing a schema by URL — any string containing
6
6
  * `://`.
@@ -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,7 +183,7 @@ 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
@@ -206,7 +205,7 @@ var SchemaResolver = class SchemaResolver {
206
205
  * tolerated, or the typed error.
207
206
  */
208
207
  static declarationOf(data, options) {
209
- if (!isMapping(data) || !Object.hasOwn(data, "$schema")) return options?.requireDeclaration === true ? Result.fail(new SchemaDeclarationMissingError()) : Result.succeed(void 0);
208
+ if (!Predicate.isObject(data) || !Object.hasOwn(data, "$schema")) return options?.requireDeclaration === true ? Result.fail(new SchemaDeclarationMissingError()) : Result.succeed(void 0);
210
209
  return SchemaResolver.classify(data.$schema);
211
210
  }
212
211
  /**
@@ -267,5 +266,4 @@ var SchemaResolver = class SchemaResolver {
267
266
  }
268
267
  };
269
268
 
270
- //#endregion
271
269
  export { SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError };
@@ -2,7 +2,7 @@ import { FrontmatterFormat } from "./MarkdownNode.js";
2
2
  import { scanRawFrontmatter } from "./internal/blocks/frontmatter.js";
3
3
  import { Effect, Schema } from "effect";
4
4
 
5
- //#region src/FrontmatterSource.ts
5
+ // src/FrontmatterSource.ts
6
6
  /**
7
7
  * The three line-terminator spellings a fence line may end with. Recorded by
8
8
  * {@link FrontmatterSource.split} as fidelity — what the source's fence
@@ -169,5 +169,4 @@ var FrontmatterSource = class {
169
169
  }
170
170
  };
171
171
 
172
- //#endregion
173
172
  export { FrontmatterNewline, FrontmatterSource, FrontmatterSourceBlock, FrontmatterSourceSplit };
@@ -2,7 +2,7 @@ import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismat
2
2
  import { Effect } from "effect";
3
3
  import { Jsonc } from "@effected/jsonc";
4
4
 
5
- //#region src/JsonFrontmatter.ts
5
+ // src/JsonFrontmatter.ts
6
6
  /**
7
7
  * The json frontmatter codec, over `@effected/jsonc`.
8
8
  *
@@ -42,5 +42,4 @@ const JsonFrontmatter = {
42
42
  })))
43
43
  };
44
44
 
45
- //#endregion
46
45
  export { JsonFrontmatter };
package/Markdown.js CHANGED
@@ -6,7 +6,7 @@ import { stringifyBlocks } from "./internal/stringify.js";
6
6
  import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
7
7
  import { Effect, Result, Schema, SchemaIssue, SchemaTransformation } from "effect";
8
8
 
9
- //#region src/Markdown.ts
9
+ // src/Markdown.ts
10
10
  /**
11
11
  * The markdown dialects the parser can be pointed at. `"gfm"` — CommonMark
12
12
  * 0.31.2 plus the GitHub extensions (tables, strikethrough, autolink
@@ -426,5 +426,4 @@ var Markdown = class Markdown {
426
426
  static MarkdownFromString = Markdown.fromString();
427
427
  };
428
428
 
429
- //#endregion
430
429
  export { Markdown, MarkdownDialect, MarkdownParseError, MarkdownParseOptions, MarkdownStringifyError, parsePassResult };
@@ -1,7 +1,7 @@
1
1
  import { MARKDOWN_PARSE_ERROR_CODES } from "./internal/carriers.js";
2
2
  import { Schema } from "effect";
3
3
 
4
- //#region src/MarkdownDiagnostic.ts
4
+ // src/MarkdownDiagnostic.ts
5
5
  /**
6
6
  * Error codes `Markdown.parse`/`MarkdownDocument.parse` can fail with.
7
7
  * Currently exactly one, `"NestingDepthExceeded"`, the hardening-guard trip;
@@ -84,5 +84,4 @@ function lineChar(text, offset) {
84
84
  };
85
85
  }
86
86
 
87
- //#endregion
88
87
  export { MarkdownDiagnostic, MarkdownParseErrorCode };
@@ -6,9 +6,9 @@ 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
- //#region src/MarkdownDocument.ts
11
+ // src/MarkdownDocument.ts
12
12
  /**
13
13
  * A heading-delimited span from {@link MarkdownDocument.sections}: the
14
14
  * heading, its depth, the source `range` the section occupies and the
@@ -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);
@@ -391,5 +400,4 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
391
400
  static parse = Effect.fn("MarkdownDocument.parse")((text, options) => Effect.fromResult(MarkdownDocument.parseResult(text, options)));
392
401
  };
393
402
 
394
- //#endregion
395
403
  export { DocumentSection, MarkdownDocument };
package/MarkdownEdit.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Schema } from "effect";
2
2
 
3
- //#region src/MarkdownEdit.ts
3
+ // src/MarkdownEdit.ts
4
4
  /**
5
5
  * A range within a markdown document, expressed as a zero-based character
6
6
  * `offset` and a `length` in UTF-16 code units. Pass to `MarkdownFormat.format`
@@ -52,5 +52,4 @@ var MarkdownEdit = class extends Schema.Class("MarkdownEdit")({
52
52
  }
53
53
  };
54
54
 
55
- //#endregion
56
55
  export { MarkdownEdit, MarkdownRange };
package/MarkdownFormat.js CHANGED
@@ -3,7 +3,7 @@ import { BulletChar, EmphasisChar, FenceChar, HeadingStyle, Paragraph, Root, Tex
3
3
  import { Markdown, MarkdownDialect, MarkdownParseOptions } from "./Markdown.js";
4
4
  import { Effect, Result, Schema } from "effect";
5
5
 
6
- //#region src/MarkdownFormat.ts
6
+ // src/MarkdownFormat.ts
7
7
  /**
8
8
  * The two ways CommonMark spells a code block: `fenced` (a backtick or tilde
9
9
  * fence) and `indented` (four-space indentation).
@@ -511,5 +511,4 @@ var MarkdownFormat = class MarkdownFormat {
511
511
  });
512
512
  };
513
513
 
514
- //#endregion
515
514
  export { CodeBlockStyle, MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode };
package/MarkdownNode.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
2
 
3
- //#region src/MarkdownNode.ts
3
+ // src/MarkdownNode.ts
4
4
  /**
5
5
  * A single point in a source document: 1-based `line` and `column`, 0-based
6
6
  * `offset`.
@@ -854,5 +854,4 @@ const MarkdownNode = Schema.suspend(() => Schema.Union([
854
854
  TableContent
855
855
  ]));
856
856
 
857
- //#endregion
858
857
  export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterFormat, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, MarkdownNode, MdxFlowExpression, MdxJsxAttribute, MdxJsxAttributeContent, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar };
@@ -1,6 +1,6 @@
1
1
  import { rootHeadingBounds } from "./internal/sectionBounds.js";
2
2
 
3
- //#region src/MarkdownTransform.ts
3
+ // src/MarkdownTransform.ts
4
4
  /**
5
5
  * Pure, total tree-to-tree transforms over the markdown node classes. Not
6
6
  * instantiable.
@@ -163,5 +163,4 @@ function mapNode(node, handlers) {
163
163
  return handler === void 0 ? current : handler(current);
164
164
  }
165
165
 
166
- //#endregion
167
166
  export { MarkdownTransform };
@@ -1,7 +1,7 @@
1
1
  import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
2
2
  import { Data, Stream } from "effect";
3
3
 
4
- //#region src/MarkdownVisitor.ts
4
+ // src/MarkdownVisitor.ts
5
5
  /**
6
6
  * Constructors and matchers for the `MarkdownVisitorEvent` union (e.g.
7
7
  * `MarkdownVisitorEvent.Enter({ node, path, depth })`,
@@ -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({
@@ -105,5 +145,4 @@ function* walkNode(node, path, depth) {
105
145
  });
106
146
  }
107
147
 
108
- //#endregion
109
148
  export { MarkdownVisitor, MarkdownVisitorEvent };
package/Mdast.js CHANGED
@@ -1,8 +1,9 @@
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
- //#region src/Mdast.ts
6
+ // src/Mdast.ts
6
7
  /**
7
8
  * Indicates that foreign mdast input failed to decode into the package's
8
9
  * node classes.
@@ -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
@@ -520,5 +536,4 @@ function nodeText(node, includeImageAlt, includeHtml) {
520
536
  return "";
521
537
  }
522
538
 
523
- //#endregion
524
539
  export { Mdast, MdastDecodeError };
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.
@@ -2,7 +2,7 @@ import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismat
2
2
  import { Effect } from "effect";
3
3
  import { Toml } from "@effected/toml";
4
4
 
5
- //#region src/TomlFrontmatter.ts
5
+ // src/TomlFrontmatter.ts
6
6
  /**
7
7
  * The toml frontmatter codec, over `@effected/toml`.
8
8
  *
@@ -40,5 +40,4 @@ const TomlFrontmatter = {
40
40
  })))
41
41
  };
42
42
 
43
- //#endregion
44
43
  export { TomlFrontmatter };
@@ -2,7 +2,7 @@ import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismat
2
2
  import { Effect } from "effect";
3
3
  import { Yaml } from "@effected/yaml";
4
4
 
5
- //#region src/YamlFrontmatter.ts
5
+ // src/YamlFrontmatter.ts
6
6
  /**
7
7
  * The yaml frontmatter codec, over `@effected/yaml`.
8
8
  *
@@ -41,5 +41,4 @@ const YamlFrontmatter = {
41
41
  })))
42
42
  };
43
43
 
44
- //#endregion
45
44
  export { YamlFrontmatter };