sheng 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.watchr +99 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +62 -0
- data/Rakefile +10 -0
- data/docs/creating_templates.docx +0 -0
- data/docs/creating_templates.md +392 -0
- data/lib/sheng/block.rb +59 -0
- data/lib/sheng/check_box.rb +43 -0
- data/lib/sheng/conditional_block.rb +30 -0
- data/lib/sheng/data_set.rb +45 -0
- data/lib/sheng/docx.rb +75 -0
- data/lib/sheng/merge_field.rb +208 -0
- data/lib/sheng/merge_field_set.rb +90 -0
- data/lib/sheng/path_helpers.rb +15 -0
- data/lib/sheng/sequence.rb +66 -0
- data/lib/sheng/support.rb +57 -0
- data/lib/sheng/version.rb +3 -0
- data/lib/sheng/wml_file.rb +47 -0
- data/lib/sheng.rb +21 -0
- data/sheng.gemspec +31 -0
- data/spec/fixtures/bad_docx_files/with_field_not_in_dataset.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_missing_sequence_start.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_old_mergefields.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_poorly_nested_sequences.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_unended_sequence.docx +0 -0
- data/spec/fixtures/docx_files/input_document.docx +0 -0
- data/spec/fixtures/docx_files/old_style/input_document.docx +0 -0
- data/spec/fixtures/docx_files/old_style/output_document.docx +0 -0
- data/spec/fixtures/docx_files/output_document.docx +0 -0
- data/spec/fixtures/inputs/complete.json +61 -0
- data/spec/fixtures/inputs/incomplete.json +52 -0
- data/spec/fixtures/trees/embedded_sequence.yml +13 -0
- data/spec/fixtures/trees/merge_field_set.yml +16 -0
- data/spec/fixtures/xml_fragments/input/check_box/check_box.xml +11 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/bad/badly_nested_conditional.xml +105 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/bad/unclosed_conditional.xml +35 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_if.xml +84 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_inline.xml +59 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_unless.xml +51 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_in_table.xml +176 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/embedded_conditional.xml +79 -0
- data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_new.xml +19 -0
- data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_old.xml +9 -0
- data/spec/fixtures/xml_fragments/input/merge_field/bad/unclosed_merge_field.xml +25 -0
- data/spec/fixtures/xml_fragments/input/merge_field/inline_merge_field.xml +26 -0
- data/spec/fixtures/xml_fragments/input/merge_field/merge_field.xml +14 -0
- data/spec/fixtures/xml_fragments/input/merge_field/new_merge_field.xml +28 -0
- data/spec/fixtures/xml_fragments/input/merge_field/split_merge_field.xml +33 -0
- data/spec/fixtures/xml_fragments/input/merge_field_set/complex_nesting_and_reuse.xml +231 -0
- data/spec/fixtures/xml_fragments/input/merge_field_set/merge_field_set.xml +89 -0
- data/spec/fixtures/xml_fragments/input/merge_field_set/with_non_mergefield_fields.xml +43 -0
- data/spec/fixtures/xml_fragments/input/sequence/array_sequence.xml +23 -0
- data/spec/fixtures/xml_fragments/input/sequence/bad/badly_nested_sequence.xml +62 -0
- data/spec/fixtures/xml_fragments/input/sequence/bad/unclosed_sequence.xml +32 -0
- data/spec/fixtures/xml_fragments/input/sequence/embedded_sequence.xml +68 -0
- data/spec/fixtures/xml_fragments/input/sequence/inline_sequence.xml +24 -0
- data/spec/fixtures/xml_fragments/input/sequence/overridden_iterator_array_sequence.xml +23 -0
- data/spec/fixtures/xml_fragments/input/sequence/sequence.xml +39 -0
- data/spec/fixtures/xml_fragments/input/sequence/sequence_in_table.xml +125 -0
- data/spec/fixtures/xml_fragments/input/sequence/sequence_with_section_formatting.xml +41 -0
- data/spec/fixtures/xml_fragments/input/sequence/series_with_commas.xml +73 -0
- data/spec/fixtures/xml_fragments/output/check_box/check_box.xml +11 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_does_not_exist.xml +52 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_exists.xml +84 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_both.xml +11 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_inside.xml +5 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_outside.xml +8 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/if_does_not_exist.xml +5 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/if_exists.xml +19 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/inline_does_not_exist.xml +10 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/inline_exists.xml +13 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/unless_does_not_exist.xml +11 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/unless_exists.xml +5 -0
- data/spec/fixtures/xml_fragments/output/merge_field/inline_merge_field.xml +11 -0
- data/spec/fixtures/xml_fragments/output/merge_field/merge_field.xml +12 -0
- data/spec/fixtures/xml_fragments/output/merge_field/split_merge_field.xml +10 -0
- data/spec/fixtures/xml_fragments/output/merge_field_set/complex_nesting_and_reuse.xml +190 -0
- data/spec/fixtures/xml_fragments/output/merge_field_set/merge_field_set.xml +75 -0
- data/spec/fixtures/xml_fragments/output/merge_field_set/with_non_mergefield_fields.xml +31 -0
- data/spec/fixtures/xml_fragments/output/sequence/array_sequence.xml +17 -0
- data/spec/fixtures/xml_fragments/output/sequence/embedded_sequence.xml +56 -0
- data/spec/fixtures/xml_fragments/output/sequence/inline_sequence.xml +16 -0
- data/spec/fixtures/xml_fragments/output/sequence/overridden_iterator_array_sequence.xml +12 -0
- data/spec/fixtures/xml_fragments/output/sequence/sequence.xml +28 -0
- data/spec/fixtures/xml_fragments/output/sequence/sequence_in_table.xml +120 -0
- data/spec/fixtures/xml_fragments/output/sequence/sequence_with_section_formatting.xml +46 -0
- data/spec/fixtures/xml_fragments/output/sequence/series_with_commas.xml +43 -0
- data/spec/fixtures/xml_fragments/output/sequence/series_with_commas_two_items.xml +31 -0
- data/spec/lib/sheng/check_box_spec.rb +87 -0
- data/spec/lib/sheng/conditional_block_spec.rb +151 -0
- data/spec/lib/sheng/data_set_spec.rb +65 -0
- data/spec/lib/sheng/docx_spec.rb +146 -0
- data/spec/lib/sheng/merge_field_set_spec.rb +165 -0
- data/spec/lib/sheng/merge_field_spec.rb +276 -0
- data/spec/lib/sheng/sequence_spec.rb +201 -0
- data/spec/lib/sheng/wml_file_spec.rb +38 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/path_helper.rb +15 -0
- data/spec/support/xml_helper.rb +28 -0
- metadata +355 -0
@@ -0,0 +1,146 @@
|
|
1
|
+
describe Sheng::Docx do
|
2
|
+
let(:output_file) { "/tmp/sheng_output_document.docx" }
|
3
|
+
let(:expected_output_file) { fixture_path("docx_files/output_document.docx") }
|
4
|
+
let(:input_file) { fixture_path("docx_files/input_document.docx") }
|
5
|
+
let(:input_hash) { JSON.parse(File.read(fixture_path("inputs/complete.json"))) }
|
6
|
+
let(:mutable_documents) {
|
7
|
+
['word/document.xml', 'word/numbering.xml', 'word/header1.xml']
|
8
|
+
}
|
9
|
+
|
10
|
+
subject { described_class.new(input_file, input_hash) }
|
11
|
+
|
12
|
+
after(:each) do
|
13
|
+
FileUtils.rm(output_file) if File.exists?(output_file)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#generate' do
|
17
|
+
it 'should produce the same document as fixtures output_file' do
|
18
|
+
subject.generate(output_file)
|
19
|
+
in_mutable_wml_files(output_file) do |file_name, output_wml|
|
20
|
+
expected_wml = Zip::File.new(expected_output_file).read(file_name)
|
21
|
+
expect(output_wml).to be_equivalent_to expected_wml
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'raises an exception if file already exists and force option not given' do
|
26
|
+
File.open(output_file, "w").write("nothing")
|
27
|
+
expect {
|
28
|
+
subject.generate(output_file)
|
29
|
+
}.to raise_error(described_class::FileAlreadyExists)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'overwrites file if file already exists but force option is true' do
|
33
|
+
File.open(output_file, "w").write("nothing")
|
34
|
+
subject.generate(output_file, force: true)
|
35
|
+
in_mutable_wml_files(output_file) do |file_name, output_wml|
|
36
|
+
expected_wml = Zip::File.new(expected_output_file).read(file_name)
|
37
|
+
expect(output_wml).to be_equivalent_to expected_wml
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "with older style mergefields" do
|
42
|
+
let(:expected_output_file) { fixture_path("docx_files/old_style/output_document.docx") }
|
43
|
+
let(:input_file) { fixture_path("docx_files/old_style/input_document.docx") }
|
44
|
+
|
45
|
+
it 'still works' do
|
46
|
+
subject.generate(output_file)
|
47
|
+
in_mutable_wml_files(output_file) do |file_name, output_wml|
|
48
|
+
expected_wml = Zip::File.new(expected_output_file).read(file_name)
|
49
|
+
expect(output_wml).to be_equivalent_to expected_wml
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should replace all mergefields when given all mergefield values" do
|
55
|
+
subject.generate(output_file)
|
56
|
+
Zip::File.new(output_file).entries.each do |file|
|
57
|
+
if mutable_documents.include?(file.name)
|
58
|
+
Zip::File.open(output_file) do |zip|
|
59
|
+
xml = zip.read(file)
|
60
|
+
expect(Nokogiri::XML(xml).xpath("//w:fldSimple[contains(@w:instr, 'MERGEFIELD')]")).to be_empty
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should raise an error when one or more mergefields isn't merged" do
|
67
|
+
incomplete_hash = JSON.parse(File.read(fixture_path("inputs/incomplete.json")))
|
68
|
+
doc = described_class.new(input_file, incomplete_hash)
|
69
|
+
expect {
|
70
|
+
doc.generate(output_file)
|
71
|
+
}.to raise_error(Sheng::WMLFile::MergefieldNotReplacedError, "Mergefields not replaced: first_name, last_name")
|
72
|
+
end
|
73
|
+
|
74
|
+
shared_examples_for 'a bad document' do |filename, error, error_message = nil|
|
75
|
+
it "should raise #{error} when given #{filename}" do
|
76
|
+
doc = described_class.new(fixture_path("bad_docx_files/#{filename}"), input_hash)
|
77
|
+
expect {
|
78
|
+
doc.generate(output_file)
|
79
|
+
}.to raise_error(error, error_message)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it_should_behave_like 'a bad document', 'with_field_not_in_dataset.docx',
|
84
|
+
Sheng::WMLFile::MergefieldNotReplacedError
|
85
|
+
|
86
|
+
it_should_behave_like 'a bad document', 'with_unended_sequence.docx',
|
87
|
+
Sheng::Sequence::MissingEndTag, "no end tag for start:owner_signature"
|
88
|
+
|
89
|
+
it_should_behave_like 'a bad document', 'with_missing_sequence_start.docx',
|
90
|
+
Sheng::WMLFile::MergefieldNotReplacedError
|
91
|
+
|
92
|
+
it_should_behave_like 'a bad document', 'with_poorly_nested_sequences.docx',
|
93
|
+
Sheng::Sequence::ImproperNesting, "expected end tag for start:birds, got end:animals"
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#new' do
|
97
|
+
it "should raise an error if zip file not found" do
|
98
|
+
expect {
|
99
|
+
described_class.new('definitely/not/a/real/path', {})
|
100
|
+
}.to raise_error(described_class::InvalidFile, "File definitely/not/a/real/path not found")
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should raise an ArgumentError if params is not a hash" do
|
104
|
+
allow(Sheng::DataSet).to receive(:new).with(:not_a_hash).and_raise(ArgumentError)
|
105
|
+
expect {
|
106
|
+
described_class.new(input_file, :not_a_hash)
|
107
|
+
}.to raise_error(ArgumentError)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'with fake document' do
|
112
|
+
let(:zip_double) {
|
113
|
+
entry1 = double(Zip::Entry, :name => 'word/document.xml', :get_input_stream => :document)
|
114
|
+
entry2 = double(Zip::Entry, :name => 'word/footer2.xml', :get_input_stream => :footer2)
|
115
|
+
entry3 = double(Zip::Entry, :name => 'not_wml.xml', :get_input_stream => :not_wml)
|
116
|
+
|
117
|
+
double(Zip::File, :entries => [entry1, entry2, entry3])
|
118
|
+
}
|
119
|
+
subject { described_class.new('a_fake_file.docx', {}) }
|
120
|
+
|
121
|
+
before(:each) do
|
122
|
+
allow(Sheng::WMLFile).to receive(:new).with('word/document.xml', :document).
|
123
|
+
and_return(double(:filename => 'word/document.xml', :to_tree => :document_tree, :required_hash => { :a => [{ :b => 1 }] }))
|
124
|
+
allow(Sheng::WMLFile).to receive(:new).with('word/footer2.xml', :footer2).
|
125
|
+
and_return(double(:filename => 'word/footer2.xml', :to_tree => :footer2_tree, :required_hash => { :a => [{ :c => 2 }] }))
|
126
|
+
allow(Zip::File).to receive(:new).with('a_fake_file.docx').and_return(zip_double)
|
127
|
+
end
|
128
|
+
|
129
|
+
describe '#to_tree' do
|
130
|
+
it "returns trees for every WML file in document" do
|
131
|
+
expect(subject.to_tree).to eq([
|
132
|
+
{ :file => 'word/document.xml', :tree => :document_tree },
|
133
|
+
{ :file => 'word/footer2.xml', :tree => :footer2_tree }
|
134
|
+
])
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe '#required_hash' do
|
139
|
+
it "returns deep merged #required_hash values from all WML files" do
|
140
|
+
expect(subject.required_hash).to eq({
|
141
|
+
:a => [{ :b => 1, :c => 2 }]
|
142
|
+
})
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
describe Sheng::MergeFieldSet do
|
2
|
+
let(:fragment) { xml_fragment('input/merge_field_set/merge_field_set') }
|
3
|
+
subject { described_class.new('key', fragment) }
|
4
|
+
|
5
|
+
describe '#interpolate' do
|
6
|
+
it 'iterates through nodes and calls interpolate on each' do
|
7
|
+
node1, node2 = double('Node'), double('Node')
|
8
|
+
expect(node1).to receive(:interpolate).with(:the_data_set)
|
9
|
+
expect(node2).to receive(:interpolate).with(:the_data_set)
|
10
|
+
allow(subject).to receive(:nodes).and_return([node1, node2])
|
11
|
+
subject.interpolate(:the_data_set)
|
12
|
+
end
|
13
|
+
|
14
|
+
context "with fields that are not valid mergefields" do
|
15
|
+
let(:fragment) { xml_fragment('input/merge_field_set/with_non_mergefield_fields') }
|
16
|
+
|
17
|
+
it "ignores the non-mergefield fields and interpolates the rest" do
|
18
|
+
dataset = Sheng::DataSet.new({ :color => "browns" })
|
19
|
+
|
20
|
+
subject.interpolate(dataset)
|
21
|
+
expect(subject.xml_fragment).to be_equivalent_to xml_fragment('output/merge_field_set/with_non_mergefield_fields')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns expected interpolated fragment' do
|
26
|
+
dataset = Sheng::DataSet.new({
|
27
|
+
:person => {
|
28
|
+
:first_name => 'Brad',
|
29
|
+
:last_name => 'Tucklemoof',
|
30
|
+
:socks => [
|
31
|
+
{ :color => 'Green', :size => 'Stumungous' },
|
32
|
+
{ :color => 'Whitish', :size => 'Teensy' }
|
33
|
+
]
|
34
|
+
},
|
35
|
+
:veggies => { :green => { :spinach => true } }
|
36
|
+
})
|
37
|
+
|
38
|
+
subject.interpolate(dataset)
|
39
|
+
expect(subject.xml_fragment).to be_equivalent_to xml_fragment('output/merge_field_set/merge_field_set')
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with complex nesting and reuse" do
|
43
|
+
let(:fragment) { xml_fragment('input/merge_field_set/complex_nesting_and_reuse') }
|
44
|
+
it 'works' do
|
45
|
+
dataset = Sheng::DataSet.new({
|
46
|
+
:people => [
|
47
|
+
{
|
48
|
+
:first_name => "Bringo",
|
49
|
+
:last_name => "Brango",
|
50
|
+
:favorites => {
|
51
|
+
:color => "Ghost",
|
52
|
+
:numbers => [1, "maybe", 0],
|
53
|
+
:loves_candy => false
|
54
|
+
}
|
55
|
+
},
|
56
|
+
{
|
57
|
+
:first_name => "Uncle",
|
58
|
+
:last_name => "Hork",
|
59
|
+
:favorites => {
|
60
|
+
:color => "Xi",
|
61
|
+
:numbers => ["unicorn", "paper"],
|
62
|
+
:loves_candy => true
|
63
|
+
}
|
64
|
+
}
|
65
|
+
],
|
66
|
+
:frogs => [
|
67
|
+
{
|
68
|
+
:warts => "bioluminescent",
|
69
|
+
:legs => "yak",
|
70
|
+
:feelings => ["insignificant", "worthless"]
|
71
|
+
},
|
72
|
+
{
|
73
|
+
:warts => 198,
|
74
|
+
:legs => "silky smooth",
|
75
|
+
:feelings => ["stylish", "nap"]
|
76
|
+
}
|
77
|
+
],
|
78
|
+
:king => {
|
79
|
+
:house => {
|
80
|
+
:windows => "bony",
|
81
|
+
:doors => "pig"
|
82
|
+
}
|
83
|
+
}
|
84
|
+
})
|
85
|
+
|
86
|
+
subject.interpolate(dataset)
|
87
|
+
expect(subject.xml_fragment).to be_equivalent_to xml_fragment('output/merge_field_set/complex_nesting_and_reuse')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#to_tree' do
|
93
|
+
it 'returns array of nodes for set, with nested sequences' do
|
94
|
+
expect(subject.to_tree).to eq tree_fixture('merge_field_set')
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'returns proper tree with embedded sequence' do
|
98
|
+
subject = described_class.new('key', xml_fragment('input/sequence/embedded_sequence'))
|
99
|
+
expect(subject.to_tree).to eq tree_fixture('embedded_sequence')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'throws exception if sequence missing end tag' do
|
103
|
+
subject = described_class.new('key', xml_fragment('input/sequence/bad/unclosed_sequence'))
|
104
|
+
expect {
|
105
|
+
subject.to_tree
|
106
|
+
}.to raise_error(Sheng::Sequence::MissingEndTag, "no end tag for start:library.books")
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'throws exception if sequence nesting is wrong' do
|
110
|
+
subject = described_class.new('key', xml_fragment('input/sequence/bad/badly_nested_sequence'))
|
111
|
+
expect {
|
112
|
+
subject.to_tree
|
113
|
+
}.to raise_error(Sheng::Sequence::ImproperNesting, "expected end tag for start:birds, got end:animals")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe '#required_hash' do
|
118
|
+
it 'returns skeleton hash demonstrating required data for interpolation' do
|
119
|
+
expect(subject.required_hash).to eq({
|
120
|
+
"person" => { "first_name" => nil, "last_name" => nil, "socks" => [{"color"=>nil, "size"=>nil}] },
|
121
|
+
"veggies"=> { "green" => { "spinach" => nil } }
|
122
|
+
})
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'uses given value as placeholder for mergefields/checkboxes' do
|
126
|
+
expect(subject.required_hash(:foo)).to eq({
|
127
|
+
"person" => { "first_name" => :foo, "last_name" => :foo, "socks" => [{"color"=>:foo, "size"=>:foo}] },
|
128
|
+
"veggies"=> { "green" => { "spinach" => :foo } }
|
129
|
+
})
|
130
|
+
end
|
131
|
+
|
132
|
+
context "with multiple blocks using the same variable" do
|
133
|
+
let(:fragment) { xml_fragment('input/merge_field_set/complex_nesting_and_reuse') }
|
134
|
+
|
135
|
+
it 'properly merges all requirements when multiple blocks reuse the same variable' do
|
136
|
+
expect(subject.required_hash).to eq({
|
137
|
+
"people" => [
|
138
|
+
{
|
139
|
+
"first_name" => nil,
|
140
|
+
"last_name" => nil,
|
141
|
+
"favorites" => {
|
142
|
+
"color" => nil,
|
143
|
+
"numbers" => [],
|
144
|
+
"loves_candy" => nil
|
145
|
+
}
|
146
|
+
}
|
147
|
+
],
|
148
|
+
"frogs" => [
|
149
|
+
{
|
150
|
+
"warts" => nil,
|
151
|
+
"legs" => nil,
|
152
|
+
"feelings" => []
|
153
|
+
}
|
154
|
+
],
|
155
|
+
"king" => {
|
156
|
+
"house" => {
|
157
|
+
"windows" => nil,
|
158
|
+
"doors" => nil
|
159
|
+
}
|
160
|
+
}
|
161
|
+
})
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
describe Sheng::MergeField do
|
2
|
+
let(:fragment) { xml_fragment('input/merge_field/merge_field') }
|
3
|
+
let(:element) { fragment.xpath("//w:fldSimple[contains(@w:instr, 'MERGEFIELD')]").first }
|
4
|
+
subject { described_class.new(element) }
|
5
|
+
|
6
|
+
describe ".from_element" do
|
7
|
+
it "returns a new MergeField for the given element" do
|
8
|
+
expect(described_class.from_element(element)).to eq(subject)
|
9
|
+
end
|
10
|
+
|
11
|
+
context "when given a new-style non-mergefield" do
|
12
|
+
let(:fragment) { xml_fragment('input/merge_field/bad/not_a_real_mergefield_new') }
|
13
|
+
let(:element) { fragment.xpath("//w:fldChar[contains(@w:fldCharType, 'begin')]").first }
|
14
|
+
|
15
|
+
it "returns nil" do
|
16
|
+
expect(described_class.from_element(element)).to be_nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "when given an old-style non-mergefield" do
|
21
|
+
let(:fragment) { xml_fragment('input/merge_field/bad/not_a_real_mergefield_old') }
|
22
|
+
let(:element) { fragment.xpath("//w:fldSimple").first }
|
23
|
+
|
24
|
+
it "returns nil" do
|
25
|
+
expect(described_class.from_element(element)).to be_nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "new style merge field" do
|
31
|
+
let(:fragment) { xml_fragment('input/merge_field/new_merge_field') }
|
32
|
+
let(:element) { fragment.xpath("//w:fldChar[contains(@w:fldCharType, 'begin')]").first }
|
33
|
+
|
34
|
+
describe '#raw_key' do
|
35
|
+
it 'returns the mergefield name from the element' do
|
36
|
+
expect(subject.raw_key).to eq 'ocean.fishy'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#interpolate' do
|
41
|
+
it 'interpolates values from dataset into mergefield' do
|
42
|
+
dataset = Sheng::DataSet.new({
|
43
|
+
:ocean => { :fishy => "scrumblefish" }
|
44
|
+
})
|
45
|
+
|
46
|
+
allow(subject).to receive(:filter_value).with("scrumblefish").and_return("l33tphish")
|
47
|
+
subject.interpolate(dataset)
|
48
|
+
expect(subject.xml_document).to be_equivalent_to xml_fragment('output/merge_field/merge_field')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#xml" do
|
53
|
+
it "returns full nodeset surrounding element" do
|
54
|
+
expect(subject.xml.count).to eq(5)
|
55
|
+
expect(subject.xml[0]).to eq(element.ancestors[0])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "with split mergefield instruction text runs" do
|
60
|
+
let(:fragment) { xml_fragment('input/merge_field/split_merge_field') }
|
61
|
+
|
62
|
+
describe '#interpolate' do
|
63
|
+
it 'works' do
|
64
|
+
dataset = Sheng::DataSet.new({
|
65
|
+
:persimmon_face => "Lavender"
|
66
|
+
})
|
67
|
+
|
68
|
+
subject.interpolate(dataset)
|
69
|
+
expect(subject.xml_document).to be_equivalent_to xml_fragment('output/merge_field/split_merge_field')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "with mergefields in line with other text" do
|
75
|
+
let(:fragment) { xml_fragment('input/merge_field/inline_merge_field') }
|
76
|
+
|
77
|
+
describe '#interpolate' do
|
78
|
+
it 'works' do
|
79
|
+
dataset = Sheng::DataSet.new({
|
80
|
+
:prefix => "snuffle"
|
81
|
+
})
|
82
|
+
|
83
|
+
subject.interpolate(dataset)
|
84
|
+
expect(subject.xml_document).to be_equivalent_to xml_fragment('output/merge_field/inline_merge_field')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "with badly formed mergefield tags" do
|
90
|
+
let(:fragment) { xml_fragment('input/merge_field/bad/unclosed_merge_field') }
|
91
|
+
|
92
|
+
describe ".new" do
|
93
|
+
it "raises an exception" do
|
94
|
+
expect {
|
95
|
+
subject
|
96
|
+
}.to raise_error(described_class::NotAMergeFieldError, "MERGEFIELD this_has_a_beginning_but_no_")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "when not an actual mergefield" do
|
102
|
+
let(:fragment) { xml_fragment('input/merge_field/bad/not_a_real_mergefield_new') }
|
103
|
+
|
104
|
+
describe ".new" do
|
105
|
+
it "raises an exception" do
|
106
|
+
expect {
|
107
|
+
subject
|
108
|
+
}.to raise_error(described_class::NotAMergeFieldError, "PAGE \\* MERGEFORMAT ")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe ".new" do
|
115
|
+
context "when not actually a mergefield" do
|
116
|
+
let(:fragment) { xml_fragment('input/merge_field/bad/not_a_real_mergefield_old') }
|
117
|
+
let(:element) { fragment.xpath("//w:fldSimple").first }
|
118
|
+
|
119
|
+
it "raises an exception" do
|
120
|
+
expect {
|
121
|
+
subject
|
122
|
+
}.to raise_error(described_class::NotAMergeFieldError, " PAGE \\* MERGEFORMAT ")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe '#interpolate' do
|
128
|
+
it 'interpolates filtered values from dataset into mergefield' do
|
129
|
+
dataset = Sheng::DataSet.new({
|
130
|
+
:ocean => { :fishy => "scrumblefish" }
|
131
|
+
})
|
132
|
+
|
133
|
+
allow(subject).to receive(:filter_value).with("scrumblefish").and_return("l33tphish")
|
134
|
+
subject.interpolate(dataset)
|
135
|
+
expect(subject.xml_document).to be_equivalent_to xml_fragment('output/merge_field/merge_field')
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe "#xml" do
|
140
|
+
it "returns element" do
|
141
|
+
expect(subject.xml).to eq(subject.element)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe '#raw_key' do
|
146
|
+
it 'returns the mergefield name from the element' do
|
147
|
+
expect(subject.raw_key).to eq 'ocean.fishy'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe '#key' do
|
152
|
+
it 'returns the raw key with start/end metadata stripped off' do
|
153
|
+
allow(subject).to receive(:raw_key).and_return('start:whipple.dooter')
|
154
|
+
expect(subject.key).to eq 'whipple.dooter'
|
155
|
+
allow(subject).to receive(:raw_key).and_return('end:smunch.dooter')
|
156
|
+
expect(subject.key).to eq 'smunch.dooter'
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'returns the raw key with filters stripped off' do
|
160
|
+
allow(subject).to receive(:raw_key).and_return("whumpies | cook | dress(frock)")
|
161
|
+
expect(subject.key).to eq 'whumpies'
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "#filters" do
|
166
|
+
it "returns filters extracted from raw_key" do
|
167
|
+
allow(subject).to receive(:raw_key).and_return("whumpies | cook | dress(frock)")
|
168
|
+
expect(subject.filters).to eq(["cook", "dress(frock)"])
|
169
|
+
end
|
170
|
+
|
171
|
+
it "doesn't care about whitespace" do
|
172
|
+
allow(subject).to receive(:raw_key).and_return("whumpies|cook|dress(frock)")
|
173
|
+
expect(subject.filters).to eq(["cook", "dress(frock)"])
|
174
|
+
end
|
175
|
+
|
176
|
+
it "returns empty array if no filters in raw key" do
|
177
|
+
allow(subject).to receive(:raw_key).and_return("whatever.this.is")
|
178
|
+
expect(subject.filters).to eq([])
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
describe "#is_start?" do
|
183
|
+
it "returns true if mergefield is start of block" do
|
184
|
+
allow(subject).to receive(:raw_key).and_return("start:whatever")
|
185
|
+
expect(subject.is_start?).to be_truthy
|
186
|
+
allow(subject).to receive(:raw_key).and_return("if:whatever")
|
187
|
+
expect(subject.is_start?).to be_truthy
|
188
|
+
allow(subject).to receive(:raw_key).and_return("unless:whatever")
|
189
|
+
expect(subject.is_start?).to be_truthy
|
190
|
+
end
|
191
|
+
|
192
|
+
it "returns false if mergefield is end of block" do
|
193
|
+
allow(subject).to receive(:raw_key).and_return("end:whatever")
|
194
|
+
expect(subject.is_start?).to be_falsy
|
195
|
+
end
|
196
|
+
|
197
|
+
it "returns false if mergefield is not a block bracket" do
|
198
|
+
allow(subject).to receive(:raw_key).and_return("whatever")
|
199
|
+
expect(subject.is_start?).to be_falsy
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "#is_end?" do
|
204
|
+
it "returns true if mergefield is end of block" do
|
205
|
+
allow(subject).to receive(:raw_key).and_return("end:whatever")
|
206
|
+
expect(subject.is_end?).to be_truthy
|
207
|
+
allow(subject).to receive(:raw_key).and_return("end_if:whatever")
|
208
|
+
expect(subject.is_end?).to be_truthy
|
209
|
+
allow(subject).to receive(:raw_key).and_return("end_unless:whatever")
|
210
|
+
expect(subject.is_end?).to be_truthy
|
211
|
+
end
|
212
|
+
|
213
|
+
it "returns false if mergefield is start of block" do
|
214
|
+
allow(subject).to receive(:raw_key).and_return("start:whatever")
|
215
|
+
expect(subject.is_end?).to be_falsy
|
216
|
+
end
|
217
|
+
|
218
|
+
it "returns false if mergefield is not a block bracket" do
|
219
|
+
allow(subject).to receive(:raw_key).and_return("whatever")
|
220
|
+
expect(subject.is_end?).to be_falsy
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
describe "#filter_value" do
|
225
|
+
it "can upcase" do
|
226
|
+
allow(subject).to receive(:filters).and_return(["upcase"])
|
227
|
+
expect(subject.filter_value("HorSes")).to eq("HORSES")
|
228
|
+
end
|
229
|
+
|
230
|
+
it "can downcase" do
|
231
|
+
allow(subject).to receive(:filters).and_return(["downcase"])
|
232
|
+
expect(subject.filter_value("HorSes")).to eq("horses")
|
233
|
+
end
|
234
|
+
|
235
|
+
it "can reverse" do
|
236
|
+
allow(subject).to receive(:filters).and_return(["reverse"])
|
237
|
+
expect(subject.filter_value("Maple")).to eq("elpaM")
|
238
|
+
end
|
239
|
+
|
240
|
+
it "can titleize" do
|
241
|
+
allow(subject).to receive(:filters).and_return(["titleize"])
|
242
|
+
expect(subject.filter_value("ribbons are grand")).to eq("Ribbons Are Grand")
|
243
|
+
end
|
244
|
+
|
245
|
+
it "can capitalize" do
|
246
|
+
allow(subject).to receive(:filters).and_return(["capitalize"])
|
247
|
+
expect(subject.filter_value("ribbons are grand")).to eq("Ribbons are grand")
|
248
|
+
end
|
249
|
+
|
250
|
+
it "works with multiple filters" do
|
251
|
+
allow(subject).to receive(:filters).and_return(["reverse", "capitalize"])
|
252
|
+
expect(subject.filter_value("maple")).to eq("Elpam")
|
253
|
+
end
|
254
|
+
|
255
|
+
it "does nothing if filter not recognized" do
|
256
|
+
allow(subject).to receive(:filters).and_return(["elephantize"])
|
257
|
+
expect(subject.filter_value("ribbons are grand")).to eq("ribbons are grand")
|
258
|
+
end
|
259
|
+
|
260
|
+
it "does nothing if value doesn't respond to filter" do
|
261
|
+
allow(subject).to receive(:filters).and_return(["upcase"])
|
262
|
+
expect(subject.filter_value(130)).to eq(130)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
describe "#iteration_variable" do
|
267
|
+
it "returns :item" do
|
268
|
+
expect(subject.iteration_variable).to eq(:item)
|
269
|
+
end
|
270
|
+
|
271
|
+
it "can be overridden with an 'as' filter" do
|
272
|
+
allow(subject).to receive(:raw_key).and_return("lunch.parts | as(tasties)")
|
273
|
+
expect(subject.iteration_variable).to eq(:tasties)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|