Initial commit
Hugo/PaperMod static site tracking 13 conferences and 7 journals for edge and cloud systems research. Includes FullCalendar deadline view, ICORE/SCImago rankings, DBLP paper digest pipeline, and Python fetch/generate scripts. PaperMod added as a git submodule.
This commit is contained in:
606
src/publish_assistant/generate_content.py
Normal file
606
src/publish_assistant/generate_content.py
Normal file
@@ -0,0 +1,606 @@
|
||||
#!/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", {})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markdown body renderers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _conf_body(fm: dict) -> 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 ""
|
||||
event = fm.get("next_event") or "—"
|
||||
domains = ", ".join(f"`{d}`" for d in (fm.get("domain") or []))
|
||||
notes = fm.get("notes") or ""
|
||||
src = fm.get("deadline_source") or ""
|
||||
|
||||
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 += [
|
||||
f"| **Paper deadline** | {dl} |",
|
||||
f"| **Event dates** | {event} |",
|
||||
]
|
||||
if src:
|
||||
lines.append(f"| **Source** | `{src}` |")
|
||||
if notes:
|
||||
lines += ["\n## Notes\n", notes]
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _journal_body(fm: dict) -> 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 ""
|
||||
|
||||
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 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 (Q1–Q4)\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, {})
|
||||
deadline = dl.get("submission_deadline") or dl.get("abstract_deadline") or "TBD"
|
||||
domains = ", ".join(f"`{d}`" for d in conf.get("domain", []))
|
||||
rows.append((_ICORE_ORDER.get(rank, 99), acronym, conf.get("full_name", ""), rank, deadline, domains))
|
||||
rows.sort(key=lambda r: (r[0], r[1]))
|
||||
|
||||
lines = ["\n| Conference | Full name | ICORE | Next deadline | Domains |",
|
||||
"|---|---|---|---|---|"]
|
||||
for _, acronym, full_name, rank, deadline, domains in rows:
|
||||
link = f"[{acronym}](/venues/conferences/{acronym.lower()}/)"
|
||||
lines.append(f"| {link} | {full_name} | **{rank}** | {deadline} | {domains} |")
|
||||
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 "—"
|
||||
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, model, domains))
|
||||
rows.sort(key=lambda r: (r[0], r[1]))
|
||||
|
||||
lines = ["\n| Journal | Full name | SCImago | SJR | Model | Domains |",
|
||||
"|---|---|---|---|---|---|"]
|
||||
for _, acronym, full_name, q, sjr, model, domains in rows:
|
||||
link = f"[{acronym}](/venues/journals/{acronym.lower()}/)"
|
||||
lines.append(f"| {link} | {full_name} | **{q}** | {sjr} | {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()}/"
|
||||
|
||||
for field, label, color in [
|
||||
("abstract_deadline", "abstract deadline", "#f4a261"),
|
||||
("submission_deadline", "paper deadline", "#e63946"),
|
||||
]:
|
||||
iso = _parse_date(dl.get(field) or "")
|
||||
if iso:
|
||||
events.append({"title": f"{acronym} — {label}", "start": iso,
|
||||
"url": url, "color": color, "allDay": True})
|
||||
|
||||
start, end = _parse_date_range(dl.get("event_dates") or "")
|
||||
if start:
|
||||
ev: dict = {"title": acronym, "start": start, "url": url,
|
||||
"color": "#457b9d", "allDay": True}
|
||||
if end:
|
||||
ev["end"] = end
|
||||
events.append(ev)
|
||||
return events
|
||||
|
||||
|
||||
def _calendar_body(venues: dict, icore: dict, deadlines: dict) -> str:
|
||||
from datetime import datetime
|
||||
|
||||
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 # TBD / unparseable goes last
|
||||
|
||||
rows = []
|
||||
for conf in venues.get("conferences") or []:
|
||||
acronym = conf.get("acronym", "")
|
||||
rank = icore.get(acronym.upper(), "—") or "—"
|
||||
dl_data = deadlines.get(acronym, {})
|
||||
deadline = (
|
||||
dl_data.get("submission_deadline")
|
||||
or dl_data.get("abstract_deadline")
|
||||
or "TBD"
|
||||
)
|
||||
event = dl_data.get("event_dates") or "—"
|
||||
rows.append((acronym, deadline, event, rank))
|
||||
|
||||
rows.sort(key=lambda r: (parse_deadline(r[1]), r[0]))
|
||||
|
||||
lines = [
|
||||
"\n| Conference | Submission deadline | Event dates | ICORE |",
|
||||
"|------------|---------------------|-------------|-------|",
|
||||
]
|
||||
for acronym, dl, event, rank in rows:
|
||||
lines.append(f"| [{acronym}](/venues/conferences/{acronym.lower()}/) | {dl} | {event} | {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:
|
||||
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) -> 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("event_dates"):
|
||||
new_fm["next_event"] = dl["event_dates"]
|
||||
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)
|
||||
write_hugo_file(path, fm, _conf_body(fm))
|
||||
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) -> 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)
|
||||
write_hugo_file(path, fm, _journal_body(fm))
|
||||
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 gen_digests(papers_dir: Path, root: Path) -> int:
|
||||
if not papers_dir.exists():
|
||||
return 0
|
||||
count = 0
|
||||
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
|
||||
|
||||
# Load candidate pool for full metadata (authors, DOI, URL, etc.)
|
||||
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
|
||||
|
||||
# Build enriched paper list
|
||||
papers_out = []
|
||||
for sel in digest.get("selected", []):
|
||||
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}"
|
||||
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)
|
||||
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)
|
||||
icore = load_icore(args.icore)
|
||||
scimago = load_scimago(args.scimago)
|
||||
deadlines = load_deadlines(args.deadlines)
|
||||
|
||||
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)
|
||||
|
||||
print("\nGenerating journal pages …", file=sys.stderr)
|
||||
n_jour = gen_journals(venues, scimago, content_root)
|
||||
|
||||
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(Path(args.papers_dir), content_root)
|
||||
|
||||
print(f"\nDone: {n_conf} conferences, {n_jour} journals, {n_dig} digests", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user