diff --git a/lib/uniword/builder/paragraph_builder.rb b/lib/uniword/builder/paragraph_builder.rb index ef4269a9..af634ff6 100644 --- a/lib/uniword/builder/paragraph_builder.rb +++ b/lib/uniword/builder/paragraph_builder.rb @@ -98,12 +98,11 @@ def align=(value) # @param rule [String, nil] Line rule ('auto', 'exact', 'atLeast') # @return [self] def spacing(before: nil, after: nil, line: nil, rule: nil) - ensure_properties.spacing ||= Properties::Spacing.new - props = @model.properties.spacing - props.before = before if before - props.after = after if after - props.line = line if line - props.line_rule = rule if rule + written = { + before: before, after: after, line: line, line_rule: rule + }.select { |_, value| value } + sp = ensure_properties.ensure_spacing(*written.keys) + written.each { |field, value| sp.public_send(:"#{field}=", value) } self end diff --git a/lib/uniword/builder/style_builder.rb b/lib/uniword/builder/style_builder.rb index 8a8f29c9..56657ddd 100644 --- a/lib/uniword/builder/style_builder.rb +++ b/lib/uniword/builder/style_builder.rb @@ -102,11 +102,11 @@ def align(value) # @param line [Integer, nil] Line spacing in twips # @return [self] def spacing(before: nil, after: nil, line: nil) - ensure_para_props - @model.pPr.spacing ||= Properties::Spacing.new - @model.pPr.spacing.before = before if before - @model.pPr.spacing.after = after if after - @model.pPr.spacing.line = line if line + written = { + before: before, after: after, line: line + }.select { |_, value| value } + sp = ensure_para_props.ensure_spacing(*written.keys) + written.each { |field, value| sp.public_send(:"#{field}=", value) } self end diff --git a/lib/uniword/wordprocessingml/paragraph_properties.rb b/lib/uniword/wordprocessingml/paragraph_properties.rb index b9fc49ff..72fa7cf7 100644 --- a/lib/uniword/wordprocessingml/paragraph_properties.rb +++ b/lib/uniword/wordprocessingml/paragraph_properties.rb @@ -22,8 +22,9 @@ class ParagraphProperties < Lutaml::Model::Serializable attribute :outline_level, Properties::OutlineLevel attribute :numbering_properties, Properties::NumberingProperties - # Complex spacing object - attribute :spacing, Properties::Spacing + # Complex spacing objects. Word can emit more than one w:spacing in a + # single w:pPr, so this is a collection. + attribute :spacing, Properties::Spacing, collection: true # Complex indentation object attribute :indentation, Properties::Indentation @@ -296,6 +297,33 @@ def initialize(attrs = {}) convert_flat_attributes! end + # First w:spacing entry, appending one when absent + # + # Word can emit more than one w:spacing inside a single w:pPr, so the + # attribute is a collection. Callers setting individual spacing fields + # want the first entry. + # + # Each field named in +clearing+ is dropped from the later entries. A + # value written into the first entry while a later one still carries its + # own would emit two contradictory values, and a last-wins reader keeps + # the stale one. + # + # @param clearing [Array] fields the caller is about to write + # @return [Properties::Spacing] the first spacing entry + def ensure_spacing(*clearing) + entries = Array(spacing) + if entries.empty? + entries = [Properties::Spacing.new] + self.spacing = entries + end + + entries.drop(1).each do |stale| + clearing.each { |field| stale.public_send(:"#{field}=", nil) } + end + + entries.first + end + # Convert flat convenience attributes to proper wrapper objects # This handles cases like ParagraphProperties.new(spacing_before: 120) # where the flat attribute is set but the wrapper object is not @@ -303,13 +331,16 @@ def convert_flat_attributes! # Alignment - convert string to Alignment wrapper self.alignment = Properties::Alignment.new(value: @alignment) if @alignment.is_a?(String) - # Spacing - create spacing object from flat spacing attributes - if (@spacing_before || @spacing_after || @line_spacing || @line_rule) && !@spacing - self.spacing = Properties::Spacing.new - spacing.before = @spacing_before if @spacing_before - spacing.after = @spacing_after if @spacing_after - spacing.line = @line_spacing.to_i if @line_spacing - spacing.line_rule = @line_rule if @line_rule + # Spacing - create spacing object from flat spacing attributes. + # Array() covers both absent forms: nil when built, [] when parsed. + if (@spacing_before || @spacing_after || @line_spacing || @line_rule) && + Array(@spacing).empty? + entry = Properties::Spacing.new + entry.before = @spacing_before if @spacing_before + entry.after = @spacing_after if @spacing_after + entry.line = @line_spacing.to_i if @line_spacing + entry.line_rule = @line_rule if @line_rule + self.spacing = [entry] end # Indentation - create indentation object from flat indent attributes diff --git a/lib/uniword/wordprocessingml/style.rb b/lib/uniword/wordprocessingml/style.rb index 72b750e6..69735d34 100644 --- a/lib/uniword/wordprocessingml/style.rb +++ b/lib/uniword/wordprocessingml/style.rb @@ -159,12 +159,14 @@ def quick_format val == true end + # Word splits one paragraph's spacing across several w:spacing elements, + # so take the first entry that actually carries the value. def spacing_before - pPr&.spacing&.before || pPr&.spacing_before + Array(pPr&.spacing).filter_map(&:before).first || pPr&.spacing_before end def spacing_after - pPr&.spacing&.after || pPr&.spacing_after + Array(pPr&.spacing).filter_map(&:after).first || pPr&.spacing_after end def alignment diff --git a/spec/uniword/builder/paragraph_builder_spec.rb b/spec/uniword/builder/paragraph_builder_spec.rb index e2fe3259..153f91e4 100644 --- a/spec/uniword/builder/paragraph_builder_spec.rb +++ b/spec/uniword/builder/paragraph_builder_spec.rb @@ -180,15 +180,61 @@ it "sets spacing before and after" do builder = described_class.new builder.spacing(before: 240, after: 120) - expect(builder.model.properties.spacing.before).to eq(240) - expect(builder.model.properties.spacing.after).to eq(120) + spacing = builder.model.properties.spacing.first + expect(spacing.before).to eq(240) + expect(spacing.after).to eq(120) end it "sets line spacing with rule" do builder = described_class.new builder.spacing(line: 360, rule: "exact") - expect(builder.model.properties.spacing.line).to eq(360) - expect(builder.model.properties.spacing.line_rule).to eq("exact") + spacing = builder.model.properties.spacing.first + expect(spacing.line).to eq(360) + expect(spacing.line_rule).to eq("exact") + end + + it "reuses the existing entry instead of appending a second" do + builder = described_class.new + builder.spacing(before: 240) + builder.spacing(after: 120) + expect(builder.model.properties.spacing.size).to eq(1) + expect(builder.model.properties.spacing.first.before).to eq(240) + expect(builder.model.properties.spacing.first.after).to eq(120) + end + + # A pPr whose spacing Word split across two w:spacing elements. + context "when a later entry already carries the field" do + let(:two_entries_xml) do + <<~XML + + + + + XML + end + let(:properties) do + Uniword::Wordprocessingml::ParagraphProperties.from_xml(two_entries_xml) + end + let(:paragraph) do + para = Uniword::Wordprocessingml::Paragraph.new + para.properties = properties + para + end + + # Leaving the stale 240 standing emits two w:line, and a last-wins + # reader keeps 240 rather than the 360 that was asked for. + it "emits only the value it was asked for" do + described_class.new(paragraph).spacing(line: 360) + + expect(paragraph.properties.to_xml(prefix: true).scan(/w:line="\d+"/)) + .to eq(['w:line="360"']) + end + + it "leaves fields it was not asked to set alone" do + described_class.new(paragraph).spacing(after: 120) + + expect(paragraph.properties.spacing.map(&:line)).to eq([nil, 240]) + end end end diff --git a/spec/uniword/builder/style_builder_spec.rb b/spec/uniword/builder/style_builder_spec.rb index e278f81f..82db8622 100644 --- a/spec/uniword/builder/style_builder_spec.rb +++ b/spec/uniword/builder/style_builder_spec.rb @@ -55,8 +55,34 @@ it "sets spacing" do sb = described_class.new("S") sb.spacing(before: 240, after: 120) - expect(sb.model.pPr.spacing.before).to eq(240) - expect(sb.model.pPr.spacing.after).to eq(120) + spacing = sb.model.pPr.spacing.first + expect(spacing.before).to eq(240) + expect(spacing.after).to eq(120) + end + + # A .dotx style whose spacing Word split across two w:spacing elements. + context "when a later entry already carries the field" do + let(:two_entries_xml) do + <<~XML + + + + + XML + end + let(:style_builder) do + sb = described_class.new("S") + sb.model.pPr = + Uniword::Wordprocessingml::ParagraphProperties.from_xml(two_entries_xml) + sb + end + + it "sets spacing without leaving the stale value standing" do + style_builder.spacing(before: 111) + + expect(style_builder.model.pPr.spacing.map(&:before)).to eq([111, nil]) + expect(style_builder.model.pPr.spacing.map(&:line)).to eq([nil, 240]) + end end end diff --git a/spec/uniword/docx/reconciler_spec.rb b/spec/uniword/docx/reconciler_spec.rb index b8639f32..3d6e5704 100644 --- a/spec/uniword/docx/reconciler_spec.rb +++ b/spec/uniword/docx/reconciler_spec.rb @@ -44,7 +44,7 @@ def build_package(settings: nil, footnotes: nil, endnotes: nil) para = sep.paragraphs.first expect(para).not_to be_nil expect(para.properties).not_to be_nil - spacing = para.properties.spacing + spacing = Array(para.properties.spacing).first expect(spacing).not_to be_nil expect(spacing.after).to eq(0) expect(spacing.line).to eq(240) diff --git a/spec/uniword/properties/paragraph_properties_spec.rb b/spec/uniword/properties/paragraph_properties_spec.rb index c1bb240c..8c0770a3 100644 --- a/spec/uniword/properties/paragraph_properties_spec.rb +++ b/spec/uniword/properties/paragraph_properties_spec.rb @@ -20,8 +20,16 @@ ) expect(Array(props.style).first.value).to eq("Heading1") expect(props.alignment.value).to eq("center") - expect(props.spacing&.before).to eq(240) - expect(props.spacing&.after).to eq(120) + expect(props.spacing.first.before).to eq(240) + expect(props.spacing.first.after).to eq(120) + end + + it "builds a single spacing entry from flat attributes" do + props = described_class.new(spacing_before: 120, spacing_after: 240) + + expect(props.spacing.size).to eq(1) + expect(props.spacing.first.before).to eq(120) + expect(props.spacing.first.after).to eq(240) end it "allows mutation for test compatibility" do @@ -129,6 +137,93 @@ end end + describe "#ensure_spacing" do + let(:ns) do + "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + end + let(:two_entries_xml) do + <<~XML + + + + + XML + end + + it "appends an entry when the properties were built without spacing" do + props = described_class.new + + props.ensure_spacing.before = 240 + + expect(props.spacing.size).to eq(1) + expect(props.spacing.first.before).to eq(240) + end + + it "appends an entry when parsing left the collection empty" do + props = described_class.from_xml(%()) + + props.ensure_spacing.before = 240 + + expect(props.spacing.size).to eq(1) + expect(props.spacing.first.before).to eq(240) + end + + # Reconciler builds properties this way (notes.rb, parts.rb), passing a + # single Spacing rather than an array. + # + # Assert the entry and the emitted XML, not the shape of `spacing` itself: + # whether lutaml-model wraps a scalar assignment into a one-element array + # differs between lutaml-model releases, and that is not our contract. + it "handles properties constructed with a scalar spacing" do + props = described_class.new( + spacing: Uniword::Properties::Spacing.new(after: 0, line: 240), + ) + + entry = props.ensure_spacing + + expect(entry.after).to eq(0) + expect(entry.line).to eq(240) + expect(props.to_xml(prefix: true)).to include('w:line="240"') + end + + it "returns the existing first entry instead of appending" do + props = described_class.from_xml( + %(), + ) + + props.ensure_spacing.after = 120 + + expect(props.spacing.size).to eq(1) + expect(props.spacing.first.before).to eq(60) + expect(props.spacing.first.after).to eq(120) + end + + it "returns the first of several entries Word emitted" do + props = described_class.from_xml(two_entries_xml) + + expect(props.ensure_spacing).to equal(props.spacing.first) + expect(props.spacing.size).to eq(2) + end + + it "clears the named fields from the later entries" do + props = described_class.from_xml(two_entries_xml) + + props.ensure_spacing(:line).line = 360 + + expect(props.spacing.map(&:line)).to eq([360, nil]) + expect(props.to_xml(prefix: true).scan(/w:line="\d+"/)) + .to eq(['w:line="360"']) + end + + it "leaves the later entries alone when no field is named" do + props = described_class.from_xml(two_entries_xml) + + props.ensure_spacing.before = 60 + + expect(props.spacing.map(&:line)).to eq([nil, 240]) + end + end + describe "inheritance" do it "inherits from Lutaml::Model::Serializable" do expect(described_class.ancestors) diff --git a/spec/uniword/wordprocessingml/line_spacing_spec.rb b/spec/uniword/wordprocessingml/line_spacing_spec.rb index 217f9d41..26c196f2 100644 --- a/spec/uniword/wordprocessingml/line_spacing_spec.rb +++ b/spec/uniword/wordprocessingml/line_spacing_spec.rb @@ -17,23 +17,22 @@ context "with numeric value (RAW OOXML twips)" do it "sets line spacing as raw twips value" do para.properties ||= Uniword::Wordprocessingml::ParagraphProperties.new - para.properties.spacing ||= Uniword::Properties::Spacing.new - para.properties.spacing.line = 360 - expect(para.properties&.spacing&.line).to eq(360) + para.properties.ensure_spacing.line = 360 + expect(Array(para.properties&.spacing).first&.line).to eq(360) end it "sets line spacing with rule via ParagraphBuilder" do Uniword::Builder::ParagraphBuilder.new(para).spacing(line: 240, rule: "exact") - expect(para.properties.spacing.line).to eq(240) - expect(para.properties.spacing.line_rule).to eq("exact") + expect(para.properties.spacing.first.line).to eq(240) + expect(para.properties.spacing.first.line_rule).to eq("exact") end it "sets line spacing with auto rule" do Uniword::Builder::ParagraphBuilder.new(para).spacing(line: 360, rule: "auto") - expect(para.properties.spacing.line).to eq(360) - expect(para.properties.spacing.line_rule).to eq("auto") + expect(para.properties.spacing.first.line).to eq(360) + expect(para.properties.spacing.first.line_rule).to eq("auto") end end @@ -41,22 +40,22 @@ it "sets exact line spacing" do Uniword::Builder::ParagraphBuilder.new(para).spacing(line: 240, rule: "exact") - expect(para.properties.spacing.line).to eq(240) - expect(para.properties.spacing.line_rule).to eq("exact") + expect(para.properties.spacing.first.line).to eq(240) + expect(para.properties.spacing.first.line_rule).to eq("exact") end it 'sets "at least" line spacing' do Uniword::Builder::ParagraphBuilder.new(para).spacing(line: 280, rule: "atLeast") - expect(para.properties.spacing.line).to eq(280) - expect(para.properties.spacing.line_rule).to eq("atLeast") + expect(para.properties.spacing.first.line).to eq(280) + expect(para.properties.spacing.first.line_rule).to eq("atLeast") end it "handles string keys" do Uniword::Builder::ParagraphBuilder.new(para).spacing(line: 240, rule: "exact") - expect(para.properties.spacing.line).to eq(240) - expect(para.properties.spacing.line_rule).to eq("exact") + expect(para.properties.spacing.first.line).to eq(240) + expect(para.properties.spacing.first.line_rule).to eq("exact") end end end @@ -73,13 +72,12 @@ it "returns raw integer value (twips)" do para.properties ||= Uniword::Wordprocessingml::ParagraphProperties.new - para.properties.spacing ||= Uniword::Properties::Spacing.new - para.properties.spacing.line = 360 - expect(para.properties&.spacing&.line).to eq(360) + para.properties.ensure_spacing.line = 360 + expect(Array(para.properties&.spacing).first&.line).to eq(360) end it "returns nil when not set" do - expect(para.properties&.spacing&.line).to be_nil + expect(Array(para.properties&.spacing).first&.line).to be_nil end end @@ -144,8 +142,8 @@ doc = Uniword::Wordprocessingml::DocumentRoot.from_xml(xml) para = doc.body.paragraphs.first - expect(para.properties.spacing.line).to eq(240) - expect(para.properties.spacing.line_rule).to eq("exact") + expect(para.properties.spacing.first.line).to eq(240) + expect(para.properties.spacing.first.line_rule).to eq("exact") end it "deserializes auto line spacing" do @@ -165,8 +163,8 @@ doc = Uniword::Wordprocessingml::DocumentRoot.from_xml(xml) para = doc.body.paragraphs.first - expect(para.properties.spacing.line).to eq(360) - expect(para.properties.spacing.line_rule).to eq("auto") + expect(para.properties.spacing.first.line).to eq(360) + expect(para.properties.spacing.first.line_rule).to eq("auto") end it 'deserializes "at least" line spacing' do @@ -186,8 +184,8 @@ doc = Uniword::Wordprocessingml::DocumentRoot.from_xml(xml) para = doc.body.paragraphs.first - expect(para.properties.spacing.line).to eq(280) - expect(para.properties.spacing.line_rule).to eq("atLeast") + expect(para.properties.spacing.first.line).to eq(280) + expect(para.properties.spacing.first.line_rule).to eq("atLeast") end end @@ -205,8 +203,8 @@ doc2 = Uniword::Wordprocessingml::DocumentRoot.from_xml(xml) para2 = doc2.body.paragraphs.first - expect(para2.properties.spacing.line).to eq(240) - expect(para2.properties.spacing.line_rule).to eq("exact") + expect(para2.properties.spacing.first.line).to eq(240) + expect(para2.properties.spacing.first.line_rule).to eq("exact") end it "preserves auto line spacing through round-trip" do @@ -222,8 +220,8 @@ doc2 = Uniword::Wordprocessingml::DocumentRoot.from_xml(xml) para2 = doc2.body.paragraphs.first - expect(para2.properties.spacing.line).to eq(360) - expect(para2.properties.spacing.line_rule).to eq("auto") + expect(para2.properties.spacing.first.line).to eq(360) + expect(para2.properties.spacing.first.line_rule).to eq("auto") end it 'preserves "at least" line spacing through round-trip' do @@ -239,8 +237,8 @@ doc2 = Uniword::Wordprocessingml::DocumentRoot.from_xml(xml) para2 = doc2.body.paragraphs.first - expect(para2.properties.spacing.line).to eq(280) - expect(para2.properties.spacing.line_rule).to eq("atLeast") + expect(para2.properties.spacing.first.line).to eq(280) + expect(para2.properties.spacing.first.line_rule).to eq("atLeast") end end end diff --git a/spec/uniword/wordprocessingml/paragraph_enhanced_properties_spec.rb b/spec/uniword/wordprocessingml/paragraph_enhanced_properties_spec.rb index 3d4ce4bd..c25fb97f 100644 --- a/spec/uniword/wordprocessingml/paragraph_enhanced_properties_spec.rb +++ b/spec/uniword/wordprocessingml/paragraph_enhanced_properties_spec.rb @@ -156,7 +156,7 @@ expect(Array(paragraph.properties&.style).first&.value).to eq("Heading1") expect(paragraph.properties&.alignment&.value).to eq("center") - expect(paragraph.properties&.spacing&.before).to eq(240) + expect(Array(paragraph.properties&.spacing).first&.before).to eq(240) expect(paragraph.properties.borders).not_to be_nil expect(paragraph.properties.shading).not_to be_nil expect(paragraph.properties.tabs).not_to be_nil diff --git a/spec/uniword/wordprocessingml/style_roundtrip_spec.rb b/spec/uniword/wordprocessingml/style_roundtrip_spec.rb index 5cb1472f..5d107037 100644 --- a/spec/uniword/wordprocessingml/style_roundtrip_spec.rb +++ b/spec/uniword/wordprocessingml/style_roundtrip_spec.rb @@ -100,4 +100,56 @@ expect(style.font_color_theme_tint).to eq("BF") end end + + # Word splits one paragraph's spacing across several w:spacing elements + # (seen in Modern.dotx). Covered with inline XML so it runs without the + # private fixture submodule. + describe "repeated w:spacing in one w:pPr" do + let(:repeated_spacing_xml) do + <<~XML + + + + + + + + + XML + end + let(:style) do + Uniword::Wordprocessingml::Style.from_xml(repeated_spacing_xml) + end + + it "keeps every entry when parsing" do + expect(style.paragraph_properties.spacing.map do |s| + [s.before, s.after, s.line, s.line_rule] + end).to eq([[0, 0, nil, nil], [nil, nil, 240, "auto"]]) + end + + it "keeps every entry through a round-trip" do + reparsed = Uniword::Wordprocessingml::Style.from_xml( + style.to_xml(prefix: true), + ) + + expect(reparsed.paragraph_properties.spacing.map do |s| + [s.before, s.after, s.line, s.line_rule] + end).to eq([[0, 0, nil, nil], [nil, nil, 240, "auto"]]) + end + + it "reads spacing_before and spacing_after across entries" do + expect([style.spacing_before, style.spacing_after]).to eq([0, 0]) + end + + it "finds a value that lives on a later entry" do + style = Uniword::Wordprocessingml::Style.from_xml( + repeated_spacing_xml.sub('w:before="0" w:after="0"', 'w:line="240"') + .sub('w:line="240" w:lineRule="auto"', + 'w:before="120" w:after="60"'), + ) + + expect([style.spacing_before, style.spacing_after]).to eq([120, 60]) + end + end end diff --git a/spec/uniword/wordprocessingml/styleset_integration_spec.rb b/spec/uniword/wordprocessingml/styleset_integration_spec.rb index 3e0f82d1..2cde54d6 100644 --- a/spec/uniword/wordprocessingml/styleset_integration_spec.rb +++ b/spec/uniword/wordprocessingml/styleset_integration_spec.rb @@ -133,7 +133,7 @@ expect(heading1.run_properties).not_to be_nil pPr = heading1.paragraph_properties - expect(pPr.spacing.before).to eq(300) if pPr.spacing + expect(heading1.spacing_before).to eq(300) expect(pPr.outline_level.value).to eq(0) if pPr.outline_level rPr = heading1.run_properties diff --git a/spec/uniword/wordprocessingml/styleset_roundtrip_spec.rb b/spec/uniword/wordprocessingml/styleset_roundtrip_spec.rb index 6d60f561..d95f350a 100644 --- a/spec/uniword/wordprocessingml/styleset_roundtrip_spec.rb +++ b/spec/uniword/wordprocessingml/styleset_roundtrip_spec.rb @@ -38,14 +38,17 @@ it "preserves spacing" do skip "Heading1 not found" unless heading1 skip "No paragraph properties" unless heading1.paragraph_properties - skip "No spacing" unless heading1.paragraph_properties.spacing - original = heading1.paragraph_properties.spacing + original = Array(heading1.paragraph_properties.spacing) + skip "No spacing" if original.empty? + xml = heading1.to_xml(prefix: true) reparsed = Uniword::Wordprocessingml::Style.from_xml(xml) + roundtripped = Array(reparsed.paragraph_properties.spacing) - expect(reparsed.paragraph_properties.spacing.before).to eq(original.before) - expect(reparsed.paragraph_properties.spacing.after).to eq(original.after) + expect(roundtripped.size).to eq(original.size) + expect(roundtripped.map(&:before)).to eq(original.map(&:before)) + expect(roundtripped.map(&:after)).to eq(original.map(&:after)) end end @@ -111,14 +114,17 @@ it "preserves spacing" do skip "Heading1 not found" unless heading1 skip "No paragraph properties" unless heading1.paragraph_properties - skip "No spacing" unless heading1.paragraph_properties.spacing - original = heading1.paragraph_properties.spacing + original = Array(heading1.paragraph_properties.spacing) + skip "No spacing" if original.empty? + xml = heading1.to_xml(prefix: true) reparsed = Uniword::Wordprocessingml::Style.from_xml(xml) + roundtripped = Array(reparsed.paragraph_properties.spacing) - expect(reparsed.paragraph_properties.spacing.before).to eq(original.before) - expect(reparsed.paragraph_properties.spacing.after).to eq(original.after) + expect(roundtripped.size).to eq(original.size) + expect(roundtripped.map(&:before)).to eq(original.map(&:before)) + expect(roundtripped.map(&:after)).to eq(original.map(&:after)) end it "preserves alignment" do @@ -167,4 +173,32 @@ after(:all) do Dir.glob("spec/fixtures/uniword-private/word-resources/quick-styles/*.dotx").count end + + # Word emits two w:spacing elements in one w:pPr in Modern.dotx, which is + # why ParagraphProperties#spacing is a collection. Keep both. + describe "repeated w:spacing in a single w:pPr" do + let(:file) do + "spec/fixtures/uniword-private/word-resources/quick-styles/Modern.dotx" + end + let(:style) do + skip "Modern.dotx not available" unless File.exist?(file) + Uniword::StyleSet.from_dotx(file).styles.find { |s| s.id == "NoSpacing" } + end + let(:fields) do + ->(list) { list.map { |s| [s.before, s.after, s.line, s.line_rule] } } + end + + it "keeps both entries when loading" do + expect(style.paragraph_properties.spacing.size).to eq(2) + end + + it "keeps both entries through an XML round-trip" do + reparsed = Uniword::Wordprocessingml::Style.from_xml( + style.to_xml(prefix: true), + ) + + expect(fields.call(reparsed.paragraph_properties.spacing)) + .to eq(fields.call(style.paragraph_properties.spacing)) + end + end end