@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,519 @@
|
|
|
1
|
+
import { GuardExceeded } from "./carriers.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/stringify.ts
|
|
4
|
+
const FLOW_CONTEXT = {
|
|
5
|
+
singleLine: false,
|
|
6
|
+
inTable: false,
|
|
7
|
+
inHeading: false
|
|
8
|
+
};
|
|
9
|
+
const HEADING_CONTEXT = {
|
|
10
|
+
singleLine: true,
|
|
11
|
+
inTable: false,
|
|
12
|
+
inHeading: true
|
|
13
|
+
};
|
|
14
|
+
const CELL_CONTEXT = {
|
|
15
|
+
singleLine: true,
|
|
16
|
+
inTable: true,
|
|
17
|
+
inHeading: false
|
|
18
|
+
};
|
|
19
|
+
/** Characters escaped wherever they appear in inline text. */
|
|
20
|
+
const ALWAYS_ESCAPE = /* @__PURE__ */ new Set([
|
|
21
|
+
"\\",
|
|
22
|
+
"`",
|
|
23
|
+
"*",
|
|
24
|
+
"_",
|
|
25
|
+
"[",
|
|
26
|
+
"]",
|
|
27
|
+
"<",
|
|
28
|
+
">",
|
|
29
|
+
"&",
|
|
30
|
+
"~",
|
|
31
|
+
"|"
|
|
32
|
+
]);
|
|
33
|
+
/** Line-start characters that could open a block construct. */
|
|
34
|
+
const LINE_START_ESCAPE = /* @__PURE__ */ new Set([
|
|
35
|
+
"#",
|
|
36
|
+
">",
|
|
37
|
+
"+",
|
|
38
|
+
"-",
|
|
39
|
+
"=",
|
|
40
|
+
"~",
|
|
41
|
+
"`"
|
|
42
|
+
]);
|
|
43
|
+
const SCHEME_WORDS = /* @__PURE__ */ new Set([
|
|
44
|
+
"http",
|
|
45
|
+
"https",
|
|
46
|
+
"ftp",
|
|
47
|
+
"mailto",
|
|
48
|
+
"xmpp"
|
|
49
|
+
]);
|
|
50
|
+
const ORDERED_MARKER = /^\d{1,9}[.)]/;
|
|
51
|
+
const guard = (state, node) => {
|
|
52
|
+
state.depth += 1;
|
|
53
|
+
if (state.depth > 256) throw new GuardExceeded("NestingDepthExceeded", 256, state.depth, node.position.start.offset);
|
|
54
|
+
};
|
|
55
|
+
const unguard = (state) => {
|
|
56
|
+
state.depth -= 1;
|
|
57
|
+
};
|
|
58
|
+
/** Whether `value[index]` ends a `www` run preceded by a word boundary. */
|
|
59
|
+
const isWwwDot = (value, index) => {
|
|
60
|
+
if (value.slice(Math.max(0, index - 3), index).toLowerCase() !== "www") return false;
|
|
61
|
+
if (index - 3 <= 0) return true;
|
|
62
|
+
const before = value[index - 4];
|
|
63
|
+
return before === void 0 || !/[\p{L}\p{N}]/u.test(before);
|
|
64
|
+
};
|
|
65
|
+
/** Whether the run of letters ending at `value[index]` is a scheme word. */
|
|
66
|
+
const isSchemeColon = (value, index) => {
|
|
67
|
+
let start = index;
|
|
68
|
+
while (start > 0 && /[a-zA-Z]/.test(value[start - 1])) start -= 1;
|
|
69
|
+
return SCHEME_WORDS.has(value.slice(start, index).toLowerCase());
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Escape one text value into `out`, tracking line starts. Returns whether the
|
|
73
|
+
* emission ended at a line start.
|
|
74
|
+
*/
|
|
75
|
+
const escapeText = (value, context, atLineStart) => {
|
|
76
|
+
let out = "";
|
|
77
|
+
let lineStart = atLineStart;
|
|
78
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
79
|
+
const char = value[index];
|
|
80
|
+
if (char === "\n") {
|
|
81
|
+
const adjacent = value[index - 1] === "\n" || value[index + 1] === "\n";
|
|
82
|
+
const atBoundary = index === 0 || index === value.length - 1;
|
|
83
|
+
if (context.singleLine) {
|
|
84
|
+
out += " ";
|
|
85
|
+
lineStart = false;
|
|
86
|
+
} else if (adjacent || atBoundary) {
|
|
87
|
+
out += " ";
|
|
88
|
+
lineStart = false;
|
|
89
|
+
} else {
|
|
90
|
+
out += "\n";
|
|
91
|
+
lineStart = true;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (lineStart) {
|
|
96
|
+
if (char === " ") {
|
|
97
|
+
out += "	";
|
|
98
|
+
lineStart = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (char === " ") {
|
|
102
|
+
out += " ";
|
|
103
|
+
lineStart = false;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const lineEnd = value.indexOf("\n", index);
|
|
107
|
+
const restOfLine = value.slice(index, lineEnd === -1 ? void 0 : lineEnd);
|
|
108
|
+
const ordered = ORDERED_MARKER.exec(restOfLine);
|
|
109
|
+
if (ordered !== null) {
|
|
110
|
+
const digits = ordered[0].slice(0, -1);
|
|
111
|
+
const delimiter = ordered[0].slice(-1);
|
|
112
|
+
out += `${digits}\\${delimiter}`;
|
|
113
|
+
index += ordered[0].length - 1;
|
|
114
|
+
lineStart = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (LINE_START_ESCAPE.has(char)) {
|
|
118
|
+
out += `\\${char}`;
|
|
119
|
+
lineStart = false;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
lineStart = false;
|
|
124
|
+
if (ALWAYS_ESCAPE.has(char)) {
|
|
125
|
+
out += `\\${char}`;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (context.inHeading && char === "#") {
|
|
129
|
+
out += "\\#";
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === "." && isWwwDot(value, index)) {
|
|
133
|
+
out += "\\.";
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (char === ":" && isSchemeColon(value, index)) {
|
|
137
|
+
out += "\\:";
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
out += char;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
text: out,
|
|
144
|
+
atLineStart: lineStart
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/** Pick the effective emphasis marker, flipping away from a forbidden char. */
|
|
148
|
+
const emphasisMarker = (fidelity, forbidden) => {
|
|
149
|
+
const chosen = fidelity ?? "*";
|
|
150
|
+
return chosen === forbidden ? chosen === "*" ? "_" : "*" : chosen;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Emit a reference label for its bracket. Labels (and identifiers) in this
|
|
154
|
+
* tree are SOURCE-FORM — the parser keeps character escapes unparsed, per
|
|
155
|
+
* the mdast identifier contract — so the label already carries any escapes
|
|
156
|
+
* its content needs and re-emitting it verbatim reproduces the original
|
|
157
|
+
* bracket exactly. The only intervention is protecting a synthesized label's
|
|
158
|
+
* UNESCAPED square brackets, which would otherwise break the bracket
|
|
159
|
+
* structure. Definitions and references run through this same function, so
|
|
160
|
+
* identifiers agree on both ends and resolution holds.
|
|
161
|
+
*/
|
|
162
|
+
const escapeLabel = (label) => label.replace(/(?<!\\)([[\]])/g, "\\$1");
|
|
163
|
+
/** Whether a destination character forces the pointy-bracket form. */
|
|
164
|
+
const forcesPointy = (char) => {
|
|
165
|
+
return char.codePointAt(0) <= 32 || char === "<" || char === ">" || char === "[" || char === "]" || char === "\\";
|
|
166
|
+
};
|
|
167
|
+
/** Wrap a link/image destination, pointy-bracketed when it needs it. */
|
|
168
|
+
const destination = (url) => {
|
|
169
|
+
if (url === "" || Array.from(url).some(forcesPointy) || !balancedParens(url)) return `<${url.replace(/[<>\\]/g, (char) => `\\${char}`)}>`;
|
|
170
|
+
return url;
|
|
171
|
+
};
|
|
172
|
+
const balancedParens = (url) => {
|
|
173
|
+
let open = 0;
|
|
174
|
+
for (const char of url) {
|
|
175
|
+
if (char === "(") open += 1;
|
|
176
|
+
if (char === ")") {
|
|
177
|
+
open -= 1;
|
|
178
|
+
if (open < 0) return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return open === 0;
|
|
182
|
+
};
|
|
183
|
+
const titleSuffix = (title) => title === void 0 ? "" : ` "${title.replace(/[\\"]/g, (char) => `\\${char}`)}"`;
|
|
184
|
+
/** Serialize an inline-code span with a fence run longer than any interior run. */
|
|
185
|
+
const inlineCode = (value) => {
|
|
186
|
+
let longest = 0;
|
|
187
|
+
for (const run of value.match(/`+/g) ?? []) longest = Math.max(longest, run.length);
|
|
188
|
+
const fence = "`".repeat(Math.max(1, longest + 1));
|
|
189
|
+
return `${fence}${!(value !== "" && value.trim() === "") && (value.startsWith("`") || value.endsWith("`") || value.startsWith(" ") || value.endsWith(" ")) ? ` ${value} ` : value}${fence}`;
|
|
190
|
+
};
|
|
191
|
+
const flipMarker = (marker) => marker === "*" ? "_" : "*";
|
|
192
|
+
/** The edge-marker choice for a nested emphasis-family child. */
|
|
193
|
+
const nestedMarker = (child, fidelity, guard) => {
|
|
194
|
+
if (guard.parent === "emphasis") return child === "strong" ? guard.marker : flipMarker(guard.marker);
|
|
195
|
+
return child === "emphasis" ? flipMarker(guard.marker) : fidelity ?? guard.marker;
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Serialize a run of phrasing children. `junctionGuard` carries the marker
|
|
199
|
+
* the PARENT emits at this run's edges, so a first or last emphasis-family
|
|
200
|
+
* child flips away from it; mid-run children only flip away from an actual
|
|
201
|
+
* adjacent marker char.
|
|
202
|
+
*/
|
|
203
|
+
const serializeInlines = (children, context, state, startsLine, junctionGuard) => {
|
|
204
|
+
let out = "";
|
|
205
|
+
let atLineStart = startsLine;
|
|
206
|
+
let index = 0;
|
|
207
|
+
for (const child of children) {
|
|
208
|
+
guard(state, child);
|
|
209
|
+
const lastChar = out === "" ? void 0 : out[out.length - 1];
|
|
210
|
+
const atEdge = out === "" || index === children.length - 1;
|
|
211
|
+
const adjacentMarker = lastChar === "*" || lastChar === "_" ? lastChar : void 0;
|
|
212
|
+
const bangGuard = () => {
|
|
213
|
+
if (out.endsWith("!") && !out.endsWith("\\!")) out = `${out.slice(0, -1)}\\!`;
|
|
214
|
+
};
|
|
215
|
+
switch (child.type) {
|
|
216
|
+
case "text": {
|
|
217
|
+
const escaped = escapeText(child.value, context, atLineStart);
|
|
218
|
+
out += escaped.text;
|
|
219
|
+
atLineStart = escaped.atLineStart;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "inlineCode":
|
|
223
|
+
out += inlineCode(child.value);
|
|
224
|
+
atLineStart = false;
|
|
225
|
+
break;
|
|
226
|
+
case "html":
|
|
227
|
+
out += child.value;
|
|
228
|
+
atLineStart = false;
|
|
229
|
+
break;
|
|
230
|
+
case "break":
|
|
231
|
+
if (context.singleLine) {
|
|
232
|
+
out += " ";
|
|
233
|
+
atLineStart = false;
|
|
234
|
+
} else {
|
|
235
|
+
out += child.breakStyle === "spaces" ? " \n" : "\\\n";
|
|
236
|
+
atLineStart = true;
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case "emphasis":
|
|
240
|
+
case "strong": {
|
|
241
|
+
const kind = child.type;
|
|
242
|
+
let marker;
|
|
243
|
+
if (atEdge && junctionGuard !== void 0) marker = nestedMarker(kind, child.markerChar, junctionGuard);
|
|
244
|
+
else marker = emphasisMarker(child.markerChar, adjacentMarker);
|
|
245
|
+
if (marker === "_" && lastChar !== void 0 && /[\p{L}\p{N}]/u.test(lastChar)) marker = "*";
|
|
246
|
+
const run = kind === "strong" ? marker.repeat(2) : marker;
|
|
247
|
+
const inner = serializeInlines(child.children, context, state, false, {
|
|
248
|
+
parent: kind,
|
|
249
|
+
marker
|
|
250
|
+
});
|
|
251
|
+
out += `${run}${inner}${run}`;
|
|
252
|
+
atLineStart = false;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
case "delete":
|
|
256
|
+
out += `~~${serializeInlines(child.children, context, state, false)}~~`;
|
|
257
|
+
atLineStart = false;
|
|
258
|
+
break;
|
|
259
|
+
case "link":
|
|
260
|
+
bangGuard();
|
|
261
|
+
out += `[${serializeInlines(child.children, context, state, false)}](${destination(child.url)}${titleSuffix(child.title)})`;
|
|
262
|
+
atLineStart = false;
|
|
263
|
+
break;
|
|
264
|
+
case "image": {
|
|
265
|
+
const alt = (child.alt ?? "").replace(/[\\[\]]/g, (char) => `\\${char}`);
|
|
266
|
+
out += `}${titleSuffix(child.title)})`;
|
|
267
|
+
atLineStart = false;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "linkReference": {
|
|
271
|
+
bangGuard();
|
|
272
|
+
const label = escapeLabel(child.label ?? child.identifier);
|
|
273
|
+
if (child.referenceType === "full") {
|
|
274
|
+
const content = serializeInlines(child.children, context, state, false);
|
|
275
|
+
out += `[${content}][${label}]`;
|
|
276
|
+
} else if (child.referenceType === "collapsed") out += `[${label}][]`;
|
|
277
|
+
else out += `[${label}]`;
|
|
278
|
+
atLineStart = false;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "imageReference": {
|
|
282
|
+
const label = escapeLabel(child.label ?? child.identifier);
|
|
283
|
+
if (child.referenceType === "full") {
|
|
284
|
+
const alt = (child.alt ?? "").replace(/[\\[\]]/g, (char) => `\\${char}`);
|
|
285
|
+
out += `![${alt}][${label}]`;
|
|
286
|
+
} else if (child.referenceType === "collapsed") out += `![${label}][]`;
|
|
287
|
+
else out += `![${label}]`;
|
|
288
|
+
atLineStart = false;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case "footnoteReference":
|
|
292
|
+
out += `[^${escapeLabel(child.label ?? child.identifier)}]`;
|
|
293
|
+
atLineStart = false;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
index += 1;
|
|
297
|
+
unguard(state);
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
};
|
|
301
|
+
/** Prefix every line of `content`; blank lines take the trimmed prefix. */
|
|
302
|
+
const prefixLines = (content, prefix, blankPrefix) => content.split("\n").map((line) => line === "" ? blankPrefix : `${prefix}${line}`).join("\n");
|
|
303
|
+
/** First line gets `marker`, continuation lines get spaces of its width. */
|
|
304
|
+
const hangingIndent = (content, marker) => {
|
|
305
|
+
const lines = content.split("\n");
|
|
306
|
+
const indent = " ".repeat(marker.length);
|
|
307
|
+
return lines.map((line, index) => {
|
|
308
|
+
if (index === 0) return `${marker}${line}`;
|
|
309
|
+
return line === "" ? "" : `${indent}${line}`;
|
|
310
|
+
}).join("\n");
|
|
311
|
+
};
|
|
312
|
+
const serializeCode = (node, forceFence) => {
|
|
313
|
+
const value = node.value.endsWith("\n") ? node.value.slice(0, -1) : node.value;
|
|
314
|
+
const lines = value === "" ? [] : value.split("\n");
|
|
315
|
+
if (!forceFence && node.fenceChar === void 0 && node.lang === void 0 && node.meta === void 0 && lines.length > 0 && lines[0] !== "" && lines[lines.length - 1] !== "") return lines.map((line) => line === "" ? "" : ` ${line}`).join("\n");
|
|
316
|
+
const char = node.fenceChar ?? "`";
|
|
317
|
+
let longest = 0;
|
|
318
|
+
const runs = value.match(char === "`" ? /`+/g : /~+/g) ?? [];
|
|
319
|
+
for (const run of runs) longest = Math.max(longest, run.length);
|
|
320
|
+
const length = Math.max(3, node.fenceLength ?? 0, longest + 1);
|
|
321
|
+
const fence = char.repeat(length);
|
|
322
|
+
return `${fence}${`${node.lang ?? ""}${node.meta !== void 0 ? ` ${node.meta}` : ""}`}\n${value === "" ? "" : `${value}\n`}${fence}`;
|
|
323
|
+
};
|
|
324
|
+
const setextUnderline = (heading, content) => {
|
|
325
|
+
const lastLine = content.split("\n").at(-1) ?? "";
|
|
326
|
+
return (heading.depth === 1 ? "=" : "-").repeat(Math.max(1, lastLine.length));
|
|
327
|
+
};
|
|
328
|
+
const serializeHeading = (heading, state) => {
|
|
329
|
+
const content = serializeInlines(heading.children, HEADING_CONTEXT, state, false);
|
|
330
|
+
if (heading.headingStyle === "setext" && heading.depth <= 2 && content !== "") return `${content}\n${setextUnderline(heading, content)}`;
|
|
331
|
+
const hashes = "#".repeat(heading.depth);
|
|
332
|
+
return content === "" ? hashes : `${hashes} ${content}`;
|
|
333
|
+
};
|
|
334
|
+
const serializeDefinition = (node) => `[${escapeLabel(node.label ?? node.identifier)}]: ${destination(node.url)}${titleSuffix(node.title)}`;
|
|
335
|
+
const serializeFootnoteDefinition = (node, state) => {
|
|
336
|
+
const lines = serializeBlocks(node.children, state).split("\n");
|
|
337
|
+
const marker = `[^${escapeLabel(node.label ?? node.identifier)}]:`;
|
|
338
|
+
if (node.children[0]?.type === "code") return [marker, ...lines.map((line) => line === "" ? "" : ` ${line}`)].join("\n");
|
|
339
|
+
return lines.map((line, index) => {
|
|
340
|
+
if (index === 0) return `${marker} ${line}`;
|
|
341
|
+
return line === "" ? "" : ` ${line}`;
|
|
342
|
+
}).join("\n");
|
|
343
|
+
};
|
|
344
|
+
const alignCell = (align) => {
|
|
345
|
+
switch (align) {
|
|
346
|
+
case "left": return ":--";
|
|
347
|
+
case "right": return "--:";
|
|
348
|
+
case "center": return ":-:";
|
|
349
|
+
default: return "---";
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const serializeTable = (table, state) => {
|
|
353
|
+
const columnCount = Math.max(1, ...table.children.map((row) => row.children.length));
|
|
354
|
+
const rows = table.children.map((row) => {
|
|
355
|
+
guard(state, row);
|
|
356
|
+
const cells = row.children.map((cell) => {
|
|
357
|
+
guard(state, cell);
|
|
358
|
+
const content = serializeInlines(cell.children, CELL_CONTEXT, state, false).replace(/(?<!\\)\|/g, "\\|");
|
|
359
|
+
unguard(state);
|
|
360
|
+
return content;
|
|
361
|
+
});
|
|
362
|
+
unguard(state);
|
|
363
|
+
while (cells.length < columnCount) cells.push("");
|
|
364
|
+
return `| ${cells.join(" | ")} |`;
|
|
365
|
+
});
|
|
366
|
+
const alignRow = `| ${Array.from({ length: columnCount }, (_, column) => alignCell(table.align?.[column])).join(" | ")} |`;
|
|
367
|
+
const [header, ...body] = rows;
|
|
368
|
+
return [
|
|
369
|
+
header ?? `| ${Array.from({ length: columnCount }, () => "").join(" | ")} |`,
|
|
370
|
+
alignRow,
|
|
371
|
+
...body
|
|
372
|
+
].join("\n");
|
|
373
|
+
};
|
|
374
|
+
/** The marker actually used by a list, for adjacency comparison. */
|
|
375
|
+
const effectiveListMarker = (list, flipped) => {
|
|
376
|
+
if (list.ordered === true) {
|
|
377
|
+
const delimiter = list.delimiter ?? ".";
|
|
378
|
+
return flipped ? delimiter === "." ? ")" : "." : delimiter;
|
|
379
|
+
}
|
|
380
|
+
const bullet = list.bulletChar ?? "-";
|
|
381
|
+
return flipped ? bullet === "-" ? "*" : "-" : bullet;
|
|
382
|
+
};
|
|
383
|
+
const serializeListItem = (item, marker, state, tight) => {
|
|
384
|
+
guard(state, item);
|
|
385
|
+
const inner = serializeBlocks(item.children, state, tight);
|
|
386
|
+
unguard(state);
|
|
387
|
+
const content = `${item.checked === void 0 ? "" : item.checked ? "[x] " : "[ ] "}${inner}`;
|
|
388
|
+
if (content === "") return marker.trimEnd();
|
|
389
|
+
return hangingIndent(content, marker);
|
|
390
|
+
};
|
|
391
|
+
const serializeList = (list, state, flipped) => {
|
|
392
|
+
const tight = !(list.spread ?? false);
|
|
393
|
+
return list.children.map((item, index) => {
|
|
394
|
+
const marker = list.ordered === true ? `${(list.start ?? 1) + index}${effectiveListMarker(list, flipped)} ` : `${effectiveListMarker(list, flipped)} `;
|
|
395
|
+
return serializeListItem(item, marker, state, tight);
|
|
396
|
+
}).join(tight ? "\n" : "\n\n");
|
|
397
|
+
};
|
|
398
|
+
/** Whether `next` interrupts a paragraph without a blank line before it. */
|
|
399
|
+
const interruptsParagraph = (next) => {
|
|
400
|
+
switch (next.type) {
|
|
401
|
+
case "list": return next.ordered !== true || (next.start ?? 1) === 1;
|
|
402
|
+
case "blockquote":
|
|
403
|
+
case "thematicBreak": return true;
|
|
404
|
+
case "heading": return next.headingStyle !== "setext";
|
|
405
|
+
case "code": return next.fenceChar !== void 0 || next.lang !== void 0;
|
|
406
|
+
default: return false;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
/**
|
|
410
|
+
* Whether `prev` and `next` may sit on adjacent lines with no blank line
|
|
411
|
+
* between them without either absorbing the other on re-parse. Drives tight
|
|
412
|
+
* list items; anything not provably safe takes the blank line.
|
|
413
|
+
*/
|
|
414
|
+
const canJoinWithoutBlank = (prev, next) => {
|
|
415
|
+
switch (prev.type) {
|
|
416
|
+
case "heading": return true;
|
|
417
|
+
case "thematicBreak": return true;
|
|
418
|
+
case "code":
|
|
419
|
+
if (prev.fenceChar !== void 0 || prev.lang !== void 0) return true;
|
|
420
|
+
return !(next.type === "code" && next.fenceChar === void 0 && next.lang === void 0);
|
|
421
|
+
case "paragraph": return interruptsParagraph(next);
|
|
422
|
+
case "blockquote": return next.type !== "blockquote" && next.type !== "paragraph" && interruptsParagraph(next);
|
|
423
|
+
default: return false;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* Serialize a sequence of flow blocks, handling sibling adjacency. With
|
|
428
|
+
* `tight`, safe pairs join with a bare newline (tight-list items).
|
|
429
|
+
*/
|
|
430
|
+
const serializeBlocks = (children, state, tight = false) => {
|
|
431
|
+
const parts = [];
|
|
432
|
+
const nodes = [];
|
|
433
|
+
let previous;
|
|
434
|
+
let previousListMarker;
|
|
435
|
+
for (const child of children) {
|
|
436
|
+
guard(state, child);
|
|
437
|
+
switch (child.type) {
|
|
438
|
+
case "paragraph":
|
|
439
|
+
parts.push(serializeInlines(child.children, FLOW_CONTEXT, state, true));
|
|
440
|
+
previousListMarker = void 0;
|
|
441
|
+
break;
|
|
442
|
+
case "heading":
|
|
443
|
+
parts.push(serializeHeading(child, state));
|
|
444
|
+
previousListMarker = void 0;
|
|
445
|
+
break;
|
|
446
|
+
case "thematicBreak":
|
|
447
|
+
parts.push((child.markerChar ?? "*").repeat(3));
|
|
448
|
+
previousListMarker = void 0;
|
|
449
|
+
break;
|
|
450
|
+
case "code":
|
|
451
|
+
parts.push(serializeCode(child, previous?.type === "list"));
|
|
452
|
+
previousListMarker = void 0;
|
|
453
|
+
break;
|
|
454
|
+
case "html":
|
|
455
|
+
parts.push(child.value);
|
|
456
|
+
previousListMarker = void 0;
|
|
457
|
+
break;
|
|
458
|
+
case "blockquote": {
|
|
459
|
+
const inner = serializeBlocks(child.children, state);
|
|
460
|
+
parts.push(prefixLines(inner, "> ", ">"));
|
|
461
|
+
previousListMarker = void 0;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case "list": {
|
|
465
|
+
const flipped = previousListMarker !== void 0 && previousListMarker === effectiveListMarker(child, false);
|
|
466
|
+
parts.push(serializeList(child, state, flipped));
|
|
467
|
+
previousListMarker = effectiveListMarker(child, flipped);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case "definition":
|
|
471
|
+
parts.push(serializeDefinition(child));
|
|
472
|
+
previousListMarker = void 0;
|
|
473
|
+
break;
|
|
474
|
+
case "footnoteDefinition":
|
|
475
|
+
parts.push(serializeFootnoteDefinition(child, state));
|
|
476
|
+
previousListMarker = void 0;
|
|
477
|
+
break;
|
|
478
|
+
case "table":
|
|
479
|
+
parts.push(serializeTable(child, state));
|
|
480
|
+
previousListMarker = void 0;
|
|
481
|
+
break;
|
|
482
|
+
case "frontmatter": {
|
|
483
|
+
const open = child.format === "toml" ? "+++" : child.format === "json" ? "---json" : "---";
|
|
484
|
+
const close = child.format === "toml" ? "+++" : "---";
|
|
485
|
+
parts.push(child.value === "" ? `${open}\n${close}` : `${open}\n${child.value}\n${close}`);
|
|
486
|
+
previousListMarker = void 0;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
previous = child;
|
|
491
|
+
nodes.push(child);
|
|
492
|
+
unguard(state);
|
|
493
|
+
}
|
|
494
|
+
if (!tight) return parts.join("\n\n");
|
|
495
|
+
let out = "";
|
|
496
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
497
|
+
if (index > 0) {
|
|
498
|
+
const prev = nodes[index - 1];
|
|
499
|
+
const next = nodes[index];
|
|
500
|
+
out += canJoinWithoutBlank(prev, next) ? "\n" : "\n\n";
|
|
501
|
+
}
|
|
502
|
+
out += parts[index];
|
|
503
|
+
}
|
|
504
|
+
return out;
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* Serialize a document tree to canonical markdown source.
|
|
508
|
+
*
|
|
509
|
+
* Throws the raw `GuardExceeded` carrier when the tree nests past
|
|
510
|
+
* `MAX_NESTING_DEPTH`; the facade materializes the typed error. Output is
|
|
511
|
+
* empty for an empty root and ends with exactly one newline otherwise.
|
|
512
|
+
*/
|
|
513
|
+
const stringifyTree = (root) => {
|
|
514
|
+
const body = serializeBlocks(root.children, { depth: 0 });
|
|
515
|
+
return body === "" ? "" : `${body}\n`;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
//#endregion
|
|
519
|
+
export { stringifyTree };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { decodeEntity } from "./entities.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/unescape.ts
|
|
4
|
+
/** The punctuation set a backslash may escape, per the spec. */
|
|
5
|
+
const ESCAPABLE = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]";
|
|
6
|
+
/** One entity, in any of the three spec forms. */
|
|
7
|
+
const ENTITY = "&(?:#x[a-f0-9]{1,6}|#[0-9]{1,7}|[a-z][a-z0-9]{1,31});";
|
|
8
|
+
const reBackslashOrAmp = /[\\&]/;
|
|
9
|
+
const reEntityOrEscapedChar = new RegExp(`\\\\${ESCAPABLE}|${ENTITY}`, "gi");
|
|
10
|
+
const unescapeChar = (source) => {
|
|
11
|
+
if (source.charCodeAt(0) === 92) return source.charAt(1);
|
|
12
|
+
return decodeEntity(source) ?? source;
|
|
13
|
+
};
|
|
14
|
+
/** Replace every backslash escape and character reference with its literal. */
|
|
15
|
+
const unescapeString = (source) => reBackslashOrAmp.test(source) ? source.replace(reEntityOrEscapedChar, unescapeChar) : source;
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ENTITY, ESCAPABLE, unescapeString };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@effected/markdown",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"markdown",
|
|
8
|
+
"commonmark",
|
|
9
|
+
"gfm",
|
|
10
|
+
"mdast",
|
|
11
|
+
"parser",
|
|
12
|
+
"effect",
|
|
13
|
+
"schema",
|
|
14
|
+
"effected"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/markdown#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/spencerbeggs/effected/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/spencerbeggs/effected.git",
|
|
23
|
+
"directory": "packages/markdown"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": {
|
|
27
|
+
"name": "C. Spencer Beggs",
|
|
28
|
+
"email": "spencer@beggs.codes",
|
|
29
|
+
"url": "https://spencerbeg.gs"
|
|
30
|
+
},
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"type": "module",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./index.d.ts",
|
|
36
|
+
"import": "./index.js",
|
|
37
|
+
"default": "./index.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@effected/jsonc": "0.4.0",
|
|
43
|
+
"@effected/toml": "0.2.0",
|
|
44
|
+
"@effected/yaml": "0.4.0",
|
|
45
|
+
"effect": "4.0.0-beta.99"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@effected/jsonc": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@effected/toml": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@effected/yaml": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=24.11.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.58.11"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|