Skip to content

excelano/ditto

Repository files navigation

ditto

ditto builds plain-text sources into the Microsoft Office and HTML deliverables people expect, the way cargo builds a crate. You author Markdown, CSV, and other text under src/; ditto presses each file into its Word, Excel, PowerPoint, or portable-HTML counterpart under dist/; and an optional publish step mirrors dist/ to a SharePoint library or a local folder. The text is the master you version; the Office files are the impression you hand to people who live in Word and Excel.

The name is the ditto machine, the old spirit duplicator: the source and the built document are two pulls of the same master, not a copy.

ditto reimplements nothing. It conducts tools that already exist — office-convert for Markdown and CSV conversion, cleave for portable HTML, and xsync for the SharePoint push. Install whichever of those you need; ditto warns about what is missing.

The manifest is authoritative

Manifest.toml lists every deliverable. If a file is not listed, it is not built — there is no implicit globbing. Each target names an input under src/, an output under dist/, and optionally a style template, a view, or a custom converter.

[project]
name = "parsolvo-assessment"

[publish]                              # optional; omit to skip publishing
root = "https://parsolvo.sharepoint.com/sites/Engagement/Shared Documents/Deliverables"

[[target]]
input = "assessment.md"                # relative to src/
output = "deliverable1/drafts/Assessment.docx"   # relative to dist/
reference = "brand/excelano.docx"      # optional styling template

[[target]]
input = "inventory.csv"
output = "deliverable1/supporting/Asset Inventory.xlsx"

The output extension picks the converter, so the same Markdown file can build a Word document in one target and a slide deck in another just by naming .docx in one and .pptx in the other.

When a project has a house style, most .docx targets want the same reference doc and most .pptx targets the same slide template. Set them once at the [project] level with default_reference_docx and default_reference_pptx rather than repeating reference on every target; a target's own reference still overrides the default.

[project]
name = "parsolvo-assessment"
default_reference_docx = "brand/excelano.docx"
default_reference_pptx = "brand/excelano.pptx"

src/ is fixed — it is the curated master tree, and scan reports anything in it that no target covers, so it must hold only real sources. The output side is not fixed: set dist at the [project] level to build and publish through a directory other than the default dist/ (a ~ is expanded), for instance an external staging folder outside the repo. A dist that resolves inside src/ is rejected, since building there would write outputs into the source tree.

[project]
name = "parsolvo-assessment"
dist = "~/austin-contract2-stage"
Input Output Engine
.md .docx office-convert (pandoc)
.md .pptx office-convert (pandoc)
.csv .xlsx office-convert (openpyxl)
several .csv .xlsx office-convert (one sheet per CSV)
.md / .csv / .ics .html cleave

For anything the built-ins do not cover, set converter on the target to your own script. It is called as converter <input> <output> with the output's parent directories already created; a non-zero exit fails that target. Any reference and view are passed to it as the REFERENCE_DOC and VIEW environment variables.

The script path is resolved relative to the project root, so build.py and converters/build.py both refer to a file in the project; a bare name not found there falls through to a $PATH lookup. The script is run directly, so it must be executable (chmod +x) and start with a shebang like #!/usr/bin/env python3. The same holds for pipeline scripts below.

When one deliverable is assembled from several sources — say a multi-tab workbook built from three CSVs — list them all with inputs instead of input. The first path is still passed as <input>, the positional argument, and the whole set is handed to the converter as the INPUTS environment variable, one absolute path per line. The positional <input> and <output> are relative to the project root, while INPUTS lines are absolute, so a converter that changes its working directory should read sources from INPUTS rather than the positional argument. Naming every source here keeps the manifest honest about what the deliverable depends on, and stops ditto scan from reporting the extra files as uncovered. inputs pairs with a custom converter for a deliverable with real presentation logic; the one built-in that takes several inputs is .xlsx, which turns several CSVs into a sheet each with no converter (below).

[[target]]
inputs = [
  "contracts_enriched.csv",
  "contracts_expired.csv",
  "contract_monthly_summary.csv",
]
output = "deliverable3/Contract Action Calendar.xlsx"
converter = "converters/build_calendar.py"

When several CSVs just need to land as separate tabs with no extra logic — each already final, no coloring or derived columns — point inputs at them with an .xlsx output and no converter. The built-in writes one sheet per CSV, named after the input filename stem, in the order listed. Anything fancier than plain tabs stays a custom converter.

[[target]]
inputs = ["active.csv", "expired.csv"]
output = "Asset Register.xlsx"

Often the real source of a deliverable is further upstream than the file the converter reads: a raw export gets pulled, then classified and enriched, and only the result is ready to press into a workbook. List those upstream steps with pipeline and ditto runs them in order before the converter, each with no arguments, failing the target on the first non-zero exit. They run before the inputs are checked for existence, because they are what produce those inputs, so ditto build reproduces the whole derivation rather than just the last hop. A pipeline belongs to its target and runs whenever that target builds; two targets that share the same pipeline each run it, in keeping with ditto rebuilding everything rather than tracking what is already up to date.

[[target]]
pipeline = ["extract_contracts.py", "enrich_contracts.py"]
input = "contracts_enriched.csv"
output = "deliverable3/Contract Action Calendar.xlsx"
converter = "converters/build_calendar.py"

Commands

ditto new <name>      # scaffold src/, dist/, Manifest.toml, .gitignore
ditto build           # build every target into dist/
ditto build <prefix>  # build only targets whose output is under <prefix>/
ditto scan            # report files in src/ that no target covers
ditto scan --write    # append a target per uncovered file, using defaults
ditto publish         # mirror dist/ to the publish root
ditto publish <prefix>  # mirror only dist/<prefix>/ to <root>/<prefix>/
ditto publish -n      # preview the publish without writing anything
ditto publish --delete  # also remove root files no longer in dist/

On an engagement with many deliverables, a single manifest holds dozens of targets but you are usually iterating on one. build and publish take an optional prefix — the leading segment of an output path — to work on just that slice: ditto build D3 builds only the targets whose output is under D3/, and ditto publish D3 mirrors only dist/D3/ to the matching D3/ subfolder of the root, so neither one pays the cost of touching every other deliverable. The prefix matches on a path boundary, so D3 selects D3/… without also catching D30/….

The prefix is the only lever here: build always rebuilds every target it selects, rerunning each pipeline and converter with no freshness or up-to-date check. Unlike cargo, which skips crates that have not changed, ditto has no incremental mode — a plain ditto build reruns the whole derivation, slow extract/enrich steps included. That is deliberate: it keeps a build fully reproducible and the manifest the only thing that decides what runs. For a handful of deliverables the redone work is cheap; when it is not, scope the build to the one deliverable you are iterating on with a prefix.

publish mirrors dist/ to the [publish] root: a SharePoint library (through xsync) or, when the root is a filesystem path, a plain recursive copy. Each target's output already names the destination subfolder and filename, so dist/ is the publish layout — there is no separate staging step.

One thing to expect on SharePoint: the library rewrites Office files on upload (it binds them to a content type), so the stored bytes differ from what you sent. The deliverable content is untouched, but xsync will re-upload .docx, .xlsx, and .pptx files on every publish rather than skipping them as unchanged. This is SharePoint's behavior, not ditto's.

ditto scan with no flag only reports; --write appends a [[target]] for each uncovered file using the default output (.md to .docx, .csv to .xlsx), mirroring the src/ tree. It appends rather than rewriting, so your hand-edited names and comments are left alone. The formats it cannot guess — .md to a deck, anything to HTML, unknown extensions — it leaves for you.

Install

On Debian and Ubuntu, install from the Excelano apt repo so ditto updates with the rest of your system. Add the repo once, then install:

curl -fsSL https://excelano.com/apt/setup.sh | sudo sh
sudo apt install ditto

Anywhere else with a Linux binary, fetch the latest release:

curl -fsSL https://raw.githubusercontent.com/excelano/ditto/main/install.sh | sh

The script drops the binary into /usr/local/bin (or ~/.local/bin if that is not writable). Pin a version with DITTO_VERSION=v0.1.0 or choose the directory with DITTO_INSTALL_DIR. Uninstall with the matching uninstall.sh, or sudo apt remove ditto if you installed via apt.

To build from source (and on macOS, where the system already ships its own /usr/bin/ditto, so pick a different output name):

go build -o ~/bin/ditto .

ditto is a single Go binary with no runtime dependencies of its own; the converters it calls (office-convert, cleave, xsync) are separate installs, and apt install ditto recommends pandoc for the common Markdown-to-Word path.

Claude Code skill

skills/ditto/ is a Claude Code skill so an AI coding agent drives ditto instead of hand-running the converters it orchestrates. Install it into your skills directory:

cp -r skills/ditto ~/.claude/skills/

The agent then reaches for ditto build/ditto publish on a manifest-governed deliverable set, and hands a genuine one-off conversion straight to the underlying converter.

About

Cargo-style build system for Office and HTML deliverables: author text, build to Word/Excel/PowerPoint/HTML, publish to SharePoint.

Resources

License

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors