Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
11 changes: 5 additions & 6 deletions lib/uniword/builder/paragraph_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
sp = ensure_properties.ensure_spacing
sp.before = before if before
sp.after = after if after
sp.line = line if line
sp.line_rule = rule if rule
self
end

Expand Down
9 changes: 4 additions & 5 deletions lib/uniword/builder/style_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,10 @@ 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
sp = ensure_para_props.ensure_spacing
sp.before = before if before
sp.after = after if after
sp.line = line if line
self
end

Expand Down
38 changes: 29 additions & 9 deletions lib/uniword/wordprocessingml/paragraph_properties.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -296,20 +297,39 @@ 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.
#
# @return [Properties::Spacing] the first spacing entry
def ensure_spacing
entries = Array(spacing)
entries.first || begin
entry = Properties::Spacing.new
self.spacing = entries + [entry]
entry
end
end
Comment thread
HassanAkbar marked this conversation as resolved.
Outdated

# 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
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
Expand Down
6 changes: 4 additions & 2 deletions lib/uniword/wordprocessingml/style.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions spec/uniword/builder/paragraph_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,26 @@
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
end

Expand Down
5 changes: 3 additions & 2 deletions spec/uniword/builder/style_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@
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
end

Expand Down
2 changes: 1 addition & 1 deletion spec/uniword/docx/reconciler_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 61 additions & 2 deletions spec/uniword/properties/paragraph_properties_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -129,6 +137,57 @@
end
end

describe "#ensure_spacing" do
let(:ns) do
"http://schemas.openxmlformats.org/wordprocessingml/2006/main"
end
let(:two_entries_xml) do
<<~XML
<w:pPr xmlns:w="#{ns}">
<w:spacing w:before="0" w:after="0"/>
<w:spacing w:line="240" w:lineRule="auto"/>
</w:pPr>
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(%(<w:pPr xmlns:w="#{ns}"/>))

props.ensure_spacing.before = 240

expect(props.spacing.size).to eq(1)
expect(props.spacing.first.before).to eq(240)
end

it "returns the existing first entry instead of appending" do
props = described_class.from_xml(
%(<w:pPr xmlns:w="#{ns}"><w:spacing w:before="60"/></w:pPr>),
)

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
end

describe "inheritance" do
it "inherits from Lutaml::Model::Serializable" do
expect(described_class.ancestors)
Expand Down
56 changes: 27 additions & 29 deletions spec/uniword/wordprocessingml/line_spacing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,46 +17,45 @@
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

context "with hash format" do
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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading