react-diff-viewer-continued 4.3.0 → 4.4.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 +14 -0
- package/lib/cjs/src/expand.d.ts +1 -1
- package/lib/cjs/src/fold.d.ts +1 -1
- package/lib/cjs/src/highlight-theme.d.ts +29 -0
- package/lib/cjs/src/highlight-theme.js +99 -0
- package/lib/cjs/src/highlight.d.ts +69 -0
- package/lib/cjs/src/highlight.js +315 -0
- package/lib/cjs/src/index.d.ts +51 -1
- package/lib/cjs/src/index.js +210 -10
- package/lib/cjs/src/styles.d.ts +3 -0
- package/lib/cjs/src/styles.js +52 -1
- package/lib/esm/src/highlight-theme.js +99 -0
- package/lib/esm/src/highlight.js +311 -0
- package/lib/esm/src/index.js +207 -9
- package/lib/esm/src/styles.js +52 -1
- package/package.json +7 -6
package/lib/cjs/src/index.js
CHANGED
|
@@ -7,6 +7,56 @@ import { DiffMethod, DiffType, computeLineInformationWorker, computeDiff, } from
|
|
|
7
7
|
import { Expand } from "./expand.js";
|
|
8
8
|
import computeStyles from "./styles.js";
|
|
9
9
|
import { Fold } from "./fold.js";
|
|
10
|
+
import { ensureLanguage, highlightSide, mergeHighlightWithDiff, renderHighlightedPlain, sliceLineTokens, } from "./highlight.js";
|
|
11
|
+
import { defaultDarkHighlightTheme, defaultLightHighlightTheme, } from "./highlight-theme.js";
|
|
12
|
+
const LEADING_WHITESPACE = /^[ \t]+/;
|
|
13
|
+
/**
|
|
14
|
+
* Splits a line's leading whitespace off its content so it can be rendered as a
|
|
15
|
+
* separate fixed-width indent column (see `contentFlex`/`lineIndent`/`lineBody`
|
|
16
|
+
* styles). Peeling happens *before* the value is highlighted/word-diffed, so the
|
|
17
|
+
* indent never gets swept into a syntax token or a diff chunk.
|
|
18
|
+
*
|
|
19
|
+
* Handles both shapes `renderLine` receives: a raw string, or a word-diff array
|
|
20
|
+
* whose leading whitespace may span one or more leading chunks (WORDS_WITH_SPACE
|
|
21
|
+
* isolates indentation as its own chunk) or prefix the first chunk. The remaining
|
|
22
|
+
* chunks are returned intact so their diff types/colours are preserved.
|
|
23
|
+
*/
|
|
24
|
+
function splitLeadingWhitespace(value) {
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
const match = value.match(LEADING_WHITESPACE);
|
|
27
|
+
return match
|
|
28
|
+
? { indent: match[0], rest: value.slice(match[0].length) }
|
|
29
|
+
: { indent: "", rest: value };
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
const chunks = value.map((chunk) => (Object.assign({}, chunk)));
|
|
33
|
+
let indent = "";
|
|
34
|
+
let i = 0;
|
|
35
|
+
for (; i < chunks.length; i++) {
|
|
36
|
+
const chunkValue = typeof chunks[i].value === "string"
|
|
37
|
+
? chunks[i].value
|
|
38
|
+
: null;
|
|
39
|
+
if (chunkValue === null)
|
|
40
|
+
break;
|
|
41
|
+
const match = chunkValue.match(LEADING_WHITESPACE);
|
|
42
|
+
if (!match)
|
|
43
|
+
break;
|
|
44
|
+
if (match[0].length === chunkValue.length) {
|
|
45
|
+
// Whole chunk is whitespace — consume it and keep scanning.
|
|
46
|
+
indent += chunkValue;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Chunk starts with whitespace then real content — keep the trimmed remainder.
|
|
50
|
+
indent += match[0];
|
|
51
|
+
chunks[i] = Object.assign(Object.assign({}, chunks[i]), { value: chunkValue.slice(match[0].length) });
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
return indent
|
|
55
|
+
? { indent, rest: chunks.slice(i) }
|
|
56
|
+
: { indent: "", rest: value };
|
|
57
|
+
}
|
|
58
|
+
return { indent: "", rest: value };
|
|
59
|
+
}
|
|
10
60
|
/**
|
|
11
61
|
* Applies diff styling (ins/del tags) to pre-highlighted HTML by walking through
|
|
12
62
|
* the HTML and wrapping text portions based on character positions in the diff.
|
|
@@ -154,6 +204,8 @@ class DiffViewer extends React.Component {
|
|
|
154
204
|
super(props);
|
|
155
205
|
// Cache for on-demand word diff computation
|
|
156
206
|
this.wordDiffCache = new Map();
|
|
207
|
+
// Ensures the highlightLanguage/renderContent precedence warning fires once.
|
|
208
|
+
this.highlightPrecedenceWarned = false;
|
|
157
209
|
// Refs for measuring content column width and character width
|
|
158
210
|
this.contentColumnRef = React.createRef();
|
|
159
211
|
this.charMeasureRef = React.createRef();
|
|
@@ -334,7 +386,7 @@ class DiffViewer extends React.Component {
|
|
|
334
386
|
* @param additionalPrefix Similar to prefix but for additional line number.
|
|
335
387
|
*/
|
|
336
388
|
this.renderLine = (lineNumber, type, prefix, value, additionalLineNumber, additionalPrefix) => {
|
|
337
|
-
var _a;
|
|
389
|
+
var _a, _b;
|
|
338
390
|
const lineNumberTemplate = `${prefix}-${lineNumber}`;
|
|
339
391
|
const additionalLineNumberTemplate = `${additionalPrefix}-${additionalLineNumber}`;
|
|
340
392
|
const highlightLines = (_a = this.props.highlightLines) !== null && _a !== void 0 ? _a : [];
|
|
@@ -343,16 +395,56 @@ class DiffViewer extends React.Component {
|
|
|
343
395
|
const added = type === DiffType.ADDED;
|
|
344
396
|
const removed = type === DiffType.REMOVED;
|
|
345
397
|
const changed = type === DiffType.CHANGED;
|
|
398
|
+
// Peel the leading whitespace into a separate indent column before any
|
|
399
|
+
// highlighting/word-diffing runs, so wrapped continuation lines hang-indent
|
|
400
|
+
// under the line's first character instead of the cell's left edge.
|
|
401
|
+
const { indent, rest } = splitLeadingWhitespace(value);
|
|
402
|
+
const hasWordDiff = Array.isArray(rest);
|
|
403
|
+
// First-party syntax highlighting: look up this line's whole-side tokens and
|
|
404
|
+
// rebase them past the peeled indent so they line up with the diff body.
|
|
405
|
+
const highlightMap = this.state.highlightResult
|
|
406
|
+
? prefix === LineNumberPrefix.LEFT
|
|
407
|
+
? this.state.highlightResult.left
|
|
408
|
+
: this.state.highlightResult.right
|
|
409
|
+
: null;
|
|
410
|
+
const sideLineNumber = (_b = lineNumber !== null && lineNumber !== void 0 ? lineNumber : additionalLineNumber) !== null && _b !== void 0 ? _b : undefined;
|
|
411
|
+
const fullLineTokens = highlightMap && sideLineNumber != null ? highlightMap.get(sideLineNumber) : undefined;
|
|
412
|
+
const bodyTokens = fullLineTokens ? sliceLineTokens(fullLineTokens, indent.length) : undefined;
|
|
413
|
+
// Longest body we'll merge char-by-char before falling back to plain colour.
|
|
414
|
+
const MAX_HIGHLIGHT_MERGE_LENGTH = 500;
|
|
346
415
|
let content;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
416
|
+
if (bodyTokens) {
|
|
417
|
+
if (hasWordDiff) {
|
|
418
|
+
const bodyText = rest
|
|
419
|
+
.map((chunk) => (typeof chunk.value === "string" ? chunk.value : ""))
|
|
420
|
+
.join("");
|
|
421
|
+
content =
|
|
422
|
+
bodyText.length > MAX_HIGHLIGHT_MERGE_LENGTH
|
|
423
|
+
? renderHighlightedPlain(bodyText, bodyTokens)
|
|
424
|
+
: mergeHighlightWithDiff(bodyText, bodyTokens, rest, {
|
|
425
|
+
styles: {
|
|
426
|
+
wordDiff: this.styles.wordDiff,
|
|
427
|
+
wordAdded: this.styles.wordAdded,
|
|
428
|
+
wordRemoved: this.styles.wordRemoved,
|
|
429
|
+
},
|
|
430
|
+
showHighlight: this.shouldHighlightWordDiff(),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else if (typeof rest === "string") {
|
|
434
|
+
content = renderHighlightedPlain(rest, bodyTokens);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
content = rest;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (hasWordDiff) {
|
|
441
|
+
content = this.renderWordDiff(rest, this.props.renderContent);
|
|
350
442
|
}
|
|
351
|
-
else if (this.props.renderContent && typeof
|
|
352
|
-
content = this.props.renderContent(
|
|
443
|
+
else if (this.props.renderContent && typeof rest === "string") {
|
|
444
|
+
content = this.props.renderContent(rest);
|
|
353
445
|
}
|
|
354
446
|
else {
|
|
355
|
-
content =
|
|
447
|
+
content = rest;
|
|
356
448
|
}
|
|
357
449
|
let ElementType = "div";
|
|
358
450
|
if (added && !hasWordDiff) {
|
|
@@ -361,6 +453,9 @@ class DiffViewer extends React.Component {
|
|
|
361
453
|
else if (removed && !hasWordDiff) {
|
|
362
454
|
ElementType = "del";
|
|
363
455
|
}
|
|
456
|
+
// A line is only "empty" (gets the empty-line background) when it has neither
|
|
457
|
+
// body content nor peeled indentation — a whitespace-only line is not empty.
|
|
458
|
+
const isEmpty = !content && !indent;
|
|
364
459
|
return (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers && (_jsx("td", { onClick: lineNumber && this.onLineNumberClickProxy(lineNumberTemplate), className: cn(this.styles.gutter, {
|
|
365
460
|
[this.styles.emptyGutter]: !lineNumber,
|
|
366
461
|
[this.styles.diffAdded]: added,
|
|
@@ -385,13 +480,13 @@ class DiffViewer extends React.Component {
|
|
|
385
480
|
styles: this.styles,
|
|
386
481
|
})
|
|
387
482
|
: null, _jsx("td", { className: cn(this.styles.marker, {
|
|
388
|
-
[this.styles.emptyLine]:
|
|
483
|
+
[this.styles.emptyLine]: isEmpty,
|
|
389
484
|
[this.styles.diffAdded]: added,
|
|
390
485
|
[this.styles.diffRemoved]: removed,
|
|
391
486
|
[this.styles.diffChanged]: changed,
|
|
392
487
|
[this.styles.highlightedLine]: highlightLine,
|
|
393
488
|
}), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
|
|
394
|
-
[this.styles.emptyLine]:
|
|
489
|
+
[this.styles.emptyLine]: isEmpty,
|
|
395
490
|
[this.styles.diffAdded]: added,
|
|
396
491
|
[this.styles.diffRemoved]: removed,
|
|
397
492
|
[this.styles.diffChanged]: changed,
|
|
@@ -416,7 +511,7 @@ class DiffViewer extends React.Component {
|
|
|
416
511
|
? "Added line"
|
|
417
512
|
: removed && !hasWordDiff
|
|
418
513
|
? "Removed line"
|
|
419
|
-
: undefined, children:
|
|
514
|
+
: undefined, children: _jsxs(ElementType, { className: cn(this.styles.contentText, this.styles.contentFlex), children: [indent ? _jsx("span", { className: this.styles.lineIndent, children: indent }) : null, _jsx("span", { className: this.styles.lineBody, children: content })] }) })] }));
|
|
420
515
|
};
|
|
421
516
|
/**
|
|
422
517
|
* Generates lines for split view.
|
|
@@ -516,6 +611,7 @@ class DiffViewer extends React.Component {
|
|
|
516
611
|
const cacheKey = this.getMemoisedKey();
|
|
517
612
|
if (!!this.state.computedDiffResult[cacheKey]) {
|
|
518
613
|
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false })));
|
|
614
|
+
this.updateHighlight();
|
|
519
615
|
return;
|
|
520
616
|
}
|
|
521
617
|
// Defer word diff computation when using infinite loading with reasonable container height
|
|
@@ -536,6 +632,8 @@ class DiffViewer extends React.Component {
|
|
|
536
632
|
const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
|
|
537
633
|
this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
|
|
538
634
|
this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })), () => {
|
|
635
|
+
// Recompute syntax highlighting now that fresh line information exists.
|
|
636
|
+
this.updateHighlight();
|
|
539
637
|
// Trigger offset recalculation after diff is computed and rendered
|
|
540
638
|
// Use requestAnimationFrame to ensure DOM is ready for measurement
|
|
541
639
|
if (this.props.infiniteLoading) {
|
|
@@ -543,6 +641,100 @@ class DiffViewer extends React.Component {
|
|
|
543
641
|
}
|
|
544
642
|
});
|
|
545
643
|
};
|
|
644
|
+
/**
|
|
645
|
+
* Extracts the raw text of one side of a line for whole-side highlighting.
|
|
646
|
+
* Prefers `rawValue` (deferred word diff), then a plain string value, then the
|
|
647
|
+
* concatenation of word-diff chunk values.
|
|
648
|
+
*/
|
|
649
|
+
this.lineToText = (info) => {
|
|
650
|
+
if (typeof info.rawValue === "string")
|
|
651
|
+
return info.rawValue;
|
|
652
|
+
if (typeof info.value === "string")
|
|
653
|
+
return info.value;
|
|
654
|
+
if (Array.isArray(info.value)) {
|
|
655
|
+
return info.value.map((chunk) => (typeof chunk.value === "string" ? chunk.value : "")).join("");
|
|
656
|
+
}
|
|
657
|
+
return "";
|
|
658
|
+
};
|
|
659
|
+
/**
|
|
660
|
+
* Reconstructs one side's full document from the computed line information and
|
|
661
|
+
* highlights it in a single pass, returning per-line tokens keyed by line
|
|
662
|
+
* number. Highlighting the whole side (rather than line-by-line) is what keeps
|
|
663
|
+
* lexer state correct across line boundaries.
|
|
664
|
+
*/
|
|
665
|
+
this.buildSideTokens = (lineInformation, side, language, theme) => {
|
|
666
|
+
var _a;
|
|
667
|
+
const entries = [];
|
|
668
|
+
for (const line of lineInformation) {
|
|
669
|
+
const info = line[side];
|
|
670
|
+
if (!info || info.lineNumber == null)
|
|
671
|
+
continue;
|
|
672
|
+
entries.push({ lineNumber: info.lineNumber, text: this.lineToText(info) });
|
|
673
|
+
}
|
|
674
|
+
entries.sort((a, b) => a.lineNumber - b.lineNumber);
|
|
675
|
+
const result = new Map();
|
|
676
|
+
if (entries.length === 0)
|
|
677
|
+
return result;
|
|
678
|
+
const perLine = highlightSide(entries.map((e) => e.text).join("\n"), language, theme);
|
|
679
|
+
if (!perLine)
|
|
680
|
+
return result;
|
|
681
|
+
for (let i = 0; i < entries.length; i++) {
|
|
682
|
+
result.set(entries[i].lineNumber, (_a = perLine[i]) !== null && _a !== void 0 ? _a : []);
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* Resolves the active highlight theme (built-in light/dark, with any
|
|
688
|
+
* `highlightTheme` overrides merged on top).
|
|
689
|
+
*/
|
|
690
|
+
this.resolveHighlightTheme = () => {
|
|
691
|
+
const base = this.props.useDarkTheme ? defaultDarkHighlightTheme : defaultLightHighlightTheme;
|
|
692
|
+
return this.props.highlightTheme ? Object.assign(Object.assign({}, base), this.props.highlightTheme) : base;
|
|
693
|
+
};
|
|
694
|
+
/**
|
|
695
|
+
* Computes syntax-highlight tokens for both sides when `highlightLanguage` is
|
|
696
|
+
* set, storing them in state. Lazily loads the grammar, is memoised by
|
|
697
|
+
* (diff, language, theme, dark) so it is cheap to call repeatedly, and clears
|
|
698
|
+
* the result when highlighting is disabled or the language is unavailable.
|
|
699
|
+
*/
|
|
700
|
+
this.updateHighlight = async () => {
|
|
701
|
+
var _a, _b, _c, _d;
|
|
702
|
+
const { highlightLanguage } = this.props;
|
|
703
|
+
if (!highlightLanguage) {
|
|
704
|
+
if (this.state.highlightResult)
|
|
705
|
+
this.setState({ highlightResult: null });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const diffKey = this.getMemoisedKey();
|
|
709
|
+
const diffResult = this.state.computedDiffResult[diffKey];
|
|
710
|
+
// Diff not ready yet — memoisedCompute re-invokes updateHighlight when it is.
|
|
711
|
+
if (!diffResult)
|
|
712
|
+
return;
|
|
713
|
+
const dark = (_a = this.props.useDarkTheme) !== null && _a !== void 0 ? _a : false;
|
|
714
|
+
const key = `${diffKey}::${highlightLanguage}::${dark}::${JSON.stringify((_b = this.props.highlightTheme) !== null && _b !== void 0 ? _b : null)}`;
|
|
715
|
+
if (((_c = this.state.highlightResult) === null || _c === void 0 ? void 0 : _c.key) === key)
|
|
716
|
+
return;
|
|
717
|
+
const canonical = await ensureLanguage(highlightLanguage);
|
|
718
|
+
// Bail if props changed while the grammar was loading.
|
|
719
|
+
if (this.props.highlightLanguage !== highlightLanguage || this.getMemoisedKey() !== diffKey)
|
|
720
|
+
return;
|
|
721
|
+
if (!canonical) {
|
|
722
|
+
if (this.state.highlightResult)
|
|
723
|
+
this.setState({ highlightResult: null });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (!this.highlightPrecedenceWarned &&
|
|
727
|
+
this.props.renderContent &&
|
|
728
|
+
typeof process !== "undefined" &&
|
|
729
|
+
((_d = process.env) === null || _d === void 0 ? void 0 : _d.NODE_ENV) !== "production") {
|
|
730
|
+
this.highlightPrecedenceWarned = true;
|
|
731
|
+
console.warn("[react-diff-viewer] `highlightLanguage` takes precedence over `renderContent`; `renderContent` is ignored for line content while highlighting is active.");
|
|
732
|
+
}
|
|
733
|
+
const theme = this.resolveHighlightTheme();
|
|
734
|
+
const left = this.buildSideTokens(diffResult.lineInformation, "left", canonical, theme);
|
|
735
|
+
const right = this.buildSideTokens(diffResult.lineInformation, "right", canonical, theme);
|
|
736
|
+
this.setState({ highlightResult: { key, left, right } });
|
|
737
|
+
};
|
|
546
738
|
/**
|
|
547
739
|
* Handles scroll events on the scrollable container.
|
|
548
740
|
*
|
|
@@ -855,6 +1047,7 @@ class DiffViewer extends React.Component {
|
|
|
855
1047
|
charWidth: null,
|
|
856
1048
|
cumulativeOffsets: null,
|
|
857
1049
|
isScrolling: false,
|
|
1050
|
+
highlightResult: null,
|
|
858
1051
|
};
|
|
859
1052
|
}
|
|
860
1053
|
/**
|
|
@@ -978,6 +1171,12 @@ class DiffViewer extends React.Component {
|
|
|
978
1171
|
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0, cumulativeOffsets: null })));
|
|
979
1172
|
this.memoisedCompute();
|
|
980
1173
|
}
|
|
1174
|
+
else if (prevProps.highlightLanguage !== this.props.highlightLanguage ||
|
|
1175
|
+
prevProps.highlightTheme !== this.props.highlightTheme ||
|
|
1176
|
+
prevProps.useDarkTheme !== this.props.useDarkTheme) {
|
|
1177
|
+
// Highlighting inputs changed but the diff itself did not — just re-tokenise.
|
|
1178
|
+
this.updateHighlight();
|
|
1179
|
+
}
|
|
981
1180
|
}
|
|
982
1181
|
componentDidMount() {
|
|
983
1182
|
this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true })));
|
|
@@ -1022,3 +1221,4 @@ export default DiffViewer;
|
|
|
1022
1221
|
export { DiffMethod };
|
|
1023
1222
|
export { default as computeStyles } from "./styles.js";
|
|
1024
1223
|
export { defaultLightThemeVariables, defaultDarkThemeVariables, } from "./styles.js";
|
|
1224
|
+
export { defaultLightHighlightTheme, defaultDarkHighlightTheme, } from "./highlight-theme.js";
|
package/lib/cjs/src/styles.d.ts
CHANGED
package/lib/cjs/src/styles.js
CHANGED
|
@@ -147,6 +147,30 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
147
147
|
textDecoration: "none",
|
|
148
148
|
label: "content-text",
|
|
149
149
|
});
|
|
150
|
+
// Line content is laid out as two flex items: a fixed indent column carrying the
|
|
151
|
+
// line's leading whitespace, and a flexible body that wraps. This gives wrapped
|
|
152
|
+
// continuation lines a hanging indent — they align under the first character of
|
|
153
|
+
// the line's content instead of falling back to the cell's left edge.
|
|
154
|
+
const contentFlex = css({
|
|
155
|
+
display: "flex",
|
|
156
|
+
alignItems: "baseline",
|
|
157
|
+
label: "content-flex",
|
|
158
|
+
});
|
|
159
|
+
// The indent column. `pre` + `flex-shrink: 0` keep the whitespace intact and
|
|
160
|
+
// prevent it from ever collapsing or wrapping, so it sizes exactly to the indent.
|
|
161
|
+
const lineIndent = css({
|
|
162
|
+
whiteSpace: "pre",
|
|
163
|
+
wordBreak: "keep-all",
|
|
164
|
+
flex: "0 0 auto",
|
|
165
|
+
label: "line-indent",
|
|
166
|
+
});
|
|
167
|
+
// The wrapping body. `min-width: 0` is required so a long unbreakable run wraps
|
|
168
|
+
// within the flex item instead of overflowing the row.
|
|
169
|
+
const lineBody = css({
|
|
170
|
+
flex: "1 1 auto",
|
|
171
|
+
minWidth: 0,
|
|
172
|
+
label: "line-body",
|
|
173
|
+
});
|
|
150
174
|
const unselectable = css({
|
|
151
175
|
userSelect: "none",
|
|
152
176
|
label: "unselectable",
|
|
@@ -257,14 +281,30 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
257
281
|
const codeFoldContentContainer = css({
|
|
258
282
|
// Neutralize host `:where(th,td){ padding }` on the fold content cell.
|
|
259
283
|
padding: 0,
|
|
284
|
+
// Clip the button to the cell box so any residual misalignment can't bleed into the row flow.
|
|
285
|
+
overflow: "hidden",
|
|
286
|
+
// `diffContainer`'s `& td` isolation reset pins vertical-align:baseline at specificity
|
|
287
|
+
// (0,2,0), which out-ranks this single class and drags the fold cell back onto the row
|
|
288
|
+
// baseline (the ~2-3px offset). Self-chain the selector to (0,3,0) so the fold cell's
|
|
289
|
+
// middle alignment wins over that reset.
|
|
290
|
+
"&&&": {
|
|
291
|
+
verticalAlign: "middle",
|
|
292
|
+
},
|
|
260
293
|
label: "code-fold-content-container",
|
|
261
294
|
});
|
|
262
295
|
const codeFoldExpandButton = css({
|
|
263
296
|
background: variables.codeFoldBackground,
|
|
264
297
|
cursor: "pointer",
|
|
265
|
-
display:
|
|
298
|
+
// `display: inline` puts the button in the cell's line box, so baseline/line-height
|
|
299
|
+
// metrics nudge it ~2-3px down from the cell top and it spills past the bottom.
|
|
300
|
+
// A block box leaves the inline formatting context and sits flush.
|
|
301
|
+
display: "block",
|
|
266
302
|
margin: 0,
|
|
303
|
+
padding: 0,
|
|
267
304
|
border: "none",
|
|
305
|
+
font: "inherit",
|
|
306
|
+
lineHeight: "inherit",
|
|
307
|
+
textAlign: "left",
|
|
268
308
|
fill: variables.codeFoldContentColor,
|
|
269
309
|
label: "code-fold-expand-button",
|
|
270
310
|
});
|
|
@@ -296,6 +336,14 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
296
336
|
fontWeight: 700,
|
|
297
337
|
cursor: "pointer",
|
|
298
338
|
label: "code-fold",
|
|
339
|
+
// The fold row has empty/classless placeholder cells (spacer gutters, line-number
|
|
340
|
+
// stand-ins) that pin no padding of their own. Host `:where(th,td){ padding }`
|
|
341
|
+
// (daisyui `.table`) inflates their vertical padding and stretches the fold row
|
|
342
|
+
// out of line with the code rows. Zero block padding on every cell in the row.
|
|
343
|
+
"& td": {
|
|
344
|
+
paddingTop: 0,
|
|
345
|
+
paddingBottom: 0,
|
|
346
|
+
},
|
|
299
347
|
"&:hover": {
|
|
300
348
|
color: variables.diffViewerColor,
|
|
301
349
|
fill: variables.diffViewerColor,
|
|
@@ -429,6 +477,9 @@ export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
429
477
|
emptyLine,
|
|
430
478
|
lineNumber,
|
|
431
479
|
contentText,
|
|
480
|
+
contentFlex,
|
|
481
|
+
lineIndent,
|
|
482
|
+
lineBody,
|
|
432
483
|
content,
|
|
433
484
|
column,
|
|
434
485
|
codeFoldContent,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax-highlighting colour themes for the first-party `highlightLanguage`
|
|
3
|
+
* feature. Colours are applied inline (`style={{ color }}`) on token spans, so
|
|
4
|
+
* they never collide with host-page CSS and require no stylesheet import —
|
|
5
|
+
* matching the way the rest of the diff viewer pins its own presentation.
|
|
6
|
+
*
|
|
7
|
+
* Keys are Prism token types (the class Prism emits after the leading `token`,
|
|
8
|
+
* e.g. `keyword`, `attr-name`). The special `default` key colours text that
|
|
9
|
+
* carries no token type. Unknown token types fall back to `default`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Light palette, harmonised with the GitHub-light diff theme used by
|
|
13
|
+
* `defaultLightThemeVariables` in `styles.ts`.
|
|
14
|
+
*/
|
|
15
|
+
export const defaultLightHighlightTheme = {
|
|
16
|
+
default: "#24292e",
|
|
17
|
+
comment: "#6a737d",
|
|
18
|
+
prolog: "#6a737d",
|
|
19
|
+
doctype: "#6a737d",
|
|
20
|
+
cdata: "#6a737d",
|
|
21
|
+
punctuation: "#24292e",
|
|
22
|
+
property: "#005cc5",
|
|
23
|
+
tag: "#22863a",
|
|
24
|
+
boolean: "#005cc5",
|
|
25
|
+
number: "#005cc5",
|
|
26
|
+
constant: "#005cc5",
|
|
27
|
+
symbol: "#005cc5",
|
|
28
|
+
deleted: "#b31d28",
|
|
29
|
+
selector: "#6f42c1",
|
|
30
|
+
"attr-name": "#6f42c1",
|
|
31
|
+
string: "#032f62",
|
|
32
|
+
char: "#032f62",
|
|
33
|
+
builtin: "#005cc5",
|
|
34
|
+
inserted: "#22863a",
|
|
35
|
+
operator: "#d73a49",
|
|
36
|
+
entity: "#22863a",
|
|
37
|
+
url: "#032f62",
|
|
38
|
+
"attr-value": "#032f62",
|
|
39
|
+
keyword: "#d73a49",
|
|
40
|
+
atrule: "#d73a49",
|
|
41
|
+
"class-name": "#6f42c1",
|
|
42
|
+
function: "#6f42c1",
|
|
43
|
+
regex: "#032f62",
|
|
44
|
+
important: "#e36209",
|
|
45
|
+
variable: "#e36209",
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Dark palette, tuned to the Dracula-family colours the diff viewer's dark
|
|
49
|
+
* theme already leans on (and what consumers previously paired with
|
|
50
|
+
* `useDarkTheme`).
|
|
51
|
+
*/
|
|
52
|
+
export const defaultDarkHighlightTheme = {
|
|
53
|
+
default: "#f8f8f2",
|
|
54
|
+
comment: "#6272a4",
|
|
55
|
+
prolog: "#6272a4",
|
|
56
|
+
doctype: "#6272a4",
|
|
57
|
+
cdata: "#6272a4",
|
|
58
|
+
punctuation: "#f8f8f2",
|
|
59
|
+
property: "#8be9fd",
|
|
60
|
+
tag: "#ff79c6",
|
|
61
|
+
boolean: "#bd93f9",
|
|
62
|
+
number: "#bd93f9",
|
|
63
|
+
constant: "#bd93f9",
|
|
64
|
+
symbol: "#bd93f9",
|
|
65
|
+
deleted: "#ff5555",
|
|
66
|
+
selector: "#50fa7b",
|
|
67
|
+
"attr-name": "#50fa7b",
|
|
68
|
+
string: "#f1fa8c",
|
|
69
|
+
char: "#f1fa8c",
|
|
70
|
+
builtin: "#8be9fd",
|
|
71
|
+
inserted: "#50fa7b",
|
|
72
|
+
operator: "#f8f8f2",
|
|
73
|
+
entity: "#ff79c6",
|
|
74
|
+
url: "#f1fa8c",
|
|
75
|
+
"attr-value": "#f1fa8c",
|
|
76
|
+
keyword: "#ff79c6",
|
|
77
|
+
atrule: "#ff79c6",
|
|
78
|
+
"class-name": "#8be9fd",
|
|
79
|
+
function: "#50fa7b",
|
|
80
|
+
regex: "#ffb86c",
|
|
81
|
+
important: "#ffb86c",
|
|
82
|
+
variable: "#f8f8f2",
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the colour for a token whose Prism class list is `classNames`
|
|
86
|
+
* (e.g. `["token", "attr-value"]` or `["token", "punctuation", "attr-equals"]`).
|
|
87
|
+
* Picks the most specific class present in the theme, walking from the most
|
|
88
|
+
* specific class back toward the generic ones; falls back to `default`.
|
|
89
|
+
*/
|
|
90
|
+
export const resolveTokenColor = (theme, classNames) => {
|
|
91
|
+
for (let i = classNames.length - 1; i >= 0; i--) {
|
|
92
|
+
const name = classNames[i];
|
|
93
|
+
if (name === "token")
|
|
94
|
+
continue;
|
|
95
|
+
if (theme[name])
|
|
96
|
+
return theme[name];
|
|
97
|
+
}
|
|
98
|
+
return theme.default;
|
|
99
|
+
};
|