marksites 0.2.2 → 0.2.4
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 +2 -0
- package/dist/cli.js +0 -0
- package/dist/conversion/directory.js +3 -2
- package/dist/conversion/rendering.d.ts +1 -0
- package/dist/conversion/rendering.js +3 -0
- package/dist/features/annotations/index.js +24 -6
- package/dist/features/header/index.js +2 -2
- package/dist/utils/icons.d.ts +1 -0
- package/dist/utils/icons.js +3 -0
- package/package.json +1 -1
- package/dist/features/annotations.d.ts +0 -9
- package/dist/features/annotations.js +0 -78
- package/dist/features/code-blocks.d.ts +0 -5
- package/dist/features/code-blocks.js +0 -89
- package/dist/features/file-tree.d.ts +0 -7
- package/dist/features/file-tree.js +0 -325
- package/dist/features/header.d.ts +0 -6
- package/dist/features/header.js +0 -47
- package/dist/features/sidebar.d.ts +0 -13
- package/dist/features/sidebar.js +0 -55
- package/dist/features/table-of-contents.d.ts +0 -16
- package/dist/features/table-of-contents.js +0 -91
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ npx marksites docs
|
|
|
99
99
|
|
|
100
100
|
軽量ローカルサーバーの起動中だけ、本文の選択範囲へコメントを追加・編集・削除できます。
|
|
101
101
|
|
|
102
|
+
本文で文字列を選択すると、コピー操作の隣に`文字列置換`が表示されます。選択文字列を変換元として画面中央の管理ポップアップへ置換後の文字列を入力すると、表示中の本文にある同じ文字列をすべてブラウザ上で一時的に置換でき、アクセストークンなどを当てはめてからコピーできます。本文カード右上の`置換`ボタンから選択なしで新しいルールを追加することもでき、複数ルールを一覧で管理して個別にクリアできます。ポップアップは閉じるアイコンまたは範囲外のクリックで閉じます。置換箇所は枠付きでハイライトされます。置換値自体は管理ボタンへ表示しません。元のMarkdownと生成HTMLは変更されず、ページを再読み込みしても元に戻ります。
|
|
103
|
+
|
|
102
104
|
右サイドバーでは目次と現在ページのコメント一覧をタブで切り替えられます。タブにはコメント件数が表示され、コメントを選ぶと本文の対象箇所へ移動します。一覧先頭の追加ボタンから、範囲を選択しない文書全体へのコメントも作成できます。ヘッダーは固定され、各パネルは独立してスクロールします。
|
|
103
105
|
|
|
104
106
|
```sh
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -7,7 +7,7 @@ import { atomicWriteFile } from "../utils/files.js";
|
|
|
7
7
|
import { BUILD_MANIFEST, assertNoOutputCollisions, loadManifest, writeManifest, } from "./manifest.js";
|
|
8
8
|
import { buildBreadcrumbs, buildFileTree } from "./navigation.js";
|
|
9
9
|
import { DEFAULT_OUTPUT_DIRECTORY, findMarkdownFiles, firstExistingPath, pathExists, toLegacyMetadataPaths, } from "./paths.js";
|
|
10
|
-
import { OUTPUT_COMPATIBILITY_VERSION, contentHash, renderFingerprint, rewriteMarkdownLinks, } from "./rendering.js";
|
|
10
|
+
import { GENERATOR_VERSION, OUTPUT_COMPATIBILITY_VERSION, contentHash, renderFingerprint, rewriteMarkdownLinks, } from "./rendering.js";
|
|
11
11
|
import { prepareImageAssets } from "./assets.js";
|
|
12
12
|
async function migrateLegacyMetadata(files, output) {
|
|
13
13
|
let moved = 0;
|
|
@@ -204,6 +204,7 @@ export async function convertDirectoryDetailed(input, outputArgument, options =
|
|
|
204
204
|
const fingerprint = renderFingerprint();
|
|
205
205
|
const full = !previous ||
|
|
206
206
|
loaded.warning !== undefined ||
|
|
207
|
+
previous.generator.version !== GENERATOR_VERSION ||
|
|
207
208
|
previous.generator.outputCompatibilityVersion !==
|
|
208
209
|
OUTPUT_COMPATIBILITY_VERSION ||
|
|
209
210
|
previous.generator.renderFingerprint !== fingerprint ||
|
|
@@ -215,7 +216,7 @@ export async function convertDirectoryDetailed(input, outputArgument, options =
|
|
|
215
216
|
schemaVersion: 1,
|
|
216
217
|
generator: {
|
|
217
218
|
name: "marksites",
|
|
218
|
-
version:
|
|
219
|
+
version: GENERATOR_VERSION,
|
|
219
220
|
outputCompatibilityVersion: OUTPUT_COMPATIBILITY_VERSION,
|
|
220
221
|
renderFingerprint: fingerprint,
|
|
221
222
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Token } from "marked";
|
|
2
|
+
export declare const GENERATOR_VERSION: string;
|
|
2
3
|
export declare const OUTPUT_COMPATIBILITY_VERSION = 7;
|
|
3
4
|
export declare function contentHash(value: string | Buffer): string;
|
|
4
5
|
export declare function rewriteMarkdownLinks(token: Token): void;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { emptyAnnotationDocument } from "../annotations/model.js";
|
|
3
4
|
import { renderMarkdown } from "../markdown-to-html.js";
|
|
5
|
+
const packageMetadata = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
6
|
+
export const GENERATOR_VERSION = packageMetadata.version;
|
|
4
7
|
export const OUTPUT_COMPATIBILITY_VERSION = 7;
|
|
5
8
|
export function contentHash(value) {
|
|
6
9
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { countActiveAnnotations, } from "../../annotations/model.js";
|
|
2
|
-
import { renderAddIcon, renderArchiveIcon, renderCopyIcon, renderDeleteIcon, renderEditIcon, renderRestoreIcon, } from "../../utils/icons.js";
|
|
2
|
+
import { renderAddIcon, renderArchiveIcon, renderCloseIcon, renderCopyIcon, renderDeleteIcon, renderEditIcon, renderRestoreIcon, } from "../../utils/icons.js";
|
|
3
3
|
function safeJson(value) {
|
|
4
4
|
return JSON.stringify(value)
|
|
5
5
|
.replace(/</g, "\\u003c")
|
|
@@ -12,9 +12,19 @@ export function createAnnotationsFeature(data) {
|
|
|
12
12
|
const markup = `<script id="marksites-annotations-data" type="application/json">${safeJson(data)}</script>
|
|
13
13
|
<div class="selection-actions" id="selection-actions" hidden role="toolbar" aria-label="選択範囲の操作">
|
|
14
14
|
<button type="button" data-selection-action="copy" data-tooltip="選択範囲をコピー">${renderCopyIcon()}<span data-copy-label>コピー</span></button>
|
|
15
|
+
<button type="button" data-selection-action="replace" data-tooltip="文書内の同じ文字列を置換">${renderEditIcon()}<span>文字列置換</span></button>
|
|
15
16
|
<button type="button" data-selection-action="ai" data-tooltip="AI向けの形式でコピー">${renderCopyIcon()}<span data-copy-label>AI向けコピー</span></button>
|
|
16
17
|
<button type="button" data-selection-action="comment" data-tooltip="選択範囲にコメントを追加" disabled title="コメントを追加するにはmarksites serveを起動してください">${renderAddIcon()}<span>コメント</span></button>
|
|
17
|
-
</div
|
|
18
|
+
</div>
|
|
19
|
+
<form class="text-replace-alert" id="text-replace-alert" hidden role="alert" aria-live="polite">
|
|
20
|
+
<div class="text-replace-alert-heading"><strong>文字列置換</strong><button type="button" data-cancel-replace aria-label="置換リストを閉じる">${renderCloseIcon()}</button></div>
|
|
21
|
+
<div class="text-replacement-list" data-replacement-list></div>
|
|
22
|
+
<label><span>置換する文字列</span><input type="text" name="search" autocomplete="off" required></label>
|
|
23
|
+
<label><span>置換後の文字列</span><input type="text" name="replacement" autocomplete="off"></label>
|
|
24
|
+
<p class="text-replace-error" data-replacement-error role="alert" hidden></p>
|
|
25
|
+
<div class="text-replace-alert-actions"><button type="submit">置換</button><button type="button" data-cancel-replace>キャンセル</button></div>
|
|
26
|
+
</form>
|
|
27
|
+
<button type="button" class="document-replacement-menu" data-replacement-menu aria-expanded="false" title="この文書の文字列置換を管理" aria-label="この文書の文字列置換を管理">${renderEditIcon()}<span>置換</span><span class="text-replacement-count" data-replacement-count hidden>0</span></button>`;
|
|
18
28
|
const panel = `<section class="annotations-panel sidebar-panel" id="sidebar-panel-comments" role="tabpanel" aria-labelledby="sidebar-tab-comments" hidden>
|
|
19
29
|
<div class="annotations-actions"><button type="button" class="annotation-action-add" data-add-document-comment data-tooltip="文書全体にコメントを追加" disabled title="コメントを追加するにはmarksites serveを起動してください">${renderAddIcon()}<span>追加</span></button><button type="button" class="annotation-action-copy" data-copy-all-comments data-tooltip="有効なコメントだけをコピー" aria-label="有効なコメントをコピー">${renderCopyIcon()}<span data-copy-comments-label>コピー</span></button></div>
|
|
20
30
|
<p class="annotations-status" id="annotations-status" role="status"></p>
|
|
@@ -24,10 +34,11 @@ export function createAnnotationsFeature(data) {
|
|
|
24
34
|
</section>`;
|
|
25
35
|
const styles = `
|
|
26
36
|
.selection-actions{position:fixed;z-index:20;display:flex;gap:4px;padding:6px;background:#24292f;border:1px solid #57606a;border-radius:6px;box-shadow:0 8px 24px #140f0f26}
|
|
27
|
-
.selection-actions[hidden],#annotation-form[hidden],.annotations-empty[hidden]{display:none}
|
|
37
|
+
.selection-actions[hidden],.text-replace-alert[hidden],.text-replacement-count[hidden],#annotation-form[hidden],.annotations-empty[hidden]{display:none}
|
|
28
38
|
.selection-actions button{display:inline-flex;align-items:center;justify-content:center;gap:5px;border:0;border-radius:4px;padding:5px 9px;color:#fff;background:transparent;font:12px/1 system-ui;cursor:pointer}
|
|
29
39
|
.selection-actions button:hover:not(:disabled){background:#57606a}.selection-actions button:disabled{color:#8c959f;cursor:not-allowed}
|
|
30
40
|
.selection-actions [data-tooltip],.annotations-panel [data-tooltip]{position:relative}.selection-actions [data-tooltip]::after,.annotations-panel [data-tooltip]::after{position:absolute;z-index:30;top:calc(100% + 6px);right:0;width:max-content;max-width:220px;padding:5px 7px;color:#fff;font-size:.6875rem;font-weight:400;line-height:1.3;white-space:normal;background:#24292f;border-radius:4px;box-shadow:0 3px 10px rgba(31,35,40,.2);content:attr(data-tooltip);opacity:0;pointer-events:none;transform:translateY(-2px);transition:opacity 120ms ease,transform 120ms ease}.selection-actions [data-tooltip]:hover::after,.selection-actions [data-tooltip]:focus-visible::after,.annotations-panel [data-tooltip]:hover::after,.annotations-panel [data-tooltip]:focus-visible::after{opacity:1;transform:translateY(0)}
|
|
41
|
+
.markdown-content{position:relative}.document-replacement-menu{position:absolute;top:12px;right:12px;display:inline-flex;height:32px;align-items:center;justify-content:center;gap:6px;padding:0 9px;color:var(--fgColor-muted,#59636e);font:inherit;font-size:.75rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.document-replacement-menu:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.document-replacement-menu:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.document-replacement-menu svg{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round}.document-replacement-menu>span:not(.text-replacement-count){line-height:16px}.text-replace-alert{position:fixed;z-index:45;top:50%;left:50%;box-sizing:border-box;display:grid;width:min(440px,calc(100vw - 24px));max-height:calc(100vh - 24px);grid-template-columns:1fr 1fr;gap:10px;padding:12px;overflow-y:auto;color:var(--fgColor-default,#1f2328);background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;box-shadow:0 8px 24px rgba(31,35,40,.2);transform:translate(-50%,-50%)}.text-replace-alert-heading,.text-replacement-list{grid-column:1/-1}.text-replace-alert-heading{display:flex;align-items:center;justify-content:space-between}.text-replace-alert label{display:block;min-width:0}.text-replace-alert label span{display:block;margin-bottom:5px;color:var(--fgColor-muted,#59636e);font-size:.75rem;font-weight:600}.text-replace-alert input{box-sizing:border-box;width:100%;height:34px;padding:5px 8px;color:var(--fgColor-default,#1f2328);font:inherit;background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px}.text-replace-alert input:focus{border-color:var(--borderColor-accent-emphasis,#0969da);outline:2px solid var(--bgColor-accent-muted,#ddf4ff)}.text-replace-error{grid-column:1/-1;margin:0;color:var(--fgColor-danger,#d1242f);font-size:.75rem}.text-replace-error[hidden]{display:none}.text-replace-alert-actions{display:flex;grid-column:1/-1;justify-content:flex-end;gap:6px}.text-replace-alert button{height:34px;padding:0 10px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.8125rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.text-replace-alert button[type=submit]{color:#fff;background:var(--button-primary-bgColor-rest,#1f883d);border-color:transparent}.text-replace-alert .text-replace-alert-heading button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;padding:0;color:var(--fgColor-muted,#59636e);background:transparent;border:0;border-radius:4px}.text-replace-alert .text-replace-alert-heading button:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.text-replace-alert .text-replace-alert-heading button:focus-visible{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2);outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:1px}.text-replace-alert-heading button svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round}.text-replacement-list:empty{display:none}.text-replacement-rule{display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr) auto;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--borderColor-muted,#d8dee4);font-size:.75rem}.text-replacement-rule code{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-replacement-rule button{height:28px;padding:0 8px}.text-replacement{padding:1px 2px;background:var(--bgColor-accent-muted,#ddf4ff);border:1px solid var(--borderColor-accent-emphasis,#0969da);border-radius:3px;box-decoration-break:clone;-webkit-box-decoration-break:clone}.text-replacement-count{display:inline-flex;min-width:16px;height:16px;align-items:center;justify-content:center;margin-left:3px;padding:0 3px;color:#fff;font-size:.6875rem;line-height:16px;background:var(--fgColor-accent,#0969da);border-radius:999px}
|
|
31
42
|
.annotations-panel{color:var(--fgColor-default,#1f2328)}
|
|
32
43
|
.annotations-actions{position:sticky;z-index:1;top:-12px;display:flex;gap:8px;margin:-12px -12px 0;padding:10px 12px;background:var(--bgColor-default,#fff);border-bottom:1px solid var(--borderColor-muted,#d8dee4)}.annotations-panel button{padding:6px 10px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.8125rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.annotations-panel button:hover:not(:disabled){background:var(--button-default-bgColor-hover,#eaeef2)}.annotations-panel button:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.annotations-panel button:disabled{color:var(--fgColor-muted,#818b98);cursor:not-allowed}.annotations-actions button{display:inline-flex;min-width:0;flex:1;align-items:center;justify-content:center;gap:5px}.annotations-panel .annotation-action-add:not(:disabled){color:#fff;background:var(--button-primary-bgColor-rest,#1f883d);border-color:var(--button-primary-borderColor-rest,#1f232826)}.annotations-panel .annotation-action-add:hover:not(:disabled){background:var(--button-primary-bgColor-hover,#1a7f37)}.annotations-panel .annotation-action-copy{color:var(--fgColor-muted,#59636e);background:transparent;border-color:var(--borderColor-muted,#d8dee4)}
|
|
33
44
|
.annotations-empty{margin:0;padding:28px 12px 20px;color:var(--fgColor-muted,#59636e);font-size:.8125rem;line-height:1.6;text-align:center}
|
|
@@ -40,8 +51,8 @@ export function createAnnotationsFeature(data) {
|
|
|
40
51
|
`;
|
|
41
52
|
const script = `<script>(()=>{
|
|
42
53
|
const dataElement=document.getElementById('marksites-annotations-data');if(!dataElement)return;
|
|
43
|
-
let state=JSON.parse(dataElement.textContent||'{}'),editable=false,pendingSelection=null,editingId=null;
|
|
44
|
-
const content=document.querySelector('.markdown-content'),toolbar=document.getElementById('selection-actions'),addDocumentComment=document.querySelector('[data-add-document-comment]'),copyAllComments=document.querySelector('[data-copy-all-comments]'),list=document.getElementById('annotations-list'),empty=document.getElementById('annotations-empty'),count=document.getElementById('annotation-count'),panelStatus=document.getElementById('annotations-status'),form=document.getElementById('annotation-form'),formHome=document.createComment('annotation-form-home');form.before(formHome);
|
|
54
|
+
let state=JSON.parse(dataElement.textContent||'{}'),editable=false,pendingSelection=null,editingId=null,replacementSequence=0,replacementRules=[];
|
|
55
|
+
const content=document.querySelector('.markdown-content'),toolbar=document.getElementById('selection-actions'),replaceAlert=document.getElementById('text-replace-alert'),replacementError=document.querySelector('[data-replacement-error]'),replacementMenu=document.querySelector('[data-replacement-menu]'),replacementList=document.querySelector('[data-replacement-list]'),addDocumentComment=document.querySelector('[data-add-document-comment]'),copyAllComments=document.querySelector('[data-copy-all-comments]'),list=document.getElementById('annotations-list'),empty=document.getElementById('annotations-empty'),count=document.getElementById('annotation-count'),panelStatus=document.getElementById('annotations-status'),form=document.getElementById('annotation-form'),formHome=document.createComment('annotation-form-home');form.before(formHome);content.prepend(replacementMenu);
|
|
45
56
|
const excluded='button,textarea,input,.selection-actions,.annotations-panel,.file-tree,.table-of-contents,.code-block-actions';
|
|
46
57
|
const groupParameter='comments',groupDefinitions=[['comments',''],['archived','アーカイブ']],validGroups=new Set(['archived']),openGroups=new Set((new URL(location.href).searchParams.get(groupParameter)||'').split(',').filter(key=>validGroups.has(key)));
|
|
47
58
|
function syncGroupState(){const updateUrl=url=>{url.searchParams.delete(groupParameter);if(openGroups.size)url.searchParams.set(groupParameter,groupDefinitions.map(([key])=>key).filter(key=>openGroups.has(key)).join(','));return url};history.replaceState(null,'',updateUrl(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=updateUrl(url).href}}
|
|
@@ -54,8 +65,15 @@ function annotationMetadata(a,title='コメント'){const documentComment=a.sele
|
|
|
54
65
|
function allCommentsMetadata(){const annotations=sortedAnnotations().filter(a=>a.status==='open');return annotations.length?annotations.map((a,index)=>annotationMetadata(a,'コメント '+(index+1))).join('\\n\\n'):['# コメント','文書: '+state.document,'','有効なコメントはありません。'].join('\\n')}
|
|
55
66
|
function headingFor(node){if(!node)return null;const el=node.nodeType===1?node:node.parentElement,direct=el&&el.closest('h1,h2,h3,h4,h5,h6');if(direct)return direct.id;let previous=null;for(const heading of content.querySelectorAll('h1,h2,h3,h4,h5,h6')){if(heading.compareDocumentPosition(node)&Node.DOCUMENT_POSITION_FOLLOWING)previous=heading;else break}return previous?previous.id:null}
|
|
56
67
|
function selectionData(){const selection=getSelection();if(!selection||selection.rangeCount!==1||selection.isCollapsed)return null;const range=selection.getRangeAt(0);if(!content.contains(range.commonAncestorContainer)||(range.commonAncestorContainer.parentElement&&range.commonAncestorContainer.parentElement.closest(excluded)))return null;const exact=selection.toString().trim();if(!exact)return null;const all=content.innerText,index=all.indexOf(exact);return{exact,prefix:index<0?'':all.slice(Math.max(0,index-40),index),suffix:index<0?'':all.slice(index+exact.length,index+exact.length+40),headingId:headingFor(range.startContainer),startOffset:Math.max(0,index),endOffset:Math.max(0,index)+exact.length}}
|
|
68
|
+
function renderReplacementRules(){replacementList.textContent='';for(const rule of replacementRules){const row=document.createElement('div');row.className='text-replacement-rule';const search=document.createElement('code'),arrow=document.createElement('span'),replacement=document.createElement('code'),clear=document.createElement('button');search.textContent=rule.search;arrow.textContent='→';replacement.textContent=rule.replacement;clear.type='button';clear.textContent='クリア';clear.dataset.clearReplacement=String(rule.id);row.append(search,arrow,replacement,clear);replacementList.append(row)}const count=replacementRules.length,countElement=replacementMenu.querySelector('[data-replacement-count]');countElement.hidden=count===0;countElement.textContent=String(count)}
|
|
69
|
+
function setReplacementError(message=''){replacementError.textContent=message;replacementError.hidden=!message}
|
|
70
|
+
function openReplacementMenu(search=''){replaceAlert.hidden=false;replacementMenu.setAttribute('aria-expanded','true');replaceAlert.search.value=search;replaceAlert.replacement.value='';setReplacementError();replaceAlert.search.focus();if(search)replaceAlert.replacement.focus()}
|
|
71
|
+
function closeReplacementMenu(){replaceAlert.hidden=true;replacementMenu.setAttribute('aria-expanded','false')}
|
|
72
|
+
function replaceText(search,replacement){const id=++replacementSequence,walker=document.createTreeWalker(content,NodeFilter.SHOW_TEXT,{acceptNode:node=>node.parentElement?.closest(excluded+',.text-replacement')?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}),nodes=[];let node,total=0;while(node=walker.nextNode())nodes.push(node);for(const text of nodes){const parts=text.data.split(search);if(parts.length===1)continue;const fragment=document.createDocumentFragment();parts.forEach((part,index)=>{fragment.append(document.createTextNode(part));if(index<parts.length-1){const mark=document.createElement('span');mark.className='text-replacement';mark.dataset.replacementId=String(id);mark.textContent=replacement;fragment.append(mark);total++}});text.replaceWith(fragment)}if(total)replacementRules.push({id,search,replacement,total});renderReplacementRules()}
|
|
57
73
|
document.addEventListener('selectionchange',()=>{clearTimeout(window.__marksitesSelectionTimer);window.__marksitesSelectionTimer=setTimeout(()=>{const next=selectionData();if(!next){toolbar.hidden=true;return}pendingSelection=next;const r=getSelection().getRangeAt(0).getBoundingClientRect();toolbar.style.left=Math.max(8,Math.min(innerWidth-toolbar.offsetWidth-8,r.left))+'px';toolbar.style.top=Math.max(8,r.top-44)+'px';toolbar.hidden=false},80)});
|
|
58
|
-
toolbar.addEventListener('mousedown',e=>e.preventDefault());toolbar.addEventListener('click',async e=>{const button=e.target.closest('button');if(!button||!pendingSelection)return;const label=button.querySelector('[data-copy-label]');try{if(button.dataset.selectionAction==='copy')await copy(pendingSelection.exact);if(button.dataset.selectionAction==='ai')await copy('文書: '+state.document+'\\n見出し: '+(pendingSelection.headingId||'なし')+'\\n\\n> '+pendingSelection.exact.replace(/\\n/g,'\\n> '));if(button.dataset.selectionAction==='comment'&&editable){editingId=null;dispatchEvent(new Event('marksites:show-comments'));openForm()}if(label){label.textContent='コピーしました';setTimeout(()=>label.textContent=button.dataset.selectionAction==='ai'?'AI向けコピー':'コピー',1000)}}catch{if(label)label.textContent='コピー失敗'}});
|
|
74
|
+
toolbar.addEventListener('mousedown',e=>e.preventDefault());toolbar.addEventListener('click',async e=>{const button=e.target.closest('button');if(!button||!pendingSelection)return;const label=button.querySelector('[data-copy-label]');try{if(button.dataset.selectionAction==='copy')await copy(pendingSelection.exact);if(button.dataset.selectionAction==='replace'){openReplacementMenu(pendingSelection.exact);toolbar.hidden=true}if(button.dataset.selectionAction==='ai')await copy('文書: '+state.document+'\\n見出し: '+(pendingSelection.headingId||'なし')+'\\n\\n> '+pendingSelection.exact.replace(/\\n/g,'\\n> '));if(button.dataset.selectionAction==='comment'&&editable){editingId=null;dispatchEvent(new Event('marksites:show-comments'));openForm()}if(label){label.textContent='コピーしました';setTimeout(()=>label.textContent=button.dataset.selectionAction==='ai'?'AI向けコピー':'コピー',1000)}}catch{if(label)label.textContent='コピー失敗'}});
|
|
75
|
+
replaceAlert.addEventListener('submit',e=>{e.preventDefault();const search=replaceAlert.search.value,replacement=replaceAlert.replacement.value;if(!search){setReplacementError('置換する文字列を入力してください。');replaceAlert.search.focus();return}if(!replacement){setReplacementError('置換後の文字列を入力してください。');replaceAlert.replacement.focus();return}const before=replacementRules.length;replaceText(search,replacement);if(replacementRules.length===before){setReplacementError('置換する文字列が本文内に見つかりません。');replaceAlert.search.focus();return}setReplacementError();replaceAlert.search.value='';replaceAlert.replacement.value='';replaceAlert.search.focus();getSelection()?.removeAllRanges()});replaceAlert.addEventListener('input',()=>setReplacementError());replaceAlert.querySelectorAll('[data-cancel-replace]').forEach(button=>button.addEventListener('click',closeReplacementMenu));replacementMenu.addEventListener('click',()=>{if(replaceAlert.hidden)openReplacementMenu();else closeReplacementMenu()});replacementList.addEventListener('click',e=>{const button=e.target.closest('[data-clear-replacement]');if(!button)return;const id=Number(button.dataset.clearReplacement),rule=replacementRules.find(item=>item.id===id);if(!rule)return;for(const mark of content.querySelectorAll('.text-replacement[data-replacement-id="'+id+'"]'))mark.replaceWith(document.createTextNode(rule.search));replacementRules=replacementRules.filter(item=>item.id!==id);renderReplacementRules()});
|
|
76
|
+
document.addEventListener('click',e=>{if(replaceAlert.hidden||replaceAlert.contains(e.target)||replacementMenu.contains(e.target)||e.target.closest('[data-selection-action=replace]'))return;closeReplacementMenu()});
|
|
59
77
|
addDocumentComment.addEventListener('click',()=>{if(!editable)return;editingId=null;pendingSelection={exact:'',prefix:'',suffix:'',headingId:null,startOffset:0,endOffset:0};openForm()});
|
|
60
78
|
copyAllComments.addEventListener('click',async()=>{const label=copyAllComments.querySelector('[data-copy-comments-label]');try{await copy(allCommentsMetadata());label.textContent='コピーしました';setTimeout(()=>label.textContent='コピー',1000)}catch{label.textContent='コピー失敗'}});
|
|
61
79
|
function locate(a){const preferred=a.selection.headingId?document.getElementById(a.selection.headingId):null,roots=preferred&&preferred!==content?[preferred,content]:[content];for(const root of roots){const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);let node,text='',nodes=[];while(node=walker.nextNode()){nodes.push([node,text.length]);text+=node.data}let index=text.indexOf(a.selection.exact);if(index<0)continue;const matches=[];while(index>=0){matches.push(index);index=text.indexOf(a.selection.exact,index+1)}if(matches.length>1){const contextual=matches.find(i=>text.slice(Math.max(0,i-a.selection.prefix.length),i)===a.selection.prefix&&text.slice(i+a.selection.exact.length,i+a.selection.exact.length+a.selection.suffix.length)===a.selection.suffix);index=contextual??matches.reduce((best,current)=>Math.abs(current-a.selection.startOffset)<Math.abs(best-a.selection.startOffset)?current:best)}else index=matches[0];let start,end,so=0,eo=0;for(const [n,offset] of nodes){if(!start&&offset+n.data.length>=index){start=n;so=index-offset}if(offset+n.data.length>=index+a.selection.exact.length){end=n;eo=index+a.selection.exact.length-offset;break}}if(!start||!end)continue;const range=document.createRange();range.setStart(start,so);range.setEnd(end,eo);return range}return null}
|
|
@@ -27,12 +27,12 @@ const languageParameter='lang',themeParameter='theme',pageUrl=new URL(location.h
|
|
|
27
27
|
const english=new Map(${JSON.stringify([
|
|
28
28
|
["目次", "Outline"], ["コメント", "Comments"], ["ファイル", "Files"], ["アーカイブ", "Archived"],
|
|
29
29
|
["追加", "Add"], ["コピー", "Copy"], ["コメントを追加", "Add comment"], ["コメントをすべてコピー", "Copy all comments"], ["コメントはありません。", "No comments."], ["このページにコメントはありません。", "No comments on this page."],
|
|
30
|
-
["コピー", "Copy"], ["AI向けコピー", "Copy for AI"], ["折り返す", "Wrap"], ["コピーしました", "Copied"], ["保存", "Save"], ["キャンセル", "Cancel"],
|
|
30
|
+
["コピー", "Copy"], ["文字列置換", "Replace text"], ["置換", "Replace"], ["置換する文字列", "Text to replace"], ["置換後の文字列", "Replacement text"], ["置換する文字列を入力してください。", "Enter text to replace."], ["置換後の文字列を入力してください。", "Enter replacement text."], ["置換する文字列が本文内に見つかりません。", "Text to replace was not found in the document."], ["この文書の文字列置換を管理", "Manage replacements in this document"], ["置換リストを閉じる", "Close replacement list"], ["クリア", "Clear"], ["AI向けコピー", "Copy for AI"], ["折り返す", "Wrap"], ["コピーしました", "Copied"], ["保存", "Save"], ["キャンセル", "Cancel"],
|
|
31
31
|
["引用", "Quote"], ["引用先なし", "Quote unavailable"], ["文書全体", "Whole document"],
|
|
32
32
|
["ファイルを検索", "Filter files"], ["一致するファイルはありません", "No matching files"], ["ファイル表示", "File view"], ["ツリー", "Tree"], ["更新順", "Recently updated"], ["更新日時のあるファイルはありません", "No files with update times"],
|
|
33
33
|
["ファイルサイドバーを開く", "Open file sidebar"], ["ファイルサイドバーを閉じる", "Close file sidebar"], ["ファイルを開く", "Open files"], ["ファイルを閉じる", "Close files"],
|
|
34
34
|
["ファイル名で検索", "Filter files by name"], ["ファイルパスをコピー", "Copy file path"], ["ファイルパスをコピーしました", "File path copied"], ["ファイルパスをコピーできませんでした", "Could not copy file path"],
|
|
35
|
-
["選択範囲の操作", "Selection actions"], ["選択範囲をコピー", "Copy selection"], ["AI向けの形式でコピー", "Copy for AI"], ["選択範囲にコメントを追加", "Add comment to selection"], ["文書全体にコメントを追加", "Add comment to document"],
|
|
35
|
+
["選択範囲の操作", "Selection actions"], ["選択範囲をコピー", "Copy selection"], ["文書内の同じ文字列を置換", "Replace matching text in document"], ["AI向けの形式でコピー", "Copy for AI"], ["選択範囲にコメントを追加", "Add comment to selection"], ["文書全体にコメントを追加", "Add comment to document"],
|
|
36
36
|
["コメントをコピー", "Copy comment"], ["コメントを編集", "Edit comment"], ["コメントをアーカイブ", "Archive comment"], ["コメントを復元", "Restore comment"], ["コメントを削除", "Delete comment"],
|
|
37
37
|
["アーカイブを開く", "Open archive"], ["アーカイブを閉じる", "Close archive"], ["コメントを保存", "Save comment"], ["編集をキャンセル", "Cancel editing"],
|
|
38
38
|
["有効なコメントをすべてコピー", "Copy all active comments"], ["有効なコメントだけをコピー", "Copy active comments only"], ["有効なコメントをコピー", "Copy active comments"], ["コメントをコピーしました", "Comment copied"], ["コピー失敗", "Copy failed"], ["コピーに失敗しました", "Copy failed"], ["保存中…", "Saving…"], ["保存に失敗しました", "Save failed"], ["このコメントを削除しますか?", "Delete this comment?"],
|
package/dist/utils/icons.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare function renderCopyIcon(): string;
|
|
|
2
2
|
export declare function renderAddIcon(): string;
|
|
3
3
|
export declare function renderWrapIcon(): string;
|
|
4
4
|
export declare function renderEditIcon(): string;
|
|
5
|
+
export declare function renderCloseIcon(): string;
|
|
5
6
|
export declare function renderDeleteIcon(): string;
|
|
6
7
|
export declare function renderArchiveIcon(): string;
|
|
7
8
|
export declare function renderRestoreIcon(): string;
|
package/dist/utils/icons.js
CHANGED
|
@@ -10,6 +10,9 @@ export function renderWrapIcon() {
|
|
|
10
10
|
export function renderEditIcon() {
|
|
11
11
|
return '<svg class="action-icon edit-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="m10.5 2.5 3 3-7.75 7.75-3.5.5.5-3.5Z"/><path d="m9 4 3 3"/></svg>';
|
|
12
12
|
}
|
|
13
|
+
export function renderCloseIcon() {
|
|
14
|
+
return '<svg class="action-icon close-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="m4 4 8 8M12 4l-8 8"/></svg>';
|
|
15
|
+
}
|
|
13
16
|
export function renderDeleteIcon() {
|
|
14
17
|
return '<svg class="action-icon delete-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 5h9M6 2.5h4l.75 2.5h-5.5Z"/><path d="m4.5 5 .5 8.5h6L11.5 5M7 7.5v3.5m2-3.5v3.5"/></svg>';
|
|
15
18
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type AnnotationDocument } from "../annotations/model.js";
|
|
2
|
-
export interface AnnotationsFeature {
|
|
3
|
-
markup: string;
|
|
4
|
-
panel: string;
|
|
5
|
-
styles: string;
|
|
6
|
-
script: string;
|
|
7
|
-
count: number;
|
|
8
|
-
}
|
|
9
|
-
export declare function createAnnotationsFeature(data?: AnnotationDocument): AnnotationsFeature;
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { countActiveAnnotations, } from "../annotations/model.js";
|
|
2
|
-
import { renderAddIcon, renderArchiveIcon, renderCopyIcon, renderDeleteIcon, renderEditIcon, renderRestoreIcon, } from "../utils/icons.js";
|
|
3
|
-
function safeJson(value) {
|
|
4
|
-
return JSON.stringify(value)
|
|
5
|
-
.replace(/</g, "\\u003c")
|
|
6
|
-
.replace(/>/g, "\\u003e")
|
|
7
|
-
.replace(/&/g, "\\u0026");
|
|
8
|
-
}
|
|
9
|
-
export function createAnnotationsFeature(data) {
|
|
10
|
-
if (!data)
|
|
11
|
-
return { markup: "", panel: "", styles: "", script: "", count: 0 };
|
|
12
|
-
const markup = `<script id="marksites-annotations-data" type="application/json">${safeJson(data)}</script>
|
|
13
|
-
<div class="selection-actions" id="selection-actions" hidden role="toolbar" aria-label="選択範囲の操作">
|
|
14
|
-
<button type="button" data-selection-action="copy" data-tooltip="選択範囲をコピー">${renderCopyIcon()}<span data-copy-label>コピー</span></button>
|
|
15
|
-
<button type="button" data-selection-action="ai" data-tooltip="AI向けの形式でコピー">${renderCopyIcon()}<span data-copy-label>AI向けコピー</span></button>
|
|
16
|
-
<button type="button" data-selection-action="comment" data-tooltip="選択範囲にコメントを追加" disabled title="コメントを追加するにはmarksites serveを起動してください">${renderAddIcon()}<span>コメント</span></button>
|
|
17
|
-
</div>`;
|
|
18
|
-
const panel = `<section class="annotations-panel sidebar-panel" id="sidebar-panel-comments" role="tabpanel" aria-labelledby="sidebar-tab-comments" hidden>
|
|
19
|
-
<div class="annotations-actions"><button type="button" data-add-document-comment data-tooltip="文書全体にコメントを追加" disabled title="コメントを追加するにはmarksites serveを起動してください">${renderAddIcon()}<span>コメントを追加</span></button><button type="button" data-copy-all-comments data-tooltip="有効なコメントをすべてコピー">${renderCopyIcon()}<span data-copy-comments-label>コメントをすべてコピー</span></button></div>
|
|
20
|
-
<p class="annotations-status" id="annotations-status" role="status"></p>
|
|
21
|
-
<div id="annotations-list"></div>
|
|
22
|
-
<p class="annotations-empty" id="annotations-empty" hidden>このページにコメントはありません。</p>
|
|
23
|
-
<form id="annotation-form" hidden><label><span class="annotation-form-label">コメント</span><textarea name="body" maxlength="10000" required></textarea></label><div class="annotation-form-actions"><button type="submit" data-tooltip="コメントを保存">保存</button><button type="button" data-cancel-comment data-tooltip="編集をキャンセル">キャンセル</button></div><p role="status"></p></form>
|
|
24
|
-
</section>`;
|
|
25
|
-
const styles = `
|
|
26
|
-
.selection-actions{position:fixed;z-index:20;display:flex;gap:4px;padding:6px;background:#24292f;border:1px solid #57606a;border-radius:6px;box-shadow:0 8px 24px #140f0f26}
|
|
27
|
-
.selection-actions[hidden],#annotation-form[hidden],.annotations-empty[hidden]{display:none}
|
|
28
|
-
.selection-actions button{display:inline-flex;align-items:center;justify-content:center;gap:5px;border:0;border-radius:4px;padding:5px 9px;color:#fff;background:transparent;font:12px/1 system-ui;cursor:pointer}
|
|
29
|
-
.selection-actions button:hover:not(:disabled){background:#57606a}.selection-actions button:disabled{color:#8c959f;cursor:not-allowed}
|
|
30
|
-
.selection-actions [data-tooltip],.annotations-panel [data-tooltip]{position:relative}.selection-actions [data-tooltip]::after,.annotations-panel [data-tooltip]::after{position:absolute;z-index:30;top:calc(100% + 6px);right:0;width:max-content;max-width:220px;padding:5px 7px;color:#fff;font-size:.6875rem;font-weight:400;line-height:1.3;white-space:normal;background:#24292f;border-radius:4px;box-shadow:0 3px 10px rgba(31,35,40,.2);content:attr(data-tooltip);opacity:0;pointer-events:none;transform:translateY(-2px);transition:opacity 120ms ease,transform 120ms ease}.selection-actions [data-tooltip]:hover::after,.selection-actions [data-tooltip]:focus-visible::after,.annotations-panel [data-tooltip]:hover::after,.annotations-panel [data-tooltip]:focus-visible::after{opacity:1;transform:translateY(0)}
|
|
31
|
-
.annotations-panel{color:#1f2328}
|
|
32
|
-
.annotations-actions{position:sticky;z-index:1;top:-12px;display:flex;flex-direction:column;gap:6px;margin:-12px -12px 0;padding:10px 12px;background:var(--bgColor-default,#fff);border-bottom:1px solid var(--borderColor-muted,#d8dee4)}.annotations-panel button{padding:6px 10px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.8125rem;font-weight:600;background:var(--button-default-bgColor-rest,#f6f8fa);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;cursor:pointer}.annotations-panel button:hover:not(:disabled){background:var(--button-default-bgColor-hover,#eaeef2)}.annotations-panel button:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.annotations-panel button:disabled{color:var(--fgColor-muted,#818b98);cursor:not-allowed}.annotations-actions button{display:inline-flex;width:100%;align-items:center;justify-content:center;gap:5px}
|
|
33
|
-
.annotations-empty{margin:24px 8px;color:var(--fgColor-muted,#59636e);font-size:.8125rem;text-align:center}
|
|
34
|
-
.annotations-status{margin:8px;color:var(--fgColor-muted,#59636e);font-size:.75rem}.annotations-status:empty{display:none}.annotation-group{margin:0 -12px;overflow:visible}.annotation-group:last-child{margin-bottom:-12px}.annotation-group[data-comment-group="archived"]{border-top:1px solid var(--borderColor-muted,#d8dee4)}.annotation-group-header{display:flex;align-items:center;min-height:40px;padding:0 12px;background:var(--bgColor-muted,#f6f8fa)}.annotations-panel .annotation-group-toggle{display:flex;min-width:0;flex:1;align-items:center;gap:6px;padding:8px 0;background:transparent;border:0;text-align:left}.annotation-group-toggle .panel-toggle-icon{width:14px;height:14px;flex:none;fill:none;stroke:currentColor;stroke-width:1.5}.annotation-group-toggle[aria-expanded="false"] .panel-toggle-icon{transform:rotate(-90deg)}.annotation-group-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.annotation-group-body[hidden]{display:none}.annotation-group-empty{margin:14px 12px;color:var(--fgColor-muted,#59636e);font-size:.75rem;text-align:center}
|
|
35
|
-
.annotation-card{position:relative;margin:0;padding:12px;border:0;border-bottom:1px solid var(--borderColor-muted,#d8dee4);font-size:.8125rem;line-height:1.5;cursor:pointer}.annotation-card:hover{background:var(--bgColor-muted,#f6f8fa)}.annotation-card:focus-visible,.annotation-card.is-focused{background:var(--bgColor-accent-muted,#ddf4ff);outline:2px solid var(--borderColor-accent-emphasis,#0969da);outline-offset:-2px}.annotation-card:focus-visible .annotation-quote,.annotation-card.is-focused .annotation-quote{border-left-color:var(--borderColor-accent-emphasis,#0969da);color:var(--fgColor-accent,#0969da)}.annotation-quote{display:-webkit-box;margin:0 0 6px;padding-left:9px;overflow:hidden;border-left:3px solid #54aeff;color:#57606a;font-size:.75rem;line-height:1.5;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.annotation-context{display:block;margin:0 0 6px;color:var(--fgColor-muted,#59636e);font-size:.75rem}.annotation-source{display:-webkit-box;margin:0 0 6px;overflow:hidden;color:var(--fgColor-muted,#59636e);font-size:.75rem;line-height:1.5;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.annotation-card p{white-space:pre-wrap}.annotation-meta{display:flex;align-items:center;gap:8px;margin-top:8px;color:var(--fgColor-muted,#59636e);font-size:.6875rem;line-height:1.4}.annotation-updated{display:block;margin-left:auto}.annotation-kind{display:inline-flex;align-items:center;padding:2px 7px;color:var(--fgColor-muted,#59636e);font-weight:600;background:var(--bgColor-muted,#f6f8fa);border:1px solid var(--borderColor-muted,#d8dee4);border-radius:999px}.annotation-card-actions{position:absolute;top:6px;right:6px;display:flex;gap:1px;padding:2px;background:color-mix(in srgb,var(--bgColor-default,#fff) 94%,transparent);border:1px solid var(--borderColor-muted,#d8dee4);border-radius:6px;box-shadow:0 1px 2px rgba(31,35,40,.08);opacity:0;pointer-events:none;transition:opacity 120ms ease}.annotations-panel .annotation-card-actions button{display:inline-flex;width:28px;height:28px;align-items:center;justify-content:center;margin:0;padding:0;color:var(--fgColor-muted,#59636e);background:transparent;border:0;border-radius:4px}.annotations-panel .annotation-card-actions button:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.annotations-panel .annotation-card-actions [data-annotation-action="delete"]:hover{color:var(--fgColor-danger,#d1242f);background:var(--bgColor-danger-muted,#ffebe9)}.annotation-card:hover>.annotation-card-actions,.annotation-card-actions:focus-within,.annotation-card.is-touch-actions>.annotation-card-actions{opacity:1;pointer-events:auto}.annotation-card.is-editing,.annotation-card.is-editing:hover,.annotation-card.is-editing:focus-visible{padding:12px;background:var(--bgColor-default,#fff);outline:0;box-shadow:0 1px 3px rgba(31,35,40,.08);cursor:default}.annotation-card.is-editing>.annotation-quote{margin-bottom:12px;color:var(--fgColor-muted,#59636e);border-left-color:var(--borderColor-accent-emphasis,#0969da)}.annotation-card.is-editing>p,.annotation-card.is-editing>.annotation-meta,.annotation-card.is-editing>.annotation-card-actions{display:none}.annotation-card.is-editing #annotation-form{margin:0}
|
|
36
|
-
.annotation-highlight{background:#fff8c5;border-bottom:2px solid #bf8700;cursor:pointer}.annotation-highlight.is-focused{background:var(--bgColor-accent-muted,#ddf4ff);border-bottom-color:var(--borderColor-accent-emphasis,#0969da)}
|
|
37
|
-
#annotation-form{margin-top:12px}.annotation-form-label{display:block;margin-bottom:6px;color:var(--fgColor-muted,#59636e);font-size:.75rem;font-weight:600}.annotation-form-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:8px}.annotations-panel .annotation-form-actions button{margin:0}.annotations-panel .annotation-form-actions button[type="submit"]{color:#fff;background:var(--button-primary-bgColor-rest,#1f883d);border-color:var(--button-primary-borderColor-rest,#1f232826);box-shadow:0 1px 0 rgba(31,35,40,.1)}.annotations-panel .annotation-form-actions button[type="submit"]:hover{background:var(--button-primary-bgColor-hover,#1a7f37)}#annotation-form textarea{box-sizing:border-box;width:100%;min-height:96px;display:block;resize:vertical;margin:0;padding:8px 10px;color:var(--fgColor-default,#1f2328);font:inherit;line-height:1.45;background:var(--bgColor-default,#fff);border:1px solid var(--borderColor-default,#d0d7de);border-radius:6px;outline:0}#annotation-form textarea:focus{border-color:var(--borderColor-accent-emphasis,#0969da);box-shadow:0 0 0 3px var(--bgColor-accent-muted,#ddf4ff)}#annotation-form [role="status"]{margin:6px 0 0;color:var(--fgColor-muted,#59636e);font-size:.75rem}
|
|
38
|
-
@media(prefers-reduced-motion:reduce){.annotation-card-actions{transition:none}}
|
|
39
|
-
`;
|
|
40
|
-
const script = `<script>(()=>{
|
|
41
|
-
const dataElement=document.getElementById('marksites-annotations-data');if(!dataElement)return;
|
|
42
|
-
let state=JSON.parse(dataElement.textContent||'{}'),editable=false,pendingSelection=null,editingId=null;
|
|
43
|
-
const content=document.querySelector('.markdown-content'),toolbar=document.getElementById('selection-actions'),addDocumentComment=document.querySelector('[data-add-document-comment]'),copyAllComments=document.querySelector('[data-copy-all-comments]'),list=document.getElementById('annotations-list'),empty=document.getElementById('annotations-empty'),count=document.getElementById('annotation-count'),panelStatus=document.getElementById('annotations-status'),form=document.getElementById('annotation-form'),formHome=document.createComment('annotation-form-home');form.before(formHome);
|
|
44
|
-
const excluded='button,textarea,input,.selection-actions,.annotations-panel,.file-tree,.table-of-contents,.code-block-actions';
|
|
45
|
-
const groupParameter='comments',groupDefinitions=[['comments',''],['archived','アーカイブ']],validGroups=new Set(['archived']),openGroups=new Set((new URL(location.href).searchParams.get(groupParameter)||'').split(',').filter(key=>validGroups.has(key)));
|
|
46
|
-
function syncGroupState(){const updateUrl=url=>{url.searchParams.delete(groupParameter);if(openGroups.size)url.searchParams.set(groupParameter,groupDefinitions.map(([key])=>key).filter(key=>openGroups.has(key)).join(','));return url};history.replaceState(null,'',updateUrl(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=updateUrl(url).href}}
|
|
47
|
-
function resetFormPosition(){form.hidden=true;formHome.after(form)}
|
|
48
|
-
function openForm(card=null){resetFormPosition();form.reset();form.hidden=false;if(card){card.classList.add('is-editing');card.append(form)}else list.before(form);form.querySelector('textarea').focus()}
|
|
49
|
-
function copy(text){if(navigator.clipboard&&location.protocol!=='file:')return navigator.clipboard.writeText(text);const area=document.createElement('textarea');area.value=text;area.style.position='fixed';area.style.opacity='0';document.body.append(area);area.select();const ok=document.execCommand('copy');area.remove();return ok?Promise.resolve():Promise.reject(new Error('コピーに失敗しました'))}
|
|
50
|
-
function formatTimestamp(value){const date=new Date(value),pad=part=>String(part).padStart(2,'0');if(Number.isNaN(date.getTime()))return value;return date.getFullYear()+'-'+pad(date.getMonth()+1)+'-'+pad(date.getDate())+' '+pad(date.getHours())+':'+pad(date.getMinutes())+':'+pad(date.getSeconds())}
|
|
51
|
-
function sortedAnnotations(){return[...state.annotations].sort((left,right)=>Date.parse(right.createdAt)-Date.parse(left.createdAt))}
|
|
52
|
-
function annotationMetadata(a,title='コメント'){const located=a.selection.headingId?null:locate(a),headingId=a.selection.headingId||headingFor(located?.startContainer),heading=headingId?document.getElementById(headingId):null,headingText=heading?.textContent?.trim()||'なし',documentComment=a.selection.exact==='';return['# '+title,'文書: '+state.document,'見出し: '+headingText,'','引用:',documentComment?'文書全体':'> '+a.selection.exact.replace(/\\n/g,'\\n> '),'','コメント:',a.comment.body].join('\\n')}
|
|
53
|
-
function allCommentsMetadata(){const annotations=sortedAnnotations().filter(a=>a.status==='open');return annotations.length?annotations.map((a,index)=>annotationMetadata(a,'コメント '+(index+1))).join('\\n\\n'):['# コメント','文書: '+state.document,'','有効なコメントはありません。'].join('\\n')}
|
|
54
|
-
function headingFor(node){if(!node)return null;const el=node.nodeType===1?node:node.parentElement,direct=el&&el.closest('h1,h2,h3,h4,h5,h6');if(direct)return direct.id;let previous=null;for(const heading of content.querySelectorAll('h1,h2,h3,h4,h5,h6')){if(heading.compareDocumentPosition(node)&Node.DOCUMENT_POSITION_FOLLOWING)previous=heading;else break}return previous?previous.id:null}
|
|
55
|
-
function selectionData(){const selection=getSelection();if(!selection||selection.rangeCount!==1||selection.isCollapsed)return null;const range=selection.getRangeAt(0);if(!content.contains(range.commonAncestorContainer)||(range.commonAncestorContainer.parentElement&&range.commonAncestorContainer.parentElement.closest(excluded)))return null;const exact=selection.toString().trim();if(!exact)return null;const all=content.innerText,index=all.indexOf(exact);return{exact,prefix:index<0?'':all.slice(Math.max(0,index-40),index),suffix:index<0?'':all.slice(index+exact.length,index+exact.length+40),headingId:headingFor(range.startContainer),startOffset:Math.max(0,index),endOffset:Math.max(0,index)+exact.length}}
|
|
56
|
-
document.addEventListener('selectionchange',()=>{clearTimeout(window.__marksitesSelectionTimer);window.__marksitesSelectionTimer=setTimeout(()=>{const next=selectionData();if(!next){toolbar.hidden=true;return}pendingSelection=next;const r=getSelection().getRangeAt(0).getBoundingClientRect();toolbar.style.left=Math.max(8,Math.min(innerWidth-toolbar.offsetWidth-8,r.left))+'px';toolbar.style.top=Math.max(8,r.top-44)+'px';toolbar.hidden=false},80)});
|
|
57
|
-
toolbar.addEventListener('mousedown',e=>e.preventDefault());toolbar.addEventListener('click',async e=>{const button=e.target.closest('button');if(!button||!pendingSelection)return;const label=button.querySelector('[data-copy-label]');try{if(button.dataset.selectionAction==='copy')await copy(pendingSelection.exact);if(button.dataset.selectionAction==='ai')await copy('文書: '+state.document+'\\n見出し: '+(pendingSelection.headingId||'なし')+'\\n\\n> '+pendingSelection.exact.replace(/\\n/g,'\\n> '));if(button.dataset.selectionAction==='comment'&&editable){editingId=null;dispatchEvent(new Event('marksites:show-comments'));openForm()}if(label){label.textContent='コピーしました';setTimeout(()=>label.textContent=button.dataset.selectionAction==='ai'?'AI向けコピー':'コピー',1000)}}catch{if(label)label.textContent='コピー失敗'}});
|
|
58
|
-
addDocumentComment.addEventListener('click',()=>{if(!editable)return;editingId=null;pendingSelection={exact:'',prefix:'',suffix:'',headingId:null,startOffset:0,endOffset:0};openForm()});
|
|
59
|
-
copyAllComments.addEventListener('click',async()=>{const label=copyAllComments.querySelector('[data-copy-comments-label]');try{await copy(allCommentsMetadata());label.textContent='コピーしました';setTimeout(()=>label.textContent='コメントをすべてコピー',1000)}catch{label.textContent='コピー失敗'}});
|
|
60
|
-
function locate(a){const preferred=a.selection.headingId?document.getElementById(a.selection.headingId):null,roots=preferred&&preferred!==content?[preferred,content]:[content];for(const root of roots){const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT);let node,text='',nodes=[];while(node=walker.nextNode()){nodes.push([node,text.length]);text+=node.data}let index=text.indexOf(a.selection.exact);if(index<0)continue;const matches=[];while(index>=0){matches.push(index);index=text.indexOf(a.selection.exact,index+1)}if(matches.length>1){const contextual=matches.find(i=>text.slice(Math.max(0,i-a.selection.prefix.length),i)===a.selection.prefix&&text.slice(i+a.selection.exact.length,i+a.selection.exact.length+a.selection.suffix.length)===a.selection.suffix);index=contextual??matches.reduce((best,current)=>Math.abs(current-a.selection.startOffset)<Math.abs(best-a.selection.startOffset)?current:best)}else index=matches[0];let start,end,so=0,eo=0;for(const [n,offset] of nodes){if(!start&&offset+n.data.length>=index){start=n;so=index-offset}if(offset+n.data.length>=index+a.selection.exact.length){end=n;eo=index+a.selection.exact.length-offset;break}}if(!start||!end)continue;const range=document.createRange();range.setStart(start,so);range.setEnd(end,eo);return range}return null}
|
|
61
|
-
function activeAnnotations(){return state.annotations.filter(a=>a.status==='open')}
|
|
62
|
-
function updateCurrentFileCount(){const links=document.querySelectorAll('.file-tree a[aria-current="page"]'),total=activeAnnotations().length;for(const link of links){let badge=link.querySelector('.file-tree-comment-count');if(total===0){if(badge)badge.remove();continue}if(!badge){badge=document.createElement('span');badge.className='file-tree-comment-count';link.append(badge)}badge.textContent=String(total);badge.setAttribute('aria-label','コメント'+total+'件')}}
|
|
63
|
-
function classify(a){const key=a.status==='archived'?'archived':'comments';if(a.selection.exact==='')return{key,kind:'document',range:null};const range=locate(a);return range&&!range.collapsed?{key,kind:'quoted',range}:{key,kind:'unavailable',range:null}}
|
|
64
|
-
function render(){resetFormPosition();document.querySelectorAll('.annotation-highlight').forEach(el=>el.replaceWith(...el.childNodes));list.textContent='';const active=activeAnnotations();count.textContent=String(active.length);empty.hidden=state.annotations.length!==0;updateCurrentFileCount();const classified=sortedAnnotations().map(a=>({a,...classify(a)}));for(const [key,title] of groupDefinitions){const items=classified.filter(item=>item.key===key),section=document.createElement('section');section.className='annotation-group';section.dataset.commentGroup=key;const groupBody=document.createElement('div');groupBody.className='annotation-group-body';groupBody.hidden=key==='archived'&&!openGroups.has(key);if(key==='archived'){const header=document.createElement('div');header.className='annotation-group-header';const toggle=document.createElement('button'),expanded=openGroups.has(key),tip=expanded?'アーカイブを閉じる':'アーカイブを開く';toggle.type='button';toggle.className='annotation-group-toggle';toggle.dataset.toggleCommentGroup=key;toggle.dataset.tooltip=tip;toggle.title=tip;toggle.setAttribute('aria-label',tip);toggle.setAttribute('aria-expanded',String(expanded));toggle.innerHTML='<svg class="panel-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg><span class="annotation-group-title">'+title+'</span>';header.append(toggle);section.append(header)}if(items.length===0){const noItems=document.createElement('p');noItems.className='annotation-group-empty';noItems.textContent='コメントはありません。';groupBody.append(noItems)}section.append(groupBody);list.append(section);for(const {a,range,kind} of items){const card=document.createElement('article');card.className='annotation-card';card.dataset.id=a.id;card.tabIndex=0;if(kind==='quoted'){const quote=document.createElement('blockquote');quote.className='annotation-quote';quote.textContent=a.selection.exact;card.append(quote)}else{const context=document.createElement('small');context.className='annotation-context';context.textContent=kind==='document'?'文書全体':'引用先なし';card.append(context);if(kind==='unavailable'){const source=document.createElement('div');source.className='annotation-source';source.textContent=a.selection.exact;card.append(source)}}const body=document.createElement('p');body.textContent=a.comment.body;const meta=document.createElement('div');meta.className='annotation-meta';const kindLabel=document.createElement('span');kindLabel.className='annotation-kind';kindLabel.textContent=kind==='quoted'?'引用':kind==='unavailable'?'引用先なし':'文書全体';const updated=document.createElement('time');updated.className='annotation-updated';updated.dateTime=a.updatedAt;updated.textContent='更新 '+formatTimestamp(a.updatedAt);meta.append(kindLabel,updated);card.append(body,meta);const actions=document.createElement('div');actions.className='annotation-card-actions';const availableActions=[['コメントをコピー','copy',${JSON.stringify(renderCopyIcon())}]];if(editable){if(a.status==='archived')availableActions.push(['コメントを復元','restore',${JSON.stringify(renderRestoreIcon())}]);else availableActions.push(['コメントを編集','edit',${JSON.stringify(renderEditIcon())}],['コメントをアーカイブ','archive',${JSON.stringify(renderArchiveIcon())}]);availableActions.push(['コメントを削除','delete',${JSON.stringify(renderDeleteIcon())}])}for(const [label,action,icon] of availableActions){const b=document.createElement('button');b.type='button';b.innerHTML=icon;b.setAttribute('aria-label',label);b.title=label;b.dataset.tooltip=label;b.dataset.annotationAction=action;b.dataset.id=a.id;actions.append(b)}card.append(actions);groupBody.append(card);if(kind==='quoted'&&range&&a.status==='open'){const mark=document.createElement('mark');mark.className='annotation-highlight';mark.dataset.annotationId=a.id;try{range.surroundContents(mark)}catch{}}}}}
|
|
65
|
-
function focusAnnotation(id){list.querySelectorAll('.annotation-card.is-focused').forEach(item=>item.classList.remove('is-focused'));document.querySelectorAll('.annotation-highlight.is-focused').forEach(item=>item.classList.remove('is-focused'));const escaped=CSS.escape(id),card=list.querySelector('[data-id="'+escaped+'"]'),mark=document.querySelector('.annotation-highlight[data-annotation-id="'+escaped+'"]');if(card)card.classList.add('is-focused');if(mark)mark.classList.add('is-focused');return{card,mark}}
|
|
66
|
-
function showAnnotation(id){dispatchEvent(new Event('marksites:show-comments'));const annotation=state.annotations.find(item=>item.id===id),focused=focusAnnotation(id);if(!annotation||annotation.selection.exact==='')return;if(focused.mark)focused.mark.scrollIntoView({behavior:'smooth',block:'center'})}
|
|
67
|
-
function setGroupOpen(key,open,sync=true){const section=list.querySelector('[data-comment-group="'+CSS.escape(key)+'"]');if(open)openGroups.add(key);else openGroups.delete(key);if(section){const toggle=section.querySelector('[data-toggle-comment-group]'),tip=open?'アーカイブを閉じる':'アーカイブを開く';toggle.setAttribute('aria-expanded',String(open));toggle.setAttribute('aria-label',tip);toggle.dataset.tooltip=tip;toggle.title=tip;section.querySelector('.annotation-group-body').hidden=!open}if(sync)syncGroupState()}
|
|
68
|
-
list.addEventListener('click',e=>{const toggle=e.target.closest('[data-toggle-comment-group]');if(toggle)setGroupOpen(toggle.dataset.toggleCommentGroup,toggle.getAttribute('aria-expanded')!=='true')});
|
|
69
|
-
document.addEventListener('click',async e=>{const mark=e.target.closest('.annotation-highlight');if(mark){dispatchEvent(new Event('marksites:show-comments'));const focused=focusAnnotation(mark.dataset.annotationId);if(focused.card)focused.card.scrollIntoView({block:'nearest'})}const card=e.target.closest('.annotation-card');if(card&&!e.target.closest('button,textarea,form'))showAnnotation(card.dataset.id);const action=e.target.closest('[data-annotation-action]');if(action){const a=state.annotations.find(x=>x.id===action.dataset.id);if(!a)return;focusAnnotation(a.id);if(action.dataset.annotationAction==='copy'){try{await copy(annotationMetadata(a));action.setAttribute('aria-label','コメントをコピーしました');action.title='コピーしました';action.dataset.tooltip='コピーしました';setTimeout(()=>{action.setAttribute('aria-label','コメントをコピー');action.title='コメントをコピー';action.dataset.tooltip='コメントをコピー'},1000)}catch{action.title='コピーに失敗しました';action.dataset.tooltip='コピーに失敗しました'}}else if(action.dataset.annotationAction==='edit'){editingId=a.id;pendingSelection=a.selection;dispatchEvent(new Event('marksites:show-comments'));openForm(card);form.body.value=a.comment.body;form.body.focus()}else if(action.dataset.annotationAction==='archive')mutate('PATCH','/annotations/'+encodeURIComponent(a.id),{baseRevision:state.revision,status:'archived'});else if(action.dataset.annotationAction==='restore')mutate('PATCH','/annotations/'+encodeURIComponent(a.id),{baseRevision:state.revision,status:'open'});else if(action.dataset.annotationAction==='delete'&&confirm(window.marksitesTranslate?window.marksitesTranslate('このコメントを削除しますか?'):'このコメントを削除しますか?'))mutate('DELETE','/annotations/'+encodeURIComponent(a.id),{baseRevision:state.revision})}});
|
|
70
|
-
document.addEventListener('pointerdown',e=>{if(e.pointerType!=='touch')return;const card=e.target.closest('.annotation-card');list.querySelectorAll('.annotation-card.is-touch-actions').forEach(item=>item.classList.toggle('is-touch-actions',item===card));if(card)card.classList.add('is-touch-actions')});
|
|
71
|
-
list.addEventListener('keydown',e=>{const card=e.target.closest('.annotation-card');if(card&&!e.target.closest('form')&&(e.key==='Enter'||e.key===' ')){e.preventDefault();showAnnotation(card.dataset.id)}});
|
|
72
|
-
form.addEventListener('submit',e=>{e.preventDefault();const body=form.body.value;if(editingId)mutate('PATCH','/annotations/'+encodeURIComponent(editingId),{baseRevision:state.revision,comment:{body}});else mutate('POST','/annotations',{document:state.document,baseRevision:state.revision,selection:pendingSelection,comment:{body}})});form.querySelector('[data-cancel-comment]').addEventListener('click',()=>{editingId=null;render()});
|
|
73
|
-
async function mutate(method,path,body){body.document=state.document;const formStatus=form.querySelector('[role=status]'),previousIds=new Set(state.annotations.map(item=>item.id)),setStatus=value=>{formStatus.textContent=value;panelStatus.textContent=value};setStatus('保存中…');try{const response=await fetch('/_marksites/api/v1'+path,{method,headers:{'content-type':'application/json'},body:JSON.stringify(body)});const result=await response.json();if(response.status===409){const latest=await fetch('/_marksites/api/v1/annotations?document='+encodeURIComponent(state.document),{cache:'no-store'}).then(r=>r.json());if(latest.data)state=latest.data;render();setStatus('別の画面でコメントが更新されました。最新の内容を確認して、もう一度操作してください。');return}if(!response.ok)throw new Error(result.error&&result.error.message||'保存に失敗しました');state=result.data;const created=method==='POST'?state.annotations.find(item=>!previousIds.has(item.id)):null;form.hidden=true;setStatus('');render();syncGroupState();dispatchEvent(new Event('marksites:show-comments'));if(created){const focused=focusAnnotation(created.id);focused.card?.focus({preventScroll:true});focused.card?.scrollIntoView({block:'nearest'})}}catch(error){setStatus(error.message);if(error instanceof TypeError){editable=false;toolbar.querySelector('[data-selection-action=comment]').disabled=true;addDocumentComment.disabled=true;render()}}}
|
|
74
|
-
async function connect(){if(location.protocol==='file:')return;try{const controller=new AbortController();setTimeout(()=>controller.abort(),1500);const response=await fetch('/_marksites/api/v1/runtime',{cache:'no-store',signal:controller.signal});const runtime=await response.json();if(runtime.data&&runtime.data.service==='marksites'&&runtime.data.apiVersion===1&&runtime.data.projectId&&runtime.data.capabilities.includes('annotations:write')){editable=true;const selectionComment=toolbar.querySelector('[data-selection-action=comment]');selectionComment.disabled=false;selectionComment.title='選択範囲にコメントを追加';addDocumentComment.disabled=false;addDocumentComment.title='文書全体にコメントを追加';const latest=await fetch('/_marksites/api/v1/annotations?document='+encodeURIComponent(state.document),{cache:'no-store'}).then(r=>r.json());if(latest.data)state=latest.data;render()}}catch{}}
|
|
75
|
-
render();syncGroupState();connect();
|
|
76
|
-
})()</script>`;
|
|
77
|
-
return { markup, panel, styles, script, count: countActiveAnnotations(data) };
|
|
78
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import hljs from "highlight.js/lib/common";
|
|
2
|
-
import { escapeHtml } from "../utils/html.js";
|
|
3
|
-
import { renderCopyIcon, renderWrapIcon } from "../utils/icons.js";
|
|
4
|
-
function renderCodeBlockScript() {
|
|
5
|
-
return `<script>
|
|
6
|
-
(() => {
|
|
7
|
-
const copyText = async (text) => {
|
|
8
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
9
|
-
await navigator.clipboard.writeText(text);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const textarea = document.createElement('textarea');
|
|
14
|
-
textarea.value = text;
|
|
15
|
-
textarea.style.position = 'fixed';
|
|
16
|
-
textarea.style.opacity = '0';
|
|
17
|
-
document.body.append(textarea);
|
|
18
|
-
textarea.select();
|
|
19
|
-
document.execCommand('copy');
|
|
20
|
-
textarea.remove();
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
document.addEventListener('click', async (event) => {
|
|
24
|
-
const button = event.target.closest('[data-code-action]');
|
|
25
|
-
if (!button) return;
|
|
26
|
-
const block = button.closest('.code-block');
|
|
27
|
-
if (!block) return;
|
|
28
|
-
|
|
29
|
-
if (button.dataset.codeAction === 'copy') {
|
|
30
|
-
const code = block.querySelector('code');
|
|
31
|
-
if (!code) return;
|
|
32
|
-
try {
|
|
33
|
-
await copyText(code.textContent);
|
|
34
|
-
const label = button.querySelector('.code-tool-label');
|
|
35
|
-
const previous = label.textContent;
|
|
36
|
-
label.textContent = 'コピーしました';
|
|
37
|
-
button.setAttribute('aria-label', 'コードをコピーしました');
|
|
38
|
-
setTimeout(() => {
|
|
39
|
-
label.textContent = previous;
|
|
40
|
-
button.setAttribute('aria-label', 'コードをコピー');
|
|
41
|
-
}, 1600);
|
|
42
|
-
} catch {
|
|
43
|
-
button.setAttribute('aria-label', 'コードをコピーできませんでした');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (button.dataset.codeAction === 'wrap') {
|
|
48
|
-
const wrapped = block.classList.toggle('is-wrapped');
|
|
49
|
-
button.setAttribute('aria-pressed', String(wrapped));
|
|
50
|
-
button.setAttribute('aria-label', wrapped ? '折り返しを解除' : '長い行を折り返す');
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
})();
|
|
54
|
-
</script>`;
|
|
55
|
-
}
|
|
56
|
-
function renderCodeBlock(code, language, highlighted = false) {
|
|
57
|
-
const languageClass = `${highlighted ? "hljs " : ""}${language ? `language-${escapeHtml(language)}` : ""}`.trim();
|
|
58
|
-
const languageLabel = language
|
|
59
|
-
? `<span class="code-language">${escapeHtml(language)}</span>`
|
|
60
|
-
: "<span></span>";
|
|
61
|
-
return `<div class="code-block">
|
|
62
|
-
<div class="code-toolbar">
|
|
63
|
-
${languageLabel}
|
|
64
|
-
<div class="code-tools">
|
|
65
|
-
<button type="button" class="code-tool" data-code-action="wrap" aria-label="長い行を折り返す" aria-pressed="false">${renderWrapIcon()}<span class="code-tool-label">折り返す</span></button>
|
|
66
|
-
<button type="button" class="code-tool" data-code-action="copy" aria-label="コードをコピー">${renderCopyIcon()}<span class="code-tool-label">コピー</span></button>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<pre><code${languageClass ? ` class="${languageClass}"` : ""}>${code}</code></pre>
|
|
70
|
-
</div>\n`;
|
|
71
|
-
}
|
|
72
|
-
export function createCodeBlocksFeature(renderer, highlight) {
|
|
73
|
-
let hasCodeBlocks = false;
|
|
74
|
-
renderer.code = ({ text, lang }) => {
|
|
75
|
-
hasCodeBlocks = true;
|
|
76
|
-
const requestedLanguage = lang?.trim().split(/\s+/, 1)[0];
|
|
77
|
-
if (highlight && requestedLanguage && hljs.getLanguage(requestedLanguage)) {
|
|
78
|
-
const result = hljs.highlight(text, {
|
|
79
|
-
language: requestedLanguage,
|
|
80
|
-
ignoreIllegals: true,
|
|
81
|
-
});
|
|
82
|
-
return renderCodeBlock(result.value, requestedLanguage, true);
|
|
83
|
-
}
|
|
84
|
-
return renderCodeBlock(escapeHtml(text), requestedLanguage);
|
|
85
|
-
};
|
|
86
|
-
return {
|
|
87
|
-
renderScript: () => (hasCodeBlocks ? renderCodeBlockScript() : ""),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
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;
|
|
@@ -1,325 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/features/header.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
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 {};
|
package/dist/features/sidebar.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Renderer } from "marked";
|
|
2
|
-
interface TableOfContentsConfig {
|
|
3
|
-
enabled: boolean;
|
|
4
|
-
title: string;
|
|
5
|
-
minDepth: number;
|
|
6
|
-
maxDepth: number;
|
|
7
|
-
}
|
|
8
|
-
export interface TableOfContentsFeature {
|
|
9
|
-
render(): {
|
|
10
|
-
markup: string;
|
|
11
|
-
script: string;
|
|
12
|
-
title: string;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
export declare function createTableOfContentsFeature(renderer: Renderer, config: TableOfContentsConfig): TableOfContentsFeature;
|
|
16
|
-
export {};
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import GithubSlugger from "github-slugger";
|
|
2
|
-
import { escapeHtml, plainTextFromHtml } from "../utils/html.js";
|
|
3
|
-
function renderTableOfContents(items, title, minDepth) {
|
|
4
|
-
if (items.length === 0)
|
|
5
|
-
return "";
|
|
6
|
-
const links = items
|
|
7
|
-
.map(({ depth, id, text }) => ` <li style="--toc-level: ${depth - minDepth}"><a href="#${escapeHtml(id)}">${escapeHtml(text)}</a></li>`)
|
|
8
|
-
.join("\n");
|
|
9
|
-
const escapedTitle = escapeHtml(title);
|
|
10
|
-
return `<nav class="table-of-contents sidebar-panel" id="sidebar-panel-toc" role="tabpanel" aria-labelledby="sidebar-tab-toc" aria-label="${escapedTitle}">
|
|
11
|
-
<div class="toc-panel">
|
|
12
|
-
<ul>
|
|
13
|
-
${links}
|
|
14
|
-
</ul>
|
|
15
|
-
</div>
|
|
16
|
-
</nav>
|
|
17
|
-
`;
|
|
18
|
-
}
|
|
19
|
-
function renderTableOfContentsScript() {
|
|
20
|
-
return `<script>
|
|
21
|
-
(() => {
|
|
22
|
-
const navigation = document.querySelector('.table-of-contents');
|
|
23
|
-
if (!navigation) return;
|
|
24
|
-
|
|
25
|
-
const panel = navigation.querySelector('.toc-panel');
|
|
26
|
-
const links = [...panel.querySelectorAll('a[href^="#"]')];
|
|
27
|
-
const entries = links
|
|
28
|
-
.map((link) => ({ link, heading: document.getElementById(link.getAttribute('href').slice(1)) }))
|
|
29
|
-
.filter((entry) => entry.heading);
|
|
30
|
-
if (entries.length === 0) return;
|
|
31
|
-
|
|
32
|
-
let scheduled = false;
|
|
33
|
-
const update = () => {
|
|
34
|
-
scheduled = false;
|
|
35
|
-
const marker = Math.min(160, window.innerHeight * 0.25);
|
|
36
|
-
let active = entries[0];
|
|
37
|
-
|
|
38
|
-
for (const entry of entries) {
|
|
39
|
-
if (entry.heading.getBoundingClientRect().top > marker) break;
|
|
40
|
-
active = entry;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
if (entry === active) entry.link.setAttribute('aria-current', 'location');
|
|
45
|
-
else entry.link.removeAttribute('aria-current');
|
|
46
|
-
}
|
|
47
|
-
const navigationRect = navigation.getBoundingClientRect();
|
|
48
|
-
const activeRect = active.link.getBoundingClientRect();
|
|
49
|
-
if (!navigation.hidden && (activeRect.top < navigationRect.top || activeRect.bottom > navigationRect.bottom)) {
|
|
50
|
-
navigation.scrollTop += activeRect.top - navigationRect.top - navigation.clientHeight / 2;
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
const schedule = () => {
|
|
54
|
-
if (scheduled) return;
|
|
55
|
-
scheduled = true;
|
|
56
|
-
requestAnimationFrame(update);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
addEventListener('scroll', schedule, { passive: true });
|
|
60
|
-
addEventListener('resize', schedule);
|
|
61
|
-
update();
|
|
62
|
-
})();
|
|
63
|
-
</script>`;
|
|
64
|
-
}
|
|
65
|
-
export function createTableOfContentsFeature(renderer, config) {
|
|
66
|
-
const items = [];
|
|
67
|
-
const slugger = new GithubSlugger();
|
|
68
|
-
renderer.heading = ({ tokens, depth }) => {
|
|
69
|
-
const renderedText = renderer.parser.parseInline(tokens);
|
|
70
|
-
const plainText = plainTextFromHtml(renderedText);
|
|
71
|
-
const id = slugger.slug(plainText);
|
|
72
|
-
if (config.enabled &&
|
|
73
|
-
depth >= config.minDepth &&
|
|
74
|
-
depth <= config.maxDepth) {
|
|
75
|
-
items.push({ depth, id, text: plainText });
|
|
76
|
-
}
|
|
77
|
-
return `<h${depth} id="${escapeHtml(id)}">${renderedText}</h${depth}>\n`;
|
|
78
|
-
};
|
|
79
|
-
return {
|
|
80
|
-
render() {
|
|
81
|
-
const markup = config.enabled
|
|
82
|
-
? renderTableOfContents(items, config.title, config.minDepth)
|
|
83
|
-
: "";
|
|
84
|
-
return {
|
|
85
|
-
markup,
|
|
86
|
-
script: markup ? renderTableOfContentsScript() : "",
|
|
87
|
-
title: config.title,
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|