@dr-ishaan/rehype-perfect-code-blocks 2.1.0 → 2.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/CHANGELOG.md +144 -0
- package/dist/classes.d.ts +58 -0
- package/dist/classes.d.ts.map +1 -0
- package/dist/classes.js +62 -0
- package/dist/classes.js.map +1 -0
- package/dist/diagrams.d.ts +41 -0
- package/dist/diagrams.d.ts.map +1 -0
- package/dist/diagrams.js +114 -0
- package/dist/diagrams.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/meta.d.ts.map +1 -1
- package/dist/meta.js +18 -2
- package/dist/meta.js.map +1 -1
- package/dist/shiki.d.ts.map +1 -1
- package/dist/shiki.js +80 -0
- package/dist/shiki.js.map +1 -1
- package/dist/styles.css +160 -0
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js +76 -3
- package/dist/transformer.js.map +1 -1
- package/dist/types.d.ts +78 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/classes.ts +64 -0
- package/src/diagrams.ts +125 -0
- package/src/index.ts +12 -0
- package/src/mermaid.d.ts +11 -0
- package/src/meta.ts +19 -2
- package/src/shiki.ts +82 -0
- package/src/styles.css +160 -0
- package/src/transformer.ts +79 -3
- package/src/types.ts +93 -1
package/src/shiki.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { visit } from 'unist-util-visit';
|
|
|
19
19
|
import type { PerfectCodeOptions } from './types.js';
|
|
20
20
|
import { computeThemeAwareDefaults } from './color-utils.js';
|
|
21
21
|
import { isMathLanguage, renderMath, resolveMathOptions, MATH_LANGS } from './math.js';
|
|
22
|
+
import { isMermaidLanguage, renderMermaid, isCsvLanguage, buildCsvTable } from './diagrams.js';
|
|
22
23
|
import {
|
|
23
24
|
transformerNotationDiff,
|
|
24
25
|
transformerNotationFocus,
|
|
@@ -490,6 +491,87 @@ export async function runShikiOnRawBlocks(
|
|
|
490
491
|
targets.splice(0, targets.length, ...codeTargets);
|
|
491
492
|
}
|
|
492
493
|
|
|
494
|
+
// v2.3.0: Handle Mermaid diagram blocks — render as SVG instead of Shiki.
|
|
495
|
+
if ((opts as { mermaid?: boolean }).mermaid) {
|
|
496
|
+
const mermaidTargets: Element[] = [];
|
|
497
|
+
const remainingTargets: Element[] = [];
|
|
498
|
+
for (const pre of targets) {
|
|
499
|
+
const code = pre.children.find(
|
|
500
|
+
(c): c is Element => c.type === 'element' && c.tagName === 'code'
|
|
501
|
+
);
|
|
502
|
+
if (!code) { remainingTargets.push(pre); continue; }
|
|
503
|
+
const cls = (code.properties?.className as string[] | undefined) ?? [];
|
|
504
|
+
const langClass = cls.find((c) => c.startsWith('language-'));
|
|
505
|
+
const lang = langClass ? langClass.replace('language-', '') : '';
|
|
506
|
+
if (isMermaidLanguage(lang)) {
|
|
507
|
+
mermaidTargets.push(pre);
|
|
508
|
+
} else {
|
|
509
|
+
remainingTargets.push(pre);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (const pre of mermaidTargets) {
|
|
514
|
+
const code = pre.children.find(
|
|
515
|
+
(c): c is Element => c.type === 'element' && c.tagName === 'code'
|
|
516
|
+
);
|
|
517
|
+
if (!code) continue;
|
|
518
|
+
const text = extractText(code).replace(/\r\n?/g, '\n').trim();
|
|
519
|
+
const { svg, isError } = await renderMermaid(text);
|
|
520
|
+
const mermaidDiv: Element = {
|
|
521
|
+
type: 'element',
|
|
522
|
+
tagName: 'div',
|
|
523
|
+
properties: { className: ['pcb__mermaid', isError ? 'pcb__mermaid--error' : 'pcb__mermaid--rendered'] },
|
|
524
|
+
children: svg
|
|
525
|
+
? [{ type: 'text', value: svg }]
|
|
526
|
+
: [{ type: 'element', tagName: 'pre', properties: {}, children: [{ type: 'text', value: text }] }],
|
|
527
|
+
};
|
|
528
|
+
Object.assign(pre, mermaidDiv);
|
|
529
|
+
}
|
|
530
|
+
targets.splice(0, targets.length, ...remainingTargets);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// v2.3.0: Handle CSV/TSV table blocks — render as HTML table instead of Shiki.
|
|
534
|
+
if ((opts as { csvTables?: boolean }).csvTables) {
|
|
535
|
+
const csvTargets: Element[] = [];
|
|
536
|
+
const remainingTargets: Element[] = [];
|
|
537
|
+
for (const pre of targets) {
|
|
538
|
+
const code = pre.children.find(
|
|
539
|
+
(c): c is Element => c.type === 'element' && c.tagName === 'code'
|
|
540
|
+
);
|
|
541
|
+
if (!code) { remainingTargets.push(pre); continue; }
|
|
542
|
+
const cls = (code.properties?.className as string[] | undefined) ?? [];
|
|
543
|
+
const langClass = cls.find((c) => c.startsWith('language-'));
|
|
544
|
+
const lang = langClass ? langClass.replace('language-', '') : '';
|
|
545
|
+
if (isCsvLanguage(lang)) {
|
|
546
|
+
csvTargets.push(pre);
|
|
547
|
+
} else {
|
|
548
|
+
remainingTargets.push(pre);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (const pre of csvTargets) {
|
|
553
|
+
const code = pre.children.find(
|
|
554
|
+
(c): c is Element => c.type === 'element' && c.tagName === 'code'
|
|
555
|
+
);
|
|
556
|
+
if (!code) continue;
|
|
557
|
+
const text = extractText(code).replace(/\r\n?/g, '\n').trim();
|
|
558
|
+
const cls = (code.properties?.className as string[] | undefined) ?? [];
|
|
559
|
+
const langClass = cls.find((c) => c.startsWith('language-'));
|
|
560
|
+
const lang = langClass ? langClass.replace('language-', '') : 'csv';
|
|
561
|
+
const delimiter = lang.toLowerCase() === 'tsv' ? '\t' : ',';
|
|
562
|
+
const tableEl = buildCsvTable(text, delimiter);
|
|
563
|
+
// Replace the <pre> with the table wrapped in a div
|
|
564
|
+
const tableDiv: Element = {
|
|
565
|
+
type: 'element',
|
|
566
|
+
tagName: 'div',
|
|
567
|
+
properties: { className: ['pcb__csv-table'] },
|
|
568
|
+
children: [tableEl],
|
|
569
|
+
};
|
|
570
|
+
Object.assign(pre, tableDiv);
|
|
571
|
+
}
|
|
572
|
+
targets.splice(0, targets.length, ...remainingTargets);
|
|
573
|
+
}
|
|
574
|
+
|
|
493
575
|
if (targets.length === 0) return;
|
|
494
576
|
|
|
495
577
|
// Build theme keys — supports single (string), dual ({light,dark}), and
|
package/src/styles.css
CHANGED
|
@@ -654,3 +654,163 @@
|
|
|
654
654
|
html.no-js .pcb__copy {
|
|
655
655
|
display: none !important;
|
|
656
656
|
}
|
|
657
|
+
|
|
658
|
+
/* ============================================================
|
|
659
|
+
v2.2.0: Phase 3 — Split diff, annotations, attribution
|
|
660
|
+
============================================================ */
|
|
661
|
+
|
|
662
|
+
/* ---------- Split diff view ---------- */
|
|
663
|
+
:where(.pcb--split-diff) .pcb__body {
|
|
664
|
+
display: grid;
|
|
665
|
+
grid-template-columns: 1fr 1fr;
|
|
666
|
+
}
|
|
667
|
+
:where(.pcb--split-diff) .pcb__body > pre {
|
|
668
|
+
border-right: 1px solid var(--pcb-border);
|
|
669
|
+
}
|
|
670
|
+
:where(.pcb--split-diff) .pcb__body > pre:last-child) {
|
|
671
|
+
border-right: none;
|
|
672
|
+
}
|
|
673
|
+
@media (max-width: 768px) {
|
|
674
|
+
:where(.pcb--split-diff) .pcb__body {
|
|
675
|
+
grid-template-columns: 1fr;
|
|
676
|
+
}
|
|
677
|
+
:where(.pcb--split-diff) .pcb__body > pre {
|
|
678
|
+
border-right: none;
|
|
679
|
+
border-bottom: 1px solid var(--pcb-border);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/* ---------- Line annotations ---------- */
|
|
684
|
+
:where(.pcb__ann) {
|
|
685
|
+
display: none; /* hidden by default, shown when .pcb--annotations */
|
|
686
|
+
}
|
|
687
|
+
:where(.pcb--annotations) .pcb__line[data-ann] {
|
|
688
|
+
display: grid;
|
|
689
|
+
grid-template-columns: auto 1fr auto;
|
|
690
|
+
align-items: baseline;
|
|
691
|
+
}
|
|
692
|
+
:where(.pcb--annotations) .pcb__ann) {
|
|
693
|
+
display: block;
|
|
694
|
+
padding-left: 1rem;
|
|
695
|
+
color: var(--pcb-text-muted);
|
|
696
|
+
font-size: 0.8125em;
|
|
697
|
+
font-style: italic;
|
|
698
|
+
white-space: normal;
|
|
699
|
+
border-left: 2px solid var(--pcb-border);
|
|
700
|
+
user-select: none;
|
|
701
|
+
}
|
|
702
|
+
@media (max-width: 768px) {
|
|
703
|
+
:where(.pcb--annotations) .pcb__line[data-ann]) {
|
|
704
|
+
grid-template-columns: auto 1fr;
|
|
705
|
+
}
|
|
706
|
+
:where(.pcb--annotations) .pcb__ann) {
|
|
707
|
+
grid-column: 1 / -1;
|
|
708
|
+
padding-left: 0;
|
|
709
|
+
border-left: none;
|
|
710
|
+
border-top: 1px dashed var(--pcb-border);
|
|
711
|
+
margin-top: 0.25rem;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/* ---------- Attribution footer ---------- */
|
|
716
|
+
:where(.pcb__attribution) {
|
|
717
|
+
padding: 0.5rem 1rem;
|
|
718
|
+
font-family: var(--pcb-bar-font);
|
|
719
|
+
font-size: var(--pcb-bar-font-size);
|
|
720
|
+
color: var(--pcb-caption-color);
|
|
721
|
+
background: var(--pcb-caption-bg);
|
|
722
|
+
border-top: 1px solid var(--pcb-border);
|
|
723
|
+
font-style: italic;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/* ============================================================
|
|
727
|
+
v2.3.0: P2 — Retro preset, Mermaid, CSV tables, ASCII art, A11y
|
|
728
|
+
============================================================ */
|
|
729
|
+
|
|
730
|
+
/* ---------- Retro CRT preset ---------- */
|
|
731
|
+
:where(.pcb--retro) {
|
|
732
|
+
--pcb-bg: #000000;
|
|
733
|
+
--pcb-fg: #00ff41;
|
|
734
|
+
--pcb-text-muted: #008f11;
|
|
735
|
+
--pcb-border: #00ff41;
|
|
736
|
+
--pcb-bg-header: #001100;
|
|
737
|
+
--pcb-bg-gutter: #000000;
|
|
738
|
+
--pcb-radius: 0px;
|
|
739
|
+
--pcb-font-mono: 'Courier New', 'Lucida Console', monospace;
|
|
740
|
+
--pcb-bar-font: 'Courier New', monospace;
|
|
741
|
+
text-shadow: 0 0 2px currentColor, 0 0 4px rgba(0, 255, 65, 0.3);
|
|
742
|
+
}
|
|
743
|
+
:where(.pcb--retro) .pcb__body {
|
|
744
|
+
position: relative;
|
|
745
|
+
}
|
|
746
|
+
:where(.pcb--retro) .pcb__body::after {
|
|
747
|
+
content: '';
|
|
748
|
+
position: absolute;
|
|
749
|
+
inset: 0;
|
|
750
|
+
background: repeating-linear-gradient(
|
|
751
|
+
0deg,
|
|
752
|
+
rgba(0, 0, 0, 0) 0px,
|
|
753
|
+
rgba(0, 0, 0, 0) 2px,
|
|
754
|
+
rgba(0, 0, 0, 0.08) 2px,
|
|
755
|
+
rgba(0, 0, 0, 0.08) 4px
|
|
756
|
+
);
|
|
757
|
+
pointer-events: none;
|
|
758
|
+
}
|
|
759
|
+
:where(.pcb--retro) .pcb__dots span) {
|
|
760
|
+
background: #00ff41;
|
|
761
|
+
opacity: 0.6;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* ---------- Mermaid diagram container ---------- */
|
|
765
|
+
:where(.pcb__mermaid) {
|
|
766
|
+
padding: 1rem;
|
|
767
|
+
background: var(--pcb-bg);
|
|
768
|
+
overflow-x: auto;
|
|
769
|
+
text-align: center;
|
|
770
|
+
}
|
|
771
|
+
:where(.pcb__mermaid svg) {
|
|
772
|
+
max-width: 100%;
|
|
773
|
+
height: auto;
|
|
774
|
+
}
|
|
775
|
+
:where(.pcb__mermaid--error) {
|
|
776
|
+
color: var(--pcb-text-muted);
|
|
777
|
+
font-family: var(--pcb-font-mono);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* ---------- CSV/TSV table ---------- */
|
|
781
|
+
:where(.pcb__csv-table) {
|
|
782
|
+
padding: 0;
|
|
783
|
+
background: var(--pcb-bg);
|
|
784
|
+
overflow-x: auto;
|
|
785
|
+
}
|
|
786
|
+
:where(.pcb__table) {
|
|
787
|
+
width: 100%;
|
|
788
|
+
border-collapse: collapse;
|
|
789
|
+
font-family: var(--pcb-font-mono);
|
|
790
|
+
font-size: var(--pcb-font-size);
|
|
791
|
+
}
|
|
792
|
+
:where(.pcb__th) {
|
|
793
|
+
padding: 0.5rem 1rem;
|
|
794
|
+
text-align: left;
|
|
795
|
+
border-bottom: 2px solid var(--pcb-border);
|
|
796
|
+
color: var(--pcb-text-bar);
|
|
797
|
+
font-weight: 600;
|
|
798
|
+
}
|
|
799
|
+
:where(.pcb__td) {
|
|
800
|
+
padding: 0.4rem 1rem;
|
|
801
|
+
border-bottom: 1px solid var(--pcb-border);
|
|
802
|
+
color: var(--pcb-text);
|
|
803
|
+
}
|
|
804
|
+
:where(.pcb__tr:nth-child(even) .pcb__td) {
|
|
805
|
+
background: rgba(127, 127, 127, 0.05);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/* ---------- ASCII art: disable ligatures ---------- */
|
|
809
|
+
:where(.pcb[data-language="text"] .pcb__code),
|
|
810
|
+
:where(.pcb[data-language="plaintext"] .pcb__code),
|
|
811
|
+
:where(.pcb[data-language="txt"] .pcb__code),
|
|
812
|
+
:where(.pcb[data-language="ascii"] .pcb__code),
|
|
813
|
+
:where(.pcb[data-language="plain"] .pcb__code) {
|
|
814
|
+
font-variant-ligatures: none;
|
|
815
|
+
font-feature-settings: "liga" 0, "calt" 0;
|
|
816
|
+
}
|
package/src/transformer.ts
CHANGED
|
@@ -249,6 +249,14 @@ export function rehypePerfectCodeBlocks(userOptions: PerfectCodeOptions = {}) {
|
|
|
249
249
|
scope: undefined as unknown as string,
|
|
250
250
|
math: undefined as unknown as NonNullable<PerfectCodeOptions['math']>,
|
|
251
251
|
devWarnings: process.env.NODE_ENV !== 'production',
|
|
252
|
+
// v2.2.0: Phase 3
|
|
253
|
+
diffMode: 'unified' as const,
|
|
254
|
+
annotations: false,
|
|
255
|
+
attribution: false,
|
|
256
|
+
// v2.3.0: P2
|
|
257
|
+
mermaid: false,
|
|
258
|
+
csvTables: false,
|
|
259
|
+
asciiArtLangs: ['text', 'plaintext', 'txt', 'ascii', 'plain'] as string[],
|
|
252
260
|
inline: false,
|
|
253
261
|
...rest,
|
|
254
262
|
};
|
|
@@ -639,6 +647,31 @@ async function transformPre(
|
|
|
639
647
|
figureChildren.push(cap);
|
|
640
648
|
}
|
|
641
649
|
|
|
650
|
+
// v2.2.0: Attribution footer — render author/year/source as a footer below the code block.
|
|
651
|
+
if ((opts as { attribution?: boolean }).attribution && (meta.author || meta.year || meta.source)) {
|
|
652
|
+
const parts: string[] = [];
|
|
653
|
+
if (meta.author) parts.push(meta.author);
|
|
654
|
+
if (meta.year) parts.push(`(${meta.year})`);
|
|
655
|
+
if (meta.source) parts.push(`. ${meta.source}.`);
|
|
656
|
+
else if (meta.author || meta.year) parts.push('.');
|
|
657
|
+
const attrText = parts.join(' ').trim();
|
|
658
|
+
if (attrText) {
|
|
659
|
+
figureChildren.push(
|
|
660
|
+
h('figcaption', { className: ['pcb__attribution'] }, [hText(attrText)])
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// v2.2.0: Add pcb--split-diff class when diffMode is 'split'
|
|
666
|
+
if ((opts as { diffMode?: string }).diffMode === 'split') {
|
|
667
|
+
figClasses.push('pcb--split-diff');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// v2.2.0: Add pcb--annotations class when annotations are enabled
|
|
671
|
+
if ((opts as { annotations?: boolean }).annotations) {
|
|
672
|
+
figClasses.push('pcb--annotations');
|
|
673
|
+
}
|
|
674
|
+
|
|
642
675
|
return h('figure', { className: figClasses }, figureChildren);
|
|
643
676
|
}
|
|
644
677
|
|
|
@@ -941,6 +974,19 @@ function toLineSpans(
|
|
|
941
974
|
// Map word-highlight spans inside this line.
|
|
942
975
|
const mappedChildren = mapWordHighlights(line.children);
|
|
943
976
|
|
|
977
|
+
// v2.2.0: Parse and strip // [!ann: "text"] annotation notation.
|
|
978
|
+
let annotationText: string | null = null;
|
|
979
|
+
if ((opts as { annotations?: boolean }).annotations) {
|
|
980
|
+
const lineText = extractLineText(line);
|
|
981
|
+
const annMatch = lineText.match(/\[!ann:\s*"([^"]*)"\s*\]/);
|
|
982
|
+
if (annMatch) {
|
|
983
|
+
annotationText = annMatch[1];
|
|
984
|
+
// Strip the annotation from the line's text content
|
|
985
|
+
// (replace in all text nodes within the line)
|
|
986
|
+
stripAnnotationFromLine(line, annMatch[0]);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
944
990
|
// The line wrapper itself (the Shiki <span class="line ...">) becomes the
|
|
945
991
|
// content of .pcb__code. Strip its classes — we've already mapped them
|
|
946
992
|
// onto the outer .pcb__line wrapper, so they shouldn't also appear here.
|
|
@@ -953,18 +999,36 @@ function toLineSpans(
|
|
|
953
999
|
children: mappedChildren,
|
|
954
1000
|
};
|
|
955
1001
|
|
|
956
|
-
// Build the row: [gutter-cell?, code-cell]
|
|
1002
|
+
// Build the row: [gutter-cell?, code-cell, annotation?]
|
|
957
1003
|
const lineChildren: ElementContent[] = [];
|
|
958
1004
|
if (resolved.lineNumbers) {
|
|
959
1005
|
lineChildren.push(
|
|
960
|
-
h('span', { className: ['pcb__ln'], ariaHidden: true }, [hText(String(lineNumber))])
|
|
1006
|
+
h('span', { className: ['pcb__ln'], ariaHidden: true, 'aria-label': `Line ${lineNumber}` }, [hText(String(lineNumber))])
|
|
961
1007
|
);
|
|
962
1008
|
}
|
|
963
1009
|
lineChildren.push(
|
|
964
1010
|
h('span', { className: ['pcb__code'] }, [innerWrapper])
|
|
965
1011
|
);
|
|
966
1012
|
|
|
967
|
-
|
|
1013
|
+
// v2.2.0: Add annotation cell if this line has an annotation
|
|
1014
|
+
if (annotationText !== null) {
|
|
1015
|
+
lineChildren.push(
|
|
1016
|
+
h('span', { className: ['pcb__ann'], 'dataAnn': annotationText }, [hText(annotationText)])
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const lineProps: Record<string, unknown> = { className: [...classes] };
|
|
1021
|
+
if (annotationText !== null) {
|
|
1022
|
+
lineProps['dataAnn'] = annotationText;
|
|
1023
|
+
}
|
|
1024
|
+
// v2.3.0: Add aria-label for diff lines (accessibility)
|
|
1025
|
+
if (classes.has('pcb__line--add')) {
|
|
1026
|
+
lineProps['aria-label'] = 'Added line';
|
|
1027
|
+
} else if (classes.has('pcb__line--del')) {
|
|
1028
|
+
lineProps['aria-label'] = 'Removed line';
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return h('span', lineProps, lineChildren);
|
|
968
1032
|
});
|
|
969
1033
|
}
|
|
970
1034
|
|
|
@@ -1153,6 +1217,18 @@ function hText(value: string): Text {
|
|
|
1153
1217
|
return { type: 'text', value };
|
|
1154
1218
|
}
|
|
1155
1219
|
|
|
1220
|
+
/** v2.2.0: Strip an annotation notation from all text nodes in a line element. */
|
|
1221
|
+
function stripAnnotationFromLine(line: Element, annotation: string): void {
|
|
1222
|
+
const walk = (node: ElementContent): void => {
|
|
1223
|
+
if (node.type === 'text') {
|
|
1224
|
+
node.value = node.value.replace(annotation, '');
|
|
1225
|
+
} else if (node.type === 'element') {
|
|
1226
|
+
for (const child of node.children) walk(child);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
for (const child of line.children) walk(child);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1156
1232
|
/* ---------- Pattern 5: word-level diff (selective adoption from expressive-code) ---------- */
|
|
1157
1233
|
|
|
1158
1234
|
/**
|
package/src/types.ts
CHANGED
|
@@ -343,7 +343,7 @@ export interface PerfectCodeOptions {
|
|
|
343
343
|
|
|
344
344
|
/* ---------- Styling ---------- */
|
|
345
345
|
/** Visual preset. Default: 'default' */
|
|
346
|
-
preset?: 'default' | 'terminal' | 'minimal';
|
|
346
|
+
preset?: 'default' | 'terminal' | 'minimal' | 'retro';
|
|
347
347
|
/** Inject the bundled CSS automatically. Set false to ship your own. Default: true */
|
|
348
348
|
injectStyles?: boolean;
|
|
349
349
|
/** Manual theme override. Default: 'auto' (prefers-color-scheme) */
|
|
@@ -550,6 +550,94 @@ export interface PerfectCodeOptions {
|
|
|
550
550
|
*/
|
|
551
551
|
devWarnings?: boolean;
|
|
552
552
|
|
|
553
|
+
/* ---------- v2.2.0: Diff & Comparison ---------- */
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Side-by-side diff view. When enabled, adjacent `+`/`-` diff line pairs
|
|
557
|
+
* are rendered in a two-column layout (Before | After) with synchronized
|
|
558
|
+
* scrolling, instead of the default unified diff view.
|
|
559
|
+
*
|
|
560
|
+
* On mobile (viewport < 768px), the columns stack vertically.
|
|
561
|
+
*
|
|
562
|
+
* Default: `'unified'` (backward-compatible with v1.x/v2.0/v2.1)
|
|
563
|
+
*/
|
|
564
|
+
diffMode?: 'unified' | 'split';
|
|
565
|
+
|
|
566
|
+
/* ---------- v2.2.0: Annotations ---------- */
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Line annotations — margin notes attached to specific code lines.
|
|
570
|
+
*
|
|
571
|
+
* Use `// [!ann: "explanation"]` notation inside code to annotate a line.
|
|
572
|
+
* The annotation appears on the right side of the code block, connected
|
|
573
|
+
* to the annotated line with a subtle connector line. On mobile,
|
|
574
|
+
* annotations appear inline below the annotated line.
|
|
575
|
+
*
|
|
576
|
+
* Example:
|
|
577
|
+
* ```ts
|
|
578
|
+
* const attention = Q.dot(K.T) / Math.sqrt(d) // [!ann: "Scaled dot-product"]
|
|
579
|
+
* const weights = softmax(attention) // [!ann: "Normalized weights"]
|
|
580
|
+
* ```
|
|
581
|
+
*
|
|
582
|
+
* Default: `false` (opt-in)
|
|
583
|
+
*/
|
|
584
|
+
annotations?: boolean;
|
|
585
|
+
|
|
586
|
+
/* ---------- v2.2.0: Attribution ---------- */
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Code attribution — structured metadata for code blocks.
|
|
590
|
+
*
|
|
591
|
+
* When enabled, the plugin parses `author`, `year`, and `source` attributes
|
|
592
|
+
* from the fence meta string and renders them as a footer below the code block:
|
|
593
|
+
*
|
|
594
|
+
* ````markdown
|
|
595
|
+
* ```ts title="perceptron.ts" author="Rosenblatt" year="1958" source="Principles of Neurodynamics"
|
|
596
|
+
* const output = stepFunction(inputs.dot(weights))
|
|
597
|
+
* ```
|
|
598
|
+
* ````
|
|
599
|
+
*
|
|
600
|
+
* Renders:
|
|
601
|
+
* ```
|
|
602
|
+
* ┌─ perceptron.ts ──────────────────┐
|
|
603
|
+
* │ const output = stepFunction(...) │
|
|
604
|
+
* └───────────────────────────────────┘
|
|
605
|
+
* Rosenblatt (1958). Principles of Neurodynamics.
|
|
606
|
+
* ```
|
|
607
|
+
*
|
|
608
|
+
* Default: `false` (opt-in; when disabled, author/year/source meta is silently ignored)
|
|
609
|
+
*/
|
|
610
|
+
attribution?: boolean;
|
|
611
|
+
|
|
612
|
+
/* ---------- v2.3.0: P2 — Diagrams, Tables, ASCII, Frames, A11y ---------- */
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Mermaid diagram rendering. When a fenced code block has language `mermaid`,
|
|
616
|
+
* render it as an SVG diagram instead of syntax-highlighted code.
|
|
617
|
+
*
|
|
618
|
+
* `mermaid` must be installed: `npm install mermaid`
|
|
619
|
+
*
|
|
620
|
+
* Default: `false` (mermaid blocks render as plain code)
|
|
621
|
+
*/
|
|
622
|
+
mermaid?: boolean;
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* CSV/TSV table rendering. When a fenced code block has language `csv` or
|
|
626
|
+
* `tsv`, render it as a styled HTML table instead of code.
|
|
627
|
+
*
|
|
628
|
+
* Default: `false` (CSV/TSV blocks render as plain code)
|
|
629
|
+
*/
|
|
630
|
+
csvTables?: boolean;
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* ASCII art preservation. For languages in the list, disable ligatures,
|
|
634
|
+
* preserve trailing whitespace, and set `font-variant-ligatures: none` to
|
|
635
|
+
* ensure ASCII art alignment is maintained.
|
|
636
|
+
*
|
|
637
|
+
* Default: `['text', 'plaintext', 'txt', 'ascii', 'plain']`
|
|
638
|
+
*/
|
|
639
|
+
asciiArtLangs?: string[];
|
|
640
|
+
|
|
553
641
|
/* ---------- Inline code (legacy cosmetic option) ---------- */
|
|
554
642
|
/** Also style inline `code` cosmetically (no tokenization). Default: false */
|
|
555
643
|
inline?: boolean;
|
|
@@ -574,6 +662,10 @@ export interface ParsedMeta {
|
|
|
574
662
|
wordHighlights: { text: string; range?: [number, number]; id?: string }[];
|
|
575
663
|
lineNumbersStart: number | null; // from ln{N} or showLineNumbers{N}
|
|
576
664
|
collapseRanges: { from: number; to: number }[]; // from collapse="5-12,20-30"
|
|
665
|
+
// v2.2.0: Attribution metadata
|
|
666
|
+
author: string | null; // from author="..."
|
|
667
|
+
year: string | null; // from year="..."
|
|
668
|
+
source: string | null; // from source="..."
|
|
577
669
|
flags: {
|
|
578
670
|
wrap: boolean | null;
|
|
579
671
|
lineNumbers: boolean | null;
|