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.
@@ -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
- const hasWordDiff = Array.isArray(value);
348
- if (hasWordDiff) {
349
- content = this.renderWordDiff(value, this.props.renderContent);
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 value === "string") {
352
- content = this.props.renderContent(value);
443
+ else if (this.props.renderContent && typeof rest === "string") {
444
+ content = this.props.renderContent(rest);
353
445
  }
354
446
  else {
355
- content = value;
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]: !content,
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]: !content,
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: _jsx(ElementType, { className: this.styles.contentText, children: content }) })] }));
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";
@@ -11,6 +11,9 @@ export interface ReactDiffViewerStyles {
11
11
  gutter: string;
12
12
  highlightedLine: string;
13
13
  lineNumber: string;
14
+ contentFlex: string;
15
+ lineIndent: string;
16
+ lineBody: string;
14
17
  marker: string;
15
18
  wordDiff: string;
16
19
  wordAdded: string;
@@ -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: "inline",
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
+ };