` | an unsupported element |
| `:::note` … `:::` | a container |
| `::bogus`, `::left::`, `::right::` | a leaf directive |
| `::note` with no closing `::` | an unclosed note |
| `---` used as a rule | a slide delimiter — use `***` |
That list is the point of the package. The thing writing a deck may be a model, and a model can
act on a message and cannot act on a deck that quietly came out wrong.
## Three things that look like they work and do not
These are worth stating because a reader would reasonably assume otherwise.
**A marker inside a list item is drawn as text.** `- ::click` produces a list item whose content is
literally `- ::click`. The refusal walk does not descend into a list, and a marker inside one would
mean splitting a single list block into several.
**`::click{foo=bar}` and `::click[label]` render as an empty paragraph** — not an error, and not as
text. The marker patterns are anchored to the end of a line, so the brace and bracket forms fall
through to the generic directive parser, whose output nothing translates. Write the bare form or
`=N`.
**A frontmatter block must start at the very first byte.** A deck whose source begins with a blank
line has its `theme:` attributed to slide 2, and is refused for naming a deck directive there.
Source: https://markdown-office.github.io/presentation/directives/
---
## Themes and layout
A theme is a directory. `theme.yml` is required; `styles.yml`, `layouts.yml` and
`template.pptx` are optional.
```text
themes/corporate-dark/
├── theme.yml required
├── styles.yml the Word half's vocabulary — the renderer does not read it
├── layouts.yml which PowerPoint layout each role maps to
└── template.pptx optional, a path
```
## `theme.yml`
| Key | Type | Default | Notes |
| --- | --- | --- | --- |
| `name` | string | — | **required**, non-empty |
| `description` | string | `null` | |
| `author` | string | `null` | |
| `dark` | boolean | `false` | boolean only — `dark: yes` is refused |
| `colors:` | mapping | see below | `#rrggbb`, stored upper-cased |
| `fonts:` | mapping | Calibri / Calibri / Consolas | `heading`, `body`, `mono` |
| `style:` | mapping | see below | the geometry |
`dark: yes` being refused is not pedantry. YAML 1.1 truthy strings are what make a theme written
by hand behave differently from one written by a machine, and this one is for machines.
## The twelve colour slots
```text
dk1 lt1 dk2 lt2 accent1 accent2 accent3 accent4 accent5 accent6 hlink folHlink
```
Defaults: `#000000` `#FFFFFF` `#44546A` `#E7E6E6` `#4472C4` `#ED7D31` `#A5A5A5` `#FFC000`
`#5B9BD5` `#70AD47` `#0563C1` `#954F72`.
Four read-friendly aliases are accepted: `dark1`, `light1`, `dark2`, `light2`, `hyperlink`,
`followedHyperlink`.
`dark: true` swaps four of them — the page becomes `dk1`, the ink `lt1`, the panel `dk2`, the
subtle ink `lt2` — which is why the same theme can be dark without being a second theme.
## `style:` — the geometry
| Key | Type | Default | Minimum |
| --- | --- | --- | --- |
| `margin` | int, px | 40 | 1 |
| `gutter` | int, px | 20 | 1 |
| `block` | int, px | 12 | 1 |
| `rule` | int, px | 2 | 1 |
| `body` | int, pt | 16 | 1 |
| `title` | int, pt | 28 | 1 |
| `cover` | int, pt | 40 | 1 |
| `section` | int, pt | 44 | 1 |
| `headings` | list of int, pt | `[22, 20, 18, 16, 14]` | each ≥ 1 |
| `leading` | number | 1.2 | 1.0 |
| `heading-weight` | `bold` or `regular` | `bold` | |
| `heading-ink` | `ink` or `accent` | `ink` | |
## Layout roles
Eight, and only these:
```text
title content two-column section blank image-text image-title image-slide
```
`blank` and `image-slide` have no title box. `layouts.yml` maps each role to a layout name in the
master — the file may be a bare mapping or nested under a single `layouts:` key.
Only the roles a deck actually uses are built, so a ten-slide deck does not carry eight masters.
## The four that ship
| Theme | Margin | Block | Body | Headings | Leading | Weight | Ink |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `default` | 40 | 12 | 16pt | 22/20/18/16/14 | 1.2 | bold | ink |
| `corporate-dark` | 48 | 16 | 18pt | 26/23/21/19/17 | 1.32 | bold | **accent** |
| `minimal` | 64 | 14 | 17pt | 23/22/21/20/19 | 1.12 | **regular** | ink |
| `dense` | 28 | 8 | 13pt | 17/16/15/14/13 | 1.1 | bold | ink |
`minimal` has more margin and less ink; `dense` has less margin and smaller type. Those are the
two axes a deck is usually fighting over, and they are separate keys so you can move one.
## Resolution order
1. an explicit path — anything containing `/`
2. `
/themes/`
3. the package's own `themes/`
There is **no silent fallback to `default`**. A name that does not resolve raises `ThemeNotFound`
and lists every theme that does. A deck that came out looking like something other than what was
asked for is worse than a deck that did not build.
## Word themes
The Word half has themes too, and they are the other shape: a directory with `theme.yml` —
`name`, `description`, `author`, and `name` required — and an optional `styles.yml` holding the
same slot names the [Word configuration](/word/configuration/) page lists. Four ship:
`default`, `minimal`, `corporate`, `report`.
A theme is a **starting point**, not a replacement: your own `styles:` sits on top of it, and
which keys you wrote is remembered so a partial override does not silently discard the rest.
Source: https://markdown-office.github.io/presentation/themes/
---
## Errors
Every exception Markdown Presentation throws, in one place. The one to catch, and the advice that
is about both libraries, is on [Errors in general](/errors/).
## Presentation
| Exception | Means |
| --- | --- |
| `InvalidInput` | the base for everything a deck author can fix |
| `UnknownDirective` | a key or marker outside the closed set — the message names the set |
| `UnknownMarker` | `::click=0`, backwards, over 500, unclosed, or a stray closer |
| `UnknownLayoutRole` | `layout:` is not one of the eight |
| `UnsupportedColumn` | more `::column` markers than the layout has columns |
| `UnsupportedImageFormat` | not an image, no decoder, or over the ceiling |
| `UnsupportedLink` | `javascript:`, `data:`, `phar://`, `file://`, a UNC share |
| `ContentOverflow` | a block does not fit and `strict` is on |
| `TooManySlides` | more than 1000 |
| `MalformedFrontmatter` | not a mapping, or a value of the wrong type |
| `FencedFrontmatter` | a ```` ```yaml ```` block at the top of a slide |
| `MalformedTheme` / `ThemeNotFound` | as above |
| `UnknownStyleSlot` | `styles.yml` names a slot that does not exist |
| `MalformedTemplate` | a `template.pptx` is not one |
| `ImageNotFound` | a path, a URL, or something outside the roots |
| `UnreadablePackage` | **not** `InvalidInput`; nothing a deck names fixes it |
Source: https://markdown-office.github.io/presentation/errors/
---
## How directives work
Both halves of the suite read the same syntax, and it is
[Generic Directives](https://github.com/markdown-office/commonmark-generic-directives) — one
`league/commonmark` extension, implemented in `markdown-office/commonmark-generic-directives`.
A directive is a run of colons, a name, and optionally a label and some attributes. **The number
of colons is the whole difference between the three shapes**, and it is what makes the syntax
readable at a glance:
| Shape | Written | Contains | Node |
| --- | --- | --- | --- |
| Text | `:name[label]{attrs}` | nothing — it sits in a paragraph | `DirectiveInline` |
| Leaf | `::name[label]{attrs}` | nothing — it is one line | `DirectiveBlock` |
| Container | `:::name{attrs}` … `:::` | any Markdown | `DirectiveBlock` |
There is no fourth shape. Four spaces of indent is a code block in CommonMark, not a directive,
and `{…}` is a modifier available on all three shapes rather than a shape of its own.
## What each half does with a name
Here the two packages make opposite decisions, and both are defensible.
**Markdown Word is permissive.** It draws exactly two directives — `::toc` and `:footnote` — and
renders every other name as its own source text. A document nobody intended as a set of
directives keeps working, and the text an author wrote is what a reader sees.
**Markdown Presentation has a closed set.** Anything outside it fails the build with a message
naming what would have worked. A deck is a thing a person presents, and a slide with the wrong
thing on it is worse than a deck that did not build.
Both then agree on one thing: an unrecognised directive is never silently dropped.
- [Word directives](/word/directives/) — `::toc`, `:footnote`, and the rest rendered as text.
- [Presentation directives](/presentation/directives/) — `::click`, `:::click`, `::column`,
`::note`, and the refusals.
- [The syntax itself](/directives/syntax/) — every rule, including the ones that surprise people.
## The exact grammar
Names are a letter followed by letters, digits, `_` and `-`. There are no namespace prefixes in
the document syntax: `w:toc` is not a thing, and a name that needs one is two names.
````md word "A directive nobody claims is written out as it was typed"
Status :green[go] before lunch.
:::note
This container is not one this library draws.
:::
::somethingelse{key=value}
````
Every line above parses. None of them is a Word directive, so none of them is drawn — they are
written into the document exactly as typed, which is what the output above is showing.
## Registering your own
The directives package is usable on its own. A renderer over the node, and a registry if you want
the parser to refuse what you have not listed:
```php
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use MarkdownOffice\CommonMark\Directive\Node\DirectiveBlock;
use MarkdownOffice\CommonMark\Directive\Directive\DirectiveRegistry;
use MarkdownOffice\CommonMark\Directive\GenericDirectiveExtension;
final class CalloutRenderer implements NodeRendererInterface
{
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
\assert($node instanceof DirectiveBlock);
return sprintf(
'',
\htmlspecialchars($node->getAttributes()->getString('type', 'note')),
$childRenderer->renderNodes($node->children()),
);
}
}
$environment->addExtension(new GenericDirectiveExtension(
new DirectiveRegistry(['callout', 'badge:text']),
));
$environment->addRenderer(DirectiveBlock::class, new CalloutRenderer(), 10);
```
`match` is written without a leading backslash above for the same reason it is in this project:
`\match` is a parse error, because PHP lexes it as a namespaced name rather than the keyword.
**A registry with entries is strict.** An empty one is permissive, which is what the RFC
describes and what both halves of this suite rely on. The mode worth running in is the strict one,
when a machine is writing the Markdown: the alternative to an error is a document that renders
without the block it asked for and says nothing about why.
Note the `:text` suffix in `'badge:text'` — that is how a registry narrows a name to one shape.
It is a **registry** spelling, not a document one.
Source: https://markdown-office.github.io/directives/
---
## The directive syntax
{% raw %}
This page is the syntax itself, without either library in it. It is implemented by
`markdown-office/commonmark-generic-directives` and it is what both halves of the suite parse.
## Three shapes, and the colons are the difference
| Shape | Written | Applies | Contains |
| --- | --- | --- | --- |
| Text | `:name[label]{attrs}` | inline | nothing |
| Leaf | `::name[label]{attrs}` | block, one line | nothing |
| Container | `:::name{attrs}` … `:::` | block | any Markdown |
A leaf has **no body**. It is one line, and the line after it is not part of it. That is
deliberate: a leaf written on the line before a paragraph would otherwise swallow the paragraph.
```markdown
::image[Chart]{src="a.png"}
The paragraph below is not part of the leaf.
```
## Names
```php
DirectiveSyntax::NAME // '[A-Za-z][A-Za-z0-9_-]*'
```
A letter, then letters, digits, `_` and `-`. A name may not start with a digit, and `.` and `:`
are not name characters — so `::2fast` is text, not a directive.
There are **no namespace prefixes in document syntax**. `w:toc` is not a directive; it is text.
Attribute *keys* may contain `:` and `-`, which is a different thing:
```markdown
::note{data-x=1 xml:lang="en"}
```
parses, with two attributes: `data-x` and `xml:lang`.
## Labels
One pair of square brackets, and they do not nest:
```php
DirectiveSyntax::LABEL // '(?:\[([^\]\[]*)\])?'
```
A `[` inside a label has no closing partner, so the directive is left as literal text — which is
the recoverable outcome. There is no escape character anywhere in this grammar, so there is no
way to write a `]` in a label.
## Attributes
Four forms, and they are all of them:
```text
{width=50%} bare value, runs to whitespace
{title="a b c"} double-quoted, so spaces survive
{title='a "b" c'} single-quoted, so double quotes survive
{wide} no value at all — present and true
{#lead .terse} shorthand: # sets id, . appends to class
```
Three edge cases that catch people out:
- **`{wide=}` is the empty string, not `true`.** `{wide}` is `true`; `{wide=}` is `''`. A bare key
with no equals sign is the one that means true.
- **No whitespace around `=`.** `{a = 1}` is two bare attributes called `a` and `1`, both true.
- **A bare value stops at whitespace, `{` or `}`.** `{expr=a=b}` is one attribute whose value is
`a=b`.
Shorthands may also be written bare after the name, and must touch the braces:
```markdown
::note#lead.terse{title="x"} parses: id=lead, class=terse, title=x
::note #lead .terse {title="x"} does not parse
```
Where both are given, the braces win: `{#fromBraces}` beats a bare `#fromBare`.
## Fences
A container closes on a line of **at least as many** colons as opened it:
```markdown
:::: outer
::: inner
:::
::::
```
Four colons to open, three nested, three to close the inner, four to close the outer. A `:::`
inside does not close a `::::`.
The opening line may also end in colons, which mean nothing — as long as something else is on the
line, because `:::` alone is how a container closes:
```markdown
:::::::::::: SPOILER ::::::::::::We're going to spoil it in three
easy steps:
::::::::::::::::::::::::::::::::::
```
A container with **no name** is legal, and only with three or more colons:
```markdown
::: {.myClass}
some markdown
:::
```
The name has to be there for a leaf, because a leaf has nowhere to put the name otherwise — an
empty element in place of what the author wrote is not a recoverable outcome. `:: {.myClass}` is
literal text.
## What is deliberately not a directive
- `Note: prose` — a colon before a space
- `3:2`, `10:30` — a colon between digits
- `status:error` — a word before the colon
- ` :::a` — four spaces of indent, which is a code block in CommonMark
- anything inside a fenced or indented code block
The fence case is structural rather than a list: CommonMark does not look for new block starts
inside a block that is not a container, so a `:::` inside a fence is code before this extension
is ever consulted.
## The one thing worth knowing before you rely on it
**`__toString()` does not round-trip a value containing a double quote.** It writes `\"`, and the
reader has no escape handling, so the value comes back split:
```php
in : {q="say \"hi\""}
out : {q="say \"hi\""}
back: {q="say \"", hi=true} ← corrupted
```
The package documents the round trip on its own example, which has no quotes in it and does round
trip. The general claim is not true, and this page says so rather than repeating it.
{% endraw %}
Source: https://markdown-office.github.io/directives/syntax/
---
## Images
## The decision
| | |
| --- | --- |
| **Carried** as-is | GIF, JPEG, PNG |
| **Converted** to PNG | anything GD can decode — WebP, BMP |
| **Rasterised** | SVG, when `ext-imagick` can draw it |
| **Refused** | anything else, anything unreadable, anything too large |
A format nothing here can decode is **refused by name**, with a message naming the format. It is
never drawn as a placeholder, because a deck with a picture missing is the one outcome this suite
refuses to produce anywhere.
Alpha survives a conversion: the alpha channel is kept and the result is written as PNG.
## The ceiling
**16,777,216 pixels** — 4096 by 4096 — on both halves, enforced on the *header*.
This is worth a moment, because the obvious implementation of it is wrong. Measuring a raster
means decoding it, and decoding a picture is what allocates the memory you were trying to bound.
For a vector it is worse: a file of **152 bytes** declaring a 16000-square drawing is a file that
wants a 1.9 GB canvas before anything has decided whether it may have one.
So both halves read the size without drawing:
```text
152-byte SVG -> refused in 0.05s, 4 MB peak
```
which is what `pingImage()` is for. It reads the header and stops, and the header is worth
trusting because the delegate sizes the canvas from the same header: a file declaring
`width="10"` beside a `viewBox` of 9000 is drawn at 10, not at 9000.
**There is no second line of defence**, and it is worth saying why rather than shipping one.
ImageMagick's own resource limits do not bound an SVG render: measured against a 4000-square
file, `-limit area 1000 -limit memory 4MiB -limit map 4MiB` on the command line produced the
picture anyway. An area limit would have read as protection and been nothing.
## SVG
Word and PowerPoint both hold a vector only *beside* a raster of it, so something has to draw the
raster. GD has no entry point for a vector in any PHP version, so `ext-imagick` is the only
rasteriser here — and it is in `suggest`, not `require`.
```json
"ext-imagick": "Embeds SVG. PowerPoint holds a vector only beside a raster of it, so
something has to draw the raster; without this an SVG raises rather than going in as a picture
no reader can show."
```
Without it, an SVG is refused by name rather than rasterised badly.
**A build is not just "is the extension loaded".** `SvgRasteriser::available()` renders a probe
and checks for ink. A build of ImageMagick with no fonts configured answers `--version` perfectly
and draws every `` as nothing; that is a real configuration found on a real machine, and the
probe is what catches it.
### What SVG costs you
The part in the package is a **PNG**, not the vector. What appears in the deck is what this
rasteriser drew, not what the file described — and:
- A `` is in the picture only as far as the delegate draws a letter.
- **An SVG carrying its own font as a data-URI `@font-face` is silently substituted.** The render
comes out identical to the "that family does not exist" fallback. Nothing detects this; it is
documented here because it costs an afternoon otherwise.
Source: https://markdown-office.github.io/images/
---
## Errors in general
An exception is only useful if it says what to do next. Every message in this suite names the
thing that would have worked instead, and the pages below are those messages in one place.
## The one to catch
**`MarkdownWord\Exception\InvalidConfiguration`** is the one type to catch for the Word half.
`UnknownConfigurationKey` covers the key half — a key nobody reads — and
`InvalidConfigurationValue` covers the rest. Both extend it, and both extend `Exception\InvalidInput`.
```php
use MarkdownWord\Exception\Exception as WordException;
use MarkdownWord\Exception\InvalidConfiguration;
try {
(new MarkdownToWord($markdown))->save('out.docx');
} catch (InvalidConfiguration $error) {
// Every problem at once, with line numbers and a "Did you mean".
fwrite(STDERR, $error->getMessage());
}
```
**`MarkdownPresentation\Exception\Exception`** covers everything the presentation half reports,
so one `catch` does not also catch the failures of the libraries underneath. The failures a deck
author can fix by naming something else extend `Exception\InvalidInput` below it.
One sits beside those two rather than under `InvalidInput`: `Exception\UnreadablePackage`. Nothing
a deck names fixes a package that is not a package.
## The two `UnknownDirective`
Two unrelated classes share the name. Importing the wrong one is a confusing afternoon:
| Class | Thrown when |
| --- | --- |
| `MarkdownOffice\CommonMark\Directive\Exception\UnknownDirective` | a name is not in a `DirectiveRegistry`, during parsing |
| `MarkdownPresentation\Exception\UnknownDirective` | a Slidev directive or marker is not drawn, during rendering |
Both extend `RuntimeException`; only the second extends `InvalidInput`.
## Messages repeat absolute paths
This is deliberate and there is no switch for it:
```text
Unable to read "ppt/presentation.xml" out of "/Users/you/deck.pptx".
```
The alternative is a message a machine cannot act on. A switch would be something somebody would
eventually get wrong — by turning it on in the place where the paths are harmless and off where
they are the only way to find the file.
## When something is lost rather than refused
Not everything is an exception. A feature a format cannot carry is reported rather than thrown:
```php
$converter = (new MarkdownToWord('notes.md'));
$converter->save('notes.docx');
foreach ($converter->pendingLosses() as $loss) {
echo $loss->feature(), ': ', $loss->explain(), "\n";
}
```
`.docx` carries everything, so `pendingLosses()` is empty. `.odt` and `.rtf` drop named features —
every list item left out of an RTF, an SVG flattened to a raster, a `::toc` left as its text — and
the command line prints them to standard error rather than letting the document quietly lose them.
Source: https://markdown-office.github.io/errors/
---
## Installation
`mdoffice` is the whole suite at a terminal. This page is how to get it; [Basic
use](/command-line/basic/) is how to run it.
## The executables
Every release carries one file per platform, with no PHP and no extensions in it and
nothing to install:
| | |
| --- | --- |
| `mdoffice--macos-arm64` | macOS on Apple silicon |
| `mdoffice--macos-x86_64` | macOS on Intel |
| `mdoffice--linux-x86_64` | Linux on x86-64 |
| `mdoffice--linux-aarch64` | Linux on ARM |
| `mdoffice--windows-x64.exe` | Windows on x64 |
They come from [the releases page](https://github.com/markdown-office/mdoffice/releases),
and the page carries a `SHA256SUMS` file with the checksum of each of them.
**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. Fetching the file with
`curl` does not set the flag:
```bash
curl -L -o mdoffice https://github.com/markdown-office/mdoffice/releases/latest/download/mdoffice-0.1.0-macos-arm64
chmod +x mdoffice
```
If the dialog appears on a file that was fetched some other way, the flag comes off
like this, and the program runs:
```bash
xattr -d com.apple.quarantine mdoffice
```
The macOS executables are signed with an ad-hoc signature, which is not a developer
certificate and not notarised. It is what an arm64 Mac needs — an unsigned arm64
binary is killed by the kernel — and nothing more.
## The archive
The same program as a PHAR, for a machine that has PHP and would rather not download
a second copy of it:
```bash
curl -L -o mdoffice.phar https://github.com/markdown-office/mdoffice/releases/latest/download/mdoffice-0.1.0.phar
php mdoffice.phar word notes.md
```
The archive carries both converter libraries inside it. It needs PHP 8.2 or newer with
`ext-dom`, `ext-gd`, `ext-mbstring`, `ext-xmlwriter` and `ext-zip`.
## Composer
```bash
composer require markdown-office/mdoffice
```
That 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`](https://github.com/cweagans/composer-patches),
which Composer only runs when the project allows it:
```json
{
"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: the package carries the stability it needs itself,
and every dependency it resolves to is a stable release.
## Homebrew
```bash
brew tap markdown-office/mdoffice
brew install mdoffice
```
The tap is [markdown-office/homebrew-mdoffice](https://github.com/markdown-office/homebrew-mdoffice),
and the formula carries the checksum of the file it installs. It installs the
binary for the machine it is on: Apple silicon or Intel, x86-64 or ARM.
## winget
```powershell
winget install markdown-office.mdoffice
```
The manifest is written from the release rather than by hand, so it carries the
checksum of the file it names.
Source: https://markdown-office.github.io/command-line/installation/
---
## Basic use
`mdoffice` is the whole suite at a terminal. It is a program in its own right,
[`markdown-office/mdoffice`](https://github.com/markdown-office/mdoffice), which requires both
converter packages and calls them — so what it does is what those libraries do.
The options of each half are on a page of their own: [For word](/command-line/word/) and [For
presentations](/command-line/presentations/). How to get the tool is on [Installation](/command-line/installation/).
```bash
mdoffice word README.md # writes README.docx
mdoffice word README.docx # writes README.md
mdoffice slides deck.md # writes deck.pptx
```
## Parts
The first argument says which converter the run is for.
| Part | Does |
| --- | --- |
| `mdoffice word …` | Markdown to `.docx`, `.odt` and `.rtf`, and `.docx` back to Markdown |
| `mdoffice slides …` | Markdown to `.pptx` |
**A bare file is not a run.** `mdoffice notes.md` is refused, and the parts are listed:
```text
mdoffice: Unknown part "notes.md".
mdoffice — one of:
word Markdown to .docx, .odt and .rtf, and .docx back to Markdown
slides Markdown to .pptx
```
The Word half decides the direction from the file's first bytes, but with more than one converter
here a 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.
## Commands
| Part | Command | Does |
| --- | --- | --- |
| `word` | `to-docx` | Markdown to `.docx` |
| `word` | `to-odt` | Markdown to `.odt` |
| `word` | `to-rtf` | Markdown to `.rtf` |
| `word` | `to-markdown` | a Word document to Markdown |
| `slides` | `to-pptx` | Markdown to `.pptx` |
In the Word part, with no command, the direction is detected. `--to` overrides it: `docx` or
`word` for a Word document, `odt` for an OpenDocument Text, `rtf` for Rich Text Format, and
`markdown` or `md` for Markdown. `opendocument` and `rich text` say the same as `odt` and `rtf`.
The slides part has one command, so `mdoffice slides deck.md` is the same run as `mdoffice slides
to-pptx deck.md`.
A command named on the command line decides the direction itself, and `--to` is then only allowed
to agree with it:
```text
mdoffice word: --to docx does not match what this reads; it takes odt.
mdoffice word: Omit it and let mdoffice word work the direction out from the file.
```
The two forms do not take quite the same options. With no command named, the direction is worked
out from a list of its own, and that list does not carry `--theme` or `--require-control`:
```text
mdoffice word: Unknown option "--theme".
mdoffice word: Run `mdoffice help` to see what each command accepts.
```
Naming the command takes them, so `mdoffice word to-docx notes.md --theme corporate` is the way
to write it.
Every command answers `--help` on its own, and every part answers `help`:
```bash
mdoffice help
mdoffice word help
mdoffice word to-docx --help
mdoffice slides to-pptx --help
```
## Exit codes
`0` on success, and `1` for anything that did not finish — whether the cause is a mistake the
caller can put right or a defect they cannot. A mistake is printed as the sentence alone,
because it is the caller's to fix:
```text
mdoffice word: The file "nosuch.md" does not exist.
mdoffice word: Name the file to read, or omit it to read standard input.
```
A defect is printed with its class and the line it happened on, because it is nobody's to fix and
somebody's to see: `mdoffice word: : `, and then the file and the line
underneath it.
## Things it is easy to get wrong
- `--output=x`, `--output x` and `-o x` are the same. Leading dashes are stripped before the
lookup, so even `-o=x` works.
- Everything after a bare `--` is an operand, and a lone `-` is standard input.
- An option no command takes is named, and the hint says which command it does belong to:
```text
mdoffice word: The option "--media" does not apply here.
mdoffice word: It belongs to `to-markdown`.
```
- An option no command takes at all is refused with the way out:
```text
mdoffice word: Unknown option "--nope".
mdoffice word: Run `mdoffice help` to see what each command accepts.
```
## Pipes
```bash
cat notes.md | mdoffice word to-docx - -o - | pbcopy
cat deck.md | mdoffice slides to-pptx - -o - | pbcopy
```
Reading standard input and writing standard output both work, which makes it a filter rather than
a command that has to be handed a filename. The result leaves through standard output and
everything else goes to standard error, so a pipe gets a document and not a progress line.
## What is lost
A run that dropped something says so on standard error, naming the feature:
```text
mdoffice word: odt cannot carry table-borders: a table comes out with no borders
```
The Word half reports what a target format cannot carry. The slides half reports what it had to
shrink to make fit — a block that is clamped rather than drawn over whatever comes next, so the
file itself says nothing about it.
## `--version`, and which library produced the document
An installed copy answers with its own version and the two libraries it holds:
```bash
$ mdoffice --version
mdoffice 0.1.0
markdown-office/markdown-word 0.2.0
markdown-office/markdown-presentation 0.2.0
```
The tool versions itself separately from the libraries it holds, because it is neither of them: a
CLI-only change is a reason to release, and one version across three repositories would mean a
Word-only fix forcing a presentation release. The libraries are read from the install rather
than declared, so a `--version` and the files beside it cannot disagree — and a checkout
answers with the branch it resolved rather than with a release, because that is what it holds.
`mdoffice word --version` reports the Word half alone, on one line:
```bash
$ mdoffice word --version
mdoffice word 0.2.0
```
Source: https://markdown-office.github.io/command-line/basic/
---
## For presentations
The slides part has one command, so `mdoffice slides deck.md` is the same run as `mdoffice slides
to-pptx deck.md`. The options it takes are here; the options the Word part takes are on [For
word](/command-line/word/).
## Options for `to-pptx`
| Option | Value | Default |
| --- | --- | --- |
| `-o, --output ` | path | beside the input; `-` for standard output |
| `--strict` | flag | off — a deck that does not fit is shrunk to fit |
| `--image-base ` | path; may be repeated | the deck's own directory |
| `-h, --help` | flag | off |
A deck is read with the Slidev-compatible dialect, so a `---` line closes the slide being read
and `---` on its own opens the frontmatter block.
There is no `--theme`: the renderer resolves the theme from the deck's own `theme:`.
Source: https://markdown-office.github.io/command-line/presentations/
---
## For word
The Word part decides the direction from the file's first bytes, and `--to` overrides it. The
options it takes are here; the slides part has one command and one set of options, on [For
presentations](/command-line/presentations/).
## Options for `to-docx`, `to-odt` and `to-rtf`
| Option | Value | Default |
| --- | --- | --- |
| `-o, --output ` | path | beside the input; `-` for standard output |
| `-c, --config ` | path | none |
| `--theme ` | name or path | the frontmatter's `theme:` |
| `-t, --template ` | `.docx` or `.dotx` | none |
| `--require-control` | flag | off |
| `--images ` | `embed`, `placeholder`, `skip` | `embed` |
| `--no-images` | flag | shorthand for `--images skip` |
| `--image-base ` | path | the input's directory; the working directory for standard input |
| `--svg-fallback ` | `raster`, `none` | `raster` |
| `--table-width ` | fiftieths of a percent; 0–5000 | `5000`, the full width; `0` leaves the width to Word |
| `--plain` | flag | off |
| `--to ` | `docx`, `word` for `to-docx`; `odt` for `to-odt`; `rtf` for `to-rtf` | the command names the direction |
| `-h, --help` | flag | off |
`--plain` is the fastest thing to try: no code colouring, no quote style, no table borders, no
added spacing.
Only `to-docx` takes `--template`. The others answer:
```text
mdoffice word: A template cannot be filled in for odt.
mdoffice word: A template is a .docx or .dotx, and only `mdoffice word to-docx` renders into one.
```
A document that names its own template does so with `template_file:` in its frontmatter, and that
is read whichever of the three formats the document is converted to — so a document with a
`template_file:` is refused for `.odt` and `.rtf` in the same words. `--template` outranks the
frontmatter.
## Options for `to-markdown`
| Option | Value | Default |
| --- | --- | --- |
| `-o, --output ` | path | beside the input, `.md` |
| `--media ` | path | no extraction |
| `--line-ending ` | `lf`, `crlf` | `lf` |
| `--setext` | flag | off — headings are written as `# Heading` |
| `--no-fence` | flag | off — a monospaced paragraph comes back as a fenced code block |
| `--no-header` | flag | off — the first row of a table is a header row |
| `--to ` | `markdown`, `md` | the command names the direction |
| `-h, --help` | flag | off |
A line ending may also be written `unix`, `n`, `dos`, `windows` or `r`.
Source: https://markdown-office.github.io/command-line/word/
---
## The suite
## There is no umbrella library, and one umbrella tool
The three packages are independent. There is no `markdown-office/office` that depends on all of
them, no shared base class, and no facade: no umbrella **library**. **A consumer installs what
they need** and calls the one class it wants.
```bash
composer require markdown-office/markdown-word
composer require markdown-office/markdown-presentation
composer require markdown-office/commonmark-generic-directives # both of the above already have it
```
That is a deliberate choice rather than an unfinished one. A facade would have to choose a
configuration story for two packages that disagree about what an unrecognised directive should do,
and that choice would then be the only way to use them.
What there *is* is [`markdown-office/mdoffice`](https://github.com/markdown-office/mdoffice): one
umbrella **tool**, a single command line over both converters, requiring both packages and calling
them rather than reimplementing either.
**A shipped program is not a library.** It has no public API for converting anything — a document
is made by running it, not by calling a class — so nothing about it constrains how the packages
below are used on their own, and nothing there is a second way to reach `MarkdownToWord`. A facade
would have been exactly that, and would have carried the configuration choice above. This does
not.
```bash
mdoffice word notes.md # → notes.docx
mdoffice slides deck.md # → deck.pptx
```
The command line is documented in full under [The command line](/command-line/basic/).
## The three
| Package | Markdown in | Document out | Back to Markdown |
| --- | --- | --- | --- |
| `markdown-office/markdown-word` | CommonMark + GFM | `.docx` `.odt` `.rtf` | **yes**, for standard Markdown |
| `markdown-office/markdown-presentation` | Slidev-inspired | `.pptx` | **no** |
| `markdown-office/commonmark-generic-directives` | — | — | — a parser extension, on its own |
## What each one is
| Package | What it is |
| --- | --- |
| [`markdown-office/markdown-word`](/word/directives/) | Markdown to `.docx`, `.odt` and `.rtf`, and `.docx` back to Markdown |
| [`markdown-office/markdown-presentation`](/presentation/directives/) | Markdown to `.pptx`, with a Slidev-compatible splitter |
| [`markdown-office/commonmark-generic-directives`](/directives/syntax/) | the directive syntax, for your own parser if you want one |
Both converters require the directives package at the same constraint, `^0.2.0`, and both use it
permissively — no registry — because the presentation half claims `::click`, `::column` and
`:::click` itself, in front of the generic extension, since the generic grammar has no room for an
argument written after an equals sign.
### The spreadsheet is next
The third half will do the same thing to `.xlsx`: the same frontmatter, the same directive syntax,
the same refusals. It is not written yet, and this page says so rather than linking to a page that
is not there.
What it will share, because the other two do: the same configuration keys, the same way of being
loud about what it will not do, and a build where every example on the page is a workbook the
library actually produced.
## Versions and constraints
| | PHP | Notable |
| --- | --- | --- |
| `commonmark-generic-directives` | `^8.2` | needs `league/commonmark ^2.10` |
| `markdown-word` | `^8.2` | needs `ext-mbstring`, `ext-dom`, `ext-gd`, `ext-zip` |
| `markdown-presentation` | `^8.2` | needs `ext-dom`, `ext-gd`, `ext-zip` — **not** `ext-mbstring` |
Installing both means the intersection of their `symfony/yaml` constraints: `^7.4 || ^8.0`. A
project on 7.2 or 7.3 cannot install the pair together.
`ext-imagick` is in `suggest` for both, and buys SVG. See [Images](/images/).
The presentation package pins `phpoffice/phppresentation` to a **commit on `dev-master`**, not a
tag, because the fixes it needs are not in a release. That is why `composer validate` runs without
`--strict` there: the pin is a warning, and the warning is the point.
## The conventions they share
- MIT, and `declare(strict_types=1)` in every file.
- Every exception message names what would have worked instead.
- Every example in every README is a test.
- Both converters walk the **syntax tree**, never flattened HTML — so what lands in the document is
the document the Markdown described.
Source: https://markdown-office.github.io/suite/