Skip to content

A link's run properties are written in the order the schema names them - #41

Closed
fabeat wants to merge 4 commits into
mainfrom
fix/hyperlink-run-properties
Closed

fabeat wants to merge 4 commits into
mainfrom
fix/hyperlink-run-properties

Conversation

@fabeat

@fabeat fabeat commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

The defect

src/Writer/HyperlinkPass.php writes the w:rPr of a hyperlink's run by hand —
PHPWord's Link element holds one plain string and cannot carry a label with
emphasis in it, so the runs are recorded and written afterwards.

The children were appended in the order the properties came to mind:

w:b, w:i, w:strike, w:u, w:color, w:sz, w:rFonts

w:rPr is the sequence EG_RPrBase in ECMA-376's wml.xsd and not a set. A
reader walks the children in the order they are written and stops at the first
one it did not expect, and the schema names them:

rStyle, rFonts, b, bCs, i, iCs, caps, smallCaps, strike, dstrike, outline,
shadow, emboss, imprint, noProof, snapToGrid, vanish, webHidden, color,
spacing, w, kern, position, sz, szCs, highlight, u, effect, bdr, shd,
fitText, vertAlign, rtl, cs, em, lang, eastAsianLayout, specVanish, oMath

So the order was wrong twice over: the typeface was written last where the
schema names it second, and the underline was written before the colour where
the schema names it after the size.

Microsoft's Open XML SDK validator:

[Schema] The element has unexpected child element 'w:color'.
  part=/word/document.xml path=/w:document[1]/w:body[1]/w:p[9]/w:hyperlink[2]/w:r[5]/w:rPr[1]

Word opens a document that gets this wrong, because Word is lenient where the
schema is not. A reader that walks the sequence — a validator, a converter — does
not.

The fix

The children are written in the schema's order:

w:rFonts, w:b, w:i, w:strike, w:color, w:sz, w:u

The measurement

Over the 29 .docx files in examples/out/documents/ (built with
php examples/build.php), with Microsoft's own validator:

before after
w:color unexpected inside a hyperlink run's w:rPr 19 0
errors inside a hyperlink run's w:rPr 22 3
all errors in the 29 documents 1002 983

The three that are left are PHPWord's own: they are in a hyperlink PHPWord
wrote itself, on the path where the label has no emphasis in it, and they are
the same defect in PhpOffice\PhpWord\Writer\Word2007\Style\Font::writeStyle(),
which writes color, sz, b, bCs, i in that order — and that is
PHPOffice/PHPWord#2964. The
rest are defects in PHPWord and in the .dotx templates, and are being fixed
separately.

The test

tests/Unit/hyperlink-run-properties.php builds a document whose link carries
a typeface, a size, a colour, bold, italic, strikethrough and an underline,
and asserts the exact order of the children of the w:rPr inside the
hyperlink's run. All four of its tests fail without the fix and pass with it.

The audit

Every place in the repository that builds a w:rPr or a w:pPr by hand:

Place What it writes
src/Writer/HyperlinkPass.php the w:rPr of a link's run was wrong; fixed
src/Writer/BuiltInHeadingPass.php w:outlineLvl in a heading style's w:pPr, and the w:pPr in a w:style right
src/Template/WordFields.php a field's value run right — the w:rPr is copied whole
src/Writer/TocPass.php the runs of a TOC field right — no w:rPr at all
src/Writer/NumberingMerger.php w:num in word/numbering.xml right
src/Template/ContentControl.php w:sdt markup right — no w:rPr or w:pPr
examples/build.php w:pBdr in a style's w:pPr right

BuiltInHeadingPass puts w:outlineLvl before the w:rPr of the w:pPr,
which is where CT_PPr puts it: CT_PPr is the CT_PPrBase sequence followed
by rPr?, and w:outlineLvl is the last of the CT_PPrBase elements a
heading style carries. The built documents read
<w:pPr><w:keepNext/><w:spacing/><w:outlineLvl/></w:pPr> and the validator
reports nothing for them.

A shared list would be better

The schema's order now lives in three places and in three forms: this change
puts it in the order the code writes, BuiltInHeadingPass states the
CT_Style and CT_PPr order in prose, and examples/build.php has
AFTER_PBDR, a list of the w:pPr elements that follow w:pBdr. A shared
list of the two sequences would be better than three of them, and would give
BuiltInHeadingPass and the example build the same source. It is a larger
change than this defect, and it wants a test of its own.

The properties of a hyperlink's run were written by hand, because PHPWord's
Link element holds one plain string and cannot carry a label with emphasis
in it. 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 the sequence EG_RPrBase in ECMA-376's wml.xsd and not a set, so a
reader walks the 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, and a document that gets this wrong is one
Word refuses to open.

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. There are none of them now.
Two of them named a position in the schema's sequence, which is a number
that goes stale the moment the schema is read again, and one of them said
a run can hold every property, which is a claim about the schema rather
than about this library.
@sonarqubecloud

Copy link
Copy Markdown

@fabeat

fabeat commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

Combined into #49, which carries this and the rest of the campaign in one pull request. Nothing is lost: the commits are in it, and the release notes are folded into the 0.3.1 section.

@fabeat fabeat closed this Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant