@code-yeongyu/senpi-tui 2026.11.11 → 2026.11.13
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/dist/components/box.js +32 -7
- package/dist/components/markdown-source.d.ts +17 -0
- package/dist/components/markdown-source.js +262 -0
- package/dist/components/markdown.d.ts +7 -0
- package/dist/components/markdown.js +412 -48
- package/dist/components/scroll-view.js +4 -1
- package/dist/components/spacer.js +2 -0
- package/dist/components/text.js +5 -0
- package/dist/components/v-stack.js +11 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/selection-source.d.ts +43 -0
- package/dist/selection-source.js +200 -0
- package/dist/source-ranges.d.ts +25 -0
- package/dist/source-ranges.js +122 -0
- package/dist/tui-alt-screen.d.ts +1 -0
- package/dist/tui-alt-screen.js +20 -5
- package/dist/tui.js +12 -4
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +39 -6
- package/package.json +1 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
// allow: SIZE_OK - existing markdown renderer is oversized; this merge only preserves behavior and cache-key correctness.
|
|
2
2
|
import { Marked, Tokenizer } from "marked";
|
|
3
|
+
import { copyLineSources, getLineSources, setLineSource, setLineSources, } from "../selection-source.js";
|
|
4
|
+
import { concatRanges, emptyRanges, rangeAt, repeatedRange, sliceRanges } from "../source-ranges.js";
|
|
3
5
|
import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
|
|
4
6
|
import { nextRenderRevision } from "../tui.js";
|
|
5
|
-
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
7
|
+
import { applyBackgroundToLine, flattenLines, stripTerminalSequences, visibleWidth, wrapTextWithAnsi, } from "../utils.js";
|
|
6
8
|
import { latexToUnicode } from "./latex.js";
|
|
9
|
+
import { lexWithSources, sourceExtensions, tokenSource } from "./markdown-source.js";
|
|
7
10
|
const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
|
8
11
|
class StrictStrikethroughTokenizer extends Tokenizer {
|
|
9
12
|
del(src) {
|
|
@@ -31,6 +34,24 @@ function isLatexToken(token) {
|
|
|
31
34
|
"text" in token &&
|
|
32
35
|
typeof token.text === "string");
|
|
33
36
|
}
|
|
37
|
+
function formulaRanges(token, rendered) {
|
|
38
|
+
const source = tokenSource(token);
|
|
39
|
+
if (!source)
|
|
40
|
+
return repeatedRange(null, rendered.length);
|
|
41
|
+
const { raw, text } = source;
|
|
42
|
+
const end = raw.text.trimEnd().length;
|
|
43
|
+
if (raw.text === rendered)
|
|
44
|
+
return raw.ranges;
|
|
45
|
+
if (raw.text.trim() === rendered) {
|
|
46
|
+
return sliceRanges(raw.ranges, raw.text.length - raw.text.trimStart().length, end);
|
|
47
|
+
}
|
|
48
|
+
if (text?.text === rendered)
|
|
49
|
+
return text.ranges;
|
|
50
|
+
// A converted glyph has no reliable character-level correspondence to TeX.
|
|
51
|
+
const first = rangeAt(raw.ranges, 0);
|
|
52
|
+
const last = rangeAt(raw.ranges, end - 1);
|
|
53
|
+
return repeatedRange(first && last ? { start: first.start, end: last.end } : null, rendered.length);
|
|
54
|
+
}
|
|
34
55
|
function previousRawCharacter(tokens) {
|
|
35
56
|
const raw = tokens[tokens.length - 1]?.raw;
|
|
36
57
|
return raw ? Array.from(raw).at(-1) : undefined;
|
|
@@ -264,20 +285,26 @@ function trimPartialClosingFences(tokens) {
|
|
|
264
285
|
}
|
|
265
286
|
const markdownParser = new Marked();
|
|
266
287
|
markdownParser.use({ extensions: [blockMathTokenizer, inlineMathTokenizer] });
|
|
288
|
+
markdownParser.use({ extensions: sourceExtensions });
|
|
267
289
|
markdownParser.setOptions({
|
|
268
290
|
tokenizer: new StrictStrikethroughTokenizer(),
|
|
269
291
|
});
|
|
270
292
|
const RENDER_CACHE_MAX = 256;
|
|
271
293
|
const PARSE_CACHE_MAX = 128;
|
|
294
|
+
// Original-source work is paid only on copy, with a small cache separate from normal rendering.
|
|
295
|
+
const SOURCE_CACHE_MAX = 8;
|
|
272
296
|
const CONTENT_KEY_CACHE_MAX = 128;
|
|
273
297
|
const HIGHLIGHT_CACHE_MAX = 512;
|
|
274
298
|
const MAX_HIGHLIGHT_BYTES = 200_000;
|
|
275
299
|
const MAX_HIGHLIGHT_LINES = 2000;
|
|
276
300
|
const renderCache = new Map();
|
|
277
301
|
const parseCache = new Map();
|
|
302
|
+
const sourceRenderCache = new Map();
|
|
303
|
+
const sourceParseCache = new Map();
|
|
278
304
|
const contentKeyCache = new Map();
|
|
279
305
|
const highlightCache = new Map();
|
|
280
306
|
const objectIds = new WeakMap();
|
|
307
|
+
const enclosedTokens = new WeakSet();
|
|
281
308
|
let nextObjectId = 0;
|
|
282
309
|
let markdownHighlightCallCount = 0;
|
|
283
310
|
function objectId(value) {
|
|
@@ -316,6 +343,8 @@ function contentKey(text) {
|
|
|
316
343
|
export function clearRenderCache() {
|
|
317
344
|
renderCache.clear();
|
|
318
345
|
parseCache.clear();
|
|
346
|
+
sourceRenderCache.clear();
|
|
347
|
+
sourceParseCache.clear();
|
|
319
348
|
contentKeyCache.clear();
|
|
320
349
|
highlightCache.clear();
|
|
321
350
|
}
|
|
@@ -327,6 +356,7 @@ export function resetMarkdownHighlightCallCount() {
|
|
|
327
356
|
}
|
|
328
357
|
export class Markdown {
|
|
329
358
|
constructor(text, paddingX, paddingY, theme, defaultTextStyle, options) {
|
|
359
|
+
this.collectSources = false;
|
|
330
360
|
this.revision = nextRenderRevision();
|
|
331
361
|
this.text = text;
|
|
332
362
|
this.paddingX = paddingX;
|
|
@@ -343,6 +373,7 @@ export class Markdown {
|
|
|
343
373
|
this.cachedText = undefined;
|
|
344
374
|
this.cachedWidth = undefined;
|
|
345
375
|
this.cachedLines = undefined;
|
|
376
|
+
this.activeSourceDocument = undefined;
|
|
346
377
|
this.markRevision();
|
|
347
378
|
}
|
|
348
379
|
/** Instances that expose a revision advance the shared clock; others (e.g. an animated subclass) stay local. */
|
|
@@ -370,12 +401,10 @@ export class Markdown {
|
|
|
370
401
|
this.cachedLines = result;
|
|
371
402
|
return result;
|
|
372
403
|
}
|
|
373
|
-
// Replace tabs with 3 spaces for consistent rendering
|
|
374
|
-
const normalizedText = text.replace(/\t/g, " ");
|
|
375
|
-
const normalizedContentKey = contentKey(normalizedText);
|
|
376
404
|
const styleKey = this.defaultTextStyle ? objectId(this.defaultTextStyle) : -1;
|
|
377
405
|
const renderKey = [
|
|
378
|
-
|
|
406
|
+
contentKey(this.text),
|
|
407
|
+
contentKey(text),
|
|
379
408
|
width,
|
|
380
409
|
contentWidth,
|
|
381
410
|
this.paddingX,
|
|
@@ -389,44 +418,62 @@ export class Markdown {
|
|
|
389
418
|
getCapabilities().images ?? "",
|
|
390
419
|
getCapabilities().hyperlinks ? 1 : 0,
|
|
391
420
|
].join("\x00");
|
|
392
|
-
const
|
|
393
|
-
|
|
421
|
+
const selectedRenderCache = this.collectSources ? sourceRenderCache : renderCache;
|
|
422
|
+
const cachedRender = selectedRenderCache.get(renderKey);
|
|
423
|
+
if (cachedRender?.source === this.text && cachedRender.transformed === text) {
|
|
394
424
|
this.cachedText = this.text;
|
|
395
425
|
this.cachedWidth = width;
|
|
396
|
-
this.cachedLines = cachedRender.lines;
|
|
397
|
-
return
|
|
426
|
+
this.cachedLines = this.ownSources(cachedRender.lines);
|
|
427
|
+
return this.cachedLines;
|
|
398
428
|
}
|
|
399
429
|
// Parse markdown to HTML-like tokens
|
|
400
|
-
const
|
|
430
|
+
const parseKey = contentKey(text);
|
|
431
|
+
const selectedParseCache = this.collectSources ? sourceParseCache : parseCache;
|
|
432
|
+
const cachedParse = selectedParseCache.get(parseKey);
|
|
401
433
|
let tokens;
|
|
402
|
-
if (cachedParse?.source ===
|
|
434
|
+
if (cachedParse?.source === text) {
|
|
403
435
|
tokens = cachedParse.tokens;
|
|
404
436
|
}
|
|
405
437
|
else {
|
|
406
|
-
tokens =
|
|
438
|
+
tokens = this.collectSources
|
|
439
|
+
? lexWithSources(text, markdownParser.defaults)
|
|
440
|
+
: markdownParser.lexer(text.replace(/\t/g, " "));
|
|
407
441
|
trimPartialClosingFences(tokens);
|
|
408
|
-
cacheSet(
|
|
442
|
+
cacheSet(selectedParseCache, parseKey, { source: text, tokens }, this.collectSources ? SOURCE_CACHE_MAX : PARSE_CACHE_MAX);
|
|
409
443
|
}
|
|
410
444
|
// Convert tokens to styled terminal output
|
|
445
|
+
this.activeSourceDocument = this.collectSources
|
|
446
|
+
? (tokens.map((token) => tokenSource(token)?.raw.document).find((document) => document) ?? {
|
|
447
|
+
text,
|
|
448
|
+
enclosures: [],
|
|
449
|
+
})
|
|
450
|
+
: undefined;
|
|
411
451
|
const renderedLines = [];
|
|
412
452
|
for (let i = 0; i < tokens.length; i++) {
|
|
413
453
|
const token = tokens[i];
|
|
414
454
|
const nextToken = tokens[i + 1];
|
|
415
455
|
const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
|
|
456
|
+
const offset = renderedLines.length;
|
|
416
457
|
for (const tokenLine of tokenLines) {
|
|
417
458
|
renderedLines.push(tokenLine);
|
|
418
459
|
}
|
|
460
|
+
copyLineSources(tokenLines, renderedLines, offset);
|
|
419
461
|
}
|
|
420
462
|
// Wrap lines (NO padding, NO background yet)
|
|
421
463
|
const wrappedLines = [];
|
|
422
|
-
for (
|
|
464
|
+
for (let row = 0; row < renderedLines.length; row++) {
|
|
465
|
+
const line = renderedLines[row];
|
|
466
|
+
const mapping = getLineSources(renderedLines)?.[row];
|
|
467
|
+
const offset = wrappedLines.length;
|
|
423
468
|
if (isImageLine(line)) {
|
|
424
469
|
wrappedLines.push(line);
|
|
425
470
|
}
|
|
426
471
|
else {
|
|
427
|
-
|
|
472
|
+
const fragments = this.wrapSource(line, contentWidth, mapping);
|
|
473
|
+
for (const wrappedLine of fragments) {
|
|
428
474
|
wrappedLines.push(wrappedLine);
|
|
429
475
|
}
|
|
476
|
+
copyLineSources(fragments, wrappedLines, offset);
|
|
430
477
|
}
|
|
431
478
|
}
|
|
432
479
|
// Add margins and background to each wrapped line
|
|
@@ -450,6 +497,7 @@ export class Markdown {
|
|
|
450
497
|
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
451
498
|
}
|
|
452
499
|
}
|
|
500
|
+
copyLineSources(wrappedLines, contentLines, 0, this.paddingX);
|
|
453
501
|
// Add top/bottom padding (empty lines)
|
|
454
502
|
const emptyLine = " ".repeat(width);
|
|
455
503
|
const emptyLines = [];
|
|
@@ -459,13 +507,117 @@ export class Markdown {
|
|
|
459
507
|
}
|
|
460
508
|
// Combine top padding, content, and bottom padding
|
|
461
509
|
const result = emptyLines.concat(contentLines, emptyLines);
|
|
510
|
+
copyLineSources(contentLines, result, this.paddingY);
|
|
511
|
+
const mapped = getLineSources(result);
|
|
512
|
+
const document = mapped?.find((line) => line)?.document;
|
|
513
|
+
if (document) {
|
|
514
|
+
setLineSources(result, result.map((_, row) => mapped?.[row] ?? { document, ranges: emptyRanges }));
|
|
515
|
+
}
|
|
516
|
+
if (text !== this.text) {
|
|
517
|
+
const transformedDocument = { text: this.text, enclosures: [] };
|
|
518
|
+
setLineSources(result, result.map(() => ({ document: transformedDocument, ranges: emptyRanges, renderedFallback: true })));
|
|
519
|
+
}
|
|
520
|
+
else if (!this.collectSources) {
|
|
521
|
+
// Capture values, not this component. Old cached frames must retain their own source.
|
|
522
|
+
const sourceText = this.text;
|
|
523
|
+
const { paddingX, paddingY, theme, defaultTextStyle } = this;
|
|
524
|
+
const options = { ...this.options, transform: undefined };
|
|
525
|
+
const deferred = {
|
|
526
|
+
text: sourceText,
|
|
527
|
+
enclosures: [],
|
|
528
|
+
resolveLines: () => {
|
|
529
|
+
const markdown = new Markdown(sourceText, paddingX, paddingY, theme, defaultTextStyle, options);
|
|
530
|
+
markdown.collectSources = true;
|
|
531
|
+
const sourceLines = markdown.render(width);
|
|
532
|
+
if (sourceLines.length !== result.length || sourceLines.some((line, row) => line !== result[row])) {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
return getLineSources(sourceLines);
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
setLineSources(result, result.map((_, sourceRow) => ({
|
|
539
|
+
document: deferred,
|
|
540
|
+
ranges: emptyRanges,
|
|
541
|
+
sourceRow,
|
|
542
|
+
})));
|
|
543
|
+
}
|
|
462
544
|
flattenLines(result);
|
|
463
545
|
// Update cache
|
|
464
546
|
this.cachedText = this.text;
|
|
465
547
|
this.cachedWidth = width;
|
|
466
|
-
this.cachedLines = result;
|
|
467
|
-
cacheSet(
|
|
468
|
-
return
|
|
548
|
+
this.cachedLines = this.ownSources(result);
|
|
549
|
+
cacheSet(selectedRenderCache, renderKey, { source: this.text, transformed: text, lines: result }, this.collectSources ? SOURCE_CACHE_MAX : RENDER_CACHE_MAX);
|
|
550
|
+
return this.cachedLines.length > 0 ? this.cachedLines : [""];
|
|
551
|
+
}
|
|
552
|
+
/** Shared render caches must not make two separate messages one source interval. */
|
|
553
|
+
ownSources(lines) {
|
|
554
|
+
const mapping = getLineSources(lines);
|
|
555
|
+
if (!mapping)
|
|
556
|
+
return lines;
|
|
557
|
+
const original = mapping.find((line) => line)?.document;
|
|
558
|
+
if (!original)
|
|
559
|
+
return lines;
|
|
560
|
+
const document = { ...original };
|
|
561
|
+
const result = [...lines];
|
|
562
|
+
setLineSources(result, mapping.map((line) => line && { ...line, document }));
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
wrapSource(text, width, source) {
|
|
566
|
+
const mapping = [];
|
|
567
|
+
const lines = wrapTextWithAnsi(text, width, source
|
|
568
|
+
? (start, end) => {
|
|
569
|
+
mapping.push({ ...source, ranges: sliceRanges(source.ranges, start, end) });
|
|
570
|
+
}
|
|
571
|
+
: undefined);
|
|
572
|
+
if (source === null)
|
|
573
|
+
setLineSources(lines, lines.map(() => null));
|
|
574
|
+
else if (source)
|
|
575
|
+
setLineSources(lines, mapping);
|
|
576
|
+
return lines;
|
|
577
|
+
}
|
|
578
|
+
enclose(token, ranges) {
|
|
579
|
+
if (![
|
|
580
|
+
"heading",
|
|
581
|
+
"strong",
|
|
582
|
+
"em",
|
|
583
|
+
"del",
|
|
584
|
+
"codespan",
|
|
585
|
+
"code",
|
|
586
|
+
"link",
|
|
587
|
+
"image",
|
|
588
|
+
"escape",
|
|
589
|
+
"latex_inline",
|
|
590
|
+
"latex_block",
|
|
591
|
+
"table",
|
|
592
|
+
"blockquote",
|
|
593
|
+
].includes(token.type))
|
|
594
|
+
return;
|
|
595
|
+
if (enclosedTokens.has(token))
|
|
596
|
+
return;
|
|
597
|
+
const source = tokenSource(token)?.raw;
|
|
598
|
+
if (!source)
|
|
599
|
+
return;
|
|
600
|
+
const rawEnd = token.type === "latex_block" ? source.text.trimEnd().length : source.text.replace(/\n+$/, "").length;
|
|
601
|
+
const start = rangeAt(source.ranges, 0)?.start;
|
|
602
|
+
const end = rangeAt(source.ranges, rawEnd - 1)?.end;
|
|
603
|
+
if (start === undefined || end === undefined)
|
|
604
|
+
return;
|
|
605
|
+
let contentStart = Infinity;
|
|
606
|
+
let contentEnd = 0;
|
|
607
|
+
for (const { source: range } of ranges.spans) {
|
|
608
|
+
if (!range)
|
|
609
|
+
continue;
|
|
610
|
+
contentStart = Math.min(contentStart, range.start);
|
|
611
|
+
contentEnd = Math.max(contentEnd, range.end);
|
|
612
|
+
}
|
|
613
|
+
if (contentStart === Infinity)
|
|
614
|
+
return;
|
|
615
|
+
enclosedTokens.add(token);
|
|
616
|
+
source.document.enclosures.push({
|
|
617
|
+
start,
|
|
618
|
+
end,
|
|
619
|
+
content: { start: contentStart, end: contentEnd },
|
|
620
|
+
});
|
|
469
621
|
}
|
|
470
622
|
/**
|
|
471
623
|
* Apply default text style to a string.
|
|
@@ -583,6 +735,8 @@ export class Markdown {
|
|
|
583
735
|
}
|
|
584
736
|
renderToken(token, width, nextTokenType, styleContext) {
|
|
585
737
|
const lines = [];
|
|
738
|
+
const inlineRanges = this.collectSources ? [] : undefined;
|
|
739
|
+
const source = tokenSource(token);
|
|
586
740
|
switch (token.type) {
|
|
587
741
|
case "heading": {
|
|
588
742
|
const headingLevel = token.depth;
|
|
@@ -601,8 +755,26 @@ export class Markdown {
|
|
|
601
755
|
applyText: headingStyleFn,
|
|
602
756
|
stylePrefix: this.getStylePrefix(headingStyleFn),
|
|
603
757
|
};
|
|
604
|
-
const headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);
|
|
758
|
+
const headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext, inlineRanges);
|
|
605
759
|
const styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;
|
|
760
|
+
if (headingLevel >= 3 && inlineRanges) {
|
|
761
|
+
const prefix = /^( {0,3})(#{3,6})([ \t]*)/.exec(source?.raw.text ?? "");
|
|
762
|
+
if (source && prefix) {
|
|
763
|
+
const start = prefix[1].length;
|
|
764
|
+
const end = start + prefix[2].length;
|
|
765
|
+
const separatorStart = rangeAt(source.raw.ranges, end);
|
|
766
|
+
const separatorEnd = rangeAt(source.raw.ranges, end + prefix[3].length - 1);
|
|
767
|
+
inlineRanges.unshift(concatRanges([
|
|
768
|
+
sliceRanges(source.raw.ranges, start, end),
|
|
769
|
+
repeatedRange(prefix[3].length && separatorStart && separatorEnd
|
|
770
|
+
? { start: separatorStart.start, end: separatorEnd.end }
|
|
771
|
+
: undefined, 1),
|
|
772
|
+
]));
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
inlineRanges.unshift(repeatedRange(undefined, headingPrefix.length));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
606
778
|
lines.push(styledHeading);
|
|
607
779
|
if (nextTokenType && nextTokenType !== "space") {
|
|
608
780
|
lines.push(""); // Add spacing after headings (unless space token follows)
|
|
@@ -610,7 +782,7 @@ export class Markdown {
|
|
|
610
782
|
break;
|
|
611
783
|
}
|
|
612
784
|
case "paragraph": {
|
|
613
|
-
const paragraphText = this.renderInlineTokens(token.tokens || [], styleContext);
|
|
785
|
+
const paragraphText = this.renderInlineTokens(token.tokens || [], styleContext, inlineRanges);
|
|
614
786
|
lines.push(paragraphText);
|
|
615
787
|
// Don't add spacing if next token is space or list
|
|
616
788
|
if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") {
|
|
@@ -622,17 +794,64 @@ export class Markdown {
|
|
|
622
794
|
if (isLatexToken(token)) {
|
|
623
795
|
const formula = this.options.renderLatex === false ? token.raw.trim() : latexToUnicode(token.text);
|
|
624
796
|
lines.push(styleContext ? styleContext.applyText(formula) : this.applyDefaultStyle(formula));
|
|
797
|
+
if (inlineRanges)
|
|
798
|
+
inlineRanges.push(formulaRanges(token, stripTerminalSequences(lines[0])));
|
|
625
799
|
if (nextTokenType && nextTokenType !== "space") {
|
|
626
800
|
lines.push("");
|
|
627
801
|
}
|
|
628
802
|
}
|
|
629
803
|
break;
|
|
630
804
|
case "text":
|
|
631
|
-
lines.push(this.renderInlineTokens([token], styleContext));
|
|
805
|
+
lines.push(this.renderInlineTokens([token], styleContext, inlineRanges));
|
|
632
806
|
break;
|
|
633
807
|
case "code": {
|
|
634
808
|
const indent = this.theme.codeBlockIndent ?? " ";
|
|
635
809
|
this.renderCodeBlock(lines, token.text, token.lang, indent);
|
|
810
|
+
if (source?.text) {
|
|
811
|
+
const ranges = [];
|
|
812
|
+
let offset = 0;
|
|
813
|
+
const openingLength = /^[^\n]*/.exec(source.raw.text)?.[0].length ?? 0;
|
|
814
|
+
const openingStart = rangeAt(source.raw.ranges, 0);
|
|
815
|
+
const openingEnd = rangeAt(source.raw.ranges, openingLength - 1);
|
|
816
|
+
const open = token.codeBlockStyle !== "indented" && openingStart && openingEnd
|
|
817
|
+
? { start: openingStart.start, end: openingEnd.end }
|
|
818
|
+
: undefined;
|
|
819
|
+
ranges.push({
|
|
820
|
+
document: source.raw.document,
|
|
821
|
+
ranges: repeatedRange(open, stripTerminalSequences(lines[0]).length),
|
|
822
|
+
});
|
|
823
|
+
const codeLines = token.text.split("\n");
|
|
824
|
+
for (let row = 1; row < lines.length - 1; row++) {
|
|
825
|
+
const codeLine = codeLines[row - 1] ?? "";
|
|
826
|
+
const rendered = stripTerminalSequences(lines[row]).slice(indent.length);
|
|
827
|
+
ranges.push({
|
|
828
|
+
document: source.raw.document,
|
|
829
|
+
ranges: concatRanges([
|
|
830
|
+
repeatedRange(undefined, indent.length),
|
|
831
|
+
rendered === codeLine
|
|
832
|
+
? sliceRanges(source.text.ranges, offset, offset + codeLine.length)
|
|
833
|
+
: repeatedRange(null, rendered.length),
|
|
834
|
+
]),
|
|
835
|
+
});
|
|
836
|
+
offset += codeLine.length + 1;
|
|
837
|
+
}
|
|
838
|
+
const marker = /^ {0,3}(`{3,}|~{3,})/.exec(source.raw.text)?.[1];
|
|
839
|
+
const closing = marker
|
|
840
|
+
? new RegExp(`\\n( {0,3}${marker[0]}{${marker.length},}[ \\t]*)\\n?$`).exec(source.raw.text)
|
|
841
|
+
: null;
|
|
842
|
+
const closeRanges = closing
|
|
843
|
+
? sliceRanges(source.raw.ranges, closing.index + (closing[0].startsWith("\n") ? 1 : 0), source.raw.text.trimEnd().length)
|
|
844
|
+
: emptyRanges;
|
|
845
|
+
const closingStart = rangeAt(closeRanges, 0);
|
|
846
|
+
const closingEnd = rangeAt(closeRanges, closeRanges.length - 1);
|
|
847
|
+
const close = closingStart && closingEnd ? { start: closingStart.start, end: closingEnd.end } : undefined;
|
|
848
|
+
ranges.push({
|
|
849
|
+
document: source.raw.document,
|
|
850
|
+
ranges: repeatedRange(close, stripTerminalSequences(lines[lines.length - 1]).length),
|
|
851
|
+
});
|
|
852
|
+
setLineSources(lines, ranges);
|
|
853
|
+
this.enclose(token, concatRanges(ranges.map((line) => line.ranges)));
|
|
854
|
+
}
|
|
636
855
|
if (nextTokenType && nextTokenType !== "space") {
|
|
637
856
|
lines.push(""); // Add spacing after code blocks (unless space token follows)
|
|
638
857
|
}
|
|
@@ -641,6 +860,7 @@ export class Markdown {
|
|
|
641
860
|
case "list": {
|
|
642
861
|
const listLines = this.renderList(token, 0, width, styleContext);
|
|
643
862
|
lines.push(...listLines);
|
|
863
|
+
copyLineSources(listLines, lines);
|
|
644
864
|
// Don't add spacing after lists if a space token follows
|
|
645
865
|
// (the space token will handle it)
|
|
646
866
|
break;
|
|
@@ -648,6 +868,7 @@ export class Markdown {
|
|
|
648
868
|
case "table": {
|
|
649
869
|
const tableLines = this.renderTable(token, width, nextTokenType, styleContext);
|
|
650
870
|
lines.push(...tableLines);
|
|
871
|
+
copyLineSources(tableLines, lines);
|
|
651
872
|
break;
|
|
652
873
|
}
|
|
653
874
|
case "blockquote": {
|
|
@@ -674,18 +895,24 @@ export class Markdown {
|
|
|
674
895
|
for (let i = 0; i < quoteTokens.length; i++) {
|
|
675
896
|
const quoteToken = quoteTokens[i];
|
|
676
897
|
const nextQuoteToken = quoteTokens[i + 1];
|
|
677
|
-
|
|
898
|
+
const childLines = this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type, quoteInlineStyleContext);
|
|
899
|
+
const offset = renderedQuoteLines.length;
|
|
900
|
+
renderedQuoteLines.push(...childLines);
|
|
901
|
+
copyLineSources(childLines, renderedQuoteLines, offset);
|
|
678
902
|
}
|
|
679
903
|
// Avoid rendering an extra empty quote line before the outer blockquote spacing.
|
|
680
904
|
while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === "") {
|
|
681
905
|
renderedQuoteLines.pop();
|
|
682
906
|
}
|
|
683
|
-
for (
|
|
907
|
+
for (let row = 0; row < renderedQuoteLines.length; row++) {
|
|
908
|
+
const quoteLine = renderedQuoteLines[row];
|
|
684
909
|
const styledLine = applyQuoteStyle(quoteLine);
|
|
685
|
-
const wrappedLines =
|
|
910
|
+
const wrappedLines = this.wrapSource(styledLine, quoteContentWidth, getLineSources(renderedQuoteLines)?.[row]);
|
|
911
|
+
const offset = lines.length;
|
|
686
912
|
for (const wrappedLine of wrappedLines) {
|
|
687
913
|
lines.push(this.theme.quoteBorder("│ ") + wrappedLine);
|
|
688
914
|
}
|
|
915
|
+
copyLineSources(wrappedLines, lines, offset, 2);
|
|
689
916
|
}
|
|
690
917
|
if (nextTokenType && nextTokenType !== "space") {
|
|
691
918
|
lines.push(""); // Add spacing after blockquotes (unless space token follows)
|
|
@@ -694,6 +921,12 @@ export class Markdown {
|
|
|
694
921
|
}
|
|
695
922
|
case "hr":
|
|
696
923
|
lines.push(this.theme.hr("─".repeat(Math.min(width, 80))));
|
|
924
|
+
if (inlineRanges && source) {
|
|
925
|
+
// Every rule glyph selects the authored rule, excluding block separators.
|
|
926
|
+
const first = rangeAt(source.raw.ranges, 0);
|
|
927
|
+
const last = rangeAt(source.raw.ranges, source.raw.text.trimEnd().length - 1);
|
|
928
|
+
inlineRanges.push(repeatedRange(first && last ? { start: first.start, end: last.end } : null, stripTerminalSequences(lines[0]).length));
|
|
929
|
+
}
|
|
697
930
|
if (nextTokenType && nextTokenType !== "space") {
|
|
698
931
|
lines.push(""); // Add spacing after horizontal rules (unless space token follows)
|
|
699
932
|
}
|
|
@@ -714,20 +947,38 @@ export class Markdown {
|
|
|
714
947
|
lines.push(token.text);
|
|
715
948
|
}
|
|
716
949
|
}
|
|
950
|
+
if (source && !getLineSources(lines)) {
|
|
951
|
+
const ranges = inlineRanges?.length
|
|
952
|
+
? concatRanges(inlineRanges)
|
|
953
|
+
: source.text?.text === stripTerminalSequences(lines[0] ?? "")
|
|
954
|
+
? source.text.ranges
|
|
955
|
+
: source.raw.text.trim() === stripTerminalSequences(lines[0] ?? "")
|
|
956
|
+
? sliceRanges(source.raw.ranges, source.raw.text.length - source.raw.text.trimStart().length, source.raw.text.trimEnd().length)
|
|
957
|
+
: repeatedRange(null, stripTerminalSequences(lines[0] ?? "").length);
|
|
958
|
+
setLineSources(lines, lines.map((_, row) => ({ document: source.raw.document, ranges: row === 0 ? ranges : emptyRanges })));
|
|
959
|
+
this.enclose(token, ranges);
|
|
960
|
+
}
|
|
961
|
+
if (!source && this.activeSourceDocument) {
|
|
962
|
+
const document = this.activeSourceDocument;
|
|
963
|
+
setLineSources(lines, lines.map(() => ({ document, ranges: emptyRanges, unavailable: true })));
|
|
964
|
+
}
|
|
965
|
+
else if (token.type === "blockquote") {
|
|
966
|
+
this.enclose(token, concatRanges((getLineSources(lines) ?? []).map((line) => line?.ranges ?? emptyRanges)));
|
|
967
|
+
}
|
|
717
968
|
return lines;
|
|
718
969
|
}
|
|
719
970
|
/**
|
|
720
971
|
* Render inline `strong`/`em`/`del` through a theme hook. The hook is applied inside the surrounding style context,
|
|
721
972
|
* so a color it sets wins over the heading or default text color instead of being overridden by it.
|
|
722
973
|
*/
|
|
723
|
-
renderEmphasisHook(tokens, styleContext, styleFn) {
|
|
974
|
+
renderEmphasisHook(tokens, styleContext, styleFn, ranges) {
|
|
724
975
|
const innerStyleContext = {
|
|
725
976
|
applyText: (text) => styleContext.applyText(styleFn(text)),
|
|
726
977
|
stylePrefix: styleContext.stylePrefix + this.getStylePrefix(styleFn),
|
|
727
978
|
};
|
|
728
|
-
return styleFn(this.renderInlineTokens(tokens, innerStyleContext)) + styleContext.stylePrefix;
|
|
979
|
+
return styleFn(this.renderInlineTokens(tokens, innerStyleContext, ranges)) + styleContext.stylePrefix;
|
|
729
980
|
}
|
|
730
|
-
renderInlineTokens(tokens, styleContext) {
|
|
981
|
+
renderInlineTokens(tokens, styleContext, ranges) {
|
|
731
982
|
let result = "";
|
|
732
983
|
const resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();
|
|
733
984
|
const { applyText, stylePrefix } = resolvedStyleContext;
|
|
@@ -736,6 +987,8 @@ export class Markdown {
|
|
|
736
987
|
return segments.map((segment) => applyText(segment)).join("\n");
|
|
737
988
|
};
|
|
738
989
|
for (const token of tokens) {
|
|
990
|
+
const start = result.length;
|
|
991
|
+
const childRanges = ranges ? [] : undefined;
|
|
739
992
|
switch (token.type) {
|
|
740
993
|
case "escape":
|
|
741
994
|
result += applyTextWithNewlines(this.options.preserveBackslashEscapes ? token.raw : token.text);
|
|
@@ -743,7 +996,7 @@ export class Markdown {
|
|
|
743
996
|
case "text":
|
|
744
997
|
// Text tokens in list items can have nested tokens for inline formatting
|
|
745
998
|
if (token.tokens && token.tokens.length > 0) {
|
|
746
|
-
result += this.renderInlineTokens(token.tokens, resolvedStyleContext);
|
|
999
|
+
result += this.renderInlineTokens(token.tokens, resolvedStyleContext, childRanges);
|
|
747
1000
|
}
|
|
748
1001
|
else {
|
|
749
1002
|
result += applyTextWithNewlines(token.text);
|
|
@@ -751,23 +1004,23 @@ export class Markdown {
|
|
|
751
1004
|
break;
|
|
752
1005
|
case "paragraph":
|
|
753
1006
|
// Paragraph tokens contain nested inline tokens
|
|
754
|
-
result += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
1007
|
+
result += this.renderInlineTokens(token.tokens || [], resolvedStyleContext, childRanges);
|
|
755
1008
|
break;
|
|
756
1009
|
case "strong": {
|
|
757
1010
|
if (this.theme.strong) {
|
|
758
|
-
result += this.renderEmphasisHook(token.tokens || [], resolvedStyleContext, this.theme.strong);
|
|
1011
|
+
result += this.renderEmphasisHook(token.tokens || [], resolvedStyleContext, this.theme.strong, childRanges);
|
|
759
1012
|
break;
|
|
760
1013
|
}
|
|
761
|
-
const boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
1014
|
+
const boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext, childRanges);
|
|
762
1015
|
result += this.theme.bold(boldContent) + stylePrefix;
|
|
763
1016
|
break;
|
|
764
1017
|
}
|
|
765
1018
|
case "em": {
|
|
766
1019
|
if (this.theme.emphasis) {
|
|
767
|
-
result += this.renderEmphasisHook(token.tokens || [], resolvedStyleContext, this.theme.emphasis);
|
|
1020
|
+
result += this.renderEmphasisHook(token.tokens || [], resolvedStyleContext, this.theme.emphasis, childRanges);
|
|
768
1021
|
break;
|
|
769
1022
|
}
|
|
770
|
-
const italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
1023
|
+
const italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext, childRanges);
|
|
771
1024
|
result += this.theme.italic(italicContent) + stylePrefix;
|
|
772
1025
|
break;
|
|
773
1026
|
}
|
|
@@ -785,7 +1038,7 @@ export class Markdown {
|
|
|
785
1038
|
}
|
|
786
1039
|
break;
|
|
787
1040
|
case "link": {
|
|
788
|
-
const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
1041
|
+
const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext, token.autolink ? undefined : childRanges);
|
|
789
1042
|
const styledLink = this.theme.link(this.theme.underline(linkText));
|
|
790
1043
|
if (getCapabilities().hyperlinks) {
|
|
791
1044
|
// OSC 8: render as a clickable hyperlink. The URL is not printed inline,
|
|
@@ -812,10 +1065,10 @@ export class Markdown {
|
|
|
812
1065
|
break;
|
|
813
1066
|
case "del": {
|
|
814
1067
|
if (this.theme.deleted) {
|
|
815
|
-
result += this.renderEmphasisHook(token.tokens || [], resolvedStyleContext, this.theme.deleted);
|
|
1068
|
+
result += this.renderEmphasisHook(token.tokens || [], resolvedStyleContext, this.theme.deleted, childRanges);
|
|
816
1069
|
break;
|
|
817
1070
|
}
|
|
818
|
-
const delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
1071
|
+
const delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext, childRanges);
|
|
819
1072
|
result += this.theme.strikethrough(delContent) + stylePrefix;
|
|
820
1073
|
break;
|
|
821
1074
|
}
|
|
@@ -831,6 +1084,51 @@ export class Markdown {
|
|
|
831
1084
|
result += applyTextWithNewlines(token.text);
|
|
832
1085
|
}
|
|
833
1086
|
}
|
|
1087
|
+
if (!ranges)
|
|
1088
|
+
continue;
|
|
1089
|
+
const rendered = stripTerminalSequences(result.slice(start));
|
|
1090
|
+
const source = tokenSource(token);
|
|
1091
|
+
let mapped = childRanges?.length
|
|
1092
|
+
? concatRanges(childRanges)
|
|
1093
|
+
: source?.text?.text === rendered
|
|
1094
|
+
? source.text.ranges
|
|
1095
|
+
: source?.raw.text === rendered
|
|
1096
|
+
? source.raw.ranges
|
|
1097
|
+
: repeatedRange(null, rendered.length);
|
|
1098
|
+
if (token.type === "latex_inline" && isLatexToken(token)) {
|
|
1099
|
+
mapped = formulaRanges(token, rendered);
|
|
1100
|
+
}
|
|
1101
|
+
else if (token.type === "link" && token.autolink && source?.text && rendered.startsWith(source.text.text)) {
|
|
1102
|
+
// Marked's synthetic label has no child lexer location. A visible
|
|
1103
|
+
// suffix such as " (http://www.example.com)" is presentation only.
|
|
1104
|
+
mapped = concatRanges([
|
|
1105
|
+
source.text.ranges,
|
|
1106
|
+
repeatedRange(undefined, rendered.length - source.text.ranges.length),
|
|
1107
|
+
]);
|
|
1108
|
+
}
|
|
1109
|
+
else if (token.type === "link" && mapped.length < rendered.length && source?.href) {
|
|
1110
|
+
mapped = concatRanges([
|
|
1111
|
+
mapped,
|
|
1112
|
+
repeatedRange(undefined, 2),
|
|
1113
|
+
source.href.ranges,
|
|
1114
|
+
repeatedRange(undefined, 1),
|
|
1115
|
+
]);
|
|
1116
|
+
}
|
|
1117
|
+
else if (token.type === "link" && mapped.length < rendered.length && source) {
|
|
1118
|
+
// A reference destination lives elsewhere; its visible suffix selects
|
|
1119
|
+
// only this link's source, never the remote definition or intervening prose.
|
|
1120
|
+
const first = rangeAt(source.raw.ranges, 0);
|
|
1121
|
+
const last = rangeAt(source.raw.ranges, source.raw.ranges.length - 1);
|
|
1122
|
+
mapped = concatRanges([
|
|
1123
|
+
mapped,
|
|
1124
|
+
repeatedRange(first && last ? { start: first.start, end: last.end } : null, rendered.length - mapped.length),
|
|
1125
|
+
]);
|
|
1126
|
+
}
|
|
1127
|
+
if (mapped.length < rendered.length) {
|
|
1128
|
+
mapped = concatRanges([mapped, repeatedRange(null, rendered.length - mapped.length)]);
|
|
1129
|
+
}
|
|
1130
|
+
ranges.push(mapped);
|
|
1131
|
+
this.enclose(token, token.type === "link" ? (source?.text?.ranges ?? mapped) : mapped);
|
|
834
1132
|
}
|
|
835
1133
|
while (stylePrefix && result.endsWith(stylePrefix)) {
|
|
836
1134
|
result = result.slice(0, -stylePrefix.length);
|
|
@@ -871,26 +1169,68 @@ export class Markdown {
|
|
|
871
1169
|
let renderedAnyLine = false;
|
|
872
1170
|
for (const itemToken of item.tokens) {
|
|
873
1171
|
if (itemToken.type === "list") {
|
|
874
|
-
|
|
1172
|
+
const childLines = this.renderList(itemToken, depth + 1, width, styleContext);
|
|
1173
|
+
const offset = lines.length;
|
|
1174
|
+
lines.push(...childLines);
|
|
1175
|
+
copyLineSources(childLines, lines, offset);
|
|
875
1176
|
renderedAnyLine = true;
|
|
876
1177
|
continue;
|
|
877
1178
|
}
|
|
878
1179
|
const itemLines = this.renderToken(itemToken, itemWidth, undefined, styleContext);
|
|
879
|
-
for (
|
|
880
|
-
|
|
1180
|
+
for (let row = 0; row < itemLines.length; row++) {
|
|
1181
|
+
const line = itemLines[row];
|
|
1182
|
+
const fragments = this.wrapSource(line, itemWidth, getLineSources(itemLines)?.[row]);
|
|
1183
|
+
for (let part = 0; part < fragments.length; part++) {
|
|
1184
|
+
const wrappedLine = fragments[part];
|
|
881
1185
|
const linePrefix = renderedAnyLine ? continuationPrefix : firstPrefix;
|
|
882
1186
|
lines.push(linePrefix + wrappedLine);
|
|
1187
|
+
const source = getLineSources(fragments)?.[part];
|
|
1188
|
+
if (source) {
|
|
1189
|
+
let markerRange;
|
|
1190
|
+
if (!renderedAnyLine) {
|
|
1191
|
+
const raw = tokenSource(item)?.raw;
|
|
1192
|
+
const prefix = /^(?: {0,3})(?:[-+*]|\d{1,9}[.)])(?:[ \t]+|$)(?:\[[ xX]\][ \t]+)?/.exec(raw?.text ?? "");
|
|
1193
|
+
if (raw && prefix?.[0].length) {
|
|
1194
|
+
const start = rangeAt(raw.ranges, 0)?.start;
|
|
1195
|
+
const end = rangeAt(raw.ranges, prefix[0].length - 1)?.end;
|
|
1196
|
+
if (start !== undefined && end !== undefined)
|
|
1197
|
+
markerRange = { start, end };
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
setLineSource(lines, lines.length - 1, {
|
|
1201
|
+
...source,
|
|
1202
|
+
ranges: concatRanges([
|
|
1203
|
+
repeatedRange(undefined, indent.length),
|
|
1204
|
+
repeatedRange(markerRange, visibleWidth(linePrefix) - indent.length),
|
|
1205
|
+
source.ranges,
|
|
1206
|
+
]),
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
883
1209
|
renderedAnyLine = true;
|
|
884
1210
|
}
|
|
885
1211
|
}
|
|
886
1212
|
}
|
|
887
1213
|
if (!renderedAnyLine) {
|
|
888
1214
|
lines.push(firstPrefix);
|
|
1215
|
+
const raw = tokenSource(item)?.raw;
|
|
1216
|
+
const start = raw && rangeAt(raw.ranges, 0)?.start;
|
|
1217
|
+
const end = raw && rangeAt(raw.ranges, raw.ranges.length - 1)?.end;
|
|
1218
|
+
if (raw && start !== undefined && end !== undefined) {
|
|
1219
|
+
setLineSource(lines, lines.length - 1, {
|
|
1220
|
+
document: raw.document,
|
|
1221
|
+
ranges: repeatedRange({ start, end }, visibleWidth(firstPrefix)),
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
889
1224
|
}
|
|
890
1225
|
if (token.loose && !isLastItem) {
|
|
891
1226
|
lines.push("");
|
|
892
1227
|
}
|
|
893
1228
|
}
|
|
1229
|
+
const document = tokenSource(token)?.raw.document;
|
|
1230
|
+
if (document) {
|
|
1231
|
+
const mapping = getLineSources(lines);
|
|
1232
|
+
setLineSources(lines, lines.map((_, row) => mapping?.[row] ?? { document, ranges: emptyRanges }));
|
|
1233
|
+
}
|
|
894
1234
|
return lines;
|
|
895
1235
|
}
|
|
896
1236
|
/**
|
|
@@ -913,13 +1253,13 @@ export class Markdown {
|
|
|
913
1253
|
* Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled
|
|
914
1254
|
* consistently with the rest of the renderer.
|
|
915
1255
|
*/
|
|
916
|
-
wrapCellText(text, maxWidth, stylePrefix = "") {
|
|
917
|
-
const lines =
|
|
918
|
-
return lines.map((line, index) => {
|
|
1256
|
+
wrapCellText(text, maxWidth, stylePrefix = "", source) {
|
|
1257
|
+
const lines = this.wrapSource(text, Math.max(1, maxWidth), source);
|
|
1258
|
+
return copyLineSources(lines, lines.map((line, index) => {
|
|
919
1259
|
// Reset text styles after each non-final fragment, then restore the surrounding style before padding and borders.
|
|
920
1260
|
const styleReset = index < lines.length - 1 ? "\x1b[22;23;24;25;27;28;29;39m" : "";
|
|
921
1261
|
return `${line}${styleReset}${stylePrefix}`;
|
|
922
|
-
});
|
|
1262
|
+
}));
|
|
923
1263
|
}
|
|
924
1264
|
/**
|
|
925
1265
|
* Render a table with width-aware cell wrapping.
|
|
@@ -927,6 +1267,7 @@ export class Markdown {
|
|
|
927
1267
|
*/
|
|
928
1268
|
renderTable(token, availableWidth, nextTokenType, styleContext) {
|
|
929
1269
|
const lines = [];
|
|
1270
|
+
const source = tokenSource(token)?.raw;
|
|
930
1271
|
const numCols = token.header.length;
|
|
931
1272
|
if (numCols === 0) {
|
|
932
1273
|
return lines;
|
|
@@ -937,7 +1278,9 @@ export class Markdown {
|
|
|
937
1278
|
const availableForCells = availableWidth - borderOverhead;
|
|
938
1279
|
if (availableForCells < numCols) {
|
|
939
1280
|
// Too narrow to render a stable table. Fall back to raw markdown.
|
|
940
|
-
const fallbackLines = token.raw
|
|
1281
|
+
const fallbackLines = token.raw
|
|
1282
|
+
? this.wrapSource(token.raw, availableWidth, source && { document: source.document, ranges: sliceRanges(source.ranges, 0, token.raw.length) })
|
|
1283
|
+
: [];
|
|
941
1284
|
if (nextTokenType && nextTokenType !== "space") {
|
|
942
1285
|
fallbackLines.push("");
|
|
943
1286
|
}
|
|
@@ -1026,8 +1369,9 @@ export class Markdown {
|
|
|
1026
1369
|
lines.push(`┌─${topBorderCells.join("─┬─")}─┐`);
|
|
1027
1370
|
// Render header with wrapping
|
|
1028
1371
|
const headerCellLines = token.header.map((cell, i) => {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1372
|
+
const ranges = this.collectSources ? [] : undefined;
|
|
1373
|
+
const text = this.renderInlineTokens(cell.tokens || [], styleContext, ranges);
|
|
1374
|
+
return this.wrapCellText(text, columnWidths[i], styleContext?.stylePrefix, source && { document: source.document, ranges: concatRanges(ranges ?? []) });
|
|
1031
1375
|
});
|
|
1032
1376
|
const headerLineCount = Math.max(...headerCellLines.map((c) => c.length));
|
|
1033
1377
|
for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
|
|
@@ -1037,6 +1381,8 @@ export class Markdown {
|
|
|
1037
1381
|
return this.theme.bold(padded);
|
|
1038
1382
|
});
|
|
1039
1383
|
lines.push(`│ ${rowParts.join(" │ ")} │`);
|
|
1384
|
+
if (source)
|
|
1385
|
+
this.mapTableRow(lines, headerCellLines, lineIdx, columnWidths, source.document);
|
|
1040
1386
|
}
|
|
1041
1387
|
// Render separator
|
|
1042
1388
|
const separatorCells = columnWidths.map((w) => "─".repeat(w));
|
|
@@ -1046,8 +1392,9 @@ export class Markdown {
|
|
|
1046
1392
|
for (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {
|
|
1047
1393
|
const row = token.rows[rowIndex];
|
|
1048
1394
|
const rowCellLines = row.map((cell, i) => {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1395
|
+
const ranges = this.collectSources ? [] : undefined;
|
|
1396
|
+
const text = this.renderInlineTokens(cell.tokens || [], styleContext, ranges);
|
|
1397
|
+
return this.wrapCellText(text, columnWidths[i], styleContext?.stylePrefix, source && { document: source.document, ranges: concatRanges(ranges ?? []) });
|
|
1051
1398
|
});
|
|
1052
1399
|
const rowLineCount = Math.max(...rowCellLines.map((c) => c.length));
|
|
1053
1400
|
for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
|
|
@@ -1056,6 +1403,8 @@ export class Markdown {
|
|
|
1056
1403
|
return text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
|
|
1057
1404
|
});
|
|
1058
1405
|
lines.push(`│ ${rowParts.join(" │ ")} │`);
|
|
1406
|
+
if (source)
|
|
1407
|
+
this.mapTableRow(lines, rowCellLines, lineIdx, columnWidths, source.document);
|
|
1059
1408
|
}
|
|
1060
1409
|
if (rowIndex < token.rows.length - 1) {
|
|
1061
1410
|
lines.push(separatorLine);
|
|
@@ -1067,7 +1416,22 @@ export class Markdown {
|
|
|
1067
1416
|
if (nextTokenType && nextTokenType !== "space") {
|
|
1068
1417
|
lines.push(""); // Add spacing after table
|
|
1069
1418
|
}
|
|
1419
|
+
if (source) {
|
|
1420
|
+
const mapping = getLineSources(lines);
|
|
1421
|
+
setLineSources(lines, lines.map((_, row) => mapping?.[row] ?? { document: source.document, ranges: emptyRanges }));
|
|
1422
|
+
this.enclose(token, concatRanges((getLineSources(lines) ?? []).map((line) => line?.ranges ?? emptyRanges)));
|
|
1423
|
+
}
|
|
1070
1424
|
return lines;
|
|
1071
1425
|
}
|
|
1426
|
+
mapTableRow(lines, cells, row, widths, document) {
|
|
1427
|
+
const ranges = [repeatedRange(undefined, 2)];
|
|
1428
|
+
for (let col = 0; col < cells.length; col++) {
|
|
1429
|
+
const text = cells[col][row] ?? "";
|
|
1430
|
+
const cell = getLineSources(cells[col])?.[row];
|
|
1431
|
+
ranges.push(cell?.ranges ?? emptyRanges);
|
|
1432
|
+
ranges.push(repeatedRange(undefined, Math.max(0, widths[col] - visibleWidth(text)) + 3));
|
|
1433
|
+
}
|
|
1434
|
+
setLineSource(lines, lines.length - 1, { document, ranges: concatRanges(ranges) });
|
|
1435
|
+
}
|
|
1072
1436
|
}
|
|
1073
1437
|
//# sourceMappingURL=markdown.js.map
|