Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Commits that only reformat code, hidden from git blame:
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# (GitHub applies this file on its own.)

# ruff format of the whole Python code base
3397cd98c04a00b9f646108301d03a0e00c8255d
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
python-version: "3.14"
- run: python -m pip install ruff
- run: ruff check .
- run: ruff format --check .

test:
name: Test (${{ matrix.os }}, Python ${{ matrix.python-version }})
Expand Down Expand Up @@ -114,10 +115,23 @@ jobs:
- run: python -m build
- run: python -m twine check dist/*

vscode:
name: VS Code extension
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: editors/vscode
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: npm ci --ignore-scripts # the runner's own Node
- run: npm run lint
- run: npm test

gate:
name: CI gate
if: always()
needs: [lint, test, build]
needs: [lint, test, build, vscode]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ build/
.py-doc/
.structurizr/
.venv/
node_modules/
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ Pandoc writes every output, and it runs in a container with every other tool, so
| `py-doc init` | Start a project: `py-doc.yml`, a first document, and optionally a C4 model, templates and a site |
| `py-doc new` | Start a legacy document, or number an existing Markdown file, from `doc-numbers.yml` at the repository root |
| `py-doc build` | Build every document described in `py-doc.yml` (generators, lint, documents, site), or one legacy `createdocs` file with `--legacy` |
| `py-doc docx` | Convert Markdown to Word |
| `py-doc slides` | Convert Markdown to PowerPoint |
| `py-doc docx` | Convert Markdown to Word, or with `--watch` rebuild on every save and serve a live HTML preview |
| `py-doc slides` | Convert Markdown to PowerPoint, with the same `--watch` live preview |
| `py-doc lint` | Check writing style, glossary terms, acronyms, structure, links and project conventions; text, CSV, GitHub, JSON or SARIF |
| `py-doc check` | Check a project without building it: `py-doc.yml`, sources, templates, the site and lint |
| `py-doc generate` | Write the Markdown that `content.generators` makes: glossaries, indexes, traceability, API references |
| `py-doc stats` | Count the words, headings, tables, pictures and links of each document |
| `py-doc diff` | Compare two versions of documents section by section, or against a Git revision |
| `py-doc c4` | Check, export, draw and edit Structurizr workspaces (`c4 up` opens the live editor) |
| `py-doc site` | Make and build a web site of the documents with MkDocs or Astro + Starlight |
| `py-doc poster` | Build self-contained HTML posters from Markdown, and print them to PDF |
| `py-doc poster` | Build self-contained HTML posters from Markdown, and print them to PDF; `--watch` reloads them on every save |
| `py-doc import` | Convert a Word document or a PowerPoint deck to Markdown |
| `py-doc template` | Check and list the styles of a Word or PowerPoint template |
| `py-doc tools`, `doctor`, `setup` | Build and check the tool container, and install Docker Engine |
Expand Down Expand Up @@ -59,6 +59,8 @@ py-doc c4 up architecture/workspace.dsl # arrange the C4 views in Structuriz
py-doc site build # site/build/index.html
```

While writing, `py-doc docx guide.md --watch` (or `slides`, or `poster`) builds on every save and keeps a live preview open in the browser, with every diagram drawn. The [VS Code extension](editors/vscode/README.md) runs it for the open file and shows the preview beside the editor, and also previews py-doc Markdown instantly as you type.

## Documentation

py-doc documents itself with py-doc. The sources are in [`docs/`](docs), and the [`py-doc.yml`](py-doc.yml) at the root of this repository builds them: `py-doc build` for Word, HTML and PDF, `py-doc site build` for the web site.
Expand All @@ -79,8 +81,8 @@ src/
│ ├── new.py # py-doc new
│ ├── cli.py # py-doc docx (and the old py-doc2docx)
│ ├── slides_cli.py # py-doc slides (and the old py-doc2slides)
│ ├── _cli.py # options, output paths and batch conversion shared by both
│ ├── convert.py # DocxConverter and SlidesConverter, one file through pandoc
│ ├── _cli.py # options, output paths, batch conversion and --watch shared by both
│ ├── convert.py # DocxConverter and SlidesConverter, one file through pandoc, and its HTML preview twin
│ ├── config.py # py-doc.yml model and validation
│ ├── build.py # joins a document's sources and renders it
│ ├── directives.py # includes, conditions and variables
Expand All @@ -97,11 +99,12 @@ src/
│ ├── analysis/ # py-doc stats, check and diff
│ ├── migrate.py # createdocs to py-doc.yml
│ ├── report.py # the JSON build report
│ └── poster*.py # py-doc poster: Markdown to an HTML poster, its blocks, settings, themes, fonts and --watch
│ ├── poster*.py # py-doc poster: Markdown to an HTML poster, its blocks, settings, themes and fonts
│ └── watch.py # the --watch loop and preview server of poster, docx and slides
├── pydoc2docx/ # old import names, kept as aliases
├── pydoc2slides/
└── pydoclint/
editors/ # editor integrations: VS Code preview extension for documents, slides and posters
editors/vscode/ # VS Code extension: instant in-editor preview, and the --watch live preview beside the editor
docs/ # py-doc's own user guide, requirements and architecture
examples/ # a project that uses every feature
tests/
Expand All @@ -114,6 +117,7 @@ pip install -e ".[dev]"
python -m pytest
ruff check .
py-doc lint docs --strict
cd editors/vscode && npm ci && npm run lint && npm test # the VS Code extension
```

CI runs on Linux and Windows with Python 3.14, the newest supported. Python 3.10, the oldest supported, is not tested in CI. macOS is not supported.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/00_introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ keywords: [py-doc, architecture, C4]

This document describes how py-doc meets its [requirements](../requirements/00_introduction.md). It covers what py-doc is made of, how a document travels from Markdown to each output, where the tools run, and why it is built this way. It is for maintainers and contributors.

py-doc writes the C4 chapters from the Structurizr model in `docs/architecture/workspace.dsl`, the way any py-doc project documents its architecture. They cover the context, containers, components, relationships, deployment, the dynamic view of a build, and the element catalogue. Change the model, not those chapters. `py-doc c4 up docs/architecture/workspace.dsl` opens it in Structurizr to arrange its views.
py-doc writes the C4 chapters from the Structurizr model in `docs/architecture/workspace.dsl`, the way any py-doc project documents its architecture. They cover the context, containers, components, relationships, deployment, the dynamic views of a build and of a live preview, and the element catalogue. Change the model, not those chapters. `py-doc c4 up docs/architecture/workspace.dsl` opens it in Structurizr to arrange its views.

## Goals

Expand Down
11 changes: 10 additions & 1 deletion docs/architecture/02_pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,17 @@ flowchart LR
- **Generators** (`generators/`): each entry of `content.generators` returns Markdown, written only when it changes. The built-in types are in `generators/builtin.py`, and others are found through the `py_doc.generators` entry point group, so the core depends on none of their tools. They run before anything reads the sources, because the files they write are sources.
- **Lint** (`lint/`): with `build.lint` or `--lint`, the sources of the documents being built are linted before anything is written, and the issues can go to a SARIF, JSON or CSV file for CI (`lint/formats.py`).
- **Documents and bundles**: a bundle is a document whose sources are those of its documents, so it takes the same path.
- **Posters** (`poster*.py`): every poster under `posters:`, one self-contained HTML page each, with its PDF when it asks for one. The settings come in layers (`poster_settings.py`): `poster:`, the front matter, the poster's entry, then the command line. The page is built in Python, with diagrams through the same cache as the documents. A script in the page balances the columns by measured height and fits the poster to its paper. The PDF is that page printed by the container's Chromium (`diagrams/tools/poster_pdf.js`), from a copy with the web fonts inside, as the container has no network. `py-doc poster --watch` (`poster_watch.py`) runs the same build on every save and serves the page with a script that reloads it.
- **Posters** (`poster*.py`): every poster under `posters:`, one self-contained HTML page each, with its PDF when it asks for one. The settings come in layers (`poster_settings.py`): `poster:`, the front matter, the poster's entry, then the command line. The page is built in Python, with diagrams through the same cache as the documents. A script in the page balances the columns by measured height and fits the poster to its paper. The PDF is that page printed by the container's Chromium (`diagrams/tools/poster_pdf.js`), from a copy with the web fonts inside, as the container has no network. `py-doc poster --watch` (`watch.py`) runs the same build on every save and serves the page with a script that reloads it.
- **Site** (`site/`): with `build.site` or `--site`, the pages are written and the site tool runs. The posters `site.posters` names are built into it as pages of their own.
- **Check outputs**: every file the build wrote is there and not empty.

`py-doc check`, `py-doc stats` and `py-doc diff` (`analysis/`) read the same configuration and the same document model, and build nothing. The diff splits each version into sections by its top-level headings. It matches them by heading path, then by text, so a renamed or moved section is not mistaken for one removed and another added.

## Live previews

`--watch` on `py-doc poster`, `py-doc docx` and `py-doc slides` runs one loop (`watch.py`). It builds, then serves the pages on 127.0.0.1 from a thread of its own. Every half second it looks at the modification times of the source files: the Markdown, includes, pictures, diagram sources and templates below the folders of the sources. The pages it wrote and hidden folders are not watched. A change builds again. Each page is served with a small script that asks `/__status` whether a newer build is there, and reloads when it is. The script is added as the page is served, so the files are the same as without `--watch`. A build that fails keeps the last good page and shows its error over it.

A poster is already a web page. A Word document or deck is not, so `docx` and `slides` (`_cli.py`) also write an *HTML twin* of each file (`convert.py`, `convert_twin`). The twin is the same Markdown through the same pipeline to pandoc's HTML. It adds `preview.css` after `html.css` to show page breaks, speaker notes and columns. The twin of a deck also runs `slides-preview.lua`, which puts each slide in a box. A box starts where pandoc's PowerPoint writer starts a slide: at headings up to the split level and at page breaks. The twins go to `.py-doc/preview/` below the input's folder and are served by their path. No file of the user's is overwritten, and two `readme.md` in different folders stay apart. When Word or PowerPoint has the output open, the engine raises `OutputLocked`; the watch loop warns and still writes the twin.

The VS Code extension (`editors/vscode`) is outside the Python package. Its instant preview is a markdown-it plugin in the editor and runs no tool. Its live preview (`src/watch.js`) runs `py-doc <command> --watch --no-open` as a child process, with the output in an output channel. py-doc picks a free port, and the extension reads the address from the `serving` line it prints, then opens it in VS Code's Simple Browser. Stopping the preview ends the whole process tree. The extension and the instant preview choose the command with one function (`src/detect.js`), from the front matter and the blocks only a deck has.

7 changes: 7 additions & 0 deletions docs/architecture/05_decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ Each decision records the context, the choice and its consequences.
- **Context**: lint findings need exact lines and source text, and must skip code, links and front matter reliably.
- **Decision**: the linter parses with markdown-it-py into py-doc's own document model.
- **Consequences**: precise, fast checks without the container. Two parsers can disagree on edge cases, so the anchor rules are tested against pandoc's own identifiers.

## AD-08 Previews are served, not written beside the outputs

- **Context**: authors want to see a document while they write it. Word and PowerPoint have no live view, and a document open in Word cannot be written. The in-editor Markdown preview cannot draw most diagrams.
- **Decision**: `--watch` builds an HTML twin of each document with the same pipeline, and keeps it in `.py-doc/preview`. It serves the twin with a reload script, added as the page is served. The VS Code extension runs that command; its own instant preview is an approximation for typing.
- **Consequences**: the preview shows every diagram exactly as the build draws it, and a document open in Word does not stop it. It is one HTML page: slides are boxes and page breaks are lines, but Word's page layout is not shown. A first build waits for the tool container.

24 changes: 22 additions & 2 deletions docs/architecture/workspace.dsl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ workspace "py-doc" "The C4 model of py-doc, the Markdown technical documentation

pydoc = softwareSystem "py-doc" "Turns Markdown into Word, PowerPoint, PDF, HTML and web sites, checks the writing, and documents C4 models." {
cli = container "py-doc command" "The py-doc command and Python package. Reads py-doc.yml, prepares the Markdown and starts every tool in the tool image." "Python 3.10+" {
commands = component "Commands" "Parses the command line and hands each command to its module: build, docx, slides, lint, check, generate, stats, diff, c4, site, import, init, new, template, tools, doctor, setup, migrate. Runs the build pipeline around the documents." "argparse"
commands = component "Commands" "Parses the command line and hands each command to its module: build, docx, slides, poster, lint, check, generate, stats, diff, c4, site, import, init, new, template, tools, doctor, setup, migrate. Runs the build pipeline around the documents." "argparse"
configuration = component "Configuration" "Loads py-doc.yml and checks every key, so a mistake is an error that names its place." "PyYAML"
pipeline = component "Build pipeline" "Joins a document's sources, expands includes, conditions and variables, and picks the template and the output name." "Python"
diagrams = component "Diagram renderer" "Turns diagram fences into pictures with the right tool, and keeps them in a cache." "Python"
Expand All @@ -26,15 +26,23 @@ workspace "py-doc" "The C4 model of py-doc, the Markdown technical documentation
generators = component "Content generators" "Write Markdown from other sources before the build: glossaries, indexes, traceability, API references, diagram files, commands." "Python, entry points"
analysis = component "Analysis" "Counts what documents hold, checks a project without building it, and compares versions by section." "Python, Git"
report = component "Build report" "Records the tools, sources, outputs, diagrams and warnings of a build as JSON." "JSON schema"
watch = component "Live preview" "With --watch, builds documents, decks and posters again on every save, writes an HTML twin of each document and deck, and serves the pages on 127.0.0.1 with a script that reloads them." "Python, http.server"
}
toolImage = container "Tool image" "Every program that is not Python: pandoc, XeLaTeX, PlantUML, Mermaid, Graphviz, the diagram tools, Chromium, structurizr-cli, Vale, markdownlint, MkDocs and Node." "Docker image on Debian 12"
vscode = container "VS Code extension" "Previews py-doc Markdown in the editor as it is typed, and starts the live preview of the open file and shows it beside the editor." "JavaScript, markdown-it, js-yaml"
local = container "Structurizr Local" "Shows a workspace in a browser to view it and arrange its views, and draws the pictures for documents." "Docker image structurizr/structurizr"
cache = container "Caches" "Diagram pictures (user cache folder), Structurizr exports and generated chapters (.py-doc in the project)." "Files" "Store"
cache = container "Caches" "Diagram pictures (user cache folder), Structurizr exports, generated chapters and live preview pages (.py-doc in the project)." "Files" "Store"
}

author -> pydoc.cli.commands "Runs py-doc commands" "Terminal"
author -> pydoc.local "Views the model and arranges its views" "Browser, HTTP"
reader -> office "Opens the documents in"
author -> pydoc.vscode "Writes and previews documents in" "VS Code"
pydoc.vscode -> pydoc.cli.commands "Starts py-doc docx, slides or poster --watch for the open file" "Child process"
pydoc.vscode -> pydoc.cli.watch "Shows the served preview from" "HTTP on 127.0.0.1"
pydoc.cli.commands -> pydoc.cli.watch "Runs --watch with"
pydoc.cli.watch -> pydoc.cli.pipeline "Builds each document and its HTML twin with"
pydoc.cli.watch -> pydoc.cache "Keeps the HTML twins in" "Files"
ci -> pydoc.cli.commands "Runs builds and lint checks" "Shell"
pydoc.cli.commands -> pydoc.cli.configuration "Loads the project from"
pydoc.cli.commands -> pydoc.cli.pipeline "Builds documents with"
Expand Down Expand Up @@ -73,6 +81,9 @@ workspace "py-doc" "The C4 model of py-doc, the Markdown technical documentation
deploymentNode "Python" "The py-doc package, installed with pip." "Python 3.10+" {
containerInstance pydoc.cli
}
deploymentNode "Visual Studio Code" "The author's editor, with the py-doc extension installed from editors/vscode." "VS Code 1.75+" {
containerInstance pydoc.vscode
}
deploymentNode "Cache folders" "The user cache folder and the project's .py-doc folder." "File system" {
containerInstance pydoc.cache
}
Expand Down Expand Up @@ -110,6 +121,15 @@ workspace "py-doc" "The C4 model of py-doc, the Markdown technical documentation
pydoc.cli.pipeline -> pydoc.cli.report "Records the outputs"
autoLayout lr
}
dynamic pydoc.cli "LivePreview" "How the VS Code extension shows a live preview of a Word document." {
author -> pydoc.vscode "Clicks the eye button"
pydoc.vscode -> pydoc.cli.commands "py-doc docx guide.md --watch"
pydoc.cli.commands -> pydoc.cli.watch "Watches guide.md"
pydoc.cli.watch -> pydoc.cli.pipeline "Builds guide.docx and its HTML twin"
pydoc.cli.watch -> pydoc.cache "Writes the twin to .py-doc/preview"
pydoc.vscode -> pydoc.cli.watch "Shows the twin, which reloads on every save"
autoLayout lr
}
deployment pydoc live "Deployment" "Where py-doc runs on an author's computer." {
include *
autoLayout lr
Expand Down
Loading
Loading