@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 +33 -11
- package/Markdown.js +11 -5
- package/MarkdownDocument.js +29 -6
- package/MarkdownFormat.js +96 -11
- package/MarkdownNode.js +26 -8
- package/README.md +34 -4
- package/index.d.ts +144 -24
- package/index.js +3 -3
- package/internal/references.js +17 -0
- package/internal/stringify.js +0 -1
- package/package.json +1 -1
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
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
|
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}
|
|
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.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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
|
package/MarkdownDocument.js
CHANGED
|
@@ -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
|
|
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
|
|
283
|
-
* each only where the node's concrete syntax differs
|
|
284
|
-
* the rewrite is provably safe. Content is never
|
|
285
|
-
* restricts edits to the nodes intersecting it (the
|
|
286
|
-
* intersection posture, matching toml). Non-mutating — apply
|
|
287
|
-
* `MarkdownEdit.applyAll` (or use
|
|
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,
|
|
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 (
|
|
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(() =>
|
|
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(() =>
|
|
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(() =>
|
|
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
|
|
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.
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
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}
|
|
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
|
|
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
|
|
1945
|
-
* each only where the node's concrete syntax differs
|
|
1946
|
-
* the rewrite is provably safe. Content is never
|
|
1947
|
-
* restricts edits to the nodes intersecting it (the
|
|
1948
|
-
* intersection posture, matching toml). Non-mutating — apply
|
|
1949
|
-
* `MarkdownEdit.applyAll` (or use
|
|
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 };
|
package/internal/references.js
CHANGED
|
@@ -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;
|
package/internal/stringify.js
CHANGED