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.
- package/.github/workflows/pages.yml +36 -0
- package/.github/workflows/publish.yml +2 -10
- package/CHANGELOG.md +38 -0
- package/README.md +5 -3
- 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 +54 -3
- package/lib/cjs/src/index.js +211 -10
- package/lib/cjs/src/styles.d.ts +5 -0
- package/lib/cjs/src/styles.js +137 -56
- package/lib/cjs/src/workerBundle.d.ts +1 -1
- package/lib/cjs/src/workerBundle.js +1 -1
- package/lib/esm/src/highlight-theme.js +99 -0
- package/lib/esm/src/highlight.js +311 -0
- package/lib/esm/src/index.js +208 -9
- package/lib/esm/src/styles.js +137 -56
- package/lib/esm/src/workerBundle.js +1 -1
- package/package.json +16 -12
- package/pnpm-workspace.yaml +7 -0
- package/publish-examples.sh +0 -10
|
@@ -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
|
-
|
|
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' }}>
|
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
|
+
};
|