# Markdown Office — complete documentation One Markdown document becomes a Word document or a PowerPoint deck, in PHP, with no Office and no binaries. --- ## Markdown, into the file you actually send
{{ wordmark | safe }}
# Markdown, into the file you actually send A finished `.docx` or `.pptx`, from a Markdown file, in PHP. No Office installed, no headless browser, no export button.
## What each one does | | **Markdown Word** | **Markdown Presentation** | | --- | --- | --- | | **Markdown → document** | `.docx` `.odt` `.rtf` | `.pptx` | | **document → Markdown** | **yes**, for standard Markdown | **no** — one direction only | | **Markdown dialect** | CommonMark + GitHub Flavored | Slidev-inspired | | **Look and feel** | frontmatter + themes | frontmatter + themes | | **Special features** | directives | directives | | **What those are** | `::toc`, `:footnote` | `::click`, `:::click`, `::column`, `::note` | | **What it will not do** | a `:::toc` body, a one-line code block's language, a theme in a `.dotx` | a Slidev directive, `---` as a rule, an image over 16 megapixels | Two of those lines are worth reading twice. **Word converts both ways.** A `.docx` goes back to Markdown, and for standard Markdown it goes back well — headings, emphasis, code, lists and tables all survive. Directives are the exception and are written down rather than glossed over: a `::toc` comes back as the text *"Press F9 to build the table of contents"*, which is what the document actually said, and a `:footnote[…]` comes back as itself because it was never a footnote in the first place. See [Round trips](/word/round-trip/). **PowerPoint is one direction.** There is no reader, no `.pptx` back to Markdown, and no plan to add one: a deck is a thing you present, and the interesting inverse problem — what someone edited in PowerPoint should become Markdown — is a different and much larger problem than the one this package solves.
The suite
Start here
## Every example here is a real document Each one is converted by the library it documents while this page is built, and can be downloaded and opened. If an example stops working, the build stops — which is how this site came to say that `:footnote[…]` produces no footnote. ## Read it over HTTP Nothing here needs a browser or a round trip. The same three files are at [`llms.txt`](/llms.txt), [`llms-full.txt`](/llms-full.txt) and [`pages.json`](/pages.json), and every page is available as Markdown at its own URL. ## Requirements PHP 8.2 or newer, with `ext-dom`, `ext-gd` and `ext-zip` — plus `ext-mbstring` for the Word half. `ext-imagick` is optional and buys SVG; without it an SVG is refused by name rather than quietly rasterised badly. Two extensions are asked for by the dependency rather than by this code, and only matter when the extractions are compiled from source: `ext-zip`, which every normal PHP install bundles, and `ext-xmlwriter`, which PHPWord uses to write a document and which `ext-xml` does **not** provide. A from-source build that omits either produces an archive that reports its version and then cannot write anything. The command line needs none of this if you take a release instead: each executable carries its own PHP. See [the command line](/command-line/basic/). Source: https://markdown-office.github.io/ --- ## Markdown Word The dialect, the command line, and the library. In that order, because each one assumes the one before it. ```bash composer require markdown-office/markdown-word ``` PHP 8.2 or newer, with `ext-dom`, `ext-gd`, `ext-mbstring` and `ext-zip`. `ext-imagick` is optional and buys SVG. --- ## 1. The Markdown dialect ### Basic **CommonMark, plus GitHub Flavored Markdown.** Headings, emphasis, code, links, images, lists, block quotes, tables, strikethrough and task lists — all of it, and all of it walked off the syntax tree rather than flattened through HTML. ````md word # Quarterly report Some **strong emphasis** and some *emphasis*, plus `inline code` and a [link](https://commonmark.org/). - a list - with two items > A block quote, indented in the way CommonMark indents it. | Region | Revenue | | ------ | ------: | | North | 1,204 | --- Something struck through, and something done: ~~old~~ / new. ```` ### Extended: frontmatter and themes A document configures itself. Frontmatter at the top, the behaviour under `options:`, the look under `styles:` or `theme:`. **The one thing to know first.** The default parser does **not** read frontmatter. A document opening with `options:` has that block rendered as body text and the configuration ignored — silently. The command line always reads it; in PHP you name the extensions, and [§3](#3-as-a-library) shows how. ````md word --- options: maxHeadingLevel: 2 tableWidth: 5000 theme: corporate --- # Quarterly report Frontmatter configures the document that contains it, so the same file renders differently in two pipelines without two copies of the file. ## Highlights | Region | Revenue | Change | | ------- | ------: | -----: | | North | 1,204 | +8.2% | | South | 988 | -1.1% | | East | 1,533 | +12.4% | ## Notes The `theme:` key names a look that ships with the library. Everything under `options:` and `styles:` is yours and overrides it. ```` #### Every frontmatter key | Key | Type | Default | | | --- | --- | --- | --- | | `options:` | mapping | absent | handed to `Options::fromArray()` | | `styles:` | mapping | absent | handed to `new Styles(...)` | | `theme:` | string | absent | a named look, or a path to one | | `template_file:` | string | absent | a `.docx` or `.dotx` to render into | | `fields:` | mapping | `{}` | values for the template's `MERGEFIELD`s | | `header:` | mapping | empty | the running head | | `footer:` | mapping | empty | the running foot | Only `options:` and `styles:` are validated. Every other key is open-ended metadata, so a document can carry `title:` for a pipeline downstream that cares and be none the worse. #### Every option | Key | Type | Default | Values | | --- | --- | --- | --- | | `softBreak` | string | `space` | `space`, `lineBreak`, `paragraph` | | `hardBreak` | string | `line` | `remove`, `line`, `paragraph` | | `html` | string | `strip` | `strip`, `preserve`, `drop` | | `images` | string | `embed` | `embed`, `placeholder`, `skip` | | `imageBasePath` | string | `null` | where relative paths resolve from | | `imageMaxWidth` | number | `15.0` | centimetres, ≥ 0 | | `svgFallback` | string | `raster` | `raster`, `none` | | `maxHeadingLevel` | integer | `6` | clamped to 1–6 | | `orderedListFormat` | string | `decimal` | any OOXML `w:numFmt` | | `orderedListSuffix` | string | `tab` | `tab`, `space`, `nothing` | | `tableBorders` | boolean | `true` | | | `tableHeaderBold` | boolean | `true` | off makes a round trip honest about headers | | `tableWidth` | integer | `5000` | fiftieths of a percent, 0–5000 | | `codeBlockShading` | boolean | `true` | | | `linkTarget` | string | `_blank` | | | `thematicBreak` | string | `border` | `border`, `text` | | `deferredHyperlinks` | boolean | `false` | | #### Style slots Each slot holds a **style id** (a string), an **inline style** (a mapping), or `null`. A mapping merges recursively into the default; a style id or `null` replaces the slot whole. | Slot | Default | | --- | --- | | `heading.1` … `heading.6` | `Heading1`…`Heading6`, bold, 16/13/12/11/11/11pt, `2F5496` | | `paragraph` | 6pt after, 1.15 line height | | `blockQuote` | `IntenseQuote`, italic, `404040`, 720 twips either side | | `codeBlock` | 360 twips left, no space above or below | | `listParagraph` | 3pt after | | `codeFont` | Consolas 9pt `A31515` | | `linkFont` | `0563C1`, underlined | Font properties are `name`, `size`, `color`, `bold`, `italic`, `strikethrough`, `underline`. Paragraph properties are `indentation`, `space`, `shading`, `keepNext`, `alignment`, `styleName`, `lineHeight`. A colour is **six hex digits with no `#`** and no colour name. A size is a number ≥ 0.001, and `0` is refused with the reason: it is a size in points, and 0 makes the text invisible in Word. #### Headers and footers Both take `body`, `align` (`left`/`center`/`right`), `border` (`top`/`bottom`/`both`), and the switches `page`, `total`, `chapter`, `filename`, `date`, `firstPage`, `even`. A `body` may contain `{page}`, `{total}`, `{filename}`, `{chapter}` and `{date}`. ```yaml footer: align: center border: top body: "Page {page} of {total}" page: true total: true ``` #### Themes A theme is a directory: `theme.yml` — `name` required, plus `description` and `author` — and an optional `styles.yml`. Four ship: `default`, `minimal`, `corporate`, `report`. Resolution is a path, then `/themes/`, then the package's own `themes/`, and **there is no silent fallback** — a name that does not resolve raises `ThemeNotFound` listing the ones that do. ### Extended: directives Two directives, and both are real Word features rather than Markdown dressed up. **`::toc`** — a table of contents Word can build. The field goes out empty and marked dirty, with `w:updateFields` set, because PHPWord's cached page numbers are all `1` and a reader who never updates a field shows them as they are. | Attribute | Type | Default | | --- | --- | --- | | `depth` | integer | `maxHeadingLevel`, which is `6`, clamped to 1–6 | | `numbers` | boolean | `true`; `false` is Word's `\n` | | `title` | string | none — a heading drawn above the field | ````md word # Quarterly report ::toc{depth=2, title="Contents"} ## Highlights The figure was provisional and has since been confirmed. ## Method Word will build the table on open. If it does not, select it and press F9. ```` The field instruction it writes is exactly `TOC \o "1-2" \h \z \u`, and with `numbers=false` a `\n` before the `\h`. **`:footnote[…]`** — a Word footnote. The text in the brackets is the note, inserted as text rather than parsed as Markdown. There are no attributes. > **Measured, and it does not currently reach a footnote.** Building > `Body with a note:footnote[Source: the ledger.]` produced a `.docx` with **no > `word/footnotes.xml` in it at all**, and the literal text `:footnote[Source: the ledger.]` in > the body. This page says so rather than describing the intended behaviour, because the build on > this site checks the difference. **Everything else** parses and is written into the document as the text the author typed. `Status :green[go] before lunch.` parses, and reaches the document exactly as written. That is the opposite of the presentation half's policy, and deliberate — a parser nobody asked to change should not change the syntax of every document converted after it. More, including what a `:::toc` container does: [Word directives](/word/directives/). --- ## 2. Command line The command line for this half is [`mdoffice`](https://github.com/markdown-office/mdoffice), one program over both converters. It is not part of this package: this is a library, and `vendor/bin/mdword` no longer exists. What it does is what this library does. ```bash mdoffice word README.md # writes README.docx mdoffice word README.docx # writes README.md ``` It decides which way the data has to go **from the content, not the name**: a Word document is a zip and Markdown is text, and the four bytes that say which is which are part of the format. | Command | Does | | --- | --- | | `to-docx` | Markdown to `.docx` | | `to-odt` | Markdown to `.odt` | | `to-rtf` | Markdown to `.rtf` | | `to-markdown` | a Word document to Markdown | | 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` | | `--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 | `to-markdown` takes `--media `, `--line-ending `, `--setext`, `--no-fence` and `--no-header` instead. A filter, not a command that needs a filename: ```bash cat notes.md | mdoffice word --to docx - -o - | pbcopy ``` Exit `0` on success, `1` for a bad input or a configuration problem, printed as the sentence alone because it is the caller's mistake to fix. A defect is a different thing, and it is printed with its class and the line it happened on. More, including the other half, the downloads and the error messages: [The command line](/command-line/basic/). ## 3. As a library Four lines to the first document. ```php use MarkdownWord\MarkdownToWord; (new MarkdownToWord('notes.md'))->save('notes.docx'); ``` `MarkdownToWord` takes either a path or the text. A string naming a file that exists is read from it; anything else is taken as the content. Three formats from one converter: ```php use MarkdownWord\Format; use MarkdownWord\MarkdownToWord; $converter = new MarkdownToWord('notes.md'); $converter->save('notes.docx'); $converter->convertTo(Format::Odt, 'notes.odt'); $converter->convertTo(Format::Rtf, 'notes.rtf'); ``` ### The parser, if you want frontmatter ```php use League\CommonMark\Extension\FrontMatter\FrontMatterExtension; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use MarkdownOffice\CommonMark\Directive\GenericDirectiveExtension; use MarkdownWord\MarkdownToWord; use MarkdownWord\Parser\CommonMarkParser; $parser = new CommonMarkParser([ GithubFlavoredMarkdownExtension::class, GenericDirectiveExtension::class, FrontMatterExtension::class, ]); (new MarkdownToWord($markdown, parser: $parser))->save('out.docx'); ``` Both, named. `withAllExtensions()` is not the answer either — it reads frontmatter and drops directives, so `::toc` and `:footnote` stop being directives and become the text an author wrote. The available flavours are `commonMarkOnly()`, `extended()`, `directives()` (the default) and `withAllExtensions()`. Strict mode — a `DirectiveRegistry` that refuses unknown names — is **not reachable** from `CommonMarkParser`, because the extension list is a list of class names and each is built with `new`. ### Back the other way ```php use MarkdownWord\MarkdownToWord; use MarkdownWord\WordToMarkdown; (new MarkdownToWord('notes.md'))->save('notes.docx'); (new WordToMarkdown('notes.docx'))->save('notes.md'); ``` Both directions implement `MarkdownWord\Converter`, whose whole surface is `convert()` and `save()`. The constructor takes a path **or** the bytes. For standard Markdown the round trip is close to lossless. Directives are the exception and are written down rather than glossed over — see [Round trips](/word/round-trip/). ### What you can ask the converter ```php $converter = (new MarkdownToWord('notes.md')); $converter->save('notes.docx'); $converter->pendingLosses(); // what the format could not carry $converter->pendingImageConversions(); // what was re-encoded, and to what $converter->pendingHyperlinks(); // for deferred rendering ``` `pendingLosses()` is empty for `.docx`, which carries everything. `.odt` and `.rtf` drop named features and report them rather than letting the document quietly lose them. ### Errors One type to catch: `MarkdownWord\Exception\InvalidConfiguration`. `UnknownConfigurationKey` for the key half and `InvalidConfigurationValue` for the rest both extend it. ```php use MarkdownWord\Exception\InvalidConfiguration; try { (new MarkdownToWord($markdown))->save('out.docx'); } catch (InvalidConfiguration $error) { fwrite(STDERR, $error->getMessage()); // every problem at once, with "Did you mean" } ``` The whole catalogue is on [Errors](/word/errors/). --- ## Next [Markdown Presentation](/presentation/) · [The directive syntax](/directives/syntax/) · [Word directives](/word/directives/) · [Images](/images/) · [Round trips](/word/round-trip/) · [Errors](/word/errors/) Source: https://markdown-office.github.io/word/ --- ## Word directives Markdown Word draws **two** directives. Both are real Word features rather than Markdown dressed up, which is the reason they exist at all. ## `::toc` — a table of contents Word can build A block directive. Word's `TOC` element cannot draw one of these: it writes a `PAGEREF` for every title registered, and this library never registers any — it draws a heading as a paragraph, not through `addTitle()`. With none registered it writes a closing field character and no opening one, so the document carries a field that starts nowhere. The field is therefore built directly. ### Attributes | Attribute | Type | Default | Notes | | --- | --- | --- | --- | | `depth` | integer | `maxHeadingLevel`, which is `6` | clamped to 1–6 | | `numbers` | boolean | `true` | `false` is Word's `\n` | | `title` | string | none | a heading drawn above the field | ```markdown ::toc{depth=2, title="Contents"} ``` Names are matched case-insensitively and trimmed, so `::TOC` and `:: toc` both work. ### What comes out An **empty field, marked dirty**, with `w:updateFields` set so Word builds it on open. Between `separate` and `end` it says: > Press F9 to build the table of contents. That is deliberate. PHPWord's cached page numbers are all `1`, and a reader who never updates a field shows them as they are — a table of contents in which every entry claims to be on page 1 is worse than one that says plainly what to do. `F9` is what both Word and LibreOffice Writer update a field with, so it names the way rather than the menu, which is not the same in either. The field instruction it writes is exactly: ```text TOC \o "1-" \h \z \u ``` and with `numbers=false`, a `\n` before the `\h`. ````md word "A document with a table of contents, a footnote and a link" # Quarterly report Contents below are built by Word on open. ::toc{depth=2, title="Contents"} ## Highlights The figure was provisional and has since been confirmed. ## Notes Word will build the table on open. If it does not, select it and press F9. ```` ### Limitations - **Only `.docx` carries it.** `.odt` and `.rtf` get the text `::toc` instead — and the library says so in `pendingLosses()` rather than letting the document quietly lose a feature. - **Heading styles must be the built-in names.** `LookAndFeel` sets `Heading1`…`Heading6` for exactly this reason; Word collects a table of contents by style name. - **A template gets the source form.** Through a `.dotx` the handler is constructed detached, so the fallback applies and `::toc` is written as a paragraph. ## `:footnote[…]` — a real Word footnote An inline directive. The text in the brackets is the note; there are **no attributes**, and the body is inserted as text rather than parsed as Markdown. ```markdown The figure was provisional[^1] and has since been confirmed. [^1]: Source: the regional ledger, March. ``` Word's own footnote syntax is not what is parsed here. `:footnote[…]` is this suite's spelling. > **Measured, and it does not currently reach a footnote.** Building > `Body with a note:footnote[Source: the ledger.]` with the library's own default parser produced > a `.docx` with **no `word/footnotes.xml` in it at all**, and the literal text > `:footnote[Source: the ledger.]` in the body. The same happened with an explicit parser > carrying `GenericDirectiveExtension`. This page says so rather than describing the intended > behaviour, because the build on this site checks the difference and would fail the example > otherwise. - The mark sits in the run the directive stood in, so no post-pass is needed. - The note is set in the `paragraph` slot's character half, not in whatever heading the mark sits in. - An empty body — `:footnote` or `:footnote[]` — draws the source characters instead. - Only `.docx` carries it; `.odt` and `.rtf` get the text back. ## Everything else Any other name parses and is **written out as its own source text**. That is the policy, and it is the opposite of the presentation package's: ```markdown Status :green[go] before lunch. ``` parses, and `Status :green[go] before lunch.` is what reaches the document. The reasoning is that `Status :green[go]` is a sentence, and a parser nobody asked to change should not change the syntax of every document converted after it. A directive that used to be opt-in became on by default, which was a behaviour change and is written down as one. Two consequences worth knowing: - **The container form of a drawn name is also claimed.** `:::toc` … `:::` matches the block map, the handler returns `true`, and the body is **silently dropped**. There is no test pinning this either way, so treat `::toc` as the only supported spelling. - **Block and inline are separate maps.** `::footnote` is not drawn — footnotes are inline — and comes out as the text the author wrote. ## Strict mode is not reachable from here `GenericDirectiveExtension` takes a `DirectiveRegistry` that makes the parser strict. Through `CommonMarkParser` that mode cannot be reached: the extension list is a list of class names and each is built with `new`, so handing over an instance with a registry on it gets a fresh one without it. If you want strict directives, build the environment yourself — see [How directives work](/directives/). Source: https://markdown-office.github.io/word/directives/ --- ## Configuration ## Read the frontmatter before anything else **The default parser does not read frontmatter.** `CommonMarkParser::DEFAULT_FLAVOUR` is GitHub-Flavored Markdown plus directives, and a document that opens with `---` and an `options:` block has that block rendered as body text — silently, with the configuration it was meant to carry ignored. `withAllExtensions()` is not the answer either: it does read frontmatter, but it leaves directives out, so `::toc` and `:footnote` stop being directives and become the text an author wrote. So both, named: ```php use League\CommonMark\Extension\FrontMatter\FrontMatterExtension; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use MarkdownOffice\CommonMark\Directive\GenericDirectiveExtension; use MarkdownWord\MarkdownToWord; use MarkdownWord\Parser\CommonMarkParser; $parser = new CommonMarkParser([ GithubFlavoredMarkdownExtension::class, GenericDirectiveExtension::class, FrontMatterExtension::class, ]); (new MarkdownToWord($markdown, parser: $parser))->save('out.docx'); ``` The command line always has it — `mdoffice word` adds `FrontMatterExtension` on top of the default flavour — which is why this catches people writing PHP and not people using the binary. ## Top-level keys | Key | Type | Default | What it does | | --- | --- | --- | --- | | `options:` | mapping | absent | handed to `Options::fromArray()` | | `styles:` | mapping | absent | handed to `new Styles(...)` | | `theme:` | string | absent | a named look, or a path to one | | `template_file:` | string | absent | a `.docx` or `.dotx` to render into | | `fields:` | mapping | `{}` | values for the template's `MERGEFIELD`s | | `header:` | mapping | empty | the running head | | `footer:` | mapping | empty | the running foot | Only `options:` and `styles:` are validated. Every other key is open-ended metadata, so a document can carry `title:` or `author:` for a pipeline downstream that cares and be none the worse. ## Every option | Key | Type | Default | Values | | --- | --- | --- | --- | | `softBreak` | string | `space` | `space`, `lineBreak`, `paragraph` | | `hardBreak` | string | `line` | `remove`, `line`, `paragraph` | | `html` | string | `strip` | `strip`, `preserve`, `drop` | | `images` | string | `embed` | `embed`, `placeholder`, `skip` | | `imageBasePath` | string | `null` | where relative paths resolve from | | `imageMaxWidth` | number | `15.0` | centimetres, ≥ 0 | | `svgFallback` | string | `raster` | `raster`, `none` | | `maxHeadingLevel` | integer | `6` | clamped to 1–6 | | `orderedListFormat` | string | `decimal` | any OOXML `w:numFmt` — not validated | | `orderedListSuffix` | string | `tab` | `tab`, `space`, `nothing` | | `tableBorders` | boolean | `true` | | | `tableHeaderBold` | boolean | `true` | off makes a round trip honest about headers | | `tableWidth` | integer | `5000` | fiftieths of a percent, 0–5000 | | `codeBlockShading` | boolean | `true` | | | `linkTarget` | string | `_blank` | not validated | | `thematicBreak` | string | `border` | `border`, `text` | | `deferredHyperlinks` | boolean | `false` | | Booleans may be written as `true`/`false`/`yes`/`no`/`on`/`off`, in any case. ## Style slots Each slot holds a **style id** (a string), an **inline style** (a mapping), or `null`. A mapping merges recursively into the default; a style id or `null` replaces the slot whole. | Slot | Default | | --- | --- | | `heading.1` … `heading.6` | `Heading1`…`Heading6`, bold, 16/13/12/11/11/11pt, `2F5496` | | `paragraph` | 6pt after, 1.15 line height | | `blockQuote` | `IntenseQuote`, italic, `404040`, 720 twips either side | | `codeBlock` | 360 twips left, no space above or below | | `listParagraph` | 3pt after | | `codeFont` | Consolas 9pt `A31515` | | `linkFont` | `0563C1`, underlined | Font properties are `name`, `size`, `color`, `bold`, `italic`, `strikethrough`, `underline`. Paragraph properties are `indentation`, `space`, `shading`, `keepNext`, `alignment`, `styleName`, `lineHeight`. A colour is **six hex digits with no `#`** and no colour name. A size is a number ≥ 0.001, and `0` is refused with the reason: it is a size in points, and 0 makes the text invisible in Word. ## Precedence | Source | Wins over | | --- | --- | | the command line | the frontmatter | | the frontmatter | the config file | | the config file | the defaults | A theme is not a layer — it is folded into the defaults, and your own written slots sit on top of it. ## Headers and footers Both take `body`, `align` (`left`/`center`/`right`), `border` (`top`/`bottom`/`both`), and the switches `page`, `total`, `chapter`, `filename`, `date`, `firstPage`, `even`. A `body` may contain `{page}`, `{total}`, `{filename}`, `{chapter}` and `{date}`. With `dateFormat` set, `{date}` becomes a Word `DATE` field with that picture rather than a fixed date. ```yaml footer: align: center border: top body: "Page {page} of {total}" page: true total: true ``` ## Templates `template_file:` names a `.dotx`. `fields:` fills its merge fields, with `true` and `false` spelled as strings because that is what Word stores. The command line adds `--require-control`, which refuses a template with no content control instead of appending to it. Source: https://markdown-office.github.io/word/configuration/ --- ## Round trips Both directions are the same interface, so a round trip is two calls: ```php use MarkdownWord\Converter; use MarkdownWord\MarkdownToWord; use MarkdownWord\WordToMarkdown; function convert(Converter $converter, string $target): void { $converter->save($target); } convert(new MarkdownToWord('notes.md'), 'notes.docx'); convert(new WordToMarkdown('notes.docx'), 'notes.md'); ``` ## What survives For **standard Markdown** — headings, emphasis, inline code, links, lists, block quotes and tables — it is close to lossless, and it is worth trying before you assume otherwise. Here is a Word document, and the Markdown the reader makes of it. The document is a real file rather than a picture of one, and it can be downloaded and opened: ```word md _documents/quarterly-report.docx ``` The headings, the emphasis, the inline code, the link, the table, the list, the quote and the code block all came back. What follows is what did not. ## Directives do not come back Checked, rather than assumed: | Written | Comes back as | | --- | --- | | `::toc{depth=2}` | `Press F9 to build the table of contents.` | | `:footnote[Source: the ledger.]` | `:footnote[Source: the ledger.]` | The first is **not** a directive and never was on the way back — the document really does contain that sentence, inside the table-of-contents field, waiting for Word to build it. The reader reads the field's content and finds a sentence. The second round trips only by accident: `:footnote[…]` does not currently produce a `footnotes.xml` at all, so what the reader finds is the source text that was always in the body. See [Word directives](/word/directives/). **Neither is a round trip you should build on.** A pipeline that edits documents in Word and reads them back should treat directives as one-way. ## What else does not survive These are the rest of the list. Each one is a property of the formats rather than a bug, and each one is silent unless you look for it. ### A table's header row comes back bold Word records that the runs are bold, not who made them bold: ```markdown | A | B | | - | - | | 1 | 2 | ``` comes back as `| **A** | **B** |`. `tableHeaderBold: false` suppresses it. ### A fenced code block comes back without its language The fence and the text are in the file. **The info string is not** — Word stores no such thing — so detection is purely by monospaced typeface. ### A one-line code block comes back as inline code ```markdown ```php $x = 1; ``` ``` becomes `` `$x = 1;` ``, because fencing requires **two or more** consecutive monospaced paragraphs. One is prose that happens to be monospaced. ### A quote configured as indentation comes back as a paragraph With `blockQuote: null` there is no style id for the reader to match against, so `quoteIndent` only measures depth *inside* an already-identified quote. ### The last line has no newline `"# Title\n"` becomes `"# Title"`. The only exception is `lineEnding: crlf`, which adds one. ### A delimiter row is rewritten Every column comes back as one of exactly three markers: `:--`, `--:` or `:-:`. The alignment survives; the marker does not. ## And PowerPoint has no second direction at all There is no `.pptx` back to Markdown: no reader class, no reverse entry point, and `PresentationRenderer` has two methods — `render()` and `overflows()`. A deck is a thing you present, and reading back what someone edited in PowerPoint is a much larger problem than the one this package solves. ## Reading a document without writing one ```php echo (new WordToMarkdown('report.docx'))->convert(); // as a string (new WordToMarkdown('report.docx'))->save('report.md'); // straight to a file echo (new WordToMarkdown())->toMarkdown($bytes); // bytes already in hand ``` The constructor takes a path **or** the bytes — anything starting `PK\x03\x04` is treated as a document, an existing path is read and must start with it, and anything else is refused. ## What the reader trusts `Reverse\Options` is the whole vocabulary. It is worth knowing because two of its defaults are decisions rather than facts: | Key | Default | Notes | | --- | --- | --- | | `headingStyles` | `['Heading', 'Title']` | case-insensitive, optional trailing level | | `quoteStyles` | `['IntenseQuote', 'Quote', 'BlockQuote']` | **detection by style, not indentation** | | `monospaceFonts` | nineteen names | how a code block is recognised | | `quoteIndent` | `720` twips | per nesting level | | `fenceCodeBlocks` | `true` | | | `tableHeader` | `true` | **the first row**, not `w:tblHeader` | | `headingSetext` | `false` | | | `mediaDirectory` | `null` | only then are images extracted | | `lineEnding` | `"\n"` | | | `maxPartBytes` | 256 MiB | decompressed limit for one part | | `maxEntries` | 4096 | zip entries read | | `maxStyleDepth` | 32 | `basedOn` chain depth | The last three are there because a document is untrusted input: a zip can say a part is small and be otherwise, it can carry more parts than a reader has memory for, and a style table can be a cycle. `tableHeader` reading the **first row** rather than `w:trPr/w:tblHeader` is deliberate: a row Word was told to repeat is a row the author styled as a header anyway, and reading the first row is what survives a document that came from somewhere else. Source: https://markdown-office.github.io/word/round-trip/ --- ## Image options Word holds a picture one of three ways, and the option says which. The formats themselves — what is carried, what is converted, what is refused, and the ceiling on a raster — are the same as the deck's, and are on [Images](/images/). ## The options | Option | Default | | | --- | --- | --- | | `images` | `embed` | `embed`, `placeholder`, `skip` | | `imageMaxWidth` | `15.0` | centimetres | | `svgFallback` | `raster` | `none` for readers that can take the vector | `pendingImageConversions()` reports what was converted, and `pendingLosses()` says when the vector was written with no raster beside it — which means a reader that does not prefer the vector draws nothing at all where an SVG was. Source: https://markdown-office.github.io/word/images/ --- ## Errors Every exception Markdown Word throws, in one place. The one to catch, and the advice that is about both libraries, is on [Errors in general](/errors/). ## Word | Exception | Means | | --- | --- | | `InvalidConfiguration` | the key half; the base for the next one | | `UnknownConfigurationKey` | a key nothing reads | | `InvalidConfigurationValue` | a key whose value is not what it is used as | | `InvalidInput` | the base for input-shaped failures | | `NothingToConvert` | no Markdown was given | | `UnreadableFile` | the input is not a file | | `UnreadableDocument` | the bytes are not a Word document | | `MalformedDocument` | it is one, and it is broken | | `FileNotWritable` | the target cannot be written | | `TemplateNotFound` | `template_file:` names nothing | | `NoContentControl` | `--require-control`, and the template has none | | `MalformedTheme` | a theme file is not shaped like one | | `ThemeNotFound` | a theme name resolves to nothing — the message lists the ones that do | | `UnknownStyleSlot` | `styles:` names a slot that does not exist | | `UnsupportedElement` | something in the document cannot be drawn | | `UnsupportedImageFormat` | see [Images](/images/) | Source: https://markdown-office.github.io/word/errors/ --- ## Markdown Presentation The dialect and the library. The command line for both halves is [`mdoffice`](https://github.com/markdown-office/mdoffice); this page is about what you get without it. ```bash composer require markdown-office/markdown-presentation ``` PHP 8.2 or newer, with `ext-dom`, `ext-gd` and `ext-zip`. `ext-imagick` is optional and buys SVG. **One direction.** Markdown to `.pptx`, and nothing back. There is no reader class, no reverse entry point, and `PresentationRenderer` has two methods: `render()` and `overflows()`. --- ## 1. The Markdown dialect ### Basic **Slidev-inspired.** A deck is Markdown split on `---`. The splitter is a **port of Slidev's `parseSync`** pinned to a commit, so a deck written for Slidev splits the same way here; the 21 places it deliberately does not are written down in [Slidev, and where it differs](/presentation/directives/#slidev-and-where-it-differs). ````md presentation # Markdown Office Convert a Markdown document into a deck that opens in PowerPoint. --- ## Why a slide and not a page - One `---`, one slide - A theme decides the type, the colour and the grid - `::click` builds the slide up on a click ```` Within a slide it is CommonMark plus tables, strikethrough and fenced code. `---` separates slides and **cannot be a thematic break** — use `***`. `----` is also a delimiter, so a YAML document as slide content needs a different fence. ### Extended: frontmatter and themes The deck's own configuration goes in the frontmatter of slide 1. Every key is in a **closed set of seven**, and anything outside it fails the build with a message naming what would have worked. | Key | Type | Default | | | --- | --- | --- | --- | | `title` | string | `null` | the slide's title | | `name` | string | `null` | a fallback for `title` | | `level` | integer ≥ 1 | the heading's own depth | the depth a title is drawn at | | `layout` | one of eight roles | `content` | which layout to use | | `theme` | string | `default` | **slide 1 only** | | `author` | string | the theme's | **slide 1 only** | | `description` | string | `null` | **slide 1 only** | The eight layout roles: `title`, `content`, `two-column`, `section`, `blank`, `image-text`, `image-title`, `image-slide`. ````md presentation --- theme: corporate-dark title: The quarter in three slides author: Markdown Office description: What changed, what it cost, and what happens next. --- # The quarter ## What changed - Two products shipped - Three customers renewed ```` #### The whole of `theme.yml` | Key | Type | Default | | --- | --- | --- | | `name` | string | **required**, non-empty | | `description` | string | `null` | | `author` | string | `null` | | `dark` | boolean | `false` — boolean only; `dark: yes` is refused | | `colors:` | mapping | the twelve slots below | | `fonts:` | mapping | Calibri / Calibri / Consolas | | `style:` | mapping | the geometry below | The twelve colour slots — `dk1` `lt1` `dk2` `lt2` `accent1`…`accent6` `hlink` `folHlink` — with four readable aliases accepted: `dark1`, `light1`, `dark2`, `light2`, `hyperlink`, `followedHyperlink`. #### The whole of `style:` | 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` | | Four themes ship: `default`, `corporate-dark`, `minimal`, `dense`. `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 is a path, then `/themes/`, then the package's own. **No silent fallback to `default`**: a name that does not resolve raises `ThemeNotFound` listing the ones that do. More, including `layouts.yml` and every built-in theme's numbers: [Themes and layout](/presentation/themes/). ### Extended: directives Four, and the set is closed — anything outside it is refused by name rather than drawn as a paragraph. | Written | Does | | --- | --- | | `::click` | advances the build by one step | | `::click=N` | advances to step `N` | | `:::click` … `:::` | everything between the fences is at one step | | `::column` | moves the next blocks into the next column | | `::note` … `::` | the slide's speaker note | All must be alone on their line. Names are case-insensitive. ````md presentation ## Three things - One - Two ::click - Two is the interesting one - Three ::click - All three ::note Say "two" slowly. That is the point of the slide. :: ```` `::click=N` must be greater than the current step and no more than **500**. Step 1 is not a click — it is what is on the slide before anyone clicks — so a step nothing was written on costs no click. `::column` takes **no argument**; the layout decides how many columns there are, and writing more markers than that is `UnsupportedColumn`. The closer for `:::click` is **exactly three colons**. An unclosed region is refused rather than running to the end of the slide. More, including the refusal table and the three things that look like they work and do not: [Presentation directives](/presentation/directives/). --- ## 2. Command line **There is none.** The package declares no `bin`, and `examples/build.php` in the repository is the closest thing — it converts a directory of Markdown files and renders page images of each. A shell loop is the whole substitute: ```bash for f in decks/*.md; do php -r 'require "vendor/autoload.php"; (new MarkdownPresentation\Render\PresentationRenderer()) ->render((new MarkdownPresentation\Document\DeckParser())->parse(file_get_contents($argv[1])), $argv[2]);' \ "$f" "${f%.md}.pptx" done ``` The Word half has a real CLI — see [Markdown Word → command line](/word/#2-command-line). --- ## 3. As a library Two calls, and there is no single facade class. ```php use MarkdownPresentation\Document\DeckParser; use MarkdownPresentation\Render\PresentationRenderer; $deck = (new DeckParser())->parse($markdown); (new PresentationRenderer())->render($deck, 'deck.pptx'); ``` ### Reading the deck without writing it Useful for a linter, an index, or a check that runs before anything is built: ```php $deck = (new DeckParser())->parse($markdown); echo $deck->frontmatterValue('title'), "\n"; echo $deck->count(), " slides\n"; foreach ($deck->slides() as $slide) { echo $slide->index(), ' ', $slide->title(), "\n"; echo $slide->note(), "\n"; } ``` ```text 0 One 1 Two ``` ### The rendering pipeline `render()` checks every slide's frontmatter **before** anything is built, so a deck whose fourth slide names a directive nobody reads fails before it has written a byte. It then resolves the theme, resolves the layout roles, builds the masters, and writes one slide per source slide. Five passes run over the written package afterwards — `NotesColourMapPass`, `NotesChainPass`, `MeasureRoundingPass`, `PlaceholderPass` and `TimingPass` — because PHPPresentation cannot write what PowerPoint needs. The rounding pass exists because a fractional `a:spcPts` value makes PowerPoint open the deck as `[Repaired]`. ### Ceilings | | | | --- | --- | | `MAX_SLIDES` | 1000 | | `MAX_STEP` | 500 | | `MAX_PIXELS` | 16,777,216 — 4096 × 4096, enforced on the file header | The pixel ceiling is worth a moment. The obvious implementation of it is wrong: measuring a raster means decoding it, and decoding is what allocates the memory you were trying to bound. Both halves read the size from the header without drawing, so a **152-byte** file declaring a 16000-square drawing is refused in 0.05 s with a 4 MB peak. More, and the reason there is no second line of defence: [Images](/images/). ### Errors `MarkdownPresentation\Exception\Exception` covers everything this half reports, so one `catch` does not also catch the failures of the libraries underneath. The failures a deck author can fix extend `Exception\InvalidInput` below it. ```php use MarkdownPresentation\Exception\Exception as DeckException; use MarkdownPresentation\Exception\InvalidInput; try { (new PresentationRenderer())->render($deck, 'out.pptx'); } catch (InvalidInput $error) { fwrite(STDERR, $error->getMessage()); // names what would have worked } ``` One sits beside those two rather than under `InvalidInput`: `Exception\UnreadablePackage`. Nothing a deck names fixes a package that is not a package. The whole catalogue is on [Errors](/presentation/errors/). --- ## Next [Markdown Word](/word/) · [The directive syntax](/directives/syntax/) · [Presentation directives](/presentation/directives/) · [Themes and layout](/presentation/themes/) · [Images](/images/) · [Errors](/presentation/errors/) Source: https://markdown-office.github.io/presentation/ --- ## Presentation directives Markdown Presentation splits its vocabulary in two: **frontmatter keys**, which are configuration, and **in-body colon markers**, which are instructions. Both are closed sets, and both fail the build with a message naming what would have worked. ## In-body markers | Written | Shape | What it does | | --- | --- | --- | | `::click` | leaf | Advances the build by one step | | `::click=N` | leaf | Advances to step `N` | | `:::click` … `:::` | container | Everything between the fences is at one step | | `::column` | leaf | Moves the next blocks into the next column | | `::note` … `::` | container | The slide's speaker note | All must be alone on their line. Leading spaces are fine; four spaces of indent is a code block. Names are case-insensitive. ### `::click` — building a slide up ```markdown ## Three things - One - Two ::click - Two is the interesting one - Three ::click=3 - All three ``` `::click` advances by one. `::click=N` jumps to step `N`, and `N` must be greater than the current step and no more than **500**. Step 1 is not a click — it is what is on the slide before anyone clicks — so a step nothing was written on costs no click, and the first `::click` after a heading is the first click. ````md presentation "A slide that builds up on three clicks, with a speaker note" ## Three things - One - Two ::click - Two is the interesting one - Three ::click - All three ::note Say "two" slowly. That is the point of the slide. :: ```` ### `:::click` … `:::` — a step region Everything between the fences is drawn at the step the opener named, and the walk resets to that same step after the closer: ```markdown :::click=2 The second point, made visible. ::: ``` The closer is **exactly three colons** — four inside a region is a paragraph. An unclosed region is refused rather than running to the end of the slide. ### `::column` — two columns of content ```markdown ## Before and after ::column Before: a file, and a hope. ::column After: a file, and an answer. ``` The number of columns is the layout's. Writing more `::column` markers than the layout has columns is `UnsupportedColumn`, and the message names which slide and which column. `::column` takes **no argument** — `::column=2` is refused. The layout decides how many there are. ### `::note` — the speaker note ```markdown # One ::note Read this out loud. :: ``` Case-insensitive, whitespace-tolerant, and the closer is anchored to the end of the slide, so two `::note` blocks on one slide become **one** note carrying both. An unclosed note is refused. ## Slidev, and where it differs The splitter is a **port of Slidev's `parseSync`**, pinned to a commit of that file. A deck that splits the way a Slidev deck splits is the whole point: the dialect that people already write for Slidev is the one worth reading, and matching it matters more than being tidy. It is a port and not a clone, and the differences are recorded rather than discovered later. Every fixture in `tests/fixtures/slidev/expected.json` was run through Slidev's own `parseSync` at that commit, and each divergence below is written down with its reason in `tests/fixtures/slidev/divergences.json`. There are **21** of them, in four groups. ### Frontmatter it refuses | Case | Why this library differs | | --- | --- | | `list in frontmatter` | A frontmatter block that is a YAML list carries no keys to read configuration from, so it is refused instead of stored as something no caller asked for. | | `scalar frontmatter` | A frontmatter block that is a YAML scalar is not a mapping, and a scalar where a mapping was expected is a mistake rather than an empty configuration. | | `broken yaml frontmatter` | Slidev keeps parsing invalid YAML and takes what it can. A deck author gets an error naming the slide instead, because a silently half-read block is how a deck comes out with the wrong title and nobody knows why. | | `title not a string` | Slidev takes the value as the title whatever its type. A number is not a title, so it is refused rather than rendered. | | `level not an int` | Slidev passes the value through to whatever consumes the level. A level that is not an integer is refused instead. | | `fenced yaml frontmatter` | Slidev reads a slide that opens with a fenced yaml block as frontmatter. Here it would be a code block, so the ambiguous shape is refused rather than read the other way round. | ### The delimiter and the comments around it | Case | Why this library differs | | --- | --- | | `comment closed on the delimiter line` | The note is empty. An empty note is no note, so there is none rather than an empty string. | | `empty trailing comment` | The note is empty. An empty note is no note, so there is none rather than an empty string. | | `indented opener, delimiter, flush closer` | The same consequence seen from the other side: the `---` is inside the block, so the block runs to the flush closer and the delimiter no longer splits anything. | | `de-indented fence` | A closing fence may be indented less than the opening one. Slidev matches the close with `startsWith` against the raw line, so the opener's indentation has to be repeated exactly and an otherwise ordinary block is split by the delimiter inside it. | | `indented opener closed flush, delimiter between` | A consequence of the closing fence not having to repeat the opener's indentation: the flush closer closes the block, so the `---` between them is a delimiter again and splits the slide. | | `html comment on one line` | A speaker note is `::note` on a line of its own, its text, and a closing `::`. An HTML comment after a slide's last line is the note Slidev reads it as; here it is a deck written for another tool, left on the slide and refused by the renderer rather than taken. | | `comment ending the slide` | A speaker note is `::note` on a line of its own, its text, and a closing `::`. An HTML comment after a slide's last line is the note Slidev reads it as; here it is a deck written for another tool, left on the slide and refused by the renderer rather than taken. | | `two trailing comments` | A speaker note is `::note` on a line of its own, its text, and a closing `::`. An HTML comment after a slide's last line is the note Slidev reads it as; here it is a deck written for another tool, left on the slide and refused by the renderer rather than taken. | | `comment with a delimiter inside the note` | A speaker note is `::note` on a line of its own, its text, and a closing `::`. An HTML comment after a slide's last line is the note Slidev reads it as; here it is a deck written for another tool, left on the slide and refused by the renderer rather than taken. | ### Fences, which are CommonMark's rules rather than Slidev's | Case | Why this library differs | | --- | --- | | `tildes are not a fence` | Tildes fence a code block in CommonMark, so the `---` between them is code rather than a delimiter. Slidev recognises only backticks and splits the slide, leaving the block split across two slides. | | `unterminated fence` | A fence that is never closed runs to the end of the deck, which is what CommonMark says one is. Slidev leaves the index where it was and reads every following line as ordinary content, so a `---` inside an unclosed fence splits the deck and the next slide opens with a literal delimiter. | | `unterminated tilde` | The same for a tilde fence, which Slidev does not recognise as a fence at all. | | `tilde fence` | Tildes fence a code block in CommonMark. Slidev matches a backtick run only, so a `~~~` block is read as prose and a `---` inside it splits the deck. | | `tilde fence long` | A tilde run of any length fences a block, as CommonMark has it. | ### Levels | Case | Why this library differs | | --- | --- | | `level zero` | Slidev keeps the heading depth because a zero is falsy, which is a truthiness check where a range was meant. A heading level below one is refused instead. | None of these move a slide boundary for an ordinary deck. They are the corners: an unterminated fence, a comment on the delimiter line, a `level: 0`. A deck written for Slidev splits here the way it does there. ## Frontmatter directives Every key of every slide's frontmatter must be one of these seven: | Key | Type | Default | Slide | | --- | --- | --- | --- | | `title` | string | `null` | any | | `name` | string | `null` | any — a fallback for `title` | | `level` | integer ≥ 1 | the heading's own depth | any | | `layout` | one of eight roles | `content` | any | | `theme` | string | `default` | **first only** | | `author` | string | the theme's, else `markdown-presentation` | **first only** | | `description` | string | `null` | **first only** | The eight layout roles: `title`, `content`, `two-column`, `section`, `blank`, `image-text`, `image-title`, `image-slide`. Anything else — including Slidev's own `two-cols` — is refused with the list of what would have worked. A deck directive on a later slide is refused by name, because it is nearly always a slide that was meant to be the first one. ````md presentation "Every frontmatter key, on a deck that uses all seven" --- theme: corporate-dark title: The quarter in three slides author: Markdown Office description: What changed, what it cost, and what happens next. --- # The quarter ## What changed ::click - Two products shipped ::click - Three customers renewed ::note Lead with the renewal number. It is the one people remember. :: ```` ## What is refused, and why The splitter is a **port** of Slidev's `parseSync`, not a clone of it: a deck that splits the way Slidev splits is the point, and the places it deliberately does not are written down — see [Slidev, and where it differs](/presentation/directives/#slidev-and-where-it-differs). The renderer shares none of that. Everything Slidev draws with something this package does not have is refused by name rather than drawn as a paragraph: | Written | Refused as | | --- | --- | | `` | an HTML comment | | ``, `
` | 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/