@riebeckite/plugin-diff 0.0.31 → 0.0.33
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_ja.md +1 -1
- package/dist/components/post-diff.d.ts +2 -0
- package/dist/components/post-diff.js +261 -0
- package/dist/index.d.ts +7 -14
- package/dist/index.js +252 -325
- package/dist/src/api.d.ts +11 -0
- package/dist/src/components/diff-history.d.ts +2 -1
- package/dist/src/components/diff-line.d.ts +2 -1
- package/dist/src/components/diff-viewer.d.ts +2 -1
- package/dist/src/types.d.ts +3 -0
- package/package.json +7 -5
- package/dist/src/git/repo_paths.d.ts +0 -76
- package/dist/src/git/run_git.d.ts +0 -43
package/README_ja.md
CHANGED
|
@@ -32,7 +32,7 @@ const current = await api.getCurrentDiff("notes/hello.md");
|
|
|
32
32
|
| `getCurrentDiff(filePath)` | 最新と一つ前のリビジョンの差分 |
|
|
33
33
|
| `compareRevisions({ filePath, fromHash, toHash })` | 指定した二つのリビジョンの差分。`fromHash: null` なら空の本文との差分 |
|
|
34
34
|
|
|
35
|
-
`cwd` は Git
|
|
35
|
+
`cwd` は Git リポジトリを探すためのコンテンツルートで、既定値はビルド時の `config.content.directory`、それ以外では `process.cwd()` です。相対パスは `process.cwd()` 基準で解決されます。Git リポジトリ外を指定しても例外は送出せず、空の結果を返します。
|
|
36
36
|
|
|
37
37
|
## オプションと型
|
|
38
38
|
|
|
@@ -5,3 +5,5 @@ export type PostDiffProps = {
|
|
|
5
5
|
};
|
|
6
6
|
declare const PostDiff: ({ diff, title }: Readonly<PostDiffProps>) => import("hono/jsx/jsx-dev-runtime").JSX.Element;
|
|
7
7
|
export default PostDiff;
|
|
8
|
+
export type ArticlePostDiffProps = Omit<PostDiffProps, "diff">;
|
|
9
|
+
export declare function ArticlePostDiffView(props: Readonly<ArticlePostDiffProps>): Promise<any[] | import("hono/utils/html").HtmlEscapedString>;
|
|
@@ -1,3 +1,247 @@
|
|
|
1
|
+
// components/post-diff.tsx
|
|
2
|
+
import { resolvePlugins } from "@riebeckite/core";
|
|
3
|
+
import { useArticleContext, useSiteContext } from "@riebeckite/ui";
|
|
4
|
+
|
|
5
|
+
// src/diff/line_diff.ts
|
|
6
|
+
function createLineDiff(fromMarkdown, toMarkdown) {
|
|
7
|
+
const fromLines = splitLines(fromMarkdown);
|
|
8
|
+
const toLines = splitLines(toMarkdown);
|
|
9
|
+
const table = buildLongestCommonSubsequenceTable(fromLines, toLines);
|
|
10
|
+
const lines = [];
|
|
11
|
+
let fromIndex = 0;
|
|
12
|
+
let toIndex = 0;
|
|
13
|
+
let oldLineNumber = 1;
|
|
14
|
+
let newLineNumber = 1;
|
|
15
|
+
while (fromIndex < fromLines.length && toIndex < toLines.length) {
|
|
16
|
+
if (fromLines[fromIndex] === toLines[toIndex]) {
|
|
17
|
+
lines.push({
|
|
18
|
+
type: "context",
|
|
19
|
+
content: fromLines[fromIndex],
|
|
20
|
+
oldLineNumber,
|
|
21
|
+
newLineNumber
|
|
22
|
+
});
|
|
23
|
+
fromIndex += 1;
|
|
24
|
+
toIndex += 1;
|
|
25
|
+
oldLineNumber += 1;
|
|
26
|
+
newLineNumber += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (table[fromIndex + 1][toIndex] >= table[fromIndex][toIndex + 1]) {
|
|
30
|
+
lines.push({
|
|
31
|
+
type: "removed",
|
|
32
|
+
content: fromLines[fromIndex],
|
|
33
|
+
oldLineNumber,
|
|
34
|
+
newLineNumber: null
|
|
35
|
+
});
|
|
36
|
+
fromIndex += 1;
|
|
37
|
+
oldLineNumber += 1;
|
|
38
|
+
} else {
|
|
39
|
+
lines.push({
|
|
40
|
+
type: "added",
|
|
41
|
+
content: toLines[toIndex],
|
|
42
|
+
oldLineNumber: null,
|
|
43
|
+
newLineNumber
|
|
44
|
+
});
|
|
45
|
+
toIndex += 1;
|
|
46
|
+
newLineNumber += 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
while (fromIndex < fromLines.length) {
|
|
50
|
+
lines.push({
|
|
51
|
+
type: "removed",
|
|
52
|
+
content: fromLines[fromIndex],
|
|
53
|
+
oldLineNumber,
|
|
54
|
+
newLineNumber: null
|
|
55
|
+
});
|
|
56
|
+
fromIndex += 1;
|
|
57
|
+
oldLineNumber += 1;
|
|
58
|
+
}
|
|
59
|
+
while (toIndex < toLines.length) {
|
|
60
|
+
lines.push({
|
|
61
|
+
type: "added",
|
|
62
|
+
content: toLines[toIndex],
|
|
63
|
+
oldLineNumber: null,
|
|
64
|
+
newLineNumber
|
|
65
|
+
});
|
|
66
|
+
toIndex += 1;
|
|
67
|
+
newLineNumber += 1;
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
function splitLines(markdown) {
|
|
72
|
+
if (!markdown) return [];
|
|
73
|
+
return markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
74
|
+
}
|
|
75
|
+
function buildLongestCommonSubsequenceTable(fromLines, toLines) {
|
|
76
|
+
const table = Array.from(
|
|
77
|
+
{ length: fromLines.length + 1 },
|
|
78
|
+
() => Array.from({ length: toLines.length + 1 }, () => 0)
|
|
79
|
+
);
|
|
80
|
+
for (let fromIndex = fromLines.length - 1; fromIndex >= 0; fromIndex -= 1) {
|
|
81
|
+
for (let toIndex = toLines.length - 1; toIndex >= 0; toIndex -= 1) {
|
|
82
|
+
table[fromIndex][toIndex] = fromLines[fromIndex] === toLines[toIndex] ? table[fromIndex + 1][toIndex + 1] + 1 : Math.max(
|
|
83
|
+
table[fromIndex + 1][toIndex],
|
|
84
|
+
table[fromIndex][toIndex + 1]
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return table;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/git/history_reader.ts
|
|
92
|
+
import {
|
|
93
|
+
GitRepository,
|
|
94
|
+
normalizeContentPath,
|
|
95
|
+
runGit,
|
|
96
|
+
toRepositoryPath
|
|
97
|
+
} from "@riebeckite/git-history";
|
|
98
|
+
var fieldSeparator = "";
|
|
99
|
+
var recordSeparator = "";
|
|
100
|
+
var GitMarkdownHistoryReader = class {
|
|
101
|
+
#repository;
|
|
102
|
+
#historyCache = /* @__PURE__ */ new Map();
|
|
103
|
+
#markdownCache = /* @__PURE__ */ new Map();
|
|
104
|
+
/** Maps a content-relative path to `<revision hash>` → work tree path. */
|
|
105
|
+
#revisionPathCache = /* @__PURE__ */ new Map();
|
|
106
|
+
constructor(options = {}) {
|
|
107
|
+
this.#repository = new GitRepository(options);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Revision history for one file, newest first, following renames.
|
|
111
|
+
*
|
|
112
|
+
* @param filePath Content-relative path, e.g. `notes/hello.md`.
|
|
113
|
+
*/
|
|
114
|
+
getHistory(filePath) {
|
|
115
|
+
const normalizedPath = normalizeContentPath(filePath);
|
|
116
|
+
const cached = this.#historyCache.get(normalizedPath);
|
|
117
|
+
if (cached) return cached;
|
|
118
|
+
const history = this.#readHistory(normalizedPath);
|
|
119
|
+
this.#historyCache.set(normalizedPath, history);
|
|
120
|
+
return history;
|
|
121
|
+
}
|
|
122
|
+
/** Content of one revision of a file, or `null` when Git cannot read it. */
|
|
123
|
+
getRevisionMarkdown(filePath, hash) {
|
|
124
|
+
const normalizedPath = normalizeContentPath(filePath);
|
|
125
|
+
const cacheKey = `${hash}:${normalizedPath}`;
|
|
126
|
+
const cached = this.#markdownCache.get(cacheKey);
|
|
127
|
+
if (cached) return cached;
|
|
128
|
+
const markdown = this.#readRevisionMarkdown(normalizedPath, hash);
|
|
129
|
+
this.#markdownCache.set(cacheKey, markdown);
|
|
130
|
+
return markdown;
|
|
131
|
+
}
|
|
132
|
+
async #readHistory(filePath) {
|
|
133
|
+
const repository = await this.#resolvePaths();
|
|
134
|
+
if (!repository) return [];
|
|
135
|
+
const format = ["%H", "%h", "%cI", "%s", "%an"].join(fieldSeparator);
|
|
136
|
+
const result = await runGit(
|
|
137
|
+
[
|
|
138
|
+
"log",
|
|
139
|
+
"--follow",
|
|
140
|
+
"--name-status",
|
|
141
|
+
`--format=${recordSeparator}${format}`,
|
|
142
|
+
"--",
|
|
143
|
+
toRepositoryPath(repository, filePath)
|
|
144
|
+
],
|
|
145
|
+
repository.repositoryRoot
|
|
146
|
+
);
|
|
147
|
+
if (!result.ok || !result.stdout.trim()) return [];
|
|
148
|
+
const revisionPaths = /* @__PURE__ */ new Map();
|
|
149
|
+
const fallbackPath = toRepositoryPath(repository, filePath);
|
|
150
|
+
const revisions = result.stdout.split(recordSeparator).map((record) => record.trim()).filter(Boolean).map((record) => parseRevisionRecord(record, revisionPaths, fallbackPath)).filter((revision) => revision !== null);
|
|
151
|
+
this.#revisionPathCache.set(filePath, revisionPaths);
|
|
152
|
+
return revisions;
|
|
153
|
+
}
|
|
154
|
+
async #readRevisionMarkdown(filePath, hash) {
|
|
155
|
+
const repository = await this.#resolvePaths();
|
|
156
|
+
if (!repository) return null;
|
|
157
|
+
await this.getHistory(filePath);
|
|
158
|
+
const revisionPath = this.#revisionPathCache.get(filePath)?.get(hash) ?? toRepositoryPath(repository, filePath);
|
|
159
|
+
const result = await runGit(
|
|
160
|
+
["show", `${hash}:${revisionPath}`],
|
|
161
|
+
repository.repositoryRoot
|
|
162
|
+
);
|
|
163
|
+
return result.ok ? result.stdout : null;
|
|
164
|
+
}
|
|
165
|
+
getStateFingerprint() {
|
|
166
|
+
return this.#repository.getStateFingerprint();
|
|
167
|
+
}
|
|
168
|
+
/** `null` when the content directory has no readable work tree. */
|
|
169
|
+
#resolvePaths() {
|
|
170
|
+
return this.#repository.paths();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function parseRevisionRecord(record, revisionPaths, fallbackPath) {
|
|
174
|
+
const [metadata = "", ...nameStatusLines] = record.split("\n");
|
|
175
|
+
const [hash, shortHash, date, message, author] = metadata.split(fieldSeparator);
|
|
176
|
+
if (!hash || !shortHash || !date) return null;
|
|
177
|
+
revisionPaths.set(hash, getRevisionPath(nameStatusLines, fallbackPath));
|
|
178
|
+
return {
|
|
179
|
+
hash,
|
|
180
|
+
shortHash,
|
|
181
|
+
date,
|
|
182
|
+
message: message ?? "",
|
|
183
|
+
author: author ?? ""
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function getRevisionPath(nameStatusLines, fallbackPath) {
|
|
187
|
+
const line = nameStatusLines.find((entry) => entry.trim().length > 0);
|
|
188
|
+
if (!line) return fallbackPath;
|
|
189
|
+
const [, ...paths] = line.split(" ");
|
|
190
|
+
return normalizeContentPath(paths.at(-1) ?? fallbackPath);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/api.ts
|
|
194
|
+
function createPostDiffApi(options = {}) {
|
|
195
|
+
return createPostDiffApiFromReader(new GitMarkdownHistoryReader(options));
|
|
196
|
+
}
|
|
197
|
+
function createPostDiffApiFromReader(reader) {
|
|
198
|
+
return {
|
|
199
|
+
getHistory: (filePath) => reader.getHistory(filePath),
|
|
200
|
+
getRevisionMarkdown: (filePath, hash) => reader.getRevisionMarkdown(filePath, hash),
|
|
201
|
+
getCurrentDiff: async (filePath) => {
|
|
202
|
+
const history = await reader.getHistory(filePath);
|
|
203
|
+
const [current, previous] = history;
|
|
204
|
+
if (!current) return null;
|
|
205
|
+
const toMarkdown = await reader.getRevisionMarkdown(
|
|
206
|
+
filePath,
|
|
207
|
+
current.hash
|
|
208
|
+
);
|
|
209
|
+
if (toMarkdown === null) return null;
|
|
210
|
+
const fromMarkdown = previous ? await reader.getRevisionMarkdown(filePath, previous.hash) : null;
|
|
211
|
+
return buildPostDiff({
|
|
212
|
+
from: previous ?? null,
|
|
213
|
+
fromMarkdown,
|
|
214
|
+
to: current,
|
|
215
|
+
toMarkdown
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
compareRevisions: async (input) => {
|
|
219
|
+
const history = await reader.getHistory(input.filePath);
|
|
220
|
+
const from = history.find((revision) => revision.hash === input.fromHash);
|
|
221
|
+
const to = history.find((revision) => revision.hash === input.toHash);
|
|
222
|
+
if (!to) return null;
|
|
223
|
+
const [fromMarkdown, toMarkdown] = await Promise.all([
|
|
224
|
+
input.fromHash ? reader.getRevisionMarkdown(input.filePath, input.fromHash) : Promise.resolve(null),
|
|
225
|
+
reader.getRevisionMarkdown(input.filePath, input.toHash)
|
|
226
|
+
]);
|
|
227
|
+
if (toMarkdown === null) return null;
|
|
228
|
+
return buildPostDiff({
|
|
229
|
+
from: from ?? null,
|
|
230
|
+
fromMarkdown,
|
|
231
|
+
to,
|
|
232
|
+
toMarkdown
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function buildPostDiff(input) {
|
|
238
|
+
return {
|
|
239
|
+
from: input.from,
|
|
240
|
+
to: input.to,
|
|
241
|
+
lines: createLineDiff(input.fromMarkdown ?? "", input.toMarkdown)
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
1
245
|
// components/post-diff.tsx
|
|
2
246
|
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
3
247
|
var PostDiff = (({ diff, title = "History" }) => {
|
|
@@ -23,6 +267,23 @@ var PostDiff = (({ diff, title = "History" }) => {
|
|
|
23
267
|
] });
|
|
24
268
|
});
|
|
25
269
|
var post_diff_default = PostDiff;
|
|
270
|
+
async function ArticlePostDiffView(props) {
|
|
271
|
+
const site = useSiteContext();
|
|
272
|
+
const article = useArticleContext();
|
|
273
|
+
if (!site || !article?.entry) return [];
|
|
274
|
+
const options = findDiffOptions(site.config.plugins);
|
|
275
|
+
if (!options || options.ui?.enabled === false) return [];
|
|
276
|
+
const filePath = site.manifest.contentIndex.get(article.entry.slug.toLowerCase()) ?? `${article.entry.slug}.md`;
|
|
277
|
+
const diff = await createPostDiffApi({
|
|
278
|
+
...options,
|
|
279
|
+
cwd: options.cwd ?? site.config.content.directory
|
|
280
|
+
}).getCurrentDiff(filePath);
|
|
281
|
+
return /* @__PURE__ */ jsx(PostDiff, { diff, title: props.title });
|
|
282
|
+
}
|
|
283
|
+
function findDiffOptions(plugins) {
|
|
284
|
+
return resolvePlugins([...plugins]).find((plugin) => plugin.name === "diff")?.options;
|
|
285
|
+
}
|
|
26
286
|
export {
|
|
287
|
+
ArticlePostDiffView,
|
|
27
288
|
post_diff_default as default
|
|
28
289
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
import { type PluginDataHelper } from "@riebeckite/core";
|
|
2
|
-
export type { PostDiffProps } from "./components/post-diff.js";
|
|
3
|
-
export { default as PostDiffView } from "./components/post-diff.js";
|
|
2
|
+
export type { ArticlePostDiffProps, PostDiffProps, } from "./components/post-diff.js";
|
|
3
|
+
export { ArticlePostDiffView, default as PostDiffView, } from "./components/post-diff.js";
|
|
4
4
|
export { renderDiffHistory } from "./src/components/diff-history.js";
|
|
5
5
|
export { renderDiffLine } from "./src/components/diff-line.js";
|
|
6
6
|
export { renderDiffPanel, renderDiffViewer, } from "./src/components/diff-viewer.js";
|
|
7
|
-
import
|
|
7
|
+
import { type PostDiffApi } from "./src/api.js";
|
|
8
|
+
import type { DiffPluginOptions, PostDiff } from "./src/types.js";
|
|
9
|
+
export type { PostDiffApi } from "./src/api.js";
|
|
10
|
+
export { createPostDiffApi } from "./src/api.js";
|
|
8
11
|
export { createLineDiff } from "./src/diff/line_diff.js";
|
|
9
12
|
export { GitMarkdownHistoryReader } from "./src/git/history_reader.js";
|
|
10
|
-
export type { DiffLine, DiffLineType, DiffPluginUiOptions, DiffRevision, GitHistoryReaderOptions, MarkdownRevision, PostDiff, RevisionComparisonInput, } from "./src/types.js";
|
|
11
|
-
export type DiffPluginOptions = GitHistoryReaderOptions & {
|
|
12
|
-
ui?: DiffPluginUiOptions;
|
|
13
|
-
};
|
|
14
|
-
export type PostDiffApi = {
|
|
15
|
-
getHistory(filePath: string): Promise<DiffRevision[]>;
|
|
16
|
-
getRevisionMarkdown(filePath: string, hash: string): Promise<string | null>;
|
|
17
|
-
getCurrentDiff(filePath: string): Promise<PostDiff | null>;
|
|
18
|
-
compareRevisions(input: RevisionComparisonInput): Promise<PostDiff | null>;
|
|
19
|
-
};
|
|
13
|
+
export type { DiffLine, DiffLineType, DiffPluginOptions, DiffPluginUiOptions, DiffRevision, GitHistoryReaderOptions, MarkdownRevision, PostDiff, RevisionComparisonInput, } from "./src/types.js";
|
|
20
14
|
export declare const getPostDiff: PluginDataHelper<{
|
|
21
15
|
api: PostDiffApi;
|
|
22
16
|
filePath: string;
|
|
23
17
|
}, Promise<PostDiff | null>>;
|
|
24
18
|
export declare function diff(options?: DiffPluginOptions): import("@riebeckite/core").RiebeckitePlugin<DiffPluginOptions>;
|
|
25
|
-
export declare function createPostDiffApi(options?: GitHistoryReaderOptions): PostDiffApi;
|
package/dist/index.js
CHANGED
|
@@ -7,128 +7,8 @@ import {
|
|
|
7
7
|
} from "@riebeckite/core";
|
|
8
8
|
|
|
9
9
|
// components/post-diff.tsx
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
if (!diff2) return null;
|
|
13
|
-
return /* @__PURE__ */ jsxs("section", { class: "rr-diff-history", "data-rr-diff-history": true, children: [
|
|
14
|
-
/* @__PURE__ */ jsx("h2", { class: "rr-diff-history__title", children: title }),
|
|
15
|
-
/* @__PURE__ */ jsxs("p", { class: "rr-diff-history__meta", children: [
|
|
16
|
-
diff2.from ? `${diff2.from.shortHash} → ` : "",
|
|
17
|
-
diff2.to.shortHash
|
|
18
|
-
] }),
|
|
19
|
-
/* @__PURE__ */ jsx("pre", { class: "rr-diff-history__diff", children: /* @__PURE__ */ jsx("code", { children: diff2.lines.map((line, index) => /* @__PURE__ */ jsxs(
|
|
20
|
-
"span",
|
|
21
|
-
{
|
|
22
|
-
class: `rr-diff-line rr-diff-line--${line.type}`,
|
|
23
|
-
children: [
|
|
24
|
-
line.type === "added" ? "+" : line.type === "removed" ? "-" : " ",
|
|
25
|
-
line.content,
|
|
26
|
-
"\n"
|
|
27
|
-
]
|
|
28
|
-
},
|
|
29
|
-
`${line.oldLineNumber}-${line.newLineNumber}-${index}`
|
|
30
|
-
)) }) })
|
|
31
|
-
] });
|
|
32
|
-
});
|
|
33
|
-
var post_diff_default = PostDiff;
|
|
34
|
-
|
|
35
|
-
// src/components/diff-line.tsx
|
|
36
|
-
function renderDiffLine({ line }) {
|
|
37
|
-
const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
|
|
38
|
-
return `<tr class="rr-diff-history__line rr-diff-history__line--${line.type}">
|
|
39
|
-
<td class="rr-diff-history__num">${line.oldLineNumber ?? ""}</td>
|
|
40
|
-
<td class="rr-diff-history__num">${line.newLineNumber ?? ""}</td>
|
|
41
|
-
<td class="rr-diff-history__code"><code>${prefix} ${escapeHtml(line.content)}</code></td>
|
|
42
|
-
</tr>`;
|
|
43
|
-
}
|
|
44
|
-
function escapeHtml(value) {
|
|
45
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// src/components/format-date.ts
|
|
49
|
-
var MONTHS = [
|
|
50
|
-
"Jan",
|
|
51
|
-
"Feb",
|
|
52
|
-
"Mar",
|
|
53
|
-
"Apr",
|
|
54
|
-
"May",
|
|
55
|
-
"Jun",
|
|
56
|
-
"Jul",
|
|
57
|
-
"Aug",
|
|
58
|
-
"Sep",
|
|
59
|
-
"Oct",
|
|
60
|
-
"Nov",
|
|
61
|
-
"Dec"
|
|
62
|
-
];
|
|
63
|
-
function formatDiffDate(value) {
|
|
64
|
-
const date = new Date(value);
|
|
65
|
-
if (Number.isNaN(date.getTime())) return value;
|
|
66
|
-
return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// src/components/diff-viewer.tsx
|
|
70
|
-
function renderDiffViewer({
|
|
71
|
-
history,
|
|
72
|
-
selected
|
|
73
|
-
}) {
|
|
74
|
-
return `<div class="rr-diff-history__viewer">
|
|
75
|
-
<div class="rr-diff-history__controls">
|
|
76
|
-
<label>From ${renderRevisionSelect("from", history, selected.from?.hash ?? "")}</label>
|
|
77
|
-
<label>To ${renderRevisionSelect("to", history, selected.to.hash)}</label>
|
|
78
|
-
</div>
|
|
79
|
-
<div data-rr-diff-panel>${renderDiffPanel(selected)}</div>
|
|
80
|
-
</div>`;
|
|
81
|
-
}
|
|
82
|
-
function renderDiffPanel(diff2) {
|
|
83
|
-
return `<div class="rr-diff-history__diff" data-rr-diff-rendered>
|
|
84
|
-
<div class="rr-diff-history__table-wrap">
|
|
85
|
-
<table class="rr-diff-history__table">
|
|
86
|
-
<tbody>${diff2.lines.map((line) => renderDiffLine({ line })).join("")}</tbody>
|
|
87
|
-
</table>
|
|
88
|
-
</div>
|
|
89
|
-
</div>`;
|
|
90
|
-
}
|
|
91
|
-
function renderRevisionSelect(kind, history, value) {
|
|
92
|
-
const emptyOption = kind === "from" ? `<option value="">Beginning</option>` : "";
|
|
93
|
-
return `<select data-rr-diff-${kind}>${emptyOption}${history.map(
|
|
94
|
-
(revision) => `<option value="${escapeHtml(revision.hash)}"${revision.hash === value ? " selected" : ""}>${escapeHtml(revision.shortHash)} · ${escapeHtml(formatDiffDate(revision.date))}</option>`
|
|
95
|
-
).join("")}</select>`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// src/components/diff-history.tsx
|
|
99
|
-
function renderDiffHistory(input) {
|
|
100
|
-
const history = input.revisions;
|
|
101
|
-
const selected = input.selected;
|
|
102
|
-
const payloadPath = input.payloadPath;
|
|
103
|
-
if (history.length === 0 || !selected) return "";
|
|
104
|
-
return `<section class="rr-diff-history" data-rr-diff-history>
|
|
105
|
-
<details class="rr-diff-history__panel">
|
|
106
|
-
<summary class="rr-diff-history__header">
|
|
107
|
-
<h2 class="rr-diff-history__title">History</h2>
|
|
108
|
-
<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>
|
|
109
|
-
</summary>
|
|
110
|
-
${renderContent(history, selected)}
|
|
111
|
-
</details>
|
|
112
|
-
${payloadPath ? `<script type="application/json" data-rr-diff-history-data>${JSON.stringify({ path: payloadPath }).replace(/</g, "\\u003c")}</script>` : ""}
|
|
113
|
-
</section>`;
|
|
114
|
-
}
|
|
115
|
-
function renderContent(history, selected) {
|
|
116
|
-
return `<div class="rr-diff-history__layout">
|
|
117
|
-
<ol class="rr-diff-history__list" aria-label="Commit history">
|
|
118
|
-
${history.map((revision, index) => renderRevisionButton(revision, index === 0)).join("")}
|
|
119
|
-
</ol>
|
|
120
|
-
${renderDiffViewer({ history, selected })}
|
|
121
|
-
</div>`;
|
|
122
|
-
}
|
|
123
|
-
function renderRevisionButton(revision, selected) {
|
|
124
|
-
return `<li class="rr-diff-history__item">
|
|
125
|
-
<button class="rr-diff-history__commit" type="button" data-rr-diff-select="${escapeHtml(revision.hash)}" aria-pressed="${selected}">
|
|
126
|
-
<span class="rr-diff-history__date">${escapeHtml(formatDiffDate(revision.date))}</span>
|
|
127
|
-
<span class="rr-diff-history__message">${escapeHtml(revision.message || "Untitled change")}</span>
|
|
128
|
-
<span class="rr-diff-history__meta">${escapeHtml(revision.shortHash)} · ${escapeHtml(revision.author)}</span>
|
|
129
|
-
</button>
|
|
130
|
-
</li>`;
|
|
131
|
-
}
|
|
10
|
+
import { resolvePlugins } from "@riebeckite/core";
|
|
11
|
+
import { useArticleContext, useSiteContext } from "@riebeckite/ui";
|
|
132
12
|
|
|
133
13
|
// src/diff/line_diff.ts
|
|
134
14
|
function createLineDiff(fromMarkdown, toMarkdown) {
|
|
@@ -217,114 +97,22 @@ function buildLongestCommonSubsequenceTable(fromLines, toLines) {
|
|
|
217
97
|
}
|
|
218
98
|
|
|
219
99
|
// src/git/history_reader.ts
|
|
220
|
-
import
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
import { execFile } from "node:child_process";
|
|
227
|
-
import { promisify } from "node:util";
|
|
228
|
-
var execFileAsync = promisify(execFile);
|
|
229
|
-
function isGitCommandFailure(result) {
|
|
230
|
-
return result.ok === false;
|
|
231
|
-
}
|
|
232
|
-
async function runGit(args, cwd) {
|
|
233
|
-
try {
|
|
234
|
-
const { stdout } = await execFileAsync(
|
|
235
|
-
"git",
|
|
236
|
-
["-c", "core.quotePath=false", ...args],
|
|
237
|
-
{
|
|
238
|
-
...cwd === void 0 ? {} : { cwd },
|
|
239
|
-
maxBuffer: 1024 * 1024 * 10
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
return { ok: true, stdout };
|
|
243
|
-
} catch (error) {
|
|
244
|
-
return {
|
|
245
|
-
ok: false,
|
|
246
|
-
spawnError: isSpawnFailure(readErrorCode(error)),
|
|
247
|
-
detail: describeGitFailure(error)
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
function readErrorCode(error) {
|
|
252
|
-
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
253
|
-
}
|
|
254
|
-
function isSpawnFailure(code) {
|
|
255
|
-
return typeof code === "string" && !code.startsWith("ERR_");
|
|
256
|
-
}
|
|
257
|
-
function describeGitFailure(error) {
|
|
258
|
-
if (typeof error !== "object" || error === null) {
|
|
259
|
-
return error instanceof Error ? error.message : String(error);
|
|
260
|
-
}
|
|
261
|
-
const stderr = "stderr" in error ? error.stderr : void 0;
|
|
262
|
-
if (typeof stderr === "string" && stderr.trim()) return stderr.trim();
|
|
263
|
-
if (error instanceof Error) return error.message;
|
|
264
|
-
return String(error);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// src/git/repo_paths.ts
|
|
268
|
-
async function resolveRepositoryPaths(contentRoot) {
|
|
269
|
-
const resolvedContentRoot = path.resolve(contentRoot);
|
|
270
|
-
const result = await runGit([
|
|
271
|
-
"-C",
|
|
272
|
-
resolvedContentRoot,
|
|
273
|
-
"rev-parse",
|
|
274
|
-
"--show-toplevel",
|
|
275
|
-
"--show-prefix"
|
|
276
|
-
]);
|
|
277
|
-
if (isGitCommandFailure(result)) {
|
|
278
|
-
return result.spawnError ? { kind: "git-unavailable", detail: result.detail } : { kind: "content-outside-repository", detail: result.detail };
|
|
279
|
-
}
|
|
280
|
-
const [toplevel, prefix] = parseRevParseOutput(result.stdout);
|
|
281
|
-
if (!toplevel) {
|
|
282
|
-
return {
|
|
283
|
-
kind: "content-outside-repository",
|
|
284
|
-
detail: "git rev-parse reported no work tree root"
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
kind: "resolved",
|
|
289
|
-
paths: {
|
|
290
|
-
repositoryRoot: toPosixPath(path.normalize(toplevel)),
|
|
291
|
-
contentRoot: toPosixPath(resolvedContentRoot),
|
|
292
|
-
contentPrefix: toPosixPath(prefix).replace(/\/+$/, "")
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
function toRepositoryPath(paths, contentRelativePath) {
|
|
297
|
-
const normalized = normalizeContentPath(contentRelativePath);
|
|
298
|
-
if (!paths.contentPrefix) return normalized;
|
|
299
|
-
if (!normalized) return paths.contentPrefix;
|
|
300
|
-
return `${paths.contentPrefix}/${normalized}`;
|
|
301
|
-
}
|
|
302
|
-
function normalizeContentPath(value) {
|
|
303
|
-
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
304
|
-
}
|
|
305
|
-
function isResolvedRepository(resolution) {
|
|
306
|
-
return resolution.kind === "resolved";
|
|
307
|
-
}
|
|
308
|
-
function parseRevParseOutput(stdout) {
|
|
309
|
-
const [toplevel = "", prefix = ""] = stdout.split(/\r?\n/);
|
|
310
|
-
return [toplevel.trim(), prefix.trim()];
|
|
311
|
-
}
|
|
312
|
-
function toPosixPath(value) {
|
|
313
|
-
return value.replace(/\\/g, "/");
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// src/git/history_reader.ts
|
|
100
|
+
import {
|
|
101
|
+
GitRepository,
|
|
102
|
+
normalizeContentPath,
|
|
103
|
+
runGit,
|
|
104
|
+
toRepositoryPath
|
|
105
|
+
} from "@riebeckite/git-history";
|
|
317
106
|
var fieldSeparator = "";
|
|
318
107
|
var recordSeparator = "";
|
|
319
108
|
var GitMarkdownHistoryReader = class {
|
|
320
|
-
#
|
|
109
|
+
#repository;
|
|
321
110
|
#historyCache = /* @__PURE__ */ new Map();
|
|
322
111
|
#markdownCache = /* @__PURE__ */ new Map();
|
|
323
112
|
/** Maps a content-relative path to `<revision hash>` → work tree path. */
|
|
324
113
|
#revisionPathCache = /* @__PURE__ */ new Map();
|
|
325
|
-
#repository;
|
|
326
114
|
constructor(options = {}) {
|
|
327
|
-
this.#
|
|
115
|
+
this.#repository = new GitRepository(options);
|
|
328
116
|
}
|
|
329
117
|
/**
|
|
330
118
|
* Revision history for one file, newest first, following renames.
|
|
@@ -382,29 +170,12 @@ var GitMarkdownHistoryReader = class {
|
|
|
382
170
|
);
|
|
383
171
|
return result.ok ? result.stdout : null;
|
|
384
172
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (!repository) return null;
|
|
388
|
-
const results = await Promise.all([
|
|
389
|
-
runGit(["rev-parse", "HEAD"], repository.repositoryRoot),
|
|
390
|
-
runGit(
|
|
391
|
-
["status", "--porcelain=v1", "--untracked-files=all"],
|
|
392
|
-
repository.repositoryRoot
|
|
393
|
-
),
|
|
394
|
-
runGit(
|
|
395
|
-
["for-each-ref", "--format=%(refname):%(objectname)"],
|
|
396
|
-
repository.repositoryRoot
|
|
397
|
-
)
|
|
398
|
-
]);
|
|
399
|
-
if (results.some((result) => !result.ok)) return null;
|
|
400
|
-
return results.filter((result) => result.ok).map((result) => result.stdout).join("");
|
|
173
|
+
getStateFingerprint() {
|
|
174
|
+
return this.#repository.getStateFingerprint();
|
|
401
175
|
}
|
|
402
176
|
/** `null` when the content directory has no readable work tree. */
|
|
403
|
-
|
|
404
|
-
this.#repository
|
|
405
|
-
const resolution = await this.#repository;
|
|
406
|
-
if (!isResolvedRepository(resolution)) return null;
|
|
407
|
-
return resolution.paths;
|
|
177
|
+
#resolvePaths() {
|
|
178
|
+
return this.#repository.paths();
|
|
408
179
|
}
|
|
409
180
|
};
|
|
410
181
|
function parseRevisionRecord(record, revisionPaths, fallbackPath) {
|
|
@@ -427,8 +198,212 @@ function getRevisionPath(nameStatusLines, fallbackPath) {
|
|
|
427
198
|
return normalizeContentPath(paths.at(-1) ?? fallbackPath);
|
|
428
199
|
}
|
|
429
200
|
|
|
201
|
+
// src/api.ts
|
|
202
|
+
function createPostDiffApi(options = {}) {
|
|
203
|
+
return createPostDiffApiFromReader(new GitMarkdownHistoryReader(options));
|
|
204
|
+
}
|
|
205
|
+
function createPostDiffApiFromReader(reader) {
|
|
206
|
+
return {
|
|
207
|
+
getHistory: (filePath) => reader.getHistory(filePath),
|
|
208
|
+
getRevisionMarkdown: (filePath, hash) => reader.getRevisionMarkdown(filePath, hash),
|
|
209
|
+
getCurrentDiff: async (filePath) => {
|
|
210
|
+
const history = await reader.getHistory(filePath);
|
|
211
|
+
const [current, previous] = history;
|
|
212
|
+
if (!current) return null;
|
|
213
|
+
const toMarkdown = await reader.getRevisionMarkdown(
|
|
214
|
+
filePath,
|
|
215
|
+
current.hash
|
|
216
|
+
);
|
|
217
|
+
if (toMarkdown === null) return null;
|
|
218
|
+
const fromMarkdown = previous ? await reader.getRevisionMarkdown(filePath, previous.hash) : null;
|
|
219
|
+
return buildPostDiff({
|
|
220
|
+
from: previous ?? null,
|
|
221
|
+
fromMarkdown,
|
|
222
|
+
to: current,
|
|
223
|
+
toMarkdown
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
compareRevisions: async (input) => {
|
|
227
|
+
const history = await reader.getHistory(input.filePath);
|
|
228
|
+
const from = history.find((revision) => revision.hash === input.fromHash);
|
|
229
|
+
const to = history.find((revision) => revision.hash === input.toHash);
|
|
230
|
+
if (!to) return null;
|
|
231
|
+
const [fromMarkdown, toMarkdown] = await Promise.all([
|
|
232
|
+
input.fromHash ? reader.getRevisionMarkdown(input.filePath, input.fromHash) : Promise.resolve(null),
|
|
233
|
+
reader.getRevisionMarkdown(input.filePath, input.toHash)
|
|
234
|
+
]);
|
|
235
|
+
if (toMarkdown === null) return null;
|
|
236
|
+
return buildPostDiff({
|
|
237
|
+
from: from ?? null,
|
|
238
|
+
fromMarkdown,
|
|
239
|
+
to,
|
|
240
|
+
toMarkdown
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function getMarkdownRevisions(api, filePath, history) {
|
|
246
|
+
const revisions = await Promise.all(
|
|
247
|
+
history.map(async (revision) => ({
|
|
248
|
+
revision,
|
|
249
|
+
markdown: await api.getRevisionMarkdown(filePath, revision.hash)
|
|
250
|
+
}))
|
|
251
|
+
);
|
|
252
|
+
return revisions.flatMap(
|
|
253
|
+
({ revision, markdown }) => markdown === null ? [] : [{ ...revision, markdown }]
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
function buildPostDiff(input) {
|
|
257
|
+
return {
|
|
258
|
+
from: input.from,
|
|
259
|
+
to: input.to,
|
|
260
|
+
lines: createLineDiff(input.fromMarkdown ?? "", input.toMarkdown)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// components/post-diff.tsx
|
|
265
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
266
|
+
var PostDiff = (({ diff: diff2, title = "History" }) => {
|
|
267
|
+
if (!diff2) return null;
|
|
268
|
+
return /* @__PURE__ */ jsxs("section", { class: "rr-diff-history", "data-rr-diff-history": true, children: [
|
|
269
|
+
/* @__PURE__ */ jsx("h2", { class: "rr-diff-history__title", children: title }),
|
|
270
|
+
/* @__PURE__ */ jsxs("p", { class: "rr-diff-history__meta", children: [
|
|
271
|
+
diff2.from ? `${diff2.from.shortHash} → ` : "",
|
|
272
|
+
diff2.to.shortHash
|
|
273
|
+
] }),
|
|
274
|
+
/* @__PURE__ */ jsx("pre", { class: "rr-diff-history__diff", children: /* @__PURE__ */ jsx("code", { children: diff2.lines.map((line, index) => /* @__PURE__ */ jsxs(
|
|
275
|
+
"span",
|
|
276
|
+
{
|
|
277
|
+
class: `rr-diff-line rr-diff-line--${line.type}`,
|
|
278
|
+
children: [
|
|
279
|
+
line.type === "added" ? "+" : line.type === "removed" ? "-" : " ",
|
|
280
|
+
line.content,
|
|
281
|
+
"\n"
|
|
282
|
+
]
|
|
283
|
+
},
|
|
284
|
+
`${line.oldLineNumber}-${line.newLineNumber}-${index}`
|
|
285
|
+
)) }) })
|
|
286
|
+
] });
|
|
287
|
+
});
|
|
288
|
+
var post_diff_default = PostDiff;
|
|
289
|
+
async function ArticlePostDiffView(props) {
|
|
290
|
+
const site = useSiteContext();
|
|
291
|
+
const article = useArticleContext();
|
|
292
|
+
if (!site || !article?.entry) return [];
|
|
293
|
+
const options = findDiffOptions(site.config.plugins);
|
|
294
|
+
if (!options || options.ui?.enabled === false) return [];
|
|
295
|
+
const filePath = site.manifest.contentIndex.get(article.entry.slug.toLowerCase()) ?? `${article.entry.slug}.md`;
|
|
296
|
+
const diff2 = await createPostDiffApi({
|
|
297
|
+
...options,
|
|
298
|
+
cwd: options.cwd ?? site.config.content.directory
|
|
299
|
+
}).getCurrentDiff(filePath);
|
|
300
|
+
return /* @__PURE__ */ jsx(PostDiff, { diff: diff2, title: props.title });
|
|
301
|
+
}
|
|
302
|
+
function findDiffOptions(plugins) {
|
|
303
|
+
return resolvePlugins([...plugins]).find((plugin) => plugin.name === "diff")?.options;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/components/diff-line.tsx
|
|
307
|
+
function renderDiffLine({ line }) {
|
|
308
|
+
const prefix = line.type === "added" ? "+" : line.type === "removed" ? "-" : " ";
|
|
309
|
+
return `<tr class="rr-diff-history__line rr-diff-history__line--${line.type}">
|
|
310
|
+
<td class="rr-diff-history__num">${line.oldLineNumber ?? ""}</td>
|
|
311
|
+
<td class="rr-diff-history__num">${line.newLineNumber ?? ""}</td>
|
|
312
|
+
<td class="rr-diff-history__code"><code>${prefix} ${escapeHtml(line.content)}</code></td>
|
|
313
|
+
</tr>`;
|
|
314
|
+
}
|
|
315
|
+
function escapeHtml(value) {
|
|
316
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/components/format-date.ts
|
|
320
|
+
var MONTHS = [
|
|
321
|
+
"Jan",
|
|
322
|
+
"Feb",
|
|
323
|
+
"Mar",
|
|
324
|
+
"Apr",
|
|
325
|
+
"May",
|
|
326
|
+
"Jun",
|
|
327
|
+
"Jul",
|
|
328
|
+
"Aug",
|
|
329
|
+
"Sep",
|
|
330
|
+
"Oct",
|
|
331
|
+
"Nov",
|
|
332
|
+
"Dec"
|
|
333
|
+
];
|
|
334
|
+
function formatDiffDate(value) {
|
|
335
|
+
const date = new Date(value);
|
|
336
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
337
|
+
return `${MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/components/diff-viewer.tsx
|
|
341
|
+
function renderDiffViewer({
|
|
342
|
+
history,
|
|
343
|
+
selected
|
|
344
|
+
}) {
|
|
345
|
+
return `<div class="rr-diff-history__viewer">
|
|
346
|
+
<div class="rr-diff-history__controls">
|
|
347
|
+
<label>From ${renderRevisionSelect("from", history, selected.from?.hash ?? "")}</label>
|
|
348
|
+
<label>To ${renderRevisionSelect("to", history, selected.to.hash)}</label>
|
|
349
|
+
</div>
|
|
350
|
+
<div data-rr-diff-panel>${renderDiffPanel(selected)}</div>
|
|
351
|
+
</div>`;
|
|
352
|
+
}
|
|
353
|
+
function renderDiffPanel(diff2) {
|
|
354
|
+
return `<div class="rr-diff-history__diff" data-rr-diff-rendered>
|
|
355
|
+
<div class="rr-diff-history__table-wrap">
|
|
356
|
+
<table class="rr-diff-history__table">
|
|
357
|
+
<tbody>${diff2.lines.map((line) => renderDiffLine({ line })).join("")}</tbody>
|
|
358
|
+
</table>
|
|
359
|
+
</div>
|
|
360
|
+
</div>`;
|
|
361
|
+
}
|
|
362
|
+
function renderRevisionSelect(kind, history, value) {
|
|
363
|
+
const emptyOption = kind === "from" ? `<option value="">Beginning</option>` : "";
|
|
364
|
+
return `<select data-rr-diff-${kind}>${emptyOption}${history.map(
|
|
365
|
+
(revision) => `<option value="${escapeHtml(revision.hash)}"${revision.hash === value ? " selected" : ""}>${escapeHtml(revision.shortHash)} · ${escapeHtml(formatDiffDate(revision.date))}</option>`
|
|
366
|
+
).join("")}</select>`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/components/diff-history.tsx
|
|
370
|
+
function renderDiffHistory(input) {
|
|
371
|
+
const history = input.revisions;
|
|
372
|
+
const selected = input.selected;
|
|
373
|
+
const payloadPath = input.payloadPath;
|
|
374
|
+
if (history.length === 0 || !selected) return "";
|
|
375
|
+
return `<section class="rr-diff-history" data-rr-diff-history>
|
|
376
|
+
<details class="rr-diff-history__panel">
|
|
377
|
+
<summary class="rr-diff-history__header">
|
|
378
|
+
<h2 class="rr-diff-history__title">History</h2>
|
|
379
|
+
<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>
|
|
380
|
+
</summary>
|
|
381
|
+
${renderContent(history, selected)}
|
|
382
|
+
</details>
|
|
383
|
+
${payloadPath ? `<script type="application/json" data-rr-diff-history-data>${JSON.stringify({ path: payloadPath }).replace(/</g, "\\u003c")}</script>` : ""}
|
|
384
|
+
</section>`;
|
|
385
|
+
}
|
|
386
|
+
function renderContent(history, selected) {
|
|
387
|
+
return `<div class="rr-diff-history__layout">
|
|
388
|
+
<ol class="rr-diff-history__list" aria-label="Commit history">
|
|
389
|
+
${history.map((revision, index) => renderRevisionButton(revision, index === 0)).join("")}
|
|
390
|
+
</ol>
|
|
391
|
+
${renderDiffViewer({ history, selected })}
|
|
392
|
+
</div>`;
|
|
393
|
+
}
|
|
394
|
+
function renderRevisionButton(revision, selected) {
|
|
395
|
+
return `<li class="rr-diff-history__item">
|
|
396
|
+
<button class="rr-diff-history__commit" type="button" data-rr-diff-select="${escapeHtml(revision.hash)}" aria-pressed="${selected}">
|
|
397
|
+
<span class="rr-diff-history__date">${escapeHtml(formatDiffDate(revision.date))}</span>
|
|
398
|
+
<span class="rr-diff-history__message">${escapeHtml(revision.message || "Untitled change")}</span>
|
|
399
|
+
<span class="rr-diff-history__meta">${escapeHtml(revision.shortHash)} · ${escapeHtml(revision.author)}</span>
|
|
400
|
+
</button>
|
|
401
|
+
</li>`;
|
|
402
|
+
}
|
|
403
|
+
|
|
430
404
|
// index.ts
|
|
431
405
|
var getPostDiff = ({ api, filePath }) => api.getCurrentDiff(filePath);
|
|
406
|
+
var manifestConcurrency = 12;
|
|
432
407
|
function diff(options = {}) {
|
|
433
408
|
const ui = { enabled: true, maxRevisions: 20, ...options.ui };
|
|
434
409
|
let api;
|
|
@@ -486,28 +461,40 @@ function diff(options = {}) {
|
|
|
486
461
|
return;
|
|
487
462
|
}
|
|
488
463
|
api ??= createPostDiffApiFromReader(snapshot.value);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
464
|
+
const postDiffApi = api;
|
|
465
|
+
const entries = context.manifest.discoverableEntries;
|
|
466
|
+
for (let index = 0; index < entries.length; index += manifestConcurrency) {
|
|
467
|
+
const outputs = await Promise.all(
|
|
468
|
+
entries.slice(index, index + manifestConcurrency).map(async (entry) => {
|
|
469
|
+
const filePath = resolvePostFilePath({
|
|
470
|
+
slug: entry.slug,
|
|
471
|
+
contentIndex: context.contentIndex
|
|
472
|
+
});
|
|
473
|
+
const history = (await postDiffApi.getHistory(filePath)).slice(
|
|
474
|
+
0,
|
|
475
|
+
ui.maxRevisions
|
|
476
|
+
);
|
|
477
|
+
const revisions = await getMarkdownRevisions(
|
|
478
|
+
postDiffApi,
|
|
479
|
+
filePath,
|
|
480
|
+
history
|
|
481
|
+
);
|
|
482
|
+
if (revisions.length === 0) return void 0;
|
|
483
|
+
const selected = await postDiffApi.getCurrentDiff(filePath);
|
|
484
|
+
if (!selected) return void 0;
|
|
485
|
+
return {
|
|
486
|
+
path: diffPayloadPath(entry.slug),
|
|
487
|
+
content: JSON.stringify({ revisions }).replace(/</g, "\\u003c"),
|
|
488
|
+
dependencies: [
|
|
489
|
+
{ type: "content", slug: entry.slug },
|
|
490
|
+
{ type: "external", id: "diff:git-history" }
|
|
491
|
+
]
|
|
492
|
+
};
|
|
493
|
+
})
|
|
497
494
|
);
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (!selected) continue;
|
|
502
|
-
const payloadPath = diffPayloadPath(entry.slug);
|
|
503
|
-
context.output.emit({
|
|
504
|
-
path: payloadPath,
|
|
505
|
-
content: JSON.stringify({ revisions }).replace(/</g, "\\u003c"),
|
|
506
|
-
dependencies: [
|
|
507
|
-
{ type: "content", slug: entry.slug },
|
|
508
|
-
{ type: "external", id: "diff:git-history" }
|
|
509
|
-
]
|
|
510
|
-
});
|
|
495
|
+
for (const output of outputs) {
|
|
496
|
+
if (output) context.output.emit(output);
|
|
497
|
+
}
|
|
511
498
|
}
|
|
512
499
|
}
|
|
513
500
|
});
|
|
@@ -515,73 +502,13 @@ function diff(options = {}) {
|
|
|
515
502
|
function diffPayloadPath(slug) {
|
|
516
503
|
return `_riebeckite/diff/${Buffer.from(slug).toString("base64url")}.json`;
|
|
517
504
|
}
|
|
518
|
-
function createPostDiffApi(options = {}) {
|
|
519
|
-
return createPostDiffApiFromReader(new GitMarkdownHistoryReader(options));
|
|
520
|
-
}
|
|
521
|
-
function createPostDiffApiFromReader(reader) {
|
|
522
|
-
return {
|
|
523
|
-
getHistory: (filePath) => reader.getHistory(filePath),
|
|
524
|
-
getRevisionMarkdown: (filePath, hash) => reader.getRevisionMarkdown(filePath, hash),
|
|
525
|
-
getCurrentDiff: async (filePath) => {
|
|
526
|
-
const history = await reader.getHistory(filePath);
|
|
527
|
-
const [current, previous] = history;
|
|
528
|
-
if (!current) return null;
|
|
529
|
-
const toMarkdown = await reader.getRevisionMarkdown(
|
|
530
|
-
filePath,
|
|
531
|
-
current.hash
|
|
532
|
-
);
|
|
533
|
-
if (toMarkdown === null) return null;
|
|
534
|
-
const fromMarkdown = previous ? await reader.getRevisionMarkdown(filePath, previous.hash) : null;
|
|
535
|
-
return buildPostDiff({
|
|
536
|
-
from: previous ?? null,
|
|
537
|
-
fromMarkdown,
|
|
538
|
-
to: current,
|
|
539
|
-
toMarkdown
|
|
540
|
-
});
|
|
541
|
-
},
|
|
542
|
-
compareRevisions: async (input) => {
|
|
543
|
-
const history = await reader.getHistory(input.filePath);
|
|
544
|
-
const from = history.find((revision) => revision.hash === input.fromHash);
|
|
545
|
-
const to = history.find((revision) => revision.hash === input.toHash);
|
|
546
|
-
if (!to) return null;
|
|
547
|
-
const [fromMarkdown, toMarkdown] = await Promise.all([
|
|
548
|
-
input.fromHash ? reader.getRevisionMarkdown(input.filePath, input.fromHash) : Promise.resolve(null),
|
|
549
|
-
reader.getRevisionMarkdown(input.filePath, input.toHash)
|
|
550
|
-
]);
|
|
551
|
-
if (toMarkdown === null) return null;
|
|
552
|
-
return buildPostDiff({
|
|
553
|
-
from: from ?? null,
|
|
554
|
-
fromMarkdown,
|
|
555
|
-
to,
|
|
556
|
-
toMarkdown
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
async function getMarkdownRevisions(api, filePath, history) {
|
|
562
|
-
const revisions = await Promise.all(
|
|
563
|
-
history.map(async (revision) => ({
|
|
564
|
-
revision,
|
|
565
|
-
markdown: await api.getRevisionMarkdown(filePath, revision.hash)
|
|
566
|
-
}))
|
|
567
|
-
);
|
|
568
|
-
return revisions.flatMap(
|
|
569
|
-
({ revision, markdown }) => markdown === null ? [] : [{ ...revision, markdown }]
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
505
|
function resolvePostFilePath(context) {
|
|
573
506
|
const indexedPath = context.contentIndex.get(context.slug.toLowerCase());
|
|
574
507
|
const contentPath = `${indexedPath ?? context.slug}.md`;
|
|
575
508
|
return contentPath;
|
|
576
509
|
}
|
|
577
|
-
function buildPostDiff(input) {
|
|
578
|
-
return {
|
|
579
|
-
from: input.from,
|
|
580
|
-
to: input.to,
|
|
581
|
-
lines: createLineDiff(input.fromMarkdown ?? "", input.toMarkdown)
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
510
|
export {
|
|
511
|
+
ArticlePostDiffView,
|
|
585
512
|
GitMarkdownHistoryReader,
|
|
586
513
|
post_diff_default as PostDiffView,
|
|
587
514
|
createLineDiff,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { GitMarkdownHistoryReader } from "./git/history_reader.js";
|
|
2
|
+
import type { DiffRevision, GitHistoryReaderOptions, MarkdownRevision, PostDiff, RevisionComparisonInput } from "./types.js";
|
|
3
|
+
export type PostDiffApi = {
|
|
4
|
+
getHistory(filePath: string): Promise<DiffRevision[]>;
|
|
5
|
+
getRevisionMarkdown(filePath: string, hash: string): Promise<string | null>;
|
|
6
|
+
getCurrentDiff(filePath: string): Promise<PostDiff | null>;
|
|
7
|
+
compareRevisions(input: RevisionComparisonInput): Promise<PostDiff | null>;
|
|
8
|
+
};
|
|
9
|
+
export declare function createPostDiffApi(options?: GitHistoryReaderOptions): PostDiffApi;
|
|
10
|
+
export declare function createPostDiffApiFromReader(reader: GitMarkdownHistoryReader): PostDiffApi;
|
|
11
|
+
export declare function getMarkdownRevisions(api: PostDiffApi, filePath: string, history: DiffRevision[]): Promise<MarkdownRevision[]>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { MarkdownRevision, PostDiff } from "../types.js";
|
|
2
|
-
|
|
2
|
+
type DiffHistoryProps = {
|
|
3
3
|
revisions: MarkdownRevision[];
|
|
4
4
|
selected: PostDiff | null;
|
|
5
5
|
payloadPath?: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function renderDiffHistory(input: DiffHistoryProps): string;
|
|
8
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PostDiff } from "../types.js";
|
|
2
|
-
|
|
2
|
+
type DiffLineProps = {
|
|
3
3
|
line: PostDiff["lines"][number];
|
|
4
4
|
};
|
|
5
5
|
export declare function renderDiffLine({ line }: DiffLineProps): string;
|
|
6
6
|
export declare function escapeHtml(value: string): string;
|
|
7
|
+
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { DiffRevision, PostDiff } from "../types.js";
|
|
2
|
-
|
|
2
|
+
type DiffViewerProps = {
|
|
3
3
|
history: DiffRevision[];
|
|
4
4
|
selected: PostDiff;
|
|
5
5
|
};
|
|
6
6
|
export declare function renderDiffViewer({ history, selected, }: DiffViewerProps): string;
|
|
7
7
|
export declare function renderDiffPanel(diff: PostDiff): string;
|
|
8
|
+
export {};
|
package/dist/src/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-diff",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,13 +27,15 @@
|
|
|
27
27
|
"./style.css": "./style.css"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"
|
|
31
|
-
"@riebeckite/
|
|
30
|
+
"@riebeckite/core": "0.0.33",
|
|
31
|
+
"@riebeckite/git-history": "0.0.33",
|
|
32
|
+
"@riebeckite/ui": "0.0.33",
|
|
33
|
+
"hono": "^4.12.25"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
36
|
+
"@riebeckite/test": "0.0.33",
|
|
34
37
|
"@types/node": "^24.5.2",
|
|
35
|
-
"tsx": "^4.22.4"
|
|
36
|
-
"@riebeckite/test": "0.0.31"
|
|
38
|
+
"tsx": "^4.22.4"
|
|
37
39
|
},
|
|
38
40
|
"publishConfig": {
|
|
39
41
|
"access": "public"
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
}>;
|
|
@@ -1,43 +0,0 @@
|
|
|
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>;
|