Files
publish-assistant/src/publish_assistant/generate_content.py
2026-04-26 12:57:40 +00:00

749 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Generate Hugo content pages from data files.
Each page has a minimal front matter block (for Hugo taxonomy / metadata) plus
a rendered Markdown body so generic themes display content without custom templates.
Reads:
site/data/venues.yaml master venue list
site/data/rankings/icore.csv ICORE conference ranks
site/data/rankings/scimago.csv SCImago journal ranks (optional)
site/data/deadlines.yaml deadline data
site/data/papers/<V>-<Y>-candidates.yaml paper pools
site/data/papers/<V>-<Y>-digest.yaml agent-curated selections
Writes:
site/content/venues/conferences/<acronym>/_index.md
site/content/venues/journals/<acronym>/_index.md
site/content/calendar/_index.md
site/content/digests/<VENUE>-<YEAR>/index.md
PRESERVED_VENUE_FIELDS are never overwritten once set (manual edits survive).
"""
import argparse
import csv
import re
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
import yaml
PRESERVED_VENUE_FIELDS = {"notes", "deadline_source"}
# Fields removed from existing front matter on every regeneration:
# - "url" reserved by Hugo (overrides page URL)
# - "papers" moved from front matter to rendered markdown body
_STRIP_FROM_EXISTING = {"url", "papers"}
# ---------------------------------------------------------------------------
# Front matter I/O
# ---------------------------------------------------------------------------
def read_hugo_file(path: Path) -> tuple[dict, str]:
if not path.exists():
return {}, ""
text = path.read_text(encoding="utf-8")
if text.startswith("---"):
parts = text.split("---", 2)
if len(parts) == 3:
return yaml.safe_load(parts[1]) or {}, parts[2]
return {}, text
def write_hugo_file(path: Path, fm: dict, body: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fm_str = yaml.dump(fm, allow_unicode=True, sort_keys=False, default_flow_style=False)
path.write_text(f"---\n{fm_str}---\n{body}", encoding="utf-8")
def merge_fm(existing: dict, new_data: dict, preserved: set[str]) -> dict:
result = {k: v for k, v in existing.items() if k not in _STRIP_FROM_EXISTING}
for key, value in new_data.items():
if key in preserved and result.get(key):
continue
result[key] = value
return result
# ---------------------------------------------------------------------------
# Data loaders
# ---------------------------------------------------------------------------
def load_icore(path: str) -> dict[str, str]:
p = Path(path)
if not p.exists():
return {}
ranks: dict[str, str] = {}
with open(p, newline="", encoding="utf-8") as f:
sample = f.read(4096); f.seek(0)
sep = ";" if sample.count(";") > sample.count(",") else ","
for row in csv.DictReader(f, delimiter=sep):
acronym = (row.get("Acronym") or row.get("acronym") or "").strip()
rank = (row.get("Rank") or row.get("rank") or row.get("CORE Rank") or "").strip()
if acronym:
ranks[acronym.upper()] = rank
return ranks
def load_scimago(path: str) -> dict[str, dict]:
p = Path(path)
if not p.exists():
return {}
journals: dict[str, dict] = {}
with open(p, newline="", encoding="utf-8") as f:
sample = f.read(4096); f.seek(0)
sep = ";" if sample.count(";") > sample.count(",") else ","
for row in csv.DictReader(f, delimiter=sep):
title = (row.get("Title") or row.get("title") or "").strip().lower()
if title:
journals[title] = {
"scimago_quartile": (row.get("SJR Best Quartile") or row.get("Best Quartile") or "").strip(),
"scimago_sjr": (row.get("SJR") or "").strip(),
"scimago_h_index": (row.get("H index") or row.get("H Index") or "").strip(),
}
return journals
def scimago_lookup(data: dict[str, dict], full_name: str, issn: str) -> dict:
entry = data.get(full_name.lower())
if entry:
return entry
needle = full_name.lower()
for title, entry in data.items():
if needle in title or title in needle:
return entry
return {}
def load_deadlines(path: str) -> dict[str, dict]:
p = Path(path)
if not p.exists():
return {}
with open(p) as f:
data = yaml.safe_load(f) or {}
return data.get("deadlines", {})
def load_venue_digests(papers_dir: Path) -> dict[str, list[dict]]:
"""Return {acronym: [digest_info_dict, ...]} sorted newest-first."""
result: dict[str, list[dict]] = {}
if not papers_dir.exists():
return result
for f in sorted(papers_dir.glob("*-digest.yaml"), reverse=True):
with open(f, encoding="utf-8") as fp:
d = yaml.safe_load(fp) or {}
venue = d.get("venue", "")
year = d.get("year")
if venue and year:
result.setdefault(venue, []).append({
"year": int(year),
"slug": f"{venue}-{year}",
"paper_count": len(d.get("selected") or d.get("papers") or []),
"tags": d.get("tags", []),
"date": str(d.get("date", f"{year}-01-01")),
})
return result
# ---------------------------------------------------------------------------
# Markdown body renderers
# ---------------------------------------------------------------------------
def _conf_body(fm: dict, digests: list[dict] | None = None) -> str:
rank = fm.get("icore_rank") or ""
hp = fm.get("homepage") or ""
dl = fm.get("next_deadline") or "TBD"
abstract_dl = fm.get("abstract_deadline") or ""
notification = fm.get("notification") or ""
camera_ready = fm.get("camera_ready") or ""
event = fm.get("next_event") or ""
location = fm.get("location") or ""
cfp_url = fm.get("cfp_url") or ""
domains = ", ".join(f"`{d}`" for d in (fm.get("domain") or []))
notes = fm.get("notes") or ""
src = fm.get("deadline_source") or ""
title = fm.get("title", "")
lines = [f"\n*{fm.get('full_name', '')}*\n"]
lines += [
"| | |", "|---|---|",
f"| **ICORE Rank** | **{rank}** |",
f"| **Domains** | {domains} |",
]
if hp:
lines.append(f"| **Website** | [{hp}]({hp}) |")
lines += [
"\n## Upcoming Deadline\n",
"| | |", "|---|---|",
]
if abstract_dl and abstract_dl != dl:
lines.append(f"| **Abstract deadline** | {abstract_dl} |")
lines.append(f"| **Paper deadline** | {dl} |")
if notification:
lines.append(f"| **Notification** | {notification} |")
if camera_ready:
lines.append(f"| **Camera-ready** | {camera_ready} |")
lines.append(f"| **Event dates** | {event} |")
if location:
lines.append(f"| **Location** | {location} |")
if cfp_url:
lines.append(f"| **CFP** | [{cfp_url}]({cfp_url}) |")
if src:
lines.append(f"| **Source** | `{src}` |")
if digests:
lines += ["\n## Digests\n"]
for d in sorted(digests, key=lambda x: x["year"], reverse=True):
slug = d["slug"].lower()
year = d["year"]
count = d.get("paper_count", 0)
suffix = f"{count} papers" if count else "*(digest pending)*"
lines.append(f"- [{title} {year}](/digests/{slug}/) {suffix}")
if notes:
lines += ["\n## Notes\n", notes]
return "\n".join(lines) + "\n"
def _journal_body(fm: dict, digests: list[dict] | None = None) -> str:
q = fm.get("scimago_quartile") or ""
sjr = fm.get("scimago_sjr") or ""
h = fm.get("scimago_h_index") or ""
issn = fm.get("issn") or ""
hp = fm.get("homepage") or ""
model = (fm.get("submission_model") or "rolling").title()
domains = ", ".join(f"`{d}`" for d in (fm.get("domain") or []))
notes = fm.get("notes") or ""
title = fm.get("title", "")
lines = [f"\n*{fm.get('full_name', '')}*\n"]
lines += [
"| | |", "|---|---|",
f"| **SCImago Quartile** | **{q}** |",
f"| **SJR** | {sjr} |",
f"| **H-Index** | {h} |",
f"| **ISSN** | {issn} |",
f"| **Submission model** | {model} |",
f"| **Domains** | {domains} |",
]
if hp:
lines.append(f"| **Website** | [{hp}]({hp}) |")
if digests:
lines += ["\n## Digests\n"]
for d in sorted(digests, key=lambda x: x["year"], reverse=True):
slug = d["slug"].lower()
year = d["year"]
count = d.get("paper_count", 0)
suffix = f"{count} papers" if count else "*(digest pending)*"
lines.append(f"- [{title} {year}](/digests/{slug}/) {suffix}")
if notes:
lines += ["\n## Notes\n", notes]
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Date helpers
# ---------------------------------------------------------------------------
def _parse_date(s: str) -> str | None:
"""'Apr 1, 2026''2026-04-01', or None if unparseable."""
for fmt in ("%b %d, %Y", "%B %d, %Y"):
try:
return datetime.strptime(s.strip(), fmt).strftime("%Y-%m-%d")
except ValueError:
pass
return None
def _parse_date_range(s: str) -> tuple[str | None, str | None]:
"""Return (start_iso, end_iso_exclusive) from a date-range string.
Handles:
'Apr 13, 2026 - Apr 16, 2026' → ('2026-04-13', '2026-04-17')
'Nov 17-19, 2026' → ('2026-11-17', '2026-11-20')
'Apr 1, 2026' → ('2026-04-01', None)
"""
s = s.strip()
if " - " in s:
left, right = s.split(" - ", 1)
start = _parse_date(left)
end_s = _parse_date(right)
if start and end_s:
end_dt = datetime.strptime(end_s, "%Y-%m-%d") + timedelta(days=1)
return start, end_dt.strftime("%Y-%m-%d")
m = re.match(r"(\w+)\s+(\d+)-(\d+),\s+(\d{4})", s)
if m:
month, d1, d2, year = m.groups()
start = _parse_date(f"{month} {d1}, {year}")
try:
end_dt = datetime.strptime(f"{month} {d2}, {year}", "%b %d, %Y") + timedelta(days=1)
if start:
return start, end_dt.strftime("%Y-%m-%d")
except ValueError:
pass
return _parse_date(s), None
# ---------------------------------------------------------------------------
# Venue section index body renderers
# ---------------------------------------------------------------------------
_ICORE_ORDER = {"A*": 0, "A": 1, "B": 2, "C": 3}
_QUARTILE_ORDER = {"Q1": 0, "Q2": 1, "Q3": 2, "Q4": 3}
def _venues_body(venues: dict) -> str:
n_conf = len(venues.get("conferences") or [])
n_jour = len(venues.get("journals") or [])
return (
f"\nTracking **{n_conf} conferences** and **{n_jour} journals** for this domain.\n\n"
"- **[Conferences →](/venues/conferences/)** ranked by ICORE (A*, A, B, C)\n"
"- **[Journals →](/venues/journals/)** ranked by SCImago quartile (Q1Q4)\n"
)
def _conferences_section_body(venues: dict, icore: dict, deadlines: dict) -> str:
rows = []
for conf in venues.get("conferences") or []:
acronym = conf.get("acronym", "")
rank = icore.get(acronym.upper(), "") or ""
dl = deadlines.get(acronym, {})
abstract = dl.get("abstract_deadline") or ""
paper = dl.get("submission_deadline") or dl.get("abstract_deadline") or "TBD"
notif = dl.get("notification") or ""
event = dl.get("event_dates") or ""
location = dl.get("location") or ""
rows.append((_ICORE_ORDER.get(rank, 99), acronym, conf.get("full_name", ""),
rank, abstract, paper, notif, event, location))
rows.sort(key=lambda r: (r[0], r[1]))
lines = [
"\n| Conference | Full name | ICORE | Abstract | Paper deadline | Notification | Event | Location |",
"|---|---|---|---|---|---|---|---|",
]
for _, acronym, full_name, rank, abstract, paper, notif, event, location in rows:
link = f"[{acronym}](/venues/conferences/{acronym.lower()}/)"
abs_cell = abstract if (abstract and abstract != paper) else ""
lines.append(f"| {link} | {full_name} | **{rank}** | {abs_cell} | {paper} | {notif} | {event} | {location} |")
return "\n".join(lines) + "\n"
def _journals_section_body(venues: dict) -> str:
rows = []
for j in venues.get("journals") or []:
acronym = j.get("acronym", "")
q = j.get("scimago_quartile") or ""
sjr = j.get("scimago_sjr") or ""
h = j.get("scimago_h_index") or ""
model = (j.get("submission_model") or "rolling").title()
domains = ", ".join(f"`{d}`" for d in j.get("domain", []))
rows.append((_QUARTILE_ORDER.get(q, 99), acronym, j.get("full_name", ""), q, sjr, h, model, domains))
rows.sort(key=lambda r: (r[0], r[1]))
lines = [
"\n| Journal | Full name | SCImago | SJR | H-Index | Model | Domains |",
"|---|---|---|---|---|---|---|",
]
for _, acronym, full_name, q, sjr, h, model, domains in rows:
link = f"[{acronym}](/venues/journals/{acronym.lower()}/)"
lines.append(f"| {link} | {full_name} | **{q}** | {sjr} | {h} | {model} | {domains} |")
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Calendar events builder (for FullCalendar)
# ---------------------------------------------------------------------------
def _calendar_events(venues: dict, deadlines: dict) -> list[dict]:
events: list[dict] = []
for conf in venues.get("conferences") or []:
acronym = conf.get("acronym", "")
dl = deadlines.get(acronym, {})
url = f"/venues/conferences/{acronym.lower()}/"
location = dl.get("location") or ""
cfp_url = dl.get("cfp_url") or ""
for field, label, color in [
("abstract_deadline", "abstract deadline", "#f4a261"),
("submission_deadline", "paper deadline", "#e63946"),
("notification", "notification", "#2a9d8f"),
]:
iso = _parse_date(dl.get(field) or "")
if iso:
ev: dict = {"title": f"{acronym}{label}", "start": iso,
"url": url, "color": color, "allDay": True}
props: dict = {}
if location:
props["location"] = location
if cfp_url:
props["cfp_url"] = cfp_url
if props:
ev["extendedProps"] = props
events.append(ev)
start, end = _parse_date_range(dl.get("event_dates") or "")
if start:
ev = {"title": acronym, "start": start, "url": url,
"color": "#457b9d", "allDay": True}
if end:
ev["end"] = end
if location:
ev["extendedProps"] = {"location": location}
events.append(ev)
return events
def _calendar_body(venues: dict, icore: dict, deadlines: dict) -> str:
def parse_deadline(s: str):
for fmt in ("%b %d, %Y", "%b %d %Y"):
try:
return datetime.strptime(s.strip(), fmt)
except ValueError:
pass
return datetime.max
rows = []
for conf in venues.get("conferences") or []:
acronym = conf.get("acronym", "")
rank = icore.get(acronym.upper(), "") or ""
dl_data = deadlines.get(acronym, {})
abstract = dl_data.get("abstract_deadline") or ""
paper = dl_data.get("submission_deadline") or dl_data.get("abstract_deadline") or "TBD"
notif = dl_data.get("notification") or ""
event = dl_data.get("event_dates") or ""
location = dl_data.get("location") or ""
rows.append((acronym, abstract, paper, notif, event, location, rank))
rows.sort(key=lambda r: (parse_deadline(r[2]), r[0]))
lines = [
"\n| Conference | Abstract | Paper deadline | Notification | Event dates | Location | ICORE |",
"|---|---|---|---|---|---|---|",
]
for acronym, abstract, paper, notif, event, location, rank in rows:
abs_cell = abstract if (abstract and abstract != paper) else ""
lines.append(
f"| [{acronym}](/venues/conferences/{acronym.lower()}/) "
f"| {abs_cell} | {paper} | {notif} | {event} | {location} | {rank} |"
)
lines += [
"",
"> Deadlines sourced from conference websites and [WikiCFP](http://www.wikicfp.com/).",
"",
]
return "\n".join(lines) + "\n"
def _digest_body(papers: list[dict]) -> str:
if not papers:
return "\nNo papers selected yet.\n"
lines = [f"\n{len(papers)} papers selected.\n"]
for p in papers:
title = p.get("title") or "Untitled"
authors = p.get("authors") or []
author_str = (
", ".join(authors[:4]) + (" *et al.*" if len(authors) > 4 else "")
if authors else ""
)
tldr = p.get("tldr") or ""
why = p.get("why_notable") or ""
link = p.get("url") or (f"https://doi.org/{p['doi']}" if p.get("doi") else "")
lines += ["---", f"\n### {title}\n"]
if author_str:
lines.append(f"*{author_str}*\n")
if tldr:
lines.append(f"**TL;DR** — {tldr}\n")
if why:
lines.append(f"**Why notable** — {why}\n")
if link:
lines.append(f"[→ Read paper]({link})\n")
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Content generators
# ---------------------------------------------------------------------------
def gen_conferences(venues: dict, icore: dict, deadlines: dict, root: Path,
venue_digests: dict[str, list[dict]] | None = None) -> int:
count = 0
for conf in venues.get("conferences") or []:
acronym = conf.get("acronym", "").strip()
if not acronym:
continue
path = root / "venues" / "conferences" / acronym / "_index.md"
existing_fm, _ = read_hugo_file(path)
new_fm: dict = {
"title": acronym,
"full_name": conf.get("full_name", ""),
"domain": conf.get("domain", []),
"type": "conference",
"icore_rank": icore.get(acronym.upper(), ""),
"homepage": conf.get("url", ""),
"dblp_key": conf.get("dblp_key", ""),
"draft": False,
}
dl = deadlines.get(acronym, {})
if dl.get("submission_deadline"):
new_fm["next_deadline"] = dl["submission_deadline"]
elif dl.get("abstract_deadline"):
new_fm["next_deadline"] = dl["abstract_deadline"]
if dl.get("abstract_deadline") and dl.get("submission_deadline"):
new_fm["abstract_deadline"] = dl["abstract_deadline"]
if dl.get("notification"):
new_fm["notification"] = dl["notification"]
if dl.get("camera_ready"):
new_fm["camera_ready"] = dl["camera_ready"]
if dl.get("event_dates"):
new_fm["next_event"] = dl["event_dates"]
if dl.get("location"):
new_fm["location"] = dl["location"]
if dl.get("cfp_url"):
new_fm["cfp_url"] = dl["cfp_url"]
if dl.get("wikicfp_event_id"):
new_fm["deadline_source"] = f"wikicfp:{dl['wikicfp_event_id']}"
fm = merge_fm(existing_fm, new_fm, PRESERVED_VENUE_FIELDS)
digests = (venue_digests or {}).get(acronym)
write_hugo_file(path, fm, _conf_body(fm, digests))
count += 1
print(f" {path.relative_to(root.parent)}", file=sys.stderr)
return count
def gen_journals(venues: dict, scimago: dict[str, dict], root: Path,
venue_digests: dict[str, list[dict]] | None = None) -> int:
count = 0
for journal in venues.get("journals") or []:
acronym = journal.get("acronym", "").strip()
if not acronym:
continue
path = root / "venues" / "journals" / acronym / "_index.md"
existing_fm, _ = read_hugo_file(path)
inline_sjr = {
k: journal[k]
for k in ("scimago_quartile", "scimago_sjr", "scimago_h_index")
if journal.get(k)
}
sjr_data = inline_sjr or scimago_lookup(scimago, journal.get("full_name", ""), journal.get("issn", ""))
new_fm: dict = {
"title": acronym,
"full_name": journal.get("full_name", ""),
"domain": journal.get("domain", []),
"type": "journal",
"issn": journal.get("issn", ""),
"homepage": journal.get("url", ""),
"submission_model": journal.get("submission_model", "rolling"),
"dblp_key": journal.get("dblp_key", ""),
"draft": False,
**sjr_data,
}
fm = merge_fm(existing_fm, new_fm, PRESERVED_VENUE_FIELDS)
digests = (venue_digests or {}).get(acronym)
write_hugo_file(path, fm, _journal_body(fm, digests))
count += 1
print(f" {path.relative_to(root.parent)}", file=sys.stderr)
return count
def gen_calendar(venues: dict, icore: dict, deadlines: dict, root: Path) -> None:
path = root / "calendar" / "_index.md"
existing_fm, _ = read_hugo_file(path)
new_fm = {
"title": "Submission Deadlines",
"layout": "calendar",
"generated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"events": _calendar_events(venues, deadlines),
"draft": False,
}
fm = merge_fm(existing_fm, new_fm, set())
write_hugo_file(path, fm, _calendar_body(venues, icore, deadlines))
print(f" {path.relative_to(root.parent)}", file=sys.stderr)
def gen_section_indexes(venues: dict, icore: dict, deadlines: dict, root: Path) -> None:
# venues/_index.md
p = root / "venues" / "_index.md"
fm = merge_fm(read_hugo_file(p)[0], {"title": "Venues", "draft": False}, set())
write_hugo_file(p, fm, _venues_body(venues))
print(f" {p.relative_to(root.parent)}", file=sys.stderr)
# venues/conferences/_index.md
p = root / "venues" / "conferences" / "_index.md"
n = len(venues.get("conferences") or [])
fm = merge_fm(read_hugo_file(p)[0],
{"title": "Conferences", "description": f"{n} tracked conferences", "draft": False}, set())
write_hugo_file(p, fm, _conferences_section_body(venues, icore, deadlines))
print(f" {p.relative_to(root.parent)}", file=sys.stderr)
# venues/journals/_index.md
p = root / "venues" / "journals" / "_index.md"
n = len(venues.get("journals") or [])
fm = merge_fm(read_hugo_file(p)[0],
{"title": "Journals", "description": f"{n} tracked journals", "draft": False}, set())
write_hugo_file(p, fm, _journals_section_body(venues))
print(f" {p.relative_to(root.parent)}", file=sys.stderr)
def _upcoming_year(acronym: str, deadlines: dict) -> int:
"""Extract the year of the upcoming event from deadlines, falling back to current year."""
event_dates = (deadlines.get(acronym) or {}).get("event_dates") or ""
m = re.search(r'\b(20\d\d)\b', event_dates)
return int(m.group(1)) if m else datetime.now().year
def gen_digests(papers_dir: Path, root: Path,
venues: dict | None = None, deadlines: dict | None = None) -> int:
real_slugs: set[str] = set()
count = 0
if papers_dir.exists():
for digest_file in sorted(papers_dir.glob("*-digest.yaml")):
with open(digest_file, encoding="utf-8") as f:
digest = yaml.safe_load(f) or {}
venue = digest.get("venue", "")
year = digest.get("year")
if not venue or not year:
print(f" Skipping {digest_file.name}: missing venue or year", file=sys.stderr)
continue
candidates: dict[str, dict] = {}
candidate_file = papers_dir / f"{venue}-{year}-candidates.yaml"
if candidate_file.exists():
with open(candidate_file, encoding="utf-8") as f:
pool = yaml.safe_load(f) or {}
for p in pool.get("papers", []):
key = p.get("dblp_key") or p.get("openalex_id") or p.get("doi") or p.get("title", "")
candidates[key] = p
items = digest.get("selected") or []
if not items:
for p in digest.get("papers") or []:
items.append({
"title": p.get("title", ""),
"authors": p.get("authors", []),
"tldr": p.get("reason", ""),
"why_notable": "",
})
papers_out = []
for sel in items:
key = sel.get("dblp_key") or sel.get("openalex_id") or sel.get("doi") or sel.get("title", "")
papers_out.append({**candidates.get(key, {}), **sel})
slug = f"{venue}-{year}"
real_slugs.add(slug)
path = root / "digests" / slug / "index.md"
existing_fm, _ = read_hugo_file(path)
new_fm = {
"title": f"{venue} {year} Digest",
"venue": venue,
"year": int(year),
"date": str(digest.get("date", f"{year}-01-01")),
"tags": digest.get("tags", []),
"paper_count": len(papers_out),
"draft": False,
}
fm = merge_fm(existing_fm, new_fm, set())
write_hugo_file(path, fm, _digest_body(papers_out))
count += 1
print(f" {path.relative_to(root.parent)}", file=sys.stderr)
# Generate stubs for every venue that has no digest for its upcoming edition
if venues and deadlines is not None:
all_acronyms = (
[c.get("acronym", "") for c in (venues.get("conferences") or [])] +
[j.get("acronym", "") for j in (venues.get("journals") or [])]
)
for acronym in all_acronyms:
if not acronym:
continue
year = _upcoming_year(acronym, deadlines)
slug = f"{acronym}-{year}"
if slug in real_slugs:
continue
path = root / "digests" / slug / "index.md"
if path.exists():
continue # never overwrite existing stubs (may have been customised)
new_fm = {
"title": f"{acronym} {year} Digest",
"venue": acronym,
"year": year,
"date": f"{year}-01-01",
"tags": [],
"paper_count": 0,
"draft": False,
"build": {"list": "never"},
}
write_hugo_file(path, new_fm, _digest_body([]))
count += 1
print(f" {path.relative_to(root.parent)} (stub)", file=sys.stderr)
return count
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
ap = argparse.ArgumentParser(description="Generate Hugo content from data files.")
ap.add_argument("--venues", default="site/data/venues.yaml")
ap.add_argument("--icore", default="site/data/rankings/icore.csv")
ap.add_argument("--scimago", default="site/data/rankings/scimago.csv")
ap.add_argument("--deadlines", default="site/data/deadlines.yaml")
ap.add_argument("--papers-dir", default="site/data/papers", dest="papers_dir")
ap.add_argument("--content", default="site/content")
args = ap.parse_args()
venues_path = Path(args.venues)
if not venues_path.exists():
print(f"Error: {venues_path} not found. Populate site/data/venues.yaml first.", file=sys.stderr)
sys.exit(1)
with open(venues_path, encoding="utf-8") as f:
venues = yaml.safe_load(f) or {}
content_root = Path(args.content)
papers_dir = Path(args.papers_dir)
icore = load_icore(args.icore)
scimago = load_scimago(args.scimago)
deadlines = load_deadlines(args.deadlines)
venue_digests = load_venue_digests(papers_dir)
if icore: print(f"Loaded {len(icore)} ICORE entries", file=sys.stderr)
if scimago: print(f"Loaded {len(scimago)} SCImago entries", file=sys.stderr)
if deadlines: print(f"Loaded deadlines for {len(deadlines)} venues", file=sys.stderr)
print("\nGenerating venue section indexes …", file=sys.stderr)
gen_section_indexes(venues, icore, deadlines, content_root)
print("\nGenerating conference pages …", file=sys.stderr)
n_conf = gen_conferences(venues, icore, deadlines, content_root, venue_digests)
print("\nGenerating journal pages …", file=sys.stderr)
n_jour = gen_journals(venues, scimago, content_root, venue_digests)
print("\nGenerating calendar page …", file=sys.stderr)
gen_calendar(venues, icore, deadlines, content_root)
print("\nGenerating digest pages …", file=sys.stderr)
n_dig = gen_digests(papers_dir, content_root, venues, deadlines)
print(f"\nDone: {n_conf} conferences, {n_jour} journals, {n_dig} digests", file=sys.stderr)
if __name__ == "__main__":
main()