marksites 0.1.0 → 0.2.2
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 +18 -5
- package/dist/cli.js +64 -6
- package/dist/conversion/assets.d.ts +8 -0
- package/dist/conversion/assets.js +62 -0
- package/dist/conversion/directory.d.ts +2 -2
- package/dist/conversion/directory.js +35 -6
- package/dist/conversion/navigation.js +2 -0
- package/dist/conversion/paths.d.ts +1 -0
- package/dist/conversion/paths.js +23 -0
- package/dist/conversion/rendering.d.ts +1 -1
- package/dist/conversion/rendering.js +4 -2
- package/dist/conversion/single-file.js +5 -0
- package/dist/conversion/types.d.ts +8 -0
- package/dist/conversion/watch.d.ts +7 -0
- package/dist/conversion/watch.js +69 -0
- package/dist/features/file-tree/index.d.ts +1 -1
- package/dist/features/file-tree/index.js +297 -43
- package/dist/features/header/index.d.ts +7 -1
- package/dist/features/header/index.js +17 -9
- package/dist/features/image-viewer/index.d.ts +6 -0
- package/dist/features/image-viewer/index.js +28 -0
- package/dist/features/sidebar/index.js +8 -6
- package/dist/features/table-of-contents/index.js +9 -4
- package/dist/markdown-to-html.js +19 -10
- package/dist/server/html-security.js +2 -0
- package/dist/server/static-files.js +5 -0
- package/dist/template/document.d.ts +0 -2
- package/dist/template/document.js +3 -5
- package/dist/template/styles.d.ts +2 -2
- package/dist/template/styles.js +30 -3
- package/dist/types.d.ts +4 -0
- package/dist/utils/files.d.ts +1 -1
- package/package.json +6 -6
|
@@ -61,6 +61,80 @@ function countComments(nodes) {
|
|
|
61
61
|
? node.commentCount
|
|
62
62
|
: 0), 0);
|
|
63
63
|
}
|
|
64
|
+
function collectRecentFiles(nodes) {
|
|
65
|
+
const files = [];
|
|
66
|
+
const collect = (items, parentPath = "") => {
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
if (item.type === "directory") {
|
|
69
|
+
collect(item.children, parentPath ? `${parentPath}/${item.name}` : item.name);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!item.modifiedAt)
|
|
73
|
+
continue;
|
|
74
|
+
const date = new Date(item.modifiedAt);
|
|
75
|
+
if (Number.isNaN(date.getTime()))
|
|
76
|
+
throw new Error(`Invalid file tree modifiedAt timestamp: ${item.modifiedAt}`);
|
|
77
|
+
files.push({
|
|
78
|
+
name: item.name,
|
|
79
|
+
path: item.path ?? (parentPath ? `${parentPath}/${item.name}` : item.name),
|
|
80
|
+
href: item.href,
|
|
81
|
+
current: item.current === true,
|
|
82
|
+
modifiedAt: date.toISOString(),
|
|
83
|
+
timestamp: date.getTime(),
|
|
84
|
+
commentCount: item.commentCount,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
collect(nodes);
|
|
89
|
+
return files.sort((left, right) => right.timestamp - left.timestamp ||
|
|
90
|
+
left.path.localeCompare(right.path, "en"));
|
|
91
|
+
}
|
|
92
|
+
function renderRecentFiles(nodes) {
|
|
93
|
+
const files = collectRecentFiles(nodes);
|
|
94
|
+
if (files.length === 0)
|
|
95
|
+
return ` <li class="file-tree-recent-empty">更新日時のあるファイルはありません</li>`;
|
|
96
|
+
let currentDate = "";
|
|
97
|
+
let groupNumber = 0;
|
|
98
|
+
const output = [];
|
|
99
|
+
for (let index = 0; index < files.length;) {
|
|
100
|
+
const first = files[index];
|
|
101
|
+
const date = first.modifiedAt.slice(0, 10);
|
|
102
|
+
if (date !== currentDate) {
|
|
103
|
+
const dateCount = files.filter((file) => file.modifiedAt.startsWith(date)).length;
|
|
104
|
+
output.push(` <li class="file-tree-date" data-date="${date}"><button type="button" data-recent-date-toggle aria-expanded="true"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg><time datetime="${date}">${Number(date.slice(0, 4))}年${Number(date.slice(5, 7))}月${Number(date.slice(8, 10))}日</time><span aria-label="ファイル${dateCount}件">${dateCount}</span></button></li>`);
|
|
105
|
+
currentDate = date;
|
|
106
|
+
}
|
|
107
|
+
const separator = first.path.lastIndexOf("/");
|
|
108
|
+
const directory = separator === -1 ? "" : first.path.slice(0, separator + 1);
|
|
109
|
+
let end = index + 1;
|
|
110
|
+
while (end < files.length) {
|
|
111
|
+
const candidate = files[end];
|
|
112
|
+
const candidateSeparator = candidate.path.lastIndexOf("/");
|
|
113
|
+
const candidateDirectory = candidateSeparator === -1
|
|
114
|
+
? ""
|
|
115
|
+
: candidate.path.slice(0, candidateSeparator + 1);
|
|
116
|
+
if (candidate.modifiedAt.slice(0, 10) !== date ||
|
|
117
|
+
candidateDirectory !== directory)
|
|
118
|
+
break;
|
|
119
|
+
end++;
|
|
120
|
+
}
|
|
121
|
+
const grouped = end - index >= 2;
|
|
122
|
+
const groupId = grouped ? `recent-${groupNumber++}` : "";
|
|
123
|
+
const directoryLabel = directory ? `/${directory}` : "/";
|
|
124
|
+
if (grouped)
|
|
125
|
+
output.push(` <li class="file-tree-directory-group" data-recent-group="${groupId}" title="${escapeHtml(directoryLabel)}">${renderFolderIcon()}<span>${escapeHtml(directoryLabel)}</span></li>`);
|
|
126
|
+
for (; index < end; index++) {
|
|
127
|
+
const file = files[index];
|
|
128
|
+
const current = file.current ? ' aria-current="page"' : "";
|
|
129
|
+
const count = Number.isSafeInteger(file.commentCount) && file.commentCount > 0
|
|
130
|
+
? `<span class="file-tree-comment-count" aria-label="コメント${file.commentCount}件">${file.commentCount}</span>`
|
|
131
|
+
: "";
|
|
132
|
+
const time = file.modifiedAt.slice(11, 16);
|
|
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)}" title="${escapeHtml(file.path)}"${current}><time datetime="${file.modifiedAt}">${time}</time><span class="file-tree-recent-label"><span class="file-tree-name">${escapeHtml(file.name)}</span><span class="file-tree-directory-path">${renderFolderIcon()}<span>${escapeHtml(directoryLabel)}</span></span></span>${count}</a></li>`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return output.join("\n");
|
|
137
|
+
}
|
|
64
138
|
export function renderFileTree(options) {
|
|
65
139
|
if (!options || options.items.length === 0)
|
|
66
140
|
return "";
|
|
@@ -71,13 +145,20 @@ ${contents}</nav>
|
|
|
71
145
|
`;
|
|
72
146
|
}
|
|
73
147
|
function renderTreeContents(items, folderIds) {
|
|
74
|
-
return ` <div class="file-tree-
|
|
148
|
+
return ` <div class="file-tree-view-tabs" role="tablist" aria-label="ファイル表示">
|
|
149
|
+
<button type="button" role="tab" data-file-tree-view="tree" aria-selected="true">ツリー</button>
|
|
150
|
+
<button type="button" role="tab" data-file-tree-view="recent" aria-selected="false" tabindex="-1">更新順</button>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="file-tree-filter">
|
|
75
153
|
<input type="search" class="file-tree-filter-input" placeholder="ファイルを検索" aria-label="ファイル名で検索" autocomplete="off">
|
|
76
154
|
<p class="file-tree-filter-empty" hidden>一致するファイルはありません</p>
|
|
77
155
|
</div>
|
|
78
|
-
<ul class="file-tree-root">
|
|
156
|
+
<ul class="file-tree-root" data-file-tree-panel="tree" role="tabpanel">
|
|
79
157
|
${renderNodes(items, folderIds)}
|
|
80
158
|
</ul>
|
|
159
|
+
<ul class="file-tree-recent" data-file-tree-panel="recent" role="tabpanel" hidden>
|
|
160
|
+
${renderRecentFiles(items)}
|
|
161
|
+
</ul>
|
|
81
162
|
`;
|
|
82
163
|
}
|
|
83
164
|
export function renderFileSidebar(options) {
|
|
@@ -85,17 +166,12 @@ export function renderFileSidebar(options) {
|
|
|
85
166
|
return "";
|
|
86
167
|
const folderIds = createFolderIds(options.items);
|
|
87
168
|
const title = escapeHtml(options.title ?? "ファイル");
|
|
169
|
+
const contents = renderTreeContents(options.items, folderIds);
|
|
88
170
|
return `<button type="button" class="file-sidebar-close" data-file-sidebar-close aria-label="ファイルサイドバーを閉じる" title="ファイルサイドバーを閉じる"><svg class="file-sidebar-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.5" /><path d="M6 2.5v11M10.5 5.5L8 8l2.5 2.5" /></svg></button>
|
|
89
171
|
<aside class="file-sidebar" id="file-sidebar" aria-label="${title}">
|
|
90
172
|
<div class="file-sidebar-header"><span>${title}</span></div>
|
|
91
173
|
<nav class="file-tree file-tree-sidebar" aria-label="${title}">
|
|
92
|
-
|
|
93
|
-
<input type="search" class="file-tree-filter-input" placeholder="ファイルを検索" aria-label="ファイル名で検索" autocomplete="off">
|
|
94
|
-
<p class="file-tree-filter-empty" hidden>一致するファイルはありません</p>
|
|
95
|
-
</div>
|
|
96
|
-
<ul class="file-tree-root">
|
|
97
|
-
${renderNodes(options.items, folderIds)}
|
|
98
|
-
</ul>
|
|
174
|
+
${contents}
|
|
99
175
|
</nav>
|
|
100
176
|
</aside>
|
|
101
177
|
`;
|
|
@@ -117,10 +193,12 @@ export function renderFileTreeScript(enabled) {
|
|
|
117
193
|
const stateParameter = 'open';
|
|
118
194
|
const popoverParameter = 'marksites-files';
|
|
119
195
|
const sidebarParameter = 'file-sidebar';
|
|
196
|
+
const viewParameter = 'file-view';
|
|
120
197
|
const pageUrl = new URL(location.href);
|
|
121
198
|
const openPaths = new Set(pageUrl.searchParams.getAll(stateParameter));
|
|
122
199
|
const compact = matchMedia('(max-width: 900px)');
|
|
123
200
|
let sidebarPreferenceOpen = pageUrl.searchParams.get(sidebarParameter) !== 'closed';
|
|
201
|
+
let activeView = pageUrl.searchParams.get(viewParameter) === 'recent' ? 'recent' : 'tree';
|
|
124
202
|
const ignoredToggles = new WeakSet();
|
|
125
203
|
const initialOpenState = new Map();
|
|
126
204
|
|
|
@@ -141,6 +219,8 @@ export function renderFileTreeScript(enabled) {
|
|
|
141
219
|
if (!popover.hidden) url.searchParams.set(popoverParameter, 'open');
|
|
142
220
|
url.searchParams.delete(sidebarParameter);
|
|
143
221
|
if (!sidebarPreferenceOpen) url.searchParams.set(sidebarParameter, 'closed');
|
|
222
|
+
url.searchParams.delete(viewParameter);
|
|
223
|
+
if (activeView === 'recent') url.searchParams.set(viewParameter, 'recent');
|
|
144
224
|
return url;
|
|
145
225
|
};
|
|
146
226
|
|
|
@@ -182,11 +262,14 @@ export function renderFileTreeScript(enabled) {
|
|
|
182
262
|
};
|
|
183
263
|
|
|
184
264
|
const applySidebarState = (open) => {
|
|
265
|
+
if (open && !popover.hidden) setPopoverOpen(false, false, false);
|
|
185
266
|
sidebar.hidden = !open;
|
|
186
267
|
sidebarCloseButton.hidden = !open;
|
|
187
268
|
sidebarOpenButton.hidden = open;
|
|
188
269
|
sidebarOpenButton.setAttribute('aria-expanded', String(open));
|
|
189
270
|
document.body.classList.toggle('file-sidebar-collapsed', !open);
|
|
271
|
+
popoverToggle.disabled = open;
|
|
272
|
+
popoverToggle.setAttribute('aria-disabled', String(open));
|
|
190
273
|
};
|
|
191
274
|
|
|
192
275
|
const setSidebarOpen = (open, sync = true) => {
|
|
@@ -197,8 +280,162 @@ export function renderFileTreeScript(enabled) {
|
|
|
197
280
|
else sidebarOpenButton.focus();
|
|
198
281
|
};
|
|
199
282
|
|
|
200
|
-
|
|
283
|
+
const openRecentDates = new Set();
|
|
284
|
+
|
|
285
|
+
const rebuildRecentGroups = (tree) => {
|
|
286
|
+
const list = tree.querySelector('.file-tree-recent');
|
|
287
|
+
if (!list) return;
|
|
288
|
+
const files = [...list.querySelectorAll('.file-tree-recent-file')];
|
|
289
|
+
for (const heading of list.querySelectorAll('.file-tree-date,.file-tree-directory-group')) heading.remove();
|
|
290
|
+
for (const file of files) {
|
|
291
|
+
file.classList.remove('is-grouped');
|
|
292
|
+
delete file.dataset.recentGroup;
|
|
293
|
+
}
|
|
294
|
+
const pad = value => String(value).padStart(2, '0');
|
|
295
|
+
files.sort((left, right) => {
|
|
296
|
+
const dateDifference = new Date(right.dataset.modifiedAt) - new Date(left.dataset.modifiedAt);
|
|
297
|
+
return dateDifference || left.dataset.filePath.localeCompare(right.dataset.filePath, 'en');
|
|
298
|
+
});
|
|
299
|
+
let previousDate = '',groupNumber = 0;
|
|
300
|
+
for (let index = 0; index < files.length;) {
|
|
301
|
+
const first = files[index];
|
|
302
|
+
const firstDate = new Date(first.dataset.modifiedAt);
|
|
303
|
+
const dateKey = firstDate.getFullYear()+'-'+pad(firstDate.getMonth()+1)+'-'+pad(firstDate.getDate());
|
|
304
|
+
let end = index+1;
|
|
305
|
+
while (end < files.length) {
|
|
306
|
+
const candidate = files[end],candidateDate = new Date(candidate.dataset.modifiedAt);
|
|
307
|
+
const candidateDateKey = candidateDate.getFullYear()+'-'+pad(candidateDate.getMonth()+1)+'-'+pad(candidateDate.getDate());
|
|
308
|
+
if (candidateDateKey !== dateKey || candidate.dataset.directory !== first.dataset.directory) break;
|
|
309
|
+
end++;
|
|
310
|
+
}
|
|
311
|
+
const grouped = end-index >= 2;
|
|
312
|
+
const groupId = grouped ? 'recent-'+groupNumber++ : '';
|
|
313
|
+
if (dateKey !== previousDate) {
|
|
314
|
+
const heading = document.createElement('li');
|
|
315
|
+
heading.className = 'file-tree-date';
|
|
316
|
+
heading.dataset.date = dateKey;
|
|
317
|
+
const button = document.createElement('button');
|
|
318
|
+
button.type = 'button';
|
|
319
|
+
button.dataset.recentDateToggle = '';
|
|
320
|
+
button.setAttribute('aria-expanded', 'true');
|
|
321
|
+
button.innerHTML = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>';
|
|
322
|
+
const label = document.createElement('time');
|
|
323
|
+
label.dateTime = dateKey;
|
|
324
|
+
label.textContent = firstDate.getFullYear()+'年'+(firstDate.getMonth()+1)+'月'+firstDate.getDate()+'日';
|
|
325
|
+
const count = document.createElement('span');
|
|
326
|
+
const dateCount = files.filter(file => {
|
|
327
|
+
const value = new Date(file.dataset.modifiedAt);
|
|
328
|
+
return value.getFullYear()+'-'+pad(value.getMonth()+1)+'-'+pad(value.getDate()) === dateKey;
|
|
329
|
+
}).length;
|
|
330
|
+
count.textContent = String(dateCount);
|
|
331
|
+
count.setAttribute('aria-label', 'ファイル'+dateCount+'件');
|
|
332
|
+
button.append(label,count);
|
|
333
|
+
heading.append(button);
|
|
334
|
+
list.append(heading);
|
|
335
|
+
previousDate = dateKey;
|
|
336
|
+
}
|
|
337
|
+
if (grouped) {
|
|
338
|
+
const group = document.createElement('li');
|
|
339
|
+
group.className = 'file-tree-directory-group';
|
|
340
|
+
group.dataset.recentGroup = groupId;
|
|
341
|
+
group.dataset.localDate = dateKey;
|
|
342
|
+
const directoryLabel = first.dataset.directory ? '/'+first.dataset.directory : '/';
|
|
343
|
+
group.title = directoryLabel;
|
|
344
|
+
group.innerHTML = ${JSON.stringify(renderFolderIcon())};
|
|
345
|
+
const name = document.createElement('span');
|
|
346
|
+
name.textContent = directoryLabel;
|
|
347
|
+
group.append(name);
|
|
348
|
+
list.append(group);
|
|
349
|
+
}
|
|
350
|
+
for (; index < end; index++) {
|
|
351
|
+
const file = files[index];
|
|
352
|
+
const date = new Date(file.dataset.modifiedAt);
|
|
353
|
+
file.dataset.localDate = dateKey;
|
|
354
|
+
const time = file.querySelector('time');
|
|
355
|
+
time.textContent = pad(date.getHours())+':'+pad(date.getMinutes());
|
|
356
|
+
if (grouped) {
|
|
357
|
+
file.classList.add('is-grouped');
|
|
358
|
+
file.dataset.recentGroup = groupId;
|
|
359
|
+
}
|
|
360
|
+
list.append(file);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
for (const tree of trees) rebuildRecentGroups(tree);
|
|
366
|
+
|
|
367
|
+
const firstRecentTree = trees[0];
|
|
368
|
+
const firstRecentFiles = [...firstRecentTree.querySelectorAll('.file-tree-recent-file')];
|
|
369
|
+
const latestRecentDate = firstRecentFiles[0]?.dataset.localDate;
|
|
370
|
+
if (latestRecentDate) openRecentDates.add(latestRecentDate);
|
|
371
|
+
for (const file of firstRecentFiles) {
|
|
372
|
+
if (file.querySelector('a[aria-current="page"]')) openRecentDates.add(file.dataset.localDate);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const applyRecentDateState = (searching = false) => {
|
|
376
|
+
for (const tree of trees) {
|
|
377
|
+
const files = [...tree.querySelectorAll('.file-tree-recent-file')];
|
|
378
|
+
for (const heading of tree.querySelectorAll('.file-tree-date')) {
|
|
379
|
+
const expanded = searching
|
|
380
|
+
? files.some(file => file.dataset.localDate === heading.dataset.date && !file.hidden)
|
|
381
|
+
: openRecentDates.has(heading.dataset.date);
|
|
382
|
+
heading.querySelector('button').setAttribute('aria-expanded', String(expanded));
|
|
383
|
+
for (const item of tree.querySelectorAll('.file-tree-recent-file,.file-tree-directory-group')) {
|
|
384
|
+
if (item.dataset.localDate === heading.dataset.date)
|
|
385
|
+
item.classList.toggle('is-date-collapsed', !expanded);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
applyRecentDateState();
|
|
392
|
+
for (const tree of trees) {
|
|
393
|
+
for (const button of tree.querySelectorAll('[data-recent-date-toggle]')) {
|
|
394
|
+
button.addEventListener('click', () => {
|
|
395
|
+
if (tree.querySelector('.file-tree-filter-input').value.trim() !== '') return;
|
|
396
|
+
const date = button.closest('.file-tree-date').dataset.date;
|
|
397
|
+
if (openRecentDates.has(date)) openRecentDates.delete(date);
|
|
398
|
+
else openRecentDates.add(date);
|
|
399
|
+
applyRecentDateState();
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const applyView = (view, sync = true) => {
|
|
405
|
+
activeView = view;
|
|
406
|
+
for (const tree of trees) {
|
|
407
|
+
for (const button of tree.querySelectorAll('[data-file-tree-view]')) {
|
|
408
|
+
const selected = button.dataset.fileTreeView === view;
|
|
409
|
+
button.setAttribute('aria-selected', String(selected));
|
|
410
|
+
button.tabIndex = selected ? 0 : -1;
|
|
411
|
+
}
|
|
412
|
+
for (const panel of tree.querySelectorAll('[data-file-tree-panel]'))
|
|
413
|
+
panel.hidden = panel.dataset.fileTreePanel !== view;
|
|
414
|
+
const selector = view === 'tree' ? '.file-tree-file' : '.file-tree-recent-file';
|
|
415
|
+
const items = [...tree.querySelectorAll(selector)];
|
|
416
|
+
tree.querySelector('.file-tree-filter-empty').hidden = items.length === 0 || items.some(item => !item.hidden);
|
|
417
|
+
}
|
|
418
|
+
if (sync) syncState();
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
for (const tree of trees) {
|
|
422
|
+
const buttons = [...tree.querySelectorAll('[data-file-tree-view]')];
|
|
423
|
+
for (const button of buttons) {
|
|
424
|
+
button.addEventListener('click', () => applyView(button.dataset.fileTreeView));
|
|
425
|
+
button.addEventListener('keydown', (event) => {
|
|
426
|
+
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
const direction = event.key === 'ArrowRight' ? 1 : -1;
|
|
429
|
+
const next = buttons[(buttons.indexOf(button)+direction+buttons.length)%buttons.length];
|
|
430
|
+
applyView(next.dataset.fileTreeView);
|
|
431
|
+
next.focus();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
201
436
|
applySidebarState(sidebarPreferenceOpen && !compact.matches);
|
|
437
|
+
setPopoverOpen(pageUrl.searchParams.get(popoverParameter) === 'open' && sidebar.hidden, false, false);
|
|
438
|
+
applyView(activeView, false);
|
|
202
439
|
syncState();
|
|
203
440
|
|
|
204
441
|
popoverToggle?.addEventListener('click', (event) => {
|
|
@@ -208,7 +445,7 @@ export function renderFileTreeScript(enabled) {
|
|
|
208
445
|
sidebarOpenButton?.addEventListener('click', () => setSidebarOpen(true));
|
|
209
446
|
sidebarCloseButton?.addEventListener('click', () => setSidebarOpen(false));
|
|
210
447
|
document.addEventListener('pointerdown', (event) => {
|
|
211
|
-
if (popover.hidden || event.target.closest('.
|
|
448
|
+
if (popover.hidden || event.target.closest('.site-header-document-meta,.file-sidebar')) return;
|
|
212
449
|
setPopoverOpen(false);
|
|
213
450
|
});
|
|
214
451
|
document.addEventListener('keydown', (event) => {
|
|
@@ -253,50 +490,68 @@ export function renderFileTreeScript(enabled) {
|
|
|
253
490
|
}, 1000);
|
|
254
491
|
});
|
|
255
492
|
|
|
256
|
-
|
|
257
|
-
const input = tree.querySelector('.file-tree-filter-input');
|
|
493
|
+
const filterTree = (tree, query) => {
|
|
258
494
|
const empty = tree.querySelector('.file-tree-filter-empty');
|
|
259
495
|
const root = tree.querySelector('.file-tree-root');
|
|
260
496
|
const treeDirectories = [...root.querySelectorAll('.file-tree-directory')];
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
ignoredToggles.add(details);
|
|
278
|
-
details.open = open;
|
|
279
|
-
}
|
|
497
|
+
const files = [...root.querySelectorAll('.file-tree-file')];
|
|
498
|
+
let treeMatches = 0;
|
|
499
|
+
for (const file of files) {
|
|
500
|
+
const name = file.querySelector('a').dataset.fileName.toLocaleLowerCase();
|
|
501
|
+
const visible = query === '' || name.includes(query);
|
|
502
|
+
file.hidden = !visible;
|
|
503
|
+
if (visible) treeMatches += 1;
|
|
504
|
+
}
|
|
505
|
+
for (const directory of [...treeDirectories].reverse()) {
|
|
506
|
+
const details = directory.querySelector(':scope > details');
|
|
507
|
+
const hasVisibleFile = [...details.querySelectorAll('.file-tree-file')].some((file) => !file.hidden);
|
|
508
|
+
directory.hidden = !hasVisibleFile;
|
|
509
|
+
const open = query === '' ? initialOpenState.get(details.dataset.folderId) : hasVisibleFile;
|
|
510
|
+
if (details.open !== open) {
|
|
511
|
+
ignoredToggles.add(details);
|
|
512
|
+
details.open = open;
|
|
280
513
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
514
|
+
}
|
|
515
|
+
const recentFiles = [...tree.querySelectorAll('.file-tree-recent-file')];
|
|
516
|
+
let recentMatches = 0;
|
|
517
|
+
for (const file of recentFiles) {
|
|
518
|
+
const visible = query === '' || file.dataset.filePath.toLocaleLowerCase().includes(query);
|
|
519
|
+
file.hidden = !visible;
|
|
520
|
+
if (visible) recentMatches += 1;
|
|
521
|
+
}
|
|
522
|
+
for (const heading of tree.querySelectorAll('.file-tree-date'))
|
|
523
|
+
heading.hidden = !recentFiles.some(file => file.dataset.localDate === heading.dataset.date && !file.hidden);
|
|
524
|
+
for (const heading of tree.querySelectorAll('.file-tree-directory-group'))
|
|
525
|
+
heading.hidden = !recentFiles.some(file => file.dataset.recentGroup === heading.dataset.recentGroup && !file.hidden);
|
|
526
|
+
const matches = activeView === 'tree' ? treeMatches : recentMatches;
|
|
527
|
+
empty.hidden = query === '' || matches !== 0;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const applyFilter = (value) => {
|
|
531
|
+
const query = value.trim().toLocaleLowerCase();
|
|
532
|
+
for (const tree of trees) {
|
|
533
|
+
tree.querySelector('.file-tree-filter-input').value = value;
|
|
534
|
+
filterTree(tree, query);
|
|
535
|
+
}
|
|
536
|
+
applyRecentDateState(query !== '');
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
for (const tree of trees) {
|
|
540
|
+
const input = tree.querySelector('.file-tree-filter-input');
|
|
541
|
+
input.addEventListener('input', () => applyFilter(input.value));
|
|
284
542
|
input.addEventListener('keydown', (event) => {
|
|
285
543
|
if (event.key !== 'Escape' || input.value === '') return;
|
|
286
|
-
|
|
287
|
-
filter();
|
|
544
|
+
applyFilter('');
|
|
288
545
|
});
|
|
289
546
|
}
|
|
290
547
|
})();
|
|
291
548
|
</script>`;
|
|
292
549
|
}
|
|
293
|
-
export function renderBreadcrumbs(breadcrumbs
|
|
294
|
-
const timestamp = renderModifiedAt(modifiedAt);
|
|
550
|
+
export function renderBreadcrumbs(breadcrumbs) {
|
|
295
551
|
if (!breadcrumbs || breadcrumbs.length === 0)
|
|
296
552
|
return `<nav class="file-breadcrumbs" aria-label="パンくずリスト">
|
|
297
553
|
<button type="button" class="file-sidebar-open" data-file-sidebar-open aria-expanded="true" aria-controls="file-sidebar" aria-label="ファイルサイドバーを開く" title="ファイルサイドバーを開く" hidden><svg class="file-sidebar-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.5" /><path d="M6 2.5v11M8 5.5L10.5 8 8 10.5" /></svg></button>
|
|
298
554
|
<button type="button" class="file-tree-popover-toggle" data-file-tree-toggle aria-expanded="false" aria-controls="file-tree-popover" aria-haspopup="true" aria-label="ファイルを開く" title="ファイルを開く"><span>ファイル</span><svg class="panel-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg></button>
|
|
299
|
-
${timestamp}
|
|
300
555
|
</nav>
|
|
301
556
|
`;
|
|
302
557
|
const current = breadcrumbs.find((breadcrumb) => breadcrumb.current);
|
|
@@ -322,7 +577,6 @@ ${items}
|
|
|
322
577
|
<button type="button" class="file-sidebar-open" data-file-sidebar-open aria-expanded="true" aria-controls="file-sidebar" aria-label="ファイルサイドバーを開く" title="ファイルサイドバーを開く" hidden><svg class="file-sidebar-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.5" /><path d="M6 2.5v11M8 5.5L10.5 8 8 10.5" /></svg></button>
|
|
323
578
|
${trail} <button type="button" class="file-tree-popover-toggle" data-file-tree-toggle aria-expanded="false" aria-controls="file-tree-popover" aria-haspopup="true" aria-label="ファイルを開く" title="ファイルを開く"><span>${popoverLabel}</span><svg class="panel-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg></button>
|
|
324
579
|
<button type="button" class="copy-file-path" data-copy-file-path="${escapeHtml(path)}" aria-label="ファイルパスをコピー" title="ファイルパスをコピー">${renderCopyIcon()}</button>
|
|
325
|
-
${timestamp}
|
|
326
580
|
</nav>
|
|
327
581
|
`;
|
|
328
582
|
}
|
|
@@ -332,7 +586,7 @@ export function renderModifiedAt(modifiedAt) {
|
|
|
332
586
|
const date = new Date(modifiedAt);
|
|
333
587
|
if (Number.isNaN(date.getTime()))
|
|
334
588
|
throw new Error(`Invalid modifiedAt timestamp: ${modifiedAt}`);
|
|
335
|
-
const label = date.toISOString().slice(0,
|
|
589
|
+
const label = date.toISOString().slice(0, 16).replace("T", " ");
|
|
336
590
|
return `<time class="document-modified" datetime="${date.toISOString()}">更新 ${label}</time>`;
|
|
337
591
|
}
|
|
338
592
|
export function renderModifiedAtScript(enabled) {
|
|
@@ -342,6 +596,6 @@ export function renderModifiedAtScript(enabled) {
|
|
|
342
596
|
const element=document.querySelector('.document-modified');if(!element)return;
|
|
343
597
|
const date=new Date(element.dateTime);if(Number.isNaN(date.getTime()))return;
|
|
344
598
|
const pad=value=>String(value).padStart(2,'0');
|
|
345
|
-
element.textContent='更新 '+date.getFullYear()+'-'+pad(date.getMonth()+1)+'-'+pad(date.getDate())+' '+pad(date.getHours())+':'+pad(date.getMinutes())
|
|
599
|
+
element.textContent='更新 '+date.getFullYear()+'-'+pad(date.getMonth()+1)+'-'+pad(date.getDate())+' '+pad(date.getHours())+':'+pad(date.getMinutes());
|
|
346
600
|
})()</script>`;
|
|
347
601
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { DocumentFeature } from "../types.js";
|
|
2
2
|
/** Assets owned by the generated document header. */
|
|
3
3
|
export type HeaderFeature = DocumentFeature;
|
|
4
|
-
|
|
4
|
+
interface HeaderFeatureOptions {
|
|
5
|
+
documentNavigation: string;
|
|
6
|
+
documentMetadata: string;
|
|
7
|
+
fileTree: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function createHeaderFeature(options?: HeaderFeatureOptions): HeaderFeature;
|
|
10
|
+
export {};
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
export function createHeaderFeature(
|
|
1
|
+
export function createHeaderFeature(options = {
|
|
2
|
+
documentNavigation: "",
|
|
3
|
+
documentMetadata: "",
|
|
4
|
+
fileTree: "",
|
|
5
|
+
}) {
|
|
2
6
|
const markup = `<header class="site-header">
|
|
3
|
-
<div class="site-header-brand"
|
|
7
|
+
<div class="site-header-brand">
|
|
8
|
+
<strong class="site-header-logo">marksites</strong>
|
|
9
|
+
<div class="site-header-document-meta">
|
|
10
|
+
${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
|
+
</div>
|
|
4
12
|
<div class="site-header-actions">
|
|
5
13
|
<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>
|
|
6
14
|
<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>
|
|
7
15
|
</div>
|
|
8
16
|
</header>`;
|
|
9
17
|
const styles = `
|
|
10
|
-
.site-header{position:fixed;z-index:40;top:0;right:0;left:0;box-sizing:border-box;display:flex;height:56px;align-items:center;
|
|
11
|
-
.site-header-brand{display:flex;min-width:0;align-items:center}.site-header-
|
|
12
|
-
body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:88px}.document-sidebar{top:88px;max-height:calc(100vh - 120px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:88px}
|
|
18
|
+
.site-header{position:fixed;z-index:40;top:0;right:0;left:0;box-sizing:border-box;display:flex;height:56px;align-items:center;gap:16px;padding:0 16px;color:var(--fgColor-default,#1f2328);background:var(--bgColor-default,#fff);border-bottom:1px solid var(--borderColor-muted,#d8dee4)}body.markdown-body.has-file-tree .site-header{padding-left:56px}
|
|
19
|
+
.site-header-brand{display:flex;min-width:0;flex:1;align-items:center;gap:10px}.site-header-logo{display:inline-flex;height:26px;flex:none;align-items:center;padding:0 9px;color:#fff;font-size:.75rem;line-height:26px;background:#24292f;border-radius:6px}.site-header-document-meta{position:relative;display:flex;min-width:0;align-items:center}.site-header-document-meta .file-breadcrumbs{height:28px;min-width:0;align-items:center;padding:0;border:0}.site-header-document-meta .file-breadcrumbs ol{min-height:20px;align-items:center}.site-header-document-meta .file-breadcrumbs li,.site-header-document-meta .file-breadcrumbs a,.site-header-document-meta .file-breadcrumb-separator,.site-header-document-meta .file-tree-popover-toggle{height:20px;font-size:.8125rem;line-height:20px}.site-header-document-meta .file-tree-popover-toggle{top:0}.site-header-document-meta .copy-file-path{width:22px;height:20px}.site-header-metadata-separator{width:1px;height:16px;flex:none;margin:0 10px;background:var(--borderColor-default,#d0d7de)}.site-header-document-meta .document-metadata{display:flex;height:28px;flex:none;align-items:center;margin:0}.site-header-document-meta .document-modified{color:var(--fgColor-muted,#59636e);font-size:.6875rem;line-height:20px;white-space:nowrap}.site-header-actions{display:flex;flex:none;align-items:center;gap:6px}.site-header-action{display:inline-flex;width:34px;height:34px;align-items:center;justify-content:center;gap:3px;padding:0;color:var(--fgColor-muted,#59636e);font:inherit;background:transparent;border:0;border-radius:6px;cursor:pointer}.site-header-action:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.site-header-action:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.site-header-action svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round}.site-header-action svg[hidden]{display:none}.site-header-action.language-toggle{width:auto;min-width:48px;padding:0 7px}.language-toggle span{font-size:.6875rem;font-weight:700;line-height:1}
|
|
20
|
+
body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:88px}.document-sidebar{top:88px;height:calc(100vh - 120px);max-height:calc(100vh - 120px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:88px}
|
|
13
21
|
body.markdown-body[data-theme="dark"]{color-scheme:dark;color:#f0f6fc;background:#0d1117;--fgColor-default:#f0f6fc;--fgColor-muted:#a3abb5;--fgColor-accent:#58a6ff;--fgColor-danger:#ff7b72;--fgColor-attention:#e3b341;--fgColor-success:#56d364;--bgColor-default:#0d1117;--bgColor-muted:#161b22;--bgColor-neutral-muted:#6e768166;--bgColor-accent-muted:#1f6feb40;--bgColor-attention-muted:#bb800940;--bgColor-danger-muted:#67060c;--borderColor-default:#484f58;--borderColor-muted:#484f58cc;--borderColor-accent-emphasis:#388bfd;--button-default-bgColor-rest:#21262d;--button-default-bgColor-hover:#30363d;--focus-outlineColor:#58a6ff;--codeBlock-bgColor:#161b22;--codeBlock-fgColor:#e6edf3;--annotation-highlight-bgColor:#4d3b05;--annotation-highlight-borderColor:#e3b341;--color-prettylights-syntax-comment:#a3abb5;--color-prettylights-syntax-constant:#79c0ff;--color-prettylights-syntax-entity:#d2a8ff;--color-prettylights-syntax-entity-tag:#7ee787;--color-prettylights-syntax-keyword:#ff7b72;--color-prettylights-syntax-string:#a5d6ff;--color-prettylights-syntax-variable:#ffa657}
|
|
14
22
|
body.markdown-body[data-theme="light"]{color-scheme:light;color:#1f2328;background:#fff;--fgColor-default:#1f2328;--fgColor-muted:#59636e;--fgColor-accent:#0969da;--fgColor-danger:#d1242f;--fgColor-attention:#9a6700;--fgColor-success:#1a7f37;--bgColor-default:#fff;--bgColor-muted:#f6f8fa;--bgColor-neutral-muted:#818b981f;--bgColor-accent-muted:#ddf4ff;--bgColor-attention-muted:#fff8c5;--bgColor-danger-muted:#ffebe9;--borderColor-default:#d0d7de;--borderColor-muted:#d8dee4;--borderColor-accent-emphasis:#0969da;--button-default-bgColor-rest:#f6f8fa;--button-default-bgColor-hover:#eaeef2;--focus-outlineColor:#0969da;--codeBlock-bgColor:#fff;--codeBlock-fgColor:#24292f;--annotation-highlight-bgColor:#fff8c5;--annotation-highlight-borderColor:#bf8700}
|
|
15
|
-
@media(max-width:900px){body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:68px}.document-sidebar{top:68px;max-height:calc(100vh - 80px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:68px}}
|
|
23
|
+
@media(max-width:900px){.site-header{gap:10px;padding-right:10px}.site-header-document-meta .file-breadcrumbs ol,.site-header-document-meta .file-breadcrumb-separator,.site-header-document-meta .copy-file-path,.site-header-metadata-separator,.site-header-document-meta .document-metadata{display:none}body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:68px}.document-sidebar{top:68px;height:calc(100vh - 80px);max-height:calc(100vh - 80px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:68px}}
|
|
16
24
|
`;
|
|
17
25
|
const script = `<script>(()=>{
|
|
18
26
|
const languageParameter='lang',themeParameter='theme',pageUrl=new URL(location.href),textOriginal=new WeakMap(),attributeOriginal=new WeakMap();
|
|
@@ -21,7 +29,7 @@ const english=new Map(${JSON.stringify([
|
|
|
21
29
|
["追加", "Add"], ["コピー", "Copy"], ["コメントを追加", "Add comment"], ["コメントをすべてコピー", "Copy all comments"], ["コメントはありません。", "No comments."], ["このページにコメントはありません。", "No comments on this page."],
|
|
22
30
|
["コピー", "Copy"], ["AI向けコピー", "Copy for AI"], ["折り返す", "Wrap"], ["コピーしました", "Copied"], ["保存", "Save"], ["キャンセル", "Cancel"],
|
|
23
31
|
["引用", "Quote"], ["引用先なし", "Quote unavailable"], ["文書全体", "Whole document"],
|
|
24
|
-
["ファイルを検索", "Filter files"], ["一致するファイルはありません", "No matching files"],
|
|
32
|
+
["ファイルを検索", "Filter files"], ["一致するファイルはありません", "No matching files"], ["ファイル表示", "File view"], ["ツリー", "Tree"], ["更新順", "Recently updated"], ["更新日時のあるファイルはありません", "No files with update times"],
|
|
25
33
|
["ファイルサイドバーを開く", "Open file sidebar"], ["ファイルサイドバーを閉じる", "Close file sidebar"], ["ファイルを開く", "Open files"], ["ファイルを閉じる", "Close files"],
|
|
26
34
|
["ファイル名で検索", "Filter files by name"], ["ファイルパスをコピー", "Copy file path"], ["ファイルパスをコピーしました", "File path copied"], ["ファイルパスをコピーできませんでした", "Could not copy file path"],
|
|
27
35
|
["選択範囲の操作", "Selection actions"], ["選択範囲をコピー", "Copy selection"], ["AI向けの形式でコピー", "Copy for AI"], ["選択範囲にコメントを追加", "Add comment to selection"], ["文書全体にコメントを追加", "Add comment to document"],
|
|
@@ -32,8 +40,8 @@ const english=new Map(${JSON.stringify([
|
|
|
32
40
|
["コードをコピー", "Copy code"], ["長い行を折り返す", "Wrap long lines"], ["折り返しを解除", "Disable line wrapping"], ["コードをコピーしました", "Code copied"], ["コードをコピーできませんでした", "Could not copy code"],
|
|
33
41
|
["文書ナビゲーション", "Document navigation"], ["文書サイドバー", "Document sidebar"], ["パンくずリスト", "Breadcrumbs"]
|
|
34
42
|
])});
|
|
35
|
-
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('.
|
|
36
|
-
const translate=value=>{if(english.has(value))return english.get(value);if(value.startsWith('更新 '))return'Updated '+value.slice(3);if(/^コメント\d+件$/.test(value))return value.replace(/^コメント(\d+)件$/,'$1 comments');return value};
|
|
43
|
+
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
|
+
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};
|
|
37
45
|
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)}
|
|
38
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')}
|
|
39
47
|
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}}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function createImageViewerFeature(enabled) {
|
|
2
|
+
if (!enabled)
|
|
3
|
+
return { markup: "", styles: "", script: "" };
|
|
4
|
+
const markup = `<div class="image-viewer" data-image-viewer hidden role="dialog" aria-modal="true" aria-label="画像ビューアー">
|
|
5
|
+
<div class="image-viewer-toolbar">
|
|
6
|
+
<button type="button" data-image-zoom-out aria-label="縮小" title="縮小">−</button>
|
|
7
|
+
<button type="button" data-image-reset aria-label="元の大きさ" title="元の大きさ">100%</button>
|
|
8
|
+
<button type="button" data-image-zoom-in aria-label="拡大" title="拡大">+</button>
|
|
9
|
+
<button type="button" data-image-close aria-label="閉じる" title="閉じる">×</button>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="image-viewer-stage" data-image-stage><img data-image-viewer-image alt=""></div>
|
|
12
|
+
</div>`;
|
|
13
|
+
const styles = `
|
|
14
|
+
.markdown-content img { cursor: zoom-in; }
|
|
15
|
+
.markdown-content img:focus-visible { outline: 2px solid var(--focus-outlineColor, #0969da); outline-offset: 3px; }
|
|
16
|
+
.image-viewer[hidden] { display: none; }
|
|
17
|
+
.image-viewer { position: fixed; z-index: 1000; inset: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); color: #fff; background: rgba(0,0,0,.9); }
|
|
18
|
+
.image-viewer-toolbar { display: flex; justify-content: flex-end; gap: 8px; padding: 12px; }
|
|
19
|
+
.image-viewer-toolbar button { min-width: 40px; height: 40px; padding: 0 10px; color: #fff; font: inherit; font-weight: 600; background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.3); border-radius: 6px; cursor: pointer; }
|
|
20
|
+
.image-viewer-toolbar button:hover { background: rgba(255,255,255,.22); }
|
|
21
|
+
.image-viewer-toolbar button:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
|
|
22
|
+
.image-viewer-stage { min-width: 0; min-height: 0; overflow: hidden; cursor: grab; touch-action: none; user-select: none; }
|
|
23
|
+
.image-viewer-stage.is-dragging { cursor: grabbing; }
|
|
24
|
+
.image-viewer-stage img { display: block; max-width: none; max-height: none; transform-origin: center; pointer-events: none; }
|
|
25
|
+
body.has-open-image-viewer { overflow: hidden; }`;
|
|
26
|
+
const script = `<script>(()=>{const viewer=document.querySelector('[data-image-viewer]'),stage=viewer?.querySelector('[data-image-stage]'),image=viewer?.querySelector('[data-image-viewer-image]');if(!(viewer instanceof HTMLElement)||!(stage instanceof HTMLElement)||!(image instanceof HTMLImageElement))return;let scale=1,x=0,y=0,drag=null,opener=null;const draw=()=>{image.style.transform='translate('+x+'px,'+y+'px) scale('+scale+')';};const reset=()=>{scale=1;x=(stage.clientWidth-image.naturalWidth)/2;y=(stage.clientHeight-image.naturalHeight)/2;draw();};const close=()=>{viewer.hidden=true;document.body.classList.remove('has-open-image-viewer');if(opener instanceof HTMLElement)opener.focus();};const open=target=>{opener=target;image.src=target.currentSrc||target.src;image.alt=target.alt;viewer.hidden=false;document.body.classList.add('has-open-image-viewer');image.onload=reset;viewer.querySelector('[data-image-close]')?.focus();};document.querySelectorAll('.markdown-content img').forEach(target=>{target.tabIndex=0;target.setAttribute('role','button');target.setAttribute('aria-label',(target.alt||'画像')+'を拡大表示');target.addEventListener('click',()=>open(target));target.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();open(target);}});});viewer.querySelector('[data-image-close]')?.addEventListener('click',close);viewer.querySelector('[data-image-reset]')?.addEventListener('click',reset);const zoom=amount=>{scale=Math.min(8,Math.max(.1,scale*amount));draw();};viewer.querySelector('[data-image-zoom-in]')?.addEventListener('click',()=>zoom(1.25));viewer.querySelector('[data-image-zoom-out]')?.addEventListener('click',()=>zoom(.8));stage.addEventListener('wheel',event=>{event.preventDefault();zoom(event.deltaY<0?1.15:.87);},{passive:false});stage.addEventListener('pointerdown',event=>{drag={id:event.pointerId,x:event.clientX,y:event.clientY,ox:x,oy:y};stage.setPointerCapture(event.pointerId);stage.classList.add('is-dragging');});stage.addEventListener('pointermove',event=>{if(!drag||drag.id!==event.pointerId)return;x=drag.ox+event.clientX-drag.x;y=drag.oy+event.clientY-drag.y;draw();});const end=event=>{if(drag?.id===event.pointerId){drag=null;stage.classList.remove('is-dragging');}};stage.addEventListener('pointerup',end);stage.addEventListener('pointercancel',end);document.addEventListener('keydown',event=>{if(!viewer.hidden&&event.key==='Escape')close();});})();</script>`;
|
|
27
|
+
return { markup, styles, script };
|
|
28
|
+
}
|
|
@@ -23,9 +23,9 @@ ${tableOfContents}${annotations}
|
|
|
23
23
|
</div>
|
|
24
24
|
</aside>`;
|
|
25
25
|
const styles = `
|
|
26
|
-
.document-sidebar{grid-area:toc;position:sticky;top:32px;box-sizing:border-box;display:flex;max-height:calc(100vh - 64px);min-height:0;flex-direction:column;border:1px solid var(--borderColor-muted,#d8dee4);border-radius:8px;background:var(--bgColor-default,#fff);overflow:hidden}
|
|
26
|
+
.document-sidebar{grid-area:toc;position:sticky;top:32px;box-sizing:border-box;display:flex;height:calc(100vh - 64px);max-height:calc(100vh - 64px);min-height:0;flex-direction:column;border:1px solid var(--borderColor-muted,#d8dee4);border-radius:8px;background:var(--bgColor-default,#fff);overflow:hidden}
|
|
27
27
|
.sidebar-toggle{display:none}
|
|
28
|
-
.document-sidebar-body{display:flex;min-height:0;flex:
|
|
28
|
+
.document-sidebar-body{display:flex;min-height:0;flex:1 1 auto;flex-direction:column;overflow:hidden}
|
|
29
29
|
.sidebar-tabs{display:flex;flex:none;gap:4px;padding:10px 10px 0;border-bottom:1px solid var(--borderColor-muted,#d8dee4)}
|
|
30
30
|
.sidebar-tab{position:relative;min-width:0;flex:1;padding:8px 6px 10px;color:var(--fgColor-muted,#59636e);font:inherit;font-size:.8125rem;font-weight:600;line-height:1.25;background:transparent;border:0;cursor:pointer}
|
|
31
31
|
.sidebar-tab::after{position:absolute;right:4px;bottom:-1px;left:4px;height:2px;background:transparent;content:""}
|
|
@@ -34,16 +34,18 @@ ${tableOfContents}${annotations}
|
|
|
34
34
|
.sidebar-tab[aria-selected="true"]{color:var(--fgColor-default,#1f2328)}
|
|
35
35
|
.sidebar-tab[aria-selected="true"]::after{background:var(--borderColor-accent-emphasis,#0969da)}
|
|
36
36
|
.sidebar-count{display:inline-flex;min-width:18px;height:18px;align-items:center;justify-content:center;margin-left:3px;padding:0 4px;color:var(--fgColor-muted,#59636e);font-size:.6875rem;line-height:18px;background:var(--bgColor-muted,#f6f8fa);border-radius:9px}
|
|
37
|
-
.sidebar-panels{display:flex;min-height:0;flex:
|
|
38
|
-
.sidebar-panel{box-sizing:border-box;width:100%;
|
|
37
|
+
.sidebar-panels{display:flex;min-height:0;flex:1 1 auto;align-items:stretch;overflow:hidden}
|
|
38
|
+
.sidebar-panel{box-sizing:border-box;width:100%;height:100%;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:12px;scrollbar-width:thin;scrollbar-color:var(--borderColor-default,#d0d7de) transparent}
|
|
39
39
|
.sidebar-panel[hidden],.document-sidebar-body[hidden]{display:none}
|
|
40
40
|
@media(max-width:900px){.document-sidebar{z-index:10;top:12px;width:auto;max-height:calc(100vh - 24px);box-shadow:0 4px 12px rgba(31,35,40,.08)}.sidebar-toggle{box-sizing:border-box;display:flex;width:100%;min-height:44px;flex:none;align-items:center;justify-content:space-between;padding:8px 12px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.9375rem;font-weight:700;background:transparent;border:0;cursor:pointer}.sidebar-toggle:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:-2px}.sidebar-toggle[aria-expanded="false"] .panel-toggle-icon{transform:rotate(-90deg)}.sidebar-tabs{padding-top:0}}
|
|
41
41
|
@media(prefers-reduced-motion:reduce){.panel-toggle-icon{transition:none}}`;
|
|
42
42
|
const script = `<script>(()=>{
|
|
43
43
|
const sidebar=document.querySelector('.document-sidebar');if(!sidebar)return;
|
|
44
44
|
const tabs=[...sidebar.querySelectorAll('[data-sidebar-tab]')],panels=[...sidebar.querySelectorAll('.sidebar-panel')],toggle=sidebar.querySelector('.sidebar-toggle'),body=sidebar.querySelector('.document-sidebar-body'),label=sidebar.querySelector('[data-sidebar-toggle-label]'),compact=matchMedia('(max-width: 900px)');
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
const tabParameter='sidebar-tab',pageUrl=new URL(location.href),requestedTab=pageUrl.searchParams.get(tabParameter);
|
|
46
|
+
let active=tabs.some(item=>item.dataset.sidebarTab===requestedTab)?requestedTab:${JSON.stringify(initialPanel)};
|
|
47
|
+
function syncActiveTab(){const updateUrl=url=>{url.searchParams.delete(tabParameter);if(active==='comments')url.searchParams.set(tabParameter,active);return url};history.replaceState(null,'',updateUrl(new URL(location.href)));for(const link of document.querySelectorAll('a[href]')){const rawHref=link.getAttribute('href');if(!rawHref||rawHref.startsWith('#'))continue;const url=new URL(rawHref,location.href);if(url.protocol!==location.protocol||url.host!==location.host||!url.pathname.endsWith('.html'))continue;link.href=updateUrl(url).href}}
|
|
48
|
+
function activate(name,focus=false){const tab=tabs.find(item=>item.dataset.sidebarTab===name);if(!tab)return;active=name;for(const item of tabs){const selected=item===tab;item.setAttribute('aria-selected',String(selected));item.tabIndex=selected?0:-1}for(const panel of panels)panel.hidden=panel.id!=='sidebar-panel-'+name;label.textContent=tab.childNodes[0].textContent.trim();if(compact.matches){body.hidden=false;toggle.setAttribute('aria-expanded','true')}syncActiveTab();if(focus)tab.focus()}
|
|
47
49
|
function setExpanded(expanded){toggle.setAttribute('aria-expanded',String(expanded));body.hidden=!expanded}
|
|
48
50
|
tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>activate(tab.dataset.sidebarTab));tab.addEventListener('keydown',event=>{if(event.key!=='ArrowLeft'&&event.key!=='ArrowRight')return;event.preventDefault();const step=event.key==='ArrowRight'?1:-1;activate(tabs[(index+step+tabs.length)%tabs.length].dataset.sidebarTab,true)})});
|
|
49
51
|
toggle.addEventListener('click',()=>setExpanded(toggle.getAttribute('aria-expanded')!=='true'));
|