diff --git a/README.md b/README.md index e84eb02..c84d574 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ Matheus Facure의 [Causal Inference for The Brave and True](https://matheusfacur ## 함께 만들기 +- 웹북의 각 장 끝 '의견 나누기'에서 GitHub 계정으로 로그인해 댓글을 남길 수 있습니다. 댓글은 장별로 [GitHub Discussions](https://github.com/CausalInferenceLab/Causal-Inference-with-Python/discussions)에 모입니다. - 오역, 오탈자, 깨진 수식이나 그림은 [이슈](https://github.com/CausalInferenceLab/Causal-Inference-with-Python/issues)로 알려 주세요. - 직접 고치고 싶다면 [번역 가이드](TRANSLATION_GUIDE.md)를 읽고 PR을 보내 주세요. 원고는 `causal-inference-for-the-brave-and-true/` 폴더의 노트북입니다. - 그 밖의 문의는 메인테이너 [신진수](https://github.com/jsshin2022)에게 부탁드립니다. diff --git a/book/.gitignore b/book/.gitignore index cd61329..11baf8d 100644 --- a/book/.gitignore +++ b/book/.gitignore @@ -11,3 +11,4 @@ _book/ /.quarto/ **/*.quarto_ipynb glossary.qmd +comments.html diff --git a/book/_quarto.yml b/book/_quarto.yml index f174d94..a503d03 100644 --- a/book/_quarto.yml +++ b/book/_quarto.yml @@ -87,13 +87,13 @@ book: - 23-Challenges-with-Effect-Heterogeneity-and-Nonlinearity.ipynb - 24-The-Diff-in-Diff-Saga.ipynb - 25-Synthetic-Diff-in-Diff.ipynb - - glossary.qmd appendices: - Debiasing-with-Orthogonalization.ipynb - Debiasing-with-Propensity-Score.ipynb - When-Prediction-Fails.ipynb - Prediction-Metrics-For-Causal-Models.ipynb - Conformal-Inference-for-Synthetic-Control.ipynb + - glossary.qmd crossref: chapters: true @@ -120,6 +120,7 @@ format: - sidebar-resize.html - chapter-toc.html - site-search.html + - comments.html include-in-header: - text: | diff --git a/book/prepare.py b/book/prepare.py index 97c4b20..689d6ac 100644 --- a/book/prepare.py +++ b/book/prepare.py @@ -13,9 +13,11 @@ 실행: python book/prepare.py """ +import html import json import re import shutil +import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent.parent @@ -137,6 +139,30 @@ def label(key): } +def chapter_chip(key): + keys = list(CHAPTERS) + i = keys.index(key) + num = f"{i + 1}장" if i < 25 else f"부록 {chr(65 + i - 25)}" + return (f'' + f'{num} {html.escape(CHAPTERS[key])}') + + +def gloss_table(items): + rows = [] + for anchor, ko, en, others, top in items: + alias = (f'다른 표기: {html.escape(", ".join(others))}' if others else "") + chips = "".join(chapter_chip(k) for k in top) or '이 책 밖의 용어' + rows.append( + f'' + f'#' + f'{html.escape(ko)}{alias}' + f'{html.escape(en)}' + f'{chips}' + ) + return ('' + + "".join(rows) + "
번역어원어주로 나오는 장
") + + def build_glossary(): """번역 가이드 용어표로 용어집 페이지를 만든다.""" guide = (ROOT / "TRANSLATION_GUIDE.md").read_text(encoding="utf-8") @@ -153,8 +179,9 @@ def build_glossary(): "번역어는 『실무로 통하는 인과추론 with 파이썬』을 기준으로 하고, 이 책에 나오지 않는 온라인 실험 용어는 " "가짜연구소 인과추론팀이 함께 정해 나가고 있습니다.", "", - "용어마다 원어와 다른 표기, 그 용어를 주로 다루는 장을 붙였습니다. 다른 자료에서 본 표기(예: 처치군, 통제군)나 " - "띄어 쓴 표기로 검색해도 이 페이지에서 찾을 수 있고, 용어 이름 옆 링크를 복사하면 그 용어로 바로 이동하는 주소가 됩니다.", + "표마다 번역어, 원어(영어), 그 용어가 많이 나오는 장을 차례로 적었습니다. 장 이름을 누르면 그 장으로 이동합니다. " + "다른 자료에서 본 표기(예: 처치군, 통제군)나 띄어 쓴 표기로 검색해도 이 페이지에서 찾을 수 있고, " + "용어 앞의 `#`을 복사하면 그 용어로 바로 이동하는 주소가 됩니다.", "", f"::: {{.callout-tip appearance=\"simple\" title=\"용어 제안하기\"}}\n빠진 용어나 더 자연스러운 번역어가 있다면 [용어 제안 이슈]({propose})로 알려 주세요. 논의를 거쳐 번역 가이드와 이 용어집에 함께 반영합니다.\n:::", "", @@ -183,26 +210,95 @@ def score(k): return n / max(len(text), 1) * 10000 + (5 if intro else 0) scores = {k: score(k) for k in order} - top = sorted((k for k in order if scores[k]), key=lambda k: -scores[k])[:3] + top = sorted((k for k in order if scores[k]), key=lambda k: -scores[k])[:4] top.sort(key=order.index) others = [a for a in re.findall(r"`([^`]+)`", note) if "쓰지" in note] - anchor = "term-" + re.sub(r"[^0-9A-Za-z가-힣]+", "-", ko).strip("-") - line_out = f"- [**{ko}**]{{#{anchor}}} [#](#{anchor}){{.term-link aria-label=\"{ko} 링크\"}} · {en}" for w, spaced in SPACED.items(): if w in ko: alias = re.sub(r"\s*\([^)]*\)", "", ko.replace(w, spaced)).strip() if alias not in others: others.append(alias) - if others: - line_out += f" · 다른 표기: {', '.join(others)}" - if top: - line_out += " · " + ", ".join(f"[{label(k)}]({k}.html)" for k in top) - items.append(line_out) + anchor = "term-" + re.sub(r"[^0-9A-Za-z가-힣]+", "-", ko).strip("-") + items.append((anchor, ko, en, others, top)) if items: - out += [f"## {heading.strip()}", ""] + ([" ".join(intro), ""] if intro else []) + items + [""] + out += [f"## {heading.strip()}", ""] + ([" ".join(intro), ""] if intro else []) + out += ["```{=html}", gloss_table(items), "```", ""] (OUT / "glossary.qmd").write_text("\n".join(out), encoding="utf-8") +REPO = "CausalInferenceLab/Causal-Inference-with-Python" +REPO_ID = "R_kgDOHkGCqg" +COMMENT_CATEGORIES = ("웹북 댓글", "Comments", "General") + + +def giscus_category(): + """giscus 앱이 설치되어 있으면 댓글을 모을 Discussions 카테고리 (이름, ID)를 돌려준다.""" + try: + url = f"https://giscus.app/api/discussions/categories?repo={REPO}" + with urllib.request.urlopen(url, timeout=15) as res: + data = json.loads(res.read().decode()) + cats = {c["name"]: c["id"] for c in data.get("categories", [])} + for name in COMMENT_CATEGORIES: + if name in cats: + return name, cats[name] + except Exception: + pass + return None + + +def build_comments(): + """장마다 붙일 댓글 영역(include-after-body)을 만든다. + + giscus(GitHub Discussions)가 준비되어 있으면 GitHub 로그인 댓글을, 아니면 Discussions로 가는 안내만 넣는다. + """ + cat = giscus_category() + discussions = f"https://github.com/{REPO}/discussions" + head = ( + '
' + '

의견 나누기

' + '

번역이 어색한 곳, 더 쉬운 설명, 궁금한 점을 남겨 주세요. ' + 'GitHub 계정으로 로그인하면 댓글을 달 수 있고, 남긴 의견은 ' + f'GitHub Discussions에 장별로 모입니다.

' + ) + if cat: + name, cid = cat + body = ( + '
' + '' + ) + else: + body = ( + '

' + f'GitHub Discussions에서 의견 남기기 ' + f'오류 제보하기

' + ) + script = """""" + (OUT / "comments.html").write_text(head + body + "
\n" + script + "\n", encoding="utf-8") + print("comments:", f"giscus ({cat[0]})" if cat else "Discussions 링크 (giscus 미설치)") + + def prepare_notebook(key, title): nb = json.loads((SRC / f"{key}.ipynb").read_text(encoding="utf-8")) cells = nb["cells"] @@ -235,6 +331,7 @@ def main(): for key, title in CHAPTERS.items(): prepare_notebook(key, title) build_glossary() + build_comments() # 이미지·데이터와 노트북이 import하는 보조 모듈 data = OUT / "data" if data.is_symlink() or data.exists(): diff --git a/book/sidebar-resize.html b/book/sidebar-resize.html index e3ba2d8..5f01abf 100644 --- a/book/sidebar-resize.html +++ b/book/sidebar-resize.html @@ -12,6 +12,13 @@ if (get(KEY_R) === "1") root.classList.add("toc-collapsed"); document.addEventListener("DOMContentLoaded", function () { + // 용어집은 부록 묶음 밖, 목차 맨 아래에 둔다 + var gl = document.querySelector('#quarto-sidebar a.sidebar-link[href$="glossary.html"]'); + var top = document.querySelector("#quarto-sidebar .sidebar-menu-container > ul"); + if (gl && top) { + var li = gl.closest("li.sidebar-item"); + if (li && li.parentElement !== top) { li.classList.add("sidebar-glossary"); top.appendChild(li); } + } var sidebar = document.getElementById("quarto-sidebar"); if (!document.body.classList.contains("docked") || !sidebar) return; diff --git a/book/styles-dark.scss b/book/styles-dark.scss index 83478ed..c3b387f 100644 --- a/book/styles-dark.scss +++ b/book/styles-dark.scss @@ -128,3 +128,19 @@ body { background: #1b1d2a; color: #eceaff; } + +.gloss-table thead th { + background: #14151f; + color: #c9bcf5; +} +.gloss-table strong, +.ch-chip { + color: #eceaff !important; +} +.ch-chip { + background: rgba(169, 150, 214, 0.12); + border-color: rgba(169, 150, 214, 0.25); +} +.ch-chip b { + color: #c9bcf5; +} diff --git a/book/styles.scss b/book/styles.scss index 7035ad0..7e3a946 100644 --- a/book/styles.scss +++ b/book/styles.scss @@ -1084,3 +1084,163 @@ html.cil-search-open { // 검색 미리보기의 수식은 평문이므로 줄바꿈이 자연스럽게 .cil-search-snippet { word-break: keep-all; overflow-wrap: anywhere; } .cil-search-bar input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; } + +// 용어집 표 +.gloss-table { + width: 100%; + margin: 0.6rem 0 2rem; + border-collapse: separate; + border-spacing: 0; + font-size: 0.93rem; +} +.gloss-table thead th { + position: sticky; + top: 0; + z-index: 1; + padding: 0.55rem 0.8rem; + border-bottom: 2px solid rgba($indigo, 0.25); + background: $body-bg; + color: $indigo; + font-size: 0.8rem; + font-weight: 700; + text-align: left; +} +.gloss-table td { + padding: 0.7rem 0.8rem; + border-bottom: 1px solid rgba($indigo, 0.1); + vertical-align: top; +} +.gloss-table td:nth-child(1) { + width: 24%; +} +.gloss-table td:nth-child(2) { + width: 22%; + color: rgba($body-color, 0.8); +} +.gloss-table tr:target td { + background: rgba($lavender, 0.18); +} +.gloss-table strong { + color: $navy; +} +.gloss-table .term-link { + margin-right: 0.35rem; +} +.gloss-alias { + display: block; + margin-top: 0.2rem; + color: rgba($body-color, 0.6); + font-size: 0.8rem; +} +.ch-chip { + display: inline-flex; + align-items: baseline; + gap: 0.3rem; + margin: 0 0.3rem 0.3rem 0; + padding: 0.15rem 0.55rem; + border: 1px solid rgba($indigo, 0.15); + border-radius: 999px; + background: $lavender-soft; + color: $navy !important; + font-size: 0.8rem; + text-decoration: none !important; + white-space: nowrap; +} +.ch-chip b { + color: $indigo; +} +.ch-chip:hover { + border-color: $indigo; +} +.gloss-none { + color: rgba($body-color, 0.45); + font-size: 0.8rem; +} +@media (max-width: 767.98px) { + .gloss-table thead { + display: none; + } + .gloss-table, + .gloss-table tbody, + .gloss-table tr, + .gloss-table td { + display: block; + width: 100% !important; + } + .gloss-table tr { + padding: 0.7rem 0; + border-bottom: 1px solid rgba($indigo, 0.12); + } + .gloss-table td { + padding: 0.15rem 0; + border: 0; + } + .gloss-table td:nth-child(2)::before, + .gloss-table td:nth-child(3)::before { + content: attr(data-label); + display: inline-block; + min-width: 4.5rem; + color: $lavender; + font-size: 0.75rem; + font-weight: 700; + } + .ch-chip { + white-space: normal; + } +} + +// 왼쪽 목차: 장 사이 간격을 넉넉하게 +#quarto-sidebar .sidebar-item .sidebar-item-container > a, +#quarto-sidebar .sidebar-item > .sidebar-link { + padding-top: 0.32rem; + padding-bottom: 0.32rem; +} +#quarto-sidebar .sidebar-item-section { + margin-top: 0.5rem; +} +#quarto-sidebar .sidebar-item .menu-text { + line-height: 1.45; +} + +// 목차 맨 아래 용어집 +#quarto-sidebar li.sidebar-glossary { + margin-top: 0.8rem; + padding-top: 0.6rem; + border-top: 1px solid rgba($indigo, 0.12); +} +#quarto-sidebar li.sidebar-glossary .menu-text { + font-weight: 700; + color: $navy; +} +#quarto-sidebar li.sidebar-glossary .chapter-number { + display: none; +} + +// 장 끝 댓글 +.cil-comments { + margin: 3rem 0 1rem; + padding-top: 1.5rem; + border-top: 1px solid rgba($indigo, 0.15); +} +.cil-comments-title { + margin-top: 0 !important; +} +.cil-comments-lead { + color: rgba($body-color, 0.75); + font-size: 0.92rem; +} +.cil-comments-fallback { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + align-items: center; +} +.cil-comments-button { + display: inline-block; + padding: 0.5rem 1rem; + border-radius: 999px; + background: $navy; + color: #fff !important; + font-weight: 600; + text-decoration: none !important; +}