The dialect, the command line, and the library. In that order, because each one assumes the one before it.

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.

WordA document using the basic dialectbuilt by the library this page documents
The Markdown
# 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.

example.docxoutputPDFPDF

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 shows how.

WordA themed reportbuilt by the library this page documents
The Markdown
---
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.

example.docxoutputPDFPDF

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 MERGEFIELDs
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}.

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 <cwd>/themes/<name>, then the package's own themes/<name>, 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
WordA table of contents Word can buildbuilt by the library this page documents
The Markdown
# 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.

example.docxoutputPDFPDF

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.


2. Command line

The command line for this half is 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.

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 <file> path beside the input; - for standard output
-c, --config <file.php> path none
--theme <name> name or path the frontmatter's theme:
-t, --template <file> .docx or .dotx none
--require-control flag off
--images <mode> embed, placeholder, skip embed
--no-images flag shorthand for --images skip
--image-base <dir> path the input's directory; the working directory for standard input
--svg-fallback <mode> raster, none raster
--table-width <n> fiftieths of a percent; 0–5000 5000
--plain flag off
--to <format> 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 <dir>, --line-ending <lf|crlf>, --setext, --no-fence and --no-header instead.

A filter, not a command that needs a filename:

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.

3. As a library

Four lines to the first document.

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:

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

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

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.

What you can ask the converter

$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.

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.


Next

Markdown Presentation · The directive syntax · Word directives · Images · Round trips · Errors

Every example on this page was built by the library it documents while this page was built. This page as Markdown.