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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.gitignore +18 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.watchr +99 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +62 -0
  10. data/Rakefile +10 -0
  11. data/docs/creating_templates.docx +0 -0
  12. data/docs/creating_templates.md +392 -0
  13. data/lib/sheng/block.rb +59 -0
  14. data/lib/sheng/check_box.rb +43 -0
  15. data/lib/sheng/conditional_block.rb +30 -0
  16. data/lib/sheng/data_set.rb +45 -0
  17. data/lib/sheng/docx.rb +75 -0
  18. data/lib/sheng/merge_field.rb +208 -0
  19. data/lib/sheng/merge_field_set.rb +90 -0
  20. data/lib/sheng/path_helpers.rb +15 -0
  21. data/lib/sheng/sequence.rb +66 -0
  22. data/lib/sheng/support.rb +57 -0
  23. data/lib/sheng/version.rb +3 -0
  24. data/lib/sheng/wml_file.rb +47 -0
  25. data/lib/sheng.rb +21 -0
  26. data/sheng.gemspec +31 -0
  27. data/spec/fixtures/bad_docx_files/with_field_not_in_dataset.docx +0 -0
  28. data/spec/fixtures/bad_docx_files/with_missing_sequence_start.docx +0 -0
  29. data/spec/fixtures/bad_docx_files/with_old_mergefields.docx +0 -0
  30. data/spec/fixtures/bad_docx_files/with_poorly_nested_sequences.docx +0 -0
  31. data/spec/fixtures/bad_docx_files/with_unended_sequence.docx +0 -0
  32. data/spec/fixtures/docx_files/input_document.docx +0 -0
  33. data/spec/fixtures/docx_files/old_style/input_document.docx +0 -0
  34. data/spec/fixtures/docx_files/old_style/output_document.docx +0 -0
  35. data/spec/fixtures/docx_files/output_document.docx +0 -0
  36. data/spec/fixtures/inputs/complete.json +61 -0
  37. data/spec/fixtures/inputs/incomplete.json +52 -0
  38. data/spec/fixtures/trees/embedded_sequence.yml +13 -0
  39. data/spec/fixtures/trees/merge_field_set.yml +16 -0
  40. data/spec/fixtures/xml_fragments/input/check_box/check_box.xml +11 -0
  41. data/spec/fixtures/xml_fragments/input/conditional_block/bad/badly_nested_conditional.xml +105 -0
  42. data/spec/fixtures/xml_fragments/input/conditional_block/bad/unclosed_conditional.xml +35 -0
  43. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_if.xml +84 -0
  44. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_inline.xml +59 -0
  45. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_unless.xml +51 -0
  46. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_in_table.xml +176 -0
  47. data/spec/fixtures/xml_fragments/input/conditional_block/embedded_conditional.xml +79 -0
  48. data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_new.xml +19 -0
  49. data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_old.xml +9 -0
  50. data/spec/fixtures/xml_fragments/input/merge_field/bad/unclosed_merge_field.xml +25 -0
  51. data/spec/fixtures/xml_fragments/input/merge_field/inline_merge_field.xml +26 -0
  52. data/spec/fixtures/xml_fragments/input/merge_field/merge_field.xml +14 -0
  53. data/spec/fixtures/xml_fragments/input/merge_field/new_merge_field.xml +28 -0
  54. data/spec/fixtures/xml_fragments/input/merge_field/split_merge_field.xml +33 -0
  55. data/spec/fixtures/xml_fragments/input/merge_field_set/complex_nesting_and_reuse.xml +231 -0
  56. data/spec/fixtures/xml_fragments/input/merge_field_set/merge_field_set.xml +89 -0
  57. data/spec/fixtures/xml_fragments/input/merge_field_set/with_non_mergefield_fields.xml +43 -0
  58. data/spec/fixtures/xml_fragments/input/sequence/array_sequence.xml +23 -0
  59. data/spec/fixtures/xml_fragments/input/sequence/bad/badly_nested_sequence.xml +62 -0
  60. data/spec/fixtures/xml_fragments/input/sequence/bad/unclosed_sequence.xml +32 -0
  61. data/spec/fixtures/xml_fragments/input/sequence/embedded_sequence.xml +68 -0
  62. data/spec/fixtures/xml_fragments/input/sequence/inline_sequence.xml +24 -0
  63. data/spec/fixtures/xml_fragments/input/sequence/overridden_iterator_array_sequence.xml +23 -0
  64. data/spec/fixtures/xml_fragments/input/sequence/sequence.xml +39 -0
  65. data/spec/fixtures/xml_fragments/input/sequence/sequence_in_table.xml +125 -0
  66. data/spec/fixtures/xml_fragments/input/sequence/sequence_with_section_formatting.xml +41 -0
  67. data/spec/fixtures/xml_fragments/input/sequence/series_with_commas.xml +73 -0
  68. data/spec/fixtures/xml_fragments/output/check_box/check_box.xml +11 -0
  69. data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_does_not_exist.xml +52 -0
  70. data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_exists.xml +84 -0
  71. data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_both.xml +11 -0
  72. data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_inside.xml +5 -0
  73. data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_outside.xml +8 -0
  74. data/spec/fixtures/xml_fragments/output/conditional_block/if_does_not_exist.xml +5 -0
  75. data/spec/fixtures/xml_fragments/output/conditional_block/if_exists.xml +19 -0
  76. data/spec/fixtures/xml_fragments/output/conditional_block/inline_does_not_exist.xml +10 -0
  77. data/spec/fixtures/xml_fragments/output/conditional_block/inline_exists.xml +13 -0
  78. data/spec/fixtures/xml_fragments/output/conditional_block/unless_does_not_exist.xml +11 -0
  79. data/spec/fixtures/xml_fragments/output/conditional_block/unless_exists.xml +5 -0
  80. data/spec/fixtures/xml_fragments/output/merge_field/inline_merge_field.xml +11 -0
  81. data/spec/fixtures/xml_fragments/output/merge_field/merge_field.xml +12 -0
  82. data/spec/fixtures/xml_fragments/output/merge_field/split_merge_field.xml +10 -0
  83. data/spec/fixtures/xml_fragments/output/merge_field_set/complex_nesting_and_reuse.xml +190 -0
  84. data/spec/fixtures/xml_fragments/output/merge_field_set/merge_field_set.xml +75 -0
  85. data/spec/fixtures/xml_fragments/output/merge_field_set/with_non_mergefield_fields.xml +31 -0
  86. data/spec/fixtures/xml_fragments/output/sequence/array_sequence.xml +17 -0
  87. data/spec/fixtures/xml_fragments/output/sequence/embedded_sequence.xml +56 -0
  88. data/spec/fixtures/xml_fragments/output/sequence/inline_sequence.xml +16 -0
  89. data/spec/fixtures/xml_fragments/output/sequence/overridden_iterator_array_sequence.xml +12 -0
  90. data/spec/fixtures/xml_fragments/output/sequence/sequence.xml +28 -0
  91. data/spec/fixtures/xml_fragments/output/sequence/sequence_in_table.xml +120 -0
  92. data/spec/fixtures/xml_fragments/output/sequence/sequence_with_section_formatting.xml +46 -0
  93. data/spec/fixtures/xml_fragments/output/sequence/series_with_commas.xml +43 -0
  94. data/spec/fixtures/xml_fragments/output/sequence/series_with_commas_two_items.xml +31 -0
  95. data/spec/lib/sheng/check_box_spec.rb +87 -0
  96. data/spec/lib/sheng/conditional_block_spec.rb +151 -0
  97. data/spec/lib/sheng/data_set_spec.rb +65 -0
  98. data/spec/lib/sheng/docx_spec.rb +146 -0
  99. data/spec/lib/sheng/merge_field_set_spec.rb +165 -0
  100. data/spec/lib/sheng/merge_field_spec.rb +276 -0
  101. data/spec/lib/sheng/sequence_spec.rb +201 -0
  102. data/spec/lib/sheng/wml_file_spec.rb +38 -0
  103. data/spec/spec_helper.rb +16 -0
  104. data/spec/support/path_helper.rb +15 -0
  105. data/spec/support/xml_helper.rb +28 -0
  106. 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