"""bib.py — lecteur BibTeX/biblatex minimaliste (assez pour references.bib) + rendu authoryear."""
from __future__ import annotations
import html
import re
from latex2html import find_matching_brace
def _clean(v: str) -> str:
"""LaTeX → texte/HTML léger pour les champs bib."""
v = v.strip()
v = re.sub(r"\\textsuperscript\{([^}]*)\}", r"\1", v)
v = re.sub(r"\\emph\{([^}]*)\}", r"\1", v)
v = re.sub(r"\\textit\{([^}]*)\}", r"\1", v)
v = re.sub(r"\\textbf\{([^}]*)\}", r"\1", v)
v = v.replace("\\&", "&").replace("---", "—").replace("--", "–").replace("~", "\u00a0")
v = v.replace("\\'e", "é").replace("\\`a", "à").replace("\\^o", "ô").replace('\\"e', "ë")
v = re.sub(r"\\url\{([^}]*)\}", r"\1", v)
v = v.replace("{", "").replace("}", "")
v = re.sub(r"\s+", " ", v)
return v
def parse_bib(text: str) -> dict[str, dict]:
entries: dict[str, dict] = {}
pos = 0
while True:
m = re.search(r"@([a-zA-Z]+)\s*\{", text[pos:])
if not m:
break
start = pos + m.end() - 1 # position de '{'
end = find_matching_brace(text, start)
body = text[start + 1:end]
typ = m.group(1).lower()
pos = end + 1
if typ in ("comment", "preamble", "string"):
continue
key, _, rest = body.partition(",")
key = key.strip()
fields: dict[str, str] = {}
i = 0
n = len(rest)
while i < n:
fm = re.match(r"\s*([a-zA-Z_-]+)\s*=\s*", rest[i:])
if not fm:
# avancer jusqu'à la prochaine virgule
j = rest.find(",", i)
if j == -1:
break
i = j + 1
continue
name = fm.group(1).lower()
i += fm.end()
if i >= n:
break
if rest[i] == "{":
j = find_matching_brace(rest, i)
val = rest[i + 1:j]
i = j + 1
elif rest[i] == '"':
j = rest.find('"', i + 1)
val = rest[i + 1:j]
i = j + 1
else:
j = rest.find(",", i)
if j == -1:
j = n
val = rest[i:j]
i = j
fields[name] = val
# virgule
k = rest.find(",", i)
if k == -1:
break
i = k + 1
e = {"key": key, "type": typ, "raw": fields}
e["authors"] = parse_authors(fields.get("author") or fields.get("editor") or fields.get("organization") or "")
e["short"] = short_label(e["authors"])
e["year"] = _clean(fields.get("year") or (fields.get("date") or "")[:4] or "s.d.")
e["title"] = _clean(fields.get("title", ""))
for f in ("publisher", "address", "edition", "organization", "institution", "note", "url", "urldate", "journal", "volume", "number", "pages", "howpublished", "type"):
if f in fields:
e[f] = _clean(fields[f])
entries[key] = e
return entries
def parse_authors(a: str) -> list[dict]:
a = a.strip()
if not a:
return []
out = []
for part in re.split(r"\s+and\s+", a):
part = part.strip()
if not part:
continue
corporate = part.startswith("{") and part.endswith("}")
p = _clean(part)
if corporate or "," not in part and len(p.split()) > 3:
out.append({"last": p, "first": "", "corporate": True})
elif "," in part:
last, first = [x.strip() for x in p.split(",", 1)]
out.append({"last": last, "first": first, "corporate": False})
else:
toks = p.split()
out.append({"last": toks[-1], "first": " ".join(toks[:-1]), "corporate": False})
return out
def short_label(authors: list[dict]) -> str:
if not authors:
return "s.n."
names = [a["last"] for a in authors]
if len(names) == 1:
return names[0]
if len(names) == 2:
return f"{names[0]} et {names[1]}"
return f"{names[0]} et al."
def format_entry(e: dict) -> str:
"""Rendu HTML d'une entrée, style auteur-année."""
auths = e["authors"]
if auths:
parts = []
for a in auths:
if a["corporate"] or not a["first"]:
parts.append(a["last"])
else:
parts.append(f"{a['last']}, {a['first']}")
if len(parts) > 1:
authors_html = ", ".join(parts[:-1]) + " et " + parts[-1]
else:
authors_html = parts[0]
else:
authors_html = "s.n."
year = e["year"]
title = e["title"]
bits = []
if e.get("edition"):
ed = e["edition"]
if re.fullmatch(r"\d+", ed):
ed = ed + "e édition"
bits.append(ed)
pub = " : ".join(x for x in [e.get("address"), e.get("publisher") or e.get("organization") or e.get("institution")] if x)
if pub:
bits.append(pub)
if e.get("journal"):
j = f"{e['journal']}"
if e.get("volume"):
j += f", vol. {e['volume']}"
if e.get("number"):
j += f", no {e['number']}"
if e.get("pages"):
j += f", p. {e['pages']}"
bits.append(j)
if e.get("howpublished"):
bits.append(e["howpublished"])
if e.get("note"):
bits.append(e["note"])
tail = ". ".join(bits)
h = f'{authors_html} ({year}). {title}.'
if tail:
h += f" {tail}."
if e.get("url"):
u = html.escape(e["url"], quote=True)
h += f' {html.escape(e["url"])}'
if e.get("urldate"):
h += f' (consulté le {e["urldate"]})'
return h
if __name__ == "__main__":
import sys
d = parse_bib(open(sys.argv[1], encoding="utf-8").read())
for k, e in d.items():
print(k, "→", e["short"], e["year"])
print(" ", format_entry(e)[:160])