1(() => { 2const LANG_KEY = "orzbar_lang"; 3const ENGLISH_INDEX_MIN_WORDS = 180; 4 5const categoryLabels = { 6 "ai-frontier": "人工æºè½", 7 "crypto-analysis": "å å¯è§å¯", 8 "hep-data": "é«è½", 9 "science": "ç§å¦", 10 "fiction": "åä½", 11 "hep": "ç§å¦", 12 research: "论æç ç©¶", 13 developer: "å¼åè ", 14 "ai-labs": "人工æºè½å®éªå®¤", 15 "ai-tools": "人工æºè½å·¥å ·", 16 "crypto-news": "å å¯èµè®¯", 17 "hep-research": "é«è½è®ºæ", 18 "hep-science": "ç²åç§æ®", 19}; 20 21const categoryLabelsEn = { 22 "ai-frontier": "AI Frontier", 23 "crypto-analysis": "Crypto", 24 "hep-data": "HEP", 25 "science": "Science", 26 "fiction": "Fiction", 27 "hep": "Science", 28 research: "Research", 29 developer: "Developer", 30 "ai-labs": "AI Labs", 31 "ai-tools": "AI Tools", 32 "crypto-news": "Crypto News", 33 "hep-research": "HEP Paper", 34 "hep-science": "Physics", 35}; 36 37const categoryLabelsFr = { 38 "ai-frontier": "Intelligence artificielle", "crypto-analysis": "Cryptomonnaies", 39 "hep-data": "Physique des hautes énergies", science: "Sciences", fiction: "Fiction", 40 hep: "Sciences", research: "Recherche", developer: "Développement", 41 "ai-labs": "Laboratoires dâIA", "ai-tools": "Outils dâIA", 42 "crypto-news": "Actualités crypto", "hep-research": "Article de physique", 43 "hep-science": "Physique", 44}; 45 46const categoryAccents = { 47 "ai-frontier": "teal", 48 "crypto-analysis": "amber", 49 "hep-data": "orange", 50 "science": "violet", 51 "fiction": "green", 52 "hep": "violet", 53}; 54 55let activeArticleView = null; 56let activePost = null; 57 58function getCurrentLang() { 59 const value = new URLSearchParams(window.location.search).get("lang"); 60 return ["zh", "en", "fr"].includes(value) ? value : "en"; 61} 62 63function tr(zh, en, fr, lang = getCurrentLang()) { return lang === "zh" ? zh : lang === "fr" ? fr : en; } 64function localeFor(lang = getCurrentLang()) { return lang === "zh" ? "zh-CN" : lang === "fr" ? "fr-FR" : "en-US"; } 65 66function hasCjk(text) { 67 return /[\u3400-\u9fff]/.test(String(text || "")); 68} 69 70function englishWordCount(value) { 71 return (String(value || "").match(/\b[A-Za-z][A-Za-z'-]*\b/g) || []).length; 72} 73 74function hasUsableEnglish(post) { 75 if (typeof post?.english_ready === "boolean") return post.english_ready; 76 const body = String(post?.body_md_en || ""); 77 const joined = `${post?.title_en || ""}\n${post?.summary_en || ""}\n${body}`; 78 return englishWordCount(body) >= ENGLISH_INDEX_MIN_WORDS && !/english version under review|being reviewed/i.test(joined); 79} 80 81function hasUsableFrench(post) { 82 if (typeof post?.french_ready === "boolean") return post.french_ready; 83 const body = String(post?.body_md_fr || ""); 84 const count = (body.match(/\b[\p{L}][\p{L}â'-]*/gu) || []).length; 85 return count >= ENGLISH_INDEX_MIN_WORDS && !/version française en cours|traduction en cours/i.test(body); 86} 87 88function availableViewLang(post, lang) { 89 if (lang === "fr" && !hasUsableFrench(post)) return hasUsableEnglish(post) ? "en" : "zh"; 90 if (lang === "en" && !hasUsableEnglish(post)) return "zh"; 91 return lang; 92} 93 94function labelForCategory(cat, lang = getCurrentLang()) { 95 const map = lang === "zh" ? categoryLabels : lang === "fr" ? categoryLabelsFr : categoryLabelsEn; 96 return map[cat] || cat || ""; 97} 98 99function accentForCategory(cat) { return categoryAccents[cat] || "teal"; } 100 101function localizedField(post, field, lang = getCurrentLang()) { 102 if (!post) return ""; 103 const viewLang = availableViewLang(post, lang); 104 const localized = post[`${field}_${viewLang}`]; 105 if (localized) return localized; 106 107 const fallback = post[field] || ""; 108 if ((lang === "en" || lang === "fr") && hasCjk(fallback)) { 109 return fallback; 110 } 111 return fallback; 112} 113 114function localizedPost(post, lang = getCurrentLang()) { 115 const viewLang = availableViewLang(post, lang); 116 return { 117 ...post, 118 _viewLang: viewLang, 119 title: localizedField(post, "title", lang), 120 summary: localizedField(post, "summary", lang), 121 body_md: localizedField(post, "body_md", lang), 122 risk_note: localizedField(post, "risk_note", lang), 123 }; 124} 125 126function esc(v) { 127 return String(v || "") 128 .replace(/&/g, "&") 129 .replace(/</g, "<") 130 .replace(/>/g, ">") 131 .replace(/"/g, """); 132} 133 134function inlineMarkdown(text) { 135 return esc(text) 136 .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") 137 .replace(/\*([^*]+)\*/g, "<em>$1</em>") 138 .replace(/`([^`]+)`/g, "<code>$1</code>") 139 .replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, 140 '<a href="$2" target="_blank" rel="noopener">$1</a>'); 141} 142 143function markdownToHtml(markdown) { 144 const src = String(markdown || "").split(/\r?\n/); 145 const out = []; 146 let i = 0; 147 148 while (i < src.length) { 149 const line = src[i]; 150 151 // Fenced code block â keep it whole, even if it contains blank lines. 152 if (line.trim().startsWith("```")) { 153 const code = []; 154 i++; 155 while (i < src.length && !src[i].trim().startsWith("```")) { code.push(src[i]); i++; } 156 i++;
156 // skip the closing fence 157 out.push(`<pre><code>${esc(code.join("\n"))}</code></pre>`); 158 continue; 159 } 160 161 // Blank line â skip. 162 if (line.trim() === "") { i++; continue; } 163 164 // Gather a block: consecutive non-blank lines until a blank line or a code fence. 165 const lines = []; 166 while (i < src.length && src[i].trim() !== "" && !src[i].trim().startsWith("```")) { 167 lines.push(src[i]); i++; 168 } 169 170 const first = lines[0]; 171 // Headings are ## / ### only. A single "# " is treated as body text on purpose, 172 // so code comments (e.g. "# Read file") in an unfenced block never become headings. 173 if (/^#{2,4} /.test(first)) { 174 const level = first.startsWith('## ') ? 2 : 3; 175 out.push(`<h${level}>${inlineMarkdown(first.replace(/^#{2,4} /, ''))}</h${level}>`); 176 i -= lines.length - 1; 177 continue; 178 } 179 180 if (first.startsWith("> ")) { 181 const end = lines.findIndex(l => !l.startsWith('> ')); 182 const quotes = end < 0 ? lines : lines.slice(0, end); 183 const q = quotes.map(l => inlineMarkdown(l.slice(2))).join("<br>"); 184 i -= lines.length - quotes.length; 185 out.push(`<blockquote>${q}</blockquote>`); 186 continue; 187 } 188 189 const isListLine = l => /^[-*]\s/.test(l) || /^\d+\.\s/.test(l); 190 if (lines.every(isListLine)) { 191 const ordered = /^\d+\./.test(lines[0]); 192 const tag = ordered ? "ol" : "ul"; 193 const items = lines.map(l => `<li>${inlineMarkdown(l.replace(/^[-*\d.]\s*/, ""))}</li>`).join(""); 194 out.push(`<${tag}>${items}</${tag}>`); 195 continue; 196 } 197 198 out.push(`<p>${lines.map(l => inlineMarkdown(l)).join("<br>")}</p>`); 199 } 200 201 return out.join("\n"); 202} 203 204function artSeed(s) { 205 s = String(s || ""); let h = 0; 206 for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; 207 return h; 208} 209 210/* Per-article cover art as a faint full-page backdrop (kept out of the layout â 211 it just gives each article a little atmosphere). Position varies per article. */ 212function applyArtBackground(rawPost) { 213 const slug = rawPost.id || rawPost.slug; 214 document.body.classList.remove("has-art-bg", "bg-pos-a", "bg-pos-b", "bg-pos-c"); 215 if (!slug) return; 216 const url = `assets/post-art/${encodeURIComponent(slug)}.svg?v=3`; 217 const pos = ["bg-pos-a", "bg-pos-b", "bg-pos-c"][artSeed(slug) % 3]; 218 document.body.style.setProperty("--art-bg", `url('${url}')`); 219 document.body.classList.add("has-art-bg", pos); 220} 221 222function copyrightNotice(rawPost, lang) { 223 const date = new Date(rawPost.created_at || rawPost.updated_at || "2026-01-01"); 224 const year = Number.isFinite(date.getUTCFullYear()) ? date.getUTCFullYear() : 2026; 225 const digest = /^[0-9a-f]{64}$/.test(String(rawPost.copyright_sha256 || "")) 226 ? String(rawPost.copyright_sha256) 227 : ""; 228 const label = tr( 229 "ååæåä¸ç¼æä¿çæå©ãçæä¸è½¬è½½", 230 "Original editorial text and arrangement. Reuse terms", 231 "Texte et composition éditoriale dâorigine. Conditions de réutilisation", 232 lang, 233 ); 234 const proof = digest 235 ? ` · <code title="SHA-256 ${esc(digest)}">SHA-256 ${esc(digest.slice(0, 12))}â¦</code>` 236 : ""; 237 return `<aside class="article-copyright" aria-label="Copyright and reuse notice">© ${year} OrzBar · <a href="/copyright.html${lang === 'en' ? '' : `?lang=${encodeURIComponent(lang)}`}">${label}</a>${proof}</aside>`; 238} 239 240function renderArticle(articleView, rawPost) { 241 const lang = getCurrentLang(); 242 const post = localizedPost(rawPost, lang); 243 const accent = accentForCategory(rawPost.category); 244 const label = labelForCategory(rawPost.category, lang); 245 const isFiction = rawPost.category === "fiction"; 246 const dateStr = new Date(rawPost.created_at).toLocaleString(localeFor(lang), { 247 year: "numeric", month: "long", day: "numeric", 248 hour: "2-digit", minute: "2-digit", 249 }); 250 const backText = tr("â è¿å", "â Back", "â Retour", lang); 251 const homeText = tr("é¦é¡µ", "Home", "Accueil", lang); 252 const translationNote = post._viewLang !== lang 253 ? `<p class="translation-note">${tr( 254 "æéè¯è¨çæ¬æä¸å¯ç¨ï¼å½åæ¾ç¤ºå·²æçæ¬ã", 255 "The selected edition is not yet available; an existing edition is shown.", 256 "Lâédition française nâest pas encore disponible ; une autre édition existante est affichée.", 257 lang, 258 )}</p>` 259 : ""; 260 261 document.title = `${post.title} â OrzBar`; 262 document.documentElement.lang = post._viewLang === "zh" ? "zh-CN" : post._viewLang; 263
264 applyArtBackground(rawPost); 265 266 if (isFiction) { 267 articleView.innerHTML = ` 268 <a class="article-back" href="javascript:history.back()">${backText}</a> 269 <h1>${esc(post.title)}</h1> 270 ${translationNote} 271 <div class="article-body" style="--accent: var(--${esc(accent)})">${markdownToHtml(post.body_md)}</div> 272 ${copyrightNotice(rawPost, lang)} 273 `; 274 return; 275 } 276 277 articleView.innerHTML = ` 278 <a class="article-back" href="javascript:history.back()">${backText}</a> 279 <h1>${esc(post.title)}</h1> 280 <div class="article-meta"> 281 <span class="category-pill accent-${esc(accent)}">${esc(label)}</span> 282 <time datetime="${esc(rawPost.created_at)}">${dateStr}</time> 283 </div> 284 ${post.summary ? `<p style="color:var(--text-2);font-size:1.12rem;line-height:1.7;margin-bottom:32px">${esc(post.summary)}</p>` : ""} 285 ${translationNote} 286 <div class="article-body" style="--accent: var(--${esc(accent)})">${markdownToHtml(post.body_md)}</div> 287 ${post.risk_note ? `<p class="risk-note" style="margin-top:32px;padding-top:24px;border-top:1px solid var(--line)">${esc(post.risk_note)}</p>` : ""} 288 ${copyrightNotice(rawPost, lang)} 289 <div style="display:flex;gap:20px;margin-top:44px;padding-top:24px;border-top:1px solid var(--line)"> 290 <a class="article-back" href="javascript:history.back()">${backText}</a> 291 <a href="/${lang === 'en' ? '' : `?lang=${encodeURIComponent(lang)}`}" style="color:var(--muted);font-size:0.84rem;font-weight:600;transition:color 140ms" 292 onmouseover="this.style.color='var(--text)'" onmouseout="this.style.color='var(--muted)'">${homeText}</a> 293 </div> 294 `; 295} 296 297function renderLoadFailure(articleView, kind) { 298 const lang = getCurrentLang(); 299 const backText = tr("â è¿å", "â Back", "â Retour", lang); 300 const title = tr("è½½å ¥å¤±è´¥", "Loading failed", "Ãchec du chargement", lang); 301 const message = kind === "missing" 302 ? tr("æªæ¾å°è¿ç¯æç« ã", "This article is not available.", "Cet article nâest pas disponible.", lang) 303 : tr("æç« ææ¶æ æ³è½½å ¥ï¼è¯·ç¨åéè¯ã", "The article could not be loaded. Please try again.", "Impossible de charger lâarticle. Veuillez réessayer.", lang); 304 305 articleView.innerHTML = ` 306 <a class="article-back" href="javascript:history.back()">${backText}</a> 307 <h1>${title}</h1> 308 <p class="article-meta">${message}</p> 309 `; 310} 311 312async function loadArticle(articleView) { 313 const slug = new URLSearchParams(window.location.search).get("slug"); 314 const safeSlug = /^[A-Za-z0-9-]+$/.test(String(slug || "")) ? slug : ""; 315 let post = null; 316 317 if (safeSlug) { 318 try { 319 const res = await fetch(`data/post-content/${encodeURIComponent(safeSlug)}.json`, { cache: "no-store" }); 320 if (res.ok) post = await res.json(); 321 else if (res.status !== 404) throw new Error("article " + res.status); 322 } catch { 323 renderLoadFailure(articleView, "fetch"); 324 return; 325 } 326 } 327 328 // Backward-compatible lookup during deployment, never a latest-article alias. 329 if (!post) { 330 try { 331 const res = await fetch("data/site-content.json", { cache: "no-store" }); 332 if (!res.ok) throw new Error("content " + res.status); 333 const data = await res.json(); 334 post = safeSlug ? data.posts.find(p => p.slug === safeSlug) : null; 335 } catch { 336 renderLoadFailure(articleView, "fetch"); 337 return; 338 } 339 } 340 341 if (!post) { 342 renderLoadFailure(articleView, "missing"); 343 return; 344 } 345 346 if (post.category === "fiction" && window.FICTION_GATE_ENABLED && typeof window.requestFictionPassword === "function") { 347 const ok = await window.requestFictionPassword(); 348 if (!ok) return; 349 } 350 351 activeArticleView = articleView; 352 activePost = post; 353 renderArticle(articleView, post); 354} 355 356function rerenderActiveArticle() { 357 if (activeArticleView && activePost) renderArticle(activeArticleView, activePost); 358} 359 360function initArticlePage() { 361 const articleView = document.querySelector("#articleView"); 362 if (!articleView) return; 363 364 window.addEventListener("orzbar:languagechange", rerenderActiveArticle); 365 366 loadArticle(articleView).catch(() => { 367 renderLoadFailure(articleView, "fetch"); 368 }); 369} 370 371if (document.readyState === "loading") { 372 document.addEventListener("DOMContentLoaded", initArticlePage, { once: true }); 373} else { 374 initArticlePage(); 375} 376})();
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.