+ */
+
+import { marked } from "./vendor/marked.esm.js";
+import { tokenizeCJK } from "./vendor/tokenize.mjs";
+
+const VIEW = document.getElementById("app-view");
+const SEARCH_INPUT = document.getElementById("app-search-input");
+const SEARCH_PANEL = document.getElementById("app-search-panel");
+const NAV_LINKS = document.querySelectorAll(".app-nav a[data-route]");
+const REPO_BASE = "https://github.com/SwiftOldDriver/iOS-Weekly/blob/master/";
+
+marked.setOptions({ gfm: true, breaks: false });
+
+/* page.css (designed for the "Nice" WeChat editor) styles `#nice hN .content`
+ * for the colored accents. Wrap heading inner text so those rules light up. */
+marked.use({
+ renderer: {
+ heading(text, level) {
+ return `${text}\n`;
+ },
+ },
+});
+
+const state = {
+ index: null, // full index.json
+ byId: new Map(), // id -> entry
+ byType: { report: [], post: [], contributor: [] },
+ yearGroups: [], // [{ year, items: [] }]
+ docCache: new Map(), // path -> rendered html
+ miniSearch: null,
+ searchLoading: null, // in-flight promise
+};
+
+const router = createRouter([
+ { path: /^\/?$/, render: renderHome, name: "home" },
+ { path: /^\/reports\/?$/, render: renderReports, name: "/reports" },
+ { path: /^\/posts\/?$/, render: renderPosts, name: "/posts" },
+ { path: /^\/contributors\/?$/, render: renderContributors, name: "/contributors" },
+ { path: /^\/d\/(.+)$/, render: (m) => renderDetail(decodeURIComponent(m[1])) },
+]);
+
+bootstrap();
+
+async function bootstrap() {
+ try {
+ const res = await fetch("data/index.json", { cache: "default" });
+ if (!res.ok) throw new Error(`index.json HTTP ${res.status}`);
+ const data = await res.json();
+ ingest(data);
+ window.addEventListener("hashchange", router.handle);
+ router.handle();
+ installSearchHandlers();
+ installShortcuts();
+ } catch (err) {
+ VIEW.innerHTML = `索引加载失败:${escapeHtml(err.message)}
`;
+ console.error(err);
+ }
+}
+
+function ingest(data) {
+ state.index = data;
+ for (const item of data.items) {
+ state.byId.set(item.id, item);
+ if (state.byType[item.type]) state.byType[item.type].push(item);
+ }
+ state.byType.report.sort((a, b) => (b.issue || 0) - (a.issue || 0));
+ state.byType.post.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
+
+ const years = new Map();
+ for (const r of state.byType.report) {
+ const y = r.year || "其他";
+ if (!years.has(y)) years.set(y, []);
+ years.get(y).push(r);
+ }
+ state.yearGroups = [...years.entries()]
+ .map(([year, items]) => ({ year, items }))
+ .sort((a, b) => (b.year || 0) - (a.year || 0));
+}
+
+/* =====================================================================
+ * Router
+ * ===================================================================== */
+function createRouter(routes) {
+ function parseHash() {
+ const h = location.hash || "#/";
+ return h.replace(/^#/, "");
+ }
+ async function handle() {
+ const path = parseHash();
+ for (const r of routes) {
+ const m = path.match(r.path);
+ if (m) {
+ highlightNav(r.name);
+ VIEW.innerHTML = `载入中…
`;
+ try {
+ await r.render(m);
+ VIEW.scrollTop = 0;
+ document.getElementById("main").focus({ preventScroll: true });
+ window.scrollTo({ top: 0, behavior: "instant" in window ? "instant" : "auto" });
+ } catch (err) {
+ console.error(err);
+ VIEW.innerHTML = `加载失败:${escapeHtml(err.message)}
`;
+ }
+ return;
+ }
+ }
+ VIEW.innerHTML = ``;
+ }
+ return { handle };
+}
+
+function highlightNav(name) {
+ NAV_LINKS.forEach((a) => {
+ a.classList.toggle("is-active", a.dataset.route === name);
+ });
+}
+
+/* =====================================================================
+ * Views
+ * ===================================================================== */
+function renderHome() {
+ const reports = state.byType.report.slice(0, 8);
+ const posts = state.byType.post;
+ const contributors = state.byType.contributor.slice(0, 8);
+ const stats = state.index.stats || {};
+
+ const html = `
+
+ iOS 开发者的精品周报检索站
+ 收录 ${stats.reports || 0} 期老司机 iOS 周报、${stats.posts || 0} 篇精品文章与 ${stats.contributors || 0} 位贡献者,支持全文搜索(按 / 快速聚焦)。
+
+
${stats.reports || 0} Reports
+
${stats.posts || 0} Posts
+
${stats.contributors || 0} Contributors
+ ${stats.builtAt ? `
${stats.builtAt.slice(0, 10)} 索引更新
` : ""}
+
+
+
+
+
+
+ ${reports.map(cardReport).join("")}
+
+
+
+
+
+
+ WWDC 内参 系列是由「老司机技术」牵头组织的精品原创内容系列。已经做了几年了,口碑一直不错。得益于组建的审核团队和不断优化的创作流程,大家创作的内容都已经超越了视频本身的内容,非常有学习和参考意义。
+ 双审核机制:一位审核从专业性角度看内容是否正确,另外一位审核从读者角度看知识是否正确引导。
+ 仓库已脱敏开源,覆盖 WWDC 21 – 24 历年内参。
+
+
+
+
+
+
+
+ ${posts.map(cardPost).join("")}
+
+
+
+
+
+
+ ${contributors.map(cardContributor).join("")}
+
+
+ `;
+ VIEW.innerHTML = html;
+}
+
+function renderReports() {
+ const html = `
+
+
+
Reports(${state.byType.report.length} 期)
+
+ ${state.yearGroups.map(renderYearGroup).join("")}
+
+ `;
+ VIEW.innerHTML = html;
+
+ VIEW.querySelectorAll(".app-year__head").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const yr = btn.closest(".app-year");
+ yr.dataset.collapsed = yr.dataset.collapsed === "true" ? "false" : "true";
+ });
+ });
+}
+
+function renderYearGroup(group) {
+ const collapsed = group.year < (state.yearGroups[0]?.year || 0) - 1;
+ return `
+
+
+
+
+ `;
+}
+
+function renderPosts() {
+ const html = `
+
+
+
Posts(${state.byType.post.length} 篇)
+
+
+ ${state.byType.post.map(cardPost).join("")}
+
+
+ `;
+ VIEW.innerHTML = html;
+}
+
+function renderContributors() {
+ const html = `
+
+
+
Contributors(${state.byType.contributor.length} 位)
+
+
+ ${state.byType.contributor.map(cardContributor).join("")}
+
+
+ `;
+ VIEW.innerHTML = html;
+}
+
+async function renderDetail(id) {
+ const item = state.byId.get(id);
+ if (!item) {
+ VIEW.innerHTML = ``;
+ return;
+ }
+
+ const head = `
+
+ 返回${typeLabel(item.type)}列表
+
+ ${renderDetailBody(item)}
+
+ `;
+ VIEW.innerHTML = head;
+
+ const body = document.getElementById("app-detail-body");
+ try {
+ const html = await fetchAndRender(item);
+ body.innerHTML = `${html}
`;
+ applyExternalLinks(body);
+ applyInternalAnchors(body);
+ } catch (err) {
+ body.innerHTML = `内容加载失败:${escapeHtml(err.message)}
`;
+ }
+}
+
+function renderDetailBody(item) {
+ if (item.type === "contributor" && item.body) {
+ try {
+ return `${marked.parse(item.body)}
`;
+ } catch {
+ // fall through to fetch path
+ }
+ }
+ return `载入正文…
`;
+}
+
+async function fetchAndRender(item) {
+ if (state.docCache.has(item.id)) return state.docCache.get(item.id);
+
+ if (item.type === "contributor" && item.body) {
+ const html = marked.parse(item.body);
+ state.docCache.set(item.id, html);
+ return html;
+ }
+
+ const url = item.url || encodePath(item.path);
+ const res = await fetch(url, { cache: "default" });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const md = await res.text();
+ const html = marked.parse(md);
+ state.docCache.set(item.id, html);
+ if (state.docCache.size > 12) {
+ const firstKey = state.docCache.keys().next().value;
+ state.docCache.delete(firstKey);
+ }
+ return html;
+}
+
+/* =====================================================================
+ * Cards
+ * ===================================================================== */
+function cardReport(it) {
+ return `
+
+
+ 周报
+ #${it.issue ?? "—"}
+ ·
+ ${escapeHtml(it.date || "")}
+
+ ${escapeHtml(reportShortTitle(it))}
+ ${escapeHtml(it.excerpt || "")}
+
+ `;
+}
+
+function cardPost(it) {
+ return `
+
+
+ 文章
+ ${it.date ? `${escapeHtml(it.date)}` : ""}
+
+ ${escapeHtml(it.title)}
+ ${escapeHtml(it.excerpt || "")}
+
+ `;
+}
+
+function cardContributor(it) {
+ const avatar = it.avatar || "assets/ios-weekly-avatar-new.png";
+ return `
+
+
+ ${escapeHtml(it.title)}
+ ${escapeHtml(it.excerpt || "")}
+
+ `;
+}
+
+/* =====================================================================
+ * Search
+ * ===================================================================== */
+function installSearchHandlers() {
+ let activeIdx = -1;
+ let lastResults = [];
+ const debounced = debounce(runSearch, 120);
+
+ SEARCH_INPUT.addEventListener("focus", () => {
+ ensureSearchLoaded();
+ if (SEARCH_INPUT.value.trim()) showPanel();
+ });
+ SEARCH_INPUT.addEventListener("input", () => {
+ debounced(SEARCH_INPUT.value);
+ });
+ SEARCH_INPUT.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ SEARCH_INPUT.blur();
+ hidePanel();
+ return;
+ }
+ if (!lastResults.length) return;
+ if (e.key === "ArrowDown") {
+ e.preventDefault();
+ activeIdx = (activeIdx + 1) % lastResults.length;
+ updateActive();
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault();
+ activeIdx = (activeIdx - 1 + lastResults.length) % lastResults.length;
+ updateActive();
+ } else if (e.key === "Enter") {
+ e.preventDefault();
+ const pick = lastResults[Math.max(0, activeIdx)];
+ if (pick) {
+ location.hash = `#/d/${encodeURIComponent(pick.id)}`;
+ SEARCH_INPUT.value = "";
+ hidePanel();
+ SEARCH_INPUT.blur();
+ }
+ }
+ });
+
+ document.addEventListener("click", (e) => {
+ if (!e.target.closest(".app-search")) hidePanel();
+ });
+
+ function updateActive() {
+ SEARCH_PANEL.querySelectorAll(".app-search__result").forEach((el, i) => {
+ el.classList.toggle("is-active", i === activeIdx);
+ if (i === activeIdx) el.scrollIntoView({ block: "nearest" });
+ });
+ }
+
+ async function runSearch(query) {
+ const q = query.trim();
+ if (!q) {
+ lastResults = [];
+ activeIdx = -1;
+ hidePanel();
+ return;
+ }
+ showPanel(`搜索中…
`);
+ const results = await search(q);
+ lastResults = results;
+ activeIdx = results.length ? 0 : -1;
+ if (!results.length) {
+ showPanel(`没有匹配结果。
`);
+ return;
+ }
+ showPanel(results.map((r, i) => renderResult(r, i === 0, q)).join(""));
+ }
+
+ function showPanel(content) {
+ if (typeof content === "string") SEARCH_PANEL.innerHTML = content;
+ SEARCH_PANEL.hidden = false;
+ SEARCH_INPUT.setAttribute("aria-expanded", "true");
+ }
+ function hidePanel() {
+ SEARCH_PANEL.hidden = true;
+ SEARCH_INPUT.setAttribute("aria-expanded", "false");
+ }
+}
+
+function renderResult(r, active, query) {
+ const item = state.byId.get(r.id);
+ if (!item) return "";
+ const snippet = makeSnippet(item, query);
+ return `
+
+
+ ${typeLabel(item.type)}
+ ${escapeHtml(item.title)}
+ ${item.date ? `· ${escapeHtml(item.date)}` : ""}
+
+ ${snippet}
+
+ `;
+}
+
+function makeSnippet(item, query) {
+ const haystack = [item.excerpt || "", item.body || "", (item.sections || []).map((s) => `${s.heading || ""} ${s.summary || ""}`).join(" ")].join(" ");
+ const tokens = query.split(/\s+/).filter(Boolean).map((t) => t.toLowerCase());
+ if (!haystack || !tokens.length) return escapeHtml((item.excerpt || "").slice(0, 120));
+ const lower = haystack.toLowerCase();
+ let pos = -1;
+ for (const t of tokens) {
+ const p = lower.indexOf(t);
+ if (p >= 0 && (pos < 0 || p < pos)) pos = p;
+ }
+ if (pos < 0) return escapeHtml(haystack.slice(0, 120));
+ const start = Math.max(0, pos - 30);
+ const end = Math.min(haystack.length, pos + 100);
+ let frag = haystack.slice(start, end);
+ if (start > 0) frag = "…" + frag;
+ if (end < haystack.length) frag += "…";
+ let html = escapeHtml(frag);
+ for (const t of tokens) {
+ const re = new RegExp(escapeRegex(t), "gi");
+ html = html.replace(re, (m) => `${m}`);
+ }
+ return html;
+}
+
+async function ensureSearchLoaded() {
+ if (state.miniSearch) return state.miniSearch;
+ if (state.searchLoading) return state.searchLoading;
+ state.searchLoading = (async () => {
+ const [{ default: MiniSearch }, raw] = await Promise.all([
+ import("./vendor/minisearch.esm.js"),
+ fetch("data/search.json", { cache: "default" }).then((r) => {
+ if (!r.ok) throw new Error(`search.json HTTP ${r.status}`);
+ return r.json();
+ }),
+ ]);
+ state.miniSearch = MiniSearch.loadJS(raw, {
+ idField: "id",
+ fields: ["title", "sections", "body"],
+ storeFields: ["id", "title", "type", "date"],
+ tokenize: tokenizeCJK,
+ searchOptions: {
+ prefix: true,
+ fuzzy: 0.15,
+ boost: { title: 3, sections: 2 },
+ combineWith: "AND",
+ },
+ });
+ return state.miniSearch;
+ })().catch((err) => {
+ console.error("search load failed", err);
+ state.searchLoading = null;
+ throw err;
+ });
+ return state.searchLoading;
+}
+
+async function search(query) {
+ try {
+ const ms = await ensureSearchLoaded();
+ return ms.search(query).slice(0, 30);
+ } catch {
+ return fallbackSearch(query);
+ }
+}
+
+function fallbackSearch(query) {
+ const q = query.toLowerCase();
+ const tokens = q.split(/\s+/).filter(Boolean);
+ if (!tokens.length) return [];
+ return state.index.items
+ .map((it) => {
+ const hay = `${it.title}\n${it.excerpt || ""}\n${(it.sections || []).map((s) => s.heading || "").join("\n")}`.toLowerCase();
+ const score = tokens.reduce((s, t) => s + (hay.includes(t) ? 1 : 0), 0);
+ return { id: it.id, score };
+ })
+ .filter((r) => r.score > 0)
+ .sort((a, b) => b.score - a.score)
+ .slice(0, 30);
+}
+
+function installShortcuts() {
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "/" && !isTypingTarget(e.target)) {
+ e.preventDefault();
+ SEARCH_INPUT.focus();
+ SEARCH_INPUT.select();
+ }
+ });
+}
+
+/* =====================================================================
+ * Helpers
+ * ===================================================================== */
+function reportShortTitle(it) {
+ const t = it.title || "";
+ return t.replace(/^老司机[^#]*#\d+\s*\|?\s*/, "").trim() || t;
+}
+
+function typeLabel(type) {
+ return { report: "周报", post: "文章", contributor: "贡献者" }[type] || type;
+}
+
+function backHref(item) {
+ return { report: "#/reports", post: "#/posts", contributor: "#/contributors" }[item.type] || "#/";
+}
+
+function encodePath(p) {
+ return p.split("/").map((s) => encodeURIComponent(s)).join("/");
+}
+
+function applyExternalLinks(root) {
+ root.querySelectorAll("a[href]").forEach((a) => {
+ const href = a.getAttribute("href") || "";
+ if (/^https?:\/\//i.test(href)) {
+ a.target = "_blank";
+ a.rel = "noopener noreferrer";
+ }
+ });
+}
+
+function applyInternalAnchors(root) {
+ /* Collapse first H1 in body since we already show the title in meta? Keep H1 for visual continuity. */
+ root.querySelectorAll("img[src]").forEach((img) => {
+ img.loading = "lazy";
+ img.referrerPolicy = "no-referrer";
+ });
+}
+
+function debounce(fn, ms) {
+ let t;
+ return function (...args) {
+ clearTimeout(t);
+ t = setTimeout(() => fn.apply(this, args), ms);
+ };
+}
+
+function isTypingTarget(t) {
+ if (!t) return false;
+ const tag = t.tagName;
+ return tag === "INPUT" || tag === "TEXTAREA" || t.isContentEditable;
+}
+
+function escapeHtml(s) {
+ return String(s ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+function escapeAttr(s) { return escapeHtml(s); }
+function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 000000000..f32d5a4e3
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+ 老司机 iOS 周报
+
+
+
+
+
+
+
+
+ 跳到主要内容
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/site.css b/docs/site.css
new file mode 100644
index 000000000..363f7f18b
--- /dev/null
+++ b/docs/site.css
@@ -0,0 +1,739 @@
+/* =============================================================
+ * 老司机 iOS 周报 — 站点 chrome
+ * 仅 .app-* 命名空间,与 assets/page.css 中 #nice 规则正交。
+ * ============================================================= */
+
+:root {
+ --c-accent: #ffb11b;
+ --c-accent-dark: #e09a0c;
+ --c-link: #3F5481;
+ --c-link-hover: #52699D;
+ --c-text: #26324F;
+ --c-text-muted: #6b7793;
+ --c-border: #e6e8ee;
+ --c-bg: #f7f8fb;
+ --c-surface: #ffffff;
+ --c-surface-2: #fafbfd;
+ --c-shadow: 0 1px 2px rgba(38, 50, 79, 0.04), 0 4px 16px rgba(38, 50, 79, 0.06);
+ --c-shadow-lg: 0 8px 32px rgba(38, 50, 79, 0.12);
+ --radius: 10px;
+ --radius-sm: 6px;
+ --header-h: 60px;
+ --max-w: 1080px;
+ --content-w: 760px;
+ --transition: 160ms ease;
+ color-scheme: light;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --c-link: #9aaadd;
+ --c-link-hover: #c2cdee;
+ --c-text: #e4e7ef;
+ --c-text-muted: #9098ad;
+ --c-border: #2a2f3d;
+ --c-bg: #15171d;
+ --c-surface: #1c1f28;
+ --c-surface-2: #21242f;
+ --c-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 4px 16px rgba(0, 0, 0, 0.4);
+ --c-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
+ color-scheme: dark;
+ }
+}
+
+* { box-sizing: border-box; }
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: var(--c-bg);
+ color: var(--c-text);
+ font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;
+ font-size: 15px;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+a { color: var(--c-link); }
+a:hover { color: var(--c-link-hover); }
+
+/* ---------- skip link ---------- */
+.app-skip-link {
+ position: absolute;
+ left: -9999px;
+ top: 0;
+ z-index: 100;
+ background: var(--c-accent);
+ color: #1a1a1a;
+ padding: 8px 16px;
+ border-radius: 0 0 6px 0;
+ text-decoration: none;
+}
+.app-skip-link:focus { left: 0; }
+
+/* ---------- header ---------- */
+.app-header {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ background: color-mix(in srgb, var(--c-surface) 92%, transparent);
+ backdrop-filter: saturate(180%) blur(14px);
+ -webkit-backdrop-filter: saturate(180%) blur(14px);
+ border-bottom: 1px solid var(--c-border);
+}
+.app-header__inner {
+ max-width: var(--max-w);
+ margin: 0 auto;
+ padding: 0 20px;
+ height: var(--header-h);
+ display: flex;
+ align-items: center;
+ gap: 24px;
+}
+.app-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ text-decoration: none;
+ color: var(--c-text);
+ font-weight: 600;
+ flex-shrink: 0;
+}
+.app-brand img {
+ border-radius: 8px;
+ background: var(--c-surface);
+ box-shadow: var(--c-shadow);
+}
+.app-brand__name {
+ font-size: 16px;
+ letter-spacing: 0.2px;
+ white-space: nowrap;
+}
+.app-nav {
+ display: flex;
+ gap: 4px;
+ margin-left: auto;
+}
+.app-nav a {
+ color: var(--c-text-muted);
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ padding: 8px 12px;
+ border-radius: var(--radius-sm);
+ transition: background var(--transition), color var(--transition);
+}
+.app-nav a:hover { color: var(--c-text); background: var(--c-surface-2); }
+.app-nav a.is-active {
+ color: var(--c-text);
+ background: color-mix(in srgb, var(--c-accent) 18%, transparent);
+}
+.app-nav__external::after {
+ content: " ↗";
+ opacity: 0.6;
+ font-size: 11px;
+}
+
+/* ---------- search ---------- */
+.app-search { position: relative; flex-shrink: 0; }
+.app-search__label {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: var(--c-surface-2);
+ border: 1px solid var(--c-border);
+ border-radius: 999px;
+ padding: 0 12px;
+ height: 36px;
+ color: var(--c-text-muted);
+ transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
+ width: 240px;
+}
+.app-search__label:focus-within {
+ border-color: var(--c-accent);
+ background: var(--c-surface);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 25%, transparent);
+}
+.app-search__label svg { flex-shrink: 0; }
+.app-search input {
+ flex: 1;
+ min-width: 0;
+ border: 0;
+ outline: 0;
+ background: transparent;
+ font: inherit;
+ color: var(--c-text);
+ padding: 0;
+}
+.app-search input::placeholder { color: var(--c-text-muted); }
+.app-search__kbd {
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
+ background: var(--c-surface);
+ border: 1px solid var(--c-border);
+ border-radius: 4px;
+ padding: 1px 6px;
+ font-size: 11px;
+ color: var(--c-text-muted);
+}
+.app-search__panel {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ width: min(560px, calc(100vw - 32px));
+ max-height: 70vh;
+ overflow-y: auto;
+ background: var(--c-surface);
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius);
+ box-shadow: var(--c-shadow-lg);
+ padding: 6px;
+}
+.app-search__panel[hidden] { display: none; }
+.app-search__hint {
+ padding: 16px;
+ color: var(--c-text-muted);
+ font-size: 13px;
+ text-align: center;
+}
+.app-search__result {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 10px 12px;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ text-decoration: none;
+ color: inherit;
+ border: 1px solid transparent;
+}
+.app-search__result:hover,
+.app-search__result.is-active {
+ background: var(--c-surface-2);
+ border-color: var(--c-border);
+}
+.app-search__result-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ color: var(--c-text);
+ font-size: 14px;
+}
+.app-search__result-snippet {
+ font-size: 13px;
+ color: var(--c-text-muted);
+ line-height: 1.5;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+.app-search__result mark {
+ background: color-mix(in srgb, var(--c-accent) 60%, transparent);
+ color: var(--c-text);
+ padding: 0 2px;
+ border-radius: 2px;
+}
+
+/* ---------- main / view ---------- */
+.app-main {
+ flex: 1;
+ width: 100%;
+ max-width: var(--max-w);
+ margin: 0 auto;
+ padding: 28px 20px 64px;
+ outline: none;
+}
+.app-view { min-height: 60vh; }
+
+.app-loading,
+.app-empty {
+ text-align: center;
+ color: var(--c-text-muted);
+ padding: 60px 20px;
+ font-size: 14px;
+}
+
+/* ---------- type badge ---------- */
+.app-badge {
+ display: inline-flex;
+ align-items: center;
+ height: 20px;
+ padding: 0 8px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.3px;
+ background: var(--c-surface-2);
+ color: var(--c-text-muted);
+ border: 1px solid var(--c-border);
+}
+.app-badge--report { background: color-mix(in srgb, var(--c-accent) 22%, transparent); color: #8a5a00; border-color: color-mix(in srgb, var(--c-accent) 35%, transparent); }
+.app-badge--post { background: color-mix(in srgb, #5b8def 22%, transparent); color: #2a51b3; border-color: color-mix(in srgb, #5b8def 35%, transparent); }
+.app-badge--contributor { background: color-mix(in srgb, #4caf80 22%, transparent); color: #2c7956; border-color: color-mix(in srgb, #4caf80 35%, transparent); }
+
+@media (prefers-color-scheme: dark) {
+ .app-badge--report { color: #ffd58a; }
+ .app-badge--post { color: #b6c7f5; }
+ .app-badge--contributor { color: #9adcb9; }
+}
+
+/* ---------- hero (home) ---------- */
+.app-hero {
+ background: linear-gradient(135deg, color-mix(in srgb, var(--c-accent) 25%, var(--c-surface)), var(--c-surface));
+ border: 1px solid var(--c-border);
+ border-radius: 16px;
+ padding: 36px 32px;
+ margin-bottom: 32px;
+ box-shadow: var(--c-shadow);
+}
+.app-hero__title {
+ margin: 0 0 8px;
+ font-size: 26px;
+ color: var(--c-text);
+}
+.app-hero__sub {
+ margin: 0 0 16px;
+ color: var(--c-text-muted);
+ font-size: 15px;
+ line-height: 1.6;
+}
+.app-hero__stats {
+ display: flex;
+ gap: 22px;
+ flex-wrap: wrap;
+ color: var(--c-text-muted);
+ font-size: 13px;
+}
+.app-hero__stats strong {
+ display: block;
+ font-size: 22px;
+ color: var(--c-text);
+ margin-bottom: 2px;
+}
+
+/* ---------- section ---------- */
+.app-section { margin-bottom: 40px; }
+.app-section__head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 16px;
+ gap: 12px;
+}
+.app-section__title {
+ margin: 0;
+ font-size: 18px;
+ color: var(--c-text);
+}
+.app-section__more {
+ font-size: 13px;
+ color: var(--c-link);
+ text-decoration: none;
+}
+.app-section__more:hover { text-decoration: underline; }
+
+/* ---------- card grid ---------- */
+.app-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 14px;
+}
+
+.app-card {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ background: var(--c-surface);
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius);
+ padding: 16px;
+ text-decoration: none;
+ color: inherit;
+ transition: transform var(--transition), border-color var(--transition), box-shadow var(--transition);
+ contain: content;
+}
+.app-card:hover {
+ transform: translateY(-1px);
+ border-color: color-mix(in srgb, var(--c-accent) 50%, var(--c-border));
+ box-shadow: var(--c-shadow);
+}
+.app-card__meta {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ color: var(--c-text-muted);
+}
+.app-card__title {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--c-text);
+ line-height: 1.4;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+.app-card__excerpt {
+ margin: 0;
+ font-size: 13px;
+ color: var(--c-text-muted);
+ line-height: 1.55;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+}
+
+/* ---------- feature card (WWDC 内参) ---------- */
+.app-feature {
+ background: #ffffff;
+ border: 1px solid #e6e8ee;
+ border-radius: 10px;
+ padding: 24px clamp(20px, 3.5vw, 32px);
+ box-shadow: 0 1px 2px rgba(38, 50, 79, 0.04), 0 4px 16px rgba(38, 50, 79, 0.06);
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+.app-feature__lede {
+ margin: 0;
+ font-size: 15px;
+ line-height: 1.7;
+ color: #26324F;
+}
+.app-feature__lede strong {
+ color: #26324F;
+}
+.app-feature__quote {
+ margin: 0;
+ background: #f4f5f8;
+ border-left: 3px solid #ffb11b;
+ border-radius: 4px;
+ padding: 10px 14px;
+ color: #52699D;
+ font-size: 14px;
+ line-height: 1.6;
+}
+.app-feature__meta {
+ margin: 0;
+ font-size: 13px;
+ color: #6b7793;
+ line-height: 1.6;
+}
+.app-feature__actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 4px;
+}
+
+.app-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ height: 36px;
+ padding: 0 16px;
+ border-radius: 999px;
+ border: 1px solid #e6e8ee;
+ background: #ffffff;
+ color: #26324F;
+ font-size: 14px;
+ font-weight: 500;
+ text-decoration: none;
+ transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;
+}
+.app-btn:hover {
+ transform: translateY(-1px);
+ border-color: #ffb11b;
+ color: #26324F;
+ box-shadow: 0 1px 2px rgba(38, 50, 79, 0.04), 0 4px 16px rgba(38, 50, 79, 0.06);
+ text-decoration: none;
+}
+.app-btn--primary {
+ background: #ffb11b;
+ border-color: #ffb11b;
+ color: #1a1a1a;
+}
+.app-btn--primary:hover {
+ background: #e09a0c;
+ border-color: #e09a0c;
+ color: #1a1a1a;
+}
+
+/* ---------- list (reports per year, posts) ---------- */
+.app-year {
+ margin-bottom: 28px;
+ background: var(--c-surface);
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius);
+ overflow: hidden;
+}
+.app-year__head {
+ width: 100%;
+ background: transparent;
+ border: 0;
+ padding: 14px 18px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ color: var(--c-text);
+ font: inherit;
+ font-size: 16px;
+ font-weight: 600;
+ text-align: left;
+ transition: background var(--transition);
+}
+.app-year__head:hover { background: var(--c-surface-2); }
+.app-year__head::after {
+ content: "";
+ width: 8px;
+ height: 8px;
+ border-right: 2px solid currentColor;
+ border-bottom: 2px solid currentColor;
+ transform: rotate(45deg);
+ margin-left: auto;
+ transition: transform var(--transition);
+ opacity: 0.6;
+}
+.app-year[data-collapsed="true"] .app-year__head::after { transform: rotate(-45deg); }
+.app-year[data-collapsed="true"] .app-year__body { display: none; }
+.app-year__count {
+ font-weight: 400;
+ color: var(--c-text-muted);
+ font-size: 13px;
+ margin-left: 4px;
+}
+.app-year__body {
+ list-style: none;
+ margin: 0;
+ padding: 6px 8px 12px;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 4px;
+ contain: content;
+ content-visibility: auto;
+ contain-intrinsic-size: 1px 600px;
+}
+.app-year__body li { margin: 0; }
+.app-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ border-radius: var(--radius-sm);
+ text-decoration: none;
+ color: inherit;
+ font-size: 14px;
+ transition: background var(--transition);
+ border: 1px solid transparent;
+}
+.app-row:hover {
+ background: var(--c-surface-2);
+ border-color: var(--c-border);
+}
+.app-row__issue {
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
+ color: var(--c-text-muted);
+ font-size: 12px;
+ flex-shrink: 0;
+ min-width: 44px;
+}
+.app-row__title {
+ flex: 1;
+ color: var(--c-text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.app-row__date {
+ color: var(--c-text-muted);
+ font-size: 12px;
+ flex-shrink: 0;
+ font-variant-numeric: tabular-nums;
+}
+
+/* ---------- contributors ---------- */
+.app-contributors {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+ gap: 16px;
+}
+.app-contributor {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ background: var(--c-surface);
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius);
+ padding: 22px 16px;
+ text-decoration: none;
+ color: inherit;
+ gap: 10px;
+ transition: border-color var(--transition), transform var(--transition), box-shadow var(--transition);
+ contain: content;
+}
+.app-contributor:hover {
+ transform: translateY(-2px);
+ border-color: color-mix(in srgb, var(--c-accent) 50%, var(--c-border));
+ box-shadow: var(--c-shadow);
+}
+.app-contributor__avatar {
+ width: 96px;
+ height: 96px;
+ border-radius: 50%;
+ object-fit: cover;
+ background: var(--c-surface-2);
+}
+.app-contributor__name {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--c-text);
+}
+.app-contributor__bio {
+ margin: 0;
+ font-size: 12px;
+ color: var(--c-text-muted);
+ line-height: 1.55;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+}
+
+/* ---------- detail ---------- */
+.app-detail {
+ background: var(--c-surface);
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius);
+ padding: 32px clamp(20px, 4vw, 48px) 48px;
+ box-shadow: var(--c-shadow);
+}
+.app-detail__back {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--c-text-muted);
+ text-decoration: none;
+ font-size: 13px;
+ margin-bottom: 8px;
+}
+.app-detail__back:hover { color: var(--c-link); }
+.app-detail__back::before { content: "←"; }
+.app-detail__meta {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: var(--c-text-muted);
+ font-size: 13px;
+ margin-bottom: 4px;
+ flex-wrap: wrap;
+}
+.app-detail__meta a { font-size: 13px; }
+
+/* keep #nice content readable on dark mode by giving the article surface coloring */
+@media (prefers-color-scheme: dark) {
+ .app-detail #nice { color: var(--c-text); }
+ .app-detail #nice p,
+ .app-detail #nice li,
+ .app-detail #nice ul,
+ .app-detail #nice ol,
+ .app-detail #nice strong,
+ .app-detail #nice em { color: var(--c-text) !important; }
+ .app-detail #nice blockquote { background: var(--c-surface-2); border-left-color: var(--c-border) !important; }
+ .app-detail #nice blockquote p,
+ .app-detail #nice blockquote a { color: var(--c-text-muted) !important; }
+ .app-detail #nice a { color: var(--c-link) !important; }
+ .app-detail #nice h1,
+ .app-detail #nice h2 .content,
+ .app-detail #nice h3 .content,
+ .app-detail #nice h4 .content,
+ .app-detail #nice h5 .content,
+ .app-detail #nice h6 .content { color: var(--c-text) !important; }
+ .app-detail #nice p code,
+ .app-detail #nice li code { background: var(--c-surface-2); color: var(--c-link) !important; }
+}
+
+/* the WeChat page.css forces images width:100% with margin-bottom:15px which is fine here; ensure overflow */
+.app-detail #nice {
+ max-width: var(--content-w);
+ margin: 0 auto;
+}
+.app-detail #nice img {
+ max-width: 100%;
+ height: auto;
+}
+.app-detail #nice pre {
+ overflow-x: auto;
+ background: var(--c-surface-2);
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius-sm);
+ padding: 14px 16px;
+ font-size: 13px;
+ line-height: 1.55;
+ margin: 14px 0;
+}
+.app-detail #nice pre code {
+ background: transparent;
+ color: var(--c-text);
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
+}
+.app-detail #nice table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 14px 0;
+ font-size: 14px;
+}
+.app-detail #nice table th,
+.app-detail #nice table td {
+ border: 1px solid var(--c-border);
+ padding: 6px 10px;
+}
+
+/* ---------- footer ---------- */
+.app-footer {
+ border-top: 1px solid var(--c-border);
+ background: var(--c-surface);
+ margin-top: 32px;
+}
+.app-footer__inner {
+ max-width: var(--max-w);
+ margin: 0 auto;
+ padding: 22px 20px;
+ color: var(--c-text-muted);
+ font-size: 13px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 24px;
+ justify-content: space-between;
+}
+.app-footer__inner p { margin: 0; }
+
+/* ---------- responsive ---------- */
+@media (max-width: 720px) {
+ .app-header__inner {
+ flex-wrap: wrap;
+ height: auto;
+ padding: 10px 16px;
+ gap: 10px;
+ }
+ .app-nav { order: 3; flex-basis: 100%; margin-left: 0; overflow-x: auto; }
+ .app-search { order: 2; flex: 1; min-width: 160px; }
+ .app-search__label { width: 100%; }
+ .app-brand__name { font-size: 15px; }
+ .app-main { padding: 18px 14px 48px; }
+ .app-hero { padding: 24px 18px; }
+ .app-detail { padding: 22px 16px 36px; }
+}
diff --git a/docs/vendor/marked.esm.js b/docs/vendor/marked.esm.js
new file mode 100644
index 000000000..c8758abef
--- /dev/null
+++ b/docs/vendor/marked.esm.js
@@ -0,0 +1,48 @@
+/* esm.sh - marked@12.0.2 */
+function O(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var T=O();function ee(a){T=a}var te=/[&<>"']/,he=new RegExp(te.source,"g"),ne=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,pe=new RegExp(ne.source,"g"),ue={"&":"&","<":"<",">":">",'"':""","'":"'"},W=a=>ue[a];function d(a,n){if(n){if(te.test(a))return a.replace(he,W)}else if(ne.test(a))return a.replace(pe,W);return a}var fe=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;function ge(a){return a.replace(fe,(n,e)=>(e=e.toLowerCase(),e==="colon"?":":e.charAt(0)==="#"?e.charAt(1)==="x"?String.fromCharCode(parseInt(e.substring(2),16)):String.fromCharCode(+e.substring(1)):""))}var ke=/(^|[^\[])\^/g;function k(a,n){let e=typeof a=="string"?a:a.source;n=n||"";let t={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(ke,"$1"),e=e.replace(r,s),t},getRegex:()=>new RegExp(e,n)};return t}function J(a){try{a=encodeURI(a).replace(/%25/g,"%")}catch{return null}return a}var S={exec:()=>null};function K(a,n){let e=a.replace(/\|/g,(i,s,l)=>{let o=!1,p=s;for(;--p>=0&&l[p]==="\\";)o=!o;return o?"|":" |"}),t=e.split(/ \|/),r=0;if(t[0].trim()||t.shift(),t.length>0&&!t[t.length-1].trim()&&t.pop(),n)if(t.length>n)t.splice(n);else for(;t.length{let i=r.match(/^\s+/);if(i===null)return r;let[s]=i;return s.length>=t.length?r.slice(t.length):r}).join(`
+`)}var R=class{options;rules;lexer;constructor(n){this.options=n||T}space(n){let e=this.rules.block.newline.exec(n);if(e&&e[0].length>0)return{type:"space",raw:e[0]}}code(n){let e=this.rules.block.code.exec(n);if(e){let t=e[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:e[0],codeBlockStyle:"indented",text:this.options.pedantic?t:L(t,`
+`)}}}fences(n){let e=this.rules.block.fences.exec(n);if(e){let t=e[0],r=xe(t,e[3]||"");return{type:"code",raw:t,lang:e[2]?e[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):e[2],text:r}}}heading(n){let e=this.rules.block.heading.exec(n);if(e){let t=e[2].trim();if(/#$/.test(t)){let r=L(t,"#");(this.options.pedantic||!r||/ $/.test(r))&&(t=r.trim())}return{type:"heading",raw:e[0],depth:e[1].length,text:t,tokens:this.lexer.inline(t)}}}hr(n){let e=this.rules.block.hr.exec(n);if(e)return{type:"hr",raw:e[0]}}blockquote(n){let e=this.rules.block.blockquote.exec(n);if(e){let t=e[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,`
+ $1`);t=L(t.replace(/^ *>[ \t]?/gm,""),`
+`);let r=this.lexer.state.top;this.lexer.state.top=!0;let i=this.lexer.blockTokens(t);return this.lexer.state.top=r,{type:"blockquote",raw:e[0],tokens:i,text:t}}}list(n){let e=this.rules.block.list.exec(n);if(e){let t=e[1].trim(),r=t.length>1,i={type:"list",raw:"",ordered:r,start:r?+t.slice(0,-1):"",loose:!1,items:[]};t=r?`\\d{1,9}\\${t.slice(-1)}`:`\\${t}`,this.options.pedantic&&(t=r?t:"[*+-]");let s=new RegExp(`^( {0,3}${t})((?:[ ][^\\n]*)?(?:\\n|$))`),l="",o="",p=!1;for(;n;){let c=!1;if(!(e=s.exec(n))||this.rules.block.hr.test(n))break;l=e[0],n=n.substring(l.length);let u=e[2].split(`
+`,1)[0].replace(/^\t+/,B=>" ".repeat(3*B.length)),h=n.split(`
+`,1)[0],g=0;this.options.pedantic?(g=2,o=u.trimStart()):(g=e[2].search(/[^ ]/),g=g>4?1:g,o=u.slice(g),g+=e[1].length);let w=!1;if(!u&&/^ *$/.test(h)&&(l+=h+`
+`,n=n.substring(h.length+1),c=!0),!c){let B=new RegExp(`^ {0,${Math.min(3,g-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),U=new RegExp(`^ {0,${Math.min(3,g-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),X=new RegExp(`^ {0,${Math.min(3,g-1)}}(?:\`\`\`|~~~)`),G=new RegExp(`^ {0,${Math.min(3,g-1)}}#`);for(;n;){let v=n.split(`
+`,1)[0];if(h=v,this.options.pedantic&&(h=h.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),X.test(h)||G.test(h)||B.test(h)||U.test(n))break;if(h.search(/[^ ]/)>=g||!h.trim())o+=`
+`+h.slice(g);else{if(w||u.search(/[^ ]/)>=4||X.test(u)||G.test(u)||U.test(u))break;o+=`
+`+h}!w&&!h.trim()&&(w=!0),l+=v+`
+`,n=n.substring(v.length+1),u=h.slice(g)}}i.loose||(p?i.loose=!0:/\n *\n *$/.test(l)&&(p=!0));let x=null,y;this.options.gfm&&(x=/^\[[ xX]\] /.exec(o),x&&(y=x[0]!=="[ ] ",o=o.replace(/^\[[ xX]\] +/,""))),i.items.push({type:"list_item",raw:l,task:!!x,checked:y,loose:!1,text:o,tokens:[]}),i.raw+=l}i.items[i.items.length-1].raw=l.trimEnd(),i.items[i.items.length-1].text=o.trimEnd(),i.raw=i.raw.trimEnd();for(let c=0;cg.type==="space"),h=u.length>0&&u.some(g=>/\n.*\n/.test(g.raw));i.loose=h}if(i.loose)for(let c=0;c$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=e[3]?e[3].substring(1,e[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):e[3];return{type:"def",tag:t,raw:e[0],href:r,title:i}}}table(n){let e=this.rules.block.table.exec(n);if(!e||!/[:|]/.test(e[2]))return;let t=K(e[1]),r=e[2].replace(/^\||\| *$/g,"").split("|"),i=e[3]&&e[3].trim()?e[3].replace(/\n[ \t]*$/,"").split(`
+`):[],s={type:"table",raw:e[0],header:[],align:[],rows:[]};if(t.length===r.length){for(let l of r)/^ *-+: *$/.test(l)?s.align.push("right"):/^ *:-+: *$/.test(l)?s.align.push("center"):/^ *:-+ *$/.test(l)?s.align.push("left"):s.align.push(null);for(let l of t)s.header.push({text:l,tokens:this.lexer.inline(l)});for(let l of i)s.rows.push(K(l,s.header.length).map(o=>({text:o,tokens:this.lexer.inline(o)})));return s}}lheading(n){let e=this.rules.block.lheading.exec(n);if(e)return{type:"heading",raw:e[0],depth:e[2].charAt(0)==="="?1:2,text:e[1],tokens:this.lexer.inline(e[1])}}paragraph(n){let e=this.rules.block.paragraph.exec(n);if(e){let t=e[1].charAt(e[1].length-1)===`
+`?e[1].slice(0,-1):e[1];return{type:"paragraph",raw:e[0],text:t,tokens:this.lexer.inline(t)}}}text(n){let e=this.rules.block.text.exec(n);if(e)return{type:"text",raw:e[0],text:e[0],tokens:this.lexer.inline(e[0])}}escape(n){let e=this.rules.inline.escape.exec(n);if(e)return{type:"escape",raw:e[0],text:d(e[1])}}tag(n){let e=this.rules.inline.tag.exec(n);if(e)return!this.lexer.state.inLink&&/^/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:e[0]}}link(n){let e=this.rules.inline.link.exec(n);if(e){let t=e[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;let s=L(t.slice(0,-1),"\\");if((t.length-s.length)%2===0)return}else{let s=de(e[2],"()");if(s>-1){let o=(e[0].indexOf("!")===0?5:4)+e[1].length+s;e[2]=e[2].substring(0,s),e[0]=e[0].substring(0,o).trim(),e[3]=""}}let r=e[2],i="";if(this.options.pedantic){let s=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r);s&&(r=s[1],i=s[3])}else i=e[3]?e[3].slice(1,-1):"";return r=r.trim(),/^$/.test(t)?r=r.slice(1):r=r.slice(1,-1)),V(e,{href:r&&r.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},e[0],this.lexer)}}reflink(n,e){let t;if((t=this.rules.inline.reflink.exec(n))||(t=this.rules.inline.nolink.exec(n))){let r=(t[2]||t[1]).replace(/\s+/g," "),i=e[r.toLowerCase()];if(!i){let s=t[0].charAt(0);return{type:"text",raw:s,text:s}}return V(t,i,t[0],this.lexer)}}emStrong(n,e,t=""){let r=this.rules.inline.emStrongLDelim.exec(n);if(!r||r[3]&&t.match(/[\p{L}\p{N}]/u))return;if(!(r[1]||r[2]||"")||!t||this.rules.inline.punctuation.exec(t)){let s=[...r[0]].length-1,l,o,p=s,c=0,u=r[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,e=e.slice(-1*n.length+s);(r=u.exec(e))!=null;){if(l=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!l)continue;if(o=[...l].length,r[3]||r[4]){p+=o;continue}else if((r[5]||r[6])&&s%3&&!((s+o)%3)){c+=o;continue}if(p-=o,p>0)continue;o=Math.min(o,o+p+c);let h=[...r[0]][0].length,g=n.slice(0,s+r.index+h+o);if(Math.min(s,o)%2){let x=g.slice(1,-1);return{type:"em",raw:g,text:x,tokens:this.lexer.inlineTokens(x)}}let w=g.slice(2,-2);return{type:"strong",raw:g,text:w,tokens:this.lexer.inlineTokens(w)}}}}codespan(n){let e=this.rules.inline.code.exec(n);if(e){let t=e[2].replace(/\n/g," "),r=/[^ ]/.test(t),i=/^ /.test(t)&&/ $/.test(t);return r&&i&&(t=t.substring(1,t.length-1)),t=d(t,!0),{type:"codespan",raw:e[0],text:t}}}br(n){let e=this.rules.inline.br.exec(n);if(e)return{type:"br",raw:e[0]}}del(n){let e=this.rules.inline.del.exec(n);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2])}}autolink(n){let e=this.rules.inline.autolink.exec(n);if(e){let t,r;return e[2]==="@"?(t=d(e[1]),r="mailto:"+t):(t=d(e[1]),r=t),{type:"link",raw:e[0],text:t,href:r,tokens:[{type:"text",raw:t,text:t}]}}}url(n){let e;if(e=this.rules.inline.url.exec(n)){let t,r;if(e[2]==="@")t=d(e[0]),r="mailto:"+t;else{let i;do i=e[0],e[0]=this.rules.inline._backpedal.exec(e[0])?.[0]??"";while(i!==e[0]);t=d(e[0]),e[1]==="www."?r="http://"+e[0]:r=e[0]}return{type:"link",raw:e[0],text:t,href:r,tokens:[{type:"text",raw:t,text:t}]}}}inlineText(n){let e=this.rules.inline.text.exec(n);if(e){let t;return this.lexer.state.inRawBlock?t=e[0]:t=d(e[0]),{type:"text",raw:e[0],text:t}}}},be=/^(?: *(?:\n|$))+/,me=/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,we=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,E=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ye=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,se=/(?:[*+-]|\d{1,9}[.)])/,ie=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,se).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),M=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,$e=/^[^\n]+/,j=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Te=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",j).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),ze=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,se).getRegex(),Z="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",N=/|$))/,Re=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",N).replace("tag",Z).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),re=k(M).replace("hr",E).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",Z).getRegex(),_e=k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",re).getRegex(),H={blockquote:_e,code:me,def:Te,fences:we,heading:ye,hr:E,html:Re,lheading:ie,list:ze,newline:be,paragraph:re,table:S,text:$e},Y=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",E).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",Z).getRegex(),Ie={...H,table:Y,paragraph:k(M).replace("hr",E).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Y).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",Z).getRegex()},Se={...H,html:k(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",N).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:S,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(M).replace("hr",E).replace("heading",` *#{1,6} *[^
+]`).replace("lheading",ie).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},le=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,Ae=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,oe=/^( {2,}|\\)\n(?!\s*$)/,Ee=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,qe=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,C).getRegex(),Pe=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,C).getRegex(),Ze=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,C).getRegex(),Be=k(/\\([punct])/,"gu").replace(/punct/g,C).getRegex(),ve=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Qe=k(N).replace("(?:-->|$)","-->").getRegex(),De=k("^comment|^[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",Qe).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),P=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Oe=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",P).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ae=k(/^!?\[(label)\]\[(ref)\]/).replace("label",P).replace("ref",j).getRegex(),ce=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",j).getRegex(),Me=k("reflink|nolink(?!\\()","g").replace("reflink",ae).replace("nolink",ce).getRegex(),F={_backpedal:S,anyPunctuation:Be,autolink:ve,blockSkip:Le,br:oe,code:Ae,del:S,emStrongLDelim:qe,emStrongRDelimAst:Pe,emStrongRDelimUnd:Ze,escape:le,link:Oe,nolink:ce,punctuation:Ce,reflink:ae,reflinkSearch:Me,tag:De,text:Ee,url:S},je={...F,link:k(/^!?\[(label)\]\((.*?)\)/).replace("label",P).getRegex(),reflink:k(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",P).getRegex()},Q={...F,escape:k(le).replace("])","~|])").getRegex(),url:k(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\o+" ".repeat(p.length));let t,r,i,s;for(;n;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some(l=>(t=l.call({lexer:this},n,e))?(n=n.substring(t.raw.length),e.push(t),!0):!1))){if(t=this.tokenizer.space(n)){n=n.substring(t.raw.length),t.raw.length===1&&e.length>0?e[e.length-1].raw+=`
+`:e.push(t);continue}if(t=this.tokenizer.code(n)){n=n.substring(t.raw.length),r=e[e.length-1],r&&(r.type==="paragraph"||r.type==="text")?(r.raw+=`
+`+t.raw,r.text+=`
+`+t.text,this.inlineQueue[this.inlineQueue.length-1].src=r.text):e.push(t);continue}if(t=this.tokenizer.fences(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.heading(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.hr(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.blockquote(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.list(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.html(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.def(n)){n=n.substring(t.raw.length),r=e[e.length-1],r&&(r.type==="paragraph"||r.type==="text")?(r.raw+=`
+`+t.raw,r.text+=`
+`+t.raw,this.inlineQueue[this.inlineQueue.length-1].src=r.text):this.tokens.links[t.tag]||(this.tokens.links[t.tag]={href:t.href,title:t.title});continue}if(t=this.tokenizer.table(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.lheading(n)){n=n.substring(t.raw.length),e.push(t);continue}if(i=n,this.options.extensions&&this.options.extensions.startBlock){let l=1/0,o=n.slice(1),p;this.options.extensions.startBlock.forEach(c=>{p=c.call({lexer:this},o),typeof p=="number"&&p>=0&&(l=Math.min(l,p))}),l<1/0&&l>=0&&(i=n.substring(0,l+1))}if(this.state.top&&(t=this.tokenizer.paragraph(i))){r=e[e.length-1],s&&r.type==="paragraph"?(r.raw+=`
+`+t.raw,r.text+=`
+`+t.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):e.push(t),s=i.length!==n.length,n=n.substring(t.raw.length);continue}if(t=this.tokenizer.text(n)){n=n.substring(t.raw.length),r=e[e.length-1],r&&r.type==="text"?(r.raw+=`
+`+t.raw,r.text+=`
+`+t.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):e.push(t);continue}if(n){let l="Infinite loop on byte: "+n.charCodeAt(0);if(this.options.silent){console.error(l);break}else throw new Error(l)}}return this.state.top=!0,e}inline(n,e=[]){return this.inlineQueue.push({src:n,tokens:e}),e}inlineTokens(n,e=[]){let t,r,i,s=n,l,o,p;if(this.tokens.links){let c=Object.keys(this.tokens.links);if(c.length>0)for(;(l=this.tokenizer.rules.inline.reflinkSearch.exec(s))!=null;)c.includes(l[0].slice(l[0].lastIndexOf("[")+1,-1))&&(s=s.slice(0,l.index)+"["+"a".repeat(l[0].length-2)+"]"+s.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(l=this.tokenizer.rules.inline.blockSkip.exec(s))!=null;)s=s.slice(0,l.index)+"["+"a".repeat(l[0].length-2)+"]"+s.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;(l=this.tokenizer.rules.inline.anyPunctuation.exec(s))!=null;)s=s.slice(0,l.index)+"++"+s.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;n;)if(o||(p=""),o=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some(c=>(t=c.call({lexer:this},n,e))?(n=n.substring(t.raw.length),e.push(t),!0):!1))){if(t=this.tokenizer.escape(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.tag(n)){n=n.substring(t.raw.length),r=e[e.length-1],r&&t.type==="text"&&r.type==="text"?(r.raw+=t.raw,r.text+=t.text):e.push(t);continue}if(t=this.tokenizer.link(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.reflink(n,this.tokens.links)){n=n.substring(t.raw.length),r=e[e.length-1],r&&t.type==="text"&&r.type==="text"?(r.raw+=t.raw,r.text+=t.text):e.push(t);continue}if(t=this.tokenizer.emStrong(n,s,p)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.codespan(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.br(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.del(n)){n=n.substring(t.raw.length),e.push(t);continue}if(t=this.tokenizer.autolink(n)){n=n.substring(t.raw.length),e.push(t);continue}if(!this.state.inLink&&(t=this.tokenizer.url(n))){n=n.substring(t.raw.length),e.push(t);continue}if(i=n,this.options.extensions&&this.options.extensions.startInline){let c=1/0,u=n.slice(1),h;this.options.extensions.startInline.forEach(g=>{h=g.call({lexer:this},u),typeof h=="number"&&h>=0&&(c=Math.min(c,h))}),c<1/0&&c>=0&&(i=n.substring(0,c+1))}if(t=this.tokenizer.inlineText(i)){n=n.substring(t.raw.length),t.raw.slice(-1)!=="_"&&(p=t.raw.slice(-1)),o=!0,r=e[e.length-1],r&&r.type==="text"?(r.raw+=t.raw,r.text+=t.text):e.push(t);continue}if(n){let c="Infinite loop on byte: "+n.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return e}},_=class{options;constructor(n){this.options=n||T}code(n,e,t){let r=(e||"").match(/^\S*/)?.[0];return n=n.replace(/\n$/,"")+`
+`,r?''+(t?n:d(n,!0))+`
+`:""+(t?n:d(n,!0))+`
+`}blockquote(n){return`
+${n}
+`}html(n,e){return n}heading(n,e,t){return`${n}
+`}hr(){return`
+`}list(n,e,t){let r=e?"ol":"ul",i=e&&t!==1?' start="'+t+'"':"";return"<"+r+i+`>
+`+n+""+r+`>
+`}listitem(n,e,t){return`${n}
+`}checkbox(n){return"'}paragraph(n){return`${n}
+`}table(n,e){return e&&(e=`${e}`),`
+`}tablerow(n){return`
+${n}
+`}tablecell(n,e){let t=e.header?"th":"td";return(e.align?`<${t} align="${e.align}">`:`<${t}>`)+n+`${t}>
+`}strong(n){return`${n}`}em(n){return`${n}`}codespan(n){return`${n}`}br(){return"
"}del(n){return`${n}`}link(n,e,t){let r=J(n);if(r===null)return t;n=r;let i='"+t+"",i}image(n,e,t){let r=J(n);if(r===null)return t;n=r;let i=`
",i}text(n){return n}},A=class{strong(n){return n}em(n){return n}codespan(n){return n}del(n){return n}html(n){return n}text(n){return n}link(n,e,t){return""+t}image(n,e,t){return""+t}br(){return""}},m=class a{options;renderer;textRenderer;constructor(n){this.options=n||T,this.options.renderer=this.options.renderer||new _,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new A}static parse(n,e){return new a(e).parse(n)}static parseInline(n,e){return new a(e).parseInline(n)}parse(n,e=!0){let t="";for(let r=0;r0&&h.tokens[0].type==="paragraph"?(h.tokens[0].text=y+" "+h.tokens[0].text,h.tokens[0].tokens&&h.tokens[0].tokens.length>0&&h.tokens[0].tokens[0].type==="text"&&(h.tokens[0].tokens[0].text=y+" "+h.tokens[0].tokens[0].text)):h.tokens.unshift({type:"text",text:y+" "}):x+=y+" "}x+=this.parse(h.tokens,p),c+=this.renderer.listitem(x,w,!!g)}t+=this.renderer.list(c,l,o);continue}case"html":{let s=i;t+=this.renderer.html(s.text,s.block);continue}case"paragraph":{let s=i;t+=this.renderer.paragraph(this.parseInline(s.tokens));continue}case"text":{let s=i,l=s.tokens?this.parseInline(s.tokens):s.text;for(;r+1{let l=i[s].flat(1/0);t=t.concat(this.walkTokens(l,e))}):i.tokens&&(t=t.concat(this.walkTokens(i.tokens,e)))}}return t}use(...n){let e=this.defaults.extensions||{renderers:{},childTokens:{}};return n.forEach(t=>{let r={...t};if(r.async=this.defaults.async||r.async||!1,t.extensions&&(t.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let s=e.renderers[i.name];s?e.renderers[i.name]=function(...l){let o=i.renderer.apply(this,l);return o===!1&&(o=s.apply(this,l)),o}:e.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let s=e[i.level];s?s.unshift(i.tokenizer):e[i.level]=[i.tokenizer],i.start&&(i.level==="block"?e.startBlock?e.startBlock.push(i.start):e.startBlock=[i.start]:i.level==="inline"&&(e.startInline?e.startInline.push(i.start):e.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(e.childTokens[i.name]=i.childTokens)}),r.extensions=e),t.renderer){let i=this.defaults.renderer||new _(this.defaults);for(let s in t.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(s==="options")continue;let l=s,o=t.renderer[l],p=i[l];i[l]=(...c)=>{let u=o.apply(i,c);return u===!1&&(u=p.apply(i,c)),u||""}}r.renderer=i}if(t.tokenizer){let i=this.defaults.tokenizer||new R(this.defaults);for(let s in t.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;let l=s,o=t.tokenizer[l],p=i[l];i[l]=(...c)=>{let u=o.apply(i,c);return u===!1&&(u=p.apply(i,c)),u}}r.tokenizer=i}if(t.hooks){let i=this.defaults.hooks||new z;for(let s in t.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(s==="options")continue;let l=s,o=t.hooks[l],p=i[l];z.passThroughHooks.has(s)?i[l]=c=>{if(this.defaults.async)return Promise.resolve(o.call(i,c)).then(h=>p.call(i,h));let u=o.call(i,c);return p.call(i,u)}:i[l]=(...c)=>{let u=o.apply(i,c);return u===!1&&(u=p.apply(i,c)),u}}r.hooks=i}if(t.walkTokens){let i=this.defaults.walkTokens,s=t.walkTokens;r.walkTokens=function(l){let o=[];return o.push(s.call(this,l)),i&&(o=o.concat(i.call(this,l))),o}}this.defaults={...this.defaults,...r}}),this}setOptions(n){return this.defaults={...this.defaults,...n},this}lexer(n,e){return b.lex(n,e??this.defaults)}parser(n,e){return m.parse(n,e??this.defaults)}#e(n,e){return(t,r)=>{let i={...r},s={...this.defaults,...i};this.defaults.async===!0&&i.async===!1&&(s.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),s.async=!0);let l=this.#t(!!s.silent,!!s.async);if(typeof t>"u"||t===null)return l(new Error("marked(): input parameter is undefined or null"));if(typeof t!="string")return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));if(s.hooks&&(s.hooks.options=s),s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(t):t).then(o=>n(o,s)).then(o=>s.hooks?s.hooks.processAllTokens(o):o).then(o=>s.walkTokens?Promise.all(this.walkTokens(o,s.walkTokens)).then(()=>o):o).then(o=>e(o,s)).then(o=>s.hooks?s.hooks.postprocess(o):o).catch(l);try{s.hooks&&(t=s.hooks.preprocess(t));let o=n(t,s);s.hooks&&(o=s.hooks.processAllTokens(o)),s.walkTokens&&this.walkTokens(o,s.walkTokens);let p=e(o,s);return s.hooks&&(p=s.hooks.postprocess(p)),p}catch(o){return l(o)}}}#t(n,e){return t=>{if(t.message+=`
+Please report this to https://github.com/markedjs/marked.`,n){let r="An error occurred:
"+d(t.message+"",!0)+"
";return e?Promise.resolve(r):r}if(e)return Promise.reject(t);throw t}}},$=new D;function f(a,n){return $.parse(a,n)}f.options=f.setOptions=function(a){return $.setOptions(a),f.defaults=$.defaults,ee(f.defaults),f};f.getDefaults=O;f.defaults=T;f.use=function(...a){return $.use(...a),f.defaults=$.defaults,ee(f.defaults),f};f.walkTokens=function(a,n){return $.walkTokens(a,n)};f.parseInline=$.parseInline;f.Parser=m;f.parser=m.parse;f.Renderer=_;f.TextRenderer=A;f.Lexer=b;f.lexer=b.lex;f.Tokenizer=R;f.Hooks=z;f.parse=f;var He=f.options,Fe=f.setOptions,Ue=f.use,Xe=f.walkTokens,Ge=f.parseInline,We=f,Je=m.parse,Ke=b.lex;export{z as Hooks,b as Lexer,D as Marked,m as Parser,_ as Renderer,A as TextRenderer,R as Tokenizer,T as defaults,O as getDefaults,Ke as lexer,f as marked,He as options,We as parse,Ge as parseInline,Je as parser,Fe as setOptions,Ue as use,Xe as walkTokens};
+//# sourceMappingURL=marked.bundle.mjs.map
\ No newline at end of file
diff --git a/docs/vendor/minisearch.esm.js b/docs/vendor/minisearch.esm.js
new file mode 100644
index 000000000..be4ff891f
--- /dev/null
+++ b/docs/vendor/minisearch.esm.js
@@ -0,0 +1,3 @@
+/* esm.sh - minisearch@7.1.0 */
+function M(i,t,e,s){function n(o){return o instanceof e?o:new e(function(r){r(o)})}return new(e||(e=Promise))(function(o,r){function c(h){try{u(s.next(h))}catch(l){r(l)}}function d(h){try{u(s.throw(h))}catch(l){r(l)}}function u(h){h.done?o(h.value):n(h.value).then(c,d)}u((s=s.apply(i,t||[])).next())})}var X="ENTRIES",U="KEYS",q="VALUES",p="",v=class{constructor(t,e){let s=t._tree,n=Array.from(s.keys());this.set=t,this._type=e,this._path=n.length>0?[{node:s,keys:n}]:[]}next(){let t=this.dive();return this.backtrack(),t}dive(){if(this._path.length===0)return{done:!0,value:void 0};let{node:t,keys:e}=S(this._path);if(S(e)===p)return{done:!1,value:this.result()};let s=t.get(S(e));return this._path.push({node:s,keys:Array.from(s.keys())}),this.dive()}backtrack(){if(this._path.length===0)return;let t=S(this._path).keys;t.pop(),!(t.length>0)&&(this._path.pop(),this.backtrack())}key(){return this.set._prefix+this._path.map(({keys:t})=>S(t)).filter(t=>t!==p).join("")}value(){return S(this._path).node.get(p)}result(){switch(this._type){case q:return this.value();case U:return this.key();default:return[this.key(),this.value()]}}[Symbol.iterator](){return this}},S=i=>i[i.length-1],tt=(i,t,e)=>{let s=new Map;if(t===void 0)return s;let n=t.length+1,o=n+e,r=new Uint8Array(o*n).fill(e+1);for(let c=0;c{let d=o*r;t:for(let u of i.keys())if(u===p){let h=n[d-1];h<=e&&s.set(c,[i.get(u),h])}else{let h=o;for(let l=0;le)continue t}K(i.get(u),t,e,s,n,h,r,c+u)}},x=class i{constructor(t=new Map,e=""){this._size=void 0,this._tree=t,this._prefix=e}atPrefix(t){if(!t.startsWith(this._prefix))throw new Error("Mismatched prefix");let[e,s]=k(this._tree,t.slice(this._prefix.length));if(e===void 0){let[n,o]=R(s);for(let r of n.keys())if(r!==p&&r.startsWith(o)){let c=new Map;return c.set(r.slice(o.length),n.get(r)),new i(c,t)}}return new i(e,t)}clear(){this._size=void 0,this._tree.clear()}delete(t){return this._size=void 0,et(this._tree,t)}entries(){return new v(this,X)}forEach(t){for(let[e,s]of this)t(e,s,this)}fuzzyGet(t,e){return tt(this._tree,t,e)}get(t){let e=A(this._tree,t);return e!==void 0?e.get(p):void 0}has(t){let e=A(this._tree,t);return e!==void 0&&e.has(p)}keys(){return new v(this,U)}set(t,e){if(typeof t!="string")throw new Error("key must be a string");return this._size=void 0,j(this._tree,t).set(p,e),this}get size(){if(this._size)return this._size;this._size=0;let t=this.entries();for(;!t.next().done;)this._size+=1;return this._size}update(t,e){if(typeof t!="string")throw new Error("key must be a string");this._size=void 0;let s=j(this._tree,t);return s.set(p,e(s.get(p))),this}fetch(t,e){if(typeof t!="string")throw new Error("key must be a string");this._size=void 0;let s=j(this._tree,t),n=s.get(p);return n===void 0&&s.set(p,n=e()),n}values(){return new v(this,q)}[Symbol.iterator](){return this.entries()}static from(t){let e=new i;for(let[s,n]of t)e.set(s,n);return e}static fromObject(t){return i.from(Object.entries(t))}},k=(i,t,e=[])=>{if(t.length===0||i==null)return[i,e];for(let s of i.keys())if(s!==p&&t.startsWith(s))return e.push([i,s]),k(i.get(s),t.slice(s.length),e);return e.push([i,t]),k(void 0,"",e)},A=(i,t)=>{if(t.length===0||i==null)return i;for(let e of i.keys())if(e!==p&&t.startsWith(e))return A(i.get(e),t.slice(e.length))},j=(i,t)=>{let e=t.length;t:for(let s=0;i&&s{let[e,s]=k(i,t);if(e!==void 0){if(e.delete(p),e.size===0)G(s);else if(e.size===1){let[n,o]=e.entries().next().value;Y(s,n,o)}}},G=i=>{if(i.length===0)return;let[t,e]=R(i);if(t.delete(e),t.size===0)G(i.slice(0,-1));else if(t.size===1){let[s,n]=t.entries().next().value;s!==p&&Y(i.slice(0,-1),s,n)}},Y=(i,t,e)=>{if(i.length===0)return;let[s,n]=R(i);s.set(n+t,e),s.delete(n)},R=i=>i[i.length-1],P="or",Z="and",st="and_not",D=class i{constructor(t){if(t?.fields==null)throw new Error('MiniSearch: option "fields" must be provided');let e=t.autoVacuum==null||t.autoVacuum===!0?E:t.autoVacuum;this._options=Object.assign(Object.assign(Object.assign({},T),t),{autoVacuum:e,searchOptions:Object.assign(Object.assign({},J),t.searchOptions||{}),autoSuggestOptions:Object.assign(Object.assign({},ct),t.autoSuggestOptions||{})}),this._index=new x,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldIds={},this._fieldLength=new Map,this._avgFieldLength=[],this._nextId=0,this._storedFields=new Map,this._dirtCount=0,this._currentVacuum=null,this._enqueuedVacuum=null,this._enqueuedVacuumConditions=W,this.addFields(this._options.fields)}add(t){let{extractField:e,tokenize:s,processTerm:n,fields:o,idField:r}=this._options,c=e(t,r);if(c==null)throw new Error(`MiniSearch: document does not have ID field "${r}"`);if(this._idToShortId.has(c))throw new Error(`MiniSearch: duplicate ID ${c}`);let d=this.addDocumentId(c);this.saveStoredFields(d,t);for(let u of o){let h=e(t,u);if(h==null)continue;let l=s(h.toString(),u),a=this._fieldIds[u],m=new Set(l).size;this.addFieldLength(d,a,this._documentCount-1,m);for(let _ of l){let f=n(_,u);if(Array.isArray(f))for(let g of f)this.addTerm(a,d,g);else f&&this.addTerm(a,d,f)}}}addAll(t){for(let e of t)this.add(e)}addAllAsync(t,e={}){let{chunkSize:s=10}=e,n={chunk:[],promise:Promise.resolve()},{chunk:o,promise:r}=t.reduce(({chunk:c,promise:d},u,h)=>(c.push(u),(h+1)%s===0?{chunk:[],promise:d.then(()=>new Promise(l=>setTimeout(l,0))).then(()=>this.addAll(c))}:{chunk:c,promise:d}),n);return r.then(()=>this.addAll(o))}remove(t){let{tokenize:e,processTerm:s,extractField:n,fields:o,idField:r}=this._options,c=n(t,r);if(c==null)throw new Error(`MiniSearch: document does not have ID field "${r}"`);let d=this._idToShortId.get(c);if(d==null)throw new Error(`MiniSearch: cannot remove document with ID ${c}: it is not in the index`);for(let u of o){let h=n(t,u);if(h==null)continue;let l=e(h.toString(),u),a=this._fieldIds[u],m=new Set(l).size;this.removeFieldLength(d,a,this._documentCount,m);for(let _ of l){let f=s(_,u);if(Array.isArray(f))for(let g of f)this.removeTerm(a,d,g);else f&&this.removeTerm(a,d,f)}}this._storedFields.delete(d),this._documentIds.delete(d),this._idToShortId.delete(c),this._fieldLength.delete(d),this._documentCount-=1}removeAll(t){if(t)for(let e of t)this.remove(e);else{if(arguments.length>0)throw new Error("Expected documents to be present. Omit the argument to remove all documents.");this._index=new x,this._documentCount=0,this._documentIds=new Map,this._idToShortId=new Map,this._fieldLength=new Map,this._avgFieldLength=[],this._storedFields=new Map,this._nextId=0}}discard(t){let e=this._idToShortId.get(t);if(e==null)throw new Error(`MiniSearch: cannot discard document with ID ${t}: it is not in the index`);this._idToShortId.delete(t),this._documentIds.delete(e),this._storedFields.delete(e),(this._fieldLength.get(e)||[]).forEach((s,n)=>{this.removeFieldLength(e,n,this._documentCount,s)}),this._fieldLength.delete(e),this._documentCount-=1,this._dirtCount+=1,this.maybeAutoVacuum()}maybeAutoVacuum(){if(this._options.autoVacuum===!1)return;let{minDirtFactor:t,minDirtCount:e,batchSize:s,batchWait:n}=this._options.autoVacuum;this.conditionalVacuum({batchSize:s,batchWait:n},{minDirtCount:e,minDirtFactor:t})}discardAll(t){let e=this._options.autoVacuum;try{this._options.autoVacuum=!1;for(let s of t)this.discard(s)}finally{this._options.autoVacuum=e}this.maybeAutoVacuum()}replace(t){let{idField:e,extractField:s}=this._options,n=s(t,e);this.discard(n),this.add(t)}vacuum(t={}){return this.conditionalVacuum(t)}conditionalVacuum(t,e){return this._currentVacuum?(this._enqueuedVacuumConditions=this._enqueuedVacuumConditions&&e,this._enqueuedVacuum!=null?this._enqueuedVacuum:(this._enqueuedVacuum=this._currentVacuum.then(()=>{let s=this._enqueuedVacuumConditions;return this._enqueuedVacuumConditions=W,this.performVacuuming(t,s)}),this._enqueuedVacuum)):this.vacuumConditionsMet(e)===!1?Promise.resolve():(this._currentVacuum=this.performVacuuming(t),this._currentVacuum)}performVacuuming(t,e){return M(this,void 0,void 0,function*(){let s=this._dirtCount;if(this.vacuumConditionsMet(e)){let n=t.batchSize||N.batchSize,o=t.batchWait||N.batchWait,r=1;for(let[c,d]of this._index){for(let[u,h]of d)for(let[l]of h)this._documentIds.has(l)||(h.size<=1?d.delete(u):h.delete(l));this._index.get(c).size===0&&this._index.delete(c),r%n===0&&(yield new Promise(u=>setTimeout(u,o))),r+=1}this._dirtCount-=s}yield null,this._currentVacuum=this._enqueuedVacuum,this._enqueuedVacuum=null})}vacuumConditionsMet(t){if(t==null)return!0;let{minDirtCount:e,minDirtFactor:s}=t;return e=e||E.minDirtCount,s=s||E.minDirtFactor,this.dirtCount>=e&&this.dirtFactor>=s}get isVacuuming(){return this._currentVacuum!=null}get dirtCount(){return this._dirtCount}get dirtFactor(){return this._dirtCount/(1+this._documentCount+this._dirtCount)}has(t){return this._idToShortId.has(t)}getStoredFields(t){let e=this._idToShortId.get(t);if(e!=null)return this._storedFields.get(e)}search(t,e={}){let s=this.executeQuery(t,e),n=[];for(let[o,{score:r,terms:c,match:d}]of s){let u=c.length||1,h={id:this._documentIds.get(o),score:r*u,terms:Object.keys(d),queryTerms:c,match:d};Object.assign(h,this._storedFields.get(o)),(e.filter==null||e.filter(h))&&n.push(h)}return t===i.wildcard&&e.boostDocument==null&&this._options.searchOptions.boostDocument==null||n.sort(Q),n}autoSuggest(t,e={}){e=Object.assign(Object.assign({},this._options.autoSuggestOptions),e);let s=new Map;for(let{score:o,terms:r}of this.search(t,e)){let c=r.join(" "),d=s.get(c);d!=null?(d.score+=o,d.count+=1):s.set(c,{score:o,terms:r,count:1})}let n=[];for(let[o,{score:r,terms:c,count:d}]of s)n.push({suggestion:o,terms:c,score:r/d});return n.sort(Q),n}get documentCount(){return this._documentCount}get termCount(){return this._index.size}static loadJSON(t,e){if(e==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJS(JSON.parse(t),e)}static loadJSONAsync(t,e){return M(this,void 0,void 0,function*(){if(e==null)throw new Error("MiniSearch: loadJSON should be given the same options used when serializing the index");return this.loadJSAsync(JSON.parse(t),e)})}static getDefault(t){if(T.hasOwnProperty(t))return L(T,t);throw new Error(`MiniSearch: unknown option "${t}"`)}static loadJS(t,e){let{index:s,documentIds:n,fieldLength:o,storedFields:r,serializationVersion:c}=t,d=this.instantiateMiniSearch(t,e);d._documentIds=O(n),d._fieldLength=O(o),d._storedFields=O(r);for(let[u,h]of d._documentIds)d._idToShortId.set(h,u);for(let[u,h]of s){let l=new Map;for(let a of Object.keys(h)){let m=h[a];c===1&&(m=m.ds),l.set(parseInt(a,10),O(m))}d._index.set(u,l)}return d}static loadJSAsync(t,e){return M(this,void 0,void 0,function*(){let{index:s,documentIds:n,fieldLength:o,storedFields:r,serializationVersion:c}=t,d=this.instantiateMiniSearch(t,e);d._documentIds=yield F(n),d._fieldLength=yield F(o),d._storedFields=yield F(r);for(let[h,l]of d._documentIds)d._idToShortId.set(l,h);let u=0;for(let[h,l]of s){let a=new Map;for(let m of Object.keys(l)){let _=l[m];c===1&&(_=_.ds),a.set(parseInt(m,10),yield F(_))}++u%1e3===0&&(yield H(0)),d._index.set(h,a)}return d})}static instantiateMiniSearch(t,e){let{documentCount:s,nextId:n,fieldIds:o,averageFieldLength:r,dirtCount:c,serializationVersion:d}=t;if(d!==1&&d!==2)throw new Error("MiniSearch: cannot deserialize an index created with an incompatible version");let u=new i(e);return u._documentCount=s,u._nextId=n,u._idToShortId=new Map,u._fieldIds=o,u._avgFieldLength=r,u._dirtCount=c||0,u._index=new x,u}executeQuery(t,e={}){if(t===i.wildcard)return this.executeWildcardQuery(e);if(typeof t!="string"){let a=Object.assign(Object.assign(Object.assign({},e),t),{queries:void 0}),m=t.queries.map(_=>this.executeQuery(_,a));return this.combineResults(m,a.combineWith)}let{tokenize:s,processTerm:n,searchOptions:o}=this._options,r=Object.assign(Object.assign({tokenize:s,processTerm:n},o),e),{tokenize:c,processTerm:d}=r,l=c(t).flatMap(a=>d(a)).filter(a=>!!a).map(rt(r)).map(a=>this.executeQuerySpec(a,r));return this.combineResults(l,r.combineWith)}executeQuerySpec(t,e){let s=Object.assign(Object.assign({},this._options.searchOptions),e),n=(s.fields||this._options.fields).reduce((f,g)=>Object.assign(Object.assign({},f),{[g]:L(s.boost,g)||1}),{}),{boostDocument:o,weights:r,maxFuzzy:c,bm25:d}=s,{fuzzy:u,prefix:h}=Object.assign(Object.assign({},J.weights),r),l=this._index.get(t.term),a=this.termResults(t.term,t.term,1,t.termBoost,l,n,o,d),m,_;if(t.prefix&&(m=this._index.atPrefix(t.term)),t.fuzzy){let f=t.fuzzy===!0?.2:t.fuzzy,g=f<1?Math.min(c,Math.round(t.term.length*f)):f;g&&(_=this._index.fuzzyGet(t.term,g))}if(m)for(let[f,g]of m){let b=f.length-t.term.length;if(!b)continue;_?.delete(f);let w=h*f.length/(f.length+.3*b);this.termResults(t.term,f,w,t.termBoost,g,n,o,d,a)}if(_)for(let f of _.keys()){let[g,b]=_.get(f);if(!b)continue;let w=u*f.length/(f.length+b);this.termResults(t.term,f,w,t.termBoost,g,n,o,d,a)}return a}executeWildcardQuery(t){let e=new Map,s=Object.assign(Object.assign({},this._options.searchOptions),t);for(let[n,o]of this._documentIds){let r=s.boostDocument?s.boostDocument(o,"",this._storedFields.get(n)):1;e.set(n,{score:r,terms:[],match:{}})}return e}combineResults(t,e=P){if(t.length===0)return new Map;let s=e.toLowerCase(),n=nt[s];if(!n)throw new Error(`Invalid combination operator: ${e}`);return t.reduce(n)||new Map}toJSON(){let t=[];for(let[e,s]of this._index){let n={};for(let[o,r]of s)n[o]=Object.fromEntries(r);t.push([e,n])}return{documentCount:this._documentCount,nextId:this._nextId,documentIds:Object.fromEntries(this._documentIds),fieldIds:this._fieldIds,fieldLength:Object.fromEntries(this._fieldLength),averageFieldLength:this._avgFieldLength,storedFields:Object.fromEntries(this._storedFields),dirtCount:this._dirtCount,index:t,serializationVersion:2}}termResults(t,e,s,n,o,r,c,d,u=new Map){if(o==null)return u;for(let h of Object.keys(r)){let l=r[h],a=this._fieldIds[h],m=o.get(a);if(m==null)continue;let _=m.size,f=this._avgFieldLength[a];for(let g of m.keys()){if(!this._documentIds.has(g)){this.removeTerm(a,g,e),_-=1;continue}let b=c?c(this._documentIds.get(g),e,this._storedFields.get(g)):1;if(!b)continue;let w=m.get(g),V=this._fieldLength.get(g)[a],C=ot(w,_,this._documentCount,V,f,d),I=s*n*l*b*C,y=u.get(g);if(y){y.score+=I,dt(y.terms,t);let z=L(y.match,e);z?z.push(h):y.match[e]=[h]}else u.set(g,{score:I,terms:[t],match:{[e]:[h]}})}}return u}addTerm(t,e,s){let n=this._index.fetch(s,B),o=n.get(t);if(o==null)o=new Map,o.set(e,1),n.set(t,o);else{let r=o.get(e);o.set(e,(r||0)+1)}}removeTerm(t,e,s){if(!this._index.has(s)){this.warnDocumentChanged(e,t,s);return}let n=this._index.fetch(s,B),o=n.get(t);o==null||o.get(e)==null?this.warnDocumentChanged(e,t,s):o.get(e)<=1?o.size<=1?n.delete(t):o.delete(e):o.set(e,o.get(e)-1),this._index.get(s).size===0&&this._index.delete(s)}warnDocumentChanged(t,e,s){for(let n of Object.keys(this._fieldIds))if(this._fieldIds[n]===e){this._options.logger("warn",`MiniSearch: document with ID ${this._documentIds.get(t)} has changed before removal: term "${s}" was not present in field "${n}". Removing a document after it has changed can corrupt the index!`,"version_conflict");return}}addDocumentId(t){let e=this._nextId;return this._idToShortId.set(t,e),this._documentIds.set(e,t),this._documentCount+=1,this._nextId+=1,e}addFields(t){for(let e=0;eObject.prototype.hasOwnProperty.call(i,t)?i[t]:void 0,nt={[P]:(i,t)=>{for(let e of t.keys()){let s=i.get(e);if(s==null)i.set(e,t.get(e));else{let{score:n,terms:o,match:r}=t.get(e);s.score=s.score+n,s.match=Object.assign(s.match,r),$(s.terms,o)}}return i},[Z]:(i,t)=>{let e=new Map;for(let s of t.keys()){let n=i.get(s);if(n==null)continue;let{score:o,terms:r,match:c}=t.get(s);$(n.terms,r),e.set(s,{score:n.score+o,terms:n.terms,match:Object.assign(n.match,c)})}return e},[st]:(i,t)=>{for(let e of t.keys())i.delete(e);return i}},it={k:1.2,b:.7,d:.5},ot=(i,t,e,s,n,o)=>{let{k:r,b:c,d}=o;return Math.log(1+(e-t+.5)/(t+.5))*(d+i*(r+1)/(i+r*(1-c+c*s/n)))},rt=i=>(t,e,s)=>{let n=typeof i.fuzzy=="function"?i.fuzzy(t,e,s):i.fuzzy||!1,o=typeof i.prefix=="function"?i.prefix(t,e,s):i.prefix===!0,r=typeof i.boostTerm=="function"?i.boostTerm(t,e,s):1;return{term:t,fuzzy:n,prefix:o,termBoost:r}},T={idField:"id",extractField:(i,t)=>i[t],tokenize:i=>i.split(ut),processTerm:i=>i.toLowerCase(),fields:void 0,searchOptions:void 0,storeFields:[],logger:(i,t)=>{typeof console?.[i]=="function"&&console[i](t)},autoVacuum:!0},J={combineWith:P,prefix:!1,fuzzy:!1,maxFuzzy:6,boost:{},weights:{fuzzy:.45,prefix:.375},bm25:it},ct={combineWith:Z,prefix:(i,t,e)=>t===e.length-1},N={batchSize:1e3,batchWait:10},W={minDirtFactor:.1,minDirtCount:20},E=Object.assign(Object.assign({},N),W),dt=(i,t)=>{i.includes(t)||i.push(t)},$=(i,t)=>{for(let e of t)i.includes(e)||i.push(e)},Q=({score:i},{score:t})=>t-i,B=()=>new Map,O=i=>{let t=new Map;for(let e of Object.keys(i))t.set(parseInt(e,10),i[e]);return t},F=i=>M(void 0,void 0,void 0,function*(){let t=new Map,e=0;for(let s of Object.keys(i))t.set(parseInt(s,10),i[s]),++e%1e3===0&&(yield H(0));return t}),H=i=>new Promise(t=>setTimeout(t,i)),ut=/[\n\r\p{Z}\p{P}]+/u;export{D as default};
+//# sourceMappingURL=minisearch.bundle.mjs.map
\ No newline at end of file
diff --git a/docs/vendor/tokenize.mjs b/docs/vendor/tokenize.mjs
new file mode 100644
index 000000000..4667dd313
--- /dev/null
+++ b/docs/vendor/tokenize.mjs
@@ -0,0 +1,45 @@
+/**
+ * Shared tokenizer for MiniSearch indexing & querying.
+ *
+ * Splits on whitespace + punctuation; for CJK runs emits unigrams + bigrams,
+ * which lets MiniSearch find Chinese substrings without a dictionary.
+ *
+ * NOTE: This file is loaded BOTH by:
+ * - the build script (scripts/build-index.js, copy at scripts/tokenize.js)
+ * - the browser app (docs/app.js)
+ * Keep it dependency-free.
+ */
+
+export function tokenizeCJK(text) {
+ if (!text) return [];
+ const out = [];
+ const parts = String(text).split(/[\s\p{P}\p{S}]+/u).filter(Boolean);
+ const cjkRe = /\p{Script=Han}|\p{Script=Hiragana}|\p{Script=Katakana}/u;
+ for (const part of parts) {
+ if (/^[\p{ASCII}]+$/u.test(part)) {
+ out.push(part.toLowerCase());
+ continue;
+ }
+ const chars = [...part];
+ let buf = "";
+ const flush = () => {
+ if (!buf) return;
+ const cs = [...buf];
+ for (let i = 0; i < cs.length; i++) {
+ out.push(cs[i]);
+ if (i + 1 < cs.length) out.push(cs[i] + cs[i + 1]);
+ }
+ buf = "";
+ };
+ for (const ch of chars) {
+ if (cjkRe.test(ch)) {
+ buf += ch;
+ } else {
+ flush();
+ if (ch.trim()) out.push(ch.toLowerCase());
+ }
+ }
+ flush();
+ }
+ return out;
+}
diff --git a/scripts/build-index.js b/scripts/build-index.js
new file mode 100644
index 000000000..f37e076d6
--- /dev/null
+++ b/scripts/build-index.js
@@ -0,0 +1,345 @@
+#!/usr/bin/env node
+/**
+ * Build metadata + full-text search index for the iOS-Weekly GitHub Pages site.
+ *
+ * Inputs (relative to repo root):
+ * Reports//#(-YYYY.MM.DD).md
+ * Posts/*.md
+ * Contributors/README.md (one ### section per contributor)
+ *
+ * Outputs:
+ * docs/data/index.json metadata for navigation, cards, summaries
+ * docs/data/search.json serialized MiniSearch full-text index
+ */
+
+import fs from "node:fs";
+import fsp from "node:fs/promises";
+import path from "node:path";
+import url from "node:url";
+import { marked } from "marked";
+import MiniSearch from "minisearch";
+import { tokenizeCJK } from "../docs/vendor/tokenize.mjs";
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+const REPO_ROOT = path.resolve(__dirname, "..");
+const OUT_DIR = path.join(REPO_ROOT, "docs", "data");
+
+const REPORTS_DIR = path.join(REPO_ROOT, "Reports");
+const POSTS_DIR = path.join(REPO_ROOT, "Posts");
+const CONTRIBUTORS_FILE = path.join(REPO_ROOT, "Contributors", "README.md");
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
+
+async function main() {
+ await fsp.mkdir(OUT_DIR, { recursive: true });
+
+ const items = [];
+
+ items.push(...(await collectReports()));
+ items.push(...(await collectPosts()));
+ items.push(...(await collectContributors()));
+
+ items.sort((a, b) => itemSortKey(b).localeCompare(itemSortKey(a)));
+
+ const stats = {
+ reports: items.filter((i) => i.type === "report").length,
+ posts: items.filter((i) => i.type === "post").length,
+ contributors: items.filter((i) => i.type === "contributor").length,
+ builtAt: new Date().toISOString(),
+ };
+
+ const meta = items.map(slimMetaItem);
+ const indexJson = { stats, items: meta };
+
+ const ms = new MiniSearch({
+ idField: "id",
+ fields: ["title", "sections", "body"],
+ storeFields: ["id", "title", "type", "date"],
+ tokenize: (str) => tokenizeCJK(str),
+ searchOptions: {
+ prefix: true,
+ fuzzy: 0.15,
+ boost: { title: 3, sections: 2 },
+ combineWith: "AND",
+ },
+ });
+
+ ms.addAll(
+ items.map((it) => ({
+ id: it.id,
+ title: it.title,
+ type: it.type,
+ date: it.date || "",
+ sections: (it.sections || [])
+ .map((s) => `${s.heading || ""} ${s.recommender || ""} ${s.summary || ""}`)
+ .join("\n"),
+ body: it.body || it.excerpt || "",
+ })),
+ );
+
+ await fsp.writeFile(path.join(OUT_DIR, "index.json"), JSON.stringify(indexJson));
+ await fsp.writeFile(path.join(OUT_DIR, "search.json"), JSON.stringify(ms));
+
+ console.log(
+ `[build-index] wrote ${meta.length} items ` +
+ `(reports=${stats.reports}, posts=${stats.posts}, contributors=${stats.contributors}) ` +
+ `→ ${path.relative(REPO_ROOT, OUT_DIR)}`,
+ );
+}
+
+/* =========================================================================
+ * Reports
+ * ========================================================================= */
+async function collectReports() {
+ const items = [];
+ const years = (await fsp.readdir(REPORTS_DIR, { withFileTypes: true }))
+ .filter((d) => d.isDirectory())
+ .map((d) => d.name)
+ .sort();
+ for (const year of years) {
+ const dir = path.join(REPORTS_DIR, year);
+ const files = (await fsp.readdir(dir)).filter((f) => f.endsWith(".md"));
+ for (const file of files) {
+ const abs = path.join(dir, file);
+ const md = await fsp.readFile(abs, "utf8");
+ items.push(parseReport({ year: Number(year), file, md, relPath: path.posix.join("Reports", year, file) }));
+ }
+ }
+ return items;
+}
+
+function parseReport({ year, file, md, relPath }) {
+ const m = file.match(/^#(\d+)(?:-(\d{4})\.(\d{2})\.(\d{2}))?\.md$/);
+ const issue = m ? Number(m[1]) : null;
+ const date = m && m[2] ? `${m[2]}-${m[3]}-${m[4]}` : extractDateFromTitle(md);
+
+ const titleLine = (md.match(/^#\s+(.+)$/m) || [])[1] || `老司机 iOS 周报 #${issue ?? "?"}`;
+ const title = titleLine.trim();
+
+ const sections = extractReportSections(md);
+ const body = mdToPlainText(md);
+ const excerpt = makeExcerpt(sections, body);
+
+ return {
+ id: `r-${(issue ?? "x").toString().padStart(3, "0")}-${date || year}`,
+ type: "report",
+ title,
+ issue,
+ date,
+ year,
+ path: relPath,
+ url: encodePosixPath(relPath),
+ sections,
+ excerpt,
+ body,
+ };
+}
+
+function extractReportSections(md) {
+ const out = [];
+ const lines = md.split(/\r?\n/);
+ let h2 = "";
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const h2m = line.match(/^##\s+(.+)$/);
+ if (h2m) {
+ h2 = h2m[1].trim();
+ continue;
+ }
+ const h3m = line.match(/^###\s+(.+)$/);
+ if (h3m) {
+ const headingRaw = h3m[1].trim();
+ const link = headingRaw.match(/\[([^\]]+)\]\(([^)]+)\)/);
+ const heading = (link ? link[1] : headingRaw)
+ .replace(/^[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\s🌟🚧]+/u, "")
+ .trim();
+ const url = link ? link[2] : null;
+
+ // Look ahead for first non-empty paragraph (recommender + summary).
+ let para = "";
+ for (let j = i + 1; j < lines.length && j < i + 25; j++) {
+ const l = lines[j];
+ if (/^#{1,6}\s/.test(l)) break;
+ if (!l.trim()) {
+ if (para) break;
+ continue;
+ }
+ para += (para ? " " : "") + l.trim();
+ }
+ let recommender = "";
+ let summary = para;
+ const linkRecM = para.match(/^\[@?([^\]]+)\]\([^)]+\)\s*[::]\s*([\s\S]+)$/);
+ if (linkRecM) {
+ recommender = linkRecM[1].trim();
+ summary = linkRecM[2];
+ } else {
+ const plainM = para.match(/^@?([^\s::]{1,40})\s*[::]\s*([\s\S]+)$/);
+ if (plainM) {
+ recommender = plainM[1].trim();
+ summary = plainM[2];
+ }
+ }
+
+ out.push({
+ section: h2,
+ heading,
+ url,
+ recommender,
+ summary: stripMd(summary).slice(0, 140),
+ });
+ }
+ }
+ return out;
+}
+
+/* =========================================================================
+ * Posts
+ * ========================================================================= */
+async function collectPosts() {
+ const out = [];
+ const files = (await fsp.readdir(POSTS_DIR)).filter((f) => f.endsWith(".md")).sort();
+ const stats = await Promise.all(files.map((f) => fsp.stat(path.join(POSTS_DIR, f))));
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ const md = await fsp.readFile(path.join(POSTS_DIR, file), "utf8");
+ const relPath = path.posix.join("Posts", file);
+ const titleLine = (md.match(/^#{1,2}\s+(.+)$/m) || [])[1] || file.replace(/\.md$/, "");
+ const title = stripMd(titleLine).trim() || file.replace(/\.md$/, "");
+ const date = stats[i].mtime.toISOString().slice(0, 10);
+ const body = mdToPlainText(md);
+ const excerpt = body.slice(0, 200).trim();
+ out.push({
+ id: "p-" + slugify(file.replace(/\.md$/, "")),
+ type: "post",
+ title,
+ date,
+ path: relPath,
+ url: encodePosixPath(relPath),
+ excerpt,
+ body,
+ sections: [],
+ });
+ }
+ return out;
+}
+
+/* =========================================================================
+ * Contributors
+ * ========================================================================= */
+async function collectContributors() {
+ const md = await fsp.readFile(CONTRIBUTORS_FILE, "utf8");
+ const out = [];
+ const sections = md.split(/^### /m).slice(1); // drop preamble
+ for (const sec of sections) {
+ const lines = sec.split(/\r?\n/);
+ const headerLine = lines.shift().trim();
+ const rest = lines.join("\n").trim();
+
+ const name = headerLine.split("/")[0].trim();
+ const aka = (headerLine.split("/")[1] || "").trim();
+
+ const avatarMatch = rest.match(/
]+src=['"]([^'"]+)['"]/i);
+ const avatar = avatarMatch ? avatarMatch[1] : null;
+
+ const bio = rest
+ .replace(/
]*>/gi, "")
+ .replace(/<\/?[^>]+>/g, "")
+ .trim();
+
+ const body = `### ${headerLine}\n\n${rest}`;
+ const id = "c-" + slugify(aka || name || `contributor-${out.length + 1}`);
+
+ out.push({
+ id,
+ type: "contributor",
+ title: headerLine || name,
+ date: "",
+ avatar,
+ path: "Contributors/README.md",
+ url: encodePosixPath("Contributors/README.md"),
+ sections: [],
+ excerpt: bio.replace(/\s+/g, " ").slice(0, 200),
+ body,
+ });
+ }
+ return out;
+}
+
+/* =========================================================================
+ * Helpers
+ * ========================================================================= */
+function itemSortKey(it) {
+ if (it.type === "report") return `0-${(it.issue ?? 0).toString().padStart(6, "0")}`;
+ if (it.type === "post") return `1-${it.date || ""}-${it.title}`;
+ return `2-${it.title}`;
+}
+
+function slimMetaItem(it) {
+ const { body, sections, ...rest } = it;
+ if (sections && sections.length) {
+ rest.sections = sections.map((s) => ({
+ heading: s.heading,
+ url: s.url,
+ recommender: s.recommender || undefined,
+ }));
+ }
+ // contributor entries: keep body inline so the detail page renders
+ // only that person's section instead of fetching the entire README.
+ if (it.type === "contributor" && body) rest.body = body;
+ return rest;
+}
+
+function makeExcerpt(sections, body) {
+ if (sections && sections.length) {
+ const lines = sections
+ .slice(0, 3)
+ .map((s) => `${s.heading}${s.summary ? ":" + s.summary : ""}`);
+ return lines.join(";").slice(0, 220);
+ }
+ return body.slice(0, 220);
+}
+
+function extractDateFromTitle(md) {
+ const m = md.match(/(\d{4})[-./](\d{2})[-./](\d{2})/);
+ return m ? `${m[1]}-${m[2]}-${m[3]}` : "";
+}
+
+function mdToPlainText(md) {
+ let text = md;
+ text = text.replace(/```[\s\S]*?```/g, " ");
+ text = text.replace(/`[^`]*`/g, " ");
+ text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, " ");
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
+ text = text.replace(/<[^>]+>/g, " ");
+ text = text.replace(/^[#>*\-+]+\s*/gm, "");
+ text = text.replace(/\*+([^*]+)\*+/g, "$1");
+ text = text.replace(/_+([^_]+)_+/g, "$1");
+ text = text.replace(/[\r\n]+/g, " ");
+ text = text.replace(/\s+/g, " ").trim();
+ return text;
+}
+
+function stripMd(s) {
+ return mdToPlainText(s);
+}
+
+function encodePosixPath(p) {
+ return p.split("/").map((seg) => encodeURIComponent(seg)).join("/");
+}
+
+function slugify(s) {
+ return String(s)
+ .toLowerCase()
+ .replace(/[\s/\\#?&_]+/g, "-")
+ .replace(/[^\p{Letter}\p{Number}\-.]+/gu, "")
+ .replace(/-+/g, "-")
+ .replace(/^-|-$/g, "")
+ .slice(0, 80) || "x";
+}
+
+// Marked options for parity with runtime SPA (not directly used here but kept
+// so any future direct rendering produces the same output).
+marked.setOptions({ gfm: true, breaks: false, headerIds: false, mangle: false });
diff --git a/scripts/package-lock.json b/scripts/package-lock.json
new file mode 100644
index 000000000..4b71a503a
--- /dev/null
+++ b/scripts/package-lock.json
@@ -0,0 +1,34 @@
+{
+ "name": "ios-weekly-site-build",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ios-weekly-site-build",
+ "version": "1.0.0",
+ "dependencies": {
+ "marked": "^12.0.2",
+ "minisearch": "^7.1.0"
+ }
+ },
+ "node_modules/marked": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
+ "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/minisearch": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz",
+ "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/scripts/package.json b/scripts/package.json
new file mode 100644
index 000000000..05027dc28
--- /dev/null
+++ b/scripts/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "ios-weekly-site-build",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "description": "Build search index and metadata for the iOS-Weekly GitHub Pages site.",
+ "scripts": {
+ "build": "node build-index.js",
+ "build:dev": "node build-index.js && node serve.js"
+ },
+ "dependencies": {
+ "marked": "^12.0.2",
+ "minisearch": "^7.1.0"
+ }
+}
diff --git a/scripts/serve.js b/scripts/serve.js
new file mode 100644
index 000000000..157c8fadf
--- /dev/null
+++ b/scripts/serve.js
@@ -0,0 +1,98 @@
+#!/usr/bin/env node
+/**
+ * Local development helper: assemble the deployable site into `_site/` and
+ * serve it on http://localhost:8080. Mirrors the production GitHub Pages
+ * layout so paths like /assets/page.css and /Reports/... resolve identically.
+ *
+ * node scripts/serve.js # assemble + serve
+ * node scripts/serve.js --once # just assemble, no server
+ */
+
+import fs from "node:fs";
+import fsp from "node:fs/promises";
+import http from "node:http";
+import path from "node:path";
+import url from "node:url";
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+const REPO_ROOT = path.resolve(__dirname, "..");
+const SITE_DIR = path.join(REPO_ROOT, "_site");
+const PORT = Number(process.env.PORT || 8080);
+
+const MIME = {
+ ".html": "text/html; charset=utf-8",
+ ".css": "text/css; charset=utf-8",
+ ".js": "application/javascript; charset=utf-8",
+ ".mjs": "application/javascript; charset=utf-8",
+ ".json": "application/json; charset=utf-8",
+ ".md": "text/markdown; charset=utf-8",
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".svg": "image/svg+xml",
+ ".webp": "image/webp",
+ ".ico": "image/x-icon",
+ ".txt": "text/plain; charset=utf-8",
+ ".map": "application/json; charset=utf-8",
+};
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
+
+async function main() {
+ await assemble();
+ if (process.argv.includes("--once")) return;
+ serve();
+}
+
+async function assemble() {
+ await fsp.rm(SITE_DIR, { recursive: true, force: true });
+ await fsp.mkdir(SITE_DIR, { recursive: true });
+
+ await copyDir(path.join(REPO_ROOT, "docs"), SITE_DIR);
+ await copyDir(path.join(REPO_ROOT, "assets"), path.join(SITE_DIR, "assets"));
+ await copyDir(path.join(REPO_ROOT, "Reports"), path.join(SITE_DIR, "Reports"));
+ await copyDir(path.join(REPO_ROOT, "Posts"), path.join(SITE_DIR, "Posts"));
+ await copyDir(path.join(REPO_ROOT, "Contributors"), path.join(SITE_DIR, "Contributors"));
+ await fsp.writeFile(path.join(SITE_DIR, ".nojekyll"), "");
+
+ console.log(`[serve] assembled → ${path.relative(REPO_ROOT, SITE_DIR)}`);
+}
+
+async function copyDir(src, dst) {
+ if (!fs.existsSync(src)) return;
+ await fsp.cp(src, dst, { recursive: true });
+}
+
+function serve() {
+ const server = http.createServer(async (req, res) => {
+ try {
+ let pathname = decodeURIComponent(new URL(req.url, "http://x").pathname);
+ if (pathname.endsWith("/")) pathname += "index.html";
+ const safe = path.normalize(path.join(SITE_DIR, pathname));
+ if (!safe.startsWith(SITE_DIR)) {
+ res.writeHead(403).end("forbidden");
+ return;
+ }
+ const stat = await fsp.stat(safe).catch(() => null);
+ if (!stat) {
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("not found: " + pathname);
+ return;
+ }
+ const final = stat.isDirectory() ? path.join(safe, "index.html") : safe;
+ const data = await fsp.readFile(final);
+ res.writeHead(200, {
+ "content-type": MIME[path.extname(final).toLowerCase()] || "application/octet-stream",
+ "cache-control": "no-cache",
+ });
+ res.end(data);
+ } catch (err) {
+ res.writeHead(500, { "content-type": "text/plain" }).end(String(err));
+ }
+ });
+ server.listen(PORT, () => {
+ console.log(`[serve] http://localhost:${PORT}/`);
+ });
+}