"""WP38 — build literature/references.bib from literature/references_verified.csv (verified entries ONLY). Entry types follow the Crossref `type` (journal-article -> @article, book-chapter / handbook chapters -> @incollection, book / monograph / edited-book -> @book, report (NBER) -> @techreport, posted-content -> @unpublished) and, for URL-verified grey literature, the `type` given in manual_references.yaml (article/book/report). Titles are escaped for LaTeX (no bare '&', no '$'); grey literature carries `url` + `note` with the verification date. """ from __future__ import annotations import re, sys, datetime as dt from pathlib import Path import pandas as pd ROOT = Path(__file__).resolve().parents[2]; LIT = ROOT / "literature" def tex(s): if not isinstance(s, str): return "" s = s.replace("&", "&").replace("\\&", "&").replace("&", "\\&").replace("$", "\\$").replace("%", "\\%").replace("#", "\\#") s = s.replace("‐", "-").replace("—", "---").replace("–", "--") return s.strip() def protect_caps(title): """Keep acronyms / proper nouns capitalised under title-casing styles: wrap tokens with inner capitals or all-caps.""" out = [] for tok in title.split(" "): core = re.sub(r"[^A-Za-z]", "", tok) if len(core) > 1 and (core.isupper() or (core[0].isupper() and any(c.isupper() for c in core[1:])) or core in ("Quebec", "Québec", "Canada", "China", "Ireland", "eBay", "MercadoLibre", "Amazon", "Airbnb", "Google", "Pareto", "Balassa", "Internet", "Hollywood")): m = re.match(r"^(.*?)([.,:;?!)]*)$", tok) tok = "{" + m.group(1) + "}" + m.group(2) out.append(tok) return " ".join(out) def num(x): if pd.isna(x) or str(x) in ("nan", "None", ""): return None s = str(x); return s[:-2] if s.endswith(".0") else s def authors_of(r): a = r.authors if isinstance(r.authors, str) and r.authors.strip() else r.author a = str(a) if ";" in a: return " and ".join(x.strip() for x in a.split(";") if x.strip()) return a def main(): df = pd.read_csv(LIT / "references_verified.csv"); df = df[df.verified == True].copy() out = [] for _, r in df.sort_values("key").iterrows(): title = r.crossref_title if isinstance(r.crossref_title, str) and r.crossref_title else r.title title = re.sub(r"^Chapter \d+\.\s*", "", title) title = protect_caps(tex(title)) au = authors_of(r); yr = num(r.pub_year) or num(r.year) or "n.d." typ = str(r.type) if isinstance(r.type, str) else "article" f = [f" author = {{{tex(au)}}}", f" title = {{{title}}}", f" year = {{{yr}}}"] doi = r.doi if isinstance(r.doi, str) and r.doi else None url = r.url if isinstance(r.url, str) and r.url else None cont = tex(r.container) if isinstance(r.container, str) else "" if typ in ("journal-article", "article"): kind = "article"; f.append(f" journal = {{{cont}}}") if num(r.volume): f.append(f" volume = {{{num(r.volume)}}}") if num(r.issue): f.append(f" number = {{{num(r.issue)}}}") if num(r.pages): f.append(f" pages = {{{num(r.pages).replace('--', '-').replace('-', '--')}}}") elif typ in ("book-chapter",) or (typ in ("book", "edited-book") and cont): kind = "incollection"; f.append(f" booktitle = {{{cont}}}") if isinstance(r.publisher, str) and r.publisher: f.append(f" publisher = {{{tex(r.publisher)}}}") if num(r.volume): f.append(f" volume = {{{num(r.volume)}}}") if num(r.pages): f.append(f" pages = {{{num(r.pages).replace('--', '-').replace('-', '--')}}}") elif typ in ("book", "monograph", "edited-book"): kind = "book" if isinstance(r.publisher, str) and r.publisher: f.append(f" publisher = {{{tex(r.publisher)}}}") elif typ == "report": kind = "techreport" inst = tex(r.publisher) if isinstance(r.publisher, str) and r.publisher else ("National Bureau of Economic Research" if doi and doi.startswith("10.3386/") else "") if inst: f.append(f" institution = {{{inst}}}") if doi and doi.startswith("10.3386/"): f.append(f" type = {{NBER Working Paper}}"); f.append(f" number = {{{doi.split('/w')[-1]}}}") elif typ == "posted-content": kind = "unpublished"; f.append(" note = {Working paper / preprint}") else: kind = "misc" if doi: f.append(f" doi = {{{doi}}}") if r.verification_method == "url:fetch" and url: f.append(f" url = {{{url}}}") f.append(f" note = {{Accessed {r.verified_date}}}") elif url and not doi: f.append(f" url = {{{url}}}") out.append(f"@{kind}{{{r.key},\n" + ",\n".join(f) + "\n}\n") head = (f"% WP38 bibliography — generated {dt.date.today().isoformat()} by src/literature/make_bib.py from literature/references_verified.csv.\n" "% Every DOI comes from a Crossref response cached in literature/crossref_cache/; URL entries were fetched on the stated date.\n" "% Unverified seeds (literature/unverified.csv) are deliberately absent and must not be cited.\n\n") (LIT / "references.bib").write_text(head + "\n".join(out)) print(f"wrote {len(out)} entries to literature/references.bib") # sanity: parse back import bibtexparser txt = (LIT / "references.bib").read_text() if hasattr(bibtexparser, "parse_string"): # v2 db = bibtexparser.parse_string(txt); entries = [(e.key, e.fields_dict["title"].value) for e in db.entries]; failed = len(db.failed_blocks) else: # v1 db = bibtexparser.loads(txt); entries = [(e["ID"], e["title"]) for e in db.entries]; failed = 0 bad = [k for k, t in entries if "$" in t or re.search(r"(?