sheng 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|