marksites 0.1.0
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 +153 -0
- package/dist/annotations/model.d.ts +29 -0
- package/dist/annotations/model.js +56 -0
- package/dist/annotations/storage.d.ts +4 -0
- package/dist/annotations/storage.js +31 -0
- package/dist/cli/directory.d.ts +4 -0
- package/dist/cli/directory.js +3 -0
- package/dist/cli/open-browser.d.ts +12 -0
- package/dist/cli/open-browser.js +37 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +125 -0
- package/dist/conversion/directory.d.ts +3 -0
- package/dist/conversion/directory.js +208 -0
- package/dist/conversion/gitignore.d.ts +8 -0
- package/dist/conversion/gitignore.js +43 -0
- package/dist/conversion/manifest.d.ts +8 -0
- package/dist/conversion/manifest.js +40 -0
- package/dist/conversion/navigation.d.ts +4 -0
- package/dist/conversion/navigation.js +70 -0
- package/dist/conversion/paths.d.ts +10 -0
- package/dist/conversion/paths.js +86 -0
- package/dist/conversion/rendering.d.ts +5 -0
- package/dist/conversion/rendering.js +39 -0
- package/dist/conversion/single-file.d.ts +1 -0
- package/dist/conversion/single-file.js +35 -0
- package/dist/conversion/types.d.ts +39 -0
- package/dist/conversion/types.js +1 -0
- package/dist/features/annotations/index.d.ts +7 -0
- package/dist/features/annotations/index.js +79 -0
- package/dist/features/annotations.d.ts +9 -0
- package/dist/features/annotations.js +78 -0
- package/dist/features/code-blocks/index.d.ts +5 -0
- package/dist/features/code-blocks/index.js +97 -0
- package/dist/features/code-blocks.d.ts +5 -0
- package/dist/features/code-blocks.js +89 -0
- package/dist/features/file-tree/index.d.ts +7 -0
- package/dist/features/file-tree/index.js +347 -0
- package/dist/features/file-tree.d.ts +7 -0
- package/dist/features/file-tree.js +325 -0
- package/dist/features/header/index.d.ts +4 -0
- package/dist/features/header/index.js +47 -0
- package/dist/features/header.d.ts +6 -0
- package/dist/features/header.js +47 -0
- package/dist/features/sidebar/index.d.ts +10 -0
- package/dist/features/sidebar/index.js +55 -0
- package/dist/features/sidebar.d.ts +13 -0
- package/dist/features/sidebar.js +55 -0
- package/dist/features/table-of-contents/index.d.ts +16 -0
- package/dist/features/table-of-contents/index.js +91 -0
- package/dist/features/table-of-contents.d.ts +16 -0
- package/dist/features/table-of-contents.js +91 -0
- package/dist/features/types.d.ts +6 -0
- package/dist/features/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/markdown-to-html.d.ts +5 -0
- package/dist/markdown-to-html.js +77 -0
- package/dist/server/annotation-repository.d.ts +35 -0
- package/dist/server/annotation-repository.js +184 -0
- package/dist/server/api.d.ts +4 -0
- package/dist/server/api.js +77 -0
- package/dist/server/constants.d.ts +2 -0
- package/dist/server/constants.js +2 -0
- package/dist/server/html-security.d.ts +4 -0
- package/dist/server/html-security.js +61 -0
- package/dist/server/response.d.ts +2 -0
- package/dist/server/response.js +9 -0
- package/dist/server/server.d.ts +3 -0
- package/dist/server/server.js +77 -0
- package/dist/server/static-files.d.ts +2 -0
- package/dist/server/static-files.js +72 -0
- package/dist/server/types.d.ts +16 -0
- package/dist/server/types.js +1 -0
- package/dist/template/document.d.ts +20 -0
- package/dist/template/document.js +33 -0
- package/dist/template/styles.d.ts +5 -0
- package/dist/template/styles.js +109 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +1 -0
- package/dist/utils/files.d.ts +1 -0
- package/dist/utils/files.js +9 -0
- package/dist/utils/html.d.ts +2 -0
- package/dist/utils/html.js +17 -0
- package/dist/utils/icons.d.ts +8 -0
- package/dist/utils/icons.js +24 -0
- package/package.json +43 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { escapeHtml } from "../utils/html.js";
|
|
3
|
+
import { renderCopyIcon, renderFolderIcon } from "../utils/icons.js";
|
|
4
|
+
function createFolderIds(nodes) {
|
|
5
|
+
const paths = [];
|
|
6
|
+
const collect = (items, parentPath = "") => {
|
|
7
|
+
for (const item of items) {
|
|
8
|
+
if (item.type !== "directory")
|
|
9
|
+
continue;
|
|
10
|
+
const path = parentPath ? `${parentPath}/${item.name}` : item.name;
|
|
11
|
+
paths.push(path);
|
|
12
|
+
collect(item.children, path);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
collect(nodes);
|
|
16
|
+
const hashes = new Map(paths.map((path) => [
|
|
17
|
+
path,
|
|
18
|
+
createHash("sha256").update(path).digest("base64url"),
|
|
19
|
+
]));
|
|
20
|
+
let length = 6;
|
|
21
|
+
const uniqueAt = (size) => new Set([...hashes.values()].map((hash) => hash.slice(0, size))).size ===
|
|
22
|
+
hashes.size;
|
|
23
|
+
while (!uniqueAt(length) && length < 43) {
|
|
24
|
+
length++;
|
|
25
|
+
}
|
|
26
|
+
if (!uniqueAt(length))
|
|
27
|
+
throw new Error("Folder ID hash collision");
|
|
28
|
+
return new Map([...hashes].map(([path, hash]) => [path, hash.slice(0, length)]));
|
|
29
|
+
}
|
|
30
|
+
function renderNodes(nodes, folderIds, parentPath = "") {
|
|
31
|
+
return nodes
|
|
32
|
+
.map((node) => {
|
|
33
|
+
if (node.type === "directory") {
|
|
34
|
+
const path = parentPath ? `${parentPath}/${node.name}` : node.name;
|
|
35
|
+
return ` <li class="file-tree-directory">
|
|
36
|
+
<details data-folder-id="${folderIds.get(path)}">
|
|
37
|
+
<summary>${renderFolderIcon()}<span>${escapeHtml(node.name)}</span></summary>
|
|
38
|
+
<ul>
|
|
39
|
+
${renderNodes(node.children, folderIds, path)}
|
|
40
|
+
</ul>
|
|
41
|
+
</details>
|
|
42
|
+
</li>`;
|
|
43
|
+
}
|
|
44
|
+
const current = node.current ? ' aria-current="page"' : "";
|
|
45
|
+
const count = Number.isSafeInteger(node.commentCount) && node.commentCount > 0
|
|
46
|
+
? `<span class="file-tree-comment-count" aria-label="コメント${node.commentCount}件">${node.commentCount}</span>`
|
|
47
|
+
: "";
|
|
48
|
+
return ` <li class="file-tree-file"><a href="${escapeHtml(node.href)}" data-file-name="${escapeHtml(node.name)}"${current}><span class="file-tree-name">${escapeHtml(node.name)}</span>${count}</a></li>`;
|
|
49
|
+
})
|
|
50
|
+
.join("\n");
|
|
51
|
+
}
|
|
52
|
+
export function renderFileTree(options) {
|
|
53
|
+
if (!options || options.items.length === 0)
|
|
54
|
+
return "";
|
|
55
|
+
const folderIds = createFolderIds(options.items);
|
|
56
|
+
const contents = renderTreeContents(options.items, folderIds);
|
|
57
|
+
return `<nav class="file-tree file-tree-popover" id="file-tree-popover" aria-label="${escapeHtml(options.title ?? "ファイル")}" hidden>
|
|
58
|
+
${contents}</nav>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
function renderTreeContents(items, folderIds) {
|
|
62
|
+
return ` <div class="file-tree-filter">
|
|
63
|
+
<input type="search" class="file-tree-filter-input" placeholder="ファイルを検索" aria-label="ファイル名で検索" autocomplete="off">
|
|
64
|
+
<p class="file-tree-filter-empty" hidden>一致するファイルはありません</p>
|
|
65
|
+
</div>
|
|
66
|
+
<ul class="file-tree-root">
|
|
67
|
+
${renderNodes(items, folderIds)}
|
|
68
|
+
</ul>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
export function renderFileSidebar(options) {
|
|
72
|
+
if (!options || options.items.length === 0)
|
|
73
|
+
return "";
|
|
74
|
+
const folderIds = createFolderIds(options.items);
|
|
75
|
+
const title = escapeHtml(options.title ?? "ファイル");
|
|
76
|
+
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>
|
|
77
|
+
<aside class="file-sidebar" id="file-sidebar" aria-label="${title}">
|
|
78
|
+
<div class="file-sidebar-header"><span>${title}</span></div>
|
|
79
|
+
<nav class="file-tree file-tree-sidebar" aria-label="${title}">
|
|
80
|
+
<div class="file-tree-filter">
|
|
81
|
+
<input type="search" class="file-tree-filter-input" placeholder="ファイルを検索" aria-label="ファイル名で検索" autocomplete="off">
|
|
82
|
+
<p class="file-tree-filter-empty" hidden>一致するファイルはありません</p>
|
|
83
|
+
</div>
|
|
84
|
+
<ul class="file-tree-root">
|
|
85
|
+
${renderNodes(options.items, folderIds)}
|
|
86
|
+
</ul>
|
|
87
|
+
</nav>
|
|
88
|
+
</aside>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
export function renderFileTreeScript(enabled) {
|
|
92
|
+
if (!enabled)
|
|
93
|
+
return "";
|
|
94
|
+
return `<script>
|
|
95
|
+
(() => {
|
|
96
|
+
const trees = [...document.querySelectorAll('.file-tree')];
|
|
97
|
+
if (trees.length === 0) return;
|
|
98
|
+
const popover = document.querySelector('.file-tree-popover');
|
|
99
|
+
const sidebar = document.querySelector('.file-sidebar');
|
|
100
|
+
const popoverToggle = document.querySelector('[data-file-tree-toggle]');
|
|
101
|
+
const sidebarOpenButton = document.querySelector('[data-file-sidebar-open]');
|
|
102
|
+
const sidebarCloseButton = document.querySelector('[data-file-sidebar-close]');
|
|
103
|
+
const copyPath = document.querySelector('[data-copy-file-path]');
|
|
104
|
+
const directories = [...document.querySelectorAll('.file-tree-directory')];
|
|
105
|
+
const stateParameter = 'open';
|
|
106
|
+
const popoverParameter = 'marksites-files';
|
|
107
|
+
const sidebarParameter = 'file-sidebar';
|
|
108
|
+
const pageUrl = new URL(location.href);
|
|
109
|
+
const openPaths = new Set(pageUrl.searchParams.getAll(stateParameter));
|
|
110
|
+
const compact = matchMedia('(max-width: 900px)');
|
|
111
|
+
let sidebarPreferenceOpen = pageUrl.searchParams.get(sidebarParameter) !== 'closed';
|
|
112
|
+
const ignoredToggles = new WeakSet();
|
|
113
|
+
const initialOpenState = new Map();
|
|
114
|
+
|
|
115
|
+
for (const directory of directories) {
|
|
116
|
+
const details = directory.querySelector(':scope > details');
|
|
117
|
+
details.open = openPaths.has(details.dataset.folderId);
|
|
118
|
+
initialOpenState.set(details.dataset.folderId, details.open);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const syncState = () => {
|
|
122
|
+
const open = [...initialOpenState]
|
|
123
|
+
.filter(([, isOpen]) => isOpen)
|
|
124
|
+
.map(([id]) => id);
|
|
125
|
+
const updateUrl = (url) => {
|
|
126
|
+
url.searchParams.delete(stateParameter);
|
|
127
|
+
for (const path of open) url.searchParams.append(stateParameter, path);
|
|
128
|
+
url.searchParams.delete(popoverParameter);
|
|
129
|
+
if (!popover.hidden) url.searchParams.set(popoverParameter, 'open');
|
|
130
|
+
url.searchParams.delete(sidebarParameter);
|
|
131
|
+
if (!sidebarPreferenceOpen) url.searchParams.set(sidebarParameter, 'closed');
|
|
132
|
+
return url;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
history.replaceState(null, '', updateUrl(new URL(location.href)));
|
|
136
|
+
for (const link of document.querySelectorAll('a[href]')) {
|
|
137
|
+
const rawHref = link.getAttribute('href');
|
|
138
|
+
if (!rawHref || rawHref.startsWith('#')) continue;
|
|
139
|
+
const url = new URL(rawHref, location.href);
|
|
140
|
+
if (url.protocol !== location.protocol || url.host !== location.host || !url.pathname.endsWith('.html')) continue;
|
|
141
|
+
link.href = updateUrl(url).href;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
for (const tree of trees) {
|
|
146
|
+
tree.querySelector('.file-tree-root').addEventListener('toggle', (event) => {
|
|
147
|
+
if (ignoredToggles.delete(event.target)) return;
|
|
148
|
+
const input = tree.querySelector('.file-tree-filter-input');
|
|
149
|
+
if (input.value !== '' || !event.target.matches('details[data-folder-id]')) return;
|
|
150
|
+
const id = event.target.dataset.folderId;
|
|
151
|
+
initialOpenState.set(id, event.target.open);
|
|
152
|
+
for (const peer of document.querySelectorAll('details[data-folder-id="'+CSS.escape(id)+'"]')) {
|
|
153
|
+
if (peer === event.target || peer.open === event.target.open) continue;
|
|
154
|
+
ignoredToggles.add(peer);
|
|
155
|
+
peer.open = event.target.open;
|
|
156
|
+
}
|
|
157
|
+
syncState();
|
|
158
|
+
}, true);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const setPopoverOpen = (open, restoreFocus = false, sync = true) => {
|
|
162
|
+
popover.hidden = !open;
|
|
163
|
+
popoverToggle.setAttribute('aria-expanded', String(open));
|
|
164
|
+
const label = open ? 'ファイルを閉じる' : 'ファイルを開く';
|
|
165
|
+
popoverToggle.setAttribute('aria-label', label);
|
|
166
|
+
popoverToggle.title = label;
|
|
167
|
+
if (sync) syncState();
|
|
168
|
+
if (open) popover.querySelector('.file-tree-filter-input').focus();
|
|
169
|
+
else if (restoreFocus) popoverToggle.focus();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const applySidebarState = (open) => {
|
|
173
|
+
sidebar.hidden = !open;
|
|
174
|
+
sidebarCloseButton.hidden = !open;
|
|
175
|
+
sidebarOpenButton.hidden = open;
|
|
176
|
+
sidebarOpenButton.setAttribute('aria-expanded', String(open));
|
|
177
|
+
document.body.classList.toggle('file-sidebar-collapsed', !open);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const setSidebarOpen = (open, sync = true) => {
|
|
181
|
+
sidebarPreferenceOpen = open;
|
|
182
|
+
applySidebarState(open);
|
|
183
|
+
if (sync) syncState();
|
|
184
|
+
if (open) sidebar.querySelector('.file-tree-filter-input').focus();
|
|
185
|
+
else sidebarOpenButton.focus();
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
setPopoverOpen(pageUrl.searchParams.get(popoverParameter) === 'open', false, false);
|
|
189
|
+
applySidebarState(sidebarPreferenceOpen && !compact.matches);
|
|
190
|
+
syncState();
|
|
191
|
+
|
|
192
|
+
popoverToggle?.addEventListener('click', (event) => {
|
|
193
|
+
event.preventDefault();
|
|
194
|
+
setPopoverOpen(popover.hidden);
|
|
195
|
+
});
|
|
196
|
+
sidebarOpenButton?.addEventListener('click', () => setSidebarOpen(true));
|
|
197
|
+
sidebarCloseButton?.addEventListener('click', () => setSidebarOpen(false));
|
|
198
|
+
document.addEventListener('pointerdown', (event) => {
|
|
199
|
+
if (popover.hidden || event.target.closest('.file-navigation,.file-sidebar')) return;
|
|
200
|
+
setPopoverOpen(false);
|
|
201
|
+
});
|
|
202
|
+
document.addEventListener('keydown', (event) => {
|
|
203
|
+
if (event.key === 'Escape' && !popover.hidden) setPopoverOpen(false, true);
|
|
204
|
+
});
|
|
205
|
+
compact.addEventListener('change', () => applySidebarState(sidebarPreferenceOpen && !compact.matches));
|
|
206
|
+
copyPath?.addEventListener('click', async () => {
|
|
207
|
+
const path = copyPath.dataset.copyFilePath;
|
|
208
|
+
try {
|
|
209
|
+
if (navigator.clipboard && location.protocol !== 'file:') {
|
|
210
|
+
await navigator.clipboard.writeText(path);
|
|
211
|
+
} else {
|
|
212
|
+
const area = document.createElement('textarea');
|
|
213
|
+
area.value = path;
|
|
214
|
+
area.style.position = 'fixed';
|
|
215
|
+
area.style.opacity = '0';
|
|
216
|
+
document.body.append(area);
|
|
217
|
+
area.select();
|
|
218
|
+
const copied = document.execCommand('copy');
|
|
219
|
+
area.remove();
|
|
220
|
+
if (!copied) throw new Error('コピーに失敗しました');
|
|
221
|
+
}
|
|
222
|
+
copyPath.setAttribute('aria-label', 'ファイルパスをコピーしました');
|
|
223
|
+
copyPath.title = 'コピーしました';
|
|
224
|
+
} catch {
|
|
225
|
+
copyPath.setAttribute('aria-label', 'ファイルパスをコピーできませんでした');
|
|
226
|
+
copyPath.title = 'コピーに失敗しました';
|
|
227
|
+
}
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
copyPath.setAttribute('aria-label', 'ファイルパスをコピー');
|
|
230
|
+
copyPath.title = 'ファイルパスをコピー';
|
|
231
|
+
}, 1000);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
for (const tree of trees) {
|
|
235
|
+
const input = tree.querySelector('.file-tree-filter-input');
|
|
236
|
+
const empty = tree.querySelector('.file-tree-filter-empty');
|
|
237
|
+
const root = tree.querySelector('.file-tree-root');
|
|
238
|
+
const treeDirectories = [...root.querySelectorAll('.file-tree-directory')];
|
|
239
|
+
const filter = () => {
|
|
240
|
+
const query = input.value.trim().toLocaleLowerCase();
|
|
241
|
+
const files = [...root.querySelectorAll('.file-tree-file')];
|
|
242
|
+
let matches = 0;
|
|
243
|
+
for (const file of files) {
|
|
244
|
+
const name = file.querySelector('a').dataset.fileName.toLocaleLowerCase();
|
|
245
|
+
const visible = query === '' || name.includes(query);
|
|
246
|
+
file.hidden = !visible;
|
|
247
|
+
if (visible) matches += 1;
|
|
248
|
+
}
|
|
249
|
+
for (const directory of [...treeDirectories].reverse()) {
|
|
250
|
+
const details = directory.querySelector(':scope > details');
|
|
251
|
+
const hasVisibleFile = [...details.querySelectorAll('.file-tree-file')].some((file) => !file.hidden);
|
|
252
|
+
directory.hidden = !hasVisibleFile;
|
|
253
|
+
const open = query === '' ? initialOpenState.get(details.dataset.folderId) : hasVisibleFile;
|
|
254
|
+
if (details.open !== open) {
|
|
255
|
+
ignoredToggles.add(details);
|
|
256
|
+
details.open = open;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
empty.hidden = matches !== 0;
|
|
260
|
+
};
|
|
261
|
+
input.addEventListener('input', filter);
|
|
262
|
+
input.addEventListener('keydown', (event) => {
|
|
263
|
+
if (event.key !== 'Escape' || input.value === '') return;
|
|
264
|
+
input.value = '';
|
|
265
|
+
filter();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
</script>`;
|
|
270
|
+
}
|
|
271
|
+
export function renderBreadcrumbs(breadcrumbs, modifiedAt) {
|
|
272
|
+
const timestamp = renderModifiedAt(modifiedAt);
|
|
273
|
+
if (!breadcrumbs || breadcrumbs.length === 0)
|
|
274
|
+
return `<nav class="file-breadcrumbs" aria-label="パンくずリスト">
|
|
275
|
+
<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>
|
|
276
|
+
<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>
|
|
277
|
+
${timestamp}
|
|
278
|
+
</nav>
|
|
279
|
+
`;
|
|
280
|
+
const current = breadcrumbs.find((breadcrumb) => breadcrumb.current);
|
|
281
|
+
const popoverLabel = escapeHtml(current?.name ?? "ファイル");
|
|
282
|
+
const items = breadcrumbs
|
|
283
|
+
.filter((breadcrumb) => !breadcrumb.current)
|
|
284
|
+
.map((breadcrumb) => {
|
|
285
|
+
const label = breadcrumb.href
|
|
286
|
+
? `<a href="${escapeHtml(breadcrumb.href)}">${escapeHtml(breadcrumb.name)}</a>`
|
|
287
|
+
: `<span>${escapeHtml(breadcrumb.name)}</span>`;
|
|
288
|
+
return ` <li>${label}</li>`;
|
|
289
|
+
})
|
|
290
|
+
.join("\n");
|
|
291
|
+
const trail = items
|
|
292
|
+
? ` <ol>
|
|
293
|
+
${items}
|
|
294
|
+
</ol>
|
|
295
|
+
<span class="file-breadcrumb-separator" aria-hidden="true">/</span>
|
|
296
|
+
`
|
|
297
|
+
: "";
|
|
298
|
+
const path = breadcrumbs.map((breadcrumb) => breadcrumb.name).join("/");
|
|
299
|
+
return `<nav class="file-breadcrumbs" aria-label="パンくずリスト">
|
|
300
|
+
<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>
|
|
301
|
+
${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>
|
|
302
|
+
<button type="button" class="copy-file-path" data-copy-file-path="${escapeHtml(path)}" aria-label="ファイルパスをコピー" title="ファイルパスをコピー">${renderCopyIcon()}</button>
|
|
303
|
+
${timestamp}
|
|
304
|
+
</nav>
|
|
305
|
+
`;
|
|
306
|
+
}
|
|
307
|
+
export function renderModifiedAt(modifiedAt) {
|
|
308
|
+
if (!modifiedAt)
|
|
309
|
+
return "";
|
|
310
|
+
const date = new Date(modifiedAt);
|
|
311
|
+
if (Number.isNaN(date.getTime()))
|
|
312
|
+
throw new Error(`Invalid modifiedAt timestamp: ${modifiedAt}`);
|
|
313
|
+
const label = date.toISOString().slice(0, 19).replace("T", " ");
|
|
314
|
+
return `<time class="document-modified" datetime="${date.toISOString()}">更新 ${label}</time>`;
|
|
315
|
+
}
|
|
316
|
+
export function renderModifiedAtScript(enabled) {
|
|
317
|
+
if (!enabled)
|
|
318
|
+
return "";
|
|
319
|
+
return `<script>(()=>{
|
|
320
|
+
const element=document.querySelector('.document-modified');if(!element)return;
|
|
321
|
+
const date=new Date(element.dateTime);if(Number.isNaN(date.getTime()))return;
|
|
322
|
+
const pad=value=>String(value).padStart(2,'0');
|
|
323
|
+
element.textContent='更新 '+date.getFullYear()+'-'+pad(date.getMonth()+1)+'-'+pad(date.getDate())+' '+pad(date.getHours())+':'+pad(date.getMinutes())+':'+pad(date.getSeconds());
|
|
324
|
+
})()</script>`;
|
|
325
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function createHeaderFeature() {
|
|
2
|
+
const markup = `<header class="site-header">
|
|
3
|
+
<div class="site-header-brand"><strong>marksites</strong></div>
|
|
4
|
+
<div class="site-header-actions">
|
|
5
|
+
<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
|
+
<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
|
+
</div>
|
|
8
|
+
</header>`;
|
|
9
|
+
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;justify-content:space-between;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}
|
|
11
|
+
.site-header-brand{display:flex;min-width:0;align-items:center}.site-header-brand strong{font-size:.9375rem}.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}
|
|
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}
|
|
13
|
+
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
|
+
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}}
|
|
16
|
+
`;
|
|
17
|
+
const script = `<script>(()=>{
|
|
18
|
+
const languageParameter='lang',themeParameter='theme',pageUrl=new URL(location.href),textOriginal=new WeakMap(),attributeOriginal=new WeakMap();
|
|
19
|
+
const english=new Map(${JSON.stringify([
|
|
20
|
+
["目次", "Outline"], ["コメント", "Comments"], ["ファイル", "Files"], ["アーカイブ", "Archived"],
|
|
21
|
+
["追加", "Add"], ["コピー", "Copy"], ["コメントを追加", "Add comment"], ["コメントをすべてコピー", "Copy all comments"], ["コメントはありません。", "No comments."], ["このページにコメントはありません。", "No comments on this page."],
|
|
22
|
+
["コピー", "Copy"], ["AI向けコピー", "Copy for AI"], ["折り返す", "Wrap"], ["コピーしました", "Copied"], ["保存", "Save"], ["キャンセル", "Cancel"],
|
|
23
|
+
["引用", "Quote"], ["引用先なし", "Quote unavailable"], ["文書全体", "Whole document"],
|
|
24
|
+
["ファイルを検索", "Filter files"], ["一致するファイルはありません", "No matching files"],
|
|
25
|
+
["ファイルサイドバーを開く", "Open file sidebar"], ["ファイルサイドバーを閉じる", "Close file sidebar"], ["ファイルを開く", "Open files"], ["ファイルを閉じる", "Close files"],
|
|
26
|
+
["ファイル名で検索", "Filter files by name"], ["ファイルパスをコピー", "Copy file path"], ["ファイルパスをコピーしました", "File path copied"], ["ファイルパスをコピーできませんでした", "Could not copy file path"],
|
|
27
|
+
["選択範囲の操作", "Selection actions"], ["選択範囲をコピー", "Copy selection"], ["AI向けの形式でコピー", "Copy for AI"], ["選択範囲にコメントを追加", "Add comment to selection"], ["文書全体にコメントを追加", "Add comment to document"],
|
|
28
|
+
["コメントをコピー", "Copy comment"], ["コメントを編集", "Edit comment"], ["コメントをアーカイブ", "Archive comment"], ["コメントを復元", "Restore comment"], ["コメントを削除", "Delete comment"],
|
|
29
|
+
["アーカイブを開く", "Open archive"], ["アーカイブを閉じる", "Close archive"], ["コメントを保存", "Save comment"], ["編集をキャンセル", "Cancel editing"],
|
|
30
|
+
["有効なコメントをすべてコピー", "Copy all active comments"], ["有効なコメントだけをコピー", "Copy active comments only"], ["有効なコメントをコピー", "Copy active comments"], ["コメントをコピーしました", "Comment copied"], ["コピー失敗", "Copy failed"], ["コピーに失敗しました", "Copy failed"], ["保存中…", "Saving…"], ["保存に失敗しました", "Save failed"], ["このコメントを削除しますか?", "Delete this comment?"],
|
|
31
|
+
["コメントを追加するにはmarksites serveを起動してください", "Start marksites serve to add comments"], ["別の画面でコメントが更新されました。最新の内容を確認して、もう一度操作してください。", "Comments changed in another window. Review the latest version and try again."],
|
|
32
|
+
["コードをコピー", "Copy code"], ["長い行を折り返す", "Wrap long lines"], ["折り返しを解除", "Disable line wrapping"], ["コードをコピーしました", "Code copied"], ["コードをコピーできませんでした", "Could not copy code"],
|
|
33
|
+
["文書ナビゲーション", "Document navigation"], ["文書サイドバー", "Document sidebar"], ["パンくずリスト", "Breadcrumbs"]
|
|
34
|
+
])});
|
|
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('.file-navigation,.code-toolbar'))};
|
|
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};
|
|
37
|
+
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
|
+
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
|
+
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}}
|
|
40
|
+
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}
|
|
41
|
+
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();
|
|
42
|
+
document.querySelector('[data-theme-toggle]').addEventListener('click',()=>{applyTheme(document.body.dataset.theme==='dark'?'light':'dark');syncLinks()});document.querySelector('[data-language-toggle]').addEventListener('click',()=>{applyLanguage(document.body.dataset.language==='ja'?'en':'ja');applyTheme(document.body.dataset.theme);syncLinks()});
|
|
43
|
+
new MutationObserver(records=>{const language=document.body.dataset.language;if(language!=='en')return;for(const record of records){if(record.type==='attributes'){const element=record.target,name=record.attributeName,current=element.getAttribute(name);if(current&&translate(current)!==current){let originals=attributeOriginal.get(element);if(!originals){originals=new Map();attributeOriginal.set(element,originals)}originals.set(name,current)}applyNode(element,language);continue}for(const node of record.addedNodes)applyNode(node,language)}}).observe(document.body,{childList:true,subtree:true,attributes:true,attributeFilter:['aria-label','title','placeholder']});
|
|
44
|
+
window.marksitesTranslate=translate;window.marksitesApplyLanguage=()=>applyLanguage(document.body.dataset.language);
|
|
45
|
+
})()</script>`;
|
|
46
|
+
return { markup, styles, script };
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function createHeaderFeature() {
|
|
2
|
+
const markup = `<header class="site-header">
|
|
3
|
+
<div class="site-header-brand"><strong>marksites</strong></div>
|
|
4
|
+
<div class="site-header-actions">
|
|
5
|
+
<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
|
+
<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
|
+
</div>
|
|
8
|
+
</header>`;
|
|
9
|
+
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;justify-content:space-between;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}
|
|
11
|
+
.site-header-brand{display:flex;min-width:0;align-items:center}.site-header-brand strong{font-size:.9375rem}.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}
|
|
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}
|
|
13
|
+
body.markdown-body[data-theme="dark"]{color-scheme:dark;color:#f0f6fc;background:#0d1117;--fgColor-default:#f0f6fc;--fgColor-muted:#9198a1;--fgColor-accent:#4493f8;--fgColor-danger:#f85149;--bgColor-default:#0d1117;--bgColor-muted:#151b23;--bgColor-neutral-muted:#656c7633;--bgColor-accent-muted:#1f6feb26;--bgColor-danger-muted:#490202;--borderColor-default:#3d444d;--borderColor-muted:#3d444db3;--borderColor-accent-emphasis:#1f6feb;--button-default-bgColor-rest:#212830;--button-default-bgColor-hover:#262c36;--focus-outlineColor:#1f6feb}
|
|
14
|
+
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;--bgColor-default:#fff;--bgColor-muted:#f6f8fa;--bgColor-neutral-muted:#818b981f;--bgColor-accent-muted:#ddf4ff;--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}
|
|
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}}
|
|
16
|
+
`;
|
|
17
|
+
const script = `<script>(()=>{
|
|
18
|
+
const languageParameter='lang',themeParameter='theme',pageUrl=new URL(location.href),textOriginal=new WeakMap(),attributeOriginal=new WeakMap();
|
|
19
|
+
const english=new Map(${JSON.stringify([
|
|
20
|
+
["目次", "Outline"], ["コメント", "Comments"], ["ファイル", "Files"], ["アーカイブ", "Archived"],
|
|
21
|
+
["コメントを追加", "Add comment"], ["コメントをすべてコピー", "Copy all comments"], ["コメントはありません。", "No comments."], ["このページにコメントはありません。", "No comments on this page."],
|
|
22
|
+
["コピー", "Copy"], ["AI向けコピー", "Copy for AI"], ["折り返す", "Wrap"], ["コピーしました", "Copied"], ["保存", "Save"], ["キャンセル", "Cancel"],
|
|
23
|
+
["引用", "Quote"], ["引用先なし", "Quote unavailable"], ["文書全体", "Whole document"],
|
|
24
|
+
["ファイルを検索", "Filter files"], ["一致するファイルはありません", "No matching files"],
|
|
25
|
+
["ファイルサイドバーを開く", "Open file sidebar"], ["ファイルサイドバーを閉じる", "Close file sidebar"], ["ファイルを開く", "Open files"], ["ファイルを閉じる", "Close files"],
|
|
26
|
+
["ファイル名で検索", "Filter files by name"], ["ファイルパスをコピー", "Copy file path"], ["ファイルパスをコピーしました", "File path copied"], ["ファイルパスをコピーできませんでした", "Could not copy file path"],
|
|
27
|
+
["選択範囲の操作", "Selection actions"], ["選択範囲をコピー", "Copy selection"], ["AI向けの形式でコピー", "Copy for AI"], ["選択範囲にコメントを追加", "Add comment to selection"], ["文書全体にコメントを追加", "Add comment to document"],
|
|
28
|
+
["コメントをコピー", "Copy comment"], ["コメントを編集", "Edit comment"], ["コメントをアーカイブ", "Archive comment"], ["コメントを復元", "Restore comment"], ["コメントを削除", "Delete comment"],
|
|
29
|
+
["アーカイブを開く", "Open archive"], ["アーカイブを閉じる", "Close archive"], ["コメントを保存", "Save comment"], ["編集をキャンセル", "Cancel editing"],
|
|
30
|
+
["有効なコメントをすべてコピー", "Copy all active comments"], ["コメントをコピーしました", "Comment copied"], ["コピー失敗", "Copy failed"], ["コピーに失敗しました", "Copy failed"], ["保存中…", "Saving…"], ["保存に失敗しました", "Save failed"], ["このコメントを削除しますか?", "Delete this comment?"],
|
|
31
|
+
["コメントを追加するにはmarksites serveを起動してください", "Start marksites serve to add comments"], ["別の画面でコメントが更新されました。最新の内容を確認して、もう一度操作してください。", "Comments changed in another window. Review the latest version and try again."],
|
|
32
|
+
["コードをコピー", "Copy code"], ["長い行を折り返す", "Wrap long lines"], ["折り返しを解除", "Disable line wrapping"], ["コードをコピーしました", "Code copied"], ["コードをコピーできませんでした", "Could not copy code"],
|
|
33
|
+
["文書ナビゲーション", "Document navigation"], ["文書サイドバー", "Document sidebar"], ["パンくずリスト", "Breadcrumbs"]
|
|
34
|
+
])});
|
|
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('.file-navigation,.code-toolbar'))};
|
|
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};
|
|
37
|
+
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
|
+
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
|
+
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}}
|
|
40
|
+
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}
|
|
41
|
+
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();
|
|
42
|
+
document.querySelector('[data-theme-toggle]').addEventListener('click',()=>{applyTheme(document.body.dataset.theme==='dark'?'light':'dark');syncLinks()});document.querySelector('[data-language-toggle]').addEventListener('click',()=>{applyLanguage(document.body.dataset.language==='ja'?'en':'ja');applyTheme(document.body.dataset.theme);syncLinks()});
|
|
43
|
+
new MutationObserver(records=>{const language=document.body.dataset.language;if(language!=='en')return;for(const record of records){if(record.type==='attributes'){const element=record.target,name=record.attributeName,current=element.getAttribute(name);if(current&&translate(current)!==current){let originals=attributeOriginal.get(element);if(!originals){originals=new Map();attributeOriginal.set(element,originals)}originals.set(name,current)}applyNode(element,language);continue}for(const node of record.addedNodes)applyNode(node,language)}}).observe(document.body,{childList:true,subtree:true,attributes:true,attributeFilter:['aria-label','title','placeholder']});
|
|
44
|
+
window.marksitesTranslate=translate;window.marksitesApplyLanguage=()=>applyLanguage(document.body.dataset.language);
|
|
45
|
+
})()</script>`;
|
|
46
|
+
return { markup, styles, script };
|
|
47
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DocumentFeature } from "../types.js";
|
|
2
|
+
interface SidebarOptions {
|
|
3
|
+
tableOfContents: string;
|
|
4
|
+
tableOfContentsTitle: string;
|
|
5
|
+
annotations: string;
|
|
6
|
+
annotationCount: number;
|
|
7
|
+
}
|
|
8
|
+
export type SidebarFeature = DocumentFeature;
|
|
9
|
+
export declare function createSidebarFeature({ tableOfContents, tableOfContentsTitle, annotations, annotationCount, }: SidebarOptions): SidebarFeature;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { escapeHtml } from "../../utils/html.js";
|
|
2
|
+
export function createSidebarFeature({ tableOfContents, tableOfContentsTitle, annotations, annotationCount, }) {
|
|
3
|
+
if (!tableOfContents && !annotations)
|
|
4
|
+
return { markup: "", styles: "", script: "" };
|
|
5
|
+
const hasComments = annotations !== "";
|
|
6
|
+
const tocTitle = escapeHtml(tableOfContentsTitle);
|
|
7
|
+
const initialPanel = tableOfContents ? "toc" : "comments";
|
|
8
|
+
const tabs = `${tableOfContents
|
|
9
|
+
? `<button type="button" class="sidebar-tab" id="sidebar-tab-toc" role="tab" aria-selected="${initialPanel === "toc"}" aria-controls="sidebar-panel-toc" data-sidebar-tab="toc">${tocTitle}</button>`
|
|
10
|
+
: ""}${hasComments
|
|
11
|
+
? `<button type="button" class="sidebar-tab" id="sidebar-tab-comments" role="tab" aria-selected="${initialPanel === "comments"}" aria-controls="sidebar-panel-comments" data-sidebar-tab="comments">コメント <span class="sidebar-count" id="annotation-count">${annotationCount}</span></button>`
|
|
12
|
+
: ""}`;
|
|
13
|
+
const markup = `<aside class="document-sidebar" aria-label="文書ナビゲーション">
|
|
14
|
+
<button type="button" class="sidebar-toggle" aria-expanded="true" aria-controls="document-sidebar-body">
|
|
15
|
+
<span data-sidebar-toggle-label>${initialPanel === "toc" ? tocTitle : "コメント"}</span>
|
|
16
|
+
<svg class="panel-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg>
|
|
17
|
+
</button>
|
|
18
|
+
<div class="document-sidebar-body" id="document-sidebar-body">
|
|
19
|
+
<div class="sidebar-tabs" role="tablist" aria-label="文書サイドバー">${tabs}</div>
|
|
20
|
+
<div class="sidebar-panels">
|
|
21
|
+
${tableOfContents}${annotations}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</aside>`;
|
|
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}
|
|
27
|
+
.sidebar-toggle{display:none}
|
|
28
|
+
.document-sidebar-body{display:flex;min-height:0;flex:0 1 auto;flex-direction:column;overflow:hidden}
|
|
29
|
+
.sidebar-tabs{display:flex;flex:none;gap:4px;padding:10px 10px 0;border-bottom:1px solid var(--borderColor-muted,#d8dee4)}
|
|
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
|
+
.sidebar-tab::after{position:absolute;right:4px;bottom:-1px;left:4px;height:2px;background:transparent;content:""}
|
|
32
|
+
.sidebar-tab:hover{color:var(--fgColor-default,#1f2328)}
|
|
33
|
+
.sidebar-tab:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:-2px}
|
|
34
|
+
.sidebar-tab[aria-selected="true"]{color:var(--fgColor-default,#1f2328)}
|
|
35
|
+
.sidebar-tab[aria-selected="true"]::after{background:var(--borderColor-accent-emphasis,#0969da)}
|
|
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:0 1 auto;align-items:flex-start;overflow:hidden}
|
|
38
|
+
.sidebar-panel{box-sizing:border-box;width:100%;max-height:100%;min-height:0;overflow:auto;padding:12px;scrollbar-width:thin;scrollbar-color:var(--borderColor-default,#d0d7de) transparent}
|
|
39
|
+
.sidebar-panel[hidden],.document-sidebar-body[hidden]{display:none}
|
|
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
|
+
@media(prefers-reduced-motion:reduce){.panel-toggle-icon{transition:none}}`;
|
|
42
|
+
const script = `<script>(()=>{
|
|
43
|
+
const sidebar=document.querySelector('.document-sidebar');if(!sidebar)return;
|
|
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
|
+
let active=${JSON.stringify(initialPanel)};
|
|
46
|
+
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')}if(focus)tab.focus()}
|
|
47
|
+
function setExpanded(expanded){toggle.setAttribute('aria-expanded',String(expanded));body.hidden=!expanded}
|
|
48
|
+
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
|
+
toggle.addEventListener('click',()=>setExpanded(toggle.getAttribute('aria-expanded')!=='true'));
|
|
50
|
+
sidebar.addEventListener('click',event=>{if(compact.matches&&event.target.closest('.table-of-contents a'))setExpanded(false)});
|
|
51
|
+
addEventListener('marksites:show-comments',()=>activate('comments'));
|
|
52
|
+
const sync=()=>setExpanded(!compact.matches);compact.addEventListener('change',sync);activate(active);sync();
|
|
53
|
+
})()</script>`;
|
|
54
|
+
return { markup, styles, script };
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface SidebarOptions {
|
|
2
|
+
tableOfContents: string;
|
|
3
|
+
tableOfContentsTitle: string;
|
|
4
|
+
annotations: string;
|
|
5
|
+
annotationCount: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SidebarFeature {
|
|
8
|
+
markup: string;
|
|
9
|
+
styles: string;
|
|
10
|
+
script: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSidebarFeature({ tableOfContents, tableOfContentsTitle, annotations, annotationCount, }: SidebarOptions): SidebarFeature;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/html.js";
|
|
2
|
+
export function createSidebarFeature({ tableOfContents, tableOfContentsTitle, annotations, annotationCount, }) {
|
|
3
|
+
if (!tableOfContents && !annotations)
|
|
4
|
+
return { markup: "", styles: "", script: "" };
|
|
5
|
+
const hasComments = annotations !== "";
|
|
6
|
+
const tocTitle = escapeHtml(tableOfContentsTitle);
|
|
7
|
+
const initialPanel = tableOfContents ? "toc" : "comments";
|
|
8
|
+
const tabs = `${tableOfContents
|
|
9
|
+
? `<button type="button" class="sidebar-tab" id="sidebar-tab-toc" role="tab" aria-selected="${initialPanel === "toc"}" aria-controls="sidebar-panel-toc" data-sidebar-tab="toc">${tocTitle}</button>`
|
|
10
|
+
: ""}${hasComments
|
|
11
|
+
? `<button type="button" class="sidebar-tab" id="sidebar-tab-comments" role="tab" aria-selected="${initialPanel === "comments"}" aria-controls="sidebar-panel-comments" data-sidebar-tab="comments">コメント <span class="sidebar-count" id="annotation-count">${annotationCount}</span></button>`
|
|
12
|
+
: ""}`;
|
|
13
|
+
const markup = `<aside class="document-sidebar" aria-label="文書ナビゲーション">
|
|
14
|
+
<button type="button" class="sidebar-toggle" aria-expanded="true" aria-controls="document-sidebar-body">
|
|
15
|
+
<span data-sidebar-toggle-label>${initialPanel === "toc" ? tocTitle : "コメント"}</span>
|
|
16
|
+
<svg class="panel-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg>
|
|
17
|
+
</button>
|
|
18
|
+
<div class="document-sidebar-body" id="document-sidebar-body">
|
|
19
|
+
<div class="sidebar-tabs" role="tablist" aria-label="文書サイドバー">${tabs}</div>
|
|
20
|
+
<div class="sidebar-panels">
|
|
21
|
+
${tableOfContents}${annotations}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</aside>`;
|
|
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}
|
|
27
|
+
.sidebar-toggle{display:none}
|
|
28
|
+
.document-sidebar-body{display:flex;min-height:0;flex:1;flex-direction:column}
|
|
29
|
+
.sidebar-tabs{display:flex;flex:none;gap:4px;padding:10px 10px 0;border-bottom:1px solid var(--borderColor-muted,#d8dee4)}
|
|
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
|
+
.sidebar-tab::after{position:absolute;right:4px;bottom:-1px;left:4px;height:2px;background:transparent;content:""}
|
|
32
|
+
.sidebar-tab:hover{color:var(--fgColor-default,#1f2328)}
|
|
33
|
+
.sidebar-tab:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:-2px}
|
|
34
|
+
.sidebar-tab[aria-selected="true"]{color:var(--fgColor-default,#1f2328)}
|
|
35
|
+
.sidebar-tab[aria-selected="true"]::after{background:var(--borderColor-accent-emphasis,#0969da)}
|
|
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:1}
|
|
38
|
+
.sidebar-panel{box-sizing:border-box;width:100%;min-height:0;overflow:auto;padding:12px;scrollbar-width:thin;scrollbar-color:var(--borderColor-default,#d0d7de) transparent}
|
|
39
|
+
.sidebar-panel[hidden],.document-sidebar-body[hidden]{display:none}
|
|
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
|
+
@media(prefers-reduced-motion:reduce){.panel-toggle-icon{transition:none}}`;
|
|
42
|
+
const script = `<script>(()=>{
|
|
43
|
+
const sidebar=document.querySelector('.document-sidebar');if(!sidebar)return;
|
|
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
|
+
let active=${JSON.stringify(initialPanel)};
|
|
46
|
+
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')}if(focus)tab.focus()}
|
|
47
|
+
function setExpanded(expanded){toggle.setAttribute('aria-expanded',String(expanded));body.hidden=!expanded}
|
|
48
|
+
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
|
+
toggle.addEventListener('click',()=>setExpanded(toggle.getAttribute('aria-expanded')!=='true'));
|
|
50
|
+
sidebar.addEventListener('click',event=>{if(compact.matches&&event.target.closest('.table-of-contents a'))setExpanded(false)});
|
|
51
|
+
addEventListener('marksites:show-comments',()=>activate('comments'));
|
|
52
|
+
const sync=()=>setExpanded(!compact.matches);compact.addEventListener('change',sync);activate(active);sync();
|
|
53
|
+
})()</script>`;
|
|
54
|
+
return { markup, styles, script };
|
|
55
|
+
}
|