@effected/markdown 0.3.0 → 0.4.1

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
@@ -69,22 +69,43 @@ var FrontmatterEncodeError = class extends Schema.TaggedErrorClass()("Frontmatte
69
69
  }
70
70
  };
71
71
  /**
72
+ * Why a frontmatter decoder found no capture on a document.
73
+ *
74
+ * @remarks
75
+ * `"absent"` — the source genuinely has no frontmatter block.
76
+ * `"captureDisabled"` — the source opens with a well-formed frontmatter
77
+ * block, but the document carries no capture node: it was parsed without
78
+ * `frontmatter: true` (the toggle defaults off), so spec-correct CommonMark
79
+ * read the fences as ordinary content.
80
+ *
81
+ * @public
82
+ */
83
+ const FrontmatterMissingReason = Schema.Literals(["absent", "captureDisabled"]);
84
+ /**
72
85
  * Indicates that a document handed to a frontmatter decoder carries no
73
86
  * frontmatter capture.
74
87
  *
75
88
  * @remarks
76
- * Raised when the document genuinely has no frontmatter block — including
77
- * when it has one in the source but was parsed with the capture toggle off
78
- * (`MarkdownParseOptions.frontmatter` defaults to `false`). Cause-free: there
79
- * is nothing to diagnose beyond the absence itself. Consumers wanting
80
- * optional semantics can `Effect.catchTag("FrontmatterMissingError", ...)`
81
- * to a default.
89
+ * `reason` distinguishes the two ways this happens: `"absent"` when the
90
+ * source genuinely has no frontmatter block, `"captureDisabled"` when the
91
+ * source opens with one but the document was parsed with the capture toggle
92
+ * off (`MarkdownParseOptions.frontmatter` defaults to `false`) — the fix
93
+ * there is parsing with `frontmatter: true`, not editing the document. The
94
+ * discriminant is computed from the same structural signal
95
+ * `MarkdownDocument.hasFrontmatterBlock` exposes, so the error and the
96
+ * accessor can never disagree.
97
+ *
98
+ * Cause-free: there is nothing to diagnose beyond the absence itself.
99
+ * Consumers wanting optional semantics can
100
+ * `Effect.catchTag("FrontmatterMissingError", ...)` to a default.
82
101
  *
83
102
  * @public
84
103
  */
85
- var FrontmatterMissingError = class extends Schema.TaggedErrorClass()("FrontmatterMissingError", {}) {
104
+ var FrontmatterMissingError = class extends Schema.TaggedErrorClass()("FrontmatterMissingError", {
105
+ /** Why there is no capture: no block at all, or capture left off. */
106
+ reason: FrontmatterMissingReason }) {
86
107
  get message() {
87
- return "the document has no frontmatter capture; was it parsed with `frontmatter: true`?";
108
+ return this.reason === "captureDisabled" ? "the source opens with a frontmatter block, but it was not captured; parse with `frontmatter: true`" : "the document has no frontmatter block";
88
109
  }
89
110
  };
90
111
  /**
@@ -157,7 +178,8 @@ var MarkdownFrontmatter = class MarkdownFrontmatter {
157
178
  * The decoder reads the document's frontmatter capture (parse with
158
179
  * `frontmatter: true` — the toggle defaults off), decodes its raw value
159
180
  * through the codec, then validates the data against `schema`. Each stage
160
- * fails typed: no capture is {@link FrontmatterMissingError}, a
181
+ * fails typed: no capture is {@link FrontmatterMissingError} (its `reason`
182
+ * says whether the block is genuinely absent or capture was left off), a
161
183
  * wrong-format codec is {@link FrontmatterFormatMismatchError}, unparseable
162
184
  * content is {@link FrontmatterDecodeError}, and schema-invalid data is
163
185
  * {@link FrontmatterValidationError} carrying the structured issue.
@@ -178,7 +200,7 @@ var MarkdownFrontmatter = class MarkdownFrontmatter {
178
200
  static schema(schema, codec) {
179
201
  return (document) => {
180
202
  const node = document.frontmatter;
181
- return node === void 0 ? Effect.fail(new FrontmatterMissingError()) : codec.decode(node).pipe(Effect.flatMap((data) => Schema.decodeUnknownEffect(schema)(data).pipe(Effect.catchTag("SchemaError", (error) => Effect.fail(new FrontmatterValidationError({ issue: error.issue }))))));
203
+ 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 }))))));
182
204
  };
183
205
  }
184
206
  /**
@@ -269,4 +291,4 @@ var MarkdownFrontmatter = class MarkdownFrontmatter {
269
291
  };
270
292
 
271
293
  //#endregion
272
- export { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterValidationError, MarkdownFrontmatter };
294
+ export { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterValidationError, MarkdownFrontmatter };
package/Markdown.js CHANGED
@@ -28,7 +28,9 @@ const MarkdownDialect = Schema.Literals(["commonmark", "gfm"]);
28
28
  * spec-conformant reading must hold unless a consumer opts in. Enabled, the
29
29
  * engine captures `---`/`+++`/`---json` blocks at offset 0 into a raw
30
30
  * `Frontmatter` head node — decoding it is the frontmatter codec modules'
31
- * job.
31
+ * job. Parsed with the toggle off, such a document reports no frontmatter —
32
+ * `MarkdownDocument.hasFrontmatterBlock` tells "no block in the source"
33
+ * apart from "parsed with capture off".
32
34
  *
33
35
  * @public
34
36
  */
@@ -181,10 +183,14 @@ var Markdown = class Markdown {
181
183
  * @remarks
182
184
  * Canonical serialization: fidelity fields (marker characters, fence
183
185
  * style, heading spelling) win when present; documented canonical
184
- * defaults apply when absent. The output re-parses to a render-equivalent
185
- * document — the corpus-pinned contract — but is not a byte-level
186
- * round-trip of any original source; surgical editing goes through the
187
- * offset-splice edit layer instead.
186
+ * defaults apply when absent. One default surprises: a language-less
187
+ * `Code` node with no explicit `fenceChar` serializes as an
188
+ * **indented** code block — set `fenceChar` (or a `lang`) on the node for
189
+ * a fence, or normalize afterward with
190
+ * `MarkdownFormattingOptions.codeBlockStyle: "fenced"`. The output
191
+ * re-parses to a render-equivalent document — the corpus-pinned
192
+ * contract — but is not a byte-level round-trip of any original source;
193
+ * surgical editing goes through the offset-splice edit layer instead.
188
194
  *
189
195
  * Failure is rare by design, symmetric with parse: only a hardening-guard
190
196
  * trip on a tree nesting past the depth cap fails, and only synthesized
@@ -1,6 +1,8 @@
1
1
  import { MarkdownRange } from "./MarkdownEdit.js";
2
2
  import { Definition, Root } from "./MarkdownNode.js";
3
+ import { preprocessLines } from "./internal/preprocess.js";
3
4
  import { normalizeLabelText } from "./internal/references.js";
5
+ import { scanFrontmatter } from "./internal/blocks/frontmatter.js";
4
6
  import { MarkdownDiagnostic } from "./MarkdownDiagnostic.js";
5
7
  import { parsePassResult } from "./Markdown.js";
6
8
  import { Effect, Result, Schema } from "effect";
@@ -113,10 +115,7 @@ const phrasingText = (nodes) => {
113
115
  case "strong":
114
116
  case "delete":
115
117
  case "link":
116
- case "linkReference":
117
- out += phrasingText(node.children);
118
- break;
119
- default: break;
118
+ case "linkReference": out += phrasingText(node.children);
120
119
  }
121
120
  return out;
122
121
  };
@@ -164,13 +163,38 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
164
163
  * only ever sit at the head of `root.children` (the capture fires at most
165
164
  * once, at offset 0), so the tree is the single source of truth and the
166
165
  * accessor can never disagree with it. `undefined` covers both a document
167
- * with no frontmatter block and one parsed with the capture toggle off.
166
+ * with no frontmatter block and one parsed with the capture toggle off —
167
+ * {@link MarkdownDocument.hasFrontmatterBlock} tells the two apart.
168
168
  */
169
169
  get frontmatter() {
170
170
  const head = this.root.children[0];
171
171
  return head !== void 0 && head.type === "frontmatter" ? head : void 0;
172
172
  }
173
173
  /**
174
+ * Whether the source opens with a well-formed frontmatter block —
175
+ * regardless of how the document was parsed.
176
+ *
177
+ * @remarks
178
+ * The structural signal that disambiguates the `frontmatter` accessor's
179
+ * `undefined`: capture is opt-in (`MarkdownParseOptions.frontmatter`
180
+ * defaults off), so a document whose source visibly starts with `---` can
181
+ * still carry no capture node — spec-correct CommonMark reads the fences as
182
+ * a thematic break plus content. `frontmatter === undefined` with
183
+ * `hasFrontmatterBlock === true` means exactly "parsed with capture off";
184
+ * with `false` it means the source genuinely has no block.
185
+ *
186
+ * Derived, not stored: the getter runs the **same** pre-scan the parser
187
+ * runs when capture is enabled (the closed fence grammar — `---` yaml,
188
+ * `+++` toml, `---json` json; an unclosed fence is not frontmatter), so it
189
+ * is `true` exactly when parsing this source with `frontmatter: true`
190
+ * would produce a capture node, and the two can never drift. Like the
191
+ * other navigation accessors it recomputes per access — bind it once when
192
+ * checking repeatedly.
193
+ */
194
+ get hasFrontmatterBlock() {
195
+ return scanFrontmatter(preprocessLines(this.source), this.source) !== null;
196
+ }
197
+ /**
174
198
  * Every heading in the document, in document order, wherever it sits —
175
199
  * including headings nested inside blockquotes and list items.
176
200
  *
@@ -313,7 +337,6 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
313
337
  });
314
338
  break;
315
339
  }
316
- default: break;
317
340
  }
318
341
  });
319
342
  return entries;
package/MarkdownFormat.js CHANGED
@@ -5,6 +5,19 @@ import { Effect, Result, Schema } from "effect";
5
5
 
6
6
  //#region src/MarkdownFormat.ts
7
7
  /**
8
+ * The two ways CommonMark spells a code block: `fenced` (a backtick or tilde
9
+ * fence) and `indented` (four-space indentation).
10
+ *
11
+ * @remarks
12
+ * A formatting target for {@link MarkdownFormattingOptions}, not a node
13
+ * fidelity field: on a {@link Code} node the two spellings are told apart by
14
+ * the presence or absence of `fenceChar`/`fenceLength`, and a language-less
15
+ * node with neither serializes as an **indented** block by default.
16
+ *
17
+ * @public
18
+ */
19
+ const CodeBlockStyle = Schema.Literals(["fenced", "indented"]);
20
+ /**
8
21
  * Options controlling formatting: which concrete-syntax markers to normalize,
9
22
  * plus the parse knobs (`dialect`, `frontmatter`) the formatter parses the
10
23
  * source with (same defaults as `Markdown.parse`). Every marker option is
@@ -12,7 +25,25 @@ import { Effect, Result, Schema } from "effect";
12
25
  *
13
26
  * Day-one scope is marker normalization only — heading style, bullet
14
27
  * character, emphasis/strong marker, fence character, thematic-break
15
- * character. Content is never rewritten, rewrapped or reflowed.
28
+ * character, code-block style. Content is never rewritten, rewrapped or
29
+ * reflowed.
30
+ *
31
+ * @remarks
32
+ * `codeBlockStyle` is the opt-in that converts between the two code-block
33
+ * spellings for **language-less** blocks (a block with a `lang` has no
34
+ * indented spelling and is never touched). It exists because the default
35
+ * surprises: absent this option, a language-less code block keeps its
36
+ * spelling — and a language-less {@link Code} node with no explicit
37
+ * `fenceChar` **serializes as an indented block** through
38
+ * `Markdown.stringify`. `"fenced"` rewrites indented blocks to fences (the
39
+ * fence character follows `fenceChar` when also set, backtick otherwise);
40
+ * `"indented"` rewrites language-less fences to indented form where provably
41
+ * safe. Like every other option, hazardous conversions are skipped, never
42
+ * attempted: both directions apply to root-level, flush-left blocks only,
43
+ * and the `"indented"` direction additionally skips blocks whose indented
44
+ * spelling would lazily continue a paragraph, be absorbed into a preceding
45
+ * list or footnote definition, merge with an adjacent code block, or fail
46
+ * representability (empty, or blank first/last lines).
16
47
  *
17
48
  * @public
18
49
  */
@@ -23,7 +54,8 @@ var MarkdownFormattingOptions = class extends Schema.Class("MarkdownFormattingOp
23
54
  bulletChar: Schema.optionalKey(BulletChar),
24
55
  emphasisChar: Schema.optionalKey(EmphasisChar),
25
56
  fenceChar: Schema.optionalKey(FenceChar),
26
- thematicBreakChar: Schema.optionalKey(ThematicBreakChar)
57
+ thematicBreakChar: Schema.optionalKey(ThematicBreakChar),
58
+ codeBlockStyle: Schema.optionalKey(CodeBlockStyle)
27
59
  }) {};
28
60
  /**
29
61
  * Error codes `MarkdownFormat.modify` can fail with.
@@ -193,6 +225,51 @@ const formatFence = (source, emit, node, target) => {
193
225
  while (end - closeLength > start + openLength && source[end - closeLength - 1] === fidelity) closeLength++;
194
226
  if (closeLength >= 3) emit.push(node, end - closeLength, closeLength, target.repeat(newLength));
195
227
  };
228
+ /** A line that is empty or holds only spaces and tabs. */
229
+ const BLANK_LINE = /^[ \t]*$/;
230
+ /** Whether a sibling is a code block with no info string (either spelling). */
231
+ const isLanguagelessCode = (node) => node.type === "code" && node.lang === void 0 && node.meta === void 0;
232
+ /**
233
+ * Convert a language-less code block toward the requested style. Returns
234
+ * whether a whole-block conversion edit was emitted, so the caller can keep
235
+ * `formatFence` off a span this conversion already rewrote.
236
+ *
237
+ * Both directions are whole-block rewrites, so both are restricted to
238
+ * root-level, flush-left blocks — a container's continuation-line prefix
239
+ * (`> `, list indentation) is not reproducible by a single splice. The
240
+ * indented direction carries the re-parse hazards an indented block is
241
+ * subject to and skips each one: it cannot follow a non-blank line (lazy
242
+ * paragraph continuation), a list or footnote definition absorbs it as item
243
+ * continuation, an adjacent language-less code block would merge with it
244
+ * across the blank line, and an empty value or one with blank first/last
245
+ * lines (or interior whitespace-only lines, which blankness would erase) has
246
+ * no indented spelling at all.
247
+ */
248
+ const formatCodeBlockStyle = (source, emit, node, parent, siblings, index, target, fenceChar) => {
249
+ if (node.lang !== void 0 || node.meta !== void 0) return false;
250
+ if (parent === void 0 || parent.type !== "root") return false;
251
+ const { start, end } = spanOf(node);
252
+ if (lineStartOf(source, start) !== start) return false;
253
+ const value = node.value.endsWith("\n") ? node.value.slice(0, -1) : node.value;
254
+ if (target === "fenced") {
255
+ if (node.fenceChar !== void 0) return false;
256
+ const char = fenceChar ?? "`";
257
+ const fence = char.repeat(Math.max(3, longestRun(value, char) + 1));
258
+ emit.push(node, start, end - start, `${fence}\n${value}\n${fence}`);
259
+ return true;
260
+ }
261
+ if (node.fenceChar === void 0) return false;
262
+ const lines = value === "" ? [] : value.split("\n");
263
+ if (lines.length === 0 || BLANK_LINE.test(lines[0]) || BLANK_LINE.test(lines[lines.length - 1])) return false;
264
+ if (lines.some((line) => line !== "" && BLANK_LINE.test(line))) return false;
265
+ if (!previousLineBlank(source, start)) return false;
266
+ const previous = index > 0 ? siblings[index - 1] : void 0;
267
+ const next = index + 1 < siblings.length ? siblings[index + 1] : void 0;
268
+ if (previous !== void 0 && (previous.type === "list" || previous.type === "footnoteDefinition")) return false;
269
+ if (previous !== void 0 && isLanguagelessCode(previous) || next !== void 0 && isLanguagelessCode(next)) return false;
270
+ emit.push(node, start, end - start, lines.map((line) => line === "" ? "" : ` ${line}`).join("\n"));
271
+ return true;
272
+ };
196
273
  const FLOW_TYPES = /* @__PURE__ */ new Set([
197
274
  "blockquote",
198
275
  "code",
@@ -279,12 +356,18 @@ var MarkdownFormat = class MarkdownFormat {
279
356
  /**
280
357
  * Compute marker-normalization edits per the requested
281
358
  * {@link MarkdownFormattingOptions}: heading style, bullet character,
282
- * emphasis/strong marker, fence character and thematic-break character,
283
- * each only where the node's concrete syntax differs from the target and
284
- * the rewrite is provably safe. Content is never touched. `range`
285
- * restricts edits to the nodes intersecting it (the owning-node
286
- * intersection posture, matching toml). Non-mutating — apply with
287
- * `MarkdownEdit.applyAll` (or use {@link MarkdownFormat.formatToString}).
359
+ * emphasis/strong marker, fence character, thematic-break character and
360
+ * code-block style, each only where the node's concrete syntax differs
361
+ * from the target and the rewrite is provably safe. Content is never
362
+ * touched. `range` restricts edits to the nodes intersecting it (the
363
+ * owning-node intersection posture, matching toml). Non-mutating — apply
364
+ * with `MarkdownEdit.applyAll` (or use
365
+ * {@link MarkdownFormat.formatToString}).
366
+ *
367
+ * Note the code-block default: absent `codeBlockStyle`, a language-less
368
+ * code block keeps whichever spelling it has — `fenceChar` alone
369
+ * normalizes existing fences and deliberately leaves indented blocks
370
+ * indented.
288
371
  */
289
372
  static format(text, range, options) {
290
373
  const parsed = Markdown.parseResult(text, MarkdownParseOptions.make({
@@ -293,12 +376,14 @@ var MarkdownFormat = class MarkdownFormat {
293
376
  }));
294
377
  if (Result.isFailure(parsed)) return [];
295
378
  const emit = new FormatEmitter(text);
296
- walk(parsed.success, void 0, [], 0, (node, _parent, siblings, index) => {
379
+ walk(parsed.success, void 0, [], 0, (node, parent, siblings, index) => {
297
380
  if (options?.thematicBreakChar !== void 0 && node.type === "thematicBreak") formatThematicBreak(text, emit, node, options.thematicBreakChar);
298
381
  if (options?.headingStyle !== void 0 && node.type === "heading") formatHeading(text, emit, node, options.headingStyle);
299
382
  if (options?.bulletChar !== void 0 && node.type === "list") formatList(text, emit, node, siblings, index, options.bulletChar);
300
383
  if (options?.emphasisChar !== void 0 && isEmphasisLike(node)) formatEmphasis(text, emit, node, siblings, index, options.emphasisChar);
301
- if (options?.fenceChar !== void 0 && node.type === "code") formatFence(text, emit, node, options.fenceChar);
384
+ if (node.type === "code") {
385
+ if (!(options?.codeBlockStyle !== void 0 && formatCodeBlockStyle(text, emit, node, parent, siblings, index, options.codeBlockStyle, options.fenceChar)) && options?.fenceChar !== void 0) formatFence(text, emit, node, options.fenceChar);
386
+ }
302
387
  });
303
388
  return (range === void 0 ? emit.edits : emit.edits.filter((edit) => Math.max(edit.nodeStart, range.offset) <= Math.min(edit.nodeEnd, range.offset + range.length))).map((edit) => MarkdownEdit.make({
304
389
  offset: edit.offset,
@@ -377,4 +462,4 @@ var MarkdownFormat = class MarkdownFormat {
377
462
  };
378
463
 
379
464
  //#endregion
380
- export { MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode };
465
+ export { CodeBlockStyle, MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode };
package/MarkdownNode.js CHANGED
@@ -347,6 +347,12 @@ var ThematicBreak = class extends Schema.Class("ThematicBreak")({
347
347
  * for fenced blocks and **absent for indented blocks** — their absence is how
348
348
  * the two are told apart on the way back out.
349
349
  *
350
+ * A consequence worth knowing before it surprises: a language-less `Code`
351
+ * node with no explicit `fenceChar` **serializes as an indented block**
352
+ * through `Markdown.stringify`. To get a fence, set `fenceChar` on the node
353
+ * (or a `lang`, which forces one), or format the emitted source with
354
+ * `MarkdownFormattingOptions.codeBlockStyle: "fenced"`.
355
+ *
350
356
  * @public
351
357
  */
352
358
  var Code = class extends Schema.Class("Code")({
@@ -454,7 +460,7 @@ var List = class extends Schema.Class("List")({
454
460
  ordered: Schema.optionalKey(Schema.Boolean),
455
461
  start: Schema.optionalKey(Schema.Number),
456
462
  spread: Schema.optionalKey(Schema.Boolean),
457
- children: Schema.Array(Schema.suspend(() => ListItem)),
463
+ children: Schema.Array(Schema.suspend(() => ListContent)),
458
464
  position: NodePosition,
459
465
  bulletChar: Schema.optionalKey(BulletChar),
460
466
  delimiter: Schema.optionalKey(ListDelimiter)
@@ -493,9 +499,19 @@ var TableCell = class extends Schema.Class("TableCell")({
493
499
  * content — the cells in a {@link TableRow}. A one-member union, kept because
494
500
  * mdast names the category.
495
501
  *
502
+ * A REAL `Schema.Union`, not a bare suspended class reference, and the
503
+ * wrapper is load-bearing: `make` passes an already-constructed class
504
+ * instance through a union member untouched, while a class-typed field
505
+ * re-runs construction on every element of the array. On a 30k-row table
506
+ * that re-construction was 1137ms for the single `Table.make` call against
507
+ * 9ms through the union (measured; the pathological suite's "tables" case is
508
+ * the regression instrument). The `children` fields of `TableRow`, `Table`
509
+ * and `List` point at these category unions for exactly that reason — do not
510
+ * "simplify" them back to the member class.
511
+ *
496
512
  * @public
497
513
  */
498
- const RowContent = Schema.suspend(() => TableCell);
514
+ const RowContent = Schema.suspend(() => Schema.Union([TableCell]));
499
515
  /**
500
516
  * TableRow — one row of a {@link Table}: the labels of the columns if it is
501
517
  * the table's first row, a data row otherwise.
@@ -504,17 +520,18 @@ const RowContent = Schema.suspend(() => TableCell);
504
520
  */
505
521
  var TableRow = class extends Schema.Class("TableRow")({
506
522
  type: Schema.tag("tableRow"),
507
- children: Schema.Array(Schema.suspend(() => TableCell)),
523
+ children: Schema.Array(Schema.suspend(() => RowContent)),
508
524
  position: NodePosition
509
525
  }) {};
510
526
  /**
511
527
  * The union of every node that may appear where mdast expects **table**
512
528
  * content — the rows in a {@link Table}. A one-member union, kept because
513
- * mdast names the category.
529
+ * mdast names the category — and a real `Schema.Union` for the construction
530
+ * pass-through documented on `RowContent`.
514
531
  *
515
532
  * @public
516
533
  */
517
- const TableContent = Schema.suspend(() => TableRow);
534
+ const TableContent = Schema.suspend(() => Schema.Union([TableRow]));
518
535
  /**
519
536
  * Table — GFM two-dimensional data.
520
537
  *
@@ -528,7 +545,7 @@ const TableContent = Schema.suspend(() => TableRow);
528
545
  var Table = class extends Schema.Class("Table")({
529
546
  type: Schema.tag("table"),
530
547
  align: Schema.optionalKey(Schema.Array(Schema.NullOr(TableAlign))),
531
- children: Schema.Array(Schema.suspend(() => TableRow)),
548
+ children: Schema.Array(Schema.suspend(() => TableContent)),
532
549
  position: NodePosition
533
550
  }) {};
534
551
  /**
@@ -557,11 +574,12 @@ const FlowContent = Schema.suspend(() => Schema.Union([
557
574
  /**
558
575
  * The union of every node that may appear where mdast expects **list**
559
576
  * content. A one-member union, kept because mdast names the category and
560
- * later dialects widen it.
577
+ * later dialects widen it — and a real `Schema.Union` for the construction
578
+ * pass-through documented on `RowContent`.
561
579
  *
562
580
  * @public
563
581
  */
564
- const ListContent = Schema.suspend(() => ListItem);
582
+ const ListContent = Schema.suspend(() => Schema.Union([ListItem]));
565
583
  /**
566
584
  * The frontmatter formats the capture recognizes, keyed by their opening
567
585
  * fence: `---` is yaml, `+++` is toml and `---json` is json.
package/README.md CHANGED
@@ -106,7 +106,7 @@ Effect.runPromise(program).then(console.log);
106
106
 
107
107
  `find` walks the tree in document pre-order and returns the document's own node, so the match feeds `modify` by identity. Replacements are literal strings or node fragments, and both render through the canonical stringifier, so a modified document re-parses cleanly by construction — you cannot splice in raw markdown that reopens a fence or breaks a table.
108
108
 
109
- `MarkdownFormat.format` handles the other half: conservative marker normalization, never content rewriting. It converts heading style, bullet character, emphasis marker, fence character and thematic-break character, and skips any conversion that would not be safe rather than attempting it cleverly:
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
 
111
111
  ```ts
112
112
  import { MarkdownFormat, MarkdownFormattingOptions } from "@effected/markdown";
@@ -136,6 +136,8 @@ console.log(MarkdownFormat.format(source, undefined, options));
136
136
 
137
137
  `format` is pure and total, and the edits are non-mutating data — hand them to `MarkdownEdit.applyAll`, or send them to an editor as a text-edit payload.
138
138
 
139
+ `codeBlockStyle` is the option to know about before you hit its default: absent it, a language-less code block keeps whichever spelling it already has, and a language-less `Code` node with no `fenceChar` serializes as an *indented* block. `"fenced"` rewrites indented blocks to fences, `"indented"` goes the other way where the result provably re-parses the same, and both directions apply to root-level flush-left blocks only. A block with a language has no indented spelling and is never touched.
140
+
139
141
  ## Frontmatter
140
142
 
141
143
  Frontmatter capture is opt-in, because enabling it changes how a document opening with `---` parses: CommonMark reads `---\ntitle: x\n---` as a thematic break and a setext heading, and that spec-conformant reading holds unless you ask for something else. Turn it on and compose your schema with a codec for typed gray-matter parity:
@@ -166,6 +168,8 @@ Effect.runPromise(program).then(console.log);
166
168
 
167
169
  Each stage fails typed and separately: no capture is a `FrontmatterMissingError` (catch the tag for optional semantics), a codec handed the wrong fence is a `FrontmatterFormatMismatchError`, unparseable content is a `FrontmatterDecodeError` carrying the format package's own positioned failure structurally, and schema-invalid data is a `FrontmatterValidationError` carrying the structured issue tree rather than a stringified rendering.
168
170
 
171
+ Because capture is opt-in, "there is no frontmatter" has two meanings, and `FrontmatterMissingError.reason` tells them apart: `"absent"` when the source genuinely has no block, `"captureDisabled"` when it opens with one but was parsed with the toggle off — where the fix is a parse option, not a document edit. `MarkdownDocument.hasFrontmatterBlock` answers the same question directly, by re-running the parser's own pre-scan, so the accessor and the error can never disagree.
172
+
169
173
  The three codecs — `YamlFrontmatter` (`---`), `TomlFrontmatter` (`+++`) and `JsonFrontmatter` (`---json`) — are free-standing named exports, one module each, deliberately never collected into a namespace object. Naming one codec is what pulls in its format engine, so a JSON-frontmatter consumer never pays for the yaml parser.
170
174
 
171
175
  Frontmatter blocks can also describe their own schema. `SchemaResolver.classify` sorts a `$schema` value into a tagged union — `ByUrl`, `ByPath`, `Inline` and `ByName` — and `SchemaResolver.fromRegistry` resolves `ByName` declarations like `skill@2.1.0` against schemas you register. URLs, paths and inline documents are carried as data and never fetched: this is a pure package and it performs no IO.
@@ -192,7 +196,33 @@ Effect.runPromise(program).then(console.log);
192
196
  // [ "./CHANGELOG.md", "https://example.com/docs" ]
193
197
  ```
194
198
 
195
- `headings` lists every heading wherever it sits, including inside blockquotes and list items. `sections` are delimited by root-level headings only, and each section's range spans its subsections, so the edit layer can splice a whole section out in one edit.
199
+ `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:
200
+
201
+ ```ts
202
+ import { MarkdownDocument } from "@effected/markdown";
203
+ import { Effect } from "effect";
204
+
205
+ const source = `# Changelog
206
+
207
+ ## 1.2.3
208
+
209
+ Fixed a bug.
210
+
211
+ ## 1.2.2
212
+
213
+ Initial release.
214
+ `;
215
+
216
+ const program = Effect.gen(function* () {
217
+ const doc = yield* MarkdownDocument.parse(source);
218
+ return doc.sectionByHeading("1.2.3", { depth: 2 })?.body.trim();
219
+ });
220
+
221
+ Effect.runPromise(program).then(console.log);
222
+ // Fixed a bug.
223
+ ```
224
+
225
+ `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.
196
226
 
197
227
  For anything the accessors do not cover, `find` and `findAll` walk the whole tree in document pre-order. A string selector matches the node's `type` and narrows the result — `doc.findAll("heading")` is `ReadonlyArray<Heading>`, `doc.find("table")` is `Table | undefined` — and a type-guard predicate narrows the same way. The nodes come back by identity, so `doc.findAll("heading")[1]` addresses the second heading for `MarkdownFormat.modify` without raw child indexing.
198
228
 
@@ -234,13 +264,13 @@ if (Result.isSuccess(parsed)) {
234
264
  ## Features
235
265
 
236
266
  - `Markdown` — `parse`/`stringify` as `Effect`s with typed `MarkdownParseError`/`MarkdownStringifyError` channels, the pure `parseResult`/`stringifyResult` twins for synchronous callers, and the `MarkdownFromString` two-way codec.
237
- - `MarkdownDocument` — source, tree, diagnostics and the link-definition index, plus the derived `headings`, `sections` and `links` accessors, the `find`/`findAll` tree queries over type-narrowed selectors, and the `frontmatter` capture.
267
+ - `MarkdownDocument` — source, tree, diagnostics and the link-definition index, plus the derived `headings`, `sections` and `links` accessors, the `firstSection`/`sectionByHeading` finders over a `DocumentSection` (heading, depth, range, body), the `find`/`findAll` tree queries over type-narrowed selectors, and the `frontmatter` capture with `hasFrontmatterBlock` to tell an absent block from an uncaptured one.
238
268
  - The mdast-shaped node classes — the CommonMark types plus GFM's `delete`, `table`, `tableRow`, `tableCell`, `footnoteDefinition`, `footnoteReference` and task-list `checked`, each carrying unist positions with byte offsets and this package's fidelity fields. `position` defaults to the zero-width synthetic sentinel, so a replacement fragment constructs in one line — `Text.make({ value: "shipped" })`.
239
269
  - `MarkdownEdit` / `MarkdownRange` (with `applyAll`) — the non-mutating text-edit vocabulary, field-identical to `@effected/jsonc`'s, `@effected/yaml`'s and `@effected/toml`'s.
240
270
  - `MarkdownFormat` — `format`/`formatToString` compute conservative marker-normalization edits; `modify`/`modifyToString` replace a node by identity through the canonical stringifier.
241
271
  - `MarkdownVisitor` — walk a parsed tree as a lazy `Stream` of `Enter`/`Exit` events with child-index paths and depth.
242
272
  - `Mdast` — `toMdast` projects to plain mdast JSON; `fromMdast`/`fromMdastResult` admit a foreign mdast tree with validation.
243
- - `MarkdownFrontmatter.schema` plus the `YamlFrontmatter`, `TomlFrontmatter` and `JsonFrontmatter` codecs — typed frontmatter decoding over optional per-format peers, with four separately catchable failure modes.
273
+ - `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.
244
274
  - `SchemaResolver` — classify a frontmatter `$schema` declaration into `ByUrl`/`ByPath`/`Inline`/`ByName` and resolve names against a registry, with no IO and no dependencies.
245
275
  - `MarkdownDiagnostic` — the structured diagnostic (`code`, `message`, `offset`, `length`, `line`, `character`) every typed error carries, shaped identically to the sibling packages'.
246
276
 
package/index.d.ts CHANGED
@@ -388,6 +388,12 @@ declare const Code_base: Schema.Class<Code, Schema.Struct<{
388
388
  * for fenced blocks and **absent for indented blocks** — their absence is how
389
389
  * the two are told apart on the way back out.
390
390
  *
391
+ * A consequence worth knowing before it surprises: a language-less `Code`
392
+ * node with no explicit `fenceChar` **serializes as an indented block**
393
+ * through `Markdown.stringify`. To get a fence, set `fenceChar` on the node
394
+ * (or a `lang`, which forces one), or format the emitted source with
395
+ * `MarkdownFormattingOptions.codeBlockStyle: "fenced"`.
396
+ *
391
397
  * @public
392
398
  */
393
399
  declare class Code extends Code_base {}
@@ -534,6 +540,16 @@ declare class TableCell extends TableCell_base {}
534
540
  * content — the cells in a {@link TableRow}. A one-member union, kept because
535
541
  * mdast names the category.
536
542
  *
543
+ * A REAL `Schema.Union`, not a bare suspended class reference, and the
544
+ * wrapper is load-bearing: `make` passes an already-constructed class
545
+ * instance through a union member untouched, while a class-typed field
546
+ * re-runs construction on every element of the array. On a 30k-row table
547
+ * that re-construction was 1137ms for the single `Table.make` call against
548
+ * 9ms through the union (measured; the pathological suite's "tables" case is
549
+ * the regression instrument). The `children` fields of `TableRow`, `Table`
550
+ * and `List` point at these category unions for exactly that reason — do not
551
+ * "simplify" them back to the member class.
552
+ *
537
553
  * @public
538
554
  */
539
555
  declare const RowContent: Schema.Codec<RowContent>;
@@ -558,7 +574,8 @@ declare class TableRow extends TableRow_base {}
558
574
  /**
559
575
  * The union of every node that may appear where mdast expects **table**
560
576
  * content — the rows in a {@link Table}. A one-member union, kept because
561
- * mdast names the category.
577
+ * mdast names the category — and a real `Schema.Union` for the construction
578
+ * pass-through documented on `RowContent`.
562
579
  *
563
580
  * @public
564
581
  */
@@ -609,7 +626,8 @@ type FlowContent = Blockquote | Code | Definition | FootnoteDefinition | Heading
609
626
  /**
610
627
  * The union of every node that may appear where mdast expects **list**
611
628
  * content. A one-member union, kept because mdast names the category and
612
- * later dialects widen it.
629
+ * later dialects widen it — and a real `Schema.Union` for the construction
630
+ * pass-through documented on `RowContent`.
613
631
  *
614
632
  * @public
615
633
  */
@@ -808,7 +826,9 @@ declare const MarkdownParseOptions_base: Schema.Class<MarkdownParseOptions, Sche
808
826
  * spec-conformant reading must hold unless a consumer opts in. Enabled, the
809
827
  * engine captures `---`/`+++`/`---json` blocks at offset 0 into a raw
810
828
  * `Frontmatter` head node — decoding it is the frontmatter codec modules'
811
- * job.
829
+ * job. Parsed with the toggle off, such a document reports no frontmatter —
830
+ * `MarkdownDocument.hasFrontmatterBlock` tells "no block in the source"
831
+ * apart from "parsed with capture off".
812
832
  *
813
833
  * @public
814
834
  */
@@ -917,10 +937,14 @@ declare class Markdown {
917
937
  * @remarks
918
938
  * Canonical serialization: fidelity fields (marker characters, fence
919
939
  * style, heading spelling) win when present; documented canonical
920
- * defaults apply when absent. The output re-parses to a render-equivalent
921
- * document — the corpus-pinned contract — but is not a byte-level
922
- * round-trip of any original source; surgical editing goes through the
923
- * offset-splice edit layer instead.
940
+ * defaults apply when absent. One default surprises: a language-less
941
+ * `Code` node with no explicit `fenceChar` serializes as an
942
+ * **indented** code block — set `fenceChar` (or a `lang`) on the node for
943
+ * a fence, or normalize afterward with
944
+ * `MarkdownFormattingOptions.codeBlockStyle: "fenced"`. The output
945
+ * re-parses to a render-equivalent document — the corpus-pinned
946
+ * contract — but is not a byte-level round-trip of any original source;
947
+ * surgical editing goes through the offset-splice edit layer instead.
924
948
  *
925
949
  * Failure is rare by design, symmetric with parse: only a hardening-guard
926
950
  * trip on a tree nesting past the depth cap fails, and only synthesized
@@ -1202,9 +1226,32 @@ declare class MarkdownDocument extends MarkdownDocument_base {
1202
1226
  * only ever sit at the head of `root.children` (the capture fires at most
1203
1227
  * once, at offset 0), so the tree is the single source of truth and the
1204
1228
  * accessor can never disagree with it. `undefined` covers both a document
1205
- * with no frontmatter block and one parsed with the capture toggle off.
1229
+ * with no frontmatter block and one parsed with the capture toggle off —
1230
+ * {@link MarkdownDocument.hasFrontmatterBlock} tells the two apart.
1206
1231
  */
1207
1232
  get frontmatter(): Frontmatter | undefined;
1233
+ /**
1234
+ * Whether the source opens with a well-formed frontmatter block —
1235
+ * regardless of how the document was parsed.
1236
+ *
1237
+ * @remarks
1238
+ * The structural signal that disambiguates the `frontmatter` accessor's
1239
+ * `undefined`: capture is opt-in (`MarkdownParseOptions.frontmatter`
1240
+ * defaults off), so a document whose source visibly starts with `---` can
1241
+ * still carry no capture node — spec-correct CommonMark reads the fences as
1242
+ * a thematic break plus content. `frontmatter === undefined` with
1243
+ * `hasFrontmatterBlock === true` means exactly "parsed with capture off";
1244
+ * with `false` it means the source genuinely has no block.
1245
+ *
1246
+ * Derived, not stored: the getter runs the **same** pre-scan the parser
1247
+ * runs when capture is enabled (the closed fence grammar — `---` yaml,
1248
+ * `+++` toml, `---json` json; an unclosed fence is not frontmatter), so it
1249
+ * is `true` exactly when parsing this source with `frontmatter: true`
1250
+ * would produce a capture node, and the two can never drift. Like the
1251
+ * other navigation accessors it recomputes per access — bind it once when
1252
+ * checking repeatedly.
1253
+ */
1254
+ get hasFrontmatterBlock(): boolean;
1208
1255
  /**
1209
1256
  * Every heading in the document, in document order, wherever it sits —
1210
1257
  * including headings nested inside blockquotes and list items.
@@ -1441,18 +1488,46 @@ interface FrontmatterCodec {
1441
1488
  /** Serialize data into a frontmatter body, without fences. */
1442
1489
  readonly encode: (data: unknown) => Effect.Effect<string, FrontmatterEncodeError>;
1443
1490
  }
1444
- declare const FrontmatterMissingError_base: Schema.Class<FrontmatterMissingError, Schema.TaggedStruct<"FrontmatterMissingError", {}>, import("effect/Cause").YieldableError>;
1491
+ /**
1492
+ * Why a frontmatter decoder found no capture on a document.
1493
+ *
1494
+ * @remarks
1495
+ * `"absent"` — the source genuinely has no frontmatter block.
1496
+ * `"captureDisabled"` — the source opens with a well-formed frontmatter
1497
+ * block, but the document carries no capture node: it was parsed without
1498
+ * `frontmatter: true` (the toggle defaults off), so spec-correct CommonMark
1499
+ * read the fences as ordinary content.
1500
+ *
1501
+ * @public
1502
+ */
1503
+ declare const FrontmatterMissingReason: Schema.Literals<readonly ["absent", "captureDisabled"]>;
1504
+ /**
1505
+ * Why a frontmatter decoder found no capture on a document.
1506
+ *
1507
+ * @public
1508
+ */
1509
+ type FrontmatterMissingReason = typeof FrontmatterMissingReason.Type;
1510
+ declare const FrontmatterMissingError_base: Schema.Class<FrontmatterMissingError, Schema.TaggedStruct<"FrontmatterMissingError", {
1511
+ /** Why there is no capture: no block at all, or capture left off. */
1512
+ readonly reason: Schema.Literals<readonly ["absent", "captureDisabled"]>;
1513
+ }>, import("effect/Cause").YieldableError>;
1445
1514
  /**
1446
1515
  * Indicates that a document handed to a frontmatter decoder carries no
1447
1516
  * frontmatter capture.
1448
1517
  *
1449
1518
  * @remarks
1450
- * Raised when the document genuinely has no frontmatter block — including
1451
- * when it has one in the source but was parsed with the capture toggle off
1452
- * (`MarkdownParseOptions.frontmatter` defaults to `false`). Cause-free: there
1453
- * is nothing to diagnose beyond the absence itself. Consumers wanting
1454
- * optional semantics can `Effect.catchTag("FrontmatterMissingError", ...)`
1455
- * to a default.
1519
+ * `reason` distinguishes the two ways this happens: `"absent"` when the
1520
+ * source genuinely has no frontmatter block, `"captureDisabled"` when the
1521
+ * source opens with one but the document was parsed with the capture toggle
1522
+ * off (`MarkdownParseOptions.frontmatter` defaults to `false`) — the fix
1523
+ * there is parsing with `frontmatter: true`, not editing the document. The
1524
+ * discriminant is computed from the same structural signal
1525
+ * `MarkdownDocument.hasFrontmatterBlock` exposes, so the error and the
1526
+ * accessor can never disagree.
1527
+ *
1528
+ * Cause-free: there is nothing to diagnose beyond the absence itself.
1529
+ * Consumers wanting optional semantics can
1530
+ * `Effect.catchTag("FrontmatterMissingError", ...)` to a default.
1456
1531
  *
1457
1532
  * @public
1458
1533
  */
@@ -1516,7 +1591,8 @@ declare class MarkdownFrontmatter {
1516
1591
  * The decoder reads the document's frontmatter capture (parse with
1517
1592
  * `frontmatter: true` — the toggle defaults off), decodes its raw value
1518
1593
  * through the codec, then validates the data against `schema`. Each stage
1519
- * fails typed: no capture is {@link FrontmatterMissingError}, a
1594
+ * fails typed: no capture is {@link FrontmatterMissingError} (its `reason`
1595
+ * says whether the block is genuinely absent or capture was left off), a
1520
1596
  * wrong-format codec is {@link FrontmatterFormatMismatchError}, unparseable
1521
1597
  * content is {@link FrontmatterDecodeError}, and schema-invalid data is
1522
1598
  * {@link FrontmatterValidationError} carrying the structured issue.
@@ -1866,6 +1942,25 @@ type MarkdownRangeLike = MarkdownRange | {
1866
1942
  readonly offset: number;
1867
1943
  readonly length: number;
1868
1944
  };
1945
+ /**
1946
+ * The two ways CommonMark spells a code block: `fenced` (a backtick or tilde
1947
+ * fence) and `indented` (four-space indentation).
1948
+ *
1949
+ * @remarks
1950
+ * A formatting target for {@link MarkdownFormattingOptions}, not a node
1951
+ * fidelity field: on a {@link Code} node the two spellings are told apart by
1952
+ * the presence or absence of `fenceChar`/`fenceLength`, and a language-less
1953
+ * node with neither serializes as an **indented** block by default.
1954
+ *
1955
+ * @public
1956
+ */
1957
+ declare const CodeBlockStyle: Schema.Literals<readonly ["fenced", "indented"]>;
1958
+ /**
1959
+ * The union of all code-block-style string literals.
1960
+ *
1961
+ * @public
1962
+ */
1963
+ type CodeBlockStyle = typeof CodeBlockStyle.Type;
1869
1964
  declare const MarkdownFormattingOptions_base: Schema.Class<MarkdownFormattingOptions, Schema.Struct<{
1870
1965
  readonly dialect: Schema.optionalKey<Schema.Literals<readonly ["commonmark", "gfm"]>>;
1871
1966
  readonly frontmatter: Schema.optionalKey<Schema.Boolean>;
@@ -1874,6 +1969,7 @@ declare const MarkdownFormattingOptions_base: Schema.Class<MarkdownFormattingOpt
1874
1969
  readonly emphasisChar: Schema.optionalKey<Schema.Literals<readonly ["*", "_"]>>;
1875
1970
  readonly fenceChar: Schema.optionalKey<Schema.Literals<readonly ["`", "~"]>>;
1876
1971
  readonly thematicBreakChar: Schema.optionalKey<Schema.Literals<readonly ["-", "_", "*"]>>;
1972
+ readonly codeBlockStyle: Schema.optionalKey<Schema.Literals<readonly ["fenced", "indented"]>>;
1877
1973
  }>, {}>;
1878
1974
  /**
1879
1975
  * Options controlling formatting: which concrete-syntax markers to normalize,
@@ -1883,7 +1979,25 @@ declare const MarkdownFormattingOptions_base: Schema.Class<MarkdownFormattingOpt
1883
1979
  *
1884
1980
  * Day-one scope is marker normalization only — heading style, bullet
1885
1981
  * character, emphasis/strong marker, fence character, thematic-break
1886
- * character. Content is never rewritten, rewrapped or reflowed.
1982
+ * character, code-block style. Content is never rewritten, rewrapped or
1983
+ * reflowed.
1984
+ *
1985
+ * @remarks
1986
+ * `codeBlockStyle` is the opt-in that converts between the two code-block
1987
+ * spellings for **language-less** blocks (a block with a `lang` has no
1988
+ * indented spelling and is never touched). It exists because the default
1989
+ * surprises: absent this option, a language-less code block keeps its
1990
+ * spelling — and a language-less {@link Code} node with no explicit
1991
+ * `fenceChar` **serializes as an indented block** through
1992
+ * `Markdown.stringify`. `"fenced"` rewrites indented blocks to fences (the
1993
+ * fence character follows `fenceChar` when also set, backtick otherwise);
1994
+ * `"indented"` rewrites language-less fences to indented form where provably
1995
+ * safe. Like every other option, hazardous conversions are skipped, never
1996
+ * attempted: both directions apply to root-level, flush-left blocks only,
1997
+ * and the `"indented"` direction additionally skips blocks whose indented
1998
+ * spelling would lazily continue a paragraph, be absorbed into a preceding
1999
+ * list or footnote definition, merge with an adjacent code block, or fail
2000
+ * representability (empty, or blank first/last lines).
1887
2001
  *
1888
2002
  * @public
1889
2003
  */
@@ -1941,12 +2055,18 @@ declare class MarkdownFormat {
1941
2055
  /**
1942
2056
  * Compute marker-normalization edits per the requested
1943
2057
  * {@link MarkdownFormattingOptions}: heading style, bullet character,
1944
- * emphasis/strong marker, fence character and thematic-break character,
1945
- * each only where the node's concrete syntax differs from the target and
1946
- * the rewrite is provably safe. Content is never touched. `range`
1947
- * restricts edits to the nodes intersecting it (the owning-node
1948
- * intersection posture, matching toml). Non-mutating — apply with
1949
- * `MarkdownEdit.applyAll` (or use {@link MarkdownFormat.formatToString}).
2058
+ * emphasis/strong marker, fence character, thematic-break character and
2059
+ * code-block style, each only where the node's concrete syntax differs
2060
+ * from the target and the rewrite is provably safe. Content is never
2061
+ * touched. `range` restricts edits to the nodes intersecting it (the
2062
+ * owning-node intersection posture, matching toml). Non-mutating — apply
2063
+ * with `MarkdownEdit.applyAll` (or use
2064
+ * {@link MarkdownFormat.formatToString}).
2065
+ *
2066
+ * Note the code-block default: absent `codeBlockStyle`, a language-less
2067
+ * code block keeps whichever spelling it has — `fenceChar` alone
2068
+ * normalizes existing fences and deliberately leaves indented blocks
2069
+ * indented.
1950
2070
  */
1951
2071
  static format(text: string, range?: MarkdownRangeLike, options?: MarkdownFormattingOptions): ReadonlyArray<MarkdownEdit>;
1952
2072
  /**
@@ -2292,5 +2412,5 @@ declare const TomlFrontmatter: FrontmatterCodec;
2292
2412
  */
2293
2413
  declare const YamlFrontmatter: FrontmatterCodec;
2294
2414
  //#endregion
2295
- export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, type DocumentHeading, type DocumentLink, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, type FrontmatterCodec, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, type FrontmatterResolveError, type FrontmatterSchemaError, type FrontmatterSchemaResolver, FrontmatterValidationError, type FrontmatterWriteError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, type LinkBearingNode, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, type MarkdownNodeOfType, type MarkdownNodeType, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, type MarkdownPath, MarkdownRange, type MarkdownRangeLike, type MarkdownSegment, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, type MdastNode, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, type SectionHeadingMatch, type SectionQueryOptions, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
2415
+ export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, type DocumentHeading, type DocumentLink, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, type FrontmatterCodec, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, type FrontmatterResolveError, type FrontmatterSchemaError, type FrontmatterSchemaResolver, FrontmatterValidationError, type FrontmatterWriteError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, type LinkBearingNode, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, type MarkdownNodeOfType, type MarkdownNodeType, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, type MarkdownPath, MarkdownRange, type MarkdownRangeLike, type MarkdownSegment, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, type MdastNode, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, type SectionHeadingMatch, type SectionQueryOptions, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
2296
2416
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import { MarkdownEdit, MarkdownRange } from "./MarkdownEdit.js";
2
2
  import { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterFormat, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, MarkdownNode, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar } from "./MarkdownNode.js";
3
- import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterValidationError, MarkdownFrontmatter } from "./Frontmatter.js";
3
+ import { FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterValidationError, MarkdownFrontmatter } from "./Frontmatter.js";
4
4
  import { SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError } from "./FrontmatterResolver.js";
5
5
  import { JsonFrontmatter } from "./JsonFrontmatter.js";
6
6
  import { MarkdownDiagnostic, MarkdownParseErrorCode } from "./MarkdownDiagnostic.js";
7
7
  import { Markdown, MarkdownDialect, MarkdownParseError, MarkdownParseOptions, MarkdownStringifyError } from "./Markdown.js";
8
8
  import { DocumentSection, MarkdownDocument } from "./MarkdownDocument.js";
9
- import { MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode } from "./MarkdownFormat.js";
9
+ import { CodeBlockStyle, MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode } from "./MarkdownFormat.js";
10
10
  import { MarkdownVisitor, MarkdownVisitorEvent } from "./MarkdownVisitor.js";
11
11
  import { Mdast, MdastDecodeError } from "./Mdast.js";
12
12
  import { TomlFrontmatter } from "./TomlFrontmatter.js";
13
13
  import { YamlFrontmatter } from "./YamlFrontmatter.js";
14
14
 
15
- export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
15
+ export { Blockquote, Break, BreakStyle, BulletChar, Code, CodeBlockStyle, Definition, Delete, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterMissingReason, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
@@ -20,6 +20,22 @@ const reLinkLabel = /^\[(?:[^\\[\]]|\\.){0,1000}\]/s;
20
20
  * between seconds and minutes.
21
21
  */
22
22
  const isWhitespaceCode = (code) => code === 32 || code === 9 || code === 10 || code === 11 || code === 12 || code === 13;
23
+ /**
24
+ * The most `(` a bare link destination may hold open at once.
25
+ *
26
+ * cmark's `MAX_LINK_DEST_PARENS` bound in `manual_scan_link_url`, and the
27
+ * limit the spec explicitly sanctions ("Implementations may impose limits on
28
+ * parentheses nesting to avoid performance issues, but at least three levels
29
+ * of nesting should be supported"). Without it the bare-destination walk is
30
+ * the engine's one quadratic: on `"[a](b".repeat(n)` there is no whitespace
31
+ * and no unbalanced `)` to stop at, so every failed inline-link attempt
32
+ * scanned to the end of the subject — O(n) per `]`, O(n^2) overall. With the
33
+ * cap a failed attempt stops at the 33rd `(`, a constant distance, and the
34
+ * pathological suite's "unclosed links B" runs linear. commonmark.js@0.31.2
35
+ * has no such cap and shares the quadratic; the C reference is the authority
36
+ * here.
37
+ */
38
+ const MAX_LINK_DESTINATION_PARENS = 32;
23
39
  const C_BACKSLASH = 92;
24
40
  const C_COLON = 58;
25
41
  const C_LESSTHAN = 60;
@@ -81,6 +97,7 @@ var ReferenceScanner = class {
81
97
  } else if (code === C_OPEN_PAREN) {
82
98
  this.pos += 1;
83
99
  openParens += 1;
100
+ if (openParens > MAX_LINK_DESTINATION_PARENS) return;
84
101
  } else if (code === C_CLOSE_PAREN) {
85
102
  if (openParens < 1) break;
86
103
  this.pos += 1;
@@ -291,7 +291,6 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
291
291
  case "footnoteReference":
292
292
  out += `[^${escapeLabel(child.label ?? child.identifier)}]`;
293
293
  atLineStart = false;
294
- break;
295
294
  }
296
295
  index += 1;
297
296
  unguard(state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/markdown",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "private": false,
5
5
  "description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
6
6
  "keywords": [