diff --git a/.github/workflows/cd b/.github/workflows/cd deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 65b1ae2..82a1090 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,7 +18,10 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + # Actions are pinned to commits; the token is not left in .git/config for later steps. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: Refresh GitHub stats if: github.event_name != 'push' @@ -31,12 +34,11 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add data/stats.json git commit -m "Update GitHub stats" - git push + git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:main fi - name: Check data files - run: | - for f in data/*.json; do python3 -m json.tool "$f" > /dev/null || { echo "Invalid JSON: $f"; exit 1; }; done + run: python3 scripts/check_data.py - name: Build Builder pages and badges run: python3 scripts/build_builders.py @@ -47,7 +49,7 @@ jobs: cp -r index.html article.html game.html lab.html 404.html .nojekyll assets data articles builders badges _site/ - name: Publish to gh-pages - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./_site diff --git a/404.html b/404.html index 18deca6..eae6e30 100644 --- a/404.html +++ b/404.html @@ -2,22 +2,25 @@ + 페이지를 찾을 수 없습니다 | Causal Inference Hub - + + -
+

페이지를 찾을 수 없습니다

예전 블로그 주소로 들어오셨다면 글은 읽을거리로 옮겨졌습니다.

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49ddb49..ed755fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,8 @@ ## 내용 더하기 +PR을 올리기 전에 `python3 scripts/check_data.py`를 실행해 주세요. 배포 워크플로도 같은 검사를 하며, 실패하면 배포하지 않습니다. 링크는 https(또는 http)나 사이트 안의 경로만, id는 영문·숫자·하이픈만, 기수·인원은 정수만 허용합니다. 글(`articles/*.md`)의 HTML은 화면에 그릴 때 정리(sanitize)되어 스크립트가 실행되지 않습니다. + 사이트는 빌드 도구 없이 `data/` 폴더의 JSON 파일을 읽어 그립니다. 대부분의 수정은 파일 하나를 고치는 PR이면 됩니다. | 파일 | 고치는 경우 | diff --git a/article.html b/article.html index a88a677..c298e47 100644 --- a/article.html +++ b/article.html @@ -2,12 +2,13 @@ + 읽을거리 | Causal Inference Hub - + @@ -19,14 +20,7 @@ Causal Inference Hub가짜연구소 인과추론팀 - +
@@ -56,7 +50,8 @@

다른 글

- + + diff --git a/assets/css/hub.css b/assets/css/hub.css index ff2ac4b..0f2aeeb 100644 --- a/assets/css/hub.css +++ b/assets/css/hub.css @@ -55,6 +55,7 @@ body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--f-body); font-size: 16px; line-height: 1.6; -webkit-font-smoothing: antialiased; word-break: keep-all; + min-height: 100vh; min-height: 100dvh; } img { max-width: 100%; } a { color: var(--accent); text-underline-offset: 3px; } @@ -93,7 +94,7 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .site-nav::-webkit-scrollbar { display: none; } .site-nav a { color: var(--muted); text-decoration: none; padding: 8px 10px; border-radius: 8px; white-space: nowrap; font-size: 15px; font-weight: 500; } .site-nav a:hover { color: var(--ink); background: var(--surface-2); } -.site-nav a[aria-current="true"] { color: var(--ink); background: var(--surface-2); font-weight: 700; } +.site-nav a[aria-current="true"], .site-nav a[aria-current="page"] { color: var(--ink); background: var(--surface-2); font-weight: 700; } @media (max-width: 820px) { .header-row { flex-wrap: wrap; gap: 2px; padding-block: 8px; } .site-nav { margin-left: 0; width: 100%; } @@ -249,7 +250,8 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a /* Curriculum shelf */ .goal-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; } .goal-q { font-weight: 700; } -#route-note { margin-top: 10px; min-height: 1.6em; } +#route-note { margin-top: 10px; min-height: 1.6em; scroll-margin-top: 96px; } +#route-note:empty { min-height: 0; margin-top: 0; } .shelf-bar { display: flex; align-items: flex-end; gap: 16px; margin: 8px 0 12px; } .shelf-nav { display: flex; gap: 6px; flex-shrink: 0; } .shelf-nav button { width: 40px; height: 40px; display: grid; place-items: center; border-radius: 50%; border: 1.5px solid var(--rule); background: var(--surface); color: var(--ink); cursor: pointer; } @@ -314,8 +316,8 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .shelf-nav { display: none; } .chap { width: min(66vw, 240px); } .chapters .nm { display: none; } - .goal-row .route-chips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin-inline: calc(-1 * var(--gutter-l)) calc(-1 * var(--gutter-r)); padding-inline: var(--gutter-l) var(--gutter-r); } - .goal-row .route-chips .chip { flex-shrink: 0; } + .goal-row .route-chips { gap: 6px; } + .goal-row .route-chips .chip { font-size: 13.5px; padding: 6px 12px; } } /* This week (curriculum tab): spotlight, recent updates, continue */ @@ -474,12 +476,22 @@ body::before { content: ""; position: fixed; inset: 0 0 auto; height: env(safe-a .event .ppl b { font-family: var(--f-display); font-weight: 800; letter-spacing: -.02em; font-size: 24px; color: var(--ink); } .event .bar { height: 6px; border-radius: 3px; background: var(--surface-2); overflow: hidden; margin-top: 4px; } .event .bar i { display: block; height: 100%; background: var(--accent); } +.event .btn.sm { margin-top: auto; align-self: flex-start; } +@media (max-width: 560px) { + .events { grid-template-columns: minmax(0, 1fr); gap: 8px; } + .event { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 2px 12px; padding: 14px; } + .event .ord { grid-row: span 4; font-size: 26px; } + .event .ppl, .event .btn.sm { margin-top: 4px; } + .event .soon-tag { justify-self: start; } +} /* Goals */ .route-chips { display: flex; flex-wrap: wrap; gap: 8px; } .chip { font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; border: 1.5px solid var(--rule); background: var(--surface); color: var(--ink); padding: 7px 14px; border-radius: 999px; min-height: 40px; display: inline-flex; align-items: center; gap: 6px; } .chip:hover { border-color: var(--ink); } .chip[aria-checked="true"] { background: var(--ink); color: var(--surface); border-color: var(--ink); } +.chip.quiz-chip { border-style: dashed; border-color: color-mix(in srgb, var(--accent) 55%, var(--rule)); background: var(--accent-soft); color: var(--accent); } +.chip.quiz-chip:hover { border-style: solid; border-color: var(--accent); } .route-note { color: var(--muted); font-size: 14px; } .map-card { background: var(--surface); border: 1px solid var(--rule); border-radius: 16px; padding: 12px 0 16px; min-width: 0; } @@ -617,6 +629,7 @@ dialog.station::backdrop { background: rgba(10, 14, 20, .55); } .p-timeline li { position: relative; display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 10px; padding: 8px 0 8px 18px; } .p-timeline li::before { content: ""; position: absolute; left: -7px; top: 14px; width: 12px; height: 12px; border-radius: 50%; background: var(--c); box-shadow: 0 0 0 3px var(--surface); } .p-timeline li.r::before { box-sizing: border-box; background: var(--surface); border: 2px solid var(--c); } +.tl-c { white-space: nowrap; } .tl-c b { display: block; font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; } .tl-c span { font-size: 11.5px; color: var(--muted); } .tl-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; font-size: 14px; } @@ -658,14 +671,6 @@ dialog.station::backdrop { background: rgba(10, 14, 20, .55); } .notice { background: var(--surface); border: 1px solid var(--rule); border-radius: 12px; padding: 18px; margin-bottom: 16px; } /* Start-point quiz entry, quiz dialog, finish card, Builder share */ -.start-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: -2px 0 10px; } -.start-btn { font: inherit; font-size: 14px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 14px; border-radius: 999px; border: 1.5px dashed color-mix(in srgb, var(--accent) 55%, var(--rule)); background: var(--accent-soft); color: var(--ink); cursor: pointer; } -.start-btn b { color: var(--accent); font-weight: 800; } -.start-btn:hover { border-style: solid; border-color: var(--accent); } -.start-btn:focus-visible, .start-game:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } -.start-game { font-size: 14px; font-weight: 600; color: var(--muted); text-decoration: none; } -.start-game::after { content: " ›"; } -.start-game:hover { color: var(--ink); text-decoration: underline; } .btn.sm { min-height: 36px; padding: 0 12px; font-size: 14px; margin-top: 0; } button.btn { font: inherit; font-weight: 600; cursor: pointer; border: 0; } button.btn.ghost { border: 1.5px solid var(--rule); } @@ -828,6 +833,11 @@ button.btn.sm { font-size: 14px; } .daily { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 20px 28px; padding: 20px 24px; border-radius: 18px; background: var(--surface); border: 1.5px solid color-mix(in srgb, var(--accent) 30%, var(--rule)); } .daily-head h2 { font-size: 18px; letter-spacing: -.01em; } .daily-date { font-size: 13px; color: var(--muted); margin-top: 2px; } +.daily-warm { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 12px; font-size: 13.5px; font-weight: 700; } +.daily-warm span { flex-basis: 100%; font-size: 12px; font-weight: 600; color: var(--muted); } +.daily-warm a { text-decoration: none; } +.daily-warm a::after { content: " ›"; } +.daily-warm a:hover { text-decoration: underline; } .daily-streak { display: inline-block; margin-top: 10px; font-size: 13px; font-weight: 700; padding: 3px 10px; border-radius: 999px; background: #FFF1C2; color: #6B4B00; } .daily-streak.quiet { background: var(--surface-2); color: var(--muted); font-weight: 600; } .daily-body { display: grid; gap: 10px; min-width: 0; } @@ -845,7 +855,7 @@ button.btn.sm { font-size: 14px; } .daily-why { font-size: 14.5px; } .daily-more { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 14px; font-weight: 700; } .daily-next { font-size: 12.5px; color: var(--muted); } -@media (max-width: 760px) { .daily { grid-template-columns: minmax(0, 1fr); padding: 18px; } .daily-opts button { flex: 1 1 100%; } } +@media (max-width: 760px) { .daily { grid-template-columns: minmax(0, 1fr); padding: 18px; } .daily-warm span { flex-basis: auto; } .daily-opts button { flex: 1 1 100%; } } /* Confounding lab (lab.html) */ .lab { padding-block: 32px 72px; } @@ -900,13 +910,31 @@ button.btn.sm { font-size: 14px; } .lab-legend .dot { width: 10px; height: 10px; border-radius: 50%; } .lab-legend .dot.on { background: var(--l-main); } .lab-legend .dot.off { background: var(--l-exp); } +.lab-legend .ln { width: 16px; border-top: 2px solid var(--muted); } +.lab-legend .ln.dash { border-top-style: dashed; } +.lab-legend { flex-wrap: wrap; gap: 4px 16px; } .lab-learn { margin-top: 28px; max-width: 820px; } -.lab-learn h2 { font-size: 18px; margin-bottom: 10px; } +.lab-learn h2 { font-size: 18px; display: inline; } +.lab-learn summary { cursor: pointer; list-style: none; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; margin-bottom: 10px; } +.lab-learn summary::-webkit-details-marker { display: none; } +.lab-learn summary::before { content: "▸"; color: var(--muted); } +.lab-learn details[open] summary::before { content: "▾"; } +.lab-learn summary span { font-size: 13px; color: var(--muted); } +.lab-learn details[open] summary span { display: none; } +.lab-learn summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: 6px; } +.lab > .wrap > .lab-challenge { margin-top: 18px; max-width: 46em; font-size: 15px; } .lab-learn ol { margin: 0 0 18px; padding-left: 20px; display: grid; gap: 10px; font-size: 15px; } .lab-learn a { font-weight: 700; margin-left: 4px; } -@media (max-width: 900px) { .lab-grid { grid-template-columns: minmax(0, 1fr); } } +@media (max-width: 900px) { + .lab-grid { grid-template-columns: minmax(0, 1fr); } + /* keep the chart in view while the sliders move */ + .lab-chart { order: -1; position: sticky; top: var(--header-h, 56px); z-index: 5; padding: 12px 14px; box-shadow: 0 8px 20px rgba(20, 22, 43, .08); } + .lab-chart h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } + .lab-chart-sub { display: none; } + .lab-legend { margin-top: 4px; font-size: 12px; } +} .toast > span:nth-child(2) { flex: 1; min-width: 0; } -@media (max-width: 900px) { .toast { bottom: auto; top: 72px; transform: translate(-50%, -180%); } .toast.show { transform: translate(-50%, 0); } } +@media (max-width: 900px) { .toast { bottom: auto; top: 96px; transform: translate(-50%, -180%); } .toast.show { transform: translate(-50%, 0); } } .lab-ctl.off input[type=range] { opacity: .45; } .lab-ctl.off small { color: var(--live); font-weight: 600; } .lab-note { font-size: 12.5px; color: var(--muted); } @@ -914,5 +942,10 @@ button.btn.sm { font-size: 14px; } #lab-svg .fit-l.on { stroke: var(--l-main); } #lab-svg .fit-l.off { stroke: var(--l-exp); } #lab-svg .fit-gap { stroke: var(--live); stroke-width: 3; } -#lab-svg .fit-t { fill: var(--live); font-size: 12px; font-weight: 700; paint-order: stroke; stroke: var(--surface); stroke-width: 4px; } +#lab-svg .fit-t { fill: var(--live); font-size: 13px; font-weight: 800; paint-order: stroke; stroke: var(--surface); stroke-width: 5px; stroke-linejoin: round; } @media (max-width: 900px) { .lab-chart { order: -1; } } + +/* way back from a repo to the Builder it was opened from */ +.back-person { font: inherit; font-size: 13.5px; font-weight: 700; align-self: flex-start; margin: 0 0 8px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--rule); background: var(--surface-2); color: var(--ink); cursor: pointer; } +.back-person:hover { border-color: var(--ink); } +.back-person:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } diff --git a/assets/js/article.js b/assets/js/article.js index d18bd8c..567d3c6 100644 --- a/assets/js/article.js +++ b/assets/js/article.js @@ -17,16 +17,19 @@ const slug = location.hash.replace(/^#/, "") || (articles[0] && articles[0].slug); const a = articles.find((x) => x.slug === slug); more.innerHTML = articles.filter((x) => x !== a).map((x) => - `
  • ${esc(x.title)}

  • `).join(""); + `
  • ${esc(x.title)}

  • `).join(""); if (!a) { notFound(); return; } document.title = `${a.title} | Causal Inference Hub`; title.textContent = a.title; - meta.textContent = `${a.author} · ${a.date.replace(/-/g, ".")}`; + meta.textContent = `${a.author} · ${String(a.date).replace(/-/g, ".")}`; try { const res = await fetch(`articles/${a.slug}.md`, { cache: "no-cache" }); if (!res.ok) throw new Error(res.status); const md = await res.text(); - body.innerHTML = window.marked ? window.marked.parse(md) : `
    ${esc(md)}
    `; + /* articles come from pull requests: never render their HTML unsanitised */ + body.innerHTML = window.marked && window.DOMPurify + ? window.DOMPurify.sanitize(window.marked.parse(md), { USE_PROFILES: { html: true } }) + : `
    ${esc(md)}
    `; body.querySelectorAll("table").forEach((t) => { const w = document.createElement("div"); w.className = "table-wrap"; diff --git a/assets/js/ga.js b/assets/js/ga.js index 4f010a0..a6b941c 100644 --- a/assets/js/ga.js +++ b/assets/js/ga.js @@ -1,15 +1,24 @@ -/* Google Analytics 4 for every page of the hub. The first ID is the hub's property; - the second is the tag the web book already uses, kept so visits can be followed - across both sites until one property is chosen. Local previews are not counted. */ +/* Google Analytics 4 for every page of the hub: the hub's own property. Local previews are not + counted. Incoming utm_* (from shared links or other team sites) reach GA through page_location + and are then removed from the address bar so the URL people copy stays clean. */ (() => { - const IDS = ["G-C5V0X2SGD3", "G-BEBVKYH8HL"]; - if (location.hostname !== "causalinferencelab.github.io") return; - const s = document.createElement("script"); - s.async = true; - s.src = `https://www.googletagmanager.com/gtag/js?id=${IDS[0]}`; - document.head.appendChild(s); - window.dataLayer = window.dataLayer || []; - window.gtag = function gtag() { window.dataLayer.push(arguments); }; - window.gtag("js", new Date()); - IDS.forEach((id) => window.gtag("config", id)); + const ID = "G-C5V0X2SGD3"; + if (location.hostname === "causalinferencelab.github.io") { + const s = document.createElement("script"); + s.async = true; + s.src = `https://www.googletagmanager.com/gtag/js?id=${ID}`; + document.head.appendChild(s); + window.dataLayer = window.dataLayer || []; + window.gtag = function gtag() { window.dataLayer.push(arguments); }; + window.gtag("js", new Date()); + window.gtag("config", ID, { page_location: location.href, allow_google_signals: false, allow_ad_personalization_signals: false }); + } + try { + const u = new URL(location.href); + const drop = [...u.searchParams.keys()].filter((k) => k.startsWith("utm_")); + if (drop.length) { + drop.forEach((k) => u.searchParams.delete(k)); + history.replaceState(history.state, "", u.pathname + u.search + u.hash); + } + } catch (e) { /* old browsers keep the URL as it is */ } })(); diff --git a/assets/js/game.js b/assets/js/game.js index 434c541..0376341 100644 --- a/assets/js/game.js +++ b/assets/js/game.js @@ -62,7 +62,8 @@ const t = tier(state.score, n, state.right, kinds); track("game_complete", { score: state.score, total: n, cause_right: state.right.cause, corr_right: state.right.corr }); if (window.hubStamp) { window.hubStamp("game"); if (state.score >= 8) window.hubStamp("sharp"); } - const chapters = [...new Map(state.wrong.map((q) => [q.chapter.url, q.chapter])).values()]; + const chNum = (c) => parseInt(c.url.split("/").pop(), 10) || 99; + const chapters = [...new Map(state.wrong.map((q) => [q.chapter.url, q.chapter])).values()].sort((x, y) => chNum(x) - chNum(y)); const share = window.hubShareUrl ? window.hubShareUrl("/game.html", "game") : location.href; root.innerHTML = `

    결과

    @@ -96,7 +97,8 @@ async function start() { if (!state.qs.length) { const res = await fetch("data/game.json", { cache: "no-cache" }); - state.qs = (await res.json()).questions; + /* links come from data/game.json: keep only https chapter links */ + state.qs = (await res.json()).questions.map((q) => ({ ...q, chapter: { ...q.chapter, url: /^https:\/\//.test(q.chapter && q.chapter.url) ? q.chapter.url : "#" } })); } state.i = 0; state.score = 0; state.wrong = []; state.right = { corr: 0, cause: 0 }; track("game_start", { retry: state.runs > 0 }); diff --git a/assets/js/hub.js b/assets/js/hub.js index 5119f73..7aec156 100644 --- a/assets/js/hub.js +++ b/assets/js/hub.js @@ -22,7 +22,33 @@ const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); const fmt = (n) => Number(n).toLocaleString("ko-KR"); const $ = (sel, root = document) => root.querySelector(sel); - const dot = (d) => d.replace(/-/g, "."); + const dot = (d) => esc(String(d ?? "").replace(/-/g, ".")); + /* Data files come from pull requests: links must be http(s) or a site path, ids plain tokens, + counts whole numbers. scripts/check_data.py enforces the same in CI; this is the second line. */ + const URL_KEYS = new Set(["url", "link", "linkedin", "site", "cover", "href", "builderUrl"]); + const safeUrl = (u) => { + if (typeof u !== "string" || !u) return u; + try { const x = new URL(u, location.href); return /^https?:$/.test(x.protocol) ? u : "#"; } catch (e) { return "#"; } + }; + const TOKEN = /^[A-Za-z0-9_.-]+$/; + const tok = (v, fb = "none") => (typeof v === "string" && TOKEN.test(v) ? v : fb); + const int = (v) => (Number.isFinite(Number(v)) ? Math.trunc(Number(v)) : 0); + function cleanUrls(node) { + if (Array.isArray(node)) node.forEach(cleanUrls); + else if (node && typeof node === "object") Object.keys(node).forEach((k) => { if (URL_KEYS.has(k)) node[k] = safeUrl(node[k]); else cleanUrls(node[k]); }); + return node; + } + function cleanData(d) { + Object.values(d).forEach(cleanUrls); + d.levels.forEach((l) => { l.id = tok(l.id); }); + d.lines.forEach((l) => { l.id = tok(l.id); }); + d.repos.forEach((r) => { r.level = r.level ? tok(r.level, "") : r.level; r.lines = (r.lines || []).filter((x) => TOKEN.test(x)); r.cohorts = (r.cohorts || []).map(int); if (r.stars != null) r.stars = int(r.stars); }); + d.projects.forEach((p) => { p.cohort = int(p.cohort); p.runners = p.runners == null ? null : int(p.runners); }); + d.events.forEach((e) => { e.order = int(e.order); e.year = int(e.year); e.participants = e.participants == null ? null : int(e.participants); }); + d.routes.forEach((r) => { r.id = tok(r.id); }); + d.builders.forEach((b) => { (b.runnerOf || []).forEach((x) => { x.cohort = int(x.cohort); }); }); + return d; + } const track = (name, params) => { if (window.hubTrack) window.hubTrack(name, params); }; const stamp = (id) => { if (window.hubStamp) window.hubStamp(id); }; const shareUrl = (path, medium) => (window.hubShareUrl ? window.hubShareUrl(path, medium) : path); @@ -48,10 +74,10 @@ async function load() { const names = ["repos", "lines", "levels", "map", "routes", "projects", "events", "articles", "builders", "growth", "stats", "featured", "daily"]; const data = await Promise.all(names.map((n) => getJSON(`data/${n}.json`))); - const d = Object.fromEntries(names.map((n, i) => [n, data[i]])); + const d = cleanData(Object.fromEntries(names.map((n, i) => [n, data[i]]))); d.repos.forEach((r) => { const live = d.stats.repos && d.stats.repos[r.repo]; - if (live) { r.stars = live.stars; r.pushed = live.pushed; } + if (live) { r.stars = int(live.stars); r.pushed = typeof live.pushed === "string" ? live.pushed : null; } state.repos.set(r.repo, r); }); state.repos.set(d.map.external.id, { repo: d.map.external.id, kind: "external", status: "external", builders: [], cohorts: [], ...d.map.external }); @@ -327,7 +353,7 @@ return chap + items.map((r, k) => episodeHTML(r, lv.id, k, top)).join(""); }).join(""); $("#chapters").innerHTML = state.levels.map((lv) => ``).join(""); - $("#route-note").textContent = "목표를 고르면 트랙 지도와 커리큘럼에 볼 순서가 표시됩니다."; + $("#route-note").textContent = ""; } wireAvatars(shelf); @@ -375,6 +401,7 @@ function setRoute(id, source) { if (source && id !== state.route) track("select_route", { route: id, source }); state.routeBegin = 0; + $("#week").hidden = id !== "all"; state.route = id; $("#route-chips").querySelectorAll(".chip").forEach((c) => c.setAttribute("aria-checked", String(c.dataset.route === id))); renderShelf(); @@ -384,7 +411,8 @@ function renderRouteChips() { const box = $("#route-chips"); const all = [{ id: "all", label: "전체 보기" }, ...state.routes]; - box.innerHTML = all.map((r) => ``).join(""); + box.innerHTML = all.map((r) => ``).join("") + + ``; box.addEventListener("click", (e) => { const b = e.target.closest("[data-route]"); if (b) setRoute(b.dataset.route, "chip"); @@ -425,6 +453,9 @@ const shortTerm = (t) => (t || "").replace(/^20(\d\d) /, "$1 ").replace("상반기", "상").replace("하반기", "하"); const span = (list) => (list.length ? (list[0] === list[list.length - 1] ? `${list[0]}기` : `${list[0]}–${list[list.length - 1]}기`) : ""); + /* one way to say how long someone has been a Builder, everywhere */ + const tenure = (p) => (p.cohorts.length === 1 ? `${p.first}기 Builder` : `${p.first}기부터 · ${p.cohorts.length}개 기수 (${p.cohorts.join("·")}기)`); + function builderStats() { const cur = currentCohort(); const people = new Map(); @@ -480,13 +511,13 @@ state.people = list; const cohorts = [...new Set(state.projects.map((p) => p.cohort))].sort((a, b) => a - b); const terms = new Map(state.projects.map((p) => [p.cohort, p.term])); - const stripLabel = (p) => `${span(p.cohorts)}${p.cohorts.length > 1 ? ` 중 ${p.cohorts.length}개 기수` : ""}`; + const stripLabel = (p) => tenure(p); $("#top3").innerHTML = list.slice(0, 3).map((p) => { const recent = [...p.items].reverse().slice(0, 2); return `
  • `; }; @@ -505,7 +536,7 @@ const head = ``; $("#roster").innerHTML = head + `
      ${list.map((p) => `
    1. - ${p.rank}${avatarHTML(p.name, "sm")}${stripLabel(p)} + ${p.rank}${avatarHTML(p.name, "sm")}${esc(stripLabel(p))} ${stripHTML(p, cohorts)}
        ${p.items.slice(-3).reverse().map((i) => `
      • ${i.cohort}기${i.repo ? `` : i.link ? `${esc(i.name)}` : `${esc(i.name)}`}${i.live ? `진행 중` : ""}
      • `).join("")}${p.items.length > 3 ? `
      • ` : ""}
      러너 ${fmt(p.runners)}명프로젝트 ${p.projects}개 @@ -529,6 +560,7 @@ const p = (state.people || []).find((x) => x.name === name); if (!p) return; const info = builderInfo(name); + state.lastPerson = name; track("open_builder", { builder: info.slug || name, source: source || "hub" }); if (window.hubSeen) window.hubSeen("builders", info.slug || name, 3, "builder"); const runnerItems = p.runnerOf.map((x) => { @@ -540,7 +572,7 @@ const terms = new Map(state.projects.map((q) => [q.cohort, q.term])); const item = (e) => { const facts = e.role === "Builder" ? [e.runners ? `러너 ${e.runners}명` : "", e.live ? "진행 중" : ""] : [e.by ? `Builder ${esc(e.by)}` : ""]; - const go = e.repo ? `` : e.link ? `기록 보기` : ""; + const go = e.repo ? `` : e.link ? `기록 보기` : ""; const note = [...facts.filter(Boolean), go].filter(Boolean).join(''); return `
    2. ${e.cohort}기${esc(terms.get(e.cohort) || "")}${e.role}${esc(e.name)}${note ? `${note}` : ""}${retroHTML(e.retro)}
    3. `; }; @@ -549,7 +581,7 @@
      -
      ${p.rank <= 3 ? `Top ${p.rank}` : ""}${p.first}기부터 Builder
      +
      ${p.rank <= 3 ? `Top ${p.rank}` : ""}${esc(tenure(p))}
      ${avatarHTML(p.name, "lg")}

      ${esc(p.name)}

      ${info.linkedin ? `LinkedIn` : ""}
      ${info.slug ? `
      Builder 페이지
      ` : ""} ${info.intro ? `

      ${esc(info.intro)}

      ` : ""} @@ -692,7 +724,7 @@ /* ---------- events ---------- */ function renderEvents() { - const list = [...state.events].sort((a, b) => a.order - b.order); + const list = [...state.events].sort((a, b) => (b.upcoming ? 1 : 0) - (a.upcoming ? 1 : 0) || b.order - a.order); const max = Math.max(...list.map((e) => e.participants || 0)) || 1; $("#event-list").innerHTML = list.map((e) => { const when = e.date ? dot(e.date) : `${e.year}년`; @@ -702,7 +734,8 @@ ${e.upcoming ? `예정` : ""}

      ${title}

      ${when} - ${e.participants ? `
      ${fmt(e.participants)}명 참가
      ` : `
      참가 신청을 받고 있습니다.
      `} + ${e.participants ? `
      ${fmt(e.participants)}명 참가
      ` + : e.upcoming && e.link ? `참가 신청하기` : `
      ${e.upcoming ? "신청 링크는 곧 열립니다." : ""}
      `} `; }).join(""); } @@ -812,8 +845,10 @@ const today = dayNum(), q = pool.at(today % pool.length), rec = loadDaily(); const ans = rec.answers && rec.answers[today]; const now = new Date(); - const streak = rec.last === today || rec.last === today - 1 ? rec.streak || 0 : 0; + rec.streak = int(rec.streak); rec.best = int(rec.best); + const streak = rec.last === today || rec.last === today - 1 ? rec.streak : 0; const head = `

      오늘의 한 문제

      ${now.getMonth() + 1}월 ${now.getDate()}일 · 매일 바뀝니다

      +

      2분 몸풀기상관일까 인과일까 · 10문제교란 실험실

      ${streak ? `

      ${streak}일 연속${streak < 3 ? ` · ${3 - streak}일 더 풀면 스탬프` : rec.best > streak ? ` · 최고 ${rec.best}일` : ""}

      ` : `

      사흘 연속 풀면 스탬프

      `}
      `; if (!ans) { box.innerHTML = `${head}
      @@ -827,7 +862,7 @@

      이렇게 말해도 될까요?“${esc(q.claim)}”

      ${ans.correct ? "맞았습니다" : "아쉽습니다"}내 답: ${DAILY_LABEL[ans.pick] || ""} · 정답: ${DAILY_LABEL[q.answer]}

      ${esc(q.why)}

      -

      ${esc(q.chapter.title)} 읽기10문제 더 풀기실험실에서 직접 해 보기

      +

      ${esc(q.chapter.title)} 읽기

      자정에 새 문제가 열립니다.

      `; } function answerDaily(pick) { @@ -873,7 +908,7 @@ if (a.work === "research") return "research"; if (a.work === "exp") return "experiment"; if (a.work === "ml") return a.experience === "new" ? "start" : "ml"; - if (a.work === "mkt" && a.experience === "used") return "experiment"; + if (a.work === "mkt") return "marketing"; return "start"; } function showDialog() { @@ -884,6 +919,7 @@ quiz.step = 0; quiz.a = {}; track("quiz_start", { retry: !!retry }); renderQuiz(); showDialog(); + const first = $("#station-body .quiz-opts button"); if (first) first.focus(); } function renderQuiz() { const shell = (inner) => `
      @@ -926,7 +962,7 @@ const stops = [...document.querySelectorAll("#shelf .ep")]; const el = stops.at(Math.min(begin, stops.length - 1)); renderRouteNote(); - $("#shelf-wrap").scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "center" }); + $("#route-note").scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start" }); if (el) { scrollShelfTo(el, false); el.classList.add("picked"); @@ -979,7 +1015,7 @@

      ${lv.id} ${esc(lv.name)} 완주 카드

      -

      이미지를 저장해 링크드인이나 블로그에 올려 보세요. 이름과 진도는 이 브라우저 밖으로 보내지 않습니다.

      +

      이미지를 저장해 링크드인이나 블로그에 올려 보세요. 이름은 이 브라우저 밖으로 보내지 않고, 학습 진도는 이름 없이 방문 통계로만 집계합니다.

      `; showDialog(); @@ -1030,7 +1066,7 @@ r.kind === "paid" && r.credit ? `
      ${r.type === "강의" ? "강사" : "만든 사람"}
      ${esc(r.credit)}
      ` : "", builders ? `
      Builder
      ${builders}
      ` : "", r.cohorts && r.cohorts.length ? `
      기수
      ${r.cohorts.join("·")}기
      ` : "", - statusText[r.status] ? `
      상태
      ${statusText[r.status]}
      ` : "", + statusText[r.status] ? `
      상태
      ${esc(statusText[r.status].split(": ")[0])}
      ` : "", r.kind ? "" : `
      스타
      ${fmt(r.stars || 0)}
      `, r.pushed ? `
      마지막 커밋
      ${dot(r.pushed.slice(0, 10))}
      ` : "" ].join(""); @@ -1054,6 +1090,7 @@
      + ${state.backTo ? `` : ""}
      ${lineTags}${extraTag}
      ${r.cover ? `` : ""}

      ${esc(r.title)}

      @@ -1075,7 +1112,8 @@ /* Deep links: #station= opens that entry, so other team sites (the web book, for one) can link straight to it. */ const stationFromHash = () => { const m = location.hash.match(/^#station=(.+)$/); - return m ? decodeURIComponent(m[1]) : null; + if (!m) return null; + try { return decodeURIComponent(m[1]); } catch (e) { return null; } }; function openFromHash() { const id = stationFromHash(); @@ -1145,10 +1183,13 @@ const tabLink = e.target.closest('a[href="#contrib"]'); if (tabLink) { e.preventDefault(); selectTab("tab-contrib"); $("#curriculum").scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth" }); return; } const open = e.target.closest("[data-open]"); - if (open) { e.preventDefault(); openStation(open.dataset.open, sourceOf(open)); } + if (open) { e.preventDefault(); state.backTo = open.dataset.fromPerson || (open.closest(".p-repos") ? (state.lastPerson || null) : null); openStation(open.dataset.open, sourceOf(open)); } + const back = e.target.closest("[data-back-person]"); + if (back) { openPerson(back.dataset.backPerson, "back"); return; } }); dialog().addEventListener("click", (e) => { if (e.target === dialog()) closeStation(); }); dialog().addEventListener("close", () => { + state.backTo = null; if (stationFromHash()) history.replaceState(null, "", location.pathname + location.search); }); window.addEventListener("hashchange", openFromHash); diff --git a/assets/js/lab.js b/assets/js/lab.js index c0e21df..7c00dab 100644 --- a/assets/js/lab.js +++ b/assets/js/lab.js @@ -78,7 +78,8 @@ [[0, "off"], [1, "on"]].forEach(([tt, cls]) => el("line", { x1: X(-3), x2: X(3), y1: Y(a + bz * -3 + tau * tt), y2: Y(a + bz * 3 + tau * tt), class: `fit-l ${cls}` }, fitG)); const zl = -2.2, yOn = Y(a + bz * zl + tau), yOff = Y(a + bz * zl); el("line", { x1: X(zl), x2: X(zl), y1: yOn, y2: yOff, class: "fit-gap" }, fitG); - el("text", { x: X(zl) + 6, y: (yOn + yOff) / 2 + 4, class: "fit-t" }, fitG).textContent = `같은 충성도에서의 간격 = 보정 ${fmt(tau)}`; + /* label in the sparse top-left corner, so it never sits on the dots or between close lines */ + el("text", { x: L + 8, y: T + 14, class: "fit-t" }, fitG).textContent = small ? `세로 실선 = 보정 ${fmt(tau)}` : `세로 실선(같은 충성도에서의 간격) = 보정 ${fmt(tau)}`; } const means = el("g", { class: "means" }); [["on", r.mOn, "켬 평균"], ["off", r.mOff, "끔 평균"]].forEach(([cls, m, label]) => { @@ -97,6 +98,7 @@ $("#lab-ch-text").textContent = "해냈습니다. 무작위로 켜면 충성도와 알림이 따로 움직여서, 단순 비교만으로도 진짜 효과에 가까워집니다. 이것이 무작위 실험이 하는 일입니다."; track("lab_challenge_complete", { randomized: true, conf: r.conf }); if (window.hubStamp) window.hubStamp("lab"); + const learn = $("#lab-learn"); if (learn) learn.open = true; return; } if (r.rand && r.conf >= 5) { @@ -106,6 +108,11 @@ } } + /* the sticky chart sits right under the header, whose height changes with the nav wrap */ + const header = document.querySelector(".site-header"); + const setHeader = () => { if (header) document.documentElement.style.setProperty("--header-h", `${header.offsetHeight}px`); }; + setHeader(); window.addEventListener("resize", setHeader); + function update(source) { if (source === "confounding" || source === "effect") moves += 1; const r = compute(); diff --git a/assets/js/passport.js b/assets/js/passport.js index a0973a6..befb0f5 100644 --- a/assets/js/passport.js +++ b/assets/js/passport.js @@ -1,5 +1,6 @@ /* Learning passport: stamps for small actions across the hub, the game, the lab and the - Builder pages. Everything stays in this browser (localStorage); GA only gets stamp_earned. */ + Builder pages. Stamps stay in this browser (localStorage); GA gets stamp_earned and + passport_open with counts only. */ (() => { "use strict"; const KEY = "cil-hub-stamps"; @@ -10,7 +11,7 @@ { id: "streak3", mark: "3", name: "사흘 연속", how: "오늘의 한 문제를 3일 연속 풀어 보세요.", href: "/#daily" }, { id: "game", mark: "몸", name: "몸풀기 완주", how: "‘상관일까, 인과일까?’ 10문제를 끝까지 풀어 보세요.", href: "/game.html" }, { id: "sharp", mark: "감", name: "인과 감별사", how: "게임에서 8문제 이상 맞혀 보세요.", href: "/game.html" }, - { id: "lab", mark: "실", name: "실험가", how: "교란 실험실에서 편향을 0 가까이 줄여 보세요.", href: "/lab.html" }, + { id: "lab", mark: "실", name: "실험가", how: "교란 실험실에서 단순 비교와 진짜 효과의 차이를 1 이하로 줄여 보세요.", href: "/lab.html" }, { id: "finish", mark: "완", name: "한 단계 완주", how: "한 단계의 자료를 모두 학습 완료로 표시해 보세요.", href: "/#curriculum" }, { id: "builder", mark: "B", name: "Builder 탐방", how: "Builder 이력이나 페이지를 세 번 열어 보세요.", href: "/builders/" } ]; @@ -31,8 +32,11 @@ if (!el) { el = document.createElement("div"); el.id = "hub-toast"; el.className = "toast"; el.setAttribute("role", "status"); el.setAttribute("aria-live", "polite"); - document.body.appendChild(el); } + /* a modal dialog puts the page behind it, so the toast goes inside the top one */ + const open = [...document.querySelectorAll("dialog[open]")].pop(); + const host = open || document.body; + if (el.parentNode !== host) host.appendChild(el); el.innerHTML = `스탬프 획득 · ${esc(stamp.name)}${n}/${STAMPS.length}`; el.classList.add("show"); const hide = () => { toastTimer = setTimeout(() => el.classList.remove("show"), 4200); }; diff --git a/assets/js/track.js b/assets/js/track.js index e0cf985..fd0dae4 100644 --- a/assets/js/track.js +++ b/assets/js/track.js @@ -29,6 +29,8 @@ try { params = JSON.parse(el.dataset.eventParams || "{}"); } catch (err) { params = {}; } window.hubTrack(el.dataset.event, params); }); + /* Profile photos that fail to load leave the initial underneath (no inline onerror, so a CSP can forbid inline handlers). */ + document.addEventListener("error", (e) => { const t = e.target; if (t && t.tagName === "IMG" && t.hasAttribute("data-fallback")) t.remove(); }, true); /* Buttons with data-copy copy their value and confirm in place. */ document.addEventListener("click", async (e) => { const b = e.target.closest("[data-copy]"); @@ -41,3 +43,43 @@ if (ok && b.dataset.share) window.hubTrack("share", { content: b.dataset.share, method: "copy" }); }); })(); + +/* Links to the team's other sites on this domain (the web book, causal-studio, ccwr, ...): add utm_* + so that site's GA sees the visit came from the hub and from which spot, open them in the same tab + unless the reader asked for a new one, and send webbook_click or team_site_click. */ +(() => { + "use strict"; + const HUB_PATHS = /^\/(?:$|index\.html$|article\.html$|404\.html$|game\.html$|lab\.html$|assets\/|data\/|articles\/|builders\/|badges\/)/; + const WEBBOOK = "Causal-Inference-with-Python"; + const SPOTS = [ + ["#featured", "featured"], [".partner", "partner"], ["#station-dialog", "station"], ["#daily", "daily"], + ["#shelf, #week, .spot", "curriculum"], [".gm", "game"], [".lab", "lab"], [".bp", "builder_page"], + [".reader, .prose", "article"], [".site-footer", "footer"], [".banner", "banner"] + ]; + function teamSite(a) { + let u; + try { u = new URL(a.href, location.href); } catch (e) { return null; } + if (u.origin !== location.origin || HUB_PATHS.test(u.pathname)) return null; + const site = u.pathname.split("/")[1]; + return site ? { url: u, site } : null; + } + const placement = (a) => (SPOTS.find(([sel]) => a.closest(sel)) || [null, "other"])[1]; + function decorate(e) { + const a = e.target.closest && e.target.closest("a[href]"); + if (!a) return; + const t = teamSite(a); + if (!t) return; + const where = placement(a), u = t.url; + if (!u.searchParams.has("utm_source")) { + u.searchParams.set("utm_source", "hub"); + u.searchParams.set("utm_medium", "referral"); + u.searchParams.set("utm_campaign", "crosslink"); + u.searchParams.set("utm_content", where); + a.href = u.toString(); + } + if (e.type === "click" && e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) a.removeAttribute("target"); + window.hubTrack(t.site === WEBBOOK ? "webbook_click" : "team_site_click", { site: t.site, placement: where, link_url: u.origin + u.pathname }); + } + document.addEventListener("click", decorate, true); + document.addEventListener("auxclick", decorate, true); +})(); diff --git a/assets/vendor/marked-12.0.2.min.js b/assets/vendor/marked-12.0.2.min.js new file mode 100644 index 0000000..a91afe7 --- /dev/null +++ b/assets/vendor/marked-12.0.2.min.js @@ -0,0 +1,6 @@ +/** + * marked v12.0.2 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=t[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1");e=x(e.replace(/^ *>[ \t]?/gm,""),"\n");const n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).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(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="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",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).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",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).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",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).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:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=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(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
      '+(n?e:c(e,!0))+"
      \n":"
      "+(n?e:c(e,!0))+"
      \n"}blockquote(e){return`
      \n${e}
      \n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
      \n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
    4. ${e}
    5. \n`}checkbox(e){return"'}paragraph(e){return`

      ${e}

      \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
      \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
      "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
      ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

      An error occurred:

      "+c(n.message+"",!0)+"
      ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); diff --git a/assets/vendor/purify-3.4.16.min.js b/assets/vendor/purify-3.4.16.min.js new file mode 100644 index 0000000..00742c4 --- /dev/null +++ b/assets/vendor/purify-3.4.16.min.js @@ -0,0 +1,7 @@ +/*! @license DOMPurify 3.4.16 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.16/LICENSE */ +(function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t():typeof define==`function`&&define.amd?define([],t):(e=typeof globalThis<`u`?globalThis:e||self,e.DOMPurify=t())})(this,function(){"use strict";function e(e,t){this.v=e,this.k=t}function t(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n`u`?null:T(BigInt.prototype.toString),pe=typeof Symbol>`u`?null:T(Symbol.prototype.toString),x=T(Object.prototype.hasOwnProperty),S=T(Object.prototype.toString),C=T(RegExp.prototype.test),w=E(TypeError);function T(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);var n=[...arguments].slice(1);return g(e,t,n)}}function E(e){return function(){return te(e,[...arguments])}}function D(e,t){let n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:oe;if(l&&l(e,null),!v(t))return e;let r=t.length;for(;r--;){let i=t[r];if(typeof i==`string`){let e=n(i);e!==i&&(u(t)||(t[r]=e),i=e)}e[i]=!0}return e}function me(e){for(let t=0;t/g),Ae=m(/\${[\w\W]*/g),je=m(/^data-[\-\w.\u00B7-\uFFFF]+$/),Me=m(/^aria-[\-\w]+$/),Ne=m(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Pe=m(/^(?:\w+script|data):/i),Fe=m(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Ie=m(/^html$/i),Le=m(/^[a-z][.\w]*(-[.\w]+)+$/i),Re=m(/<[/\w!]/g),ze=m(/<[/\w]/g),Be=m(/<\/no(script|embed|frames)/i),Ve=m(/\/>/i),A={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,processingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},j=[`style`,`script`,`xmp`,`iframe`,`noembed`,`noframes`,`plaintext`,`noscript`],He=p(D({},j)),Ue=function(){let e={};return _(j,t=>{e[t]=m(RegExp(`])`,`i`))}),p(e)}(),We=function(){return typeof window>`u`?null:window},Ge=function(e,t){if(typeof e!=`object`||typeof e.createPolicy!=`function`)return null;let n=null,r=`data-tt-policy-suffix`;t&&t.hasAttribute(r)&&(n=t.getAttribute(r));let i=`dompurify`+(n?`#`+n:``);try{return e.createPolicy(i,{createHTML(e){return e},createScriptURL(e){return e}})}catch(e){return console.warn(`TrustedTypes policy `+i+` could not be created.`),null}},Ke=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},M=function(e,t,n,r){return x(e,t)&&v(e[t])?D(r.base?O(r.base):{},e[t],r.transform):n},qe=function(e,t,n){let r=x(e,t)?e[t]:void 0;return r&&typeof r==`object`?O(r):n()};function Je(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:We(),t=e=>Je(e);if(t.version=`3.4.16`,t.removed=[],!e||!e.document||e.document.nodeType!==A.document||!e.Element)return t.isSupported=!1,t;let n=e.document,r=n,i=r.currentScript;e.DocumentFragment;let a=e.HTMLTemplateElement,o=e.Node,s=e.Element,l=e.NodeFilter;e.NamedNodeMap===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;let u=e.DOMParser,d=e.trustedTypes,f=s.prototype,ee=k(f,`cloneNode`),g=k(f,`remove`),te=k(f,`removeAttributeNode`),fe=k(f,`nextSibling`),y=k(f,`childNodes`),b=k(f,`parentNode`),pe=k(f,`shadowRoot`),S=k(f,`attributes`),T=o&&o.prototype?k(o.prototype,`nodeType`):null,E=o&&o.prototype?k(o.prototype,`nodeName`):null,me=o&&o.prototype?k(o.prototype,`ownerDocument`):null,j=function(e){return T?T(e):e.nodeType},Ye=function(e){return E?E(e):e.nodeName};if(typeof a==`function`){let e=n.createElement(`template`);e.content&&e.content.ownerDocument&&(n=e.content.ownerDocument)}let N,P=``,Xe,Ze=!1,Qe=0,$e=function(){if(Qe>0)throw w(`A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.`)},F=function(e){$e(),Qe++;try{return N.createHTML(e)}finally{Qe--}},et=function(e){$e(),Qe++;try{return N.createScriptURL(e)}finally{Qe--}},tt=function(){return Ze||(Xe=Ge(d,i),Ze=!0),Xe},nt=n,rt=nt.implementation,it=nt.createNodeIterator,at=nt.createDocumentFragment,ot=nt.getElementsByTagName,st=r.importNode,I=Ke();t.isSupported=typeof c==`function`&&typeof b==`function`&&rt&&rt.createHTMLDocument!==void 0;let ct=Oe,lt=ke,ut=Ae,dt=je,ft=Me,pt=Pe,mt=Fe,ht=Le,gt=Ne,L=null,_t=D({},[..._e,...ve,...ye,...xe,...Ce]),R=null,vt=D({},[...we,...Te,...Ee,...De]),z=Object.seal(h(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),yt=null,bt=null,B=Object.seal(h(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}})),xt=!0,St=!0,Ct=!1,wt=!0,V=!1,H=!0,U=!1,Tt=!1,Et=null,Dt=null,Ot=!1,W=!1,kt=!1,At=!1,jt=!0,Mt=!1,Nt=`user-content-`,Pt=!0,Ft=!1,G={},K=null,It=D({},/* @__PURE__ */ `annotation-xml.audio.colgroup.desc.foreignobject.head.iframe.math.mi.mn.mo.ms.mtext.noembed.noframes.noscript.plaintext.script.selectedcontent.style.svg.template.thead.title.video.xmp`.split(`.`)),Lt=null,Rt=D({},[`audio`,`video`,`img`,`source`,`image`,`track`]),zt=null,Bt=D({},[`alt`,`class`,`for`,`id`,`label`,`name`,`pattern`,`placeholder`,`role`,`summary`,`title`,`value`,`style`,`xmlns`]),Vt=`http://www.w3.org/1998/Math/MathML`,Ht=`http://www.w3.org/2000/svg`,q=`http://www.w3.org/1999/xhtml`,J=q,Ut=!1,Wt=null,Gt=D({},[Vt,Ht,q],se),Kt=p([`mi`,`mo`,`mn`,`ms`,`mtext`]),qt=D({},Kt),Jt=p([`annotation-xml`]),Yt=D({},Jt),Xt=D({},[`title`,`style`,`font`,`a`,`script`]),Zt=null,Qt=[`application/xhtml+xml`,`text/html`],Y=null,$t=null,en=n.createElement(`form`),tn=function(e){return e instanceof RegExp||e instanceof Function},nn=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if($t&&$t===e)return;(!e||typeof e!=`object`)&&(e={}),e=O(e),Zt=Qt.indexOf(e.PARSER_MEDIA_TYPE)===-1?`text/html`:e.PARSER_MEDIA_TYPE,Y=Zt===`application/xhtml+xml`?se:oe,L=M(e,`ALLOWED_TAGS`,_t,{transform:Y}),R=M(e,`ALLOWED_ATTR`,vt,{transform:Y}),Wt=M(e,`ALLOWED_NAMESPACES`,Gt,{transform:se}),zt=M(e,`ADD_URI_SAFE_ATTR`,Bt,{transform:Y,base:Bt}),Lt=M(e,`ADD_DATA_URI_TAGS`,Rt,{transform:Y,base:Rt}),K=M(e,`FORBID_CONTENTS`,It,{transform:Y}),yt=M(e,`FORBID_TAGS`,O({}),{transform:Y}),bt=M(e,`FORBID_ATTR`,O({}),{transform:Y}),G=x(e,`USE_PROFILES`)?e.USE_PROFILES&&typeof e.USE_PROFILES==`object`?O(e.USE_PROFILES):e.USE_PROFILES:!1,xt=e.ALLOW_ARIA_ATTR!==!1,St=e.ALLOW_DATA_ATTR!==!1,Ct=e.ALLOW_UNKNOWN_PROTOCOLS||!1,wt=e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,V=e.SAFE_FOR_TEMPLATES||!1,H=e.SAFE_FOR_XML!==!1,U=e.WHOLE_DOCUMENT||!1,W=e.RETURN_DOM||!1,kt=e.RETURN_DOM_FRAGMENT||!1,At=e.RETURN_TRUSTED_TYPE||!1,Ot=e.FORCE_BODY||!1,jt=e.SANITIZE_DOM!==!1,Mt=e.SANITIZE_NAMED_PROPS||!1,Pt=e.KEEP_CONTENT!==!1,Ft=e.IN_PLACE||!1,gt=ge(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:Ne,J=typeof e.NAMESPACE==`string`?e.NAMESPACE:q,qt=qe(e,`MATHML_TEXT_INTEGRATION_POINTS`,()=>D({},Kt)),Yt=qe(e,`HTML_INTEGRATION_POINTS`,()=>D({},Jt));let t=qe(e,`CUSTOM_ELEMENT_HANDLING`,()=>h(null));if(z=h(null),x(t,`tagNameCheck`)&&tn(t.tagNameCheck)&&(z.tagNameCheck=t.tagNameCheck),x(t,`attributeNameCheck`)&&tn(t.attributeNameCheck)&&(z.attributeNameCheck=t.attributeNameCheck),x(t,`allowCustomizedBuiltInElements`)&&typeof t.allowCustomizedBuiltInElements==`boolean`&&(z.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),m(z),V&&(St=!1),kt&&(W=!0),G&&(L=D({},Ce),R=h(null),G.html===!0&&(D(L,_e),D(R,we)),G.svg===!0&&(D(L,ve),D(R,Te),D(R,De)),G.svgFilters===!0&&(D(L,ye),D(R,Te),D(R,De)),G.mathMl===!0&&(D(L,xe),D(R,Ee),D(R,De))),B.tagCheck=null,B.attributeCheck=null,x(e,`ADD_TAGS`)&&(typeof e.ADD_TAGS==`function`?B.tagCheck=e.ADD_TAGS:v(e.ADD_TAGS)&&(L===_t&&(L=O(L)),D(L,e.ADD_TAGS,Y))),x(e,`ADD_ATTR`)&&(typeof e.ADD_ATTR==`function`?B.attributeCheck=e.ADD_ATTR:v(e.ADD_ATTR)&&(R===vt&&(R=O(R)),D(R,e.ADD_ATTR,Y))),x(e,`ADD_FORBID_CONTENTS`)&&v(e.ADD_FORBID_CONTENTS)&&(K===It&&(K=O(K)),D(K,e.ADD_FORBID_CONTENTS,Y)),Pt&&(L[`#text`]=!0),U&&D(L,[`html`,`head`,`body`]),L.table&&(D(L,[`tbody`]),delete yt.tbody),e.TRUSTED_TYPES_POLICY){if(typeof e.TRUSTED_TYPES_POLICY.createHTML!=`function`)throw w(`TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.`);if(typeof e.TRUSTED_TYPES_POLICY.createScriptURL!=`function`)throw w(`TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.`);let t=N;N=e.TRUSTED_TYPES_POLICY;try{P=F(``)}catch(e){throw N=t,e}}else e.TRUSTED_TYPES_POLICY===null?(N=void 0,P=``):(N===void 0&&(N=tt()),N&&typeof P==`string`&&(P=F(``)));p&&p(e),$t=e},rn=D({},[...ve,...ye,...be]),an=D({},[...xe,...Se]),on=function(e,t,n){return t.namespaceURI===q?e===`svg`:t.namespaceURI===Vt?e===`svg`&&(n===`annotation-xml`||qt[n]):!!rn[e]},sn=function(e,t,n){return t.namespaceURI===q?e===`math`:t.namespaceURI===Ht?e===`math`&&Yt[n]:!!an[e]},cn=function(e,t,n){return t.namespaceURI===Ht&&!Yt[n]||t.namespaceURI===Vt&&!qt[n]?!1:!an[e]&&(Xt[e]||!rn[e])},ln=function(e){let t=b(e);(!t||!t.tagName)&&(t={namespaceURI:J,tagName:`template`});let n=oe(e.tagName),r=oe(t.tagName);return Wt[e.namespaceURI]?e.namespaceURI===Ht?on(n,t,r):e.namespaceURI===Vt?sn(n,t,r):e.namespaceURI===q?cn(n,t,r):!!(Zt===`application/xhtml+xml`&&Wt[e.namespaceURI]):!1},X=function(e){ie(t.removed,{element:e});try{b(e).removeChild(e)}catch(t){if(g(e),!b(e))throw w(`a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place`)}},un=function(e,t,n){try{te(e,t)}catch(t){try{e.removeAttribute(n)}catch(e){}}},dn=function(e){pn(e);let t=y(e);if(t){let e=[];_(t,t=>{ie(e,t)}),_(e,e=>{try{g(e)}catch(e){}})}let n=S(e);if(n)for(let t=n.length-1;t>=0;--t){let r=n[t],i=r&&r.name;typeof i==`string`&&un(e,r,i)}},Z=function(e,n,r){if(!r)try{r=n.getAttributeNode(e)}catch(e){r=null}ie(t.removed,{attribute:r||null,from:n});try{r?te(n,r):n.removeAttribute(e)}catch(t){try{n.removeAttribute(e)}catch(e){}}if(e===`is`){if(W||kt)try{X(n)}catch(e){}else try{n.setAttribute(e,``)}catch(e){}}},fn=function(e){let t=S(e);if(t)for(let n=t.length-1;n>=0;--n){let r=t[n],i=r&&r.name;typeof i!=`string`||R[Y(i)]||un(e,r,i)}},pn=function(e){let t=[e];for(;t.length>0;){let e=t.pop();j(e)===A.element&&fn(e);let n=y(e);if(n)for(let e=n.length-1;e>=0;--e)t.push(n[e])}},mn=function(e,t){return H?e===`patchsrc`||e===`for`&&t!==`label`&&t!==`output`:!1},hn=function(e){if(!H)return;let t=[e];for(;t.length>0;){let e=t.pop(),n=j(e);if(n===A.processingInstruction||n===A.comment&&C(ze,e.data)){try{g(e)}catch(e){}continue}if(n===A.element){let t=e,n=Y(Ye(e));try{t.hasAttribute&&t.hasAttribute(`patchsrc`)&&t.removeAttribute(`patchsrc`),t.hasAttribute&&t.hasAttribute(`for`)&&mn(`for`,n)&&t.removeAttribute(`for`)}catch(e){}}let r=y(e);if(r)for(let e=r.length-1;e>=0;--e)t.push(r[e])}},gn=function(e){let t=null,r=null;if(Ot)e=``+e;else{let t=ce(e,/^[\r\n\t ]+/);r=t&&t[0]}Zt===`application/xhtml+xml`&&J===q&&(e=``+e+``);let i=N?F(e):e;if(J===q)try{t=new u().parseFromString(i,Zt)}catch(e){}if(!t||!t.documentElement){t=rt.createDocument(J,`template`,null);try{t.documentElement.innerHTML=Ut?P:i}catch(e){}}let a=t.body||t.documentElement;return e&&r&&a.insertBefore(n.createTextNode(r),a.childNodes[0]||null),J===q?ot.call(t,U?`html`:`body`)[0]:U?t.documentElement:a},_n=function(e){let t=me?me(e):e.ownerDocument;return it.call(t||e,e,l.SHOW_ELEMENT|l.SHOW_COMMENT|l.SHOW_TEXT|l.SHOW_PROCESSING_INSTRUCTION|l.SHOW_CDATA_SECTION,null)},vn=function(e){return e=le(e,ct,` `),e=le(e,lt,` `),e=le(e,ut,` `),e},yn=function(e){var t;e.normalize();let n=me?me(e):e.ownerDocument,r=it.call(n||e,e,l.SHOW_TEXT|l.SHOW_COMMENT|l.SHOW_CDATA_SECTION|l.SHOW_PROCESSING_INSTRUCTION,null),i=r.nextNode();for(;i;)i.data=vn(i.data),i=r.nextNode();let a=(t=e.querySelectorAll)==null?void 0:t.call(e,`template`);a&&_(a,e=>{xn(e.content)&&yn(e.content)})},bn=function(e){let t=E?E(e):null;return typeof t!=`string`||Y(t)!==`form`?!1:typeof e.nodeName!=`string`||typeof e.textContent!=`string`||typeof e.removeChild!=`function`||e.attributes!==S(e)||typeof e.removeAttribute!=`function`||typeof e.removeAttributeNode!=`function`||typeof e.getAttributeNode!=`function`||typeof e.setAttribute!=`function`||typeof e.namespaceURI!=`string`||typeof e.insertBefore!=`function`||typeof e.hasChildNodes!=`function`||e.nodeType!==T(e)||e.childNodes!==y(e)},xn=function(e){if(!T||typeof e!=`object`||!e)return!1;try{return T(e)===A.documentFragment}catch(e){return!1}},Sn=function(e){if(!T||typeof e!=`object`||!e)return!1;try{return typeof T(e)==`number`}catch(e){return!1}};function Q(e,n,r){e.length!==0&&_(e,e=>{e.call(t,n,r,$t)})}let Cn=function(e,t){return!!(H&&e.hasChildNodes()&&!Sn(e.firstElementChild)&&C(Re,e.textContent)&&C(Re,e.innerHTML)||H&&e.namespaceURI===q&&He[t]&&(Sn(e.firstElementChild)||typeof e.textContent==`string`&&C(Ue[t],e.textContent))||e.nodeType===A.processingInstruction||H&&e.nodeType===A.comment&&C(ze,e.data))},wn=function(e,t){return e instanceof RegExp?C(e,t):e instanceof Function&&!!e(t,...[...arguments].slice(2))},Tn=function(e,t,n){if(!yt[t]&&An(t)&&wn(z.tagNameCheck,t))return!1;if(Pt&&!K[t]){let t=b(e),r=y(e);if(r&&t){let i=r.length;for(let a=i-1;a>=0;--a){let i=e===n?ee(r[a],!0):r[a];t.insertBefore(i,fe(e))}}}return X(e),!0},En=function(e,t,n,r){return e.length===0?t:t===n||t===r?O(t):t},$=function(e,t){return e===t||b(e)!==null?!1:(Ft&&pn(e),!0)},Dn=function(e,n){if(Q(I.beforeSanitizeElements,e,null),$(e,n))return!0;if(bn(e))return X(e),!0;let r=Y(Ye(e));if(L=En(I.uponSanitizeElement,L,_t,Et),Q(I.uponSanitizeElement,e,{tagName:r,allowedTags:L}),$(e,n))return!0;if(Cn(e,r))return X(e),!0;if(yt[r]||!(B.tagCheck instanceof Function&&B.tagCheck(r))&&!L[r]){let t=Tn(e,r,n);return t===!1&&(Q(I.afterSanitizeElements,e,null),$(e,n))?!0:t}if(j(e)===A.element&&!ln(e)||(r===`noscript`||r===`noembed`||r===`noframes`)&&C(Be,e.innerHTML))return X(e),!0;if(V&&e.nodeType===A.text){let n=vn(e.textContent);e.textContent!==n&&(ie(t.removed,{element:e.cloneNode()}),e.textContent=n)}return Q(I.afterSanitizeElements,e,null),$(e,n)},On=function(e,t,r){if(bt[t]||mn(t,e)||jt&&(t===`id`||t===`name`)&&(r in n||r in en))return!1;let i=R[t]||B.attributeCheck instanceof Function&&B.attributeCheck(t,e);return St&&C(dt,t)||xt&&C(ft,t)?!0:i?zt[t]||C(gt,le(r,mt,``))||(t===`src`||t===`xlink:href`||t===`href`)&&e!==`script`&&ue(r,`data:`)===0&&Lt[e]||Ct&&!C(pt,le(r,mt,``))?!0:!r:An(e)&&wn(z.tagNameCheck,e)&&wn(z.attributeNameCheck,t,e)||t===`is`&&z.allowCustomizedBuiltInElements&&wn(z.tagNameCheck,r)},kn=D({},[`annotation-xml`,`color-profile`,`font-face`,`font-face-format`,`font-face-name`,`font-face-src`,`font-face-uri`,`missing-glyph`]),An=function(e){return!kn[oe(e)]&&C(ht,e)},jn=function(e,t,n,r){if(N&&typeof d==`object`&&typeof d.getAttributeType==`function`&&!n)switch(d.getAttributeType(e,t)){case`TrustedHTML`:return F(r);case`TrustedScriptURL`:return et(r)}return r},Mn=function(e,t,n,r){try{return n?e.setAttributeNS(n,t,r):e.setAttribute(t,r),!bn(e)||(X(e),!1)}catch(n){return Z(t,e),!1}},Nn=function(e,n){if(Q(I.beforeSanitizeAttributes,e,null),$(e,n))return;let r=e.attributes;if(!r||bn(e))return;R=En(I.uponSanitizeAttribute,R,vt,Dt);let i={attrName:``,attrValue:``,keepAttr:!0,allowedAttributes:R,forceKeepAttr:void 0},a=r.length,o=Y(e.nodeName);for(;a--;){let n=r[a],s=n.name,c=n.namespaceURI,l=n.value,u=Y(s),d=l,f=s===`value`?d:de(d),p=!1;if(i.attrName=u,i.attrValue=f,i.keepAttr=!0,i.forceKeepAttr=void 0,Q(I.uponSanitizeAttribute,e,i),f=i.attrValue,Mt&&(u===`id`||u===`name`)&&ue(f,Nt)!==0&&(Z(s,e,n),f=Nt+f,p=!0),H&&C(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,f)){Z(s,e,n);continue}if(u===`attributename`&&ce(f,`href`)){Z(s,e,n);continue}if(!i.forceKeepAttr){if(!i.keepAttr){Z(s,e,n);continue}if(!wt&&C(Ve,f)){Z(s,e,n);continue}if(V&&(f=vn(f)),!On(o,u,f)){Z(s,e,n);continue}f=jn(o,u,c,f),f!==d&&Mn(e,s,c,f)&&p&&re(t.removed)}}Q(I.afterSanitizeAttributes,e,null),$(e,n)},Pn=function(e){let t=null,n=_n(e);for(Q(I.beforeSanitizeShadowDOM,e,null);t=n.nextNode();)if(Q(I.uponSanitizeShadowNode,t,null),Dn(t,e),Nn(t,e),xn(t.content)&&Pn(t.content),j(t)===A.element){let e=pe(t);xn(e)&&(Fn(e),Pn(e))}Q(I.afterSanitizeShadowDOM,e,null)},Fn=function(e){let t=[{node:e,shadow:null}];for(;t.length>0;){let e=t.pop();if(e.shadow){Pn(e.shadow);continue}let n=e.node,r=j(n)===A.element,i=y(n);if(i)for(let e=i.length-1;e>=0;--e)t.push({node:i[e],shadow:null});if(r){let e=E?E(n):null;if(typeof e==`string`&&Y(e)===`template`){let e=n.content;xn(e)&&t.push({node:e,shadow:null})}}if(r){let e=pe(n);xn(e)&&t.push({node:null,shadow:e},{node:e,shadow:null})}}};return t.sanitize=function(e){let n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},i=null,a=null,o=null,s=null;if(Ut=!e,Ut&&(e=``),typeof e!=`string`&&!Sn(e)&&(e=he(e),typeof e!=`string`))throw w(`dirty is not a string, aborting`);if(!t.isSupported)return e;Tt?(L=Et,R=Dt):nn(n),(I.uponSanitizeElement.length>0||I.uponSanitizeAttribute.length>0)&&(L=O(L)),I.uponSanitizeAttribute.length>0&&(R=O(R)),t.removed=[];let c=Ft&&typeof e!=`string`&&Sn(e);if(c){hn(e);let t=Ye(e);if(typeof t==`string`){let n=Y(t);if(!L[n]||yt[n])throw dn(e),w(`root node is forbidden and cannot be sanitized in-place`)}if(bn(e))throw dn(e),w(`root node is clobbered and cannot be sanitized in-place`);try{Fn(e)}catch(t){throw dn(e),t}}else if(Sn(e))i=gn(``),a=i.ownerDocument.importNode(e,!0),a.nodeType===A.element&&a.nodeName===`BODY`||a.nodeName===`HTML`?i=a:i.appendChild(a),Fn(i);else{if(!W&&!V&&!U&&e.indexOf(`<`)===-1)return N&&At?F(e):e;if(i=gn(e),!i)return W?null:At?P:``}i&&Ot&&X(i.firstChild);let l=c?e:i;try{let e=_n(l);for(;o=e.nextNode();)Dn(o,l),Nn(o,l),xn(o.content)&&Pn(o.content)}catch(n){throw c&&(dn(e),_(t.removed,e=>{e.element&&pn(e.element)})),n}if(c){let n=!1;if(_(t.removed,t=>{t.element&&(t.element===e&&(n=!0),pn(t.element))}),n)throw w(`a node selected for removal could not be safely returned; refusing to sanitize in place`);return V&&yn(e),e}if(W){if(V&&yn(i),kt)for(s=at.call(i.ownerDocument);i.firstChild;)s.appendChild(i.firstChild);else s=i;return(R.shadowroot||R.shadowrootmode)&&(s=st.call(r,s,!0)),s}let u=U?i.outerHTML:i.innerHTML;return U&&L[`!doctype`]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&C(Ie,i.ownerDocument.doctype.name)&&(u=` +`+u),V&&(u=vn(u)),N&&At?F(u):u},t.setConfig=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};nn(e),Tt=!0,Et=L,Dt=R},t.clearConfig=function(){$t=null,Tt=!1,Et=null,Dt=null,N=Xe,P=``},t.isValidAttribute=function(e,t,n){$t||nn({});let r=Y(e),i=Y(t);return On(r,i,n)},t.addHook=function(e,t){typeof t==`function`&&x(I,e)&&ie(I[e],t)},t.removeHook=function(e,t){if(x(I,e)){if(t!==void 0){let n=ne(I[e],t);return n===-1?void 0:ae(I[e],n,1)[0]}return re(I[e])}},t.removeHooks=function(e){x(I,e)&&(I[e]=[])},t.removeAllHooks=function(){I=Ke()},t}return Je()}); +//# sourceMappingURL=purify.min.js.map \ No newline at end of file diff --git a/data/routes.json b/data/routes.json index 8de46ab..67f15d7 100644 --- a/data/routes.json +++ b/data/routes.json @@ -21,6 +21,17 @@ "causal-inference-practice" ] }, + { + "id": "marketing", + "label": "마케팅 효과를 재요", + "description": "광고와 프로모션이 정말 매출을 움직였는지, 마케팅 사이언스 자료와 특강으로 따라갑니다.", + "stops": [ + "causal-studio", + "Marketing_Science", + "causal-inference-lecture", + "data-still-does-not-speak" + ] + }, { "id": "ml", "label": "ML로 효과를 추정해요", diff --git a/game.html b/game.html index 7c87a99..42799f9 100644 --- a/game.html +++ b/game.html @@ -2,6 +2,7 @@ + 상관일까, 인과일까? | Causal Inference Hub @@ -14,14 +15,15 @@ - + +
      diff --git a/index.html b/index.html index e64c1e9..5574ced 100644 --- a/index.html +++ b/index.html @@ -2,6 +2,7 @@ + Causal Inference Hub | 가짜연구소 인과추론팀 @@ -13,7 +14,7 @@ - + @@ -90,11 +91,6 @@

      커리큘럼과 오픈소스

      무엇을 하고 싶나요?
      -

      diff --git a/lab.html b/lab.html index 81ca980..13609a1 100644 --- a/lab.html +++ b/lab.html @@ -2,6 +2,7 @@ + 교란 실험실 | Causal Inference Hub @@ -14,14 +15,15 @@ - + + @@ -31,6 +33,11 @@

      알림을 켜면 사용자가 더 남을까?

      가상 사용자 800명의 데이터입니다. 충성도가 높은 사용자는 알림을 더 잘 켜고, 원래 더 오래 남습니다. 슬라이더를 움직여 ‘알림 켠 사람과 끈 사람의 단순 비교’가 진짜 효과에서 얼마나 멀어지는지 보세요.

      +
      +

      도전

      +

      교란 강도를 5 이상으로 둔 채, 단순 비교와 진짜 효과의 차이를 1 이하로 줄여 보세요.

      +
      +

      실험 조건

      @@ -53,27 +60,25 @@

      실험 조건

      보정 값도 표본에 따라 ±0.5쯤 흔들립니다. ‘새 사용자 800명 뽑기’로 확인해 보세요.

      -
      -

      도전

      -

      교란 강도를 5 이상으로 둔 채, 단순 비교와 진짜 효과의 차이를 1 이하로 줄여 보세요.

      -

      사용자 800명

      가로는 충성도, 세로는 30일 잔존 점수. 점선은 두 집단의 평균(간격이 단순 비교), 실선은 충성도가 같을 때의 두 집단(간격이 보정 값)입니다.

      알림을 켠 사용자와 끈 사용자의 충성도와 잔존 점수 산점도 -
      • 알림 켬
      • 알림 끔
      +
      • 알림 켬
      • 알림 끔
      • 집단 평균
      • 같은 충성도
      -

      여기서 본 것

      +
      +

      여기서 본 것

      도전을 풀면 열립니다 · 먼저 읽어도 됩니다
      1. 교란이 단순 비교를 부풀립니다. 충성도가 알림과 잔존을 함께 움직이면, 알림 효과가 0이어도 단순 비교는 양수로 나옵니다. 4장 그래프 인과 모델
      2. 무작위로 켜면 편향이 사라집니다. 충성도와 알림이 따로 움직이니 두 집단이 평균적으로 같아집니다. 2장 무작위 실험
      3. 교란 요인을 안다면 보정할 수 있습니다. 여기서는 충성도를 알기에 회귀로 보정한 값이 정답에 가깝습니다. 현실에서는 충성도처럼 다 재기 어려운 요인이 남는다는 것이 문제입니다. 5장 선형회귀
      +
      diff --git a/scripts/build_builders.py b/scripts/build_builders.py index df4439f..e027db8 100644 --- a/scripts/build_builders.py +++ b/scripts/build_builders.py @@ -13,7 +13,9 @@ import html import json import os +import re from collections import OrderedDict +from urllib.parse import urlsplit ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SITE = "https://causalinferencelab.github.io" @@ -29,6 +31,23 @@ def esc(s): return html.escape(str(s if s is not None else ""), quote=True) +SLUG = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") +TOKEN = re.compile(r"[A-Za-z0-9_-]+") + + +def safe_url(u): + """Only http(s) links reach an href; anything else (javascript:, data:) becomes "#".""" + try: + return u if urlsplit(str(u)).scheme in ("http", "https") else "#" + except ValueError: + return "#" + + +def token(v, fallback="none"): + """Ids that land in attributes (track ids) must be plain tokens.""" + return v if isinstance(v, str) and TOKEN.fullmatch(v) else fallback + + def builder_stats(): """Mirror builderStats() in assets/js/hub.js so the pages and the hub agree.""" builders = load("builders.json") @@ -90,6 +109,7 @@ def head(title, desc, url, image): + {esc(title)} @@ -103,14 +123,15 @@ def head(title, desc, url, image): - + + @@ -132,7 +153,7 @@ def head(title, desc, url, image): def avatar(p, size="lg"): gh = p["info"].get("github") - img = f'' if gh else "" + img = f'' if gh else "" return f'' @@ -159,15 +180,15 @@ def timeline(p, repos, projects, terms): out = [] for e in events: if e["role"] == "Builder": - facts = [f"러너 {e['runners']}명" if e.get("runners") else "", "진행 중" if e.get("live") else ""] + facts = [f"러너 {esc(e['runners'])}명" if e.get("runners") else "", "진행 중" if e.get("live") else ""] else: facts = [f"Builder {esc(e['by'])}" if e.get("by") else ""] link = (f'레포 보기' if e.get("repo") - else f'기록 보기' if e.get("link") else "") + else f'기록 보기' if e.get("link") else "") note = ''.join(x for x in facts + [link] if x) cls = "b" if e["role"] == "Builder" else "r" - out.append(f'
    6. {e["cohort"]}기{esc(terms.get(e["cohort"], ""))}' - f'{e["role"]}{esc(e["name"])}' + out.append(f'
    7. {esc(e["cohort"])}기{esc(terms.get(e["cohort"], ""))}' + f'{esc(e["role"])}{esc(e["name"])}' f'{note_html(note)}{retro_html(e.get("retro"))}
    8. ') return "".join(out) @@ -190,6 +211,12 @@ def width(text): """ +def tenure(p): + """The same wording the hub uses: "8기부터 · 3개 기수 (8·9·12기)".""" + c = p["cohorts"] + return f"{p['first']}기 Builder" if len(c) == 1 else f"{p['first']}기부터 · {len(c)}개 기수 ({'·'.join(str(x) for x in c)}기)" + + def badge_message(p): n = len(p["cohorts"]) return f"Builder · {p['first']}기" if n == 1 else f"Builder · {p['first']}기부터 · {n}개 기수" @@ -209,25 +236,25 @@ def page(p, repos, projects, terms): badge_md = f"[![CausalInferenceLab Builder]({badge_url})]({SITE}/builders/{slug}.html?utm_source=share&utm_medium=badge)" share = f"{url}?utm_source=share&utm_medium=builder_page" repo_cards = "".join( - f'
    9. ' + f'
    10. ' f'{esc(r["title"])}{esc(r.get("summary", ""))}
    11. ' for r in own) top = rank_tag(p) body = f"""
      - 허브의 Builder 전체 보기 -
      + Builder 전체 보기 +
      {avatar(p)}

      {top}Causal Inference Hub Builder

      {esc(p['name'])}

      -

      {p['first']}기부터 Builder · {s} 중 {len(p['cohorts'])}개 기수

      +

      {esc(tenure(p))}

      {f'

      {esc(intro)}

      ' if intro else ''} - + {'' if (intro and p["info"].get("github")) else f'

      본인이신가요? 소개와 사진 추가하기

      '}
      -
      • {p['projects']}프로젝트
      • {len(p['cohorts'])}기수
      • {p['runners']}함께한 러너
      • {len(own)}레포
      +
      • {esc(p['projects'])}프로젝트
      • {len(p['cohorts'])}기수
      • {esc(p['runners'])}함께한 러너
      • {len(own)}레포

      이력

        {timeline(p, repos, projects, terms)}