Conversation
References Euro-Office/DocumentServer#253 CLvl::toXML() (DocxFormat/Numbering.cpp) emitted <w:lvl> children in alphabetical order by tag name instead of the order the ECMA-376 CT_Lvl xsd:sequence mandates (start, numFmt, lvlRestart, pStyle, isLgl, suff, lvlText, lvlPicBulletId, legacy, lvlJc, pPr, rPr). Google Docs and our own reader locate children by tag name regardless of position, so the defect was invisible internally; Microsoft 365 Online's parser does not tolerate the out-of-order elements and fell back to an ambiguous list- type classification for both ordered and unordered lists. Reordered the existing WritingElement_WriteNode_* calls to match the schema sequence. No fields, conditions, or the corresponding fromXML() reader changed -- fromXML() already locates children by tag and needs no fix, which is also why opening and resaving an old, defective file is sufficient repair with no separate migration tool. Found and fixed two further, independent instances of the same defect class in other DOCX-numbering writers while auditing every writer for this construct: - RtfFile/Format/RtfProperty.cpp (RtfListLevelProperty::RenderToOOX2, RTF->DOCX): elements were badly scrambled, not merely alphabetical. - HwpFile/HwpDoc/Conversion/NumberingConverter.cpp (HWP->DOCX, the EHeadingType::BULLET/unordered-list branch specifically): lvlJc was emitted before lvlText. Adds a GoogleTest suite, docx_numbering_test (OOXML/DocxFormat/test/), registered as a new CTest target in the top-level CMakeLists.txt, asserting the schema order of toXML() output for both list types, fromXML()'s tolerance of pre-fix element order, and that a user-customised marker (non-default lvlText/format string) does not change classification -- proving the fix keys on the durable numFmt field, not on marker content. Verified against the real, rebuilt production x2t/converter binaries (both DocumentServer's and, since core is shared, a from-scratch Desktop Editors build) and confirmed in real Microsoft 365 Online and Google Docs sessions: primary bug, user-customised markers, nested lists, repeated round trips, repair-on-resave, and the RTF writer path. Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
chrip
left a comment
There was a problem hiding this comment.
Summary
This reorders <w:lvl> children to the ECMA-376 CT_Lvl sequence in three DOCX-numbering
writers and adds a GoogleTest suite for the primary one. The diagnosis holds up, the schema
sequence quoted in the commit message matches ECMA-376, and the reordering is
behavior-preserving in all three writers. Two things block merge: the Linux build job is
red because the PR invalidates three committed conversion snapshots that were not
regenerated, and the two new files carry no license header. One writer of the same defect
class was also missed by the audit.
Verification
| Claim / Item | Reality | Status |
|---|---|---|
ECMA-376 CT_Lvl sequence is start, numFmt, lvlRestart, pStyle, isLgl, suff, lvlText, lvlPicBulletId, legacy, lvlJc, pPr, rPr |
Correct, and MsBinaryFile/DocFile/NumberingMapping.cpp:575-660 already emits exactly that sequence, so the tree has an in-repo reference implementation |
✓ |
Numbering.cpp new order matches the schema |
OOXML/DocxFormat/Numbering.cpp:329-347 emits all twelve in sequence |
✓ |
| RTF writer new order matches the schema | RtfFile/Format/RtfProperty.cpp now emits start, numFmt, lvlRestart, isLgl, suff (the m_nFollow block), lvlText, lvlPicBulletId, lvlJc, pPr, rPr. pStyle and legacy are never emitted by this writer, so their absence is fine |
✓ |
| RTF reorder is behavior-preserving | GetLevelTextOOX() is pure (builds a local string, mutates no member) and sText has no other use later in the function, so moving its call down changes nothing but output order |
✓ |
| HWP fix touches only the BULLET branch | The EHeadingType::NUMBER branch already emitted start, numFmt, suff, lvlText, lvlJc, rPr, which is in sequence. Only the BULLET branch was wrong |
✓ |
fromXML() needs no change, so open-and-resave repairs old files |
CLvl::ReadElements() dispatches on oReader.GetName() in an if/else chain with no positional assumption |
✓ |
| Every writer of this construct was audited | RtfFile/Format/RtfOldList.cpp was missed. See Major 3 |
❌ |
ctest -R docx_numbering_test passes |
CI step 12, "Unit tests (CTest)", passed, so the suite builds and its cases are green | ✓ |
| No regression elsewhere | CI step 13, "Conversion Test", failed on three snapshot diffs. See Blocking 1 | ❌ |
GTEST_MAIN is a real add_core_gtest option |
common.cmake:466 declares it and writes a bundled entry point |
✓ |
DCO, AI disclosure, Assisted-by: trailer |
All present. Signed-off-by on the commit, DCO check green, disclosure in the PR body, Assisted-by: ClaudeCode:claude-sonnet-5 on the commit |
✓ |
| Euro-Office/DocumentServer#253 exists | Open, "Bullets docx not displayed correctly in m365 online" | ✓ |
Issues & Suggestions
🔴 Blocking
-
Blocking 1: three conversion snapshots are now stale and CI is red. The Linux
build
job failed at step 13, "Conversion Test"
(run 36094231170).
Test/Applications/x2tTester/conversionTest.shrunsdiff -ragainst committed
byte-for-byte references, and the RTF writer reorder changes the RTF to DOCX output for
all three word-processing fixtures. The failure is reproducible, and the references are
what need updating:Test/Applications/x2tTester/out_word_proc_snap/empty.rtf/docx/word/numbering.xml Test/Applications/x2tTester/out_word_proc_snap/medium.rtf/docx/word/numbering.xml Test/Applications/x2tTester/out_word_proc_snap/fonts_and_images.rtf/docx/word/numbering.xmlEach still opens
<w:lvl w:ilvl="0"><w:lvlJc w:val="left"/><w:lvlText w:val="%1"/><w:numFmt w:val="none"/><w:start w:val="1"/><w:suff w:val="nothing"/>...,
the pre-fix order. Please regenerate them in this PR. The failure also caused steps 16 to
23 to be skipped, so the DocumentServer e2e suite never ran on this branch. The test
plan'sctest -R docx_numbering_testline is accurate, but it only covers the new suite;
the conversion suite is a separate CI step.The odt and docx fixtures are untouched, which fits: only the
.rtfand.odtsnapshots
contain anumbering.xmlat all, and the ODF writers in
OdfFile/Reader/Format/styles_list.cppwere already in sequence. That also means
CLvl::toXML(), the primary fix, has no conversion-snapshot coverage and rests entirely
on the new unit suite. -
Blocking 2: the two new files have no license header.
OOXML/DocxFormat/test/test.cpp:1starts at#include "gtest/gtest.h"and
OOXML/DocxFormat/test/CMakeLists.txt:1atcmake_minimum_required. This fork already
has the right precedent for a file written from scratch:
OdfFile/Test/number_formats/main.cpp:1is a plain
// SPDX-License-Identifier: AGPL-3.0-only. Please add, totest.cpp:/* * SPDX-FileCopyrightText: 2026 Euro-Office contributors * SPDX-License-Identifier: AGPL-3.0-only */
and the
#-comment equivalent to the CMake file. Please do not copy the
(c) Copyright Ascensio System SIA 2010-2019block from neighbouring files such as
OOXML/test/common.cpp: it attributes fork-authored work to Ascensio and carries the
AGPL section 7(a) and CC-BY-SA clauses that are specifically ONLYOFFICE's.
AGPL-3.0-onlyrather than-or-later, because the upstream grant has no or-later
clause. In fairness the tree is not uniform here (OOXML/test/main.cpphas no header
either), but I would still add headers on the new files.
⚠️ Major
-
Major 3: one writer of the same defect class was missed. The commit message says the
fix came out of "auditing every writer for this construct", so it is worth naming the one
that got through.RtfFile/Format/RtfOldList.cpp:56-95
(RtfOldList::RenderToOOX, theRENDER_TO_OOX_PARAM_OLDLIST_ABSbranch) emits
numFmt,lvlText,pPr,rPr,lvlJc, puttinglvlJc(position 10) after both
pPr(11) andrPr(12). Same schema violation, same RTF to DOCX pipeline, same file
tree as the writer this PR already fixes.I checked the rest and they are clean, so this looks like the last one:
MsBinaryFile/DocFile/NumberingMapping.cpp(bothw:lvlwriters),
OdfFile/Reader/Format/styles_list.cpp(all four, each calling
docx_serialize_level_justificationbetweenlvlTextandpPr),
HtmlFile2/Writers/OOXMLWriter.cpp:160-172, the HWPNUMBERbranch, and the HWP
fallback block inCNumberingConverter::SaveToFile.
ℹ️ Minor / 💡 Suggestions
-
Minor 4: the test fixture exercises only half the sequence.
BuildLvl()in
OOXML/DocxFormat/test/test.cpp:18-50initialisesstart,numFmt,isLgl,suff,
lvlTextandlvlJc. The other six (lvlRestart,pStyle,lvlPicBulletId,legacy,
pPr,rPr) are never emitted, soSchemaOrderPositions()recordsnposfor them and
they get filtered out before the ordering assertion. That leaves thepStyleandpPr
inversion, which the old code also had and this PR also fixes, with no test behind it.
One more case building a fully populatedCLvlwould cover the whole sequence. -
Minor 5: the test comments point at documents that are not in the repo.
test.cppreferences "Feature 001-docx-list-numbering", "research.md Finding 4",
"T004/T005/T007", "T012/T017", "FR-004" and "FR-011". None of these resolve anywhere in
core, so a future reader has no way to follow them. The comments are useful otherwise;
I would keep the explanation and drop the identifiers, or say what each one meant. -
Minor 6:
TESTING.mdwas not updated. It keeps a curated "Done:" checklist of every
suite wired into CTest, with a note per suite on its dependencies and any quarantine. The
newdocx_numbering_testis registered inCMakeLists.txt:39but does not appear there.
It is also the first entry that is a new suite rather than a.promigration, which is
worth a line of its own. -
💡 Suggestion 7: the HWP bullet branch now switches twice on the same expression.
HwpFile/HwpDoc/Conversion/NumberingConverter.cpphas oneswitch (shIndex % 3)for
lvlTextand a second forrPr, and the two sets of branches have to stay in step by
hand. Picking the glyph and the font name into two locals in a single switch, then
writing them at their schema positions, would keep the pairing visible. Optional, and it
does not affect output.
Verdict
Request changes. The analysis and the fix are sound, and the RTF, HWP and CLvl
reorders all check out against the schema and against NumberingMapping.cpp's existing
correct implementation. But CI is red on a snapshot the PR itself invalidates, so
regenerating the three out_word_proc_snap files is required before this can go in, along
with the license headers on the two new files. Major 3 is a judgement call and could be a
follow-up, though since it is one more instance of a defect the PR already fixes twice,
folding it in seems cheaper than tracking it.
Assisted-by: ClaudeCode:claude-opus-5
|
Pushing the Blocking 2 (license headers), Major 3/task-80-adjacent wording, and Minor 4-6 changes now, plus the regenerated |
…snapshots, HWP coupling cleanup - Regenerate the three stale out_word_proc_snap RTF->DOCX numbering.xml fixtures (empty, medium, fonts_and_images), invalidated by the RtfProperty.cpp schema-order reorder in the previous commit. - Add SPDX license headers to the new test.cpp and CMakeLists.txt. - Expand the docx_numbering_test fixture with a fully-populated CLvl case covering all twelve CT_Lvl schema elements, and trim dangling spec-kit identifiers from test.cpp's comments. - Correct TESTING.md's docx_numbering_test entry (links x2tlib, not DocxFormatLib alone) and commit it. - Restructure the HWP bullet-branch marker/font selection in NumberingConverter.cpp into a single switch instead of two independently-edited ones keyed on the same index, removing an implicit-coupling risk with no output change. References Euro-Office/DocumentServer#253 Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Peter P. Lupo <pplupo@gmail.com>
f02e29c to
08420f3
Compare
|
Thanks for the thorough review — the findings check out. Status on each: Blocking 1 (stale snapshots): regenerated and pushed. Rebuilt Blocking 2 (license headers): added SPDX headers to Major 3 ( Also worth flagging while we're on "found but deliberately not fixed": during the original audit, Minor 4 (fixture coverage): added Minor 5 (dangling identifiers): removed the Minor 6 ( Suggestion 7 (HWP double-switch): agreed, this shouldn't have been left like that. Fixed — the bullet branch now selects the glyph and its font together in one switch, so they can't drift out of sync. |
chrip
left a comment
There was a problem hiding this comment.
Summary
This is a re-review after 08420f3. Every point from my earlier review was either fixed or deferred with a stated reason, and CI is green across the board. The Conversion Test and the DocumentServer e2e suite were skipped on the last run; both ran and passed this time. Two small notes below, neither of them blocking.
Prior feedback
| Point | Status |
|---|---|
| Blocking 1: stale RTF snapshots | ✓ addressed. All three out_word_proc_snap/*.rtf/docx/word/numbering.xml files were regenerated, and each <w:lvl> now reads start, numFmt, suff, lvlText, lvlJc, pPr, rPr. The "Conversion Test" step passes. |
| Blocking 2: license headers | ✓ addressed. test.cpp and CMakeLists.txt both carry the AGPL-3.0-only Euro-Office SPDX header. |
Major 3: RtfOldList.cpp |
Deferred. The reasoning is fine with me, but the deferral isn't tracked anywhere public. See Minor 1. |
| Minor 4: fixture coverage | ✓ addressed. LvlToXml_FullyPopulatedLevel_ElementsInSchemaOrder first asserts that all twelve elements are present and only then checks their order, so a missing element now fails the test instead of being filtered out. Building the fixture through fromXML() is fine, because toXML() writes in a fixed order regardless of input order. |
| Minor 5: dangling identifiers | ✓ addressed |
Minor 6: TESTING.md |
✓ addressed, and the x2tlib correction is welcome |
| Suggestion 7: HWP double switch | ✓ mostly addressed. See Minor 2. |
Remaining notes
-
Minor 1: the deferred writers need a tracking issue. The reply says the
RtfOldList::RenderToOOX()andCAbstractNum::toXML()ordering problems were "filed and documented". The documentation lives in the fix report andresearch.md, and neither is in the repo. No issue in Euro-Office/core or Euro-Office/DocumentServer mentions either of them. Deferring both is OK with me, since neither one causes the M365 misclassification. Could you open one issue that covers both and link it here? Otherwise the next person to audit these writers has nothing to find. -
Minor 2: the HWP font choice still branches twice.
HwpFile/HwpDoc/Conversion/NumberingConverter.cpp:107now picks the glyph andsFontNametogether, which was the point. But:145branches again onL"Courier New" == sFontNameto decide whetherrPrgetsw:cs. The coupling is still there, keyed on a string now instead of the index. I checked all three cases and the output is byte-identical to before, so this is cosmetic. Building the wholerPrstring inside the one switch would remove the second branch. Optional.
Verdict
Approve. Both blockers are fixed, the new test covers the full CT_Lvl sequence, and CI is green, including the conversion and e2e suites. The tracking issue from Minor 1 is the only thing I'd still ask for, and it doesn't need to hold up the merge.
Assisted-by: ClaudeCode:claude-opus-5-5
Summary
CLvl::toXML()(and two other independent writers: RTF→DOCX, HWP→DOCX) emitted<w:lvl>child elements out of the ECMA-376 CT_Lvl schema sequence.docx_numbering_testGoogleTest suite covering the primary writer.References Euro-Office/DocumentServer#253
AI disclosure
Investigation, fix, and test-suite drafting were assisted by Claude Code (Claude Sonnet 5). All changes reviewed and submitted by the undersigned contributor.
Test plan
ctest -R docx_numbering_test— all pass