(function () { 'use strict'; const documentPath = document.querySelector('[data-document-path]')?.dataset.documentPath; const documentHash = document.querySelector('[data-document-hash]')?.dataset.documentHash; const documentId = documentPath ? 'doc:' + documentPath.replace(/\.md$/, '') : null; if (!documentId) return; // Hash a paragraph's text content for anchoring function hashParagraph(el) { const text = (el.textContent || '').trim(); if (!text) return null; // Simple hash: first 16 chars of base64 of char codes (deterministic) 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.title = 'Add comment'; btn.innerHTML = '+'; btn.addEventListener('click', () => promptComment(el, hash)); el.appendChild(btn); } 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()); // Refresh comments display 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) { // Remove existing comment list const existing = el.querySelector('.comment-list'); if (existing) existing.remove(); if (!comments.length) return; const list = document.createElement('div'); list.className = 'comment-list'; comments.forEach(c => { const item = document.createElement('div'); item.className = 'comment-item'; item.innerHTML = `
${escapeHtml(c.authorName || 'Anonymous')}
${escapeHtml(c.content)}
`; list.appendChild(item); }); el.appendChild(list); } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatDate(iso) { const d = new Date(iso); return d.toLocaleString(); } // Initialize: hash all paragraphs in markdown body function init() { const body = document.querySelector('.markdown-body'); if (!body) return; const paragraphs = body.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, blockquote, pre'); paragraphs.forEach(el => { const hash = hashParagraph(el); if (!hash) return; el.dataset.anchorHash = hash; el.classList.add('commentable'); addCommentButton(el, hash); loadCommentsForAnchor(el, hash); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();