Bibliography assembly + formatting for sources and reviews.
Shared by the MCP export_bibliography tool (project-wide: every source +
citation) and the dashboard's review bibliography route (home sources of a
review's claims). A bibliography entry is a normalized dict::
{key, title, authors, year, venue, doi, doc_type}
and the two emitters — :func:to_bibtex (a .bib file) and
:func:to_formatted (an author-year reference list) — consume exactly that
shape, so the same records render either way. Callers decide which works go in;
this module only normalizes and renders them.
bibtex_key
bibtex_key(raw: str) -> str
Sanitize raw into a valid BibTeX / \cite key.
Folds any run of disallowed characters to a single underscore and trims
leading/trailing separators. Falls back to "ref" for an empty result so
a key is never blank (which BibTeX rejects).
Source code in zettelkasten/bibliography.py
| def bibtex_key(raw: str) -> str:
"""Sanitize ``raw`` into a valid BibTeX / ``\\cite`` key.
Folds any run of disallowed characters to a single underscore and trims
leading/trailing separators. Falls back to ``"ref"`` for an empty result so
a key is never blank (which BibTeX rejects).
"""
key = _KEY_SANITIZE_RE.sub("_", (raw or "").strip())
key = key.strip("_-")
return key or "ref"
|
unique_key
unique_key(base: str, taken: Iterable[str]) -> str
base if free, else base_2 / base_3 / … avoiding taken.
Two distinct sources can sanitize to the same key (e.g. a.b and a/b);
this keeps every .bib entry key — and thus every \cite target —
unambiguous.
Source code in zettelkasten/bibliography.py
| def unique_key(base: str, taken: Iterable[str]) -> str:
"""``base`` if free, else ``base_2`` / ``base_3`` / … avoiding ``taken``.
Two distinct sources can sanitize to the same key (e.g. ``a.b`` and ``a/b``);
this keeps every ``.bib`` entry key — and thus every ``\\cite`` target —
unambiguous.
"""
taken_set = set(taken)
if base not in taken_set:
return base
i = 2
while f"{base}_{i}" in taken_set:
i += 1
return f"{base}_{i}"
|
bib_entry_from_meta
bib_entry_from_meta(key: str, meta: dict, *, fallback_title: str = '') -> dict
Build a normalized bibliography entry from a source/citation metadata dict.
Works for both a source _meta.yaml and a _citations/*.yaml entry —
they share the title/authors/year/venue/doi/doc_type
vocabulary. key is used verbatim as the BibTeX cite key (sanitize it with
:func:bibtex_key first when it comes from an arbitrary id).
Source code in zettelkasten/bibliography.py
| def bib_entry_from_meta(key: str, meta: dict, *, fallback_title: str = "") -> dict:
"""Build a normalized bibliography entry from a source/citation metadata dict.
Works for both a source ``_meta.yaml`` and a ``_citations/*.yaml`` entry —
they share the ``title``/``authors``/``year``/``venue``/``doi``/``doc_type``
vocabulary. ``key`` is used verbatim as the BibTeX cite key (sanitize it with
:func:`bibtex_key` first when it comes from an arbitrary id).
"""
return {
"key": key,
"title": meta.get("title") or fallback_title or key,
"authors": _normalize_authors(meta.get("authors")),
"year": meta.get("year"),
"venue": meta.get("venue") or "",
"doi": meta.get("doi") or "",
"doc_type": meta.get("doc_type") or "",
}
|
to_bibtex
to_bibtex(entries: Iterable[dict]) -> str
Render entries as a BibTeX .bib document.
doc_type selects the entry type (@article / @book / @misc);
a venue maps to journal for articles and publisher otherwise. Missing
authors render as Unknown so every record stays well-formed.
Source code in zettelkasten/bibliography.py
| def to_bibtex(entries: Iterable[dict]) -> str:
"""Render entries as a BibTeX ``.bib`` document.
``doc_type`` selects the entry type (``@article`` / ``@book`` / ``@misc``);
a venue maps to ``journal`` for articles and ``publisher`` otherwise. Missing
authors render as ``Unknown`` so every record stays well-formed.
"""
blocks: list[str] = []
for entry in entries:
doc_type = entry.get("doc_type") or "misc"
bib_type = (
"article" if "article" in doc_type
else "book" if "book" in doc_type
else "misc"
)
authors = entry.get("authors") or []
authors_str = " and ".join(authors) if authors else "Unknown"
bib = f"@{bib_type}{{{entry['key']},\n"
bib += f" title = {{{entry['title']}}},\n"
bib += f" author = {{{authors_str}}},\n"
year = entry.get("year")
if year:
bib += f" year = {{{year}}},\n"
if entry.get("venue"):
field = "journal" if bib_type == "article" else "publisher"
bib += f" {field} = {{{entry['venue']}}},\n"
if entry.get("doi"):
bib += f" doi = {{{entry['doi']}}},\n"
bib += "}"
blocks.append(bib)
return "\n\n".join(blocks)
|
to_formatted(entries: Iterable[dict]) -> str
Render entries as an author-year reference list (one work per line).
Sorted by year then key — the same ordering export_bibliography's
formatted mode has always used.
Source code in zettelkasten/bibliography.py
| def to_formatted(entries: Iterable[dict]) -> str:
"""Render entries as an author-year reference list (one work per line).
Sorted by year then key — the same ordering ``export_bibliography``'s
``formatted`` mode has always used.
"""
rows = sorted(entries, key=lambda e: (e.get("year") or 9999, e["key"]))
lines: list[str] = []
for entry in rows:
authors = ", ".join(entry["authors"]) if entry.get("authors") else "Unknown"
year = f"({entry['year']})" if entry.get("year") else "(n.d.)"
venue = f" {entry['venue']}." if entry.get("venue") else ""
lines.append(f"{authors} {year}. {entry['title']}.{venue}")
return "\n".join(lines)
|
collect_review_sources
collect_review_sources(view: dict, load_meta: Callable[[str], dict]) -> tuple[list[dict], dict[str, str]]
Home-source bibliography for a reconciled review view.
Walks every claim in the view (placed in themes[].claims and the
unplaced worklist), resolves each to its :func:_home_source, and keeps
the deduplicated set of sources that have real metadata. load_meta reads a
source's _meta.yaml (returning {} when absent); a source with neither
title nor authors is treated as a bare stub and skipped.
Returns (entries, key_map):
entries — the de-duplicated normalized entries, ready for
:func:to_bibtex / :func:to_formatted.
key_map — each claim uid mapped to the BibTeX key of its home
source, so the exported markdown can render \cite{key}. A claim whose
home source lacks metadata is simply absent from the map.
Source code in zettelkasten/bibliography.py
| def collect_review_sources(
view: dict,
load_meta: Callable[[str], dict],
) -> tuple[list[dict], dict[str, str]]:
"""Home-source bibliography for a reconciled review view.
Walks every claim in the view (placed in ``themes[].claims`` and the
``unplaced`` worklist), resolves each to its :func:`_home_source`, and keeps
the deduplicated set of sources that have real metadata. ``load_meta`` reads a
source's ``_meta.yaml`` (returning ``{}`` when absent); a source with neither
title nor authors is treated as a bare stub and skipped.
Returns ``(entries, key_map)``:
* ``entries`` — the de-duplicated normalized entries, ready for
:func:`to_bibtex` / :func:`to_formatted`.
* ``key_map`` — each claim ``uid`` mapped to the BibTeX key of its home
source, so the exported markdown can render ``\\cite{key}``. A claim whose
home source lacks metadata is simply absent from the map.
"""
entries: dict[str, dict] = {} # source name -> normalized entry
source_key: dict[str, str] = {} # source name -> assigned bibtex key
key_map: dict[str, str] = {} # claim uid -> bibtex key
def claim_rows() -> Iterable[dict]:
for theme in view.get("themes") or []:
yield from theme.get("claims") or []
yield from view.get("unplaced") or []
for claim in claim_rows():
home = _home_source(claim)
if not home:
continue
if home not in source_key:
meta = load_meta(home) or {}
# A real bibliographic record needs at least a title or authors; a
# bare/empty _meta.yaml (e.g. an unowned stub) is not a citation.
if not (meta.get("title") or meta.get("authors")):
continue
key = unique_key(bibtex_key(home), source_key.values())
source_key[home] = key
entries[home] = bib_entry_from_meta(key, meta, fallback_title=home)
uid = claim.get("uid")
if uid:
key_map[uid] = source_key[home]
return list(entries.values()), key_map
|