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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.0 (2026-07-14)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- first-party syntax highlighting via highlightLanguage
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- build declarations against the React 18 peer floor
|
|
12
|
+
|
|
13
|
+
### Tests
|
|
14
|
+
|
|
15
|
+
- add regression repros for reported issues
|
|
16
|
+
|
|
3
17
|
## 4.3.0 (2026-07-10)
|
|
4
18
|
|
|
5
19
|
### Features
|
package/lib/cjs/src/expand.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function Expand(): import("react
|
|
1
|
+
export declare function Expand(): import("react").JSX.Element;
|
package/lib/cjs/src/fold.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function Fold(): import("react
|
|
1
|
+
export declare function Fold(): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
export type HighlightTheme = Record<string, string>;
|
|
12
|
+
/**
|
|
13
|
+
* Light palette, harmonised with the GitHub-light diff theme used by
|
|
14
|
+
* `defaultLightThemeVariables` in `styles.ts`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const defaultLightHighlightTheme: HighlightTheme;
|
|
17
|
+
/**
|
|
18
|
+
* Dark palette, tuned to the Dracula-family colours the diff viewer's dark
|
|
19
|
+
* theme already leans on (and what consumers previously paired with
|
|
20
|
+
* `useDarkTheme`).
|
|
21
|
+
*/
|
|
22
|
+
export declare const defaultDarkHighlightTheme: HighlightTheme;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the colour for a token whose Prism class list is `classNames`
|
|
25
|
+
* (e.g. `["token", "attr-value"]` or `["token", "punctuation", "attr-equals"]`).
|
|
26
|
+
* Picks the most specific class present in the theme, walking from the most
|
|
27
|
+
* specific class back toward the generic ones; falls back to `default`.
|
|
28
|
+
*/
|
|
29
|
+
export declare const resolveTokenColor: (theme: HighlightTheme, classNames: readonly string[]) => string;
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party syntax highlighting for the diff viewer.
|
|
3
|
+
*
|
|
4
|
+
* The problem this solves: highlighting a diff line-by-line loses lexer state
|
|
5
|
+
* across line boundaries (a tag or string that wraps onto the next line is
|
|
6
|
+
* mis-tokenised), and highlighting that runs *after* the diff can't be merged
|
|
7
|
+
* with the word-level change marks on a changed line. So instead we highlight
|
|
8
|
+
* each whole side once — lexer state carries across every line — then express
|
|
9
|
+
* both the highlight tokens and the diff chunks as `[start, end)` intervals over
|
|
10
|
+
* the same line string and intersect them. Every resulting atom carries one
|
|
11
|
+
* colour (from the grammar) and one diff status (default / added / removed).
|
|
12
|
+
*
|
|
13
|
+
* Grammars are loaded lazily per language via `ensureLanguage`, tokenisation is
|
|
14
|
+
* synchronous (`refractor.highlight`), and colours are applied inline
|
|
15
|
+
* (`style={{ color }}`) so they never collide with host-page CSS.
|
|
16
|
+
*/
|
|
17
|
+
import type { ReactElement } from "react";
|
|
18
|
+
import { type DiffInformation } from "./compute-lines.js";
|
|
19
|
+
import { type HighlightTheme } from "./highlight-theme.js";
|
|
20
|
+
/** A coloured run over a single line, in that line's character coordinate space. */
|
|
21
|
+
export interface HighlightToken {
|
|
22
|
+
start: number;
|
|
23
|
+
end: number;
|
|
24
|
+
color: string;
|
|
25
|
+
}
|
|
26
|
+
/** Contiguous coloured runs for one line (covers the whole line, no gaps). */
|
|
27
|
+
export type LineTokens = HighlightToken[];
|
|
28
|
+
/**
|
|
29
|
+
* Ensures the grammar for `language` is registered, loading it lazily if needed.
|
|
30
|
+
* Resolves with the canonical language name to hand to `highlightSide`, or
|
|
31
|
+
* `null` if the language is unknown or failed to load (caller falls back to no
|
|
32
|
+
* highlighting). Safe to call repeatedly — registered/negative/in-flight are
|
|
33
|
+
* all cached.
|
|
34
|
+
*/
|
|
35
|
+
export declare const ensureLanguage: (language: string) => Promise<string | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Highlights an entire side and returns per-line token arrays (index `i` is the
|
|
38
|
+
* `i`-th line of `text`). Lexer state carries across lines because the whole
|
|
39
|
+
* string is tokenised in one pass. `language` must be a canonical name from
|
|
40
|
+
* `ensureLanguage`; returns `null` if tokenisation fails.
|
|
41
|
+
*/
|
|
42
|
+
export declare const highlightSide: (text: string, language: string, theme: HighlightTheme) => LineTokens[] | null;
|
|
43
|
+
/**
|
|
44
|
+
* Re-bases a line's tokens to a sub-range starting at `from` (used to drop the
|
|
45
|
+
* peeled leading-whitespace indent so body tokens align with the diff body).
|
|
46
|
+
*/
|
|
47
|
+
export declare const sliceLineTokens: (tokens: LineTokens, from: number) => LineTokens;
|
|
48
|
+
interface MergeStyles {
|
|
49
|
+
wordDiff: string;
|
|
50
|
+
wordAdded: string;
|
|
51
|
+
wordRemoved: string;
|
|
52
|
+
}
|
|
53
|
+
interface MergeOptions {
|
|
54
|
+
styles: MergeStyles;
|
|
55
|
+
showHighlight: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Intersects a changed line's highlight tokens with its word-diff chunks. Both
|
|
59
|
+
* are contiguous partitions of the same body string, so a two-pointer walk over
|
|
60
|
+
* their union of boundaries yields atoms that each carry one colour and one diff
|
|
61
|
+
* status — preserving syntax colour *and* the `<ins>`/`<del>` word marks.
|
|
62
|
+
*/
|
|
63
|
+
export declare const mergeHighlightWithDiff: (bodyText: string, bodyTokens: LineTokens, diffArray: readonly DiffInformation[], options: MergeOptions) => ReactElement[];
|
|
64
|
+
/**
|
|
65
|
+
* Renders a non-changed (context / fully-added / fully-removed) line as coloured
|
|
66
|
+
* spans, with no word-diff wrapping.
|
|
67
|
+
*/
|
|
68
|
+
export declare const renderHighlightedPlain: (bodyText: string, bodyTokens: LineTokens) => ReactElement[];
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* First-party syntax highlighting for the diff viewer.
|
|
4
|
+
*
|
|
5
|
+
* The problem this solves: highlighting a diff line-by-line loses lexer state
|
|
6
|
+
* across line boundaries (a tag or string that wraps onto the next line is
|
|
7
|
+
* mis-tokenised), and highlighting that runs *after* the diff can't be merged
|
|
8
|
+
* with the word-level change marks on a changed line. So instead we highlight
|
|
9
|
+
* each whole side once — lexer state carries across every line — then express
|
|
10
|
+
* both the highlight tokens and the diff chunks as `[start, end)` intervals over
|
|
11
|
+
* the same line string and intersect them. Every resulting atom carries one
|
|
12
|
+
* colour (from the grammar) and one diff status (default / added / removed).
|
|
13
|
+
*
|
|
14
|
+
* Grammars are loaded lazily per language via `ensureLanguage`, tokenisation is
|
|
15
|
+
* synchronous (`refractor.highlight`), and colours are applied inline
|
|
16
|
+
* (`style={{ color }}`) so they never collide with host-page CSS.
|
|
17
|
+
*/
|
|
18
|
+
import cn from "classnames";
|
|
19
|
+
import { refractor } from "refractor/core";
|
|
20
|
+
import { DiffType } from "./compute-lines.js";
|
|
21
|
+
import { resolveTokenColor } from "./highlight-theme.js";
|
|
22
|
+
/**
|
|
23
|
+
* Lazy grammar loaders. A static map (not `import(`refractor/${name}`)`) so a
|
|
24
|
+
* bundler can see every possible chunk and code-split them individually — only
|
|
25
|
+
* the grammar a consumer actually asks for is fetched. Covers refractor's
|
|
26
|
+
* "common" language set.
|
|
27
|
+
*/
|
|
28
|
+
const LANGUAGE_LOADERS = {
|
|
29
|
+
arduino: () => import("refractor/arduino"),
|
|
30
|
+
bash: () => import("refractor/bash"),
|
|
31
|
+
basic: () => import("refractor/basic"),
|
|
32
|
+
c: () => import("refractor/c"),
|
|
33
|
+
clike: () => import("refractor/clike"),
|
|
34
|
+
cpp: () => import("refractor/cpp"),
|
|
35
|
+
csharp: () => import("refractor/csharp"),
|
|
36
|
+
css: () => import("refractor/css"),
|
|
37
|
+
diff: () => import("refractor/diff"),
|
|
38
|
+
go: () => import("refractor/go"),
|
|
39
|
+
ini: () => import("refractor/ini"),
|
|
40
|
+
java: () => import("refractor/java"),
|
|
41
|
+
javascript: () => import("refractor/javascript"),
|
|
42
|
+
json: () => import("refractor/json"),
|
|
43
|
+
kotlin: () => import("refractor/kotlin"),
|
|
44
|
+
less: () => import("refractor/less"),
|
|
45
|
+
lua: () => import("refractor/lua"),
|
|
46
|
+
makefile: () => import("refractor/makefile"),
|
|
47
|
+
markdown: () => import("refractor/markdown"),
|
|
48
|
+
markup: () => import("refractor/markup"),
|
|
49
|
+
"markup-templating": () => import("refractor/markup-templating"),
|
|
50
|
+
objectivec: () => import("refractor/objectivec"),
|
|
51
|
+
perl: () => import("refractor/perl"),
|
|
52
|
+
php: () => import("refractor/php"),
|
|
53
|
+
python: () => import("refractor/python"),
|
|
54
|
+
r: () => import("refractor/r"),
|
|
55
|
+
regex: () => import("refractor/regex"),
|
|
56
|
+
ruby: () => import("refractor/ruby"),
|
|
57
|
+
rust: () => import("refractor/rust"),
|
|
58
|
+
sass: () => import("refractor/sass"),
|
|
59
|
+
scss: () => import("refractor/scss"),
|
|
60
|
+
sql: () => import("refractor/sql"),
|
|
61
|
+
swift: () => import("refractor/swift"),
|
|
62
|
+
typescript: () => import("refractor/typescript"),
|
|
63
|
+
vbnet: () => import("refractor/vbnet"),
|
|
64
|
+
yaml: () => import("refractor/yaml"),
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Friendly names → canonical loader keys. refractor also registers a grammar's
|
|
68
|
+
* own aliases on `register` (e.g. `markup` enables `html`/`xml`/`svg`, and
|
|
69
|
+
* `typescript` enables `ts`), but we still need to know *which* loader to run
|
|
70
|
+
* when a consumer passes the alias, so map the common ones here.
|
|
71
|
+
*/
|
|
72
|
+
const LANGUAGE_ALIASES = {
|
|
73
|
+
html: "markup",
|
|
74
|
+
xml: "markup",
|
|
75
|
+
svg: "markup",
|
|
76
|
+
mathml: "markup",
|
|
77
|
+
ssml: "markup",
|
|
78
|
+
atom: "markup",
|
|
79
|
+
rss: "markup",
|
|
80
|
+
js: "javascript",
|
|
81
|
+
jsx: "javascript",
|
|
82
|
+
node: "javascript",
|
|
83
|
+
ts: "typescript",
|
|
84
|
+
tsx: "typescript",
|
|
85
|
+
py: "python",
|
|
86
|
+
rb: "ruby",
|
|
87
|
+
yml: "yaml",
|
|
88
|
+
md: "markdown",
|
|
89
|
+
sh: "bash",
|
|
90
|
+
shell: "bash",
|
|
91
|
+
zsh: "bash",
|
|
92
|
+
"c++": "cpp",
|
|
93
|
+
objc: "objectivec",
|
|
94
|
+
cs: "csharp",
|
|
95
|
+
dotnet: "csharp",
|
|
96
|
+
kt: "kotlin",
|
|
97
|
+
rs: "rust",
|
|
98
|
+
golang: "go",
|
|
99
|
+
};
|
|
100
|
+
/** Languages we've tried and failed to load — never retried. */
|
|
101
|
+
const negativeCache = new Set();
|
|
102
|
+
/** In-flight loads, deduped so concurrent requests share one import. */
|
|
103
|
+
const inflight = new Map();
|
|
104
|
+
/** Resolve a requested language name to its canonical (registerable) key. */
|
|
105
|
+
const canonicalName = (language) => {
|
|
106
|
+
var _a;
|
|
107
|
+
const requested = language.toLowerCase();
|
|
108
|
+
return (_a = LANGUAGE_ALIASES[requested]) !== null && _a !== void 0 ? _a : requested;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Ensures the grammar for `language` is registered, loading it lazily if needed.
|
|
112
|
+
* Resolves with the canonical language name to hand to `highlightSide`, or
|
|
113
|
+
* `null` if the language is unknown or failed to load (caller falls back to no
|
|
114
|
+
* highlighting). Safe to call repeatedly — registered/negative/in-flight are
|
|
115
|
+
* all cached.
|
|
116
|
+
*/
|
|
117
|
+
export const ensureLanguage = async (language) => {
|
|
118
|
+
const canonical = canonicalName(language);
|
|
119
|
+
if (refractor.registered(canonical))
|
|
120
|
+
return canonical;
|
|
121
|
+
if (negativeCache.has(canonical))
|
|
122
|
+
return null;
|
|
123
|
+
const loader = LANGUAGE_LOADERS[canonical];
|
|
124
|
+
if (!loader) {
|
|
125
|
+
negativeCache.add(canonical);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
let load = inflight.get(canonical);
|
|
129
|
+
if (!load) {
|
|
130
|
+
load = loader()
|
|
131
|
+
.then((mod) => {
|
|
132
|
+
refractor.register(mod.default);
|
|
133
|
+
})
|
|
134
|
+
.catch(() => {
|
|
135
|
+
negativeCache.add(canonical);
|
|
136
|
+
})
|
|
137
|
+
.finally(() => {
|
|
138
|
+
inflight.delete(canonical);
|
|
139
|
+
});
|
|
140
|
+
inflight.set(canonical, load);
|
|
141
|
+
}
|
|
142
|
+
await load;
|
|
143
|
+
return refractor.registered(canonical) ? canonical : null;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Walks the hast tree in document order, assigning each text leaf a character
|
|
147
|
+
* range and the colour of its innermost token class. Offsets are contiguous, so
|
|
148
|
+
* the leaves losslessly tile the input string.
|
|
149
|
+
*/
|
|
150
|
+
const collectLeaves = (nodes, theme, color, offset, out) => {
|
|
151
|
+
var _a, _b;
|
|
152
|
+
let pos = offset;
|
|
153
|
+
for (const node of nodes) {
|
|
154
|
+
if (node.type === "text") {
|
|
155
|
+
const value = (_a = node.value) !== null && _a !== void 0 ? _a : "";
|
|
156
|
+
if (value.length > 0) {
|
|
157
|
+
out.push({ start: pos, end: pos + value.length, color, text: value });
|
|
158
|
+
pos += value.length;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (node.children) {
|
|
162
|
+
const className = (_b = node.properties) === null || _b === void 0 ? void 0 : _b.className;
|
|
163
|
+
const nextColor = Array.isArray(className)
|
|
164
|
+
? resolveTokenColor(theme, className)
|
|
165
|
+
: color;
|
|
166
|
+
pos = collectLeaves(node.children, theme, nextColor, pos, out);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return pos;
|
|
170
|
+
};
|
|
171
|
+
/** Splits contiguous leaves into per-line token arrays, rebased to line-start. */
|
|
172
|
+
const tokensByLine = (leaves, lineCount) => {
|
|
173
|
+
const lines = Array.from({ length: lineCount }, () => []);
|
|
174
|
+
let line = 0;
|
|
175
|
+
let lineStart = 0;
|
|
176
|
+
for (const leaf of leaves) {
|
|
177
|
+
let segStart = leaf.start;
|
|
178
|
+
for (let i = 0; i < leaf.text.length; i++) {
|
|
179
|
+
if (leaf.text[i] === "\n") {
|
|
180
|
+
const segEnd = leaf.start + i;
|
|
181
|
+
if (segEnd > segStart && lines[line]) {
|
|
182
|
+
lines[line].push({
|
|
183
|
+
start: segStart - lineStart,
|
|
184
|
+
end: segEnd - lineStart,
|
|
185
|
+
color: leaf.color,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
line += 1;
|
|
189
|
+
lineStart = leaf.start + i + 1;
|
|
190
|
+
segStart = lineStart;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (leaf.end > segStart && lines[line]) {
|
|
194
|
+
lines[line].push({
|
|
195
|
+
start: segStart - lineStart,
|
|
196
|
+
end: leaf.end - lineStart,
|
|
197
|
+
color: leaf.color,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return lines;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Highlights an entire side and returns per-line token arrays (index `i` is the
|
|
205
|
+
* `i`-th line of `text`). Lexer state carries across lines because the whole
|
|
206
|
+
* string is tokenised in one pass. `language` must be a canonical name from
|
|
207
|
+
* `ensureLanguage`; returns `null` if tokenisation fails.
|
|
208
|
+
*/
|
|
209
|
+
export const highlightSide = (text, language, theme) => {
|
|
210
|
+
var _a;
|
|
211
|
+
if (text.length === 0)
|
|
212
|
+
return [];
|
|
213
|
+
let root;
|
|
214
|
+
try {
|
|
215
|
+
root = refractor.highlight(text, language);
|
|
216
|
+
}
|
|
217
|
+
catch (_b) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const leaves = [];
|
|
221
|
+
collectLeaves((_a = root.children) !== null && _a !== void 0 ? _a : [], theme, theme.default, 0, leaves);
|
|
222
|
+
const lineCount = text.split("\n").length;
|
|
223
|
+
return tokensByLine(leaves, lineCount);
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Re-bases a line's tokens to a sub-range starting at `from` (used to drop the
|
|
227
|
+
* peeled leading-whitespace indent so body tokens align with the diff body).
|
|
228
|
+
*/
|
|
229
|
+
export const sliceLineTokens = (tokens, from) => {
|
|
230
|
+
if (from <= 0)
|
|
231
|
+
return tokens;
|
|
232
|
+
const out = [];
|
|
233
|
+
for (const token of tokens) {
|
|
234
|
+
if (token.end <= from)
|
|
235
|
+
continue;
|
|
236
|
+
out.push({
|
|
237
|
+
start: Math.max(token.start, from) - from,
|
|
238
|
+
end: token.end - from,
|
|
239
|
+
color: token.color,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
};
|
|
244
|
+
const renderAtom = (text, color, type, { styles, showHighlight }, key) => {
|
|
245
|
+
const style = color ? { color } : undefined;
|
|
246
|
+
if (type === DiffType.ADDED) {
|
|
247
|
+
return (_jsx("ins", { className: cn(styles.wordDiff, { [styles.wordAdded]: showHighlight }), style: style, children: text }, key));
|
|
248
|
+
}
|
|
249
|
+
if (type === DiffType.REMOVED) {
|
|
250
|
+
return (_jsx("del", { className: cn(styles.wordDiff, { [styles.wordRemoved]: showHighlight }), style: style, children: text }, key));
|
|
251
|
+
}
|
|
252
|
+
return (_jsx("span", { className: styles.wordDiff, style: style, children: text }, key));
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Intersects a changed line's highlight tokens with its word-diff chunks. Both
|
|
256
|
+
* are contiguous partitions of the same body string, so a two-pointer walk over
|
|
257
|
+
* their union of boundaries yields atoms that each carry one colour and one diff
|
|
258
|
+
* status — preserving syntax colour *and* the `<ins>`/`<del>` word marks.
|
|
259
|
+
*/
|
|
260
|
+
export const mergeHighlightWithDiff = (bodyText, bodyTokens, diffArray, options) => {
|
|
261
|
+
var _a;
|
|
262
|
+
const ranges = [];
|
|
263
|
+
let pos = 0;
|
|
264
|
+
for (const diff of diffArray) {
|
|
265
|
+
const value = typeof diff.value === "string" ? diff.value : "";
|
|
266
|
+
if (value.length > 0) {
|
|
267
|
+
ranges.push({
|
|
268
|
+
start: pos,
|
|
269
|
+
end: pos + value.length,
|
|
270
|
+
type: (_a = diff.type) !== null && _a !== void 0 ? _a : DiffType.DEFAULT,
|
|
271
|
+
});
|
|
272
|
+
pos += value.length;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const total = bodyText.length;
|
|
276
|
+
const out = [];
|
|
277
|
+
let ti = 0;
|
|
278
|
+
let ri = 0;
|
|
279
|
+
let cur = 0;
|
|
280
|
+
let key = 0;
|
|
281
|
+
while (cur < total) {
|
|
282
|
+
const token = bodyTokens[ti];
|
|
283
|
+
const range = ranges[ri];
|
|
284
|
+
const tokenEnd = token ? token.end : total;
|
|
285
|
+
const rangeEnd = range ? range.end : total;
|
|
286
|
+
const end = Math.min(tokenEnd, rangeEnd, total);
|
|
287
|
+
if (end <= cur) {
|
|
288
|
+
// Zero-width or stale interval — advance past it to guarantee progress.
|
|
289
|
+
if (tokenEnd <= cur)
|
|
290
|
+
ti += 1;
|
|
291
|
+
else if (rangeEnd <= cur)
|
|
292
|
+
ri += 1;
|
|
293
|
+
else
|
|
294
|
+
break;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
out.push(renderAtom(bodyText.slice(cur, end), token === null || token === void 0 ? void 0 : token.color, range ? range.type : DiffType.DEFAULT, options, key++));
|
|
298
|
+
cur = end;
|
|
299
|
+
if (tokenEnd <= cur)
|
|
300
|
+
ti += 1;
|
|
301
|
+
if (rangeEnd <= cur)
|
|
302
|
+
ri += 1;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Renders a non-changed (context / fully-added / fully-removed) line as coloured
|
|
308
|
+
* spans, with no word-diff wrapping.
|
|
309
|
+
*/
|
|
310
|
+
export const renderHighlightedPlain = (bodyText, bodyTokens) => {
|
|
311
|
+
if (bodyTokens.length === 0) {
|
|
312
|
+
return [_jsx("span", { children: bodyText }, 0)];
|
|
313
|
+
}
|
|
314
|
+
return bodyTokens.map((token, i) => (_jsx("span", { style: { color: token.color }, children: bodyText.slice(token.start, token.end) }, i)));
|
|
315
|
+
};
|
package/lib/cjs/src/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Change } from "diff";
|
|
|
4
4
|
import { type Block } from "./compute-hidden-blocks.js";
|
|
5
5
|
import { type DiffInformation, DiffMethod, DiffType, type LineInformation } from "./compute-lines.js";
|
|
6
6
|
import { type ReactDiffViewerStyles, type ReactDiffViewerStylesOverride, type ReactDiffViewerStylesVariables } from "./styles.js";
|
|
7
|
+
import { type LineTokens } from "./highlight.js";
|
|
8
|
+
import { type HighlightTheme } from "./highlight-theme.js";
|
|
7
9
|
export declare enum LineNumberPrefix {
|
|
8
10
|
LEFT = "L",
|
|
9
11
|
RIGHT = "R"
|
|
@@ -75,11 +77,26 @@ export interface ReactDiffViewerProps {
|
|
|
75
77
|
* Useful when the worker bundle fails to load in certain bundler configurations.
|
|
76
78
|
*/
|
|
77
79
|
disableWorker?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Enable first-party syntax highlighting for the given language (a Prism/
|
|
82
|
+
* refractor language name or alias, e.g. `"typescript"`, `"json"`, `"html"`).
|
|
83
|
+
* Each side is highlighted as a whole — so constructs that wrap across lines
|
|
84
|
+
* tokenise correctly — and the highlighting is merged with word-diff marks on
|
|
85
|
+
* changed lines. Takes precedence over `renderContent` for line content.
|
|
86
|
+
* Unknown languages fall back gracefully to no highlighting.
|
|
87
|
+
*/
|
|
88
|
+
highlightLanguage?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Optional overrides for the syntax-highlight colour palette. Merged over the
|
|
91
|
+
* built-in light/dark theme (selected by `useDarkTheme`). Keys are Prism token
|
|
92
|
+
* types (`keyword`, `string`, `tag`, ...) plus `default`.
|
|
93
|
+
*/
|
|
94
|
+
highlightTheme?: HighlightTheme;
|
|
78
95
|
}
|
|
79
96
|
export interface ReactDiffViewerState {
|
|
80
97
|
expandedBlocks: number[];
|
|
81
98
|
noSelect?: "left" | "right";
|
|
82
|
-
scrollableContainerRef: RefObject<HTMLDivElement
|
|
99
|
+
scrollableContainerRef: RefObject<HTMLDivElement>;
|
|
83
100
|
computedDiffResult: Record<string, ComputedDiffResult>;
|
|
84
101
|
isLoading: boolean;
|
|
85
102
|
visibleStartRow: number;
|
|
@@ -87,10 +104,16 @@ export interface ReactDiffViewerState {
|
|
|
87
104
|
charWidth: number | null;
|
|
88
105
|
cumulativeOffsets: number[] | null;
|
|
89
106
|
isScrolling: boolean;
|
|
107
|
+
highlightResult: {
|
|
108
|
+
key: string;
|
|
109
|
+
left: Map<number, LineTokens>;
|
|
110
|
+
right: Map<number, LineTokens>;
|
|
111
|
+
} | null;
|
|
90
112
|
}
|
|
91
113
|
declare class DiffViewer extends React.Component<ReactDiffViewerProps, ReactDiffViewerState> {
|
|
92
114
|
private styles;
|
|
93
115
|
private wordDiffCache;
|
|
116
|
+
private highlightPrecedenceWarned;
|
|
94
117
|
private contentColumnRef;
|
|
95
118
|
private charMeasureRef;
|
|
96
119
|
private stickyHeaderRef;
|
|
@@ -239,6 +262,31 @@ declare class DiffViewer extends React.Component<ReactDiffViewerProps, ReactDiff
|
|
|
239
262
|
* and stores the result in the local component state.
|
|
240
263
|
*/
|
|
241
264
|
private memoisedCompute;
|
|
265
|
+
/**
|
|
266
|
+
* Extracts the raw text of one side of a line for whole-side highlighting.
|
|
267
|
+
* Prefers `rawValue` (deferred word diff), then a plain string value, then the
|
|
268
|
+
* concatenation of word-diff chunk values.
|
|
269
|
+
*/
|
|
270
|
+
private lineToText;
|
|
271
|
+
/**
|
|
272
|
+
* Reconstructs one side's full document from the computed line information and
|
|
273
|
+
* highlights it in a single pass, returning per-line tokens keyed by line
|
|
274
|
+
* number. Highlighting the whole side (rather than line-by-line) is what keeps
|
|
275
|
+
* lexer state correct across line boundaries.
|
|
276
|
+
*/
|
|
277
|
+
private buildSideTokens;
|
|
278
|
+
/**
|
|
279
|
+
* Resolves the active highlight theme (built-in light/dark, with any
|
|
280
|
+
* `highlightTheme` overrides merged on top).
|
|
281
|
+
*/
|
|
282
|
+
private resolveHighlightTheme;
|
|
283
|
+
/**
|
|
284
|
+
* Computes syntax-highlight tokens for both sides when `highlightLanguage` is
|
|
285
|
+
* set, storing them in state. Lazily loads the grammar, is memoised by
|
|
286
|
+
* (diff, language, theme, dark) so it is cheap to call repeatedly, and clears
|
|
287
|
+
* the result when highlighting is disabled or the language is unavailable.
|
|
288
|
+
*/
|
|
289
|
+
private updateHighlight;
|
|
242
290
|
private static readonly ESTIMATED_ROW_HEIGHT;
|
|
243
291
|
/**
|
|
244
292
|
* Handles scroll events on the scrollable container.
|
|
@@ -259,4 +307,6 @@ export default DiffViewer;
|
|
|
259
307
|
export { DiffMethod };
|
|
260
308
|
export { default as computeStyles } from "./styles.js";
|
|
261
309
|
export { defaultLightThemeVariables, defaultDarkThemeVariables, } from "./styles.js";
|
|
310
|
+
export { defaultLightHighlightTheme, defaultDarkHighlightTheme, } from "./highlight-theme.js";
|
|
262
311
|
export type { ReactDiffViewerStylesOverride, ReactDiffViewerStyles, ReactDiffViewerStylesVariables };
|
|
312
|
+
export type { HighlightTheme } from "./highlight-theme.js";
|