@unfold-mdx/react 0.0.1
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/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/chunk-OX5XKYQT.js +218 -0
- package/dist/codeDiff-BVwZfdt9.d.cts +19 -0
- package/dist/codeDiff-BVwZfdt9.d.ts +19 -0
- package/dist/diff/index.cjs +248 -0
- package/dist/diff/index.d.cts +37 -0
- package/dist/diff/index.d.ts +37 -0
- package/dist/diff/index.js +14 -0
- package/dist/index.cjs +626 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +378 -0
- package/dist/shiki/index.cjs +125 -0
- package/dist/shiki/index.d.cts +14 -0
- package/dist/shiki/index.d.ts +14 -0
- package/dist/shiki/index.js +100 -0
- package/package.json +88 -0
- package/theme.css +212 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shakya47
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# unfold-mdx
|
|
2
|
+
|
|
3
|
+
Progressive-depth prose & code explanations for React + MDX.
|
|
4
|
+
|
|
5
|
+
Write full text and code snapshots at each depth level — the library diffs consecutive snapshots at **sentence** and **line/token** granularity, optionally highlights what changed, and lets the reader step through at their own pace. No accordion collapses, no page navigation, no scroll-jumps.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Sentence-level prose diffing** — automatically detects added, modified, and equal sentences between steps
|
|
10
|
+
- **Line & token-level code diffing** — detects added lines, changed lines (with inline token diffs), and equal lines
|
|
11
|
+
- **Opt-in Shiki syntax highlighting** — `unfold-mdx/shiki` adapter colors your code with any Shiki theme
|
|
12
|
+
- **Headless by default** — ships zero CSS; every element uses `data-*` attributes you can target freely
|
|
13
|
+
- **Opinionated theme included** — `import 'unfold-mdx/theme.css'` for a polished dark-mode starting point
|
|
14
|
+
- **SSR / no-JS fallback** — renders the deepest level server-side; hydrates to interactive on the client
|
|
15
|
+
- **Controlled & uncontrolled** — use `defaultIndex` for fire-and-forget, or `selectedIndex` + `onChange` for full control
|
|
16
|
+
- **Accessible** — ARIA labels, `aria-live` regions, `role="tablist"` indicators, `prefers-reduced-motion` support
|
|
17
|
+
- **Tiny** — ~13 KB ESM, only `diff-match-patch` as a runtime dependency
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install unfold-mdx
|
|
25
|
+
# or
|
|
26
|
+
pnpm add unfold-mdx
|
|
27
|
+
# or
|
|
28
|
+
yarn add unfold-mdx
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Peer Dependencies
|
|
32
|
+
|
|
33
|
+
| Package | Version | Required? |
|
|
34
|
+
|-------------|----------|-----------|
|
|
35
|
+
| `react` | ≥ 18 | ✅ Yes |
|
|
36
|
+
| `react-dom` | ≥ 18 | ✅ Yes |
|
|
37
|
+
| `shiki` | ^1.0.0 | ❌ Optional |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { Depth, DepthLevel, DepthCode } from "unfold-mdx";
|
|
45
|
+
import "unfold-mdx/theme.css"; // optional — opinionated dark theme
|
|
46
|
+
|
|
47
|
+
export default function Demo() {
|
|
48
|
+
return (
|
|
49
|
+
<Depth show="both" orientation="horizontal" indicators buttonVariant="arrow">
|
|
50
|
+
<DepthLevel label="Overview">
|
|
51
|
+
Quicksort is a divide-and-conquer sorting algorithm.
|
|
52
|
+
It selects a 'pivot' element and partitions the array around it.
|
|
53
|
+
</DepthLevel>
|
|
54
|
+
<DepthCode lang="typescript">
|
|
55
|
+
{`function quicksort(arr: number[]): number[] {
|
|
56
|
+
if (arr.length <= 1) return arr;
|
|
57
|
+
}`}
|
|
58
|
+
</DepthCode>
|
|
59
|
+
|
|
60
|
+
<DepthLevel label="Partitioning">
|
|
61
|
+
Quicksort is a divide-and-conquer sorting algorithm.
|
|
62
|
+
It selects a 'pivot' element and partitions the array around it.
|
|
63
|
+
Elements smaller than the pivot go left; larger ones go right.
|
|
64
|
+
</DepthLevel>
|
|
65
|
+
<DepthCode lang="typescript">
|
|
66
|
+
{`function quicksort(arr: number[]): number[] {
|
|
67
|
+
if (arr.length <= 1) return arr;
|
|
68
|
+
|
|
69
|
+
const pivot = arr[arr.length - 1];
|
|
70
|
+
const left = arr.filter((x) => x < pivot);
|
|
71
|
+
const right = arr.filter((x) => x > pivot);
|
|
72
|
+
}`}
|
|
73
|
+
</DepthCode>
|
|
74
|
+
</Depth>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Click **Next** → the library diffs the two snapshots and renders only the current step, marking new sentences and code lines.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Adding Shiki Syntax Highlighting
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Depth, DepthLevel, DepthCode } from "unfold-mdx";
|
|
87
|
+
import { createShikiHighlighter } from "unfold-mdx/shiki";
|
|
88
|
+
import "unfold-mdx/theme.css";
|
|
89
|
+
|
|
90
|
+
// Create once at module scope
|
|
91
|
+
const highlighter = createShikiHighlighter({
|
|
92
|
+
theme: "vitesse-dark",
|
|
93
|
+
langs: ["typescript"],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export default function Demo() {
|
|
97
|
+
return (
|
|
98
|
+
<Depth show="both" highlighter={highlighter} indicators>
|
|
99
|
+
{/* ...steps... */}
|
|
100
|
+
</Depth>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Shiki loads asynchronously. Before it's ready, code renders with the raw diff tokens (no colors). Once loaded, the component re-renders with full syntax coloring — no layout shift.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### `<Depth>` Props
|
|
112
|
+
|
|
113
|
+
| Prop | Type | Default | Description |
|
|
114
|
+
|------|------|---------|-------------|
|
|
115
|
+
| `selectedIndex` | `number` | — | Controlled mode: current step index (0-based). |
|
|
116
|
+
| `defaultIndex` | `number` | `0` | Uncontrolled mode: initial step index. |
|
|
117
|
+
| `onChange` | `(i: number) => void` | — | Callback when the step changes. |
|
|
118
|
+
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Pane layout direction (when `show="both"`). |
|
|
119
|
+
| `ratio` | `number` | `0.5` | Prose/code width split (0–1). Exposed as `--unfold-ratio`. |
|
|
120
|
+
| `show` | `"both" \| "prose" \| "code"` | `"both"` | Which panes to render. |
|
|
121
|
+
| `indicators` | `boolean` | `false` | Show clickable step-indicator dots. |
|
|
122
|
+
| `buttonVariant` | `"text" \| "arrow" \| "chevron" \| "minimal"` | `"text"` | Navigation button label style. |
|
|
123
|
+
| `animate` | `boolean` | `true` | Set `data-enter="true"` for one frame on new elements. |
|
|
124
|
+
| `highlight` | `boolean` | `true` | Mark added/changed elements with `data-sentence="added"` / `data-code-line="added"`. Set `false` to disable all visual diff markers. |
|
|
125
|
+
| `highlighter` | `ShikiHighlighter` | — | Shiki adapter instance from `createShikiHighlighter()`. |
|
|
126
|
+
|
|
127
|
+
### `<DepthLevel>` Props
|
|
128
|
+
|
|
129
|
+
| Prop | Type | Description |
|
|
130
|
+
|------|------|-------------|
|
|
131
|
+
| `label` | `string` | Human-readable step name (used in ARIA labels and indicators). |
|
|
132
|
+
| `children` | `ReactNode` | Full prose snapshot for this step. |
|
|
133
|
+
|
|
134
|
+
### `<DepthCode>` Props
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Description |
|
|
137
|
+
|------|------|-------------|
|
|
138
|
+
| `lang` | `string` | Language identifier (e.g. `"typescript"`, `"python"`). |
|
|
139
|
+
| `label` | `string` | Optional label for the code pane. |
|
|
140
|
+
| `children` | `string` | Full raw code string for this step. |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Styling
|
|
145
|
+
|
|
146
|
+
### Headless (default)
|
|
147
|
+
|
|
148
|
+
`unfold-mdx` renders plain HTML with `data-*` attributes. You style everything yourself:
|
|
149
|
+
|
|
150
|
+
```css
|
|
151
|
+
/* Prose highlights */
|
|
152
|
+
[data-sentence="added"] { border-left: 2px solid #3b82f6; padding-left: 8px; }
|
|
153
|
+
[data-sentence="equal"] { /* normal text */ }
|
|
154
|
+
|
|
155
|
+
/* Code line highlights */
|
|
156
|
+
[data-code-line="added"],
|
|
157
|
+
[data-code-line="changed"] { background: rgba(59, 130, 246, 0.08); }
|
|
158
|
+
[data-code-line="equal"] { /* normal code */ }
|
|
159
|
+
|
|
160
|
+
/* Controls */
|
|
161
|
+
[data-unfold-controls] { display: flex; gap: 12px; margin-top: 16px; }
|
|
162
|
+
[data-unfold-dot][data-active="true"] { background: #3b82f6; }
|
|
163
|
+
|
|
164
|
+
/* Enter animation */
|
|
165
|
+
[data-enter="true"] { animation: flash 0.4s ease-out; }
|
|
166
|
+
@keyframes flash {
|
|
167
|
+
from { background-color: rgba(59, 130, 246, 0.15); }
|
|
168
|
+
to { background-color: transparent; }
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Opinionated Theme
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import "unfold-mdx/theme.css";
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Override any design token with CSS custom properties:
|
|
179
|
+
|
|
180
|
+
| Variable | Default | Description |
|
|
181
|
+
|----------|---------|-------------|
|
|
182
|
+
| `--unfold-ratio` | `0.5` | Prose/code split |
|
|
183
|
+
| `--unfold-prose-color` | `#e2e8f0` | Prose text color |
|
|
184
|
+
| `--unfold-code-bg` | `#0d1117` | Code pane background |
|
|
185
|
+
| `--unfold-code-border` | `rgba(56,189,248,0.15)` | Code pane border |
|
|
186
|
+
| `--unfold-code-text` | `#c9d1d9` | Code fallback text |
|
|
187
|
+
| `--unfold-highlight` | `rgba(56,189,248,0.5)` | Highlight border color |
|
|
188
|
+
| `--unfold-highlight-bg` | `rgba(56,189,248,0.06)` | Highlight background |
|
|
189
|
+
| `--unfold-btn-bg` | `rgba(255,255,255,0.06)` | Button background |
|
|
190
|
+
| `--unfold-btn-border` | `rgba(255,255,255,0.12)` | Button border |
|
|
191
|
+
| `--unfold-btn-text` | `#e2e8f0` | Button text |
|
|
192
|
+
| `--unfold-btn-hover-bg` | `rgba(255,255,255,0.12)` | Button hover background |
|
|
193
|
+
| `--unfold-dot-bg` | `rgba(255,255,255,0.15)` | Inactive dot |
|
|
194
|
+
| `--unfold-dot-active` | `#38bdf8` | Active dot |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## DOM Output
|
|
199
|
+
|
|
200
|
+
```html
|
|
201
|
+
<div data-unfold-root data-orientation="horizontal" data-show="both"
|
|
202
|
+
data-level="1" data-total-levels="3"
|
|
203
|
+
role="region" aria-label="Partitioning"
|
|
204
|
+
style="--unfold-ratio: 0.5;">
|
|
205
|
+
<div data-unfold-panes>
|
|
206
|
+
<div data-unfold-pane="prose" aria-live="polite">
|
|
207
|
+
<span data-sentence="equal">Quicksort is a divide-and-conquer sorting algorithm. </span>
|
|
208
|
+
<span data-sentence="equal">It selects a 'pivot' element... </span>
|
|
209
|
+
<span data-sentence="added" data-enter="true">Elements smaller than the pivot go left; larger ones go right. </span>
|
|
210
|
+
</div>
|
|
211
|
+
<div data-unfold-pane="code" aria-live="polite">
|
|
212
|
+
<pre data-lang="typescript">
|
|
213
|
+
<code>
|
|
214
|
+
<span data-code-line="equal"><span data-code-token="equal" style="color:#CB7676">function</span>...</span>
|
|
215
|
+
<span data-code-line="added" data-enter="true"><span data-code-token="added" style="color:#BD976A">const</span>...</span>
|
|
216
|
+
</code>
|
|
217
|
+
</pre>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div data-unfold-controls data-button-variant="arrow">
|
|
221
|
+
<button data-unfold-prev aria-disabled="false">← Prev</button>
|
|
222
|
+
<div data-unfold-indicators role="tablist">
|
|
223
|
+
<button data-unfold-dot role="tab" aria-selected="false"></button>
|
|
224
|
+
<button data-unfold-dot role="tab" aria-selected="true" data-active="true"></button>
|
|
225
|
+
<button data-unfold-dot role="tab" aria-selected="false"></button>
|
|
226
|
+
</div>
|
|
227
|
+
<button data-unfold-next aria-disabled="false">Next →</button>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## SSR / No-JS Fallback
|
|
235
|
+
|
|
236
|
+
`<Depth>` renders all children server-side with `data-unfold-active` on the deepest level. Add this CSS to hide inactive levels before hydration:
|
|
237
|
+
|
|
238
|
+
```css
|
|
239
|
+
[data-unfold-root] > [data-depth-level]:not([data-unfold-active]),
|
|
240
|
+
[data-unfold-root] > pre[data-lang]:not([data-unfold-active]) {
|
|
241
|
+
display: none;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
This is included in `theme.css` by default.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Standalone Diff Utilities
|
|
250
|
+
|
|
251
|
+
The diff engine is available as a separate entry point for use outside of React:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
import { sentenceDiff, codeDiff, tokenize } from "unfold-mdx/diff";
|
|
255
|
+
|
|
256
|
+
const prose = sentenceDiff(
|
|
257
|
+
"The sky is blue.",
|
|
258
|
+
"The sky is blue. The grass is green."
|
|
259
|
+
);
|
|
260
|
+
// => [{ kind: "equal", sentence: "The sky is blue." },
|
|
261
|
+
// { kind: "added", sentence: "The grass is green." }]
|
|
262
|
+
|
|
263
|
+
const code = codeDiff(
|
|
264
|
+
"const x = 1;",
|
|
265
|
+
"const x = 1;\nconst y = 2;"
|
|
266
|
+
);
|
|
267
|
+
// => [{ kind: "equal", line: "const x = 1;" },
|
|
268
|
+
// { kind: "added", tokens: [...] }]
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// src/diff/tokenize.ts
|
|
2
|
+
function tokenize(text) {
|
|
3
|
+
if (!text) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const splitRegex = /(?<=(?:(?<!\.)\.(?!\.)|[!?]))(?=\s|$)/;
|
|
7
|
+
return text.split(splitRegex).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/diff/sentenceDiff.ts
|
|
11
|
+
import { diff_match_patch } from "diff-match-patch";
|
|
12
|
+
function sentenceDiff(prev, next) {
|
|
13
|
+
const prevSentences = tokenize(prev);
|
|
14
|
+
const nextSentences = tokenize(next);
|
|
15
|
+
const sentenceToChar = /* @__PURE__ */ new Map();
|
|
16
|
+
const charToSentence = [];
|
|
17
|
+
function getCharForSentence(sentence) {
|
|
18
|
+
let char = sentenceToChar.get(sentence);
|
|
19
|
+
if (char === void 0) {
|
|
20
|
+
const code = charToSentence.length;
|
|
21
|
+
char = String.fromCharCode(code);
|
|
22
|
+
sentenceToChar.set(sentence, char);
|
|
23
|
+
charToSentence.push(sentence);
|
|
24
|
+
}
|
|
25
|
+
return char;
|
|
26
|
+
}
|
|
27
|
+
const prevChars = prevSentences.map(getCharForSentence).join("");
|
|
28
|
+
const nextChars = nextSentences.map(getCharForSentence).join("");
|
|
29
|
+
const dmp = new diff_match_patch();
|
|
30
|
+
const diffs = dmp.diff_main(prevChars, nextChars);
|
|
31
|
+
dmp.diff_cleanupSemantic(diffs);
|
|
32
|
+
const result = [];
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < diffs.length) {
|
|
35
|
+
const [op, text] = diffs[i];
|
|
36
|
+
if (op === 0) {
|
|
37
|
+
for (let j = 0; j < text.length; j++) {
|
|
38
|
+
result.push({
|
|
39
|
+
kind: "equal",
|
|
40
|
+
sentence: charToSentence[text.charCodeAt(j)]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
i++;
|
|
44
|
+
} else if (op === -1 && i + 1 < diffs.length && diffs[i + 1][0] === 1) {
|
|
45
|
+
const deleteText = text;
|
|
46
|
+
const insertText = diffs[i + 1][1];
|
|
47
|
+
const deleteLen = deleteText.length;
|
|
48
|
+
const insertLen = insertText.length;
|
|
49
|
+
const minLen = Math.min(deleteLen, insertLen);
|
|
50
|
+
for (let j = 0; j < minLen; j++) {
|
|
51
|
+
result.push({
|
|
52
|
+
kind: "modified",
|
|
53
|
+
before: charToSentence[deleteText.charCodeAt(j)],
|
|
54
|
+
after: charToSentence[insertText.charCodeAt(j)]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
for (let j = minLen; j < deleteLen; j++) {
|
|
58
|
+
result.push({
|
|
59
|
+
kind: "removed",
|
|
60
|
+
sentence: charToSentence[deleteText.charCodeAt(j)]
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (let j = minLen; j < insertLen; j++) {
|
|
64
|
+
result.push({
|
|
65
|
+
kind: "added",
|
|
66
|
+
sentence: charToSentence[insertText.charCodeAt(j)]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
i += 2;
|
|
70
|
+
} else if (op === -1) {
|
|
71
|
+
for (let j = 0; j < text.length; j++) {
|
|
72
|
+
result.push({
|
|
73
|
+
kind: "removed",
|
|
74
|
+
sentence: charToSentence[text.charCodeAt(j)]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
i++;
|
|
78
|
+
} else if (op === 1) {
|
|
79
|
+
for (let j = 0; j < text.length; j++) {
|
|
80
|
+
result.push({
|
|
81
|
+
kind: "added",
|
|
82
|
+
sentence: charToSentence[text.charCodeAt(j)]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/diff/codeTokenize.ts
|
|
92
|
+
function tokenizeLines(code) {
|
|
93
|
+
if (code === "") return [];
|
|
94
|
+
return code.split(/\r?\n/);
|
|
95
|
+
}
|
|
96
|
+
function tokenizeCodeLine(line) {
|
|
97
|
+
if (line === "") return [];
|
|
98
|
+
const regex = /([a-zA-Z0-9_]+|\s+|[^a-zA-Z0-9_\s])/g;
|
|
99
|
+
const tokens = [];
|
|
100
|
+
let match;
|
|
101
|
+
while ((match = regex.exec(line)) !== null) {
|
|
102
|
+
if (match[0].length > 0) {
|
|
103
|
+
tokens.push(match[0]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return tokens;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/diff/codeDiff.ts
|
|
110
|
+
import { diff_match_patch as diff_match_patch2 } from "diff-match-patch";
|
|
111
|
+
function codeDiff(prev, next) {
|
|
112
|
+
if (prev === "" && next !== "") {
|
|
113
|
+
return tokenizeLines(next).map((line) => ({
|
|
114
|
+
kind: "added",
|
|
115
|
+
tokens: tokenizeCodeLine(line).map((t) => ({ kind: "added", text: t }))
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
const prevLines = tokenizeLines(prev);
|
|
119
|
+
const nextLines = tokenizeLines(next);
|
|
120
|
+
const lineToChar = /* @__PURE__ */ new Map();
|
|
121
|
+
const charToLine = [];
|
|
122
|
+
function getCharForLine(line) {
|
|
123
|
+
let char = lineToChar.get(line);
|
|
124
|
+
if (char === void 0) {
|
|
125
|
+
const code = charToLine.length;
|
|
126
|
+
char = String.fromCharCode(code);
|
|
127
|
+
lineToChar.set(line, char);
|
|
128
|
+
charToLine.push(line);
|
|
129
|
+
}
|
|
130
|
+
return char;
|
|
131
|
+
}
|
|
132
|
+
const prevChars = prevLines.map(getCharForLine).join("");
|
|
133
|
+
const nextChars = nextLines.map(getCharForLine).join("");
|
|
134
|
+
const dmp = new diff_match_patch2();
|
|
135
|
+
const diffs = dmp.diff_main(prevChars, nextChars);
|
|
136
|
+
dmp.diff_cleanupSemantic(diffs);
|
|
137
|
+
const result = [];
|
|
138
|
+
let i = 0;
|
|
139
|
+
while (i < diffs.length) {
|
|
140
|
+
const [op, text] = diffs[i];
|
|
141
|
+
if (op === 0) {
|
|
142
|
+
for (let j = 0; j < text.length; j++) {
|
|
143
|
+
result.push({
|
|
144
|
+
kind: "equal",
|
|
145
|
+
line: charToLine[text.charCodeAt(j)]
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
i++;
|
|
149
|
+
} else if (op === -1 && i + 1 < diffs.length && diffs[i + 1][0] === 1) {
|
|
150
|
+
const deleteText = text;
|
|
151
|
+
const insertText = diffs[i + 1][1];
|
|
152
|
+
const deleteLen = deleteText.length;
|
|
153
|
+
const insertLen = insertText.length;
|
|
154
|
+
const minLen = Math.min(deleteLen, insertLen);
|
|
155
|
+
for (let j = 0; j < minLen; j++) {
|
|
156
|
+
let getCharForToken2 = function(tok) {
|
|
157
|
+
let char = tokenToChar.get(tok);
|
|
158
|
+
if (char === void 0) {
|
|
159
|
+
const code = charToToken.length;
|
|
160
|
+
char = String.fromCharCode(code);
|
|
161
|
+
tokenToChar.set(tok, char);
|
|
162
|
+
charToToken.push(tok);
|
|
163
|
+
}
|
|
164
|
+
return char;
|
|
165
|
+
};
|
|
166
|
+
var getCharForToken = getCharForToken2;
|
|
167
|
+
const prevLine = charToLine[deleteText.charCodeAt(j)];
|
|
168
|
+
const nextLine = charToLine[insertText.charCodeAt(j)];
|
|
169
|
+
const prevTokens = tokenizeCodeLine(prevLine);
|
|
170
|
+
const nextTokens = tokenizeCodeLine(nextLine);
|
|
171
|
+
const tokenToChar = /* @__PURE__ */ new Map();
|
|
172
|
+
const charToToken = [];
|
|
173
|
+
const prevTokChars = prevTokens.map(getCharForToken2).join("");
|
|
174
|
+
const nextTokChars = nextTokens.map(getCharForToken2).join("");
|
|
175
|
+
const tokDiffs = dmp.diff_main(prevTokChars, nextTokChars);
|
|
176
|
+
dmp.diff_cleanupSemantic(tokDiffs);
|
|
177
|
+
const tokens = [];
|
|
178
|
+
for (let k = 0; k < tokDiffs.length; k++) {
|
|
179
|
+
const [tokOp, tokText] = tokDiffs[k];
|
|
180
|
+
if (tokOp === 0) {
|
|
181
|
+
for (let c = 0; c < tokText.length; c++) {
|
|
182
|
+
tokens.push({ kind: "equal", text: charToToken[tokText.charCodeAt(c)] });
|
|
183
|
+
}
|
|
184
|
+
} else if (tokOp === 1) {
|
|
185
|
+
for (let c = 0; c < tokText.length; c++) {
|
|
186
|
+
tokens.push({ kind: "added", text: charToToken[tokText.charCodeAt(c)] });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
result.push({ kind: "changed", tokens });
|
|
191
|
+
}
|
|
192
|
+
for (let j = minLen; j < insertLen; j++) {
|
|
193
|
+
const line = charToLine[insertText.charCodeAt(j)];
|
|
194
|
+
const tokens = tokenizeCodeLine(line).map((t) => ({ kind: "added", text: t }));
|
|
195
|
+
result.push({ kind: "added", tokens });
|
|
196
|
+
}
|
|
197
|
+
i += 2;
|
|
198
|
+
} else if (op === -1) {
|
|
199
|
+
i++;
|
|
200
|
+
} else if (op === 1) {
|
|
201
|
+
for (let j = 0; j < text.length; j++) {
|
|
202
|
+
const line = charToLine[text.charCodeAt(j)];
|
|
203
|
+
const tokens = tokenizeCodeLine(line).map((t) => ({ kind: "added", text: t }));
|
|
204
|
+
result.push({ kind: "added", tokens });
|
|
205
|
+
}
|
|
206
|
+
i++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export {
|
|
213
|
+
tokenize,
|
|
214
|
+
sentenceDiff,
|
|
215
|
+
tokenizeLines,
|
|
216
|
+
tokenizeCodeLine,
|
|
217
|
+
codeDiff
|
|
218
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface CodeToken {
|
|
2
|
+
kind: "equal" | "added";
|
|
3
|
+
text: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
}
|
|
6
|
+
type CodeLineDiff = {
|
|
7
|
+
kind: "equal";
|
|
8
|
+
line: string;
|
|
9
|
+
tokens?: CodeToken[];
|
|
10
|
+
} | {
|
|
11
|
+
kind: "added";
|
|
12
|
+
tokens: CodeToken[];
|
|
13
|
+
} | {
|
|
14
|
+
kind: "changed";
|
|
15
|
+
tokens: CodeToken[];
|
|
16
|
+
};
|
|
17
|
+
declare function codeDiff(prev: string, next: string): CodeLineDiff[];
|
|
18
|
+
|
|
19
|
+
export { type CodeLineDiff as C, type CodeToken as a, codeDiff as c };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface CodeToken {
|
|
2
|
+
kind: "equal" | "added";
|
|
3
|
+
text: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
}
|
|
6
|
+
type CodeLineDiff = {
|
|
7
|
+
kind: "equal";
|
|
8
|
+
line: string;
|
|
9
|
+
tokens?: CodeToken[];
|
|
10
|
+
} | {
|
|
11
|
+
kind: "added";
|
|
12
|
+
tokens: CodeToken[];
|
|
13
|
+
} | {
|
|
14
|
+
kind: "changed";
|
|
15
|
+
tokens: CodeToken[];
|
|
16
|
+
};
|
|
17
|
+
declare function codeDiff(prev: string, next: string): CodeLineDiff[];
|
|
18
|
+
|
|
19
|
+
export { type CodeLineDiff as C, type CodeToken as a, codeDiff as c };
|