Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e56dc22
A link's run properties are written in the order the schema names them
fabeat Oct 30, 2026
89b6612
The comments in the new test say why, and not what
fabeat Oct 30, 2026
f314c28
The comment says what Word does with a document that gets the order w…
fabeat Oct 30, 2026
5016679
The changelog says what Word does with a document that gets the order…
fabeat Oct 30, 2026
8a19367
The documents this library writes are checked against the schema
fabeat Oct 30, 2026
8a63229
The examples can be built without their previews
fabeat Oct 30, 2026
62f933a
The README and the changelog say what the gate is
fabeat Oct 30, 2026
49d6e61
The gate runs in one job, and not in the jobs that run the rest of th…
fabeat Oct 30, 2026
204eda9
The PHPWord that writes the documents is patched, and the templates a…
fabeat Oct 30, 2026
dc8e781
The changelog says what the patch carries and what the templates were…
fabeat Oct 30, 2026
dc130b7
The 0.3.1 release
fabeat Oct 30, 2026
4575132
The consumer is told what the patch needs, and the changelog says wha…
fabeat Oct 30, 2026
db4e223
The patch is the one that carries the fixes of the second pull request
fabeat Oct 30, 2026
edaa06e
Merge remote-tracking branch 'origin/feat/phpword-ooxml-patch' into r…
fabeat Oct 30, 2026
809eb21
The lock file answers to the manifest
fabeat Oct 30, 2026
f9dcb2e
Merge remote-tracking branch 'origin/feat/phpword-ooxml-patch' into r…
fabeat Oct 30, 2026
e9ae166
The version is held to the line the patch was made against
fabeat Oct 30, 2026
9a0c7ab
Merge remote-tracking branch 'origin/feat/phpword-ooxml-patch' into r…
fabeat Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@
# --strict` fails when the two disagree, and a build is meant to be the install
# the lock describes.
/composer.lock export-ignore

# The patch lock is this repository's install state — the patch, its checksum and
# the depth it applied at. A consumer's install never reads it, and it records
# one machine's install rather than the package's.
/patches.lock.json export-ignore
47 changes: 45 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,12 @@ jobs:
}
'

# The schema gate is not run here. The runner image ships a .NET SDK, so
# the test would not skip itself and would fail this job over a document
# that is nobody's business but the gate's — and the analysis job below
# needs this one to have passed. The gate has a job of its own.
- name: Test
run: vendor/bin/pest
run: vendor/bin/pest --exclude-testsuite=ooxml

# A coverage report on its own says little; this fails if the reported
# figure is a placeholder, or if the code stops being covered at all.
Expand All @@ -250,6 +254,7 @@ jobs:
if: matrix.php == '8.5'
run: |
vendor/bin/pest --coverage --min=91 \
--exclude-testsuite=ooxml \
--coverage-clover=build/coverage/clover.xml \
--coverage-html=build/coverage/html \
--log-junit build/coverage/junit.xml
Expand All @@ -262,6 +267,39 @@ jobs:
path: build/coverage
retention-days: 7

# The documents this library writes, checked against the schema.
#
# A job of its own because the gate needs the .NET SDK at a version this
# repository chooses. The runner image ships one, so the test would not skip
# itself there either; the jobs above leave the suite out rather than run the
# gate against whatever the image happens to carry, and this one installs the
# version the validator is built for.
ooxml:
name: ooxml
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.5'
coverage: none
tools: composer:v2

- name: Install
run: composer install --prefer-dist --no-interaction --no-progress

# The version the validator is built for. A machine with a newer runtime
# runs it unchanged, so this is the floor and not a ceiling.
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: '8.0'

# This one suite and not the whole of it: the other jobs leave it out, and
# this is where the gate runs.
- name: The documents are valid Office Open XML
run: vendor/bin/pest --testsuite=ooxml

# Static analysis on SonarCloud.
#
# Its own job rather than a step in the test matrix, so that a finding and a
Expand Down Expand Up @@ -316,9 +354,14 @@ jobs:
# Both reports the analysis reads, produced in one run rather than the
# second one being repeated. The HTML report is skipped; Sonar wants the
# machine-readable one.
#
# The schema gate is left out for the reason the test job gives: it has a
# job of its own, and the analysis is about the code rather than about the
# documents it writes.
- name: Coverage and test results for the analysis
run: |
vendor/bin/pest --coverage --min=91 \
--exclude-testsuite=ooxml \
--coverage-clover=build/coverage/clover.xml \
--log-junit build/coverage/junit.xml

Expand Down Expand Up @@ -407,7 +450,7 @@ jobs:
run: git --no-pager diff --stat composer.lock

- name: The suite still passes
run: vendor/bin/pest
run: vendor/bin/pest --exclude-testsuite=ooxml

# A runtime dependency that stops supporting the version the library claims
# is the failure worth catching, and the minimum job cannot see it while it
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
# The documents and previews examples/build.php writes.
/examples/out/

# What the OOXML validator's build produces. The validator is a .NET program and
# its compiler writes beside the sources.
/tools/ooxml-validator/bin/
/tools/ooxml-validator/obj/

# Scratch space for the tests and the checking scripts.
/tmp/

Expand Down Expand Up @@ -44,3 +49,6 @@

# Parallel agent worktrees
.worktrees/

# The patch lock is this repository's install state, not the package's.
/patches.lock.json
95 changes: 95 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,99 @@ All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.1] - 2026-10-30

### Added

- **The documents this library writes are checked against the schema.** A `.docx` is
Office Open XML, and a document that breaks the schema is a document that is not
what it claims to be — Word opens it because Word is lenient where the schema is
not, and nothing else in the suite could see it. The check is Microsoft's own
validator, the one the Open XML SDK Productivity Tool uses, and it lives in
`tools/ooxml-validator`.

It is run over the examples — the whole corpus of documents this library writes,
built by `examples/build.php` — rather than over documents the test writes
itself, because a gate over a selection is a gate over the selection.

It is a gate and not a report: the test fails on a document the validator
rejects, and a test that asks the validator about a document with an error in it
proves that it can fail. The test skips itself where the .NET SDK is not, and
says what is missing; a job of its own installs the SDK, so the gate cannot be
skipped there, and the jobs that run the rest of the suite leave it out.

**The gate lands green.** Over the twenty-nine documents the build writes,
Microsoft's validator reported a thousand and two errors. With the fixes upstream
in PHPWord's writer it reported a hundred and fifty-one: eighty-eight belonged to
PHPWord, forty-four to the three committed templates, and nineteen to this
library's own. **With all five upstream pull requests, the templates written by
the fixed library, and this library's own fix, it reports zero.**

The gate comes first, so that the fixes are proved to have landed rather than
believed to have.

- **`examples/build.php --no-previews` builds the documents and leaves the pictures
alone.** Drawing the previews takes longer than everything else the build does,
and the gate that reads the documents wants the documents.

- **The project that installs this one has to allow
[`cweagans/composer-patches`][composer-patches].** The fixes are in five pull
requests upstream —
[#2964](https://github.com/PHPOffice/PHPWord/pull/2964),
[#2965](https://github.com/PHPOffice/PHPWord/pull/2965),
[#2966](https://github.com/PHPOffice/PHPWord/pull/2966),
[#2968](https://github.com/PHPOffice/PHPWord/pull/2968) and
[#2969](https://github.com/PHPOffice/PHPWord/pull/2969), all against
[#2963](https://github.com/PHPOffice/PHPWord/issues/2963) — and until they are
released they reach this library as a patch applied to the installed package.
The plugin applies it, and Composer runs it only when the project allows it.
Without that line the documents are invalid Office Open XML again, silently.

### Fixed

- **A link's run properties are written in the order the schema names them.** The
properties of a hyperlink's run were written by hand — PHPWord's `Link` element
holds one plain string and cannot carry a label with emphasis in it — and they
were written in the order they came to mind: the weight, the slant, the strike,
the underline, the colour, the size and the typeface.

`w:rPr` is a sequence in ECMA-376's `wml.xsd` and not a set, so a reader walks
its children in the order they are written and stops at the first one it did not
expect. The typeface comes before the weight, and the underline comes after the
size. Word opens a document that gets this wrong, because Word is lenient where
the schema is not; a reader that walks the sequence does not.

Microsoft's own validator reported `The element has unexpected child element
'w:color'` nineteen times across the example documents, every one of them a
link whose label was styled. The order is the schema's now: `w:rFonts`, `w:b`,
`w:i`, `w:strike`, `w:color`, `w:sz`, `w:u`.

- **The PHPWord that writes the documents is patched.** PHPWord 1.4.0 writes the
children of `w:rPr`, of `w:pPr`, of `w:tblPr`, of `w:sectPr` and of `w:style`
in an order of its own, writes a page size in twips as a fraction, and writes a
few elements the format does not allow where it puts them. A document it writes
is therefore not valid OOXML, and every document this library writes is a
document it writes.

The fixes are in five pull requests upstream, and until they are released the
library carries them as a patch — the same way it has carried PHPPresentation's
fixes until now. The patch touches eighteen files of the Word2007 writer and
nothing else: it is a patch against 1.4.0 and not against the branch, so it
carries the fixes and not the rest of what upstream has done since.

- **The three templates are written by the fixed library.** `invoice.dotx`,
`meridian-qbr.dotx` and `plain-letterhead.dotx` were written by PHPWord before
the fixes, so they carried four of its defects inside them — a page size in
twips with a fraction, a style whose properties were in the wrong order, the
settings in the wrong order and an inline picture in a header. Every document
rendered from a template inherited them, which is why forty-four of the errors
the gate found were in the templates and not in the code that writes them.

They are written by the fixed library now, and they score 0 errors each. **The
header logo of `meridian-qbr.dotx` and `plain-letterhead.dotx` is the picture the
examples carry today** — the old templates carried an earlier one, left behind
when the asset itself changed.

## [0.3.0] - 2026-10-30

### Added
Expand Down Expand Up @@ -1176,6 +1269,8 @@ on it:
`README.md` has the full list, and `Reverse\Options` is where each one is
configurable.

[0.3.1]: https://github.com/markdown-office/markdown-word/releases/tag/v0.3.1
[composer-patches]: https://github.com/cweagans/composer-patches
[0.2.2]: https://github.com/markdown-office/markdown-word/releases/tag/v0.2.2
[0.3.0]: https://github.com/markdown-office/markdown-word/releases/tag/v0.3.0
[0.2.1]: https://github.com/markdown-office/markdown-word/releases/tag/v0.2.1
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,27 @@ install of this package puts no command in `vendor/bin`. The command line is a
separate program — [`mdoffice`][mdoffice] — whose commands and options are in
the [documentation][command-line].

**The project that installs this one has to allow [`cweagans/composer-patches`][composer-patches].**
The documents this library writes are Word and Open XML, and the released PHPWord writes
a few of them in an order the schema does not allow — the properties of a run, of a
paragraph, of a table, of a section and of a style, and a few elements that belong
somewhere else. The fixes are in pull requests upstream, and until they are released
they reach this library as a patch applied to the installed package. The plugin applies
that patch, and Composer runs it only when the project allows it:

```json
{
"config": {
"allow-plugins": {
"cweagans/composer-patches": true
}
}
}
```

Without that line the install stops with Composer's own message naming
`cweagans/composer-patches`. The documents are valid Office Open XML only with it.

See [Requirements](#requirements) for what PHP and which extensions it needs.
Where to get help: [the issue tracker][issues].

Expand Down Expand Up @@ -138,6 +159,7 @@ composer test:unit # the unit tests alone
composer test:spec # just the conformance suite
composer test:roundtrip # just the round-trip suite
composer test:readme # just the examples on this page
composer test:ooxml # just the schema gate over the documents
composer test:coverage # with a report in build/coverage
php smoke.php # does the library work on this version of PHP?
php stress.php # malformed input across every configuration
Expand All @@ -161,6 +183,7 @@ Tests are written with [Pest][pest] 5, against PHPUnit 13.
| Workflow | What it does |
| --- | --- |
| `tests` | `composer validate --strict`, `composer audit`, the README examples, then the suite on PHP 8.4 and 8.5 with a coverage report |
| `ooxml` (a job in `tests`) | the documents this library writes, checked against the OOXML schema by Microsoft's own validator, which needs the .NET SDK |
| `sonarcloud` (a job in `tests`) | static analysis on [SonarCloud][sonarcloud], failing the build when the quality gate is red |

PHP 8.2 is in the matrix too, but the test suite cannot run there: **Pest 5
Expand Down Expand Up @@ -1959,6 +1982,7 @@ LGPL-3.0.
[commonmark]: https://github.com/thephpleague/commonmark
[command-line]: https://markdown-office.github.io/command-line/basic/
[issues]: https://github.com/markdown-office/markdown-word/issues
[composer-patches]: https://github.com/cweagans/composer-patches
[mdoffice]: https://github.com/markdown-office/mdoffice
[sonarcloud]: https://sonarcloud.io/summary/new_code?id=markdown-office_markdown-word
[skill]: https://markdown-office.github.io/skills/
Expand Down
16 changes: 15 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
],
"require": {
"php": "^8.2",
"cweagans/composer-patches": "^2.0",
"ext-dom": "*",
"ext-gd": "*",
"ext-mbstring": "*",
"ext-zip": "*",
"league/commonmark": "^2.10",
"markdown-office/commonmark-generic-directives": "^0.3.0",
"phpoffice/phpword": "^1.4",
"phpoffice/phpword": "~1.4.0",
"symfony/yaml": "^7.4 || ^8.1"
},
"require-dev": {
Expand All @@ -37,6 +38,17 @@
"suggest": {
"ext-imagick": "Embeds SVG as a vector. Word holds an SVG only beside a raster of it, so something has to draw the raster; without this an SVG raises instead of going in."
},
"extra": {
"patches": {
"phpoffice/phpword": [
{
"description": "PHPWord 1.4.0 writes the children of `w:rPr`, of `w:pPr`, of `w:tblPr`, of `w:sectPr` and of `w:style` in an order of its own, and writes a few elements the format does not allow there. The patch puts them where ECMA-376 names them. The version is held to the 1.4 line rather than the 1 series: the patch is against 1.4.0, and a later release of the line would carry a different writer.",
"url": "https://raw.githubusercontent.com/markdown-office/PHPWord/62f68049d287e5a52575f2ae694c2e76f4a2002a/patches/phpoffice-phpword-1.4.0-ooxml.patch",
"sha256": "193163898e7690b6eaf51086d415809616b7e25ec4090b58ce7966f66ae1203a"
}
]
}
},
"autoload": {
"psr-4": {
"MarkdownWord\\": "src/"
Expand All @@ -50,6 +62,7 @@
"config": {
"sort-packages": true,
"allow-plugins": {
"cweagans/composer-patches": true,
"pestphp/pest-plugin": true
}
},
Expand All @@ -59,6 +72,7 @@
"test:spec": "pest --testsuite=conformance",
"test:roundtrip": "pest --testsuite=roundtrip",
"test:readme": "pest --testsuite=readme",
"test:ooxml": "pest --testsuite=ooxml",
"test:coverage": "pest --coverage --min=91 --coverage-clover=build/coverage/clover.xml --coverage-html=build/coverage/html",
"check": [
"@composer validate --strict",
Expand Down
Loading
Loading