marksites 0.2.5 → 0.2.7
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 +6 -2
- package/dist/conversion/directory.js +15 -1
- package/dist/conversion/history.d.ts +4 -0
- package/dist/conversion/history.js +33 -0
- package/dist/conversion/rendering.d.ts +1 -1
- package/dist/conversion/rendering.js +2 -2
- package/dist/conversion/single-file.js +6 -1
- package/dist/conversion/types.d.ts +3 -0
- package/dist/features/annotations/index.d.ts +1 -0
- package/dist/features/annotations/index.js +22 -6
- package/dist/features/document-diff/index.d.ts +8 -0
- package/dist/features/document-diff/index.js +409 -0
- package/dist/features/document-view/index.d.ts +7 -0
- package/dist/features/document-view/index.js +33 -0
- package/dist/features/file-tree/index.js +7 -6
- package/dist/features/header/index.d.ts +1 -0
- package/dist/features/header/index.js +4 -1
- package/dist/features/table-of-contents/index.js +16 -6
- package/dist/features/table-resizer/index.d.ts +5 -0
- package/dist/features/table-resizer/index.js +14 -0
- package/dist/features/table-sorter/index.d.ts +5 -0
- package/dist/features/table-sorter/index.js +18 -0
- package/dist/markdown-to-html.d.ts +1 -1
- package/dist/markdown-to-html.js +20 -1
- package/dist/server/html-security.js +7 -0
- package/dist/template/document.d.ts +3 -0
- package/dist/template/document.js +7 -0
- package/dist/template/styles.d.ts +2 -2
- package/dist/template/styles.js +8 -9
- package/package.json +1 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import GithubSlugger from "github-slugger";
|
|
2
|
+
import { marked, Renderer } from "marked";
|
|
3
|
+
import { escapeHtml, plainTextFromHtml } from "../../utils/html.js";
|
|
4
|
+
function tokenRaw(token) {
|
|
5
|
+
return "raw" in token && typeof token.raw === "string" ? token.raw : "";
|
|
6
|
+
}
|
|
7
|
+
function diffBlocks(previous, current) {
|
|
8
|
+
const blocks = (markdown) => marked
|
|
9
|
+
.lexer(markdown)
|
|
10
|
+
.filter((token) => token.type !== "space")
|
|
11
|
+
.map((token) => ({
|
|
12
|
+
raw: tokenRaw(token),
|
|
13
|
+
key: tokenRaw(token).replace(/[ \t]+$/gm, "").trim(),
|
|
14
|
+
tokenType: token.type,
|
|
15
|
+
}))
|
|
16
|
+
.filter(({ raw }) => Boolean(raw));
|
|
17
|
+
const oldBlocks = blocks(previous);
|
|
18
|
+
const newBlocks = blocks(current);
|
|
19
|
+
if (oldBlocks.length * newBlocks.length > 2_000_000) {
|
|
20
|
+
return [
|
|
21
|
+
...oldBlocks.map(({ raw, tokenType }) => ({ kind: "delete", raw, tokenType })),
|
|
22
|
+
...newBlocks.map(({ raw, tokenType }) => ({ kind: "insert", raw, tokenType })),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
const lengths = Array.from({ length: oldBlocks.length + 1 }, () => new Uint32Array(newBlocks.length + 1));
|
|
26
|
+
for (let oldIndex = oldBlocks.length - 1; oldIndex >= 0; oldIndex--) {
|
|
27
|
+
for (let newIndex = newBlocks.length - 1; newIndex >= 0; newIndex--) {
|
|
28
|
+
lengths[oldIndex][newIndex] =
|
|
29
|
+
oldBlocks[oldIndex].key === newBlocks[newIndex].key
|
|
30
|
+
? lengths[oldIndex + 1][newIndex + 1] + 1
|
|
31
|
+
: Math.max(lengths[oldIndex + 1][newIndex], lengths[oldIndex][newIndex + 1]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const parts = [];
|
|
35
|
+
let oldIndex = 0;
|
|
36
|
+
let newIndex = 0;
|
|
37
|
+
while (oldIndex < oldBlocks.length || newIndex < newBlocks.length) {
|
|
38
|
+
if (oldIndex < oldBlocks.length &&
|
|
39
|
+
newIndex < newBlocks.length &&
|
|
40
|
+
oldBlocks[oldIndex].key === newBlocks[newIndex].key) {
|
|
41
|
+
parts.push({ kind: "same", raw: newBlocks[newIndex].raw, tokenType: newBlocks[newIndex].tokenType });
|
|
42
|
+
oldIndex++;
|
|
43
|
+
newIndex++;
|
|
44
|
+
}
|
|
45
|
+
else if (newIndex < newBlocks.length &&
|
|
46
|
+
(oldIndex === oldBlocks.length ||
|
|
47
|
+
lengths[oldIndex][newIndex + 1] >
|
|
48
|
+
lengths[oldIndex + 1][newIndex])) {
|
|
49
|
+
const block = newBlocks[newIndex++];
|
|
50
|
+
parts.push({ kind: "insert", raw: block.raw, tokenType: block.tokenType });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const block = oldBlocks[oldIndex++];
|
|
54
|
+
parts.push({ kind: "delete", raw: block.raw, tokenType: block.tokenType });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return parts;
|
|
58
|
+
}
|
|
59
|
+
function prefixIds(html) {
|
|
60
|
+
return html
|
|
61
|
+
.replace(/\bid="([^"]+)"/g, 'id="diff-$1"')
|
|
62
|
+
.replace(/\bhref="#([^"]+)"/g, 'href="#diff-$1"');
|
|
63
|
+
}
|
|
64
|
+
function wordDiff(previous, current) {
|
|
65
|
+
const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
|
|
66
|
+
const oldParts = [...segmenter.segment(previous)].map(({ segment }) => segment);
|
|
67
|
+
const newParts = [...segmenter.segment(current)].map(({ segment }) => segment);
|
|
68
|
+
if (oldParts.length * newParts.length > 100_000)
|
|
69
|
+
return `<del class="document-diff-inline-delete">${escapeHtml(previous)}</del><ins class="document-diff-inline-insert">${escapeHtml(current)}</ins>`;
|
|
70
|
+
const lengths = Array.from({ length: oldParts.length + 1 }, () => new Uint32Array(newParts.length + 1));
|
|
71
|
+
for (let oldIndex = oldParts.length - 1; oldIndex >= 0; oldIndex--) {
|
|
72
|
+
for (let newIndex = newParts.length - 1; newIndex >= 0; newIndex--) {
|
|
73
|
+
lengths[oldIndex][newIndex] =
|
|
74
|
+
oldParts[oldIndex] === newParts[newIndex]
|
|
75
|
+
? lengths[oldIndex + 1][newIndex + 1] + 1
|
|
76
|
+
: Math.max(lengths[oldIndex + 1][newIndex], lengths[oldIndex][newIndex + 1]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const output = [];
|
|
80
|
+
let oldIndex = 0;
|
|
81
|
+
let newIndex = 0;
|
|
82
|
+
let kind;
|
|
83
|
+
let buffer = "";
|
|
84
|
+
const flush = () => {
|
|
85
|
+
if (!buffer)
|
|
86
|
+
return;
|
|
87
|
+
output.push(kind === "delete"
|
|
88
|
+
? `<del class="document-diff-inline-delete">${buffer}</del>`
|
|
89
|
+
: kind === "insert"
|
|
90
|
+
? `<ins class="document-diff-inline-insert">${buffer}</ins>`
|
|
91
|
+
: buffer);
|
|
92
|
+
buffer = "";
|
|
93
|
+
};
|
|
94
|
+
const append = (nextKind, value) => {
|
|
95
|
+
if (kind !== nextKind)
|
|
96
|
+
flush();
|
|
97
|
+
kind = nextKind;
|
|
98
|
+
buffer += escapeHtml(value);
|
|
99
|
+
};
|
|
100
|
+
while (oldIndex < oldParts.length || newIndex < newParts.length) {
|
|
101
|
+
if (oldIndex < oldParts.length &&
|
|
102
|
+
newIndex < newParts.length &&
|
|
103
|
+
oldParts[oldIndex] === newParts[newIndex]) {
|
|
104
|
+
append("same", newParts[newIndex]);
|
|
105
|
+
oldIndex++;
|
|
106
|
+
newIndex++;
|
|
107
|
+
}
|
|
108
|
+
else if (newIndex < newParts.length &&
|
|
109
|
+
(oldIndex === oldParts.length ||
|
|
110
|
+
lengths[oldIndex][newIndex + 1] >
|
|
111
|
+
lengths[oldIndex + 1][newIndex])) {
|
|
112
|
+
append("insert", newParts[newIndex++]);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
append("delete", oldParts[oldIndex++]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
flush();
|
|
119
|
+
return output.join("");
|
|
120
|
+
}
|
|
121
|
+
function oneToken(raw) {
|
|
122
|
+
const tokens = marked.lexer(raw).filter((token) => token.type !== "space");
|
|
123
|
+
return tokens.length === 1 ? tokens[0] : undefined;
|
|
124
|
+
}
|
|
125
|
+
function inlineTokens(markdown) {
|
|
126
|
+
return marked.Lexer.lexInline(markdown);
|
|
127
|
+
}
|
|
128
|
+
function renderInlineToken(token, deleted = false) {
|
|
129
|
+
if (token.type === "link") {
|
|
130
|
+
const href = escapeHtml(String(token.href ?? ""));
|
|
131
|
+
const title = token.title
|
|
132
|
+
? ` title="${escapeHtml(String(token.title))}"`
|
|
133
|
+
: "";
|
|
134
|
+
const disabled = deleted
|
|
135
|
+
? ' class="document-diff-link-delete" aria-disabled="true" tabindex="-1"'
|
|
136
|
+
: "";
|
|
137
|
+
return `<a href="${href}"${title}${disabled}>${marked.parseInline(String(token.text ?? ""), { async: false })}</a>`;
|
|
138
|
+
}
|
|
139
|
+
return marked.parseInline(String(token.raw ?? ""), { async: false });
|
|
140
|
+
}
|
|
141
|
+
function inlineTokenDiff(previous, current) {
|
|
142
|
+
const oldTokens = inlineTokens(previous);
|
|
143
|
+
const newTokens = inlineTokens(current);
|
|
144
|
+
const output = [];
|
|
145
|
+
for (let index = 0; index < Math.max(oldTokens.length, newTokens.length); index++) {
|
|
146
|
+
const oldToken = oldTokens[index];
|
|
147
|
+
const newToken = newTokens[index];
|
|
148
|
+
if (!oldToken && newToken) {
|
|
149
|
+
output.push(`<ins class="document-diff-inline-insert">${renderInlineToken(newToken)}</ins>`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (oldToken && !newToken) {
|
|
153
|
+
output.push(`<del class="document-diff-inline-delete">${renderInlineToken(oldToken, true)}</del>`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!oldToken || !newToken)
|
|
157
|
+
continue;
|
|
158
|
+
if (oldToken.type === "text" && newToken.type === "text") {
|
|
159
|
+
output.push(wordDiff(String(oldToken.text), String(newToken.text)));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (oldToken.type === newToken.type && ["strong", "em", "del"].includes(newToken.type)) {
|
|
163
|
+
const tag = newToken.type === "strong" ? "strong" : newToken.type;
|
|
164
|
+
output.push(`<${tag}>${inlineTokenDiff(String(oldToken.text), String(newToken.text))}</${tag}>`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (oldToken.type === "link" && newToken.type === "link") {
|
|
168
|
+
const oldHref = String(oldToken.href ?? "");
|
|
169
|
+
const newHref = String(newToken.href ?? "");
|
|
170
|
+
const title = newToken.title
|
|
171
|
+
? ` title="${escapeHtml(String(newToken.title))}"`
|
|
172
|
+
: "";
|
|
173
|
+
output.push(`<a href="${escapeHtml(newHref)}"${title}>${inlineTokenDiff(String(oldToken.text ?? ""), String(newToken.text ?? ""))}</a>`);
|
|
174
|
+
if (oldHref !== newHref || oldToken.title !== newToken.title) {
|
|
175
|
+
output.push(`<span class="document-diff-link-target" aria-label="リンク先の変更"> (<del class="document-diff-inline-delete">${escapeHtml(oldHref)}</del><span aria-hidden="true"> → </span><ins class="document-diff-inline-insert">${escapeHtml(newHref)}</ins>)</span>`);
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (oldToken.raw === newToken.raw) {
|
|
180
|
+
output.push(renderInlineToken(newToken));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
output.push(`<del class="document-diff-inline-delete">${renderInlineToken(oldToken, true)}</del><ins class="document-diff-inline-insert">${renderInlineToken(newToken)}</ins>`);
|
|
184
|
+
}
|
|
185
|
+
return output.join("");
|
|
186
|
+
}
|
|
187
|
+
function paragraphDiff(previous, current) {
|
|
188
|
+
const oldToken = oneToken(previous);
|
|
189
|
+
const newToken = oneToken(current);
|
|
190
|
+
if (oldToken?.type !== "paragraph" || newToken?.type !== "paragraph")
|
|
191
|
+
return undefined;
|
|
192
|
+
return `<p>${inlineTokenDiff(String(oldToken.text), String(newToken.text))}</p>\n`;
|
|
193
|
+
}
|
|
194
|
+
function codeLineDiff(previous, current) {
|
|
195
|
+
const lengths = Array.from({ length: previous.length + 1 }, () => new Uint32Array(current.length + 1));
|
|
196
|
+
for (let oldIndex = previous.length - 1; oldIndex >= 0; oldIndex--) {
|
|
197
|
+
for (let newIndex = current.length - 1; newIndex >= 0; newIndex--) {
|
|
198
|
+
lengths[oldIndex][newIndex] =
|
|
199
|
+
previous[oldIndex] === current[newIndex]
|
|
200
|
+
? lengths[oldIndex + 1][newIndex + 1] + 1
|
|
201
|
+
: Math.max(lengths[oldIndex + 1][newIndex], lengths[oldIndex][newIndex + 1]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const rendered = [];
|
|
205
|
+
let oldIndex = 0;
|
|
206
|
+
let newIndex = 0;
|
|
207
|
+
while (oldIndex < previous.length || newIndex < current.length) {
|
|
208
|
+
if (previous[oldIndex] === current[newIndex]) {
|
|
209
|
+
rendered.push(`<span>${escapeHtml(current[newIndex] ?? "")}</span>`);
|
|
210
|
+
oldIndex++;
|
|
211
|
+
newIndex++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const deleted = [];
|
|
215
|
+
const inserted = [];
|
|
216
|
+
while (oldIndex < previous.length ||
|
|
217
|
+
newIndex < current.length) {
|
|
218
|
+
if (previous[oldIndex] === current[newIndex])
|
|
219
|
+
break;
|
|
220
|
+
if (newIndex < current.length &&
|
|
221
|
+
(oldIndex === previous.length ||
|
|
222
|
+
lengths[oldIndex][newIndex + 1] >
|
|
223
|
+
lengths[oldIndex + 1][newIndex])) {
|
|
224
|
+
inserted.push(current[newIndex++]);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
deleted.push(previous[oldIndex++]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (deleted.length === 1 && inserted.length > 1) {
|
|
231
|
+
const source = new Set([...new Intl.Segmenter("ja", { granularity: "word" }).segment(deleted[0])].map(({ segment }) => segment));
|
|
232
|
+
let bestIndex = 0;
|
|
233
|
+
let bestScore = -1;
|
|
234
|
+
inserted.forEach((line, index) => {
|
|
235
|
+
const score = [...new Intl.Segmenter("ja", { granularity: "word" }).segment(line)]
|
|
236
|
+
.filter(({ segment }) => source.has(segment)).length;
|
|
237
|
+
if (score > bestScore) {
|
|
238
|
+
bestIndex = index;
|
|
239
|
+
bestScore = score;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
inserted.forEach((line, index) => {
|
|
243
|
+
rendered.push(index === bestIndex
|
|
244
|
+
? `<span>${wordDiff(deleted[0], line)}</span>`
|
|
245
|
+
: `<span class="document-diff-structural-insert">${escapeHtml(line)}</span>`);
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
for (let index = 0; index < Math.max(deleted.length, inserted.length); index++) {
|
|
250
|
+
const oldLine = deleted[index];
|
|
251
|
+
const newLine = inserted[index];
|
|
252
|
+
if (oldLine !== undefined && newLine !== undefined)
|
|
253
|
+
rendered.push(`<span>${wordDiff(oldLine, newLine)}</span>`);
|
|
254
|
+
else if (oldLine !== undefined)
|
|
255
|
+
rendered.push(`<span class="document-diff-structural-delete">${escapeHtml(oldLine)}</span>`);
|
|
256
|
+
else
|
|
257
|
+
rendered.push(`<span class="document-diff-structural-insert">${escapeHtml(newLine ?? "")}</span>`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return rendered;
|
|
261
|
+
}
|
|
262
|
+
function structuredDiff(previous, current, slugger) {
|
|
263
|
+
if (previous.tokenType !== current.tokenType)
|
|
264
|
+
return undefined;
|
|
265
|
+
const oldToken = oneToken(previous.raw);
|
|
266
|
+
const newToken = oneToken(current.raw);
|
|
267
|
+
if (!oldToken || !newToken)
|
|
268
|
+
return undefined;
|
|
269
|
+
if (newToken.type === "paragraph")
|
|
270
|
+
return paragraphDiff(previous.raw, current.raw);
|
|
271
|
+
if (newToken.type === "heading") {
|
|
272
|
+
const depth = Number(newToken.depth);
|
|
273
|
+
const oldText = String(oldToken.text ?? "");
|
|
274
|
+
const newText = String(newToken.text ?? "");
|
|
275
|
+
const id = slugger.slug(plainTextFromHtml(marked.parseInline(newText, { async: false })));
|
|
276
|
+
return `<h${depth} id="${escapeHtml(id)}">${inlineTokenDiff(oldText, newText)}</h${depth}>\n`;
|
|
277
|
+
}
|
|
278
|
+
if (newToken.type === "list") {
|
|
279
|
+
const oldItems = oldToken.items;
|
|
280
|
+
const newItems = newToken.items;
|
|
281
|
+
if (oldToken.ordered !== newToken.ordered ||
|
|
282
|
+
[...oldItems, ...newItems].some((item) => item.tokens.some((token) => token.type === "list")))
|
|
283
|
+
return undefined;
|
|
284
|
+
const tag = newToken.ordered ? "ol" : "ul";
|
|
285
|
+
const items = [];
|
|
286
|
+
for (let index = 0; index < Math.max(oldItems.length, newItems.length); index++) {
|
|
287
|
+
const oldItem = oldItems[index];
|
|
288
|
+
const newItem = newItems[index];
|
|
289
|
+
if (oldItem && newItem)
|
|
290
|
+
items.push(`<li>${inlineTokenDiff(String(oldItem.text), String(newItem.text))}</li>`);
|
|
291
|
+
else if (oldItem)
|
|
292
|
+
items.push(`<li class="document-diff-structural-delete">${escapeHtml(String(oldItem.text))}</li>`);
|
|
293
|
+
else if (newItem)
|
|
294
|
+
items.push(`<li class="document-diff-structural-insert">${escapeHtml(String(newItem.text))}</li>`);
|
|
295
|
+
}
|
|
296
|
+
return `<${tag}>\n${items.join("\n")}\n</${tag}>\n`;
|
|
297
|
+
}
|
|
298
|
+
if (newToken.type === "blockquote") {
|
|
299
|
+
const oldParagraphs = oldToken.tokens.filter((token) => token.type === "paragraph");
|
|
300
|
+
const newParagraphs = newToken.tokens.filter((token) => token.type === "paragraph");
|
|
301
|
+
const paragraphs = [];
|
|
302
|
+
for (let index = 0; index < Math.max(oldParagraphs.length, newParagraphs.length); index++) {
|
|
303
|
+
const oldParagraph = oldParagraphs[index];
|
|
304
|
+
const newParagraph = newParagraphs[index];
|
|
305
|
+
if (oldParagraph && newParagraph)
|
|
306
|
+
paragraphs.push(`<p>${inlineTokenDiff(String(oldParagraph.text), String(newParagraph.text))}</p>`);
|
|
307
|
+
else if (oldParagraph)
|
|
308
|
+
paragraphs.push(`<p class="document-diff-structural-delete">${escapeHtml(String(oldParagraph.text))}</p>`);
|
|
309
|
+
else if (newParagraph)
|
|
310
|
+
paragraphs.push(`<p class="document-diff-structural-insert">${escapeHtml(String(newParagraph.text))}</p>`);
|
|
311
|
+
}
|
|
312
|
+
return `<blockquote>\n${paragraphs.join("\n")}\n</blockquote>\n`;
|
|
313
|
+
}
|
|
314
|
+
if (newToken.type === "table") {
|
|
315
|
+
const oldHeader = oldToken.header;
|
|
316
|
+
const newHeader = newToken.header;
|
|
317
|
+
const oldRows = oldToken.rows;
|
|
318
|
+
const newRows = newToken.rows;
|
|
319
|
+
const cells = (oldCells = [], newCells = [], tag = "td") => Array.from({ length: Math.max(oldCells.length, newCells.length) }, (_, index) => {
|
|
320
|
+
const oldCell = oldCells[index], newCell = newCells[index];
|
|
321
|
+
const oldText = String(oldCell?.text ?? "");
|
|
322
|
+
const newText = String(newCell?.text ?? "");
|
|
323
|
+
const kind = oldCell && !newCell ? "delete" : newCell && !oldCell ? "insert" : "";
|
|
324
|
+
const content = oldCell && newCell
|
|
325
|
+
? oldText === newText
|
|
326
|
+
? marked.parseInline(newText, { async: false })
|
|
327
|
+
: inlineTokenDiff(oldText, newText)
|
|
328
|
+
: marked.parseInline(newText || oldText, { async: false });
|
|
329
|
+
return `<${tag}${kind ? ` class="document-diff-structural-${kind}"` : ""}>${content}</${tag}>`;
|
|
330
|
+
}).join("");
|
|
331
|
+
const rows = [];
|
|
332
|
+
for (let index = 0; index < Math.max(oldRows.length, newRows.length); index++) {
|
|
333
|
+
const oldRow = oldRows[index], newRow = newRows[index];
|
|
334
|
+
const kind = oldRow && !newRow ? "delete" : newRow && !oldRow ? "insert" : "";
|
|
335
|
+
rows.push(`<tr${kind ? ` class="document-diff-structural-${kind}"` : ""}>${cells(oldRow, newRow)}</tr>`);
|
|
336
|
+
}
|
|
337
|
+
return `<table>\n<thead><tr>${cells(oldHeader, newHeader, "th")}</tr></thead>\n<tbody>${rows.join("\n")}</tbody>\n</table>\n`;
|
|
338
|
+
}
|
|
339
|
+
if (newToken.type === "code") {
|
|
340
|
+
const oldLines = String(oldToken.text ?? "").split("\n");
|
|
341
|
+
const newLines = String(newToken.text ?? "").split("\n");
|
|
342
|
+
const language = escapeHtml(String(newToken.lang ?? ""));
|
|
343
|
+
return `<pre class="document-diff-code"><code${language ? ` class="language-${language}"` : ""}>${codeLineDiff(oldLines, newLines).join("\n")}</code></pre>\n`;
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
export function createDocumentDiffFeature(current, previous, markedOptions = {}) {
|
|
348
|
+
const parts = previous === undefined ? [] : diffBlocks(previous, current);
|
|
349
|
+
const hasChanges = parts.some((part) => part.kind !== "same");
|
|
350
|
+
const slugger = new GithubSlugger();
|
|
351
|
+
const diffRenderer = new Renderer();
|
|
352
|
+
diffRenderer.heading = ({ tokens, depth }) => {
|
|
353
|
+
const rendered = diffRenderer.parser.parseInline(tokens);
|
|
354
|
+
const id = slugger.slug(plainTextFromHtml(rendered));
|
|
355
|
+
return `<h${depth} id="${escapeHtml(id)}">${rendered}</h${depth}>\n`;
|
|
356
|
+
};
|
|
357
|
+
const renderedParts = [];
|
|
358
|
+
const renderPart = (part) => {
|
|
359
|
+
const html = marked.parse(part.raw, {
|
|
360
|
+
...markedOptions,
|
|
361
|
+
renderer: diffRenderer,
|
|
362
|
+
async: false,
|
|
363
|
+
});
|
|
364
|
+
return part.kind === "same"
|
|
365
|
+
? html
|
|
366
|
+
: `<div class="document-diff-block document-diff-${part.kind}">${html}</div>\n`;
|
|
367
|
+
};
|
|
368
|
+
if (hasChanges) {
|
|
369
|
+
for (let index = 0; index < parts.length; index++) {
|
|
370
|
+
const part = parts[index];
|
|
371
|
+
if (part.kind !== "same") {
|
|
372
|
+
let end = index;
|
|
373
|
+
while (end < parts.length && parts[end].kind !== "same")
|
|
374
|
+
end++;
|
|
375
|
+
const changed = parts.slice(index, end);
|
|
376
|
+
const deleted = changed.filter((item) => item.kind === "delete");
|
|
377
|
+
const inserted = changed.filter((item) => item.kind === "insert");
|
|
378
|
+
for (let changedIndex = 0; changedIndex < Math.max(deleted.length, inserted.length); changedIndex++) {
|
|
379
|
+
const oldPart = deleted[changedIndex];
|
|
380
|
+
const newPart = inserted[changedIndex];
|
|
381
|
+
if (oldPart && newPart) {
|
|
382
|
+
const structured = structuredDiff(oldPart, newPart, slugger);
|
|
383
|
+
if (structured !== undefined) {
|
|
384
|
+
renderedParts.push(structured);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (oldPart)
|
|
389
|
+
renderedParts.push(renderPart(oldPart));
|
|
390
|
+
if (newPart)
|
|
391
|
+
renderedParts.push(renderPart(newPart));
|
|
392
|
+
}
|
|
393
|
+
index = end - 1;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
renderedParts.push(renderPart(part));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const content = hasChanges ? prefixIds(renderedParts.join("")) : "";
|
|
400
|
+
const disabled = hasChanges ? "" : " disabled";
|
|
401
|
+
const label = hasChanges ? "差分を表示" : "前回からの変更はありません";
|
|
402
|
+
const control = `<button type="button" class="site-header-action document-diff-toggle" data-document-diff-toggle aria-label="${label}" title="${label}" aria-pressed="false"${disabled}><svg data-document-diff-icon viewBox="0 0 16 16" aria-hidden="true"><circle cx="4" cy="3" r="1.5"/><circle cx="4" cy="13" r="1.5"/><circle cx="12" cy="5" r="1.5"/><path d="M4 4.5v7M5.5 4h2A4.5 4.5 0 0112 8.5V10"/></svg><svg data-document-current-icon viewBox="0 0 16 16" aria-hidden="true" hidden><path d="M3 1.75h6l4 4v8.5H3z"/><path d="M9 1.75v4h4M5.5 9h5M5.5 11.5h5"/></svg></button>`;
|
|
403
|
+
const styles = `
|
|
404
|
+
body.markdown-body[data-theme="dark"]{--diff-insert-bg:#58a6ff1a;--diff-delete-bg:#ff7b7226;--diff-delete-fg:#ff938a}body.markdown-body[data-theme="light"]{--diff-insert-bg:#0969da12;--diff-delete-bg:#cf222e18;--diff-delete-fg:#b4232c}
|
|
405
|
+
.document-diff-structural-insert{background:var(--diff-insert-bg,#0969da12);box-shadow:inset 3px 0 var(--fgColor-accent,#0969da)}.document-diff-structural-delete{color:var(--diff-delete-fg,#b4232c);background:var(--diff-delete-bg,#cf222e18);text-decoration:line-through;text-decoration-thickness:2px}.document-diff-code code>span{display:block;min-height:1.5em;white-space:pre-wrap}.document-diff-link-target{margin-inline-start:2px;color:var(--fgColor-muted,#59636e);font-size:.875em;overflow-wrap:anywhere}.document-diff-link-delete{pointer-events:none}
|
|
406
|
+
.document-diff-content{box-sizing:border-box;min-width:0;margin-bottom:72px;padding:clamp(28px,3vw,52px);color:var(--fgColor-default,#1f2328);background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-muted,#d8dee4);border-radius:8px;box-shadow:0 1px 2px rgba(31,35,40,.04)}.document-diff-content[hidden]{display:none}.document-diff-block{position:relative;margin:0 -12px;padding:1px 12px 1px 22px;border-left:2px solid var(--fgColor-muted,#59636e)}.document-diff-block::before{position:absolute;top:3px;left:7px;font-weight:700;line-height:1;content:""}.document-diff-block.document-diff-insert{background:var(--diff-insert-bg,#0969da12);border-left-style:solid;border-left-color:var(--fgColor-accent,#0969da)}.document-diff-block.document-diff-insert::before{color:var(--fgColor-accent,#0969da);content:"+"}.document-diff-block.document-diff-delete{color:var(--diff-delete-fg,#b4232c);background:var(--diff-delete-bg,#cf222e18);border-left-color:var(--diff-delete-fg,#b4232c);border-left-style:dashed;opacity:.9}.document-diff-block.document-diff-delete::before{color:var(--diff-delete-fg,#b4232c);content:"−"}.document-diff-block.document-diff-delete :is(a,button,input,select,textarea){pointer-events:none}.document-diff-inline-insert,.document-diff-inline-delete{padding:1px 2px;border-radius:2px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.document-diff-inline-insert{color:var(--fgColor-default,#1f2328);background:var(--diff-insert-bg,#0969da12);text-decoration-line:underline;text-decoration-style:double;text-decoration-color:var(--fgColor-accent,#0969da);text-underline-offset:3px}.document-diff-inline-delete{color:var(--diff-delete-fg,#b4232c);background:var(--diff-delete-bg,#cf222e18);text-decoration:line-through;text-decoration-thickness:2px}.document-diff-toggle[aria-pressed="true"]{color:var(--fgColor-accent,#0969da);background:var(--bgColor-accent-muted,#ddf4ff)}.document-diff-toggle:disabled{color:var(--fgColor-muted,#59636e);opacity:.45;cursor:not-allowed}.document-diff-toggle svg[hidden]{display:none}@media(max-width:900px){.document-diff-content{margin-bottom:32px}}@media(max-width:600px){.document-diff-content{padding:24px 20px;border-radius:6px}}
|
|
407
|
+
`;
|
|
408
|
+
return { content, control, styles, hasChanges };
|
|
409
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { escapeHtml } from "../../utils/html.js";
|
|
2
|
+
function renderSourceLines(markdown) {
|
|
3
|
+
const lines = markdown.split("\n");
|
|
4
|
+
if (lines.length > 1 && lines.at(-1) === "")
|
|
5
|
+
lines.pop();
|
|
6
|
+
return lines
|
|
7
|
+
.map((line) => `<span class="markdown-source-line">${escapeHtml(line.replace(/\r$/, ""))}</span>`)
|
|
8
|
+
.join("");
|
|
9
|
+
}
|
|
10
|
+
export function createDocumentViewFeature(markdown, hasDiff) {
|
|
11
|
+
const control = `<button type="button" class="document-content-action document-preview-toggle" data-document-preview-toggle aria-label="Previewを表示" title="Previewを表示" aria-pressed="true"><span>Preview</span></button>
|
|
12
|
+
<button type="button" class="document-content-action document-source-toggle" data-document-source-toggle aria-label="コードを表示" title="コードを表示" aria-pressed="false"><span>コード</span></button>`;
|
|
13
|
+
const content = `<main class="markdown-source-content" aria-label="Markdown原文" hidden><pre><code>${renderSourceLines(markdown)}</code></pre></main>`;
|
|
14
|
+
const styles = `
|
|
15
|
+
.document-content{position:relative;grid-area:content;min-width:0}.document-content-actions{display:flex;min-height:44px;align-items:center;justify-content:flex-start;gap:4px;margin-bottom:12px;padding:0 4px;border-bottom:1px solid var(--borderColor-muted,#d8dee4)}.document-content-action{position:relative;display:inline-flex;height:36px;align-items:center;justify-content:center;gap:6px;padding:0 10px;color:var(--fgColor-muted,#59636e);font:inherit;font-size:.75rem;font-weight:600;background:transparent;border:0;border-radius:6px 6px 0 0;cursor:pointer}.document-content-action:hover:not(:disabled){color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.document-content-action:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:-2px}.document-content-action>span{line-height:16px}.document-content-action[aria-pressed="true"]{color:var(--fgColor-default,#1f2328)}.document-content-action[aria-pressed="true"]::after{position:absolute;right:8px;bottom:-5px;left:8px;height:2px;background:var(--borderColor-accent-emphasis,#0969da);content:""}.document-content-action:disabled{color:var(--fgColor-muted,#59636e);opacity:.45;cursor:not-allowed}
|
|
16
|
+
.markdown-source-content{box-sizing:border-box;min-width:0;margin-bottom:72px;padding:8px 0 24px;color:var(--fgColor-default,#1f2328);background:transparent;border:0;border-radius:0;box-shadow:none}.markdown-source-content[hidden]{display:none}.markdown-source-content pre{margin:0;padding:8px 0;overflow:auto;color:var(--codeBlock-fgColor,#24292f);background:transparent;border:0;border-radius:0;counter-reset:markdown-source-line}.markdown-source-content code{display:block;min-width:max-content;padding:0;color:inherit;background:transparent;white-space:pre;font:12px/1.5 ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace}.markdown-source-line{position:relative;display:block;min-height:1.5em;padding:0 20px 0 64px}.markdown-source-line::before{position:absolute;top:0;bottom:0;left:0;box-sizing:border-box;width:48px;padding-right:12px;color:var(--fgColor-muted,#59636e);border-right:1px solid var(--borderColor-muted,#d8dee4);content:counter(markdown-source-line);counter-increment:markdown-source-line;text-align:right;user-select:none}
|
|
17
|
+
@media(max-width:900px){.markdown-source-content{margin-bottom:32px}}@media(max-width:600px){.document-content-actions{overflow-x:auto}.markdown-source-line{padding-right:12px;padding-left:52px}.markdown-source-line::before{width:40px;padding-right:9px}}
|
|
18
|
+
.document-content{box-sizing:border-box;margin-bottom:72px;background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-muted,#d8dee4);border-radius:8px;box-shadow:0 1px 2px rgba(31,35,40,.04);overflow:hidden}.document-content-actions{margin-bottom:0}.document-content>.markdown-content,.document-content>.document-diff-content{margin:0;border:0;border-radius:0;box-shadow:none}.document-content>.markdown-source-content{margin:0;padding-top:20px}@media(max-width:900px){.document-content{margin-bottom:32px}.document-content>.markdown-source-content{margin-bottom:0}}@media(max-width:600px){.document-content{border-radius:6px}.document-content>.markdown-content,.document-content>.document-diff-content{border-radius:0}}
|
|
19
|
+
.document-content-action{border-radius:6px}.document-replacement-menu{margin-left:auto}.document-content>.markdown-source-content{padding-top:0}.markdown-source-content pre{padding-top:0}.markdown-source-line::before{position:sticky;top:auto;bottom:auto;left:0;display:inline-block;height:1.5em;margin-left:-64px;margin-right:16px;background:var(--bgColor-default,#fff);vertical-align:top}@media(max-width:600px){.markdown-source-line::before{margin-left:-52px;margin-right:12px}}
|
|
20
|
+
.markdown-source-content pre::before{position:sticky;left:47px;display:block;width:1px;height:12px;background:var(--borderColor-muted,#d8dee4);content:""}@media(max-width:600px){.markdown-source-content pre::before{left:39px}}
|
|
21
|
+
`;
|
|
22
|
+
const script = `<script>(()=>{
|
|
23
|
+
const parameter='document-view',previewButton=document.querySelector('[data-document-preview-toggle]'),sourceButton=document.querySelector('[data-document-source-toggle]'),replacementButton=document.querySelector('[data-replacement-menu]'),diffButton=document.querySelector('[data-document-diff-toggle]'),current=document.querySelector('.markdown-content'),source=document.querySelector('.markdown-source-content'),diff=document.querySelector('.document-diff-content'),available=${hasDiff},pageUrl=new URL(location.href);
|
|
24
|
+
let intent=['markdown','diff'].includes(pageUrl.searchParams.get(parameter))?pageUrl.searchParams.get(parameter):'current';
|
|
25
|
+
const update=url=>{url.searchParams.delete(parameter);if(intent==='markdown'||intent==='diff')url.searchParams.set(parameter,intent);return url};
|
|
26
|
+
function syncLinks(){for(const link of document.querySelectorAll('a[href]')){const raw=link.getAttribute('href');if(!raw||raw.startsWith('#'))continue;const url=new URL(raw,location.href);if(url.protocol===location.protocol&&url.host===location.host&&url.pathname.endsWith('.html'))link.href=update(url).href}}
|
|
27
|
+
function syncLabels(){const english=document.body.dataset.language==='en',showDiff=document.body.dataset.documentView==='diff',previewLabel=english?'Show preview':'Previewを表示',sourceLabel=english?'Show code':'コードを表示';previewButton.setAttribute('aria-label',previewLabel);previewButton.title=previewLabel;sourceButton.setAttribute('aria-label',sourceLabel);sourceButton.title=sourceLabel;const diffLabel=available?(showDiff?(english?'Show latest':'最新版を表示'):(english?'Show changes':'差分を表示')):(english?'No changes since the previous build':'前回からの変更はありません');diffButton.setAttribute('aria-label',diffLabel);diffButton.title=diffLabel}
|
|
28
|
+
function apply(view,write=true){intent=view;const actual=view==='markdown'?'markdown':view==='diff'&&available?'diff':'current',showCurrent=actual==='current',showSource=actual==='markdown',showDiff=actual==='diff';document.body.dataset.documentView=actual;current.hidden=!showCurrent;source.hidden=!showSource;diff.hidden=!showDiff;previewButton.setAttribute('aria-pressed',String(showCurrent));sourceButton.setAttribute('aria-pressed',String(showSource));if(replacementButton)replacementButton.disabled=!showCurrent;diffButton.setAttribute('aria-pressed',String(showDiff));diffButton.querySelector('[data-document-diff-icon]').hidden=showDiff;diffButton.querySelector('[data-document-current-icon]').hidden=!showDiff;syncLabels();if(write)history.replaceState(null,'',update(new URL(location.href)));syncLinks()}
|
|
29
|
+
window.marksitesSyncDocumentViewLabels=syncLabels;
|
|
30
|
+
previewButton.addEventListener('click',()=>apply('current'));sourceButton.addEventListener('click',()=>apply('markdown'));if(available)diffButton.addEventListener('click',()=>apply(document.body.dataset.documentView==='diff'?'current':'diff'));for(const link of document.querySelectorAll('.table-of-contents a[href^="#"]'))link.addEventListener('click',()=>{if(document.body.dataset.documentView==='markdown')apply('current')});apply(intent,available||intent!=='diff');
|
|
31
|
+
})()</script>`;
|
|
32
|
+
return { control, content, styles, script };
|
|
33
|
+
}
|
|
@@ -130,8 +130,7 @@ function renderRecentFiles(nodes) {
|
|
|
130
130
|
? `<span class="file-tree-comment-count" aria-label="コメント${file.commentCount}件">${file.commentCount}</span>`
|
|
131
131
|
: "";
|
|
132
132
|
const time = file.modifiedAt.slice(11, 16);
|
|
133
|
-
|
|
134
|
-
output.push(` <li class="file-tree-recent-file${grouped ? " is-grouped" : ""}" data-file-path="${escapeHtml(file.path)}" data-directory="${escapeHtml(directory)}" data-modified-at="${file.modifiedAt}"${grouped ? ` data-recent-group="${groupId}"` : ""}><a href="${escapeHtml(file.href)}"${current}><span class="file-tree-recent-label"><span class="file-tree-name"><span class="file-tree-name-text">${escapeHtml(file.name)}</span></span></span>${count}<span class="file-tree-directory-tooltip" aria-hidden="true"><span class="file-tree-directory-tooltip-path">${renderFolderIcon()}<span>${escapeHtml(directoryLabel)}</span></span><time datetime="${file.modifiedAt}">${tooltipDate}</time></span></a></li>`);
|
|
133
|
+
output.push(` <li class="file-tree-recent-file${grouped ? " is-grouped" : ""}" data-file-path="${escapeHtml(file.path)}" data-directory="${escapeHtml(directory)}" data-modified-at="${file.modifiedAt}"${grouped ? ` data-recent-group="${groupId}"` : ""}><a href="${escapeHtml(file.href)}"${current}><time class="file-tree-recent-time" datetime="${file.modifiedAt}">${time}</time><span class="file-tree-recent-label"><span class="file-tree-name"><span class="file-tree-name-text">${escapeHtml(file.name)}</span></span></span>${count}<span class="file-tree-directory-tooltip" aria-hidden="true"><span class="file-tree-directory-tooltip-path">${renderFolderIcon()}<span>${escapeHtml(directoryLabel)}</span></span></span></a></li>`);
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
136
|
return output.join("\n");
|
|
@@ -206,9 +205,11 @@ export function renderFileTreeScript(enabled) {
|
|
|
206
205
|
const positionDirectoryTooltip = (link) => {
|
|
207
206
|
const name = link.querySelector('.file-tree-name-text');
|
|
208
207
|
const tooltip = link.querySelector('.file-tree-directory-tooltip');
|
|
209
|
-
|
|
208
|
+
const recent = link.closest('.file-tree-recent');
|
|
209
|
+
if (!name || !tooltip || !recent) return;
|
|
210
210
|
const nameRect = name.getBoundingClientRect();
|
|
211
|
-
|
|
211
|
+
const recentRect = recent.getBoundingClientRect();
|
|
212
|
+
tooltip.style.left = Math.min(recentRect.right + 8, innerWidth - tooltip.offsetWidth - 8) + 'px';
|
|
212
213
|
tooltip.style.top = Math.max(8 + tooltip.offsetHeight / 2, Math.min(nameRect.top + nameRect.height / 2, innerHeight - 8 - tooltip.offsetHeight / 2)) + 'px';
|
|
213
214
|
};
|
|
214
215
|
|
|
@@ -366,8 +367,8 @@ export function renderFileTreeScript(enabled) {
|
|
|
366
367
|
const file = files[index];
|
|
367
368
|
const date = new Date(file.dataset.modifiedAt);
|
|
368
369
|
file.dataset.localDate = dateKey;
|
|
369
|
-
const
|
|
370
|
-
|
|
370
|
+
const rowTime = file.querySelector('.file-tree-recent-time');
|
|
371
|
+
rowTime.textContent = pad(date.getHours())+':'+pad(date.getMinutes());
|
|
371
372
|
if (grouped) {
|
|
372
373
|
file.classList.add('is-grouped');
|
|
373
374
|
file.dataset.recentGroup = groupId;
|
|
@@ -2,6 +2,7 @@ export function createHeaderFeature(options = {
|
|
|
2
2
|
documentNavigation: "",
|
|
3
3
|
documentMetadata: "",
|
|
4
4
|
fileTree: "",
|
|
5
|
+
documentDiffControl: "",
|
|
5
6
|
}) {
|
|
6
7
|
const markup = `<header class="site-header">
|
|
7
8
|
<div class="site-header-brand">
|
|
@@ -10,6 +11,7 @@ export function createHeaderFeature(options = {
|
|
|
10
11
|
${options.documentNavigation}${options.documentMetadata ? ` <span class="site-header-metadata-separator" aria-hidden="true"></span>\n <div class="document-metadata">${options.documentMetadata}</div>\n` : ""}${options.fileTree} </div>
|
|
11
12
|
</div>
|
|
12
13
|
<div class="site-header-actions">
|
|
14
|
+
${options.documentDiffControl}
|
|
13
15
|
<button type="button" class="site-header-action" data-theme-toggle aria-label="ダークモードに切り替え" title="ダークモードに切り替え"><svg data-theme-dark-icon viewBox="0 0 16 16" aria-hidden="true"><path d="M13.5 10.2A5.8 5.8 0 015.8 2.5 5.8 5.8 0 1013.5 10.2z" /></svg><svg data-theme-light-icon viewBox="0 0 16 16" aria-hidden="true" hidden><circle cx="8" cy="8" r="2.5"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3.05 3.05l1.06 1.06M11.89 11.89l1.06 1.06M12.95 3.05l-1.06 1.06M4.11 11.89l-1.06 1.06"/></svg></button>
|
|
14
16
|
<button type="button" class="site-header-action language-toggle" data-language-toggle aria-label="英語に切り替え" title="英語に切り替え"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M2 8h12M8 2a9 9 0 010 12M8 2a9 9 0 000 12"/></svg><span data-language-label>JA</span></button>
|
|
15
17
|
</div>
|
|
@@ -37,13 +39,14 @@ const english=new Map(${JSON.stringify([
|
|
|
37
39
|
["アーカイブを開く", "Open archive"], ["アーカイブを閉じる", "Close archive"], ["コメントを保存", "Save comment"], ["編集をキャンセル", "Cancel editing"],
|
|
38
40
|
["有効なコメントをすべてコピー", "Copy all active comments"], ["有効なコメントだけをコピー", "Copy active comments only"], ["有効なコメントをコピー", "Copy active comments"], ["コメントをコピーしました", "Comment copied"], ["コピー失敗", "Copy failed"], ["コピーに失敗しました", "Copy failed"], ["保存中…", "Saving…"], ["保存に失敗しました", "Save failed"], ["このコメントを削除しますか?", "Delete this comment?"],
|
|
39
41
|
["コメントを追加するにはmarksites serveを起動してください", "Start marksites serve to add comments"], ["別の画面でコメントが更新されました。最新の内容を確認して、もう一度操作してください。", "Comments changed in another window. Review the latest version and try again."],
|
|
42
|
+
["差分を表示", "Show changes"], ["最新版を表示", "Show latest"], ["前回からの変更はありません", "No changes since the previous build"], ["Preview", "Preview"], ["コード", "Code"], ["Previewを表示", "Show preview"], ["コードを表示", "Show code"], ["文書表示と操作", "Document view and actions"],
|
|
40
43
|
["コードをコピー", "Copy code"], ["長い行を折り返す", "Wrap long lines"], ["折り返しを解除", "Disable line wrapping"], ["コードをコピーしました", "Code copied"], ["コードをコピーできませんでした", "Could not copy code"],
|
|
41
44
|
["文書ナビゲーション", "Document navigation"], ["文書サイドバー", "Document sidebar"], ["パンくずリスト", "Breadcrumbs"]
|
|
42
45
|
])});
|
|
43
46
|
const skipText=node=>{const parent=node.parentElement;if(!parent)return true;if(parent.closest('.file-tree-name,.file-tree summary span,.file-breadcrumbs ol,.table-of-contents a,.annotation-card>p,.annotation-quote,.annotation-source,.code-language'))return true;return Boolean(parent.closest('.markdown-content')&&!parent.closest('.code-toolbar'))};
|
|
44
47
|
const translate=value=>{if(english.has(value))return english.get(value);if(value.startsWith('更新 '))return'Updated '+value.slice(3);if(/^(\d{4})年(\d{1,2})月(\d{1,2})日$/.test(value))return value.replace(/^(\d{4})年(\d{1,2})月(\d{1,2})日$/,'$1-$2-$3');if(/^コメント\d+件$/.test(value))return value.replace(/^コメント(\d+)件$/,'$1 comments');if(/^ファイル\d+件$/.test(value))return value.replace(/^ファイル(\d+)件$/,'$1 files');return value};
|
|
45
48
|
function applyNode(node,language){if(node.nodeType===Node.TEXT_NODE){if(skipText(node))return;if(!textOriginal.has(node))textOriginal.set(node,node.data);const original=textOriginal.get(node),trimmed=original.trim(),next=language==='en'?original.replace(trimmed,translate(trimmed)):original;if(node.data!==next)node.data=next;return}if(node.nodeType!==Node.ELEMENT_NODE)return;const element=node,attributes=['aria-label','title','placeholder'];let originals=attributeOriginal.get(element);if(!originals){originals=new Map();attributeOriginal.set(element,originals)}for(const name of attributes){if(!element.hasAttribute(name)&&!originals.has(name))continue;if(!originals.has(name))originals.set(name,element.getAttribute(name));const original=originals.get(name),next=language==='en'?translate(original):original;if(element.getAttribute(name)!==next)element.setAttribute(name,next)}for(const child of element.childNodes)applyNode(child,language)}
|
|
46
|
-
function applyLanguage(language){document.documentElement.lang=language;document.body.dataset.language=language;for(const root of document.querySelectorAll('.site-header,.file-sidebar,.file-navigation,.document-sidebar,.selection-actions,.code-toolbar'))applyNode(root,language);const button=document.querySelector('[data-language-toggle]'),label=document.querySelector('[data-language-label]'),next=language==='ja'?'en':'ja';label.textContent=language==='ja'?'JA':'EN';button.setAttribute('aria-label',next==='en'?'英語に切り替え':'Switch to Japanese');button.title=button.getAttribute('aria-label')}
|
|
49
|
+
function applyLanguage(language){document.documentElement.lang=language;document.body.dataset.language=language;for(const root of document.querySelectorAll('.site-header,.file-sidebar,.file-navigation,.document-sidebar,.document-content-actions,.selection-actions,.code-toolbar'))applyNode(root,language);const button=document.querySelector('[data-language-toggle]'),label=document.querySelector('[data-language-label]'),next=language==='ja'?'en':'ja';label.textContent=language==='ja'?'JA':'EN';button.setAttribute('aria-label',next==='en'?'英語に切り替え':'Switch to Japanese');button.title=button.getAttribute('aria-label');window.marksitesSyncDocumentViewLabels?.()}
|
|
47
50
|
function syncLinks(){const language=document.body.dataset.language,theme=document.body.dataset.theme;const update=url=>{url.searchParams.delete(languageParameter);if(language==='en')url.searchParams.set(languageParameter,'en');url.searchParams.set(themeParameter,theme);return url};history.replaceState(null,'',update(new URL(location.href)));for(const link of document.querySelectorAll('a[href]')){const raw=link.getAttribute('href');if(!raw||raw.startsWith('#'))continue;const url=new URL(raw,location.href);if(url.protocol===location.protocol&&url.host===location.host&&url.pathname.endsWith('.html'))link.href=update(url).href}}
|
|
48
51
|
function applyTheme(theme){document.documentElement.dataset.theme=theme;document.body.dataset.theme=theme;const dark=theme==='dark',button=document.querySelector('[data-theme-toggle]');button.querySelector('[data-theme-dark-icon]').hidden=dark;button.querySelector('[data-theme-light-icon]').hidden=!dark;const label=dark?(document.body.dataset.language==='en'?'Switch to light mode':'ライトモードに切り替え'):(document.body.dataset.language==='en'?'Switch to dark mode':'ダークモードに切り替え');button.setAttribute('aria-label',label);button.title=label}
|
|
49
52
|
const initialLanguage=pageUrl.searchParams.get(languageParameter)==='en'?'en':'ja',initialTheme=['dark','light'].includes(pageUrl.searchParams.get(themeParameter))?pageUrl.searchParams.get(themeParameter):(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');applyLanguage(initialLanguage);applyTheme(initialTheme);syncLinks();
|
|
@@ -24,25 +24,27 @@ function renderTableOfContentsScript() {
|
|
|
24
24
|
|
|
25
25
|
const panel = navigation.querySelector('.toc-panel');
|
|
26
26
|
const links = [...panel.querySelectorAll('a[href^="#"]')];
|
|
27
|
-
const entries = links
|
|
28
|
-
.map((link) => ({ link, heading: document.getElementById(link.getAttribute('href').slice(1)) }))
|
|
29
|
-
.filter((entry) => entry.heading);
|
|
27
|
+
const entries = links.map((link) => ({ link, id: link.getAttribute('href').slice(1) }));
|
|
30
28
|
if (entries.length === 0) return;
|
|
31
29
|
|
|
30
|
+
const headingFor = (entry) => document.getElementById((document.body.dataset.documentView === 'diff' ? 'diff-' : '') + entry.id);
|
|
31
|
+
|
|
32
32
|
let scheduled = false;
|
|
33
33
|
let currentLink = null;
|
|
34
34
|
const update = () => {
|
|
35
35
|
scheduled = false;
|
|
36
36
|
const marker = Math.min(160, window.innerHeight * 0.25);
|
|
37
|
-
|
|
37
|
+
const available = entries.map(entry => ({ ...entry, heading: headingFor(entry) })).filter(entry => entry.heading);
|
|
38
|
+
if (available.length === 0) return;
|
|
39
|
+
let active = available[0];
|
|
38
40
|
|
|
39
|
-
for (const entry of
|
|
41
|
+
for (const entry of available) {
|
|
40
42
|
if (entry.heading.getBoundingClientRect().top > marker) break;
|
|
41
43
|
active = entry;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
for (const entry of entries) {
|
|
45
|
-
if (entry === active) entry.link.setAttribute('aria-current', 'location');
|
|
47
|
+
if (entry.link === active.link) entry.link.setAttribute('aria-current', 'location');
|
|
46
48
|
else entry.link.removeAttribute('aria-current');
|
|
47
49
|
}
|
|
48
50
|
if (!navigation.hidden && active.link !== currentLink) {
|
|
@@ -63,6 +65,14 @@ function renderTableOfContentsScript() {
|
|
|
63
65
|
|
|
64
66
|
addEventListener('scroll', schedule, { passive: true });
|
|
65
67
|
addEventListener('resize', schedule);
|
|
68
|
+
for (const entry of entries) entry.link.addEventListener('click', event => {
|
|
69
|
+
const heading = headingFor(entry);
|
|
70
|
+
if (!heading || document.body.dataset.documentView !== 'diff') return;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
heading.scrollIntoView();
|
|
73
|
+
history.replaceState(null, '', '#diff-' + entry.id);
|
|
74
|
+
});
|
|
75
|
+
new MutationObserver(schedule).observe(document.body, { attributes: true, attributeFilter: ['data-document-view'] });
|
|
66
76
|
update();
|
|
67
77
|
})();
|
|
68
78
|
</script>`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createTableResizerFeature(enabled) {
|
|
2
|
+
if (!enabled)
|
|
3
|
+
return { styles: "", script: "" };
|
|
4
|
+
const styles = `
|
|
5
|
+
.table-resizable-container { position: relative; max-width: 100%; margin-bottom: var(--base-size-16); overflow-x: auto; }
|
|
6
|
+
.markdown-content .table-resizable-container table.is-column-resizable { display: table; table-layout: fixed; max-width: none; margin-bottom: 0; overflow: visible; }
|
|
7
|
+
.table-column-resizer { position: absolute; z-index: 2; top: 0; width: 10px; padding: 0; background: transparent; border: 0; transform: translateX(-5px); cursor: col-resize; touch-action: none; user-select: none; }
|
|
8
|
+
.table-column-resizer::after { position: absolute; top: 0; bottom: 0; left: 4px; width: 2px; background: var(--borderColor-accent-emphasis, #0969da); content: ""; opacity: 0; }
|
|
9
|
+
.table-column-resizer:hover::after, .table-column-resizer:focus-visible::after, .table-column-resizer.is-resizing::after { opacity: 1; }
|
|
10
|
+
.table-column-resizer:focus-visible { outline: 2px solid var(--focus-outlineColor, #0969da); outline-offset: -2px; }
|
|
11
|
+
body.is-resizing-table-column { cursor: col-resize; user-select: none; }`;
|
|
12
|
+
const script = `<script>(()=>{const minimum=48;for(const table of document.querySelectorAll('.markdown-content table')){const headers=table.tHead?.rows[0]?.cells;if(!headers?.length||Array.from(headers).some(cell=>cell.colSpan!==1)||table.querySelector(':scope > colgroup'))continue;const widths=Array.from(headers,cell=>cell.getBoundingClientRect().width);const container=document.createElement('div');container.className='table-resizable-container';table.before(container);container.append(table);const group=document.createElement('colgroup');for(const width of widths){const column=document.createElement('col');column.style.width=width+'px';group.append(column)}table.prepend(group);table.classList.add('is-column-resizable');const handles=[];const layout=()=>{table.style.width=widths.reduce((sum,width)=>sum+width,0)+'px';let offset=0;handles.forEach((handle,index)=>{offset+=widths[index];handle.style.left=offset+'px';handle.style.height=table.offsetHeight+'px'})};Array.from(headers).forEach((cell,index)=>{const handle=document.createElement('button');handle.type='button';handle.className='table-column-resizer';handle.setAttribute('aria-label',(cell.textContent?.trim()||String(index+1))+'列の幅を変更');handle.setAttribute('aria-orientation','vertical');handles.push(handle);container.append(handle);let drag=null;const resize=width=>{widths[index]=Math.max(minimum,width);group.children[index].style.width=widths[index]+'px';layout()};handle.addEventListener('pointerdown',event=>{if(event.button!==0)return;event.preventDefault();event.stopPropagation();drag={id:event.pointerId,x:event.clientX,width:widths[index]};handle.setPointerCapture(event.pointerId);handle.classList.add('is-resizing');document.body.classList.add('is-resizing-table-column')});handle.addEventListener('click',event=>event.stopPropagation());handle.addEventListener('pointermove',event=>{if(!drag||drag.id!==event.pointerId)return;resize(drag.width+event.clientX-drag.x)});const finish=event=>{if(!drag||drag.id!==event.pointerId)return;drag=null;handle.classList.remove('is-resizing');document.body.classList.remove('is-resizing-table-column')};handle.addEventListener('pointerup',finish);handle.addEventListener('pointercancel',finish);handle.addEventListener('keydown',event=>{if(event.key!=='ArrowLeft'&&event.key!=='ArrowRight')return;event.preventDefault();resize(widths[index]+(event.key==='ArrowLeft'?-10:10))})});layout()}})();</script>`;
|
|
13
|
+
return { styles, script };
|
|
14
|
+
}
|