"""svdoc CLI (v1): `svdoc <file.sv>` prints to terminal; `--out md`/`--out html`
writes a file; `svdoc build <files...>` writes a multi-page linked HTML site.
"""
import argparse
import pathlib
import sys
from . import render_diagram, render_html, render_md
from .build import build_site
from .fixer import fix_file
from .ir import InterfaceDoc, ModuleDoc, PackageDoc
from .parser import build_hierarchy, parse_file, resolve_types
_RENDERERS = {
"md": {
ModuleDoc: render_md.render,
InterfaceDoc: render_md.render_interface,
PackageDoc: render_md.render_package,
},
"html": {
ModuleDoc: render_html.render,
InterfaceDoc: render_html.render_interface,
PackageDoc: render_html.render_package,
},
}
_EXTENSIONS = {"md": ".md", "html": ".html", "mmd": ".mmd", "dot": ".dot"}
_SYMBOL_RENDERERS = {
"mmd": render_diagram.render_module_symbol_mermaid,
"dot": render_diagram.render_module_symbol_dot,
}
_HIERARCHY_RENDERERS = {
"mmd": render_diagram.render_hierarchy_mermaid,
"dot": render_diagram.render_hierarchy_dot,
}
def _run_single(args):
if args.fix:
changed = fix_file(args.file)
print(f"{'fixed' if changed else 'no changes needed for'} {args.file}")
return
doc = parse_file(args.file, args.include_dir)
if args.more_files and isinstance(doc, ModuleDoc):
resolve_types(doc, [args.file] + args.more_files, args.include_dir)
if args.out in _SYMBOL_RENDERERS:
if not isinstance(doc, ModuleDoc):
sys.exit(f"--out {args.out} only supports modules, not {type(doc).__name__}")
text = _SYMBOL_RENDERERS[args.out](doc)
out_path = pathlib.Path(args.file).with_suffix(_EXTENSIONS[args.out])
out_path.write_text(text)
print(f"wrote {out_path}")
elif args.out:
text = _RENDERERS[args.out][type(doc)](doc)
out_path = pathlib.Path(args.file).with_suffix(_EXTENSIONS[args.out])
out_path.write_text(text)
print(f"wrote {out_path}")
else:
print(_RENDERERS["md"][type(doc)](doc))
def _run_build(args):
index_path = build_site(args.files, args.out_dir, args.include_dir)
print(f"wrote {len(args.files)} file(s) to a site at {index_path}")
def _run_hierarchy(args):
root = build_hierarchy(args.top_module, args.files, args.include_dir)
text = _HIERARCHY_RENDERERS[args.format](root, max_depth=args.max_depth)
out_path = pathlib.Path(args.out_file)
out_path.write_text(text)
print(f"wrote {out_path}")
def _build_parser():
ap = argparse.ArgumentParser(prog="svdoc build")
ap.add_argument("files", nargs="+", help=".sv files to document")
ap.add_argument("--out-dir", required=True, help="directory to write the site into")
ap.add_argument(
"--include-dir",
action="append",
default=[],
help="directory to search for `include targets not alongside the including file (repeatable)",
)
return ap
def _hierarchy_parser():
ap = argparse.ArgumentParser(prog="svdoc hierarchy")
ap.add_argument("top_module", help="name of the module to elaborate as the hierarchy root")
ap.add_argument("files", nargs="+", help=".sv files needed to elaborate top_module")
ap.add_argument("--out-file", required=True, help="path to write the diagram to")
ap.add_argument("--format", choices=["mmd", "dot"], default="mmd", help="diagram format")
ap.add_argument(
"--max-depth",
type=int,
default=render_diagram.DEFAULT_MAX_DEPTH,
help="collapse instances deeper than this into a placeholder node",
)
ap.add_argument(
"--include-dir",
action="append",
default=[],
help="directory to search for `include targets not alongside the including file (repeatable)",
)
return ap
def _single_parser():
ap = argparse.ArgumentParser(
prog="svdoc",
epilog="Also available: `svdoc build <files...> --out-dir <dir>` to generate a multi-page linked HTML site.",
)
ap.add_argument(
"file",
help="path to the .sv file containing the module/interface/package to document",
)
ap.add_argument(
"more_files",
nargs="*",
help="additional .sv files (e.g. packages it depends on) used only to "
"resolve cross-file types -- not documented themselves",
)
ap.add_argument(
"--out",
choices=["md", "html", "mmd", "dot"],
help="write rendered doc to a file in this format instead of printing to stdout "
"(mmd/dot render a module symbol diagram instead of docs, modules only)",
)
ap.add_argument(
"--fix",
action="store_true",
help="insert ///< TODO stubs next to undocumented ports/params in place",
)
ap.add_argument(
"--include-dir",
action="append",
default=[],
help="directory to search for `include targets not alongside the including file (repeatable)",
)
return ap
[docs]
def main(argv=None):
"""Entry point for the ``svdoc`` command (registered via ``pyproject.toml``).
Two modes, dispatched on whether the first argument is the literal
``build`` subcommand (argparse subparsers don't mix cleanly with a
same-position positional argument, so this is checked manually rather
than via ``add_subparsers``):
- ``svdoc <file.sv> [more_files...]`` — document a single module/interface/
package, optionally resolving cross-file types against ``more_files``,
printing Markdown to stdout or writing a single Markdown/HTML file
(``--out``), or scaffolding missing doc comments in place (``--fix``).
- ``svdoc build <files...> --out-dir <dir>`` — document every given file
into a single flat multi-page HTML site (one page per construct plus
an ``index.html``), so cross-links between pages always resolve
correctly regardless of where the source files live on disk.
:param argv: Argument list to parse (as :func:`sys.argv`\\ ``[1:]`` would
appear). Defaults to ``sys.argv[1:]`` when ``None``.
"""
if argv is None:
argv = sys.argv[1:]
if argv and argv[0] == "build":
args = _build_parser().parse_args(argv[1:])
_run_build(args)
return
if argv and argv[0] == "hierarchy":
args = _hierarchy_parser().parse_args(argv[1:])
_run_hierarchy(args)
return
args = _single_parser().parse_args(argv)
_run_single(args)
if __name__ == "__main__":
sys.exit(main())