Both directions are the same interface, so a round trip is two calls:

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:

WordA Word document read back to Markdownread by the library this page documents
The Markdown it reads back as
# Quarterly report

Some **strong emphasis** and some *emphasis*, plus `inline code` and a [link](https://commonmark.org/).

## Highlights

| **Region** | **Revenue** | **Change** |
| :-- | :-- | :-- |
| North | 1,204 | \+8.2% |
| South | 988 | \-1.1% |

## Notes

- a list
- with two items

> A block quote, indented in the way Word indents it.

```
$x = 1;
$y = 2;
```

example.docxsourcePDFPDF

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.

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:

| 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

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

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