"""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)