@effected/markdown 0.6.3 → 0.7.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 +143 -3
- 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
|
@@ -72,7 +72,7 @@ const isSchemeColon = (value, index) => {
|
|
|
72
72
|
* Escape one text value into `out`, tracking line starts. Returns whether the
|
|
73
73
|
* emission ended at a line start.
|
|
74
74
|
*/
|
|
75
|
-
const escapeText = (value, context, atLineStart) => {
|
|
75
|
+
const escapeText = (value, context, atLineStart, mdx) => {
|
|
76
76
|
let out = "";
|
|
77
77
|
let lineStart = atLineStart;
|
|
78
78
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -125,6 +125,10 @@ const escapeText = (value, context, atLineStart) => {
|
|
|
125
125
|
out += `\\${char}`;
|
|
126
126
|
continue;
|
|
127
127
|
}
|
|
128
|
+
if (mdx && char === "{") {
|
|
129
|
+
out += "\\{";
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
128
132
|
if (context.inHeading && char === "#") {
|
|
129
133
|
out += "\\#";
|
|
130
134
|
continue;
|
|
@@ -214,7 +218,7 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
|
|
|
214
218
|
};
|
|
215
219
|
switch (child.type) {
|
|
216
220
|
case "text": {
|
|
217
|
-
const escaped = escapeText(child.value, context, atLineStart);
|
|
221
|
+
const escaped = escapeText(child.value, context, atLineStart, state.mdx);
|
|
218
222
|
out += escaped.text;
|
|
219
223
|
atLineStart = escaped.atLineStart;
|
|
220
224
|
break;
|
|
@@ -291,12 +295,126 @@ const serializeInlines = (children, context, state, startsLine, junctionGuard) =
|
|
|
291
295
|
case "footnoteReference":
|
|
292
296
|
out += `[^${escapeLabel(child.label ?? child.identifier)}]`;
|
|
293
297
|
atLineStart = false;
|
|
298
|
+
break;
|
|
299
|
+
case "mdxJsxTextElement":
|
|
300
|
+
out += serializeMdxJsxText(child, context, state);
|
|
301
|
+
atLineStart = false;
|
|
302
|
+
break;
|
|
303
|
+
case "mdxTextExpression":
|
|
304
|
+
out += mdxExpression(child.value);
|
|
305
|
+
atLineStart = false;
|
|
294
306
|
}
|
|
295
307
|
index += 1;
|
|
296
308
|
unguard(state);
|
|
297
309
|
}
|
|
298
310
|
return out;
|
|
299
311
|
};
|
|
312
|
+
/** The tag string of every MDX node type. */
|
|
313
|
+
const MDX_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
314
|
+
"mdxJsxFlowElement",
|
|
315
|
+
"mdxJsxTextElement",
|
|
316
|
+
"mdxFlowExpression",
|
|
317
|
+
"mdxTextExpression",
|
|
318
|
+
"mdxjsEsm"
|
|
319
|
+
]);
|
|
320
|
+
/** Whether the tree carries any MDX node — iterative, deliberately unguarded. */
|
|
321
|
+
const treeContainsMdx = (root) => {
|
|
322
|
+
const stack = [root];
|
|
323
|
+
while (stack.length > 0) {
|
|
324
|
+
const node = stack.pop();
|
|
325
|
+
if (MDX_NODE_TYPES.has(node.type)) return true;
|
|
326
|
+
if (node.children !== void 0) for (const child of node.children) stack.push(child);
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* The oracle's `indentLines` line model (mdast-util-to-markdown
|
|
332
|
+
* `lib/util/indent-lines.js`): lines split on every terminator spelling
|
|
333
|
+
* (`\r\n`, `\n`, bare `\r`), terminators preserved verbatim between the
|
|
334
|
+
* mapped lines, and `blank` meaning an empty line — so a blank CRLF line
|
|
335
|
+
* stays bare instead of having a retained `\r` treated as content.
|
|
336
|
+
*/
|
|
337
|
+
const mapMdxLines = (value, map) => {
|
|
338
|
+
const eol = /\r?\n|\r/g;
|
|
339
|
+
const result = [];
|
|
340
|
+
let start = 0;
|
|
341
|
+
let index = 0;
|
|
342
|
+
for (let match = eol.exec(value); match !== null; match = eol.exec(value)) {
|
|
343
|
+
const line = value.slice(start, match.index);
|
|
344
|
+
result.push(map(line, index, line === ""), match[0]);
|
|
345
|
+
start = match.index + match[0].length;
|
|
346
|
+
index += 1;
|
|
347
|
+
}
|
|
348
|
+
const last = value.slice(start);
|
|
349
|
+
result.push(map(last, index, last === ""));
|
|
350
|
+
return result.join("");
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Serialize an MDX expression body between braces. Continuation lines take
|
|
354
|
+
* the oracle's two-space indent; the first line and blank lines take none.
|
|
355
|
+
*/
|
|
356
|
+
const mdxExpression = (value) => `{${mapMdxLines(value, (line, index, blank) => index === 0 || blank ? line : ` ${line}`)}}`;
|
|
357
|
+
/**
|
|
358
|
+
* Serialize one JSX attribute. A `null` or absent value is a boolean
|
|
359
|
+
* attribute; a string value is quoted with `"` (the oracle default), the
|
|
360
|
+
* quote itself escaped as `"` exactly as `stringify-entities` spells
|
|
361
|
+
* it; an expression value goes between braces.
|
|
362
|
+
*/
|
|
363
|
+
const serializeMdxAttribute = (attribute) => {
|
|
364
|
+
if (attribute.type === "mdxJsxExpressionAttribute") return `{${attribute.value}}`;
|
|
365
|
+
const value = attribute.value;
|
|
366
|
+
if (value === void 0 || value === null) return attribute.name;
|
|
367
|
+
if (typeof value === "string") return `${attribute.name}="${value.replaceAll("\"", """)}"`;
|
|
368
|
+
return `${attribute.name}={${value.value}}`;
|
|
369
|
+
};
|
|
370
|
+
/** Serialize a text (phrasing) JSX element. */
|
|
371
|
+
const serializeMdxJsxText = (node, context, state) => {
|
|
372
|
+
const attributes = node.attributes.map(serializeMdxAttribute);
|
|
373
|
+
const selfClosing = node.name !== null && node.children.length === 0;
|
|
374
|
+
let out = `<${node.name ?? ""}`;
|
|
375
|
+
if (attributes.length > 0) out += ` ${attributes.join(" ")}`;
|
|
376
|
+
if (selfClosing) return `${out} />`;
|
|
377
|
+
out += ">";
|
|
378
|
+
out += serializeInlines(node.children, context, state, false);
|
|
379
|
+
out += `</${node.name ?? ""}>`;
|
|
380
|
+
return out;
|
|
381
|
+
};
|
|
382
|
+
/** Prefix every line of a rendered child block; blank lines stay bare. */
|
|
383
|
+
const indentBlockLines = (content, indent) => mapMdxLines(content, (line, _index, blank) => blank ? line : `${indent}${line}`);
|
|
384
|
+
/**
|
|
385
|
+
* Serialize a flow (block) JSX element: opening tag (attributes on their own
|
|
386
|
+
* lines when one carries a line ending), children as indented block layout,
|
|
387
|
+
* closing tag — the oracle's `mdxElement` flow branch.
|
|
388
|
+
*/
|
|
389
|
+
const serializeMdxJsxFlow = (node, state) => {
|
|
390
|
+
const currentIndent = " ".repeat(state.jsxDepth);
|
|
391
|
+
const attributes = node.attributes.map(serializeMdxAttribute);
|
|
392
|
+
const selfClosing = node.name !== null && node.children.length === 0;
|
|
393
|
+
const attributesOnOneLine = attributes.join(" ");
|
|
394
|
+
const attributesOnTheirOwnLine = /[\r\n]/.test(attributesOnOneLine);
|
|
395
|
+
let value = `${currentIndent}<${node.name ?? ""}`;
|
|
396
|
+
if (attributesOnTheirOwnLine) value += `\n${attributes.map((attribute) => `${currentIndent} ${attribute}`).join("\n")}\n${currentIndent}`;
|
|
397
|
+
else if (attributesOnOneLine !== "") value += ` ${attributesOnOneLine}`;
|
|
398
|
+
if (selfClosing) value += `${attributesOnTheirOwnLine ? "" : " "}/`;
|
|
399
|
+
value += ">";
|
|
400
|
+
if (node.children.length > 0) {
|
|
401
|
+
state.jsxDepth += 1;
|
|
402
|
+
const childIndent = " ".repeat(state.jsxDepth);
|
|
403
|
+
const parts = node.children.map((child) => {
|
|
404
|
+
if (child.type === "mdxJsxFlowElement") {
|
|
405
|
+
guard(state, child);
|
|
406
|
+
const rendered = serializeMdxJsxFlow(child, state);
|
|
407
|
+
unguard(state);
|
|
408
|
+
return rendered;
|
|
409
|
+
}
|
|
410
|
+
return indentBlockLines(serializeBlocks([child], state), childIndent);
|
|
411
|
+
});
|
|
412
|
+
state.jsxDepth -= 1;
|
|
413
|
+
value += `\n${parts.join("\n\n")}\n`;
|
|
414
|
+
}
|
|
415
|
+
if (!selfClosing) value += `${currentIndent}</${node.name ?? ""}>`;
|
|
416
|
+
return value;
|
|
417
|
+
};
|
|
300
418
|
/** Prefix every line of `content`; blank lines take the trimmed prefix. */
|
|
301
419
|
const prefixLines = (content, prefix, blankPrefix) => content.split("\n").map((line) => line === "" ? blankPrefix : `${prefix}${line}`).join("\n");
|
|
302
420
|
/** First line gets `marker`, continuation lines get spaces of its width. */
|
|
@@ -381,7 +499,10 @@ const effectiveListMarker = (list, flipped) => {
|
|
|
381
499
|
};
|
|
382
500
|
const serializeListItem = (item, marker, state, tight) => {
|
|
383
501
|
guard(state, item);
|
|
502
|
+
const savedJsxDepth = state.jsxDepth;
|
|
503
|
+
state.jsxDepth = 0;
|
|
384
504
|
const inner = serializeBlocks(item.children, state, tight);
|
|
505
|
+
state.jsxDepth = savedJsxDepth;
|
|
385
506
|
unguard(state);
|
|
386
507
|
const content = `${item.checked === void 0 ? "" : item.checked ? "[x] " : "[ ] "}${inner}`;
|
|
387
508
|
if (content === "") return marker.trimEnd();
|
|
@@ -455,7 +576,10 @@ const serializeBlocks = (children, state, tight = false) => {
|
|
|
455
576
|
previousListMarker = void 0;
|
|
456
577
|
break;
|
|
457
578
|
case "blockquote": {
|
|
579
|
+
const savedJsxDepth = state.jsxDepth;
|
|
580
|
+
state.jsxDepth = 0;
|
|
458
581
|
const inner = serializeBlocks(child.children, state);
|
|
582
|
+
state.jsxDepth = savedJsxDepth;
|
|
459
583
|
parts.push(prefixLines(inner, "> ", ">"));
|
|
460
584
|
previousListMarker = void 0;
|
|
461
585
|
break;
|
|
@@ -485,6 +609,17 @@ const serializeBlocks = (children, state, tight = false) => {
|
|
|
485
609
|
previousListMarker = void 0;
|
|
486
610
|
break;
|
|
487
611
|
}
|
|
612
|
+
case "mdxJsxFlowElement":
|
|
613
|
+
parts.push(serializeMdxJsxFlow(child, state));
|
|
614
|
+
previousListMarker = void 0;
|
|
615
|
+
break;
|
|
616
|
+
case "mdxFlowExpression":
|
|
617
|
+
parts.push(mdxExpression(child.value));
|
|
618
|
+
previousListMarker = void 0;
|
|
619
|
+
break;
|
|
620
|
+
case "mdxjsEsm":
|
|
621
|
+
parts.push(child.value);
|
|
622
|
+
previousListMarker = void 0;
|
|
488
623
|
}
|
|
489
624
|
previous = child;
|
|
490
625
|
nodes.push(child);
|
|
@@ -510,7 +645,12 @@ const serializeBlocks = (children, state, tight = false) => {
|
|
|
510
645
|
* empty for an empty root and ends with exactly one newline otherwise.
|
|
511
646
|
*/
|
|
512
647
|
const stringifyTree = (root) => {
|
|
513
|
-
const
|
|
648
|
+
const state = {
|
|
649
|
+
depth: 0,
|
|
650
|
+
jsxDepth: 0,
|
|
651
|
+
mdx: treeContainsMdx(root)
|
|
652
|
+
};
|
|
653
|
+
const body = serializeBlocks(root.children, state);
|
|
514
654
|
return body === "" ? "" : `${body}\n`;
|
|
515
655
|
};
|
|
516
656
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.0",
|
|
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": {
|