@profullstack/readm3 0.1.1 → 0.3.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/README.md +127 -16
- package/dist/cli.d.ts +9 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +77 -5
- package/dist/cli.js.map +1 -1
- package/dist/editor.d.ts +77 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +308 -0
- package/dist/editor.js.map +1 -0
- package/dist/emoji.d.ts +12 -0
- package/dist/emoji.d.ts.map +1 -0
- package/dist/emoji.js +73 -0
- package/dist/emoji.js.map +1 -0
- package/dist/flavors.d.ts +68 -0
- package/dist/flavors.d.ts.map +1 -0
- package/dist/flavors.js +223 -0
- package/dist/flavors.js.map +1 -0
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +20 -3
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +455 -379
- package/dist/markdown.js.map +1 -1
- package/dist/print.d.ts +21 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +95 -0
- package/dist/print.js.map +1 -0
- package/dist/roles.d.ts +10 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +48 -0
- package/dist/roles.js.map +1 -0
- package/dist/viewer.d.ts +5 -0
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +378 -94
- package/dist/viewer.js.map +1 -1
- package/package.json +10 -4
package/dist/markdown.js
CHANGED
|
@@ -1,23 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Markdown to styled lines, instead of to a DOM.
|
|
3
|
+
*
|
|
4
|
+
* Parsing is marked's — CommonMark, GFM, and the extensions in `flavors.ts`
|
|
5
|
+
* for what GitHub and Reddit added on top. What lives here is the half marked
|
|
6
|
+
* has no opinion about: turning a token tree into fixed-width lines of spans
|
|
7
|
+
* that a terminal can paint.
|
|
3
8
|
*
|
|
4
9
|
* Spans carry a semantic `role` rather than a color; the viewer maps roles onto
|
|
5
10
|
* the active HQTUI theme. That split keeps this file pure and testable without
|
|
6
11
|
* a terminal, and lets a theme change re-color a document without re-parsing it.
|
|
7
12
|
*/
|
|
8
13
|
import { stringWidth } from "@profullstack/hqtui";
|
|
14
|
+
import { DEFAULT_FLAVOR, lex, lexInline, } from "./flavors.js";
|
|
9
15
|
const BULLETS = ["•", "◦", "▪", "·"];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A hard break inside a paragraph, carried as a sentinel because wrapping
|
|
18
|
+
* works on one logical line at a time.
|
|
19
|
+
*/
|
|
20
|
+
const BREAK = "\u0000";
|
|
21
|
+
const ALERTS = {
|
|
22
|
+
note: { role: "alert-note", label: "Note", glyph: "ℹ" },
|
|
23
|
+
tip: { role: "alert-tip", label: "Tip", glyph: "✓" },
|
|
24
|
+
important: { role: "alert-important", label: "Important", glyph: "◆" },
|
|
25
|
+
warning: { role: "alert-warning", label: "Warning", glyph: "▲" },
|
|
26
|
+
caution: { role: "alert-caution", label: "Caution", glyph: "✕" },
|
|
27
|
+
};
|
|
28
|
+
const ALERT_HEAD = /^\[!(note|tip|important|warning|caution)\][ \t]*(?:\n|$)/i;
|
|
29
|
+
function sameStyle(a, b) {
|
|
30
|
+
return (a.role === b.role &&
|
|
31
|
+
!a.bold === !b.bold &&
|
|
32
|
+
!a.italic === !b.italic &&
|
|
33
|
+
!a.underline === !b.underline &&
|
|
34
|
+
!a.dim === !b.dim &&
|
|
35
|
+
!a.strike === !b.strike);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Glue neighbouring spans that share a style back together.
|
|
39
|
+
*
|
|
40
|
+
* Wrapping splits a line into one span per word, which no consumer wants: it is
|
|
41
|
+
* an escape sequence per word when printed, and a layout child per word on
|
|
42
|
+
* screen.
|
|
43
|
+
*/
|
|
44
|
+
export function mergeSpans(spans) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const span of spans) {
|
|
47
|
+
const last = out[out.length - 1];
|
|
48
|
+
if (last && sameStyle(last, span))
|
|
49
|
+
last.text += span.text;
|
|
50
|
+
else
|
|
51
|
+
out.push({ ...span });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
21
55
|
/** The visible text of a line, with all styling dropped. Handy in tests. */
|
|
22
56
|
export function plain(line) {
|
|
23
57
|
return line.spans.map((s) => s.text).join("");
|
|
@@ -26,62 +60,7 @@ export function plain(line) {
|
|
|
26
60
|
export function toText(lines) {
|
|
27
61
|
return lines.map(plain);
|
|
28
62
|
}
|
|
29
|
-
|
|
30
|
-
function matchLink(src) {
|
|
31
|
-
if (src[0] !== "[")
|
|
32
|
-
return null;
|
|
33
|
-
let depth = 0;
|
|
34
|
-
let i = 0;
|
|
35
|
-
for (; i < src.length; i++) {
|
|
36
|
-
const c = src[i];
|
|
37
|
-
if (c === "\\") {
|
|
38
|
-
i++;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (c === "[")
|
|
42
|
-
depth++;
|
|
43
|
-
else if (c === "]") {
|
|
44
|
-
depth--;
|
|
45
|
-
if (depth === 0)
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (depth !== 0 || i >= src.length)
|
|
50
|
-
return null;
|
|
51
|
-
const text = src.slice(1, i);
|
|
52
|
-
const rest = src.slice(i + 1);
|
|
53
|
-
const inline = /^\([ \t]*(<[^>]*>|[^()\s]*(?:\([^()]*\)[^()\s]*)*)(?:[ \t]+["'(][^\n]*?["')])?[ \t]*\)/.exec(rest);
|
|
54
|
-
if (inline) {
|
|
55
|
-
const href = (inline[1] ?? "").replace(/^<|>$/g, "");
|
|
56
|
-
return { text, href, length: i + 1 + inline[0].length };
|
|
57
|
-
}
|
|
58
|
-
const ref = /^\[([^\]]*)\]/.exec(rest);
|
|
59
|
-
if (ref)
|
|
60
|
-
return { text, href: ref[1] ?? "", length: i + 1 + ref[0].length };
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Emphasis closers must not sit against a space, and `_` must not fire inside a
|
|
65
|
-
* word — otherwise every snake_case identifier in a README turns italic.
|
|
66
|
-
*/
|
|
67
|
-
function findClose(src, from, marker) {
|
|
68
|
-
let j = from;
|
|
69
|
-
while (j < src.length) {
|
|
70
|
-
const k = src.indexOf(marker, j);
|
|
71
|
-
if (k < 0)
|
|
72
|
-
return -1;
|
|
73
|
-
if (k === from || /\s/.test(src[k - 1] ?? "")) {
|
|
74
|
-
j = k + marker.length;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (marker[0] === "_" && /[\w]/.test(src[k + marker.length] ?? "")) {
|
|
78
|
-
j = k + marker.length;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
return k;
|
|
82
|
-
}
|
|
83
|
-
return -1;
|
|
84
|
-
}
|
|
63
|
+
/* ------------------------------------------------------------------ html --- */
|
|
85
64
|
/**
|
|
86
65
|
* Fold the HTML that real READMEs are full of back into markdown.
|
|
87
66
|
*
|
|
@@ -110,93 +89,154 @@ function stripTags(src) {
|
|
|
110
89
|
.replace(/[49];/g, "'")
|
|
111
90
|
.replace(/&/g, "&");
|
|
112
91
|
}
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
/* ---------------------------------------------------------------- inline --- */
|
|
93
|
+
const SUPERSCRIPTS = {
|
|
94
|
+
"0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴",
|
|
95
|
+
"5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹",
|
|
96
|
+
"+": "⁺", "-": "⁻", "=": "⁼", "(": "⁽", ")": "⁾",
|
|
97
|
+
a: "ᵃ", b: "ᵇ", c: "ᶜ", d: "ᵈ", e: "ᵉ", f: "ᶠ",
|
|
98
|
+
g: "ᵍ", h: "ʰ", i: "ⁱ", j: "ʲ", k: "ᵏ", l: "ˡ",
|
|
99
|
+
m: "ᵐ", n: "ⁿ", o: "ᵒ", p: "ᵖ", r: "ʳ", s: "ˢ",
|
|
100
|
+
t: "ᵗ", u: "ᵘ", v: "ᵛ", w: "ʷ", x: "ˣ", y: "ʸ",
|
|
101
|
+
z: "ᶻ",
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Raise text into real superscript glyphs when every character has one.
|
|
105
|
+
*
|
|
106
|
+
* Unicode has no superscript `q`, and only some capitals, so a partial mapping
|
|
107
|
+
* would read as a ransom note. Anything that does not map keeps its caret.
|
|
108
|
+
*/
|
|
109
|
+
function raise(text) {
|
|
110
|
+
let out = "";
|
|
111
|
+
for (const ch of text) {
|
|
112
|
+
const up = SUPERSCRIPTS[ch.toLowerCase()];
|
|
113
|
+
if (up === undefined)
|
|
114
|
+
return null;
|
|
115
|
+
out += up;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
115
118
|
}
|
|
116
|
-
function
|
|
119
|
+
function inlineSpans(tokens, base, ctx) {
|
|
120
|
+
// A block whose inline run holds raw HTML is re-lexed with the HTML folded
|
|
121
|
+
// into markdown. Doing it here rather than over the whole source means fenced
|
|
122
|
+
// and indented code are never touched: they are not inline runs.
|
|
123
|
+
if (tokens.some((t) => t.type === "html")) {
|
|
124
|
+
const raw = tokens.map((t) => t.raw).join("");
|
|
125
|
+
const cleaned = normalizeHtml(raw);
|
|
126
|
+
if (cleaned !== raw)
|
|
127
|
+
tokens = lexInline(cleaned, ctx.flavor);
|
|
128
|
+
}
|
|
117
129
|
const spans = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
i += 2;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (c === "`") {
|
|
134
|
-
const m = /^(`+)([\s\S]*?)\1(?!`)/.exec(src.slice(i));
|
|
135
|
-
if (m?.[2] !== undefined) {
|
|
136
|
-
flush();
|
|
137
|
-
const inner = m[2].trim() === "" ? m[2] : m[2].replace(/^ (.*) $/, "$1");
|
|
138
|
-
spans.push({ ...base, text: inner, role: "code" });
|
|
139
|
-
i += m[0].length;
|
|
140
|
-
continue;
|
|
130
|
+
for (const token of tokens) {
|
|
131
|
+
switch (token.type) {
|
|
132
|
+
case "text":
|
|
133
|
+
case "escape": {
|
|
134
|
+
const t = token;
|
|
135
|
+
// A `text` token carries its own inline children when it came out of a
|
|
136
|
+
// list item; anything else is a leaf.
|
|
137
|
+
if (t.tokens && t.tokens.length > 0)
|
|
138
|
+
spans.push(...inlineSpans(t.tokens, base, ctx));
|
|
139
|
+
else
|
|
140
|
+
spans.push({ ...base, text: decode(t.text) });
|
|
141
|
+
break;
|
|
141
142
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
143
|
+
case "strong":
|
|
144
|
+
spans.push(...inlineSpans(token.tokens, { ...base, bold: true }, ctx));
|
|
145
|
+
break;
|
|
146
|
+
case "em":
|
|
147
|
+
spans.push(...inlineSpans(token.tokens, { ...base, italic: true }, ctx));
|
|
148
|
+
break;
|
|
149
|
+
case "del":
|
|
150
|
+
spans.push(...inlineSpans(token.tokens, { ...base, dim: true, strike: true }, ctx));
|
|
151
|
+
break;
|
|
152
|
+
case "codespan":
|
|
153
|
+
spans.push({ ...base, text: decode(token.text), role: "code" });
|
|
154
|
+
break;
|
|
155
|
+
case "br":
|
|
156
|
+
spans.push({ text: BREAK });
|
|
157
|
+
break;
|
|
158
|
+
case "link": {
|
|
159
|
+
const link = token;
|
|
160
|
+
// A bare URL marked autolinked reads as a URL, not as link text.
|
|
161
|
+
const role = link.text === link.href ? "url" : "link";
|
|
162
|
+
spans.push(...inlineSpans(link.tokens, { ...base, role, underline: true }, ctx));
|
|
163
|
+
break;
|
|
150
164
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
flush();
|
|
156
|
-
spans.push(...parseSpans(m.text, { ...base, role: "link", underline: true }));
|
|
157
|
-
i += m.length;
|
|
158
|
-
continue;
|
|
165
|
+
case "image": {
|
|
166
|
+
const img = token;
|
|
167
|
+
spans.push({ ...base, text: `\u{1f5bc} ${img.text || img.href}`, role: "meta" });
|
|
168
|
+
break;
|
|
159
169
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
case "html":
|
|
171
|
+
// Survives only when normalizing changed nothing, i.e. a bare tag.
|
|
172
|
+
break;
|
|
173
|
+
case "emoji":
|
|
174
|
+
spans.push({ ...base, text: token.text });
|
|
175
|
+
break;
|
|
176
|
+
case "spoiler": {
|
|
177
|
+
const inner = token;
|
|
178
|
+
if (ctx.spoilers) {
|
|
179
|
+
spans.push(...inlineSpans(inner.tokens, { ...base, role: "spoiler" }, ctx));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Blocked out rather than colored out, so it stays hidden with
|
|
183
|
+
// --color never and in a pipe.
|
|
184
|
+
spans.push({ ...base, text: "█".repeat(Math.max(1, stringWidth(inner.text))), role: "spoiler" });
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
168
187
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
case "superscript": {
|
|
189
|
+
const sup = token;
|
|
190
|
+
const raised = raise(sup.text);
|
|
191
|
+
if (raised)
|
|
192
|
+
spans.push({ ...base, text: raised, role: "sup" });
|
|
193
|
+
else
|
|
194
|
+
spans.push({ ...base, text: `^${sup.text}`, role: "sup", dim: true });
|
|
195
|
+
break;
|
|
177
196
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
case "redditLink": {
|
|
198
|
+
const rl = token;
|
|
199
|
+
spans.push({ ...base, text: `${rl.kind}/${rl.name}`, role: "link", underline: true });
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case "footnoteRef": {
|
|
203
|
+
const ref = token;
|
|
204
|
+
spans.push({ ...base, text: `[${numberFor(ref.label, ctx)}]`, role: "footnote" });
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
default: {
|
|
208
|
+
const any = token;
|
|
209
|
+
if (any.text)
|
|
210
|
+
spans.push({ ...base, text: decode(any.text) });
|
|
211
|
+
break;
|
|
191
212
|
}
|
|
192
213
|
}
|
|
193
|
-
buf += c;
|
|
194
|
-
i++;
|
|
195
214
|
}
|
|
196
|
-
flush();
|
|
197
215
|
return spans.length > 0 ? spans : [{ ...base, text: "" }];
|
|
198
216
|
}
|
|
199
|
-
|
|
217
|
+
/** marked escapes for HTML output; a terminal wants the characters back. */
|
|
218
|
+
function decode(text) {
|
|
219
|
+
return text
|
|
220
|
+
.replace(/</g, "<")
|
|
221
|
+
.replace(/>/g, ">")
|
|
222
|
+
.replace(/"/g, '"')
|
|
223
|
+
.replace(/'/g, "'")
|
|
224
|
+
.replace(/&/g, "&");
|
|
225
|
+
}
|
|
226
|
+
function numberFor(label, ctx) {
|
|
227
|
+
const seen = ctx.numbers.get(label);
|
|
228
|
+
if (seen !== undefined)
|
|
229
|
+
return seen;
|
|
230
|
+
const next = ctx.numbers.size + 1;
|
|
231
|
+
ctx.numbers.set(label, next);
|
|
232
|
+
return next;
|
|
233
|
+
}
|
|
234
|
+
/** Inline markdown to spans. Exported because the site and tests both want it. */
|
|
235
|
+
export function parseInline(src, base = {}, options = {}) {
|
|
236
|
+
const ctx = context(options);
|
|
237
|
+
return mergeSpans(inlineSpans(lexInline(normalizeHtml(src), ctx.flavor), base, ctx));
|
|
238
|
+
}
|
|
239
|
+
/* ------------------------------------------------------------------ wrap --- */
|
|
200
240
|
/** Greedy word wrap that carries each word's styling with it. */
|
|
201
241
|
export function wrapSpans(spans, width, first = 0, hanging = first) {
|
|
202
242
|
const max = Math.max(1, width);
|
|
@@ -211,6 +251,12 @@ export function wrapSpans(spans, width, first = 0, hanging = first) {
|
|
|
211
251
|
filled = false;
|
|
212
252
|
};
|
|
213
253
|
const flush = () => {
|
|
254
|
+
// A wrapped line should not carry the space that ended it.
|
|
255
|
+
while (cur.length > 0 && cur[cur.length - 1].text.trim() === "")
|
|
256
|
+
cur.pop();
|
|
257
|
+
const last = cur[cur.length - 1];
|
|
258
|
+
if (last)
|
|
259
|
+
last.text = last.text.replace(/[ \t]+$/, "");
|
|
214
260
|
lines.push({ spans: cur.length > 0 ? cur : [{ text: "" }] });
|
|
215
261
|
indent = hanging;
|
|
216
262
|
begin();
|
|
@@ -252,30 +298,50 @@ export function wrapSpans(spans, width, first = 0, hanging = first) {
|
|
|
252
298
|
flush();
|
|
253
299
|
return lines;
|
|
254
300
|
}
|
|
255
|
-
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
for (let i = 0; i < trimmed.length; i++) {
|
|
264
|
-
const c = trimmed[i];
|
|
265
|
-
if (c === "\\" && trimmed[i + 1] === "|") {
|
|
266
|
-
buf += "|";
|
|
267
|
-
i++;
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
if (c === "|") {
|
|
271
|
-
cells.push(buf.trim());
|
|
272
|
-
buf = "";
|
|
301
|
+
/** Wrap a run that may hold hard breaks, which start a line without a blank. */
|
|
302
|
+
function wrapBlock(spans, width, first = 0, hanging = first) {
|
|
303
|
+
if (!spans.some((s) => s.text.includes(BREAK)))
|
|
304
|
+
return wrapSpans(spans, width, first, hanging);
|
|
305
|
+
const segments = [[]];
|
|
306
|
+
for (const span of spans) {
|
|
307
|
+
if (!span.text.includes(BREAK)) {
|
|
308
|
+
segments[segments.length - 1].push(span);
|
|
273
309
|
continue;
|
|
274
310
|
}
|
|
275
|
-
|
|
311
|
+
span.text.split(BREAK).forEach((part, index) => {
|
|
312
|
+
if (index > 0)
|
|
313
|
+
segments.push([]);
|
|
314
|
+
if (part)
|
|
315
|
+
segments[segments.length - 1].push({ ...span, text: part });
|
|
316
|
+
});
|
|
276
317
|
}
|
|
277
|
-
|
|
278
|
-
|
|
318
|
+
const out = [];
|
|
319
|
+
segments.forEach((segment, index) => {
|
|
320
|
+
out.push(...wrapSpans(segment, width, index === 0 ? first : hanging, hanging));
|
|
321
|
+
});
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
/* ---------------------------------------------------------------- blocks --- */
|
|
325
|
+
function rule(width, char, style) {
|
|
326
|
+
return { spans: [{ ...style, text: char.repeat(Math.max(1, width)) }] };
|
|
327
|
+
}
|
|
328
|
+
function blankLine() {
|
|
329
|
+
return { spans: [{ text: "" }] };
|
|
330
|
+
}
|
|
331
|
+
/** Drop the blank lines a nested render leaves at either end. */
|
|
332
|
+
function trim(lines) {
|
|
333
|
+
let start = 0;
|
|
334
|
+
let end = lines.length;
|
|
335
|
+
while (start < end && plain(lines[start]).trim() === "")
|
|
336
|
+
start++;
|
|
337
|
+
while (end > start && plain(lines[end - 1]).trim() === "")
|
|
338
|
+
end--;
|
|
339
|
+
return lines.slice(start, end);
|
|
340
|
+
}
|
|
341
|
+
function prefixed(lines, first, rest) {
|
|
342
|
+
return lines.map((line, index) => ({
|
|
343
|
+
spans: [...(index === 0 ? first : rest).map((s) => ({ ...s })), ...line.spans],
|
|
344
|
+
}));
|
|
279
345
|
}
|
|
280
346
|
function pad(spans, target, align) {
|
|
281
347
|
const used = spans.reduce((n, s) => n + stringWidth(s.text), 0);
|
|
@@ -290,10 +356,14 @@ function pad(spans, target, align) {
|
|
|
290
356
|
}
|
|
291
357
|
return [...spans, { text: " ".repeat(gap) }];
|
|
292
358
|
}
|
|
293
|
-
function renderTable(
|
|
359
|
+
function renderTable(token, width, ctx) {
|
|
360
|
+
const header = token.header.map((cell) => inlineSpans(cell.tokens, { role: "th", bold: true }, ctx));
|
|
361
|
+
const body = token.rows.map((row) => row.map((cell) => inlineSpans(cell.tokens, {}, ctx)));
|
|
362
|
+
const rows = [header, ...body];
|
|
294
363
|
const columns = Math.max(...rows.map((r) => r.length));
|
|
295
|
-
const
|
|
296
|
-
const
|
|
364
|
+
const aligns = token.align.map((a) => a ?? "left");
|
|
365
|
+
const cells = rows.map((row) => Array.from({ length: columns }, (_, i) => row[i] ?? [{ text: "" }]));
|
|
366
|
+
const widths = Array.from({ length: columns }, (_, i) => Math.max(1, ...cells.map((row) => row[i].reduce((n, s) => n + stringWidth(s.text), 0))));
|
|
297
367
|
// Shrink the widest columns first until the table fits the pane.
|
|
298
368
|
const gap = 2;
|
|
299
369
|
let total = widths.reduce((a, b) => a + b, 0) + gap * (columns - 1);
|
|
@@ -305,9 +375,9 @@ function renderTable(rows, aligns, width) {
|
|
|
305
375
|
total--;
|
|
306
376
|
}
|
|
307
377
|
const out = [];
|
|
308
|
-
|
|
378
|
+
cells.forEach((row, r) => {
|
|
309
379
|
const spans = [];
|
|
310
|
-
|
|
380
|
+
row.forEach((cell, c) => {
|
|
311
381
|
if (c > 0)
|
|
312
382
|
spans.push({ text: " ".repeat(gap) });
|
|
313
383
|
const w = widths[c];
|
|
@@ -317,10 +387,9 @@ function renderTable(rows, aligns, width) {
|
|
|
317
387
|
if (used >= w)
|
|
318
388
|
break;
|
|
319
389
|
const room = w - used;
|
|
320
|
-
const text = stringWidth(span.text) > room ? span.text.slice(0, Math.max(0, room - 1))
|
|
321
|
-
clipped.push(
|
|
390
|
+
const text = stringWidth(span.text) > room ? `${span.text.slice(0, Math.max(0, room - 1))}…` : span.text;
|
|
391
|
+
clipped.push({ ...span, text });
|
|
322
392
|
used += stringWidth(text);
|
|
323
|
-
clipped[clipped.length - 1] = { ...clipped[clipped.length - 1], text };
|
|
324
393
|
}
|
|
325
394
|
spans.push(...pad(clipped, w, aligns[c] ?? "left"));
|
|
326
395
|
});
|
|
@@ -337,232 +406,239 @@ function renderTable(rows, aligns, width) {
|
|
|
337
406
|
});
|
|
338
407
|
return out;
|
|
339
408
|
}
|
|
409
|
+
function renderCode(text, lang, width, framed = true) {
|
|
410
|
+
const out = [];
|
|
411
|
+
const head = lang ? `┌─ ${lang} ` : "┌─ ";
|
|
412
|
+
if (framed)
|
|
413
|
+
out.push({
|
|
414
|
+
spans: [
|
|
415
|
+
{ text: "┌─ ", role: "gutter" },
|
|
416
|
+
...(lang ? [{ text: `${lang} `, role: "lang" }] : []),
|
|
417
|
+
{ text: "─".repeat(Math.max(0, width - stringWidth(head))), role: "gutter" },
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
for (const raw of text.replace(/\n$/, "").split("\n")) {
|
|
421
|
+
const line = raw.replace(/\t/g, " ");
|
|
422
|
+
out.push({
|
|
423
|
+
spans: [
|
|
424
|
+
{ text: "│ ", role: "gutter" },
|
|
425
|
+
{ text: stringWidth(line) > width - 2 ? `${line.slice(0, width - 3)}…` : line, role: "fence" },
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (framed)
|
|
430
|
+
out.push({ spans: [{ text: `└${"─".repeat(Math.max(0, width - 1))}`, role: "gutter" }] });
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
function renderQuote(token, width, ctx) {
|
|
434
|
+
const inner = Math.max(4, width - 2);
|
|
435
|
+
const alert = ALERT_HEAD.exec(token.text);
|
|
436
|
+
if (alert) {
|
|
437
|
+
const kind = ALERTS[alert[1].toLowerCase()];
|
|
438
|
+
const body = token.text.slice(alert[0].length);
|
|
439
|
+
const lines = trim(renderTokens(lex(body, ctx.flavor), inner, ctx, 0));
|
|
440
|
+
const head = {
|
|
441
|
+
spans: [{ text: `${kind.glyph} ${kind.label}`, role: kind.role, bold: true }],
|
|
442
|
+
};
|
|
443
|
+
return prefixed([head, ...lines], [{ text: "▌ ", role: kind.role }], [
|
|
444
|
+
{ text: "▌ ", role: kind.role },
|
|
445
|
+
]);
|
|
446
|
+
}
|
|
447
|
+
const lines = trim(renderTokens(token.tokens, inner, ctx, 0));
|
|
448
|
+
return prefixed(lines.map((line) => ({ spans: line.spans.map((s) => ({ dim: true, ...s })) })), [{ text: "▌ ", role: "quote" }], [{ text: "▌ ", role: "quote" }]);
|
|
449
|
+
}
|
|
450
|
+
function renderList(token, width, ctx, depth) {
|
|
451
|
+
const out = [];
|
|
452
|
+
const start = typeof token.start === "number" && Number.isFinite(token.start) ? token.start : 1;
|
|
453
|
+
token.items.forEach((item, index) => {
|
|
454
|
+
const glyph = item.task
|
|
455
|
+
? item.checked
|
|
456
|
+
? "☑"
|
|
457
|
+
: "☐"
|
|
458
|
+
: token.ordered
|
|
459
|
+
? `${start + index}.`
|
|
460
|
+
: BULLETS[depth % BULLETS.length];
|
|
461
|
+
const lead = stringWidth(glyph) + 1;
|
|
462
|
+
const inner = Math.max(8, width - lead);
|
|
463
|
+
// The checkbox is a marker, not content: it is already in the glyph.
|
|
464
|
+
const children = item.tokens.filter((t) => t.type !== "checkbox");
|
|
465
|
+
const style = item.task && item.checked ? { dim: true } : {};
|
|
466
|
+
const lines = trim(renderTokens(children, inner, ctx, depth + 1, style, !token.loose));
|
|
467
|
+
out.push(...prefixed(lines.length > 0 ? lines : [blankLine()], [{ text: `${glyph} `, role: "bullet", bold: token.ordered }], [{ text: " ".repeat(lead) }]));
|
|
468
|
+
if (token.loose && index < token.items.length - 1)
|
|
469
|
+
out.push(blankLine());
|
|
470
|
+
});
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
function renderHeading(token, width, ctx) {
|
|
474
|
+
const role = token.depth === 1 ? "h1" : token.depth === 2 ? "h2" : "h3";
|
|
475
|
+
const prefix = token.depth >= 3 ? [{ text: "▸ ", role, dim: true }] : [];
|
|
476
|
+
const spans = [...prefix, ...inlineSpans(token.tokens, { role, bold: true }, ctx)];
|
|
477
|
+
const out = wrapBlock(spans, width);
|
|
478
|
+
const text = out.map(plain).join(" ");
|
|
479
|
+
if (token.depth === 1)
|
|
480
|
+
out.push(rule(width, "━", { role, dim: true }));
|
|
481
|
+
else if (token.depth === 2)
|
|
482
|
+
out.push(rule(Math.min(width, stringWidth(text) + 2), "─", { role, dim: true }));
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
340
485
|
/**
|
|
341
|
-
*
|
|
486
|
+
* A run of block tokens to lines, with one blank line between blocks.
|
|
342
487
|
*
|
|
343
|
-
* `
|
|
344
|
-
*
|
|
488
|
+
* `depth` only selects the bullet glyph; indentation comes from the caller
|
|
489
|
+
* prefixing what it gets back, so a nested list, a code block and a table
|
|
490
|
+
* inside a list item all line up the same way.
|
|
345
491
|
*/
|
|
346
|
-
|
|
347
|
-
const w = Math.max(20, Math.floor(width));
|
|
348
|
-
const src = source.replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->/g, "");
|
|
349
|
-
const raw = src.split("\n");
|
|
492
|
+
function renderTokens(tokens, width, ctx, depth, base = {}, tight = false) {
|
|
350
493
|
const out = [];
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
};
|
|
355
|
-
let i = 0;
|
|
356
|
-
// YAML front matter, shown rather than hidden: it is usually the metadata a
|
|
357
|
-
// reader opened the file for.
|
|
358
|
-
if (raw[0] === "---") {
|
|
359
|
-
const end = raw.findIndex((l, n) => n > 0 && (l === "---" || l === "..."));
|
|
360
|
-
if (end > 0) {
|
|
361
|
-
for (const line of raw.slice(1, end)) {
|
|
362
|
-
out.push({ spans: [{ text: "▏ ", role: "gutter" }, { text: line, role: "meta", dim: true }] });
|
|
363
|
-
}
|
|
364
|
-
out.push({ spans: [{ text: "" }] });
|
|
365
|
-
i = end + 1;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
let paragraph = [];
|
|
369
|
-
const flushParagraph = () => {
|
|
370
|
-
if (paragraph.length === 0)
|
|
494
|
+
const w = Math.max(4, width);
|
|
495
|
+
const push = (lines) => {
|
|
496
|
+
if (lines.length === 0)
|
|
371
497
|
return;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return;
|
|
376
|
-
const spans = parseInline(text);
|
|
377
|
-
// A paragraph that was only an HTML wrapper leaves nothing to show.
|
|
378
|
-
if (spans.every((span) => span.text.trim() === ""))
|
|
379
|
-
return;
|
|
380
|
-
out.push(...wrapSpans(spans, w));
|
|
498
|
+
if (out.length > 0 && !tight)
|
|
499
|
+
out.push(blankLine());
|
|
500
|
+
out.push(...lines);
|
|
381
501
|
};
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
// Setext headings close the paragraph they underline.
|
|
397
|
-
if (paragraph.length > 0 && (SETEXT_H1.test(line) || SETEXT_H2.test(line))) {
|
|
398
|
-
const text = paragraph.join(" ").trim();
|
|
399
|
-
paragraph = [];
|
|
400
|
-
const h1 = SETEXT_H1.test(line);
|
|
401
|
-
blank();
|
|
402
|
-
out.push(...wrapSpans(parseInline(text, { role: h1 ? "h1" : "h2", bold: true }), w));
|
|
403
|
-
out.push(rule(h1 ? w : Math.min(w, stringWidth(text)), h1 ? "━" : "─", { role: h1 ? "h1" : "h2", dim: true }));
|
|
404
|
-
out.push({ spans: [{ text: "" }] });
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
const fence = FENCE.exec(line);
|
|
408
|
-
if (fence) {
|
|
409
|
-
flushParagraph();
|
|
410
|
-
const marker = fence[1][0];
|
|
411
|
-
const lang = fence[2] ?? "";
|
|
412
|
-
const body = [];
|
|
413
|
-
i++;
|
|
414
|
-
for (; i < raw.length; i++) {
|
|
415
|
-
const l = raw[i];
|
|
416
|
-
if (new RegExp(`^ {0,3}${marker === "`" ? "```" : "~~~"}+[ \t]*$`).test(l))
|
|
502
|
+
for (const token of tokens) {
|
|
503
|
+
switch (token.type) {
|
|
504
|
+
case "space":
|
|
505
|
+
case "def":
|
|
506
|
+
case "footnoteDef":
|
|
507
|
+
break;
|
|
508
|
+
case "heading":
|
|
509
|
+
push(renderHeading(token, w, ctx));
|
|
510
|
+
break;
|
|
511
|
+
case "paragraph": {
|
|
512
|
+
const spans = inlineSpans(token.tokens, base, ctx);
|
|
513
|
+
if (spans.every((s) => s.text.trim() === ""))
|
|
417
514
|
break;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
blank();
|
|
421
|
-
const head = lang ? `┌─ ${lang} ` : "┌─ ";
|
|
422
|
-
out.push({
|
|
423
|
-
spans: [
|
|
424
|
-
{ text: "┌─ ", role: "gutter" },
|
|
425
|
-
...(lang ? [{ text: `${lang} `, role: "lang" }] : []),
|
|
426
|
-
{ text: "─".repeat(Math.max(0, w - stringWidth(head))), role: "gutter" },
|
|
427
|
-
],
|
|
428
|
-
});
|
|
429
|
-
for (const l of body) {
|
|
430
|
-
const text = l.replace(/\t/g, " ");
|
|
431
|
-
out.push({
|
|
432
|
-
spans: [
|
|
433
|
-
{ text: "│ ", role: "gutter" },
|
|
434
|
-
{ text: stringWidth(text) > w - 2 ? text.slice(0, w - 3) + "…" : text, role: "fence" },
|
|
435
|
-
],
|
|
436
|
-
});
|
|
515
|
+
push(wrapBlock(spans, w));
|
|
516
|
+
break;
|
|
437
517
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const heading = HEADING.exec(line);
|
|
443
|
-
if (heading) {
|
|
444
|
-
flushParagraph();
|
|
445
|
-
const level = heading[1].length;
|
|
446
|
-
const text = heading[2];
|
|
447
|
-
const role = level === 1 ? "h1" : level === 2 ? "h2" : "h3";
|
|
448
|
-
blank();
|
|
449
|
-
const prefix = level >= 3 ? [{ text: "▸ ", role, dim: true }] : [];
|
|
450
|
-
out.push(...wrapSpans([...prefix, ...parseInline(text, { role, bold: true })], w));
|
|
451
|
-
if (level === 1)
|
|
452
|
-
out.push(rule(w, "━", { role, dim: true }));
|
|
453
|
-
else if (level === 2)
|
|
454
|
-
out.push(rule(Math.min(w, stringWidth(text) + 2), "─", { role, dim: true }));
|
|
455
|
-
out.push({ spans: [{ text: "" }] });
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
if (HR.test(line)) {
|
|
459
|
-
flushParagraph();
|
|
460
|
-
blank();
|
|
461
|
-
out.push(rule(w, "─", { role: "rule" }));
|
|
462
|
-
out.push({ spans: [{ text: "" }] });
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
if (QUOTE.test(line)) {
|
|
466
|
-
flushParagraph();
|
|
467
|
-
const body = [];
|
|
468
|
-
for (; i < raw.length; i++) {
|
|
469
|
-
const m = QUOTE.exec(raw[i]);
|
|
470
|
-
if (m)
|
|
471
|
-
body.push(m[1] ?? "");
|
|
472
|
-
else if (raw[i].trim() !== "" && body.length > 0)
|
|
473
|
-
body.push(raw[i]);
|
|
474
|
-
else
|
|
518
|
+
case "text": {
|
|
519
|
+
const t = token;
|
|
520
|
+
const spans = t.tokens ? inlineSpans(t.tokens, base, ctx) : [{ ...base, text: decode(t.text) }];
|
|
521
|
+
if (spans.every((s) => s.text.trim() === ""))
|
|
475
522
|
break;
|
|
523
|
+
push(wrapBlock(spans, w));
|
|
524
|
+
break;
|
|
476
525
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
526
|
+
case "code": {
|
|
527
|
+
const code = token;
|
|
528
|
+
// Indented code has no info string and no fence to echo, so it keeps a
|
|
529
|
+
// bare gutter; only a real fence is framed.
|
|
530
|
+
push(renderCode(code.text, code.lang ?? "", w, code.codeBlockStyle !== "indented"));
|
|
531
|
+
break;
|
|
481
532
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
533
|
+
case "blockquote":
|
|
534
|
+
push(renderQuote(token, w, ctx));
|
|
535
|
+
break;
|
|
536
|
+
case "list":
|
|
537
|
+
push(renderList(token, w, ctx, depth));
|
|
538
|
+
break;
|
|
539
|
+
case "table":
|
|
540
|
+
push(renderTable(token, w, ctx));
|
|
541
|
+
break;
|
|
542
|
+
case "hr":
|
|
543
|
+
push([rule(w, "─", { role: "rule" })]);
|
|
544
|
+
break;
|
|
545
|
+
case "html": {
|
|
546
|
+
// A block of raw HTML: fold it to markdown and render what is left.
|
|
547
|
+
const cleaned = normalizeHtml(token.raw).trim();
|
|
548
|
+
if (!cleaned)
|
|
549
|
+
break;
|
|
550
|
+
push(trim(renderTokens(lex(cleaned, ctx.flavor), w, ctx, depth, base)));
|
|
551
|
+
break;
|
|
497
552
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
rest = rest.slice(task[0].length);
|
|
553
|
+
default: {
|
|
554
|
+
const any = token;
|
|
555
|
+
if (any.text?.trim())
|
|
556
|
+
push(wrapBlock([{ ...base, text: decode(any.text) }], w));
|
|
557
|
+
break;
|
|
504
558
|
}
|
|
505
|
-
const ordered = /\d/.test(marker);
|
|
506
|
-
const glyph = task ? (checked ? "☑" : "☐") : ordered ? marker : BULLETS[depth];
|
|
507
|
-
const indent = depth * 2;
|
|
508
|
-
const bullet = { text: `${glyph} `, role: "bullet", bold: ordered };
|
|
509
|
-
const body = parseInline(rest, checked ? { dim: true } : {});
|
|
510
|
-
const lines = wrapSpans([bullet, ...body], w, indent, indent + stringWidth(glyph) + 1);
|
|
511
|
-
out.push(...lines);
|
|
512
|
-
continue;
|
|
513
559
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
560
|
+
}
|
|
561
|
+
return out;
|
|
562
|
+
}
|
|
563
|
+
/** The numbered notes, rendered under a rule at the foot of the document. */
|
|
564
|
+
function renderFootnotes(width, ctx) {
|
|
565
|
+
if (ctx.defs.size === 0)
|
|
566
|
+
return [];
|
|
567
|
+
// Referenced notes first, in the order they were cited; then any that were
|
|
568
|
+
// defined and never used, which is worth seeing rather than dropping.
|
|
569
|
+
const referenced = [...ctx.numbers.entries()]
|
|
570
|
+
.sort((a, b) => a[1] - b[1])
|
|
571
|
+
.filter(([label]) => ctx.defs.has(label));
|
|
572
|
+
const orphans = [...ctx.defs.keys()].filter((label) => !ctx.numbers.has(label));
|
|
573
|
+
const entries = [
|
|
574
|
+
...referenced.map(([label, n]) => [label, `${n}.`]),
|
|
575
|
+
...orphans.map((label) => [label, "•"]),
|
|
576
|
+
];
|
|
577
|
+
if (entries.length === 0)
|
|
578
|
+
return [];
|
|
579
|
+
const out = [blankLine(), rule(width, "─", { role: "rule" }), blankLine()];
|
|
580
|
+
entries.forEach(([label, marker], index) => {
|
|
581
|
+
if (index > 0)
|
|
582
|
+
out.push(blankLine());
|
|
583
|
+
const lead = stringWidth(marker) + 1;
|
|
584
|
+
const lines = trim(renderTokens(ctx.defs.get(label), Math.max(8, width - lead), ctx, 0));
|
|
585
|
+
out.push(...prefixed(lines.length > 0 ? lines : [blankLine()], [{ text: `${marker} `, role: "footnote" }], [
|
|
586
|
+
{ text: " ".repeat(lead) },
|
|
587
|
+
]));
|
|
588
|
+
});
|
|
589
|
+
return out;
|
|
590
|
+
}
|
|
591
|
+
function collectDefs(tokens, ctx) {
|
|
592
|
+
for (const token of tokens) {
|
|
593
|
+
if (token.type === "footnoteDef") {
|
|
594
|
+
const def = token;
|
|
595
|
+
if (!ctx.defs.has(def.label))
|
|
596
|
+
ctx.defs.set(def.label, def.tokens);
|
|
531
597
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function context(options) {
|
|
601
|
+
return {
|
|
602
|
+
flavor: options.flavor ?? DEFAULT_FLAVOR,
|
|
603
|
+
spoilers: options.spoilers ?? false,
|
|
604
|
+
numbers: new Map(),
|
|
605
|
+
defs: new Map(),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Render markdown to styled lines already wrapped to `width`.
|
|
610
|
+
*
|
|
611
|
+
* `width` is the pane's interior, so a resize re-renders rather than reflows —
|
|
612
|
+
* cheap, and it keeps tables and code gutters honest.
|
|
613
|
+
*/
|
|
614
|
+
export function renderMarkdown(source, width, options = {}) {
|
|
615
|
+
const w = Math.max(20, Math.floor(width));
|
|
616
|
+
const ctx = context(options);
|
|
617
|
+
const src = source.replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->/g, "");
|
|
618
|
+
const out = [];
|
|
619
|
+
let body = src;
|
|
620
|
+
// YAML front matter, shown rather than hidden: it is usually the metadata a
|
|
621
|
+
// reader opened the file for.
|
|
622
|
+
const lines = src.split("\n");
|
|
623
|
+
if (lines[0] === "---") {
|
|
624
|
+
const end = lines.findIndex((l, n) => n > 0 && (l === "---" || l === "..."));
|
|
625
|
+
if (end > 0) {
|
|
626
|
+
for (const line of lines.slice(1, end)) {
|
|
627
|
+
out.push({ spans: [{ text: "▏ ", role: "gutter" }, { text: line, role: "meta", dim: true }] });
|
|
557
628
|
}
|
|
558
|
-
out.push(
|
|
559
|
-
|
|
629
|
+
out.push(blankLine());
|
|
630
|
+
body = lines.slice(end + 1).join("\n");
|
|
560
631
|
}
|
|
561
|
-
paragraph.push(line.trim());
|
|
562
632
|
}
|
|
563
|
-
|
|
633
|
+
const tokens = lex(body, ctx.flavor);
|
|
634
|
+
collectDefs(tokens, ctx);
|
|
635
|
+
const rendered = renderTokens(tokens, w, ctx, 0);
|
|
636
|
+
if (out.length > 0 && rendered.length > 0)
|
|
637
|
+
out.push(blankLine());
|
|
638
|
+
out.push(...rendered);
|
|
639
|
+
out.push(...renderFootnotes(w, ctx));
|
|
564
640
|
while (out.length > 0 && plain(out[out.length - 1]).trim() === "")
|
|
565
641
|
out.pop();
|
|
566
|
-
return out;
|
|
642
|
+
return out.map((line) => ({ spans: mergeSpans(line.spans) }));
|
|
567
643
|
}
|
|
568
644
|
//# sourceMappingURL=markdown.js.map
|