Files
soleprint/soleprint/station/tools/docgen/export/doc.py
2026-09-12 06:42:49 -03:00

72 lines
2.6 KiB
Python

"""
A document, before it is any particular kind of file.
Three block types and nothing else. A `Doc` does not know what a notebook is,
does not know what HTML is, and above all does not know what a graph is — the
emitters know that, and there is one per output format.
doc = Doc(title="Calling the API")
doc.md("## Parameters")
doc.code("BASE_URL = 'https://example.invalid'")
doc.md("Run the cell above, then:")
doc.code("print(call('GET', '/health'))", live_only=True)
The reason the model is this thin: the same content has to come out as three
notebook variants that differ only in what executes, and as HTML later. Any
structure richer than "ordered blocks, each either prose or code" starts
encoding one format's assumptions into the shared layer, and then the variants
drift apart because each is really its own document.
`live_only` is the one concession. A block marked with it is dropped from the
vanilla and executable variants and kept in the live one — health checks,
timings, the things that are only meaningful against a running service.
"""
from dataclasses import dataclass, field
@dataclass
class Block:
"""One cell's worth of content. `kind` is 'md' or 'code'."""
kind: str
text: str
live_only: bool = False
def lines(self) -> list[str]:
"""Source split the way notebook JSON wants it: newline kept, last line bare.
Not `text.splitlines()` — that drops the newlines, and a notebook whose
source lines have no `\\n` renders every cell as a single run-on line.
"""
parts = self.text.split("\n")
return [p + "\n" for p in parts[:-1]] + ([parts[-1]] if parts[-1] else [])
@dataclass
class Doc:
"""An ordered list of blocks, plus a title."""
title: str = ""
blocks: list[Block] = field(default_factory=list)
def md(self, text: str, live_only: bool = False) -> "Doc":
self.blocks.append(Block("md", text.strip("\n"), live_only))
return self
def code(self, text: str, live_only: bool = False) -> "Doc":
self.blocks.append(Block("code", text.strip("\n"), live_only))
return self
def for_variant(self, variant: str) -> list[Block]:
"""The blocks that belong in one variant.
vanilla / executable carry the same blocks — they differ in whether the
emitter marks the code as having run, not in what is written. Keeping
them one document is what stops the two from drifting into separate
hand-maintained files, which is how this usually goes wrong.
"""
if variant == "live":
return list(self.blocks)
return [b for b in self.blocks if not b.live_only]