sablon 0.0.1

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.
@@ -0,0 +1,34 @@
1
+ module Sablon
2
+ class Processor
3
+ class SectionProperties
4
+ def self.from_document(document_xml)
5
+ new document_xml.at_xpath(".//w:sectPr")
6
+ end
7
+
8
+ def initialize(properties_node)
9
+ @properties_node = properties_node
10
+ end
11
+
12
+ def start_page_number
13
+ pg_num_type && pg_num_type["w:start"]
14
+ end
15
+
16
+ def start_page_number=(number)
17
+ find_or_add_pg_num_type["w:start"] = number
18
+ end
19
+
20
+ private
21
+ def find_or_add_pg_num_type
22
+ pg_num_type || begin
23
+ node = Nokogiri::XML::Node.new "w:pgNumType", @properties_node.document
24
+ @properties_node.children.after node
25
+ node
26
+ end
27
+ end
28
+
29
+ def pg_num_type
30
+ @pg_num_type ||= @properties_node.at_xpath(".//w:pgNumType")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ module Sablon
2
+ class Template
3
+ def initialize(path)
4
+ @path = path
5
+ end
6
+
7
+ # Same as +render_to_string+ but writes the processed template to +output_path+.
8
+ def render_to_file(output_path, context, properties = {})
9
+ File.open(output_path, 'w') do |f|
10
+ f.write render_to_string(context, properties)
11
+ end
12
+ end
13
+
14
+ # Process the template. The +context+ hash will be available in the template.
15
+ def render_to_string(context, properties = {})
16
+ render(context, properties).string
17
+ end
18
+
19
+ private
20
+ def render(context, properties = {})
21
+ Zip::OutputStream.write_buffer(StringIO.new) do |out|
22
+ Zip::File.open(@path).each do |entry|
23
+ entry_name = entry.name
24
+ out.put_next_entry(entry_name)
25
+ content = entry.get_input_stream.read
26
+ if entry_name == 'word/document.xml'
27
+ out.write(Processor.process(Nokogiri::XML(content), context, properties).to_xml)
28
+ else
29
+ out.write(content)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Sablon
2
+ VERSION = "0.0.1"
3
+ end
data/sablon.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sablon/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "sablon"
8
+ spec.version = Sablon::VERSION
9
+ spec.authors = ["Yves Senn"]
10
+ spec.email = ["yves.senn@gmail.com"]
11
+ spec.summary = %q{docx tempalte processor}
12
+ spec.description = %q{Sablon is a document template processor. At this time it works only with docx and MailMerge fields.}
13
+ spec.homepage = "http://github.com/senny/sablon"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'nokogiri'
22
+ spec.add_runtime_dependency 'rubyzip'
23
+
24
+ spec.add_development_dependency "bundler", ">= 1.6"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "minitest", "~> 5.4"
27
+ spec.add_development_dependency "xml-simple"
28
+ end
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "test_helper"
3
+
4
+ class ExpressionTest < Sablon::TestCase
5
+ def test_variable_expression
6
+ expr = Sablon::Expression.parse("first_name")
7
+ assert_equal "Jane", expr.evaluate({"first_name" => "Jane", "last_name" => "Doe"})
8
+ end
9
+
10
+ def test_simple_method_call
11
+ user = OpenStruct.new(first_name: "Jack")
12
+ expr = Sablon::Expression.parse("user.first_name")
13
+ assert_equal "Jack", expr.evaluate({"user" => user})
14
+ end
15
+ end
Binary file
Binary file
@@ -0,0 +1,131 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "test_helper"
3
+ require "support/document_xml_helper"
4
+
5
+ module MailMergeParser
6
+ module SharedBehavior
7
+ include DocumentXMLHelper
8
+ def setup
9
+ super
10
+ @parser = Sablon::Parser::MailMerge.new
11
+ end
12
+
13
+ def fields
14
+ @document = xml
15
+ @parser.parse_fields(@document)
16
+ end
17
+
18
+ def body_xml
19
+ @document.search(".//w:body").children.map(&:to_xml).join.strip
20
+ end
21
+ end
22
+
23
+ class FldSimpleTest < Sablon::TestCase
24
+ include SharedBehavior
25
+
26
+ def test_recognizes_expression
27
+ assert_equal ["=first_name"], fields.map(&:expression)
28
+ end
29
+
30
+ def test_replace
31
+ field = fields.first
32
+ field.replace("Hello")
33
+ assert_equal <<-body_xml.strip, body_xml
34
+ <w:r w:rsidR=\"004B49F0\">
35
+ <w:rPr><w:noProof/></w:rPr>
36
+ <w:t>Hello</w:t>
37
+ </w:r>
38
+ body_xml
39
+ end
40
+
41
+ def test_replace_with_newlines
42
+ field = fields.first
43
+ field.replace("First\nSecond\n\nThird")
44
+
45
+ assert_equal <<-body_xml.strip, body_xml
46
+ <w:r w:rsidR=\"004B49F0\">
47
+ <w:rPr><w:noProof/></w:rPr>
48
+ <w:t>First</w:t><w:br/><w:t>Second</w:t><w:br/><w:br/><w:t>Third</w:t>
49
+ </w:r>
50
+ body_xml
51
+ end
52
+
53
+ private
54
+ def xml
55
+ wrap(<<-xml)
56
+ <w:fldSimple w:instr=" MERGEFIELD =first_name \\* MERGEFORMAT ">
57
+ <w:r w:rsidR="004B49F0">
58
+ <w:rPr><w:noProof/></w:rPr>
59
+ <w:t>«=first_name»</w:t>
60
+ </w:r>
61
+ </w:fldSimple>
62
+ xml
63
+ end
64
+ end
65
+
66
+ class FldCharTest < Sablon::TestCase
67
+ include SharedBehavior
68
+
69
+ def test_recognizes_expression
70
+ assert_equal ["=last_name"], fields.map(&:expression)
71
+ end
72
+
73
+ def test_replace
74
+ field = fields.first
75
+ field.replace("Hello")
76
+ assert_equal <<-body_xml.strip, body_xml
77
+ <w:r w:rsidR="004B49F0">
78
+ <w:rPr>
79
+ <w:b/>
80
+ <w:noProof/>
81
+ </w:rPr>
82
+ <w:t>Hello</w:t>
83
+ </w:r>
84
+ body_xml
85
+ end
86
+
87
+ def test_replace_with_newlines
88
+ field = fields.first
89
+ field.replace("First\nSecond\n\nThird")
90
+
91
+ assert_equal <<-body_xml.strip, body_xml
92
+ <w:r w:rsidR="004B49F0">
93
+ <w:rPr>
94
+ <w:b/>
95
+ <w:noProof/>
96
+ </w:rPr>
97
+ <w:t>First</w:t><w:br/><w:t>Second</w:t><w:br/><w:br/><w:t>Third</w:t>
98
+ </w:r>
99
+ body_xml
100
+ end
101
+
102
+ private
103
+ def xml
104
+ wrap(<<-xml)
105
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
106
+ <w:rPr><w:b/></w:rPr>
107
+ <w:fldChar w:fldCharType="begin"/>
108
+ </w:r>
109
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
110
+ <w:rPr><w:b/></w:rPr>
111
+ <w:instrText xml:space="preserve"> MERGEFIELD =last_name \\* MERGEFORMAT </w:instrText>
112
+ </w:r>
113
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
114
+ <w:rPr><w:b/></w:rPr>
115
+ <w:fldChar w:fldCharType="separate"/>
116
+ </w:r>
117
+ <w:r w:rsidR="004B49F0">
118
+ <w:rPr>
119
+ <w:b/>
120
+ <w:noProof/>
121
+ </w:rPr>
122
+ <w:t>«=last_name»</w:t>
123
+ </w:r>
124
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
125
+ <w:rPr><w:b/></w:rPr>
126
+ <w:fldChar w:fldCharType="end"/>
127
+ </w:r>
128
+ xml
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,697 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "test_helper"
3
+ require "support/document_xml_helper"
4
+
5
+ class ProcessorTest < Sablon::TestCase
6
+ include DocumentXMLHelper
7
+
8
+ def setup
9
+ super
10
+ @processor = Sablon::Processor
11
+ end
12
+
13
+ def test_simple_field_replacement
14
+ result = process(<<-documentxml, {"first_name" => "Jack"})
15
+ <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
16
+ <w:fldSimple w:instr=" MERGEFIELD =first_name \\* MERGEFORMAT ">
17
+ <w:r w:rsidR="004B49F0">
18
+ <w:rPr><w:noProof/></w:rPr>
19
+ <w:t>«=first_name»</w:t>
20
+ </w:r>
21
+ </w:fldSimple>
22
+ <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
23
+ documentxml
24
+
25
+
26
+ assert_equal "Hello! My Name is Jack , nice to meet you.", text(result)
27
+ assert_xml_equal <<-document, result
28
+ <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
29
+ <w:r w:rsidR="004B49F0">
30
+ <w:rPr><w:noProof/></w:rPr>
31
+ <w:t>Jack</w:t>
32
+ </w:r>
33
+ <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
34
+ document
35
+ end
36
+
37
+ def test_complex_field_replacement
38
+ result = process(<<-documentxml, {"last_name" => "Zane"})
39
+ <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
40
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
41
+ <w:rPr>
42
+ <w:b/>
43
+ </w:rPr>
44
+ <w:fldChar w:fldCharType="begin"/>
45
+ </w:r>
46
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
47
+ <w:rPr>
48
+ <w:b/>
49
+ </w:rPr>
50
+ <w:instrText xml:space="preserve"> MERGEFIELD =last_name \\* MERGEFORMAT </w:instrText>
51
+ </w:r>
52
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
53
+ <w:rPr>
54
+ <w:b/>
55
+ </w:rPr>
56
+ <w:fldChar w:fldCharType="separate"/>
57
+ </w:r>
58
+ <w:r w:rsidR="004B49F0">
59
+ <w:rPr>
60
+ <w:b/>
61
+ <w:noProof/>
62
+ </w:rPr>
63
+ <w:t>«=last_name»</w:t>
64
+ </w:r>
65
+ <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
66
+ <w:rPr>
67
+ <w:b/>
68
+ </w:rPr>
69
+ <w:fldChar w:fldCharType="end"/>
70
+ </w:r>
71
+ <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
72
+ documentxml
73
+
74
+ assert_equal "Hello! My Name is Zane , nice to meet you.", text(result)
75
+ assert_xml_equal <<-document, result
76
+ <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
77
+ <w:r w:rsidR="004B49F0">
78
+ <w:rPr>
79
+ <w:b/>
80
+ <w:noProof/>
81
+ </w:rPr>
82
+ <w:t>Zane</w:t>
83
+ </w:r>
84
+ <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
85
+ document
86
+ end
87
+
88
+ def test_complex_field_replacement_with_split_field
89
+ result = process(<<-documentxml, {"first_name" => "Daniel"})
90
+ <w:r>
91
+ <w:t xml:space="preserve">Hello! My Name is </w:t>
92
+ </w:r>
93
+ <w:r w:rsidR="003C4780">
94
+ <w:fldChar w:fldCharType="begin" />
95
+ </w:r>
96
+ <w:r w:rsidR="003C4780">
97
+ <w:instrText xml:space="preserve"> MERGEFIELD </w:instrText>
98
+ </w:r>
99
+ <w:r w:rsidR="003A4504">
100
+ <w:instrText>=</w:instrText>
101
+ </w:r>
102
+ <w:r w:rsidR="003C4780">
103
+ <w:instrText xml:space="preserve">first_name \\* MERGEFORMAT </w:instrText>
104
+ </w:r>
105
+ <w:r w:rsidR="003C4780">
106
+ <w:fldChar w:fldCharType="separate" />
107
+ </w:r>
108
+ <w:r w:rsidR="00441382">
109
+ <w:rPr>
110
+ <w:noProof />
111
+ </w:rPr>
112
+ <w:t>«=person.first_name»</w:t>
113
+ </w:r>
114
+ <w:r w:rsidR="003C4780">
115
+ <w:fldChar w:fldCharType="end" />
116
+ </w:r>
117
+ <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
118
+ documentxml
119
+
120
+ assert_equal "Hello! My Name is Daniel , nice to meet you.", text(result)
121
+ assert_xml_equal <<-document, result
122
+ <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
123
+ <w:r w:rsidR="00441382">
124
+ <w:rPr>
125
+ <w:noProof/>
126
+ </w:rPr>
127
+ <w:t>Daniel</w:t>
128
+ </w:r>
129
+ <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
130
+ document
131
+ end
132
+
133
+ def test_paragraph_block_replacement
134
+ item = Struct.new(:index, :label, :rating)
135
+ result = process(<<-document, {"technologies" => ["Ruby", "Rails"]})
136
+ <w:p w14:paraId="6CB29D92" w14:textId="164B70F4" w:rsidR="007F5CDE" w:rsidRDefault="007F5CDE" w:rsidP="007F5CDE">
137
+ <w:pPr>
138
+ <w:pStyle w:val="ListParagraph" />
139
+ <w:numPr>
140
+ <w:ilvl w:val="0" />
141
+ <w:numId w:val="1" />
142
+ </w:numPr>
143
+ </w:pPr>
144
+ <w:fldSimple w:instr=" MERGEFIELD technologies:each(technology) \\* MERGEFORMAT ">
145
+ <w:r>
146
+ <w:rPr>
147
+ <w:noProof />
148
+ </w:rPr>
149
+ <w:t>«technologies:each(technology)»</w:t>
150
+ </w:r>
151
+ </w:fldSimple>
152
+ </w:p>
153
+ <w:p w14:paraId="1081E316" w14:textId="3EAB5FDC" w:rsidR="00380EE8" w:rsidRDefault="00380EE8" w:rsidP="007F5CDE">
154
+ <w:pPr>
155
+ <w:pStyle w:val="ListParagraph" />
156
+ <w:numPr>
157
+ <w:ilvl w:val="0" />
158
+ <w:numId w:val="1" />
159
+ </w:numPr>
160
+ </w:pPr>
161
+ <w:r>
162
+ <w:fldChar w:fldCharType="begin" />
163
+ </w:r>
164
+ <w:r>
165
+ <w:instrText xml:space="preserve"> </w:instrText>
166
+ </w:r>
167
+ <w:r w:rsidR="009F01DA">
168
+ <w:instrText>MERGEFIELD =technology</w:instrText>
169
+ </w:r>
170
+ <w:r>
171
+ <w:instrText xml:space="preserve"> \\* MERGEFORMAT </w:instrText>
172
+ </w:r>
173
+ <w:r>
174
+ <w:fldChar w:fldCharType="separate" />
175
+ </w:r>
176
+ <w:r w:rsidR="009F01DA">
177
+ <w:rPr>
178
+ <w:noProof />
179
+ </w:rPr>
180
+ <w:t>«=technology»</w:t>
181
+ </w:r>
182
+ <w:r>
183
+ <w:fldChar w:fldCharType="end" />
184
+ </w:r>
185
+ </w:p>
186
+ <w:p w14:paraId="7F936853" w14:textId="078377AD" w:rsidR="00380EE8" w:rsidRPr="007F5CDE" w:rsidRDefault="00380EE8" w:rsidP="007F5CDE">
187
+ <w:pPr>
188
+ <w:pStyle w:val="ListParagraph" />
189
+ <w:numPr>
190
+ <w:ilvl w:val="0" />
191
+ <w:numId w:val="1" />
192
+ </w:numPr>
193
+ </w:pPr>
194
+ <w:fldSimple w:instr=" MERGEFIELD technologies:endEach \\* MERGEFORMAT ">
195
+ <w:r>
196
+ <w:rPr>
197
+ <w:noProof />
198
+ </w:rPr>
199
+ <w:t>«technologies:endEach»</w:t>
200
+ </w:r>
201
+ </w:fldSimple>
202
+ </w:p>
203
+ document
204
+
205
+ assert_equal "Ruby Rails", text(result)
206
+ assert_xml_equal <<-document, result
207
+ <w:p w14:paraId="1081E316" w14:textId="3EAB5FDC" w:rsidR="00380EE8" w:rsidRDefault="00380EE8" w:rsidP="007F5CDE">
208
+ <w:pPr>
209
+ <w:pStyle w:val="ListParagraph"/>
210
+ <w:numPr>
211
+ <w:ilvl w:val="0"/>
212
+ <w:numId w:val="1"/>
213
+ </w:numPr>
214
+ </w:pPr>
215
+ <w:r w:rsidR="009F01DA">
216
+ <w:rPr>
217
+ <w:noProof/>
218
+ </w:rPr>
219
+ <w:t>Ruby</w:t>
220
+ </w:r>
221
+ </w:p><w:p w14:paraId="1081E316" w14:textId="3EAB5FDC" w:rsidR="00380EE8" w:rsidRDefault="00380EE8" w:rsidP="007F5CDE">
222
+ <w:pPr>
223
+ <w:pStyle w:val="ListParagraph"/>
224
+ <w:numPr>
225
+ <w:ilvl w:val="0"/>
226
+ <w:numId w:val="1"/>
227
+ </w:numPr>
228
+ </w:pPr>
229
+ <w:r w:rsidR="009F01DA">
230
+ <w:rPr>
231
+ <w:noProof/>
232
+ </w:rPr>
233
+ <w:t>Rails</w:t>
234
+ </w:r>
235
+ </w:p>
236
+ document
237
+ end
238
+
239
+ def test_single_row_table_loop
240
+ item = Struct.new(:index, :label, :rating)
241
+ result = process(<<-document, {"items" => [item.new("1.", "Milk", "***"), item.new("2.", "Sugar", "**")]})
242
+ <w:tbl>
243
+ <w:tblPr>
244
+ <w:tblStyle w:val="TableGrid"/>
245
+ <w:tblW w:w="0" w:type="auto"/>
246
+ <w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>
247
+ </w:tblPr>
248
+ <w:tblGrid>
249
+ <w:gridCol w:w="2202"/>
250
+ <w:gridCol w:w="4285"/>
251
+ <w:gridCol w:w="2029"/>
252
+ </w:tblGrid>
253
+ <w:tr w:rsidR="00757DAD" w14:paraId="229B7A39" w14:textId="77777777" w:rsidTr="006333C3">
254
+ <w:tc>
255
+ <w:tcPr>
256
+ <w:tcW w:w="2202" w:type="dxa"/>
257
+ </w:tcPr>
258
+ <w:p w14:paraId="3D472BF1" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
259
+ <w:r>
260
+ <w:fldChar w:fldCharType="begin"/>
261
+ </w:r>
262
+ <w:r>
263
+ <w:instrText xml:space="preserve"> MERGEFIELD items:each(item) \\* MERGEFORMAT </w:instrText>
264
+ </w:r>
265
+ <w:r>
266
+ <w:fldChar w:fldCharType="separate"/>
267
+ </w:r>
268
+ <w:r>
269
+ <w:rPr>
270
+ <w:noProof/>
271
+ </w:rPr>
272
+ <w:t>«items:each(item)»</w:t>
273
+ </w:r>
274
+ <w:r>
275
+ <w:fldChar w:fldCharType="end"/>
276
+ </w:r>
277
+ </w:p>
278
+ </w:tc>
279
+ <w:tc>
280
+ <w:tcPr>
281
+ <w:tcW w:w="4285" w:type="dxa"/>
282
+ </w:tcPr>
283
+ <w:p w14:paraId="6E6D8DB2" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3"/>
284
+ </w:tc>
285
+ <w:tc>
286
+ <w:tcPr>
287
+ <w:tcW w:w="2029" w:type="dxa"/>
288
+ </w:tcPr>
289
+ <w:p w14:paraId="7BE1DB00" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3"/>
290
+ </w:tc>
291
+ </w:tr>
292
+ <w:tr w:rsidR="00757DAD" w14:paraId="1BD2E50A" w14:textId="77777777" w:rsidTr="006333C3">
293
+ <w:tc>
294
+ <w:tcPr>
295
+ <w:tcW w:w="2202" w:type="dxa"/>
296
+ </w:tcPr>
297
+ <w:p w14:paraId="41ACB3D9" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
298
+ <w:r>
299
+ <w:fldChar w:fldCharType="begin"/>
300
+ </w:r>
301
+ <w:r>
302
+ <w:instrText xml:space="preserve"> MERGEFIELD =item.index \\* MERGEFORMAT </w:instrText>
303
+ </w:r>
304
+ <w:r>
305
+ <w:fldChar w:fldCharType="separate"/>
306
+ </w:r>
307
+ <w:r>
308
+ <w:rPr>
309
+ <w:noProof/>
310
+ </w:rPr>
311
+ <w:t>«=item.index»</w:t>
312
+ </w:r>
313
+ <w:r>
314
+ <w:fldChar w:fldCharType="end"/>
315
+ </w:r>
316
+ </w:p>
317
+ </w:tc>
318
+ <w:tc>
319
+ <w:tcPr>
320
+ <w:tcW w:w="4285" w:type="dxa"/>
321
+ </w:tcPr>
322
+ <w:p w14:paraId="197C6F31" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
323
+ <w:r>
324
+ <w:fldChar w:fldCharType="begin"/>
325
+ </w:r>
326
+ <w:r>
327
+ <w:instrText xml:space="preserve"> MERGEFIELD =item.label \\* MERGEFORMAT </w:instrText>
328
+ </w:r>
329
+ <w:r>
330
+ <w:fldChar w:fldCharType="separate"/>
331
+ </w:r>
332
+ <w:r>
333
+ <w:rPr>
334
+ <w:noProof/>
335
+ </w:rPr>
336
+ <w:t>«=item.label»</w:t>
337
+ </w:r>
338
+ <w:r>
339
+ <w:fldChar w:fldCharType="end"/>
340
+ </w:r>
341
+ </w:p>
342
+ </w:tc>
343
+ <w:tc>
344
+ <w:tcPr>
345
+ <w:tcW w:w="2029" w:type="dxa"/>
346
+ </w:tcPr>
347
+ <w:p w14:paraId="55C258BB" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
348
+ <w:r>
349
+ <w:fldChar w:fldCharType="begin"/>
350
+ </w:r>
351
+ <w:r>
352
+ <w:instrText xml:space="preserve"> MERGEFIELD =item.rating \\* MERGEFORMAT </w:instrText>
353
+ </w:r>
354
+ <w:r>
355
+ <w:fldChar w:fldCharType="separate"/>
356
+ </w:r>
357
+ <w:r>
358
+ <w:rPr>
359
+ <w:noProof/>
360
+ </w:rPr>
361
+ <w:t>«=item.rating»</w:t>
362
+ </w:r>
363
+ <w:r>
364
+ <w:fldChar w:fldCharType="end"/>
365
+ </w:r>
366
+ </w:p>
367
+ </w:tc>
368
+ </w:tr>
369
+ <w:tr w:rsidR="00757DAD" w14:paraId="2D3C09BC" w14:textId="77777777" w:rsidTr="006333C3">
370
+ <w:tc>
371
+ <w:tcPr>
372
+ <w:tcW w:w="2202" w:type="dxa"/>
373
+ </w:tcPr>
374
+ <w:p w14:paraId="04A961B7" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
375
+ <w:r>
376
+ <w:fldChar w:fldCharType="begin"/>
377
+ </w:r>
378
+ <w:r>
379
+ <w:instrText xml:space="preserve"> MERGEFIELD items:endEach \\* MERGEFORMAT </w:instrText>
380
+ </w:r>
381
+ <w:r>
382
+ <w:fldChar w:fldCharType="separate"/>
383
+ </w:r>
384
+ <w:r>
385
+ <w:rPr>
386
+ <w:noProof/>
387
+ </w:rPr>
388
+ <w:t>«items:endEach»</w:t>
389
+ </w:r>
390
+ <w:r>
391
+ <w:fldChar w:fldCharType="end"/>
392
+ </w:r>
393
+ </w:p>
394
+ </w:tc>
395
+ <w:tc>
396
+ <w:tcPr>
397
+ <w:tcW w:w="4285" w:type="dxa"/>
398
+ </w:tcPr>
399
+ <w:p w14:paraId="71165BFB" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3"/>
400
+ </w:tc>
401
+ <w:tc>
402
+ <w:tcPr>
403
+ <w:tcW w:w="2029" w:type="dxa"/>
404
+ </w:tcPr>
405
+ <w:p w14:paraId="01D3965C" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3"/>
406
+ </w:tc>
407
+ </w:tr>
408
+ </w:tbl>
409
+ document
410
+
411
+ assert_xml_equal <<-document, result
412
+ <w:tbl>
413
+ <w:tblPr>
414
+ <w:tblStyle w:val="TableGrid"/>
415
+ <w:tblW w:w="0" w:type="auto"/>
416
+ <w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>
417
+ </w:tblPr>
418
+ <w:tblGrid>
419
+ <w:gridCol w:w="2202"/>
420
+ <w:gridCol w:w="4285"/>
421
+ <w:gridCol w:w="2029"/>
422
+ </w:tblGrid>
423
+ <w:tr w:rsidR="00757DAD" w14:paraId="1BD2E50A" w14:textId="77777777" w:rsidTr="006333C3">
424
+ <w:tc>
425
+ <w:tcPr>
426
+ <w:tcW w:w="2202" w:type="dxa"/>
427
+ </w:tcPr>
428
+ <w:p w14:paraId="41ACB3D9" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
429
+ <w:r>
430
+ <w:rPr>
431
+ <w:noProof/>
432
+ </w:rPr>
433
+ <w:t>1.</w:t>
434
+ </w:r>
435
+ </w:p>
436
+ </w:tc>
437
+ <w:tc>
438
+ <w:tcPr>
439
+ <w:tcW w:w="4285" w:type="dxa"/>
440
+ </w:tcPr>
441
+ <w:p w14:paraId="197C6F31" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
442
+ <w:r>
443
+ <w:rPr>
444
+ <w:noProof/>
445
+ </w:rPr>
446
+ <w:t>Milk</w:t>
447
+ </w:r>
448
+ </w:p>
449
+ </w:tc>
450
+ <w:tc>
451
+ <w:tcPr>
452
+ <w:tcW w:w="2029" w:type="dxa"/>
453
+ </w:tcPr>
454
+ <w:p w14:paraId="55C258BB" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
455
+ <w:r>
456
+ <w:rPr>
457
+ <w:noProof/>
458
+ </w:rPr>
459
+ <w:t>***</w:t>
460
+ </w:r>
461
+ </w:p>
462
+ </w:tc>
463
+ </w:tr><w:tr w:rsidR="00757DAD" w14:paraId="1BD2E50A" w14:textId="77777777" w:rsidTr="006333C3">
464
+ <w:tc>
465
+ <w:tcPr>
466
+ <w:tcW w:w="2202" w:type="dxa"/>
467
+ </w:tcPr>
468
+ <w:p w14:paraId="41ACB3D9" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
469
+ <w:r>
470
+ <w:rPr>
471
+ <w:noProof/>
472
+ </w:rPr>
473
+ <w:t>2.</w:t>
474
+ </w:r>
475
+ </w:p>
476
+ </w:tc>
477
+ <w:tc>
478
+ <w:tcPr>
479
+ <w:tcW w:w="4285" w:type="dxa"/>
480
+ </w:tcPr>
481
+ <w:p w14:paraId="197C6F31" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
482
+ <w:r>
483
+ <w:rPr>
484
+ <w:noProof/>
485
+ </w:rPr>
486
+ <w:t>Sugar</w:t>
487
+ </w:r>
488
+ </w:p>
489
+ </w:tc>
490
+ <w:tc>
491
+ <w:tcPr>
492
+ <w:tcW w:w="2029" w:type="dxa"/>
493
+ </w:tcPr>
494
+ <w:p w14:paraId="55C258BB" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
495
+ <w:r>
496
+ <w:rPr>
497
+ <w:noProof/>
498
+ </w:rPr>
499
+ <w:t>**</w:t>
500
+ </w:r>
501
+ </w:p>
502
+ </w:tc>
503
+ </w:tr>
504
+ </w:tbl>
505
+
506
+ document
507
+ end
508
+
509
+ def test_multi_row_table_loop
510
+ item = Struct.new(:index, :label, :body)
511
+ context = {"foods" => [item.new("1.", "Milk", "Milk is a white liquid."),
512
+ item.new("2.", "Sugar", "Sugar is the generalized name for carbohydrates.")]}
513
+ result = process(<<-document, context)
514
+ <w:tbl>
515
+ <w:tr w:rsidR="00F23752" w14:paraId="3FF89DEC" w14:textId="77777777" w:rsidTr="00213ACD">
516
+ <w:tc>
517
+ <w:tcPr>
518
+ <w:tcW w:w="2235" w:type="dxa" />
519
+ <w:shd w:val="clear" w:color="auto" w:fill="auto" />
520
+ </w:tcPr>
521
+ <w:p w14:paraId="7630A6C6" w14:textId="699D0C71" w:rsidR="00F23752" w:rsidRDefault="00F23752" w:rsidP="003F16E3">
522
+ <w:fldSimple w:instr=" MERGEFIELD foods:each(food) \\* MERGEFORMAT ">
523
+ <w:r w:rsidR="00213ACD">
524
+ <w:rPr>
525
+ <w:noProof />
526
+ </w:rPr>
527
+ <w:t>«foods:each(food)»</w:t>
528
+ </w:r>
529
+ </w:fldSimple>
530
+ </w:p>
531
+ </w:tc>
532
+ <w:tc>
533
+ <w:tcPr>
534
+ <w:tcW w:w="6287" w:type="dxa" />
535
+ <w:shd w:val="clear" w:color="auto" w:fill="auto" />
536
+ </w:tcPr>
537
+ <w:p w14:paraId="437AFC74" w14:textId="77777777" w:rsidR="00F23752" w:rsidRDefault="00F23752" w:rsidP="003F16E3" />
538
+ </w:tc>
539
+ </w:tr>
540
+ <w:tr w:rsidR="00F23752" w14:paraId="320AE02B" w14:textId="77777777" w:rsidTr="00213ACD">
541
+ <w:tc>
542
+ <w:tcPr>
543
+ <w:tcW w:w="2235" w:type="dxa" />
544
+ <w:shd w:val="clear" w:color="auto" w:fill="8DB3E2" w:themeFill="text2" w:themeFillTint="66" />
545
+ </w:tcPr>
546
+ <w:p w14:paraId="3FCF3855" w14:textId="38FA7F3B" w:rsidR="00F23752" w:rsidRDefault="00F23752" w:rsidP="00F23752">
547
+ <w:fldSimple w:instr=" MERGEFIELD =food.index \\* MERGEFORMAT ">
548
+ <w:r w:rsidR="00213ACD">
549
+ <w:rPr>
550
+ <w:noProof />
551
+ </w:rPr>
552
+ <w:t>«=food.index»</w:t>
553
+ </w:r>
554
+ </w:fldSimple>
555
+ </w:p>
556
+ </w:tc>
557
+ <w:tc>
558
+ <w:tcPr>
559
+ <w:tcW w:w="6287" w:type="dxa" />
560
+ <w:shd w:val="clear" w:color="auto" w:fill="8DB3E2" w:themeFill="text2" w:themeFillTint="66" />
561
+ </w:tcPr>
562
+ <w:p w14:paraId="0BB0E74E" w14:textId="4FA0D282" w:rsidR="00F23752" w:rsidRPr="00F576DA" w:rsidRDefault="00F23752" w:rsidP="00F23752">
563
+ <w:r w:rsidRPr="00F576DA">
564
+ <w:fldChar w:fldCharType="begin" />
565
+ </w:r>
566
+ <w:r w:rsidRPr="00F576DA">
567
+ <w:instrText xml:space="preserve"> MERGEFIELD =</w:instrText>
568
+ </w:r>
569
+ <w:r>
570
+ <w:instrText>food</w:instrText>
571
+ </w:r>
572
+ <w:r w:rsidRPr="00F576DA">
573
+ <w:instrText xml:space="preserve">.label \\* MERGEFORMAT </w:instrText>
574
+ </w:r>
575
+ <w:r w:rsidRPr="00F576DA">
576
+ <w:fldChar w:fldCharType="separate" />
577
+ </w:r>
578
+ <w:r w:rsidR="00213ACD">
579
+ <w:rPr>
580
+ <w:rFonts w:ascii="Comic Sans MS" w:hAnsi="Comic Sans MS" />
581
+ <w:noProof />
582
+ </w:rPr>
583
+ <w:t>«=food.label»</w:t>
584
+ </w:r>
585
+ <w:r w:rsidRPr="00F576DA">
586
+ <w:fldChar w:fldCharType="end" />
587
+ </w:r>
588
+ </w:p>
589
+ </w:tc>
590
+ </w:tr>
591
+ <w:tr w:rsidR="00213ACD" w14:paraId="1EA188ED" w14:textId="77777777" w:rsidTr="00213ACD">
592
+ <w:tc>
593
+ <w:tcPr>
594
+ <w:tcW w:w="8522" w:type="dxa" />
595
+ <w:gridSpan w:val="2" />
596
+ <w:shd w:val="clear" w:color="auto" w:fill="auto" />
597
+ </w:tcPr>
598
+ <w:p w14:paraId="3E9FF163" w14:textId="0F37CDFB" w:rsidR="00213ACD" w:rsidRDefault="00213ACD" w:rsidP="003F16E3">
599
+ <w:fldSimple w:instr=" MERGEFIELD =food.body \\* MERGEFORMAT ">
600
+ <w:r>
601
+ <w:rPr>
602
+ <w:noProof />
603
+ </w:rPr>
604
+ <w:t>«=food.body»</w:t>
605
+ </w:r>
606
+ </w:fldSimple>
607
+ </w:p>
608
+ </w:tc>
609
+ </w:tr>
610
+ <w:tr w:rsidR="00213ACD" w14:paraId="34315A41" w14:textId="77777777" w:rsidTr="00213ACD">
611
+ <w:tc>
612
+ <w:tcPr>
613
+ <w:tcW w:w="2235" w:type="dxa" />
614
+ <w:shd w:val="clear" w:color="auto" w:fill="auto" />
615
+ </w:tcPr>
616
+ <w:p w14:paraId="1CA83F76" w14:textId="2622C490" w:rsidR="00213ACD" w:rsidRDefault="00213ACD" w:rsidP="003F16E3">
617
+ <w:r>
618
+ <w:fldChar w:fldCharType="begin" />
619
+ </w:r>
620
+ <w:r>
621
+ <w:instrText xml:space="preserve"> MERGEFIELD foods:endEach \\* MERGEFORMAT </w:instrText>
622
+ </w:r>
623
+ <w:r>
624
+ <w:fldChar w:fldCharType="separate" />
625
+ </w:r>
626
+ <w:r>
627
+ <w:rPr>
628
+ <w:noProof />
629
+ </w:rPr>
630
+ <w:t>«foods:endEach»</w:t>
631
+ </w:r>
632
+ <w:r>
633
+ <w:fldChar w:fldCharType="end" />
634
+ </w:r>
635
+ </w:p>
636
+ </w:tc>
637
+ <w:tc>
638
+ <w:tcPr>
639
+ <w:tcW w:w="6287" w:type="dxa" />
640
+ <w:shd w:val="clear" w:color="auto" w:fill="auto" />
641
+ </w:tcPr>
642
+ <w:p w14:paraId="7D976602" w14:textId="77777777" w:rsidR="00213ACD" w:rsidRDefault="00213ACD" w:rsidP="003F16E3" />
643
+ </w:tc>
644
+ </w:tr>
645
+ </w:tbl>
646
+ document
647
+
648
+ assert_equal "1. Milk Milk is a white liquid. 2. Sugar Sugar is the generalized name for carbohydrates.", text(result)
649
+ end
650
+
651
+ def test_conditional
652
+ document = <<-documentxml
653
+ <w:r><w:t xml:space="preserve">Anthony</w:t></w:r>
654
+ <w:p>
655
+ <w:fldSimple w:instr=" MERGEFIELD middle_name:if \\* MERGEFORMAT ">
656
+ <w:r>
657
+ <w:rPr>
658
+ <w:noProof/>
659
+ </w:rPr>
660
+ <w:t>«middle_name:if»</w:t>
661
+ </w:r>
662
+ </w:fldSimple>
663
+ </w:p>
664
+ <w:p>
665
+ <w:fldSimple w:instr=" MERGEFIELD =middle_name \\* MERGEFORMAT ">
666
+ <w:r>
667
+ <w:rPr>
668
+ <w:noProof/>
669
+ </w:rPr>
670
+ <w:t>«=middle_name»</w:t>
671
+ </w:r>
672
+ </w:fldSimple>
673
+ </w:p>
674
+ <w:p>
675
+ <w:fldSimple w:instr=" MERGEFIELD middle_name:endIf \\* MERGEFORMAT ">
676
+ <w:r>
677
+ <w:rPr>
678
+ <w:noProof/>
679
+ </w:rPr>
680
+ <w:t>«middle_name:endIf»</w:t>
681
+ </w:r>
682
+ </w:fldSimple>
683
+ </w:p>
684
+ <w:r><w:t xml:space="preserve">Hall</w:t></w:r>
685
+ documentxml
686
+ result = process(document, {"middle_name" => "Michael"})
687
+ assert_equal "Anthony Michael Hall", text(result)
688
+
689
+ result = process(document, {"middle_name" => nil})
690
+ assert_equal "Anthony Hall", text(result)
691
+ end
692
+
693
+ private
694
+ def process(document, context)
695
+ @processor.process(wrap(document), context).to_xml
696
+ end
697
+ end