@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,212 @@
|
|
|
1
|
+
import { ReferenceScanner, normalizeReference } from "../references.js";
|
|
2
|
+
import { appendChild, childrenOf, insertAfter, makeInlineNode, unlink } from "../inlineNode.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/inlines/link.ts
|
|
5
|
+
const C_BANG = 33;
|
|
6
|
+
const C_CARET = 94;
|
|
7
|
+
const C_OPEN_BRACKET = 91;
|
|
8
|
+
const C_CLOSE_BRACKET = 93;
|
|
9
|
+
const C_OPEN_PAREN = 40;
|
|
10
|
+
const C_CLOSE_PAREN = 41;
|
|
11
|
+
const reWhitespaceChar = /^[ \t\n\v\f\r]/;
|
|
12
|
+
/**
|
|
13
|
+
* Run one of `references.ts`'s grammar functions over the subject at the
|
|
14
|
+
* scanner's cursor, keeping the two cursors in step.
|
|
15
|
+
*
|
|
16
|
+
* The destination, title and label grammars are identical wherever they
|
|
17
|
+
* appear, so a link reuses exactly what a definition parsed with.
|
|
18
|
+
*/
|
|
19
|
+
const withReferenceScanner = (scanner, run) => {
|
|
20
|
+
const reference = new ReferenceScanner(scanner.subject);
|
|
21
|
+
reference.pos = scanner.pos;
|
|
22
|
+
const result = run(reference);
|
|
23
|
+
scanner.pos = reference.pos;
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The plain-text flattening of a node list — an image's `alt`.
|
|
28
|
+
*
|
|
29
|
+
* ITERATIVE ON PURPOSE. This runs while the brackets close, before
|
|
30
|
+
* materialization's depth guard has seen anything, and the content it walks
|
|
31
|
+
* can nest as deeply as the input has balanced delimiters: the recursive
|
|
32
|
+
* spelling died with a `RangeError` — a defect, not a typed error — on
|
|
33
|
+
* `![` plus ten thousand nested emphasis markers. A flattening has no reason
|
|
34
|
+
* to recurse, so it does not, and needs no cap.
|
|
35
|
+
*/
|
|
36
|
+
const plainTextOf = (nodes) => {
|
|
37
|
+
let text = "";
|
|
38
|
+
const pending = [...nodes].reverse();
|
|
39
|
+
while (pending.length > 0) {
|
|
40
|
+
const node = pending.pop();
|
|
41
|
+
if (node === void 0) break;
|
|
42
|
+
if (node.type === "text" || node.type === "inlineCode") text += node.value;
|
|
43
|
+
else if (node.type === "image" || node.type === "imageReference") text += node.value;
|
|
44
|
+
else {
|
|
45
|
+
const children = childrenOf(node);
|
|
46
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
47
|
+
const child = children[index];
|
|
48
|
+
if (child !== void 0) pending.push(child);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
};
|
|
54
|
+
/** `[` — a potential link opener. */
|
|
55
|
+
const linkOpenConstruct = {
|
|
56
|
+
name: "linkOpen",
|
|
57
|
+
triggers: [C_OPEN_BRACKET],
|
|
58
|
+
parse: (scanner) => {
|
|
59
|
+
const startpos = scanner.pos;
|
|
60
|
+
scanner.pos += 1;
|
|
61
|
+
const node = scanner.appendText("[", startpos, scanner.pos);
|
|
62
|
+
scanner.addBracket(node, startpos, false);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Build the `!` construct — an image opener when a `[` follows, otherwise
|
|
68
|
+
* literal.
|
|
69
|
+
*
|
|
70
|
+
* `caretOpensImage` is the second GFM footnote seam, and the surprising one.
|
|
71
|
+
* cmark-gfm's bang handler reads
|
|
72
|
+
*
|
|
73
|
+
* ```c
|
|
74
|
+
* if (peek_char(subj) == '[' && peek_char_n(subj, 1) != '^') {
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* so under GFM `![^` NEVER opens an image: the `!` stays literal text and the
|
|
78
|
+
* `[` becomes an ordinary bracket, which is what lets `text![^1]` render as a
|
|
79
|
+
* literal `!` followed by a footnote reference (`extensions.txt` example 23
|
|
80
|
+
* pins exactly that). It also means the footnote branch in the close-bracket
|
|
81
|
+
* handler can never see an image opener, which is why that branch does not
|
|
82
|
+
* test for one. CommonMark passes `true` and is untouched.
|
|
83
|
+
*/
|
|
84
|
+
const makeImageOpenConstruct = (caretOpensImage) => ({
|
|
85
|
+
name: "imageOpen",
|
|
86
|
+
triggers: [C_BANG],
|
|
87
|
+
parse: (scanner) => {
|
|
88
|
+
const startpos = scanner.pos;
|
|
89
|
+
scanner.pos += 1;
|
|
90
|
+
if (scanner.peek() === C_OPEN_BRACKET && (caretOpensImage || scanner.subject.charCodeAt(scanner.pos + 1) !== C_CARET)) {
|
|
91
|
+
scanner.pos += 1;
|
|
92
|
+
const node = scanner.appendText("![", startpos, scanner.pos);
|
|
93
|
+
scanner.addBracket(node, startpos + 1, true);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
scanner.appendText("!", startpos, scanner.pos);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
/** `!` — an image opener when a `[` follows, otherwise literal. */
|
|
101
|
+
const imageOpenConstruct = makeImageOpenConstruct(true);
|
|
102
|
+
/**
|
|
103
|
+
* Build the `]` construct — closes a link or image, or stays literal.
|
|
104
|
+
*
|
|
105
|
+
* `onNoMatch` is the dialect seam described on {@link LinkCloseFallback}. With
|
|
106
|
+
* none, this is CommonMark's close-bracket handler exactly.
|
|
107
|
+
*/
|
|
108
|
+
const makeLinkCloseConstruct = (onNoMatch) => ({
|
|
109
|
+
name: "linkClose",
|
|
110
|
+
triggers: [C_CLOSE_BRACKET],
|
|
111
|
+
parse: (scanner) => {
|
|
112
|
+
const bracketPos = scanner.pos;
|
|
113
|
+
scanner.pos += 1;
|
|
114
|
+
const startpos = scanner.pos;
|
|
115
|
+
const opener = scanner.brackets;
|
|
116
|
+
if (opener === void 0) {
|
|
117
|
+
scanner.appendText("]", bracketPos, scanner.pos);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (!opener.active) {
|
|
121
|
+
scanner.appendText("]", bracketPos, scanner.pos);
|
|
122
|
+
scanner.removeBracket();
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const isImage = opener.image;
|
|
126
|
+
const savepos = scanner.pos;
|
|
127
|
+
let url;
|
|
128
|
+
let title;
|
|
129
|
+
let matched = false;
|
|
130
|
+
if (scanner.peek() === C_OPEN_PAREN) {
|
|
131
|
+
scanner.pos += 1;
|
|
132
|
+
const inline = withReferenceScanner(scanner, (reference) => {
|
|
133
|
+
reference.spnl();
|
|
134
|
+
const destination = reference.parseLinkDestination();
|
|
135
|
+
if (destination === void 0) return;
|
|
136
|
+
reference.spnl();
|
|
137
|
+
const parsedTitle = reWhitespaceChar.test(reference.subject.charAt(reference.pos - 1)) ? reference.parseLinkTitle() : void 0;
|
|
138
|
+
reference.spnl();
|
|
139
|
+
if (reference.peek() !== C_CLOSE_PAREN) return;
|
|
140
|
+
reference.pos += 1;
|
|
141
|
+
return {
|
|
142
|
+
destination,
|
|
143
|
+
parsedTitle
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
if (inline === void 0) scanner.pos = savepos;
|
|
147
|
+
else {
|
|
148
|
+
url = inline.destination;
|
|
149
|
+
title = inline.parsedTitle;
|
|
150
|
+
matched = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let identifier;
|
|
154
|
+
let rawLabel;
|
|
155
|
+
let referenceType = "shortcut";
|
|
156
|
+
if (!matched) {
|
|
157
|
+
const beforeLabel = scanner.pos;
|
|
158
|
+
const labelLength = withReferenceScanner(scanner, (reference) => reference.parseLinkLabel());
|
|
159
|
+
if (labelLength > 2) {
|
|
160
|
+
rawLabel = scanner.subject.slice(beforeLabel, beforeLabel + labelLength);
|
|
161
|
+
referenceType = "full";
|
|
162
|
+
} else if (opener.bracketAfter !== true) {
|
|
163
|
+
rawLabel = scanner.subject.slice(opener.index, startpos);
|
|
164
|
+
referenceType = labelLength === 2 ? "collapsed" : "shortcut";
|
|
165
|
+
}
|
|
166
|
+
if (labelLength === 0) scanner.pos = savepos;
|
|
167
|
+
if (rawLabel !== void 0) {
|
|
168
|
+
const key = normalizeReference(rawLabel);
|
|
169
|
+
if (scanner.refmap.has(key)) {
|
|
170
|
+
identifier = key.toLowerCase();
|
|
171
|
+
matched = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!matched) {
|
|
176
|
+
if (onNoMatch?.(scanner, opener, bracketPos, startpos) === true) return true;
|
|
177
|
+
scanner.removeBracket();
|
|
178
|
+
scanner.pos = startpos;
|
|
179
|
+
scanner.appendText("]", bracketPos, scanner.pos);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
const node = makeInlineNode(identifier === void 0 ? isImage ? "image" : "link" : isImage ? "imageReference" : "linkReference", opener.node.start, scanner.pos);
|
|
183
|
+
let child = opener.node.next;
|
|
184
|
+
while (child !== void 0) {
|
|
185
|
+
const following = child.next;
|
|
186
|
+
appendChild(node, child);
|
|
187
|
+
child = following;
|
|
188
|
+
}
|
|
189
|
+
insertAfter(opener.node, node);
|
|
190
|
+
if (identifier === void 0) {
|
|
191
|
+
if (url !== void 0) node.data.url = url;
|
|
192
|
+
if (title !== void 0) node.data.title = title;
|
|
193
|
+
} else {
|
|
194
|
+
node.data.identifier = identifier;
|
|
195
|
+
node.data.label = rawLabel === void 0 ? identifier : rawLabel.slice(1, -1);
|
|
196
|
+
node.data.referenceType = referenceType;
|
|
197
|
+
}
|
|
198
|
+
scanner.processEmphasis(opener.previousDelimiter);
|
|
199
|
+
scanner.removeBracket();
|
|
200
|
+
unlink(opener.node);
|
|
201
|
+
if (isImage) {
|
|
202
|
+
node.value = plainTextOf(childrenOf(node));
|
|
203
|
+
for (const orphan of childrenOf(node)) unlink(orphan);
|
|
204
|
+
} else scanner.deactivateLinkOpeners();
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
/** `]` — closes a link or image, or stays literal. The CommonMark spelling. */
|
|
209
|
+
const linkCloseConstruct = makeLinkCloseConstruct();
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
export { imageOpenConstruct, linkCloseConstruct, linkOpenConstruct, makeImageOpenConstruct, makeLinkCloseConstruct };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { reHtmlTag } from "../htmlTags.js";
|
|
2
|
+
import { makeInlineNode } from "../inlineNode.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/inlines/rawHtml.ts
|
|
5
|
+
const C_LESSTHAN = 60;
|
|
6
|
+
/** The raw-HTML forms that scan forward for a fixed closing sequence. */
|
|
7
|
+
const UNTERMINATED_FORMS = [
|
|
8
|
+
["<!--", "-->"],
|
|
9
|
+
["<?", "?>"],
|
|
10
|
+
["<![CDATA[", "]]>"]
|
|
11
|
+
];
|
|
12
|
+
/** A raw HTML tag, comment, processing instruction, declaration or CDATA. */
|
|
13
|
+
const rawHtmlConstruct = {
|
|
14
|
+
name: "rawHtml",
|
|
15
|
+
triggers: [C_LESSTHAN],
|
|
16
|
+
parse: (scanner) => {
|
|
17
|
+
const from = scanner.pos;
|
|
18
|
+
for (const [opener, closer] of UNTERMINATED_FORMS) if (scanner.subject.startsWith(opener, from) && !scanner.hasAhead(closer)) return false;
|
|
19
|
+
const matched = scanner.match(reHtmlTag);
|
|
20
|
+
if (matched === void 0) return false;
|
|
21
|
+
scanner.append(makeInlineNode("html", from, scanner.pos, matched));
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { rawHtmlConstruct };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { appendChild, insertAfter, makeInlineNode, unlink } from "../inlineNode.js";
|
|
2
|
+
import { scanDelims } from "./emphasis.js";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/inlines/strikethrough.ts
|
|
5
|
+
/** The `~` character code — the construct's only trigger. */
|
|
6
|
+
const C_TILDE = 126;
|
|
7
|
+
/**
|
|
8
|
+
* Consume a `~` run as literal text and, when it is a viable one- or
|
|
9
|
+
* two-tilde run, push it onto the shared delimiter stack.
|
|
10
|
+
*
|
|
11
|
+
* The run becomes a text node either way: a `~` that never pairs has to
|
|
12
|
+
* survive as the character it is.
|
|
13
|
+
*/
|
|
14
|
+
const handleTilde = (scanner) => {
|
|
15
|
+
const res = scanDelims(scanner, 126);
|
|
16
|
+
if (res === void 0) return false;
|
|
17
|
+
const startpos = scanner.pos;
|
|
18
|
+
scanner.pos += res.numdelims;
|
|
19
|
+
const node = scanner.appendText(scanner.subject.slice(startpos, scanner.pos), startpos, scanner.pos);
|
|
20
|
+
if ((res.canOpen || res.canClose) && (res.numdelims === 1 || res.numdelims === 2)) {
|
|
21
|
+
const delimiter = {
|
|
22
|
+
cc: 126,
|
|
23
|
+
numdelims: res.numdelims,
|
|
24
|
+
origdelims: res.numdelims,
|
|
25
|
+
node,
|
|
26
|
+
previous: scanner.delimiters,
|
|
27
|
+
next: void 0,
|
|
28
|
+
canOpen: res.canOpen,
|
|
29
|
+
canClose: res.canClose
|
|
30
|
+
};
|
|
31
|
+
if (delimiter.previous !== void 0) delimiter.previous.next = delimiter;
|
|
32
|
+
scanner.delimiters = delimiter;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Pair `opener` with `closer` into a `delete` node — upstream's `insert`.
|
|
38
|
+
*
|
|
39
|
+
* Returns the delimiter the caller's loop continues from, which upstream
|
|
40
|
+
* captures as `closer->next` BEFORE it starts unlinking, so both the matched
|
|
41
|
+
* and the mismatched path resume at the same place.
|
|
42
|
+
*/
|
|
43
|
+
const insertStrikethrough = (scanner, opener, closer) => {
|
|
44
|
+
const resume = closer.next;
|
|
45
|
+
const openerNode = opener.node;
|
|
46
|
+
const closerNode = closer.node;
|
|
47
|
+
if (openerNode.value.length === closerNode.value.length) {
|
|
48
|
+
const delims = openerNode.value.length;
|
|
49
|
+
openerNode.value = "";
|
|
50
|
+
openerNode.end -= delims;
|
|
51
|
+
closerNode.value = "";
|
|
52
|
+
closerNode.start += delims;
|
|
53
|
+
const strikethrough = makeInlineNode("delete", openerNode.end, closerNode.start);
|
|
54
|
+
let between = openerNode.next;
|
|
55
|
+
while (between !== void 0 && between !== closerNode) {
|
|
56
|
+
const following = between.next;
|
|
57
|
+
appendChild(strikethrough, between);
|
|
58
|
+
between = following;
|
|
59
|
+
}
|
|
60
|
+
insertAfter(openerNode, strikethrough);
|
|
61
|
+
unlink(openerNode);
|
|
62
|
+
unlink(closerNode);
|
|
63
|
+
}
|
|
64
|
+
let delimiter = closer;
|
|
65
|
+
while (delimiter !== void 0 && delimiter !== opener) {
|
|
66
|
+
const previous = delimiter.previous;
|
|
67
|
+
scanner.removeDelimiter(delimiter);
|
|
68
|
+
delimiter = previous;
|
|
69
|
+
}
|
|
70
|
+
scanner.removeDelimiter(opener);
|
|
71
|
+
return resume;
|
|
72
|
+
};
|
|
73
|
+
/** GFM strikethrough: `~foo~` and `~~foo~~`. */
|
|
74
|
+
const strikethroughConstruct = {
|
|
75
|
+
name: "strikethrough",
|
|
76
|
+
triggers: [126],
|
|
77
|
+
parse: (scanner) => handleTilde(scanner)
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { insertStrikethrough, strikethroughConstruct };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/internal/inlines/text.ts
|
|
2
|
+
const reMain = /^[^\n`[\]\\!<&*_'"]+/;
|
|
3
|
+
const reMainGfm = /^[^\n`[\]\\!<&*_'"~w:]+/;
|
|
4
|
+
/** Consume a run of ordinary characters, stopping before any construct's. */
|
|
5
|
+
const runConstruct = (name, pattern) => ({
|
|
6
|
+
name,
|
|
7
|
+
triggers: [],
|
|
8
|
+
parse: (scanner) => {
|
|
9
|
+
const from = scanner.pos;
|
|
10
|
+
const matched = scanner.match(pattern);
|
|
11
|
+
if (matched === void 0) return false;
|
|
12
|
+
scanner.appendText(matched, from, scanner.pos);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
/** Ordinary text: the fallback construct, with no trigger of its own. */
|
|
17
|
+
const textConstruct = runConstruct("text", reMain);
|
|
18
|
+
/** Ordinary text under `gfm`, which has three more characters to yield to. */
|
|
19
|
+
const gfmTextConstruct = runConstruct("text", reMainGfm);
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
export { gfmTextConstruct, textConstruct };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//#region src/internal/lineIndex.ts
|
|
2
|
+
/**
|
|
3
|
+
* A precomputed index of line-start offsets over a fixed source text,
|
|
4
|
+
* answering `positionAt(offset)` in `O(log n)` via binary search rather than
|
|
5
|
+
* rescanning the text per query.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Only `\n` begins a new line. A `\r` immediately before a `\n` (CRLF) is
|
|
9
|
+
* left as the trailing character of the prior line — the pair still reads as
|
|
10
|
+
* a single boundary because the following `\n` is what advances the line
|
|
11
|
+
* counter, not the `\r`. A bare `\r` (old Mac line endings) is not
|
|
12
|
+
* recognized as a break; CommonMark's preprocessing pass normalizes those
|
|
13
|
+
* before this index is ever built.
|
|
14
|
+
*/
|
|
15
|
+
var LineIndex = class LineIndex {
|
|
16
|
+
text;
|
|
17
|
+
lineStarts;
|
|
18
|
+
constructor(text, lineStarts) {
|
|
19
|
+
this.text = text;
|
|
20
|
+
this.lineStarts = lineStarts;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a {@link LineIndex} over `text` with one forward scan.
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* Recognizes only `\n`, which is why the block pass hands over its own
|
|
27
|
+
* line table instead — see {@link LineIndex.fromLineStarts}.
|
|
28
|
+
*/
|
|
29
|
+
static make(text) {
|
|
30
|
+
const lineStarts = [0];
|
|
31
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) lineStarts.push(i + 1);
|
|
32
|
+
return new LineIndex(text, lineStarts);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build a {@link LineIndex} over `text` from a line table someone else
|
|
36
|
+
* already computed.
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* The parser splits lines on `\r\n`, `\n` AND a bare `\r`; this index's
|
|
40
|
+
* own scan recognizes only `\n`. For any document without a bare `\r` the
|
|
41
|
+
* two agree, but for one with them they disagree about what a line is, and
|
|
42
|
+
* every reported line number would be wrong. Handing the parser's own
|
|
43
|
+
* table over removes the possibility rather than documenting it: there is
|
|
44
|
+
* one definition of a line, and it belongs to the preprocessor.
|
|
45
|
+
*
|
|
46
|
+
* `starts` must be ascending and begin at 0; a table that is neither is a
|
|
47
|
+
* wiring bug and dies as a defect.
|
|
48
|
+
*/
|
|
49
|
+
static fromLineStarts(text, starts) {
|
|
50
|
+
if (starts.length === 0 || starts[0] !== 0) throw new TypeError("line index: a line table must be non-empty and start at offset 0");
|
|
51
|
+
return new LineIndex(text, starts);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The 1-based `{ line, column }` of `offset` within this index's text.
|
|
55
|
+
* Out-of-range offsets clamp to the nearest valid position (`0` or
|
|
56
|
+
* `text.length`) rather than throwing.
|
|
57
|
+
*/
|
|
58
|
+
positionAt(offset) {
|
|
59
|
+
const clamped = Math.min(Math.max(offset, 0), this.text.length);
|
|
60
|
+
let low = 0;
|
|
61
|
+
let high = this.lineStarts.length - 1;
|
|
62
|
+
while (low < high) {
|
|
63
|
+
const mid = low + high + 1 >> 1;
|
|
64
|
+
if ((this.lineStarts[mid] ?? 0) <= clamped) low = mid;
|
|
65
|
+
else high = mid - 1;
|
|
66
|
+
}
|
|
67
|
+
const lineStart = this.lineStarts[low] ?? 0;
|
|
68
|
+
return {
|
|
69
|
+
line: low + 1,
|
|
70
|
+
column: clamped - lineStart + 1
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { LineIndex };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/internal/patterns.ts
|
|
2
|
+
const stickyCache = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
const globalCache = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
const clone = (pattern, flag, dropCaret) => {
|
|
5
|
+
const source = dropCaret && pattern.source.startsWith("^") ? pattern.source.slice(1) : pattern.source;
|
|
6
|
+
return new RegExp(source, `${pattern.flags.replace(/[gy]/g, "")}${flag}`);
|
|
7
|
+
};
|
|
8
|
+
/** The sticky twin of `pattern`, anchored at `lastIndex`. */
|
|
9
|
+
const stickyOf = (pattern) => {
|
|
10
|
+
const cached = stickyCache.get(pattern);
|
|
11
|
+
if (cached !== void 0) return cached;
|
|
12
|
+
const made = clone(pattern, "y", true);
|
|
13
|
+
stickyCache.set(pattern, made);
|
|
14
|
+
return made;
|
|
15
|
+
};
|
|
16
|
+
/** The global twin of `pattern`, searching forward from `lastIndex`. */
|
|
17
|
+
const globalOf = (pattern) => {
|
|
18
|
+
const cached = globalCache.get(pattern);
|
|
19
|
+
if (cached !== void 0) return cached;
|
|
20
|
+
const made = clone(pattern, "g", false);
|
|
21
|
+
globalCache.set(pattern, made);
|
|
22
|
+
return made;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { globalOf, stickyOf };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//#region src/internal/preprocess.ts
|
|
2
|
+
/** The tab stop width CommonMark fixes for indentation arithmetic. */
|
|
3
|
+
const TAB_STOP = 4;
|
|
4
|
+
/**
|
|
5
|
+
* Split `text` into lines, preserving each line's absolute start offset.
|
|
6
|
+
*
|
|
7
|
+
* `\r\n`, `\n` and `\r` all terminate a line. U+0000 is replaced with U+FFFD
|
|
8
|
+
* per the spec's security note; the replacement is length-preserving so
|
|
9
|
+
* offsets stay aligned with the original source.
|
|
10
|
+
*
|
|
11
|
+
* A single trailing `\n` does not produce a final empty line (upstream's
|
|
12
|
+
* rule — the document "foo\n" is one line, not two). A trailing bare `\r`
|
|
13
|
+
* does, which is also upstream's behavior: the extra blank line is inert.
|
|
14
|
+
*/
|
|
15
|
+
const preprocessLines = (text) => {
|
|
16
|
+
const lines = [];
|
|
17
|
+
let start = 0;
|
|
18
|
+
let index = 0;
|
|
19
|
+
while (index < text.length) {
|
|
20
|
+
const code = text.charCodeAt(index);
|
|
21
|
+
if (code === 10) {
|
|
22
|
+
lines.push({
|
|
23
|
+
text: replaceNul(text.slice(start, index)),
|
|
24
|
+
start
|
|
25
|
+
});
|
|
26
|
+
index += 1;
|
|
27
|
+
start = index;
|
|
28
|
+
} else if (code === 13) {
|
|
29
|
+
lines.push({
|
|
30
|
+
text: replaceNul(text.slice(start, index)),
|
|
31
|
+
start
|
|
32
|
+
});
|
|
33
|
+
index += text.charCodeAt(index + 1) === 10 ? 2 : 1;
|
|
34
|
+
start = index;
|
|
35
|
+
} else index += 1;
|
|
36
|
+
}
|
|
37
|
+
if (start < text.length || !text.endsWith("\n")) lines.push({
|
|
38
|
+
text: replaceNul(text.slice(start)),
|
|
39
|
+
start
|
|
40
|
+
});
|
|
41
|
+
return lines;
|
|
42
|
+
};
|
|
43
|
+
/** The spec's security substitution: U+0000 becomes U+FFFD, 1:1 by length. */
|
|
44
|
+
const replaceNul = (line) => line.includes("\0") ? line.replaceAll("\0", "�") : line;
|
|
45
|
+
/** True for the two characters CommonMark treats as horizontal whitespace. */
|
|
46
|
+
const isSpaceOrTab = (code) => code === 32 || code === 9;
|
|
47
|
+
/** The char code at `position`, or `-1` past the end of `line`. */
|
|
48
|
+
const peekCode = (line, position) => position >= 0 && position < line.length ? line.charCodeAt(position) : -1;
|
|
49
|
+
/**
|
|
50
|
+
* Columns from `column` to the next tab stop — the width a `\t` expands to.
|
|
51
|
+
*
|
|
52
|
+
* `column` is the tab-expanded column the block pass tracks, not a character
|
|
53
|
+
* index: a tab at column 2 is two columns wide, at column 4 it is four.
|
|
54
|
+
*/
|
|
55
|
+
const columnsToNextTabStop = (column) => 4 - column % 4;
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { columnsToNextTabStop, isSpaceOrTab, peekCode, preprocessLines, replaceNul };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { parseInlines } from "./inlineParser.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/rawInline.ts
|
|
4
|
+
const reWhitespace = /\s/;
|
|
5
|
+
const reCmarkSpace = /[ \t\n\r]/;
|
|
6
|
+
/**
|
|
7
|
+
* Trim `text` the way commonmark.js does before inline parsing, carrying the
|
|
8
|
+
* segment table along so the surviving characters keep their source offsets.
|
|
9
|
+
*/
|
|
10
|
+
const trimWithSegments = (text, segments, whitespace) => {
|
|
11
|
+
let start = 0;
|
|
12
|
+
let end = text.length;
|
|
13
|
+
while (start < end && whitespace.test(text.charAt(start))) start += 1;
|
|
14
|
+
while (end > start && whitespace.test(text.charAt(end - 1))) end -= 1;
|
|
15
|
+
if (start === 0 && end === text.length) return {
|
|
16
|
+
text,
|
|
17
|
+
segments
|
|
18
|
+
};
|
|
19
|
+
const trimmed = [];
|
|
20
|
+
for (const segment of segments) {
|
|
21
|
+
const from = Math.max(segment.textOffset, start);
|
|
22
|
+
const to = Math.min(segment.textOffset + segment.length, end);
|
|
23
|
+
if (to > from) trimmed.push({
|
|
24
|
+
textOffset: from - start,
|
|
25
|
+
sourceOffset: segment.sourceOffset + (from - segment.textOffset),
|
|
26
|
+
length: to - from
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
text: text.slice(start, end),
|
|
31
|
+
segments: trimmed
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Prepare a leaf block's accumulated content and run the inline pass over it:
|
|
36
|
+
* trim it, keep its source provenance, and parse it into phrasing content.
|
|
37
|
+
*/
|
|
38
|
+
const prepareInline = (block, position, refmap, dialect = "commonmark", footnoteLabels = /* @__PURE__ */ new Set()) => {
|
|
39
|
+
const { text, segments } = trimWithSegments(block.stringContent, block.segments, block.type === "tableCell" ? reCmarkSpace : reWhitespace);
|
|
40
|
+
const first = segments[0];
|
|
41
|
+
const last = segments[segments.length - 1];
|
|
42
|
+
const startOffset = first === void 0 ? block.startOffset : first.sourceOffset;
|
|
43
|
+
return {
|
|
44
|
+
text,
|
|
45
|
+
startOffset,
|
|
46
|
+
endOffset: last === void 0 ? startOffset : last.sourceOffset + last.length,
|
|
47
|
+
segments,
|
|
48
|
+
children: text.length === 0 ? [] : parseInlines({
|
|
49
|
+
text,
|
|
50
|
+
startOffset,
|
|
51
|
+
segments
|
|
52
|
+
}, refmap, position, dialect, footnoteLabels)
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { prepareInline };
|