Skip to content

fix: preserve text after <w:br/> by making CTR run-inner-content attributes collections - #10

Merged
ronaldtse merged 1 commit into
mainfrom
fix/ctr-text-after-br
Jul 15, 2026
Merged

fix: preserve text after <w:br/> by making CTR run-inner-content attributes collections#10
ronaldtse merged 1 commit into
mainfrom
fix/ctr-text-after-br

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

Fixes the plurimath "Bug B" report: text after <w:br/> is dropped on parse.

Per reference-docs/shared-math.xsd lines 421-438, CT_R's content model is:

<xsd:choice minOccurs="0" maxOccurs="unbounded">
  <xsd:group ref="w:EG_RunInnerContent" />
  <xsd:element name="t" type="CT_Text" minOccurs="0" />
</xsd:choice>

The choice has maxOccurs="unbounded", so every element it contains can repeat, interleaved with the others. A math run like:

<m:r><m:t>x</m:t><w:br/><m:t>y</m:t></m:r>

is schema-valid and Word-emitted: text on both sides of a break.

The bug

CTR#t was declared as a scalar attribute (attribute :t, :ct_text, no collection:), so parsing the above XML stored only one t value — the second overwrote the first. Same bug applied to every non-collection attribute in EGRunInnerContent and EGWordRunInnerContent: br, del_text, instr_text, del_instr_text, no_break_hyphen, object, pict, fld_char, ruby, footnote_reference, endnote_reference, comment_reference, drawing.

The previous PR (#8) made br parseable (via CTWordprocessingEmpty/CTWordprocessingText parallel types) but didn't catch that all these attributes needed to be collections for the unbounded choice to work correctly.

The fix

  • EGRunInnerContent: attribute :t, :ct_textattribute :t, :ct_text, collection: true
  • EGWordRunInnerContent: every attribute gets collection: true (replaces the inconsistent mix of scalar and collection: 0..1)
  • CTR: add ordered declaration to the xml block so the serializer emits children in parsed document order, not grouped by source group. Without ordered, <m:t>x</m:t><w:br/><m:t>y</m:t> would serialize as <w:br/><m:t>x</m:t><m:t>y</m:t> — wrong order, breaks round-trip fidelity and changes document semantics.

Test plan

  • bundle exec rspec — 335 examples, 0 failures (was 333; +2 new interleaving tests)
  • bundle exec rubocop — clean
  • Manual verification: <m:r><m:t>x</m:t><w:br/><m:t>y</m:t></m:r> now parses with r.t.map(&:content) == ["x", "y"] and r.br.size == 1
  • Round-trip preserves element order: serialize produces <m:t>x</m:t><w:br/><m:t>y</m:t> (not grouped)
  • Updated existing specs that assumed scalar access (.t.content.t.first.content)

New specs

spec/omml/wordprocessing_in_math_spec.rb adds a "multiple occurrences within one run" describe block:

  • "preserves text appearing both before and after <w:br/>"
  • "preserves multiple <w:br/> interleaved with text"
  • "round-trips a complex run with multiple wordprocessing children"

Impact for consumers

Breaking change for direct attribute access: r.t now returns an array, not a single CTText. Consumers (Uniword, Plurimath) accessing r.t.content need to update to r.t.first.content or r.t.map(&:content).

This breaking change is necessary for schema correctness. The previous scalar form silently lost data on every multi-element math run — any document with text on both sides of a break, multiple breaks, or interleaved wordprocessing elements was corrupted on parse.

…ibutes collections

Per shared-math.xsd CT_R lines 421-438, the choice has
maxOccurs="unbounded", so every element it contains can repeat
interleaved with the others. A math run like

  <m:r><m:t>x</m:t><w:br/><m:t>y</m:t></m:r>

is schema-valid and Word-emitted: text on both sides of a break.

The Omml model had CTR#t as a scalar attribute, so the second <m:t>
overwrote the first on parse — silent data loss. Same for every
other element in EGRunInnerContent and EGWordRunInnerContent that
was declared without 'collection: true' (br, del_text, instr_text,
del_instr_text, no_break_hyphen, object, pict, fld_char, ruby,
footnote_reference, endnote_reference, comment_reference, drawing).

Fix:

- EGRunInnerContent: attribute :t, :ct_text, collection: true
- EGWordRunInnerContent: every attribute gets collection: true
  (replaces the inconsistent mix of scalar and collection: 0..1)
- CTR xml block: add 'ordered' declaration so the serializer emits
  children in parsed order, not grouped by source group. Without
  'ordered', <m:t>x</m:t><w:br/><m:t>y</m:t> would serialize as
  <w:br/><m:t>x</m:t><m:t>y</m:t> — wrong order, breaks round-trip.

Update existing specs that assumed scalar access (.t.content ->
.t.first.content). Add new specs in wordprocessing_in_math_spec.rb
verifying interleaved text+br round-trips without loss and that
the serializer preserves document order.

Resolves the plurimath 'Bug B' report: text after <w:br/> is no
longer dropped.
@ronaldtse
ronaldtse merged commit 1e8e51c into main Jul 15, 2026
14 checks passed
@ronaldtse
ronaldtse deleted the fix/ctr-text-after-br branch July 15, 2026 05:04
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