Skip to content

Assembly pipeline

MarkdownCollection.concatenate() first creates one normalized in-memory document. The public concatenate(collection, output) function (module scribpy.core.assembly.concatenate) then threads an AssembledDocument through pure transforms in a fixed order and writes the result to disk.

uml diagram

Why this design

apply_transforms() (module scribpy.core.assembly.pipeline) implements a functional pipeline: each TransformFn is Callable[[AssembledDocument], AssembledDocument], and apply_transforms simply folds the tuple of transforms over the initial document, left to right. Each transform is a free function (or a small closure over the manifest's BuildSettings) with no dependency on the others — this makes every step independently unit testable, and makes the pipeline order itself explicit and readable in one place (concatenate.py), rather than scattered across conditionals. Adding a step means writing a new pure function and inserting it into the tuple; it does not require touching apply_transforms() or any other transform.

AssembledDocument follows the same immutable-prototype pattern as MarkdownDocument: with_content() returns a new instance. This means a bug in one transform cannot leak mutated state into a transform that runs before it, and it makes each transform trivial to test with a hand-built AssembledDocument and no filesystem.

The AssembledDocument carrier

Field Type Role
content str Markdown source text currently being transformed
source_root Path collection root, used to resolve relative image/link targets
output Path destination file path for the final assembled Markdown

Pipeline steps

Order Transform Conditional on Role
1 _number_headings (mkforge.renumber_markdown_headings) build.heading_numbering.enabled Numbers assembled headings via MkForge, start_level=2
2 rewrite_internal_links always Replaces [label](file.md) with [label](#slug)
3 generate_toc build.toc == True Inserts a Markdown list after the H1, up to build.toc_depth levels deep (default 3, i.e. H2–H4)
4 render_diagram_blocks always Renders fenced ```plantuml / ```mermaid blocks to PNG via the configured backends
5 collect_images always Copies local images into output.parent/assets/ and rewrites their targets

Step 3 runs after step 2 so that its slugs are computed the same way the link rewriter computed them. When heading numbering (step 1) is active, the link rewriter recomputes its slug map from the already-numbered content via build_numbered_file_slug_map(), instead of the plain build_file_slug_map(); this keeps the TOC and the rewritten links pointing at the exact same anchors as the final numbered headings.

Sequence

uml diagram

Diagram rendering details

render_diagram_blocks() (module scribpy.core.diagram_blocks) is the single shared entry point used by both the assembly pipeline and the MkDocs exporter — see Diagram renderers for the backend architecture. Each rendered PNG is named after the SHA-256 hex digest of its diagram source (png_filename()), written under output.parent/assets/generated/, and referenced with ![diagram](assets/generated/<hash>.png). Identical diagram sources therefore share one generated file with no re-render, and unrelated diagrams never collide.

Example

from pathlib import Path
from scribpy.core.markdown_collection import MarkdownCollection
from scribpy.core.assembly.concatenate import concatenate

collection = MarkdownCollection.from_tree(Path("docs-source"))
concatenate(collection, Path("build/output.md"))
# build/output.md now holds the assembled Markdown;
# build/assets/ and build/assets/generated/ hold copied images and PNGs.

concatenate() raises PlantUmlRenderError or MermaidRenderError if a configured backend fails, and NotImplementedError if a local PlantUML backend is selected (see Diagram renderers).