253 lines
9.8 KiB
Python
253 lines
9.8 KiB
Python
"""
|
|
python3 -m docgen emit auto <ir.json> -o DIR whatever the structure asks for
|
|
python3 -m docgen emit dot <ir.json> [-o out.svg|.dot]
|
|
python3 -m docgen emit erd <ir.json> [-o out.svg]
|
|
python3 -m docgen emit index <ir.json> [-o out.md|.json]
|
|
python3 -m docgen emit minimap <ir.json> [-o out.svg] [--scale 0.55]
|
|
python3 -m docgen emit notebook <ir.json> [-o out.ipynb] [--overlay f.json]
|
|
python3 -m docgen emit site <ir.json> -o DIR
|
|
python3 -m docgen emit explore <ir.json> -o DIR
|
|
|
|
IR -> an artifact. Every emitter validates its input first, so a broken document
|
|
is reported here rather than drawn wrong. `--style` and `--theme` apply to every
|
|
emitter that draws.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from .common import PROG, Abort, emit as write, load_style, read_ir, style_args
|
|
|
|
|
|
def _parser(name: str, *, styled: bool = True, output_help: str = "") -> argparse.ArgumentParser:
|
|
p = argparse.ArgumentParser(prog=f"{PROG} emit {name}")
|
|
p.add_argument("ir", type=Path)
|
|
p.add_argument("--output", "-o", type=Path, help=output_help or None)
|
|
if styled:
|
|
style_args(p)
|
|
return p
|
|
|
|
|
|
def _size(svg: str) -> str:
|
|
m = re.search(r'width="(\d+)pt" height="(\d+)pt"', svg)
|
|
return f"{m.group(1)}x{m.group(2)}" if m else ""
|
|
|
|
|
|
def auto(argv) -> int:
|
|
p = _parser("auto", output_help="Directory to write into.")
|
|
p.add_argument("--force", help="Use this emitter regardless of what fits.")
|
|
args = p.parse_args(argv)
|
|
from ..emitters.auto import draw
|
|
|
|
data, style = read_ir(args.ir), load_style(args.style, args.theme)
|
|
drawn = draw(data, style, force=args.force)
|
|
print(f" {drawn['verdict']['kind']:<8} -> {drawn['emitter']}")
|
|
print(f" {drawn['why']}")
|
|
if drawn["content"] is None:
|
|
raise Abort(drawn["why"])
|
|
out_dir = args.output or Path(".")
|
|
path = out_dir / f"{args.ir.stem}{drawn['suffix']}"
|
|
write(drawn["content"], path, f" {path}")
|
|
return 0
|
|
|
|
|
|
def dot(argv) -> int:
|
|
p = _parser("dot", output_help="Write here. .dot or .svg by suffix.")
|
|
p.add_argument("--max-depth", type=int, default=None)
|
|
p.add_argument("--quiet", "-q", action="store_true", help="Do not warn about shape.")
|
|
args = p.parse_args(argv)
|
|
from ..emitters.dot import RenderError, emit, render
|
|
from ..ops import shape
|
|
|
|
data, style = read_ir(args.ir), load_style(args.style, args.theme)
|
|
|
|
# Aspect ratio is a property of the graph, not of the renderer: a layered
|
|
# engine puts one dependency level in one row, so the widest level is the
|
|
# width. Say so before writing the file, because the alternative is finding
|
|
# out from a 13671pt image — and the fix is never a layout flag, it is a
|
|
# smaller question.
|
|
if not args.quiet:
|
|
sh = shape(data)
|
|
if sh["widest_level"] > 20 or sh["nodes"] > 60:
|
|
est = sh["widest_level"] / max(sh["levels"], 1)
|
|
print(f" note: {sh['nodes']} nodes, {sh['levels']} levels, widest level "
|
|
f"{sh['widest_level']} — this will render roughly {est:.0f}:1.\n"
|
|
" Around 20 nodes is where it stops being a diagram. Try "
|
|
"`view --split`,\n `--around <id> --hops 2`, or `--subtree <id>`. "
|
|
"Layout flags will not fix it.", file=sys.stderr)
|
|
if sh["isolated"] > sh["nodes"] // 3:
|
|
print(f" {sh['isolated']} of {sh['nodes']} nodes have no edges; they are "
|
|
"laid out side by side.", file=sys.stderr)
|
|
|
|
dot_text = emit(data, style, max_depth=args.max_depth)
|
|
if not args.output or args.output.suffix == ".dot":
|
|
write(dot_text, args.output, f" {args.style}/{style.theme:6} {args.output}")
|
|
return 0
|
|
try:
|
|
rendered = render(dot_text, fmt=args.output.suffix.lstrip(".") or "svg")
|
|
except RenderError as e:
|
|
raise Abort(str(e)) from None
|
|
write(rendered, args.output, f" {args.style}/{style.theme:6} {args.output}")
|
|
return 0
|
|
|
|
|
|
def erd(argv) -> int:
|
|
args = _parser("erd").parse_args(argv)
|
|
from ..emitters.erd import emit
|
|
|
|
data, style = read_ir(args.ir), load_style(args.style, args.theme)
|
|
try:
|
|
svg = emit(data, style)
|
|
except ValueError as e:
|
|
raise Abort(str(e)) from None
|
|
write(svg, args.output, f" erd/{style.theme:6} {args.output} {_size(svg)}")
|
|
return 0
|
|
|
|
|
|
def index(argv) -> int:
|
|
p = _parser("index", styled=False,
|
|
output_help=".md for the document, .json for a sidebar.")
|
|
p.add_argument("--title", default="")
|
|
args = p.parse_args(argv)
|
|
from ..emitters.index import to_markdown, to_sidebar
|
|
|
|
data = read_ir(args.ir)
|
|
if args.output and args.output.suffix == ".json":
|
|
text = json.dumps(to_sidebar(data), indent=2) + "\n"
|
|
else:
|
|
text = to_markdown(data, title=args.title)
|
|
write(text, args.output, f" index {args.output}")
|
|
return 0
|
|
|
|
|
|
def minimap(argv) -> int:
|
|
p = _parser("minimap")
|
|
p.add_argument("--scale", type=float, default=0.55, help="Pixels per source line.")
|
|
p.add_argument("--width", type=int, default=1180, help="Wrap a shelf past this.")
|
|
args = p.parse_args(argv)
|
|
from ..emitters.minimap import emit
|
|
|
|
data, style = read_ir(args.ir), load_style(args.style, args.theme)
|
|
try:
|
|
svg = emit(data, style, scale=args.scale, target_width=args.width)
|
|
except ValueError as e:
|
|
raise Abort(str(e)) from None
|
|
write(svg, args.output, f" minimap {args.output} {_size(svg)}")
|
|
return 0
|
|
|
|
|
|
def notebook(argv) -> int:
|
|
p = _parser("notebook", styled=False)
|
|
p.add_argument("--overlay", type=Path, help="Hand-written additions, re-applied.")
|
|
p.add_argument("--spec-out", type=Path, help="Write the generated spec too.")
|
|
p.add_argument("--scaffold", type=Path, help="Write a blank overlay and stop.")
|
|
p.add_argument("--base-url", default="https://api.example.invalid")
|
|
args = p.parse_args(argv)
|
|
from ..emitters.notebook import emit
|
|
from ..notebook import dump, from_ir, merge, scaffold
|
|
|
|
base = from_ir(read_ir(args.ir), base_url=args.base_url)
|
|
|
|
if args.scaffold:
|
|
dump(scaffold(base), args.scaffold)
|
|
print(f" overlay {args.scaffold} {len(base['steps'])} step(s), none filled in")
|
|
return 0
|
|
|
|
overlay = None
|
|
if args.overlay:
|
|
if args.overlay.exists():
|
|
overlay = json.loads(args.overlay.read_text())
|
|
else:
|
|
print(f" note: no overlay at {args.overlay} — generating the base only",
|
|
file=sys.stderr)
|
|
|
|
spec, drift = merge(base, overlay)
|
|
for d in drift:
|
|
# The base moved under the overlay. Worth saying out loud; not a reason
|
|
# to refuse to build the document.
|
|
print(f" drift: {d}", file=sys.stderr)
|
|
|
|
if args.spec_out:
|
|
dump(spec, args.spec_out)
|
|
print(f" spec {args.spec_out}")
|
|
|
|
text = emit(spec)
|
|
cells = len(json.loads(text)["cells"])
|
|
extra = f", {len(drift)} drift" if drift else ""
|
|
write(text, args.output,
|
|
f" notebook {args.output} {len(spec['steps'])} steps, {cells} cells{extra}")
|
|
return 0
|
|
|
|
|
|
def site(argv) -> int:
|
|
p = argparse.ArgumentParser(prog=f"{PROG} emit site")
|
|
p.add_argument("ir", type=Path)
|
|
p.add_argument("--output", "-o", type=Path, required=True, help="Directory.")
|
|
style_args(p)
|
|
p.add_argument("--title", default="")
|
|
p.add_argument("--no-graph", action="store_true")
|
|
args = p.parse_args(argv)
|
|
from ..emitters.auto import draw
|
|
from ..emitters.site import write as site_write
|
|
|
|
data, style = read_ir(args.ir), load_style(args.style, args.theme)
|
|
args.output.mkdir(parents=True, exist_ok=True)
|
|
|
|
graph_name = None
|
|
if not args.no_graph:
|
|
# Whatever the structure asks for, so the page carries the right picture
|
|
# — the same `draw()` the book and `emit auto` use.
|
|
drawn = draw(data, style)
|
|
if drawn["content"] is not None and drawn["suffix"] == ".svg":
|
|
graph_name = "graph.svg"
|
|
write(drawn["content"], args.output / graph_name)
|
|
else:
|
|
print(f" note: {drawn['verdict']['kind']} — {drawn['why']}", file=sys.stderr)
|
|
print(" no diagram on the page; the index is the artifact", file=sys.stderr)
|
|
|
|
for f in site_write(data, style, args.output, graph=graph_name, title=args.title):
|
|
print(f" site {f}")
|
|
if graph_name:
|
|
print(f" site {args.output / graph_name}")
|
|
return 0
|
|
|
|
|
|
def explore(argv) -> int:
|
|
p = argparse.ArgumentParser(prog=f"{PROG} emit explore")
|
|
p.add_argument("ir", type=Path)
|
|
p.add_argument("--output", "-o", type=Path, required=True, help="Directory.")
|
|
style_args(p)
|
|
p.add_argument("--scale", type=float, default=0.55)
|
|
p.add_argument("--width", type=int, default=1100)
|
|
p.add_argument("--hops", type=int, default=1)
|
|
p.add_argument("--title", default="")
|
|
args = p.parse_args(argv)
|
|
from ..emitters.explore import write as explore_write
|
|
|
|
data, style = read_ir(args.ir), load_style(args.style, args.theme)
|
|
try:
|
|
path = explore_write(data, style, args.output, scale=args.scale, width=args.width,
|
|
hops=args.hops, title=args.title)
|
|
except ValueError as e:
|
|
raise Abort(str(e)) from None
|
|
graphs = args.output / "graphs"
|
|
count = len(list(graphs.glob("*.svg"))) if graphs.exists() else 0
|
|
print(f" explore {path} {count} neighbourhood diagram(s)")
|
|
return 0
|
|
|
|
|
|
EMITTERS = {"auto": auto, "dot": dot, "erd": erd, "index": index, "minimap": minimap,
|
|
"notebook": notebook, "site": site, "explore": explore}
|
|
|
|
|
|
def main(argv) -> int:
|
|
if not argv or argv[0] in ("-h", "--help"):
|
|
print(__doc__.strip("\n"))
|
|
return 0 if argv else 2
|
|
if argv[0] not in EMITTERS:
|
|
raise Abort(f"no emitter {argv[0]!r} — have: {', '.join(EMITTERS)}")
|
|
return EMITTERS[argv[0]](argv[1:])
|