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,347 @@
|
|
|
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
|
+
const commentCount = countComments(node.children);
|
|
36
|
+
const count = commentCount
|
|
37
|
+
? `<span class="file-tree-comment-count file-tree-directory-comment-count" aria-label="配下のコメント${commentCount}件">${commentCount}</span>`
|
|
38
|
+
: "";
|
|
39
|
+
return ` <li class="file-tree-directory">
|
|
40
|
+
<details data-folder-id="${folderIds.get(path)}">
|
|
41
|
+
<summary>${renderFolderIcon()}<span>${escapeHtml(node.name)}</span>${count}</summary>
|
|
42
|
+
<ul>
|
|
43
|
+
${renderNodes(node.children, folderIds, path)}
|
|
44
|
+
</ul>
|
|
45
|
+
</details>
|
|
46
|
+
</li>`;
|
|
47
|
+
}
|
|
48
|
+
const current = node.current ? ' aria-current="page"' : "";
|
|
49
|
+
const count = Number.isSafeInteger(node.commentCount) && node.commentCount > 0
|
|
50
|
+
? `<span class="file-tree-comment-count" aria-label="コメント${node.commentCount}件">${node.commentCount}</span>`
|
|
51
|
+
: "";
|
|
52
|
+
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>`;
|
|
53
|
+
})
|
|
54
|
+
.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function countComments(nodes) {
|
|
57
|
+
return nodes.reduce((total, node) => total +
|
|
58
|
+
(node.type === "directory"
|
|
59
|
+
? countComments(node.children)
|
|
60
|
+
: Number.isSafeInteger(node.commentCount) && node.commentCount > 0
|
|
61
|
+
? node.commentCount
|
|
62
|
+
: 0), 0);
|
|
63
|
+
}
|
|
64
|
+
export function renderFileTree(options) {
|
|
65
|
+
if (!options || options.items.length === 0)
|
|
66
|
+
return "";
|
|
67
|
+
const folderIds = createFolderIds(options.items);
|
|
68
|
+
const contents = renderTreeContents(options.items, folderIds);
|
|
69
|
+
return `<nav class="file-tree file-tree-popover" id="file-tree-popover" aria-label="${escapeHtml(options.title ?? "ファイル")}" hidden>
|
|
70
|
+
${contents}</nav>
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
function renderTreeContents(items, folderIds) {
|
|
74
|
+
return ` <div class="file-tree-filter">
|
|
75
|
+
<input type="search" class="file-tree-filter-input" placeholder="ファイルを検索" aria-label="ファイル名で検索" autocomplete="off">
|
|
76
|
+
<p class="file-tree-filter-empty" hidden>一致するファイルはありません</p>
|
|
77
|
+
</div>
|
|
78
|
+
<ul class="file-tree-root">
|
|
79
|
+
${renderNodes(items, folderIds)}
|
|
80
|
+
</ul>
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
export function renderFileSidebar(options) {
|
|
84
|
+
if (!options || options.items.length === 0)
|
|
85
|
+
return "";
|
|
86
|
+
const folderIds = createFolderIds(options.items);
|
|
87
|
+
const title = escapeHtml(options.title ?? "ファイル");
|
|
88
|
+
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
|
+
<aside class="file-sidebar" id="file-sidebar" aria-label="${title}">
|
|
90
|
+
<div class="file-sidebar-header"><span>${title}</span></div>
|
|
91
|
+
<nav class="file-tree file-tree-sidebar" aria-label="${title}">
|
|
92
|
+
<div class="file-tree-filter">
|
|
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>
|
|
99
|
+
</nav>
|
|
100
|
+
</aside>
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
export function renderFileTreeScript(enabled) {
|
|
104
|
+
if (!enabled)
|
|
105
|
+
return "";
|
|
106
|
+
return `<script>
|
|
107
|
+
(() => {
|
|
108
|
+
const trees = [...document.querySelectorAll('.file-tree')];
|
|
109
|
+
if (trees.length === 0) return;
|
|
110
|
+
const popover = document.querySelector('.file-tree-popover');
|
|
111
|
+
const sidebar = document.querySelector('.file-sidebar');
|
|
112
|
+
const popoverToggle = document.querySelector('[data-file-tree-toggle]');
|
|
113
|
+
const sidebarOpenButton = document.querySelector('[data-file-sidebar-open]');
|
|
114
|
+
const sidebarCloseButton = document.querySelector('[data-file-sidebar-close]');
|
|
115
|
+
const copyPath = document.querySelector('[data-copy-file-path]');
|
|
116
|
+
const directories = [...document.querySelectorAll('.file-tree-directory')];
|
|
117
|
+
const stateParameter = 'open';
|
|
118
|
+
const popoverParameter = 'marksites-files';
|
|
119
|
+
const sidebarParameter = 'file-sidebar';
|
|
120
|
+
const pageUrl = new URL(location.href);
|
|
121
|
+
const openPaths = new Set(pageUrl.searchParams.getAll(stateParameter));
|
|
122
|
+
const compact = matchMedia('(max-width: 900px)');
|
|
123
|
+
let sidebarPreferenceOpen = pageUrl.searchParams.get(sidebarParameter) !== 'closed';
|
|
124
|
+
const ignoredToggles = new WeakSet();
|
|
125
|
+
const initialOpenState = new Map();
|
|
126
|
+
|
|
127
|
+
for (const directory of directories) {
|
|
128
|
+
const details = directory.querySelector(':scope > details');
|
|
129
|
+
details.open = openPaths.has(details.dataset.folderId);
|
|
130
|
+
initialOpenState.set(details.dataset.folderId, details.open);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const syncState = () => {
|
|
134
|
+
const open = [...initialOpenState]
|
|
135
|
+
.filter(([, isOpen]) => isOpen)
|
|
136
|
+
.map(([id]) => id);
|
|
137
|
+
const updateUrl = (url) => {
|
|
138
|
+
url.searchParams.delete(stateParameter);
|
|
139
|
+
for (const path of open) url.searchParams.append(stateParameter, path);
|
|
140
|
+
url.searchParams.delete(popoverParameter);
|
|
141
|
+
if (!popover.hidden) url.searchParams.set(popoverParameter, 'open');
|
|
142
|
+
url.searchParams.delete(sidebarParameter);
|
|
143
|
+
if (!sidebarPreferenceOpen) url.searchParams.set(sidebarParameter, 'closed');
|
|
144
|
+
return url;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
history.replaceState(null, '', updateUrl(new URL(location.href)));
|
|
148
|
+
for (const link of document.querySelectorAll('a[href]')) {
|
|
149
|
+
const rawHref = link.getAttribute('href');
|
|
150
|
+
if (!rawHref || rawHref.startsWith('#')) continue;
|
|
151
|
+
const url = new URL(rawHref, location.href);
|
|
152
|
+
if (url.protocol !== location.protocol || url.host !== location.host || !url.pathname.endsWith('.html')) continue;
|
|
153
|
+
link.href = updateUrl(url).href;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
for (const tree of trees) {
|
|
158
|
+
tree.querySelector('.file-tree-root').addEventListener('toggle', (event) => {
|
|
159
|
+
if (ignoredToggles.delete(event.target)) return;
|
|
160
|
+
const input = tree.querySelector('.file-tree-filter-input');
|
|
161
|
+
if (input.value !== '' || !event.target.matches('details[data-folder-id]')) return;
|
|
162
|
+
const id = event.target.dataset.folderId;
|
|
163
|
+
initialOpenState.set(id, event.target.open);
|
|
164
|
+
for (const peer of document.querySelectorAll('details[data-folder-id="'+CSS.escape(id)+'"]')) {
|
|
165
|
+
if (peer === event.target || peer.open === event.target.open) continue;
|
|
166
|
+
ignoredToggles.add(peer);
|
|
167
|
+
peer.open = event.target.open;
|
|
168
|
+
}
|
|
169
|
+
syncState();
|
|
170
|
+
}, true);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const setPopoverOpen = (open, restoreFocus = false, sync = true) => {
|
|
174
|
+
popover.hidden = !open;
|
|
175
|
+
popoverToggle.setAttribute('aria-expanded', String(open));
|
|
176
|
+
const label = open ? 'ファイルを閉じる' : 'ファイルを開く';
|
|
177
|
+
popoverToggle.setAttribute('aria-label', label);
|
|
178
|
+
popoverToggle.title = label;
|
|
179
|
+
if (sync) syncState();
|
|
180
|
+
if (open) popover.querySelector('.file-tree-filter-input').focus();
|
|
181
|
+
else if (restoreFocus) popoverToggle.focus();
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const applySidebarState = (open) => {
|
|
185
|
+
sidebar.hidden = !open;
|
|
186
|
+
sidebarCloseButton.hidden = !open;
|
|
187
|
+
sidebarOpenButton.hidden = open;
|
|
188
|
+
sidebarOpenButton.setAttribute('aria-expanded', String(open));
|
|
189
|
+
document.body.classList.toggle('file-sidebar-collapsed', !open);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const setSidebarOpen = (open, sync = true) => {
|
|
193
|
+
sidebarPreferenceOpen = open;
|
|
194
|
+
applySidebarState(open);
|
|
195
|
+
if (sync) syncState();
|
|
196
|
+
if (open) sidebar.querySelector('.file-tree-filter-input').focus();
|
|
197
|
+
else sidebarOpenButton.focus();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
setPopoverOpen(pageUrl.searchParams.get(popoverParameter) === 'open', false, false);
|
|
201
|
+
applySidebarState(sidebarPreferenceOpen && !compact.matches);
|
|
202
|
+
syncState();
|
|
203
|
+
|
|
204
|
+
popoverToggle?.addEventListener('click', (event) => {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
setPopoverOpen(popover.hidden);
|
|
207
|
+
});
|
|
208
|
+
sidebarOpenButton?.addEventListener('click', () => setSidebarOpen(true));
|
|
209
|
+
sidebarCloseButton?.addEventListener('click', () => setSidebarOpen(false));
|
|
210
|
+
document.addEventListener('pointerdown', (event) => {
|
|
211
|
+
if (popover.hidden || event.target.closest('.file-navigation,.file-sidebar')) return;
|
|
212
|
+
setPopoverOpen(false);
|
|
213
|
+
});
|
|
214
|
+
document.addEventListener('keydown', (event) => {
|
|
215
|
+
if (event.key === 'Escape' && !popover.hidden) setPopoverOpen(false, true);
|
|
216
|
+
});
|
|
217
|
+
compact.addEventListener('change', () => applySidebarState(sidebarPreferenceOpen && !compact.matches));
|
|
218
|
+
copyPath?.addEventListener('click', async () => {
|
|
219
|
+
const path = copyPath.dataset.copyFilePath;
|
|
220
|
+
try {
|
|
221
|
+
let copied = false;
|
|
222
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
223
|
+
try {
|
|
224
|
+
await navigator.clipboard.writeText(path);
|
|
225
|
+
copied = true;
|
|
226
|
+
} catch {
|
|
227
|
+
// Continue with the user-gesture-compatible fallback below.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!copied) {
|
|
231
|
+
const area = document.createElement('textarea');
|
|
232
|
+
area.value = path;
|
|
233
|
+
area.style.position = 'fixed';
|
|
234
|
+
area.style.opacity = '0';
|
|
235
|
+
document.body.append(area);
|
|
236
|
+
area.focus();
|
|
237
|
+
area.select();
|
|
238
|
+
try {
|
|
239
|
+
if (!document.execCommand('copy')) throw new Error('コピーに失敗しました');
|
|
240
|
+
} finally {
|
|
241
|
+
area.remove();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
copyPath.setAttribute('aria-label', 'ファイルパスをコピーしました');
|
|
245
|
+
copyPath.title = 'コピーしました';
|
|
246
|
+
} catch {
|
|
247
|
+
copyPath.setAttribute('aria-label', 'ファイルパスをコピーできませんでした');
|
|
248
|
+
copyPath.title = 'コピーに失敗しました';
|
|
249
|
+
}
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
copyPath.setAttribute('aria-label', 'ファイルパスをコピー');
|
|
252
|
+
copyPath.title = 'ファイルパスをコピー';
|
|
253
|
+
}, 1000);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
for (const tree of trees) {
|
|
257
|
+
const input = tree.querySelector('.file-tree-filter-input');
|
|
258
|
+
const empty = tree.querySelector('.file-tree-filter-empty');
|
|
259
|
+
const root = tree.querySelector('.file-tree-root');
|
|
260
|
+
const treeDirectories = [...root.querySelectorAll('.file-tree-directory')];
|
|
261
|
+
const filter = () => {
|
|
262
|
+
const query = input.value.trim().toLocaleLowerCase();
|
|
263
|
+
const files = [...root.querySelectorAll('.file-tree-file')];
|
|
264
|
+
let matches = 0;
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
const name = file.querySelector('a').dataset.fileName.toLocaleLowerCase();
|
|
267
|
+
const visible = query === '' || name.includes(query);
|
|
268
|
+
file.hidden = !visible;
|
|
269
|
+
if (visible) matches += 1;
|
|
270
|
+
}
|
|
271
|
+
for (const directory of [...treeDirectories].reverse()) {
|
|
272
|
+
const details = directory.querySelector(':scope > details');
|
|
273
|
+
const hasVisibleFile = [...details.querySelectorAll('.file-tree-file')].some((file) => !file.hidden);
|
|
274
|
+
directory.hidden = !hasVisibleFile;
|
|
275
|
+
const open = query === '' ? initialOpenState.get(details.dataset.folderId) : hasVisibleFile;
|
|
276
|
+
if (details.open !== open) {
|
|
277
|
+
ignoredToggles.add(details);
|
|
278
|
+
details.open = open;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
empty.hidden = matches !== 0;
|
|
282
|
+
};
|
|
283
|
+
input.addEventListener('input', filter);
|
|
284
|
+
input.addEventListener('keydown', (event) => {
|
|
285
|
+
if (event.key !== 'Escape' || input.value === '') return;
|
|
286
|
+
input.value = '';
|
|
287
|
+
filter();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
</script>`;
|
|
292
|
+
}
|
|
293
|
+
export function renderBreadcrumbs(breadcrumbs, modifiedAt) {
|
|
294
|
+
const timestamp = renderModifiedAt(modifiedAt);
|
|
295
|
+
if (!breadcrumbs || breadcrumbs.length === 0)
|
|
296
|
+
return `<nav class="file-breadcrumbs" aria-label="パンくずリスト">
|
|
297
|
+
<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
|
+
<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
|
+
</nav>
|
|
301
|
+
`;
|
|
302
|
+
const current = breadcrumbs.find((breadcrumb) => breadcrumb.current);
|
|
303
|
+
const popoverLabel = escapeHtml(current?.name ?? "ファイル");
|
|
304
|
+
const items = breadcrumbs
|
|
305
|
+
.filter((breadcrumb) => !breadcrumb.current)
|
|
306
|
+
.map((breadcrumb) => {
|
|
307
|
+
const label = breadcrumb.href
|
|
308
|
+
? `<a href="${escapeHtml(breadcrumb.href)}">${escapeHtml(breadcrumb.name)}</a>`
|
|
309
|
+
: `<span>${escapeHtml(breadcrumb.name)}</span>`;
|
|
310
|
+
return ` <li>${label}</li>`;
|
|
311
|
+
})
|
|
312
|
+
.join("\n");
|
|
313
|
+
const trail = items
|
|
314
|
+
? ` <ol>
|
|
315
|
+
${items}
|
|
316
|
+
</ol>
|
|
317
|
+
<span class="file-breadcrumb-separator" aria-hidden="true">/</span>
|
|
318
|
+
`
|
|
319
|
+
: "";
|
|
320
|
+
const path = breadcrumbs.map((breadcrumb) => breadcrumb.name).join("/");
|
|
321
|
+
return `<nav class="file-breadcrumbs" aria-label="パンくずリスト">
|
|
322
|
+
<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
|
+
${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
|
+
<button type="button" class="copy-file-path" data-copy-file-path="${escapeHtml(path)}" aria-label="ファイルパスをコピー" title="ファイルパスをコピー">${renderCopyIcon()}</button>
|
|
325
|
+
${timestamp}
|
|
326
|
+
</nav>
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
export function renderModifiedAt(modifiedAt) {
|
|
330
|
+
if (!modifiedAt)
|
|
331
|
+
return "";
|
|
332
|
+
const date = new Date(modifiedAt);
|
|
333
|
+
if (Number.isNaN(date.getTime()))
|
|
334
|
+
throw new Error(`Invalid modifiedAt timestamp: ${modifiedAt}`);
|
|
335
|
+
const label = date.toISOString().slice(0, 19).replace("T", " ");
|
|
336
|
+
return `<time class="document-modified" datetime="${date.toISOString()}">更新 ${label}</time>`;
|
|
337
|
+
}
|
|
338
|
+
export function renderModifiedAtScript(enabled) {
|
|
339
|
+
if (!enabled)
|
|
340
|
+
return "";
|
|
341
|
+
return `<script>(()=>{
|
|
342
|
+
const element=document.querySelector('.document-modified');if(!element)return;
|
|
343
|
+
const date=new Date(element.dateTime);if(Number.isNaN(date.getTime()))return;
|
|
344
|
+
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())+':'+pad(date.getSeconds());
|
|
346
|
+
})()</script>`;
|
|
347
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileBreadcrumb, FileTreeOptions } from "../types.js";
|
|
2
|
+
export declare function renderFileTree(options?: FileTreeOptions): string;
|
|
3
|
+
export declare function renderFileSidebar(options?: FileTreeOptions): string;
|
|
4
|
+
export declare function renderFileTreeScript(enabled: boolean): string;
|
|
5
|
+
export declare function renderBreadcrumbs(breadcrumbs?: FileBreadcrumb[], modifiedAt?: string): string;
|
|
6
|
+
export declare function renderModifiedAt(modifiedAt?: string): string;
|
|
7
|
+
export declare function renderModifiedAtScript(enabled: boolean): string;
|