@promptctl/rich-js 0.23.0 → 0.24.1
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/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/animation.d.ts +78 -0
- package/dist/core/animation.js +107 -0
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +17 -11
- package/dist/core/cells.js +100 -25
- package/dist/core/clock.d.ts +9 -1
- package/dist/core/clock.js +22 -2
- package/dist/core/color.d.ts +10 -0
- package/dist/core/color.js +36 -1
- package/dist/core/console.js +5 -5
- package/dist/core/easing.d.ts +138 -7
- package/dist/core/easing.js +355 -17
- package/dist/core/json.js +2 -1
- package/dist/core/measure.js +2 -2
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +2 -2
- package/dist/core/protocol.d.ts +73 -19
- package/dist/core/protocol.js +56 -15
- package/dist/core/render.js +2 -1
- package/dist/core/segment.d.ts +28 -1
- package/dist/core/segment.js +60 -44
- package/dist/core/strip.d.ts +107 -29
- package/dist/core/strip.js +242 -131
- package/dist/core/style.d.ts +18 -1
- package/dist/core/style.js +49 -9
- package/dist/core/text.js +26 -15
- package/dist/core/timeline.d.ts +84 -0
- package/dist/core/timeline.js +144 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +69 -0
- package/dist/host/app.js +4 -6
- package/dist/host/index.d.ts +1 -1
- package/dist/host/input.d.ts +37 -13
- package/dist/host/input.js +146 -56
- package/dist/host/terminal-host.d.ts +16 -0
- package/dist/host/terminal-host.js +1 -0
- package/dist/index.d.ts +16 -11
- package/dist/index.js +15 -5
- package/dist/node/terminal-host.d.ts +8 -1
- package/dist/node/terminal-host.js +2 -0
- package/dist/renderables/align.d.ts +3 -2
- package/dist/renderables/align.js +3 -0
- package/dist/renderables/columns.d.ts +3 -2
- package/dist/renderables/columns.js +7 -2
- package/dist/renderables/constrain.d.ts +3 -2
- package/dist/renderables/constrain.js +5 -1
- package/dist/renderables/effect.d.ts +83 -30
- package/dist/renderables/effect.js +155 -57
- package/dist/renderables/effects.d.ts +88 -39
- package/dist/renderables/effects.js +444 -105
- package/dist/renderables/embed.d.ts +10 -0
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.d.ts +3 -2
- package/dist/renderables/flexStrip.js +6 -1
- package/dist/renderables/frames.d.ts +116 -0
- package/dist/renderables/frames.js +133 -0
- package/dist/renderables/group.d.ts +3 -2
- package/dist/renderables/group.js +5 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +3 -2
- package/dist/renderables/layout.js +15 -2
- package/dist/renderables/list-view.d.ts +3 -2
- package/dist/renderables/list-view.js +12 -5
- package/dist/renderables/live.d.ts +23 -7
- package/dist/renderables/live.js +69 -35
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1421 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +7 -6
- package/dist/renderables/markdown.js +363 -499
- package/dist/renderables/padding.d.ts +8 -20
- package/dist/renderables/padding.js +32 -50
- package/dist/renderables/panel.d.ts +26 -26
- package/dist/renderables/panel.js +67 -103
- package/dist/renderables/progress.d.ts +5 -3
- package/dist/renderables/progress.js +14 -3
- package/dist/renderables/progressBar.js +4 -4
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +24 -0
- package/dist/renderables/spinner.d.ts +73 -27
- package/dist/renderables/spinner.js +110 -51
- package/dist/renderables/status.d.ts +8 -3
- package/dist/renderables/status.js +8 -5
- package/dist/renderables/syntax.js +2 -1
- package/dist/renderables/table.d.ts +19 -11
- package/dist/renderables/table.js +140 -156
- package/dist/renderables/traceback.d.ts +1 -1
- package/dist/renderables/traceback.js +6 -5
- package/dist/renderables/tree.d.ts +6 -5
- package/dist/renderables/tree.js +11 -7
- package/dist/renderables/viewport.d.ts +13 -5
- package/dist/renderables/viewport.js +41 -20
- package/dist/template-bindings/animation-funcs.d.ts +37 -0
- package/dist/template-bindings/animation-funcs.js +83 -0
- package/dist/template-bindings/color-funcs.d.ts +13 -0
- package/dist/template-bindings/color-funcs.js +1 -1
- package/dist/template-bindings/helpers.d.ts +6 -0
- package/dist/template-bindings/helpers.js +12 -3
- package/dist/template-bindings/index.d.ts +6 -2
- package/dist/template-bindings/index.js +8 -3
- package/dist/themes/ramp.d.ts +18 -3
- package/dist/themes/ramp.js +23 -2
- package/dist/widgets/focus-manager.d.ts +21 -14
- package/dist/widgets/focus-manager.js +28 -29
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/static-item.d.ts +4 -3
- package/dist/widgets/static-item.js +9 -7
- package/dist/widgets/text-input.d.ts +28 -18
- package/dist/widgets/text-input.js +144 -101
- package/dist/widgets/widget-app.d.ts +19 -3
- package/dist/widgets/widget-app.js +39 -21
- package/dist/widgets/widget-base.js +0 -3
- package/package.json +2 -5
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown's parse, with no dependency: a source read into blocks, then the
|
|
3
|
+
* text of each block read into inlines — the two passes markdown-it, Rich's
|
|
4
|
+
* parser, makes. Nothing here reads a `RenderOptions`, so a `Markdown` parses
|
|
5
|
+
* its source once, when it is made, as Rich's does, and every render after
|
|
6
|
+
* only walks the tokens.
|
|
7
|
+
*
|
|
8
|
+
* Block structure is read as CommonMark reads it: a blockquote and a list
|
|
9
|
+
* item are both containers, their lines tokenized as Markdown of their own
|
|
10
|
+
* once the container's marker or indentation is cut off. A table, which
|
|
11
|
+
* CommonMark has none of, and an HTML block are read as markdown-it reads
|
|
12
|
+
* them.
|
|
13
|
+
*/
|
|
14
|
+
import { linkHref, normalizeLinkText } from "./markdown-url.js";
|
|
15
|
+
import { PY_WHITESPACE, pyStrip } from "./python-space.js";
|
|
16
|
+
import { namedReference } from "./html-entities.js";
|
|
17
|
+
/** A line of the source as the top level holds it. */
|
|
18
|
+
function sourceLine(text) {
|
|
19
|
+
return { text, marked: 0, quoteStart: 0, margin: 0, lead: advance(0, LEAD.exec(text)[0]), lazy: false };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A container's lines, each made the first time its block pass reads it, as
|
|
23
|
+
* markdown-it's block pass reads a container's lines in place. A container
|
|
24
|
+
* costs the lines its block pass reads, not every line it might take: a quote
|
|
25
|
+
* whose block pass stops at the first lazy continuation line it does not take
|
|
26
|
+
* reads that line, not the rest of the document after it.
|
|
27
|
+
*/
|
|
28
|
+
class Lines {
|
|
29
|
+
source;
|
|
30
|
+
made = [];
|
|
31
|
+
constructor(source) {
|
|
32
|
+
this.source = source;
|
|
33
|
+
}
|
|
34
|
+
/** The line at `i`, or nothing where the container ends before it. */
|
|
35
|
+
at(i) {
|
|
36
|
+
while (this.made.length <= i) {
|
|
37
|
+
const next = this.source.next();
|
|
38
|
+
if (next.done)
|
|
39
|
+
return undefined;
|
|
40
|
+
this.made.push(next.value);
|
|
41
|
+
}
|
|
42
|
+
return this.made[i];
|
|
43
|
+
}
|
|
44
|
+
/** The lines from `start` up to `end`. */
|
|
45
|
+
slice(start, end) {
|
|
46
|
+
this.at(end - 1);
|
|
47
|
+
return this.made.slice(start, end);
|
|
48
|
+
}
|
|
49
|
+
/** How many lines the container holds, every one of them made. */
|
|
50
|
+
count() {
|
|
51
|
+
while (this.at(this.made.length) !== undefined)
|
|
52
|
+
;
|
|
53
|
+
return this.made.length;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** An ATX heading's opening: one to six `#`, then a space, a tab or the end of the line. */
|
|
57
|
+
const HEADING = /^#{1,6}(?=[ \t]|$)/;
|
|
58
|
+
/**
|
|
59
|
+
* An ATX heading's closing sequence, as markdown-it cuts one: a run of `#` at
|
|
60
|
+
* the end of the line, spaces and tabs after it allowed, with a space or tab
|
|
61
|
+
* before it — so `# x#` keeps its `#` and `# #` is an empty heading.
|
|
62
|
+
*/
|
|
63
|
+
const HEADING_CLOSE = /(?<=[ \t])#+[ \t]*$/;
|
|
64
|
+
/** A thematic break: three or more of one of `*`, `-` or `_`, spaces and tabs between them allowed. */
|
|
65
|
+
const RULE = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
|
|
66
|
+
const SETEXT = /^(=+|-+)[ \t]*$/;
|
|
67
|
+
/**
|
|
68
|
+
* A code fence's opening, as markdown-it's fence rule reads one: three or more
|
|
69
|
+
* backticks, the info string after them holding no backtick, or three or more
|
|
70
|
+
* tildes, the info string free to hold anything. The run is captured whole, so
|
|
71
|
+
* its closing can be read off it.
|
|
72
|
+
*/
|
|
73
|
+
const FENCE = /^(`{3,}(?=[^`]*$)|~{3,})/;
|
|
74
|
+
const QUOTE = /^>/;
|
|
75
|
+
const ITEM_MARKER = /^(?:[*+-]|(\d{1,9})[.)])/;
|
|
76
|
+
/**
|
|
77
|
+
* A line's leading space and tab: CommonMark's whitespace, and the only
|
|
78
|
+
* whitespace markdown-it's block pass reads — what indents a line, what makes
|
|
79
|
+
* one blank. A no-break space or any other Unicode space is text to it.
|
|
80
|
+
*/
|
|
81
|
+
const LEAD = /^[ \t]*/;
|
|
82
|
+
const BLOCK_STARTS = [HEADING, FENCE, QUOTE, RULE];
|
|
83
|
+
/** A line that may be a table's delimiter row, screened as markdown-it screens one: pipes, dashes, colons and space only, and not a dash then a space, which opens a list item. */
|
|
84
|
+
const DELIMITER_ROW = /^(?!-[ \t])[-:|][-:| \t]+$/;
|
|
85
|
+
const DELIMITER_CELL = /^:?-+:?$/;
|
|
86
|
+
/**
|
|
87
|
+
* How many empty cells a table's short rows may be filled out with before the
|
|
88
|
+
* table ends, as markdown-it caps them: a header of many columns over many
|
|
89
|
+
* one-cell rows would otherwise grow a few kilobytes of source into millions of cells.
|
|
90
|
+
*/
|
|
91
|
+
const MAX_AUTOCOMPLETED_CELLS = 0x10000;
|
|
92
|
+
/** Indentation that makes a line indented code when it opens a block, as CommonMark counts it. */
|
|
93
|
+
const CODE_INDENT = 4;
|
|
94
|
+
/**
|
|
95
|
+
* markdown-it's `maxNesting`, 20 in the commonmark preset Rich parses with: how
|
|
96
|
+
* deep its blocks nest — a quote one level, a list and its item one each —
|
|
97
|
+
* and how deep it follows a link label holding a bracket holding another.
|
|
98
|
+
* Past it the block parser reads nothing more of a container, and the label
|
|
99
|
+
* scan stops looking, so neither pass recurses further however deep the
|
|
100
|
+
* source goes.
|
|
101
|
+
*/
|
|
102
|
+
const MAX_NESTING = 20;
|
|
103
|
+
/** The block-level element names an HTML block of the sixth kind opens with, as markdown-it-py lists them. */
|
|
104
|
+
const HTML_BLOCK_NAMES = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|" +
|
|
105
|
+
"fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|" +
|
|
106
|
+
"main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|" +
|
|
107
|
+
"title|tr|track|ul";
|
|
108
|
+
// The tag grammar of markdown-it-py's common/html_re, shared by the HTML block a lone tag opens and the inline HTML it reads in text.
|
|
109
|
+
const HTML_ATTRIBUTE = String.raw `(?:${PY_WHITESPACE}+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:${PY_WHITESPACE}*=${PY_WHITESPACE}*(?:[^"'=<>\`\x00-\x20]+|'[^']*'|"[^"]*"))?)`;
|
|
110
|
+
const HTML_OPEN_TAG = String.raw `<[A-Za-z][A-Za-z0-9-]*${HTML_ATTRIBUTE}*${PY_WHITESPACE}*\/?>`;
|
|
111
|
+
const HTML_CLOSE_TAG = String.raw `<\/[A-Za-z][A-Za-z0-9-]*${PY_WHITESPACE}*>`;
|
|
112
|
+
const HTML_BLOCKS = [
|
|
113
|
+
{ start: new RegExp(String.raw `^<(?:script|pre|style|textarea)(?=${PY_WHITESPACE}|>|$)`, "i"), end: /<\/(?:script|pre|style|textarea)>/i, interrupts: true },
|
|
114
|
+
{ start: /^<!--/, end: /-->/, interrupts: true },
|
|
115
|
+
{ start: /^<\?/, end: /\?>/, interrupts: true },
|
|
116
|
+
// Uppercase only, as markdown-it-py has it, where markdown-it in JS and CommonMark 0.31 take `<!doctype` too.
|
|
117
|
+
{ start: /^<![A-Z]/, end: />/, interrupts: true },
|
|
118
|
+
{ start: /^<!\[CDATA\[/, end: /\]\]>/, interrupts: true },
|
|
119
|
+
{ start: new RegExp(String.raw `^<\/?(?:${HTML_BLOCK_NAMES})(?=${PY_WHITESPACE}|\/?>|$)`, "i"), end: "blank", interrupts: true },
|
|
120
|
+
{
|
|
121
|
+
start: new RegExp(String.raw `^(?:${HTML_OPEN_TAG}|${HTML_CLOSE_TAG})${PY_WHITESPACE}*$`),
|
|
122
|
+
end: "blank",
|
|
123
|
+
interrupts: false,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
/** The column `ws` ends at when it starts at column `from`, a tab reaching the next stop of 4 as CommonMark counts it. */
|
|
127
|
+
function advance(from, ws) {
|
|
128
|
+
return [...ws].reduce((col, ch) => (ch === "\t" ? col + 4 - (col % 4) : col + 1), from);
|
|
129
|
+
}
|
|
130
|
+
/** How far `line` is indented inside its container. */
|
|
131
|
+
function indentOf(line) {
|
|
132
|
+
return Math.max(0, line.lead - line.margin);
|
|
133
|
+
}
|
|
134
|
+
/** `line` from its first character that is not leading whitespace. */
|
|
135
|
+
function body(line) {
|
|
136
|
+
const unmarked = line.text.slice(line.marked);
|
|
137
|
+
return unmarked.slice(LEAD.exec(unmarked)[0].length);
|
|
138
|
+
}
|
|
139
|
+
function isBlank(line) {
|
|
140
|
+
return body(line) === "";
|
|
141
|
+
}
|
|
142
|
+
/** Whether `line` sits less than a code indent into its container and its text matches `re`. */
|
|
143
|
+
function startsWith(line, re) {
|
|
144
|
+
return indentOf(line) < CODE_INDENT && re.test(body(line));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* `line` from `n` columns into its container on: a block's own indentation
|
|
148
|
+
* cut off, the rest as written. A tab straddling the cut leaves the columns
|
|
149
|
+
* past it as spaces.
|
|
150
|
+
*/
|
|
151
|
+
function dropColumns(line, n) {
|
|
152
|
+
const to = line.margin + n;
|
|
153
|
+
let col = 0;
|
|
154
|
+
let i = 0;
|
|
155
|
+
for (; i < line.text.length && col < to; i++) {
|
|
156
|
+
const ch = i < line.marked && line.text[i] !== "\t" ? " " : line.text[i];
|
|
157
|
+
if (ch !== " " && ch !== "\t")
|
|
158
|
+
break;
|
|
159
|
+
const next = advance(col, ch);
|
|
160
|
+
if (next > to)
|
|
161
|
+
return " ".repeat(next - to) + line.text.slice(i + 1);
|
|
162
|
+
col = next;
|
|
163
|
+
}
|
|
164
|
+
return line.text.slice(i);
|
|
165
|
+
}
|
|
166
|
+
function itemStart(line) {
|
|
167
|
+
const text = body(line);
|
|
168
|
+
const m = ITEM_MARKER.exec(text);
|
|
169
|
+
if (!m || indentOf(line) >= CODE_INDENT || RULE.test(text))
|
|
170
|
+
return undefined;
|
|
171
|
+
const after = text.slice(m[0].length);
|
|
172
|
+
const spacing = LEAD.exec(after)[0];
|
|
173
|
+
const rest = after.slice(spacing.length);
|
|
174
|
+
if (spacing === "" && rest !== "")
|
|
175
|
+
return undefined;
|
|
176
|
+
const markerEnd = line.lead + m[0].length;
|
|
177
|
+
const spaced = advance(markerEnd, spacing) - markerEnd;
|
|
178
|
+
const content = rest === "" || spaced > CODE_INDENT ? markerEnd + 1 : markerEnd + spaced;
|
|
179
|
+
return {
|
|
180
|
+
marker: m[1] === undefined ? { bullet: m[0] } : { delimiter: m[0].slice(-1), start: parseInt(m[1], 10) },
|
|
181
|
+
indent: content - line.margin,
|
|
182
|
+
listIndent: line.margin - line.quoteStart,
|
|
183
|
+
first: rest === "" ? undefined : { ...line, marked: line.text.length - after.length, margin: content, lead: markerEnd + spaced, lazy: false },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Whether `line` may be a table's row: a line of its container's own, neither lazy paragraph text nor indented code. */
|
|
187
|
+
function tableLine(line) {
|
|
188
|
+
return !line.lazy && indentOf(line) < CODE_INDENT;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A table row's cells, as markdown-it splits one: at every `|` no backslash escapes,
|
|
192
|
+
* a pipe at either end of the row closing it rather than opening an empty
|
|
193
|
+
* cell, and the row and each cell stripped. An escaped `\|` is a `|` in its cell, inside a
|
|
194
|
+
* code span too.
|
|
195
|
+
*/
|
|
196
|
+
function rowCells(row) {
|
|
197
|
+
const cells = [""];
|
|
198
|
+
for (const ch of pyStrip(row)) {
|
|
199
|
+
const last = cells.length - 1;
|
|
200
|
+
if (ch !== "|")
|
|
201
|
+
cells[last] += ch;
|
|
202
|
+
else if (cells[last].endsWith("\\"))
|
|
203
|
+
cells[last] = `${cells[last].slice(0, -1)}|`;
|
|
204
|
+
else
|
|
205
|
+
cells.push("");
|
|
206
|
+
}
|
|
207
|
+
const from = cells[0] === "" ? 1 : 0;
|
|
208
|
+
const to = cells.length > from && cells[cells.length - 1] === "" ? -1 : undefined;
|
|
209
|
+
return cells.slice(from, to).map(pyStrip);
|
|
210
|
+
}
|
|
211
|
+
/** The alignment of each column `line` declares, if it is a table's delimiter row: a run of dashes per column, a colon on the side its cells sit. */
|
|
212
|
+
function alignments(line) {
|
|
213
|
+
const text = body(line);
|
|
214
|
+
if (!tableLine(line) || !DELIMITER_ROW.test(text))
|
|
215
|
+
return undefined;
|
|
216
|
+
const cells = rowCells(text);
|
|
217
|
+
if (!cells.every((cell) => DELIMITER_CELL.test(cell)))
|
|
218
|
+
return undefined;
|
|
219
|
+
return cells.map((cell) => (cell.endsWith(":") ? (cell.startsWith(":") ? "center" : "right") : cell.startsWith(":") ? "left" : undefined));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Whether `line` may be a table's header row: a `tableLine`, or a line lazy
|
|
223
|
+
* only to list items. markdown-it tries a table at a list item's lazy line to
|
|
224
|
+
* end the paragraph open before it, and measures it against the item, which
|
|
225
|
+
* it is short of, so it is never code; a quote's lazy line is never tried.
|
|
226
|
+
*/
|
|
227
|
+
function headerLine(line) {
|
|
228
|
+
return line.lazy === "item" || tableLine(line);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The columns of the table opening at `start`, if one does, as markdown-it
|
|
232
|
+
* reads one: a header row with a pipe in it, then a delimiter row naming as
|
|
233
|
+
* many columns.
|
|
234
|
+
*/
|
|
235
|
+
function tableAt(lines, start) {
|
|
236
|
+
const head = lines.at(start);
|
|
237
|
+
const delimiter = lines.at(start + 1);
|
|
238
|
+
if (delimiter === undefined || !headerLine(head) || !body(head).includes("|"))
|
|
239
|
+
return undefined;
|
|
240
|
+
const aligns = alignments(delimiter);
|
|
241
|
+
if (!aligns)
|
|
242
|
+
return undefined;
|
|
243
|
+
const headers = rowCells(body(head));
|
|
244
|
+
return headers.length === aligns.length ? headers.map((header, n) => ({ header, align: aligns[n] })) : undefined;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Whether two item starts belong to one list: the same bullet, or numbers with
|
|
248
|
+
* the same delimiter — markdown-it's marker character, the two kinds' characters
|
|
249
|
+
* never coinciding.
|
|
250
|
+
*/
|
|
251
|
+
function sameList(a, b) {
|
|
252
|
+
const markerChar = ({ marker }) => ("bullet" in marker ? marker.bullet : marker.delimiter);
|
|
253
|
+
return markerChar(a) === markerChar(b);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A `>` line as a line of its quote, as markdown-it's blockquote reads one:
|
|
257
|
+
* the quote's content starts past the marker and the one column of space that
|
|
258
|
+
* may follow it — one column of a tab, when a tab follows it, the rest of the
|
|
259
|
+
* tab left in the content unless that column was all of it. The whitespace
|
|
260
|
+
* after the marker is measured in `line`'s own columns, and the content is
|
|
261
|
+
* then counted from where the content of `line`'s quote starts, not from
|
|
262
|
+
* `line`'s column 0.
|
|
263
|
+
*/
|
|
264
|
+
function quoted(line) {
|
|
265
|
+
const after = body(line).slice(1);
|
|
266
|
+
const ws = LEAD.exec(after)[0];
|
|
267
|
+
const marked = line.lead + 1;
|
|
268
|
+
const space = ws === "" ? 0 : 1;
|
|
269
|
+
const start = line.lead - line.quoteStart + 1 + space;
|
|
270
|
+
const whole = after[0] === " " || (after[0] === "\t" && marked % 4 === 3);
|
|
271
|
+
const rest = whole ? after.slice(1) : after;
|
|
272
|
+
const text = " ".repeat(start) + rest;
|
|
273
|
+
return { text, marked: 0, quoteStart: start, margin: start, lead: start + advance(marked, ws) - marked - space, lazy: false };
|
|
274
|
+
}
|
|
275
|
+
/** The kind of HTML block `line` opens, if it opens one: the first kind whose opening it matches, as markdown-it tries them in order. */
|
|
276
|
+
function htmlBlockAt(line) {
|
|
277
|
+
return HTML_BLOCKS.find((kind) => startsWith(line, kind.start));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Whether `line` opens a block of its own that may cut short the block before
|
|
281
|
+
* it — a heading, a fence, a quote, a rule, a list item or an HTML block of
|
|
282
|
+
* any kind but a lone tag. A line that does not is text, and under an open
|
|
283
|
+
* paragraph it continues that paragraph.
|
|
284
|
+
*/
|
|
285
|
+
function opensBlock(line) {
|
|
286
|
+
return BLOCK_STARTS.some((re) => startsWith(line, re)) || itemStart(line) !== undefined || htmlBlockAt(line)?.interrupts === true;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Whether `line` ends a paragraph: `opensBlock`, read as CommonMark reads it
|
|
290
|
+
* under a paragraph. Indented code cannot interrupt one; a list item does only
|
|
291
|
+
* when it has text and, numbered, counts from 1, so prose wrapped onto "2024.
|
|
292
|
+
* That year" stays prose; and a line of `=` or `-` is the paragraph's heading
|
|
293
|
+
* underline.
|
|
294
|
+
*/
|
|
295
|
+
function endsParagraph(line) {
|
|
296
|
+
if (underline(line))
|
|
297
|
+
return true;
|
|
298
|
+
const item = itemStart(line);
|
|
299
|
+
return item ? item.first !== undefined && ("bullet" in item.marker || item.marker.start === 1) : opensBlock(line);
|
|
300
|
+
}
|
|
301
|
+
/** The setext underline `line` is — a line of `=` or `-` under a paragraph — if it is one. A lazy line never is. */
|
|
302
|
+
function underline(line) {
|
|
303
|
+
return line && !line.lazy && indentOf(line) < CODE_INDENT ? SETEXT.exec(body(line)) : null;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Whether `line` has text and opens no block of its own: what a container
|
|
307
|
+
* takes short of its marker or indentation as a lazy continuation line, and
|
|
308
|
+
* what a definition reads on through, as markdown-it's `getNextLine` hands one
|
|
309
|
+
* over. A line outside a container is read without the paragraph under it, so
|
|
310
|
+
* any list item — whatever it counts from — ends the container rather than
|
|
311
|
+
* continuing it. Whether a paragraph or a definition open before a lazy line
|
|
312
|
+
* takes it is the container's own block pass to find, which ends the
|
|
313
|
+
* container where neither does.
|
|
314
|
+
*/
|
|
315
|
+
function continuesText(line) {
|
|
316
|
+
return !isBlank(line) && !opensBlock(line);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The text of the paragraph begun at `start`, every line after it up to a
|
|
320
|
+
* blank, a line that ends a paragraph or a table's header row, and the index of the line that ended
|
|
321
|
+
* it. The lines are joined at their line endings as written, each cut only
|
|
322
|
+
* to its container's margin, as markdown-it's `getLines` cuts paragraph
|
|
323
|
+
* content: the indentation past it is the inline pass's to read, which drops
|
|
324
|
+
* it after a line ending and keeps it in a code span, as is whether a line
|
|
325
|
+
* ending breaks soft or hard, since an escape or a code span decides it.
|
|
326
|
+
*/
|
|
327
|
+
function continuation(lines, start) {
|
|
328
|
+
let i = start + 1;
|
|
329
|
+
for (let line = lines.at(i); line !== undefined && !isBlank(line) && !endsParagraph(line) && !tableAt(lines, i); line = lines.at(++i))
|
|
330
|
+
;
|
|
331
|
+
return { text: pyStrip(lines.slice(start, i).map((line) => dropColumns(line, 0)).join("\n")), next: i };
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* markdown-it's `normalizeReference`, which makes a definition's label and a
|
|
335
|
+
* reference's the same key where CommonMark matches them: stripped, each run
|
|
336
|
+
* of whitespace one space, as Python reads whitespace, then case-folded as
|
|
337
|
+
* `lower().upper()` folds it.
|
|
338
|
+
*/
|
|
339
|
+
function normalizeReference(label) {
|
|
340
|
+
return pyStrip(label).replace(WHITESPACE_RUN, " ").toLowerCase().toUpperCase();
|
|
341
|
+
}
|
|
342
|
+
const WHITESPACE_RUN = new RegExp(`${PY_WHITESPACE}+`, "g");
|
|
343
|
+
/**
|
|
344
|
+
* The link reference definition opening at `start`, if one does, as
|
|
345
|
+
* markdown-it's reference rule reads it: `[label]:`, space, a destination, and
|
|
346
|
+
* a title only if space came before it, each of them free to run onto the
|
|
347
|
+
* lines after it that `getNextLine` hands over — up to a blank line, or a line
|
|
348
|
+
* that opens a block of its own. The title and the space after it must end
|
|
349
|
+
* the line; where a title is followed by more, the definition is read again
|
|
350
|
+
* without it, ending with its destination's line. `next` is the index of the
|
|
351
|
+
* line after it.
|
|
352
|
+
*/
|
|
353
|
+
function definitionAt(lines, start) {
|
|
354
|
+
if (!body(lines.at(start)).startsWith("["))
|
|
355
|
+
return undefined;
|
|
356
|
+
// The definition's text so far, each line from its first character that is
|
|
357
|
+
// not leading whitespace and ended by its line ending. markdown-it takes a
|
|
358
|
+
// line on only as a read reaches the end of the last; this takes them on in
|
|
359
|
+
// runs that double, which changes no read but the destination's — every
|
|
360
|
+
// other read that reaches the end takes more on — and copies the text of a
|
|
361
|
+
// definition running over n lines log n times rather than n.
|
|
362
|
+
let text = `${body(lines.at(start))}\n`;
|
|
363
|
+
let taken = start + 1;
|
|
364
|
+
let run = 1;
|
|
365
|
+
const takeLines = () => {
|
|
366
|
+
const more = [];
|
|
367
|
+
for (; more.length < run && continuesDefinition(lines, taken); taken++)
|
|
368
|
+
more.push(`${body(lines.at(taken))}\n`);
|
|
369
|
+
run *= 2;
|
|
370
|
+
return (text += more.join(""));
|
|
371
|
+
};
|
|
372
|
+
// markdown-it's space between the parts: spaces and tabs, and line endings, taking lines on at each.
|
|
373
|
+
const spaceEnd = (from) => {
|
|
374
|
+
let i = from;
|
|
375
|
+
for (; i < text.length && (text[i] === " " || text[i] === "\t" || text[i] === "\n"); i++)
|
|
376
|
+
if (i === text.length - 1)
|
|
377
|
+
takeLines();
|
|
378
|
+
return i;
|
|
379
|
+
};
|
|
380
|
+
// The label: up to the first `]` no backslash escapes, with no `[` in it.
|
|
381
|
+
let labelEnd = 1;
|
|
382
|
+
for (; labelEnd < text.length && text[labelEnd] !== "]"; labelEnd++) {
|
|
383
|
+
if (text[labelEnd] === "[")
|
|
384
|
+
return undefined;
|
|
385
|
+
if (text[labelEnd] === "\\")
|
|
386
|
+
labelEnd++;
|
|
387
|
+
if (labelEnd === text.length - 1)
|
|
388
|
+
takeLines();
|
|
389
|
+
}
|
|
390
|
+
if (text[labelEnd + 1] !== ":")
|
|
391
|
+
return undefined;
|
|
392
|
+
const destinationStart = spaceEnd(labelEnd + 2);
|
|
393
|
+
// markdown-it reads the destination in the text it holds, which ends with the destination's own line.
|
|
394
|
+
const held = text.slice(0, text.indexOf("\n", destinationStart) + 1);
|
|
395
|
+
const destination = destinationAt(held, destinationStart);
|
|
396
|
+
// An empty destination outside angle brackets is none, as markdown-it's `parseLinkDestination` reads it.
|
|
397
|
+
if (destination === undefined || destination.end === destinationStart)
|
|
398
|
+
return undefined;
|
|
399
|
+
const url = linkHref(unescapeAll(destination.raw));
|
|
400
|
+
if (url === undefined)
|
|
401
|
+
return undefined;
|
|
402
|
+
// The line ending the definition ends at.
|
|
403
|
+
let end = held.length - 1;
|
|
404
|
+
// A destination that took its line's ending, behind a backslash, ends the
|
|
405
|
+
// definition at it: markdown-it holds no text past it to read a title in.
|
|
406
|
+
if (destination.end < held.length) {
|
|
407
|
+
const titleStart = spaceEnd(destination.end);
|
|
408
|
+
const title = titleStart > destination.end ? titleAt(text, titleStart, takeLines) : undefined;
|
|
409
|
+
// The definition ends at its line's end, past the spaces and tabs there.
|
|
410
|
+
const lineEnd = (from) => from + matchAt(LINE_INDENT, text, from)[0].length;
|
|
411
|
+
end = lineEnd(title?.end ?? destination.end);
|
|
412
|
+
// markdown-it gives up only a title with text in it for what follows it on its line, so an empty one followed by more ends no definition.
|
|
413
|
+
if (text[end] !== "\n" && title?.text === true)
|
|
414
|
+
end = lineEnd(destination.end);
|
|
415
|
+
if (text[end] !== "\n")
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
const label = normalizeReference(text.slice(1, labelEnd));
|
|
419
|
+
if (label === "")
|
|
420
|
+
return undefined;
|
|
421
|
+
// The line after it: one past the line its end is on.
|
|
422
|
+
let next = start + 1;
|
|
423
|
+
for (let i = text.indexOf("\n"); i < end; i = text.indexOf("\n", i + 1))
|
|
424
|
+
next++;
|
|
425
|
+
return { label, url, next };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Whether the line at `at` continues a definition, as markdown-it's
|
|
429
|
+
* `getNextLine` hands one over: text, and not a table's header row.
|
|
430
|
+
*/
|
|
431
|
+
function continuesDefinition(lines, at) {
|
|
432
|
+
const line = lines.at(at);
|
|
433
|
+
return line !== undefined && continuesText(line) && !tableAt(lines, at);
|
|
434
|
+
}
|
|
435
|
+
/** The character that closes a link title, by the one that opens it. */
|
|
436
|
+
const TITLE_CLOSE = { '"': '"', "'": "'", "(": ")" };
|
|
437
|
+
/**
|
|
438
|
+
* The link title opening at `start` in `text`, as markdown-it's
|
|
439
|
+
* `parseLinkTitle` reads one in a definition: in `"`, `'` or `(…)`, a
|
|
440
|
+
* backslash taking the character after it and a `(` refusing a `(…)` title,
|
|
441
|
+
* reading on to the lines `takeLines` takes on — handing back the text grown
|
|
442
|
+
* by them, or as it was where it takes none — until it closes. `end` is the
|
|
443
|
+
* index past it, and `text` whether it holds any.
|
|
444
|
+
*/
|
|
445
|
+
function titleAt(text, start, takeLines) {
|
|
446
|
+
const close = TITLE_CLOSE[text[start]];
|
|
447
|
+
if (close === undefined)
|
|
448
|
+
return undefined;
|
|
449
|
+
for (let i = start + 1;; i++) {
|
|
450
|
+
if (i === text.length)
|
|
451
|
+
text = takeLines();
|
|
452
|
+
if (i === text.length)
|
|
453
|
+
return undefined;
|
|
454
|
+
if (text[i] === close)
|
|
455
|
+
return { end: i + 1, text: i > start + 1 };
|
|
456
|
+
if (text[i] === "(" && close === ")")
|
|
457
|
+
return undefined;
|
|
458
|
+
if (text[i] === "\\")
|
|
459
|
+
i++;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* The lines of the list item opened at `start`: its first line's text, then
|
|
464
|
+
* every line after it indented to the content column, each line with text in
|
|
465
|
+
* it short of that a lazy continuation line, and each blank line with a line
|
|
466
|
+
* indented to the content column after it. Blank lines trailing the item are
|
|
467
|
+
* left to whatever follows it, and an item that opens with a blank line ends
|
|
468
|
+
* at the next one: it is empty.
|
|
469
|
+
*/
|
|
470
|
+
function* itemLines(item, lines, start) {
|
|
471
|
+
const held = (line) => !line.lazy && indentOf(line) >= item.indent;
|
|
472
|
+
const inItem = (line) => ({ ...line, margin: line.margin + item.indent, lazy: false });
|
|
473
|
+
const short = (line) => ({ ...line, margin: line.quoteStart + item.listIndent, lazy: "item" });
|
|
474
|
+
// Whether the item holds a line with text yet, which a blank line or a lazy one must come after.
|
|
475
|
+
let text = item.first !== undefined;
|
|
476
|
+
if (item.first !== undefined)
|
|
477
|
+
yield item.first;
|
|
478
|
+
for (let i = start + 1;; i++) {
|
|
479
|
+
let line = lines.at(i);
|
|
480
|
+
let after = i;
|
|
481
|
+
while (line !== undefined && isBlank(line))
|
|
482
|
+
line = lines.at(++after);
|
|
483
|
+
if (line === undefined || (after > i && !(text && held(line))))
|
|
484
|
+
return;
|
|
485
|
+
for (; i < after; i++)
|
|
486
|
+
yield inItem(lines.at(i));
|
|
487
|
+
if (held(line))
|
|
488
|
+
yield inItem(line);
|
|
489
|
+
// A quote's lazy line the item's paragraph takes whatever it holds; any other is lazy to the item.
|
|
490
|
+
else if (text && line.lazy === "quote")
|
|
491
|
+
yield line;
|
|
492
|
+
else if (text && continuesText(outdented(short(line))))
|
|
493
|
+
yield short(line);
|
|
494
|
+
else
|
|
495
|
+
return;
|
|
496
|
+
text = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* A lazy `line` as a block inside its container reads it, indented less than
|
|
501
|
+
* any block — but a list item on an `"item"` line, which markdown-it's list
|
|
502
|
+
* refuses at a line short of the item and a code indent past the list's
|
|
503
|
+
* container.
|
|
504
|
+
*/
|
|
505
|
+
function outdented(line) {
|
|
506
|
+
const read = { ...line, margin: line.lead };
|
|
507
|
+
return line.lazy === "item" && itemStart(read) !== undefined ? line : read;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The lines of the quote opening at `start`: its `>` lines with the marker
|
|
511
|
+
* blanked, and each unmarked line after one with text a lazy continuation line.
|
|
512
|
+
* markdown-it's quote takes a `>` line after its first however far it is
|
|
513
|
+
* indented. A line already lazy where the quote sits is short of its
|
|
514
|
+
* container's indentation, so the quote reads it as markdown-it does, indented
|
|
515
|
+
* less than any block: never a `>` line of its own, and opening whatever block
|
|
516
|
+
* its text starts, however far it is indented — but a list item on a line lazy
|
|
517
|
+
* only to items, which markdown-it's list opens only where the line sits.
|
|
518
|
+
*/
|
|
519
|
+
function* quoteLines(lines, start) {
|
|
520
|
+
// Whether the line before has text, which a lazy line must come after.
|
|
521
|
+
let text = false;
|
|
522
|
+
for (let j = start, line = lines.at(j); line !== undefined; line = lines.at(++j)) {
|
|
523
|
+
if (!line.lazy && QUOTE.test(body(line))) {
|
|
524
|
+
const inQuote = quoted(line);
|
|
525
|
+
text = !isBlank(inQuote);
|
|
526
|
+
yield inQuote;
|
|
527
|
+
}
|
|
528
|
+
else if (text && continuesText(line.lazy ? outdented(line) : line))
|
|
529
|
+
yield { ...line, lazy: "quote" };
|
|
530
|
+
else
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The list item opened at `start`, at `level`: its blocks, read from its
|
|
536
|
+
* lines up to where its own block pass stops — and the index of the line
|
|
537
|
+
* that ended it.
|
|
538
|
+
*/
|
|
539
|
+
function listItem(item, level, lines, start) {
|
|
540
|
+
const { tokens, end } = tokenize(new Lines(itemLines(item, lines, start)), level);
|
|
541
|
+
// An item with no text on its first line holds its lines from the line after it.
|
|
542
|
+
return { children: tokens, next: (item.first === undefined ? start + 1 : start) + end };
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* The line that closes the fence opened by the run `open`: a run of the same
|
|
546
|
+
* marker at least as long, then nothing but spaces and tabs.
|
|
547
|
+
*/
|
|
548
|
+
function closingFence(open) {
|
|
549
|
+
return new RegExp(`^${open}${open[0]}*[ \\t]*$`);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Whether a list item holding `children` swallows the rest of the lines its
|
|
553
|
+
* list sits in, as markdown-it's does when its content is past `MAX_NESTING`:
|
|
554
|
+
* an item's block pass is handed every line to the end of the enclosing quote
|
|
555
|
+
* or document and stops at the first line short of its indentation, and the
|
|
556
|
+
* pass that skips unread content skips to that end. An item whose last block
|
|
557
|
+
* is a list that swallowed them ends in the `unread` that list leaves, and so
|
|
558
|
+
* swallows them too.
|
|
559
|
+
*/
|
|
560
|
+
function swallowsRest(children) {
|
|
561
|
+
return children[children.length - 1]?.type === "unread";
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* The list opened at `start`, at `level`: each item holding its blocks — and,
|
|
565
|
+
* where an item swallowed the rest of `lines`, the `unread` they are, ending
|
|
566
|
+
* where they do.
|
|
567
|
+
*/
|
|
568
|
+
function list(first, level, lines, start) {
|
|
569
|
+
const items = [];
|
|
570
|
+
let i = start;
|
|
571
|
+
// Where the last item's lines end.
|
|
572
|
+
let end = start;
|
|
573
|
+
let swallowed = false;
|
|
574
|
+
for (let item = first; item && sameList(first, item); item = lines.at(i) === undefined ? undefined : itemStart(lines.at(i))) {
|
|
575
|
+
// A list and its item each nest a level, as markdown-it's tokens do.
|
|
576
|
+
const { children, next } = listItem(item, level + 2, lines, i);
|
|
577
|
+
items.push(children);
|
|
578
|
+
swallowed = swallowsRest(children);
|
|
579
|
+
end = swallowed ? lines.count() : next;
|
|
580
|
+
// An empty item with a blank line after it sends markdown-it's list rule two
|
|
581
|
+
// lines on, past no more blank lines, so a second blank line ends the list.
|
|
582
|
+
const after = lines.at(i + 1);
|
|
583
|
+
if (item.first === undefined && after !== undefined && isBlank(after))
|
|
584
|
+
i += 2;
|
|
585
|
+
else
|
|
586
|
+
for (i = end; lines.at(i) !== undefined && isBlank(lines.at(i)); i++)
|
|
587
|
+
;
|
|
588
|
+
}
|
|
589
|
+
const token = { type: "list", start: "start" in first.marker ? first.marker.start : undefined, items };
|
|
590
|
+
if (swallowed)
|
|
591
|
+
return { tokens: [token, { type: "unread" }], next: end };
|
|
592
|
+
// The blank lines after the last item belong to whatever follows the list.
|
|
593
|
+
return { tokens: [token], next: end };
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* `lines` read into blocks at `level`, how deep in containers they sit, and
|
|
597
|
+
* the index of the line the read stopped at: the first lazy continuation
|
|
598
|
+
* line no block before it took, where markdown-it's block pass stops at a
|
|
599
|
+
* line short of its container's indentation, or else the end. At
|
|
600
|
+
* `MAX_NESTING` markdown-it's block pass reads nothing, and neither does this:
|
|
601
|
+
* it skips blank lines and stops at a lazy one before it checks the level, so
|
|
602
|
+
* only content with text in it is left unread.
|
|
603
|
+
*/
|
|
604
|
+
function tokenize(lines, level) {
|
|
605
|
+
const tokens = [];
|
|
606
|
+
let i = 0;
|
|
607
|
+
for (let line = lines.at(i); line !== undefined; line = lines.at(i)) {
|
|
608
|
+
if (isBlank(line)) {
|
|
609
|
+
i++;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (line.lazy)
|
|
613
|
+
break;
|
|
614
|
+
if (level >= MAX_NESTING)
|
|
615
|
+
return { tokens: [{ type: "unread" }], end: lines.count() };
|
|
616
|
+
// Table: before every other block, as markdown-it tries it, so a header
|
|
617
|
+
// row that reads as a heading or a list item is still a header. Its body
|
|
618
|
+
// rows run to a line `pyStrip` empties or a line that opens a block.
|
|
619
|
+
const columns = tableAt(lines, i);
|
|
620
|
+
if (columns) {
|
|
621
|
+
const rows = [];
|
|
622
|
+
let autocompleted = 0;
|
|
623
|
+
for (let row = lines.at((i += 2)); row !== undefined && pyStrip(row.text.slice(row.marked)) !== "" && tableLine(row) && !opensBlock(row); row = lines.at(++i)) {
|
|
624
|
+
const cells = rowCells(body(row));
|
|
625
|
+
autocompleted += columns.length - cells.length;
|
|
626
|
+
if (autocompleted > MAX_AUTOCOMPLETED_CELLS)
|
|
627
|
+
break;
|
|
628
|
+
rows.push(columns.map((_, n) => cells[n] ?? ""));
|
|
629
|
+
}
|
|
630
|
+
tokens.push({ type: "table", columns, rows });
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
// Indented code: its lines, and the blank lines between them, with the indent cut off
|
|
634
|
+
if (indentOf(line) >= CODE_INDENT) {
|
|
635
|
+
let end = i + 1;
|
|
636
|
+
for (let j = end, code = lines.at(j); code !== undefined && (isBlank(code) || (!code.lazy && indentOf(code) >= CODE_INDENT)); code = lines.at(++j)) {
|
|
637
|
+
if (!isBlank(code))
|
|
638
|
+
end = j + 1;
|
|
639
|
+
}
|
|
640
|
+
tokens.push({ type: "code_block", code: lines.slice(i, end).map((code) => dropColumns(code, CODE_INDENT)).join("\n") });
|
|
641
|
+
i = end;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
// Heading
|
|
645
|
+
const headingMatch = HEADING.exec(body(line));
|
|
646
|
+
if (headingMatch) {
|
|
647
|
+
// [LAW:parse-dont-validate] `HEADING` matches one to six `#`s, so its length is a level.
|
|
648
|
+
tokens.push({ type: "heading", level: headingMatch[0].length, text: pyStrip(body(line).slice(headingMatch[0].length).replace(HEADING_CLOSE, "")) });
|
|
649
|
+
i++;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
// Horizontal rule
|
|
653
|
+
if (startsWith(line, RULE)) {
|
|
654
|
+
tokens.push({ type: "hr" });
|
|
655
|
+
i++;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
// Fenced code block: its lines with the fence's own indentation cut off,
|
|
659
|
+
// to a closing fence or to the end of its container
|
|
660
|
+
const fence = FENCE.exec(body(line));
|
|
661
|
+
if (fence) {
|
|
662
|
+
const indent = indentOf(line);
|
|
663
|
+
const close = closingFence(fence[1]);
|
|
664
|
+
const codeLines = [];
|
|
665
|
+
for (let code = lines.at(++i); code !== undefined && !code.lazy && !startsWith(code, close); code = lines.at(++i))
|
|
666
|
+
codeLines.push(dropColumns(code, indent));
|
|
667
|
+
if (lines.at(i)?.lazy === false)
|
|
668
|
+
i++; // skip the closing fence
|
|
669
|
+
tokens.push({ type: "code_block", code: codeLines.join("\n") });
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
// Blockquote: Markdown of its own — its `>` lines from the quote's content
|
|
673
|
+
// on, and each unmarked line after one with text a lazy continuation line,
|
|
674
|
+
// up to where its own block pass stops
|
|
675
|
+
if (startsWith(line, QUOTE)) {
|
|
676
|
+
const { tokens: children, end } = tokenize(new Lines(quoteLines(lines, i)), level + 1);
|
|
677
|
+
tokens.push({ type: "blockquote", children });
|
|
678
|
+
i += end;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
// List: its items, each Markdown of its own, as a quote is
|
|
682
|
+
const item = itemStart(line);
|
|
683
|
+
if (item) {
|
|
684
|
+
const { tokens: listed, next } = list(item, level, lines, i);
|
|
685
|
+
tokens.push(...listed);
|
|
686
|
+
i = next;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
// Link reference definition
|
|
690
|
+
const definition = definitionAt(lines, i);
|
|
691
|
+
if (definition) {
|
|
692
|
+
tokens.push({ type: "definition", label: definition.label, url: definition.url });
|
|
693
|
+
i = definition.next;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
// HTML block: to the line that closes its kind, or to the end of its
|
|
697
|
+
// container when none does; a closing blank line is left to what follows
|
|
698
|
+
const html = htmlBlockAt(line);
|
|
699
|
+
if (html) {
|
|
700
|
+
const { end } = html;
|
|
701
|
+
const closes = end === "blank" ? isBlank : (closing) => end.test(body(closing));
|
|
702
|
+
for (let at = lines.at(i); at !== undefined && !at.lazy && !closes(at); at = lines.at(++i))
|
|
703
|
+
;
|
|
704
|
+
if (lines.at(i)?.lazy === false && end !== "blank")
|
|
705
|
+
i++;
|
|
706
|
+
tokens.push({ type: "html_block" });
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
// Paragraph, or a setext heading when a line of `=` or `-` underlines it
|
|
710
|
+
const { text, next } = continuation(lines, i);
|
|
711
|
+
const setext = underline(lines.at(next));
|
|
712
|
+
tokens.push(setext
|
|
713
|
+
? { type: "heading", level: setext[1].startsWith("=") ? 1 : 2, text }
|
|
714
|
+
: { type: "paragraph", text });
|
|
715
|
+
i = setext ? next + 1 : next;
|
|
716
|
+
}
|
|
717
|
+
return { tokens, end: i };
|
|
718
|
+
}
|
|
719
|
+
/** markdown-it's `isWhiteSpace`: Unicode's space separators, and tab, line feed, vertical tab, form feed and carriage return. */
|
|
720
|
+
const WHITESPACE = /^[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]$/;
|
|
721
|
+
/** markdown-it's `isPunctChar`, which takes in all of ASCII's punctuation: Unicode's punctuation and symbols. */
|
|
722
|
+
const PUNCTUATION = /^[\p{P}\p{S}]$/u;
|
|
723
|
+
const TICKS = /`+/y;
|
|
724
|
+
const TICK_RUNS = /`+/g;
|
|
725
|
+
const LINE_INDENT = /[ \t]*/y;
|
|
726
|
+
/** markdown-it's autolink: `<`, then text holding no `<` up to the first `>`. */
|
|
727
|
+
const AUTOLINK = /<([^<>]*)>/y;
|
|
728
|
+
/** What an autolink's text must be: a URI, scheme first, or an email address. */
|
|
729
|
+
const AUTOLINK_URI = /^[a-zA-Z][a-zA-Z0-9+.\-]{1,31}:[^<>\x00-\x20]*$/;
|
|
730
|
+
const AUTOLINK_EMAIL = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
731
|
+
/** Space as markdown-it's link rule skips it around a destination and title: a space, tab or line ending, not Unicode's. */
|
|
732
|
+
const LINK_SPACE = /[ \t\n]*/y;
|
|
733
|
+
/**
|
|
734
|
+
* A link title, as markdown-it's `parseLinkTitle` reads one: in `"`, `'` or
|
|
735
|
+
* `(…)`, a backslash taking the character after it, and a `(` refusing a
|
|
736
|
+
* `(…)` title.
|
|
737
|
+
*/
|
|
738
|
+
const LINK_TITLE = /"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|\((?:\\[\s\S]|[^()\\])*\)/y;
|
|
739
|
+
/** How many parentheses markdown-it's `parseLinkDestination` lets a bare destination hold open: one more and it reads none. */
|
|
740
|
+
const MAX_DESTINATION_NESTING = 32;
|
|
741
|
+
/** A backslash escape, as markdown-it reads one: the ASCII punctuation character after it, standing for itself. */
|
|
742
|
+
const ESCAPE = /\\([!-\/:-@[-`{-~])/y;
|
|
743
|
+
/** A numeric character reference, as markdown-it reads one in text: up to seven decimal digits, or up to six hex. */
|
|
744
|
+
const NUMERIC_REFERENCE = /&#([xX][0-9a-fA-F]{1,6}|[0-9]{1,7});/y;
|
|
745
|
+
/** A named character reference, as markdown-it reads one in text: a letter, then one to 31 letters or digits. */
|
|
746
|
+
const NAMED_REFERENCE = /&([A-Za-z][A-Za-z0-9]{1,31});/y;
|
|
747
|
+
/**
|
|
748
|
+
* A backslash escape or a character reference, as markdown-it's `unescapeAll`
|
|
749
|
+
* reads them in a link's destination: the reference read more widely than in
|
|
750
|
+
* text, a name or a `#` then one to 31 letters or digits.
|
|
751
|
+
*/
|
|
752
|
+
const UNESCAPE_ALL = new RegExp(`${ESCAPE.source}|&[A-Za-z#][A-Za-z0-9]{1,31};`, "g");
|
|
753
|
+
/** A numeric reference as `unescapeAll` reads one: up to eight digits of either base. */
|
|
754
|
+
const DESTINATION_NUMERIC = /^&#([xX][0-9a-fA-F]{1,8}|[0-9]{1,8});$/;
|
|
755
|
+
/** Inline HTML, as markdown-it-py's HTML_TAG_RE reads it: a tag, a comment, a processing instruction, a declaration or a CDATA section. */
|
|
756
|
+
const HTML_TAG = new RegExp([
|
|
757
|
+
HTML_OPEN_TAG,
|
|
758
|
+
HTML_CLOSE_TAG,
|
|
759
|
+
String.raw `<!---?>|<!--(?:[^-]|-[^-]|--[^>])*-->`,
|
|
760
|
+
String.raw `<[?][\s\S]*?[?]>`,
|
|
761
|
+
String.raw `<![A-Za-z][^>]*>`,
|
|
762
|
+
String.raw `<!\[CDATA\[[\s\S]*?\]\]>`,
|
|
763
|
+
].join("|"), "y");
|
|
764
|
+
/** `re`, a sticky pattern, matched exactly at `i` in `text`. */
|
|
765
|
+
function matchAt(re, text, i) {
|
|
766
|
+
re.lastIndex = i;
|
|
767
|
+
return re.exec(text);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* The character a numeric reference to `code` names, unless markdown-it's
|
|
771
|
+
* `isValidEntityCode` refuses it: a surrogate, a noncharacter, DEL, a C0 or
|
|
772
|
+
* C1 control other than tab, line feed, form feed and carriage return, or
|
|
773
|
+
* anything past U+10FFFF.
|
|
774
|
+
*/
|
|
775
|
+
function referencedCharacter(code) {
|
|
776
|
+
const refused = (code >= 0xd800 && code <= 0xdfff) ||
|
|
777
|
+
(code >= 0xfdd0 && code <= 0xfdef) ||
|
|
778
|
+
(code & 0xfffe) === 0xfffe ||
|
|
779
|
+
code <= 0x08 ||
|
|
780
|
+
code === 0x0b ||
|
|
781
|
+
(code >= 0x0e && code <= 0x1f) ||
|
|
782
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
783
|
+
code > 0x10ffff;
|
|
784
|
+
return refused ? undefined : String.fromCodePoint(code);
|
|
785
|
+
}
|
|
786
|
+
/** The code point a numeric reference's `digits` name: hex after an `x`, decimal without one. */
|
|
787
|
+
function referencedCode(digits) {
|
|
788
|
+
return /^[xX]/.test(digits) ? parseInt(digits.slice(1), 16) : parseInt(digits, 10);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* The character reference at `i` in text, as markdown-it's entity rule reads
|
|
792
|
+
* one: a numeric reference as the character it names, or U+FFFD where that is
|
|
793
|
+
* no character it accepts, and a named one only where HTML names it.
|
|
794
|
+
*/
|
|
795
|
+
function referenceAt(text, i) {
|
|
796
|
+
const numeric = matchAt(NUMERIC_REFERENCE, text, i);
|
|
797
|
+
if (numeric) {
|
|
798
|
+
const chars = referencedCharacter(referencedCode(numeric[1])) ?? "\uFFFD";
|
|
799
|
+
return { kind: "text", text: chars, end: i + numeric[0].length };
|
|
800
|
+
}
|
|
801
|
+
const named = matchAt(NAMED_REFERENCE, text, i);
|
|
802
|
+
if (!named)
|
|
803
|
+
return undefined;
|
|
804
|
+
const chars = namedReference(named[1]);
|
|
805
|
+
return chars === undefined ? undefined : { kind: "text", text: chars, end: i + named[0].length };
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* markdown-it's `unescapeAll`, which a link's destination is read through:
|
|
809
|
+
* each backslash escape as the character it escapes, and each reference
|
|
810
|
+
* `UNESCAPE_ALL` finds as the characters it names — a reference naming
|
|
811
|
+
* nothing, or no character `isValidEntityCode` accepts, left as written.
|
|
812
|
+
*/
|
|
813
|
+
function unescapeAll(text) {
|
|
814
|
+
return text.replace(UNESCAPE_ALL, (whole, escaped) => {
|
|
815
|
+
if (escaped !== undefined)
|
|
816
|
+
return escaped;
|
|
817
|
+
const numeric = DESTINATION_NUMERIC.exec(whole);
|
|
818
|
+
const chars = numeric ? referencedCharacter(referencedCode(numeric[1])) : namedReference(whole.slice(1, -1));
|
|
819
|
+
return chars ?? whole;
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* markdown-it's inline state, as much of it as outlives one construct: the
|
|
824
|
+
* document's `definitions` a label is resolved against, the backtick runs of
|
|
825
|
+
* one block's text, and the link-label scan — `parseLinkLabel` stepping over
|
|
826
|
+
* the text a token at a time with `skipToken` — with the cache those steps
|
|
827
|
+
* share across that text. Every text the state is handed is that block's text
|
|
828
|
+
* or a link's label cut from its start at the label's `]`, so a position means
|
|
829
|
+
* the same character in each. A link's label may hold no link: a `[` it meets that
|
|
830
|
+
* opens one refuses the label, so only the innermost of nested links links.
|
|
831
|
+
* Each `[` the scan meets is tried as a link one level deeper, and at
|
|
832
|
+
* `MAX_NESTING` a step lands past the end of the text, as markdown-it's does —
|
|
833
|
+
* which is what keeps the scan shallow and, with the cache, linear in a run of
|
|
834
|
+
* brackets however long.
|
|
835
|
+
*/
|
|
836
|
+
class InlineState {
|
|
837
|
+
definitions;
|
|
838
|
+
// markdown-it's `state.cache`: where a step from a position lands, kept
|
|
839
|
+
// whatever level it was taken at and whatever text it was taken in, as there.
|
|
840
|
+
steps = new Map();
|
|
841
|
+
backticks;
|
|
842
|
+
constructor(definitions, text) {
|
|
843
|
+
this.definitions = definitions;
|
|
844
|
+
this.backticks = new BacktickRuns(text);
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* The index of the `]` closing the label whose `[` is at `open` in `text`,
|
|
848
|
+
* scanned at `level` — or nothing, where no `]` does or, for a link's
|
|
849
|
+
* label, where the label holds a link.
|
|
850
|
+
*/
|
|
851
|
+
labelClose(text, open, level, refusesLinks) {
|
|
852
|
+
let depth = 1;
|
|
853
|
+
for (let i = open + 1; i < text.length;) {
|
|
854
|
+
if (text[i] === "]" && --depth === 0)
|
|
855
|
+
return i;
|
|
856
|
+
const next = this.step(text, i, level);
|
|
857
|
+
if (text[i] === "[") {
|
|
858
|
+
// A `[` stepped over alone is text; one that took more opened a link.
|
|
859
|
+
if (next === i + 1)
|
|
860
|
+
depth++;
|
|
861
|
+
else if (refusesLinks)
|
|
862
|
+
return undefined;
|
|
863
|
+
}
|
|
864
|
+
i = next;
|
|
865
|
+
}
|
|
866
|
+
return undefined;
|
|
867
|
+
}
|
|
868
|
+
/** Where markdown-it's `skipToken` lands from `i` in `text` at `level`: past the construct opening there, if one does, tried a level deeper. */
|
|
869
|
+
step(text, i, level) {
|
|
870
|
+
const cached = this.steps.get(i);
|
|
871
|
+
if (cached !== undefined)
|
|
872
|
+
return cached;
|
|
873
|
+
const next = level >= MAX_NESTING ? text.length + 1 : (inlineAt(text, i, this, level + 1)?.end ?? textRunEnd(text, i));
|
|
874
|
+
this.steps.set(i, next);
|
|
875
|
+
return next;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Where the first whole run of `length` backticks at or after `from` in `text` starts, if one does: the closer of a code span `length` long.
|
|
879
|
+
* A label's cut falls on a `]`, never inside a run, so the runs of `text` are the block's runs that start before its end.
|
|
880
|
+
*/
|
|
881
|
+
codeClose(text, from, length) {
|
|
882
|
+
const close = this.backticks.after(from, length);
|
|
883
|
+
return close !== undefined && close < text.length ? close : undefined;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Every whole backtick run in a text, read once: markdown-it-py's backticks
|
|
888
|
+
* rule keeps `state.backticks` so that an opener with no closer does not
|
|
889
|
+
* rescan the rest of the text, and a run of openers each left unclosed reads
|
|
890
|
+
* the text once, not once each.
|
|
891
|
+
*/
|
|
892
|
+
class BacktickRuns {
|
|
893
|
+
// Where each run starts, by its length, in order.
|
|
894
|
+
starts = new Map();
|
|
895
|
+
constructor(text) {
|
|
896
|
+
for (const run of text.matchAll(TICK_RUNS)) {
|
|
897
|
+
const starts = this.starts.get(run[0].length);
|
|
898
|
+
if (starts)
|
|
899
|
+
starts.push(run.index);
|
|
900
|
+
else
|
|
901
|
+
this.starts.set(run[0].length, [run.index]);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/** Where the first run of `length` at or after `from` starts, if one does. */
|
|
905
|
+
after(from, length) {
|
|
906
|
+
const starts = this.starts.get(length) ?? [];
|
|
907
|
+
let low = 0;
|
|
908
|
+
for (let high = starts.length; low < high;) {
|
|
909
|
+
const middle = (low + high) >> 1;
|
|
910
|
+
if (starts[middle] < from)
|
|
911
|
+
low = middle + 1;
|
|
912
|
+
else
|
|
913
|
+
high = middle;
|
|
914
|
+
}
|
|
915
|
+
return starts[low];
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/** Every character `inlineAt` opens a construct at, and the `]` a link label ends at. */
|
|
919
|
+
const STEP_STOPS = new Set(["`", "*", "_", "~", "&", "\n", "\\", "<", "!", "[", "]"]);
|
|
920
|
+
/**
|
|
921
|
+
* Where markdown-it's `text` rule leaves a step from `i` in `text` that opens
|
|
922
|
+
* nothing: past the whole run of characters that cannot, so a label scan
|
|
923
|
+
* crosses a paragraph of plain text in one step, or past `i` alone where it
|
|
924
|
+
* is a character that could and did not.
|
|
925
|
+
*/
|
|
926
|
+
function textRunEnd(text, i) {
|
|
927
|
+
if (STEP_STOPS.has(text[i]))
|
|
928
|
+
return i + 1;
|
|
929
|
+
let end = i + 1;
|
|
930
|
+
while (end < text.length && !STEP_STOPS.has(text[end]))
|
|
931
|
+
end++;
|
|
932
|
+
return end;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* The `(destination "title")` opening at `open`, as markdown-it's link rule
|
|
936
|
+
* reads it: space, the destination `destinationAt` reads, a title only if
|
|
937
|
+
* space came before it, space, then the `)`. The destination, its backslash
|
|
938
|
+
* escapes and character references read and then made an href as markdown-it
|
|
939
|
+
* makes one, and the index past the `)`. Where markdown-it refuses either,
|
|
940
|
+
* `resume` is one past where its read stopped, which is where its link rule
|
|
941
|
+
* looks for a reference's label instead; and where only space follows the
|
|
942
|
+
* `(`, there is nothing, and the brackets before it are neither.
|
|
943
|
+
*/
|
|
944
|
+
function linkDestination(text, open) {
|
|
945
|
+
const start = linkSpaceEnd(text, open + 1);
|
|
946
|
+
if (start >= text.length)
|
|
947
|
+
return undefined;
|
|
948
|
+
const destination = destinationAt(text, start);
|
|
949
|
+
const href = destination && linkHref(unescapeAll(destination.raw));
|
|
950
|
+
if (destination === undefined || href === undefined)
|
|
951
|
+
return { resume: start + 1 };
|
|
952
|
+
let i = linkSpaceEnd(text, destination.end);
|
|
953
|
+
const title = i > destination.end ? matchAt(LINK_TITLE, text, i) : null;
|
|
954
|
+
if (title)
|
|
955
|
+
i = linkSpaceEnd(text, i + title[0].length);
|
|
956
|
+
return text[i] === ")" ? { url: href, end: i + 1 } : { resume: i + 1 };
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Where the link or image whose label opens at `open` and closes at `close`
|
|
960
|
+
* links, as markdown-it's link and image rules read it: the
|
|
961
|
+
* `(destination "title")` after it, or else a reference — and, for a link,
|
|
962
|
+
* a reference also where that inline form is refused, read from where the
|
|
963
|
+
* refusal leaves it. markdown-it's image rule takes no reference there.
|
|
964
|
+
*/
|
|
965
|
+
function linkTarget(text, open, close, state, level, image) {
|
|
966
|
+
if (text[close + 1] !== "(")
|
|
967
|
+
return reference(text, open, close, close + 1, state, level);
|
|
968
|
+
const destination = linkDestination(text, close + 1);
|
|
969
|
+
if (destination === undefined || "url" in destination)
|
|
970
|
+
return destination;
|
|
971
|
+
return image ? undefined : reference(text, open, close, destination.resume, state, level);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* The definition a reference names, as markdown-it's link and image rules
|
|
975
|
+
* resolve one: a full reference's own label, in the brackets at `from` — or,
|
|
976
|
+
* where those are empty or no brackets close there, the label from `open` to
|
|
977
|
+
* `close`, a collapsed or shortcut reference. The url its definition gives, and
|
|
978
|
+
* the index past the reference; nothing where no definition has its label.
|
|
979
|
+
*/
|
|
980
|
+
function reference(text, open, close, from, state, level) {
|
|
981
|
+
const fullClose = text[from] === "[" ? state.labelClose(text, from, level, false) : undefined;
|
|
982
|
+
const full = fullClose === undefined ? "" : text.slice(from + 1, fullClose);
|
|
983
|
+
const url = state.definitions.get(normalizeReference(full || text.slice(open + 1, close)));
|
|
984
|
+
return url === undefined ? undefined : { url, end: fullClose === undefined ? close + 1 : fullClose + 1 };
|
|
985
|
+
}
|
|
986
|
+
/** The index past the `LINK_SPACE` at `i` in `text`. */
|
|
987
|
+
function linkSpaceEnd(text, i) {
|
|
988
|
+
return i + matchAt(LINK_SPACE, text, i)[0].length;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* The link destination at `start` in `text`, as markdown-it's
|
|
992
|
+
* `parseLinkDestination` reads one, its escapes still in it, and the index
|
|
993
|
+
* past it. In angle brackets it is anything up to the `>` but a line ending or
|
|
994
|
+
* a `<`; bare, it runs to a space or an ASCII control character, or a `)` it
|
|
995
|
+
* did not open, and its parentheses must balance and nest no deeper than
|
|
996
|
+
* `MAX_DESTINATION_NESTING` — the cap that keeps a run of `](` from each
|
|
997
|
+
* scanning to the end of the text. A backslash takes the character after it,
|
|
998
|
+
* but for a space, which ends a bare destination before the backslash. An
|
|
999
|
+
* empty one reads as `""`, where markdown-it reads none: its link rule takes
|
|
1000
|
+
* the `)` after it all the same, with the same empty href.
|
|
1001
|
+
*/
|
|
1002
|
+
function destinationAt(text, start) {
|
|
1003
|
+
if (text[start] === "<") {
|
|
1004
|
+
for (let i = start + 1; i < text.length; i++) {
|
|
1005
|
+
if (text[i] === "\n" || text[i] === "<")
|
|
1006
|
+
return undefined;
|
|
1007
|
+
if (text[i] === ">")
|
|
1008
|
+
return { raw: text.slice(start + 1, i), end: i + 1 };
|
|
1009
|
+
if (text[i] === "\\")
|
|
1010
|
+
i++;
|
|
1011
|
+
}
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
let level = 0;
|
|
1015
|
+
let i = start;
|
|
1016
|
+
for (; i < text.length; i++) {
|
|
1017
|
+
const code = text.charCodeAt(i);
|
|
1018
|
+
if (code <= 0x20 || code === 0x7f)
|
|
1019
|
+
break;
|
|
1020
|
+
if (code === 0x5c && i + 1 < text.length) {
|
|
1021
|
+
if (text[i + 1] === " ")
|
|
1022
|
+
break;
|
|
1023
|
+
i++;
|
|
1024
|
+
}
|
|
1025
|
+
else if (code === 0x28) {
|
|
1026
|
+
level++;
|
|
1027
|
+
if (level > MAX_DESTINATION_NESTING)
|
|
1028
|
+
return undefined;
|
|
1029
|
+
}
|
|
1030
|
+
else if (code === 0x29) {
|
|
1031
|
+
if (level === 0)
|
|
1032
|
+
break;
|
|
1033
|
+
level--;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return level === 0 ? { raw: text.slice(start, i), end: i } : undefined;
|
|
1037
|
+
}
|
|
1038
|
+
/** The character before `i` in `text`, a surrogate pair whole, or a space at its start, as `scanDelims` reads the line. */
|
|
1039
|
+
function charBefore(text, i) {
|
|
1040
|
+
if (i === 0)
|
|
1041
|
+
return " ";
|
|
1042
|
+
return i >= 2 && text.codePointAt(i - 2) > 0xffff ? text.slice(i - 2, i) : text[i - 1];
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* The run of `*`, `_` or `~` starting at `i` in `text`, as markdown-it's
|
|
1046
|
+
* `scanDelims` reads it. A run is left-flanking where what follows it is no
|
|
1047
|
+
* space, and is punctuation only if what precedes it is a space or
|
|
1048
|
+
* punctuation too; right-flanking is the mirror. A `*` or `~` run opens where
|
|
1049
|
+
* it is left-flanking and closes where it is right-flanking; a `_` run may
|
|
1050
|
+
* not do either inside a word. A `~` run is strikethrough's, which takes its
|
|
1051
|
+
* tildes two at a time and none at all from a run of one — that `~` is text.
|
|
1052
|
+
*/
|
|
1053
|
+
function delimiterRun(text, i) {
|
|
1054
|
+
const marker = text[i];
|
|
1055
|
+
let end = i;
|
|
1056
|
+
while (text[end] === marker)
|
|
1057
|
+
end++;
|
|
1058
|
+
const before = charBefore(text, i);
|
|
1059
|
+
const after = end < text.length ? String.fromCodePoint(text.codePointAt(end)) : " ";
|
|
1060
|
+
const [spaceBefore, spaceAfter] = [WHITESPACE.test(before), WHITESPACE.test(after)];
|
|
1061
|
+
const [punctBefore, punctAfter] = [PUNCTUATION.test(before), PUNCTUATION.test(after)];
|
|
1062
|
+
const left = !spaceAfter && (!punctAfter || spaceBefore || punctBefore);
|
|
1063
|
+
const right = !spaceBefore && (!punctBefore || spaceAfter || punctAfter);
|
|
1064
|
+
const inWord = marker !== "_";
|
|
1065
|
+
const length = end - i;
|
|
1066
|
+
const strike = marker === "~";
|
|
1067
|
+
if (strike && length < 2)
|
|
1068
|
+
return undefined;
|
|
1069
|
+
return {
|
|
1070
|
+
kind: "delimiters",
|
|
1071
|
+
marker,
|
|
1072
|
+
unit: strike ? "~~" : marker,
|
|
1073
|
+
count: strike ? Math.floor(length / 2) : length,
|
|
1074
|
+
length: strike ? 0 : length,
|
|
1075
|
+
lone: strike && length % 2 === 1,
|
|
1076
|
+
canOpen: left && (inWord || !right || punctBefore),
|
|
1077
|
+
canClose: right && (inWord || !left || punctAfter),
|
|
1078
|
+
end,
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Each delimiter that can close paired with the nearest one before it that
|
|
1083
|
+
* can open, as markdown-it's `balance_pairs` pairs them, so the pairs nest:
|
|
1084
|
+
* the same marker, never one of its own run, and not where either can do both
|
|
1085
|
+
* and the runs' lengths sum to a multiple of 3, unless both lengths are.
|
|
1086
|
+
* `jumps` skips what a pairing has enclosed and `openersBottom` the openers a
|
|
1087
|
+
* closer like this one already failed to find, so the pass is linear.
|
|
1088
|
+
*/
|
|
1089
|
+
function balancePairs(delimiters) {
|
|
1090
|
+
const openersBottom = { "*": [-1, -1, -1, -1, -1, -1], _: [-1, -1, -1, -1, -1, -1], "~": [-1, -1, -1, -1, -1, -1] };
|
|
1091
|
+
const jumps = [];
|
|
1092
|
+
let headerIdx = 0;
|
|
1093
|
+
let lastPiece = -2;
|
|
1094
|
+
for (const [closerIdx, closer] of delimiters.entries()) {
|
|
1095
|
+
jumps.push(0);
|
|
1096
|
+
// headerIdx is the first delimiter of the closer's own run.
|
|
1097
|
+
if (delimiters[headerIdx].marker !== closer.marker || lastPiece !== closer.piece - 1)
|
|
1098
|
+
headerIdx = closerIdx;
|
|
1099
|
+
lastPiece = closer.piece;
|
|
1100
|
+
if (!closer.close)
|
|
1101
|
+
continue;
|
|
1102
|
+
const bottom = (closer.open ? 3 : 0) + (closer.length % 3);
|
|
1103
|
+
let openerIdx = headerIdx - jumps[headerIdx] - 1;
|
|
1104
|
+
let newBottom = openerIdx;
|
|
1105
|
+
for (; openerIdx > openersBottom[closer.marker][bottom]; openerIdx -= jumps[openerIdx] + 1) {
|
|
1106
|
+
const opener = delimiters[openerIdx];
|
|
1107
|
+
if (opener.marker !== closer.marker || !opener.open || opener.end >= 0)
|
|
1108
|
+
continue;
|
|
1109
|
+
const oddMatch = (opener.close || closer.open) &&
|
|
1110
|
+
(opener.length + closer.length) % 3 === 0 &&
|
|
1111
|
+
(opener.length % 3 !== 0 || closer.length % 3 !== 0);
|
|
1112
|
+
if (oddMatch)
|
|
1113
|
+
continue;
|
|
1114
|
+
const lastJump = openerIdx > 0 && !delimiters[openerIdx - 1].open ? jumps[openerIdx - 1] + 1 : 0;
|
|
1115
|
+
jumps[closerIdx] = closerIdx - openerIdx + lastJump;
|
|
1116
|
+
jumps[openerIdx] = lastJump;
|
|
1117
|
+
closer.open = false;
|
|
1118
|
+
opener.end = closerIdx;
|
|
1119
|
+
opener.close = false;
|
|
1120
|
+
newBottom = -1;
|
|
1121
|
+
lastPiece = -2;
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
if (newBottom !== -1)
|
|
1125
|
+
openersBottom[closer.marker][bottom] = newBottom;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* `pieces` with each pair of delimiters `balancePairs` made into emphasis, as
|
|
1130
|
+
* markdown-it's strikethrough and emphasis `postProcess` make them: a `~~`
|
|
1131
|
+
* pair `s`; and, innermost pair first, a `*` or `_` pair `strong` where the
|
|
1132
|
+
* pair enclosing it is of the same marker and touches it at both ends, so the
|
|
1133
|
+
* two are spent as one, and `em` otherwise. The `lone` `~` of an odd run, met
|
|
1134
|
+
* just before a `~~` that closes, is moved past every `~~` closing beside it,
|
|
1135
|
+
* as strikethrough's is, so `x ~~~a~~~` strikes `a` between two plain `~`. A
|
|
1136
|
+
* delimiter left unpaired is the text it is, joined to the text beside it as
|
|
1137
|
+
* markdown-it's `fragments_join` joins it, so a run of text between line
|
|
1138
|
+
* endings is one piece.
|
|
1139
|
+
*/
|
|
1140
|
+
function emphasized(pieces, delimiters, lone) {
|
|
1141
|
+
const roles = new Map();
|
|
1142
|
+
const strikeCloses = new Set();
|
|
1143
|
+
for (let i = delimiters.length - 1; i >= 0; i--) {
|
|
1144
|
+
const opener = delimiters[i];
|
|
1145
|
+
if (opener.end < 0)
|
|
1146
|
+
continue;
|
|
1147
|
+
const closer = delimiters[opener.end];
|
|
1148
|
+
const outer = delimiters[i - 1];
|
|
1149
|
+
const strike = opener.marker === "~";
|
|
1150
|
+
const strong = !strike &&
|
|
1151
|
+
outer !== undefined &&
|
|
1152
|
+
outer.end === opener.end + 1 &&
|
|
1153
|
+
outer.marker === opener.marker &&
|
|
1154
|
+
outer.piece === opener.piece - 1 &&
|
|
1155
|
+
delimiters[opener.end + 1].piece === closer.piece + 1;
|
|
1156
|
+
roles.set(opener.piece, strike ? "s" : strong ? "strong" : "em").set(closer.piece, "close");
|
|
1157
|
+
if (strike)
|
|
1158
|
+
strikeCloses.add(closer.piece);
|
|
1159
|
+
if (strong) {
|
|
1160
|
+
roles.set(outer.piece, "spent").set(delimiters[opener.end + 1].piece, "spent");
|
|
1161
|
+
i--;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
// The pieces in the order they are read, each lone `~` swapped with the
|
|
1165
|
+
// last `~~` of the closing run after it, as strikethrough's postProcess swaps
|
|
1166
|
+
// its tokens.
|
|
1167
|
+
const order = pieces.map((_, p) => p);
|
|
1168
|
+
for (const p of [...lone].filter((p) => strikeCloses.has(p + 1))) {
|
|
1169
|
+
let last = p + 1;
|
|
1170
|
+
while (strikeCloses.has(last + 1))
|
|
1171
|
+
last++;
|
|
1172
|
+
[order[p], order[last]] = [order[last], order[p]];
|
|
1173
|
+
}
|
|
1174
|
+
const open = [[]];
|
|
1175
|
+
const tags = [];
|
|
1176
|
+
for (const p of order) {
|
|
1177
|
+
const piece = pieces[p];
|
|
1178
|
+
const role = roles.get(p);
|
|
1179
|
+
switch (role) {
|
|
1180
|
+
case "em":
|
|
1181
|
+
case "strong":
|
|
1182
|
+
case "s":
|
|
1183
|
+
tags.push(role);
|
|
1184
|
+
open.push([]);
|
|
1185
|
+
break;
|
|
1186
|
+
case "close": {
|
|
1187
|
+
const children = open.pop();
|
|
1188
|
+
open.at(-1).push({ kind: "emphasis", tag: tags.pop(), children });
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
case "spent":
|
|
1192
|
+
break;
|
|
1193
|
+
case undefined: {
|
|
1194
|
+
const into = open.at(-1);
|
|
1195
|
+
const last = into.at(-1);
|
|
1196
|
+
if (piece.kind === "text" && last?.kind === "text")
|
|
1197
|
+
into[into.length - 1] = { kind: "text", text: last.text + piece.text };
|
|
1198
|
+
else
|
|
1199
|
+
into.push(piece);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return open[0];
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* The autolink whose `<` is at `i` in `text`, as markdown-it reads one: the
|
|
1207
|
+
* text up to the first `>`, holding no `<`, a URI or an email address that
|
|
1208
|
+
* links to `mailto:` it — and so long as markdown-it accepts that href.
|
|
1209
|
+
*/
|
|
1210
|
+
function autolinkAt(text, i) {
|
|
1211
|
+
const angled = matchAt(AUTOLINK, text, i);
|
|
1212
|
+
if (!angled)
|
|
1213
|
+
return undefined;
|
|
1214
|
+
const source = angled[1];
|
|
1215
|
+
const href = AUTOLINK_URI.test(source)
|
|
1216
|
+
? linkHref(source)
|
|
1217
|
+
: AUTOLINK_EMAIL.test(source)
|
|
1218
|
+
? linkHref(`mailto:${source}`)
|
|
1219
|
+
: undefined;
|
|
1220
|
+
return href === undefined ? undefined : { kind: "autolink", source, url: href, end: i + angled[0].length };
|
|
1221
|
+
}
|
|
1222
|
+
/** Where the text of the line starting at `from` starts: past its spaces and tabs, which markdown-it's newline and escape rules skip after a line ending. */
|
|
1223
|
+
function nextLine(text, from) {
|
|
1224
|
+
return from + matchAt(LINE_INDENT, text, from)[0].length;
|
|
1225
|
+
}
|
|
1226
|
+
/** The inline construct opening at `i` in `text`, if one does, read at `level` in `state`. */
|
|
1227
|
+
function inlineAt(text, i, state, level) {
|
|
1228
|
+
switch (text[i]) {
|
|
1229
|
+
case "`": {
|
|
1230
|
+
const ticks = matchAt(TICKS, text, i)[0];
|
|
1231
|
+
const close = state.codeClose(text, i + ticks.length, ticks.length);
|
|
1232
|
+
if (close === undefined)
|
|
1233
|
+
return { kind: "text", text: ticks, end: i + ticks.length };
|
|
1234
|
+
// A code span's line endings are spaces, and a space each side pads it unless `str.strip` empties it, as markdown-it-py's backticks rule has them. markdown-it in JS strips by `/^ (.+) $/` and so pads an all-space span too.
|
|
1235
|
+
const code = text.slice(i + ticks.length, close).replaceAll("\n", " ");
|
|
1236
|
+
const padded = code.startsWith(" ") && code.endsWith(" ") && pyStrip(code) !== "";
|
|
1237
|
+
return { kind: "code", code: padded ? code.slice(1, -1) : code, end: close + ticks.length };
|
|
1238
|
+
}
|
|
1239
|
+
case "*":
|
|
1240
|
+
case "_":
|
|
1241
|
+
case "~":
|
|
1242
|
+
return delimiterRun(text, i);
|
|
1243
|
+
case "&":
|
|
1244
|
+
return referenceAt(text, i);
|
|
1245
|
+
case "\n":
|
|
1246
|
+
return { kind: "line_ending", end: nextLine(text, i + 1) };
|
|
1247
|
+
case "\\": {
|
|
1248
|
+
// markdown-it's escape rule: a backslash before a line ending breaks it
|
|
1249
|
+
// hard, and one before anything else takes that character with it, the
|
|
1250
|
+
// pair standing as written unless it escapes — so a space after it
|
|
1251
|
+
// counts toward no hard break.
|
|
1252
|
+
if (text[i + 1] === "\n")
|
|
1253
|
+
return { kind: "hardbreak", end: nextLine(text, i + 2) };
|
|
1254
|
+
const escape = matchAt(ESCAPE, text, i);
|
|
1255
|
+
if (escape)
|
|
1256
|
+
return { kind: "text", text: escape[1], end: i + escape[0].length };
|
|
1257
|
+
if (i + 1 === text.length)
|
|
1258
|
+
return undefined;
|
|
1259
|
+
const next = String.fromCodePoint(text.codePointAt(i + 1));
|
|
1260
|
+
return { kind: "text", text: `\\${next}`, end: i + 1 + next.length };
|
|
1261
|
+
}
|
|
1262
|
+
case "<": {
|
|
1263
|
+
// markdown-it tries an autolink before inline HTML.
|
|
1264
|
+
const autolink = autolinkAt(text, i);
|
|
1265
|
+
if (autolink)
|
|
1266
|
+
return autolink;
|
|
1267
|
+
const tag = matchAt(HTML_TAG, text, i);
|
|
1268
|
+
return tag ? { kind: "html", content: tag[0], end: i + tag[0].length } : undefined;
|
|
1269
|
+
}
|
|
1270
|
+
case "!":
|
|
1271
|
+
case "[": {
|
|
1272
|
+
const image = text[i] === "!";
|
|
1273
|
+
const open = image ? i + 1 : i;
|
|
1274
|
+
if (text[open] !== "[")
|
|
1275
|
+
return undefined;
|
|
1276
|
+
const close = state.labelClose(text, open, level, !image);
|
|
1277
|
+
if (close === undefined)
|
|
1278
|
+
return undefined;
|
|
1279
|
+
const target = linkTarget(text, open, close, state, level, image);
|
|
1280
|
+
if (!target)
|
|
1281
|
+
return undefined;
|
|
1282
|
+
return image
|
|
1283
|
+
? { kind: "image", alt: text.slice(open + 1, close), src: target.url, end: target.end }
|
|
1284
|
+
: { kind: "link", label: open + 1, close, url: target.url, end: target.end };
|
|
1285
|
+
}
|
|
1286
|
+
default:
|
|
1287
|
+
return undefined;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* `text` from `from` to its end read into inlines, as markdown-it's inline
|
|
1292
|
+
* pass reads it: each construct `inlineAt` finds, the text between them as it
|
|
1293
|
+
* is, and its delimiters then paired into emphasis. `text` runs on before
|
|
1294
|
+
* `from` as the block's own text does, so a delimiter run reads what borders
|
|
1295
|
+
* it there; `level` is how many links the text sits in, where a label scan
|
|
1296
|
+
* starts counting.
|
|
1297
|
+
*/
|
|
1298
|
+
function inlines(text, from, state, level) {
|
|
1299
|
+
const pieces = [];
|
|
1300
|
+
const delimiters = [];
|
|
1301
|
+
// The pieces that are an odd `~` run's lone `~`.
|
|
1302
|
+
const lone = new Set();
|
|
1303
|
+
let plain = from;
|
|
1304
|
+
for (let i = from; i < text.length;) {
|
|
1305
|
+
const found = inlineAt(text, i, state, level);
|
|
1306
|
+
if (!found) {
|
|
1307
|
+
i++;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
// markdown-it's newline rule: the spaces ending the text before a line
|
|
1311
|
+
// ending are dropped, and two or more of them make the break hard.
|
|
1312
|
+
let kept = i;
|
|
1313
|
+
if (found.kind === "line_ending")
|
|
1314
|
+
while (kept > plain && text[kept - 1] === " ")
|
|
1315
|
+
kept--;
|
|
1316
|
+
if (plain < kept)
|
|
1317
|
+
pieces.push({ kind: "text", text: text.slice(plain, kept) });
|
|
1318
|
+
if (found.kind === "line_ending")
|
|
1319
|
+
pieces.push({ kind: i - kept >= 2 ? "hardbreak" : "softbreak" });
|
|
1320
|
+
else if (found.kind === "delimiters") {
|
|
1321
|
+
if (found.lone)
|
|
1322
|
+
lone.add(pieces.push({ kind: "text", text: "~" }) - 1);
|
|
1323
|
+
for (let k = 0; k < found.count; k++) {
|
|
1324
|
+
delimiters.push({ marker: found.marker, length: found.length, piece: pieces.length, open: found.canOpen, close: found.canClose, end: -1 });
|
|
1325
|
+
pieces.push({ kind: "text", text: found.unit });
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
else
|
|
1329
|
+
pieces.push(matched(found, text, state, level));
|
|
1330
|
+
i = plain = found.end;
|
|
1331
|
+
}
|
|
1332
|
+
if (plain < text.length)
|
|
1333
|
+
pieces.push({ kind: "text", text: text.slice(plain) });
|
|
1334
|
+
balancePairs(delimiters);
|
|
1335
|
+
return emphasized(pieces, delimiters, lone);
|
|
1336
|
+
}
|
|
1337
|
+
/** `text` read into inlines from its start, its references resolved against `definitions`. */
|
|
1338
|
+
function parseInline(text, definitions) {
|
|
1339
|
+
return inlines(text, 0, new InlineState(definitions, text), 0);
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* The inline `found` in `text` is, a link's label read into inlines of its
|
|
1343
|
+
* own: the block's text up to the label's `]`, so the delimiters inside it
|
|
1344
|
+
* pair only with each other, as markdown-it's are. The text ends where
|
|
1345
|
+
* markdown-it's `posMax` does, so a run at the label's end borders the end of
|
|
1346
|
+
* the text, as `scanDelims` reads it, and not the `]`.
|
|
1347
|
+
*/
|
|
1348
|
+
function matched(found, text, state, level) {
|
|
1349
|
+
switch (found.kind) {
|
|
1350
|
+
case "link":
|
|
1351
|
+
return { kind: "link", children: inlines(text.slice(0, found.close), found.label, state, level + 1), url: found.url };
|
|
1352
|
+
case "autolink":
|
|
1353
|
+
return { kind: "autolink", text: normalizeLinkText(found.source), url: found.url };
|
|
1354
|
+
case "code":
|
|
1355
|
+
return { kind: "code", code: found.code };
|
|
1356
|
+
case "image":
|
|
1357
|
+
return { kind: "image", alt: found.alt, src: found.src };
|
|
1358
|
+
case "text":
|
|
1359
|
+
return { kind: "text", text: found.text };
|
|
1360
|
+
case "html":
|
|
1361
|
+
return { kind: "html", content: found.content };
|
|
1362
|
+
case "hardbreak":
|
|
1363
|
+
return { kind: "hardbreak" };
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* The link reference definitions among `tokens` and every container they
|
|
1368
|
+
* hold, in the order the source gives them, each label defined by the first
|
|
1369
|
+
* definition of it, as markdown-it keeps them.
|
|
1370
|
+
*/
|
|
1371
|
+
function definitionsIn(tokens, into = new Map()) {
|
|
1372
|
+
for (const token of tokens) {
|
|
1373
|
+
switch (token.type) {
|
|
1374
|
+
case "definition":
|
|
1375
|
+
if (!into.has(token.label))
|
|
1376
|
+
into.set(token.label, token.url);
|
|
1377
|
+
break;
|
|
1378
|
+
case "list":
|
|
1379
|
+
for (const item of token.items)
|
|
1380
|
+
definitionsIn(item, into);
|
|
1381
|
+
break;
|
|
1382
|
+
case "blockquote":
|
|
1383
|
+
definitionsIn(token.children, into);
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return into;
|
|
1388
|
+
}
|
|
1389
|
+
/** The blocks among `tokens`, their marks dropped, with their text read into inlines: markdown-it's inline pass. */
|
|
1390
|
+
function inlined(tokens, definitions) {
|
|
1391
|
+
return tokens
|
|
1392
|
+
.filter((token) => token.type !== "unread" && token.type !== "definition")
|
|
1393
|
+
.map((token) => inlinedBlock(token, definitions));
|
|
1394
|
+
}
|
|
1395
|
+
/** `token` with its text, and the text of every block inside it, read into inlines. */
|
|
1396
|
+
function inlinedBlock(token, definitions) {
|
|
1397
|
+
const parse = (text) => parseInline(text, definitions);
|
|
1398
|
+
switch (token.type) {
|
|
1399
|
+
case "heading":
|
|
1400
|
+
case "paragraph":
|
|
1401
|
+
return { ...token, text: parse(token.text) };
|
|
1402
|
+
case "list":
|
|
1403
|
+
return { ...token, items: token.items.map((item) => inlined(item, definitions)) };
|
|
1404
|
+
case "blockquote":
|
|
1405
|
+
return { ...token, children: inlined(token.children, definitions) };
|
|
1406
|
+
case "table":
|
|
1407
|
+
return {
|
|
1408
|
+
...token,
|
|
1409
|
+
columns: token.columns.map((column) => ({ ...column, header: parse(column.header) })),
|
|
1410
|
+
rows: token.rows.map((row) => row.map(parse)),
|
|
1411
|
+
};
|
|
1412
|
+
default:
|
|
1413
|
+
return token;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/** `source` read as Markdown: its blocks, and the text in each read into inlines. A line ends at `\r\n`, `\r` or `\n`, as markdown-it normalizes them. */
|
|
1417
|
+
export function parseMarkdown(source) {
|
|
1418
|
+
const lines = source.split(/\r\n?|\n/).map(sourceLine);
|
|
1419
|
+
const { tokens } = tokenize(new Lines(lines.values()), 0);
|
|
1420
|
+
return inlined(tokens, definitionsIn(tokens));
|
|
1421
|
+
}
|