@effected/markdown 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Frontmatter.js +272 -0
- package/FrontmatterResolver.js +273 -0
- package/JsonFrontmatter.js +46 -0
- package/LICENSE +21 -0
- package/Markdown.js +251 -0
- package/MarkdownDiagnostic.js +85 -0
- package/MarkdownDocument.js +248 -0
- package/MarkdownEdit.js +52 -0
- package/MarkdownFormat.js +380 -0
- package/MarkdownNode.js +641 -0
- package/MarkdownVisitor.js +88 -0
- package/Mdast.js +384 -0
- package/README.md +255 -0
- package/TomlFrontmatter.js +44 -0
- package/YamlFrontmatter.js +45 -0
- package/index.d.ts +2190 -0
- package/index.js +15 -0
- package/internal/blockParser.js +419 -0
- package/internal/blockRegistry.js +90 -0
- package/internal/blockTypes.js +27 -0
- package/internal/blocks/atxHeading.js +54 -0
- package/internal/blocks/blockquote.js +40 -0
- package/internal/blocks/code.js +90 -0
- package/internal/blocks/document.js +17 -0
- package/internal/blocks/fencedCode.js +26 -0
- package/internal/blocks/footnoteDefinition.js +84 -0
- package/internal/blocks/frontmatter.js +56 -0
- package/internal/blocks/htmlBlock.js +72 -0
- package/internal/blocks/indentedCode.js +16 -0
- package/internal/blocks/linkReferenceDefinition.js +72 -0
- package/internal/blocks/list.js +159 -0
- package/internal/blocks/paragraph.js +27 -0
- package/internal/blocks/setextHeading.js +24 -0
- package/internal/blocks/table.js +378 -0
- package/internal/blocks/taskListItem.js +36 -0
- package/internal/blocks/thematicBreak.js +35 -0
- package/internal/carriers.js +42 -0
- package/internal/entities.js +26 -0
- package/internal/entityMap.js +7 -0
- package/internal/htmlTags.js +18 -0
- package/internal/inlineNode.js +54 -0
- package/internal/inlineParser.js +403 -0
- package/internal/inlineRegistry.js +68 -0
- package/internal/inlines/autolink.js +36 -0
- package/internal/inlines/autolinkLiteral.js +374 -0
- package/internal/inlines/codeSpan.js +32 -0
- package/internal/inlines/emphasis.js +79 -0
- package/internal/inlines/entity.js +21 -0
- package/internal/inlines/escape.js +34 -0
- package/internal/inlines/footnoteReference.js +63 -0
- package/internal/inlines/lineBreak.js +25 -0
- package/internal/inlines/link.js +212 -0
- package/internal/inlines/rawHtml.js +27 -0
- package/internal/inlines/strikethrough.js +81 -0
- package/internal/inlines/text.js +22 -0
- package/internal/lineIndex.js +76 -0
- package/internal/patterns.js +26 -0
- package/internal/preprocess.js +58 -0
- package/internal/rawInline.js +57 -0
- package/internal/references.js +160 -0
- package/internal/segments.js +36 -0
- package/internal/stringify.js +519 -0
- package/internal/unescape.js +18 -0
- package/package.json +61 -0
- package/tsdoc-metadata.json +11 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { MarkdownEdit } from "./MarkdownEdit.js";
|
|
2
|
+
import { BulletChar, EmphasisChar, FenceChar, HeadingStyle, Paragraph, Root, Text, ThematicBreakChar } from "./MarkdownNode.js";
|
|
3
|
+
import { Markdown, MarkdownDialect, MarkdownParseOptions } from "./Markdown.js";
|
|
4
|
+
import { Effect, Result, Schema } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/MarkdownFormat.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options controlling formatting: which concrete-syntax markers to normalize,
|
|
9
|
+
* plus the parse knobs (`dialect`, `frontmatter`) the formatter parses the
|
|
10
|
+
* source with (same defaults as `Markdown.parse`). Every marker option is
|
|
11
|
+
* optional and independent; an absent option normalizes nothing.
|
|
12
|
+
*
|
|
13
|
+
* Day-one scope is marker normalization only — heading style, bullet
|
|
14
|
+
* character, emphasis/strong marker, fence character, thematic-break
|
|
15
|
+
* character. Content is never rewritten, rewrapped or reflowed.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
var MarkdownFormattingOptions = class extends Schema.Class("MarkdownFormattingOptions")({
|
|
20
|
+
dialect: Schema.optionalKey(MarkdownDialect),
|
|
21
|
+
frontmatter: Schema.optionalKey(Schema.Boolean),
|
|
22
|
+
headingStyle: Schema.optionalKey(HeadingStyle),
|
|
23
|
+
bulletChar: Schema.optionalKey(BulletChar),
|
|
24
|
+
emphasisChar: Schema.optionalKey(EmphasisChar),
|
|
25
|
+
fenceChar: Schema.optionalKey(FenceChar),
|
|
26
|
+
thematicBreakChar: Schema.optionalKey(ThematicBreakChar)
|
|
27
|
+
}) {};
|
|
28
|
+
/**
|
|
29
|
+
* Error codes `MarkdownFormat.modify` can fail with.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
const MarkdownModificationErrorCode = Schema.Literals([
|
|
34
|
+
"NodeNotInDocument",
|
|
35
|
+
"UnsupportedTarget",
|
|
36
|
+
"FragmentCategoryMismatch",
|
|
37
|
+
"FragmentUnrenderable"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Raised when `MarkdownFormat.modify` cannot perform the requested
|
|
41
|
+
* replacement: the target node is not in the document (`NodeNotInDocument`),
|
|
42
|
+
* the target kind or splice context is outside the day-one scope
|
|
43
|
+
* (`UnsupportedTarget`), the fragment's content category does not fit the
|
|
44
|
+
* target's slot (`FragmentCategoryMismatch`), or the fragment trips the
|
|
45
|
+
* stringifier's hardening guard (`FragmentUnrenderable`). Carries the typed
|
|
46
|
+
* `code` plus the target's `offset`/`length` where known — never a collapsed
|
|
47
|
+
* reason string alone.
|
|
48
|
+
*
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
var MarkdownModificationError = class extends Schema.TaggedErrorClass()("MarkdownModificationError", {
|
|
52
|
+
code: MarkdownModificationErrorCode,
|
|
53
|
+
detail: Schema.String,
|
|
54
|
+
offset: Schema.Number,
|
|
55
|
+
length: Schema.Number
|
|
56
|
+
}) {
|
|
57
|
+
get message() {
|
|
58
|
+
return `Markdown modification failed: ${this.code} ${this.detail}`;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const childrenOf = (node) => node.children ?? [];
|
|
62
|
+
/** Walk the tree, invoking `visit` with each node, its parent and its siblings. */
|
|
63
|
+
const walk = (node, parent, siblings, index, visit) => {
|
|
64
|
+
visit(node, parent, siblings, index);
|
|
65
|
+
const children = childrenOf(node);
|
|
66
|
+
for (let i = 0; i < children.length; i++) walk(children[i], node, children, i, visit);
|
|
67
|
+
};
|
|
68
|
+
const spanOf = (node) => ({
|
|
69
|
+
start: node.position.start.offset,
|
|
70
|
+
end: node.position.end.offset
|
|
71
|
+
});
|
|
72
|
+
const ALPHANUMERIC = /[\p{L}\p{N}]/u;
|
|
73
|
+
/** The start offset of the line containing `offset`. */
|
|
74
|
+
const lineStartOf = (source, offset) => {
|
|
75
|
+
let i = offset;
|
|
76
|
+
while (i > 0 && source.charCodeAt(i - 1) !== 10) i--;
|
|
77
|
+
return i;
|
|
78
|
+
};
|
|
79
|
+
/** True when the line before the one containing `offset` is blank or absent. */
|
|
80
|
+
const previousLineBlank = (source, offset) => {
|
|
81
|
+
const lineStart = lineStartOf(source, offset);
|
|
82
|
+
if (lineStart === 0) return true;
|
|
83
|
+
let i = lineStart - 1;
|
|
84
|
+
if (i > 0 && source.charCodeAt(i - 1) === 13) i--;
|
|
85
|
+
const prevStart = lineStartOf(source, i);
|
|
86
|
+
for (let j = prevStart; j < i; j++) {
|
|
87
|
+
const code = source.charCodeAt(j);
|
|
88
|
+
if (code !== 32 && code !== 9) return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
/** Content that cannot stand as a setext heading's text line. */
|
|
93
|
+
const SETEXT_CONTENT_HAZARD = /^(?:[-+*>#=]|\d{1,9}[.)](?:\s|$)|`{3}|~{3}|\s)/;
|
|
94
|
+
/** The longest run of `char` anywhere in `value`. */
|
|
95
|
+
const longestRun = (value, char) => {
|
|
96
|
+
let longest = 0;
|
|
97
|
+
let current = 0;
|
|
98
|
+
for (const ch of value) {
|
|
99
|
+
current = ch === char ? current + 1 : 0;
|
|
100
|
+
if (current > longest) longest = current;
|
|
101
|
+
}
|
|
102
|
+
return longest;
|
|
103
|
+
};
|
|
104
|
+
const isUnorderedList = (node) => node.type === "list" && node.ordered !== true;
|
|
105
|
+
/** True when only blank text separates the two spans. */
|
|
106
|
+
const adjacentSpans = (source, endOfFirst, startOfSecond) => {
|
|
107
|
+
for (let i = endOfFirst; i < startOfSecond; i++) {
|
|
108
|
+
const code = source.charCodeAt(i);
|
|
109
|
+
if (code !== 32 && code !== 9 && code !== 10 && code !== 13) return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
113
|
+
/** Accumulates format edits; drops no-op splices to keep format idempotent. */
|
|
114
|
+
var FormatEmitter = class {
|
|
115
|
+
source;
|
|
116
|
+
edits = [];
|
|
117
|
+
constructor(source) {
|
|
118
|
+
this.source = source;
|
|
119
|
+
}
|
|
120
|
+
push(node, offset, length, content) {
|
|
121
|
+
if (this.source.slice(offset, offset + length) !== content) {
|
|
122
|
+
const { start, end } = spanOf(node);
|
|
123
|
+
this.edits.push({
|
|
124
|
+
offset,
|
|
125
|
+
length,
|
|
126
|
+
content,
|
|
127
|
+
nodeStart: start,
|
|
128
|
+
nodeEnd: end
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const formatThematicBreak = (source, emit, node, target) => {
|
|
134
|
+
if ((node.markerChar ?? "*") === target) return;
|
|
135
|
+
const { start, end } = spanOf(node);
|
|
136
|
+
if (target === "-" && !previousLineBlank(source, start)) return;
|
|
137
|
+
emit.push(node, start, end - start, target.repeat(3));
|
|
138
|
+
};
|
|
139
|
+
const formatHeading = (source, emit, node, target) => {
|
|
140
|
+
if ((node.headingStyle ?? "atx") === target || node.children.length === 0) return;
|
|
141
|
+
const { start, end } = spanOf(node);
|
|
142
|
+
const contentStart = node.children[0].position.start.offset;
|
|
143
|
+
const contentEnd = node.children[node.children.length - 1].position.end.offset;
|
|
144
|
+
const content = source.slice(contentStart, contentEnd);
|
|
145
|
+
if (content.includes("\n") || content.includes("\r")) return;
|
|
146
|
+
if (target === "atx") {
|
|
147
|
+
emit.push(node, start, end - start, `${"#".repeat(node.depth)} ${content}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (node.depth > 2 || content.length === 0 || SETEXT_CONTENT_HAZARD.test(content)) return;
|
|
151
|
+
if (start !== 0 && source.charCodeAt(start - 1) !== 10) return;
|
|
152
|
+
const underline = (node.depth === 1 ? "=" : "-").repeat(Math.max(3, content.length));
|
|
153
|
+
emit.push(node, start, end - start, `${content}\n${underline}`);
|
|
154
|
+
};
|
|
155
|
+
const formatList = (source, emit, node, siblings, index, target) => {
|
|
156
|
+
if (!isUnorderedList(node) || (node.bulletChar ?? "-") === target) return;
|
|
157
|
+
const previous = index > 0 ? siblings[index - 1] : void 0;
|
|
158
|
+
const next = index + 1 < siblings.length ? siblings[index + 1] : void 0;
|
|
159
|
+
const { start, end } = spanOf(node);
|
|
160
|
+
if (previous !== void 0 && isUnorderedList(previous) && adjacentSpans(source, spanOf(previous).end, start)) return;
|
|
161
|
+
if (next !== void 0 && isUnorderedList(next) && adjacentSpans(source, end, spanOf(next).start)) return;
|
|
162
|
+
for (const item of node.children) emit.push(node, item.position.start.offset, 1, target);
|
|
163
|
+
};
|
|
164
|
+
const isEmphasisLike = (node) => node.type === "emphasis" || node.type === "strong";
|
|
165
|
+
const formatEmphasis = (source, emit, node, siblings, index, target) => {
|
|
166
|
+
if ((node.markerChar ?? "*") === target) return;
|
|
167
|
+
const markerLength = node.type === "strong" ? 2 : 1;
|
|
168
|
+
const { start, end } = spanOf(node);
|
|
169
|
+
if (target === "_" && (start > 0 && ALPHANUMERIC.test(source[start - 1]) || end < source.length && ALPHANUMERIC.test(source[end]))) return;
|
|
170
|
+
for (const child of childrenOf(node)) {
|
|
171
|
+
if (!isEmphasisLike(child)) continue;
|
|
172
|
+
const childSpan = spanOf(child);
|
|
173
|
+
if (childSpan.start === start + markerLength || childSpan.end === end - markerLength) return;
|
|
174
|
+
}
|
|
175
|
+
const previous = index > 0 ? siblings[index - 1] : void 0;
|
|
176
|
+
const next = index + 1 < siblings.length ? siblings[index + 1] : void 0;
|
|
177
|
+
if (previous !== void 0 && isEmphasisLike(previous) && spanOf(previous).end === start) return;
|
|
178
|
+
if (next !== void 0 && isEmphasisLike(next) && spanOf(next).start === end) return;
|
|
179
|
+
emit.push(node, start, markerLength, target.repeat(markerLength));
|
|
180
|
+
emit.push(node, end - markerLength, markerLength, target.repeat(markerLength));
|
|
181
|
+
};
|
|
182
|
+
const formatFence = (source, emit, node, target) => {
|
|
183
|
+
const fidelity = node.fenceChar;
|
|
184
|
+
if (fidelity === void 0 || fidelity === target) return;
|
|
185
|
+
if (target === "`" && `${node.lang ?? ""}${node.meta ?? ""}`.includes("`")) return;
|
|
186
|
+
const { start, end } = spanOf(node);
|
|
187
|
+
let openLength = 0;
|
|
188
|
+
while (start + openLength < end && source[start + openLength] === fidelity) openLength++;
|
|
189
|
+
if (openLength === 0) return;
|
|
190
|
+
const newLength = Math.max(3, openLength, longestRun(node.value, target) + 1);
|
|
191
|
+
emit.push(node, start, openLength, target.repeat(newLength));
|
|
192
|
+
let closeLength = 0;
|
|
193
|
+
while (end - closeLength > start + openLength && source[end - closeLength - 1] === fidelity) closeLength++;
|
|
194
|
+
if (closeLength >= 3) emit.push(node, end - closeLength, closeLength, target.repeat(newLength));
|
|
195
|
+
};
|
|
196
|
+
const FLOW_TYPES = /* @__PURE__ */ new Set([
|
|
197
|
+
"blockquote",
|
|
198
|
+
"code",
|
|
199
|
+
"definition",
|
|
200
|
+
"footnoteDefinition",
|
|
201
|
+
"heading",
|
|
202
|
+
"html",
|
|
203
|
+
"list",
|
|
204
|
+
"paragraph",
|
|
205
|
+
"table",
|
|
206
|
+
"thematicBreak"
|
|
207
|
+
]);
|
|
208
|
+
const PHRASING_TYPES = /* @__PURE__ */ new Set([
|
|
209
|
+
"break",
|
|
210
|
+
"delete",
|
|
211
|
+
"emphasis",
|
|
212
|
+
"footnoteReference",
|
|
213
|
+
"html",
|
|
214
|
+
"image",
|
|
215
|
+
"imageReference",
|
|
216
|
+
"inlineCode",
|
|
217
|
+
"link",
|
|
218
|
+
"linkReference",
|
|
219
|
+
"strong",
|
|
220
|
+
"text"
|
|
221
|
+
]);
|
|
222
|
+
/** Parent types whose child slot holds flow content. */
|
|
223
|
+
const FLOW_PARENTS = /* @__PURE__ */ new Set([
|
|
224
|
+
"root",
|
|
225
|
+
"blockquote",
|
|
226
|
+
"listItem",
|
|
227
|
+
"footnoteDefinition"
|
|
228
|
+
]);
|
|
229
|
+
/** Parent types whose child slot holds phrasing content. */
|
|
230
|
+
const PHRASING_PARENTS = /* @__PURE__ */ new Set([
|
|
231
|
+
"paragraph",
|
|
232
|
+
"heading",
|
|
233
|
+
"emphasis",
|
|
234
|
+
"strong",
|
|
235
|
+
"delete",
|
|
236
|
+
"link",
|
|
237
|
+
"linkReference",
|
|
238
|
+
"tableCell"
|
|
239
|
+
]);
|
|
240
|
+
/** Ancestor types whose continuation lines carry a prefix (or forbid newlines outright). */
|
|
241
|
+
const NO_MULTILINE_ANCESTORS = /* @__PURE__ */ new Set([
|
|
242
|
+
"blockquote",
|
|
243
|
+
"list",
|
|
244
|
+
"listItem",
|
|
245
|
+
"footnoteDefinition",
|
|
246
|
+
"table",
|
|
247
|
+
"tableRow",
|
|
248
|
+
"tableCell",
|
|
249
|
+
"heading"
|
|
250
|
+
]);
|
|
251
|
+
/** Find `target` by identity; returns its ancestor chain (nearest first) or undefined. */
|
|
252
|
+
const findAncestry = (root, target) => {
|
|
253
|
+
const search = (node, trail) => {
|
|
254
|
+
if (node === target) return trail;
|
|
255
|
+
for (const child of childrenOf(node)) {
|
|
256
|
+
const found = search(child, [node, ...trail]);
|
|
257
|
+
if (found !== void 0) return found;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
return search(root, []);
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Formatting and modification statics. Not instantiable.
|
|
264
|
+
*
|
|
265
|
+
* @remarks
|
|
266
|
+
* `format`/`formatToString` are pure and total: input that trips a parse
|
|
267
|
+
* hardening guard yields no edits rather than corrupting the document, and
|
|
268
|
+
* every emitted edit is guarded against the re-parse hazards listed in the
|
|
269
|
+
* module documentation — a hazardous conversion is skipped, never attempted.
|
|
270
|
+
* `modify`/`modifyToString` carry a real error channel
|
|
271
|
+
* ({@link MarkdownModificationError}) and render every replacement through
|
|
272
|
+
* the canonical stringifier, so a modified document re-parses cleanly by
|
|
273
|
+
* construction.
|
|
274
|
+
*
|
|
275
|
+
* @public
|
|
276
|
+
*/
|
|
277
|
+
var MarkdownFormat = class MarkdownFormat {
|
|
278
|
+
constructor() {}
|
|
279
|
+
/**
|
|
280
|
+
* Compute marker-normalization edits per the requested
|
|
281
|
+
* {@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}).
|
|
288
|
+
*/
|
|
289
|
+
static format(text, range, options) {
|
|
290
|
+
const parsed = Markdown.parseResult(text, MarkdownParseOptions.make({
|
|
291
|
+
...options?.dialect !== void 0 ? { dialect: options.dialect } : {},
|
|
292
|
+
...options?.frontmatter !== void 0 ? { frontmatter: options.frontmatter } : {}
|
|
293
|
+
}));
|
|
294
|
+
if (Result.isFailure(parsed)) return [];
|
|
295
|
+
const emit = new FormatEmitter(text);
|
|
296
|
+
walk(parsed.success, void 0, [], 0, (node, _parent, siblings, index) => {
|
|
297
|
+
if (options?.thematicBreakChar !== void 0 && node.type === "thematicBreak") formatThematicBreak(text, emit, node, options.thematicBreakChar);
|
|
298
|
+
if (options?.headingStyle !== void 0 && node.type === "heading") formatHeading(text, emit, node, options.headingStyle);
|
|
299
|
+
if (options?.bulletChar !== void 0 && node.type === "list") formatList(text, emit, node, siblings, index, options.bulletChar);
|
|
300
|
+
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);
|
|
302
|
+
});
|
|
303
|
+
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
|
+
offset: edit.offset,
|
|
305
|
+
length: edit.length,
|
|
306
|
+
content: edit.content
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Format `text` and apply the resulting edits in one step
|
|
311
|
+
* (`MarkdownEdit.applyAll ∘ format`). Pure and total.
|
|
312
|
+
*/
|
|
313
|
+
static formatToString(text, range, options) {
|
|
314
|
+
return MarkdownEdit.applyAll(text, MarkdownFormat.format(text, range, options));
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Compute the edit that replaces `target` — a node from `document`'s own
|
|
318
|
+
* tree, matched by identity — with `replacement`: a plain string (treated
|
|
319
|
+
* as literal text and escaped; block-wrapped when the target is a flow
|
|
320
|
+
* node) or a node fragment whose content category must fit the target's
|
|
321
|
+
* slot (flow for flow targets, phrasing for phrasing targets and table
|
|
322
|
+
* cells). Every replacement renders through the canonical stringifier, so
|
|
323
|
+
* the modified document re-parses cleanly by construction. Day-one scope:
|
|
324
|
+
* list items, table rows, frontmatter and the root refuse with
|
|
325
|
+
* `UnsupportedTarget`, as does a multi-line replacement whose target sits
|
|
326
|
+
* inside a container (a blockquote, list, table or heading) whose
|
|
327
|
+
* continuation lines the splice cannot prefix.
|
|
328
|
+
*/
|
|
329
|
+
static modify = Effect.fn("MarkdownFormat.modify")(function* (document, target, replacement) {
|
|
330
|
+
const { start, end } = spanOf(target);
|
|
331
|
+
const fail = (code, detail) => new MarkdownModificationError({
|
|
332
|
+
code,
|
|
333
|
+
detail,
|
|
334
|
+
offset: start,
|
|
335
|
+
length: end - start
|
|
336
|
+
});
|
|
337
|
+
const ancestry = findAncestry(document.root, target);
|
|
338
|
+
if (ancestry === void 0) return yield* fail("NodeNotInDocument", "the target node is not part of the document's tree");
|
|
339
|
+
const parent = ancestry[0];
|
|
340
|
+
if (parent === void 0 || target.type === "frontmatter") return yield* fail("UnsupportedTarget", `a ${target.type} node cannot be replaced`);
|
|
341
|
+
let slot;
|
|
342
|
+
if (target.type === "tableCell" && parent.type === "tableRow") slot = "cell";
|
|
343
|
+
else if (FLOW_PARENTS.has(parent.type)) slot = "flow";
|
|
344
|
+
else if (PHRASING_PARENTS.has(parent.type)) slot = "phrasing";
|
|
345
|
+
else return yield* fail("UnsupportedTarget", `a ${target.type} node inside a ${parent.type} cannot be replaced`);
|
|
346
|
+
let renderRoot;
|
|
347
|
+
if (typeof replacement === "string") {
|
|
348
|
+
const textNode = Text.make({ value: replacement });
|
|
349
|
+
const paragraph = Paragraph.make({ children: [textNode] });
|
|
350
|
+
renderRoot = Root.make({ children: [paragraph] });
|
|
351
|
+
} else if (slot === "flow") {
|
|
352
|
+
if (!FLOW_TYPES.has(replacement.type)) return yield* fail("FragmentCategoryMismatch", `a ${replacement.type} fragment does not fit a flow slot — pass flow content or a plain string`);
|
|
353
|
+
renderRoot = Root.make({ children: [replacement] });
|
|
354
|
+
} else {
|
|
355
|
+
if (!PHRASING_TYPES.has(replacement.type)) return yield* fail("FragmentCategoryMismatch", `a ${replacement.type} fragment does not fit a phrasing slot — pass phrasing content or a plain string`);
|
|
356
|
+
const paragraph = Paragraph.make({ children: [replacement] });
|
|
357
|
+
renderRoot = Root.make({ children: [paragraph] });
|
|
358
|
+
}
|
|
359
|
+
const rendered = Markdown.stringifyResult(renderRoot);
|
|
360
|
+
if (Result.isFailure(rendered)) return yield* fail("FragmentUnrenderable", rendered.failure.message);
|
|
361
|
+
const content = rendered.success.replace(/\n+$/, "");
|
|
362
|
+
if (content.includes("\n") && ancestry.some((ancestor) => NO_MULTILINE_ANCESTORS.has(ancestor.type))) return yield* fail("UnsupportedTarget", "a multi-line replacement cannot be spliced inside a container whose continuation lines carry a prefix");
|
|
363
|
+
return [MarkdownEdit.make({
|
|
364
|
+
offset: start,
|
|
365
|
+
length: end - start,
|
|
366
|
+
content
|
|
367
|
+
})];
|
|
368
|
+
});
|
|
369
|
+
/**
|
|
370
|
+
* Modify `document` and apply the resulting edit in one step
|
|
371
|
+
* (`MarkdownEdit.applyAll ∘ modify`).
|
|
372
|
+
*/
|
|
373
|
+
static modifyToString = Effect.fn("MarkdownFormat.modifyToString")(function* (document, target, replacement) {
|
|
374
|
+
const edits = yield* MarkdownFormat.modify(document, target, replacement);
|
|
375
|
+
return MarkdownEdit.applyAll(document.source, edits);
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
//#endregion
|
|
380
|
+
export { MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode };
|