(function () { 'use strict'; const documentPath = document.querySelector('[data-document-path]')?.dataset.documentPath; const documentHash = document.querySelector('[data-document-hash]')?.dataset.documentHash; const canComment = document.querySelector('[data-document-path]')?.dataset.canComment === 'true'; const article = document.querySelector('[data-document-path]'); const workspaceShell = article?.closest('.workspace-shell--document'); const commentsAsideList = document.querySelector('[data-comments-aside-list]'); const stripTrack = document.querySelector('[data-comments-strip-track]'); const documentId = documentPath ? 'doc:' + documentPath.replace(/\.md$/, '') : null; const commentToggle = document.querySelector('[data-comment-toggle]'); const commentVisibilityKey = documentId ? `cairnquire:document-comments:${documentId}` : null; function readCommentVisibility() { if (!commentVisibilityKey) return false; try { return window.localStorage.getItem(commentVisibilityKey) === 'hidden'; } catch (err) { return false; } } if (!documentId) return; function syncVisibility() { const hidden = readCommentVisibility(); if (article) article.classList.toggle('comments-hidden', hidden); if (workspaceShell) workspaceShell.classList.toggle('comments-visible', !hidden); } syncVisibility(); function commentsHidden() { return Boolean(workspaceShell?.classList.contains('comments-visible')) === false; } function setCommentsHidden(hidden) { if (article) article.classList.toggle('comments-hidden', hidden); if (workspaceShell) workspaceShell.classList.toggle('comments-visible', !hidden); if (commentVisibilityKey) { try { window.localStorage.setItem(commentVisibilityKey, hidden ? 'hidden' : 'visible'); } catch (err) { // Ignore persistence failures } } if (commentToggle) { commentToggle.setAttribute('aria-pressed', hidden ? 'false' : 'true'); commentToggle.setAttribute('aria-label', hidden ? 'Show comments' : 'Hide comments'); commentToggle.title = hidden ? 'Show comments' : 'Hide comments'; } if (!hidden) { renderCommentsAside(); updateStripMarkers(); } } if (commentToggle) { commentToggle.setAttribute('aria-pressed', commentsHidden() ? 'false' : 'true'); commentToggle.addEventListener('click', () => { setCommentsHidden(!commentsHidden()); }); } function hashParagraph(el) { const text = (el.textContent || '').trim(); if (!text) return null; let hash = 0; for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return 'h' + Math.abs(hash).toString(36); } function addCommentButton(el, hash) { const btn = document.createElement('button'); btn.className = 'comment-anchor-btn'; btn.type = 'button'; btn.title = 'Add comment'; btn.setAttribute('aria-label', 'Add comment'); btn.innerHTML = ` Add comment `; btn.addEventListener('click', () => promptComment(el, hash)); el.appendChild(btn); } function updateCommentButtonState(el, count) { const btn = el.querySelector('.comment-anchor-btn'); if (!btn) return; const hasComments = count > 0; btn.classList.toggle('comment-anchor-btn--has-comments', hasComments); btn.setAttribute('aria-label', hasComments ? `${count} comment${count === 1 ? '' : 's'}` : 'Add comment'); btn.title = hasComments ? `${count} comment${count === 1 ? '' : 's'}` : 'Add comment'; const countNode = btn.querySelector('.comment-anchor-count'); if (countNode) { countNode.hidden = !hasComments; countNode.textContent = count > 99 ? '99+' : String(count); } } async function promptComment(el, anchorHash) { const text = window.prompt('Comment on this paragraph:'); if (!text || !text.trim()) return; try { const res = await fetch('/api/comments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documentId, versionHash: documentHash, content: text.trim(), anchorHash }), }); if (!res.ok) throw new Error(await res.text()); await loadCommentsForAnchor(el, anchorHash); } catch (err) { window.alert('Failed to post comment: ' + err.message); } } async function loadCommentsForAnchor(el, anchorHash) { try { const res = await fetch(`/api/comments?document_id=${encodeURIComponent(documentId)}&anchor_hash=${encodeURIComponent(anchorHash)}`); if (!res.ok) return; const data = await res.json(); renderComments(el, data.comments || []); } catch (err) { console.error('load comments', err); } } function renderComments(el, comments) { const existing = el.querySelector('.comment-list'); if (existing) existing.remove(); updateCommentButtonState(el, comments.length); el.dataset.hasComments = comments.length > 0 ? 'true' : 'false'; if (!comments.length) { renderCommentsAside(); updateStripMarkers(); return; } const list = document.createElement('div'); list.className = 'comment-list'; comments.forEach(c => { const item = document.createElement('div'); item.className = 'comment-item'; item.innerHTML = `
No comments yet.
'; return; } commentables.forEach(el => { const comments = []; el.querySelectorAll('.comment-item').forEach(item => { const meta = item.querySelector('.comment-meta'); const bodyEl = item.querySelector('.comment-body'); comments.push({ authorName: meta?.querySelector('strong')?.textContent || 'Anonymous', createdAt: meta?.querySelector('time')?.textContent || '', content: bodyEl?.textContent || '', }); }); if (!comments.length) return; const targetText = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120); const truncated = targetText.length > 120 ? targetText + '…' : targetText; const asideItem = document.createElement('div'); asideItem.className = 'comments-aside-item'; asideItem.dataset.anchorHash = el.dataset.anchorHash; const targetEl = document.createElement('div'); targetEl.className = 'comments-aside-item__target'; targetEl.textContent = truncated || 'Commented section'; targetEl.addEventListener('click', () => { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); asideItem.appendChild(targetEl); comments.forEach(c => { const commentEl = document.createElement('div'); commentEl.className = 'comments-aside-item__comment'; commentEl.innerHTML = `