@riebeckite/plugin-diff 0.0.14 → 0.0.16
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/README.md +5 -2
- package/README_ja.md +4 -2
- package/dist/client.js +162 -15
- package/dist/index.js +182 -81
- package/dist/src/components/diff-history.d.ts +4 -3
- package/dist/src/git/history_reader.d.ts +13 -0
- package/dist/src/git/repo_paths.d.ts +76 -0
- package/dist/src/git/run_git.d.ts +43 -0
- package/dist/src/types.d.ts +6 -0
- package/package.json +3 -3
- package/style.css +88 -14
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@ export default defineConfig({
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`diff(options?)` accepts the same `GitHistoryReaderOptions` as the API.
|
|
31
|
+
The revision panel renders the initial comparison during the build and computes
|
|
32
|
+
other selected comparisons in the browser. It embeds each available revision
|
|
33
|
+
once rather than every pairwise diff, keeping generated pages compact.
|
|
31
34
|
|
|
32
35
|
## Programmatic API
|
|
33
36
|
|
|
@@ -61,7 +64,7 @@ const compare = await api.compareRevisions({
|
|
|
61
64
|
|
|
62
65
|
| Option | Type | Default | Description |
|
|
63
66
|
| ------ | ---- | ------- | ----------- |
|
|
64
|
-
| `cwd` | `string` | `process.cwd()` |
|
|
67
|
+
| `cwd` | `string` | `config.content.directory`, else `process.cwd()` | Content root used to locate the Git work tree |
|
|
65
68
|
|
|
66
69
|
When `cwd` is not inside a Git repository, API calls resolve to empty results
|
|
67
70
|
(`[]` / `null`) instead of throwing.
|
|
@@ -87,4 +90,4 @@ When `cwd` is not inside a Git repository, API calls resolve to empty results
|
|
|
87
90
|
|
|
88
91
|
## See also
|
|
89
92
|
|
|
90
|
-
- [Plugin guide](../../../docs/en/reference/plugin-api.md)
|
|
93
|
+
- [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
|
package/README_ja.md
CHANGED
|
@@ -28,7 +28,9 @@ const current = await api.getCurrentDiff("notes/hello.md");
|
|
|
28
28
|
| `getCurrentDiff(filePath)` | 最新と一つ前のリビジョンの差分 |
|
|
29
29
|
| `compareRevisions(input)` | 指定した二つのリビジョンの差分 |
|
|
30
30
|
|
|
31
|
-
`cwd` は Git
|
|
31
|
+
`cwd` は Git リポジトリを探すためのコンテンツルートで、既定値はビルド内の `config.content.directory`、それ以外では `process.cwd()` です。相対パスは `process.cwd()` 基準で解決されます。Git リポジトリ外を指定しても例外は投げず、空の結果を返します。
|
|
32
|
+
|
|
33
|
+
リビジョンパネルは初期表示の差分だけをビルド時に描画し、利用者が選んだ比較はブラウザ上で計算します。各リビジョンは一度だけ埋め込むため、全組み合わせの差分を出力する場合より生成ページを小さく保てます。
|
|
32
34
|
|
|
33
35
|
## 公開 API
|
|
34
36
|
|
|
@@ -38,4 +40,4 @@ const current = await api.getCurrentDiff("notes/hello.md");
|
|
|
38
40
|
|
|
39
41
|
## 関連資料
|
|
40
42
|
|
|
41
|
-
- [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
|
|
43
|
+
- [プラグインシステム](../../../docs/ja/docs/reference/plugin-api.md)
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,92 @@ function escapeHtml(value) {
|
|
|
3
3
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
// src/diff/line_diff.ts
|
|
7
|
+
function createLineDiff(fromMarkdown, toMarkdown) {
|
|
8
|
+
const fromLines = splitLines(fromMarkdown);
|
|
9
|
+
const toLines = splitLines(toMarkdown);
|
|
10
|
+
const table = buildLongestCommonSubsequenceTable(fromLines, toLines);
|
|
11
|
+
const lines = [];
|
|
12
|
+
let fromIndex = 0;
|
|
13
|
+
let toIndex = 0;
|
|
14
|
+
let oldLineNumber = 1;
|
|
15
|
+
let newLineNumber = 1;
|
|
16
|
+
while (fromIndex < fromLines.length && toIndex < toLines.length) {
|
|
17
|
+
if (fromLines[fromIndex] === toLines[toIndex]) {
|
|
18
|
+
lines.push({
|
|
19
|
+
type: "context",
|
|
20
|
+
content: fromLines[fromIndex],
|
|
21
|
+
oldLineNumber,
|
|
22
|
+
newLineNumber
|
|
23
|
+
});
|
|
24
|
+
fromIndex += 1;
|
|
25
|
+
toIndex += 1;
|
|
26
|
+
oldLineNumber += 1;
|
|
27
|
+
newLineNumber += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (table[fromIndex + 1][toIndex] >= table[fromIndex][toIndex + 1]) {
|
|
31
|
+
lines.push({
|
|
32
|
+
type: "removed",
|
|
33
|
+
content: fromLines[fromIndex],
|
|
34
|
+
oldLineNumber,
|
|
35
|
+
newLineNumber: null
|
|
36
|
+
});
|
|
37
|
+
fromIndex += 1;
|
|
38
|
+
oldLineNumber += 1;
|
|
39
|
+
} else {
|
|
40
|
+
lines.push({
|
|
41
|
+
type: "added",
|
|
42
|
+
content: toLines[toIndex],
|
|
43
|
+
oldLineNumber: null,
|
|
44
|
+
newLineNumber
|
|
45
|
+
});
|
|
46
|
+
toIndex += 1;
|
|
47
|
+
newLineNumber += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
while (fromIndex < fromLines.length) {
|
|
51
|
+
lines.push({
|
|
52
|
+
type: "removed",
|
|
53
|
+
content: fromLines[fromIndex],
|
|
54
|
+
oldLineNumber,
|
|
55
|
+
newLineNumber: null
|
|
56
|
+
});
|
|
57
|
+
fromIndex += 1;
|
|
58
|
+
oldLineNumber += 1;
|
|
59
|
+
}
|
|
60
|
+
while (toIndex < toLines.length) {
|
|
61
|
+
lines.push({
|
|
62
|
+
type: "added",
|
|
63
|
+
content: toLines[toIndex],
|
|
64
|
+
oldLineNumber: null,
|
|
65
|
+
newLineNumber
|
|
66
|
+
});
|
|
67
|
+
toIndex += 1;
|
|
68
|
+
newLineNumber += 1;
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
function splitLines(markdown) {
|
|
73
|
+
if (!markdown) return [];
|
|
74
|
+
return markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
75
|
+
}
|
|
76
|
+
function buildLongestCommonSubsequenceTable(fromLines, toLines) {
|
|
77
|
+
const table = Array.from(
|
|
78
|
+
{ length: fromLines.length + 1 },
|
|
79
|
+
() => Array.from({ length: toLines.length + 1 }, () => 0)
|
|
80
|
+
);
|
|
81
|
+
for (let fromIndex = fromLines.length - 1; fromIndex >= 0; fromIndex -= 1) {
|
|
82
|
+
for (let toIndex = toLines.length - 1; toIndex >= 0; toIndex -= 1) {
|
|
83
|
+
table[fromIndex][toIndex] = fromLines[fromIndex] === toLines[toIndex] ? table[fromIndex + 1][toIndex + 1] + 1 : Math.max(
|
|
84
|
+
table[fromIndex + 1][toIndex],
|
|
85
|
+
table[fromIndex][toIndex + 1]
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return table;
|
|
90
|
+
}
|
|
91
|
+
|
|
6
92
|
// client/diff-viewer.ts
|
|
7
93
|
function initDiffHistory() {
|
|
8
94
|
for (const root of document.querySelectorAll(
|
|
@@ -13,7 +99,9 @@ function initDiffHistory() {
|
|
|
13
99
|
}
|
|
14
100
|
function initDiffHistoryRoot(root) {
|
|
15
101
|
const payload = readPayload(root);
|
|
16
|
-
if (!payload
|
|
102
|
+
if (!payload) return;
|
|
103
|
+
let revisions = revisionsMap(payload.revisions ?? []);
|
|
104
|
+
let revisionsRequest = null;
|
|
17
105
|
const fromSelect = root.querySelector(
|
|
18
106
|
"[data-rr-diff-from]"
|
|
19
107
|
);
|
|
@@ -25,25 +113,86 @@ function initDiffHistoryRoot(root) {
|
|
|
25
113
|
)) {
|
|
26
114
|
button.addEventListener("click", () => {
|
|
27
115
|
const toHash = button.dataset.rrDiffSelect;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
116
|
+
if (!toHash) return;
|
|
117
|
+
void updateDiff({
|
|
118
|
+
root,
|
|
119
|
+
panel,
|
|
120
|
+
getRevisions: () => revisions,
|
|
121
|
+
setRevisions: (loaded) => {
|
|
122
|
+
revisions = loaded;
|
|
123
|
+
},
|
|
124
|
+
load: () => {
|
|
125
|
+
revisionsRequest ??= loadRevisions(payload.path);
|
|
126
|
+
return revisionsRequest;
|
|
127
|
+
},
|
|
128
|
+
fromHash: fromSelect.value,
|
|
129
|
+
toHash,
|
|
130
|
+
fromSelect,
|
|
131
|
+
toSelect
|
|
132
|
+
});
|
|
34
133
|
});
|
|
35
134
|
}
|
|
36
135
|
const updateFromSelects = () => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
136
|
+
void updateDiff({
|
|
137
|
+
root,
|
|
138
|
+
panel,
|
|
139
|
+
getRevisions: () => revisions,
|
|
140
|
+
setRevisions: (loaded) => {
|
|
141
|
+
revisions = loaded;
|
|
142
|
+
},
|
|
143
|
+
load: () => {
|
|
144
|
+
revisionsRequest ??= loadRevisions(payload.path);
|
|
145
|
+
return revisionsRequest;
|
|
146
|
+
},
|
|
147
|
+
fromHash: fromSelect.value,
|
|
148
|
+
toHash: toSelect.value,
|
|
149
|
+
fromSelect,
|
|
150
|
+
toSelect
|
|
151
|
+
});
|
|
43
152
|
};
|
|
44
153
|
fromSelect.addEventListener("change", updateFromSelects);
|
|
45
154
|
toSelect.addEventListener("change", updateFromSelects);
|
|
46
155
|
}
|
|
156
|
+
async function updateDiff(input) {
|
|
157
|
+
const { root, panel, fromHash, toHash, fromSelect, toSelect } = input;
|
|
158
|
+
const token = `${fromHash}
|
|
159
|
+
${toHash}`;
|
|
160
|
+
root.dataset.rrDiffPending = token;
|
|
161
|
+
const loaded = await input.load();
|
|
162
|
+
if (loaded) input.setRevisions(loaded);
|
|
163
|
+
if (root.dataset.rrDiffPending !== token) return;
|
|
164
|
+
const diff = createDiff(input.getRevisions(), fromHash, toHash);
|
|
165
|
+
if (!diff) return;
|
|
166
|
+
fromSelect.value = diff.from?.hash ?? "";
|
|
167
|
+
toSelect.value = diff.to.hash;
|
|
168
|
+
updateSelectedCommit(root, diff.to.hash);
|
|
169
|
+
renderPanel(panel, diff);
|
|
170
|
+
}
|
|
171
|
+
async function loadRevisions(path) {
|
|
172
|
+
if (!path) return null;
|
|
173
|
+
try {
|
|
174
|
+
const response = await fetch(path);
|
|
175
|
+
if (!response.ok) return null;
|
|
176
|
+
const payload = await response.json();
|
|
177
|
+
return revisionsMap(payload.revisions ?? []);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function revisionsMap(revisions) {
|
|
183
|
+
return new Map(revisions.map((revision) => [revision.hash, revision]));
|
|
184
|
+
}
|
|
185
|
+
function createDiff(revisions, fromHash, toHash) {
|
|
186
|
+
const to = revisions.get(toHash);
|
|
187
|
+
if (!to) return null;
|
|
188
|
+
const from = fromHash ? revisions.get(fromHash) : null;
|
|
189
|
+
if (fromHash && !from) return null;
|
|
190
|
+
return {
|
|
191
|
+
from: from ?? null,
|
|
192
|
+
to,
|
|
193
|
+
lines: createLineDiff(from?.markdown ?? "", to.markdown)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
47
196
|
function readPayload(root) {
|
|
48
197
|
const script = root.querySelector(
|
|
49
198
|
"[data-rr-diff-history-data]"
|
|
@@ -66,9 +215,7 @@ function updateSelectedCommit(root, hash) {
|
|
|
66
215
|
}
|
|
67
216
|
}
|
|
68
217
|
function renderPanel(panel, diff) {
|
|
69
|
-
const fromLabel = diff.from?.shortHash ?? "Beginning";
|
|
70
218
|
panel.innerHTML = `<div class="rr-diff-history__diff" data-rr-diff-rendered>
|
|
71
|
-
<div class="rr-diff-history__diff-head">${escapeHtml(fromLabel)} → ${escapeHtml(diff.to.shortHash)}</div>
|
|
72
219
|
<div class="rr-diff-history__table-wrap">
|
|
73
220
|
<table class="rr-diff-history__table">
|
|
74
221
|
<tbody>${diff.lines.map(renderLine).join("")}</tbody>
|
package/dist/index.js
CHANGED
|
@@ -53,9 +53,7 @@ function renderDiffViewer({
|
|
|
53
53
|
</div>`;
|
|
54
54
|
}
|
|
55
55
|
function renderDiffPanel(diff2) {
|
|
56
|
-
const fromLabel = diff2.from?.shortHash ?? "Beginning";
|
|
57
56
|
return `<div class="rr-diff-history__diff" data-rr-diff-rendered>
|
|
58
|
-
<div class="rr-diff-history__diff-head">${escapeHtml(fromLabel)} → ${escapeHtml(diff2.to.shortHash)}</div>
|
|
59
57
|
<div class="rr-diff-history__table-wrap">
|
|
60
58
|
<table class="rr-diff-history__table">
|
|
61
59
|
<tbody>${diff2.lines.map((line) => renderDiffLine({ line })).join("")}</tbody>
|
|
@@ -72,23 +70,26 @@ function renderRevisionSelect(kind, history, value) {
|
|
|
72
70
|
|
|
73
71
|
// src/components/diff-history.tsx
|
|
74
72
|
function renderDiffHistory(input) {
|
|
75
|
-
const
|
|
76
|
-
const
|
|
73
|
+
const history = input.revisions;
|
|
74
|
+
const selected = input.selected;
|
|
75
|
+
const payloadPath = input.payloadPath;
|
|
77
76
|
return `<section class="rr-diff-history" data-rr-diff-history>
|
|
78
|
-
<
|
|
77
|
+
<details class="rr-diff-history__panel">
|
|
78
|
+
<summary class="rr-diff-history__header">
|
|
79
79
|
<h2 class="rr-diff-history__title">History</h2>
|
|
80
|
-
<span class="rr-diff-history__count">${
|
|
81
|
-
</
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
<span class="rr-diff-history__header-meta"><span class="rr-diff-history__count">${history.length} changes</span><span class="rr-diff-history__toggle-label rr-diff-history__toggle-label--open">Collapse</span><span class="rr-diff-history__toggle-label rr-diff-history__toggle-label--closed">Expand</span><span class="rr-diff-history__toggle" aria-hidden="true"><svg class="rr-diff-history__toggle-icon" viewBox="0 0 16 16" width="16" height="16" focusable="false"><path d="M5.2 3.4 10.8 8l-5.6 4.6" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8"/></svg></span></span>
|
|
81
|
+
</summary>
|
|
82
|
+
${history.length === 0 || !selected ? renderEmptyState() : renderContent(history, selected)}
|
|
83
|
+
</details>
|
|
84
|
+
${payloadPath ? `<script type="application/json" data-rr-diff-history-data>${JSON.stringify({ path: payloadPath }).replace(/</g, "\\u003c")}</script>` : ""}
|
|
84
85
|
</section>`;
|
|
85
86
|
}
|
|
86
|
-
function renderContent(
|
|
87
|
+
function renderContent(history, selected) {
|
|
87
88
|
return `<div class="rr-diff-history__layout">
|
|
88
89
|
<ol class="rr-diff-history__list" aria-label="Commit history">
|
|
89
|
-
${
|
|
90
|
+
${history.map((revision, index) => renderRevisionButton(revision, index === 0)).join("")}
|
|
90
91
|
</ol>
|
|
91
|
-
${renderDiffViewer({ history
|
|
92
|
+
${renderDiffViewer({ history, selected })}
|
|
92
93
|
</div>`;
|
|
93
94
|
}
|
|
94
95
|
function renderEmptyState() {
|
|
@@ -191,30 +192,131 @@ function buildLongestCommonSubsequenceTable(fromLines, toLines) {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
// src/git/history_reader.ts
|
|
195
|
+
import path2 from "node:path";
|
|
196
|
+
|
|
197
|
+
// src/git/repo_paths.ts
|
|
198
|
+
import path from "node:path";
|
|
199
|
+
|
|
200
|
+
// src/git/run_git.ts
|
|
194
201
|
import { execFile } from "node:child_process";
|
|
195
202
|
import { promisify } from "node:util";
|
|
196
203
|
var execFileAsync = promisify(execFile);
|
|
204
|
+
function isGitCommandFailure(result) {
|
|
205
|
+
return result.ok === false;
|
|
206
|
+
}
|
|
207
|
+
async function runGit(args, cwd) {
|
|
208
|
+
try {
|
|
209
|
+
const { stdout } = await execFileAsync(
|
|
210
|
+
"git",
|
|
211
|
+
["-c", "core.quotePath=false", ...args],
|
|
212
|
+
{
|
|
213
|
+
...cwd === void 0 ? {} : { cwd },
|
|
214
|
+
maxBuffer: 1024 * 1024 * 10
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
return { ok: true, stdout };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
spawnError: isSpawnFailure(readErrorCode(error)),
|
|
222
|
+
detail: describeGitFailure(error)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function readErrorCode(error) {
|
|
227
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
228
|
+
}
|
|
229
|
+
function isSpawnFailure(code) {
|
|
230
|
+
return typeof code === "string" && !code.startsWith("ERR_");
|
|
231
|
+
}
|
|
232
|
+
function describeGitFailure(error) {
|
|
233
|
+
if (typeof error !== "object" || error === null) {
|
|
234
|
+
return error instanceof Error ? error.message : String(error);
|
|
235
|
+
}
|
|
236
|
+
const stderr = "stderr" in error ? error.stderr : void 0;
|
|
237
|
+
if (typeof stderr === "string" && stderr.trim()) return stderr.trim();
|
|
238
|
+
if (error instanceof Error) return error.message;
|
|
239
|
+
return String(error);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/git/repo_paths.ts
|
|
243
|
+
async function resolveRepositoryPaths(contentRoot) {
|
|
244
|
+
const resolvedContentRoot = path.resolve(contentRoot);
|
|
245
|
+
const result = await runGit([
|
|
246
|
+
"-C",
|
|
247
|
+
resolvedContentRoot,
|
|
248
|
+
"rev-parse",
|
|
249
|
+
"--show-toplevel",
|
|
250
|
+
"--show-prefix"
|
|
251
|
+
]);
|
|
252
|
+
if (isGitCommandFailure(result)) {
|
|
253
|
+
return result.spawnError ? { kind: "git-unavailable", detail: result.detail } : { kind: "content-outside-repository", detail: result.detail };
|
|
254
|
+
}
|
|
255
|
+
const [toplevel, prefix] = parseRevParseOutput(result.stdout);
|
|
256
|
+
if (!toplevel) {
|
|
257
|
+
return {
|
|
258
|
+
kind: "content-outside-repository",
|
|
259
|
+
detail: "git rev-parse reported no work tree root"
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
kind: "resolved",
|
|
264
|
+
paths: {
|
|
265
|
+
repositoryRoot: toPosixPath(path.normalize(toplevel)),
|
|
266
|
+
contentRoot: toPosixPath(resolvedContentRoot),
|
|
267
|
+
contentPrefix: toPosixPath(prefix).replace(/\/+$/, "")
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function toRepositoryPath(paths, contentRelativePath) {
|
|
272
|
+
const normalized = normalizeContentPath(contentRelativePath);
|
|
273
|
+
if (!paths.contentPrefix) return normalized;
|
|
274
|
+
if (!normalized) return paths.contentPrefix;
|
|
275
|
+
return `${paths.contentPrefix}/${normalized}`;
|
|
276
|
+
}
|
|
277
|
+
function normalizeContentPath(value) {
|
|
278
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
279
|
+
}
|
|
280
|
+
function isResolvedRepository(resolution) {
|
|
281
|
+
return resolution.kind === "resolved";
|
|
282
|
+
}
|
|
283
|
+
function parseRevParseOutput(stdout) {
|
|
284
|
+
const [toplevel = "", prefix = ""] = stdout.split(/\r?\n/);
|
|
285
|
+
return [toplevel.trim(), prefix.trim()];
|
|
286
|
+
}
|
|
287
|
+
function toPosixPath(value) {
|
|
288
|
+
return value.replace(/\\/g, "/");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/git/history_reader.ts
|
|
197
292
|
var fieldSeparator = "";
|
|
198
293
|
var recordSeparator = "";
|
|
199
294
|
var GitMarkdownHistoryReader = class {
|
|
200
|
-
#
|
|
295
|
+
#contentRoot;
|
|
201
296
|
#historyCache = /* @__PURE__ */ new Map();
|
|
202
297
|
#markdownCache = /* @__PURE__ */ new Map();
|
|
298
|
+
/** Maps a content-relative path to `<revision hash>` → work tree path. */
|
|
203
299
|
#revisionPathCache = /* @__PURE__ */ new Map();
|
|
204
|
-
#
|
|
300
|
+
#repository;
|
|
205
301
|
constructor(options = {}) {
|
|
206
|
-
this.#
|
|
302
|
+
this.#contentRoot = path2.resolve(options.cwd ?? process.cwd());
|
|
207
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Revision history for one file, newest first, following renames.
|
|
306
|
+
*
|
|
307
|
+
* @param filePath Content-relative path, e.g. `notes/hello.md`.
|
|
308
|
+
*/
|
|
208
309
|
getHistory(filePath) {
|
|
209
|
-
const normalizedPath =
|
|
310
|
+
const normalizedPath = normalizeContentPath(filePath);
|
|
210
311
|
const cached = this.#historyCache.get(normalizedPath);
|
|
211
312
|
if (cached) return cached;
|
|
212
313
|
const history = this.#readHistory(normalizedPath);
|
|
213
314
|
this.#historyCache.set(normalizedPath, history);
|
|
214
315
|
return history;
|
|
215
316
|
}
|
|
317
|
+
/** Content of one revision of a file, or `null` when Git cannot read it. */
|
|
216
318
|
getRevisionMarkdown(filePath, hash) {
|
|
217
|
-
const normalizedPath =
|
|
319
|
+
const normalizedPath = normalizeContentPath(filePath);
|
|
218
320
|
const cacheKey = `${hash}:${normalizedPath}`;
|
|
219
321
|
const cached = this.#markdownCache.get(cacheKey);
|
|
220
322
|
if (cached) return cached;
|
|
@@ -223,35 +325,44 @@ var GitMarkdownHistoryReader = class {
|
|
|
223
325
|
return markdown;
|
|
224
326
|
}
|
|
225
327
|
async #readHistory(filePath) {
|
|
226
|
-
|
|
328
|
+
const repository = await this.#resolvePaths();
|
|
329
|
+
if (!repository) return [];
|
|
227
330
|
const format = ["%H", "%h", "%cI", "%s", "%an"].join(fieldSeparator);
|
|
228
|
-
const result = await runGit(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
331
|
+
const result = await runGit(
|
|
332
|
+
[
|
|
333
|
+
"log",
|
|
334
|
+
"--follow",
|
|
335
|
+
"--name-status",
|
|
336
|
+
`--format=${recordSeparator}${format}`,
|
|
337
|
+
"--",
|
|
338
|
+
toRepositoryPath(repository, filePath)
|
|
339
|
+
],
|
|
340
|
+
repository.repositoryRoot
|
|
341
|
+
);
|
|
236
342
|
if (!result.ok || !result.stdout.trim()) return [];
|
|
237
343
|
const revisionPaths = /* @__PURE__ */ new Map();
|
|
238
|
-
const
|
|
344
|
+
const fallbackPath = toRepositoryPath(repository, filePath);
|
|
345
|
+
const revisions = result.stdout.split(recordSeparator).map((record) => record.trim()).filter(Boolean).map((record) => parseRevisionRecord(record, revisionPaths, fallbackPath)).filter((revision) => revision !== null);
|
|
239
346
|
this.#revisionPathCache.set(filePath, revisionPaths);
|
|
240
347
|
return revisions;
|
|
241
348
|
}
|
|
242
349
|
async #readRevisionMarkdown(filePath, hash) {
|
|
243
|
-
|
|
350
|
+
const repository = await this.#resolvePaths();
|
|
351
|
+
if (!repository) return null;
|
|
244
352
|
await this.getHistory(filePath);
|
|
245
|
-
const revisionPath = this.#revisionPathCache.get(filePath)?.get(hash) ?? filePath;
|
|
246
|
-
const result = await runGit(
|
|
353
|
+
const revisionPath = this.#revisionPathCache.get(filePath)?.get(hash) ?? toRepositoryPath(repository, filePath);
|
|
354
|
+
const result = await runGit(
|
|
355
|
+
["show", `${hash}:${revisionPath}`],
|
|
356
|
+
repository.repositoryRoot
|
|
357
|
+
);
|
|
247
358
|
return result.ok ? result.stdout : null;
|
|
248
359
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return
|
|
360
|
+
/** `null` when the content directory has no readable work tree. */
|
|
361
|
+
async #resolvePaths() {
|
|
362
|
+
this.#repository ??= resolveRepositoryPaths(this.#contentRoot);
|
|
363
|
+
const resolution = await this.#repository;
|
|
364
|
+
if (!isResolvedRepository(resolution)) return null;
|
|
365
|
+
return resolution.paths;
|
|
255
366
|
}
|
|
256
367
|
};
|
|
257
368
|
function parseRevisionRecord(record, revisionPaths, fallbackPath) {
|
|
@@ -271,48 +382,54 @@ function getRevisionPath(nameStatusLines, fallbackPath) {
|
|
|
271
382
|
const line = nameStatusLines.find((entry) => entry.trim().length > 0);
|
|
272
383
|
if (!line) return fallbackPath;
|
|
273
384
|
const [, ...paths] = line.split(" ");
|
|
274
|
-
return paths.at(-1) ?? fallbackPath;
|
|
275
|
-
}
|
|
276
|
-
function normalizeGitPath(filePath) {
|
|
277
|
-
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
278
|
-
}
|
|
279
|
-
async function runGit(cwd, args) {
|
|
280
|
-
try {
|
|
281
|
-
const { stdout } = await execFileAsync("git", args, {
|
|
282
|
-
cwd,
|
|
283
|
-
maxBuffer: 1024 * 1024 * 10
|
|
284
|
-
});
|
|
285
|
-
return { ok: true, stdout };
|
|
286
|
-
} catch (error) {
|
|
287
|
-
const stdout = typeof error === "object" && error !== null && "stdout" in error && typeof error.stdout === "string" ? error.stdout : "";
|
|
288
|
-
return { ok: false, stdout };
|
|
289
|
-
}
|
|
385
|
+
return normalizeContentPath(paths.at(-1) ?? fallbackPath);
|
|
290
386
|
}
|
|
291
387
|
|
|
292
388
|
// index.ts
|
|
293
389
|
function diff(options = {}) {
|
|
294
|
-
const api = createPostDiffApi(options);
|
|
295
390
|
const ui = { enabled: true, maxRevisions: 20, ...options.ui };
|
|
391
|
+
let api;
|
|
296
392
|
return definePlugin({
|
|
297
393
|
name: "diff",
|
|
394
|
+
processedContentCache: {
|
|
395
|
+
version: "diff-v2",
|
|
396
|
+
dependencyMode: "unsafe"
|
|
397
|
+
},
|
|
298
398
|
options,
|
|
299
399
|
assets: ui.enabled === false ? [] : [createStyleAsset("diff")],
|
|
300
400
|
clientEntries: ui.enabled === false ? [] : [createClientEntry("diff", "initDiffHistory")],
|
|
301
401
|
onPostProcessed: async (context) => {
|
|
302
402
|
if (ui.enabled === false) return;
|
|
403
|
+
api ??= createPostDiffApi({
|
|
404
|
+
...options,
|
|
405
|
+
cwd: options.cwd ?? context.config?.content.directory
|
|
406
|
+
});
|
|
303
407
|
const filePath = resolvePostFilePath(context);
|
|
304
408
|
const history = (await api.getHistory(filePath)).slice(
|
|
305
409
|
0,
|
|
306
410
|
ui.maxRevisions
|
|
307
411
|
);
|
|
308
|
-
const
|
|
412
|
+
const revisions = await getMarkdownRevisions(api, filePath, history);
|
|
413
|
+
const selected = await api.getCurrentDiff(filePath);
|
|
414
|
+
const payloadPath = revisions.length > 0 ? diffPayloadPath(context.slug) : void 0;
|
|
415
|
+
if (payloadPath) {
|
|
416
|
+
context.output.emit({
|
|
417
|
+
path: payloadPath,
|
|
418
|
+
content: JSON.stringify({ revisions }).replace(/</g, "\\u003c"),
|
|
419
|
+
dependencies: [{ type: "content", slug: context.slug }]
|
|
420
|
+
});
|
|
421
|
+
}
|
|
309
422
|
context.content.html += renderDiffHistory({
|
|
310
|
-
|
|
311
|
-
|
|
423
|
+
revisions,
|
|
424
|
+
selected,
|
|
425
|
+
payloadPath: payloadPath ? `/${payloadPath}` : void 0
|
|
312
426
|
});
|
|
313
427
|
}
|
|
314
428
|
});
|
|
315
429
|
}
|
|
430
|
+
function diffPayloadPath(slug) {
|
|
431
|
+
return `_riebeckite/diff/${Buffer.from(slug).toString("base64url")}.json`;
|
|
432
|
+
}
|
|
316
433
|
function createPostDiffApi(options = {}) {
|
|
317
434
|
const reader = new GitMarkdownHistoryReader(options);
|
|
318
435
|
return {
|
|
@@ -354,32 +471,16 @@ function createPostDiffApi(options = {}) {
|
|
|
354
471
|
}
|
|
355
472
|
};
|
|
356
473
|
}
|
|
357
|
-
function
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
comparisons.push({
|
|
367
|
-
filePath,
|
|
368
|
-
fromHash: previous?.hash ?? null,
|
|
369
|
-
toHash: to.hash
|
|
370
|
-
});
|
|
371
|
-
for (let fromIndex = toIndex + 2; fromIndex < history.length; fromIndex += 1) {
|
|
372
|
-
comparisons.push({
|
|
373
|
-
filePath,
|
|
374
|
-
fromHash: history[fromIndex].hash,
|
|
375
|
-
toHash: to.hash
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
const diffs = await Promise.all(
|
|
380
|
-
comparisons.map((comparison) => api.compareRevisions(comparison))
|
|
474
|
+
async function getMarkdownRevisions(api, filePath, history) {
|
|
475
|
+
const revisions = await Promise.all(
|
|
476
|
+
history.map(async (revision) => ({
|
|
477
|
+
revision,
|
|
478
|
+
markdown: await api.getRevisionMarkdown(filePath, revision.hash)
|
|
479
|
+
}))
|
|
480
|
+
);
|
|
481
|
+
return revisions.flatMap(
|
|
482
|
+
({ revision, markdown }) => markdown === null ? [] : [{ ...revision, markdown }]
|
|
381
483
|
);
|
|
382
|
-
return diffs.filter((entry) => entry !== null);
|
|
383
484
|
}
|
|
384
485
|
function resolvePostFilePath(context) {
|
|
385
486
|
const indexedPath = context.contentIndex.get(context.slug.toLowerCase());
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MarkdownRevision, PostDiff } from "../types.js";
|
|
2
2
|
export type DiffHistoryProps = {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
revisions: MarkdownRevision[];
|
|
4
|
+
selected: PostDiff | null;
|
|
5
|
+
payloadPath?: string;
|
|
5
6
|
};
|
|
6
7
|
export declare function renderDiffHistory(input: DiffHistoryProps): string;
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { DiffRevision, GitHistoryReaderOptions } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads Markdown revisions out of a local Git repository.
|
|
4
|
+
*
|
|
5
|
+
* Callers speak content-relative paths — the same paths the content index
|
|
6
|
+
* reports. The reader resolves the owning work tree once and translates every
|
|
7
|
+
* path itself, so the process working directory never influences a result.
|
|
8
|
+
*/
|
|
2
9
|
export declare class GitMarkdownHistoryReader {
|
|
3
10
|
#private;
|
|
4
11
|
constructor(options?: GitHistoryReaderOptions);
|
|
12
|
+
/**
|
|
13
|
+
* Revision history for one file, newest first, following renames.
|
|
14
|
+
*
|
|
15
|
+
* @param filePath Content-relative path, e.g. `notes/hello.md`.
|
|
16
|
+
*/
|
|
5
17
|
getHistory(filePath: string): Promise<DiffRevision[]>;
|
|
18
|
+
/** Content of one revision of a file, or `null` when Git cannot read it. */
|
|
6
19
|
getRevisionMarkdown(filePath: string, hash: string): Promise<string | null>;
|
|
7
20
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paths that locate a content directory inside a Git work tree.
|
|
3
|
+
*
|
|
4
|
+
* Every field is a POSIX-separated path (`git` prints and accepts those on every
|
|
5
|
+
* platform, including Windows).
|
|
6
|
+
*/
|
|
7
|
+
export type GitRepositoryPaths = {
|
|
8
|
+
/** Work tree root reported by `git rev-parse --show-toplevel`. */
|
|
9
|
+
repositoryRoot: string;
|
|
10
|
+
/** Absolute path of the content directory. */
|
|
11
|
+
contentRoot: string;
|
|
12
|
+
/**
|
|
13
|
+
* Content directory relative to `repositoryRoot`, without a trailing slash.
|
|
14
|
+
* Empty when the content directory is the work tree root itself.
|
|
15
|
+
*/
|
|
16
|
+
contentPrefix: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Why Git history cannot be read for a content directory.
|
|
20
|
+
*
|
|
21
|
+
* The reason is modelled explicitly so a plugin can report an actionable
|
|
22
|
+
* diagnostic instead of quietly rendering nothing.
|
|
23
|
+
*/
|
|
24
|
+
export type RepositoryPathsResolution = {
|
|
25
|
+
kind: "resolved";
|
|
26
|
+
paths: GitRepositoryPaths;
|
|
27
|
+
}
|
|
28
|
+
/** Git is not installed, or could not be started. */
|
|
29
|
+
| {
|
|
30
|
+
kind: "git-unavailable";
|
|
31
|
+
detail: string;
|
|
32
|
+
}
|
|
33
|
+
/** Git ran, but the content directory does not live in a work tree. */
|
|
34
|
+
| {
|
|
35
|
+
kind: "content-outside-repository";
|
|
36
|
+
detail: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Locates the Git work tree that owns `contentRoot`.
|
|
40
|
+
*
|
|
41
|
+
* Both the work tree root and the content prefix come from Git itself, so their
|
|
42
|
+
* casing and symlink resolution always agree with the paths `git log` prints —
|
|
43
|
+
* the reason content-relative paths can be translated without guessing.
|
|
44
|
+
*
|
|
45
|
+
* The work tree is resolved through `git -C <contentRoot>` rather than by
|
|
46
|
+
* running Git in `contentRoot`, so a missing directory surfaces as a Git
|
|
47
|
+
* failure instead of an indistinguishable spawn failure.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveRepositoryPaths(contentRoot: string): Promise<RepositoryPathsResolution>;
|
|
50
|
+
/**
|
|
51
|
+
* Translates a content-relative path into the work tree-relative path that
|
|
52
|
+
* `git log` pathspecs and `git show <rev>:<path>` expect.
|
|
53
|
+
*/
|
|
54
|
+
export declare function toRepositoryPath(paths: GitRepositoryPaths, contentRelativePath: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Translates a work tree-relative path printed by `git` back into a
|
|
57
|
+
* content-relative path, or returns `null` when the path lies outside the
|
|
58
|
+
* content directory.
|
|
59
|
+
*/
|
|
60
|
+
export declare function toContentPath(paths: GitRepositoryPaths, repositoryRelativePath: string): string | null;
|
|
61
|
+
/** Normalizes a path to the POSIX form used for content and work tree paths. */
|
|
62
|
+
export declare function normalizeContentPath(value: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* True when the content directory has a usable work tree. Narrows the
|
|
65
|
+
* resolution so callers can read `paths`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isResolvedRepository(resolution: RepositoryPathsResolution): resolution is Extract<RepositoryPathsResolution, {
|
|
68
|
+
kind: "resolved";
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* True when history cannot be read and a reason is available. Narrows the
|
|
72
|
+
* resolution so callers can read `detail`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isRepositoryUnavailable(resolution: RepositoryPathsResolution): resolution is Exclude<RepositoryPathsResolution, {
|
|
75
|
+
kind: "resolved";
|
|
76
|
+
}>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of a `git` invocation.
|
|
3
|
+
*
|
|
4
|
+
* Failures are returned instead of thrown: a site whose Git history cannot be
|
|
5
|
+
* read must still build, so every caller decides what an unavailable repository
|
|
6
|
+
* means for its own output.
|
|
7
|
+
*/
|
|
8
|
+
export type GitCommandResult = {
|
|
9
|
+
ok: true;
|
|
10
|
+
stdout: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `spawnError` separates "Git could not be started" (missing or unexecutable
|
|
14
|
+
* binary) from "Git ran and rejected the command" (not a work tree, unknown
|
|
15
|
+
* revision, unreadable path). The distinction drives which diagnostic a
|
|
16
|
+
* plugin reports.
|
|
17
|
+
*/
|
|
18
|
+
| {
|
|
19
|
+
ok: false;
|
|
20
|
+
spawnError: boolean;
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Narrows a failed command so its failure fields can be read.
|
|
25
|
+
*
|
|
26
|
+
* Type predicates are used instead of `result.ok` checks throughout this
|
|
27
|
+
* package because the build type-checks without `strictNullChecks`, where
|
|
28
|
+
* discriminant narrowing of an inline condition does not apply.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isGitCommandFailure(result: GitCommandResult): result is Extract<GitCommandResult, {
|
|
31
|
+
ok: false;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Runs `git` with `args`.
|
|
35
|
+
*
|
|
36
|
+
* `core.quotePath=false` keeps non-ASCII paths verbatim so the printed paths
|
|
37
|
+
* match the ones the content index reports; Git otherwise escapes them into
|
|
38
|
+
* octal sequences that no path lookup can resolve.
|
|
39
|
+
*
|
|
40
|
+
* @param cwd Directory Git runs in. Omit when the command carries its own
|
|
41
|
+
* `-C <dir>` argument.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runGit(args: readonly string[], cwd?: string): Promise<GitCommandResult>;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -26,6 +26,12 @@ export type RevisionComparisonInput = {
|
|
|
26
26
|
toHash: string;
|
|
27
27
|
};
|
|
28
28
|
export type GitHistoryReaderOptions = {
|
|
29
|
+
/**
|
|
30
|
+
* Content root: the directory holding the notes, used to locate the owning
|
|
31
|
+
* Git work tree. Defaults to `config.content.directory` when the plugin runs
|
|
32
|
+
* inside a build, otherwise `process.cwd()`. A relative value is resolved
|
|
33
|
+
* against `process.cwd()`.
|
|
34
|
+
*/
|
|
29
35
|
cwd?: string;
|
|
30
36
|
};
|
|
31
37
|
export type DiffPluginUiOptions = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-diff",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"./style.css": "./style.css"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@riebeckite/core": "0.0.
|
|
24
|
+
"@riebeckite/core": "0.0.16"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^24.5.2",
|
|
28
28
|
"tsx": "^4.22.4",
|
|
29
|
-
"@riebeckite/test": "0.0.
|
|
29
|
+
"@riebeckite/test": "0.0.16"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|
package/style.css
CHANGED
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
justify-content: space-between;
|
|
13
13
|
gap: 1rem;
|
|
14
14
|
margin-block-end: 1rem;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
user-select: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.rr-diff-history__panel {
|
|
20
|
+
interpolate-size: allow-keywords;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.rr-diff-history__header::marker {
|
|
24
|
+
content: "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.rr-diff-history__header::-webkit-details-marker {
|
|
28
|
+
display: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.rr-diff-history__header:focus-visible {
|
|
32
|
+
outline: 2px solid var(--rb-color-accent, #f66620);
|
|
33
|
+
outline-offset: 2px;
|
|
15
34
|
}
|
|
16
35
|
|
|
17
36
|
.rr-diff-history__title {
|
|
@@ -21,6 +40,72 @@
|
|
|
21
40
|
text-transform: uppercase;
|
|
22
41
|
}
|
|
23
42
|
|
|
43
|
+
.rr-diff-history__header-meta {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 0.5rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.rr-diff-history__toggle-label {
|
|
50
|
+
color: var(
|
|
51
|
+
--rb-color-muted,
|
|
52
|
+
color-mix(in srgb, currentColor 62%, transparent)
|
|
53
|
+
);
|
|
54
|
+
font-size: 0.75rem;
|
|
55
|
+
letter-spacing: 0.04em;
|
|
56
|
+
text-transform: uppercase;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.rr-diff-history__toggle-label--open {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.rr-diff-history__panel[open] .rr-diff-history__toggle-label--open {
|
|
64
|
+
display: inline;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.rr-diff-history__panel[open] .rr-diff-history__toggle-label--closed {
|
|
68
|
+
display: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.rr-diff-history__toggle {
|
|
72
|
+
display: inline-grid;
|
|
73
|
+
place-items: center;
|
|
74
|
+
width: 1.5rem;
|
|
75
|
+
height: 1.5rem;
|
|
76
|
+
color: var(
|
|
77
|
+
--rb-color-muted,
|
|
78
|
+
color-mix(in srgb, currentColor 62%, transparent)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.rr-diff-history__toggle-icon {
|
|
83
|
+
transform: rotate(180deg);
|
|
84
|
+
transition: transform 180ms ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.rr-diff-history__panel[open] .rr-diff-history__toggle-icon {
|
|
88
|
+
transform: rotate(90deg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@supports selector(::details-content) {
|
|
92
|
+
.rr-diff-history__panel::details-content {
|
|
93
|
+
block-size: 0;
|
|
94
|
+
overflow: clip;
|
|
95
|
+
opacity: 0;
|
|
96
|
+
transition:
|
|
97
|
+
block-size 220ms ease,
|
|
98
|
+
content-visibility 220ms ease allow-discrete,
|
|
99
|
+
opacity 160ms ease;
|
|
100
|
+
transition-behavior: allow-discrete;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.rr-diff-history__panel[open]::details-content {
|
|
104
|
+
block-size: auto;
|
|
105
|
+
opacity: 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
24
109
|
.rr-diff-history__count,
|
|
25
110
|
.rr-diff-history__date,
|
|
26
111
|
.rr-diff-history__meta {
|
|
@@ -136,27 +221,16 @@
|
|
|
136
221
|
var(--rb-color-border, color-mix(in srgb, currentColor 18%, transparent));
|
|
137
222
|
}
|
|
138
223
|
|
|
139
|
-
.rr-diff-history__diff-head {
|
|
140
|
-
border-block-end: 1px solid
|
|
141
|
-
var(--rb-color-border, color-mix(in srgb, currentColor 18%, transparent));
|
|
142
|
-
padding: 0.625rem 0.75rem;
|
|
143
|
-
font-family: var(
|
|
144
|
-
--rb-font-mono,
|
|
145
|
-
ui-monospace,
|
|
146
|
-
SFMono-Regular,
|
|
147
|
-
Consolas,
|
|
148
|
-
monospace
|
|
149
|
-
);
|
|
150
|
-
font-size: 0.8125rem;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
224
|
.rr-diff-history__table-wrap {
|
|
154
225
|
overflow-x: auto;
|
|
155
226
|
}
|
|
156
227
|
|
|
157
228
|
.rr-diff-history__table {
|
|
229
|
+
display: table;
|
|
158
230
|
width: 100%;
|
|
159
231
|
min-width: 42rem;
|
|
232
|
+
margin-block: 0;
|
|
233
|
+
overflow-x: visible;
|
|
160
234
|
border-collapse: collapse;
|
|
161
235
|
font-family: var(
|
|
162
236
|
--rb-font-mono,
|