@effected/markdown 0.6.3 → 0.8.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/FrontmatterSource.js +188 -0
- package/Markdown.js +86 -0
- package/MarkdownDocument.js +4 -3
- package/MarkdownNode.js +154 -3
- package/Mdast.js +66 -1
- package/README.md +38 -0
- package/index.d.ts +375 -8
- package/index.js +3 -2
- package/internal/blockParser.js +1 -1
- package/internal/blocks/frontmatter.js +47 -1
- package/internal/phrasing.js +57 -0
- package/internal/rawInline.js +3 -1
- package/internal/stringify.js +194 -10
- package/package.json +3 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Point, Position } from "../MarkdownNode.js";
|
|
2
|
+
import { preprocessLines } from "./preprocess.js";
|
|
3
|
+
import { LineIndex } from "./lineIndex.js";
|
|
4
|
+
import { parseInlines } from "./inlineParser.js";
|
|
5
|
+
import { trimWithSegments } from "./rawInline.js";
|
|
6
|
+
|
|
7
|
+
//#region src/internal/phrasing.ts
|
|
8
|
+
const EMPTY_REFMAP = /* @__PURE__ */ new Map();
|
|
9
|
+
const EMPTY_FOOTNOTE_LABELS = /* @__PURE__ */ new Set();
|
|
10
|
+
/**
|
|
11
|
+
* Parse a text fragment as a single paragraph's inline content.
|
|
12
|
+
*/
|
|
13
|
+
const parsePhrasingText = (text, dialect) => {
|
|
14
|
+
const lines = preprocessLines(text);
|
|
15
|
+
const segments = [];
|
|
16
|
+
let content = "";
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
segments.push({
|
|
19
|
+
textOffset: content.length,
|
|
20
|
+
sourceOffset: line.start,
|
|
21
|
+
length: line.text.length
|
|
22
|
+
});
|
|
23
|
+
content += `${line.text}\n`;
|
|
24
|
+
}
|
|
25
|
+
const trimmed = trimWithSegments(content, segments, /\s/);
|
|
26
|
+
if (trimmed.text.length === 0) return [];
|
|
27
|
+
const first = trimmed.segments[0];
|
|
28
|
+
const startOffset = first === void 0 ? 0 : first.sourceOffset;
|
|
29
|
+
const lineIndex = LineIndex.fromLineStarts(text, lines.map((line) => line.start));
|
|
30
|
+
const sourceLength = text.length;
|
|
31
|
+
const positionOf = (start, end) => {
|
|
32
|
+
const clampedStart = Math.min(Math.max(start, 0), sourceLength);
|
|
33
|
+
const clampedEnd = Math.min(Math.max(end, clampedStart), sourceLength);
|
|
34
|
+
const startPoint = lineIndex.positionAt(clampedStart);
|
|
35
|
+
const endPoint = lineIndex.positionAt(clampedEnd);
|
|
36
|
+
return Position.make({
|
|
37
|
+
start: Point.make({
|
|
38
|
+
line: startPoint.line,
|
|
39
|
+
column: startPoint.column,
|
|
40
|
+
offset: clampedStart
|
|
41
|
+
}),
|
|
42
|
+
end: Point.make({
|
|
43
|
+
line: endPoint.line,
|
|
44
|
+
column: endPoint.column,
|
|
45
|
+
offset: clampedEnd
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
return parseInlines({
|
|
50
|
+
text: trimmed.text,
|
|
51
|
+
startOffset,
|
|
52
|
+
segments: trimmed.segments
|
|
53
|
+
}, EMPTY_REFMAP, positionOf, dialect, EMPTY_FOOTNOTE_LABELS);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { parsePhrasingText };
|
package/internal/rawInline.js
CHANGED
|
@@ -6,6 +6,8 @@ const reCmarkSpace = /[ \t\n\r]/;
|
|
|
6
6
|
/**
|
|
7
7
|
* Trim `text` the way commonmark.js does before inline parsing, carrying the
|
|
8
8
|
* segment table along so the surviving characters keep their source offsets.
|
|
9
|
+
* Exported for the phrasing-level parse entry point (`phrasing.ts`), which
|
|
10
|
+
* prepares content the same way a paragraph does.
|
|
9
11
|
*/
|
|
10
12
|
const trimWithSegments = (text, segments, whitespace) => {
|
|
11
13
|
let start = 0;
|
|
@@ -54,4 +56,4 @@ const prepareInline = (block, position, refmap, dialect = "commonmark", footnote
|
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
//#endregion
|
|
57
|
-
export { prepareInline };
|
|
59
|
+
export { prepareInline, trimWithSegments };
|
package/internal/stringify.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENTITY } from "./unescape.js";
|
|
1
2
|
import { GuardExceeded } from "./carriers.js";
|
|
2
3
|
|
|
3
4
|
//#region src/internal/stringify.ts
|
|
@@ -21,15 +22,15 @@ const ALWAYS_ESCAPE = /* @__PURE__ */ new Set([
|
|
|
21
22
|
"\\",
|
|
22
23
|
"`",
|
|
23
24
|
"*",
|
|
24
|
-
"_",
|
|
25
25
|
"[",
|
|
26
26
|
"]",
|
|
27
27
|
"<",
|
|
28
|
-
">",
|
|
29
|
-
"&",
|
|
30
28
|
"~",
|
|
31
29
|
"|"
|
|
32
30
|
]);
|
|
31
|
+
/** A character reference at the head of the string: the engine's own grammar. */
|
|
32
|
+
const reEntityAhead = new RegExp(`^${ENTITY}`, "i");
|
|
33
|
+
const reAlphanumeric = /[\p{L}\p{N}]/u;
|
|
33
34
|
/** Line-start characters that could open a block construct. */
|
|
34
35
|
const LINE_START_ESCAPE = /* @__PURE__ */ new Set([
|
|
35
36
|
"#",
|
|
@@ -68,11 +69,37 @@ const isSchemeColon = (value, index) => {
|
|
|
68
69
|
while (start > 0 && /[a-zA-Z]/.test(value[start - 1])) start -= 1;
|
|
69
70
|
return SCHEME_WORDS.has(value.slice(start, index).toLowerCase());
|
|
70
71
|
};
|
|
72
|
+
/** Whether `value[index]` is a Unicode letter or number (`undefined` is not). */
|
|
73
|
+
const isAlphanumericAt = (value, index) => {
|
|
74
|
+
const char = value[index];
|
|
75
|
+
return char !== void 0 && reAlphanumeric.test(char);
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Whether the `#` at `value[index]` heads a run that would read as an ATX
|
|
79
|
+
* closing sequence: preceded by whitespace or the value start, and followed
|
|
80
|
+
* by nothing but whitespace to the end of the contiguous text (this value
|
|
81
|
+
* plus `followingText`). A non-text sibling after that may contribute only
|
|
82
|
+
* blank content, so it keeps the escape (conservative).
|
|
83
|
+
*/
|
|
84
|
+
const isClosingHashRun = (value, index, followingText, nonTextFollows) => {
|
|
85
|
+
const before = value[index - 1];
|
|
86
|
+
if (before !== void 0 && before !== " " && before !== " ") return false;
|
|
87
|
+
const rest = value.slice(index) + followingText;
|
|
88
|
+
let end = 0;
|
|
89
|
+
while (rest[end] === "#") end += 1;
|
|
90
|
+
return nonTextFollows || /^[ \t\n]*$/.test(rest.slice(end));
|
|
91
|
+
};
|
|
92
|
+
/** A run that a later sibling could still complete into a character reference. */
|
|
93
|
+
const reEntityPrefix = /^&[#a-z0-9]*$/i;
|
|
71
94
|
/**
|
|
72
95
|
* Escape one text value into `out`, tracking line starts. Returns whether the
|
|
73
|
-
* emission ended at a line start.
|
|
96
|
+
* emission ended at a line start. `followingText` is the concatenated value
|
|
97
|
+
* of the contiguous text siblings after this one (the parser never splits a
|
|
98
|
+
* text run, but a synthesized tree may), and `nonTextFollows` says whether a
|
|
99
|
+
* non-text inline comes after those; together they decide the value-end
|
|
100
|
+
* cases (`&`, and `#` in a heading) against what will actually be emitted.
|
|
74
101
|
*/
|
|
75
|
-
const escapeText = (value, context, atLineStart) => {
|
|
102
|
+
const escapeText = (value, context, atLineStart, mdx, followingText, nonTextFollows) => {
|
|
76
103
|
let out = "";
|
|
77
104
|
let lineStart = atLineStart;
|
|
78
105
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -125,7 +152,21 @@ const escapeText = (value, context, atLineStart) => {
|
|
|
125
152
|
out += `\\${char}`;
|
|
126
153
|
continue;
|
|
127
154
|
}
|
|
128
|
-
if (
|
|
155
|
+
if (mdx && char === "{") {
|
|
156
|
+
out += "\\{";
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char === "_" && !(isAlphanumericAt(value, index - 1) && isAlphanumericAt(value, index + 1))) {
|
|
160
|
+
out += "\\_";
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (char === "&") {
|
|
164
|
+
const ahead = value.slice(index) + followingText;
|
|
165
|
+
const entityShaped = reEntityAhead.test(ahead) || nonTextFollows && reEntityPrefix.test(ahead);
|
|
166
|
+
out += entityShaped ? "\\&" : "&";
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (context.inHeading && char === "#" && isClosingHashRun(value, index, followingText, nonTextFollows)) {
|
|
129
170
|
out += "\\#";
|
|
130
171
|
continue;
|
|
131
172
|
}
|
|
@@ -214,7 +255,13 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
|
|
|
214
255
|
};
|
|
215
256
|
switch (child.type) {
|
|
216
257
|
case "text": {
|
|
217
|
-
|
|
258
|
+
let followingText = "";
|
|
259
|
+
let next = index + 1;
|
|
260
|
+
while (next < children.length && children[next]?.type === "text") {
|
|
261
|
+
followingText += children[next].value;
|
|
262
|
+
next += 1;
|
|
263
|
+
}
|
|
264
|
+
const escaped = escapeText(child.value, context, atLineStart, state.mdx, followingText, next < children.length);
|
|
218
265
|
out += escaped.text;
|
|
219
266
|
atLineStart = escaped.atLineStart;
|
|
220
267
|
break;
|
|
@@ -291,12 +338,126 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
|
|
|
291
338
|
case "footnoteReference":
|
|
292
339
|
out += `[^${escapeLabel(child.label ?? child.identifier)}]`;
|
|
293
340
|
atLineStart = false;
|
|
341
|
+
break;
|
|
342
|
+
case "mdxJsxTextElement":
|
|
343
|
+
out += serializeMdxJsxText(child, context, state);
|
|
344
|
+
atLineStart = false;
|
|
345
|
+
break;
|
|
346
|
+
case "mdxTextExpression":
|
|
347
|
+
out += mdxExpression(child.value);
|
|
348
|
+
atLineStart = false;
|
|
294
349
|
}
|
|
295
350
|
index += 1;
|
|
296
351
|
unguard(state);
|
|
297
352
|
}
|
|
298
353
|
return out;
|
|
299
354
|
};
|
|
355
|
+
/** The tag string of every MDX node type. */
|
|
356
|
+
const MDX_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
357
|
+
"mdxJsxFlowElement",
|
|
358
|
+
"mdxJsxTextElement",
|
|
359
|
+
"mdxFlowExpression",
|
|
360
|
+
"mdxTextExpression",
|
|
361
|
+
"mdxjsEsm"
|
|
362
|
+
]);
|
|
363
|
+
/** Whether the tree carries any MDX node — iterative, deliberately unguarded. */
|
|
364
|
+
const treeContainsMdx = (root) => {
|
|
365
|
+
const stack = [root];
|
|
366
|
+
while (stack.length > 0) {
|
|
367
|
+
const node = stack.pop();
|
|
368
|
+
if (MDX_NODE_TYPES.has(node.type)) return true;
|
|
369
|
+
if (node.children !== void 0) for (const child of node.children) stack.push(child);
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* The oracle's `indentLines` line model (mdast-util-to-markdown
|
|
375
|
+
* `lib/util/indent-lines.js`): lines split on every terminator spelling
|
|
376
|
+
* (`\r\n`, `\n`, bare `\r`), terminators preserved verbatim between the
|
|
377
|
+
* mapped lines, and `blank` meaning an empty line — so a blank CRLF line
|
|
378
|
+
* stays bare instead of having a retained `\r` treated as content.
|
|
379
|
+
*/
|
|
380
|
+
const mapMdxLines = (value, map) => {
|
|
381
|
+
const eol = /\r?\n|\r/g;
|
|
382
|
+
const result = [];
|
|
383
|
+
let start = 0;
|
|
384
|
+
let index = 0;
|
|
385
|
+
for (let match = eol.exec(value); match !== null; match = eol.exec(value)) {
|
|
386
|
+
const line = value.slice(start, match.index);
|
|
387
|
+
result.push(map(line, index, line === ""), match[0]);
|
|
388
|
+
start = match.index + match[0].length;
|
|
389
|
+
index += 1;
|
|
390
|
+
}
|
|
391
|
+
const last = value.slice(start);
|
|
392
|
+
result.push(map(last, index, last === ""));
|
|
393
|
+
return result.join("");
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Serialize an MDX expression body between braces. Continuation lines take
|
|
397
|
+
* the oracle's two-space indent; the first line and blank lines take none.
|
|
398
|
+
*/
|
|
399
|
+
const mdxExpression = (value) => `{${mapMdxLines(value, (line, index, blank) => index === 0 || blank ? line : ` ${line}`)}}`;
|
|
400
|
+
/**
|
|
401
|
+
* Serialize one JSX attribute. A `null` or absent value is a boolean
|
|
402
|
+
* attribute; a string value is quoted with `"` (the oracle default), the
|
|
403
|
+
* quote itself escaped as `"` exactly as `stringify-entities` spells
|
|
404
|
+
* it; an expression value goes between braces.
|
|
405
|
+
*/
|
|
406
|
+
const serializeMdxAttribute = (attribute) => {
|
|
407
|
+
if (attribute.type === "mdxJsxExpressionAttribute") return `{${attribute.value}}`;
|
|
408
|
+
const value = attribute.value;
|
|
409
|
+
if (value === void 0 || value === null) return attribute.name;
|
|
410
|
+
if (typeof value === "string") return `${attribute.name}="${value.replaceAll("\"", """)}"`;
|
|
411
|
+
return `${attribute.name}={${value.value}}`;
|
|
412
|
+
};
|
|
413
|
+
/** Serialize a text (phrasing) JSX element. */
|
|
414
|
+
const serializeMdxJsxText = (node, context, state) => {
|
|
415
|
+
const attributes = node.attributes.map(serializeMdxAttribute);
|
|
416
|
+
const selfClosing = node.name !== null && node.children.length === 0;
|
|
417
|
+
let out = `<${node.name ?? ""}`;
|
|
418
|
+
if (attributes.length > 0) out += ` ${attributes.join(" ")}`;
|
|
419
|
+
if (selfClosing) return `${out} />`;
|
|
420
|
+
out += ">";
|
|
421
|
+
out += serializeInlines(node.children, context, state, false);
|
|
422
|
+
out += `</${node.name ?? ""}>`;
|
|
423
|
+
return out;
|
|
424
|
+
};
|
|
425
|
+
/** Prefix every line of a rendered child block; blank lines stay bare. */
|
|
426
|
+
const indentBlockLines = (content, indent) => mapMdxLines(content, (line, _index, blank) => blank ? line : `${indent}${line}`);
|
|
427
|
+
/**
|
|
428
|
+
* Serialize a flow (block) JSX element: opening tag (attributes on their own
|
|
429
|
+
* lines when one carries a line ending), children as indented block layout,
|
|
430
|
+
* closing tag — the oracle's `mdxElement` flow branch.
|
|
431
|
+
*/
|
|
432
|
+
const serializeMdxJsxFlow = (node, state) => {
|
|
433
|
+
const currentIndent = " ".repeat(state.jsxDepth);
|
|
434
|
+
const attributes = node.attributes.map(serializeMdxAttribute);
|
|
435
|
+
const selfClosing = node.name !== null && node.children.length === 0;
|
|
436
|
+
const attributesOnOneLine = attributes.join(" ");
|
|
437
|
+
const attributesOnTheirOwnLine = /[\r\n]/.test(attributesOnOneLine);
|
|
438
|
+
let value = `${currentIndent}<${node.name ?? ""}`;
|
|
439
|
+
if (attributesOnTheirOwnLine) value += `\n${attributes.map((attribute) => `${currentIndent} ${attribute}`).join("\n")}\n${currentIndent}`;
|
|
440
|
+
else if (attributesOnOneLine !== "") value += ` ${attributesOnOneLine}`;
|
|
441
|
+
if (selfClosing) value += `${attributesOnTheirOwnLine ? "" : " "}/`;
|
|
442
|
+
value += ">";
|
|
443
|
+
if (node.children.length > 0) {
|
|
444
|
+
state.jsxDepth += 1;
|
|
445
|
+
const childIndent = " ".repeat(state.jsxDepth);
|
|
446
|
+
const parts = node.children.map((child) => {
|
|
447
|
+
if (child.type === "mdxJsxFlowElement") {
|
|
448
|
+
guard(state, child);
|
|
449
|
+
const rendered = serializeMdxJsxFlow(child, state);
|
|
450
|
+
unguard(state);
|
|
451
|
+
return rendered;
|
|
452
|
+
}
|
|
453
|
+
return indentBlockLines(serializeBlocks([child], state), childIndent);
|
|
454
|
+
});
|
|
455
|
+
state.jsxDepth -= 1;
|
|
456
|
+
value += `\n${parts.join("\n\n")}\n`;
|
|
457
|
+
}
|
|
458
|
+
if (!selfClosing) value += `${currentIndent}</${node.name ?? ""}>`;
|
|
459
|
+
return value;
|
|
460
|
+
};
|
|
300
461
|
/** Prefix every line of `content`; blank lines take the trimmed prefix. */
|
|
301
462
|
const prefixLines = (content, prefix, blankPrefix) => content.split("\n").map((line) => line === "" ? blankPrefix : `${prefix}${line}`).join("\n");
|
|
302
463
|
/** First line gets `marker`, continuation lines get spaces of its width. */
|
|
@@ -325,8 +486,9 @@ const setextUnderline = (heading, content) => {
|
|
|
325
486
|
return (heading.depth === 1 ? "=" : "-").repeat(Math.max(1, lastLine.length));
|
|
326
487
|
};
|
|
327
488
|
const serializeHeading = (heading, state) => {
|
|
328
|
-
const
|
|
329
|
-
|
|
489
|
+
const setext = heading.headingStyle === "setext" && heading.depth <= 2;
|
|
490
|
+
const content = serializeInlines(heading.children, HEADING_CONTEXT, state, setext);
|
|
491
|
+
if (setext && content !== "") return `${content}\n${setextUnderline(heading, content)}`;
|
|
330
492
|
const hashes = "#".repeat(heading.depth);
|
|
331
493
|
return content === "" ? hashes : `${hashes} ${content}`;
|
|
332
494
|
};
|
|
@@ -381,7 +543,10 @@ const effectiveListMarker = (list, flipped) => {
|
|
|
381
543
|
};
|
|
382
544
|
const serializeListItem = (item, marker, state, tight) => {
|
|
383
545
|
guard(state, item);
|
|
546
|
+
const savedJsxDepth = state.jsxDepth;
|
|
547
|
+
state.jsxDepth = 0;
|
|
384
548
|
const inner = serializeBlocks(item.children, state, tight);
|
|
549
|
+
state.jsxDepth = savedJsxDepth;
|
|
385
550
|
unguard(state);
|
|
386
551
|
const content = `${item.checked === void 0 ? "" : item.checked ? "[x] " : "[ ] "}${inner}`;
|
|
387
552
|
if (content === "") return marker.trimEnd();
|
|
@@ -455,7 +620,10 @@ const serializeBlocks = (children, state, tight = false) => {
|
|
|
455
620
|
previousListMarker = void 0;
|
|
456
621
|
break;
|
|
457
622
|
case "blockquote": {
|
|
623
|
+
const savedJsxDepth = state.jsxDepth;
|
|
624
|
+
state.jsxDepth = 0;
|
|
458
625
|
const inner = serializeBlocks(child.children, state);
|
|
626
|
+
state.jsxDepth = savedJsxDepth;
|
|
459
627
|
parts.push(prefixLines(inner, "> ", ">"));
|
|
460
628
|
previousListMarker = void 0;
|
|
461
629
|
break;
|
|
@@ -485,6 +653,17 @@ const serializeBlocks = (children, state, tight = false) => {
|
|
|
485
653
|
previousListMarker = void 0;
|
|
486
654
|
break;
|
|
487
655
|
}
|
|
656
|
+
case "mdxJsxFlowElement":
|
|
657
|
+
parts.push(serializeMdxJsxFlow(child, state));
|
|
658
|
+
previousListMarker = void 0;
|
|
659
|
+
break;
|
|
660
|
+
case "mdxFlowExpression":
|
|
661
|
+
parts.push(mdxExpression(child.value));
|
|
662
|
+
previousListMarker = void 0;
|
|
663
|
+
break;
|
|
664
|
+
case "mdxjsEsm":
|
|
665
|
+
parts.push(child.value);
|
|
666
|
+
previousListMarker = void 0;
|
|
488
667
|
}
|
|
489
668
|
previous = child;
|
|
490
669
|
nodes.push(child);
|
|
@@ -510,7 +689,12 @@ const serializeBlocks = (children, state, tight = false) => {
|
|
|
510
689
|
* empty for an empty root and ends with exactly one newline otherwise.
|
|
511
690
|
*/
|
|
512
691
|
const stringifyTree = (root) => {
|
|
513
|
-
const
|
|
692
|
+
const state = {
|
|
693
|
+
depth: 0,
|
|
694
|
+
jsxDepth: 0,
|
|
695
|
+
mdx: treeContainsMdx(root)
|
|
696
|
+
};
|
|
697
|
+
const body = serializeBlocks(root.children, state);
|
|
514
698
|
return body === "" ? "" : `${body}\n`;
|
|
515
699
|
};
|
|
516
700
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
|
|
6
6
|
"keywords": [
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"./package.json": "./package.json"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@effected/jsonc": "^0.
|
|
42
|
+
"@effected/jsonc": "^0.8.1",
|
|
43
43
|
"@effected/toml": "^0.5.0",
|
|
44
|
-
"@effected/yaml": "^0.
|
|
44
|
+
"@effected/yaml": "^0.12.0",
|
|
45
45
|
"effect": "4.0.0-rc.109"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|