Metadata-Version: 2.5
Name: safetygence-mad
Version: 0.1.0
Summary: Markdown reports and Vue slides: Quarto/Typst, Slidev, shared caches and incremental builds
Project-URL: Homepage, https://gitlab.com/exida-oss/stig/mad
Project-URL: Documentation, https://gitlab.com/exida-oss/stig/mad/-/blob/main/README.md
Project-URL: Repository, https://gitlab.com/exida-oss/stig/mad
Project-URL: Issues, https://gitlab.com/exida-oss/stig/mad/-/issues
Project-URL: Changelog, https://gitlab.com/exida-oss/stig/mad/-/blob/main/CHANGELOG.md
Author: SAFETYGENCE
License-Expression: 0BSD
License-File: LICENSE
License-File: NOTICE
Keywords: incremental-builds,markdown,pixi,presentations,quarto,reports,slidev,typst,vue
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Documentation
Classifier: Topic :: Text Processing :: Markup :: Markdown
Requires-Python: >=3.12
Requires-Dist: filelock==4.2.0
Requires-Dist: platformdirs==4.13.3
Requires-Dist: pydantic==2.14.1
Requires-Dist: rich==15.0.0
Requires-Dist: ruamel-yaml==0.19.1
Requires-Dist: typer==0.27.3
Requires-Dist: watchfiles==1.3.0
Description-Content-Type: text/markdown

# MAD — Markdown Authoring and Delivery

<img src="https://gitlab.com/exida-oss/stig/mad/-/raw/v0.1.0/docs/assets/mad-mascot.png" alt="MAD's scheming Mr Evil mascot above a document and a presentation" width="180">

**Markdown reports and Vue presentations. One installation. Clean project folders.**

MAD is the mischievous genius coordinating a team of excellent tools.
It sits on top of [Quarto](https://quarto.org/) and [Typst](https://typst.app/)
for professional documents, and [Slidev](https://sli.dev/) and [Vue](https://vuejs.org/)
for beautiful presentations. You write Markdown and add images; MAD takes care
of setup, preview, incremental builds and publishing through one command.

![Markdown and images enter MAD, which manages shared caches and incremental builds, then produces Slidev/Vue presentations in HTML and PDF or Quarto/Typst documents in PDF and HTML](https://gitlab.com/exida-oss/stig/mad/-/raw/v0.1.0/docs/assets/workflow.svg)

- **Powerful engines, already integrated.** Quarto with its embedded Typst for
  document PDFs and HTML; Slidev with Vue for presentation HTML and PDF exports.
- **Shared caches.** Pixi manages the toolchain; Python, Node, renderer dependencies
  and browsers live in user directories and are reused across projects.
- **Incremental builds.** Pixi tracks inputs and outputs, reusing unchanged HTML
  and PDF renders. Source edits, removed inputs and missing outputs trigger the
  relevant build task.
- **Content stays portable.** Markdown, images, layouts and configuration stay in
  your project. There are no project-local virtual environments or `node_modules`.
- **One workflow.** Create from local or Git templates, build, preview, clean, and
  publish to GitLab Pages or GitHub Pages with the same commands for both types.

The command is `mad`; the Python distribution is `safetygence-mad` because the unrelated
name `mad` is already registered on PyPI. MAD is free and open source under
[0BSD](https://gitlab.com/exida-oss/stig/mad/-/blob/v0.1.0/LICENSE), by SAFETYGENCE. Version 0.1.0 is the first preview release.

## Install once

On **Linux or macOS**, install Pixi from a terminal:

```sh
curl -fsSL https://pixi.sh/install.sh | sh
```

On **Windows**, download and run the `.msi` installer from the
[official Pixi releases](https://github.com/prefix-dev/pixi/releases/latest).
See [Pixi's installation instructions](https://pixi.prefix.dev/latest/installation/)
for other methods. Open a new terminal so Pixi's command directory is on `PATH`.
MAD requires Pixi 0.80 or later; update an older script-installed Pixi with
`pixi self-update`.

**MAD currently supports Linux x86-64.** The Pixi setup instructions above cover
all three operating systems, but MAD's renderer lockfile does not yet support
macOS or native Windows.

Install Git through Pixi if it is not already available, then install MAD directly
from its public repository:

```sh
pixi global install git
pixi global install --git https://gitlab.com/exida-oss/stig/mad.git --tag v0.1.0
mad --version
```

No manual clone or environment activation is needed. Pixi fetches and builds the
MAD source from GitLab, installs the resulting package in
`~/.pixi/envs/safetygence-mad`, and exposes `mad` in
`~/.pixi/bin`. Python and Git are included in that environment. The command works
from any project directory. Node, pnpm, Quarto and its embedded Typst are managed
by MAD's shared cached runtime; no separate installation is needed. Chromium is
downloaded into its shared user cache for the first presentation PDF. It requires
the usual host operating-system libraries; MAD does not install system packages.

This installation route uses GitLab as the source for MAD, not PyPI. Pixi gets
Python and native tools from Conda-forge. MAD's renderer lockfile also selects
Python packages from PyPI and Node packages from npm. These environments and
downloads are shared by all authored projects. MAD does not install packages into
system Python; its Pixi environment also disables Python's user-package search
path so existing `pip install --user` packages cannot override its dependencies.

The command above selects the fixed `v0.1.0` release. Install a newer release by
running it again with the new tag. To follow development updates, install with
`--branch main` instead of `--tag v0.1.0`, then update with:

```sh
pixi global update safetygence-mad
pixi global uninstall safetygence-mad
```

For a fixed commit, use `--rev COMMIT` instead of `--tag v0.1.0`.
Uninstalling the tool leaves authored projects
and render caches intact. Pixi documents this workflow under
[installing global tools from source](https://pixi.prefix.dev/latest/global_tools/introduction/#install-dependencies-from-source).

```sh
mad --help
mad --install-completion
```

### Optional PyPI installation

The Python distribution is already packaged as `safetygence-mad`. PyPI is the package
registry; pipx and uv install the same package into an isolated user environment.
A virtual environment is an installation location, not a publishing destination.

For releases uploaded to [PyPI](https://pypi.org/project/safetygence-mad/), users
who already use Python tools can choose either of these alternatives:

```sh
pipx install safetygence-mad
# or
uv tool install safetygence-mad
```

These alternatives still require Pixi and Git. They provide the same `mad`
command, bundled templates and shared renderer caches, without a project-local
venv. Pixi installation from Git is the recommended route today.

## Create and work

```sh
mad create report --type report --title "Technical report" --author "Alex Example"
mad create slides --type slides --title "Project review" --author "Alex Example"
cd report
mad edit
mad build
mad serve --open
```

The syntax is `mad [GLOBAL OPTIONS] COMMAND [OPTIONS] [DIRECTORY]`. Actions are
commands (`create`, `build`, `serve`, `publish`); choices use named options
(`--type`, `--format`, `--provider`). The destination after `create` is always a
directory, with `.` as the default. Use `--type report` for a Word-like document
or `--type slides` for a PowerPoint-like presentation. These names also appear
as `type: report` and `type: slides` in `mad.yml` and as the bundled template names.

Creation requires either `--type` or `--template`. A template supplies its own
type, so `mad create report --template ../templates/quotation` needs no `--type`.
If both options are provided, they must agree. Directory names never select a
renderer. This keeps custom templates, folder names and project types independent.

Both project types have the same commands:

| Command | Result |
| --- | --- |
| `mad setup` | Download the selected toolchain into shared caches |
| `mad doctor` | Show selected project, runtime and output locations |
| `mad build` | Generate both HTML and PDF incrementally |
| `mad build --format html`, `mad build --format pdf` | Generate one format |
| `mad build --force` | Regenerate even when cached outputs are current |
| `mad build --format pdf --watch` | Regenerate PDF after edits |
| `mad serve --port 3030` | Serve HTML, rebuilding and reloading after edits |
| `mad serve --open` | Also open the preview in a browser |
| `mad serve --no-watch` | Build and serve HTML without watching sources |
| `mad publish --provider github` | Publish HTML through GitHub Pages CI |
| `mad publish --provider gitlab --format all` | Publish HTML with PDF downloads through GitLab Pages CI |
| `mad clean` | Remove this project's generated output |
| `mad clean --cache` | Also remove this project's cached renders |
| `mad cache prune --days 30` | Remove inactive project workspaces; retain shared runtimes |
| `mad lock` | Explicitly select the currently installed toolchain |

Use `mad --project /path/to/project build --format pdf` from another directory.
Relative project paths, such as `mad --project ../report build`, start from your
working directory. Global options precede the command. `--output` / `-o` overrides the output
directory for `build`, `serve` and `clean`; relative output paths are resolved
from the project root. `build --base-path /reports/` sets the HTML deployment
path. Only empty or MAD-owned output
directories can be replaced or cleaned. An existing nonempty unrelated directory
is preserved and produces an error.

All commands support `-h` / `--help`. Typer provides completion for commands and
option choices, including project types, formats, providers and template names.
`templates list/export` and `cache path/prune` group actions on those resources;
project types and rendering formats are option values, never extra commands.
The interface follows the [Command Line Interface Guidelines](https://clig.dev/#arguments-and-flags)
for named options and the [pip Git URL convention](https://pip.pypa.io/en/stable/topics/vcs-support/)
for optional `git+https://` and `git+ssh://` template sources.

Live preview uses a production HTML build followed by an incremental rebuild and
browser reload. Its behavior is the same for documents and presentations. It
does not provide Slidev's editor or Vue component hot replacement. Preview binds
to `127.0.0.1` by default; `--host` changes that explicitly. A browser opens only
when `--open` is supplied.

## What to copy or commit

```text
report-or-slides/
├── mad.yml                 # type, entry point, metadata, optional history and publishing
├── mad.lock                # selected MAD version and toolchain fingerprint
├── README.md
├── .gitignore
├── src/
│   ├── index.md            # document entry; presentation uses slides.md
│   ├── chapters/           # optional; files can also be directly under src/
│   ├── images/
│   └── ...                 # authored styles, fonts, layouts, render configuration
├── resources/              # optional supporting authored inputs
└── output/                 # generated and ignored
    ├── html/               # complete static site
    └── pdf/                # current PDF
```

**Share editable work:** copy the project excluding `output/`, or clone its Git
repository. The recipient installs MAD separately. There is no environment or
tool implementation to copy. **Share a finished PDF:** send the PDF in
`output/pdf/`. **Share offline HTML:** send all of `output/html/` and serve it over
HTTP; a single `index.html` does not contain its JavaScript, CSS, fonts and images.

`src/` and `resources/` are copied into a cached render workspace. Python locates
the project from the caller or `--project`; it never assumes user content is next
to the installed Python module. Authored source symlinks are rejected so a shared
project cannot silently depend on files outside its directory.

Renderer configuration and styling are authored project data. They stay with
the project so it remains editable and visually reproducible. Node and Python
dependencies are tool data and are installed centrally. Projects do not need a
`package.json`, `pyproject.toml`, `pixi.toml`, wrapper script, `node_modules`,
`.venv`, `.pixi`, or environment symlink.

## Documents: Quarto and Typst

The `report` template is a generic document with a cover, management summary,
history, contents, chapters, references and shared HTML/PDF styling. It is not
tied to a questionnaire or any particular industry.

Quarto configuration lives in `src/_quarto.yml`; the supplied report layout lives
in `src/_extensions/report/`. Put common metadata in `mad.yml`, and keep chapter
order, typography, citations and renderer settings in `_quarto.yml`. Duplicate
common metadata across the two files is rejected. For a Quarto book, the title
is injected into `book.title` automatically.

PDF rendering always selects `typst` or a custom `*-typst` format. Quarto supplies
its own Typst binary. MAD does not install a second Typst binary or a TeX stack.
The supplied Lua filter manages report front matter, history and references; it
does not contain questionnaire-specific formatting rules.

A single-file document also works: use `entry: src/index.md` in `mad.yml`, remove
the `book` configuration, and use this `_quarto.yml`:

```yaml
project:
  type: default
format:
  html: {}
  typst: {}
```

## Presentations: Slidev and Vue

The `slides` template includes neutral Vue layouts, reusable cards, local
Manrope fonts and an example image. Content starts at `src/slides.md`. You may
keep every slide there, separated by `---`, or include chapter files using
Slidev's native syntax:

```markdown
---
src: ./chapters/overview.md
---
```

Keep native Slidev options in the entry file's front matter. Keep title, author
and other shared metadata in `mad.yml`. Images belong in `src/images/`, custom
Vue components in `src/components/`, layouts in `src/layouts/`, and styles in
`src/style.css`. Use relative image and font references so deployment under a
subdirectory works. The lockfile deliberately pins FloatingVue 5.2.2 because the
selected Shiki Twoslash integration relies on its component structure.
MAD defaults `preloadImages` to `false`: Slidev's optional extra preloader uses
raw Markdown image URLs, which do not match Vite's bundled assets. Normal image
loading still works. An explicit native front-matter setting can override this.

## Templates and personal defaults

```sh
mad templates list
mad templates export
mad create quotation --template ~/.local/share/mad/templates/report
mad create . --template /path/to/my-template
mad create report --template ../templates/report
```

Relative paths are resolved from the caller's directory (or `--project`), not
from the installed tool. `--template` also fetches templates directly from Git:

```sh
# Repository whose root contains mad.yml
mad create report --template git+https://gitlab.com/example/report-template.git
mad create report --template git@gitlab.com:example/report-template.git
mad create report --template git+ssh://git@gitlab.com/example/report-template.git

# A folder in a template collection, optionally pinned to a branch, tag or commit
mad create report --template git+https://gitlab.com/exida-oss/stig/mad.git \
  --subdir templates/report --ref main

# The same repository, revision and folder in one quoted URL
mad create report --template 'git+https://gitlab.com/exida-oss/stig/mad.git@main#subdirectory=templates/report'

# A GitLab browser link directly to the desired template folder
mad create slides --template https://gitlab.com/exida-oss/stig/mad/-/tree/main/templates/slides
```

HTTPS repository URLs with or without `.git` and standard SSH clone URLs work,
with optional `git+` prefixes on HTTPS/SSH scheme URLs. For a single source string,
`git+https://REPOSITORY.git@REF#subdirectory=FOLDER` (or `git+ssh://...`) selects a
revision and folder. Quote URLs containing shell punctuation. A GitLab browser
folder URL also works with a `git+https://` prefix.

`--subdir` selects a folder inside the repository; the default is its root.
`--ref` defaults to the repository's current `HEAD`. Use a tag or commit
for a fixed template version. GitLab folder links supply both values directly;
for branch names containing `/`, use the clone URL with `--ref` and
`--subdir`, or percent-encode the slash in the link's revision component. Explicit
`--ref` and `--subdir` options override selections embedded in a URL. For a local
template, pass its directory directly; Git selection options do not apply.

Git uses your existing SSH agent/configuration or HTTPS credential helper. URL
passwords and access tokens are not accepted or written into project files.
Each creation fetches the requested revision and records the repository, folder,
requested ref and resolved commit in `mad.lock`. A failed fetch reports an error
without silently using an older cached revision or creating a partial project.

The bare Git repository and its current extracted template stay in
`~/.cache/mad/templates/`. Creation copies only authored template files, without
Git history, tool dependencies or environments. One extracted template per URL
is retained, even when switching revisions; `mad cache prune --days 30` also
removes inactive Git-template caches. Git acquisition does not run template setup
scripts or fetch submodules. Ordinary local template paths remain usable offline.

Exported templates live in `~/.local/share/mad/templates/`, a durable location
outside caches. You can instead run `mad templates export ~/bin/mad-templates`.
Export never overwrites existing templates. A template is a normal authored
project; creation copies its sources and assets, gives it a fresh project ID,
and selects the installed toolchain. Existing nonempty destinations are refused.

Template lookup uses paths configured in `~/.config/mad/config.yml`, then the
standard user template directory, `~/bin/mad-templates`, and bundled defaults:

```yaml
defaults:
  author: Alex Example
template-paths:
  - ~/company-templates
```

`--author` and `--title` override template values. This supports personal and
company templates without putting private branding or customer information into
the public tool repository.

## Version and lifecycle status

History is optional and works for either project type. If present, its last
entry defines the current version, date and status. The report filter renders
that history; there is no second manually maintained history table.

```sh
mad status
mad version
mad version --bump minor --message "Added analysis"
mad version 1.0 --message "First release"
mad edit --status review
mad edit --status approved --date 2026-10-16
mad edit --title "Technical report" --author "Alex Example"
```

`mad --version` prints the installed tool version; `mad version` prints the
project version. Set a version number or use `--bump major|minor`, with a required
`--message` / `-m`. New history entries accept `--author`, `--status` and `--date`;
status defaults to `draft` and date to today. `mad edit --status` changes the
current history entry without adding another version. With no options,
`mad edit` prompts for metadata; supplied options update only the named values.

PDF names follow `<name>-v<version>-<status>.pdf`; approved PDFs omit the status.
Without history the name is simply `<name>.pdf`. Output is replaced on each
successful render, so obsolete filenames do not accumulate. Keep deliberately
released versions in a separate authored `releases/` directory or a release
registry; `clean` only owns generated output.

## Publishing

Set the actual Pages site root in `mad.yml`. Git remote URLs cannot reliably
identify it, especially with GitLab unique domains or custom domains.

```yaml
publish:
  gitlab:
    remote: origin
    branch: github_pages
    base-url: https://example.gitlab.io/reports/
    subdir: annual-report
  github:
    remote: github
    branch: github_pages
    base-url: https://example.github.io/reports/
    subdir: annual-report
```

The resulting URL is `base-url + subdir + /`. Use `subdir: ''` to deploy at the
site root. That replaces the whole site; a nonempty subdirectory replaces only
that project's subtree and preserves siblings. Configure separate subdirectories
for projects sharing one deployment branch.

```sh
mad publish --provider gitlab --format all --dry-run
mad publish --provider gitlab --format all
mad publish --provider github --branch pages --base-url https://example.github.io/reports/
```

The default branch is `github_pages` for both providers. All configuration fields
can be overridden on the command line. Publishing requires a Git repository with
at least one source commit, a configured remote, Git identity and push access.
It uses a temporary worktree, boots an orphan deployment branch when needed,
and pushes normally without force-pushing or switching the source checkout.
Uncommitted source edits remain intact.

GitLab receives a real Pages job with `pages.publish: site`. GitHub receives a
pinned Actions workflow that uploads and deploys the Pages artifact. For GitHub,
select **GitHub Actions** in repository Settings → Pages before the first
deployment; ensure any environment branch restrictions allow the deployment
branch. GitLab needs an available runner and Pages enabled. Existing conflicting
CI configuration is preserved and reported as an error. A successful push is
reported as **deployment pending**, not as confirmation that the site is live.

`--format html` (the default) publishes the site only, excluding PDFs left by
previous builds. `--format all` also places PDFs under `downloads/` with a
download index. Pages publishing requires HTML; standalone PDF delivery uses
`mad build --format pdf`. Speaker notes are omitted from published slides by
default; use `--include-notes` to include them. Local builds retain notes.

## Caching, cleanup and reproducibility

Pixi's native task input/output fingerprints decide whether HTML and PDF need
rendering. The task inputs include the source tree, resources, metadata,
lockfile, output options and tool implementation signature. Missing outputs and
removed inputs invalidate the relevant task. MAD mirrors input deletions into
staging and replaces complete output directories after successful rendering.
A failed build leaves the last successful project output intact.

The shared cache layout is approximately:

```text
~/.cache/mad/
├── toolchains/<lock-hash>/   # manifest, native Pixi task cache and project workspaces
├── node/<dependency-hash>/  # shared frozen pnpm installation
├── templates/<url-hash>/    # downloaded Git repository and current template
└── locks/                  # serialize changes to a project
~/.cache/rattler/            # Pixi's detached environments and downloaded packages
~/.cache/ms-playwright/      # shared Chromium installation
```

Every project shares runtime installations; render workspaces are isolated by
absolute project path and publishing variant. Copying a project creates an
independent cache. XDG locations are respected; `MAD_CACHE_HOME` can override
MAD's cache directory and must be outside the authored project.

Use `mad clean` for deliverables, `mad clean --cache` for one project's render
cache, and `mad cache prune --days 30` for inactive project and Git-template workspaces. These
commands retain shared runtimes and personal templates. Old runtime versions
remain available until removed using Pixi's cache/environment management; there
is no automatic deletion of an environment another installation may be using.

`mad.lock` records the exact toolchain fingerprint. A mismatch requires an
explicit `mad lock` or the matching MAD installation. Native and Node dependency
locks are maintained in the tool repository. This makes local inputs and tools
repeatable; remote images, custom code that fetches data and host-provided fonts
are outside the cache's dependency graph. Vendor those inputs into `src/` or
`resources/` when reproducibility matters. Templates and custom renderer code
execute locally, so use templates from sources you trust.

## Development and verification

Only contributors need a source checkout:

```sh
git clone https://gitlab.com/exida-oss/stig/mad.git
cd mad
pixi run --locked mad --help
```

```sh
pixi run --locked lint
pixi run --locked test
pixi run --locked integration
pixi run --locked package
```

Unit tests cover project isolation, metadata, path safety and publication to
temporary bare Git repositories for both providers. Template tests fetch real
synthetic repositories through HTTPS/SSH URL rewrites and cover revisions,
provenance, source isolation, download failures and cache cleanup. Integration tests render
single-file and chapter-based documents/presentations and verify cache reuse,
input deletion, missing-output regeneration, source isolation and cleanup.
Integration tests download the same locked runtime and Chromium used normally.
Wheels and source archives are written to `output/packages/`. CI runs the unit
checks, builds the Python and Pixi packages, and tests installations from the
wheel, source archive and Git revision outside the source checkout. These checks
cover bundled resources, relative project paths and isolation from user-installed
Python packages. Real renderer checks can be run with `integration`.

`pixi publish --target-dir output/packages` builds the local Conda package used
by Pixi's source installer; it does not upload to a registry. The Python backend
reads dependencies and build requirements from `pyproject.toml`. Native renderer
versions stay in `pixi.toml` / `pixi.lock`.

### PyPI releases

The `publish-pypi` GitLab job uploads the verified wheel and source archive on
protected release tags matching `vX.Y.Z`. Tests require the tag, CLI,
`pyproject.toml` and Pixi package versions to agree. Branch pipelines never
publish. The job uses
[PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/using-a-publisher/)
and `uv publish --trusted-publishing always`; no permanent PyPI token is needed.

Before the first release, a maintainer must configure a
[pending PyPI publisher](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
in the account that will own the package:

| PyPI field | Value |
| --- | --- |
| Project name | `safetygence-mad` |
| Publisher | GitLab CI/CD on `gitlab.com` |
| Namespace | `exida-oss/stig` |
| Project / repository | `mad` |
| Pipeline filepath | `.gitlab-ci.yml` |
| Environment | `pypi` |

In GitLab, protect the `v*` tag pattern and allow release maintainers to create
those tags. Then update all package/CLI versions together, run the verification
commands, and push the matching release tag. The publishing job consumes the
artifacts produced by `verify`; it does not rebuild them. Confirm that the
publishing job succeeds and the version appears on PyPI before announcing its
PyPI availability. A Git tag or a successful build alone does not confirm upload.

The software is licensed under [0BSD](https://gitlab.com/exida-oss/stig/mad/-/blob/v0.1.0/LICENSE). Bundled third-party assets retain
their licenses; see [NOTICE](https://gitlab.com/exida-oss/stig/mad/-/blob/v0.1.0/NOTICE). Fedora/Ubuntu repositories and a public
Conda channel are not distribution targets yet. See
[the existing PyPI MAD project](https://pypi.org/project/MAD/)
for the unrelated package occupying that distribution name.
