sheng 0.3.2

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