@effected/markdown 0.18.0 → 0.20.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 +1 -2
  2. package/FrontmatterResolver.js +1 -2
  3. package/FrontmatterSource.js +1 -2
  4. package/JsonFrontmatter.js +1 -2
  5. package/Markdown.js +25 -14
  6. package/MarkdownDiagnostic.js +1 -2
  7. package/MarkdownDocument.js +5 -39
  8. package/MarkdownEdit.js +1 -2
  9. package/MarkdownFormat.js +1 -2
  10. package/MarkdownNode.js +1 -2
  11. package/MarkdownTransform.js +166 -0
  12. package/MarkdownVisitor.js +1 -2
  13. package/Mdast.js +50 -2
  14. package/TomlFrontmatter.js +1 -2
  15. package/YamlFrontmatter.js +1 -2
  16. package/index.d.ts +310 -39
  17. package/index.js +2 -1
  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 +43 -0
  63. package/internal/segments.js +1 -2
  64. package/internal/stringify.js +8 -11
  65. package/internal/unescape.js +1 -2
  66. package/package.json +4 -4
package/Frontmatter.js CHANGED
@@ -2,7 +2,7 @@ import { MarkdownEdit } from "./MarkdownEdit.js";
2
2
  import { FrontmatterFormat } from "./MarkdownNode.js";
3
3
  import { Effect, 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.
@@ -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
1
  import { Effect, 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
  * `://`.
@@ -267,5 +267,4 @@ var SchemaResolver = class SchemaResolver {
267
267
  }
268
268
  };
269
269
 
270
- //#endregion
271
270
  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
@@ -2,11 +2,11 @@ import { Root } from "./MarkdownNode.js";
2
2
  import { isGuardExceeded, isRawMarkdownError } from "./internal/carriers.js";
3
3
  import { parseBlocks } from "./internal/blockParser.js";
4
4
  import { parsePhrasingText } from "./internal/phrasing.js";
5
- import { stringifyTree } from "./internal/stringify.js";
5
+ 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
@@ -256,11 +256,20 @@ var Markdown = class Markdown {
256
256
  */
257
257
  static parsePhrasing = Effect.fn("Markdown.parsePhrasing")((text, options) => Effect.fromResult(Markdown.parsePhrasingResult(text, options)));
258
258
  /**
259
- * Serialize a {@link Root} tree to canonical markdown, synchronously, as a
260
- * `Result`. The pure primitive twin of {@link Markdown.stringify}, on the
261
- * same terms as {@link Markdown.parseResult}.
259
+ * Serialize a {@link Root} tree, or a single {@link (FlowContent:type)} node, to
260
+ * canonical markdown, synchronously, as a `Result`. The pure primitive twin
261
+ * of {@link Markdown.stringify}, on the same terms as
262
+ * {@link Markdown.parseResult}.
262
263
  *
263
264
  * @remarks
265
+ * **A single flow node is defined as its one-child document**: passing
266
+ * `node` is exactly `stringifyResult(Root.make({ children: [node] }))` —
267
+ * the same code path, the same trailing `\n`, the same canonical choices.
268
+ * This lets a caller serialize one block (a table, a section's paragraph,
269
+ * an MDX flow element) without building the wrapper by hand.
270
+ * `Frontmatter` and `MdxjsEsm` are deliberately not admitted alone — each
271
+ * is meaningful only at a document's root — so wrap them in a `Root`.
272
+ *
264
273
  * Canonical serialization: fidelity fields (marker characters, fence
265
274
  * style, heading spelling) win when present; documented canonical
266
275
  * defaults apply when absent. One default surprises: a language-less
@@ -343,13 +352,13 @@ var Markdown = class Markdown {
343
352
  * trip on a tree nesting past the depth cap fails, and only synthesized
344
353
  * or decoded trees can nest that far.
345
354
  *
346
- * @param root - The document tree to serialize.
355
+ * @param node - The document tree, or a single flow node, to serialize.
347
356
  * @returns A `Result` succeeding with markdown source, or failing with
348
357
  * {@link MarkdownStringifyError}.
349
358
  */
350
- static stringifyResult(root) {
359
+ static stringifyResult(node) {
351
360
  try {
352
- return Result.succeed(stringifyTree(root));
361
+ return Result.succeed(stringifyBlocks(node.type === "root" ? node.children : [node]));
353
362
  } catch (caught) {
354
363
  if (isGuardExceeded(caught)) return Result.fail(new MarkdownStringifyError({ diagnostic: MarkdownDiagnostic.make({
355
364
  code: caught.reason,
@@ -363,9 +372,9 @@ var Markdown = class Markdown {
363
372
  }
364
373
  }
365
374
  /**
366
- * Serialize a {@link Root} tree to canonical markdown. Defined in terms of
367
- * {@link Markdown.stringifyResult} — synchronous callers can use that
368
- * variant directly.
375
+ * Serialize a {@link Root} tree, or a single {@link (FlowContent:type)} node, to
376
+ * canonical markdown. Defined in terms of {@link Markdown.stringifyResult}
377
+ * — synchronous callers can use that variant directly.
369
378
  *
370
379
  * @remarks
371
380
  * The canonical form is a **stability commitment** and safe to assert
@@ -381,11 +390,14 @@ var Markdown = class Markdown {
381
390
  * `escapeStyle`, an emitter instruction rather than a source spelling,
382
391
  * which the boundary admits.
383
392
  *
384
- * @param root - The document tree to serialize.
393
+ * A single flow node serializes exactly as its one-child document does;
394
+ * see {@link Markdown.stringifyResult}.
395
+ *
396
+ * @param node - The document tree, or a single flow node, to serialize.
385
397
  * @returns An `Effect` that succeeds with markdown source, or fails with
386
398
  * {@link MarkdownStringifyError}.
387
399
  */
388
- static stringify = Effect.fn("Markdown.stringify")((root) => Effect.fromResult(Markdown.stringifyResult(root)));
400
+ static stringify = Effect.fn("Markdown.stringify")((node) => Effect.fromResult(Markdown.stringifyResult(node)));
389
401
  /**
390
402
  * A `Schema<Root, string>` decoding markdown source into a {@link Root}
391
403
  * tree and encoding a tree back to canonical markdown via
@@ -414,5 +426,4 @@ var Markdown = class Markdown {
414
426
  static MarkdownFromString = Markdown.fromString();
415
427
  };
416
428
 
417
- //#endregion
418
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 };
@@ -5,9 +5,10 @@ import { preprocessLines } from "./internal/preprocess.js";
5
5
  import { normalizeLabelText } from "./internal/references.js";
6
6
  import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
7
7
  import { parsePassResult } from "./Markdown.js";
8
+ import { rootHeadingBounds } from "./internal/sectionBounds.js";
8
9
  import { Effect, Result, Schema } from "effect";
9
10
 
10
- //#region src/MarkdownDocument.ts
11
+ // src/MarkdownDocument.ts
11
12
  /**
12
13
  * A heading-delimited span from {@link MarkdownDocument.sections}: the
13
14
  * heading, its depth, the source `range` the section occupies and the
@@ -239,43 +240,9 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
239
240
  get sections() {
240
241
  const blocks = this.root.children;
241
242
  const sections = [];
242
- const headings = [];
243
- for (let index = 0; index < blocks.length; index += 1) {
244
- const block = blocks[index];
245
- if (block !== void 0 && block.type === "heading") headings.push({
246
- index,
247
- node: block
248
- });
249
- }
250
- const nextByDepth = [
251
- void 0,
252
- void 0,
253
- void 0,
254
- void 0,
255
- void 0,
256
- void 0,
257
- void 0
258
- ];
259
- const boundaryByHeading = new Array(headings.length);
260
- for (let index = headings.length - 1; index >= 0; index -= 1) {
261
- const current = headings[index]?.node;
262
- if (current === void 0) continue;
263
- let boundary;
264
- for (let depth = 1; depth <= current.depth; depth += 1) {
265
- const candidate = nextByDepth[depth];
266
- if (candidate !== void 0 && (boundary === void 0 || candidate < boundary)) boundary = candidate;
267
- }
268
- boundaryByHeading[index] = boundary;
269
- nextByDepth[current.depth] = index;
270
- }
271
- for (let index = 0; index < headings.length; index += 1) {
272
- const current = headings[index];
273
- if (current === void 0) continue;
274
- const block = current.node;
275
- const boundaryHeading = boundaryByHeading[index];
276
- const boundaryNode = boundaryHeading === void 0 ? void 0 : headings[boundaryHeading]?.node;
243
+ for (const { heading: block, index, end } of rootHeadingBounds(blocks)) {
244
+ const boundaryNode = blocks[end];
277
245
  const boundary = boundaryNode === void 0 ? this.source.length : boundaryNode.position.start.offset;
278
- const end = boundaryHeading === void 0 ? blocks.length : headings[boundaryHeading]?.index ?? blocks.length;
279
246
  const start = block.position.start.offset;
280
247
  const bodyStart = block.position.end.offset;
281
248
  sections.push(new DocumentSection({
@@ -285,7 +252,7 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
285
252
  offset: start,
286
253
  length: boundary - start
287
254
  }),
288
- children: blocks.slice(current.index + 1, end).filter((child) => child.type !== "frontmatter" && child.type !== "mdxjsEsm"),
255
+ children: blocks.slice(index + 1, end).filter((child) => child.type !== "frontmatter" && child.type !== "mdxjsEsm"),
289
256
  text: phrasingText(block.children),
290
257
  bodyRange: MarkdownRange.make({
291
258
  offset: bodyStart,
@@ -424,5 +391,4 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
424
391
  static parse = Effect.fn("MarkdownDocument.parse")((text, options) => Effect.fromResult(MarkdownDocument.parseResult(text, options)));
425
392
  };
426
393
 
427
- //#endregion
428
394
  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 };
@@ -0,0 +1,166 @@
1
+ import { rootHeadingBounds } from "./internal/sectionBounds.js";
2
+
3
+ // src/MarkdownTransform.ts
4
+ /**
5
+ * Pure, total tree-to-tree transforms over the markdown node classes. Not
6
+ * instantiable.
7
+ *
8
+ * @remarks
9
+ * Every method returns a plain value — none has an error channel. They work
10
+ * on a tree, never on source text, so they compose with
11
+ * {@link Markdown.stringify} for output and with each other; offset-splice
12
+ * editing stays the job of `MarkdownFormat` and `MarkdownEdit`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { LinkReference, Markdown, MarkdownTransform } from "@effected/markdown";
17
+ * import { Result } from "effect";
18
+ *
19
+ * const parsed = Markdown.parseResult("# Title\n\nSee [docs](https://example.com).\n");
20
+ * if (Result.isSuccess(parsed)) {
21
+ * const shifted = MarkdownTransform.shiftHeadings(parsed.success, 1);
22
+ * const rewritten = MarkdownTransform.map(shifted, {
23
+ * link: (link) =>
24
+ * LinkReference.make({ identifier: "docs", referenceType: "full", children: link.children }),
25
+ * });
26
+ * Markdown.stringifyResult(rewritten); // "## Title\n\nSee [docs][docs].\n"
27
+ * }
28
+ * ```
29
+ *
30
+ * @public
31
+ */
32
+ var MarkdownTransform = class {
33
+ constructor() {}
34
+ static map(node, handlers) {
35
+ return mapNode(node, handlers);
36
+ }
37
+ /**
38
+ * Shift every heading's depth by `by`, clamped into `[min, max]`.
39
+ *
40
+ * @remarks
41
+ * The new depth is `clamp(depth + by, min ?? 1, max ?? 6)`. A heading
42
+ * whose new depth exceeds 2 loses `headingStyle: "setext"` (the key is
43
+ * omitted), since setext spells depths 1 and 2 only; it then serializes
44
+ * as ATX. A heading whose depth does not change, and needs no setext
45
+ * drop, keeps its identity; structural sharing follows
46
+ * {@link MarkdownTransform.(map:1)}, on which this is built. Accepts any node,
47
+ * including a lone {@link Heading}.
48
+ *
49
+ * Built on `map`, it recurses the same way: a hand-built tree nested far
50
+ * deeper than anything `Markdown.parse` or `Mdast.fromMdast` produces can
51
+ * overflow the call stack.
52
+ *
53
+ * @param node - The node whose headings to shift.
54
+ * @param by - The signed depth offset; must be an integer.
55
+ * @param options - Optional {@link ShiftHeadingsOptions} clamp bounds.
56
+ * @returns The node with headings shifted.
57
+ * @throws `RangeError` when `by` is not an integer, or `min` / `max` is not
58
+ * a heading depth or `min > max` — a wiring error, not a data error.
59
+ */
60
+ static shiftHeadings(node, by, options) {
61
+ if (!Number.isInteger(by)) throw new RangeError(`MarkdownTransform.shiftHeadings: by must be an integer, got ${by}`);
62
+ const min = options?.min ?? 1;
63
+ const max = options?.max ?? 6;
64
+ if (!isHeadingDepth(min) || !isHeadingDepth(max) || min > max) throw new RangeError(`MarkdownTransform.shiftHeadings: invalid depth bounds min=${min} max=${max}`);
65
+ return mapNode(node, { heading: (heading) => {
66
+ const depth = Math.min(max, Math.max(min, heading.depth + by));
67
+ const dropSetext = depth > 2 && heading.headingStyle === "setext";
68
+ if (depth === heading.depth && !dropSetext) return heading;
69
+ const { headingStyle, ...rest } = heading;
70
+ const ctor = heading.constructor;
71
+ return new ctor({
72
+ ...rest,
73
+ depth,
74
+ ...headingStyle !== void 0 && !dropSetext ? { headingStyle } : {}
75
+ }, { disableChecks: true });
76
+ } });
77
+ }
78
+ /**
79
+ * Locate the root-level sections of one heading depth by child index —
80
+ * no source text needed, so it works on a tree a
81
+ * `MarkdownTransform.map` has reshaped.
82
+ *
83
+ * @remarks
84
+ * Returns the root-level headings of exactly `depth` whose index lies in
85
+ * `[start, end)`, in document order. Each section ends at the next
86
+ * root-level heading of equal or shallower depth, or at `end` — the same
87
+ * boundary rule as `MarkdownDocument.sections`, from one shared
88
+ * implementation. The window is what lets a caller nest: find the depth-2
89
+ * blocks, then the depth-3 sections inside one block's
90
+ * `[headingIndex + 1, end)`.
91
+ *
92
+ * **A shallower heading ends a section too.** Querying depth 2 on
93
+ * `## Features`, a list, a stray `# Top` and another list yields one span
94
+ * whose `end` is the `# Top` heading's index, so neither `# Top` nor the
95
+ * list after it belongs to the section. That is the outline reading. A
96
+ * caller that wants each section to run to the next heading of the SAME
97
+ * depth (slicing a changeset or changelog by entry, where a stray H1 is
98
+ * content) computes each body's end from the next span's `headingIndex`
99
+ * instead, or the window's end for the last span, as in the example.
100
+ *
101
+ * @example
102
+ * Bodies that run to the next same-depth heading:
103
+ *
104
+ * ```ts
105
+ * import { Markdown, MarkdownTransform } from "@effected/markdown";
106
+ * import { Result } from "effect";
107
+ *
108
+ * const root = Result.getOrThrow(Markdown.parseResult("## Features\n\n- a\n\n# Top\n\n- b\n"));
109
+ * const spans = MarkdownTransform.sections(root, { depth: 2 });
110
+ * // spans[0].end === 2: the outline section stops at `# Top`
111
+ * const bodies = spans.map((span, index) =>
112
+ * root.children.slice(span.headingIndex + 1, spans[index + 1]?.headingIndex ?? root.children.length),
113
+ * );
114
+ * // bodies[0] holds the first list, `# Top` and the second list
115
+ * ```
116
+ *
117
+ * @param root - The tree whose root-level headings delimit sections.
118
+ * @param options - The {@link SectionSpanOptions} depth and window.
119
+ * @returns The matching {@link SectionSpan}s, in document order.
120
+ * @throws `RangeError` when `depth` is not an integer from 1 to 6, when
121
+ * `start` or `end` is not an integer in `[0, root.children.length]`, or
122
+ * when `start > end`.
123
+ */
124
+ static sections(root, options) {
125
+ if (!isHeadingDepth(options.depth)) throw new RangeError(`MarkdownTransform.sections: depth must be a heading depth 1-6, got ${options.depth}`);
126
+ const length = root.children.length;
127
+ const start = options.start ?? 0;
128
+ const end = options.end ?? length;
129
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end > length || start > end) throw new RangeError(`MarkdownTransform.sections: invalid window start=${start} end=${end} for ${length} children`);
130
+ const spans = [];
131
+ for (const bound of rootHeadingBounds(root.children)) {
132
+ if (bound.index < start || bound.index >= end || bound.heading.depth !== options.depth) continue;
133
+ spans.push({
134
+ heading: bound.heading,
135
+ headingIndex: bound.index,
136
+ end: Math.min(bound.end, end)
137
+ });
138
+ }
139
+ return spans;
140
+ }
141
+ };
142
+ const isHeadingDepth = (value) => Number.isInteger(value) && value >= 1 && value <= 6;
143
+ function mapNode(node, handlers) {
144
+ let current = node;
145
+ if ("children" in node) {
146
+ const children = node.children;
147
+ let next;
148
+ for (let index = 0; index < children.length; index += 1) {
149
+ const child = children[index];
150
+ const mapped = mapNode(child, handlers);
151
+ if (next === void 0 && mapped !== child) next = children.slice(0, index);
152
+ next?.push(mapped);
153
+ }
154
+ if (next !== void 0) {
155
+ const ctor = node.constructor;
156
+ current = new ctor({
157
+ ...node,
158
+ children: next
159
+ }, { disableChecks: true });
160
+ }
161
+ }
162
+ const handler = handlers[current.type];
163
+ return handler === void 0 ? current : handler(current);
164
+ }
165
+
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 })`,
@@ -105,5 +105,4 @@ function* walkNode(node, path, depth) {
105
105
  });
106
106
  }
107
107
 
108
- //#endregion
109
108
  export { MarkdownVisitor, MarkdownVisitorEvent };
package/Mdast.js CHANGED
@@ -2,7 +2,7 @@ import { Root } from "./MarkdownNode.js";
2
2
  import { unescapeString } from "./internal/unescape.js";
3
3
  import { Effect, Result, Schema } from "effect";
4
4
 
5
- //#region src/Mdast.ts
5
+ // src/Mdast.ts
6
6
  /**
7
7
  * Indicates that foreign mdast input failed to decode into the package's
8
8
  * node classes.
@@ -469,7 +469,55 @@ var Mdast = class Mdast {
469
469
  * fails with {@link MdastDecodeError}.
470
470
  */
471
471
  static fromMdast = Effect.fn("Mdast.fromMdast")((input) => Effect.fromResult(Mdast.fromMdastResult(input)));
472
+ /**
473
+ * The plain-text content of a node, defined as `mdast-util-to-string`
474
+ * applied to the node's {@link Mdast.toMdast} projection.
475
+ *
476
+ * @remarks
477
+ * A node carrying a string `value` yields it (an `html` node only when
478
+ * `includeHtml`); an `image` or `imageReference` yields its non-empty
479
+ * `alt` when `includeImageAlt`; a node with children yields the
480
+ * concatenation of its children's text; anything else — a `break`, a
481
+ * `thematicBreak`, a `definition` — yields `""`. Both options default to
482
+ * `true`. Pure and total.
483
+ *
484
+ * **Values are read as the projection spells them.** The one value the
485
+ * projection changes is a `code` node's: this package keeps the final line
486
+ * ending of a code block's content, plain mdast does not, so `toText`
487
+ * strips it exactly as {@link Mdast.toMdast} does — `"let x"`, not
488
+ * `"let x\n"`, for a fenced block holding one line.
489
+ *
490
+ * This is the remark-ecosystem derivation, and it deliberately differs
491
+ * from the display derivation behind `DocumentHeading.text` and
492
+ * `DocumentSection.text`, which render a hard break as a space and drop
493
+ * raw HTML. Reach for `toText` when matching what remark tooling computes
494
+ * (slugs, search indexes); reach for the document accessors for display.
495
+ *
496
+ * The walk recurses once per nesting level. Every tree `Markdown.parse` or
497
+ * `Mdast.fromMdast` produces stays within the package's nesting cap, but a
498
+ * hand-built tree nested far deeper than that can overflow the call stack.
499
+ *
500
+ * @param node - The node to flatten.
501
+ * @param options - Optional {@link MdastToTextOptions}.
502
+ * @returns The node's text content.
503
+ */
504
+ static toText(node, options) {
505
+ return nodeText(node, options?.includeImageAlt ?? true, options?.includeHtml ?? true);
506
+ }
472
507
  };
508
+ /** `mdast-util-to-string`'s `one`, over the node classes as `toMdast` projects them. */
509
+ function nodeText(node, includeImageAlt, includeHtml) {
510
+ if ("value" in node) {
511
+ if (node.type === "html") return includeHtml ? node.value : "";
512
+ return node.type === "code" ? stripFinalLineEnding(node.value) : node.value;
513
+ }
514
+ if (includeImageAlt && "alt" in node && node.alt) return node.alt;
515
+ if ("children" in node) {
516
+ let text = "";
517
+ for (const child of node.children) text += nodeText(child, includeImageAlt, includeHtml);
518
+ return text;
519
+ }
520
+ return "";
521
+ }
473
522
 
474
- //#endregion
475
523
  export { Mdast, MdastDecodeError };
@@ -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 };