#!/usr/bin/env python3 """Convert docs/pisg-doc.xml (DocBook subset) into a single self-contained HTML page. Usage: python3 docs/xml2html.py [input.xml] [output.html] Defaults: docs/pisg-doc.xml -> docs/pisg-doc.html """ import html import re import sys import textwrap import urllib.parse import xml.etree.ElementTree as ET from pathlib import Path here = Path(__file__).resolve().parent src = Path(sys.argv[1]) if len(sys.argv) > 1 else here / "pisg-doc.xml" dst = Path(sys.argv[2]) if len(sys.argv) > 2 else here / "pisg-doc.html" raw = src.read_text(encoding="utf-8", errors="replace") # The file starts with a comment before the XML declaration and references a # DTD we don't have; drop both so a plain XML parser accepts it. raw = re.sub(r"^\s*\s*", "", raw, count=1, flags=re.S) raw = re.sub(r"<\?xml[^>]*\?>", "", raw, count=1) raw = re.sub(r"", "", raw, count=1, flags=re.S) root = ET.fromstring(raw) esc = html.escape # id -> display text, used to resolve labels = {} for el in root.iter(): i = el.get("id") if not i: continue if el.tag == "refentry": labels[i] = el.findtext("refnamediv/refname") or i else: labels[i] = (el.findtext("title") or i).strip() chapter_no = 0 def inline(el): """Render an element's mixed content (text + children + tails).""" out = [esc(el.text or "")] for ch in el: out.append(node(ch)) out.append(esc(ch.tail or "")) return "".join(out) def node(el): global chapter_no t = el.tag if t == "para": return "

" + inline(el).strip() + "

\n" if t in ("programlisting", "screen"): text = "".join(el.itertext()).strip("\n") text = re.sub(r"^\s*\n", "", text) return "
" + esc(textwrap.dedent(text).rstrip()) + "
\n" if t == "itemizedlist": return "\n" if t == "listitem": body = "".join(node(c) for c in el) return "
  • " + body + "
  • \n" if t == "xref": i = el.get("linkend", "") return f'{esc(labels.get(i, i))}' if t == "link": i = el.get("linkend") if i: return f'{inline(el)}' return inline(el) if t == "ulink": return f'{inline(el).strip()}' if t in ("command", "filename", "userinput", "prompt"): return "" + inline(el) + "" if t == "emphasis": return "" + inline(el) + "" if t == "chapter": chapter_no += 1 title = el.findtext("title") or "" body = "".join(node(c) for c in el if c.tag != "title") return (f'
    ' f"

    {chapter_no}. {esc(title.strip())}

    \n{body}
    \n") if t == "sect1": title = el.findtext("title") or "" body = "".join(node(c) for c in el if c.tag != "title") return (f'
    ' f"

    {esc(title.strip())}

    \n{body}
    \n") if t == "refentry": name = el.findtext("refnamediv/refname") or "" purpose = el.findtext("refnamediv/refpurpose") or "" parts = [f'
    ', f"

    {esc(name.strip())}" f' {esc(" ".join(purpose.split()))}

    \n'] syn = el.find("refsynopsisdiv") if syn is not None: parts.append("".join(node(c) for c in syn)) for rs in el.findall("refsect1"): parts.append(node(rs)) parts.append("
    \n") return "".join(parts) if t == "refsect1": title = el.findtext("title") or "" body = "".join(node(c) for c in el if c.tag != "title") return f"

    {esc(title.strip())}

    \n{body}" if t in ("toc", "title", "subtitle", "bookinfo"): return "" # Unknown element: keep its text so nothing silently disappears. return inline(el) def toc(): rows = [] n = 0 for ch in root.findall("chapter"): n += 1 rows.append(f'
  • {n}. {esc(ch.findtext("title").strip())}') subs = [(s.get("id"), (s.findtext("title") or "").strip()) for s in ch.findall("sect1")] subs += [(r.get("id"), (r.findtext("refnamediv/refname") or "").strip()) for r in ch.findall("refentry")] if subs: if len(subs) > 12: # long option lists: compact inline index rows.append('
    ' + " ".join( f'{esc(s)}' for i, s in subs) + "
    ") else: rows.append("") rows.append("
  • ") rows.append(example_toc) return "" body = "".join(node(c) for c in root if c.tag == "chapter") # numbers the chapters first # Final chapter: the complete example configuration, straight from pisg.cfg.example # (regenerate that with docs/gen-example-config.py). Skipped if the file is absent. example_file = here.parent / "pisg.cfg.example" example_html = "" example_toc = "" if example_file.exists(): cfg_text = example_file.read_text(encoding="utf-8") chapter_no += 1 download = "data:text/plain;charset=utf-8," + urllib.parse.quote(cfg_text) example_toc = (f'
  • {chapter_no}. Complete example configuration
  • ') example_html = f"""

    {chapter_no}. Complete example configuration

    A ready-to-use pisg.cfg for a channel called #example. It lists every option pisg understands, with what it does, so you can copy it and change only the lines marked EDIT: the log file, its format, the network name, the output file and your name. Options are at pisg's own defaults unless the comment says recommended.

    1. Copy the file below (or download it) and save it as pisg.cfg next to pisg.
    2. Change the EDIT values in the <channel> block.
    3. Run ./pisg.
    pisg.cfg Download
    {esc(cfg_text)}
    """ title = root.findtext("bookinfo/title") or "pisg documentation" subtitle = root.findtext("bookinfo/subtitle") or "" body += example_html page = f""" {esc(title)}

    {esc(title)}

    {esc(subtitle)}

    {body}
    """ dst.write_text(page, encoding="utf-8") print(f"wrote {dst} ({len(page)//1024} KiB, {chapter_no} chapters)")