marksites 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/dist/annotations/model.d.ts +29 -0
- package/dist/annotations/model.js +56 -0
- package/dist/annotations/storage.d.ts +4 -0
- package/dist/annotations/storage.js +31 -0
- package/dist/cli/directory.d.ts +4 -0
- package/dist/cli/directory.js +3 -0
- package/dist/cli/open-browser.d.ts +12 -0
- package/dist/cli/open-browser.js +37 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +125 -0
- package/dist/conversion/directory.d.ts +3 -0
- package/dist/conversion/directory.js +208 -0
- package/dist/conversion/gitignore.d.ts +8 -0
- package/dist/conversion/gitignore.js +43 -0
- package/dist/conversion/manifest.d.ts +8 -0
- package/dist/conversion/manifest.js +40 -0
- package/dist/conversion/navigation.d.ts +4 -0
- package/dist/conversion/navigation.js +70 -0
- package/dist/conversion/paths.d.ts +10 -0
- package/dist/conversion/paths.js +86 -0
- package/dist/conversion/rendering.d.ts +5 -0
- package/dist/conversion/rendering.js +39 -0
- package/dist/conversion/single-file.d.ts +1 -0
- package/dist/conversion/single-file.js +35 -0
- package/dist/conversion/types.d.ts +39 -0
- package/dist/conversion/types.js +1 -0
- package/dist/features/annotations/index.d.ts +7 -0
- package/dist/features/annotations/index.js +79 -0
- package/dist/features/annotations.d.ts +9 -0
- package/dist/features/annotations.js +78 -0
- package/dist/features/code-blocks/index.d.ts +5 -0
- package/dist/features/code-blocks/index.js +97 -0
- package/dist/features/code-blocks.d.ts +5 -0
- package/dist/features/code-blocks.js +89 -0
- package/dist/features/file-tree/index.d.ts +7 -0
- package/dist/features/file-tree/index.js +347 -0
- package/dist/features/file-tree.d.ts +7 -0
- package/dist/features/file-tree.js +325 -0
- package/dist/features/header/index.d.ts +4 -0
- package/dist/features/header/index.js +47 -0
- package/dist/features/header.d.ts +6 -0
- package/dist/features/header.js +47 -0
- package/dist/features/sidebar/index.d.ts +10 -0
- package/dist/features/sidebar/index.js +55 -0
- package/dist/features/sidebar.d.ts +13 -0
- package/dist/features/sidebar.js +55 -0
- package/dist/features/table-of-contents/index.d.ts +16 -0
- package/dist/features/table-of-contents/index.js +91 -0
- package/dist/features/table-of-contents.d.ts +16 -0
- package/dist/features/table-of-contents.js +91 -0
- package/dist/features/types.d.ts +6 -0
- package/dist/features/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/markdown-to-html.d.ts +5 -0
- package/dist/markdown-to-html.js +77 -0
- package/dist/server/annotation-repository.d.ts +35 -0
- package/dist/server/annotation-repository.js +184 -0
- package/dist/server/api.d.ts +4 -0
- package/dist/server/api.js +77 -0
- package/dist/server/constants.d.ts +2 -0
- package/dist/server/constants.js +2 -0
- package/dist/server/html-security.d.ts +4 -0
- package/dist/server/html-security.js +61 -0
- package/dist/server/response.d.ts +2 -0
- package/dist/server/response.js +9 -0
- package/dist/server/server.d.ts +3 -0
- package/dist/server/server.js +77 -0
- package/dist/server/static-files.d.ts +2 -0
- package/dist/server/static-files.js +72 -0
- package/dist/server/types.d.ts +16 -0
- package/dist/server/types.js +1 -0
- package/dist/template/document.d.ts +20 -0
- package/dist/template/document.js +33 -0
- package/dist/template/styles.d.ts +5 -0
- package/dist/template/styles.js +109 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +1 -0
- package/dist/utils/files.d.ts +1 -0
- package/dist/utils/files.js +9 -0
- package/dist/utils/html.d.ts +2 -0
- package/dist/utils/html.js +17 -0
- package/dist/utils/icons.d.ts +8 -0
- package/dist/utils/icons.js +24 -0
- package/package.json +43 -0
|
@@ -0,0 +1,79 @@
|
|
|
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" 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
|
+
<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:var(--fgColor-default,#1f2328)}
|
|
32
|
+
.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
|
+
.annotations-empty{margin:0;padding:28px 12px 20px;color:var(--fgColor-muted,#59636e);font-size:.8125rem;line-height:1.6;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;min-height:40px;padding:0;background:var(--bgColor-muted,#f6f8fa)}.annotations-panel .annotation-group-toggle{display:flex;width:100%;min-width:0;flex:1;align-items:center;gap:6px;padding:10px 12px;background:transparent;border:0;border-radius:0;text-align:left}.annotations-panel .annotation-group-toggle:hover{background:var(--button-default-bgColor-hover,#eaeef2)}.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:0;padding:20px 12px;color:var(--fgColor-muted,#59636e);font-size:.75rem;line-height:1.5;text-align:center}
|
|
35
|
+
.annotation-card{position:relative;margin:0;padding:12px;color:var(--fgColor-default,#1f2328);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 var(--fgColor-accent,#54aeff);color:var(--fgColor-muted,#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;color:var(--fgColor-default,#1f2328);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-context{display:none}.annotation-source{text-decoration:line-through;text-decoration-thickness:1px}.annotation-group[data-comment-group="archived"]:has(.annotation-group-empty),#annotations-list:has(+ .annotations-empty:not([hidden])){display:none}
|
|
37
|
+
.annotation-highlight{color:inherit;background:var(--annotation-highlight-bgColor,#fff8c5);border-bottom:2px solid var(--annotation-highlight-borderColor,#bf8700);cursor:pointer}.annotation-highlight.is-focused{background:var(--bgColor-accent-muted,#ddf4ff);border-bottom-color:var(--borderColor-accent-emphasis,#0969da)}
|
|
38
|
+
#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}
|
|
39
|
+
@media(prefers-reduced-motion:reduce){.annotation-card-actions{transition:none}}
|
|
40
|
+
`;
|
|
41
|
+
const script = `<script>(()=>{
|
|
42
|
+
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);
|
|
45
|
+
const excluded='button,textarea,input,.selection-actions,.annotations-panel,.file-tree,.table-of-contents,.code-block-actions';
|
|
46
|
+
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
|
+
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}}
|
|
48
|
+
function resetFormPosition(){form.hidden=true;formHome.after(form)}
|
|
49
|
+
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()}
|
|
50
|
+
async function copy(text){if(navigator.clipboard&&window.isSecureContext){try{await navigator.clipboard.writeText(text);return}catch{}}const area=document.createElement('textarea');area.value=text;area.style.position='fixed';area.style.opacity='0';document.body.append(area);area.focus();area.select();try{if(!document.execCommand('copy'))throw new Error('コピーに失敗しました')}finally{area.remove()}}
|
|
51
|
+
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())}
|
|
52
|
+
function sortedAnnotations(){return[...state.annotations].sort((left,right)=>Date.parse(right.createdAt)-Date.parse(left.createdAt))}
|
|
53
|
+
function annotationMetadata(a,title='コメント'){const documentComment=a.selection.exact==='',located=(documentComment||a.selection.headingId)?null:locate(a),headingId=a.selection.headingId||headingFor(located?.startContainer),heading=headingId?document.getElementById(headingId):null,headingText=heading?.textContent?.trim()||'なし';return['# '+title,'文書: '+state.document,'見出し: '+headingText,'','引用:',documentComment?'文書全体':'> '+a.selection.exact.replace(/\\n/g,'\\n> '),'','コメント:',a.comment.body].join('\\n')}
|
|
54
|
+
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
|
+
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
|
+
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}}
|
|
57
|
+
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='コピー失敗'}});
|
|
59
|
+
addDocumentComment.addEventListener('click',()=>{if(!editable)return;editingId=null;pendingSelection={exact:'',prefix:'',suffix:'',headingId:null,startOffset:0,endOffset:0};openForm()});
|
|
60
|
+
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
|
+
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}
|
|
62
|
+
function activeAnnotations(){return state.annotations.filter(a=>a.status==='open')}
|
|
63
|
+
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()}else{if(!badge){badge=document.createElement('span');badge.className='file-tree-comment-count';link.append(badge)}badge.textContent=String(total);badge.setAttribute('aria-label','コメント'+total+'件')}}for(const tree of document.querySelectorAll('.file-tree')){for(const directory of [...tree.querySelectorAll('.file-tree-directory')].reverse()){const details=directory.querySelector(':scope > details'),summary=details.querySelector(':scope > summary');let badge=summary.querySelector('.file-tree-directory-comment-count');const count=[...details.querySelectorAll('.file-tree-file .file-tree-comment-count')].reduce((sum,item)=>sum+(Number(item.textContent)||0),0);if(count===0){badge?.remove();continue}if(!badge){badge=document.createElement('span');badge.className='file-tree-comment-count file-tree-directory-comment-count';summary.append(badge)}badge.textContent=String(count);badge.setAttribute('aria-label','配下のコメント'+count+'件')}}}
|
|
64
|
+
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}}
|
|
65
|
+
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{}}}}}
|
|
66
|
+
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}}
|
|
67
|
+
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'})}
|
|
68
|
+
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()}
|
|
69
|
+
list.addEventListener('click',e=>{const toggle=e.target.closest('[data-toggle-comment-group]');if(toggle)setGroupOpen(toggle.dataset.toggleCommentGroup,toggle.getAttribute('aria-expanded')!=='true')});
|
|
70
|
+
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})}});
|
|
71
|
+
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')});
|
|
72
|
+
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)}});
|
|
73
|
+
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()});
|
|
74
|
+
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()}}}
|
|
75
|
+
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{}}
|
|
76
|
+
render();syncGroupState();connect();
|
|
77
|
+
})()</script>`;
|
|
78
|
+
return { markup, panel, styles, script, count: countActiveAnnotations(data) };
|
|
79
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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;
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
try {
|
|
10
|
+
await navigator.clipboard.writeText(text);
|
|
11
|
+
return;
|
|
12
|
+
} catch {
|
|
13
|
+
// Continue with the user-gesture-compatible fallback below.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const textarea = document.createElement('textarea');
|
|
18
|
+
textarea.value = text;
|
|
19
|
+
textarea.style.position = 'fixed';
|
|
20
|
+
textarea.style.opacity = '0';
|
|
21
|
+
document.body.append(textarea);
|
|
22
|
+
textarea.focus();
|
|
23
|
+
textarea.select();
|
|
24
|
+
try {
|
|
25
|
+
if (!document.execCommand('copy')) throw new Error('Copy command failed');
|
|
26
|
+
} finally {
|
|
27
|
+
textarea.remove();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
document.addEventListener('click', async (event) => {
|
|
32
|
+
const button = event.target.closest('[data-code-action]');
|
|
33
|
+
if (!button) return;
|
|
34
|
+
const block = button.closest('.code-block');
|
|
35
|
+
if (!block) return;
|
|
36
|
+
|
|
37
|
+
if (button.dataset.codeAction === 'copy') {
|
|
38
|
+
const code = block.querySelector('code');
|
|
39
|
+
if (!code) return;
|
|
40
|
+
try {
|
|
41
|
+
await copyText(code.textContent);
|
|
42
|
+
const label = button.querySelector('.code-tool-label');
|
|
43
|
+
const previous = label.textContent;
|
|
44
|
+
label.textContent = 'コピーしました';
|
|
45
|
+
button.setAttribute('aria-label', 'コードをコピーしました');
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
label.textContent = previous;
|
|
48
|
+
button.setAttribute('aria-label', 'コードをコピー');
|
|
49
|
+
}, 1600);
|
|
50
|
+
} catch {
|
|
51
|
+
button.setAttribute('aria-label', 'コードをコピーできませんでした');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (button.dataset.codeAction === 'wrap') {
|
|
56
|
+
const wrapped = block.classList.toggle('is-wrapped');
|
|
57
|
+
button.setAttribute('aria-pressed', String(wrapped));
|
|
58
|
+
button.setAttribute('aria-label', wrapped ? '折り返しを解除' : '長い行を折り返す');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
})();
|
|
62
|
+
</script>`;
|
|
63
|
+
}
|
|
64
|
+
function renderCodeBlock(code, language, highlighted = false) {
|
|
65
|
+
const languageClass = `${highlighted ? "hljs " : ""}${language ? `language-${escapeHtml(language)}` : ""}`.trim();
|
|
66
|
+
const languageLabel = language
|
|
67
|
+
? `<span class="code-language">${escapeHtml(language)}</span>`
|
|
68
|
+
: "<span></span>";
|
|
69
|
+
return `<div class="code-block">
|
|
70
|
+
<div class="code-toolbar">
|
|
71
|
+
${languageLabel}
|
|
72
|
+
<div class="code-tools">
|
|
73
|
+
<button type="button" class="code-tool" data-code-action="wrap" aria-label="長い行を折り返す" aria-pressed="false">${renderWrapIcon()}<span class="code-tool-label">折り返す</span></button>
|
|
74
|
+
<button type="button" class="code-tool" data-code-action="copy" aria-label="コードをコピー">${renderCopyIcon()}<span class="code-tool-label">コピー</span></button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<pre><code${languageClass ? ` class="${languageClass}"` : ""}>${code}</code></pre>
|
|
78
|
+
</div>\n`;
|
|
79
|
+
}
|
|
80
|
+
export function createCodeBlocksFeature(renderer, highlight) {
|
|
81
|
+
let hasCodeBlocks = false;
|
|
82
|
+
renderer.code = ({ text, lang }) => {
|
|
83
|
+
hasCodeBlocks = true;
|
|
84
|
+
const requestedLanguage = lang?.trim().split(/\s+/, 1)[0];
|
|
85
|
+
if (highlight && requestedLanguage && hljs.getLanguage(requestedLanguage)) {
|
|
86
|
+
const result = hljs.highlight(text, {
|
|
87
|
+
language: requestedLanguage,
|
|
88
|
+
ignoreIllegals: true,
|
|
89
|
+
});
|
|
90
|
+
return renderCodeBlock(result.value, requestedLanguage, true);
|
|
91
|
+
}
|
|
92
|
+
return renderCodeBlock(escapeHtml(text), requestedLanguage);
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
renderScript: () => (hasCodeBlocks ? renderCodeBlockScript() : ""),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileBreadcrumb, FileTreeOptions } from "../../types.js";
|
|
2
|
+
export declare function renderFileTree(options?: FileTreeOptions): string;
|
|
3
|
+
export declare function renderFileSidebar(options?: FileTreeOptions): string;
|
|
4
|
+
export declare function renderFileTreeScript(enabled: boolean): string;
|
|
5
|
+
export declare function renderBreadcrumbs(breadcrumbs?: FileBreadcrumb[], modifiedAt?: string): string;
|
|
6
|
+
export declare function renderModifiedAt(modifiedAt?: string): string;
|
|
7
|
+
export declare function renderModifiedAtScript(enabled: boolean): string;
|