1(() => { 2 const LANG_KEY = "orzbar_lang"; 3 const root = document.querySelector("#commentsSection"); 4 if (!root) return; 5 6 const slug = new URLSearchParams(window.location.search).get("slug"); 7 if (!slug) { 8 root.hidden = true; 9 return; 10 } 11 12 const copy = { 13 zh: { 14 title: "çè¨", 15 intro: "å¯ä»¥å¿åï¼ä¹å¯ä»¥çä¸ååãçè¨ä¼å ¬å¼æ¾ç¤ºå¨è¿ç¯æç« ä¸ã", 16 name: "ååï¼å¯éï¼", 17 namePlaceholder: "å¿å", 18 message: "çè¨", 19 messagePlaceholder: "åä¸ä½ çæ³æ³â¦", 20 submit: "åå¸çè¨", 21 sending: "æ£å¨åå¸â¦", 22 empty: "è¿æ²¡æçè¨ãæ¥åç¬¬ä¸æ¡å§ã", 23 loadMore: "å è½½æ´æ©çè¨", 24 anonymous: "å¿å访客", 25 success: "çè¨å·²åå¸ã", 26 failure: "ææ¶æ æ³åå¸ï¼è¯·ç¨ååè¯ã", 27 captcha: "ç®åéªè¯", 28 }, 29 en: { 30 title: "Comments", 31 intro: "Post anonymously or leave a name. Your comment will appear publicly below this article.", 32 name: "Name (optional)", 33 namePlaceholder: "Anonymous", 34 message: "Comment", 35 messagePlaceholder: "Share a thoughtâ¦", 36 submit: "Post comment", 37 sending: "Postingâ¦", 38 empty: "No comments yet. Start the conversation.", 39 loadMore: "Load earlier comments", 40 anonymous: "Anonymous visitor", 41 success: "Your comment is live.", 42 failure: "Could not post right now. Please try again.", 43 captcha: "Quick check", 44 }, 45 fr: { 46 title: "Commentaires", 47 intro: "Publiez anonymement ou indiquez votre nom. Votre commentaire apparaîtra publiquement sous cet article.", 48 name: "Nom (facultatif)", 49 namePlaceholder: "Anonyme", 50 message: "Commentaire", 51 messagePlaceholder: "Partagez votre point de vueâ¦", 52 submit: "Publier", 53 sending: "Publicationâ¦", 54 empty: "Aucun commentaire pour le moment. Lancez la discussion.", 55 loadMore: "Afficher les commentaires précédents", 56 anonymous: "Visiteur anonyme", 57 success: "Votre commentaire est publié.", 58 failure: "Publication impossible pour le moment. Veuillez réessayer.", 59 captcha: "Vérification rapide", 60 }, 61 }; 62 63 let comments = []; 64 let nextBeforeId = null; 65 let captchaEnabled = false; 66 let captchaToken = ""; 67 68 function language() { 69 const lang = new URLSearchParams(window.location.search).get("lang") || localStorage.getItem(LANG_KEY); 70 return ["zh", "en", "fr"].includes(lang) ? lang : "en"; 71 } 72 73 function text() { 74 return copy[language()]; 75 } 76 77 function setStatus(message, kind = "") { 78 const status = root.querySelector("#commentStatus"); 79 if (!status) return; 80 status.textContent = message; 81 status.dataset.kind = kind; 82 } 83 84 function commentElement(comment) { 85 const article = document.createElement("article"); 86 article.className = "comment-item"; 87 article.id = `comment-${comment.id}`; 88 const header = document.createElement("header"); 89 const author = document.createElement("strong"); 90 author.textContent = comment.author || text().anonymous; 91 const time = document.createElement("time"); 92 time.dateTime = comment.createdAt; 93 const locale = language() === "zh" ? "zh-CN" : language() === "fr" ? "fr-FR" : "en-US"; 94 time.textContent = new Date(comment.createdAt).toLocaleString(locale, {
95 year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", 96 }); 97 const body = document.createElement("p"); 98 body.textContent = comment.body; 99 header.append(author, time); 100 article.append(header, body); 101 return article; 102 } 103 104 // ä»ç»è®¡é¡µææéé®ä»¶ç¹è¿æ¥ç #comment-<id>ï¼çè¨å表æ¯å¼æ¥å è½½çï¼æ¸²æååå®ä½ä¸æ¬¡ã 105 let linkedCommentFocused = false; 106 function focusLinkedComment() { 107 if (linkedCommentFocused) return; 108 const match = /^#comment-(\d+)$/.exec(window.location.hash || ""); 109 if (!match) return; 110 const target = document.getElementById(`comment-${match[1]}`); 111 if (!target) return; 112 linkedCommentFocused = true; 113 target.classList.add("comment-linked"); 114 target.scrollIntoView({ behavior: "smooth", block: "center" }); 115 } 116 117 function renderList() { 118 const list = root.querySelector("#commentList"); 119 const empty = root.querySelector("#commentEmpty"); 120 const more = root.querySelector("#commentMore"); 121 if (!list || !empty || !more) return; 122 list.replaceChildren(...comments.map(commentElement)); 123 focusLinkedComment(); 124 empty.textContent = text().empty; 125 empty.hidden = comments.length !== 0; 126 more.textContent = text().loadMore; 127 more.hidden = !nextBeforeId; 128 } 129 130 async function loadCaptcha() { 131 const field = root.querySelector("#commentCaptchaField"); 132 if (!captchaEnabled || !field) { 133 if (field) field.hidden = true; 134 return; 135 } 136 const response = await fetch("/api/comments/captcha", { cache: "no-store" }); 137 if (!response.ok) throw new Error("captcha unavailable"); 138 const challenge = await response.json(); 139 captchaToken = challenge.token; 140 field.hidden = false; 141 field.querySelector("span").textContent = `${text().captcha}: ${challenge.question}`; 142 field.querySelector("input").value = ""; 143 } 144
145 function renderShell() { 146 const t = text(); 147 root.querySelector("h2").textContent = t.title; 148 root.querySelector(".comments-intro").textContent = t.intro; 149 root.querySelector("label[for=commentAuthor] span").textContent = t.name; 150 root.querySelector("#commentAuthor").placeholder = t.namePlaceholder; 151 root.querySelector("label[for=commentBody] span").textContent = t.message; 152 root.querySelector("#commentBody").placeholder = t.messagePlaceholder; 153 root.querySelector("#commentSubmit").textContent = t.submit; 154 renderList(); 155 if (captchaEnabled) loadCaptcha().catch(() => setStatus(t.failure, "error")); 156 } 157 158 async function loadComments(append = false) { 159 const suffix = append && nextBeforeId ? `&before=${encodeURIComponent(nextBeforeId)}` : ""; 160 const response = await fetch(`/api/comments?slug=${encodeURIComponent(slug)}${suffix}`, { cache: "no-store" }); 161 if (!response.ok) throw new Error("comments unavailable"); 162 const payload = await response.json(); 163 comments = append ? comments.concat(payload.comments) : payload.comments; 164 nextBeforeId = payload.nextBeforeId; 165 captchaEnabled = payload.captchaEnabled === true; 166 renderList(); 167 if (captchaEnabled && !append) await loadCaptcha(); 168 } 169 170 root.querySelector("form").addEventListener("submit", async (event) => { 171 event.preventDefault(); 172 const t = text(); 173 const button = root.querySelector("#commentSubmit"); 174 button.disabled = true; 175 button.textContent = t.sending; 176 setStatus(""); 177 try { 178 const response = await fetch("/api/comments", { 179 method: "POST", 180 headers: { "content-type": "application/json" }, 181 body: JSON.stringify({ 182 postSlug: slug, 183 author: root.querySelector("#commentAuthor").value, 184 body: root.querySelector("#commentBody").value, 185 captchaToken, 186 captchaAnswer: root.querySelector("#commentCaptchaAnswer").value, 187 }), 188 }); 189 if (!response.ok) throw new Error("post failed"); 190 const payload = await response.json(); 191 comments.unshift(payload.comment); 192 root.querySelector("#commentBody").value = ""; 193 setStatus(t.success, "success"); 194 renderList(); 195 if (captchaEnabled) await loadCaptcha(); 196 } catch { 197 setStatus(t.failure, "error"); 198 if (captchaEnabled) loadCaptcha().catch(() => {}); 199 } finally { 200 button.disabled = false; 201 button.textContent = t.submit; 202 } 203 }); 204 205 root.querySelector("#commentMore").addEventListener("click", () => { 206 loadComments(true).catch(() => setStatus(text().failure, "error")); 207 }); 208 window.addEventListener("orzbar:languagechange", renderShell); 209 210 renderShell(); 211 loadComments().catch(() => setStatus(text().failure, "error")); 212})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.