#!/usr/bin/env python3 """Publish a .drawio as a single interactive HTML viewer. Exports every page to SVG via the draw.io CLI and inlines them into ONE self-contained .html with pan (drag), zoom (wheel / buttons), page tabs, node search, and working links — external links open normally and internal page links ("data:page/id,…", e.g. a C4 model's drill-down) switch tabs inside the viewer. Share the file with anyone: no draw.io, no server, no external requests. python3 drawiohtml.py architecture.drawio -o architecture.html python3 drawiohtml.py c4.drawio # -> c4.html, drill-down works Search matches node text (draw.io wraps every cell in ); matches glow, Enter cycles through them and centres each. Internal page links survive export by being rewritten to "#page-" fragments first (draw.io drops raw data:page/id links from SVG). Usage: python3 drawiohtml.py [-o out.html] """ import argparse import html import json import os import re import subprocess import sys import tempfile import xml.etree.ElementTree as ET PAGE_LINK = "data:page/id," def pages_of(path): """[(id, name)] of the pages, in order.""" try: root = ET.parse(path).getroot() except (ET.ParseError, OSError) as exc: sys.exit(f"error: cannot parse {path}: {exc}") return [(d.get("id") or f"p{i}", d.get("name") or f"Page {i + 1}") for i, d in enumerate(root.findall("diagram"))] def rewrite_page_links(tree): """data:page/id,X links -> #page-X (fragments survive SVG export). Returns count.""" n = 0 for el in tree.getroot().iter(): link = el.get("link") if link and link.startswith(PAGE_LINK): el.set("link", "#page-" + link[len(PAGE_LINK):]) n += 1 return n def export_svg(drawio_file, index, out_svg): """Export one page (1-based index) to SVG via the draw.io CLI.""" r = subprocess.run(["drawio", "-x", "-f", "svg", "--embed-svg-images", "--page-index", str(index), "-o", out_svg, drawio_file], capture_output=True) return r.returncode == 0 and os.path.exists(out_svg) def strip_prolog(svg): """Drop any XML declaration / doctype so the SVG can be inlined in HTML.""" return re.sub(r"^\s*(<\?xml[^>]*\?>\s*|]*>\s*)*", "", svg) def build_html(title, page_meta, svgs): """One self-contained viewer page. page_meta = [(id, name)] aligned with svgs.""" sections = "\n".join( f'
{svg}
' for (pid, _), svg in zip(page_meta, svgs)) tabs = json.dumps([{"id": pid, "name": name} for pid, name in page_meta]) \ .replace(" {html.escape(title)}

{html.escape(title)}

{sections}
""" def main(): ap = argparse.ArgumentParser(description="Export a .drawio to a self-contained interactive HTML viewer.") ap.add_argument("file") ap.add_argument("-o", "--output", help="output .html (default: alongside input)") args = ap.parse_args() if not os.path.isfile(args.file): sys.exit(f"error: {args.file} not found") meta = pages_of(args.file) if not meta: sys.exit(f"error: no pages in {args.file}") tree = ET.parse(args.file) relinked = rewrite_page_links(tree) svgs, kept = [], [] with tempfile.TemporaryDirectory() as tmp: src = args.file if relinked: # export the rewritten copy instead src = os.path.join(tmp, "relinked.drawio") tree.write(src, encoding="utf-8", xml_declaration=False) for i, (pid, name) in enumerate(meta, 1): # draw.io --page-index is 1-based out = os.path.join(tmp, f"p{i}.svg") if not export_svg(src, i, out): sys.stderr.write(f"warning: page {i} ({name}) export failed — skipped\n") continue with open(out, encoding="utf-8") as f: svgs.append(strip_prolog(f.read())) kept.append((pid, name)) if not svgs: sys.exit("error: no pages exported (is the draw.io CLI installed?)") title = os.path.splitext(os.path.basename(args.file))[0] out = args.output or os.path.splitext(args.file)[0] + ".html" with open(out, "w", encoding="utf-8") as f: f.write(build_html(title, kept, svgs)) sys.stderr.write(f"wrote {out} ({len(svgs)} page{'s' if len(svgs) != 1 else ''}" + (f", {relinked} drill-down link{'s' if relinked != 1 else ''}" if relinked else "") + ")\n") if __name__ == "__main__": main()