init docgen
This commit is contained in:
1
soleprint/atlas2/docgen/.gitignore
vendored
1
soleprint/atlas2/docgen/.gitignore
vendored
@@ -3,3 +3,4 @@ out/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv/
|
||||
.docgen/
|
||||
|
||||
@@ -43,6 +43,7 @@ SLUG ?=
|
||||
# en_US.UTF-8 from the environment and --reader rejects it.
|
||||
READER ?= python
|
||||
OVERLAY ?=
|
||||
NOTES ?=
|
||||
CONFIG ?= docgen.toml
|
||||
ONLY ?=
|
||||
CHECK ?=
|
||||
@@ -54,7 +55,7 @@ SLUG_ARG := $(if $(SLUG),--slug $(SLUG))
|
||||
OVER_ARG := $(if $(OVERLAY),--overlay $(OVERLAY))
|
||||
ONLY_ARGS := $(foreach n,$(subst $(comma), ,$(ONLY)),--only $(n))
|
||||
|
||||
.PHONY: help sync lock book run check ir db code view graph index site minimap explore docs self doctor clean
|
||||
.PHONY: help sync lock book run scaffold check ir db code view graph index site minimap explore docs self doctor clean
|
||||
|
||||
help: ## List every target
|
||||
@echo "docgen — static analysis of a tree, and the artifacts that fall out of it"
|
||||
@@ -68,6 +69,7 @@ help: ## List every target
|
||||
@echo " BOOK=/path where a book goes, and which book to check"
|
||||
@echo " READER=python|code ast, or tree-sitter SLUG=name what to call the book"
|
||||
@echo " OVERLAY=overlay.json hand-written notebook additions, re-applied every build"
|
||||
@echo " NOTES=notes/ markdown filed by page: notes/<book>/<page>.md"
|
||||
@echo " CONFIG=docgen.toml a run file ONLY=a,b just these books"
|
||||
@echo " CHECK=1 with run: each book's own level after building it"
|
||||
@echo
|
||||
@@ -92,11 +94,14 @@ book: ## SRC (or SCHEMA/OPENAPI/HAR) -> one operation, measured at both ends
|
||||
$(if $(OPENAPI),--openapi "$(OPENAPI)") \
|
||||
$(if $(HAR),--har "$(HAR)") \
|
||||
-o "$(if $(BOOK),$(BOOK),$(OUT)/book)" \
|
||||
$(STYLE_ARGS) $(SLUG_ARG) $(OVER_ARG)
|
||||
$(STYLE_ARGS) $(SLUG_ARG) $(OVER_ARG) $(if $(NOTES),--notes "$(NOTES)")
|
||||
|
||||
run: ## CONFIG (a run file) -> every book it lists; ONLY=a,b for some
|
||||
run: ## CONFIG (a run file or a project file) -> every book it lists; ONLY=a,b for some
|
||||
@$(CLI) run "$(CONFIG)" $(ONLY_ARGS) $(if $(CHECK),--check)
|
||||
|
||||
scaffold: ## CONFIG -> an empty note for every page and step, never overwriting
|
||||
@$(CLI) scaffold "$(CONFIG)" $(ONLY_ARGS)
|
||||
|
||||
check: ## Prove docgen (or one book, with BOOK=<dir>)
|
||||
@$(CLI) check $(if $(BOOK),"$(BOOK)")
|
||||
|
||||
|
||||
@@ -17,17 +17,25 @@ extractors/ → graph IR (JSON) → emitters/
|
||||
make sync # optional: .venv with every group, from uv.lock
|
||||
make check # prove it, on a tree it builds itself
|
||||
make book SRC=/path/to/repo # one whole operation, measured at both ends
|
||||
make run CONFIG=docgen.toml # every book a run file lists
|
||||
make run CONFIG=docgen.toml # every book a run file lists, or a project's document
|
||||
make scaffold CONFIG=docgen.toml # an empty note for every page and step
|
||||
python3 -m docgen contract # the instructions to hand a model
|
||||
make help
|
||||
```
|
||||
|
||||
The full documentation is `docs/index.html` — open it in a browser.
|
||||
The full documentation is `docs/index.html` — open it in a browser. Start with
|
||||
**Your first project** for a step-by-step walkthrough.
|
||||
|
||||
**For a model** writing `docgen.toml` and notes: `python3 -m docgen contract` prints
|
||||
`docs/models.md` — every key, every rule, every error with its fix. Attach it (with
|
||||
explode's contract, if replies go through explode). The suite checks it against the
|
||||
loaders, so it cannot say something docgen does not accept.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
cli/ the command line — every command, and nothing else
|
||||
book/ an operation: larder measure, steps, web output, run files
|
||||
book/ an operation: larder measure, steps, pages, notes, documents, run files
|
||||
extractors/ source -> IR, one per source type
|
||||
ir/ the contract: schema.json, the dataclasses, the validator
|
||||
ops/ IR -> a smaller IR
|
||||
@@ -69,6 +77,40 @@ stops the others; unknown keys are refused; a rebuild replaces the previous
|
||||
build's outputs and never touches a hand-written `checks.py`.
|
||||
`docgen.example.toml` runs against the shipped fixtures.
|
||||
|
||||
## Project files, pages and notes
|
||||
|
||||
A **project file** is one document built from several sources — the shape a model
|
||||
writes unprompted:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "orders-api"
|
||||
|
||||
[extract.openapi]
|
||||
spec = "doc/contracts/openapi.yaml"
|
||||
|
||||
[extract.code]
|
||||
root = "src"
|
||||
ext = [".cs"]
|
||||
|
||||
[emit.notebook]
|
||||
output = "../docs_out/orders_api_notebook"
|
||||
```
|
||||
|
||||
Each `[extract.*]` is a book — a *part* of every output — and each `[emit.*]` an
|
||||
output made of all of them. Keys docgen deliberately doesn't do (`[extract.db] url`,
|
||||
shell/YAML/SQL `ext`, unknown readers) are refused with what to do instead.
|
||||
|
||||
A book's diagram is split into **pages** by structure — overview first, one page per
|
||||
subsystem — measured until each fits its column. On the site the diagrams are inline,
|
||||
so a box opens its page; in a notebook, a page index links every figure, and each
|
||||
figure is an SVG cell attachment that renders without running anything.
|
||||
|
||||
Hand-written text goes in **notes**: `notes/<book>/<page>.md` lands above that page.
|
||||
`make scaffold` writes an empty stub for every spot; a note whose page disappeared is
|
||||
reported and fails the check level. `docgen.project.example.toml` builds the fixtures
|
||||
into one document with a sample note.
|
||||
|
||||
Or as three composable commands, which is what the Makefile is wrapping — run
|
||||
from the directory above this one:
|
||||
|
||||
@@ -240,8 +282,8 @@ about either.
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
make check # docgen's own suite: 287 after make sync, 272 with nothing
|
||||
make check BOOK=out/book/x # one book's own level — generated and hand-written checks
|
||||
make check # docgen's own suite: 346 after make sync, 331 with nothing
|
||||
make check BOOK=out/book/x # one book's own level — or a project's .docgen/<name>
|
||||
make doctor # the machine; never fails
|
||||
```
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ EXTRACTORS = {
|
||||
TAKES_EXCLUDE = {"python", "code"}
|
||||
|
||||
|
||||
def extract(kind: str, source, *, exclude=(), identity=None):
|
||||
def extract(kind: str, source, *, exclude=(), identity=None, suffixes=None):
|
||||
"""(IR dict, Larder) for one source type, dispatched through EXTRACTORS."""
|
||||
from importlib import import_module
|
||||
|
||||
@@ -91,6 +91,8 @@ def extract(kind: str, source, *, exclude=(), identity=None):
|
||||
kwargs = {"identity": identity or str(source)}
|
||||
if kind in TAKES_EXCLUDE:
|
||||
kwargs["exclude"] = exclude
|
||||
if kind == "code" and suffixes:
|
||||
kwargs["suffixes"] = suffixes
|
||||
g = fn(source, **kwargs)
|
||||
|
||||
ir = g.to_dict()
|
||||
@@ -102,7 +104,7 @@ def extract(kind: str, source, *, exclude=(), identity=None):
|
||||
return ir, Larder.from_dict(measured)
|
||||
|
||||
|
||||
def spec_from(book: Book, ir: dict) -> dict:
|
||||
def spec_from(book: Book, ir: dict, *, prelude_dir: str | None = None) -> dict:
|
||||
"""The book's ledger -> a notebook spec, with the two measures at the ends.
|
||||
|
||||
Reuses `notebook.spec`'s step vocabulary rather than inventing one, so
|
||||
@@ -128,7 +130,10 @@ def spec_from(book: Book, ir: dict) -> dict:
|
||||
),
|
||||
),
|
||||
spec_mod._step("prelude", "code", title="Reading a step on its own",
|
||||
code=PRELUDE),
|
||||
# In its own book directory the notebook reads steps from
|
||||
# where it is opened; elsewhere it points back at the book.
|
||||
code=PRELUDE if prelude_dir is None else PRELUDE.replace(
|
||||
"BOOK = Path.cwd()", f"BOOK = Path.cwd() / {prelude_dir!r}", 1)),
|
||||
]
|
||||
|
||||
for s in book.steps:
|
||||
@@ -191,7 +196,9 @@ def _load_cell(step_id: str, artifact: str) -> str:
|
||||
if artifact.endswith(".json"):
|
||||
return (
|
||||
f'data = load("{artifact}")\n'
|
||||
'if data:\n'
|
||||
'if isinstance(data, list):\n'
|
||||
' print(f"{len(data)} item(s)")\n'
|
||||
'elif data:\n'
|
||||
' if "nodes" in data:\n'
|
||||
' print(f\'{len(data["nodes"])} nodes, {len(data.get("edges", []))} edges\')\n'
|
||||
' else:\n'
|
||||
@@ -222,7 +229,20 @@ def _load_cell(step_id: str, artifact: str) -> str:
|
||||
# remove first. Named, never globbed: a book directory also holds the hand-written
|
||||
# `checks.py` and `overlay.json`, and a misconfigured output path could point at
|
||||
# somebody's source tree — so nothing outside this list is ever deleted.
|
||||
GENERATED = ("book.json", "notebook.ipynb", "steps", "site", "explore")
|
||||
GENERATED = ("book.json", "notebook.ipynb", "steps", "site", "explore", "document.json")
|
||||
|
||||
# The steps a book runs, by id — fixed, so a note can be filed under one before
|
||||
# the book has ever been built, and a skipped step does not make its note drift.
|
||||
STEP_LABELS = (
|
||||
("ir", "the graph, extracted"),
|
||||
("view", "the default view for this source"),
|
||||
("pages", "the diagram, split into pages that fit"),
|
||||
("graph", "the whole view in one drawing"),
|
||||
("index", "readable without a diagram"),
|
||||
("sidebar", "navigation, for whatever renders it"),
|
||||
("minimap", "what is where, read from the colours"),
|
||||
("explore", "navigate on one side, explore on the other"),
|
||||
)
|
||||
|
||||
|
||||
def clear(out) -> list[str]:
|
||||
@@ -253,12 +273,19 @@ def clear(out) -> list[str]:
|
||||
return removed
|
||||
|
||||
|
||||
def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid",
|
||||
theme: str | None = None, exclude=(), overlay=None, quiet: bool = False) -> Book:
|
||||
"""The whole book, in one process. Returns it; `book.json` is written."""
|
||||
def build_book(kind: str, source, out, *, slug: str | None = None, exclude=(),
|
||||
ops: dict | None = None, suffixes=None, quiet: bool = False) -> Book:
|
||||
"""One book's steps: larder, IR, view, pages, drawings. No site, no notebook.
|
||||
|
||||
Those are a document's outputs and are assembled by `book/document.py`, which
|
||||
may put several books into one of each. The book is returned open — its last
|
||||
step is added once the outputs it belongs to exist.
|
||||
"""
|
||||
from ..ir import check
|
||||
from ..ops import classify, overview
|
||||
from ..ops.pages import for_view
|
||||
from ..style import Style
|
||||
from .document import apply_ops
|
||||
|
||||
out = Path(out)
|
||||
slug = slug or Path(str(source)).name or "book"
|
||||
@@ -270,7 +297,7 @@ def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid"
|
||||
if not quiet:
|
||||
print(text)
|
||||
|
||||
ir, larder = extract(kind, source, exclude=exclude)
|
||||
ir, larder = extract(kind, source, exclude=exclude, suffixes=suffixes)
|
||||
if larder is None:
|
||||
larder = Larder(kind=kind, identity=str(source), unit="document", seen=0)
|
||||
book = Book(slug=slug, larder=larder, out=out)
|
||||
@@ -289,22 +316,31 @@ def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid"
|
||||
p.write_text(json.dumps(ir, indent=2) + "\n")
|
||||
book.step("ir", "the graph, extracted", p, note=f"{len(ir['nodes'])} nodes")
|
||||
|
||||
view = overview(ir)
|
||||
view = apply_ops(overview(ir), ops or {})
|
||||
book.view = view
|
||||
p = steps_dir / "view.json"
|
||||
p.write_text(json.dumps(view, indent=2) + "\n")
|
||||
verdict = classify(view)
|
||||
book.step("view", "the default view for this source", p,
|
||||
note=f"{verdict['kind']} — {verdict['why']}")
|
||||
|
||||
style_obj = Style.load(style, theme=theme)
|
||||
# Which pages exist — width-free, so it is decided once. Each output lays
|
||||
# them out for its own column when it is assembled.
|
||||
book.pages = for_view(ir, view)
|
||||
p = steps_dir / "pages.json"
|
||||
p.write_text(json.dumps(
|
||||
[{k: v for k, v in page.items() if k != "ir"} for page in book.pages],
|
||||
indent=2) + "\n")
|
||||
book.step("pages", "the diagram, split into pages that fit", p,
|
||||
note=f"{len(book.pages)} page(s)")
|
||||
|
||||
style_obj = Style.load("lucid")
|
||||
|
||||
# The drawing, chosen by structure rather than by the caller — the same
|
||||
# `draw()` the `emit auto` and `emit site` commands use, so the three can
|
||||
# never disagree about what a graph wants.
|
||||
# The whole view in one drawing — still worth having as a step on its own,
|
||||
# and the same `draw()` the `emit auto` command uses.
|
||||
from ..emitters.auto import draw
|
||||
|
||||
drawn = draw(view, style_obj)
|
||||
graph_rel = None
|
||||
labels = {"erd": "drawn as an ERD", "dot": f"drawn as a {verdict['kind']}",
|
||||
"index": "not a diagram — written as a list"}
|
||||
if drawn["content"] is None:
|
||||
@@ -315,8 +351,6 @@ def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid"
|
||||
p.write_bytes(drawn["content"])
|
||||
else:
|
||||
p.write_text(drawn["content"])
|
||||
if drawn["suffix"] == ".svg":
|
||||
graph_rel = f"steps/{p.name}"
|
||||
book.step("graph", labels[drawn["emitter"]], p, note=drawn["why"])
|
||||
|
||||
from ..emitters.index import to_markdown, to_sidebar
|
||||
@@ -343,38 +377,41 @@ def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid"
|
||||
except Exception as e: # noqa: BLE001
|
||||
book.step("explore", "explorer not built", skipped=f"{type(e).__name__}: {e}")
|
||||
|
||||
# -- the last step ----------------------------------------------------
|
||||
from ..emitters.site import write as site_write
|
||||
site_dir = out / "site"
|
||||
site_write(view, style_obj, site_dir,
|
||||
graph=Path(graph_rel).name if graph_rel else None,
|
||||
title=slug, book=book.to_dict())
|
||||
if graph_rel and (out / graph_rel).exists():
|
||||
(site_dir / Path(graph_rel).name).write_bytes((out / graph_rel).read_bytes())
|
||||
|
||||
# close() BEFORE the notebook spec is built, not after. The spec quotes the
|
||||
# book measure, and the book measure only includes the site once the last
|
||||
# step exists — build it the other way round and the notebook says 7
|
||||
# artifacts while book.json says 8, which is exactly what it did.
|
||||
book.close(site=site_dir)
|
||||
|
||||
from ..emitters.notebook import write as nb_write
|
||||
from ..notebook import spec as spec_mod
|
||||
spec, spec_problems = spec_mod.merge(spec_from(book, ir), overlay)
|
||||
nb_write(spec, out / "notebook.ipynb")
|
||||
# Recorded by name and not by size: a notebook cannot report its own byte
|
||||
# count without changing it.
|
||||
book.notebook = "notebook.ipynb"
|
||||
return book
|
||||
|
||||
for pr in spec_problems:
|
||||
say(f" overlay {pr}")
|
||||
path = book.write()
|
||||
|
||||
def run(kind: str, source, out, *, slug: str | None = None, style: str = "lucid",
|
||||
theme: str | None = None, exclude=(), overlay=None, notes=None,
|
||||
quiet: bool = False) -> Book:
|
||||
"""One book, as a one-book document whose outputs live in its own directory.
|
||||
|
||||
`book.json`, `steps/`, `site/` and `notebook.ipynb` land where they always did;
|
||||
the site and notebook are now assembled the same way a many-book document's are.
|
||||
"""
|
||||
from .document import Document, Output, Part, build
|
||||
|
||||
out = Path(out)
|
||||
slug = slug or Path(str(source)).name or "book"
|
||||
doc = Document(
|
||||
name=slug, title=slug, build=out, inline=True,
|
||||
notes=Path(notes) if notes else None,
|
||||
parts=[Part(name=slug, kind=kind, source=Path(source), out=out,
|
||||
exclude=tuple(exclude), overlay=overlay)],
|
||||
outputs=[Output("site", out / "site", style=style, theme=theme),
|
||||
Output("notebook", out / "notebook.ipynb", style=style, theme=theme)],
|
||||
)
|
||||
build(doc, quiet=quiet)
|
||||
book = doc.books[0]
|
||||
|
||||
for pr in doc.problems:
|
||||
if not quiet:
|
||||
print(f" note {pr}")
|
||||
m = book.measure()
|
||||
say(f" book {m['nodes']} nodes · {m['edges']} edges · "
|
||||
f"{m['external']} external · {len(m['artifacts'])} artifacts")
|
||||
for r in book.compare():
|
||||
say(f" {'ok ' if r['ok'] else 'LOST'} {r['claim']}")
|
||||
say(f" open {site_dir / 'index.html'}")
|
||||
say(f" {path}")
|
||||
if not quiet:
|
||||
print(f" book {m['nodes']} nodes · {m['edges']} edges · "
|
||||
f"{m['external']} external · {len(m['artifacts'])} artifacts")
|
||||
for r in book.compare():
|
||||
print(f" {'ok ' if r['ok'] else 'LOST'} {r['claim']}")
|
||||
print(f" open {out / 'site' / 'index.html'}")
|
||||
print(f" {out / 'book.json'}")
|
||||
return book
|
||||
|
||||
@@ -90,8 +90,9 @@ class Loaded:
|
||||
class Report:
|
||||
"""rig's reporting shape: sections, one line per decision, nothing aborts."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, subject: str = "book"):
|
||||
self.passed, self.failed, self.skipped = 0, [], 0
|
||||
self.subject = subject
|
||||
|
||||
def note(self, text: str) -> None:
|
||||
print(f"\n{text}")
|
||||
@@ -116,7 +117,7 @@ class Report:
|
||||
# it summarises — which is how this was found, piping to `tail`.
|
||||
sys.stdout.flush()
|
||||
if not self.failed:
|
||||
print(f"{self.passed} checks passed — this book still holds"
|
||||
print(f"{self.passed} checks passed — this {self.subject} still holds"
|
||||
+ (f", {self.skipped} skipped" if self.skipped else ""))
|
||||
return 0
|
||||
print(f"FAILED — {len(self.failed)} of {self.passed + len(self.failed)}: "
|
||||
@@ -204,6 +205,12 @@ def generated(book: Loaded, r: Report) -> None:
|
||||
r.check("the page admits the book is incomplete", True,
|
||||
"This book is incomplete" in html)
|
||||
|
||||
# A book built inline — its own site and notebook in its own directory —
|
||||
# also carries the document ledger, and with it the notes it used.
|
||||
doc_ledger = book.dir / "document.json"
|
||||
if doc_ledger.is_file():
|
||||
_notes_drift(json.loads(doc_ledger.read_text()), r)
|
||||
|
||||
r.note("the book is reproducible")
|
||||
# A timestamp would make two builds of an unchanged larder differ, which
|
||||
# destroys the only useful property a ledger has: that a diff means a real
|
||||
@@ -272,3 +279,99 @@ def _run_cells(cells: list, cwd: Path) -> tuple[int, str | None]:
|
||||
finally:
|
||||
os.chdir(previous)
|
||||
return ran, failure
|
||||
|
||||
|
||||
# -- documents --------------------------------------------------------------------
|
||||
|
||||
def _notes_drift(ledger: dict, r: Report) -> None:
|
||||
r.note("notes land where they are filed")
|
||||
# A note filed under a page that no longer exists has nowhere to go. It is
|
||||
# reported rather than dropped, because dropping it is how hand-written text
|
||||
# goes stale without anyone noticing — and this is the check that fails on it.
|
||||
drift = (ledger.get("notes") or {}).get("drift") or []
|
||||
r.check("no note is filed under a page or step that is gone", [], drift)
|
||||
|
||||
|
||||
def document(build_dir, r: Report) -> None:
|
||||
"""A document's own assertions: its books, its notes, every output it wrote."""
|
||||
import re
|
||||
|
||||
build_dir = Path(build_dir)
|
||||
ledger = json.loads((build_dir / "document.json").read_text())
|
||||
books = ledger.get("books") or []
|
||||
|
||||
r.note("the document is made of its books")
|
||||
r.check("it has at least one book", True, len(books) > 0)
|
||||
for b in books:
|
||||
r.check(f"book {b['name']} has its ledger", True, (Path(b["dir"]) / "book.json").is_file())
|
||||
|
||||
_notes_drift(ledger, r)
|
||||
|
||||
total_pages = sum(len(b.get("pages") or []) for b in books)
|
||||
for out in ledger.get("outputs") or []:
|
||||
path = Path(out["path"])
|
||||
if out["emitter"] == "notebook":
|
||||
r.note(f"the notebook — {path.name}")
|
||||
if not path.is_file():
|
||||
r.check(f"{path} exists", True, False)
|
||||
continue
|
||||
nb = json.loads(path.read_text())
|
||||
text = ["".join(c.get("source") or []) for c in nb.get("cells") or []]
|
||||
# Both ends of every part: the rule holds per book, not only overall.
|
||||
# Matched by book name, because the document's own closing cell is
|
||||
# also headed "— what came out" and a bare count mistook it for a part.
|
||||
def heading(name, end):
|
||||
return sum(1 for t in text if f"## {name} — what came {end}" in t)
|
||||
|
||||
r.check("every book part opens with what came in", [],
|
||||
[b["name"] for b in books if heading(b["name"], "in") != 1])
|
||||
r.check("every book part closes with what came out", [],
|
||||
[b["name"] for b in books if heading(b["name"], "out") != 1])
|
||||
figures = [c for c in nb.get("cells") or [] if '<a name="page-' in "".join(c.get("source") or [])]
|
||||
r.check("every page is a figure", total_pages, len(figures))
|
||||
drawn = sum(1 for c in figures if (c.get("attachments") or {}))
|
||||
if drawn < len(figures):
|
||||
r.skip("figures carry their SVG", f"{len(figures) - drawn} not drawn — Graphviz absent?")
|
||||
else:
|
||||
r.check("every figure carries its SVG, so it renders without running", True, True)
|
||||
elif out["emitter"] == "site":
|
||||
r.note(f"the site — {path}")
|
||||
index = path / "index.html"
|
||||
r.check("index.html exists", True, index.is_file())
|
||||
if not index.is_file():
|
||||
continue
|
||||
html = index.read_text()
|
||||
# Counted on the ledger's own labels, not the phrase: the page's
|
||||
# introduction says "what came in" too, and was counted as a book.
|
||||
r.check("the index shows what came in, for every book", len(books),
|
||||
html.count("<dt>what came in</dt>"))
|
||||
r.check("the index shows what came out, for every book", len(books),
|
||||
html.count("<dt>what came out</dt>"))
|
||||
manifest = path / ".docgen-output.json"
|
||||
written = json.loads(manifest.read_text()).get("written", []) if manifest.is_file() else []
|
||||
r.check("the output lists what it wrote", True, bool(written))
|
||||
r.check("every file it lists is there", [], [w for w in written if not (path / w).is_file()])
|
||||
# Every link on every page resolves — the drill-down is only worth
|
||||
# having if following it never lands on nothing.
|
||||
broken = []
|
||||
for page in sorted(path.glob("*/*.html")):
|
||||
for href in re.findall(r'href="([^"#?]+)', page.read_text()):
|
||||
if href.startswith(("http:", "https:", "mailto:")):
|
||||
continue
|
||||
if not (page.parent / href).exists():
|
||||
broken.append(f"{page.relative_to(path)} -> {href}")
|
||||
r.check("every link on every page opens something", [], broken[:10])
|
||||
pages = sum(1 for b in books for p in (b.get("pages") or [])
|
||||
if (path / b["name"] / f"{p['slug']}.html").is_file())
|
||||
r.check("every page has its own HTML", total_pages, pages)
|
||||
|
||||
|
||||
def document_failures(build_dir, *, quiet: bool = True) -> list[str]:
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
report = Report("document")
|
||||
sink = io.StringIO() if quiet else None
|
||||
with contextlib.redirect_stdout(sink) if sink else contextlib.nullcontext():
|
||||
document(build_dir, report)
|
||||
return list(report.failed)
|
||||
|
||||
@@ -71,8 +71,8 @@ SOURCES = {"root": "python", "schema": "db", "openapi": "openapi", "har": "usage
|
||||
READERS = ("python", "code")
|
||||
|
||||
TOP_KEYS = {"reference", "defaults", "book"}
|
||||
DEFAULT_KEYS = {"out", "style", "theme", "exclude", "reader"}
|
||||
BOOK_KEYS = {"name", "out", "style", "theme", "exclude", "reader", "overlay", *SOURCES}
|
||||
DEFAULT_KEYS = {"out", "style", "theme", "exclude", "reader", "notes"}
|
||||
BOOK_KEYS = {"name", "out", "style", "theme", "exclude", "reader", "overlay", "notes", *SOURCES}
|
||||
|
||||
# A book name becomes a directory name and a slug, so it is held to what is safe
|
||||
# as both — no separators, nothing that means something to a shell.
|
||||
@@ -99,6 +99,7 @@ class Entry:
|
||||
theme: str | None = None
|
||||
overlay: Path | None = None
|
||||
exclude: tuple = ()
|
||||
notes: Path | None = None
|
||||
|
||||
def line(self) -> str:
|
||||
return f"{self.name:<18} {self.kind:<8} {self.source} -> {self.out}"
|
||||
@@ -137,18 +138,38 @@ def _within(inner: Path, outer: Path) -> bool:
|
||||
return inner == outer or outer in inner.parents
|
||||
|
||||
|
||||
def load(path) -> RunFile:
|
||||
"""Read and resolve a run file. Raises ConfigError listing every problem."""
|
||||
path = Path(path)
|
||||
def _read(path: Path) -> dict:
|
||||
if tomllib is None:
|
||||
raise ConfigError(path, ["run files need Python 3.11+ (tomllib)"])
|
||||
try:
|
||||
data = tomllib.loads(path.read_text())
|
||||
return tomllib.loads(path.read_text())
|
||||
except OSError as e:
|
||||
raise ConfigError(path, [f"cannot read: {e}"]) from None
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
raise ConfigError(path, [f"not valid TOML: {e}"]) from None
|
||||
|
||||
|
||||
def load(path):
|
||||
"""Read and resolve a run file of either shape. Raises ConfigError listing
|
||||
every problem.
|
||||
|
||||
Returns a `RunFile` for a `[[book]]` list, or a `ProjectFile` for a project.
|
||||
A file with both is refused: which one it meant is exactly what nobody should
|
||||
have to guess.
|
||||
"""
|
||||
path = Path(path)
|
||||
data = _read(path)
|
||||
if "project" in data and "book" in data:
|
||||
raise ConfigError(path, [
|
||||
"this file has both [project] and [[book]] — use one shape per file: "
|
||||
"[project] for one document built from several sources, [[book]] for a "
|
||||
"list of independent books"])
|
||||
if "project" in data:
|
||||
return load_project(path, data)
|
||||
return _load_books(path, data)
|
||||
|
||||
|
||||
def _load_books(path: Path, data: dict) -> "RunFile":
|
||||
base = path.resolve().parent
|
||||
problems: list[str] = []
|
||||
|
||||
@@ -246,10 +267,12 @@ def load(path) -> RunFile:
|
||||
if source_key == "root" and source.is_dir() and _within(out, source):
|
||||
problems.append(f"{where}: out {out} is inside the tree it reads ({source})")
|
||||
|
||||
notes = _path(pick("notes", "notes"), base)
|
||||
|
||||
books.append(Entry(
|
||||
name=name, kind=kind, source=source, out=out,
|
||||
style=pick("style", "lucid"), theme=pick("theme"),
|
||||
overlay=overlay, exclude=tuple(exclude),
|
||||
overlay=overlay, exclude=tuple(exclude), notes=notes,
|
||||
))
|
||||
|
||||
seen_names, seen_outs = {}, {}
|
||||
@@ -282,13 +305,30 @@ def apply_reference(runfile: RunFile) -> str | None:
|
||||
return str(runfile.reference)
|
||||
|
||||
|
||||
def run(runfile: RunFile, names=None, *, check: bool = False, quiet: bool = True):
|
||||
def _checks(directory, quiet: bool) -> list[str]:
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
from . import checks as checks_mod
|
||||
|
||||
report = checks_mod.Report()
|
||||
loaded = checks_mod.Loaded(directory)
|
||||
sink = io.StringIO() if quiet else None
|
||||
with contextlib.redirect_stdout(sink) if sink else contextlib.nullcontext():
|
||||
checks_mod.generated(loaded, report)
|
||||
checks_mod.custom(loaded, report)
|
||||
return list(report.failed)
|
||||
|
||||
|
||||
def run(runfile, names=None, *, check: bool = False, quiet: bool = True):
|
||||
"""Build every selected book, in order. One failure never stops the rest.
|
||||
|
||||
Returns one result per book:
|
||||
Returns one result per book, plus one per output for a project:
|
||||
{"name", "out", "ok", "error", "lost": [claims], "checks_failed": [names]}
|
||||
"""
|
||||
from . import checks as checks_mod
|
||||
if isinstance(runfile, ProjectFile):
|
||||
return run_project(runfile, names, check=check, quiet=quiet)
|
||||
|
||||
from .build import run as build
|
||||
|
||||
apply_reference(runfile)
|
||||
@@ -303,7 +343,7 @@ def run(runfile: RunFile, names=None, *, check: bool = False, quiet: bool = True
|
||||
overlay = spec_mod.load(entry.overlay)
|
||||
book = build(entry.kind, entry.source, entry.out, slug=entry.name,
|
||||
style=entry.style, theme=entry.theme, exclude=entry.exclude,
|
||||
overlay=overlay, quiet=quiet)
|
||||
overlay=overlay, notes=entry.notes, quiet=quiet)
|
||||
result["lost"] = [r["claim"] for r in book.compare() if not r["ok"]]
|
||||
except Exception as e: # noqa: BLE001 - one bad book must not cost the run
|
||||
result["error"] = f"{type(e).__name__}: {e}"
|
||||
@@ -311,17 +351,265 @@ def run(runfile: RunFile, names=None, *, check: bool = False, quiet: bool = True
|
||||
continue
|
||||
|
||||
if check:
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
report = checks_mod.Report()
|
||||
loaded = checks_mod.Loaded(entry.out)
|
||||
sink = io.StringIO() if quiet else None
|
||||
with contextlib.redirect_stdout(sink) if sink else contextlib.nullcontext():
|
||||
checks_mod.generated(loaded, report)
|
||||
checks_mod.custom(loaded, report)
|
||||
result["checks_failed"] = list(report.failed)
|
||||
result["checks_failed"] = _checks(entry.out, quiet)
|
||||
|
||||
result["ok"] = not result["lost"] and not result["checks_failed"]
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
# -- the project shape ----------------------------------------------------------
|
||||
#
|
||||
# One file per project, the shape a model writes unprompted:
|
||||
#
|
||||
# [project] name, title, notes, build, reference
|
||||
# [extract.<kind>] one book per table; [[extract.<kind>]] for several
|
||||
# [ops] applied to every book's view, before paging
|
||||
# [emit.<output>] notebook, site — each assembles every book
|
||||
#
|
||||
# It compiles to the same Document a [[book]] entry does, so everything after
|
||||
# loading is one code path.
|
||||
|
||||
PROJECT_KEYS = {"name", "title", "notes", "build", "reference"}
|
||||
PROJECT_TOP = {"project", "extract", "ops", "emit"}
|
||||
EXTRACT_KEYS = {
|
||||
"python": {"name", "root", "exclude", "overlay"},
|
||||
"code": {"name", "root", "ext", "exclude", "overlay"},
|
||||
"db": {"name", "schema", "overlay"},
|
||||
"openapi": {"name", "spec", "overlay"},
|
||||
"usage": {"name", "har", "overlay"},
|
||||
}
|
||||
EXTRACT_SOURCE = {"python": "root", "code": "root", "db": "schema",
|
||||
"openapi": "spec", "usage": "har"}
|
||||
OPS_KEYS = {"drop_stdlib", "drop_builtins", "drop_external", "only", "drop"}
|
||||
EMITTERS = {"notebook", "site"}
|
||||
EMIT_KEYS = {"output", "style", "theme", "width"}
|
||||
|
||||
# Keys a model reaches for that docgen deliberately does not do, with what to do
|
||||
# instead. Refused with the reason rather than with a bare "unknown key", because
|
||||
# the reason is the useful half of the refusal.
|
||||
NOT_SUPPORTED = {
|
||||
("db", "url"): (
|
||||
"docgen reads a schema dump and never connects to a database — and a URL in "
|
||||
"a committed file is a credential in a committed file. Write the schema with "
|
||||
"`modelgen from-db`, then set `schema = \"path/to/schema.json\"`"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProjectFile:
|
||||
path: Path
|
||||
document: object # book.document.Document
|
||||
reference: Path | None = None
|
||||
|
||||
@property
|
||||
def books(self):
|
||||
return self.document.parts
|
||||
|
||||
|
||||
def load_project(path: Path, data: dict) -> ProjectFile:
|
||||
from ..extractors.code import LANGUAGES
|
||||
from .document import Document, Output, Part
|
||||
|
||||
base = path.resolve().parent
|
||||
problems: list[str] = []
|
||||
|
||||
for key in sorted(set(data) - PROJECT_TOP):
|
||||
problems.append(f"unknown top-level key {key!r} — a project file has: "
|
||||
f"{', '.join(sorted(PROJECT_TOP))}")
|
||||
|
||||
project = data.get("project") or {}
|
||||
for key in sorted(set(project) - PROJECT_KEYS):
|
||||
problems.append(f"[project] has unknown key {key!r} — have: {', '.join(sorted(PROJECT_KEYS))}")
|
||||
name = project.get("name")
|
||||
if not isinstance(name, str) or not NAME.match(name):
|
||||
problems.append("[project] needs a name of letters, digits, '.', '_' or '-'")
|
||||
name = "project"
|
||||
title = project.get("title") or name
|
||||
notes = _path(project.get("notes", "notes"), base)
|
||||
build_root = _path(project.get("build", ".docgen"), base)
|
||||
reference = _path(project["reference"], base) if "reference" in project else None
|
||||
if reference is not None and not reference.is_dir():
|
||||
problems.append(f"[project] reference {reference} is not a directory")
|
||||
|
||||
# -- extract ----------------------------------------------------------------
|
||||
extract = data.get("extract") or {}
|
||||
if not isinstance(extract, dict) or not extract:
|
||||
problems.append("no sources — add at least one [extract.<kind>] table "
|
||||
f"({', '.join(sorted(EXTRACT_KEYS))})")
|
||||
extract = {}
|
||||
parts, names = [], set()
|
||||
for kind, tables in extract.items():
|
||||
if kind not in EXTRACT_KEYS:
|
||||
problems.append(f"[extract.{kind}]: no reader named {kind!r} — have: "
|
||||
f"{', '.join(sorted(EXTRACT_KEYS))}")
|
||||
continue
|
||||
tables = tables if isinstance(tables, list) else [tables]
|
||||
for i, raw in enumerate(tables):
|
||||
where = f"[extract.{kind}]" + (f"[{i}]" if len(tables) > 1 else "")
|
||||
if not isinstance(raw, dict):
|
||||
problems.append(f"{where} must be a table")
|
||||
continue
|
||||
for key in sorted(set(raw) - EXTRACT_KEYS[kind]):
|
||||
reason = NOT_SUPPORTED.get((kind, key))
|
||||
problems.append(f"{where} {key}: {reason}" if reason else
|
||||
f"{where} has unknown key {key!r} — have: "
|
||||
f"{', '.join(sorted(EXTRACT_KEYS[kind]))}")
|
||||
book = raw.get("name") or (kind if len(tables) == 1 else None)
|
||||
if not isinstance(book, str) or not NAME.match(book):
|
||||
problems.append(f"{where} needs a name when there are several "
|
||||
f"[[extract.{kind}]] tables")
|
||||
continue
|
||||
if book in names:
|
||||
problems.append(f"{where}: a book named {book!r} already exists — set `name`")
|
||||
continue
|
||||
names.add(book)
|
||||
|
||||
src_key = EXTRACT_SOURCE[kind]
|
||||
if src_key not in raw:
|
||||
problems.append(f"{where} needs `{src_key}`")
|
||||
continue
|
||||
source = _path(raw[src_key], base)
|
||||
if src_key == "root":
|
||||
if not source.is_dir():
|
||||
problems.append(f"{where}: root {source} is not a directory")
|
||||
elif not source.is_file():
|
||||
problems.append(f"{where}: {src_key} {source} is not a file")
|
||||
|
||||
exclude = raw.get("exclude", [])
|
||||
if not isinstance(exclude, list) or not all(isinstance(x, str) for x in exclude):
|
||||
problems.append(f"{where}: exclude must be a list of directory names")
|
||||
exclude = []
|
||||
|
||||
ext = raw.get("ext")
|
||||
if ext is not None:
|
||||
if not isinstance(ext, list) or not all(isinstance(x, str) for x in ext):
|
||||
problems.append(f"{where}: ext must be a list like [\".cs\", \".ts\"]")
|
||||
else:
|
||||
missing = [x for x in ext if x not in LANGUAGES]
|
||||
if missing:
|
||||
problems.append(
|
||||
f"{where}: no grammar for {', '.join(missing)} — code reads "
|
||||
f"{', '.join(sorted(LANGUAGES))}. Shell, YAML and SQL have no "
|
||||
"reader yet; leave them out rather than expect them in the book")
|
||||
|
||||
overlay = None
|
||||
if "overlay" in raw:
|
||||
overlay_path = _path(raw["overlay"], base)
|
||||
if not overlay_path.is_file():
|
||||
problems.append(f"{where}: overlay {overlay_path} is not a file")
|
||||
else:
|
||||
import json
|
||||
overlay = json.loads(overlay_path.read_text())
|
||||
|
||||
out = build_root / name / "books" / book
|
||||
if src_key == "root" and source.is_dir() and _within(out, source):
|
||||
problems.append(f"{where}: build directory {out} is inside the tree it reads "
|
||||
f"({source}) — set [project] build outside it")
|
||||
parts.append(Part(name=book, kind=kind, source=source, out=out,
|
||||
exclude=tuple(exclude), overlay=overlay,
|
||||
suffixes=tuple(ext) if ext else None))
|
||||
|
||||
# -- ops --------------------------------------------------------------------
|
||||
ops = data.get("ops") or {}
|
||||
for key in sorted(set(ops) - OPS_KEYS):
|
||||
problems.append(f"[ops] has unknown key {key!r} — have: {', '.join(sorted(OPS_KEYS))}")
|
||||
for key in ("only", "drop"):
|
||||
if key in ops and not (isinstance(ops[key], list)
|
||||
and all(isinstance(x, str) for x in ops[key])):
|
||||
problems.append(f"[ops] {key} must be a list of kinds")
|
||||
for part in parts:
|
||||
part.ops = {k: v for k, v in ops.items() if k in OPS_KEYS}
|
||||
|
||||
# -- emit -------------------------------------------------------------------
|
||||
emit = data.get("emit") or {}
|
||||
outputs = []
|
||||
if not emit:
|
||||
problems.append("no outputs — add [emit.notebook] or [emit.site] with an `output`")
|
||||
for emitter, raw in emit.items():
|
||||
where = f"[emit.{emitter}]"
|
||||
if emitter not in EMITTERS:
|
||||
problems.append(f"{where}: no output named {emitter!r} — have: "
|
||||
f"{', '.join(sorted(EMITTERS))}")
|
||||
continue
|
||||
if not isinstance(raw, dict):
|
||||
problems.append(f"{where} must be a table")
|
||||
continue
|
||||
for key in sorted(set(raw) - EMIT_KEYS):
|
||||
problems.append(f"{where} has unknown key {key!r} — have: {', '.join(sorted(EMIT_KEYS))}")
|
||||
if "output" not in raw:
|
||||
problems.append(f"{where} needs `output`")
|
||||
continue
|
||||
width = raw.get("width")
|
||||
if width is not None and (not isinstance(width, int) or width < 200):
|
||||
problems.append(f"{where}: width must be a number of pixels, 200 or more")
|
||||
width = None
|
||||
target = _path(raw["output"], base)
|
||||
for part in parts:
|
||||
if part.kind in ("python", "code") and part.source.is_dir() \
|
||||
and _within(target, part.source):
|
||||
problems.append(f"{where}: output {target} is inside the tree book "
|
||||
f"{part.name!r} reads — the next run would read it")
|
||||
outputs.append(Output(emitter=emitter, path=target, style=raw.get("style", "lucid"),
|
||||
theme=raw.get("theme"), width=width))
|
||||
|
||||
if problems:
|
||||
raise ConfigError(path, problems)
|
||||
doc = Document(name=name, title=title, build=build_root / name, parts=parts,
|
||||
outputs=outputs, notes=notes)
|
||||
return ProjectFile(path=path, document=doc, reference=reference)
|
||||
|
||||
|
||||
def select_parts(project: ProjectFile, names) -> list:
|
||||
if not names:
|
||||
return list(project.document.parts)
|
||||
known = {p.name for p in project.document.parts}
|
||||
unknown = [n for n in names if n not in known]
|
||||
if unknown:
|
||||
raise ConfigError(project.path, [
|
||||
f"no book named {n!r} — have: {', '.join(sorted(known))}" for n in unknown])
|
||||
return [p for p in project.document.parts if p.name in set(names)]
|
||||
|
||||
|
||||
def run_project(project: ProjectFile, names=None, *, check: bool = False, quiet: bool = True):
|
||||
"""Build a project's document: every book, then every output.
|
||||
|
||||
`--only` narrows the books that go into this build's outputs, which is useful
|
||||
while writing notes for one of them — the outputs are then that subset, and
|
||||
say so in their ledger.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
|
||||
from .document import build
|
||||
|
||||
apply_reference(project)
|
||||
doc = replace(project.document, parts=select_parts(project, names), books=[], problems=[])
|
||||
results = []
|
||||
try:
|
||||
build(doc, quiet=quiet)
|
||||
except Exception as e: # noqa: BLE001 - reported, not raised
|
||||
return [{"name": doc.name, "out": str(doc.build), "ok": False,
|
||||
"error": f"{type(e).__name__}: {e}", "lost": [], "checks_failed": []}]
|
||||
|
||||
for part in doc.parts:
|
||||
if part.name in doc.failed:
|
||||
results.append({"name": part.name, "out": str(part.out), "ok": False,
|
||||
"error": doc.failed[part.name], "lost": [], "checks_failed": []})
|
||||
for book in doc.books:
|
||||
lost = [r["claim"] for r in book.compare() if not r["ok"]]
|
||||
failed = _checks(book.out, quiet) if check else []
|
||||
results.append({"name": book.slug, "out": str(book.out), "ok": not lost and not failed,
|
||||
"error": None, "lost": lost, "checks_failed": failed})
|
||||
if check and doc.books:
|
||||
from . import checks as checks_mod
|
||||
failed = checks_mod.document_failures(doc.build, quiet=quiet)
|
||||
else:
|
||||
failed = []
|
||||
# A failed book is already its own row; the document row carries what else
|
||||
# went wrong — notes that drifted, an overlay that no longer matches.
|
||||
rest = [p for p in doc.problems if not p.startswith("book ") or "was not built" not in p]
|
||||
results.append({"name": f"{doc.name} (document)", "out": str(doc.build),
|
||||
"ok": not rest and not failed and bool(doc.books), "error": None,
|
||||
"lost": rest, "checks_failed": failed})
|
||||
return results
|
||||
|
||||
448
soleprint/atlas2/docgen/book/document.py
Normal file
448
soleprint/atlas2/docgen/book/document.py
Normal file
@@ -0,0 +1,448 @@
|
||||
"""
|
||||
A document: several books, assembled into each output.
|
||||
|
||||
Document
|
||||
├── books one per source — each keeps its own larder → steps → book
|
||||
├── notes hand-written markdown, filed by page (book/notes.py)
|
||||
└── outputs a notebook, a site, … — each one made of every book, as parts
|
||||
|
||||
A book is a templated part of the final output, not the output itself. A project
|
||||
with an OpenAPI contract and a C# tree is one document of two books: one notebook
|
||||
and one site, each with a part per book. A `[[book]]` run file is the same thing
|
||||
with one book per document, which is what it always was.
|
||||
|
||||
## Built once, rendered per output
|
||||
|
||||
Extraction, the view and the page structure happen once per book. Rendering
|
||||
does not, because what a page looks like depends on where it is going: a site
|
||||
column is narrower than a notebook cell, a site's boxes are links and a
|
||||
notebook's cannot be, and each output can have its own theme. So each output
|
||||
lays the pages out for its own width and draws them itself.
|
||||
|
||||
## Both ends, at both levels
|
||||
|
||||
Every book part still opens with what came in and closes with what came out. A
|
||||
document of several books also opens with every larder and closes with every
|
||||
reconciliation, so the rule — any operation is measured at both ends — holds for
|
||||
the whole as well as for each part.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from . import Book
|
||||
from .notes import Notes, data_uri, images
|
||||
from .larder import Larder
|
||||
|
||||
MANIFEST = ".docgen-output.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Output:
|
||||
emitter: str # "notebook" | "site"
|
||||
path: Path
|
||||
style: str = "lucid"
|
||||
theme: str | None = None
|
||||
width: int | None = None # px; each emitter has its own default
|
||||
|
||||
|
||||
@dataclass
|
||||
class Part:
|
||||
"""One book to build: a source, and where its artifacts go."""
|
||||
|
||||
name: str
|
||||
kind: str
|
||||
source: Path
|
||||
out: Path
|
||||
exclude: tuple = ()
|
||||
overlay: dict | None = None
|
||||
ops: dict = field(default_factory=dict)
|
||||
suffixes: tuple | None = None # code only: which extensions to read
|
||||
|
||||
|
||||
@dataclass
|
||||
class Document:
|
||||
name: str
|
||||
title: str
|
||||
build: Path # where document.json goes
|
||||
parts: list[Part]
|
||||
outputs: list[Output]
|
||||
notes: Path | None = None
|
||||
inline: bool = False # a single [[book]]: outputs live inside its directory
|
||||
books: list[Book] = field(default_factory=list)
|
||||
problems: list[str] = field(default_factory=list)
|
||||
failed: dict = field(default_factory=dict) # book name -> why it was not built
|
||||
|
||||
|
||||
# -- views --------------------------------------------------------------------
|
||||
|
||||
OPS = ("drop_stdlib", "drop_builtins", "drop_external", "only", "drop")
|
||||
|
||||
|
||||
def apply_ops(view: dict, ops: dict) -> dict:
|
||||
"""A project's `[ops]`, on top of the default view. Unknown keys never get here."""
|
||||
from ..ops import filter as F
|
||||
|
||||
if ops.get("drop_stdlib"):
|
||||
view = F.drop_stdlib(view)
|
||||
if ops.get("drop_builtins"):
|
||||
view = F.drop_builtins(view)
|
||||
if ops.get("drop_external"):
|
||||
view = F.drop_external(view)
|
||||
if ops.get("drop"):
|
||||
view = F.drop_kinds(view, ops["drop"])
|
||||
if ops.get("only"):
|
||||
view = F.only_kinds(view, ops["only"])
|
||||
return view
|
||||
|
||||
|
||||
# -- output bookkeeping -------------------------------------------------------
|
||||
|
||||
def clear_output(directory: Path) -> list[str]:
|
||||
"""Remove exactly what the last build wrote into an output directory.
|
||||
|
||||
A page that disappeared between builds would otherwise linger — still on disk,
|
||||
still linked from nowhere, still found by search. The manifest lists every file
|
||||
written; anything not in it, including a hand-written file someone dropped in,
|
||||
is left alone.
|
||||
"""
|
||||
manifest = directory / MANIFEST
|
||||
if not manifest.is_file():
|
||||
return []
|
||||
try:
|
||||
written = json.loads(manifest.read_text()).get("written", [])
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return []
|
||||
removed = []
|
||||
for rel in written:
|
||||
target = (directory / rel)
|
||||
if target.is_file() and directory in target.resolve().parents:
|
||||
target.unlink()
|
||||
removed.append(rel)
|
||||
# Directories docgen made and has now emptied — never one with anything left.
|
||||
for d in sorted({(directory / r).parent for r in written}, key=lambda p: -len(p.parts)):
|
||||
if d != directory and d.is_dir() and not any(d.iterdir()):
|
||||
d.rmdir()
|
||||
manifest.unlink()
|
||||
return removed
|
||||
|
||||
|
||||
def write_manifest(directory: Path, paths: list[Path]) -> None:
|
||||
rel = sorted({p.relative_to(directory).as_posix() for p in paths if p.is_file()})
|
||||
(directory / MANIFEST).write_text(json.dumps({"written": rel}, indent=2) + "\n")
|
||||
|
||||
|
||||
# -- pages, rendered ----------------------------------------------------------
|
||||
|
||||
def rendered_pages(book: Book, style, width: int, *, url=None):
|
||||
"""[(page, svg or None)] laid out for `width`. `url(page)` makes boxes links."""
|
||||
from ..emitters.paged import fits_for, links_for, render
|
||||
from ..ops.pages import layout
|
||||
|
||||
fits, measured = fits_for(style, width)
|
||||
laid = layout(book.pages, fits)
|
||||
known = {n["id"] for n in book.ir["nodes"]}
|
||||
out = []
|
||||
for page in laid:
|
||||
links = None
|
||||
if url is not None:
|
||||
links = links_for(laid, page, url)
|
||||
# A box with no page of its own opens its section of the reference
|
||||
# page, where its docstring and file are — when it is a real node.
|
||||
for n in page["ir"]["nodes"]:
|
||||
if n["id"] not in links and n["id"] in known \
|
||||
and not (n.get("attrs") or {}).get("context"):
|
||||
links[n["id"]] = f"reference.html#{n['id']}"
|
||||
page["measured"] = measured
|
||||
out.append((page, render(page, style, width, links=links)))
|
||||
return out
|
||||
|
||||
|
||||
def caption(page: dict) -> str:
|
||||
nodes = page["ir"]["nodes"]
|
||||
parts = [f"{len(nodes)} box{'es' if len(nodes) != 1 else ''}",
|
||||
f"{len(page['ir']['edges'])} relationship{'s' if len(page['ir']['edges']) != 1 else ''}"]
|
||||
context = sum(1 for n in nodes if (n.get("attrs") or {}).get("context"))
|
||||
if context:
|
||||
parts.append(f"{context} outside this subsystem")
|
||||
if page.get("children"):
|
||||
parts.append(f"opens into {len(page['children'])} page{'s' if len(page['children']) != 1 else ''}")
|
||||
if (page.get("options") or {}).get("rankdir") == "LR":
|
||||
parts.append("drawn left to right to fit the width")
|
||||
if page.get("too_wide"):
|
||||
parts.append("still wider than the page — this level has no smaller structure to split into")
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
# -- the notebook ---------------------------------------------------------------
|
||||
|
||||
def _note_step(text: str, base: Path) -> tuple[str, dict]:
|
||||
"""A note for a notebook cell: local images become cell attachments."""
|
||||
attachments = {}
|
||||
for ref, name, mime, data in images(text, base):
|
||||
import base64
|
||||
text = text.replace(f"]({ref})", f"](attachment:{name})")
|
||||
attachments[name] = {mime: base64.b64encode(data).decode("ascii")}
|
||||
return text, attachments
|
||||
|
||||
|
||||
def _with_note(step: dict, notes: Notes, book: str | None, spot: str) -> dict:
|
||||
text = notes.book(book, spot) if book else notes.document(spot)
|
||||
if text is None:
|
||||
return step
|
||||
path = notes.path(book, spot)
|
||||
text, attachments = _note_step(text, path.parent)
|
||||
step = dict(step)
|
||||
step["before"] = text + ("\n\n" + step["before"] if step.get("before") else "")
|
||||
if attachments:
|
||||
step["before_attachments"] = {**(step.get("before_attachments") or {}), **attachments}
|
||||
return step
|
||||
|
||||
|
||||
def notebook_spec(doc: Document, notes: Notes, output: Output, notebook_dir: Path) -> dict:
|
||||
"""Every book's part, in order, with the document's own ends when there are several."""
|
||||
from ..emitters.paged import NOTEBOOK_WIDTH
|
||||
from ..notebook import spec as spec_mod
|
||||
from ..ops.pages import slugify
|
||||
from ..style import Style
|
||||
from .build import spec_from
|
||||
|
||||
style = Style.load(output.style, theme=output.theme)
|
||||
width = output.width or NOTEBOOK_WIDTH
|
||||
several = len(doc.books) > 1
|
||||
steps: list[dict] = []
|
||||
|
||||
missing = ""
|
||||
if doc.failed:
|
||||
missing = ("\n\n**This document is incomplete** — these books could not be built "
|
||||
"and are not below:\n\n"
|
||||
+ "\n".join(f"- `{n}` — {why}" for n, why in sorted(doc.failed.items())))
|
||||
if several or doc.failed:
|
||||
lines = "\n".join(f"- **{b.slug}** — {b.larder.line()}" for b in doc.books)
|
||||
steps.append(spec_mod._step(
|
||||
"document", "md", title=doc.title,
|
||||
text=(f"Built from {len(doc.books)} book(s). Each part below opens with what "
|
||||
f"came in and closes with what came out.\n\n{lines}{missing}")))
|
||||
|
||||
for i, book in enumerate(doc.books):
|
||||
rel = os.path.relpath(book.out, notebook_dir)
|
||||
base = spec_from(book, book.ir, prelude_dir=None if doc.inline else rel)
|
||||
base_steps = base["steps"]
|
||||
|
||||
figures = []
|
||||
index_lines = []
|
||||
for page, svg in rendered_pages(book, style, width):
|
||||
anchor = f"page-{slugify(book.slug)}-{page['slug']}"
|
||||
index_lines.append(f"{' ' * page['depth']}- [{page['title']}](#{anchor})"
|
||||
+ (" — *too wide*" if page.get("too_wide") else ""))
|
||||
figures.append(spec_mod._step(
|
||||
f"page-{page['slug']}", "figure", title=page["title"], anchor=anchor,
|
||||
depth=page["depth"], svg=svg, name=f"{page['slug']}.svg",
|
||||
caption=caption(page),
|
||||
why="Graphviz is not installed" if svg is None else None))
|
||||
index = spec_mod._step("pages", "md", title=f"{book.slug} — pages",
|
||||
text="Each page is one subsystem, drawn small enough to "
|
||||
"read. Follow a link to jump to it.\n\n"
|
||||
+ "\n".join(index_lines))
|
||||
|
||||
ordered = []
|
||||
for step in base_steps:
|
||||
if step["id"] == "book":
|
||||
ordered.extend(figures)
|
||||
ordered.append(step)
|
||||
if step["id"] == "larder":
|
||||
ordered.append(index)
|
||||
|
||||
spec, problems = spec_mod.merge({"meta": {}, "steps": ordered},
|
||||
doc.parts_built[i].overlay)
|
||||
doc.problems += [f"{book.slug}: overlay: {p}" for p in problems]
|
||||
|
||||
# Notes are applied after the overlay, so an overlay that moves a step moves
|
||||
# its note with it. A page's note is filed under the page's slug; every
|
||||
# other step under its own id. `_with_note` prepends, so applying the book's
|
||||
# `_intro` last puts it above the larder's own note.
|
||||
part = []
|
||||
for n, step in enumerate(spec["steps"]):
|
||||
sid = step["id"]
|
||||
spot = sid[len("page-"):] if step.get("kind") == "figure" else slugify(sid)
|
||||
step = _with_note(step, notes, book.slug, spot)
|
||||
if n == 0:
|
||||
step = _with_note(step, notes, book.slug, "_intro")
|
||||
if several:
|
||||
step = dict(step, id=f"{book.slug}/{sid}")
|
||||
part.append(step)
|
||||
steps.extend(part)
|
||||
|
||||
if several:
|
||||
lines = []
|
||||
for b in doc.books:
|
||||
m = b.measure()
|
||||
lines.append(f"- **{b.slug}** — {m['nodes']} nodes · {m['edges']} edges · "
|
||||
f"{m['external']} external")
|
||||
lines += [f" - {'✓' if r['ok'] else '✗'} {r['claim']}" for r in b.compare()]
|
||||
steps.append(spec_mod._step("document-end", "md", title=f"{doc.title} — what came out",
|
||||
text="\n".join(lines)))
|
||||
if steps:
|
||||
steps[0] = _with_note(steps[0], notes, None, "_intro")
|
||||
end = notes.document("_end")
|
||||
if end is not None:
|
||||
text, att = _note_step(end, notes.path(None, "_end").parent)
|
||||
steps.append(spec_mod._step("_end", "md", text=text,
|
||||
**({"before_attachments": att} if att else {})))
|
||||
return {"version": spec_mod.SPEC_VERSION, "steps": steps}
|
||||
|
||||
|
||||
# -- building -----------------------------------------------------------------
|
||||
|
||||
def build(doc: Document, *, quiet: bool = True) -> Document:
|
||||
"""Build every book, then every output. Returns the document, ledgers written."""
|
||||
from ..emitters.notebook import write as nb_write
|
||||
from ..ops.pages import slugify
|
||||
from ..style import Style
|
||||
from .build import build_book
|
||||
|
||||
def say(text):
|
||||
if not quiet:
|
||||
print(text)
|
||||
|
||||
notes = Notes(doc.notes)
|
||||
doc.books, doc.failed = [], {}
|
||||
built_parts = []
|
||||
for part in doc.parts:
|
||||
# One book that cannot be built must not cost the others — the same rule a
|
||||
# run of independent books follows. The outputs are assembled from what
|
||||
# was built, and the ledger names what was not and why, so a partial
|
||||
# document is visibly partial rather than quietly short.
|
||||
try:
|
||||
book = build_book(part.kind, part.source, part.out, slug=part.name,
|
||||
exclude=part.exclude, ops=part.ops, suffixes=part.suffixes,
|
||||
quiet=quiet)
|
||||
except Exception as e: # noqa: BLE001 - recorded, not raised
|
||||
reason = f"{type(e).__name__}: {str(e).splitlines()[0] if str(e) else ''}"
|
||||
doc.failed[part.name] = reason
|
||||
doc.problems.append(f"book {part.name!r} was not built — {reason}")
|
||||
say(f" FAIL {part.name}: {reason}")
|
||||
continue
|
||||
doc.books.append(book)
|
||||
built_parts.append(part)
|
||||
doc.parts_built = built_parts
|
||||
|
||||
# Sites first: in a single inline book the site is its last step's artifact,
|
||||
# and the notebook quotes the book measure — which only counts the site once
|
||||
# the last step exists.
|
||||
sites = [o for o in doc.outputs if o.emitter == "site"]
|
||||
notebooks = [o for o in doc.outputs if o.emitter == "notebook"]
|
||||
written_outputs = []
|
||||
|
||||
for output in sites:
|
||||
from ..emitters import docsite
|
||||
|
||||
site_dir = Path(output.path)
|
||||
clear_output(site_dir)
|
||||
style = Style.load(output.style, theme=output.theme)
|
||||
width = output.width or docsite.CONTENT_WIDTH
|
||||
books = []
|
||||
for book in doc.books:
|
||||
pages = rendered_pages(book, style, width, url=lambda p: f"{p['slug']}.html")
|
||||
page_notes = {}
|
||||
for page, _ in pages:
|
||||
text = notes.book(book.slug, page["slug"])
|
||||
if text is not None:
|
||||
base = notes.path(book.slug, page["slug"]).parent
|
||||
page_notes[page["slug"]] = (text, {ref: data_uri(mime, data)
|
||||
for ref, _, mime, data in images(text, base)})
|
||||
intro = notes.book(book.slug, "_intro")
|
||||
books.append({"book": book, "pages": pages, "notes": page_notes,
|
||||
"intro": (intro, {}) if intro else None})
|
||||
doc_intro = notes.document("_intro")
|
||||
doc_end = notes.document("_end")
|
||||
files = docsite.write(site_dir, style, title=doc.title, books=books,
|
||||
intro=doc_intro, end=doc_end, failed=doc.failed)
|
||||
write_manifest(site_dir, files)
|
||||
written_outputs.append({"emitter": "site", "path": str(site_dir), "width": width,
|
||||
"files": len(files)})
|
||||
say(f" site {site_dir / 'index.html'}")
|
||||
|
||||
for book in doc.books:
|
||||
inside = [o for o in sites if doc.inline and Path(o.path).parent == book.out]
|
||||
book.close(site=Path(inside[0].path) if inside else None)
|
||||
|
||||
for output in notebooks:
|
||||
path = Path(output.path)
|
||||
if path.suffix != ".ipynb":
|
||||
clear_output(path)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
target = path / f"{slugify(doc.name)}.ipynb"
|
||||
else:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target = path
|
||||
spec = notebook_spec(doc, notes, output, target.parent)
|
||||
nb_write(spec, target)
|
||||
if path.suffix != ".ipynb":
|
||||
write_manifest(path, [target])
|
||||
if doc.inline and len(doc.books) == 1:
|
||||
doc.books[0].notebook = os.path.relpath(target, doc.books[0].out)
|
||||
written_outputs.append({"emitter": "notebook", "path": str(target),
|
||||
"width": output.width or 960})
|
||||
say(f" notebook {target}")
|
||||
|
||||
known = {b.slug: known_spots(b) for b in doc.books}
|
||||
drift = notes.drift(known)
|
||||
doc.problems += drift
|
||||
|
||||
for book in doc.books:
|
||||
book.write()
|
||||
|
||||
ledger = {
|
||||
"name": doc.name,
|
||||
"title": doc.title,
|
||||
"failed": doc.failed,
|
||||
"books": [{"name": b.slug, "dir": str(b.out),
|
||||
"pages": [{"id": p["id"], "slug": p["slug"], "parent": p["parent"]}
|
||||
for p in b.pages]}
|
||||
for b in doc.books],
|
||||
"outputs": written_outputs,
|
||||
"notes": {"root": str(doc.notes) if doc.notes else None,
|
||||
"used": sorted(str(p.relative_to(doc.notes)) for p in notes.used) if doc.notes else [],
|
||||
"drift": drift},
|
||||
"problems": doc.problems,
|
||||
}
|
||||
doc.build.mkdir(parents=True, exist_ok=True)
|
||||
(doc.build / "document.json").write_text(json.dumps(ledger, indent=2) + "\n")
|
||||
return doc
|
||||
|
||||
|
||||
def _spots(ir: dict, pages: list[dict]) -> list[tuple[str, str]]:
|
||||
"""[(spot id, what it is)] for one book, in reading order."""
|
||||
from ..notebook import spec as spec_mod
|
||||
from ..ops.pages import slugify
|
||||
from .build import STEP_LABELS
|
||||
|
||||
out = [("larder", "what came in"), ("pages", "the page index")]
|
||||
out += [(p["slug"], f"page {p['title']}") for p in pages]
|
||||
out += [(f"step-{sid}", f"step: {label}") for sid, label in STEP_LABELS]
|
||||
# An API-shaped book also carries the endpoint walkthrough; each call is a
|
||||
# spot of its own, under the slug of its id.
|
||||
if any(n["kind"] in ("endpoint", "operation") for n in ir.get("nodes") or []):
|
||||
out += [(slugify(s["id"]), f"{s['kind']}: {s.get('title', s['id'])}")
|
||||
for s in spec_mod.from_ir(ir)["steps"] if s["id"] != "intro"]
|
||||
out.append(("book", "what came out"))
|
||||
return out
|
||||
|
||||
|
||||
def known_spots(book: Book) -> set[str]:
|
||||
"""Every id a note for this book may be filed under."""
|
||||
return {spot for spot, _ in _spots(book.ir, book.pages)}
|
||||
|
||||
|
||||
def plan_spots(part: Part) -> list[tuple[str, str]]:
|
||||
"""The spots a book will have, without building it — extraction and pages only."""
|
||||
from ..ops import overview
|
||||
from ..ops.pages import for_view
|
||||
from .build import extract
|
||||
|
||||
ir, _ = extract(part.kind, part.source, exclude=part.exclude, suffixes=part.suffixes)
|
||||
view = apply_ops(overview(ir), part.ops)
|
||||
return _spots(ir, for_view(ir, view))
|
||||
161
soleprint/atlas2/docgen/book/notes.py
Normal file
161
soleprint/atlas2/docgen/book/notes.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""
|
||||
Notes: hand-written markdown, filed by the page or step it belongs above.
|
||||
|
||||
notes/
|
||||
├── _intro.md top of the document
|
||||
├── _end.md bottom of the document
|
||||
└── <book>/
|
||||
├── _intro.md top of that book's part
|
||||
├── larder.md above the "what came in" step
|
||||
├── overview.md above the overview page
|
||||
└── docgen.book.md above the page for `docgen.book`
|
||||
|
||||
## Why files named by id
|
||||
|
||||
Generation alone gives a document that is never stale and never says anything a
|
||||
parser could not work out. Prose is the part only a person can add, and it has
|
||||
to survive every rebuild — so it cannot live in the output. An overlay could
|
||||
carry it, but as JSON strings with `\\n` escapes, addressed by ids you have to
|
||||
guess. A folder of markdown files, one per spot, is something a person or a
|
||||
model writes without reading a schema: to put text above a page, create the
|
||||
file named after it.
|
||||
|
||||
`python3 -m docgen scaffold` writes an empty stub for every spot that has none,
|
||||
so the ids are listed rather than guessed. A stub is a single HTML comment, and a
|
||||
note that is nothing but that comment renders nothing — so scaffolding a
|
||||
hundred spots changes the output not at all until one is filled in.
|
||||
|
||||
## Drift is reported, never dropped
|
||||
|
||||
When a page is renamed or removed its note has nowhere to go. It is not quietly
|
||||
discarded — that is how hand-written text goes stale without anyone noticing —
|
||||
it is reported, and the book's own check level fails on it. The same rule the
|
||||
notebook overlay follows.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import mimetypes
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
DOCUMENT_SPOTS = ("_intro", "_end")
|
||||
BOOK_SPOTS = ("_intro",)
|
||||
STUB = re.compile(r"\A\s*<!--\s*docgen note:.*?-->\s*", re.S)
|
||||
IMAGE = re.compile(r"!\[([^\]]*)\]\(([^)\s]+)\)")
|
||||
|
||||
|
||||
def body(text: str | None) -> str | None:
|
||||
"""A note's content, without its stub comment. None when nothing is left."""
|
||||
if text is None:
|
||||
return None
|
||||
text = STUB.sub("", text, count=1).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def stub(where: str) -> str:
|
||||
return (f"<!-- docgen note: {where}. Write markdown below; this comment does "
|
||||
f"not render, and an empty note adds nothing. -->\n")
|
||||
|
||||
|
||||
class Notes:
|
||||
"""The notes folder of one document. A missing folder is simply no notes."""
|
||||
|
||||
def __init__(self, root):
|
||||
self.root = Path(root) if root else None
|
||||
self.used: set[Path] = set()
|
||||
|
||||
def _read(self, path: Path) -> str | None:
|
||||
if self.root is None or not path.is_file():
|
||||
return None
|
||||
text = body(path.read_text())
|
||||
if text is not None:
|
||||
self.used.add(path)
|
||||
return text
|
||||
|
||||
def document(self, spot: str) -> str | None:
|
||||
return self._read(self.root / f"{spot}.md") if self.root else None
|
||||
|
||||
def book(self, book: str, spot: str) -> str | None:
|
||||
return self._read(self.root / book / f"{spot}.md") if self.root else None
|
||||
|
||||
def path(self, book: str | None, spot: str) -> Path | None:
|
||||
if self.root is None:
|
||||
return None
|
||||
return (self.root / book / f"{spot}.md") if book else (self.root / f"{spot}.md")
|
||||
|
||||
def drift(self, known: dict[str, set[str]]) -> list[str]:
|
||||
"""Notes filed under a spot that no longer exists, as readable problems.
|
||||
|
||||
`known` is {book name: every page slug and step id it has}.
|
||||
"""
|
||||
if self.root is None or not self.root.is_dir():
|
||||
return []
|
||||
problems = []
|
||||
for path in sorted(self.root.rglob("*.md")):
|
||||
rel = path.relative_to(self.root)
|
||||
if len(rel.parts) == 1:
|
||||
if rel.stem not in DOCUMENT_SPOTS:
|
||||
problems.append(f"note {rel} is not a document spot "
|
||||
f"({', '.join(s + '.md' for s in DOCUMENT_SPOTS)})")
|
||||
continue
|
||||
book = rel.parts[0]
|
||||
if book not in known:
|
||||
problems.append(f"note {rel} is filed under book {book!r}, which this "
|
||||
f"document does not have — have: {', '.join(sorted(known))}")
|
||||
continue
|
||||
spot = Path(*rel.parts[1:]).with_suffix("").as_posix()
|
||||
if spot not in known[book] and spot not in BOOK_SPOTS:
|
||||
if body(path.read_text()) is None:
|
||||
continue # an empty stub for a vanished page says nothing
|
||||
problems.append(f"note {rel} belongs above {spot!r}, which book "
|
||||
f"{book!r} no longer has — rename or remove it")
|
||||
return problems
|
||||
|
||||
def scaffold(self, spots: dict[str, list[tuple[str, str]]]) -> list[Path]:
|
||||
"""Write a stub for every spot without a note. Never overwrites.
|
||||
|
||||
`spots` is {book name: [(spot id, what it is)]}. Returns the files written.
|
||||
"""
|
||||
if self.root is None:
|
||||
return []
|
||||
written = []
|
||||
for spot in DOCUMENT_SPOTS:
|
||||
path = self.root / f"{spot}.md"
|
||||
where = "top of the document" if spot == "_intro" else "end of the document"
|
||||
if not path.exists():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(stub(where))
|
||||
written.append(path)
|
||||
for book, entries in spots.items():
|
||||
for spot, where in [("_intro", f"top of book {book}")] + list(entries):
|
||||
path = self.root / book / f"{spot}.md"
|
||||
if path.exists():
|
||||
continue
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(stub(f"{book} › {where}"))
|
||||
written.append(path)
|
||||
return written
|
||||
|
||||
|
||||
def images(text: str, base: Path) -> list[tuple[str, str, str, bytes]]:
|
||||
"""Local images a note links to: [(markdown ref, name, mime type, bytes)].
|
||||
|
||||
Resolved against the note's own folder. Remote and missing images are left as
|
||||
written — a link that does not resolve here is the author's to see, not ours
|
||||
to delete.
|
||||
"""
|
||||
out = []
|
||||
for m in IMAGE.finditer(text):
|
||||
ref = m.group(2)
|
||||
if re.match(r"^[a-z]+:", ref):
|
||||
continue
|
||||
path = (base / ref).resolve()
|
||||
if not path.is_file():
|
||||
continue
|
||||
mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
||||
out.append((ref, path.name, mime, path.read_bytes()))
|
||||
return out
|
||||
|
||||
|
||||
def data_uri(mime: str, data: bytes) -> str:
|
||||
return f"data:{mime};base64,{base64.b64encode(data).decode('ascii')}"
|
||||
@@ -6,6 +6,8 @@ The command line — every command docgen has, and nothing else.
|
||||
book one operation, measured at both ends
|
||||
run every book a run file lists
|
||||
check docgen's own suite, or one book's own level
|
||||
scaffold empty notes for every page and step, never overwriting
|
||||
contract the instructions to hand a model that writes docgen.toml
|
||||
extract source -> ir.json
|
||||
validate check an ir.json at the boundary
|
||||
view ir.json -> a narrower ir.json
|
||||
@@ -36,6 +38,8 @@ COMMANDS = {
|
||||
"book": ("book", "one operation, measured at both ends"),
|
||||
"run": ("run", "every book a run file lists"),
|
||||
"check": ("check", "docgen's own suite, or one book's own level"),
|
||||
"scaffold": ("scaffold", "empty notes for every page and step, never overwriting"),
|
||||
"contract": ("contract", "the instructions to hand a model that writes docgen.toml"),
|
||||
"extract": ("extract", "source -> ir.json"),
|
||||
"validate": ("validate", "check an ir.json at the boundary"),
|
||||
"view": ("view", "ir.json -> a narrower ir.json"),
|
||||
|
||||
@@ -37,6 +37,9 @@ def main(argv) -> int:
|
||||
help="A hand-written notebook overlay, re-applied on every build.")
|
||||
p.add_argument("--exclude", action="append", default=[],
|
||||
help="Directory name to skip. Repeatable.")
|
||||
p.add_argument("--notes", type=Path,
|
||||
help="A notes folder: <slug>/<page>.md lands above that page. "
|
||||
"See `scaffold`.")
|
||||
p.add_argument("--quiet", "-q", action="store_true")
|
||||
args = p.parse_args(argv)
|
||||
|
||||
@@ -66,7 +69,7 @@ def main(argv) -> int:
|
||||
try:
|
||||
book = run(kind, source, args.output, slug=args.slug, style=args.style,
|
||||
theme=args.theme, exclude=tuple(args.exclude), overlay=overlay,
|
||||
quiet=args.quiet)
|
||||
notes=args.notes, quiet=args.quiet)
|
||||
except Exception as e: # noqa: BLE001 - the CLI reports, it does not traceback
|
||||
raise Abort(f"{type(e).__name__}: {e}") from None
|
||||
|
||||
|
||||
@@ -34,7 +34,14 @@ def main(argv) -> int:
|
||||
# what a fresh one would do.
|
||||
return subprocess.call([sys.executable, str(HERE / "selftest.py")])
|
||||
|
||||
from ..book.checks import Loaded, Report, custom, generated
|
||||
from ..book.checks import Loaded, Report, custom, document, generated
|
||||
|
||||
if not (args.book / "book.json").is_file() and (args.book / "document.json").is_file():
|
||||
# A project's build directory: the document's own level.
|
||||
print(f"{args.book} — document")
|
||||
r = Report("document")
|
||||
document(args.book, r)
|
||||
return r.total()
|
||||
|
||||
try:
|
||||
book = Loaded(args.book)
|
||||
|
||||
33
soleprint/atlas2/docgen/cli/contract.py
Normal file
33
soleprint/atlas2/docgen/cli/contract.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
python3 -m docgen contract > docgen-contract.md
|
||||
|
||||
Print the instructions to hand to a model that writes `docgen.toml` and notes —
|
||||
`docs/models.md`, verbatim.
|
||||
|
||||
One copy of the wording, printed rather than remembered, the same arrangement as
|
||||
`explode.sh --contract`: what a model is asked to write and what docgen accepts
|
||||
cannot drift apart, because `selftest.py` checks the file against the loaders —
|
||||
every key named, every example loading. Attach it; do not paste it into a
|
||||
message body, where markdown gets a say first.
|
||||
|
||||
With `explode`, attach both: explode's contract says how to format the reply,
|
||||
this one says what goes in the files.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from .common import PROG, Abort
|
||||
|
||||
CONTRACT = Path(__file__).resolve().parent.parent / "docs" / "models.md"
|
||||
|
||||
|
||||
def main(argv) -> int:
|
||||
p = argparse.ArgumentParser(prog=f"{PROG} contract",
|
||||
description="Print the instructions for a model.")
|
||||
p.parse_args(argv)
|
||||
if not CONTRACT.is_file():
|
||||
raise Abort(f"{CONTRACT} is missing — is the docgen folder intact?")
|
||||
sys.stdout.write(CONTRACT.read_text())
|
||||
return 0
|
||||
@@ -30,13 +30,17 @@ def main(argv) -> int:
|
||||
help="Show each book's own output, not just the summary.")
|
||||
args = p.parse_args(argv)
|
||||
|
||||
from ..book.config import ConfigError, load, run
|
||||
from ..book.config import ConfigError, ProjectFile, load, run, select_parts
|
||||
|
||||
if not args.config.exists():
|
||||
raise Abort(f"no run file at {args.config} — see docgen.example.toml")
|
||||
raise Abort(f"no run file at {args.config} — see docgen.example.toml "
|
||||
"or docgen.project.example.toml")
|
||||
try:
|
||||
runfile = load(args.config)
|
||||
selected = runfile.select(args.only)
|
||||
if isinstance(runfile, ProjectFile):
|
||||
selected = select_parts(runfile, args.only)
|
||||
else:
|
||||
selected = runfile.select(args.only)
|
||||
except ConfigError as e:
|
||||
raise Abort(str(e)) from None
|
||||
|
||||
@@ -46,13 +50,26 @@ def main(argv) -> int:
|
||||
print(f"{runfile.path} — {len(selected)} of {len(runfile.books)} book(s)")
|
||||
if runfile.reference:
|
||||
print(f" reference {runfile.reference}")
|
||||
for b in selected:
|
||||
print(f" {b.line()}")
|
||||
if isinstance(runfile, ProjectFile):
|
||||
doc = runfile.document
|
||||
print(f" project {doc.name} — {doc.title}")
|
||||
print(f" build {doc.build}")
|
||||
print(f" notes {doc.notes}")
|
||||
for part in selected:
|
||||
print(f" book {part.name:<16} {part.kind:<8} {part.source}")
|
||||
for o in doc.outputs:
|
||||
print(f" output {o.emitter:<16} {o.path}"
|
||||
+ (f" (width {o.width}px)" if o.width else ""))
|
||||
else:
|
||||
for b in selected:
|
||||
print(f" {b.line()}")
|
||||
return 0
|
||||
|
||||
print(f"{runfile.path} — {len(selected)} book(s)")
|
||||
results = run(runfile, [b.name for b in selected], check=args.check,
|
||||
quiet=not args.verbose)
|
||||
if not results:
|
||||
raise Abort("nothing was built")
|
||||
|
||||
width = max(len(r["name"]) for r in results)
|
||||
for r in results:
|
||||
@@ -70,10 +87,12 @@ def main(argv) -> int:
|
||||
print(f" {status} {r['name']:<{width}} {detail}")
|
||||
|
||||
failed = [r for r in results if not r["ok"]]
|
||||
documents = sum(1 for r in results if r["name"].endswith("(document)"))
|
||||
what = f"{len(results) - documents} book(s)" + (f" in {documents} document" if documents else "")
|
||||
print()
|
||||
if failed:
|
||||
print(f"{len(failed)} of {len(results)} book(s) did not hold"
|
||||
print(f"{len(failed)} of {len(results)} did not hold ({what})"
|
||||
+ ("" if args.verbose else " — --verbose for each book's own output"))
|
||||
return 1
|
||||
print(f"{len(results)} book(s) built" + (" and checked" if args.check else ""))
|
||||
print(f"{what} built" + (" and checked" if args.check else ""))
|
||||
return 0
|
||||
|
||||
87
soleprint/atlas2/docgen/cli/scaffold.py
Normal file
87
soleprint/atlas2/docgen/cli/scaffold.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
python3 -m docgen scaffold <docgen.toml> [--only NAME ...]
|
||||
python3 -m docgen scaffold <docgen.toml> --list # print the spots, write nothing
|
||||
|
||||
Write an empty note for every spot a document has — the document's top and end,
|
||||
each book's top, what came in, the page index, every page, every step, every
|
||||
API call, what came out — so the ids are listed rather than guessed.
|
||||
|
||||
Never overwrites: a spot that already has a note is left alone, so running it
|
||||
again after the sources change only adds the spots that are new. A stub is a
|
||||
single HTML comment and renders nothing until somebody writes below it.
|
||||
|
||||
Extraction and page structure only — nothing is drawn and no output is written.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from .common import PROG, Abort
|
||||
|
||||
|
||||
def main(argv) -> int:
|
||||
p = argparse.ArgumentParser(prog=f"{PROG} scaffold")
|
||||
p.add_argument("config", type=Path, nargs="?", default=Path("docgen.toml"))
|
||||
p.add_argument("--only", action="append", default=[], metavar="NAME",
|
||||
help="Just these books. Repeatable.")
|
||||
p.add_argument("--list", action="store_true",
|
||||
help="Print every spot a note can be filed under, and write nothing — "
|
||||
"the list to hand a model instead of letting it guess page ids.")
|
||||
args = p.parse_args(argv)
|
||||
|
||||
from ..book.config import ConfigError, ProjectFile, apply_reference, load, select_parts
|
||||
from ..book.document import Part, plan_spots
|
||||
from ..book.notes import Notes
|
||||
|
||||
if not args.config.exists():
|
||||
raise Abort(f"no run file at {args.config}")
|
||||
try:
|
||||
loaded = load(args.config)
|
||||
apply_reference(loaded)
|
||||
if isinstance(loaded, ProjectFile):
|
||||
groups = {loaded.document.notes: select_parts(loaded, args.only)}
|
||||
else:
|
||||
groups = {}
|
||||
for e in loaded.select(args.only):
|
||||
part = Part(name=e.name, kind=e.kind, source=e.source, out=e.out,
|
||||
exclude=e.exclude)
|
||||
groups.setdefault(e.notes, []).append(part)
|
||||
except ConfigError as e:
|
||||
raise Abort(str(e)) from None
|
||||
|
||||
total = 0
|
||||
for root, parts in groups.items():
|
||||
spots = {}
|
||||
for part in parts:
|
||||
try:
|
||||
spots[part.name] = plan_spots(part)
|
||||
except Exception as e: # noqa: BLE001 - one bad source must not cost the rest
|
||||
print(f" skip {part.name}: {type(e).__name__}: {str(e).splitlines()[0]}")
|
||||
if args.list:
|
||||
# Relative to the run file's folder, which is how a note's path is
|
||||
# written in a reply — not as an absolute path on this machine.
|
||||
base = args.config.resolve().parent
|
||||
try:
|
||||
shown = root.relative_to(base)
|
||||
except ValueError:
|
||||
shown = root
|
||||
rows = [(f"{shown}/_intro.md", "top of the document"),
|
||||
(f"{shown}/_end.md", "end of the document")]
|
||||
for book, entries in spots.items():
|
||||
rows.append((f"{shown}/{book}/_intro.md", f"top of book {book}"))
|
||||
for spot, where in entries:
|
||||
has = " (has a note)" if Notes(root).book(book, spot) else ""
|
||||
rows.append((f"{shown}/{book}/{spot}.md", where + has))
|
||||
width = max(len(r[0]) for r in rows)
|
||||
for path, where in rows:
|
||||
print(f"{path:<{width}} {where}")
|
||||
continue
|
||||
written = Notes(root).scaffold(spots)
|
||||
total += len(written)
|
||||
existing = sum(len(v) for v in spots.values()) + 2 + len(spots) - len(written)
|
||||
print(f" notes {root} — {len(written)} new stub(s), {existing} already there")
|
||||
for path in written[:12]:
|
||||
print(f" {path.relative_to(root)}")
|
||||
if len(written) > 12:
|
||||
print(f" … and {len(written) - 12} more")
|
||||
return 0
|
||||
53
soleprint/atlas2/docgen/docgen.project.example.toml
Normal file
53
soleprint/atlas2/docgen/docgen.project.example.toml
Normal file
@@ -0,0 +1,53 @@
|
||||
# A project file: one document built from several sources.
|
||||
#
|
||||
# make run CONFIG=docgen.project.example.toml CHECK=1
|
||||
# make scaffold CONFIG=docgen.project.example.toml # a stub note for every spot
|
||||
# python3 -m docgen run docgen.project.example.toml --list
|
||||
#
|
||||
# Every [extract.*] table is a book — a part of each output. Every [emit.*] table
|
||||
# is an output made of all of them. Paths are relative to THIS FILE. Unknown keys
|
||||
# are refused with a reason, never ignored.
|
||||
#
|
||||
# The other shape, a list of independent books, is docgen.example.toml.
|
||||
|
||||
[project]
|
||||
name = "docgen-fixtures"
|
||||
title = "docgen's own fixtures"
|
||||
notes = "fixtures/notes" # notes/<book>/<page>.md lands above that page
|
||||
build = ".docgen" # book artifacts; gitignore it
|
||||
# reference = "../../" # the repo holding station/tools/modelgen, for OpenAPI
|
||||
|
||||
# A tree of Python. Several trees: [[extract.python]] with a `name` each.
|
||||
[extract.python]
|
||||
name = "extractors"
|
||||
root = "extractors"
|
||||
|
||||
# A database schema, as `modelgen from-db` writes it. docgen reads the dump and
|
||||
# never connects: `url = ...` is refused, since a URL in a committed file is a
|
||||
# credential in a committed file.
|
||||
[extract.db]
|
||||
name = "schema"
|
||||
schema = "fixtures/shop.json"
|
||||
|
||||
# An OpenAPI document. Needs pyyaml and the reference repo's modelgen; without
|
||||
# them this book fails with a one-line reason and the others still build.
|
||||
[extract.openapi]
|
||||
name = "api"
|
||||
spec = "fixtures/orders.yaml"
|
||||
|
||||
# Also available:
|
||||
# [extract.code] root = "src", ext = [".cs", ".ts"] (tree-sitter)
|
||||
# [extract.usage] har = "session.har"
|
||||
|
||||
[ops]
|
||||
drop_stdlib = true
|
||||
drop_builtins = true
|
||||
|
||||
[emit.notebook]
|
||||
output = "out/project/notebook" # a directory: <name>.ipynb goes inside
|
||||
theme = "lucid"
|
||||
width = 960 # px — pages are split until each fits
|
||||
|
||||
[emit.site]
|
||||
output = "out/project/site"
|
||||
theme = "dark"
|
||||
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 88 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="886pt" height="2238pt" viewBox="0 0 886 2238">
|
||||
<rect width="886" height="2238" fill="#0a0a0a"/>
|
||||
<rect x="28.0" y="46.0" width="74.0" height="1164.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.selftest — 2328 lines</title></rect>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="886pt" height="2781pt" viewBox="0 0 886 2781">
|
||||
<rect width="886" height="2781" fill="#0a0a0a"/>
|
||||
<rect x="28.0" y="46.0" width="74.0" height="1387.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.selftest — 2775 lines</title></rect>
|
||||
<rect x="28.0" y="94.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.own_py_files" data-kind="function" class="blk"><title>own_py_files — function, 3 lines</title></rect>
|
||||
<rect x="28.0" y="96.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest.check" data-kind="function" class="blk"><title>check — function, 5 lines</title></rect>
|
||||
<rect x="28.0" y="99.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._err" data-kind="function" class="blk"><title>_err — function, 7 lines</title></rect>
|
||||
@@ -15,6 +15,12 @@
|
||||
<rect x="28.0" y="1106.5" width="74.0" height="97.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._runfiles" data-kind="function" class="blk"><title>_runfiles — function, 195 lines</title></rect>
|
||||
<rect x="35.0" y="1112.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.selftest._runfiles.write" data-kind="function" class="blk"><title>write — function, 4 lines</title></rect>
|
||||
<rect x="35.0" y="1115.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.selftest._runfiles.problems" data-kind="function" class="blk"><title>problems — function, 6 lines</title></rect>
|
||||
<rect x="28.0" y="1209.0" width="74.0" height="57.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._pages" data-kind="function" class="blk"><title>_pages — function, 114 lines</title></rect>
|
||||
<rect x="28.0" y="1270.5" width="74.0" height="80.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._documents" data-kind="function" class="blk"><title>_documents — function, 161 lines</title></rect>
|
||||
<rect x="28.0" y="1355.5" width="74.0" height="29.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._projects" data-kind="function" class="blk"><title>_projects — function, 59 lines</title></rect>
|
||||
<rect x="35.0" y="1359.5" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.selftest._projects.load" data-kind="function" class="blk"><title>load — function, 7 lines</title></rect>
|
||||
<rect x="28.0" y="1390.0" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.selftest._contract" data-kind="function" class="blk"><title>_contract — function, 75 lines</title></rect>
|
||||
<rect x="35.0" y="1399.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.selftest._contract.named" data-kind="function" class="blk"><title>named — function, 2 lines</title></rect>
|
||||
<rect x="110.0" y="46.0" width="74.0" height="158.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book — 316 lines</title></rect>
|
||||
<rect x="110.0" y="75.0" width="74.0" height="10.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.Step" data-kind="class" class="blk"><title>Step — class, 20 lines</title></rect>
|
||||
<rect x="117.0" y="80.0" width="60.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.Step.to_dict" data-kind="function" class="blk"><title>to_dict — function, 10 lines</title></rect>
|
||||
@@ -54,353 +60,449 @@
|
||||
<rect x="274.0" y="82.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.describe" data-kind="function" class="blk"><title>describe — function, 10 lines</title></rect>
|
||||
<rect x="274.0" y="88.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.on_path" data-kind="function" class="blk"><title>on_path — function, 13 lines</title></rect>
|
||||
<rect x="274.0" y="95.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.reference.missing" data-kind="function" class="blk"><title>missing — function, 8 lines</title></rect>
|
||||
<rect x="356.0" y="46.0" width="74.0" height="38.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli — 76 lines</title></rect>
|
||||
<rect x="356.0" y="68.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.usage" data-kind="function" class="blk"><title>usage — function, 7 lines</title></rect>
|
||||
<rect x="356.0" y="73.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.main" data-kind="function" class="blk"><title>main — function, 21 lines</title></rect>
|
||||
<rect x="356.0" y="46.0" width="74.0" height="40.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli — 80 lines</title></rect>
|
||||
<rect x="356.0" y="70.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.usage" data-kind="function" class="blk"><title>usage — function, 7 lines</title></rect>
|
||||
<rect x="356.0" y="75.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.main" data-kind="function" class="blk"><title>main — function, 21 lines</title></rect>
|
||||
<rect x="438.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops — 28 lines</title></rect>
|
||||
<rect x="520.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.notebook — 15 lines</title></rect>
|
||||
<rect x="602.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters — 12 lines</title></rect>
|
||||
<rect x="684.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.lab — 11 lines</title></rect>
|
||||
<rect x="766.0" y="46.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.__main__ — 9 lines</title></rect>
|
||||
<rect x="28.0" y="1248.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir — 7 lines</title></rect>
|
||||
<rect x="110.0" y="1248.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors — 2 lines</title></rect>
|
||||
<rect x="210.0" y="1248.0" width="74.0" height="126.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.emit — 253 lines</title></rect>
|
||||
<rect x="210.0" y="1260.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit._parser" data-kind="function" class="blk"><title>_parser — function, 7 lines</title></rect>
|
||||
<rect x="210.0" y="1264.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit._size" data-kind="function" class="blk"><title>_size — function, 3 lines</title></rect>
|
||||
<rect x="210.0" y="1267.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.auto" data-kind="function" class="blk"><title>auto — function, 16 lines</title></rect>
|
||||
<rect x="210.0" y="1276.0" width="74.0" height="19.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.dot" data-kind="function" class="blk"><title>dot — function, 38 lines</title></rect>
|
||||
<rect x="210.0" y="1296.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.erd" data-kind="function" class="blk"><title>erd — function, 11 lines</title></rect>
|
||||
<rect x="210.0" y="1302.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.index" data-kind="function" class="blk"><title>index — function, 14 lines</title></rect>
|
||||
<rect x="210.0" y="1310.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.minimap" data-kind="function" class="blk"><title>minimap — function, 14 lines</title></rect>
|
||||
<rect x="210.0" y="1318.5" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.notebook" data-kind="function" class="blk"><title>notebook — function, 41 lines</title></rect>
|
||||
<rect x="210.0" y="1340.0" width="74.0" height="15.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.site" data-kind="function" class="blk"><title>site — function, 31 lines</title></rect>
|
||||
<rect x="210.0" y="1356.5" width="74.0" height="11.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.explore" data-kind="function" class="blk"><title>explore — function, 22 lines</title></rect>
|
||||
<rect x="210.0" y="1370.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.main" data-kind="function" class="blk"><title>main — function, 7 lines</title></rect>
|
||||
<rect x="292.0" y="1248.0" width="74.0" height="59.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.extract — 119 lines</title></rect>
|
||||
<rect x="292.0" y="1257.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract._parser" data-kind="function" class="blk"><title>_parser — function, 5 lines</title></rect>
|
||||
<rect x="292.0" y="1261.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract._counts" data-kind="function" class="blk"><title>_counts — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="1263.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.python" data-kind="function" class="blk"><title>python — function, 15 lines</title></rect>
|
||||
<rect x="292.0" y="1272.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.code" data-kind="function" class="blk"><title>code — function, 14 lines</title></rect>
|
||||
<rect x="292.0" y="1280.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.db" data-kind="function" class="blk"><title>db — function, 11 lines</title></rect>
|
||||
<rect x="292.0" y="1286.5" width="74.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.openapi" data-kind="function" class="blk"><title>openapi — function, 12 lines</title></rect>
|
||||
<rect x="292.0" y="1293.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.usage" data-kind="function" class="blk"><title>usage — function, 15 lines</title></rect>
|
||||
<rect x="292.0" y="1303.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.main" data-kind="function" class="blk"><title>main — function, 7 lines</title></rect>
|
||||
<rect x="374.0" y="1248.0" width="74.0" height="45.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.view — 90 lines</title></rect>
|
||||
<rect x="374.0" y="1255.0" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.view.main" data-kind="function" class="blk"><title>main — function, 75 lines</title></rect>
|
||||
<rect x="456.0" y="1248.0" width="74.0" height="40.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.common — 80 lines</title></rect>
|
||||
<rect x="456.0" y="1258.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.cli.common.Abort" data-kind="class" class="blk"><title>Abort — class, 2 lines</title></rect>
|
||||
<rect x="456.0" y="1260.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.read_json" data-kind="function" class="blk"><title>read_json — function, 5 lines</title></rect>
|
||||
<rect x="456.0" y="1263.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.read_ir" data-kind="function" class="blk"><title>read_ir — function, 11 lines</title></rect>
|
||||
<rect x="456.0" y="1270.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.load_style" data-kind="function" class="blk"><title>load_style — function, 7 lines</title></rect>
|
||||
<rect x="456.0" y="1274.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.style_args" data-kind="function" class="blk"><title>style_args — function, 3 lines</title></rect>
|
||||
<rect x="456.0" y="1277.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.emit" data-kind="function" class="blk"><title>emit — function, 16 lines</title></rect>
|
||||
<rect x="456.0" y="1286.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.dump_ir" data-kind="function" class="blk"><title>dump_ir — function, 3 lines</title></rect>
|
||||
<rect x="538.0" y="1248.0" width="74.0" height="40.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.run — 80 lines</title></rect>
|
||||
<rect x="538.0" y="1258.0" width="74.0" height="29.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.run.main" data-kind="function" class="blk"><title>main — function, 59 lines</title></rect>
|
||||
<rect x="620.0" y="1248.0" width="74.0" height="37.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.book — 74 lines</title></rect>
|
||||
<rect x="620.0" y="1258.5" width="74.0" height="26.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.book.main" data-kind="function" class="blk"><title>main — function, 52 lines</title></rect>
|
||||
<rect x="702.0" y="1248.0" width="74.0" height="25.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.check — 51 lines</title></rect>
|
||||
<rect x="702.0" y="1258.0" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.check.main" data-kind="function" class="blk"><title>main — function, 30 lines</title></rect>
|
||||
<rect x="784.0" y="1248.0" width="74.0" height="16.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.validate — 33 lines</title></rect>
|
||||
<rect x="784.0" y="1255.0" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.validate.main" data-kind="function" class="blk"><title>main — function, 18 lines</title></rect>
|
||||
<rect x="28.0" y="1412.5" width="74.0" height="247.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.site — 494 lines</title></rect>
|
||||
<rect x="28.0" y="1548.5" width="74.0" height="27.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._ledger" data-kind="function" class="blk"><title>_ledger — function, 54 lines</title></rect>
|
||||
<rect x="28.0" y="1576.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._slots" data-kind="function" class="blk"><title>_slots — function, 13 lines</title></rect>
|
||||
<rect x="28.0" y="1584.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._fill" data-kind="function" class="blk"><title>_fill — function, 4 lines</title></rect>
|
||||
<rect x="28.0" y="1587.0" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sidebar" data-kind="function" class="blk"><title>_sidebar — function, 17 lines</title></rect>
|
||||
<rect x="28.0" y="1596.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sections" data-kind="function" class="blk"><title>_sections — function, 15 lines</title></rect>
|
||||
<rect x="28.0" y="1605.0" width="74.0" height="48.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.emit" data-kind="function" class="blk"><title>emit — function, 96 lines</title></rect>
|
||||
<rect x="28.0" y="1654.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.write" data-kind="function" class="blk"><title>write — function, 10 lines</title></rect>
|
||||
<rect x="110.0" y="1412.5" width="74.0" height="158.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.explore — 316 lines</title></rect>
|
||||
<rect x="110.0" y="1432.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._is_schema" data-kind="function" class="blk"><title>_is_schema — function, 4 lines</title></rect>
|
||||
<rect x="110.0" y="1435.0" width="74.0" height="24.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._neighbourhood_svgs" data-kind="function" class="blk"><title>_neighbourhood_svgs — function, 48 lines</title></rect>
|
||||
<rect x="117.0" y="1441.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
|
||||
<rect x="117.0" y="1445.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one#L66" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
|
||||
<rect x="110.0" y="1460.0" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._facts" data-kind="function" class="blk"><title>_facts — function, 27 lines</title></rect>
|
||||
<rect x="110.0" y="1474.5" width="74.0" height="86.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.emit" data-kind="function" class="blk"><title>emit — function, 172 lines</title></rect>
|
||||
<rect x="110.0" y="1561.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.write" data-kind="function" class="blk"><title>write — function, 17 lines</title></rect>
|
||||
<rect x="192.0" y="1412.5" width="74.0" height="146.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.dot — 292 lines</title></rect>
|
||||
<rect x="192.0" y="1431.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.RenderError" data-kind="class" class="blk"><title>RenderError — class, 2 lines</title></rect>
|
||||
<rect x="192.0" y="1433.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._esc" data-kind="function" class="blk"><title>_esc — function, 2 lines</title></rect>
|
||||
<rect x="192.0" y="1435.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._attrs" data-kind="function" class="blk"><title>_attrs — function, 3 lines</title></rect>
|
||||
<rect x="192.0" y="1437.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._style_words" data-kind="function" class="blk"><title>_style_words — function, 9 lines</title></rect>
|
||||
<rect x="192.0" y="1443.0" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._node_attrs" data-kind="function" class="blk"><title>_node_attrs — function, 24 lines</title></rect>
|
||||
<rect x="192.0" y="1456.0" width="74.0" height="52.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.emit" data-kind="function" class="blk"><title>emit — function, 105 lines</title></rect>
|
||||
<rect x="199.0" y="1469.5" width="60.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot.emit.write" data-kind="function" class="blk"><title>write — function, 29 lines</title></rect>
|
||||
<rect x="192.0" y="1509.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._within" data-kind="function" class="blk"><title>_within — function, 8 lines</title></rect>
|
||||
<rect x="192.0" y="1514.5" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._endpoints" data-kind="function" class="blk"><title>_endpoints — function, 41 lines</title></rect>
|
||||
<rect x="199.0" y="1519.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.walk" data-kind="function" class="blk"><title>walk — function, 4 lines</title></rect>
|
||||
<rect x="199.0" y="1523.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.collapsed" data-kind="function" class="blk"><title>collapsed — function, 2 lines</title></rect>
|
||||
<rect x="199.0" y="1525.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.first_leaf" data-kind="function" class="blk"><title>first_leaf — function, 4 lines</title></rect>
|
||||
<rect x="192.0" y="1536.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._safe" data-kind="function" class="blk"><title>_safe — function, 2 lines</title></rect>
|
||||
<rect x="192.0" y="1538.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._q" data-kind="function" class="blk"><title>_q — function, 2 lines</title></rect>
|
||||
<rect x="192.0" y="1541.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.have_graphviz" data-kind="function" class="blk"><title>have_graphviz — function, 2 lines</title></rect>
|
||||
<rect x="192.0" y="1543.5" width="74.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.render" data-kind="function" class="blk"><title>render — function, 29 lines</title></rect>
|
||||
<rect x="274.0" y="1412.5" width="74.0" height="139.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.notebook — 279 lines</title></rect>
|
||||
<rect x="274.0" y="1433.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._cell" data-kind="function" class="blk"><title>_cell — function, 13 lines</title></rect>
|
||||
<rect x="274.0" y="1441.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._example" data-kind="function" class="blk"><title>_example — function, 26 lines</title></rect>
|
||||
<rect x="274.0" y="1474.5" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._params_cell" data-kind="function" class="blk"><title>_params_cell — function, 18 lines</title></rect>
|
||||
<rect x="274.0" y="1484.5" width="74.0" height="20.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_cell" data-kind="function" class="blk"><title>_call_cell — function, 40 lines</title></rect>
|
||||
<rect x="274.0" y="1505.5" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_md" data-kind="function" class="blk"><title>_call_md — function, 26 lines</title></rect>
|
||||
<rect x="274.0" y="1519.5" width="74.0" height="26.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.build" data-kind="function" class="blk"><title>build — function, 52 lines</title></rect>
|
||||
<rect x="274.0" y="1546.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.emit" data-kind="function" class="blk"><title>emit — function, 3 lines</title></rect>
|
||||
<rect x="274.0" y="1549.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.write" data-kind="function" class="blk"><title>write — function, 5 lines</title></rect>
|
||||
<rect x="356.0" y="1412.5" width="74.0" height="139.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.minimap — 278 lines</title></rect>
|
||||
<rect x="356.0" y="1444.0" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._files" data-kind="function" class="blk"><title>_files — function, 57 lines</title></rect>
|
||||
<rect x="363.0" y="1448.0" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.declared" data-kind="function" class="blk"><title>declared — function, 18 lines</title></rect>
|
||||
<rect x="363.0" y="1457.5" width="60.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.build" data-kind="function" class="blk"><title>build — function, 11 lines</title></rect>
|
||||
<rect x="356.0" y="1473.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._bands" data-kind="function" class="blk"><title>_bands — function, 7 lines</title></rect>
|
||||
<rect x="356.0" y="1478.0" width="74.0" height="9.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._blocks" data-kind="function" class="blk"><title>_blocks — function, 19 lines</title></rect>
|
||||
<rect x="356.0" y="1488.5" width="74.0" height="60.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.emit" data-kind="function" class="blk"><title>emit — function, 120 lines</title></rect>
|
||||
<rect x="356.0" y="1549.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.marks_to_labels" data-kind="function" class="blk"><title>marks_to_labels — function, 3 lines</title></rect>
|
||||
<rect x="438.0" y="1412.5" width="74.0" height="130.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.erd — 260 lines</title></rect>
|
||||
<rect x="438.0" y="1439.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._truncate" data-kind="function" class="blk"><title>_truncate — function, 3 lines</title></rect>
|
||||
<rect x="438.0" y="1442.0" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._tables" data-kind="function" class="blk"><title>_tables — function, 20 lines</title></rect>
|
||||
<rect x="438.0" y="1453.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._card_height" data-kind="function" class="blk"><title>_card_height — function, 3 lines</title></rect>
|
||||
<rect x="438.0" y="1455.5" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.layout" data-kind="function" class="blk"><title>layout — function, 20 lines</title></rect>
|
||||
<rect x="438.0" y="1466.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._field_y" data-kind="function" class="blk"><title>_field_y — function, 3 lines</title></rect>
|
||||
<rect x="438.0" y="1469.0" width="74.0" height="73.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.emit" data-kind="function" class="blk"><title>emit — function, 146 lines</title></rect>
|
||||
<rect x="520.0" y="1412.5" width="74.0" height="82.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.index — 164 lines</title></rect>
|
||||
<rect x="520.0" y="1429.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._tree" data-kind="function" class="blk"><title>_tree — function, 8 lines</title></rect>
|
||||
<rect x="520.0" y="1434.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._anchor" data-kind="function" class="blk"><title>_anchor — function, 5 lines</title></rect>
|
||||
<rect x="520.0" y="1438.0" width="74.0" height="42.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_markdown" data-kind="function" class="blk"><title>to_markdown — function, 84 lines</title></rect>
|
||||
<rect x="527.0" y="1449.5" width="60.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_markdown.walk" data-kind="function" class="blk"><title>walk — function, 27 lines</title></rect>
|
||||
<rect x="520.0" y="1481.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_sidebar" data-kind="function" class="blk"><title>to_sidebar — function, 26 lines</title></rect>
|
||||
<rect x="527.0" y="1484.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_sidebar.build" data-kind="function" class="blk"><title>build — function, 13 lines</title></rect>
|
||||
<rect x="602.0" y="1412.5" width="74.0" height="32.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.auto — 64 lines</title></rect>
|
||||
<rect x="602.0" y="1426.0" width="74.0" height="18.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.auto.draw" data-kind="function" class="blk"><title>draw — function, 36 lines</title></rect>
|
||||
<rect x="702.0" y="1412.5" width="74.0" height="141.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.code — 282 lines</title></rect>
|
||||
<rect x="702.0" y="1460.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.code.MissingParser" data-kind="class" class="blk"><title>MissingParser — class, 2 lines</title></rect>
|
||||
<rect x="702.0" y="1462.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._parser" data-kind="function" class="blk"><title>_parser — function, 21 lines</title></rect>
|
||||
<rect x="702.0" y="1473.5" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._name" data-kind="function" class="blk"><title>_name — function, 10 lines</title></rect>
|
||||
<rect x="702.0" y="1479.5" width="74.0" height="17.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._walk" data-kind="function" class="blk"><title>_walk — function, 34 lines</title></rect>
|
||||
<rect x="702.0" y="1497.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract_file" data-kind="function" class="blk"><title>extract_file — function, 27 lines</title></rect>
|
||||
<rect x="702.0" y="1512.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._count_errors" data-kind="function" class="blk"><title>_count_errors — function, 5 lines</title></rect>
|
||||
<rect x="702.0" y="1515.5" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract" data-kind="function" class="blk"><title>extract — function, 75 lines</title></rect>
|
||||
<rect x="784.0" y="1412.5" width="74.0" height="134.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.usage — 269 lines</title></rect>
|
||||
<rect x="784.0" y="1444.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._template" data-kind="function" class="blk"><title>_template — function, 27 lines</title></rect>
|
||||
<rect x="784.0" y="1459.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._body" data-kind="function" class="blk"><title>_body — function, 10 lines</title></rect>
|
||||
<rect x="784.0" y="1465.0" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._shape" data-kind="function" class="blk"><title>_shape — function, 15 lines</title></rect>
|
||||
<rect x="784.0" y="1473.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._graphql" data-kind="function" class="blk"><title>_graphql — function, 11 lines</title></rect>
|
||||
<rect x="784.0" y="1480.0" width="74.0" height="66.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage.extract" data-kind="function" class="blk"><title>extract — function, 133 lines</title></rect>
|
||||
<rect x="28.0" y="1697.5" width="74.0" height="95.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.openapi — 191 lines</title></rect>
|
||||
<rect x="28.0" y="1713.0" width="74.0" height="14.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._modelgen" data-kind="function" class="blk"><title>_modelgen — function, 28 lines</title></rect>
|
||||
<rect x="28.0" y="1728.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._type_name" data-kind="function" class="blk"><title>_type_name — function, 6 lines</title></rect>
|
||||
<rect x="28.0" y="1732.0" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._refs" data-kind="function" class="blk"><title>_refs — function, 43 lines</title></rect>
|
||||
<rect x="28.0" y="1754.5" width="74.0" height="38.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi.extract" data-kind="function" class="blk"><title>extract — function, 76 lines</title></rect>
|
||||
<rect x="110.0" y="1697.5" width="74.0" height="80.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.db — 161 lines</title></rect>
|
||||
<rect x="110.0" y="1717.0" width="74.0" height="41.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.from_schema_dict" data-kind="function" class="blk"><title>from_schema_dict — function, 82 lines</title></rect>
|
||||
<rect x="110.0" y="1759.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._relation" data-kind="function" class="blk"><title>_relation — function, 10 lines</title></rect>
|
||||
<rect x="110.0" y="1765.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._plain_type" data-kind="function" class="blk"><title>_plain_type — function, 6 lines</title></rect>
|
||||
<rect x="110.0" y="1769.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._dedupe" data-kind="function" class="blk"><title>_dedupe — function, 9 lines</title></rect>
|
||||
<rect x="110.0" y="1774.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.extract" data-kind="function" class="blk"><title>extract — function, 6 lines</title></rect>
|
||||
<rect x="192.0" y="1697.5" width="74.0" height="18.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python — 37 lines</title></rect>
|
||||
<rect x="192.0" y="1707.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.extract" data-kind="function" class="blk"><title>extract — function, 14 lines</title></rect>
|
||||
<rect x="292.0" y="1697.5" width="74.0" height="190.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.build — 381 lines</title></rect>
|
||||
<rect x="292.0" y="1735.0" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.extract" data-kind="function" class="blk"><title>extract — function, 27 lines</title></rect>
|
||||
<rect x="292.0" y="1749.5" width="74.0" height="34.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.spec_from" data-kind="function" class="blk"><title>spec_from — function, 68 lines</title></rect>
|
||||
<rect x="292.0" y="1784.5" width="74.0" height="22.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build._load_cell" data-kind="function" class="blk"><title>_load_cell — function, 44 lines</title></rect>
|
||||
<rect x="292.0" y="1811.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.clear" data-kind="function" class="blk"><title>clear — function, 26 lines</title></rect>
|
||||
<rect x="292.0" y="1825.0" width="74.0" height="62.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.run" data-kind="function" class="blk"><title>run — function, 125 lines</title></rect>
|
||||
<rect x="299.0" y="1831.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.build.run.say" data-kind="function" class="blk"><title>say — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="1697.5" width="74.0" height="164.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.config — 328 lines</title></rect>
|
||||
<rect x="374.0" y="1738.0" width="74.0" height="3.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.ConfigError" data-kind="class" class="blk"><title>ConfigError — class, 6 lines</title></rect>
|
||||
<rect x="381.0" y="1739.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.ConfigError.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="1742.5" width="74.0" height="7.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.Entry" data-kind="class" class="blk"><title>Entry — class, 14 lines</title></rect>
|
||||
<rect x="381.0" y="1748.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.Entry.line" data-kind="function" class="blk"><title>line — function, 2 lines</title></rect>
|
||||
<rect x="374.0" y="1751.0" width="74.0" height="8.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.RunFile" data-kind="class" class="blk"><title>RunFile — class, 17 lines</title></rect>
|
||||
<rect x="381.0" y="1753.5" width="60.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.RunFile.select" data-kind="function" class="blk"><title>select — function, 12 lines</title></rect>
|
||||
<rect x="374.0" y="1760.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._path" data-kind="function" class="blk"><title>_path — function, 6 lines</title></rect>
|
||||
<rect x="374.0" y="1764.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._within" data-kind="function" class="blk"><title>_within — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="1767.0" width="74.0" height="65.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.load" data-kind="function" class="blk"><title>load — function, 130 lines</title></rect>
|
||||
<rect x="381.0" y="1799.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.load.pick" data-kind="function" class="blk"><title>pick — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="1833.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.apply_reference" data-kind="function" class="blk"><title>apply_reference — function, 11 lines</title></rect>
|
||||
<rect x="374.0" y="1839.5" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.run" data-kind="function" class="blk"><title>run — function, 43 lines</title></rect>
|
||||
<rect x="456.0" y="1697.5" width="74.0" height="137.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.checks — 275 lines</title></rect>
|
||||
<rect x="456.0" y="1728.0" width="74.0" height="13.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Loaded" data-kind="class" class="blk"><title>Loaded — class, 26 lines</title></rect>
|
||||
<rect x="463.0" y="1729.5" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.__init__" data-kind="function" class="blk"><title>__init__ — function, 15 lines</title></rect>
|
||||
<rect x="463.0" y="1738.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.larder" data-kind="function" class="blk"><title>larder — function, 2 lines</title></rect>
|
||||
<rect x="463.0" y="1740.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.measure" data-kind="function" class="blk"><title>measure — function, 2 lines</title></rect>
|
||||
<rect x="456.0" y="1742.0" width="74.0" height="18.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Report" data-kind="class" class="blk"><title>Report — class, 37 lines</title></rect>
|
||||
<rect x="463.0" y="1743.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.__init__" data-kind="function" class="blk"><title>__init__ — function, 2 lines</title></rect>
|
||||
<rect x="463.0" y="1745.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.note" data-kind="function" class="blk"><title>note — function, 2 lines</title></rect>
|
||||
<rect x="463.0" y="1746.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.check" data-kind="function" class="blk"><title>check — function, 8 lines</title></rect>
|
||||
<rect x="463.0" y="1751.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.skip" data-kind="function" class="blk"><title>skip — function, 3 lines</title></rect>
|
||||
<rect x="463.0" y="1753.0" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.total" data-kind="function" class="blk"><title>total — function, 15 lines</title></rect>
|
||||
<rect x="456.0" y="1761.5" width="74.0" height="41.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.generated" data-kind="function" class="blk"><title>generated — function, 83 lines</title></rect>
|
||||
<rect x="456.0" y="1804.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.custom" data-kind="function" class="blk"><title>custom — function, 26 lines</title></rect>
|
||||
<rect x="456.0" y="1818.0" width="74.0" height="16.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks._run_cells" data-kind="function" class="blk"><title>_run_cells — function, 33 lines</title></rect>
|
||||
<rect x="538.0" y="1697.5" width="74.0" height="98.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.larder — 196 lines</title></rect>
|
||||
<rect x="538.0" y="1729.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder._count" data-kind="function" class="blk"><title>_count — function, 13 lines</title></rect>
|
||||
<rect x="538.0" y="1736.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.redact" data-kind="function" class="blk"><title>redact — function, 32 lines</title></rect>
|
||||
<rect x="538.0" y="1754.0" width="74.0" height="38.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.larder.Larder" data-kind="class" class="blk"><title>Larder — class, 77 lines</title></rect>
|
||||
<rect x="545.0" y="1761.0" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 7 lines</title></rect>
|
||||
<rect x="545.0" y="1765.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.read" data-kind="function" class="blk"><title>read — function, 3 lines</title></rect>
|
||||
<rect x="545.0" y="1767.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.fail" data-kind="function" class="blk"><title>fail — function, 3 lines</title></rect>
|
||||
<rect x="545.0" y="1769.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.to_dict" data-kind="function" class="blk"><title>to_dict — function, 13 lines</title></rect>
|
||||
<rect x="545.0" y="1777.0" width="60.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.from_dict" data-kind="function" class="blk"><title>from_dict — function, 10 lines</title></rect>
|
||||
<rect x="545.0" y="1782.5" width="60.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.line" data-kind="function" class="blk"><title>line — function, 20 lines</title></rect>
|
||||
<rect x="538.0" y="1793.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.of" data-kind="function" class="blk"><title>of — function, 3 lines</title></rect>
|
||||
<rect x="638.0" y="1697.5" width="74.0" height="118.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.collect — 236 lines</title></rect>
|
||||
<rect x="638.0" y="1714.0" width="74.0" height="5.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Definition" data-kind="class" class="blk"><title>Definition — class, 10 lines</title></rect>
|
||||
<rect x="638.0" y="1720.5" width="74.0" height="6.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Module" data-kind="class" class="blk"><title>Module — class, 12 lines</title></rect>
|
||||
<rect x="638.0" y="1727.5" width="74.0" height="33.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._Collector" data-kind="class" class="blk"><title>_Collector — class, 67 lines</title></rect>
|
||||
<rect x="645.0" y="1729.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
|
||||
<rect x="645.0" y="1732.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector._define" data-kind="function" class="blk"><title>_define — function, 17 lines</title></rect>
|
||||
<rect x="645.0" y="1741.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ClassDef" data-kind="function" class="blk"><title>visit_ClassDef — function, 5 lines</title></rect>
|
||||
<rect x="645.0" y="1744.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_FunctionDef" data-kind="function" class="blk"><title>visit_FunctionDef — function, 5 lines</title></rect>
|
||||
<rect x="645.0" y="1749.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_Import" data-kind="function" class="blk"><title>visit_Import — function, 8 lines</title></rect>
|
||||
<rect x="645.0" y="1754.0" width="60.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ImportFrom" data-kind="function" class="blk"><title>visit_ImportFrom — function, 14 lines</title></rect>
|
||||
<rect x="638.0" y="1762.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._first_line" data-kind="function" class="blk"><title>_first_line — function, 5 lines</title></rect>
|
||||
<rect x="638.0" y="1765.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._name_of" data-kind="function" class="blk"><title>_name_of — function, 15 lines</title></rect>
|
||||
<rect x="638.0" y="1774.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._resolve_relative" data-kind="function" class="blk"><title>_resolve_relative — function, 16 lines</title></rect>
|
||||
<rect x="638.0" y="1783.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.module_name" data-kind="function" class="blk"><title>module_name — function, 26 lines</title></rect>
|
||||
<rect x="638.0" y="1797.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect_file" data-kind="function" class="blk"><title>collect_file — function, 21 lines</title></rect>
|
||||
<rect x="638.0" y="1808.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect" data-kind="function" class="blk"><title>collect — function, 13 lines</title></rect>
|
||||
<rect x="720.0" y="1697.5" width="74.0" height="92.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.resolve — 184 lines</title></rect>
|
||||
<rect x="720.0" y="1711.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._id_for" data-kind="function" class="blk"><title>_id_for — function, 2 lines</title></rect>
|
||||
<rect x="720.0" y="1713.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._resolve" data-kind="function" class="blk"><title>_resolve — function, 32 lines</title></rect>
|
||||
<rect x="720.0" y="1730.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.larder_of" data-kind="function" class="blk"><title>larder_of — function, 17 lines</title></rect>
|
||||
<rect x="720.0" y="1740.0" width="74.0" height="49.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.to_ir" data-kind="function" class="blk"><title>to_ir — function, 98 lines</title></rect>
|
||||
<rect x="727.0" y="1769.5" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.resolve.to_ir._point_at" data-kind="function" class="blk"><title>_point_at — function, 9 lines</title></rect>
|
||||
<rect x="28.0" y="1926.0" width="74.0" height="139.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.validate — 278 lines</title></rect>
|
||||
<rect x="28.0" y="1947.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.validate.IRError" data-kind="class" class="blk"><title>IRError — class, 2 lines</title></rect>
|
||||
<rect x="28.0" y="1949.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._schema" data-kind="function" class="blk"><title>_schema — function, 2 lines</title></rect>
|
||||
<rect x="28.0" y="1951.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._props" data-kind="function" class="blk"><title>_props — function, 5 lines</title></rect>
|
||||
<rect x="28.0" y="1954.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._fields" data-kind="function" class="blk"><title>_fields — function, 4 lines</title></rect>
|
||||
<rect x="28.0" y="1957.5" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check" data-kind="function" class="blk"><title>check — function, 108 lines</title></rect>
|
||||
<rect x="28.0" y="2019.0" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._check_larder" data-kind="function" class="blk"><title>_check_larder — function, 58 lines</title></rect>
|
||||
<rect x="28.0" y="2049.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.validate" data-kind="function" class="blk"><title>validate — function, 6 lines</title></rect>
|
||||
<rect x="28.0" y="2053.0" width="74.0" height="11.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check_model_matches_schema" data-kind="function" class="blk"><title>check_model_matches_schema — function, 23 lines</title></rect>
|
||||
<rect x="110.0" y="1926.0" width="74.0" height="82.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.model — 165 lines</title></rect>
|
||||
<rect x="110.0" y="1942.5" width="74.0" height="19.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Meta" data-kind="class" class="blk"><title>Meta — class, 38 lines</title></rect>
|
||||
<rect x="117.0" y="1952.5" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Meta.to_dict" data-kind="function" class="blk"><title>to_dict — function, 18 lines</title></rect>
|
||||
<rect x="110.0" y="1963.0" width="74.0" height="10.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Node" data-kind="class" class="blk"><title>Node — class, 21 lines</title></rect>
|
||||
<rect x="117.0" y="1967.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 3 lines</title></rect>
|
||||
<rect x="117.0" y="1969.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.to_dict" data-kind="function" class="blk"><title>to_dict — function, 8 lines</title></rect>
|
||||
<rect x="110.0" y="1975.0" width="74.0" height="7.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Edge" data-kind="class" class="blk"><title>Edge — class, 15 lines</title></rect>
|
||||
<rect x="117.0" y="1979.0" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Edge.to_dict" data-kind="function" class="blk"><title>to_dict — function, 7 lines</title></rect>
|
||||
<rect x="110.0" y="1984.0" width="74.0" height="24.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Graph" data-kind="class" class="blk"><title>Graph — class, 48 lines</title></rect>
|
||||
<rect x="117.0" y="1988.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.node" data-kind="function" class="blk"><title>node — function, 4 lines</title></rect>
|
||||
<rect x="117.0" y="1991.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.edge" data-kind="function" class="blk"><title>edge — function, 4 lines</title></rect>
|
||||
<rect x="117.0" y="1993.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.has" data-kind="function" class="blk"><title>has — function, 2 lines</title></rect>
|
||||
<rect x="117.0" y="1996.0" width="60.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.to_dict" data-kind="function" class="blk"><title>to_dict — function, 16 lines</title></rect>
|
||||
<rect x="117.0" y="2005.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.from_dict" data-kind="function" class="blk"><title>from_dict — function, 6 lines</title></rect>
|
||||
<rect x="210.0" y="1926.0" width="74.0" height="111.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.extract — 222 lines</title></rect>
|
||||
<rect x="210.0" y="1956.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._values_from" data-kind="function" class="blk"><title>_values_from — function, 17 lines</title></rect>
|
||||
<rect x="210.0" y="1966.0" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._normalise" data-kind="function" class="blk"><title>_normalise — function, 45 lines</title></rect>
|
||||
<rect x="210.0" y="1989.5" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._svg_files" data-kind="function" class="blk"><title>_svg_files — function, 25 lines</title></rect>
|
||||
<rect x="210.0" y="2003.0" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.harvest" data-kind="function" class="blk"><title>harvest — function, 43 lines</title></rect>
|
||||
<rect x="210.0" y="2025.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
|
||||
<rect x="210.0" y="2030.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.summarise" data-kind="function" class="blk"><title>summarise — function, 13 lines</title></rect>
|
||||
<rect x="292.0" y="1926.0" width="74.0" height="105.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.tokens — 211 lines</title></rect>
|
||||
<rect x="292.0" y="1954.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._top" data-kind="function" class="blk"><title>_top — function, 2 lines</title></rect>
|
||||
<rect x="292.0" y="1956.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._mode" data-kind="function" class="blk"><title>_mode — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="1958.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._luminance" data-kind="function" class="blk"><title>_luminance — function, 6 lines</title></rect>
|
||||
<rect x="292.0" y="1962.5" width="74.0" height="50.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.derive" data-kind="function" class="blk"><title>derive — function, 101 lines</title></rect>
|
||||
<rect x="299.0" y="1984.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.tokens.derive.accent" data-kind="function" class="blk"><title>accent — function, 2 lines</title></rect>
|
||||
<rect x="292.0" y="2014.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.geometry" data-kind="function" class="blk"><title>geometry — function, 14 lines</title></rect>
|
||||
<rect x="292.0" y="2022.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
|
||||
<rect x="292.0" y="2026.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.from_folder" data-kind="function" class="blk"><title>from_folder — function, 9 lines</title></rect>
|
||||
<rect x="392.0" y="1926.0" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen — 2 lines</title></rect>
|
||||
<rect x="492.0" y="1926.0" width="74.0" height="75.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.lab.pg_probe — 150 lines</title></rect>
|
||||
<rect x="492.0" y="1961.5" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.probe" data-kind="function" class="blk"><title>probe — function, 30 lines</title></rect>
|
||||
<rect x="492.0" y="1983.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe._simplify" data-kind="function" class="blk"><title>_simplify — function, 3 lines</title></rect>
|
||||
<rect x="492.0" y="1986.0" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.main" data-kind="function" class="blk"><title>main — function, 25 lines</title></rect>
|
||||
<rect x="592.0" y="1926.0" width="74.0" height="132.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.notebook.spec — 264 lines</title></rect>
|
||||
<rect x="592.0" y="1954.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec._step" data-kind="function" class="blk"><title>_step — function, 4 lines</title></rect>
|
||||
<rect x="592.0" y="1957.0" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.from_ir" data-kind="function" class="blk"><title>from_ir — function, 108 lines</title></rect>
|
||||
<rect x="599.0" y="1964.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.notebook.spec.from_ir._order" data-kind="function" class="blk"><title>_order — function, 5 lines</title></rect>
|
||||
<rect x="592.0" y="2012.0" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.scaffold" data-kind="function" class="blk"><title>scaffold — function, 20 lines</title></rect>
|
||||
<rect x="592.0" y="2023.0" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.merge" data-kind="function" class="blk"><title>merge — function, 58 lines</title></rect>
|
||||
<rect x="592.0" y="2053.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.load" data-kind="function" class="blk"><title>load — function, 2 lines</title></rect>
|
||||
<rect x="592.0" y="2055.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.dump" data-kind="function" class="blk"><title>dump — function, 5 lines</title></rect>
|
||||
<rect x="692.0" y="1926.0" width="74.0" height="242.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops.filter — 485 lines</title></rect>
|
||||
<rect x="692.0" y="1944.0" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter._rebuild" data-kind="function" class="blk"><title>_rebuild — function, 57 lines</title></rect>
|
||||
<rect x="699.0" y="1948.5" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.surviving_parent" data-kind="function" class="blk"><title>surviving_parent — function, 5 lines</title></rect>
|
||||
<rect x="699.0" y="1955.5" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.lift" data-kind="function" class="blk"><title>lift — function, 6 lines</title></rect>
|
||||
<rect x="692.0" y="1973.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_kinds" data-kind="function" class="blk"><title>drop_kinds — function, 14 lines</title></rect>
|
||||
<rect x="692.0" y="1981.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.only_kinds" data-kind="function" class="blk"><title>only_kinds — function, 14 lines</title></rect>
|
||||
<rect x="692.0" y="1989.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_stdlib" data-kind="function" class="blk"><title>drop_stdlib — function, 13 lines</title></rect>
|
||||
<rect x="692.0" y="1997.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_external" data-kind="function" class="blk"><title>drop_external — function, 3 lines</title></rect>
|
||||
<rect x="692.0" y="1999.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.subtree" data-kind="function" class="blk"><title>subtree — function, 13 lines</title></rect>
|
||||
<rect x="692.0" y="2007.0" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.neighbourhood" data-kind="function" class="blk"><title>neighbourhood — function, 45 lines</title></rect>
|
||||
<rect x="692.0" y="2030.5" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.collapse_to_depth" data-kind="function" class="blk"><title>collapse_to_depth — function, 18 lines</title></rect>
|
||||
<rect x="699.0" y="2035.5" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.collapse_to_depth.level" data-kind="function" class="blk"><title>level — function, 6 lines</title></rect>
|
||||
<rect x="692.0" y="2040.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_builtins" data-kind="function" class="blk"><title>drop_builtins — function, 14 lines</title></rect>
|
||||
<rect x="692.0" y="2048.5" width="74.0" height="17.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.overview" data-kind="function" class="blk"><title>overview — function, 35 lines</title></rect>
|
||||
<rect x="692.0" y="2067.0" width="74.0" height="36.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.shape" data-kind="function" class="blk"><title>shape — function, 72 lines</title></rect>
|
||||
<rect x="699.0" y="2091.0" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.shape.rank_of" data-kind="function" class="blk"><title>rank_of — function, 9 lines</title></rect>
|
||||
<rect x="692.0" y="2104.0" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.split" data-kind="function" class="blk"><title>split — function, 24 lines</title></rect>
|
||||
<rect x="692.0" y="2117.0" width="74.0" height="51.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.classify" data-kind="function" class="blk"><title>classify — function, 102 lines</title></rect>
|
||||
<rect x="28.0" y="1471.5" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir — 7 lines</title></rect>
|
||||
<rect x="110.0" y="1471.5" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors — 2 lines</title></rect>
|
||||
<rect x="210.0" y="1471.5" width="74.0" height="269.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.site — 539 lines</title></rect>
|
||||
<rect x="210.0" y="1629.0" width="74.0" height="27.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._ledger" data-kind="function" class="blk"><title>_ledger — function, 54 lines</title></rect>
|
||||
<rect x="210.0" y="1657.0" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._slots" data-kind="function" class="blk"><title>_slots — function, 15 lines</title></rect>
|
||||
<rect x="210.0" y="1665.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._fill" data-kind="function" class="blk"><title>_fill — function, 4 lines</title></rect>
|
||||
<rect x="210.0" y="1668.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sidebar" data-kind="function" class="blk"><title>_sidebar — function, 17 lines</title></rect>
|
||||
<rect x="210.0" y="1678.0" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site._sections" data-kind="function" class="blk"><title>_sections — function, 15 lines</title></rect>
|
||||
<rect x="210.0" y="1686.5" width="74.0" height="48.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.emit" data-kind="function" class="blk"><title>emit — function, 96 lines</title></rect>
|
||||
<rect x="210.0" y="1735.5" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.site.write" data-kind="function" class="blk"><title>write — function, 10 lines</title></rect>
|
||||
<rect x="292.0" y="1471.5" width="74.0" height="160.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.notebook — 320 lines</title></rect>
|
||||
<rect x="292.0" y="1492.5" width="74.0" height="9.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._cell" data-kind="function" class="blk"><title>_cell — function, 19 lines</title></rect>
|
||||
<rect x="292.0" y="1503.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._example" data-kind="function" class="blk"><title>_example — function, 26 lines</title></rect>
|
||||
<rect x="292.0" y="1536.5" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._params_cell" data-kind="function" class="blk"><title>_params_cell — function, 18 lines</title></rect>
|
||||
<rect x="292.0" y="1546.5" width="74.0" height="20.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_cell" data-kind="function" class="blk"><title>_call_cell — function, 40 lines</title></rect>
|
||||
<rect x="292.0" y="1567.5" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._call_md" data-kind="function" class="blk"><title>_call_md — function, 26 lines</title></rect>
|
||||
<rect x="292.0" y="1581.5" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._figure_md" data-kind="function" class="blk"><title>_figure_md — function, 21 lines</title></rect>
|
||||
<rect x="292.0" y="1593.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook._figure_attachments" data-kind="function" class="blk"><title>_figure_attachments — function, 7 lines</title></rect>
|
||||
<rect x="292.0" y="1597.5" width="74.0" height="27.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.build" data-kind="function" class="blk"><title>build — function, 55 lines</title></rect>
|
||||
<rect x="292.0" y="1626.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.emit" data-kind="function" class="blk"><title>emit — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="1628.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.notebook.write" data-kind="function" class="blk"><title>write — function, 5 lines</title></rect>
|
||||
<rect x="374.0" y="1471.5" width="74.0" height="158.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.explore — 316 lines</title></rect>
|
||||
<rect x="374.0" y="1491.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._is_schema" data-kind="function" class="blk"><title>_is_schema — function, 4 lines</title></rect>
|
||||
<rect x="374.0" y="1494.0" width="74.0" height="24.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._neighbourhood_svgs" data-kind="function" class="blk"><title>_neighbourhood_svgs — function, 48 lines</title></rect>
|
||||
<rect x="381.0" y="1500.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
|
||||
<rect x="381.0" y="1504.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.explore._neighbourhood_svgs.render_one#L66" data-kind="function" class="blk"><title>render_one — function, 2 lines</title></rect>
|
||||
<rect x="374.0" y="1519.0" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore._facts" data-kind="function" class="blk"><title>_facts — function, 27 lines</title></rect>
|
||||
<rect x="374.0" y="1533.5" width="74.0" height="86.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.emit" data-kind="function" class="blk"><title>emit — function, 172 lines</title></rect>
|
||||
<rect x="374.0" y="1620.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.explore.write" data-kind="function" class="blk"><title>write — function, 17 lines</title></rect>
|
||||
<rect x="456.0" y="1471.5" width="74.0" height="154.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.dot — 309 lines</title></rect>
|
||||
<rect x="456.0" y="1490.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.RenderError" data-kind="class" class="blk"><title>RenderError — class, 2 lines</title></rect>
|
||||
<rect x="456.0" y="1492.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._esc" data-kind="function" class="blk"><title>_esc — function, 2 lines</title></rect>
|
||||
<rect x="456.0" y="1494.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._attrs" data-kind="function" class="blk"><title>_attrs — function, 3 lines</title></rect>
|
||||
<rect x="456.0" y="1496.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._style_words" data-kind="function" class="blk"><title>_style_words — function, 9 lines</title></rect>
|
||||
<rect x="456.0" y="1502.0" width="74.0" height="16.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._node_attrs" data-kind="function" class="blk"><title>_node_attrs — function, 33 lines</title></rect>
|
||||
<rect x="456.0" y="1519.5" width="74.0" height="56.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.emit" data-kind="function" class="blk"><title>emit — function, 113 lines</title></rect>
|
||||
<rect x="463.0" y="1536.0" width="60.0" height="15.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot.emit.write" data-kind="function" class="blk"><title>write — function, 31 lines</title></rect>
|
||||
<rect x="456.0" y="1577.0" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._within" data-kind="function" class="blk"><title>_within — function, 8 lines</title></rect>
|
||||
<rect x="456.0" y="1582.0" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._endpoints" data-kind="function" class="blk"><title>_endpoints — function, 41 lines</title></rect>
|
||||
<rect x="463.0" y="1587.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.walk" data-kind="function" class="blk"><title>walk — function, 4 lines</title></rect>
|
||||
<rect x="463.0" y="1591.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.collapsed" data-kind="function" class="blk"><title>collapsed — function, 2 lines</title></rect>
|
||||
<rect x="463.0" y="1592.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.dot._endpoints.first_leaf" data-kind="function" class="blk"><title>first_leaf — function, 4 lines</title></rect>
|
||||
<rect x="456.0" y="1603.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._safe" data-kind="function" class="blk"><title>_safe — function, 2 lines</title></rect>
|
||||
<rect x="456.0" y="1605.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot._q" data-kind="function" class="blk"><title>_q — function, 2 lines</title></rect>
|
||||
<rect x="456.0" y="1609.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.have_graphviz" data-kind="function" class="blk"><title>have_graphviz — function, 2 lines</title></rect>
|
||||
<rect x="456.0" y="1611.0" width="74.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.dot.render" data-kind="function" class="blk"><title>render — function, 29 lines</title></rect>
|
||||
<rect x="538.0" y="1471.5" width="74.0" height="139.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.minimap — 278 lines</title></rect>
|
||||
<rect x="538.0" y="1503.0" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._files" data-kind="function" class="blk"><title>_files — function, 57 lines</title></rect>
|
||||
<rect x="545.0" y="1507.0" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.declared" data-kind="function" class="blk"><title>declared — function, 18 lines</title></rect>
|
||||
<rect x="545.0" y="1516.5" width="60.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.minimap._files.build" data-kind="function" class="blk"><title>build — function, 11 lines</title></rect>
|
||||
<rect x="538.0" y="1532.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._bands" data-kind="function" class="blk"><title>_bands — function, 7 lines</title></rect>
|
||||
<rect x="538.0" y="1537.0" width="74.0" height="9.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap._blocks" data-kind="function" class="blk"><title>_blocks — function, 19 lines</title></rect>
|
||||
<rect x="538.0" y="1547.5" width="74.0" height="60.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.emit" data-kind="function" class="blk"><title>emit — function, 120 lines</title></rect>
|
||||
<rect x="538.0" y="1608.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.minimap.marks_to_labels" data-kind="function" class="blk"><title>marks_to_labels — function, 3 lines</title></rect>
|
||||
<rect x="620.0" y="1471.5" width="74.0" height="138.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.erd — 276 lines</title></rect>
|
||||
<rect x="620.0" y="1498.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._truncate" data-kind="function" class="blk"><title>_truncate — function, 3 lines</title></rect>
|
||||
<rect x="620.0" y="1501.0" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._tables" data-kind="function" class="blk"><title>_tables — function, 20 lines</title></rect>
|
||||
<rect x="620.0" y="1512.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._card_height" data-kind="function" class="blk"><title>_card_height — function, 3 lines</title></rect>
|
||||
<rect x="620.0" y="1514.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.max_columns" data-kind="function" class="blk"><title>max_columns — function, 9 lines</title></rect>
|
||||
<rect x="620.0" y="1520.0" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.layout" data-kind="function" class="blk"><title>layout — function, 25 lines</title></rect>
|
||||
<rect x="620.0" y="1533.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd._field_y" data-kind="function" class="blk"><title>_field_y — function, 3 lines</title></rect>
|
||||
<rect x="620.0" y="1536.0" width="74.0" height="73.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.erd.emit" data-kind="function" class="blk"><title>emit — function, 146 lines</title></rect>
|
||||
<rect x="702.0" y="1471.5" width="74.0" height="135.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.docsite — 270 lines</title></rect>
|
||||
<rect x="702.0" y="1504.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.docsite._note" data-kind="function" class="blk"><title>_note — function, 7 lines</title></rect>
|
||||
<rect x="702.0" y="1509.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.docsite._missing" data-kind="function" class="blk"><title>_missing — function, 9 lines</title></rect>
|
||||
<rect x="702.0" y="1514.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.docsite._tree" data-kind="function" class="blk"><title>_tree — function, 15 lines</title></rect>
|
||||
<rect x="709.0" y="1517.5" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.docsite._tree.walk" data-kind="function" class="blk"><title>walk — function, 7 lines</title></rect>
|
||||
<rect x="702.0" y="1523.0" width="74.0" height="34.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.docsite._page" data-kind="function" class="blk"><title>_page — function, 69 lines</title></rect>
|
||||
<rect x="702.0" y="1558.5" width="74.0" height="47.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.docsite.write" data-kind="function" class="blk"><title>write — function, 95 lines</title></rect>
|
||||
<rect x="709.0" y="1566.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.docsite.write.put" data-kind="function" class="blk"><title>put — function, 4 lines</title></rect>
|
||||
<rect x="784.0" y="1471.5" width="74.0" height="82.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.index — 164 lines</title></rect>
|
||||
<rect x="784.0" y="1488.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._tree" data-kind="function" class="blk"><title>_tree — function, 8 lines</title></rect>
|
||||
<rect x="784.0" y="1493.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index._anchor" data-kind="function" class="blk"><title>_anchor — function, 5 lines</title></rect>
|
||||
<rect x="784.0" y="1497.0" width="74.0" height="42.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_markdown" data-kind="function" class="blk"><title>to_markdown — function, 84 lines</title></rect>
|
||||
<rect x="791.0" y="1508.5" width="60.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_markdown.walk" data-kind="function" class="blk"><title>walk — function, 27 lines</title></rect>
|
||||
<rect x="784.0" y="1540.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.index.to_sidebar" data-kind="function" class="blk"><title>to_sidebar — function, 26 lines</title></rect>
|
||||
<rect x="791.0" y="1543.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.index.to_sidebar.build" data-kind="function" class="blk"><title>build — function, 13 lines</title></rect>
|
||||
<rect x="28.0" y="1779.0" width="74.0" height="73.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.markdown — 146 lines</title></rect>
|
||||
<rect x="28.0" y="1789.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.markdown._url" data-kind="function" class="blk"><title>_url — function, 7 lines</title></rect>
|
||||
<rect x="28.0" y="1798.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.markdown._inline" data-kind="function" class="blk"><title>_inline — function, 13 lines</title></rect>
|
||||
<rect x="35.0" y="1800.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.markdown._inline.keep" data-kind="function" class="blk"><title>keep — function, 3 lines</title></rect>
|
||||
<rect x="28.0" y="1806.0" width="74.0" height="45.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.markdown.to_html" data-kind="function" class="blk"><title>to_html — function, 91 lines</title></rect>
|
||||
<rect x="35.0" y="1809.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.markdown.to_html.para" data-kind="function" class="blk"><title>para — function, 3 lines</title></rect>
|
||||
<rect x="35.0" y="1841.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.markdown.to_html.cells" data-kind="function" class="blk"><title>cells — function, 2 lines</title></rect>
|
||||
<rect x="110.0" y="1779.0" width="74.0" height="59.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.paged — 119 lines</title></rect>
|
||||
<rect x="110.0" y="1795.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.paged.svg_width_px" data-kind="function" class="blk"><title>svg_width_px — function, 6 lines</title></rect>
|
||||
<rect x="110.0" y="1799.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.paged._estimate_px" data-kind="function" class="blk"><title>_estimate_px — function, 10 lines</title></rect>
|
||||
<rect x="110.0" y="1805.0" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.paged.fits_for" data-kind="function" class="blk"><title>fits_for — function, 17 lines</title></rect>
|
||||
<rect x="117.0" y="1809.5" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.emitters.paged.fits_for.fits" data-kind="function" class="blk"><title>fits — function, 6 lines</title></rect>
|
||||
<rect x="110.0" y="1814.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.paged.links_for" data-kind="function" class="blk"><title>links_for — function, 15 lines</title></rect>
|
||||
<rect x="110.0" y="1823.0" width="74.0" height="9.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.paged.render" data-kind="function" class="blk"><title>render — function, 19 lines</title></rect>
|
||||
<rect x="110.0" y="1833.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.paged.inline" data-kind="function" class="blk"><title>inline — function, 9 lines</title></rect>
|
||||
<rect x="192.0" y="1779.0" width="74.0" height="32.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.emitters.auto — 64 lines</title></rect>
|
||||
<rect x="192.0" y="1792.5" width="74.0" height="18.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.emitters.auto.draw" data-kind="function" class="blk"><title>draw — function, 36 lines</title></rect>
|
||||
<rect x="292.0" y="1779.0" width="74.0" height="126.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.emit — 253 lines</title></rect>
|
||||
<rect x="292.0" y="1791.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit._parser" data-kind="function" class="blk"><title>_parser — function, 7 lines</title></rect>
|
||||
<rect x="292.0" y="1795.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit._size" data-kind="function" class="blk"><title>_size — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="1798.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.auto" data-kind="function" class="blk"><title>auto — function, 16 lines</title></rect>
|
||||
<rect x="292.0" y="1807.0" width="74.0" height="19.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.dot" data-kind="function" class="blk"><title>dot — function, 38 lines</title></rect>
|
||||
<rect x="292.0" y="1827.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.erd" data-kind="function" class="blk"><title>erd — function, 11 lines</title></rect>
|
||||
<rect x="292.0" y="1833.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.index" data-kind="function" class="blk"><title>index — function, 14 lines</title></rect>
|
||||
<rect x="292.0" y="1841.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.minimap" data-kind="function" class="blk"><title>minimap — function, 14 lines</title></rect>
|
||||
<rect x="292.0" y="1849.5" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.notebook" data-kind="function" class="blk"><title>notebook — function, 41 lines</title></rect>
|
||||
<rect x="292.0" y="1871.0" width="74.0" height="15.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.site" data-kind="function" class="blk"><title>site — function, 31 lines</title></rect>
|
||||
<rect x="292.0" y="1887.5" width="74.0" height="11.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.explore" data-kind="function" class="blk"><title>explore — function, 22 lines</title></rect>
|
||||
<rect x="292.0" y="1901.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.emit.main" data-kind="function" class="blk"><title>main — function, 7 lines</title></rect>
|
||||
<rect x="374.0" y="1779.0" width="74.0" height="59.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.extract — 119 lines</title></rect>
|
||||
<rect x="374.0" y="1788.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract._parser" data-kind="function" class="blk"><title>_parser — function, 5 lines</title></rect>
|
||||
<rect x="374.0" y="1792.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract._counts" data-kind="function" class="blk"><title>_counts — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="1794.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.python" data-kind="function" class="blk"><title>python — function, 15 lines</title></rect>
|
||||
<rect x="374.0" y="1803.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.code" data-kind="function" class="blk"><title>code — function, 14 lines</title></rect>
|
||||
<rect x="374.0" y="1811.0" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.db" data-kind="function" class="blk"><title>db — function, 11 lines</title></rect>
|
||||
<rect x="374.0" y="1817.5" width="74.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.openapi" data-kind="function" class="blk"><title>openapi — function, 12 lines</title></rect>
|
||||
<rect x="374.0" y="1824.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.usage" data-kind="function" class="blk"><title>usage — function, 15 lines</title></rect>
|
||||
<rect x="374.0" y="1834.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.extract.main" data-kind="function" class="blk"><title>main — function, 7 lines</title></rect>
|
||||
<rect x="456.0" y="1779.0" width="74.0" height="49.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.run — 99 lines</title></rect>
|
||||
<rect x="456.0" y="1789.0" width="74.0" height="39.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.run.main" data-kind="function" class="blk"><title>main — function, 78 lines</title></rect>
|
||||
<rect x="538.0" y="1779.0" width="74.0" height="45.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.view — 90 lines</title></rect>
|
||||
<rect x="538.0" y="1786.0" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.view.main" data-kind="function" class="blk"><title>main — function, 75 lines</title></rect>
|
||||
<rect x="620.0" y="1779.0" width="74.0" height="44.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.scaffold — 88 lines</title></rect>
|
||||
<rect x="620.0" y="1789.5" width="74.0" height="33.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.scaffold.main" data-kind="function" class="blk"><title>main — function, 66 lines</title></rect>
|
||||
<rect x="702.0" y="1779.0" width="74.0" height="40.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.common — 80 lines</title></rect>
|
||||
<rect x="702.0" y="1789.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.cli.common.Abort" data-kind="class" class="blk"><title>Abort — class, 2 lines</title></rect>
|
||||
<rect x="702.0" y="1791.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.read_json" data-kind="function" class="blk"><title>read_json — function, 5 lines</title></rect>
|
||||
<rect x="702.0" y="1794.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.read_ir" data-kind="function" class="blk"><title>read_ir — function, 11 lines</title></rect>
|
||||
<rect x="702.0" y="1801.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.load_style" data-kind="function" class="blk"><title>load_style — function, 7 lines</title></rect>
|
||||
<rect x="702.0" y="1805.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.style_args" data-kind="function" class="blk"><title>style_args — function, 3 lines</title></rect>
|
||||
<rect x="702.0" y="1808.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.emit" data-kind="function" class="blk"><title>emit — function, 16 lines</title></rect>
|
||||
<rect x="702.0" y="1817.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.common.dump_ir" data-kind="function" class="blk"><title>dump_ir — function, 3 lines</title></rect>
|
||||
<rect x="784.0" y="1779.0" width="74.0" height="38.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.book — 77 lines</title></rect>
|
||||
<rect x="784.0" y="1789.5" width="74.0" height="27.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.book.main" data-kind="function" class="blk"><title>main — function, 55 lines</title></rect>
|
||||
<rect x="28.0" y="1943.5" width="74.0" height="29.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.check — 58 lines</title></rect>
|
||||
<rect x="28.0" y="1953.5" width="74.0" height="18.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.check.main" data-kind="function" class="blk"><title>main — function, 37 lines</title></rect>
|
||||
<rect x="110.0" y="1943.5" width="74.0" height="17.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.contract — 34 lines</title></rect>
|
||||
<rect x="110.0" y="1956.0" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.contract.main" data-kind="function" class="blk"><title>main — function, 8 lines</title></rect>
|
||||
<rect x="192.0" y="1943.5" width="74.0" height="16.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.cli.validate — 33 lines</title></rect>
|
||||
<rect x="192.0" y="1950.5" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.cli.validate.main" data-kind="function" class="blk"><title>main — function, 18 lines</title></rect>
|
||||
<rect x="292.0" y="1943.5" width="74.0" height="308.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.config — 616 lines</title></rect>
|
||||
<rect x="292.0" y="1984.0" width="74.0" height="3.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.ConfigError" data-kind="class" class="blk"><title>ConfigError — class, 6 lines</title></rect>
|
||||
<rect x="299.0" y="1985.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.ConfigError.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="1988.5" width="74.0" height="7.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.Entry" data-kind="class" class="blk"><title>Entry — class, 15 lines</title></rect>
|
||||
<rect x="299.0" y="1995.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.Entry.line" data-kind="function" class="blk"><title>line — function, 2 lines</title></rect>
|
||||
<rect x="292.0" y="1997.5" width="74.0" height="8.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.RunFile" data-kind="class" class="blk"><title>RunFile — class, 17 lines</title></rect>
|
||||
<rect x="299.0" y="2000.0" width="60.0" height="6.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.RunFile.select" data-kind="function" class="blk"><title>select — function, 12 lines</title></rect>
|
||||
<rect x="292.0" y="2007.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._path" data-kind="function" class="blk"><title>_path — function, 6 lines</title></rect>
|
||||
<rect x="292.0" y="2011.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._within" data-kind="function" class="blk"><title>_within — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="2013.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._read" data-kind="function" class="blk"><title>_read — function, 9 lines</title></rect>
|
||||
<rect x="292.0" y="2019.0" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.load" data-kind="function" class="blk"><title>load — function, 18 lines</title></rect>
|
||||
<rect x="292.0" y="2029.0" width="74.0" height="60.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._load_books" data-kind="function" class="blk"><title>_load_books — function, 121 lines</title></rect>
|
||||
<rect x="299.0" y="2055.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config._load_books.pick" data-kind="function" class="blk"><title>pick — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="2090.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.apply_reference" data-kind="function" class="blk"><title>apply_reference — function, 11 lines</title></rect>
|
||||
<rect x="292.0" y="2097.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config._checks" data-kind="function" class="blk"><title>_checks — function, 13 lines</title></rect>
|
||||
<rect x="292.0" y="2104.5" width="74.0" height="18.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.run" data-kind="function" class="blk"><title>run — function, 36 lines</title></rect>
|
||||
<rect x="292.0" y="2143.5" width="74.0" height="4.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.config.ProjectFile" data-kind="class" class="blk"><title>ProjectFile — class, 8 lines</title></rect>
|
||||
<rect x="299.0" y="2146.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.config.ProjectFile.books" data-kind="function" class="blk"><title>books — function, 2 lines</title></rect>
|
||||
<rect x="292.0" y="2148.5" width="74.0" height="75.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.load_project" data-kind="function" class="blk"><title>load_project — function, 151 lines</title></rect>
|
||||
<rect x="292.0" y="2225.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.select_parts" data-kind="function" class="blk"><title>select_parts — function, 9 lines</title></rect>
|
||||
<rect x="292.0" y="2230.5" width="74.0" height="20.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.config.run_project" data-kind="function" class="blk"><title>run_project — function, 41 lines</title></rect>
|
||||
<rect x="374.0" y="1943.5" width="74.0" height="224.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.document — 449 lines</title></rect>
|
||||
<rect x="374.0" y="1964.5" width="74.0" height="3.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.document.Output" data-kind="class" class="blk"><title>Output — class, 6 lines</title></rect>
|
||||
<rect x="374.0" y="1969.0" width="74.0" height="5.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.document.Part" data-kind="class" class="blk"><title>Part — class, 11 lines</title></rect>
|
||||
<rect x="374.0" y="1976.0" width="74.0" height="5.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.document.Document" data-kind="class" class="blk"><title>Document — class, 11 lines</title></rect>
|
||||
<rect x="374.0" y="1985.0" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.apply_ops" data-kind="function" class="blk"><title>apply_ops — function, 15 lines</title></rect>
|
||||
<rect x="374.0" y="1994.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.clear_output" data-kind="function" class="blk"><title>clear_output — function, 27 lines</title></rect>
|
||||
<rect x="374.0" y="2009.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.write_manifest" data-kind="function" class="blk"><title>write_manifest — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="2012.5" width="74.0" height="11.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.rendered_pages" data-kind="function" class="blk"><title>rendered_pages — function, 22 lines</title></rect>
|
||||
<rect x="374.0" y="2024.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.caption" data-kind="function" class="blk"><title>caption — function, 14 lines</title></rect>
|
||||
<rect x="374.0" y="2033.5" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document._note_step" data-kind="function" class="blk"><title>_note_step — function, 8 lines</title></rect>
|
||||
<rect x="374.0" y="2038.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document._with_note" data-kind="function" class="blk"><title>_with_note — function, 11 lines</title></rect>
|
||||
<rect x="374.0" y="2045.0" width="74.0" height="45.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.notebook_spec" data-kind="function" class="blk"><title>notebook_spec — function, 91 lines</title></rect>
|
||||
<rect x="374.0" y="2092.5" width="74.0" height="58.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.build" data-kind="function" class="blk"><title>build — function, 116 lines</title></rect>
|
||||
<rect x="381.0" y="2096.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.document.build.say" data-kind="function" class="blk"><title>say — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="2151.5" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document._spots" data-kind="function" class="blk"><title>_spots — function, 16 lines</title></rect>
|
||||
<rect x="374.0" y="2160.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.known_spots" data-kind="function" class="blk"><title>known_spots — function, 3 lines</title></rect>
|
||||
<rect x="374.0" y="2163.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.document.plan_spots" data-kind="function" class="blk"><title>plan_spots — function, 9 lines</title></rect>
|
||||
<rect x="456.0" y="1943.5" width="74.0" height="209.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.build — 418 lines</title></rect>
|
||||
<rect x="456.0" y="1981.0" width="74.0" height="14.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.extract" data-kind="function" class="blk"><title>extract — function, 29 lines</title></rect>
|
||||
<rect x="456.0" y="1996.5" width="74.0" height="35.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.spec_from" data-kind="function" class="blk"><title>spec_from — function, 71 lines</title></rect>
|
||||
<rect x="456.0" y="2033.0" width="74.0" height="23.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build._load_cell" data-kind="function" class="blk"><title>_load_cell — function, 46 lines</title></rect>
|
||||
<rect x="456.0" y="2067.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.clear" data-kind="function" class="blk"><title>clear — function, 26 lines</title></rect>
|
||||
<rect x="456.0" y="2081.0" width="74.0" height="52.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.build_book" data-kind="function" class="blk"><title>build_book — function, 105 lines</title></rect>
|
||||
<rect x="463.0" y="2091.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.build.build_book.say" data-kind="function" class="blk"><title>say — function, 3 lines</title></rect>
|
||||
<rect x="456.0" y="2134.5" width="74.0" height="17.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.build.run" data-kind="function" class="blk"><title>run — function, 35 lines</title></rect>
|
||||
<rect x="538.0" y="1943.5" width="74.0" height="189.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.checks — 378 lines</title></rect>
|
||||
<rect x="538.0" y="1974.0" width="74.0" height="13.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Loaded" data-kind="class" class="blk"><title>Loaded — class, 26 lines</title></rect>
|
||||
<rect x="545.0" y="1975.5" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.__init__" data-kind="function" class="blk"><title>__init__ — function, 15 lines</title></rect>
|
||||
<rect x="545.0" y="1984.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.larder" data-kind="function" class="blk"><title>larder — function, 2 lines</title></rect>
|
||||
<rect x="545.0" y="1986.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Loaded.measure" data-kind="function" class="blk"><title>measure — function, 2 lines</title></rect>
|
||||
<rect x="538.0" y="1988.0" width="74.0" height="19.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.checks.Report" data-kind="class" class="blk"><title>Report — class, 38 lines</title></rect>
|
||||
<rect x="545.0" y="1989.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
|
||||
<rect x="545.0" y="1991.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.note" data-kind="function" class="blk"><title>note — function, 2 lines</title></rect>
|
||||
<rect x="545.0" y="1993.0" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.check" data-kind="function" class="blk"><title>check — function, 8 lines</title></rect>
|
||||
<rect x="545.0" y="1997.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.skip" data-kind="function" class="blk"><title>skip — function, 3 lines</title></rect>
|
||||
<rect x="545.0" y="1999.5" width="60.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.Report.total" data-kind="function" class="blk"><title>total — function, 15 lines</title></rect>
|
||||
<rect x="538.0" y="2008.0" width="74.0" height="44.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.generated" data-kind="function" class="blk"><title>generated — function, 89 lines</title></rect>
|
||||
<rect x="538.0" y="2053.5" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.custom" data-kind="function" class="blk"><title>custom — function, 26 lines</title></rect>
|
||||
<rect x="538.0" y="2067.5" width="74.0" height="16.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks._run_cells" data-kind="function" class="blk"><title>_run_cells — function, 33 lines</title></rect>
|
||||
<rect x="538.0" y="2086.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks._notes_drift" data-kind="function" class="blk"><title>_notes_drift — function, 7 lines</title></rect>
|
||||
<rect x="538.0" y="2090.5" width="74.0" height="36.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.document" data-kind="function" class="blk"><title>document — function, 72 lines</title></rect>
|
||||
<rect x="545.0" y="2104.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.checks.document.heading" data-kind="function" class="blk"><title>heading — function, 2 lines</title></rect>
|
||||
<rect x="538.0" y="2127.5" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.checks.document_failures" data-kind="function" class="blk"><title>document_failures — function, 9 lines</title></rect>
|
||||
<rect x="620.0" y="1943.5" width="74.0" height="98.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.larder — 196 lines</title></rect>
|
||||
<rect x="620.0" y="1975.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder._count" data-kind="function" class="blk"><title>_count — function, 13 lines</title></rect>
|
||||
<rect x="620.0" y="1982.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.redact" data-kind="function" class="blk"><title>redact — function, 32 lines</title></rect>
|
||||
<rect x="620.0" y="2000.0" width="74.0" height="38.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.larder.Larder" data-kind="class" class="blk"><title>Larder — class, 77 lines</title></rect>
|
||||
<rect x="627.0" y="2007.0" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 7 lines</title></rect>
|
||||
<rect x="627.0" y="2011.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.read" data-kind="function" class="blk"><title>read — function, 3 lines</title></rect>
|
||||
<rect x="627.0" y="2013.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.fail" data-kind="function" class="blk"><title>fail — function, 3 lines</title></rect>
|
||||
<rect x="627.0" y="2015.5" width="60.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.to_dict" data-kind="function" class="blk"><title>to_dict — function, 13 lines</title></rect>
|
||||
<rect x="627.0" y="2023.0" width="60.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.from_dict" data-kind="function" class="blk"><title>from_dict — function, 10 lines</title></rect>
|
||||
<rect x="627.0" y="2028.5" width="60.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.larder.Larder.line" data-kind="function" class="blk"><title>line — function, 20 lines</title></rect>
|
||||
<rect x="620.0" y="2039.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.larder.of" data-kind="function" class="blk"><title>of — function, 3 lines</title></rect>
|
||||
<rect x="702.0" y="1943.5" width="74.0" height="81.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.book.notes — 162 lines</title></rect>
|
||||
<rect x="702.0" y="1966.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.notes.body" data-kind="function" class="blk"><title>body — function, 6 lines</title></rect>
|
||||
<rect x="702.0" y="1970.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.notes.stub" data-kind="function" class="blk"><title>stub — function, 3 lines</title></rect>
|
||||
<rect x="702.0" y="1973.0" width="74.0" height="39.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.book.notes.Notes" data-kind="class" class="blk"><title>Notes — class, 78 lines</title></rect>
|
||||
<rect x="709.0" y="1974.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.notes.Notes.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
|
||||
<rect x="709.0" y="1976.5" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.notes.Notes._read" data-kind="function" class="blk"><title>_read — function, 7 lines</title></rect>
|
||||
<rect x="709.0" y="1980.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.notes.Notes.document" data-kind="function" class="blk"><title>document — function, 2 lines</title></rect>
|
||||
<rect x="709.0" y="1982.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.notes.Notes.book" data-kind="function" class="blk"><title>book — function, 2 lines</title></rect>
|
||||
<rect x="709.0" y="1983.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.notes.Notes.path" data-kind="function" class="blk"><title>path — function, 4 lines</title></rect>
|
||||
<rect x="709.0" y="1986.0" width="60.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.notes.Notes.drift" data-kind="function" class="blk"><title>drift — function, 27 lines</title></rect>
|
||||
<rect x="709.0" y="2000.0" width="60.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.book.notes.Notes.scaffold" data-kind="function" class="blk"><title>scaffold — function, 24 lines</title></rect>
|
||||
<rect x="702.0" y="2013.0" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.notes.images" data-kind="function" class="blk"><title>images — function, 18 lines</title></rect>
|
||||
<rect x="702.0" y="2023.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.book.notes.data_uri" data-kind="function" class="blk"><title>data_uri — function, 2 lines</title></rect>
|
||||
<rect x="28.0" y="2289.5" width="74.0" height="141.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.code — 282 lines</title></rect>
|
||||
<rect x="28.0" y="2337.0" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.code.MissingParser" data-kind="class" class="blk"><title>MissingParser — class, 2 lines</title></rect>
|
||||
<rect x="28.0" y="2339.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._parser" data-kind="function" class="blk"><title>_parser — function, 21 lines</title></rect>
|
||||
<rect x="28.0" y="2350.5" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._name" data-kind="function" class="blk"><title>_name — function, 10 lines</title></rect>
|
||||
<rect x="28.0" y="2356.5" width="74.0" height="17.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._walk" data-kind="function" class="blk"><title>_walk — function, 34 lines</title></rect>
|
||||
<rect x="28.0" y="2374.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract_file" data-kind="function" class="blk"><title>extract_file — function, 27 lines</title></rect>
|
||||
<rect x="28.0" y="2389.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code._count_errors" data-kind="function" class="blk"><title>_count_errors — function, 5 lines</title></rect>
|
||||
<rect x="28.0" y="2392.5" width="74.0" height="37.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.code.extract" data-kind="function" class="blk"><title>extract — function, 75 lines</title></rect>
|
||||
<rect x="110.0" y="2289.5" width="74.0" height="134.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.usage — 269 lines</title></rect>
|
||||
<rect x="110.0" y="2321.5" width="74.0" height="13.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._template" data-kind="function" class="blk"><title>_template — function, 27 lines</title></rect>
|
||||
<rect x="110.0" y="2336.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._body" data-kind="function" class="blk"><title>_body — function, 10 lines</title></rect>
|
||||
<rect x="110.0" y="2342.0" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._shape" data-kind="function" class="blk"><title>_shape — function, 15 lines</title></rect>
|
||||
<rect x="110.0" y="2350.5" width="74.0" height="5.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage._graphql" data-kind="function" class="blk"><title>_graphql — function, 11 lines</title></rect>
|
||||
<rect x="110.0" y="2357.0" width="74.0" height="66.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.usage.extract" data-kind="function" class="blk"><title>extract — function, 133 lines</title></rect>
|
||||
<rect x="192.0" y="2289.5" width="74.0" height="95.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.openapi — 191 lines</title></rect>
|
||||
<rect x="192.0" y="2305.0" width="74.0" height="14.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._modelgen" data-kind="function" class="blk"><title>_modelgen — function, 28 lines</title></rect>
|
||||
<rect x="192.0" y="2320.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._type_name" data-kind="function" class="blk"><title>_type_name — function, 6 lines</title></rect>
|
||||
<rect x="192.0" y="2324.0" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi._refs" data-kind="function" class="blk"><title>_refs — function, 43 lines</title></rect>
|
||||
<rect x="192.0" y="2346.5" width="74.0" height="38.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.openapi.extract" data-kind="function" class="blk"><title>extract — function, 76 lines</title></rect>
|
||||
<rect x="274.0" y="2289.5" width="74.0" height="80.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.db — 161 lines</title></rect>
|
||||
<rect x="274.0" y="2309.0" width="74.0" height="41.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.from_schema_dict" data-kind="function" class="blk"><title>from_schema_dict — function, 82 lines</title></rect>
|
||||
<rect x="274.0" y="2351.0" width="74.0" height="5.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._relation" data-kind="function" class="blk"><title>_relation — function, 10 lines</title></rect>
|
||||
<rect x="274.0" y="2357.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._plain_type" data-kind="function" class="blk"><title>_plain_type — function, 6 lines</title></rect>
|
||||
<rect x="274.0" y="2361.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db._dedupe" data-kind="function" class="blk"><title>_dedupe — function, 9 lines</title></rect>
|
||||
<rect x="274.0" y="2366.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.db.extract" data-kind="function" class="blk"><title>extract — function, 6 lines</title></rect>
|
||||
<rect x="356.0" y="2289.5" width="74.0" height="18.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python — 37 lines</title></rect>
|
||||
<rect x="356.0" y="2299.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.extract" data-kind="function" class="blk"><title>extract — function, 14 lines</title></rect>
|
||||
<rect x="456.0" y="2289.5" width="74.0" height="118.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.collect — 236 lines</title></rect>
|
||||
<rect x="456.0" y="2306.0" width="74.0" height="5.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Definition" data-kind="class" class="blk"><title>Definition — class, 10 lines</title></rect>
|
||||
<rect x="456.0" y="2312.5" width="74.0" height="6.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.Module" data-kind="class" class="blk"><title>Module — class, 12 lines</title></rect>
|
||||
<rect x="456.0" y="2319.5" width="74.0" height="33.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._Collector" data-kind="class" class="blk"><title>_Collector — class, 67 lines</title></rect>
|
||||
<rect x="463.0" y="2321.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.__init__" data-kind="function" class="blk"><title>__init__ — function, 3 lines</title></rect>
|
||||
<rect x="463.0" y="2324.0" width="60.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector._define" data-kind="function" class="blk"><title>_define — function, 17 lines</title></rect>
|
||||
<rect x="463.0" y="2333.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ClassDef" data-kind="function" class="blk"><title>visit_ClassDef — function, 5 lines</title></rect>
|
||||
<rect x="463.0" y="2336.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_FunctionDef" data-kind="function" class="blk"><title>visit_FunctionDef — function, 5 lines</title></rect>
|
||||
<rect x="463.0" y="2341.5" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_Import" data-kind="function" class="blk"><title>visit_Import — function, 8 lines</title></rect>
|
||||
<rect x="463.0" y="2346.0" width="60.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.collect._Collector.visit_ImportFrom" data-kind="function" class="blk"><title>visit_ImportFrom — function, 14 lines</title></rect>
|
||||
<rect x="456.0" y="2354.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._first_line" data-kind="function" class="blk"><title>_first_line — function, 5 lines</title></rect>
|
||||
<rect x="456.0" y="2357.5" width="74.0" height="7.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._name_of" data-kind="function" class="blk"><title>_name_of — function, 15 lines</title></rect>
|
||||
<rect x="456.0" y="2366.0" width="74.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect._resolve_relative" data-kind="function" class="blk"><title>_resolve_relative — function, 16 lines</title></rect>
|
||||
<rect x="456.0" y="2375.0" width="74.0" height="13.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.module_name" data-kind="function" class="blk"><title>module_name — function, 26 lines</title></rect>
|
||||
<rect x="456.0" y="2389.0" width="74.0" height="10.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect_file" data-kind="function" class="blk"><title>collect_file — function, 21 lines</title></rect>
|
||||
<rect x="456.0" y="2400.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.collect.collect" data-kind="function" class="blk"><title>collect — function, 13 lines</title></rect>
|
||||
<rect x="538.0" y="2289.5" width="74.0" height="92.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.extractors.python.resolve — 184 lines</title></rect>
|
||||
<rect x="538.0" y="2303.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._id_for" data-kind="function" class="blk"><title>_id_for — function, 2 lines</title></rect>
|
||||
<rect x="538.0" y="2305.5" width="74.0" height="16.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve._resolve" data-kind="function" class="blk"><title>_resolve — function, 32 lines</title></rect>
|
||||
<rect x="538.0" y="2322.5" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.larder_of" data-kind="function" class="blk"><title>larder_of — function, 17 lines</title></rect>
|
||||
<rect x="538.0" y="2332.0" width="74.0" height="49.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.extractors.python.resolve.to_ir" data-kind="function" class="blk"><title>to_ir — function, 98 lines</title></rect>
|
||||
<rect x="545.0" y="2361.5" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.extractors.python.resolve.to_ir._point_at" data-kind="function" class="blk"><title>_point_at — function, 9 lines</title></rect>
|
||||
<rect x="638.0" y="2289.5" width="74.0" height="139.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.validate — 278 lines</title></rect>
|
||||
<rect x="638.0" y="2310.5" width="74.0" height="2.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.validate.IRError" data-kind="class" class="blk"><title>IRError — class, 2 lines</title></rect>
|
||||
<rect x="638.0" y="2312.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._schema" data-kind="function" class="blk"><title>_schema — function, 2 lines</title></rect>
|
||||
<rect x="638.0" y="2314.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._props" data-kind="function" class="blk"><title>_props — function, 5 lines</title></rect>
|
||||
<rect x="638.0" y="2318.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._fields" data-kind="function" class="blk"><title>_fields — function, 4 lines</title></rect>
|
||||
<rect x="638.0" y="2321.0" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check" data-kind="function" class="blk"><title>check — function, 108 lines</title></rect>
|
||||
<rect x="638.0" y="2382.5" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate._check_larder" data-kind="function" class="blk"><title>_check_larder — function, 58 lines</title></rect>
|
||||
<rect x="638.0" y="2412.5" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.validate" data-kind="function" class="blk"><title>validate — function, 6 lines</title></rect>
|
||||
<rect x="638.0" y="2416.5" width="74.0" height="11.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ir.validate.check_model_matches_schema" data-kind="function" class="blk"><title>check_model_matches_schema — function, 23 lines</title></rect>
|
||||
<rect x="720.0" y="2289.5" width="74.0" height="82.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ir.model — 165 lines</title></rect>
|
||||
<rect x="720.0" y="2306.0" width="74.0" height="19.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Meta" data-kind="class" class="blk"><title>Meta — class, 38 lines</title></rect>
|
||||
<rect x="727.0" y="2316.0" width="60.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Meta.to_dict" data-kind="function" class="blk"><title>to_dict — function, 18 lines</title></rect>
|
||||
<rect x="720.0" y="2326.5" width="74.0" height="10.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Node" data-kind="class" class="blk"><title>Node — class, 21 lines</title></rect>
|
||||
<rect x="727.0" y="2331.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.__post_init__" data-kind="function" class="blk"><title>__post_init__ — function, 3 lines</title></rect>
|
||||
<rect x="727.0" y="2333.0" width="60.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Node.to_dict" data-kind="function" class="blk"><title>to_dict — function, 8 lines</title></rect>
|
||||
<rect x="720.0" y="2338.5" width="74.0" height="7.5" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Edge" data-kind="class" class="blk"><title>Edge — class, 15 lines</title></rect>
|
||||
<rect x="727.0" y="2342.5" width="60.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Edge.to_dict" data-kind="function" class="blk"><title>to_dict — function, 7 lines</title></rect>
|
||||
<rect x="720.0" y="2347.5" width="74.0" height="24.0" rx="2" fill="#1d4ed8" stroke="none" opacity="0.95" data-id="docgen.ir.model.Graph" data-kind="class" class="blk"><title>Graph — class, 48 lines</title></rect>
|
||||
<rect x="727.0" y="2352.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.node" data-kind="function" class="blk"><title>node — function, 4 lines</title></rect>
|
||||
<rect x="727.0" y="2354.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.edge" data-kind="function" class="blk"><title>edge — function, 4 lines</title></rect>
|
||||
<rect x="727.0" y="2357.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.has" data-kind="function" class="blk"><title>has — function, 2 lines</title></rect>
|
||||
<rect x="727.0" y="2359.5" width="60.0" height="8.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.to_dict" data-kind="function" class="blk"><title>to_dict — function, 16 lines</title></rect>
|
||||
<rect x="727.0" y="2368.5" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ir.model.Graph.from_dict" data-kind="function" class="blk"><title>from_dict — function, 6 lines</title></rect>
|
||||
<rect x="28.0" y="2468.5" width="74.0" height="242.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops.filter — 485 lines</title></rect>
|
||||
<rect x="28.0" y="2486.5" width="74.0" height="28.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter._rebuild" data-kind="function" class="blk"><title>_rebuild — function, 57 lines</title></rect>
|
||||
<rect x="35.0" y="2491.0" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.surviving_parent" data-kind="function" class="blk"><title>surviving_parent — function, 5 lines</title></rect>
|
||||
<rect x="35.0" y="2498.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter._rebuild.lift" data-kind="function" class="blk"><title>lift — function, 6 lines</title></rect>
|
||||
<rect x="28.0" y="2516.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_kinds" data-kind="function" class="blk"><title>drop_kinds — function, 14 lines</title></rect>
|
||||
<rect x="28.0" y="2524.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.only_kinds" data-kind="function" class="blk"><title>only_kinds — function, 14 lines</title></rect>
|
||||
<rect x="28.0" y="2532.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_stdlib" data-kind="function" class="blk"><title>drop_stdlib — function, 13 lines</title></rect>
|
||||
<rect x="28.0" y="2539.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_external" data-kind="function" class="blk"><title>drop_external — function, 3 lines</title></rect>
|
||||
<rect x="28.0" y="2542.0" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.subtree" data-kind="function" class="blk"><title>subtree — function, 13 lines</title></rect>
|
||||
<rect x="28.0" y="2549.5" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.neighbourhood" data-kind="function" class="blk"><title>neighbourhood — function, 45 lines</title></rect>
|
||||
<rect x="28.0" y="2573.0" width="74.0" height="9.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.collapse_to_depth" data-kind="function" class="blk"><title>collapse_to_depth — function, 18 lines</title></rect>
|
||||
<rect x="35.0" y="2578.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.collapse_to_depth.level" data-kind="function" class="blk"><title>level — function, 6 lines</title></rect>
|
||||
<rect x="28.0" y="2583.0" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.drop_builtins" data-kind="function" class="blk"><title>drop_builtins — function, 14 lines</title></rect>
|
||||
<rect x="28.0" y="2591.0" width="74.0" height="17.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.overview" data-kind="function" class="blk"><title>overview — function, 35 lines</title></rect>
|
||||
<rect x="28.0" y="2609.5" width="74.0" height="36.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.shape" data-kind="function" class="blk"><title>shape — function, 72 lines</title></rect>
|
||||
<rect x="35.0" y="2633.5" width="60.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.filter.shape.rank_of" data-kind="function" class="blk"><title>rank_of — function, 9 lines</title></rect>
|
||||
<rect x="28.0" y="2646.5" width="74.0" height="12.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.split" data-kind="function" class="blk"><title>split — function, 24 lines</title></rect>
|
||||
<rect x="28.0" y="2659.5" width="74.0" height="51.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.filter.classify" data-kind="function" class="blk"><title>classify — function, 102 lines</title></rect>
|
||||
<rect x="110.0" y="2468.5" width="74.0" height="191.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.ops.pages — 383 lines</title></rect>
|
||||
<rect x="110.0" y="2499.0" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages.slugify" data-kind="function" class="blk"><title>slugify — function, 8 lines</title></rect>
|
||||
<rect x="110.0" y="2504.0" width="74.0" height="4.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages._key" data-kind="function" class="blk"><title>_key — function, 8 lines</title></rect>
|
||||
<rect x="110.0" y="2509.0" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages._under" data-kind="function" class="blk"><title>_under — function, 5 lines</title></rect>
|
||||
<rect x="110.0" y="2512.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages._representative" data-kind="function" class="blk"><title>_representative — function, 13 lines</title></rect>
|
||||
<rect x="110.0" y="2520.0" width="74.0" height="33.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages.page_ir" data-kind="function" class="blk"><title>page_ir — function, 67 lines</title></rect>
|
||||
<rect x="110.0" y="2554.5" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages.layout" data-kind="function" class="blk"><title>layout — function, 20 lines</title></rect>
|
||||
<rect x="110.0" y="2565.5" width="74.0" height="30.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages.paginate" data-kind="function" class="blk"><title>paginate — function, 61 lines</title></rect>
|
||||
<rect x="117.0" y="2574.0" width="60.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.pages.paginate.unique" data-kind="function" class="blk"><title>unique — function, 6 lines</title></rect>
|
||||
<rect x="117.0" y="2577.5" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.pages.paginate.opens" data-kind="function" class="blk"><title>opens — function, 3 lines</title></rect>
|
||||
<rect x="117.0" y="2579.5" width="60.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.ops.pages.paginate.visit" data-kind="function" class="blk"><title>visit — function, 30 lines</title></rect>
|
||||
<rect x="110.0" y="2597.0" width="74.0" height="38.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages.paginate_tables" data-kind="function" class="blk"><title>paginate_tables — function, 76 lines</title></rect>
|
||||
<rect x="110.0" y="2636.0" width="74.0" height="14.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages.coverage" data-kind="function" class="blk"><title>coverage — function, 28 lines</title></rect>
|
||||
<rect x="110.0" y="2651.0" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.ops.pages.for_view" data-kind="function" class="blk"><title>for_view — function, 17 lines</title></rect>
|
||||
<rect x="210.0" y="2468.5" width="74.0" height="111.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.extract — 222 lines</title></rect>
|
||||
<rect x="210.0" y="2499.0" width="74.0" height="8.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._values_from" data-kind="function" class="blk"><title>_values_from — function, 17 lines</title></rect>
|
||||
<rect x="210.0" y="2508.5" width="74.0" height="22.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._normalise" data-kind="function" class="blk"><title>_normalise — function, 45 lines</title></rect>
|
||||
<rect x="210.0" y="2532.0" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract._svg_files" data-kind="function" class="blk"><title>_svg_files — function, 25 lines</title></rect>
|
||||
<rect x="210.0" y="2545.5" width="74.0" height="21.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.harvest" data-kind="function" class="blk"><title>harvest — function, 43 lines</title></rect>
|
||||
<rect x="210.0" y="2568.0" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
|
||||
<rect x="210.0" y="2572.5" width="74.0" height="6.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.extract.summarise" data-kind="function" class="blk"><title>summarise — function, 13 lines</title></rect>
|
||||
<rect x="292.0" y="2468.5" width="74.0" height="105.5" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.style.tokens — 211 lines</title></rect>
|
||||
<rect x="292.0" y="2496.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._top" data-kind="function" class="blk"><title>_top — function, 2 lines</title></rect>
|
||||
<rect x="292.0" y="2498.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._mode" data-kind="function" class="blk"><title>_mode — function, 3 lines</title></rect>
|
||||
<rect x="292.0" y="2501.0" width="74.0" height="3.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens._luminance" data-kind="function" class="blk"><title>_luminance — function, 6 lines</title></rect>
|
||||
<rect x="292.0" y="2505.0" width="74.0" height="50.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.derive" data-kind="function" class="blk"><title>derive — function, 101 lines</title></rect>
|
||||
<rect x="299.0" y="2527.0" width="60.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.style.tokens.derive.accent" data-kind="function" class="blk"><title>accent — function, 2 lines</title></rect>
|
||||
<rect x="292.0" y="2556.5" width="74.0" height="7.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.geometry" data-kind="function" class="blk"><title>geometry — function, 14 lines</title></rect>
|
||||
<rect x="292.0" y="2564.5" width="74.0" height="3.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.write" data-kind="function" class="blk"><title>write — function, 7 lines</title></rect>
|
||||
<rect x="292.0" y="2569.0" width="74.0" height="4.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.style.tokens.from_folder" data-kind="function" class="blk"><title>from_folder — function, 9 lines</title></rect>
|
||||
<rect x="392.0" y="2468.5" width="74.0" height="14.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen — 2 lines</title></rect>
|
||||
<rect x="492.0" y="2468.5" width="74.0" height="75.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.lab.pg_probe — 150 lines</title></rect>
|
||||
<rect x="492.0" y="2504.0" width="74.0" height="15.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.probe" data-kind="function" class="blk"><title>probe — function, 30 lines</title></rect>
|
||||
<rect x="492.0" y="2526.0" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe._simplify" data-kind="function" class="blk"><title>_simplify — function, 3 lines</title></rect>
|
||||
<rect x="492.0" y="2528.5" width="74.0" height="12.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.lab.pg_probe.main" data-kind="function" class="blk"><title>main — function, 25 lines</title></rect>
|
||||
<rect x="592.0" y="2468.5" width="74.0" height="132.0" rx="2" fill="#141414" stroke="#333333" stroke-width="1"><title>docgen.notebook.spec — 264 lines</title></rect>
|
||||
<rect x="592.0" y="2496.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec._step" data-kind="function" class="blk"><title>_step — function, 4 lines</title></rect>
|
||||
<rect x="592.0" y="2499.5" width="74.0" height="54.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.from_ir" data-kind="function" class="blk"><title>from_ir — function, 108 lines</title></rect>
|
||||
<rect x="599.0" y="2506.5" width="60.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.8" data-id="docgen.notebook.spec.from_ir._order" data-kind="function" class="blk"><title>_order — function, 5 lines</title></rect>
|
||||
<rect x="592.0" y="2554.5" width="74.0" height="10.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.scaffold" data-kind="function" class="blk"><title>scaffold — function, 20 lines</title></rect>
|
||||
<rect x="592.0" y="2565.5" width="74.0" height="29.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.merge" data-kind="function" class="blk"><title>merge — function, 58 lines</title></rect>
|
||||
<rect x="592.0" y="2595.5" width="74.0" height="2.0" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.load" data-kind="function" class="blk"><title>load — function, 2 lines</title></rect>
|
||||
<rect x="592.0" y="2597.5" width="74.0" height="2.5" rx="2" fill="#15803d" stroke="none" opacity="0.95" data-id="docgen.notebook.spec.dump" data-kind="function" class="blk"><title>dump — function, 5 lines</title></rect>
|
||||
<text x="28" y="40" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen</text>
|
||||
<text x="28" y="1242" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen</text>
|
||||
<text x="210" y="1242" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.cli</text>
|
||||
<text x="28" y="1406" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.emitters</text>
|
||||
<text x="702" y="1406" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors</text>
|
||||
<text x="28" y="1692" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors</text>
|
||||
<text x="292" y="1692" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.book</text>
|
||||
<text x="638" y="1692" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors.python</text>
|
||||
<text x="28" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ir</text>
|
||||
<text x="210" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.style</text>
|
||||
<text x="392" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">(root)</text>
|
||||
<text x="492" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.lab</text>
|
||||
<text x="592" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.notebook</text>
|
||||
<text x="692" y="1920" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ops</text>
|
||||
<text x="28" y="1219" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">selftest</text>
|
||||
<text x="28" y="1466" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen</text>
|
||||
<text x="210" y="1466" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.emitters</text>
|
||||
<text x="28" y="1773" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.emitters</text>
|
||||
<text x="292" y="1773" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.cli</text>
|
||||
<text x="28" y="1938" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.cli</text>
|
||||
<text x="292" y="1938" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.book</text>
|
||||
<text x="28" y="2284" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors</text>
|
||||
<text x="456" y="2284" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.extractors.python</text>
|
||||
<text x="638" y="2284" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ir</text>
|
||||
<text x="28" y="2462" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.ops</text>
|
||||
<text x="210" y="2462" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.style</text>
|
||||
<text x="392" y="2462" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">(root)</text>
|
||||
<text x="492" y="2462" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.lab</text>
|
||||
<text x="592" y="2462" font-family="Helvetica,sans-Serif" font-size="10" font-weight="bold" fill="#a3a3a3">docgen.notebook</text>
|
||||
<text x="28" y="1442" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">selftest</text>
|
||||
<text x="110" y="213" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">book</text>
|
||||
<text x="192" y="152" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">style</text>
|
||||
<text x="274" y="109" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">reference</text>
|
||||
<text x="356" y="93" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli</text>
|
||||
<text x="356" y="95" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">cli</text>
|
||||
<text x="438" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">ops</text>
|
||||
<text x="520" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notebook</text>
|
||||
<text x="602" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">emitters</text>
|
||||
<text x="684" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">lab</text>
|
||||
<text x="766" y="69" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">__main__</text>
|
||||
<text x="28" y="1271" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">ir</text>
|
||||
<text x="110" y="1271" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extractors</text>
|
||||
<text x="210" y="1384" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">emit</text>
|
||||
<text x="292" y="1316" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extract</text>
|
||||
<text x="374" y="1302" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">view</text>
|
||||
<text x="456" y="1297" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">common</text>
|
||||
<text x="538" y="1297" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">run</text>
|
||||
<text x="620" y="1294" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">book</text>
|
||||
<text x="702" y="1282" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">check</text>
|
||||
<text x="784" y="1274" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">validate</text>
|
||||
<text x="28" y="1668" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">site</text>
|
||||
<text x="110" y="1580" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">explore</text>
|
||||
<text x="192" y="1568" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">dot</text>
|
||||
<text x="274" y="1561" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notebook</text>
|
||||
<text x="356" y="1560" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">minimap</text>
|
||||
<text x="438" y="1552" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">erd</text>
|
||||
<text x="520" y="1504" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">index</text>
|
||||
<text x="602" y="1454" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">auto</text>
|
||||
<text x="702" y="1562" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">code</text>
|
||||
<text x="784" y="1556" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">usage</text>
|
||||
<text x="28" y="1802" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">openapi</text>
|
||||
<text x="110" y="1787" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">db</text>
|
||||
<text x="192" y="1725" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">python</text>
|
||||
<text x="292" y="1897" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">build</text>
|
||||
<text x="374" y="1870" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">config</text>
|
||||
<text x="456" y="1844" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">checks</text>
|
||||
<text x="538" y="1804" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">larder</text>
|
||||
<text x="638" y="1824" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">collect</text>
|
||||
<text x="720" y="1798" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">resolve</text>
|
||||
<text x="28" y="2074" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">validate</text>
|
||||
<text x="110" y="2018" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">model</text>
|
||||
<text x="210" y="2046" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extract</text>
|
||||
<text x="292" y="2040" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">tokens</text>
|
||||
<text x="392" y="1949" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">docgen</text>
|
||||
<text x="492" y="2010" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">pg_probe</text>
|
||||
<text x="592" y="2067" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">spec</text>
|
||||
<text x="692" y="2178" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">filter</text>
|
||||
<rect x="28" y="2217.5" width="9" height="9" rx="2" fill="#1a1a1a"/>
|
||||
<text x="41" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">module</text>
|
||||
<rect x="86" y="2217.5" width="9" height="9" rx="2" fill="#1d4ed8"/>
|
||||
<text x="99" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">class</text>
|
||||
<rect x="138" y="2217.5" width="9" height="9" rx="2" fill="#d4a574"/>
|
||||
<text x="151" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">interface</text>
|
||||
<rect x="214" y="2217.5" width="9" height="9" rx="2" fill="#15803d"/>
|
||||
<text x="227" y="2225.5" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">function</text>
|
||||
<text x="858" y="2225.5" text-anchor="end" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">47 files · 10,350 lines · 1px ≈ 2.0 lines</text>
|
||||
<text x="28" y="1494" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">ir</text>
|
||||
<text x="110" y="1494" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extractors</text>
|
||||
<text x="210" y="1750" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">site</text>
|
||||
<text x="292" y="1640" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notebook</text>
|
||||
<text x="374" y="1638" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">explore</text>
|
||||
<text x="456" y="1635" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">dot</text>
|
||||
<text x="538" y="1620" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">minimap</text>
|
||||
<text x="620" y="1618" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">erd</text>
|
||||
<text x="702" y="1616" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">docsite</text>
|
||||
<text x="784" y="1562" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">index</text>
|
||||
<text x="28" y="1861" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">markdown</text>
|
||||
<text x="110" y="1848" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">paged</text>
|
||||
<text x="192" y="1820" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">auto</text>
|
||||
<text x="292" y="1914" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">emit</text>
|
||||
<text x="374" y="1848" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extract</text>
|
||||
<text x="456" y="1838" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">run</text>
|
||||
<text x="538" y="1833" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">view</text>
|
||||
<text x="620" y="1832" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">scaffold</text>
|
||||
<text x="702" y="1828" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">common</text>
|
||||
<text x="784" y="1826" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">book</text>
|
||||
<text x="28" y="1982" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">check</text>
|
||||
<text x="110" y="1970" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">contract</text>
|
||||
<text x="192" y="1969" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">validate</text>
|
||||
<text x="292" y="2260" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">config</text>
|
||||
<text x="374" y="2177" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">document</text>
|
||||
<text x="456" y="2162" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">build</text>
|
||||
<text x="538" y="2142" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">checks</text>
|
||||
<text x="620" y="2050" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">larder</text>
|
||||
<text x="702" y="2034" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">notes</text>
|
||||
<text x="28" y="2440" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">code</text>
|
||||
<text x="110" y="2433" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">usage</text>
|
||||
<text x="192" y="2394" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">openapi</text>
|
||||
<text x="274" y="2379" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">db</text>
|
||||
<text x="356" y="2317" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">python</text>
|
||||
<text x="456" y="2416" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">collect</text>
|
||||
<text x="538" y="2390" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">resolve</text>
|
||||
<text x="638" y="2438" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">validate</text>
|
||||
<text x="720" y="2381" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">model</text>
|
||||
<text x="28" y="2720" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">filter</text>
|
||||
<text x="110" y="2669" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">pages</text>
|
||||
<text x="210" y="2588" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">extract</text>
|
||||
<text x="292" y="2583" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">tokens</text>
|
||||
<text x="392" y="2492" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">docgen</text>
|
||||
<text x="492" y="2552" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">pg_probe</text>
|
||||
<text x="592" y="2610" font-family="Helvetica,sans-Serif" font-size="7" fill="#666666">spec</text>
|
||||
<rect x="28" y="2760.0" width="9" height="9" rx="2" fill="#1a1a1a"/>
|
||||
<text x="41" y="2768.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">module</text>
|
||||
<rect x="86" y="2760.0" width="9" height="9" rx="2" fill="#1d4ed8"/>
|
||||
<text x="99" y="2768.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">class</text>
|
||||
<rect x="138" y="2760.0" width="9" height="9" rx="2" fill="#d4a574"/>
|
||||
<text x="151" y="2768.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">interface</text>
|
||||
<rect x="214" y="2760.0" width="9" height="9" rx="2" fill="#15803d"/>
|
||||
<text x="227" y="2768.0" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">function</text>
|
||||
<text x="858" y="2768.0" text-anchor="end" font-family="Helvetica,sans-Serif" font-size="9" fill="#666666">55 files · 13,028 lines · 1px ≈ 2.0 lines</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 100 KiB |
@@ -19,12 +19,15 @@
|
||||
<a href="#what">What it is</a>
|
||||
<a href="#idea">The idea</a>
|
||||
<a href="#quick">Five minutes</a>
|
||||
<a href="#first">Your first project</a>
|
||||
<a href="#models">Working with a model</a>
|
||||
|
||||
<div class="group">Architecture</div>
|
||||
<a href="#layers">The three concerns</a>
|
||||
<a href="#book">The book</a>
|
||||
<a href="#ir">The IR</a>
|
||||
<a href="#shape">Shape decides the drawing</a>
|
||||
<a href="#pages">Pages</a>
|
||||
|
||||
<div class="group">Reading</div>
|
||||
<a href="#extractors">Extractors</a>
|
||||
@@ -37,11 +40,13 @@
|
||||
<a href="#emitters">Emitters</a>
|
||||
<a href="#explore">Explore</a>
|
||||
<a href="#notebooks">Notebooks</a>
|
||||
<a href="#notes">Notes</a>
|
||||
<a href="#style">Style & colour</a>
|
||||
|
||||
<div class="group">Reference</div>
|
||||
<a href="#standalone">Standalone</a>
|
||||
<a href="#runs">Run files</a>
|
||||
<a href="#projects">Project files</a>
|
||||
<a href="#commands">Commands</a>
|
||||
<a href="#deps">Dependencies</a>
|
||||
<a href="#testing">Testing</a>
|
||||
@@ -154,6 +159,145 @@ make self <span class="c"># docgen's book of soleprint
|
||||
the outputs open over <code>file://</code>.
|
||||
</p>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="first">Your first project</h2>
|
||||
|
||||
<p>
|
||||
From nothing to a notebook and a site for a project of yours, with your own text
|
||||
in the right places. docgen lives in its own folder, and the project only gets one
|
||||
config file and, later, a folder of notes.
|
||||
</p>
|
||||
|
||||
<h3>1. Put docgen somewhere and check the machine</h3>
|
||||
|
||||
<pre><code>cp -r docgen ~/tools/docgen
|
||||
cd ~/tools/docgen
|
||||
make doctor <span class="c"># what is installed; never fails</span>
|
||||
make sync <span class="c"># optional: C#/TypeScript, OpenAPI, all from uv.lock</span></code></pre>
|
||||
|
||||
<p>
|
||||
From anywhere else, docgen runs as <code>python3 -m docgen</code> with its parent
|
||||
folder on the path:
|
||||
</p>
|
||||
|
||||
<pre><code>export PYTHONPATH=~/tools <span class="c"># the folder that holds docgen/</span>
|
||||
python3 -m docgen --help</code></pre>
|
||||
|
||||
<h3>2. Write <code>docgen.toml</code> at the project's root</h3>
|
||||
|
||||
<pre><code>[project]
|
||||
name = "orders-service"
|
||||
|
||||
[extract.code]
|
||||
root = "src"
|
||||
ext = [".cs"]
|
||||
|
||||
[emit.notebook]
|
||||
output = "../docs_out/orders_service_notebook"
|
||||
|
||||
[emit.site]
|
||||
output = "../docs_out/orders_service_site"</code></pre>
|
||||
|
||||
<p>
|
||||
Paths are relative to this file. Keep the outputs outside the source tree. See
|
||||
<a href="#projects">project files</a> for every key, and add
|
||||
<code>.docgen/</code> to the project's <code>.gitignore</code>.
|
||||
</p>
|
||||
|
||||
<h3>3. Check what it will do, then build</h3>
|
||||
|
||||
<pre><code>cd ~/work/orders-service
|
||||
python3 -m docgen run docgen.toml --list <span class="c"># resolved paths; builds nothing</span>
|
||||
python3 -m docgen run docgen.toml --check <span class="c"># build, then check every book</span></code></pre>
|
||||
|
||||
<pre><code>docgen.toml — 1 book(s)
|
||||
ok code …/.docgen/orders-service/books/code
|
||||
ok orders-service (document) …/.docgen/orders-service
|
||||
|
||||
1 book(s) in 1 document built and checked</code></pre>
|
||||
|
||||
<p>
|
||||
A file docgen can't use is refused before anything is built, with every problem
|
||||
listed at once and what to do about each. Open
|
||||
<code>../docs_out/orders_service_site/index.html</code>: each book shows what
|
||||
came in and what came out, and its pages click through from the overview.
|
||||
</p>
|
||||
|
||||
<h3>4. Add your own text</h3>
|
||||
|
||||
<pre><code>python3 -m docgen scaffold docgen.toml --list <span class="c"># every spot a note can go</span>
|
||||
python3 -m docgen scaffold docgen.toml <span class="c"># or: write an empty stub for each</span></code></pre>
|
||||
|
||||
<p>
|
||||
Pick a spot and write markdown into <code>notes/<book>/<spot>.md</code>,
|
||||
for example <code>notes/code/overview.md</code> above the overview page. Rebuild
|
||||
and it's there, in the notebook and on the site. Stubs you leave empty add
|
||||
nothing.
|
||||
</p>
|
||||
|
||||
<div class="note">
|
||||
<b>If a note stops appearing</b>, the page it was filed under was probably
|
||||
renamed, for instance a package that moved. The rebuild says so and
|
||||
<code>--check</code> fails with the note's name. Rename the file to the new spot
|
||||
(<code>scaffold --list</code> shows them) or delete it.
|
||||
</div>
|
||||
|
||||
<h3>5. Rebuild whenever the code moves</h3>
|
||||
|
||||
<p>
|
||||
<code>python3 -m docgen run docgen.toml --check</code> again. A rebuild replaces
|
||||
exactly what the last one wrote and never touches your notes or anything you
|
||||
added to the output folders. <code>--only code</code> rebuilds one book while
|
||||
you're writing its notes.
|
||||
</p>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="models">Working with a model</h2>
|
||||
|
||||
<p>
|
||||
A model can write the config and the notes. It shouldn't write anything else,
|
||||
since every other file is generated. docgen ships the instructions to give it:
|
||||
</p>
|
||||
|
||||
<pre><code>python3 -m docgen contract > docgen-contract.md</code></pre>
|
||||
|
||||
<p>
|
||||
That file is <code>docs/models.md</code>, printed verbatim. It lists every key
|
||||
docgen accepts and the extensions it reads, has rules like "never a credential"
|
||||
and "don't guess page ids", and ends with a table that maps each error docgen
|
||||
can produce to the fix. docgen's own suite checks it against the code, so it
|
||||
can't quietly go out of date: a key the loader accepts but the doc doesn't
|
||||
name, or an example in it that doesn't load, fails <code>make check</code>.
|
||||
</p>
|
||||
|
||||
<h3>The loop</h3>
|
||||
|
||||
<ol>
|
||||
<li><b>Give it the project and the contract.</b> A digest of the project
|
||||
(<code>distill</code>), plus <code>docgen-contract.md</code> as an
|
||||
attachment rather than pasted. If replies go through <code>explode</code>,
|
||||
attach its contract too: explode's says how to format the reply, docgen's
|
||||
says what goes in the files.</li>
|
||||
<li><b>Ask for <code>docgen.toml</code> first.</b> Unpack it, then run
|
||||
<code>run docgen.toml --list</code>. If it's refused, paste the errors back
|
||||
verbatim. Each one is in the contract's repair table.</li>
|
||||
<li><b>Build once, then list the spots.</b> Run
|
||||
<code>scaffold docgen.toml --list</code> and paste the output back. Page
|
||||
names depend on the real directories, and a model that guesses them writes
|
||||
notes that land nowhere.</li>
|
||||
<li><b>Ask for notes for those spots.</b> Unpack, rebuild with
|
||||
<code>--check</code>, and paste back any note reported as filed under a page
|
||||
that doesn't exist.</li>
|
||||
</ol>
|
||||
|
||||
<div class="note warn">
|
||||
<b>What a model tends to write that docgen refuses.</b> In a dry run, a model
|
||||
asked for a live database URL (with its password), a <code>seed</code> reader,
|
||||
and shell, YAML and SQL files as code. All three are refused with a reason, and
|
||||
the URL is never echoed in the error. The contract tells the model up front, but
|
||||
the refusal is what makes it safe.
|
||||
</div>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="layers">The three concerns</h2>
|
||||
|
||||
@@ -641,6 +785,69 @@ python3 -m docgen view ir.json --shape <span class="c"># what will this
|
||||
dark for everything that is not a declaration — imports, constants, prose.</figcaption>
|
||||
</figure>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="pages">Pages</h2>
|
||||
|
||||
<p>
|
||||
A notebook doesn't scroll a wide image sideways: it scales it down to the cell
|
||||
width until the labels can't be read. Zooming and panning is a workaround for a
|
||||
picture that holds too much. So a book's diagram is split into <b>pages</b>,
|
||||
each drawn small enough to fit the column it's going into.
|
||||
</p>
|
||||
|
||||
<pre><code>overview one box per top-level group, relationships lifted
|
||||
└── station one box per child of station
|
||||
└── station.tools one box per child of station.tools, and so on</code></pre>
|
||||
|
||||
<p>
|
||||
Pages follow structure, never scissors. Slicing one large image cuts boxes and
|
||||
edges at the seams, and a slice means nothing on its own. A page is a subsystem:
|
||||
its boxes are the scope's children, and anything outside the scope that they
|
||||
touch appears as a <em>context</em> box, collapsed to the branch it sits on. A
|
||||
page still says what its subsystem reaches without drawing the rest of the tree.
|
||||
</p>
|
||||
|
||||
<h3>Fitted by measuring, not estimating</h3>
|
||||
|
||||
<p>
|
||||
Whether a page fits depends on its labels, fonts and spacing, so an estimate from
|
||||
node counts is wrong the moment the names are long. Each page is rendered and its
|
||||
width read back. If a page is too wide top-to-bottom, it's drawn left-to-right,
|
||||
since height is free in a notebook and width isn't. If it still doesn't fit
|
||||
(a level of many peers with no smaller structure to split into), the page is
|
||||
kept and flagged <em>too wide</em> in its caption and in the index. Never cut,
|
||||
never dropped.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Measured on soleprint, whose module graph rendered at 14:1 as one picture: 69
|
||||
pages, and at the site's 800px column the widest flagged page is 988px.
|
||||
Schemas page differently: cards in columns capped to the width, grouped by
|
||||
tables joined by foreign keys, so a schema grows downward instead of sideways.
|
||||
</p>
|
||||
|
||||
<div class="note">
|
||||
<b>Which pages exist doesn't depend on width</b>, only how each is laid out
|
||||
does. A notebook column and a site column get the same pages, which is what lets
|
||||
a <a href="#notes">note</a> filed under a page land in the same place in both.
|
||||
</div>
|
||||
|
||||
<h3>Clicking through, on the site</h3>
|
||||
|
||||
<p>
|
||||
Every page is its own HTML file with the diagram <b>inline</b>, not in an
|
||||
<code><img></code>, which swallows clicks. A box standing for a subsystem
|
||||
opens that subsystem's page. A module box opens its section of the book's
|
||||
reference page. A context box opens its own branch. Breadcrumbs lead back up.
|
||||
The size is adjustable: <b>fit</b> scales to the column, <b>1:1</b> shows the
|
||||
drawing at natural size with scrollbars, the choice is remembered, and
|
||||
<b>full screen</b> opens the pan-and-zoom viewer.
|
||||
</p>
|
||||
<p>
|
||||
A notebook can't follow a link inside an image, so it gets a <b>page index</b>
|
||||
instead: every page, indented by depth, each a link to its figure.
|
||||
</p>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="explore">Explore</h2>
|
||||
|
||||
@@ -747,6 +954,56 @@ python3 -m docgen view ir.json --shape <span class="c"># what will this
|
||||
<pre><code>python3 -m docgen emit notebook ir.json --scaffold overlay.json
|
||||
python3 -m docgen emit notebook ir.json --overlay overlay.json -o walkthrough.ipynb</code></pre>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="notes">Notes</h2>
|
||||
|
||||
<p>
|
||||
Generation alone gives a document that's never stale and never says anything a
|
||||
parser couldn't work out. Prose is the part only a person adds, and it has to
|
||||
survive every rebuild, so it can't live in the output. It lives in a
|
||||
<b>notes folder</b>, one markdown file per spot, named after the page or step it
|
||||
belongs above:
|
||||
</p>
|
||||
|
||||
<pre><code>notes/
|
||||
├── _intro.md top of the document
|
||||
├── _end.md bottom of the document
|
||||
└── <book>/
|
||||
├── _intro.md top of that book's part
|
||||
├── larder.md above "what came in"
|
||||
├── overview.md above the overview page
|
||||
└── station.tools.md above the page for station.tools</code></pre>
|
||||
|
||||
<p>
|
||||
To put text above a page, create the file named after it. No schema, no JSON,
|
||||
no ids to look up. That's something a person or a model can do without reading
|
||||
anything first. Local images in a note are embedded: as a cell attachment in the
|
||||
notebook, as a data URI on the site.
|
||||
</p>
|
||||
|
||||
<pre><code>make scaffold CONFIG=docgen.toml <span class="c"># an empty stub for every spot, never overwriting</span></code></pre>
|
||||
|
||||
<p>
|
||||
<code>scaffold</code> lists the spots rather than making you guess them. A stub
|
||||
is a single HTML comment and renders nothing, so scaffolding a hundred spots
|
||||
changes the output not at all until one is filled in. Run it again after the
|
||||
sources change and it only adds the new spots.
|
||||
</p>
|
||||
|
||||
<div class="note warn">
|
||||
<b>A note whose page is gone is reported, not dropped.</b> Renaming a package
|
||||
renames its page, and the note filed under the old name has nowhere to land.
|
||||
Discarding it quietly is how hand-written text goes stale without anyone
|
||||
noticing, so it appears in the document ledger and fails the check level. It's
|
||||
the same rule the notebook overlay follows.
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The <a href="#notebooks">overlay</a> stays for what notes can't do: drop a
|
||||
step, reorder, replace generated code. Notes are applied after the overlay, so a
|
||||
step that moves takes its note with it.
|
||||
</p>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="style">Style & colour</h2>
|
||||
|
||||
@@ -872,10 +1129,10 @@ make doctor
|
||||
|
||||
<table>
|
||||
<tr><th>context</th><th>checks</th><th>skipped</th></tr>
|
||||
<tr><td>in the repo, synced</td><td>287</td><td>—</td></tr>
|
||||
<tr><td>copied out, bare <code>python3</code></td><td>270</td><td>tree-sitter, OpenAPI, the in-place case</td></tr>
|
||||
<tr><td>copied out, synced</td><td>279</td><td>OpenAPI, the in-place case</td></tr>
|
||||
<tr><td>copied out, synced, <code>DOCGEN_REFERENCE</code> set</td><td>286</td>
|
||||
<tr><td>in the repo, synced</td><td>346</td><td>—</td></tr>
|
||||
<tr><td>copied out, bare <code>python3</code></td><td>329</td><td>tree-sitter, OpenAPI, the in-place case</td></tr>
|
||||
<tr><td>copied out, synced</td><td>338</td><td>OpenAPI, the in-place case</td></tr>
|
||||
<tr><td>copied out, synced, <code>DOCGEN_REFERENCE</code> set</td><td>345</td>
|
||||
<td>the in-place case — correctly, there is no repo above</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -973,6 +1230,80 @@ python3 -m docgen run docgen.toml --list <span class="c"># resolved paths
|
||||
because those are data that tools write.
|
||||
</p>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="projects">Project files</h2>
|
||||
|
||||
<p>
|
||||
A run file lists independent books. A <b>project file</b> describes one
|
||||
<em>document</em> built from several sources: an API contract and the code
|
||||
behind it, a schema and the service that uses it. It's the shape a model wrote
|
||||
unprompted in a dry run, so docgen reads it as written:
|
||||
</p>
|
||||
|
||||
<pre><code><span class="c"># <project>/docgen.toml</span>
|
||||
[project]
|
||||
name = "orders-api"
|
||||
title = "Orders API"
|
||||
notes = "notes" <span class="c"># default</span>
|
||||
build = ".docgen" <span class="c"># book artifacts, default; gitignore it</span>
|
||||
|
||||
[extract.openapi]
|
||||
spec = "doc/contracts/openapi.yaml"
|
||||
|
||||
[extract.code]
|
||||
root = "src"
|
||||
ext = [".cs"]
|
||||
|
||||
[ops]
|
||||
drop_stdlib = true
|
||||
drop_builtins = true
|
||||
|
||||
[emit.notebook]
|
||||
output = "../docs_out/orders_api_notebook"
|
||||
theme = "lucid"
|
||||
|
||||
[emit.site]
|
||||
output = "../docs_out/orders_api_site"
|
||||
width = 800</code></pre>
|
||||
|
||||
<p>
|
||||
Each <code>[extract.*]</code> table is a <b>book</b>, and a book is a part of
|
||||
every output rather than an output of its own. Each <code>[emit.*]</code> table
|
||||
is an output made of all of them: one notebook and one site, each with a part
|
||||
per book. Several sources of one kind use <code>[[extract.code]]</code> with a
|
||||
<code>name</code> each. <code>[ops]</code> applies to every book's view before
|
||||
paging.
|
||||
</p>
|
||||
<p>
|
||||
Every part still opens with what came in and closes with what came out, and the
|
||||
document opens with every larder and closes with every reconciliation. A book
|
||||
that can't be built doesn't cost the others: the outputs are assembled from
|
||||
what was built, and both say <em>this document is incomplete</em> and name what's
|
||||
missing.
|
||||
</p>
|
||||
|
||||
<h3>Refused, with what to do instead</h3>
|
||||
|
||||
<table>
|
||||
<tr><th>written</th><th>why not</th></tr>
|
||||
<tr><td><code>[extract.db] url = …</code></td>
|
||||
<td>docgen reads a schema dump and never connects, and a URL in a committed
|
||||
file is a credential in a committed file. Write the schema with
|
||||
<code>modelgen from-db</code>, then <code>schema = …</code>. The URL is never
|
||||
repeated in the error.</td></tr>
|
||||
<tr><td><code>ext = [".sh", ".yaml", ".sql"]</code></td>
|
||||
<td>No grammar for those yet. The error names the extensions that are read.</td></tr>
|
||||
<tr><td><code>[extract.seed]</code></td>
|
||||
<td>No such reader. The error names the ones that exist.</td></tr>
|
||||
<tr><td><code>[project]</code> and <code>[[book]]</code> in one file</td>
|
||||
<td>Which one it meant is exactly what nobody should have to guess.</td></tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<code>docgen.project.example.toml</code> ships beside the run file example and
|
||||
builds docgen's own fixtures into one document, with a sample note.
|
||||
</p>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────── -->
|
||||
<h2 id="commands">Commands</h2>
|
||||
|
||||
@@ -981,7 +1312,9 @@ python3 -m docgen run docgen.toml --list <span class="c"># resolved paths
|
||||
<tr><th>target</th><th>does</th></tr>
|
||||
<tr><td><code>make sync</code></td><td><code>.venv</code> with every optional group, from <code>uv.lock</code></td></tr>
|
||||
<tr><td><code>make book SRC=…</code></td><td><b>one whole operation</b>, measured at both ends</td></tr>
|
||||
<tr><td><code>make run CONFIG=…</code></td><td>every book a <a href="#runs">run file</a> lists</td></tr>
|
||||
<tr><td><code>make run CONFIG=…</code></td><td>every book a <a href="#runs">run file</a> lists, or a <a href="#projects">project</a>'s document</td></tr>
|
||||
<tr><td><code>make scaffold CONFIG=…</code></td><td>an empty <a href="#notes">note</a> for every page and step</td></tr>
|
||||
<tr><td><code>python3 -m docgen contract</code></td><td>the <a href="#models">instructions for a model</a> that writes the config and notes</td></tr>
|
||||
<tr><td><code>make check</code></td><td>docgen's own suite, offline, nothing installed</td></tr>
|
||||
<tr><td><code>make check BOOK=…</code></td><td>one book's own level — generated and custom</td></tr>
|
||||
<tr><td><code>make doctor</code></td><td>what this machine has and what it is missing</td></tr>
|
||||
@@ -1028,6 +1361,10 @@ python3 -m docgen run docgen.toml --list <span class="c"># resolved paths
|
||||
python3 -m docgen run docgen.toml <span class="c"># every book a run file lists</span>
|
||||
python3 -m docgen check <span class="c"># docgen's own suite</span>
|
||||
python3 -m docgen check out/book/slug <span class="c"># that book's level</span>
|
||||
python3 -m docgen check .docgen/<project> <span class="c"># a project document's level</span>
|
||||
python3 -m docgen scaffold docgen.toml <span class="c"># stub notes for every spot</span>
|
||||
python3 -m docgen scaffold docgen.toml --list <span class="c"># the spots, written nowhere</span>
|
||||
python3 -m docgen contract <span class="c"># the instructions for a model</span>
|
||||
|
||||
python3 -m docgen extract python --root SRC -o ir.json
|
||||
python3 -m docgen extract code --root SRC -o ir.json
|
||||
@@ -1136,14 +1473,14 @@ make lock <span class="c"># after editing pyproject
|
||||
<tr><th>command</th><th>asks about</th><th>fails?</th></tr>
|
||||
<tr><td><code>make doctor</code></td><td>the machine — what is installed</td>
|
||||
<td>never; it reports</td></tr>
|
||||
<tr><td><code>make check</code></td><td>docgen — 287 checks</td>
|
||||
<tr><td><code>make check</code></td><td>docgen — 346 checks</td>
|
||||
<td>exit 1</td></tr>
|
||||
<tr><td><code>make check BOOK=<dir></code></td><td>that one book</td>
|
||||
<tr><td><code>make check BOOK=<dir></code></td><td>that one book, or a project's document</td>
|
||||
<td>exit 1</td></tr>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
All 287 after <code>make sync</code>, 272 with nothing optional installed — the
|
||||
All 346 after <code>make sync</code>, 331 with nothing optional installed — the
|
||||
suite skips rather than fails when tree-sitter, lxml or the OpenAPI reader is
|
||||
absent. See <a href="#standalone">standalone</a> for the counts outside the repo.
|
||||
Among them, two that keep this page's structure true: nothing outside
|
||||
|
||||
259
soleprint/atlas2/docgen/docs/models.md
Normal file
259
soleprint/atlas2/docgen/docs/models.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# docgen — instructions for a model
|
||||
|
||||
You are helping document a software project with **docgen**. docgen reads source
|
||||
artifacts (a code tree, a database schema dump, an OpenAPI document, a HAR recording)
|
||||
and generates a notebook and a website from them. You do not generate those outputs.
|
||||
You write the two kinds of file a person would otherwise write by hand:
|
||||
|
||||
1. **`docgen.toml`**: which sources to read and which outputs to build.
|
||||
2. **Notes**: short markdown files that docgen places above a specific page or step.
|
||||
|
||||
Everything else is generated, and anything you write into a generated file is
|
||||
overwritten on the next build.
|
||||
|
||||
If your replies are unpacked by `explode`, its contract decides the reply format. This
|
||||
file decides only what goes in the files.
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
Follow all of these. A file that breaks one is refused when it is loaded, and the error
|
||||
is pasted back to you.
|
||||
|
||||
1. **Use only the keys listed below, spelled exactly as listed.** Unknown keys are
|
||||
refused, never ignored. Do not invent keys, readers or outputs.
|
||||
2. **Never put a database URL, password, token or any credential in any file.** docgen
|
||||
does not connect to databases. It reads a schema file written by another tool.
|
||||
3. **Every path is relative to the `docgen.toml` file itself.**
|
||||
4. **Outputs must be outside every source tree** (`output = "../docs_out/..."` is the
|
||||
usual choice). A build written inside a tree it reads is refused.
|
||||
5. **Only list file extensions docgen can read** (see `ext` below). If the project's
|
||||
interesting files are shell scripts, YAML or SQL, leave them out and say so in a
|
||||
note. docgen does not read them yet.
|
||||
6. **Do not guess page ids for notes.** Write a note only for a spot you were given,
|
||||
either by the output of `docgen scaffold --list` or by the table in "Notes" below. A
|
||||
note for a spot that does not exist fails the build's checks.
|
||||
7. **Notes say what the code cannot.** Why something exists, what it replaced, how it is
|
||||
meant to be used, what to watch out for. Do not restate what the diagram already shows
|
||||
(names, counts, which module imports which).
|
||||
8. If something you need to write a correct file is missing (the project's directory
|
||||
layout, where the OpenAPI file is), ask for it instead of guessing.
|
||||
|
||||
---
|
||||
|
||||
## `docgen.toml` for one project
|
||||
|
||||
One file at the project root. It describes one **document** built from several sources.
|
||||
Each `[extract.*]` table is a **book**, one part of the document. Each `[emit.*]` table
|
||||
is an output that contains every book.
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "orders-service"
|
||||
title = "Orders Service"
|
||||
|
||||
[extract.openapi]
|
||||
spec = "doc/contracts/openapi.yaml"
|
||||
|
||||
[extract.code]
|
||||
root = "src"
|
||||
ext = [".cs"]
|
||||
|
||||
[ops]
|
||||
drop_stdlib = true
|
||||
drop_builtins = true
|
||||
|
||||
[emit.notebook]
|
||||
output = "../docs_out/orders_service_notebook"
|
||||
theme = "lucid"
|
||||
|
||||
[emit.site]
|
||||
output = "../docs_out/orders_service_site"
|
||||
```
|
||||
|
||||
### `[project]`
|
||||
|
||||
| key | required | value |
|
||||
|---|---|---|
|
||||
| `name` | yes | letters, digits, `.`, `_`, `-`; starts with a letter or digit |
|
||||
| `title` | no | any text; defaults to `name` |
|
||||
| `notes` | no | folder of notes; default `"notes"` |
|
||||
| `build` | no | where intermediate files go; default `".docgen"` (should be gitignored) |
|
||||
| `reference` | no | path to the spr repository; needed only to read OpenAPI |
|
||||
|
||||
### `[extract.<reader>]`: the sources
|
||||
|
||||
| reader | required key | optional keys | reads |
|
||||
|---|---|---|---|
|
||||
| `python` | `root` (a directory) | `name`, `exclude`, `overlay` | Python, with imports and inheritance |
|
||||
| `code` | `root` (a directory) | `name`, `ext`, `exclude`, `overlay` | C#, TypeScript, Python structure (no edges) |
|
||||
| `db` | `schema` (a file) | `name`, `overlay` | a schema JSON dump, as `modelgen from-db` writes |
|
||||
| `openapi` | `spec` (a file) | `name`, `overlay` | an OpenAPI document |
|
||||
| `usage` | `har` (a file) | `name`, `overlay` | a HAR recording of real API traffic |
|
||||
|
||||
- `ext`: a list chosen from `".cs"`, `".ts"`, `".tsx"`, `".mts"`, `".py"`. Nothing else.
|
||||
- `exclude`: a list of directory names to skip, e.g. `["tests", "migrations"]`.
|
||||
- `name`: the book's name. It defaults to the reader's name. Use `[[extract.<reader>]]`
|
||||
(double brackets) with a different `name` in each table to read two trees of the same
|
||||
kind:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "storefront"
|
||||
|
||||
[[extract.code]]
|
||||
name = "backend"
|
||||
root = "src"
|
||||
ext = [".cs"]
|
||||
|
||||
[[extract.code]]
|
||||
name = "frontend"
|
||||
root = "web/src"
|
||||
ext = [".ts", ".tsx"]
|
||||
|
||||
[emit.site]
|
||||
output = "../docs_out/storefront_site"
|
||||
```
|
||||
|
||||
### `[ops]`: applied to every book before drawing
|
||||
|
||||
| key | value |
|
||||
|---|---|
|
||||
| `drop_stdlib` | `true` removes standard-library imports |
|
||||
| `drop_builtins` | `true` removes built-in names |
|
||||
| `drop_external` | `true` removes every name the source does not define |
|
||||
| `only` | a list of kinds to keep |
|
||||
| `drop` | a list of kinds to remove |
|
||||
|
||||
Kinds: `module`, `class`, `interface`, `function`, `external`, `table`, `column`,
|
||||
`endpoint`, `operation`.
|
||||
|
||||
### `[emit.<output>]`: the outputs
|
||||
|
||||
The outputs are `notebook` and `site`, nothing else.
|
||||
|
||||
| key | required | value |
|
||||
|---|---|---|
|
||||
| `output` | yes | a directory. A notebook output may also be a path ending in `.ipynb` |
|
||||
| `theme` | no | `"dark"` (default) or `"lucid"` (light, for print) |
|
||||
| `style` | no | `"lucid"` (default) |
|
||||
| `width` | no | pixels a diagram page must fit; default 960 for a notebook, 800 for a site |
|
||||
|
||||
---
|
||||
|
||||
## `docgen.toml` for many independent books
|
||||
|
||||
A different shape, for a list of unrelated books with one output folder each. Use it
|
||||
only when asked. A file may not contain both `[project]` and `[[book]]`.
|
||||
|
||||
```toml
|
||||
[defaults]
|
||||
out = "../docs_out"
|
||||
exclude = ["tests"]
|
||||
|
||||
[[book]]
|
||||
name = "billing"
|
||||
root = "src"
|
||||
|
||||
[[book]]
|
||||
name = "schema"
|
||||
schema = "schema/schema.json"
|
||||
```
|
||||
|
||||
| where | keys |
|
||||
|---|---|
|
||||
| top level | `reference`, `defaults`, `book` |
|
||||
| `[defaults]` | `out`, `style`, `theme`, `exclude`, `reader`, `notes` |
|
||||
| `[[book]]` | `name`, exactly one of `root` / `schema` / `openapi` / `har`, and optionally `reader` (`"python"` or `"code"`, only with `root`), `out`, `style`, `theme`, `exclude`, `overlay`, `notes` |
|
||||
|
||||
A book's own value replaces the default for that key. Values are never merged.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
A note is a markdown file. docgen puts its content directly above the spot it is named
|
||||
after, in both the notebook and the site. Its path is:
|
||||
|
||||
```
|
||||
<notes>/_intro.md top of the whole document
|
||||
<notes>/_end.md end of the whole document
|
||||
<notes>/<book>/_intro.md top of that book's part
|
||||
<notes>/<book>/<spot>.md above that spot in the book
|
||||
```
|
||||
|
||||
`<book>` is the book's `name` (the reader's name if none was given).
|
||||
|
||||
### Spots every book has
|
||||
|
||||
| spot | appears above |
|
||||
|---|---|
|
||||
| `larder` | "what came in": what was read |
|
||||
| `pages` | the index of diagram pages |
|
||||
| `overview` | the first diagram page |
|
||||
| `book` | "what came out": the closing summary |
|
||||
| `step-ir`, `step-view`, `step-pages`, `step-graph`, `step-index`, `step-sidebar`, `step-minimap`, `step-explore` | each intermediate step |
|
||||
|
||||
### Spots that depend on the source
|
||||
|
||||
- **Pages of a code tree**: named by a package's or directory's dotted path from the
|
||||
root, for example `billing.invoices`. Not every directory gets a page: a level that
|
||||
holds a single group is merged into it, and the top page is always `overview`.
|
||||
- **Pages of a schema**: `overview`, then `tables-01`, `tables-02`, …
|
||||
- **An API** (`openapi`, `usage`): `setup`, `client`, `shapes`, and one spot per call:
|
||||
the method and path with every character outside letters, digits, `.`, `_` and `-`
|
||||
replaced by `-`. So `GET /orders/{orderId}/lines` becomes `GET-orders-orderId-lines`.
|
||||
|
||||
Page ids for a code tree depend on the actual directories. **Do not derive them yourself
|
||||
unless you have the full directory listing.** Ask for the output of:
|
||||
|
||||
```
|
||||
python3 -m docgen scaffold docgen.toml --list
|
||||
```
|
||||
|
||||
It prints every spot that exists, one per line.
|
||||
|
||||
### Writing a note
|
||||
|
||||
- Plain markdown: headings, paragraphs, lists, **bold**, *italic*, `code`, fenced code
|
||||
blocks, links, tables, quotes.
|
||||
- No raw HTML. It is shown as text.
|
||||
- Images: a path relative to the note file, e.g. ``. They are
|
||||
embedded.
|
||||
- Keep it short: a few paragraphs. Start with `##` headings, because the page already
|
||||
has its own title.
|
||||
- A note that contains only the stub comment docgen writes renders nothing. Write below
|
||||
the comment, or replace the file's contents.
|
||||
|
||||
Example: `notes/code/billing.invoices.md`:
|
||||
|
||||
```markdown
|
||||
## Why invoices are immutable
|
||||
|
||||
An invoice is never edited after it is issued. Corrections are a new credit note that
|
||||
references it. This is a legal requirement, not a design preference, so do not add
|
||||
update endpoints here.
|
||||
|
||||
Start reading at `InvoiceIssuer`. It is the only place an invoice is created.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When the build refuses your file
|
||||
|
||||
The person running docgen pastes the errors back. Each one says what to change:
|
||||
|
||||
| error contains | fix |
|
||||
|---|---|
|
||||
| `has unknown key 'x'` | remove the key or use a listed one; check the spelling |
|
||||
| `docgen reads a schema dump and never connects` | remove `url`; use `schema = "path/to/schema.json"` |
|
||||
| `no grammar for .sh` | remove those extensions from `ext` |
|
||||
| `no reader named 'x'` | use one of `python`, `code`, `db`, `openapi`, `usage` |
|
||||
| `no output named 'x'` | use `notebook` or `site` |
|
||||
| `is inside the tree it reads` | move `output` or `build` outside the source directory |
|
||||
| `needs a name when there are several` | give each `[[extract.x]]` table a different `name` |
|
||||
| `must name exactly one source` | a `[[book]]` needs exactly one of `root`, `schema`, `openapi`, `har` |
|
||||
| `is not a directory` / `is not a file` | the path is wrong; ask for the real layout |
|
||||
| `note … belongs above 'x', which book 'y' no longer has` | that spot does not exist: rename the note to a listed spot, or delete it |
|
||||
| `both [project] and [[book]]` | use one shape per file |
|
||||
269
soleprint/atlas2/docgen/emitters/docsite.py
Normal file
269
soleprint/atlas2/docgen/emitters/docsite.py
Normal file
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
A document's site: an index of books, and every book as pages you click through.
|
||||
|
||||
site/
|
||||
├── index.html every book, with what came in and what came out
|
||||
├── site.css viewer.html
|
||||
└── <book>/
|
||||
├── overview.html the book's top page
|
||||
├── <page>.html one subsystem each; its boxes open the next level
|
||||
├── <page>.svg the same drawing, for the full-screen viewer
|
||||
└── reference.html every module with its docstring — where a leaf opens
|
||||
|
||||
## Inline, so the boxes are links
|
||||
|
||||
The single-page site put its diagram in an `<img>`, and an image swallows every
|
||||
click. Here the SVG is inlined, so a box that stands for a subsystem opens that
|
||||
subsystem's page, a box for a module opens its section of the reference page,
|
||||
and a context box — something outside this subsystem — opens its own branch.
|
||||
Breadcrumbs lead back up.
|
||||
|
||||
## Adjustable
|
||||
|
||||
A page is drawn to fit the content column, so it reads without zooming. The size
|
||||
is still the reader's: `fit` scales to the column, `1:1` shows the drawing at its
|
||||
natural size with scrollbars, and the choice is remembered. `full screen` opens
|
||||
the pan-and-zoom viewer for anything still too dense.
|
||||
|
||||
This writes HTML and nothing else. What goes on each page, the links and the
|
||||
notes are prepared by `book/document.py` and handed in as data — an emitter has
|
||||
never heard of a book's extractor, and this one has not either.
|
||||
"""
|
||||
|
||||
from html import escape
|
||||
from pathlib import Path
|
||||
|
||||
from .markdown import to_html
|
||||
from .paged import inline
|
||||
from .site import CONTENT_W, CSS, SIDEBAR_W, VIEWER, _fill, _ledger, _slots
|
||||
from .site import emit as reference_page
|
||||
|
||||
CONTENT_WIDTH = CONTENT_W
|
||||
|
||||
SIZE_SCRIPT = """<script>
|
||||
// fit | 1:1 — a choice that sticks, because a reader who wants 1:1 on one page
|
||||
// wants it on the next. No dependency, no build step.
|
||||
(function () {
|
||||
var key = 'docgen.size';
|
||||
function set(mode) {
|
||||
document.querySelectorAll('.fig').forEach(function (f) {
|
||||
f.classList.toggle('actual', mode === 'actual');
|
||||
});
|
||||
document.querySelectorAll('.figbar button[data-size]').forEach(function (b) {
|
||||
b.classList.toggle('on', b.dataset.size === mode);
|
||||
});
|
||||
try { localStorage.setItem(key, mode); } catch (e) {}
|
||||
}
|
||||
document.querySelectorAll('.figbar button[data-size]').forEach(function (b) {
|
||||
b.addEventListener('click', function () { set(b.dataset.size); });
|
||||
});
|
||||
var saved = 'fit';
|
||||
try { saved = localStorage.getItem(key) || 'fit'; } catch (e) {}
|
||||
set(saved);
|
||||
})();
|
||||
</script>"""
|
||||
|
||||
|
||||
def _note(note, cls="note") -> str:
|
||||
if not note:
|
||||
return ""
|
||||
text, uris = note
|
||||
return f'<div class="{cls}">' + to_html(
|
||||
text, image=lambda alt, src: f'<img src="{escape(uris.get(src, src))}" alt="{escape(alt)}">'
|
||||
) + "</div>"
|
||||
|
||||
|
||||
def _missing(failed: dict | None) -> str:
|
||||
"""Books that were meant to be here and are not. Above the ones that are,
|
||||
because a reader who does not see this has no way to know the site is short."""
|
||||
if not failed:
|
||||
return ""
|
||||
items = "".join(f"<li><code>{escape(name)}</code> — {escape(why)}</li>"
|
||||
for name, why in sorted(failed.items()))
|
||||
return ('<div class="gap"><b>This document is incomplete.</b> '
|
||||
f"{len(failed)} book(s) could not be built, so they are not below.<ul>{items}</ul></div>")
|
||||
|
||||
|
||||
def _tree(pages: list[dict], current: str | None) -> str:
|
||||
by_parent: dict = {}
|
||||
for p in pages:
|
||||
by_parent.setdefault(p["parent"], []).append(p)
|
||||
root = pages[0]["id"] if pages else None
|
||||
|
||||
def walk(page, depth):
|
||||
active = ' class="active"' if page["slug"] == current else ""
|
||||
link = f'<a href="{escape(page["slug"])}.html"{active}>{escape(page["title"])}</a>'
|
||||
kids = by_parent.get(page["id"], [])
|
||||
if not kids:
|
||||
return f"<li>{link}</li>"
|
||||
return f"<li>{link}<ul>" + "".join(walk(k, depth + 1) for k in kids) + "</ul></li>"
|
||||
|
||||
return "<ul>" + "".join(walk(p, 0) for p in pages if p["id"] == root) + "</ul>"
|
||||
|
||||
|
||||
def _page(title: str, book: dict, page: dict, svg: str | None, pages: list[dict],
|
||||
note, doc_title: str) -> str:
|
||||
b = book["book"]
|
||||
by_id = {p["id"]: p for p in pages}
|
||||
trail, cur = [], page
|
||||
while cur is not None:
|
||||
trail.append(cur)
|
||||
cur = by_id.get(cur["parent"]) if cur.get("parent") else None
|
||||
crumbs = [f'<a href="../index.html">{escape(doc_title)}</a>']
|
||||
for p in reversed(trail):
|
||||
if p is page:
|
||||
crumbs.append(escape(p["title"]))
|
||||
else:
|
||||
crumbs.append(f'<a href="{escape(p["slug"])}.html">{escape(p["title"])}</a>')
|
||||
|
||||
from ..book.document import caption # noqa: F401 - kept out of module import time
|
||||
cap = caption(page)
|
||||
warn = " warn" if page.get("too_wide") else ""
|
||||
if svg is not None:
|
||||
figure = (
|
||||
'<div class="figbar"><button data-size="fit">fit</button>'
|
||||
'<button data-size="actual">1:1</button>'
|
||||
f'<a href="../viewer.html?src={escape(b.slug)}/{escape(page["slug"])}.svg">full screen</a>'
|
||||
"</div>"
|
||||
f'<div class="fig">{inline(svg)}</div>'
|
||||
)
|
||||
else:
|
||||
figure = '<p class="caption warn">Not drawn — Graphviz is not installed here.</p>'
|
||||
|
||||
children = ""
|
||||
if page.get("children"):
|
||||
items = "".join(
|
||||
f'<li><a href="{escape(by_id[c]["slug"])}.html">{escape(by_id[c]["title"])}</a></li>'
|
||||
for c in page["children"] if c in by_id
|
||||
)
|
||||
children = f"<h3>Opens into</h3><ul class=\"children\">{items}</ul>"
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{escape(page["title"])} — {escape(b.slug)} — {escape(doc_title)}</title>
|
||||
<link rel="stylesheet" href="../site.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header"><b>{escape(b.slug)}</b><small>{escape(doc_title)}</small></div>
|
||||
{_tree(pages, page["slug"])}
|
||||
<ul>
|
||||
<li><a href="reference.html">reference</a></li>
|
||||
<li><a href="explore/explore.html">explore</a></li>
|
||||
<li><a href="../index.html">all books</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="content">
|
||||
<div class="crumbs">{" › ".join(crumbs)}</div>
|
||||
<h1>{escape(page["title"])}</h1>
|
||||
{_note(note)}
|
||||
{figure}
|
||||
<p class="caption{warn}">{escape(cap)}</p>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
{SIZE_SCRIPT}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def write(out_dir, style, *, title: str, books: list[dict], intro=None, end=None,
|
||||
failed: dict | None = None) -> list[Path]:
|
||||
"""Write the site. Returns every file written, for the output manifest.
|
||||
|
||||
`books` is a list of::
|
||||
|
||||
{"book": Book, "pages": [(page, svg)], "notes": {slug: (markdown, {ref: data uri})},
|
||||
"intro": (markdown, {}) or None}
|
||||
"""
|
||||
import shutil
|
||||
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
values = _slots(style)
|
||||
written: list[Path] = []
|
||||
|
||||
def put(path: Path, text: str):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
written.append(path)
|
||||
|
||||
put(out_dir / "site.css",
|
||||
_fill(CSS.replace("__SIDEBAR__", str(SIDEBAR_W)).replace("__CONTENT__", str(CONTENT_W)),
|
||||
values))
|
||||
put(out_dir / "viewer.html", _fill(VIEWER.replace("__TITLE__", escape(title)), values))
|
||||
|
||||
cards, nav = [], []
|
||||
for entry in books:
|
||||
b = entry["book"]
|
||||
pages = [p for p, _ in entry["pages"]]
|
||||
book_dir = out_dir / b.slug
|
||||
for page, svg in entry["pages"]:
|
||||
put(book_dir / f"{page['slug']}.html",
|
||||
_page(title, entry, page, svg, pages, entry["notes"].get(page["slug"]), title))
|
||||
if svg is not None:
|
||||
put(book_dir / f"{page['slug']}.svg", svg)
|
||||
|
||||
# The reference page: the single-page site as it was, one per book, where
|
||||
# a module box on any page opens its own section.
|
||||
ref = reference_page(b.view, style, title=f"{b.slug} — reference")["index.html"]
|
||||
ref = ref.replace('href="site.css"', 'href="../site.css"')
|
||||
put(book_dir / "reference.html", ref)
|
||||
|
||||
explore = b.out / "explore"
|
||||
if explore.is_dir():
|
||||
for f in sorted(explore.rglob("*")):
|
||||
if f.is_file():
|
||||
target = book_dir / "explore" / f.relative_to(explore)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(f, target)
|
||||
written.append(target)
|
||||
|
||||
first = pages[0]["slug"] if pages else "reference"
|
||||
too_wide = sum(1 for p in pages if p.get("too_wide"))
|
||||
cards.append(
|
||||
f'<div class="bookcard"><h2><a href="{escape(b.slug)}/{escape(first)}.html">'
|
||||
f"{escape(b.slug)}</a></h2>"
|
||||
+ _note(entry.get("intro"))
|
||||
+ _ledger(b.to_dict())
|
||||
+ '<div class="links">'
|
||||
+ f'<a href="{escape(b.slug)}/{escape(first)}.html">{len(pages)} page(s)</a>'
|
||||
+ f'<a href="{escape(b.slug)}/reference.html">reference</a>'
|
||||
+ (f'<a href="{escape(b.slug)}/explore/explore.html">explore</a>' if explore.is_dir() else "")
|
||||
+ (f'<span class="caption warn">{too_wide} too wide</span>' if too_wide else "")
|
||||
+ "</div></div>"
|
||||
)
|
||||
nav.append(f'<li><a href="{escape(b.slug)}/{escape(first)}.html">{escape(b.slug)}</a></li>')
|
||||
|
||||
put(out_dir / "index.html", f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{escape(title)}</title>
|
||||
<link rel="stylesheet" href="site.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header"><b>{escape(title)}</b><small>{len(books)} book(s)</small></div>
|
||||
<ul>{"".join(nav)}</ul>
|
||||
</nav>
|
||||
<main class="content">
|
||||
<h1>{escape(title)}</h1>
|
||||
<p class="lede">Regenerated, not edited. Each book opens with what came in and what came out.</p>
|
||||
{_note((intro, {}) if intro else None)}
|
||||
{_missing(failed)}
|
||||
<div class="books">{"".join(cards)}</div>
|
||||
{_note((end, {}) if end else None)}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
return written
|
||||
@@ -59,7 +59,7 @@ def _style_words(rule: dict, *, filled: bool = True) -> str:
|
||||
return ",".join(words)
|
||||
|
||||
|
||||
def _node_attrs(node, rule: dict, style) -> dict:
|
||||
def _node_attrs(node, rule: dict, style, links: dict | None = None) -> dict:
|
||||
a = {
|
||||
"label": node.get("label") or node["id"],
|
||||
"shape": rule.get("shape", "box"),
|
||||
@@ -76,7 +76,16 @@ def _node_attrs(node, rule: dict, style) -> dict:
|
||||
"class": node["kind"],
|
||||
}
|
||||
attrs = node.get("attrs") or {}
|
||||
if attrs.get("file"):
|
||||
if links and node["id"] in links:
|
||||
# A link the caller supplied wins over the source anchor: on a paginated
|
||||
# site a box is a way into its own page, which is worth more than the
|
||||
# file it came from.
|
||||
a["href"] = links[node["id"]]
|
||||
a["tooltip"] = attrs.get("doc") or f"open {node['id']}"
|
||||
elif attrs.get("file") and links is None:
|
||||
# Only when the output is not managing links itself. A relative source
|
||||
# path is a dead link on a web page, so an output that passes `links` —
|
||||
# even an empty one — owns every href in the drawing.
|
||||
line = attrs.get("line")
|
||||
a["href"] = f"{attrs['file']}#L{line}" if line else attrs["file"]
|
||||
a["tooltip"] = attrs.get("doc") or node["id"]
|
||||
@@ -86,8 +95,14 @@ def _node_attrs(node, rule: dict, style) -> dict:
|
||||
|
||||
|
||||
def emit(ir: dict, style, *, max_depth: int | None = None,
|
||||
rankdir: str | None = None) -> str:
|
||||
"""IR document (a dict) + Style -> DOT text."""
|
||||
rankdir: str | None = None, links: dict | None = None) -> str:
|
||||
"""IR document (a dict) + Style -> DOT text.
|
||||
|
||||
`links` maps node ids to URLs and becomes each box's `href` in the SVG.
|
||||
Passed in rather than read from the IR, because where a box leads is a fact
|
||||
about the output — a site page, a notebook anchor — and the IR is the same
|
||||
for every output. Same rule that keeps colour out of it.
|
||||
"""
|
||||
nodes = {n["id"]: n for n in ir["nodes"]}
|
||||
children: dict[str | None, list[str]] = {}
|
||||
for n in ir["nodes"]:
|
||||
@@ -119,7 +134,7 @@ def emit(ir: dict, style, *, max_depth: int | None = None,
|
||||
pad = " " * (depth + 1)
|
||||
|
||||
if not kids or too_deep:
|
||||
out.append(f"{pad}{_q(node_id)}{_attrs(_node_attrs(node, style.node(node['kind']), style))}")
|
||||
out.append(f"{pad}{_q(node_id)}{_attrs(_node_attrs(node, style.node(node['kind']), style, links))}")
|
||||
return
|
||||
|
||||
rule = style.group(node["kind"])
|
||||
@@ -138,6 +153,8 @@ def emit(ir: dict, style, *, max_depth: int | None = None,
|
||||
out.append(f'{pad} labeljust=l')
|
||||
out.append(f'{pad} id="{_esc(node_id)}"')
|
||||
out.append(f'{pad} class="{node["kind"]}"')
|
||||
if links and node_id in links:
|
||||
out.append(f'{pad} href="{_esc(links[node_id])}"')
|
||||
for kid in kids:
|
||||
write(kid, depth + 1)
|
||||
out.append(f"{pad}}}")
|
||||
|
||||
@@ -84,7 +84,19 @@ def _card_height(table: dict, columns: bool = True) -> int:
|
||||
return header + (len(table["fields"]) * FIELD_H if columns else 0)
|
||||
|
||||
|
||||
def layout(tables: list[dict], edges: list[dict], columns: bool = True) -> dict[str, tuple[int, int]]:
|
||||
def max_columns(width_px: int) -> int:
|
||||
"""How many card columns fit a width budget given in CSS pixels.
|
||||
|
||||
The SVG is written in points (`width="Npt"`), and a browser draws a point as
|
||||
4/3 of a pixel — so the budget is converted before dividing, or a page sized
|
||||
for 960px renders at 1280px and the notebook shrinks it after all.
|
||||
"""
|
||||
usable = width_px * 0.75 - 2 * PAD + COL_GAP
|
||||
return max(1, int(usable // (CARD_W + COL_GAP)))
|
||||
|
||||
|
||||
def layout(tables: list[dict], edges: list[dict], columns: bool = True,
|
||||
max_cols: int | None = None) -> dict[str, tuple[int, int]]:
|
||||
"""Place cards in √n columns, referenced tables first.
|
||||
|
||||
Sorting by "is the target of a foreign key" puts the tables everything
|
||||
@@ -95,6 +107,10 @@ def layout(tables: list[dict], edges: list[dict], columns: bool = True) -> dict[
|
||||
ordered = sorted(tables, key=lambda t: (t["id"] not in referenced, t["id"]))
|
||||
|
||||
cols = max(2, int((len(ordered) * 1.2) ** 0.5 + 0.999))
|
||||
if max_cols is not None:
|
||||
# Capped, not chosen: the page grows downward instead, which a notebook
|
||||
# scrolls and does not shrink.
|
||||
cols = max(1, min(cols, max_cols))
|
||||
col_w = CARD_W + COL_GAP
|
||||
cursor = [PAD] * cols
|
||||
|
||||
@@ -111,7 +127,7 @@ def _field_y(table: dict, index: int, top: int) -> float:
|
||||
return top + header + (max(index, 0) + 0.5) * FIELD_H
|
||||
|
||||
|
||||
def emit(ir: dict, style, *, columns: bool = True) -> str:
|
||||
def emit(ir: dict, style, *, columns: bool = True, width: int | None = None) -> str:
|
||||
"""IR (a db document) + Style -> SVG text.
|
||||
|
||||
`columns=False` draws the header of every card and none of its contents —
|
||||
@@ -127,7 +143,7 @@ def emit(ir: dict, style, *, columns: bool = True) -> str:
|
||||
)
|
||||
edges = [e for e in ir["edges"] if e["kind"] in ("foreign_key", "references")]
|
||||
by_id = {t["id"]: t for t in tables}
|
||||
pos = layout(tables, edges, columns)
|
||||
pos = layout(tables, edges, columns, max_cols=max_columns(width) if width else None)
|
||||
|
||||
s = style.slot
|
||||
width = max(x for x, _ in pos.values()) + CARD_W + PAD
|
||||
|
||||
145
soleprint/atlas2/docgen/emitters/markdown.py
Normal file
145
soleprint/atlas2/docgen/emitters/markdown.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Markdown -> HTML, for notes on the site. Stdlib only, and deliberately small.
|
||||
|
||||
A notebook renders markdown itself, so notes go into it as written. The site has
|
||||
no renderer, and adding a dependency to turn a few paragraphs into HTML would
|
||||
break the rule that the structural path is the stdlib. So this covers what notes
|
||||
are actually written with:
|
||||
|
||||
# headings paragraphs - bullet / 1. numbered lists
|
||||
**bold** *italic* `code` ```fenced code```
|
||||
[links](url)  | pipe | tables |
|
||||
> quotes ---
|
||||
|
||||
Anything it does not recognise is **escaped, not passed through** — a note is
|
||||
text, and raw HTML in it is shown rather than executed.
|
||||
"""
|
||||
|
||||
import re
|
||||
from html import escape
|
||||
|
||||
|
||||
def _url(raw: str) -> str:
|
||||
"""An attribute-safe URL. The text around it is already escaped without
|
||||
quotes, so a `"` here would close the attribute; and a `javascript:` link is
|
||||
refused outright — a note is text, not a place to run code."""
|
||||
if re.match(r"^\s*(javascript|vbscript|data):", raw, re.I):
|
||||
return "#"
|
||||
return raw.replace('"', """)
|
||||
|
||||
|
||||
INLINE = [
|
||||
(re.compile(r"!\[([^\]]*)\]\(([^)\s]+)\)"), lambda m, img: img(m.group(1), m.group(2))),
|
||||
(re.compile(r"\[([^\]]+)\]\(([^)\s]+)\)"),
|
||||
lambda m, img: f'<a href="{_url(m.group(2))}">{m.group(1)}</a>'),
|
||||
(re.compile(r"\*\*(.+?)\*\*"), lambda m, img: f"<strong>{m.group(1)}</strong>"),
|
||||
(re.compile(r"(?<![*\w])\*(?!\s)(.+?)(?<!\s)\*(?![*\w])"), lambda m, img: f"<em>{m.group(1)}</em>"),
|
||||
]
|
||||
|
||||
|
||||
def _inline(text: str, image) -> str:
|
||||
# Code spans first and protected, so nothing inside them is formatted.
|
||||
spans: list[str] = []
|
||||
|
||||
def keep(m):
|
||||
spans.append(f"<code>{escape(m.group(1))}</code>")
|
||||
return f"\x00{len(spans) - 1}\x00"
|
||||
|
||||
text = re.sub(r"`([^`]+)`", keep, text)
|
||||
text = escape(text, quote=False)
|
||||
for pattern, render in INLINE:
|
||||
text = pattern.sub(lambda m: render(m, image), text)
|
||||
return re.sub(r"\x00(\d+)\x00", lambda m: spans[int(m.group(1))], text)
|
||||
|
||||
|
||||
def to_html(text: str, *, image=None) -> str:
|
||||
"""Render a note. `image(alt, src) -> html` lets the caller embed local images."""
|
||||
image = image or (lambda alt, src: f'<img src="{_url(src)}" alt="{alt.replace(chr(34), """)}">')
|
||||
lines = text.replace("\r\n", "\n").split("\n")
|
||||
out: list[str] = []
|
||||
i = 0
|
||||
|
||||
def para(buf):
|
||||
if buf:
|
||||
out.append(f"<p>{_inline(' '.join(s.strip() for s in buf), image)}</p>")
|
||||
|
||||
buf: list[str] = []
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
if stripped.startswith("```"):
|
||||
para(buf); buf = []
|
||||
code = []
|
||||
i += 1
|
||||
while i < len(lines) and not lines[i].strip().startswith("```"):
|
||||
code.append(lines[i])
|
||||
i += 1
|
||||
out.append(f"<pre><code>{escape(chr(10).join(code))}</code></pre>")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if not stripped:
|
||||
para(buf); buf = []
|
||||
i += 1
|
||||
continue
|
||||
|
||||
m = re.match(r"^(#{1,6})\s+(.*)$", stripped)
|
||||
if m:
|
||||
para(buf); buf = []
|
||||
level = len(m.group(1))
|
||||
out.append(f"<h{level}>{_inline(m.group(2), image)}</h{level}>")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if re.match(r"^(-{3,}|\*{3,})$", stripped):
|
||||
para(buf); buf = []
|
||||
out.append("<hr>")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if stripped.startswith(">"):
|
||||
para(buf); buf = []
|
||||
quote = []
|
||||
while i < len(lines) and lines[i].strip().startswith(">"):
|
||||
quote.append(lines[i].strip()[1:].strip())
|
||||
i += 1
|
||||
out.append(f"<blockquote>{to_html(chr(10).join(quote), image=image)}</blockquote>")
|
||||
continue
|
||||
|
||||
if re.match(r"^([-*+]|\d+[.)])\s+", stripped):
|
||||
para(buf); buf = []
|
||||
ordered = bool(re.match(r"^\d", stripped))
|
||||
items = []
|
||||
while i < len(lines) and re.match(r"^\s*([-*+]|\d+[.)])\s+", lines[i]):
|
||||
items.append(re.sub(r"^\s*([-*+]|\d+[.)])\s+", "", lines[i]))
|
||||
i += 1
|
||||
tag = "ol" if ordered else "ul"
|
||||
out.append(f"<{tag}>" + "".join(f"<li>{_inline(it, image)}</li>" for it in items)
|
||||
+ f"</{tag}>")
|
||||
continue
|
||||
|
||||
if stripped.startswith("|") and i + 1 < len(lines) \
|
||||
and re.match(r"^\|?\s*:?-{3,}", lines[i + 1].strip()):
|
||||
para(buf); buf = []
|
||||
|
||||
def cells(row):
|
||||
return [c.strip() for c in row.strip().strip("|").split("|")]
|
||||
|
||||
head = cells(lines[i])
|
||||
i += 2
|
||||
rows = []
|
||||
while i < len(lines) and lines[i].strip().startswith("|"):
|
||||
rows.append(cells(lines[i]))
|
||||
i += 1
|
||||
html = "<table><tr>" + "".join(f"<th>{_inline(h, image)}</th>" for h in head) + "</tr>"
|
||||
for r in rows:
|
||||
html += "<tr>" + "".join(f"<td>{_inline(c, image)}</td>" for c in r) + "</tr>"
|
||||
out.append(html + "</table>")
|
||||
continue
|
||||
|
||||
buf.append(line)
|
||||
i += 1
|
||||
|
||||
para(buf)
|
||||
return "\n".join(out)
|
||||
@@ -40,7 +40,7 @@ METADATA = {
|
||||
}
|
||||
|
||||
|
||||
def _cell(index: int, kind: str, text: str) -> dict:
|
||||
def _cell(index: int, kind: str, text: str, attachments: dict | None = None) -> dict:
|
||||
lines = text.split("\n")
|
||||
source = [ln + "\n" for ln in lines[:-1]] + ([lines[-1]] if lines[-1] else [])
|
||||
cell = {
|
||||
@@ -49,6 +49,12 @@ def _cell(index: int, kind: str, text: str) -> dict:
|
||||
"metadata": {},
|
||||
"source": source,
|
||||
}
|
||||
if attachments and kind == "md":
|
||||
# nbformat 4 lets a markdown cell carry its own images, referenced as
|
||||
# `attachment:<name>`. That is how a figure renders without the notebook
|
||||
# being run: nothing is an output, so "run all" cannot clear it, and the
|
||||
# same SVG gives the same bytes every build.
|
||||
cell["attachments"] = {name: dict(bundle) for name, bundle in sorted(attachments.items())}
|
||||
if kind == "code":
|
||||
cell["execution_count"] = None
|
||||
cell["outputs"] = []
|
||||
@@ -212,15 +218,50 @@ def _call_md(step: dict) -> str:
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _figure_md(step: dict) -> str:
|
||||
"""A page of a paginated diagram, as markdown.
|
||||
|
||||
The anchor is an explicit `<a name>` rather than the heading's own: JupyterLab
|
||||
and VS Code derive heading anchors differently, and JupyterLab's sanitiser
|
||||
keeps `name` where it drops `id`. The page index links to these.
|
||||
"""
|
||||
parts = []
|
||||
if step.get("anchor"):
|
||||
parts.append(f'<a name="{step["anchor"]}"></a>')
|
||||
parts.append("")
|
||||
if step.get("title"):
|
||||
parts.append(f'{"#" * min(6, 2 + int(step.get("depth", 0)))} {step["title"]}')
|
||||
parts.append("")
|
||||
if step.get("svg"):
|
||||
parts.append(f'')
|
||||
else:
|
||||
parts.append(f'*Not drawn here — {step.get("why", "no renderer")}.*')
|
||||
if step.get("caption"):
|
||||
parts += ["", f'*{step["caption"]}*']
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _figure_attachments(step: dict) -> dict | None:
|
||||
if not step.get("svg"):
|
||||
return None
|
||||
import base64
|
||||
|
||||
data = step["svg"].encode("utf-8") if isinstance(step["svg"], str) else step["svg"]
|
||||
return {step["name"]: {"image/svg+xml": base64.b64encode(data).decode("ascii")}}
|
||||
|
||||
|
||||
def build(spec: dict) -> dict:
|
||||
"""A merged notebook spec -> the notebook, as a dict."""
|
||||
blocks: list[tuple[str, str]] = []
|
||||
blocks: list[tuple] = []
|
||||
|
||||
for step in spec["steps"]:
|
||||
kind = step.get("kind", "md")
|
||||
before = step.get("before")
|
||||
if before:
|
||||
blocks.append(("md", before))
|
||||
blocks.append(("md", before, step.get("before_attachments")))
|
||||
|
||||
if kind == "figure":
|
||||
blocks.append(("md", _figure_md(step), _figure_attachments(step)))
|
||||
|
||||
if kind == "md":
|
||||
text = []
|
||||
@@ -259,7 +300,7 @@ def build(spec: dict) -> dict:
|
||||
blocks.append(("md", after))
|
||||
|
||||
return {
|
||||
"cells": [_cell(i, k, t) for i, (k, t) in enumerate(blocks)],
|
||||
"cells": [_cell(i, *block) for i, block in enumerate(blocks)],
|
||||
"metadata": METADATA,
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5,
|
||||
|
||||
118
soleprint/atlas2/docgen/emitters/paged.py
Normal file
118
soleprint/atlas2/docgen/emitters/paged.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Draw pages: measure each one against a width, then render it for an output.
|
||||
|
||||
`ops/pages.py` decides which pages exist and never renders anything — `ops/`
|
||||
imports no emitter. This is the other half: the `fits` test it needs, and the
|
||||
drawing of a laid-out page, with the links an output wants.
|
||||
|
||||
## Measured, not estimated
|
||||
|
||||
A layered drawing's width is its widest level, but *how* wide depends on the
|
||||
labels, the font and the spacing the style sets. A count-based estimate was off
|
||||
by enough to matter, so when Graphviz is present a page is rendered and its
|
||||
`width="Npt"` read back. Without Graphviz the estimate is all there is, and the
|
||||
page records `measured: False` so nobody mistakes one for the other.
|
||||
|
||||
## Points and pixels
|
||||
|
||||
Graphviz and the ERD emitter both write sizes in points, and a browser draws a
|
||||
point as 4/3 of a CSS pixel. Budgets here are pixels, because the column they
|
||||
have to fit — a notebook cell, a site's content column — is sized in pixels.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
PT_TO_PX = 4 / 3
|
||||
|
||||
# Budgets for the outputs docgen writes. A notebook cell in JupyterLab or VS Code
|
||||
# is a little under a thousand pixels at a normal window size; the site's content
|
||||
# column is set by `site.CONTENT_W`.
|
||||
NOTEBOOK_WIDTH = 960
|
||||
|
||||
|
||||
def svg_width_px(svg: str) -> float | None:
|
||||
m = re.search(r'<svg[^>]*\swidth="([\d.]+)(pt|px)?"', svg)
|
||||
if not m:
|
||||
return None
|
||||
value = float(m.group(1))
|
||||
return value * PT_TO_PX if (m.group(2) or "pt") == "pt" else value
|
||||
|
||||
|
||||
def _estimate_px(ir: dict, options: dict) -> float:
|
||||
"""Width without a renderer: widest level of boxes, or levels side by side."""
|
||||
from ..ops import shape
|
||||
|
||||
sh = shape(ir)
|
||||
labels = [len(n.get("label") or n["id"]) for n in ir["nodes"]] or [8]
|
||||
box = 7 * (sum(labels) / len(labels)) + 40
|
||||
if options.get("rankdir") == "LR":
|
||||
return sh["levels"] * (box + 58) + 30
|
||||
return sh["widest_level"] * (box + 36) + 30
|
||||
|
||||
|
||||
def fits_for(style, width: int):
|
||||
"""A `fits(page_ir, options) -> bool` for `ops.pages.layout`, at `width` px.
|
||||
|
||||
Also returns whether it measured or estimated, so the ledger can say which.
|
||||
"""
|
||||
from .dot import emit, have_graphviz, render
|
||||
|
||||
measured = have_graphviz()
|
||||
|
||||
def fits(ir: dict, options: dict) -> bool:
|
||||
if not measured:
|
||||
return _estimate_px(ir, options) <= width
|
||||
svg = render(emit(ir, style, rankdir=options.get("rankdir"))).decode("utf-8")
|
||||
px = svg_width_px(svg)
|
||||
return px is not None and px <= width
|
||||
|
||||
return fits, measured
|
||||
|
||||
|
||||
def links_for(pages: list[dict], page: dict, url) -> dict:
|
||||
"""{box id on this page: url of the page that box opens}.
|
||||
|
||||
`url(target_page)` builds the address, so the site and anything else that
|
||||
can follow a link decide their own scheme. Context boxes link too, when their
|
||||
branch has a page of its own — that is how a reader crosses to a sibling.
|
||||
"""
|
||||
by_scope = {p.get("scope") or p["id"]: p for p in pages}
|
||||
by_id = {p["id"]: p for p in pages}
|
||||
out = {}
|
||||
for n in page["ir"]["nodes"]:
|
||||
target = by_id.get(n["id"]) if page.get("tables") else by_scope.get(n["id"])
|
||||
if target is not None and target is not page:
|
||||
out[n["id"]] = url(target)
|
||||
return out
|
||||
|
||||
|
||||
def render(page: dict, style, width: int, *, links: dict | None = None) -> str | None:
|
||||
"""A laid-out page as SVG text, or None when it cannot be drawn here.
|
||||
|
||||
Schema pages go through the ERD emitter with its columns capped to `width`;
|
||||
everything else, schema overviews included, through Graphviz with the page's
|
||||
layout options and the caller's links.
|
||||
"""
|
||||
if page.get("tables") and page["id"] != "overview":
|
||||
from .erd import emit as erd
|
||||
|
||||
return erd(page["ir"], style, width=width)
|
||||
|
||||
from .dot import emit, have_graphviz, render as dot_render
|
||||
|
||||
if not have_graphviz():
|
||||
return None
|
||||
svg = dot_render(emit(page["ir"], style, rankdir=(page.get("options") or {}).get("rankdir"),
|
||||
links=links)).decode("utf-8")
|
||||
return svg
|
||||
|
||||
|
||||
def inline(svg: str) -> str:
|
||||
"""SVG text ready to sit inside HTML: no XML declaration, no DOCTYPE.
|
||||
|
||||
Inline rather than `<img>`, because an image swallows every click and the
|
||||
whole point of a page's boxes is that they open other pages.
|
||||
"""
|
||||
svg = re.sub(r"<\?xml[^>]*\?>\s*", "", svg)
|
||||
svg = re.sub(r"<!DOCTYPE[^>]*>\s*", "", svg)
|
||||
return re.sub(r"<!--.*?-->\s*", "", svg, flags=re.S).strip()
|
||||
@@ -177,6 +177,11 @@ CSS = """/* Generated by docgen. The layout five demos converged on: a sticky si
|
||||
--muted: __MUTED__;
|
||||
--dim: __DIM__;
|
||||
--accent: __ACCENT__;
|
||||
/* Used by the ledger's "incomplete" panel. Declared here because it was not,
|
||||
and an undefined custom property falls back silently — the warning rendered
|
||||
in the ordinary text colour, which is the one place it must not. */
|
||||
--artery: __ARTERY__;
|
||||
--ok: __OK__;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -267,6 +272,44 @@ body {
|
||||
.gap b { color: var(--artery); }
|
||||
.gap ul { margin: .4rem 0 0 1.1rem; color: var(--muted); }
|
||||
.gap code { font-size: 11px; }
|
||||
|
||||
/* -- pages ------------------------------------------------------------------
|
||||
A page is one subsystem drawn small enough to read. The diagram is inline SVG,
|
||||
not an <img>, so its boxes are links: a group opens its own page, a module
|
||||
opens its section of the reference page. */
|
||||
.crumbs { color: var(--dim); font-size: 11px; margin-bottom: .6rem; }
|
||||
.crumbs a { color: var(--muted); text-decoration: none; }
|
||||
.crumbs a:hover { color: var(--accent); }
|
||||
.note { margin: 0 0 1.2rem; }
|
||||
.note h1, .note h2, .note h3 { color: var(--text); margin: 1rem 0 .4rem; font-size: 14px; }
|
||||
.note p, .note li { color: var(--muted); margin-bottom: .5rem; }
|
||||
.note ul, .note ol { margin: 0 0 .8rem 1.2rem; }
|
||||
.note pre { background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: .6rem .8rem; overflow-x: auto; margin: .6rem 0; }
|
||||
.note code { font-size: 11px; }
|
||||
.note table { border-collapse: collapse; margin: .6rem 0; }
|
||||
.note th, .note td { border-bottom: 1px solid var(--border); padding: 4px 12px 4px 0; text-align: left; }
|
||||
.note blockquote { border-left: 2px solid var(--border); padding-left: .8rem; color: var(--dim); }
|
||||
.note img { max-width: 100%; }
|
||||
.figbar { display: flex; gap: 6px; align-items: center; margin-bottom: .4rem; font-size: 11px; color: var(--dim); }
|
||||
.figbar button, .figbar a {
|
||||
background: var(--surface); color: var(--muted); border: 1px solid var(--border);
|
||||
border-radius: 5px; padding: 2px 8px; font-size: 11px; cursor: pointer; text-decoration: none; }
|
||||
.figbar button.on { color: var(--accent); border-color: var(--accent); }
|
||||
.fig { border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
|
||||
padding: 8px; overflow: hidden; }
|
||||
.fig svg { display: block; max-width: 100%; height: auto; margin: 0 auto; }
|
||||
.fig.actual { overflow: auto; }
|
||||
.fig.actual svg { max-width: none; }
|
||||
.fig a:hover polygon, .fig a:hover path { stroke: var(--accent); }
|
||||
.caption { color: var(--dim); font-size: 11px; margin: .4rem 0 1.2rem; }
|
||||
.caption.warn { color: var(--artery); }
|
||||
.children { columns: 2; margin: .4rem 0 1.4rem; list-style: none; }
|
||||
.children li { margin-bottom: 2px; }
|
||||
.books { display: grid; gap: 1rem; margin: 1rem 0 2rem; }
|
||||
.bookcard { border: 1px solid var(--border); border-radius: 8px; padding: .9rem 1rem; }
|
||||
.bookcard h2 { font-size: 15px; margin-bottom: .5rem; }
|
||||
.bookcard .links a { margin-right: 1rem; font-size: 12px; }
|
||||
"""
|
||||
|
||||
|
||||
@@ -337,6 +380,8 @@ def _slots(style) -> dict:
|
||||
"__MUTED__": s("text-muted"),
|
||||
"__DIM__": s("text-dim"),
|
||||
"__ACCENT__": s("accent"),
|
||||
"__ARTERY__": s("error") or s("artery"),
|
||||
"__OK__": s("ok") or s("atlas"),
|
||||
"__FONT__": '"Segoe UI", Inter, system-ui, -apple-system, Arial, sans-serif',
|
||||
}
|
||||
|
||||
|
||||
3
soleprint/atlas2/docgen/fixtures/notes/_intro.md
Normal file
3
soleprint/atlas2/docgen/fixtures/notes/_intro.md
Normal file
@@ -0,0 +1,3 @@
|
||||
<!-- docgen note: top of the document. Write markdown below; this comment does not render, and an empty note adds nothing. -->
|
||||
The fixtures docgen tests itself against: a Python tree, a schema and an API
|
||||
contract, built into one document to show the parts fitting together.
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- docgen note: schema › page Overview. Write markdown below; this comment does not render, and an empty note adds nothing. -->
|
||||
## Where to start
|
||||
|
||||
`orders` is the centre: every line belongs to one, and every order to one user.
|
||||
Open the page to see the keys.
|
||||
@@ -51,7 +51,7 @@ from pathlib import Path
|
||||
SPEC_VERSION = "1"
|
||||
|
||||
# Step kinds. Small and closed, the same discipline the IR's `kind` follows.
|
||||
KINDS = ("md", "code", "params", "call")
|
||||
KINDS = ("md", "code", "params", "call", "figure")
|
||||
|
||||
|
||||
def _step(id: str, kind: str, **fields) -> dict:
|
||||
|
||||
382
soleprint/atlas2/docgen/ops/pages.py
Normal file
382
soleprint/atlas2/docgen/ops/pages.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
Pages: a graph too big for one picture, as a set of pictures that each fit.
|
||||
|
||||
pages = paginate(view) # the structure: which pages exist
|
||||
laid = layout(pages, fits) # per output: fits(page_ir, options) -> bool
|
||||
|
||||
A notebook scales an image down to its cell width, so a wide graph does not
|
||||
scroll — it shrinks until its labels are unreadable. The only fix that holds is
|
||||
to draw less per picture. This decides *what* goes on each picture; the caller
|
||||
decides whether it fits, by rendering it and measuring, because a width
|
||||
estimated from node counts is wrong the moment the labels are long.
|
||||
|
||||
**Which pages exist does not depend on width** — only how each one is laid out
|
||||
does. So the two are separate calls: the page ids a note is filed under are the
|
||||
same for a notebook column and a site column, and listing them needs no
|
||||
renderer at all.
|
||||
|
||||
## By structure, never by slicing
|
||||
|
||||
Tiling one large image cuts boxes and edges at the seams, and a tile means
|
||||
nothing on its own. A page here is a subsystem:
|
||||
|
||||
overview one box per top-level group, relationships lifted
|
||||
└── station one box per child of `station`
|
||||
└── station.tools one box per child of `station.tools`, and so on
|
||||
|
||||
Clicking a box — on the site — or following the index — in a notebook — moves
|
||||
down a level. Every page is a real graph: the boxes are the scope's children,
|
||||
and anything outside the scope that they touch appears as a **context** box,
|
||||
collapsed to the branch it sits on, so a page still says what its subsystem
|
||||
reaches without drawing the rest of the tree.
|
||||
|
||||
## Groups come from the id, not from `parent`
|
||||
|
||||
A directory without `__init__.py` is not a Python package, so its modules have
|
||||
no `parent` — soleprint has 173 such roots — but their dotted ids still carry
|
||||
the directory. `split()` groups on id segments for the same reason. A group with
|
||||
no node of its own (such a directory) is synthesised on the page as a box of
|
||||
the kind most of its contents are, marked `attrs.group`.
|
||||
|
||||
## When a level cannot be narrowed
|
||||
|
||||
A scope whose children genuinely are many peers has no smaller structure to
|
||||
split into. It is redrawn left-to-right — height is free in a notebook, width is
|
||||
not — and if that still does not fit, the page is kept and flagged `too_wide`.
|
||||
Never cut, never dropped: a flagged page is honest, a missing one is not.
|
||||
|
||||
## Schemas page differently
|
||||
|
||||
A schema is peers with references, drawn as cards in columns whose count the
|
||||
emitter caps to the width, so width is never the problem — count is. Tables are
|
||||
paged by connected component (tables joined by foreign keys stay together), and
|
||||
tables with no keys at all are chunked alphabetically.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
OVERVIEW = "overview"
|
||||
MAX_TABLES = 20 # past this a schema page stops being scannable
|
||||
|
||||
|
||||
def slugify(page_id: str) -> str:
|
||||
"""A filename- and anchor-safe name for a page id.
|
||||
|
||||
Ids are not safe as they stand: an endpoint id is `GET /orders/{id}`, and a
|
||||
tree-sitter id can carry `#L12`.
|
||||
"""
|
||||
slug = re.sub(r"[^A-Za-z0-9._-]+", "-", page_id).strip("-.")
|
||||
return slug or "page"
|
||||
|
||||
|
||||
def _key(node_id: str, scope: str) -> str:
|
||||
"""The box at `scope` that `node_id` falls into: scope + one more segment."""
|
||||
if scope == "":
|
||||
return node_id.split(".", 1)[0]
|
||||
if node_id == scope:
|
||||
return scope
|
||||
rest = node_id[len(scope) + 1:]
|
||||
return f"{scope}.{rest.split('.', 1)[0]}"
|
||||
|
||||
|
||||
def _under(node: dict, scope: str) -> bool:
|
||||
if node["kind"] == "external":
|
||||
return False # externals are context everywhere, never members
|
||||
nid = node["id"]
|
||||
return scope == "" or nid == scope or nid.startswith(scope + ".")
|
||||
|
||||
|
||||
def _representative(node_id: str, scope: str) -> str:
|
||||
"""Where an outside node shows on this page: the branch it diverges on.
|
||||
|
||||
The common prefix of the scope and the node, plus one more segment of the
|
||||
node's id. That box is never an ancestor of the scope (it is on the node's
|
||||
branch, past the point they part), so it never swallows the page's own
|
||||
members — and two outside nodes on the same branch share one box.
|
||||
"""
|
||||
s, n = scope.split(".") if scope else [], node_id.split(".")
|
||||
common = 0
|
||||
while common < len(s) and common < len(n) and s[common] == n[common]:
|
||||
common += 1
|
||||
return ".".join(n[: common + 1])
|
||||
|
||||
|
||||
def page_ir(view: dict, scope: str) -> tuple[dict, dict]:
|
||||
"""The graph for one page, and {member key: [node ids it stands for]}.
|
||||
|
||||
Built directly rather than through `_rebuild`, because a synthesised group
|
||||
box and a context box have no node in the source graph to keep.
|
||||
"""
|
||||
by_id = {n["id"]: n for n in view["nodes"]}
|
||||
members: dict[str, list] = {}
|
||||
for n in view["nodes"]:
|
||||
if _under(n, scope):
|
||||
members.setdefault(_key(n["id"], scope), []).append(n["id"])
|
||||
|
||||
where = {nid: key for key, ids in members.items() for nid in ids}
|
||||
context: dict[str, list] = {}
|
||||
for e in view["edges"]:
|
||||
s_in, t_in = e["source"] in where, e["target"] in where
|
||||
if s_in == t_in:
|
||||
continue # internal to the page (handled below) or unrelated
|
||||
outside = e["target"] if s_in else e["source"]
|
||||
rep = _representative(outside, scope)
|
||||
if rep in members:
|
||||
continue # cannot happen for a well-formed tree; stay safe
|
||||
context.setdefault(rep, []).append(outside)
|
||||
where.setdefault(outside, rep)
|
||||
|
||||
nodes = []
|
||||
for key, ids in sorted(members.items()) + sorted(context.items()):
|
||||
is_context = key in context and key not in members
|
||||
own = by_id.get(key)
|
||||
ids = sorted(set(ids))
|
||||
if own is not None and (ids == [key] or not is_context):
|
||||
node = {"id": key, "kind": own["kind"], "label": own.get("label") or key,
|
||||
"parent": None, "attrs": dict(own.get("attrs") or {})}
|
||||
else:
|
||||
kinds = [by_id[i]["kind"] for i in ids if i in by_id]
|
||||
kind = max(set(kinds), key=lambda k: (kinds.count(k), k)) if kinds else "module"
|
||||
node = {"id": key, "kind": kind, "label": key.rsplit(".", 1)[-1],
|
||||
"parent": None, "attrs": {}}
|
||||
# Semantic, not visual: which boxes are subsystems to open, and which are
|
||||
# the outside world seen from here. How that looks is style's decision.
|
||||
grouped = [i for i in ids if i != key]
|
||||
if grouped or (own is None):
|
||||
node["attrs"]["group"] = True
|
||||
node["attrs"]["count"] = len(ids)
|
||||
if is_context:
|
||||
node["attrs"]["context"] = True
|
||||
nodes.append(node)
|
||||
|
||||
merged: dict[tuple, dict] = {}
|
||||
for e in view["edges"]:
|
||||
src, dst = where.get(e["source"]), where.get(e["target"])
|
||||
if src is None or dst is None or src == dst:
|
||||
continue
|
||||
if src in context and dst in context:
|
||||
continue # two outside boxes: not this page's relationship
|
||||
k = (src, dst, e["kind"])
|
||||
if k in merged:
|
||||
merged[k]["attrs"]["weight"] = merged[k]["attrs"].get("weight", 1) + 1
|
||||
continue
|
||||
attrs = dict(e.get("attrs") or {})
|
||||
if src != e["source"] or dst != e["target"]:
|
||||
attrs = {"weight": 1}
|
||||
merged[k] = {"source": src, "target": dst, "kind": e["kind"], "attrs": attrs}
|
||||
|
||||
ir = {"meta": dict(view["meta"]), "nodes": nodes,
|
||||
"edges": sorted(merged.values(), key=lambda e: (e["source"], e["target"], e["kind"]))}
|
||||
return ir, members
|
||||
|
||||
|
||||
def layout(pages: list[dict], fits) -> list[dict]:
|
||||
"""The same pages, each with the layout that fits: top-to-bottom, then
|
||||
left-to-right, then flagged `too_wide`. Returns new page dicts.
|
||||
|
||||
Schema pages are left alone — the ERD emitter caps its own columns to the
|
||||
width, so they always fit — except the overview, which is a graph.
|
||||
"""
|
||||
out = []
|
||||
for page in pages:
|
||||
page = dict(page)
|
||||
if page.get("tables") and page["id"] != OVERVIEW:
|
||||
page["options"], page["too_wide"] = {}, False
|
||||
elif fits(page["ir"], {}):
|
||||
page["options"], page["too_wide"] = {}, False
|
||||
elif fits(page["ir"], {"rankdir": "LR"}):
|
||||
page["options"], page["too_wide"] = {"rankdir": "LR"}, False
|
||||
else:
|
||||
page["options"], page["too_wide"] = {"rankdir": "LR"}, True
|
||||
out.append(page)
|
||||
return out
|
||||
|
||||
|
||||
def paginate(view: dict, *, max_depth: int = 6) -> list[dict]:
|
||||
"""Pages for a graph, overview first, each drawn as a subsystem.
|
||||
|
||||
Returns a list of::
|
||||
|
||||
{"id", "slug", "title", "scope", "parent", "depth", "ir",
|
||||
"children": [page ids], "options": {}, "too_wide": False}
|
||||
|
||||
`options` and `too_wide` are placeholders until `layout()` fills them.
|
||||
|
||||
in reading order — a page is followed by its own children, depth first — so
|
||||
a notebook can emit them in sequence and a site can build its tree from
|
||||
`parent` and `children`.
|
||||
"""
|
||||
pages: list[dict] = []
|
||||
slugs: set[str] = set()
|
||||
|
||||
def unique(slug: str) -> str:
|
||||
base, i = slug, 2
|
||||
while slug in slugs:
|
||||
slug, i = f"{base}-{i}", i + 1
|
||||
slugs.add(slug)
|
||||
return slug
|
||||
|
||||
def opens(key: str, scope: str, ids: list) -> bool:
|
||||
# A box opens into a page when it stands for more than itself.
|
||||
return key != scope and any(i != key for i in ids)
|
||||
|
||||
def visit(scope: str, parent: str | None, depth: int, page_id: str | None = None):
|
||||
ir, members = page_ir(view, scope)
|
||||
if not ir["nodes"]:
|
||||
return
|
||||
# A page whose only member is one group says nothing but "open this" —
|
||||
# docgen's own overview was one box reading `docgen`. Descend instead,
|
||||
# keeping the id the caller expects, so the overview is still `overview`.
|
||||
groups = [k for k, ids in members.items() if opens(k, scope, ids)]
|
||||
if len(members) == 1 and len(groups) == 1:
|
||||
return visit(groups[0], parent, depth, page_id or (scope or OVERVIEW))
|
||||
page_id = page_id or scope or OVERVIEW
|
||||
page = {
|
||||
"id": page_id,
|
||||
"slug": unique(slugify(page_id)),
|
||||
"title": scope if scope and page_id == scope else (scope or "Overview"),
|
||||
"scope": scope,
|
||||
"parent": parent,
|
||||
"depth": depth,
|
||||
"ir": ir,
|
||||
"options": {},
|
||||
"children": [],
|
||||
"too_wide": False,
|
||||
}
|
||||
pages.append(page)
|
||||
if depth >= max_depth:
|
||||
return
|
||||
for key in sorted(members):
|
||||
if opens(key, scope, members[key]):
|
||||
page["children"].append(key)
|
||||
visit(key, page_id, depth + 1)
|
||||
|
||||
visit("", None, 0)
|
||||
return pages
|
||||
|
||||
|
||||
def paginate_tables(view: dict, *, max_tables: int = MAX_TABLES) -> list[dict]:
|
||||
"""Pages for a schema: connected components, packed, then chunked.
|
||||
|
||||
Each page is the tables plus their columns, so the ERD emitter draws cards.
|
||||
The overview has one box per page, which is what a reader opens from.
|
||||
"""
|
||||
tables = sorted(n["id"] for n in view["nodes"] if n["kind"] == "table")
|
||||
links: dict[str, set] = {t: set() for t in tables}
|
||||
for e in view["edges"]:
|
||||
if e["source"] in links and e["target"] in links:
|
||||
links[e["source"]].add(e["target"])
|
||||
links[e["target"]].add(e["source"])
|
||||
|
||||
seen, components = set(), []
|
||||
for t in tables:
|
||||
if t in seen:
|
||||
continue
|
||||
stack, comp = [t], []
|
||||
while stack:
|
||||
cur = stack.pop()
|
||||
if cur in seen:
|
||||
continue
|
||||
seen.add(cur)
|
||||
comp.append(cur)
|
||||
stack.extend(links[cur] - seen)
|
||||
components.append(sorted(comp))
|
||||
|
||||
connected = sorted((c for c in components if len(c) > 1), key=lambda c: (-len(c), c[0]))
|
||||
lonely = sorted(c[0] for c in components if len(c) == 1)
|
||||
|
||||
groups: list[list[str]] = []
|
||||
for comp in connected:
|
||||
# A component larger than a page is still split — by name, since a
|
||||
# foreign-key cluster has no finer structure to split on — and the cut
|
||||
# keys show as edges to the other page's tables in neither, which the
|
||||
# overview's lifted edges make up for.
|
||||
for i in range(0, len(comp), max_tables):
|
||||
groups.append(comp[i:i + max_tables])
|
||||
for i in range(0, len(lonely), max_tables):
|
||||
groups.append(lonely[i:i + max_tables])
|
||||
|
||||
by_id = {n["id"]: n for n in view["nodes"]}
|
||||
pages: list[dict] = []
|
||||
table_page: dict[str, str] = {}
|
||||
for i, group in enumerate(groups, start=1):
|
||||
pid = group[0] if len(group) == 1 else f"{group[0]}…{group[-1]}"
|
||||
slug = f"tables-{i:02d}"
|
||||
for t in group:
|
||||
table_page[t] = slug
|
||||
keep = set(group) | {n["id"] for n in view["nodes"] if n.get("parent") in set(group)}
|
||||
ir = {"meta": dict(view["meta"]),
|
||||
"nodes": [dict(by_id[n]) for n in sorted(keep)],
|
||||
"edges": [dict(e) for e in view["edges"]
|
||||
if e["source"] in keep and e["target"] in keep]}
|
||||
pages.append({"id": slug, "slug": slug, "title": pid, "scope": slug,
|
||||
"parent": OVERVIEW, "depth": 1, "ir": ir, "options": {},
|
||||
"children": [], "too_wide": False, "tables": True})
|
||||
|
||||
over_nodes = [{"id": p["id"], "kind": "table", "label": p["title"], "parent": None,
|
||||
"attrs": {"group": True, "count": sum(
|
||||
1 for n in p["ir"]["nodes"] if n["kind"] == "table")}}
|
||||
for p in pages]
|
||||
merged: dict[tuple, dict] = {}
|
||||
for e in view["edges"]:
|
||||
a, b = table_page.get(e["source"]), table_page.get(e["target"])
|
||||
if a and b and a != b:
|
||||
k = (a, b, e["kind"])
|
||||
merged.setdefault(k, {"source": a, "target": b, "kind": e["kind"],
|
||||
"attrs": {"weight": 0}})
|
||||
merged[k]["attrs"]["weight"] += 1
|
||||
overview = {"id": OVERVIEW, "slug": OVERVIEW, "title": "Overview", "scope": "",
|
||||
"tables": True, "parent": None,
|
||||
"depth": 0, "ir": {"meta": dict(view["meta"]), "nodes": over_nodes,
|
||||
"edges": list(merged.values())},
|
||||
"options": {}, "children": [p["id"] for p in pages], "too_wide": False}
|
||||
return [overview] + pages
|
||||
|
||||
|
||||
def coverage(view: dict, pages: list[dict]) -> list[str]:
|
||||
"""Node ids of the view that no page accounts for. Empty means nothing lost.
|
||||
|
||||
A node is accounted for when a page shows it — as itself, or inside a box
|
||||
that opens onto a page that does. Externals count when their branch appears
|
||||
as context somewhere.
|
||||
"""
|
||||
shown = set()
|
||||
for p in pages:
|
||||
shown |= {n["id"] for n in p["ir"]["nodes"]}
|
||||
# Schema pages draw tables and their columns; an API's endpoints are the
|
||||
# notebook's walkthrough, not a diagram, so they are not a page's to account for.
|
||||
tables_only = bool(pages) and pages[0].get("tables")
|
||||
missing = []
|
||||
for n in view["nodes"]:
|
||||
if tables_only and n["kind"] not in ("table", "column"):
|
||||
continue
|
||||
nid = n["id"]
|
||||
if nid in shown:
|
||||
continue
|
||||
parts = nid.split(".")
|
||||
if n["kind"] == "external" and any(".".join(parts[:i]) in shown
|
||||
for i in range(1, len(parts) + 1)):
|
||||
continue
|
||||
if n.get("parent") in shown and n["kind"] == "column":
|
||||
continue
|
||||
missing.append(nid)
|
||||
return sorted(missing)
|
||||
|
||||
|
||||
def for_view(ir: dict, view: dict, *, max_depth: int = 6) -> list[dict]:
|
||||
"""The pages a book gets, whatever its source.
|
||||
|
||||
A schema pages by table — from the full IR, since a card needs its columns and
|
||||
the default view drops them — and anything else pages by structure from the
|
||||
view. The same decision `classify` makes about which emitter draws it.
|
||||
"""
|
||||
from .filter import classify
|
||||
|
||||
if classify(view)["emitter"] == "erd":
|
||||
tables = {"meta": ir["meta"],
|
||||
"nodes": [n for n in ir["nodes"] if n["kind"] in ("table", "column")],
|
||||
"edges": [e for e in ir["edges"]
|
||||
if e["kind"] in ("foreign_key", "references")]}
|
||||
if any(n["kind"] == "table" for n in tables["nodes"]):
|
||||
return paginate_tables(tables)
|
||||
return paginate(view, max_depth=max_depth)
|
||||
@@ -2319,6 +2319,453 @@ def _runfiles():
|
||||
_runfiles()
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
print("\n19. pages — a graph too big for one picture, as pictures that each fit")
|
||||
|
||||
|
||||
def _pages():
|
||||
pages_mod = __import__(f"{PKG}.ops.pages", fromlist=["*"])
|
||||
paged = __import__(f"{PKG}.emitters.paged", fromlist=["*"])
|
||||
dot = __import__(f"{PKG}.emitters.dot", fromlist=["*"])
|
||||
from_ops = __import__(f"{PKG}.ops", fromlist=["*"])
|
||||
|
||||
# ops/ decides what goes on a page and must never draw one. Paging is exactly
|
||||
# where the temptation lives — measuring a width wants a renderer — so the
|
||||
# measure is passed in as `fits`, and this holds the line.
|
||||
reaching = []
|
||||
for path in sorted((HERE / "ops").glob("*.py")):
|
||||
for node in ast.walk(ast.parse(path.read_text())):
|
||||
if isinstance(node, ast.ImportFrom) and node.module and node.level \
|
||||
and node.module.split(".")[0] in ("emitters", "book", "cli", "extractors"):
|
||||
reaching.append(f"{path.name}: {node.module}")
|
||||
check("ops/ imports no emitter, book, command line or extractor", not reaching, reaching)
|
||||
|
||||
view = from_ops.overview(py_ex.extract(HERE).to_dict())
|
||||
structure = pages_mod.paginate(view)
|
||||
|
||||
check(
|
||||
"the first page is the overview",
|
||||
structure[0]["id"] == pages_mod.OVERVIEW,
|
||||
f"got {structure[0]['id']}",
|
||||
)
|
||||
check(
|
||||
"every page is a valid IR on its own",
|
||||
not [p["id"] for p in structure if check_ir(p["ir"])],
|
||||
)
|
||||
# The claim pagination is allowed to make: drawn smaller, never drawn less.
|
||||
lost = pages_mod.coverage(view, structure)
|
||||
check("pagination loses no node — each is on a page or inside a box that opens one",
|
||||
not lost, f"{lost[:5]}")
|
||||
ids = {p["id"] for p in structure}
|
||||
check(
|
||||
"every box that opens a page opens one that exists",
|
||||
not [c for p in structure for c in p["children"] if c not in ids],
|
||||
)
|
||||
check(
|
||||
"slugs are unique and safe as file names",
|
||||
len({p["slug"] for p in structure}) == len(structure)
|
||||
and all(re.fullmatch(r"[A-Za-z0-9._-]+", p["slug"]) for p in structure),
|
||||
)
|
||||
check(
|
||||
"an endpoint id becomes a safe slug",
|
||||
pages_mod.slugify("GET /orders/{orderId}") == "GET-orders-orderId",
|
||||
pages_mod.slugify("GET /orders/{orderId}"),
|
||||
)
|
||||
# docgen's own overview used to be a single box reading `docgen`: a page
|
||||
# whose only content is "open this". A lone group is descended into instead.
|
||||
check(
|
||||
"a page is never just one box that opens another page",
|
||||
not [p["id"] for p in structure
|
||||
if len([n for n in p["ir"]["nodes"] if not (n.get("attrs") or {}).get("context")]) == 1
|
||||
and p["children"]],
|
||||
)
|
||||
# Which pages exist must not depend on width — notes are filed by page id,
|
||||
# and a note has to land in the same place in a notebook and on a site.
|
||||
check(
|
||||
"which pages exist does not depend on width",
|
||||
[p["id"] for p in pages_mod.layout(structure, lambda ir, o: False)]
|
||||
== [p["id"] for p in pages_mod.layout(structure, lambda ir, o: True)],
|
||||
)
|
||||
narrow = pages_mod.layout(structure, lambda ir, o: False)
|
||||
check(
|
||||
"a page that fits no way is kept and flagged, never dropped",
|
||||
len(narrow) == len(structure) and all(p["too_wide"] for p in narrow),
|
||||
)
|
||||
|
||||
# -- measured against a real width --------------------------------------
|
||||
if not dot.have_graphviz():
|
||||
skip("pages fit their width", "Graphviz is not installed")
|
||||
else:
|
||||
lucid_style = Style.load("lucid")
|
||||
fits, measured = paged.fits_for(lucid_style, 800)
|
||||
laid = pages_mod.layout(structure, fits)
|
||||
over = []
|
||||
for p in laid:
|
||||
px = paged.svg_width_px(paged.render(p, lucid_style, 800))
|
||||
if px > 800 and not p["too_wide"]:
|
||||
over.append(f"{p['id']} {px:.0f}px")
|
||||
check("no page is wider than its budget unless it says so", not over, "; ".join(over))
|
||||
# Links are the output's, never the IR's: a site passes them, and then a
|
||||
# relative source path — a dead link on a web page — must not survive.
|
||||
page = next(p for p in laid if p["children"])
|
||||
links = paged.links_for(laid, page, lambda t: f"{t['slug']}.html")
|
||||
svg = paged.render(page, lucid_style, 800, links=links)
|
||||
check(
|
||||
"a box that opens a page links to it",
|
||||
all(f'href="{url}"' in svg for url in links.values()) and links,
|
||||
f"{len(links)} links",
|
||||
)
|
||||
check(
|
||||
"an output that manages links drops the source-file hrefs",
|
||||
".py" not in "".join(re.findall(r'href="([^"]+)"', svg)),
|
||||
)
|
||||
|
||||
# Schemas page by table, and a card grows downward rather than sideways.
|
||||
shop = db_ex.extract(HERE / "fixtures" / "shop.json").to_dict()
|
||||
tables = pages_mod.for_view(shop, from_ops.overview(shop))
|
||||
check(
|
||||
"a schema pages by connected tables, cards with their columns",
|
||||
tables[0]["id"] == "overview" and tables[1].get("tables")
|
||||
and any(n["kind"] == "column" for n in tables[1]["ir"]["nodes"]),
|
||||
)
|
||||
many = {"models": {f"t{i:02d}": {"fields": {"id": {"type": "int", "pk": True}}}
|
||||
for i in range(12)}}
|
||||
wide_ir = db_ex.from_schema_dict(many, root="x").to_dict()
|
||||
svg = erd_mod.emit(wide_ir, lucid, width=800)
|
||||
check(
|
||||
"an ERD capped to a width stays inside it",
|
||||
paged.svg_width_px(svg) <= 800,
|
||||
f"{paged.svg_width_px(svg):.0f}px",
|
||||
)
|
||||
|
||||
|
||||
_pages()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
print("\n20. notes, figures and pages — the hand-written part, and where it lands")
|
||||
|
||||
|
||||
def _documents():
|
||||
doc_mod = __import__(f"{PKG}.book.document", fromlist=["*"])
|
||||
notes_mod = __import__(f"{PKG}.book.notes", fromlist=["*"])
|
||||
build_mod = __import__(f"{PKG}.book.build", fromlist=["*"])
|
||||
checks_mod = __import__(f"{PKG}.book.checks", fromlist=["*"])
|
||||
md = __import__(f"{PKG}.emitters.markdown", fromlist=["*"])
|
||||
dot = __import__(f"{PKG}.emitters.dot", fromlist=["*"])
|
||||
import io as _io3
|
||||
import contextlib as _ctx3
|
||||
|
||||
base = Path(tmp.name) / "documents"
|
||||
src = base / "src" / "shop"
|
||||
for sub in ("orders", "billing"):
|
||||
(src / sub).mkdir(parents=True, exist_ok=True)
|
||||
(src / sub / "__init__.py").write_text(f'"""{sub}."""\n')
|
||||
(src / sub / "models.py").write_text(f'"""{sub} models."""\nfrom shop.{sub} import x\n')
|
||||
(src / "__init__.py").write_text('"""shop."""\n')
|
||||
notes = base / "notes"
|
||||
|
||||
# -- notes ---------------------------------------------------------------
|
||||
stubbed = notes_mod.Notes(notes).scaffold({"app": [("overview", "page Overview")]})
|
||||
check("scaffold writes a stub per spot", (notes / "app" / "overview.md") in stubbed)
|
||||
check("a stub renders nothing", notes_mod.body((notes / "app" / "overview.md").read_text()) is None)
|
||||
(notes / "app" / "overview.md").write_text(
|
||||
(notes / "app" / "overview.md").read_text() + "## Written by hand\n\nKept.\n")
|
||||
again = notes_mod.Notes(notes).scaffold({"app": [("overview", "page Overview")]})
|
||||
check(
|
||||
"scaffold never overwrites a note",
|
||||
(notes / "app" / "overview.md") not in again
|
||||
and "Written by hand" in (notes / "app" / "overview.md").read_text(),
|
||||
)
|
||||
(notes / "img").mkdir(exist_ok=True)
|
||||
(notes / "img" / "dot.svg").write_text('<svg xmlns="http://www.w3.org/2000/svg"/>')
|
||||
(notes / "app" / "overview.md").write_text(
|
||||
(notes / "app" / "overview.md").read_text() + "\n\n")
|
||||
(notes / "app" / "no-such-page.md").write_text("Filed under a page that is gone.\n")
|
||||
|
||||
# A note is text: raw HTML is shown, not run, and a link cannot break out of
|
||||
# its attribute or run script.
|
||||
html = md.to_html('<script>x</script> [a](b"onclick=1) [c](javascript:alert(1))')
|
||||
check(
|
||||
"a note's HTML is escaped and its links are attribute-safe",
|
||||
# The quote stays inside the attribute as " — asserted on the exact
|
||||
# attribute, since stripping the entity first (as this check once did)
|
||||
# makes the attribute's own closing quote look like a break-out.
|
||||
"<script>" not in html and 'href="b"onclick=1"' in html
|
||||
and not re.search(r'"\s*onclick=', html) and 'href="#"' in html,
|
||||
html,
|
||||
)
|
||||
|
||||
# -- a one-book document, built inline -------------------------------------
|
||||
out = base / "book"
|
||||
book = build_mod.run("python", src, out, slug="app", notes=notes, quiet=True)
|
||||
ledger = json.loads((out / "document.json").read_text())
|
||||
check(
|
||||
"a note filed under a vanished page is reported, not dropped",
|
||||
any("no-such-page" in d for d in ledger["notes"]["drift"]),
|
||||
ledger["notes"]["drift"],
|
||||
)
|
||||
r = checks_mod.Report()
|
||||
with _ctx3.redirect_stdout(_io3.StringIO()):
|
||||
checks_mod.generated(checks_mod.Loaded(out), r)
|
||||
check("...and fails the book's own checks", "no note is filed under a page or step that is gone" in r.failed)
|
||||
|
||||
nb = json.loads((out / "notebook.ipynb").read_text())
|
||||
cells = ["".join(c["source"]) for c in nb["cells"]]
|
||||
note_at = next(i for i, t in enumerate(cells) if "Written by hand" in t)
|
||||
check(
|
||||
"a note lands directly above its page",
|
||||
'<a name="page-app-overview">' in cells[note_at + 1],
|
||||
cells[note_at + 1][:60],
|
||||
)
|
||||
check(
|
||||
"a note's local image travels with it, as an attachment",
|
||||
"attachment:dot.svg" in cells[note_at] and nb["cells"][note_at].get("attachments"),
|
||||
)
|
||||
figures = [c for c in nb["cells"] if '<a name="page-' in "".join(c["source"])]
|
||||
check(
|
||||
"every page is a figure in the notebook",
|
||||
len(figures) == len(book.pages), f"{len(figures)} figures, {len(book.pages)} pages",
|
||||
)
|
||||
if dot.have_graphviz():
|
||||
check(
|
||||
"a figure carries its SVG, so it renders without running anything",
|
||||
all("image/svg+xml" in next(iter((c.get("attachments") or {"": {}}).values()))
|
||||
for c in figures),
|
||||
)
|
||||
else:
|
||||
skip("figures carry SVG", "Graphviz is not installed")
|
||||
check("the page index links every page", all(
|
||||
f"(#page-app-{p['slug']})" in "".join(cells) for p in book.pages))
|
||||
ran, failure = checks_mod._run_cells(nb["cells"], out)
|
||||
check(f"the notebook still runs ({ran} code cells)", failure is None, failure or "")
|
||||
|
||||
# Two builds, same bytes — attachments included.
|
||||
first = (out / "notebook.ipynb").read_bytes()
|
||||
build_mod.run("python", src, out, slug="app", notes=notes, quiet=True)
|
||||
check("the same sources and notes build the same notebook", first == (out / "notebook.ipynb").read_bytes())
|
||||
|
||||
# -- the site: drill-down --------------------------------------------------
|
||||
site = out / "site"
|
||||
page_files = sorted((site / "app").glob("*.html"))
|
||||
broken = []
|
||||
for f in page_files:
|
||||
for href in re.findall(r'href="([^"#?]+)', f.read_text()):
|
||||
if not href.startswith(("http:", "https:")) and not (f.parent / href).exists():
|
||||
broken.append(f"{f.name} -> {href}")
|
||||
check("every link on every site page opens something", not broken, broken[:5])
|
||||
overview_html = (site / "app" / "overview.html").read_text()
|
||||
check(
|
||||
"a page's diagram is inline, so its boxes can be clicked",
|
||||
"<svg" in overview_html and "<?xml" not in overview_html and "<img" not in overview_html.split('class="fig"')[1][:200]
|
||||
if dot.have_graphviz() else True,
|
||||
)
|
||||
child = next((p for p in book.pages if p["parent"]), None)
|
||||
if child is not None:
|
||||
child_html = (site / "app" / f"{child['slug']}.html").read_text()
|
||||
check(
|
||||
"a page links back up through its breadcrumbs",
|
||||
'href="overview.html"' in child_html.split('class="crumbs"')[1].split("</div>")[0],
|
||||
)
|
||||
check("a page has a fit / 1:1 control", 'data-size="actual"' in overview_html)
|
||||
check("the site renders the note above the diagram",
|
||||
overview_html.index("Written by hand") < overview_html.index('class="figbar"'))
|
||||
|
||||
# Manifests: a rebuild removes what the last one wrote, and nothing else.
|
||||
project_out = base / "project-site"
|
||||
doc = doc_mod.Document(
|
||||
name="p", title="P", build=base / "p-build", notes=None,
|
||||
parts=[doc_mod.Part(name="app", kind="python", source=src, out=base / "p-build" / "app")],
|
||||
outputs=[doc_mod.Output("site", project_out)])
|
||||
doc_mod.build(doc)
|
||||
(project_out / "app" / "stale.html").write_text("left by a previous build")
|
||||
manifest = json.loads((project_out / ".docgen-output.json").read_text())
|
||||
manifest["written"].append("app/stale.html")
|
||||
(project_out / ".docgen-output.json").write_text(json.dumps(manifest))
|
||||
(project_out / "hand-written.md").write_text("mine")
|
||||
doc_mod.build(doc)
|
||||
check("a rebuild removes a page the last build wrote and this one did not",
|
||||
not (project_out / "app" / "stale.html").exists())
|
||||
check("...and never a file it did not write", (project_out / "hand-written.md").exists())
|
||||
|
||||
# One book that cannot be built must not cost the document — and the document
|
||||
# has to say it is short, where a reader will see it, not only in a terminal.
|
||||
(base / "broken.har").write_text("not a recording")
|
||||
partial_site = base / "partial-site"
|
||||
partial = doc_mod.Document(
|
||||
name="partial", title="Partial", build=base / "partial-build", notes=None,
|
||||
parts=[doc_mod.Part(name="bad", kind="usage", source=base / "broken.har",
|
||||
out=base / "partial-build" / "bad"),
|
||||
doc_mod.Part(name="app", kind="python", source=src,
|
||||
out=base / "partial-build" / "app")],
|
||||
outputs=[doc_mod.Output("site", partial_site),
|
||||
doc_mod.Output("notebook", base / "partial.ipynb")])
|
||||
doc_mod.build(partial)
|
||||
check("a book that fails does not stop the others",
|
||||
[b.slug for b in partial.books] == ["app"] and "bad" in partial.failed)
|
||||
check("the site says the document is incomplete",
|
||||
"This document is incomplete" in (partial_site / "index.html").read_text())
|
||||
check("...and so does the notebook",
|
||||
"This document is incomplete" in (base / "partial.ipynb").read_text())
|
||||
|
||||
|
||||
_documents()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
print("\n21. project files — one document, several sources, the shape a model writes")
|
||||
|
||||
|
||||
def _projects():
|
||||
cfg = __import__(f"{PKG}.book.config", fromlist=["*"])
|
||||
|
||||
base = Path(tmp.name) / "projects"
|
||||
(base / "src").mkdir(parents=True, exist_ok=True)
|
||||
(base / "src" / "a.py").write_text('"""A."""\n')
|
||||
(base / "shop.json").write_text((HERE / "fixtures" / "shop.json").read_text())
|
||||
|
||||
def load(text, name="docgen.toml"):
|
||||
path = base / name
|
||||
path.write_text(text)
|
||||
try:
|
||||
return cfg.load(path), []
|
||||
except cfg.ConfigError as e:
|
||||
return None, e.problems
|
||||
|
||||
project, problems = load(
|
||||
'[project]\nname = "p"\n\n[extract.python]\nroot = "src"\n\n'
|
||||
'[extract.db]\nschema = "shop.json"\n\n[ops]\ndrop_stdlib = true\n\n'
|
||||
'[emit.notebook]\noutput = "out/nb"\n\n[emit.site]\noutput = "out/site"\nwidth = 700\n')
|
||||
check("a project file loads", isinstance(project, cfg.ProjectFile), problems)
|
||||
if project is None:
|
||||
return
|
||||
doc = project.document
|
||||
check(
|
||||
"each [extract.*] is a book, each [emit.*] an output of all of them",
|
||||
[p.name for p in doc.parts] == ["python", "db"]
|
||||
and sorted(o.emitter for o in doc.outputs) == ["notebook", "site"],
|
||||
)
|
||||
check("[ops] applies to every book", all(p.ops.get("drop_stdlib") for p in doc.parts))
|
||||
check("an output's width is kept", next(o for o in doc.outputs if o.emitter == "site").width == 700)
|
||||
books_file, _ = load('[[book]]\nname = "a"\nroot = "src"\n', "books.toml")
|
||||
check("a [[book]] list still loads as it did", isinstance(books_file, cfg.RunFile))
|
||||
|
||||
_, both = load('[project]\nname = "p"\n[[book]]\nname = "a"\nroot = "src"\n', "both.toml")
|
||||
check("a file with both shapes is refused", any("both [project] and [[book]]" in p for p in both), both)
|
||||
|
||||
# The keys a model reached for in a real dry run, each refused with what to
|
||||
# do instead — and the database URL never repeated back, since it is a
|
||||
# credential.
|
||||
_, refused = load(
|
||||
'[project]\nname = "p"\n\n'
|
||||
'[extract.db]\nurl = "postgresql://user:hunter2@localhost:5432/app"\n\n'
|
||||
'[extract.code]\nroot = "src"\next = [".sh", ".yaml", ".sql"]\n\n'
|
||||
'[extract.seed]\nroot = "src"\n\n[emit.site]\noutput = "out"\n', "refused.toml")
|
||||
text = "\n".join(refused)
|
||||
check("a database url is refused, pointing at modelgen from-db",
|
||||
"never connects" in text and "modelgen from-db" in text, refused)
|
||||
check("...and the credential is not repeated in the error", "hunter2" not in text)
|
||||
check("an extension with no grammar is refused, naming what exists",
|
||||
"no grammar for .sh, .yaml, .sql" in text and ".cs" in text)
|
||||
check("an unknown reader is refused, naming the readers", "no reader named 'seed'" in text)
|
||||
_, inside = load('[project]\nname = "p"\n[extract.python]\nroot = "src"\n'
|
||||
'[emit.site]\noutput = "src/site"\n', "inside.toml")
|
||||
check("an output inside a tree it reads is refused", any("inside the tree" in p for p in inside), inside)
|
||||
|
||||
example = cfg.load(HERE / "docgen.project.example.toml")
|
||||
check("the shipped project example loads",
|
||||
[p.name for p in example.document.parts] == ["extractors", "schema", "api"])
|
||||
|
||||
|
||||
_projects()
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
print("\n22. the instructions for a model say what docgen accepts")
|
||||
|
||||
|
||||
def _contract():
|
||||
"""docs/models.md is what a model is told. It is only useful while true.
|
||||
|
||||
A model follows it literally: a key it names that the loader refuses is a
|
||||
refused file, and a key the loader accepts that it never mentions is a
|
||||
capability no model will use. So it is checked against the loaders
|
||||
themselves, the way explode's contract is one copy printed by the script
|
||||
that parses the reply.
|
||||
"""
|
||||
cfg = __import__(f"{PKG}.book.config", fromlist=["*"])
|
||||
code_ex = __import__(f"{PKG}.extractors.code", fromlist=["*"])
|
||||
build_mod = __import__(f"{PKG}.book.build", fromlist=["*"])
|
||||
|
||||
doc_path = HERE / "docs" / "models.md"
|
||||
check("docs/models.md exists", doc_path.is_file())
|
||||
if not doc_path.is_file():
|
||||
return
|
||||
text = doc_path.read_text()
|
||||
|
||||
def named(word):
|
||||
return f"`{word}`" in text or f'`"{word}"`' in text
|
||||
|
||||
keys = (cfg.TOP_KEYS | cfg.DEFAULT_KEYS | cfg.BOOK_KEYS | cfg.PROJECT_KEYS
|
||||
| cfg.OPS_KEYS | cfg.EMIT_KEYS | set().union(*cfg.EXTRACT_KEYS.values()))
|
||||
check("every key a loader accepts is named", not sorted(k for k in keys if not named(k)),
|
||||
sorted(k for k in keys if not named(k)))
|
||||
check("every reader is named", all(named(r) for r in cfg.EXTRACT_KEYS))
|
||||
check("every output is named", all(named(e) for e in cfg.EMITTERS))
|
||||
# Read off the one line that lists them, not the whole file: a bare scan for
|
||||
# quoted dot-words also caught the default build folder, `".docgen"`.
|
||||
ext_line = next((ln for ln in text.splitlines() if ln.startswith("- `ext`:")), "")
|
||||
listed = set(re.findall(r'"(\.[a-z]+)"', ext_line))
|
||||
check("the ext list names exactly the extensions code reads",
|
||||
listed == set(code_ex.LANGUAGES), f"doc {sorted(listed)} vs code {sorted(code_ex.LANGUAGES)}")
|
||||
check("every step spot is named",
|
||||
all(named(f"step-{sid}") for sid, _ in build_mod.STEP_LABELS))
|
||||
|
||||
# Every example a model is shown must be one that loads. The shipped
|
||||
# examples use a fixed set of paths, created here as stand-ins.
|
||||
proj = Path(tmp.name) / "contract-project"
|
||||
for d in ("src", "web/src"):
|
||||
(proj / d).mkdir(parents=True, exist_ok=True)
|
||||
for f in ("doc/contracts/openapi.yaml", "schema/schema.json", "recordings/session.har"):
|
||||
(proj / f).parent.mkdir(parents=True, exist_ok=True)
|
||||
(proj / f).write_text("{}")
|
||||
blocks = re.findall(r"```toml\n(.*?)```", text, re.S)
|
||||
failures = []
|
||||
for i, block in enumerate(blocks):
|
||||
path = proj / f"example-{i}.toml"
|
||||
path.write_text(block)
|
||||
try:
|
||||
cfg.load(path)
|
||||
except cfg.ConfigError as e:
|
||||
failures.append(f"example {i}: {e.problems[:2]}")
|
||||
check(f"every toml example in it loads ({len(blocks)})", blocks and not failures, failures)
|
||||
|
||||
# The error table is what a model uses to repair a refused file, so every
|
||||
# message it quotes has to be one the code can actually produce.
|
||||
source = "".join((HERE / "book" / f).read_text() for f in ("config.py", "notes.py"))
|
||||
table = text.split("## When the build refuses your file")[-1]
|
||||
# Every backticked message in the first column — a cell can hold two.
|
||||
quoted = [m for row in table.splitlines() if row.startswith("| `")
|
||||
for m in re.findall(r"`([^`]+)`", row.split("|")[1])]
|
||||
unknown = []
|
||||
for message in quoted:
|
||||
fragments = [f.strip() for f in re.split(r"'[^']*'|…|\.sh", message) if f.strip()]
|
||||
if not all(frag in source for frag in fragments):
|
||||
unknown.append(message)
|
||||
check(f"every error in its repair table is one the loaders produce ({len(quoted)})",
|
||||
quoted and not unknown, unknown)
|
||||
|
||||
env = dict(__import__("os").environ, PYTHONPATH=str(HERE.parent))
|
||||
printed = _sub.run([sys.executable, "-m", PKG, "contract"], capture_output=True,
|
||||
text=True, env=env)
|
||||
check("`contract` prints it verbatim", printed.returncode == 0 and printed.stdout == text)
|
||||
|
||||
|
||||
_contract()
|
||||
|
||||
|
||||
print(f"{len(PASS)} passed, {len(FAIL)} failed, {len(SKIP)} skipped")
|
||||
if FAIL:
|
||||
print("\nfailed:")
|
||||
|
||||
Reference in New Issue
Block a user