@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,90 @@
|
|
|
1
|
+
import { Code } from "../../MarkdownNode.js";
|
|
2
|
+
import { isSpaceOrTab, peekCode } from "../preprocess.js";
|
|
3
|
+
import { unescapeString } from "../unescape.js";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/blocks/code.ts
|
|
6
|
+
const reBlankLine = /^[ \t]*$/;
|
|
7
|
+
const reClosingCodeFence = /^(?:`{3,}|~{3,})(?=[ \t]*$)/;
|
|
8
|
+
/**
|
|
9
|
+
* Drop the trailing blank lines an indented code block accumulates, and pull
|
|
10
|
+
* the block's end back to the last line that survived.
|
|
11
|
+
*/
|
|
12
|
+
const finalizeIndented = (block) => {
|
|
13
|
+
const lines = block.stringContent.split("\n");
|
|
14
|
+
while (lines.length > 0 && reBlankLine.test(lines[lines.length - 1] ?? "")) lines.pop();
|
|
15
|
+
block.stringContent = lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
|
16
|
+
const lastSegment = block.segments[lines.length - 1];
|
|
17
|
+
if (lastSegment !== void 0) {
|
|
18
|
+
block.endOffset = lastSegment.sourceOffset + lastSegment.length;
|
|
19
|
+
block.endLine = block.startLine + lines.length - 1;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Split a fenced block's first line off as the info string, and the rest as
|
|
24
|
+
* the literal content.
|
|
25
|
+
*
|
|
26
|
+
* mdast splits the info string into `lang` (the first word) and `meta` (the
|
|
27
|
+
* rest); upstream keeps it whole as `info`. Each half is unescaped
|
|
28
|
+
* separately, so a backslash-escaped space inside the language word does not
|
|
29
|
+
* become a split point.
|
|
30
|
+
*/
|
|
31
|
+
const finalizeFenced = (block) => {
|
|
32
|
+
const content = block.stringContent;
|
|
33
|
+
const newlineAt = content.indexOf("\n");
|
|
34
|
+
const firstLine = (newlineAt === -1 ? content : content.slice(0, newlineAt)).trim();
|
|
35
|
+
block.stringContent = newlineAt === -1 ? "" : content.slice(newlineAt + 1);
|
|
36
|
+
if (firstLine.length === 0) return;
|
|
37
|
+
const wordEnd = firstLine.search(/[ \t]/);
|
|
38
|
+
if (wordEnd === -1) {
|
|
39
|
+
block.data.lang = unescapeString(firstLine);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
block.data.lang = unescapeString(firstLine.slice(0, wordEnd));
|
|
43
|
+
const meta = firstLine.slice(wordEnd).trim();
|
|
44
|
+
if (meta.length > 0) block.data.meta = unescapeString(meta);
|
|
45
|
+
};
|
|
46
|
+
/** Code: absorbs lines verbatim, contains nothing. */
|
|
47
|
+
const codeConstruct = {
|
|
48
|
+
type: "code",
|
|
49
|
+
acceptsLines: true,
|
|
50
|
+
canContain: () => false,
|
|
51
|
+
continue: (scanner, block) => {
|
|
52
|
+
if (block.data.isFenced === true) {
|
|
53
|
+
const line = scanner.currentLine;
|
|
54
|
+
const closing = scanner.indent <= 3 && line.charAt(scanner.nextNonspace) === block.data.fenceChar ? reClosingCodeFence.exec(line.slice(scanner.nextNonspace)) : null;
|
|
55
|
+
if (closing !== null && closing[0].length >= (block.data.fenceLength ?? 3)) {
|
|
56
|
+
scanner.setLastLineLength(scanner.offset + scanner.indent + closing[0].length);
|
|
57
|
+
scanner.finalizeBlock(block, scanner.lineNumber);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
let remaining = block.data.fenceOffset ?? 0;
|
|
61
|
+
while (remaining > 0 && isSpaceOrTab(peekCode(line, scanner.offset))) {
|
|
62
|
+
scanner.advanceOffset(1, true);
|
|
63
|
+
remaining -= 1;
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
if (scanner.indent >= 4) scanner.advanceOffset(4, true);
|
|
68
|
+
else if (scanner.blank) scanner.advanceNextNonspace();
|
|
69
|
+
else return 1;
|
|
70
|
+
return 0;
|
|
71
|
+
},
|
|
72
|
+
finalize: (_scanner, block) => {
|
|
73
|
+
if (block.data.isFenced === true) finalizeFenced(block);
|
|
74
|
+
else finalizeIndented(block);
|
|
75
|
+
},
|
|
76
|
+
materialize: (block, _children, context) => {
|
|
77
|
+
const { isFenced, fenceChar, fenceLength, lang, meta } = block.data;
|
|
78
|
+
return Code.make({
|
|
79
|
+
value: block.stringContent,
|
|
80
|
+
position: context.position(block.startOffset, block.endOffset),
|
|
81
|
+
...lang === void 0 ? {} : { lang },
|
|
82
|
+
...meta === void 0 ? {} : { meta },
|
|
83
|
+
...isFenced === true && fenceChar !== void 0 ? { fenceChar } : {},
|
|
84
|
+
...isFenced === true && fenceLength !== void 0 ? { fenceLength } : {}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
export { codeConstruct };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Root } from "../../MarkdownNode.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/blocks/document.ts
|
|
4
|
+
/** The document root: contains everything except a bare list item. */
|
|
5
|
+
const documentConstruct = {
|
|
6
|
+
type: "document",
|
|
7
|
+
acceptsLines: false,
|
|
8
|
+
canContain: (child) => child !== "listItem",
|
|
9
|
+
continue: () => 0,
|
|
10
|
+
materialize: (block, children, context) => Root.make({
|
|
11
|
+
children,
|
|
12
|
+
position: context.position(block.startOffset, block.endOffset)
|
|
13
|
+
})
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
export { documentConstruct };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/internal/blocks/fencedCode.ts
|
|
2
|
+
const reCodeFence = /^`{3,}(?!.*`)|^~{3,}/;
|
|
3
|
+
const fenceCharOf = (char) => char === "`" || char === "~" ? char : void 0;
|
|
4
|
+
/** The fenced-code block start: three or more backticks or tildes. */
|
|
5
|
+
const fencedCodeStart = {
|
|
6
|
+
name: "fencedCode",
|
|
7
|
+
trigger: (scanner) => {
|
|
8
|
+
if (scanner.indented) return 0;
|
|
9
|
+
const match = reCodeFence.exec(scanner.currentLine.slice(scanner.nextNonspace));
|
|
10
|
+
if (match === null) return 0;
|
|
11
|
+
const fenceChar = fenceCharOf(match[0].charAt(0));
|
|
12
|
+
if (fenceChar === void 0) return 0;
|
|
13
|
+
scanner.closeUnmatchedBlocks();
|
|
14
|
+
const container = scanner.addChild("code", scanner.nextNonspace);
|
|
15
|
+
container.data.isFenced = true;
|
|
16
|
+
container.data.fenceLength = match[0].length;
|
|
17
|
+
container.data.fenceChar = fenceChar;
|
|
18
|
+
container.data.fenceOffset = scanner.indent;
|
|
19
|
+
scanner.advanceNextNonspace();
|
|
20
|
+
scanner.advanceOffset(match[0].length, false);
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { fencedCodeStart };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { FootnoteDefinition } from "../../MarkdownNode.js";
|
|
2
|
+
import { flowChildren } from "../blockTypes.js";
|
|
3
|
+
import { stickyOf } from "../patterns.js";
|
|
4
|
+
import { normalizeLabelText } from "../references.js";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/blocks/footnoteDefinition.ts
|
|
7
|
+
/**
|
|
8
|
+
* `_scan_footnote_definition` from `src/scanners.re`:
|
|
9
|
+
*
|
|
10
|
+
* ```re2c
|
|
11
|
+
* '[^' ([^\] \r\n\x00\t]+) ']:' [ \t]*
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The label class is the whole grammar and the whole surprise: a footnote
|
|
15
|
+
* label may hold no whitespace at all, which is where it parts company with a
|
|
16
|
+
* link reference label. The line terminators in the class are unreachable here
|
|
17
|
+
* (the block pass hands over one line at a time, terminator stripped) but stay
|
|
18
|
+
* so the class reads as upstream's.
|
|
19
|
+
*/
|
|
20
|
+
const reFootnoteDefinition = /\[\^([^\]\0\t\n\r ]+)\]:[ \t]*/;
|
|
21
|
+
/** Columns of indentation a continuation line must carry. */
|
|
22
|
+
const CONTINUATION_INDENT = 4;
|
|
23
|
+
/**
|
|
24
|
+
* FootnoteDefinition: a container whose children are flow blocks, continued by
|
|
25
|
+
* four columns of indentation.
|
|
26
|
+
*/
|
|
27
|
+
const footnoteDefinitionConstruct = {
|
|
28
|
+
type: "footnoteDefinition",
|
|
29
|
+
acceptsLines: false,
|
|
30
|
+
canContain: (child) => child !== "listItem",
|
|
31
|
+
continue: (scanner) => {
|
|
32
|
+
if (scanner.indent >= CONTINUATION_INDENT) {
|
|
33
|
+
scanner.advanceOffset(CONTINUATION_INDENT, true);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
return scanner.currentLine.length === 0 ? 0 : 1;
|
|
37
|
+
},
|
|
38
|
+
materialize: (block, children, context) => {
|
|
39
|
+
const footnote = block.data.footnote;
|
|
40
|
+
if (footnote === void 0) return;
|
|
41
|
+
return FootnoteDefinition.make({
|
|
42
|
+
identifier: footnote.identifier,
|
|
43
|
+
label: footnote.label,
|
|
44
|
+
children: flowChildren(children),
|
|
45
|
+
position: context.position(block.startOffset, block.endOffset)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The footnote-definition block start: an unindented `[^label]:`.
|
|
51
|
+
*
|
|
52
|
+
* Registered at the CORE position cmark-gfm runs it from — after the thematic
|
|
53
|
+
* break, before the list marker — rather than with the GFM extensions at the
|
|
54
|
+
* end of the table, because that is literally where the branch sits in
|
|
55
|
+
* `open_new_blocks`. It carries no `cont_type == PARAGRAPH` guard, so unlike
|
|
56
|
+
* the thematic break above it, a footnote definition interrupts a paragraph.
|
|
57
|
+
*/
|
|
58
|
+
const footnoteDefinitionStart = {
|
|
59
|
+
name: "footnoteDefinition",
|
|
60
|
+
trigger: (scanner) => {
|
|
61
|
+
if (scanner.indented) return 0;
|
|
62
|
+
const sticky = stickyOf(reFootnoteDefinition);
|
|
63
|
+
sticky.lastIndex = scanner.nextNonspace;
|
|
64
|
+
const found = sticky.exec(scanner.currentLine);
|
|
65
|
+
const rawLabel = found?.[1];
|
|
66
|
+
if (found === null || rawLabel === void 0) return 0;
|
|
67
|
+
const key = normalizeLabelText(rawLabel);
|
|
68
|
+
if (key === "") return 0;
|
|
69
|
+
scanner.closeUnmatchedBlocks();
|
|
70
|
+
const start = scanner.nextNonspace;
|
|
71
|
+
scanner.advanceNextNonspace();
|
|
72
|
+
scanner.advanceOffset(found[0].length, false);
|
|
73
|
+
const block = scanner.addChild("footnoteDefinition", start);
|
|
74
|
+
block.data.footnote = {
|
|
75
|
+
key,
|
|
76
|
+
identifier: key.toLowerCase(),
|
|
77
|
+
label: rawLabel
|
|
78
|
+
};
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { footnoteDefinitionConstruct, footnoteDefinitionStart };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/internal/blocks/frontmatter.ts
|
|
2
|
+
const FENCES = /* @__PURE__ */ new Map([
|
|
3
|
+
["---", {
|
|
4
|
+
format: "yaml",
|
|
5
|
+
close: "---"
|
|
6
|
+
}],
|
|
7
|
+
["+++", {
|
|
8
|
+
format: "toml",
|
|
9
|
+
close: "+++"
|
|
10
|
+
}],
|
|
11
|
+
["---json", {
|
|
12
|
+
format: "json",
|
|
13
|
+
close: "---"
|
|
14
|
+
}]
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Scan the head of a preprocessed document for a frontmatter block.
|
|
18
|
+
*
|
|
19
|
+
* `lines` is the preprocessor's line table (U+0000 already replaced,
|
|
20
|
+
* terminators stripped, absolute `start` offsets); `text` is the original
|
|
21
|
+
* source, consulted only for the terminators between value lines — a
|
|
22
|
+
* terminator can never contain U+0000, so slicing it from the source is
|
|
23
|
+
* exact, and the value keeps CRLF interiors verbatim while the line content
|
|
24
|
+
* keeps the preprocessor's U+FFFD replacement.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` when the document has no frontmatter — which is the common
|
|
27
|
+
* case and never an error.
|
|
28
|
+
*/
|
|
29
|
+
const scanFrontmatter = (lines, text) => {
|
|
30
|
+
const opening = lines[0];
|
|
31
|
+
if (opening === void 0 || opening.start !== 0) return null;
|
|
32
|
+
const rule = FENCES.get(opening.text);
|
|
33
|
+
if (rule === void 0) return null;
|
|
34
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
35
|
+
const line = lines[index];
|
|
36
|
+
if (line === void 0 || line.text !== rule.close) continue;
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (let inner = 1; inner < index; inner += 1) {
|
|
39
|
+
const current = lines[inner];
|
|
40
|
+
const next = lines[inner + 1];
|
|
41
|
+
if (current === void 0 || next === void 0) break;
|
|
42
|
+
parts.push(current.text);
|
|
43
|
+
if (inner + 1 < index) parts.push(text.slice(current.start + current.text.length, next.start));
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
format: rule.format,
|
|
47
|
+
value: parts.join(""),
|
|
48
|
+
lineCount: index + 1,
|
|
49
|
+
endOffset: line.start + line.text.length
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { scanFrontmatter };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Html } from "../../MarkdownNode.js";
|
|
2
|
+
import { peekCode } from "../preprocess.js";
|
|
3
|
+
import { CLOSETAG, OPENTAG } from "../htmlTags.js";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/blocks/htmlBlock.ts
|
|
6
|
+
const C_LESSTHAN = 60;
|
|
7
|
+
const reHtmlBlockOpen = [
|
|
8
|
+
/./,
|
|
9
|
+
/^<(?:script|pre|textarea|style)(?:\s|>|$)/i,
|
|
10
|
+
/^<!--/,
|
|
11
|
+
/^<[?]/,
|
|
12
|
+
/^<![A-Za-z]/,
|
|
13
|
+
/^<!\[CDATA\[/,
|
|
14
|
+
/^<[/]?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|search|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|[/]?[>]|$)/i,
|
|
15
|
+
new RegExp(`^(?:${OPENTAG}|${CLOSETAG})\\s*$`, "i")
|
|
16
|
+
];
|
|
17
|
+
const reHtmlBlockClose = [
|
|
18
|
+
/./,
|
|
19
|
+
/<\/(?:script|pre|textarea|style)>/i,
|
|
20
|
+
/-->/,
|
|
21
|
+
/\?>/,
|
|
22
|
+
/>/,
|
|
23
|
+
/\]\]>/
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Whether `rest` closes `block` — only types 1 through 5 have a closing
|
|
27
|
+
* pattern, and the line loop calls this after the line has been appended.
|
|
28
|
+
*/
|
|
29
|
+
const isHtmlBlockEnd = (block, rest) => {
|
|
30
|
+
const type = block.data.htmlBlockType;
|
|
31
|
+
if (type === void 0 || type < 1 || type > 5) return false;
|
|
32
|
+
return reHtmlBlockClose[type]?.test(rest) ?? false;
|
|
33
|
+
};
|
|
34
|
+
/** HTML block: absorbs lines verbatim, contains nothing. */
|
|
35
|
+
const htmlBlockConstruct = {
|
|
36
|
+
type: "html",
|
|
37
|
+
acceptsLines: true,
|
|
38
|
+
canContain: () => false,
|
|
39
|
+
continue: (scanner, block) => {
|
|
40
|
+
const type = block.data.htmlBlockType;
|
|
41
|
+
return scanner.blank && (type === 6 || type === 7) ? 1 : 0;
|
|
42
|
+
},
|
|
43
|
+
finalize: (_scanner, block) => {
|
|
44
|
+
block.stringContent = block.stringContent.replace(/\n$/, "");
|
|
45
|
+
},
|
|
46
|
+
materialize: (block, _children, context) => Html.make({
|
|
47
|
+
value: block.stringContent,
|
|
48
|
+
position: context.position(block.startOffset, block.endOffset)
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
/** The HTML block start: one of seven open conditions after an unindented `<`. */
|
|
52
|
+
const htmlBlockStart = {
|
|
53
|
+
name: "htmlBlock",
|
|
54
|
+
trigger: (scanner, container) => {
|
|
55
|
+
if (scanner.indented || peekCode(scanner.currentLine, scanner.nextNonspace) !== C_LESSTHAN) return 0;
|
|
56
|
+
const rest = scanner.currentLine.slice(scanner.nextNonspace);
|
|
57
|
+
for (let blockType = 1; blockType <= 7; blockType += 1) {
|
|
58
|
+
const opens = reHtmlBlockOpen[blockType]?.test(rest) ?? false;
|
|
59
|
+
const mayOpen = blockType < 7 || container.type !== "paragraph" && !(!scanner.allClosed && !scanner.blank && scanner.tip.type === "paragraph");
|
|
60
|
+
if (opens && mayOpen) {
|
|
61
|
+
scanner.closeUnmatchedBlocks();
|
|
62
|
+
const block = scanner.addChild("html", scanner.offset);
|
|
63
|
+
block.data.htmlBlockType = blockType;
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { htmlBlockConstruct, htmlBlockStart, isHtmlBlockEnd };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/internal/blocks/indentedCode.ts
|
|
2
|
+
/** The indented-code block start: four columns of indentation. */
|
|
3
|
+
const indentedCodeStart = {
|
|
4
|
+
name: "indentedCode",
|
|
5
|
+
trigger: (scanner) => {
|
|
6
|
+
if (!scanner.indented || scanner.tip.type === "paragraph" || scanner.blank) return 0;
|
|
7
|
+
const start = scanner.offset;
|
|
8
|
+
scanner.advanceOffset(4, true);
|
|
9
|
+
scanner.closeUnmatchedBlocks();
|
|
10
|
+
scanner.addChild("code", start);
|
|
11
|
+
return 2;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { indentedCodeStart };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Definition } from "../../MarkdownNode.js";
|
|
2
|
+
import { makeBlockNode } from "../blockTypes.js";
|
|
3
|
+
import { parseReference } from "../references.js";
|
|
4
|
+
import { sliceWithSegments, sourceOffsetAt } from "../segments.js";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/blocks/linkReferenceDefinition.ts
|
|
7
|
+
const C_OPEN_BRACKET = 91;
|
|
8
|
+
/**
|
|
9
|
+
* Split every leading link reference definition out of `block`, inserting one
|
|
10
|
+
* `definition` node per definition into `block`'s parent, immediately before
|
|
11
|
+
* `block` itself.
|
|
12
|
+
*
|
|
13
|
+
* Definitions can only ever start a paragraph — they cannot interrupt one —
|
|
14
|
+
* so a single leading pass is exhaustive.
|
|
15
|
+
*/
|
|
16
|
+
const extractDefinitions = (block) => {
|
|
17
|
+
const parent = block.parent;
|
|
18
|
+
if (parent === void 0) return;
|
|
19
|
+
while (block.stringContent.charCodeAt(0) === C_OPEN_BRACKET) {
|
|
20
|
+
const reference = parseReference(block.stringContent);
|
|
21
|
+
if (reference === void 0) return;
|
|
22
|
+
const consumed = block.stringContent.slice(0, reference.length);
|
|
23
|
+
const startOffset = sourceOffsetAt(block.segments, 0, block.startOffset);
|
|
24
|
+
const linesConsumed = (consumed.match(/\n/g) ?? []).length;
|
|
25
|
+
const contentLength = consumed.replace(/[ \t\r\n]+$/, "").length;
|
|
26
|
+
const endOffset = sourceOffsetAt(block.segments, contentLength, startOffset);
|
|
27
|
+
const contentLines = (consumed.slice(0, contentLength).match(/\n/g) ?? []).length;
|
|
28
|
+
const node = makeBlockNode("definition", startOffset, block.startLine, block.depth);
|
|
29
|
+
node.parent = parent;
|
|
30
|
+
node.open = false;
|
|
31
|
+
node.endOffset = endOffset;
|
|
32
|
+
node.endLine = block.startLine + contentLines;
|
|
33
|
+
node.data.definition = {
|
|
34
|
+
key: reference.key,
|
|
35
|
+
identifier: reference.identifier,
|
|
36
|
+
label: reference.label,
|
|
37
|
+
url: reference.url,
|
|
38
|
+
...reference.title === void 0 ? {} : { title: reference.title }
|
|
39
|
+
};
|
|
40
|
+
const at = parent.children.indexOf(block);
|
|
41
|
+
parent.children.splice(at === -1 ? parent.children.length : at, 0, node);
|
|
42
|
+
const remaining = sliceWithSegments(block.segments, reference.length, block.stringContent.length);
|
|
43
|
+
block.stringContent = block.stringContent.slice(reference.length);
|
|
44
|
+
block.segments.splice(0, block.segments.length, ...remaining);
|
|
45
|
+
block.startLine += linesConsumed;
|
|
46
|
+
block.startOffset = remaining[0]?.sourceOffset ?? endOffset;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Definition: a node with no block start of its own — {@link extractDefinitions}
|
|
51
|
+
* is the only thing that ever creates one, already closed.
|
|
52
|
+
*/
|
|
53
|
+
const definitionConstruct = {
|
|
54
|
+
type: "definition",
|
|
55
|
+
acceptsLines: false,
|
|
56
|
+
canContain: () => false,
|
|
57
|
+
continue: () => 1,
|
|
58
|
+
materialize: (block, _children, context) => {
|
|
59
|
+
const definition = block.data.definition;
|
|
60
|
+
if (definition === void 0) return;
|
|
61
|
+
return Definition.make({
|
|
62
|
+
identifier: definition.identifier,
|
|
63
|
+
label: definition.label,
|
|
64
|
+
url: definition.url,
|
|
65
|
+
position: context.position(block.startOffset, block.endOffset),
|
|
66
|
+
...definition.title === void 0 ? {} : { title: definition.title }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { definitionConstruct, extractDefinitions };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { List, ListItem } from "../../MarkdownNode.js";
|
|
2
|
+
import { flowChildren, listItemChildren } from "../blockTypes.js";
|
|
3
|
+
import { isSpaceOrTab, peekCode } from "../preprocess.js";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/blocks/list.ts
|
|
6
|
+
const reBulletListMarker = /^[*+-]/;
|
|
7
|
+
const reOrderedListMarker = /^(\d{1,9})([.)])/;
|
|
8
|
+
const reNonSpace = /[^ \t\f\v\r\n]/;
|
|
9
|
+
const bulletCharOf = (char) => char === "-" || char === "*" || char === "+" ? char : void 0;
|
|
10
|
+
const delimiterOf = (char) => char === "." || char === ")" ? char : void 0;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a list marker at the scanner's next non-whitespace position,
|
|
13
|
+
* advancing past it on success.
|
|
14
|
+
*/
|
|
15
|
+
const parseListMarker = (scanner, container) => {
|
|
16
|
+
if (scanner.indent >= 4) return;
|
|
17
|
+
const rest = scanner.currentLine.slice(scanner.nextNonspace);
|
|
18
|
+
const markerOffset = scanner.indent;
|
|
19
|
+
let data;
|
|
20
|
+
let markerLength;
|
|
21
|
+
const bullet = reBulletListMarker.exec(rest);
|
|
22
|
+
const ordered = bullet === null ? reOrderedListMarker.exec(rest) : null;
|
|
23
|
+
if (bullet !== null) {
|
|
24
|
+
const bulletChar = bulletCharOf(bullet[0].charAt(0));
|
|
25
|
+
data = {
|
|
26
|
+
type: "bullet",
|
|
27
|
+
tight: true,
|
|
28
|
+
padding: 0,
|
|
29
|
+
markerOffset,
|
|
30
|
+
...bulletChar === void 0 ? {} : { bulletChar }
|
|
31
|
+
};
|
|
32
|
+
markerLength = bullet[0].length;
|
|
33
|
+
} else if (ordered !== null && (container.type !== "paragraph" || Number.parseInt(ordered[1] ?? "", 10) === 1)) {
|
|
34
|
+
const delimiter = delimiterOf(ordered[2] ?? "");
|
|
35
|
+
data = {
|
|
36
|
+
type: "ordered",
|
|
37
|
+
tight: true,
|
|
38
|
+
padding: 0,
|
|
39
|
+
markerOffset,
|
|
40
|
+
start: Number.parseInt(ordered[1] ?? "1", 10),
|
|
41
|
+
...delimiter === void 0 ? {} : { delimiter }
|
|
42
|
+
};
|
|
43
|
+
markerLength = ordered[0].length;
|
|
44
|
+
} else return;
|
|
45
|
+
const afterMarker = peekCode(scanner.currentLine, scanner.nextNonspace + markerLength);
|
|
46
|
+
if (!(afterMarker === -1 || isSpaceOrTab(afterMarker))) return;
|
|
47
|
+
if (container.type === "paragraph" && !reNonSpace.test(scanner.currentLine.slice(scanner.nextNonspace + markerLength))) return;
|
|
48
|
+
scanner.advanceNextNonspace();
|
|
49
|
+
scanner.advanceOffset(markerLength, true);
|
|
50
|
+
const spacesStartColumn = scanner.column;
|
|
51
|
+
const spacesStartOffset = scanner.offset;
|
|
52
|
+
do
|
|
53
|
+
scanner.advanceOffset(1, true);
|
|
54
|
+
while (scanner.column - spacesStartColumn < 5 && isSpaceOrTab(peekCode(scanner.currentLine, scanner.offset)));
|
|
55
|
+
const blankItem = peekCode(scanner.currentLine, scanner.offset) === -1;
|
|
56
|
+
const spacesAfterMarker = scanner.column - spacesStartColumn;
|
|
57
|
+
if (spacesAfterMarker >= 5 || spacesAfterMarker < 1 || blankItem) {
|
|
58
|
+
data.padding = markerLength + 1;
|
|
59
|
+
scanner.setScanPosition(spacesStartOffset, spacesStartColumn);
|
|
60
|
+
if (isSpaceOrTab(peekCode(scanner.currentLine, scanner.offset))) scanner.advanceOffset(1, true);
|
|
61
|
+
} else data.padding = markerLength + spacesAfterMarker;
|
|
62
|
+
return data;
|
|
63
|
+
};
|
|
64
|
+
/** Two markers belong to the same list when type, delimiter and bullet agree. */
|
|
65
|
+
const listsMatch = (listData, itemData) => listData.type === itemData.type && listData.delimiter === itemData.delimiter && listData.bulletChar === itemData.bulletChar;
|
|
66
|
+
/** Whether `block` is followed by a sibling that a blank line separates from it. */
|
|
67
|
+
const endsWithBlankLine = (block, next) => next !== void 0 && block.endLine !== next.startLine - 1;
|
|
68
|
+
/** Whether any of `block`'s children are separated from the next by a blank line. */
|
|
69
|
+
const hasBlankLineBetweenChildren = (block) => block.children.some((child, index) => endsWithBlankLine(child, block.children[index + 1]));
|
|
70
|
+
/** List: a container of list items and nothing else. */
|
|
71
|
+
const listConstruct = {
|
|
72
|
+
type: "list",
|
|
73
|
+
acceptsLines: false,
|
|
74
|
+
canContain: (child) => child === "listItem",
|
|
75
|
+
continue: () => 0,
|
|
76
|
+
finalize: (_scanner, block) => {
|
|
77
|
+
let tight = true;
|
|
78
|
+
for (const [index, item] of block.children.entries()) {
|
|
79
|
+
const itemSpread = hasBlankLineBetweenChildren(item);
|
|
80
|
+
item.data.spread = itemSpread;
|
|
81
|
+
if (endsWithBlankLine(item, block.children[index + 1]) || itemSpread) tight = false;
|
|
82
|
+
}
|
|
83
|
+
const listData = block.data.listData;
|
|
84
|
+
if (listData !== void 0) listData.tight = tight;
|
|
85
|
+
block.data.spread = !tight;
|
|
86
|
+
const last = block.children[block.children.length - 1];
|
|
87
|
+
if (last !== void 0) {
|
|
88
|
+
block.endOffset = last.endOffset;
|
|
89
|
+
block.endLine = last.endLine;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
materialize: (block, children, context) => {
|
|
93
|
+
const listData = block.data.listData;
|
|
94
|
+
const ordered = listData?.type === "ordered";
|
|
95
|
+
return List.make({
|
|
96
|
+
ordered,
|
|
97
|
+
spread: block.data.spread ?? false,
|
|
98
|
+
children: listItemChildren(children),
|
|
99
|
+
position: context.position(block.startOffset, block.endOffset),
|
|
100
|
+
...ordered && listData?.start !== void 0 ? { start: listData.start } : {},
|
|
101
|
+
...listData?.bulletChar === void 0 ? {} : { bulletChar: listData.bulletChar },
|
|
102
|
+
...listData?.delimiter === void 0 ? {} : { delimiter: listData.delimiter }
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/** List item: a container of anything but a bare list item. */
|
|
107
|
+
const listItemConstruct = {
|
|
108
|
+
type: "listItem",
|
|
109
|
+
acceptsLines: false,
|
|
110
|
+
canContain: (child) => child !== "listItem",
|
|
111
|
+
continue: (scanner, block) => {
|
|
112
|
+
const listData = block.data.listData;
|
|
113
|
+
if (listData === void 0) return 1;
|
|
114
|
+
if (scanner.blank) {
|
|
115
|
+
if (block.children.length === 0) return 1;
|
|
116
|
+
scanner.advanceNextNonspace();
|
|
117
|
+
} else if (scanner.indent >= listData.markerOffset + listData.padding) scanner.advanceOffset(listData.markerOffset + listData.padding, true);
|
|
118
|
+
else return 1;
|
|
119
|
+
return 0;
|
|
120
|
+
},
|
|
121
|
+
finalize: (_scanner, block) => {
|
|
122
|
+
const last = block.children[block.children.length - 1];
|
|
123
|
+
if (last !== void 0) {
|
|
124
|
+
block.endOffset = last.endOffset;
|
|
125
|
+
block.endLine = last.endLine;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
block.endLine = block.startLine;
|
|
129
|
+
block.endOffset = block.startOffset + (block.data.listData?.padding ?? 0);
|
|
130
|
+
},
|
|
131
|
+
materialize: (block, children, context) => ListItem.make({
|
|
132
|
+
spread: block.data.spread ?? false,
|
|
133
|
+
children: flowChildren(children),
|
|
134
|
+
position: context.position(block.startOffset, block.endOffset),
|
|
135
|
+
...block.data.checked === void 0 ? {} : { checked: block.data.checked }
|
|
136
|
+
})
|
|
137
|
+
};
|
|
138
|
+
/** The list-item block start: a bullet or ordered marker. */
|
|
139
|
+
const listItemStart = {
|
|
140
|
+
name: "listItem",
|
|
141
|
+
trigger: (scanner, container) => {
|
|
142
|
+
if (scanner.indented && container.type !== "list") return 0;
|
|
143
|
+
const data = parseListMarker(scanner, container);
|
|
144
|
+
if (data === void 0) return 0;
|
|
145
|
+
scanner.closeUnmatchedBlocks();
|
|
146
|
+
const openList = scanner.tip;
|
|
147
|
+
const openListData = openList.data.listData;
|
|
148
|
+
if (openList.type !== "list" || openListData === void 0 || !listsMatch(openListData, data)) {
|
|
149
|
+
const list = scanner.addChild("list", scanner.nextNonspace);
|
|
150
|
+
list.data.listData = data;
|
|
151
|
+
}
|
|
152
|
+
const item = scanner.addChild("listItem", scanner.nextNonspace);
|
|
153
|
+
item.data.listData = data;
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
//#endregion
|
|
159
|
+
export { listConstruct, listItemConstruct, listItemStart };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Paragraph } from "../../MarkdownNode.js";
|
|
2
|
+
import { extractDefinitions } from "./linkReferenceDefinition.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/blocks/paragraph.ts
|
|
5
|
+
/** Paragraph: absorbs lines until a blank one, and contains nothing. */
|
|
6
|
+
const paragraphConstruct = {
|
|
7
|
+
type: "paragraph",
|
|
8
|
+
acceptsLines: true,
|
|
9
|
+
canContain: () => false,
|
|
10
|
+
continue: (scanner) => scanner.blank ? 1 : 0,
|
|
11
|
+
finalize: (_scanner, block) => {
|
|
12
|
+
extractDefinitions(block);
|
|
13
|
+
},
|
|
14
|
+
materialize: (block, _children, context) => {
|
|
15
|
+
const inline = context.inlineSlice(block);
|
|
16
|
+
if (inline.text.length === 0) return;
|
|
17
|
+
const node = Paragraph.make({
|
|
18
|
+
children: inline.children,
|
|
19
|
+
position: context.position(block.startOffset, block.endOffset)
|
|
20
|
+
});
|
|
21
|
+
context.registerInline(node, inline);
|
|
22
|
+
return node;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { paragraphConstruct };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { extractDefinitions } from "./linkReferenceDefinition.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/blocks/setextHeading.ts
|
|
4
|
+
const reSetextHeadingLine = /^(?:=+|-+)[ \t]*$/;
|
|
5
|
+
/** The setext heading block start: a run of `=` or `-` under a paragraph. */
|
|
6
|
+
const setextHeadingStart = {
|
|
7
|
+
name: "setextHeading",
|
|
8
|
+
trigger: (scanner, container) => {
|
|
9
|
+
if (scanner.indented || container.type !== "paragraph") return 0;
|
|
10
|
+
const match = reSetextHeadingLine.exec(scanner.currentLine.slice(scanner.nextNonspace));
|
|
11
|
+
if (match === null) return 0;
|
|
12
|
+
scanner.closeUnmatchedBlocks();
|
|
13
|
+
extractDefinitions(container);
|
|
14
|
+
if (container.stringContent.length === 0) return 0;
|
|
15
|
+
const heading = scanner.replaceBlock(container, "heading");
|
|
16
|
+
heading.data.level = match[0].charAt(0) === "=" ? 1 : 2;
|
|
17
|
+
heading.data.headingStyle = "setext";
|
|
18
|
+
scanner.advanceOffset(scanner.currentLine.length - scanner.offset, false);
|
|
19
|
+
return 2;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { setextHeadingStart };
|