spb/uqo-imm1003
Public
JavaScript 68%
CSS 32%
1"""bib.py — lecteur BibTeX/biblatex minimaliste (assez pour references.bib) + rendu authoryear."""2from __future__ import annotations34import html5import re67from latex2html import find_matching_brace8910def _clean(v: str) -> str:11 """LaTeX → texte/HTML léger pour les champs bib."""12 v = v.strip()13 v = re.sub(r"\\textsuperscript\{([^}]*)\}", r"<sup>\1</sup>", v)14 v = re.sub(r"\\emph\{([^}]*)\}", r"<em>\1</em>", v)15 v = re.sub(r"\\textit\{([^}]*)\}", r"<em>\1</em>", v)16 v = re.sub(r"\\textbf\{([^}]*)\}", r"<strong>\1</strong>", v)17 v = v.replace("\\&", "&").replace("---", "—").replace("--", "–").replace("~", "\u00a0")18 v = v.replace("\\'e", "é").replace("\\`a", "à").replace("\\^o", "ô").replace('\\"e', "ë")19 v = re.sub(r"\\url\{([^}]*)\}", r"\1", v)20 v = v.replace("{", "").replace("}", "")21 v = re.sub(r"\s+", " ", v)22 return v232425def parse_bib(text: str) -> dict[str, dict]:26 entries: dict[str, dict] = {}27 pos = 028 while True:29 m = re.search(r"@([a-zA-Z]+)\s*\{", text[pos:])30 if not m:31 break32 start = pos + m.end() - 1 # position de '{'33 end = find_matching_brace(text, start)34 body = text[start + 1:end]35 typ = m.group(1).lower()36 pos = end + 137 if typ in ("comment", "preamble", "string"):38 continue39 key, _, rest = body.partition(",")40 key = key.strip()41 fields: dict[str, str] = {}42 i = 043 n = len(rest)44 while i < n:45 fm = re.match(r"\s*([a-zA-Z_-]+)\s*=\s*", rest[i:])46 if not fm:47 # avancer jusqu'à la prochaine virgule48 j = rest.find(",", i)49 if j == -1:50 break51 i = j + 152 continue53 name = fm.group(1).lower()54 i += fm.end()55 if i >= n:56 break57 if rest[i] == "{":58 j = find_matching_brace(rest, i)59 val = rest[i + 1:j]60 i = j + 161 elif rest[i] == '"':62 j = rest.find('"', i + 1)63 val = rest[i + 1:j]64 i = j + 165 else:66 j = rest.find(",", i)67 if j == -1:68 j = n69 val = rest[i:j]70 i = j71 fields[name] = val72 # virgule73 k = rest.find(",", i)74 if k == -1:75 break76 i = k + 177 e = {"key": key, "type": typ, "raw": fields}78 e["authors"] = parse_authors(fields.get("author") or fields.get("editor") or fields.get("organization") or "")79 e["short"] = short_label(e["authors"])80 e["year"] = _clean(fields.get("year") or (fields.get("date") or "")[:4] or "s.d.")81 e["title"] = _clean(fields.get("title", ""))82 for f in ("publisher", "address", "edition", "organization", "institution", "note", "url", "urldate", "journal", "volume", "number", "pages", "howpublished", "type"):83 if f in fields:84 e[f] = _clean(fields[f])85 entries[key] = e86 return entries878889def parse_authors(a: str) -> list[dict]:90 a = a.strip()91 if not a:92 return []93 out = []94 for part in re.split(r"\s+and\s+", a):95 part = part.strip()96 if not part:97 continue98 corporate = part.startswith("{") and part.endswith("}")99 p = _clean(part)100 if corporate or "," not in part and len(p.split()) > 3:101 out.append({"last": p, "first": "", "corporate": True})102 elif "," in part:103 last, first = [x.strip() for x in p.split(",", 1)]104 out.append({"last": last, "first": first, "corporate": False})105 else:106 toks = p.split()107 out.append({"last": toks[-1], "first": " ".join(toks[:-1]), "corporate": False})108 return out109110111def short_label(authors: list[dict]) -> str:112 if not authors:113 return "s.n."114 names = [a["last"] for a in authors]115 if len(names) == 1:116 return names[0]117 if len(names) == 2:118 return f"{names[0]} et {names[1]}"119 return f"{names[0]} et al."120121122def format_entry(e: dict) -> str:123 """Rendu HTML d'une entrée, style auteur-année."""124 auths = e["authors"]125 if auths:126 parts = []127 for a in auths:128 if a["corporate"] or not a["first"]:129 parts.append(a["last"])130 else:131 parts.append(f"{a['last']}, {a['first']}")132 if len(parts) > 1:133 authors_html = ", ".join(parts[:-1]) + " et " + parts[-1]134 else:135 authors_html = parts[0]136 else:137 authors_html = "s.n."138 year = e["year"]139 title = e["title"]140 bits = []141 if e.get("edition"):142 ed = e["edition"]143 if re.fullmatch(r"\d+", ed):144 ed = ed + "<sup>e</sup> édition"145 bits.append(ed)146 pub = " : ".join(x for x in [e.get("address"), e.get("publisher") or e.get("organization") or e.get("institution")] if x)147 if pub:148 bits.append(pub)149 if e.get("journal"):150 j = f"<em>{e['journal']}</em>"151 if e.get("volume"):152 j += f", vol. {e['volume']}"153 if e.get("number"):154 j += f", n<sup>o</sup> {e['number']}"155 if e.get("pages"):156 j += f", p. {e['pages']}"157 bits.append(j)158 if e.get("howpublished"):159 bits.append(e["howpublished"])160 if e.get("note"):161 bits.append(e["note"])162 tail = ". ".join(bits)163 h = f'<span class="bib-auth">{authors_html}</span> <span class="bib-year">({year})</span>. <em class="bib-title">{title}</em>.'164 if tail:165 h += f" {tail}."166 if e.get("url"):167 u = html.escape(e["url"], quote=True)168 h += f' <a class="bib-url" href="{u}" target="_blank" rel="noopener">{html.escape(e["url"])}</a>'169 if e.get("urldate"):170 h += f' <span class="bib-urldate">(consulté le {e["urldate"]})</span>'171 return h172173174if __name__ == "__main__":175 import sys176 d = parse_bib(open(sys.argv[1], encoding="utf-8").read())177 for k, e in d.items():178 print(k, "→", e["short"], e["year"])179 print(" ", format_entry(e)[:160])180