@earendil-works/pi-tui 1.4.0 → 1.5.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/README.md +2 -0
- package/dist/components/editor.d.ts +2 -0
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +9 -2
- package/dist/components/editor.js.map +1 -1
- package/dist/components/markdown.d.ts +6 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +152 -54
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/select-list.d.ts +1 -0
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +10 -1
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +1 -0
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +8 -1
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +3 -0
- package/dist/components/text.js.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +59 -44
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +6 -0
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +15 -0
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +60 -6
- package/dist/keys.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +18 -13
- package/dist/layout.js.map +1 -1
- package/dist/program-status.d.ts.map +1 -1
- package/dist/program-status.js +2 -2
- package/dist/program-status.js.map +1 -1
- package/dist/terminal-image.d.ts +2 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +18 -3
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +11 -4
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +2 -1
- package/dist/tui-alt-screen.d.ts.map +1 -1
- package/dist/tui-alt-screen.js +13 -10
- package/dist/tui-alt-screen.js.map +1 -1
- package/dist/tui-main-screen.d.ts.map +1 -1
- package/dist/tui-main-screen.js +4 -1
- package/dist/tui-main-screen.js.map +1 -1
- package/dist/tui.d.ts +5 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +5 -3
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +74 -7
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -36,6 +36,7 @@ export interface MarkdownTheme {
|
|
|
36
36
|
italic: (text: string) => string;
|
|
37
37
|
strikethrough: (text: string) => string;
|
|
38
38
|
underline: (text: string) => string;
|
|
39
|
+
/** Highlighted lines of a code block. A Markdown component reuses the result for the same code until invalidate(). */
|
|
39
40
|
highlightCode?: (code: string, lang?: string) => string[];
|
|
40
41
|
/** Prefix applied to each rendered code block line (default: " ") */
|
|
41
42
|
codeBlockIndent?: string;
|
|
@@ -62,10 +63,15 @@ export declare class Markdown implements Component {
|
|
|
62
63
|
private cachedWidth?;
|
|
63
64
|
private cachedLines?;
|
|
64
65
|
private cachedTokens?;
|
|
66
|
+
private blocks?;
|
|
67
|
+
private blockHighlightedCode?;
|
|
65
68
|
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, options?: MarkdownOptions);
|
|
66
69
|
setText(text: string): void;
|
|
67
70
|
invalidate(): void;
|
|
71
|
+
private clearRenderCache;
|
|
68
72
|
render(width: number): string[];
|
|
73
|
+
/** Render one top-level block to output lines: wrapped, with margins, and padded or backgrounded to the width. */
|
|
74
|
+
private renderBlock;
|
|
69
75
|
/**
|
|
70
76
|
* Apply default text style to a string.
|
|
71
77
|
* This is the base styling applied to all text content.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,9 +2,18 @@ import { Marked, Tokenizer } from "marked";
|
|
|
2
2
|
import markedCjkFriendly from "marked-cjk-friendly";
|
|
3
3
|
import { renderLatex } from "../latex.js";
|
|
4
4
|
import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
|
|
5
|
-
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
5
|
+
import { applyBackgroundToLine, cjkPunctuationRegex, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
6
6
|
const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
|
7
|
-
class
|
|
7
|
+
class MarkdownTokenizer extends Tokenizer {
|
|
8
|
+
url(src) {
|
|
9
|
+
const token = super.url(src);
|
|
10
|
+
if (!token)
|
|
11
|
+
return undefined;
|
|
12
|
+
// Bare URLs stop at CJK punctuation; explicit links and angle autolinks
|
|
13
|
+
// use separate tokenizers and can still contain these characters.
|
|
14
|
+
const punctuationIndex = token.raw.search(cjkPunctuationRegex);
|
|
15
|
+
return punctuationIndex < 0 ? token : super.url(token.raw.slice(0, punctuationIndex));
|
|
16
|
+
}
|
|
8
17
|
del(src) {
|
|
9
18
|
const match = STRICT_STRIKETHROUGH_REGEX.exec(src);
|
|
10
19
|
if (!match) {
|
|
@@ -159,9 +168,40 @@ function trimPartialClosingFences(tokens) {
|
|
|
159
168
|
}
|
|
160
169
|
token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, "");
|
|
161
170
|
}
|
|
171
|
+
// An unclosed comment runs to the end of an HTML block, as in CommonMark, so streamed comments stay hidden.
|
|
172
|
+
const HTML_COMMENT_REGEX = /<!--[\s\S]*?(?:-->|$)/g;
|
|
173
|
+
function stripHtmlComments(html) {
|
|
174
|
+
return html.replace(HTML_COMMENT_REGEX, "");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Remove block HTML tokens that hold only comments, so they render no line. A removed comment between two blank
|
|
178
|
+
* lines, or at the start or end, also drops a blank line, so it leaves no extra gap.
|
|
179
|
+
* See https://github.com/earendil-works/pi/issues/11710.
|
|
180
|
+
*/
|
|
181
|
+
function removeHtmlCommentBlocks(tokens) {
|
|
182
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
183
|
+
const token = tokens[i];
|
|
184
|
+
if (token.type === "list") {
|
|
185
|
+
for (const item of token.items)
|
|
186
|
+
removeHtmlCommentBlocks(item.tokens);
|
|
187
|
+
}
|
|
188
|
+
else if (token.type === "blockquote") {
|
|
189
|
+
removeHtmlCommentBlocks(token.tokens ?? []);
|
|
190
|
+
}
|
|
191
|
+
else if (token.type === "html" && stripHtmlComments(token.raw).trim() === "") {
|
|
192
|
+
tokens.splice(i, 1);
|
|
193
|
+
if (tokens[i]?.type === "space" && (i === 0 || tokens[i - 1]?.type === "space")) {
|
|
194
|
+
tokens.splice(i, 1);
|
|
195
|
+
}
|
|
196
|
+
else if (i === tokens.length && tokens[i - 1]?.type === "space") {
|
|
197
|
+
tokens.splice(--i, 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
162
202
|
const markdownParser = new Marked();
|
|
163
203
|
markdownParser.setOptions({
|
|
164
|
-
tokenizer: new
|
|
204
|
+
tokenizer: new MarkdownTokenizer(),
|
|
165
205
|
});
|
|
166
206
|
markdownParser.use({
|
|
167
207
|
tokenizer: { emStrong: markedCjkFriendly().tokenizer?.emStrong },
|
|
@@ -183,6 +223,13 @@ export class Markdown {
|
|
|
183
223
|
// about ten times the size of its source, and every message of a long transcript keeps a Markdown component. The
|
|
184
224
|
// tokens survive a burst of re-renders, such as a theme preview, and are collected afterwards.
|
|
185
225
|
cachedTokens;
|
|
226
|
+
// The last render's top-level blocks. While a message streams only its last block changes, so a render styles and
|
|
227
|
+
// wraps only that block. Each render keeps only the blocks it used, which drops a streamed block's earlier versions.
|
|
228
|
+
// Held weakly like the tokens: the blocks repeat the source in their keys and the code in their highlighting, and
|
|
229
|
+
// only a burst of renders, such as streaming or a resize, needs them.
|
|
230
|
+
blocks;
|
|
231
|
+
// Highlighted code of the block being rendered, only set while renderBlock runs.
|
|
232
|
+
blockHighlightedCode;
|
|
186
233
|
constructor(text, paddingX, paddingY, theme, defaultTextStyle, options) {
|
|
187
234
|
this.text = text;
|
|
188
235
|
this.paddingX = paddingX;
|
|
@@ -193,9 +240,15 @@ export class Markdown {
|
|
|
193
240
|
}
|
|
194
241
|
setText(text) {
|
|
195
242
|
this.text = text;
|
|
196
|
-
this.
|
|
243
|
+
this.clearRenderCache();
|
|
197
244
|
}
|
|
198
245
|
invalidate() {
|
|
246
|
+
this.clearRenderCache();
|
|
247
|
+
// The theme may have changed, which changes every styled line and the default style's prefix.
|
|
248
|
+
this.blocks = undefined;
|
|
249
|
+
this.defaultStylePrefix = undefined;
|
|
250
|
+
}
|
|
251
|
+
clearRenderCache() {
|
|
199
252
|
this.cachedText = undefined;
|
|
200
253
|
this.cachedWidth = undefined;
|
|
201
254
|
this.cachedLines = undefined;
|
|
@@ -210,6 +263,7 @@ export class Markdown {
|
|
|
210
263
|
const text = this.options.transform?.(this.text, contentWidth) ?? this.text;
|
|
211
264
|
// Don't render anything if there's no actual text
|
|
212
265
|
if (!text || text.trim() === "") {
|
|
266
|
+
this.blocks = undefined;
|
|
213
267
|
const result = [];
|
|
214
268
|
// Update cache
|
|
215
269
|
this.cachedText = this.text;
|
|
@@ -225,67 +279,77 @@ export class Markdown {
|
|
|
225
279
|
if (!tokens) {
|
|
226
280
|
tokens = markdownParser.lexer(normalizedText);
|
|
227
281
|
trimPartialClosingFences(tokens);
|
|
282
|
+
removeHtmlCommentBlocks(tokens);
|
|
228
283
|
this.cachedTokens = new WeakRef({ source: normalizedText, tokens });
|
|
229
284
|
}
|
|
230
|
-
//
|
|
231
|
-
|
|
285
|
+
// A reference link resolves through a definition anywhere in the text, so blocks with the same source can render
|
|
286
|
+
// differently; such texts are rare in practice and render without reusing blocks.
|
|
287
|
+
const reuseBlocks = Object.keys(tokens.links).length === 0;
|
|
288
|
+
const previousBlocks = this.blocks?.deref();
|
|
289
|
+
const blocks = new Map();
|
|
290
|
+
this.blocks = new WeakRef(blocks);
|
|
291
|
+
const contentLines = [];
|
|
232
292
|
for (let i = 0; i < tokens.length; i++) {
|
|
233
293
|
const token = tokens[i];
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
// Wrap lines (NO padding, NO background yet)
|
|
241
|
-
const wrappedLines = [];
|
|
242
|
-
for (const line of renderedLines) {
|
|
243
|
-
if (isImageLine(line)) {
|
|
244
|
-
wrappedLines.push(line);
|
|
294
|
+
const nextTokenType = tokens[i + 1]?.type;
|
|
295
|
+
const reused = reuseBlocks ? (blocks.get(token.raw) ?? previousBlocks?.get(token.raw)) : undefined;
|
|
296
|
+
const block = reused ?? { width: -1, nextTokenType, lines: [], highlightedCode: new Map() };
|
|
297
|
+
if (block.width !== width || block.nextTokenType !== nextTokenType) {
|
|
298
|
+
this.renderBlock(block, token, nextTokenType, width, contentWidth);
|
|
245
299
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
// Add margins and background to each wrapped line
|
|
253
|
-
const leftMargin = " ".repeat(this.paddingX);
|
|
254
|
-
const rightMargin = " ".repeat(this.paddingX);
|
|
255
|
-
const bgFn = this.defaultTextStyle?.bgColor;
|
|
256
|
-
const contentLines = [];
|
|
257
|
-
for (const line of wrappedLines) {
|
|
258
|
-
if (isImageLine(line)) {
|
|
300
|
+
if (reuseBlocks)
|
|
301
|
+
blocks.set(token.raw, block);
|
|
302
|
+
for (const line of block.lines) {
|
|
259
303
|
contentLines.push(line);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
const lineWithMargins = leftMargin + line + rightMargin;
|
|
263
|
-
if (bgFn) {
|
|
264
|
-
contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
// No background - just pad to width
|
|
268
|
-
const visibleLen = visibleWidth(lineWithMargins);
|
|
269
|
-
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
270
|
-
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
271
304
|
}
|
|
272
305
|
}
|
|
273
306
|
// Add top/bottom padding (empty lines)
|
|
307
|
+
const bgFn = this.defaultTextStyle?.bgColor;
|
|
274
308
|
const emptyLine = " ".repeat(width);
|
|
275
309
|
const emptyLines = [];
|
|
276
310
|
for (let i = 0; i < this.paddingY; i++) {
|
|
277
311
|
const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
|
|
278
312
|
emptyLines.push(line);
|
|
279
313
|
}
|
|
314
|
+
flattenLines(emptyLines);
|
|
280
315
|
// Combine top padding, content, and bottom padding
|
|
281
316
|
const result = emptyLines.concat(contentLines, emptyLines);
|
|
282
|
-
flattenLines(result);
|
|
283
317
|
// Update cache
|
|
284
318
|
this.cachedText = this.text;
|
|
285
319
|
this.cachedWidth = width;
|
|
286
320
|
this.cachedLines = result;
|
|
287
321
|
return result.length > 0 ? result : [""];
|
|
288
322
|
}
|
|
323
|
+
/** Render one top-level block to output lines: wrapped, with margins, and padded or backgrounded to the width. */
|
|
324
|
+
renderBlock(block, token, nextTokenType, width, contentWidth) {
|
|
325
|
+
this.blockHighlightedCode = block.highlightedCode;
|
|
326
|
+
const leftMargin = " ".repeat(this.paddingX);
|
|
327
|
+
const rightMargin = " ".repeat(this.paddingX);
|
|
328
|
+
const bgFn = this.defaultTextStyle?.bgColor;
|
|
329
|
+
const lines = [];
|
|
330
|
+
for (const line of this.renderToken(token, contentWidth, nextTokenType)) {
|
|
331
|
+
if (isImageLine(line)) {
|
|
332
|
+
lines.push(line);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
for (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {
|
|
336
|
+
const lineWithMargins = leftMargin + wrappedLine + rightMargin;
|
|
337
|
+
if (bgFn) {
|
|
338
|
+
lines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// No background - just pad to width
|
|
342
|
+
const paddingNeeded = Math.max(0, width - visibleWidth(lineWithMargins));
|
|
343
|
+
lines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
this.blockHighlightedCode = undefined;
|
|
348
|
+
flattenLines(lines);
|
|
349
|
+
block.lines = lines;
|
|
350
|
+
block.width = width;
|
|
351
|
+
block.nextTokenType = nextTokenType;
|
|
352
|
+
}
|
|
289
353
|
/**
|
|
290
354
|
* Apply default text style to a string.
|
|
291
355
|
* This is the base styling applied to all text content.
|
|
@@ -398,9 +462,13 @@ export class Markdown {
|
|
|
398
462
|
break;
|
|
399
463
|
case "latexBlock": {
|
|
400
464
|
const latexToken = token;
|
|
401
|
-
|
|
402
|
-
?
|
|
403
|
-
:
|
|
465
|
+
let rendered = !latexToken.pending && this.options.renderLatex !== false
|
|
466
|
+
? renderLatex(latexToken.text, { display: true })
|
|
467
|
+
: undefined;
|
|
468
|
+
// A 2D layout cannot be wrapped row by row; fall back to the raw source when it does not fit.
|
|
469
|
+
if (rendered === undefined || rendered.split("\n").some((line) => visibleWidth(line) > width)) {
|
|
470
|
+
rendered = latexToken.raw.trim();
|
|
471
|
+
}
|
|
404
472
|
for (const line of rendered.split("\n")) {
|
|
405
473
|
lines.push(this.applyDefaultStyle(line));
|
|
406
474
|
}
|
|
@@ -413,8 +481,13 @@ export class Markdown {
|
|
|
413
481
|
const indent = this.theme.codeBlockIndent ?? " ";
|
|
414
482
|
lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
415
483
|
if (this.theme.highlightCode) {
|
|
416
|
-
const
|
|
417
|
-
|
|
484
|
+
const key = `${token.lang ?? ""}\0${token.text}`;
|
|
485
|
+
let highlighted = this.blockHighlightedCode?.get(key);
|
|
486
|
+
if (!highlighted) {
|
|
487
|
+
highlighted = this.theme.highlightCode(token.text, token.lang);
|
|
488
|
+
this.blockHighlightedCode?.set(key, highlighted);
|
|
489
|
+
}
|
|
490
|
+
for (const hlLine of highlighted) {
|
|
418
491
|
lines.push(`${indent}${hlLine}`);
|
|
419
492
|
}
|
|
420
493
|
}
|
|
@@ -453,8 +526,20 @@ export class Markdown {
|
|
|
453
526
|
const lineWithReappliedStyle = line.replace(/\x1b\[0m/g, `\x1b[0m${quoteStylePrefix}`);
|
|
454
527
|
return quoteStyle(lineWithReappliedStyle);
|
|
455
528
|
};
|
|
529
|
+
// With no room for another border ("│ " plus at least one content column), render the
|
|
530
|
+
// nested content without one. Otherwise each extra level re-wraps every child line at
|
|
531
|
+
// width 1 and the output grows exponentially with nesting depth.
|
|
532
|
+
if (width < 3) {
|
|
533
|
+
const quoteTokens = token.tokens || [];
|
|
534
|
+
for (let i = 0; i < quoteTokens.length; i++) {
|
|
535
|
+
for (const line of this.renderToken(quoteTokens[i], width, quoteTokens[i + 1]?.type, styleContext)) {
|
|
536
|
+
lines.push(line);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
456
541
|
// Calculate available width for quote content (subtract border "│ " = 2 chars)
|
|
457
|
-
const quoteContentWidth =
|
|
542
|
+
const quoteContentWidth = width - 2;
|
|
458
543
|
// Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render
|
|
459
544
|
// children with renderToken() instead of renderInlineTokens().
|
|
460
545
|
// Default message style should not apply inside blockquotes.
|
|
@@ -467,7 +552,9 @@ export class Markdown {
|
|
|
467
552
|
for (let i = 0; i < quoteTokens.length; i++) {
|
|
468
553
|
const quoteToken = quoteTokens[i];
|
|
469
554
|
const nextQuoteToken = quoteTokens[i + 1];
|
|
470
|
-
|
|
555
|
+
for (const line of this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type, quoteInlineStyleContext)) {
|
|
556
|
+
renderedQuoteLines.push(line);
|
|
557
|
+
}
|
|
471
558
|
}
|
|
472
559
|
// Avoid rendering an extra empty quote line before the outer blockquote spacing.
|
|
473
560
|
while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === "") {
|
|
@@ -492,9 +579,9 @@ export class Markdown {
|
|
|
492
579
|
}
|
|
493
580
|
break;
|
|
494
581
|
case "html":
|
|
495
|
-
// Render HTML as plain text (escaped for terminal)
|
|
582
|
+
// Render HTML as plain text (escaped for terminal), without its comments
|
|
496
583
|
if ("raw" in token && typeof token.raw === "string") {
|
|
497
|
-
lines.push(this.applyDefaultStyle(token.raw.trim()));
|
|
584
|
+
lines.push(this.applyDefaultStyle(stripHtmlComments(token.raw).trim()));
|
|
498
585
|
}
|
|
499
586
|
break;
|
|
500
587
|
case "space":
|
|
@@ -517,7 +604,15 @@ export class Markdown {
|
|
|
517
604
|
const segments = text.split("\n");
|
|
518
605
|
return segments.map((segment) => applyText(segment)).join("\n");
|
|
519
606
|
};
|
|
520
|
-
|
|
607
|
+
// After a skipped HTML comment, drop the next text's leading space if the text before ended with one,
|
|
608
|
+
// so "a <!-- c --> b" renders as "a b".
|
|
609
|
+
let dropLeadingSpace = false;
|
|
610
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
611
|
+
let token = tokens[i];
|
|
612
|
+
if (dropLeadingSpace && token.type === "text" && !token.tokens?.length && /^[ \t]/.test(token.text)) {
|
|
613
|
+
token = { ...token, text: token.text.replace(/^[ \t]+/, "") };
|
|
614
|
+
}
|
|
615
|
+
dropLeadingSpace = false;
|
|
521
616
|
switch (token.type) {
|
|
522
617
|
case "latex": {
|
|
523
618
|
const latexToken = token;
|
|
@@ -588,8 +683,11 @@ export class Markdown {
|
|
|
588
683
|
break;
|
|
589
684
|
}
|
|
590
685
|
case "html":
|
|
591
|
-
// Render inline HTML as plain text
|
|
592
|
-
if (
|
|
686
|
+
// Render inline HTML as plain text; comments are hidden
|
|
687
|
+
if (token.raw.startsWith("<!--")) {
|
|
688
|
+
dropLeadingSpace = i === 0 || /[ \t]$/.test(tokens[i - 1].raw);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
593
691
|
result += applyTextWithNewlines(token.raw);
|
|
594
692
|
}
|
|
595
693
|
break;
|