The command line for markdown-office: Markdown to Word and to PowerPoint, in one program.
mdoffice word notes.md # → notes.docx
mdoffice word report.docx # → report.md
mdoffice word notes.md --to odt # → notes.odt
mdoffice slides deck.md # → deck.pptx
mdoffice theme init deck.pptx # → deck/
mdoffice helpThe converters themselves are separate libraries and are meant to be used on their own:
markdown-office/markdown-word— Markdown to.docx,.odtand.rtf, and.docxback to Markdownmarkdown-office/markdown-presentation— Markdown to.pptx, and a deck back to Markdownmarkdown-office/commonmark-generic-directives— the directive syntax, for your own parser
This repository holds the command line, the PHAR and the build pipeline. It has no public API for converting anything: a document is made by running the program, not by calling a class. That is the line between the suite's libraries and this tool, and the documentation sets it out in a chapter of its own — the command line — with the options of each part on a page of its own.
The command name says which converter a run is for:
| Part | Does |
|---|---|
mdoffice word … |
.docx, .odt, .rtf, and .docx back to Markdown |
mdoffice slides … |
.pptx, and .pptx back to Markdown |
mdoffice theme … |
a theme scaffolded from a deck |
A bare file is not a run. mdoffice notes.md is refused and the parts are listed: with
more than one converter here, a file name alone cannot say which way the data goes, and
an error that guessed would be right half the time and wrong the other half.
mdoffice theme init deck.pptx writes a theme directory from a deck: theme.yml with
the deck's colours, fonts and type scale, layouts.yml with the eight layout roles
mapped onto the deck's own layouts, and template.pptx with the deck's layouts and
none of its slides. The directory goes beside the deck unless -o says otherwise.
It reads the deck rather than asking about it, and it says what it read:
- The colours come from the deck's own theme part where the deck is drawn in that theme, and from the deck's slides where it is not — a deck made in a tool that does not do themes carries Office's palette without ever drawing in it.
- The type scale is read off the deck's text, because a deck records a size for every paragraph rather than a scale. The command reports the sizes it found and where it read them from.
- The layouts are mapped by the placeholders they hold and not by their names: a deck calls its layouts whatever it likes. A role the deck has no layout for is reported rather than pointed at a layout that is something else.
A theme that is already there is not overwritten without --force.
mdoffice slides diff reads two decks and says what changed between them. It is the
tool for the question every template workflow asks — I changed the design, what did it
break? — and it is how a reproduction is checked without a pixel diff.
mdoffice slides diff before.pptx after.pptxmdoffice slides: diff of before.pptx against after.pptx
slide 1: the text "Volumen niedrig" was removed
slide 1: the shape "Body 2" moved from 10.58mm, 27.52mm to 10.58mm, 31.68mm
slide 2: the text "Ergebnis" changed colour from 00B4D8 to 087995
What is compared is the text of every shape, where every shape is and how big it is, and the colour of every fill and every run. Positions and sizes are in millimetres, because the EMU a package holds are a hundredth of a millimetre divided by 360 and nobody reads them.
The two decks are paired slide by slide by the id each slide carries, which survives an edit and a reorder where a position does not: a slide inserted at the front moves every slide after it, and a diff that paired them by their place would report the whole deck as changed.
The archive's own bytes are never compared. A .pptx carries a time in every part it
holds and another in the document's own properties, so two renderings of the same deck
are two different files and a byte comparison would say that every deck ever written had
changed.
The report goes to standard error and says what it looked at as well as what it found: a reader told "no differences" by a tool that looked at half the deck is worse off than one told what was looked at.
The exit code is the program's own. A run that compared two decks and found differences
did what it was asked, and says so in its report; --exit-code makes a difference exit
with 1, for a build that wants to stop on one.
PHP 8.2 or newer with ext-dom, ext-gd, ext-mbstring, ext-xmlwriter and ext-zip.
ext-xmlwriter is here because markdown-word uses it to merge numbering definitions,
not because anything in this repository names it.
A deck with pictures in it needs more memory than PHP's own default of 128 MB allows, so the program raises its own limit to 2 GB. A machine that gave it more keeps what it gave.
composer require markdown-office/mdofficeThat gives the command as vendor/bin/mdoffice.
The project has to allow a plugin. markdown-presentation writes a few things a
released PHPPresentation cannot — Table::setFirstRow(), AutoShape::setRoundRectCorner()
among them — and gets them from a patch applied to the installed package. The patch is
applied by cweagans/composer-patches, which Composer only runs when
the project allows it:
{
"require": {
"markdown-office/mdoffice": "^0.1"
},
"config": {
"allow-plugins": {
"cweagans/composer-patches": true
}
}
}Without that line the install stops with Composer's own message naming the plugin. Nothing else has to be relaxed: every dependency is a stable release, so the default stability holds.
The plugin writes a patches.lock.json into the project that installs this one. It
records the patches applied and is worth committing, so that a change to one of them is
seen rather than absorbed.
All three repositories, side by side:
git clone https://github.com/markdown-office/mdoffice.git
git clone https://github.com/markdown-office/markdown-word.git
git clone https://github.com/markdown-office/markdown-presentation.git
cd mdoffice
composer install
php bin/mdoffice word README.mdThe siblings are not optional. The manifest resolves both libraries through path
repositories, which point at ../markdown-word and ../markdown-presentation, so a
clone of this repository on its own has nothing for Composer to resolve. The path
entries are for development, and an installed copy never sees them: Composer reads
repositories from the root package only, so the two libraries are resolved from
Packagist instead.
For a machine with no PHP at all, download a release from the releases page. Each one carries a self-contained executable per platform — no PHP, no extensions, nothing to install — and the archive the executables are built around, for a machine that has PHP and would rather not download a second copy of it:
mdoffice-<version>-macos-arm64 |
macOS on Apple silicon |
mdoffice-<version>-macos-x86_64 |
macOS on Intel |
mdoffice-<version>-linux-x86_64 |
Linux on x86-64 |
mdoffice-<version>-linux-aarch64 |
Linux on ARM |
mdoffice-<version>-windows-x64.exe |
Windows on x64 |
mdoffice-<version>.phar |
the archive on its own, for a machine with PHP |
SHA256SUMS |
the checksum of each of the above |
The Linux and macOS executables are the interpreter upstream publishes for that platform, with the archive appended to it. The Windows one is compiled here, because upstream publishes no Windows build of it.
Three things differ between them, and all three are deliberate.
The macOS executables are signed with an ad-hoc signature, and that is not optional.
A Mac with Apple silicon does not run native arm64 code without a signature, and an
unsigned one is killed by the kernel before Gatekeeper is ever asked. The signature is
ad-hoc, so it is not a developer certificate and not notarised: it satisfies the
kernel and nothing more. It is a signature on the interpreter, which the archive is
appended to rather than signed over: codesign will not sign a file with a payload
appended to it, so the executable keeps the signature it was given. Upstream signs the
Apple silicon build and does not sign the Intel one, so the pipeline writes the
signature the Intel build is missing.
The macOS executables are the largest on disk. The published macOS interpreter is
three times the size of the published Linux one. The Windows executable is compiled
here rather than packed: PHP's own Windows build links with /GUARD:CF, and UPX
refuses to pack a PE file that has Control Flow Guard enabled. The downloads are much
closer together than the files are, because the Linux one barely compresses.
The Windows executable has no SVG support. ext-imagick cannot be built into a
static PHP on Windows — ImageMagick's own Windows build needs a C++ compiler that
the static toolchain does not provide — so the extension is not there. A deck or a
document with an SVG in it says so, with what to do about it, rather than failing
quietly. The Linux and macOS executables have it.
A download in a browser asks a question on a Mac that a download in a terminal does
not. The quarantine flag is set by the application that fetched the file rather than
by where it came from, so a file saved by a browser is quarantined and Gatekeeper
asks — the dialog that says Apple could not check whether "mdoffice" is free of
malware that could harm your Mac or violate your privacy. It asks because the
signature is an ad-hoc one and the binary is not notarised, which is what makes
Apple's check impossible rather than merely unconfirmed. A file fetched with curl
is not flagged and is not asked about.
The flag comes off like this, and the program runs:
xattr -d com.apple.quarantine mdofficeOr fetch it with curl in the first place, and there is no dialog:
curl -L -o mdoffice https://github.com/markdown-office/mdoffice/releases/latest/download/mdoffice-0.1.3-macos-arm64composer.json declares both libraries as path repositories, which point at
../markdown-word and ../markdown-presentation. That is what makes a change in
either library visible to the command line without a reinstall, and it is why a
clone needs its siblings beside it. An installed copy never sees them: Composer
reads repositories from the root package only, so the two libraries are resolved
from Packagist instead.
Each path repository also declares the version its sibling is taken to be — 0.3.x-dev
for both, against the ^0.3 the manifest requires. The number is the branch-alias
form of the line the sibling is on, so a stable-preferring resolve takes the
sibling's code rather than the tag, and it satisfies the constraint, which dev-main
would not. Without it the resolve fails outright: a path repository is
canonical, so the branch it offers is the only one that counts, and dev-main matches
no constraint.
tools/build-phar.php rewrites them to vcs for the build, because a relative
path resolves differently once the manifest is staged.
A clone needs its siblings beside it, because that is what the path entries point at.
The same is why there is no committed composer.lock: it would record whichever
branches those two checkouts happened to be on, rather than anything this repository
chose. The three libraries commit none either, and tools/build-phar.php stages the
manifest with the two rewritten as vcs for the same reason — a relative path resolves
differently once the manifest has been staged, and a lock built against path
repositories points at a directory that is not there.
So a released archive holds a published release of each library: the build asks
for the series the manifest names — ^0.3 for both — and takes the newest release
in it, so --version names a version a reader can look up rather than a branch that
moves. The build refuses a requirement that names no series, and refuses an archive
whose libraries came back as a branch.
Which means the libraries have to be released before this one is: a v0.1.0 of
mdoffice holds the v0.3.0 of markdown-word and the v0.3.0 of
markdown-presentation that existed when the build ran, and no later.
tools/install.sh # what continuous integration runs; --no-dev for a runtime-only tree
composer test # the suite
composer check # validate, the binary's own version, and the suite
composer build:phar # build/mdoffice.pharThe build prints what it did, prunes the dependencies' own tests and documentation, and then runs the archive it wrote — a version check, a conversion in each direction, a check that the document kept its table, and a deck. An archive that fails any of those is deleted rather than left behind.
Three tests need the .NET SDK, which not every machine has: the documents the program
writes are checked against the schema with Microsoft's own validator, which lives in
tools/ooxml-validator. They skip themselves where the SDK is not, and say what is
missing. The gate is a job of its own in continuous integration, and that job installs
the SDK and refuses a skipped test, so the gate cannot pass without having run.
MIT. See LICENSE.