Source code for svdoc.render_html

"""HTML renderer: Doc IR -> a single self-contained .html page per construct.

Cross-links are convention-based, not verified: a Port/Param whose type_ref is
"pkg_name::type_name" links to "pkg_name.html#type_name", assuming that page
was (or will be) generated by rendering pkg_name's own package file the same
way. Nothing here checks that the target page or anchor actually exists.
"""

from html import escape

from .ir import InterfaceDoc, ModuleDoc, PackageDoc

_STYLE = """
body { font-family: -apple-system, sans-serif; max-width: 60rem; margin: 2rem auto; padding: 0 1rem; }
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
th, td { border: 1px solid #ccc; padding: 0.4rem 0.6rem; text-align: left; }
th { background: #f0f0f0; }
code { background: #f5f5f5; padding: 0.1rem 0.3rem; border-radius: 3px; }
h2 { border-bottom: 1px solid #ccc; padding-bottom: 0.2rem; margin-top: 2rem; }
details { margin: 0; }
details summary { cursor: pointer; color: #555; padding: 0.2rem 0; }
details table { margin: 0.5rem 0 0.5rem 1.5rem; width: calc(100% - 1.5rem); }
"""


[docs] def page(title: str, body_lines: list) -> str: """Wrap ``body_lines`` (raw HTML fragments) in a self-contained HTML page with the shared inline stylesheet. Exposed for :mod:`svdoc.build`, which uses it to render the site's ``index.html``.""" body = "\n".join(body_lines) return ( f'<!doctype html><html><head><meta charset="utf-8">' f"<title>{escape(title)}</title><style>{_STYLE}</style></head>" f"<body>\n{body}\n</body></html>" )
def _type_cell(type_str: str, type_ref: str) -> str: if not type_ref: return f"<code>{escape(type_str)}</code>" pkg, _, member = type_ref.partition("::") return f'<code><a href="{escape(pkg)}.html#{escape(member)}">{escape(type_ref)}</a></code>' def _params_section(params) -> list: if not params: return [] lines = [ "<h2>Parameters</h2>", "<table><tr><th>Name</th><th>Type</th><th>Default</th><th>Description</th></tr>", ] for p in params: lines.append( f"<tr><td><code>{escape(p.name)}</code></td>" f"<td>{_type_cell(p.type, p.type_ref)}</td>" f"<td><code>{escape(p.default or '')}</code></td>" f"<td>{escape(p.doc or '')}</td></tr>" ) lines.append("</table>") return lines def _modport_preview(mp) -> str: """A collapsed-by-default inline quick view of a modport's ins/outs, for an interface-typed port -- lets a reader see the direction/signal list without leaving the current page, while the type_ref link (see _type_cell) still offers the full interface page for deeper detail.""" rows = ["<table><tr><th>Direction</th><th>Signals</th><th>Description</th></tr>"] for g in mp.port_groups: signals = ", ".join(f"<code>{escape(s)}</code>" for s in g.signals) rows.append( f"<tr><td>{escape(g.direction)}</td><td>{signals}</td><td>{escape(g.doc or '')}</td></tr>" ) rows.append("</table>") doc = f"<p>{escape(mp.doc)}</p>" if mp.doc else "" return ( f"<details><summary>modport <code>{escape(mp.name)}</code> ins/outs</summary>" f"{doc}{''.join(rows)}</details>" ) def _ports_section(ports) -> list: if not ports: return [] lines = [ "<h2>Ports</h2>", "<table><tr><th>Name</th><th>Direction</th><th>Type</th><th>Description</th></tr>", ] for p in ports: lines.append( f"<tr><td><code>{escape(p.name)}</code></td>" f"<td>{escape(p.direction)}</td>" f"<td>{_type_cell(p.type, p.type_ref)}</td>" f"<td>{escape(p.doc or '')}</td></tr>" ) if p.modport_preview: lines.append( f'<tr><td colspan="4">{_modport_preview(p.modport_preview)}</td></tr>' ) lines.append("</table>") return lines
[docs] def render(mod: ModuleDoc) -> str: """Render a :class:`~svdoc.ir.ModuleDoc` to a self-contained HTML page.""" lines = [f"<h1>Module: <code>{escape(mod.name)}</code></h1>"] if mod.doc: lines.append(f"<p>{escape(mod.doc)}</p>") lines += _params_section(mod.params) lines += _ports_section(mod.ports) return page(f"Module: {mod.name}", lines)
[docs] def render_interface(iface: InterfaceDoc) -> str: """Render an :class:`~svdoc.ir.InterfaceDoc` to a self-contained HTML page.""" lines = [f"<h1>Interface: <code>{escape(iface.name)}</code></h1>"] if iface.doc: lines.append(f"<p>{escape(iface.doc)}</p>") lines += _params_section(iface.params) lines += _ports_section(iface.ports) if iface.signals: lines += [ "<h2>Signals</h2>", "<table><tr><th>Name</th><th>Type</th><th>Description</th></tr>", ] for s in iface.signals: lines.append( f"<tr><td><code>{escape(s.name)}</code></td>" f"<td><code>{escape(s.type)}</code></td>" f"<td>{escape(s.doc or '')}</td></tr>" ) lines.append("</table>") if iface.modports: lines.append("<h2>Modports</h2>") for mp in iface.modports: lines.append( f'<h3 id="{escape(mp.name)}"><code>{escape(mp.name)}</code></h3>' ) if mp.doc: lines.append(f"<p>{escape(mp.doc)}</p>") lines.append( "<table><tr><th>Direction</th><th>Signals</th><th>Description</th></tr>" ) for g in mp.port_groups: signals = ", ".join(f"<code>{escape(s)}</code>" for s in g.signals) lines.append( f"<tr><td>{escape(g.direction)}</td><td>{signals}</td><td>{escape(g.doc or '')}</td></tr>" ) lines.append("</table>") return page(f"Interface: {iface.name}", lines)
[docs] def render_package(pkg: PackageDoc) -> str: """Render a :class:`~svdoc.ir.PackageDoc` to a self-contained HTML page.""" lines = [f"<h1>Package: <code>{escape(pkg.name)}</code></h1>"] if pkg.doc: lines.append(f"<p>{escape(pkg.doc)}</p>") for t in pkg.typedefs: lines.append(f'<h2 id="{escape(t.name)}"><code>{escape(t.name)}</code></h2>') if t.doc: lines.append(f"<p>{escape(t.doc)}</p>") if t.kind == "enum": lines.append( f"<p>Enum (<code>{escape(t.base_type)}</code>)</p>" if t.base_type else "<p>Enum</p>" ) lines.append( "<table><tr><th>Value</th><th>Number</th><th>Description</th></tr>" ) for v in t.values: lines.append( f"<tr><td><code>{escape(v.name)}</code></td>" f"<td>{escape(v.value or '')}</td>" f"<td>{escape(v.doc or '')}</td></tr>" ) lines.append("</table>") elif t.kind == "struct": lines.append("<p>Packed struct</p>") lines.append( "<table><tr><th>Field</th><th>Type</th><th>Description</th></tr>" ) for f in t.fields: lines.append( f"<tr><td><code>{escape(f.name)}</code></td>" f"<td><code>{escape(f.type)}</code></td>" f"<td>{escape(f.doc or '')}</td></tr>" ) lines.append("</table>") else: lines.append(f"<p>Alias for <code>{escape(t.alias_type)}</code></p>") for s in pkg.subroutines: lines.append(f'<h2 id="{escape(s.name)}"><code>{escape(s.name)}</code></h2>') if s.doc: lines.append(f"<p>{escape(s.doc)}</p>") header = ( f"Function returning <code>{escape(s.return_type)}</code>" if s.kind == "function" else "Task" ) lines.append(f"<p>{header}</p>") if s.args: lines.append( "<table><tr><th>Name</th><th>Direction</th><th>Type</th><th>Description</th></tr>" ) for a in s.args: lines.append( f"<tr><td><code>{escape(a.name)}</code></td><td>{escape(a.direction)}</td>" f"<td><code>{escape(a.type)}</code></td><td>{escape(a.doc or '')}</td></tr>" ) lines.append("</table>") return page(f"Package: {pkg.name}", lines)