PageSourceSearch

https://orzbar.com/post.js?v=22

js orzbar.com collected 2026-10-02 01:38:38 UTC 14,009 bytes, 376 lines download raw bytes

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, "&lt;")
130    .replace(/>/g, "&gt;")
131    .replace(/"/g, "&quot;");
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.