"""svdoc build: a flat multi-page HTML site (one page per construct) with a
navigable index, so cross-links between pages always resolve correctly --
avoids the relative-path problem of generating standalone pages into
whatever directories the source files happen to live in.
"""
import pathlib
import re
from html import escape
from typing import Optional
from . import render_html
from .ir import InterfaceDoc, ModuleDoc, PackageDoc
from .parser import parse_file, resolve_types
_UNSAFE_NAME_CHARS = re.compile(r"[^\w.-]")
def _safe_page_name(name: str) -> str:
"""Sanitize a construct name for use as an output filename. SV escaped
identifiers (``\\../../foo``) parse without error and can contain path
separators, so page filenames can't trust ``doc.name`` directly."""
return _UNSAFE_NAME_CHARS.sub("_", name)
_RENDERERS = {
ModuleDoc: render_html.render,
InterfaceDoc: render_html.render_interface,
PackageDoc: render_html.render_package,
}
_KIND_LABELS = {
ModuleDoc: "Module",
InterfaceDoc: "Interface",
PackageDoc: "Package",
}
[docs]
def build_site(paths: list, out_dir: str, include_dirs: Optional[list] = None) -> str:
"""Parse every file in ``paths`` and write one HTML page per construct
into a single flat ``out_dir``, plus an ``index.html`` linking to all of
them. Because every page lands in the same directory, the convention-based
cross-links in :mod:`svdoc.render_html` (``pkg_name.html#member``) always
resolve correctly, regardless of where the source ``.sv`` files live.
:param paths: All ``.sv`` files to document. Each is parsed independently
(one construct per file, same as :func:`svdoc.parser.parse_file`);
cross-file type resolution runs against the full set for every module.
:param out_dir: Directory to write the site into. Created if missing.
:param include_dirs: Optional directories to search for ```include``
targets that don't live alongside any file in ``paths`` (e.g. a
shared ``include/`` directory separate from per-module source dirs).
:returns: Path to the written ``index.html``.
:raises ValueError: If any file fails to parse cleanly.
"""
out = pathlib.Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
docs = [parse_file(p, include_dirs) for p in paths]
for doc in docs:
if isinstance(doc, ModuleDoc):
resolve_types(doc, paths, include_dirs)
index_lines = ["<h1>svdoc site</h1>", "<ul>"]
for doc in sorted(docs, key=lambda d: (_KIND_LABELS[type(d)], d.name)):
page_name = f"{_safe_page_name(doc.name)}.html"
(out / page_name).write_text(_RENDERERS[type(doc)](doc))
label = _KIND_LABELS[type(doc)]
index_lines.append(f'<li>{label}: <a href="{escape(page_name)}">{escape(doc.name)}</a></li>')
index_lines.append("</ul>")
index_html = render_html.page("svdoc site", index_lines)
index_path = out / "index.html"
index_path.write_text(index_html)
return str(index_path)