sablon 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 975fe24c74bf896987909e597c4600087b4aee50
4
- data.tar.gz: c884b29cb782ed4e30125e3020e80f52af34a92e
3
+ metadata.gz: 1f2bdc7b8b84da3ad1a1893b515d23530fa6f597
4
+ data.tar.gz: c872738213cbc5ebd926a9287512c40393a27fe2
5
5
  SHA512:
6
- metadata.gz: aec7e1fa1c03628473f4ea9122ea44c64dedf6041d62a891e21267f12456cca58751cb43be982b588937bd8051ed4358e56ab97cebff85127f19f089a28e058a
7
- data.tar.gz: 024a467e6a943623413deb38b2e2422a234b53d24ba1b7985778ba6d38ae66443af6df6c52321ec5ae5312c8eb195023701def63f40d7e027fa0dcb90f5dbf5f
6
+ metadata.gz: b12594e4647d2787ed4b8a2020ac44f668d2834f53cf8a16df4ff384fe52587386b8af8004cfdcb2c0da89d7c64481fb33363d1a59b43235cbb19f2b65f3bfa1
7
+ data.tar.gz: 5a4ea402a07886f1009d24917ec850f7adef2d248511e2d5e93272794d0d49ff632af0554a2cd4cfcd1ddda7535db6f081e117d00b9d333f7e859b0abcaa5076
data/README.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/senny/sablon.svg?branch=master)](https://travis-ci.org/senny/sablon)
4
4
 
5
- Is a document template processor based on `docx`. Tags are represented using
6
- MailMerge fields.
5
+ Is a document template processor for Word `docx` files. It leverages Word's
6
+ built-in formatting and layouting capabilities to make it easy to create your
7
+ templates.
7
8
 
8
9
  ## Installation
9
10
 
@@ -33,7 +34,7 @@ Sablon templates are normal word documents (`.docx`) sprinkled with MergeFields
33
34
  to perform operations. The following section will use the notation `«=title»` to
34
35
  refer to [Word MailMerge](http://en.wikipedia.org/wiki/Mail_merge) fields.
35
36
 
36
- #### Inserting content
37
+ #### Content Insertion
37
38
 
38
39
  The most basic operation is to insert content. The contents of a context
39
40
  variable can be inserted using a field like:
@@ -61,8 +62,16 @@ context variable. Conditional fields are inserted around the content.
61
62
  ```
62
63
 
63
64
  This will render the enclosed markup only if the expression is truthy.
64
- Note that `nil`, `false` and `[]` are considered falsy. Everything else is truthy.
65
+ Note that `nil`, `false` and `[]` are considered falsy. Everything else is
66
+ truthy.
65
67
 
68
+ For more complex conditionals you can use a predicate like so:
69
+
70
+ ```
71
+ «body:if(present?)»
72
+ ... arbitrary document markup ...
73
+ «body:endIf»
74
+ ```
66
75
 
67
76
  #### Loops
68
77
 
@@ -88,9 +97,24 @@ It is possible to nest loops and conditionals.
88
97
 
89
98
  ### Examples
90
99
 
91
- [This test](test/sablon_test.rb) is a good example of what Sablon can do for
92
- you. Make sure to look at the [template](test/fixtures/sablon_template.docx) and
93
- the corresponding [output](test/fixtures/sablon_sample.docx).
100
+ There is a [sample template](test/fixtures/sablon_template.docx) in the
101
+ repository, which illustrates the functionality of sablon:
102
+
103
+ <p align="center">
104
+ <img
105
+ src="https://raw.githubusercontent.com/senny/sablon/master/misc/template.png"
106
+ alt="Sablon Template"/>
107
+ </p>
108
+
109
+ Processing this template with some sample data yields the following
110
+ [output document](test/fixtures/sablon_sample.docx).
111
+ For more details, check out this [test case](test/sablon_test.rb).
112
+
113
+ <p align="center">
114
+ <img
115
+ src="https://raw.githubusercontent.com/senny/sablon/master/misc/output.png"
116
+ alt="Sablon Output"/>
117
+ </p>
94
118
 
95
119
 
96
120
  ## Contributing
@@ -101,6 +125,7 @@ the corresponding [output](test/fixtures/sablon_sample.docx).
101
125
  4. Push to the branch (`git push origin my-new-feature`)
102
126
  5. Create a new Pull Request
103
127
 
128
+
104
129
  ## Inspiration
105
130
 
106
131
  The following projects address a similar goal and inspired the work on Sablon:
@@ -16,9 +16,10 @@ module Sablon
16
16
  end
17
17
  end
18
18
 
19
- class Condition < Struct.new(:conditon_expr, :block)
19
+ class Condition < Struct.new(:conditon_expr, :block, :predicate)
20
20
  def evaluate(context)
21
- if truthy?(conditon_expr.evaluate(context))
21
+ value = conditon_expr.evaluate(context)
22
+ if truthy?(predicate ? value.public_send(predicate) : value)
22
23
  block.replace(block.process(context).reverse)
23
24
  else
24
25
  block.replace([])
@@ -10,7 +10,7 @@ module Sablon
10
10
  private
11
11
  def replace_field_display(node, text)
12
12
  display_node = node.search(".//w:t").first
13
- text.scan(/[^\n]+|\n/).reverse.each do |part|
13
+ text.to_s.scan(/[^\n]+|\n/).reverse.each do |part|
14
14
  if part == "\n"
15
15
  display_node.add_next_sibling Nokogiri::XML::Node.new "w:br", display_node.document
16
16
  else
@@ -67,20 +67,26 @@ module Sablon
67
67
  def parse_fields(xml)
68
68
  fields = []
69
69
  xml.traverse do |node|
70
- if node.name == "fldSimple" && node.namespace && node.namespace.prefix == "w"
71
- fields << SimpleField.new(node)
72
- elsif node.name == "fldChar" && node.namespace && node.namespace.prefix == "w" && node["w:fldCharType"] == "begin"
73
- possible_field_node = node.parent
74
- field_nodes = [possible_field_node]
75
- while possible_field_node && possible_field_node.search(".//w:fldChar[@w:fldCharType='end']").empty?
76
- possible_field_node = possible_field_node.next_element
77
- field_nodes << possible_field_node
78
- end
79
- fields << ComplexField.new(field_nodes)
70
+ if node.name == "fldSimple"
71
+ field = SimpleField.new(node)
72
+ elsif node.name == "fldChar" && node["w:fldCharType"] == "begin"
73
+ field = build_complex_field(node)
80
74
  end
75
+ fields << field if field && field.expression
81
76
  end
82
77
  fields
83
78
  end
79
+
80
+ private
81
+ def build_complex_field(node)
82
+ possible_field_node = node.parent
83
+ field_nodes = [possible_field_node]
84
+ while possible_field_node && possible_field_node.search(".//w:fldChar[@w:fldCharType='end']").empty?
85
+ possible_field_node = possible_field_node.next_element
86
+ field_nodes << possible_field_node
87
+ end
88
+ ComplexField.new(field_nodes)
89
+ end
84
90
  end
85
91
  end
86
92
  end
@@ -38,16 +38,15 @@ module Sablon
38
38
  class Block < Struct.new(:start_field, :end_field)
39
39
  def self.enclosed_by(start_field, end_field)
40
40
  @blocks ||= [RowBlock, ParagraphBlock]
41
- block_class = @blocks.detect { |klass| klass.possible?(start_field) && klass.possible?(end_field) }
41
+ block_class = @blocks.detect { |klass| klass.encloses?(start_field, end_field) }
42
42
  block_class.new start_field, end_field
43
43
  end
44
44
 
45
45
  def process(context)
46
- body.map do |template_node|
47
- replaced_node = template_node.dup
48
- Processor.process replaced_node, context
49
- replaced_node
50
- end
46
+ replaced_node = Nokogiri::XML::Node.new("tmp", start_node.document)
47
+ replaced_node.children = Nokogiri::XML::NodeSet.new(start_node.document, body.map(&:dup))
48
+ Processor.process replaced_node, context
49
+ replaced_node.children
51
50
  end
52
51
 
53
52
  def replace(content)
@@ -76,8 +75,8 @@ module Sablon
76
75
  @end_node ||= self.class.parent(end_field).first
77
76
  end
78
77
 
79
- def self.possible?(node)
80
- parent(node).any?
78
+ def self.encloses?(start_field, end_field)
79
+ parent(start_field).any? && parent(end_field).any?
81
80
  end
82
81
  end
83
82
 
@@ -85,6 +84,12 @@ module Sablon
85
84
  def self.parent(node)
86
85
  node.ancestors ".//w:tr"
87
86
  end
87
+
88
+ def self.encloses?(start_field, end_field)
89
+ if super
90
+ parent(start_field) != parent(end_field)
91
+ end
92
+ end
88
93
  end
89
94
 
90
95
  class ParagraphBlock < Block
@@ -108,6 +113,7 @@ module Sablon
108
113
 
109
114
  def consume(allow_insertion)
110
115
  @field = @fields.shift
116
+ return unless @field
111
117
  case @field.expression
112
118
  when /^=/
113
119
  if allow_insertion
@@ -116,6 +122,9 @@ module Sablon
116
122
  when /([^ ]+):each\(([^ ]+)\)/
117
123
  block = consume_block("#{$1}:endEach")
118
124
  Statement::Loop.new(Expression.parse($1), $2, block)
125
+ when /([^ ]+):if\(([^)]+)\)/
126
+ block = consume_block("#{$1}:endIf")
127
+ Statement::Condition.new(Expression.parse($1), block, $2)
119
128
  when /([^ ]+):if/
120
129
  block = consume_block("#{$1}:endIf")
121
130
  Statement::Condition.new(Expression.parse($1), block)
@@ -125,10 +134,15 @@ module Sablon
125
134
  def consume_block(end_expression)
126
135
  start_field = end_field = @field
127
136
  while end_field && end_field.expression != end_expression
128
- @operations << consume(false)
137
+ consume(false)
129
138
  end_field = @field
130
139
  end
131
- Block.enclosed_by start_field, end_field
140
+
141
+ if end_field
142
+ Block.enclosed_by start_field, end_field
143
+ else
144
+ raise "no end field for #{start_field.expression}. Was looking for #{end_expression}"
145
+ end
132
146
  end
133
147
  end
134
148
  end
@@ -25,6 +25,8 @@ module Sablon
25
25
  content = entry.get_input_stream.read
26
26
  if entry_name == 'word/document.xml'
27
27
  out.write(Processor.process(Nokogiri::XML(content), context, properties).to_xml)
28
+ elsif entry_name =~ /word\/header\d*\.xml/ || entry_name =~ /word\/footer\d*\.xml/
29
+ out.write(Processor.process(Nokogiri::XML(content), context).to_xml)
28
30
  else
29
31
  out.write(content)
30
32
  end
@@ -0,0 +1 @@
1
+ require "sablon/test/assertions"
@@ -0,0 +1,22 @@
1
+ module Sablon
2
+ module Test
3
+ module Assertions
4
+ def assert_docx_equal(expected_path, actual_path)
5
+ if get_document_xml(expected_path) != get_document_xml(actual_path)
6
+ msg = <<-error
7
+ The generated document does not match the sample. Please investigate.
8
+
9
+ If the generated document is correct, the sample needs to be updated:
10
+ \t cp #{actual_path} #{expected_path}
11
+ error
12
+ fail msg
13
+ end
14
+ end
15
+
16
+ def get_document_xml(path)
17
+ document_xml_entry = Zip::File.open(path).get_entry("word/document.xml")
18
+ document_xml_entry.get_input_stream.read
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module Sablon
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/misc/output.png ADDED
Binary file
data/misc/template.png ADDED
Binary file
data/sablon.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_runtime_dependency 'nokogiri'
22
- spec.add_runtime_dependency 'rubyzip'
22
+ spec.add_runtime_dependency 'rubyzip', ">= 1.1"
23
23
 
24
24
  spec.add_development_dependency "bundler", ">= 1.6"
25
25
  spec.add_development_dependency "rake", "~> 10.0"
Binary file
Binary file
@@ -10,13 +10,20 @@ module MailMergeParser
10
10
  @parser = Sablon::Parser::MailMerge.new
11
11
  end
12
12
 
13
+ def field
14
+ @field ||= fields.first
15
+ end
16
+
13
17
  def fields
14
- @document = xml
15
- @parser.parse_fields(@document)
18
+ @parser.parse_fields(document)
16
19
  end
17
20
 
18
21
  def body_xml
19
- @document.search(".//w:body").children.map(&:to_xml).join.strip
22
+ document.search(".//w:body").children.map(&:to_xml).join.strip
23
+ end
24
+
25
+ def document
26
+ @document ||= xml
20
27
  end
21
28
  end
22
29
 
@@ -28,7 +35,6 @@ module MailMergeParser
28
35
  end
29
36
 
30
37
  def test_replace
31
- field = fields.first
32
38
  field.replace("Hello")
33
39
  assert_equal <<-body_xml.strip, body_xml
34
40
  <w:r w:rsidR=\"004B49F0\">
@@ -39,7 +45,6 @@ body_xml
39
45
  end
40
46
 
41
47
  def test_replace_with_newlines
42
- field = fields.first
43
48
  field.replace("First\nSecond\n\nThird")
44
49
 
45
50
  assert_equal <<-body_xml.strip, body_xml
@@ -50,6 +55,28 @@ body_xml
50
55
  body_xml
51
56
  end
52
57
 
58
+ def test_replace_with_nil
59
+ field.replace(nil)
60
+
61
+ assert_equal <<-body_xml.strip, body_xml.gsub(/^\s+$/,'')
62
+ <w:r w:rsidR=\"004B49F0\">
63
+ <w:rPr><w:noProof/></w:rPr>
64
+
65
+ </w:r>
66
+ body_xml
67
+ end
68
+
69
+ def test_replace_with_numeric
70
+ field.replace(45)
71
+
72
+ assert_equal <<-body_xml.strip, body_xml.gsub(/^\s+$/,'')
73
+ <w:r w:rsidR=\"004B49F0\">
74
+ <w:rPr><w:noProof/></w:rPr>
75
+ <w:t>45</w:t>
76
+ </w:r>
77
+ body_xml
78
+ end
79
+
53
80
  private
54
81
  def xml
55
82
  wrap(<<-xml)
@@ -71,7 +98,6 @@ body_xml
71
98
  end
72
99
 
73
100
  def test_replace
74
- field = fields.first
75
101
  field.replace("Hello")
76
102
  assert_equal <<-body_xml.strip, body_xml
77
103
  <w:r w:rsidR="004B49F0">
@@ -85,7 +111,6 @@ body_xml
85
111
  end
86
112
 
87
113
  def test_replace_with_newlines
88
- field = fields.first
89
114
  field.replace("First\nSecond\n\nThird")
90
115
 
91
116
  assert_equal <<-body_xml.strip, body_xml
@@ -99,6 +124,20 @@ body_xml
99
124
  body_xml
100
125
  end
101
126
 
127
+ def test_replace_with_nil
128
+ field.replace(nil)
129
+
130
+ assert_equal <<-body_xml.strip, body_xml.gsub(/^\s+$/,'')
131
+ <w:r w:rsidR="004B49F0">
132
+ <w:rPr>
133
+ <w:b/>
134
+ <w:noProof/>
135
+ </w:rPr>
136
+
137
+ </w:r>
138
+ body_xml
139
+ end
140
+
102
141
  private
103
142
  def xml
104
143
  wrap(<<-xml)
@@ -128,4 +167,58 @@ body_xml
128
167
  xml
129
168
  end
130
169
  end
170
+
171
+ class NonSablonFieldTest < Sablon::TestCase
172
+ include SharedBehavior
173
+
174
+ def test_is_ignored
175
+ assert_equal [], fields.map(&:class)
176
+ end
177
+
178
+ private
179
+ def xml
180
+ wrap(<<-xml)
181
+ <w:p w14:paraId="0CF428D7" w14:textId="77777777" w:rsidR="00043618" w:rsidRDefault="00043618" w:rsidP="00B960C2">
182
+ <w:pPr>
183
+ <w:pStyle w:val="Footer" />
184
+ <w:framePr w:wrap="around" w:vAnchor="text" w:hAnchor="margin" w:xAlign="right" w:y="1" />
185
+ <w:rPr>
186
+ <w:rStyle w:val="PageNumber" />
187
+ </w:rPr>
188
+ </w:pPr>
189
+ <w:r>
190
+ <w:rPr>
191
+ <w:rStyle w:val="PageNumber" />
192
+ </w:rPr>
193
+ <w:fldChar w:fldCharType="begin" />
194
+ </w:r>
195
+ <w:r>
196
+ <w:rPr>
197
+ <w:rStyle w:val="PageNumber" />
198
+ </w:rPr>
199
+ <w:instrText xml:space="preserve">PAGE </w:instrText>
200
+ </w:r>
201
+ <w:r>
202
+ <w:rPr>
203
+ <w:rStyle w:val="PageNumber" />
204
+ </w:rPr>
205
+ <w:fldChar w:fldCharType="separate" />
206
+ </w:r>
207
+ <w:r w:rsidR="00326FC5">
208
+ <w:rPr>
209
+ <w:rStyle w:val="PageNumber" />
210
+ <w:noProof />
211
+ </w:rPr>
212
+ <w:t>1</w:t>
213
+ </w:r>
214
+ <w:r>
215
+ <w:rPr>
216
+ <w:rStyle w:val="PageNumber" />
217
+ </w:rPr>
218
+ <w:fldChar w:fldCharType="end" />
219
+ </w:r>
220
+ </w:p>
221
+ xml
222
+ end
223
+ end
131
224
  end
@@ -38,34 +38,23 @@ class ProcessorTest < Sablon::TestCase
38
38
  result = process(<<-documentxml, {"last_name" => "Zane"})
39
39
  <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
40
40
  <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
41
- <w:rPr>
42
- <w:b/>
43
- </w:rPr>
41
+ <w:rPr><w:b/></w:rPr>
44
42
  <w:fldChar w:fldCharType="begin"/>
45
43
  </w:r>
46
44
  <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
47
- <w:rPr>
48
- <w:b/>
49
- </w:rPr>
45
+ <w:rPr><w:b/></w:rPr>
50
46
  <w:instrText xml:space="preserve"> MERGEFIELD =last_name \\* MERGEFORMAT </w:instrText>
51
47
  </w:r>
52
48
  <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
53
- <w:rPr>
54
- <w:b/>
55
- </w:rPr>
49
+ <w:rPr><w:b/></w:rPr>
56
50
  <w:fldChar w:fldCharType="separate"/>
57
51
  </w:r>
58
52
  <w:r w:rsidR="004B49F0">
59
- <w:rPr>
60
- <w:b/>
61
- <w:noProof/>
62
- </w:rPr>
53
+ <w:rPr><w:b/><w:noProof/></w:rPr>
63
54
  <w:t>«=last_name»</w:t>
64
55
  </w:r>
65
56
  <w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
66
- <w:rPr>
67
- <w:b/>
68
- </w:rPr>
57
+ <w:rPr><w:b/></w:rPr>
69
58
  <w:fldChar w:fldCharType="end"/>
70
59
  </w:r>
71
60
  <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
@@ -75,10 +64,7 @@ class ProcessorTest < Sablon::TestCase
75
64
  assert_xml_equal <<-document, result
76
65
  <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
77
66
  <w:r w:rsidR="004B49F0">
78
- <w:rPr>
79
- <w:b/>
80
- <w:noProof/>
81
- </w:rPr>
67
+ <w:rPr><w:b/><w:noProof/></w:rPr>
82
68
  <w:t>Zane</w:t>
83
69
  </w:r>
84
70
  <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
@@ -106,9 +92,7 @@ class ProcessorTest < Sablon::TestCase
106
92
  <w:fldChar w:fldCharType="separate" />
107
93
  </w:r>
108
94
  <w:r w:rsidR="00441382">
109
- <w:rPr>
110
- <w:noProof />
111
- </w:rPr>
95
+ <w:rPr><w:noProof /></w:rPr>
112
96
  <w:t>«=person.first_name»</w:t>
113
97
  </w:r>
114
98
  <w:r w:rsidR="003C4780">
@@ -121,9 +105,7 @@ class ProcessorTest < Sablon::TestCase
121
105
  assert_xml_equal <<-document, result
122
106
  <w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
123
107
  <w:r w:rsidR="00441382">
124
- <w:rPr>
125
- <w:noProof/>
126
- </w:rPr>
108
+ <w:rPr><w:noProof/></w:rPr>
127
109
  <w:t>Daniel</w:t>
128
110
  </w:r>
129
111
  <w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
@@ -131,7 +113,6 @@ class ProcessorTest < Sablon::TestCase
131
113
  end
132
114
 
133
115
  def test_paragraph_block_replacement
134
- item = Struct.new(:index, :label, :rating)
135
116
  result = process(<<-document, {"technologies" => ["Ruby", "Rails"]})
136
117
  <w:p w14:paraId="6CB29D92" w14:textId="164B70F4" w:rsidR="007F5CDE" w:rsidRDefault="007F5CDE" w:rsidP="007F5CDE">
137
118
  <w:pPr>
@@ -143,9 +124,7 @@ class ProcessorTest < Sablon::TestCase
143
124
  </w:pPr>
144
125
  <w:fldSimple w:instr=" MERGEFIELD technologies:each(technology) \\* MERGEFORMAT ">
145
126
  <w:r>
146
- <w:rPr>
147
- <w:noProof />
148
- </w:rPr>
127
+ <w:rPr><w:noProof /></w:rPr>
149
128
  <w:t>«technologies:each(technology)»</w:t>
150
129
  </w:r>
151
130
  </w:fldSimple>
@@ -174,9 +153,7 @@ class ProcessorTest < Sablon::TestCase
174
153
  <w:fldChar w:fldCharType="separate" />
175
154
  </w:r>
176
155
  <w:r w:rsidR="009F01DA">
177
- <w:rPr>
178
- <w:noProof />
179
- </w:rPr>
156
+ <w:rPr><w:noProof /></w:rPr>
180
157
  <w:t>«=technology»</w:t>
181
158
  </w:r>
182
159
  <w:r>
@@ -193,9 +170,7 @@ class ProcessorTest < Sablon::TestCase
193
170
  </w:pPr>
194
171
  <w:fldSimple w:instr=" MERGEFIELD technologies:endEach \\* MERGEFORMAT ">
195
172
  <w:r>
196
- <w:rPr>
197
- <w:noProof />
198
- </w:rPr>
173
+ <w:rPr><w:noProof /></w:rPr>
199
174
  <w:t>«technologies:endEach»</w:t>
200
175
  </w:r>
201
176
  </w:fldSimple>
@@ -213,9 +188,7 @@ class ProcessorTest < Sablon::TestCase
213
188
  </w:numPr>
214
189
  </w:pPr>
215
190
  <w:r w:rsidR="009F01DA">
216
- <w:rPr>
217
- <w:noProof/>
218
- </w:rPr>
191
+ <w:rPr><w:noProof/></w:rPr>
219
192
  <w:t>Ruby</w:t>
220
193
  </w:r>
221
194
  </w:p><w:p w14:paraId="1081E316" w14:textId="3EAB5FDC" w:rsidR="00380EE8" w:rsidRDefault="00380EE8" w:rsidP="007F5CDE">
@@ -227,15 +200,76 @@ class ProcessorTest < Sablon::TestCase
227
200
  </w:numPr>
228
201
  </w:pPr>
229
202
  <w:r w:rsidR="009F01DA">
230
- <w:rPr>
231
- <w:noProof/>
232
- </w:rPr>
203
+ <w:rPr><w:noProof/></w:rPr>
233
204
  <w:t>Rails</w:t>
234
205
  </w:r>
235
206
  </w:p>
236
207
  document
237
208
  end
238
209
 
210
+ def test_paragraph_block_within_table_cell
211
+ result = process(<<-document, {"technologies" => ["Puppet", "Chef"]})
212
+ <w:tbl>
213
+ <w:tblGrid>
214
+ <w:gridCol w:w="2202"/>
215
+ </w:tblGrid>
216
+ <w:tr w:rsidR="00757DAD">
217
+ <w:tc>
218
+ <w:p>
219
+ <w:fldSimple w:instr=" MERGEFIELD technologies:each(technology) \\* MERGEFORMAT ">
220
+ <w:r w:rsidR="004B49F0">
221
+ <w:rPr><w:noProof/></w:rPr>
222
+ <w:t>«technologies:each(technology)»</w:t>
223
+ </w:r>
224
+ </w:fldSimple>
225
+ </w:p>
226
+ <w:p>
227
+ <w:fldSimple w:instr=" MERGEFIELD =technology \\* MERGEFORMAT ">
228
+ <w:r w:rsidR="004B49F0">
229
+ <w:rPr><w:noProof/></w:rPr>
230
+ <w:t>«=technology»</w:t>
231
+ </w:r>
232
+ </w:fldSimple>
233
+ </w:p>
234
+ <w:p>
235
+ <w:fldSimple w:instr=" MERGEFIELD technologies:endEach \\* MERGEFORMAT ">
236
+ <w:r w:rsidR="004B49F0">
237
+ <w:rPr><w:noProof/></w:rPr>
238
+ <w:t>«technologies:endEach»</w:t>
239
+ </w:r>
240
+ </w:fldSimple>
241
+ </w:p>
242
+ </w:tc>
243
+ </w:tr>
244
+ </w:tbl>
245
+ document
246
+
247
+ assert_equal "Puppet Chef", text(result)
248
+ assert_xml_equal <<-document, result
249
+ <w:tbl>
250
+ <w:tblGrid>
251
+ <w:gridCol w:w="2202"/>
252
+ </w:tblGrid>
253
+ <w:tr w:rsidR="00757DAD">
254
+ <w:tc>
255
+ <w:p>
256
+ <w:r w:rsidR="004B49F0">
257
+ <w:rPr><w:noProof/></w:rPr>
258
+ <w:t>Puppet</w:t>
259
+ </w:r>
260
+ </w:p>
261
+ <w:p>
262
+ <w:r w:rsidR="004B49F0">
263
+ <w:rPr><w:noProof/></w:rPr>
264
+ <w:t>Chef</w:t>
265
+ </w:r>
266
+ </w:p>
267
+ </w:tc>
268
+ </w:tr>
269
+ </w:tbl>
270
+ document
271
+ end
272
+
239
273
  def test_single_row_table_loop
240
274
  item = Struct.new(:index, :label, :rating)
241
275
  result = process(<<-document, {"items" => [item.new("1.", "Milk", "***"), item.new("2.", "Sugar", "**")]})
@@ -266,9 +300,7 @@ class ProcessorTest < Sablon::TestCase
266
300
  <w:fldChar w:fldCharType="separate"/>
267
301
  </w:r>
268
302
  <w:r>
269
- <w:rPr>
270
- <w:noProof/>
271
- </w:rPr>
303
+ <w:rPr><w:noProof/></w:rPr>
272
304
  <w:t>«items:each(item)»</w:t>
273
305
  </w:r>
274
306
  <w:r>
@@ -305,9 +337,7 @@ class ProcessorTest < Sablon::TestCase
305
337
  <w:fldChar w:fldCharType="separate"/>
306
338
  </w:r>
307
339
  <w:r>
308
- <w:rPr>
309
- <w:noProof/>
310
- </w:rPr>
340
+ <w:rPr><w:noProof/></w:rPr>
311
341
  <w:t>«=item.index»</w:t>
312
342
  </w:r>
313
343
  <w:r>
@@ -330,9 +360,7 @@ class ProcessorTest < Sablon::TestCase
330
360
  <w:fldChar w:fldCharType="separate"/>
331
361
  </w:r>
332
362
  <w:r>
333
- <w:rPr>
334
- <w:noProof/>
335
- </w:rPr>
363
+ <w:rPr><w:noProof/></w:rPr>
336
364
  <w:t>«=item.label»</w:t>
337
365
  </w:r>
338
366
  <w:r>
@@ -355,9 +383,7 @@ class ProcessorTest < Sablon::TestCase
355
383
  <w:fldChar w:fldCharType="separate"/>
356
384
  </w:r>
357
385
  <w:r>
358
- <w:rPr>
359
- <w:noProof/>
360
- </w:rPr>
386
+ <w:rPr><w:noProof/></w:rPr>
361
387
  <w:t>«=item.rating»</w:t>
362
388
  </w:r>
363
389
  <w:r>
@@ -382,9 +408,7 @@ class ProcessorTest < Sablon::TestCase
382
408
  <w:fldChar w:fldCharType="separate"/>
383
409
  </w:r>
384
410
  <w:r>
385
- <w:rPr>
386
- <w:noProof/>
387
- </w:rPr>
411
+ <w:rPr><w:noProof/></w:rPr>
388
412
  <w:t>«items:endEach»</w:t>
389
413
  </w:r>
390
414
  <w:r>
@@ -427,9 +451,7 @@ class ProcessorTest < Sablon::TestCase
427
451
  </w:tcPr>
428
452
  <w:p w14:paraId="41ACB3D9" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
429
453
  <w:r>
430
- <w:rPr>
431
- <w:noProof/>
432
- </w:rPr>
454
+ <w:rPr><w:noProof/></w:rPr>
433
455
  <w:t>1.</w:t>
434
456
  </w:r>
435
457
  </w:p>
@@ -440,9 +462,7 @@ class ProcessorTest < Sablon::TestCase
440
462
  </w:tcPr>
441
463
  <w:p w14:paraId="197C6F31" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
442
464
  <w:r>
443
- <w:rPr>
444
- <w:noProof/>
445
- </w:rPr>
465
+ <w:rPr><w:noProof/></w:rPr>
446
466
  <w:t>Milk</w:t>
447
467
  </w:r>
448
468
  </w:p>
@@ -453,9 +473,7 @@ class ProcessorTest < Sablon::TestCase
453
473
  </w:tcPr>
454
474
  <w:p w14:paraId="55C258BB" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
455
475
  <w:r>
456
- <w:rPr>
457
- <w:noProof/>
458
- </w:rPr>
476
+ <w:rPr><w:noProof/></w:rPr>
459
477
  <w:t>***</w:t>
460
478
  </w:r>
461
479
  </w:p>
@@ -467,9 +485,7 @@ class ProcessorTest < Sablon::TestCase
467
485
  </w:tcPr>
468
486
  <w:p w14:paraId="41ACB3D9" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
469
487
  <w:r>
470
- <w:rPr>
471
- <w:noProof/>
472
- </w:rPr>
488
+ <w:rPr><w:noProof/></w:rPr>
473
489
  <w:t>2.</w:t>
474
490
  </w:r>
475
491
  </w:p>
@@ -480,9 +496,7 @@ class ProcessorTest < Sablon::TestCase
480
496
  </w:tcPr>
481
497
  <w:p w14:paraId="197C6F31" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
482
498
  <w:r>
483
- <w:rPr>
484
- <w:noProof/>
485
- </w:rPr>
499
+ <w:rPr><w:noProof/></w:rPr>
486
500
  <w:t>Sugar</w:t>
487
501
  </w:r>
488
502
  </w:p>
@@ -493,9 +507,7 @@ class ProcessorTest < Sablon::TestCase
493
507
  </w:tcPr>
494
508
  <w:p w14:paraId="55C258BB" w14:textId="77777777" w:rsidR="00757DAD" w:rsidRDefault="00757DAD" w:rsidP="006333C3">
495
509
  <w:r>
496
- <w:rPr>
497
- <w:noProof/>
498
- </w:rPr>
510
+ <w:rPr><w:noProof/></w:rPr>
499
511
  <w:t>**</w:t>
500
512
  </w:r>
501
513
  </w:p>
@@ -521,9 +533,7 @@ class ProcessorTest < Sablon::TestCase
521
533
  <w:p w14:paraId="7630A6C6" w14:textId="699D0C71" w:rsidR="00F23752" w:rsidRDefault="00F23752" w:rsidP="003F16E3">
522
534
  <w:fldSimple w:instr=" MERGEFIELD foods:each(food) \\* MERGEFORMAT ">
523
535
  <w:r w:rsidR="00213ACD">
524
- <w:rPr>
525
- <w:noProof />
526
- </w:rPr>
536
+ <w:rPr><w:noProof /></w:rPr>
527
537
  <w:t>«foods:each(food)»</w:t>
528
538
  </w:r>
529
539
  </w:fldSimple>
@@ -546,9 +556,7 @@ class ProcessorTest < Sablon::TestCase
546
556
  <w:p w14:paraId="3FCF3855" w14:textId="38FA7F3B" w:rsidR="00F23752" w:rsidRDefault="00F23752" w:rsidP="00F23752">
547
557
  <w:fldSimple w:instr=" MERGEFIELD =food.index \\* MERGEFORMAT ">
548
558
  <w:r w:rsidR="00213ACD">
549
- <w:rPr>
550
- <w:noProof />
551
- </w:rPr>
559
+ <w:rPr><w:noProof /></w:rPr>
552
560
  <w:t>«=food.index»</w:t>
553
561
  </w:r>
554
562
  </w:fldSimple>
@@ -598,9 +606,7 @@ class ProcessorTest < Sablon::TestCase
598
606
  <w:p w14:paraId="3E9FF163" w14:textId="0F37CDFB" w:rsidR="00213ACD" w:rsidRDefault="00213ACD" w:rsidP="003F16E3">
599
607
  <w:fldSimple w:instr=" MERGEFIELD =food.body \\* MERGEFORMAT ">
600
608
  <w:r>
601
- <w:rPr>
602
- <w:noProof />
603
- </w:rPr>
609
+ <w:rPr><w:noProof /></w:rPr>
604
610
  <w:t>«=food.body»</w:t>
605
611
  </w:r>
606
612
  </w:fldSimple>
@@ -624,9 +630,7 @@ class ProcessorTest < Sablon::TestCase
624
630
  <w:fldChar w:fldCharType="separate" />
625
631
  </w:r>
626
632
  <w:r>
627
- <w:rPr>
628
- <w:noProof />
629
- </w:rPr>
633
+ <w:rPr><w:noProof /></w:rPr>
630
634
  <w:t>«foods:endEach»</w:t>
631
635
  </w:r>
632
636
  <w:r>
@@ -654,9 +658,7 @@ class ProcessorTest < Sablon::TestCase
654
658
  <w:p>
655
659
  <w:fldSimple w:instr=" MERGEFIELD middle_name:if \\* MERGEFORMAT ">
656
660
  <w:r>
657
- <w:rPr>
658
- <w:noProof/>
659
- </w:rPr>
661
+ <w:rPr><w:noProof/></w:rPr>
660
662
  <w:t>«middle_name:if»</w:t>
661
663
  </w:r>
662
664
  </w:fldSimple>
@@ -664,9 +666,7 @@ class ProcessorTest < Sablon::TestCase
664
666
  <w:p>
665
667
  <w:fldSimple w:instr=" MERGEFIELD =middle_name \\* MERGEFORMAT ">
666
668
  <w:r>
667
- <w:rPr>
668
- <w:noProof/>
669
- </w:rPr>
669
+ <w:rPr><w:noProof/></w:rPr>
670
670
  <w:t>«=middle_name»</w:t>
671
671
  </w:r>
672
672
  </w:fldSimple>
@@ -674,9 +674,7 @@ class ProcessorTest < Sablon::TestCase
674
674
  <w:p>
675
675
  <w:fldSimple w:instr=" MERGEFIELD middle_name:endIf \\* MERGEFORMAT ">
676
676
  <w:r>
677
- <w:rPr>
678
- <w:noProof/>
679
- </w:rPr>
677
+ <w:rPr><w:noProof/></w:rPr>
680
678
  <w:t>«middle_name:endIf»</w:t>
681
679
  </w:r>
682
680
  </w:fldSimple>
@@ -690,6 +688,35 @@ class ProcessorTest < Sablon::TestCase
690
688
  assert_equal "Anthony Hall", text(result)
691
689
  end
692
690
 
691
+ def test_conditional_with_predicate
692
+ document = <<-documentxml
693
+ <w:p>
694
+ <w:fldSimple w:instr=" MERGEFIELD body:if(empty?) \\* MERGEFORMAT ">
695
+ <w:r>
696
+ <w:rPr><w:noProof/></w:rPr>
697
+ <w:t>«body:if(empty?)»</w:t>
698
+ </w:r>
699
+ </w:fldSimple>
700
+ </w:p>
701
+ <w:p>
702
+ <w:t>some content</w:t>
703
+ </w:p>
704
+ <w:p>
705
+ <w:fldSimple w:instr=" MERGEFIELD body:endIf \\* MERGEFORMAT ">
706
+ <w:r>
707
+ <w:rPr><w:noProof/></w:rPr>
708
+ <w:t>«body:endIf»</w:t>
709
+ </w:r>
710
+ </w:fldSimple>
711
+ </w:p>
712
+ documentxml
713
+ result = process(document, {"body" => ""})
714
+ assert_equal "some content", text(result)
715
+
716
+ result = process(document, {"body" => "not empty"})
717
+ assert_equal "", text(result)
718
+ end
719
+
693
720
  private
694
721
  def process(document, context)
695
722
  @processor.process(wrap(document), context).to_xml
data/test/sablon_test.rb CHANGED
@@ -2,6 +2,8 @@
2
2
  require "test_helper"
3
3
 
4
4
  class SablonTest < Sablon::TestCase
5
+ include Sablon::Test::Assertions
6
+
5
7
  def setup
6
8
  super
7
9
  @base_path = Pathname.new(File.expand_path("../", __FILE__))
@@ -15,11 +17,14 @@ class SablonTest < Sablon::TestCase
15
17
  position = Struct.new(:duration, :label, :description)
16
18
  language = Struct.new(:name, :skill)
17
19
  context = {
20
+ "current_time" => Time.now.strftime("%d.%m.%Y %H:%M"),
21
+ "author" => "Yves Senn",
18
22
  "title" => "Letter of application",
19
23
  "person" => person,
20
24
  "items" => [item.new("1.", "Ruby", "★" * 5), item.new("2.", "Java", "★" * 1), item.new("3.", "Python", "★" * 3)],
21
25
  "career" => [position.new("1999 - 2006", "Junior Java Engineer", "Lorem ipsum dolor\nsit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."),
22
- position.new("2006 - 2013", "Senior Ruby Developer", "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.")],
26
+ position.new("2006 - 2013", "Senior Ruby Developer", "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."),
27
+ position.new("2013 - today", "Sales...", nil)],
23
28
  "technologies" => ["HTML", "CSS", "SASS", "LESS", "JavaScript"],
24
29
  "languages" => [language.new("German", "native speaker"), language.new("English", "fluent")],
25
30
  "training" => "At vero eos et accusam et justo duo dolores et ea rebum.\n\nStet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
@@ -31,22 +36,4 @@ class SablonTest < Sablon::TestCase
31
36
 
32
37
  assert_docx_equal @base_path + "fixtures/sablon_sample.docx", @output_path
33
38
  end
34
-
35
- private
36
- def assert_docx_equal(expected_path, actual_path)
37
- if get_document_xml(expected_path) != get_document_xml(actual_path)
38
- msg = <<-error
39
- The generated document does not match the sample. Please investigate.
40
-
41
- If the generated document is correct, the sample needs to be updated:
42
- \t cp #{actual_path} #{expected_path}
43
- error
44
- fail msg
45
- end
46
- end
47
-
48
- def get_document_xml(path)
49
- document_xml_entry = Zip::File.open(path).get_entry("word/document.xml")
50
- document_xml_entry.get_input_stream.read
51
- end
52
39
  end
data/test/test_helper.rb CHANGED
@@ -8,6 +8,7 @@ require "pathname"
8
8
 
9
9
  $: << File.expand_path('../../lib', __FILE__)
10
10
  require "sablon"
11
+ require "sablon/test"
11
12
 
12
13
  class Sablon::TestCase < MiniTest::Test
13
14
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sablon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yves Senn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-30 00:00:00.000000000 Z
11
+ date: 2014-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '1.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '1.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -114,7 +114,11 @@ files:
114
114
  - lib/sablon/processor.rb
115
115
  - lib/sablon/processor/section_properties.rb
116
116
  - lib/sablon/template.rb
117
+ - lib/sablon/test.rb
118
+ - lib/sablon/test/assertions.rb
117
119
  - lib/sablon/version.rb
120
+ - misc/output.png
121
+ - misc/template.png
118
122
  - sablon.gemspec
119
123
  - test/expression_test.rb
120
124
  - test/fixtures/sablon_sample.docx