react-diff-viewer-continued 4.2.2 → 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.
@@ -0,0 +1,36 @@
1
+ name: Deploy GitHub Pages
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: pnpm/action-setup@v4
18
+ with:
19
+ version: latest
20
+
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 'lts/*'
24
+ cache: 'pnpm'
25
+
26
+ - run: pnpm install
27
+
28
+ - name: Configure git
29
+ run: |
30
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
31
+ git config --global user.name "github-actions[bot]"
32
+
33
+ - name: Deploy examples to GitHub Pages
34
+ run: pnpm publish:examples
35
+ env:
36
+ GITHUB_REPO_URL: https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
@@ -33,16 +33,8 @@ jobs:
33
33
  - run: pnpm install
34
34
  - run: pnpm build
35
35
 
36
- # Upgrade npm for provenance support (GitHub runners ship with old npm)
37
- - run: npm install -g npm@latest
38
-
39
- - name: Publish to npm
40
- run: pnpm publish --access public --provenance --no-git-checks
41
- env:
42
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
43
-
44
- # Create GitHub release with changelog notes
45
- - name: Create GitHub release
36
+ - name: Publish and create GitHub release
46
37
  run: pnpm just-release
47
38
  env:
48
39
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
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
+
17
+ ## 4.3.0 (2026-07-10)
18
+
19
+ ### Features
20
+
21
+ - export default light and dark theme variables
22
+
23
+ ### Bug Fixes
24
+
25
+ - do not do extra publish step, just release publishes
26
+ - make exports immutable
27
+ - freeze dark theme variables and add export-existence tests
28
+ - add GitHub Pages deployment workflow and Vite base path
29
+ - isolate diff table from host table CSS (daisyui/bootstrap)
30
+ - bump js-yaml to ^4.2.0 (CVE-2026-53550)
31
+ - clear remaining audit advisories in dev toolchain
32
+
33
+ ### Documentation
34
+
35
+ - add overscan, disableWorker, and loading overlay to README
36
+
37
+ ### Chores
38
+
39
+ - make pnpm canonical (drop npm lockfile, approve builds)
40
+
3
41
  ## 4.2.2 (2026-04-23)
4
42
 
5
43
  ### Bug Fixes
package/README.md CHANGED
@@ -92,8 +92,9 @@ class Diff extends PureComponent {
92
92
  | linesOffset | `number` | `0` | Number to start count code lines from. |
93
93
  | summary | `string \| ReactElement` | `undefined` | Text or element to display in the summary bar (e.g., filename). |
94
94
  | hideSummary | `boolean` | `false` | Hide the summary bar (expand/collapse button, change count, summary text). |
95
- | infiniteLoading | `{ pageSize: number, containerHeight: string }` | `undefined` | Enable virtualization for large diffs. When enabled, only visible rows are rendered. `containerHeight` sets the scrollable container height (e.g., `'500px'` or `'80vh'`). |
96
- | loadingElement | `() => ReactElement` | `undefined` | Function that returns an element to display while the diff is being computed. Useful with `infiniteLoading` for large files. |
95
+ | infiniteLoading | `{ pageSize: number, containerHeight: string, overscan?: number }` | `undefined` | Enable virtualization for large diffs. When enabled, only visible rows are rendered. `containerHeight` sets the scrollable container height (e.g., `'500px'` or `'80vh'`). `overscan` controls how many extra rows to render above and below the viewport (default `20`). |
96
+ | loadingElement | `() => ReactElement` | `undefined` | Function that returns an element to display while the diff is being computed or when content is hidden during fast scrolling. Useful with `infiniteLoading` for large files. |
97
+ | disableWorker | `boolean` | `false` | Disable the Web Worker used for diff computation, falling back to synchronous computation on the main thread. Useful when the worker bundle fails to load in certain bundler configurations. |
97
98
  | nonce | `string` | `''` | Nonce to use for inline styles (for CSP). |
98
99
 
99
100
  ## Instance Methods
@@ -114,7 +115,8 @@ Enable virtualization to only render visible rows:
114
115
  newValue={largeNewFile}
115
116
  infiniteLoading={{
116
117
  pageSize: 20,
117
- containerHeight: '80vh'
118
+ containerHeight: '80vh',
119
+ overscan: 20,
118
120
  }}
119
121
  loadingElement={() => (
120
122
  <div style={{ padding: '20px', textAlign: 'center' }}>
@@ -1 +1 @@
1
- export declare function Expand(): import("react/jsx-runtime").JSX.Element;
1
+ export declare function Expand(): import("react").JSX.Element;
@@ -1 +1 @@
1
- export declare function Fold(): import("react/jsx-runtime").JSX.Element;
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
+ };