sablon 0.2.1 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 660482be15bbf9eb3c4fe5bfab0f241e40ec8963
4
- data.tar.gz: 23c4bba792f8d725d301efd8c8b83dd5d4344dee
3
+ metadata.gz: 3e4309c45c7d9eb7360f7acb59f0ec625d2c0eef
4
+ data.tar.gz: 20b6e2f1917f5af629078ec07d507058392ad88e
5
5
  SHA512:
6
- metadata.gz: d814af12b612cefbf73e0c9a2dae79aa8415d7676aaf62330afba3a67fbc05fe3af820788ffb9dace829abaff49be2ee046f6e16c4cb1d821760e846d69d59a4
7
- data.tar.gz: 53441ca3c630dfecab9f3455c72564cfe0188d20c47a80f9318a2abf2b038656a3545df9682160af2c5f331332b91fa48f1053d2e4a4a89a1c8bb49ddd9448ac
6
+ metadata.gz: c37fe0cea7659f36f3498667bb9ea984308112306fb074b6ce9b771674423c52672ac81943628c48673fb80009d6a830c1cb12bbc11e90cd079ca38a9ca816f2
7
+ data.tar.gz: a4040805a95867d75573b0b7d2ae705e673b4c9a588793c2eaa05dbd563ff7585006a12a1e81fcf549e3f3939ca3c17e2639a77d7c0836d3e8a43389aa8fd89e
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sablon (0.2.1)
4
+ sablon (0.3.0)
5
5
  nokogiri (>= 1.6.0)
6
6
  rubyzip (>= 1.1.1)
7
7
 
data/README.md CHANGED
@@ -295,6 +295,18 @@ For more complex conditionals you can use a predicate like so:
295
295
  «body:endIf»
296
296
  ```
297
297
 
298
+ Finally, you can also mix in `elsif` and `else` clauses as well.
299
+
300
+ ```
301
+ «body:if(present?)»
302
+ ... arbitrary document markup ...
303
+ «body:elsif(nil?)»
304
+ ... arbitrary document markup ...
305
+ [additional elsif blocks...]
306
+ «body:else»
307
+ ... arbitrary document markup ...
308
+ «body:endIf»
309
+ ```
298
310
 
299
311
  #### Loops
300
312
 
@@ -24,7 +24,10 @@ module Sablon
24
24
  # existing file
25
25
  define_method(:add_media) do |name, data, rel_attr|
26
26
  rel_attr[:Target] = "media/#{name}"
27
- extension = name.match(/\.(\w+?)$/).to_a[1]
27
+ # This matches any characters after the last "." in the filename
28
+ unless (extension = name.match(/.+\.(.+?$)/).to_a[1])
29
+ raise ArgumentError, "Filename: '#{name}' has no discernable extension"
30
+ end
28
31
  type = rel_attr[:Type].match(%r{/(\w+?)$}).to_a[1] + "/#{extension}"
29
32
  #
30
33
  if @zip_contents["word/#{rel_attr[:Target]}"]
@@ -53,22 +53,64 @@ module Sablon
53
53
  end
54
54
  end
55
55
 
56
- class Condition < Struct.new(:conditon_expr, :block, :predicate)
56
+ class Condition
57
+ def initialize(conditions)
58
+ @conditions = conditions
59
+ @else_block = nil
60
+ return unless @conditions.last[:block].start_field.expression =~ /:else/
61
+ #
62
+ # store the else block separately because it is always "true"
63
+ @else_block = @conditions.pop[:block]
64
+ end
65
+
57
66
  def evaluate(env)
58
- value = conditon_expr.evaluate(env.context)
59
- if truthy?(predicate ? value.public_send(predicate) : value)
60
- block.replace(block.process(env).reverse)
61
- else
62
- block.replace([])
67
+ #
68
+ # process conditional blocks, if and elsif(s)
69
+ any_true = eval_conditional_blocks(env)
70
+ #
71
+ # clear the blocks for any remaining conditions
72
+ @conditions.map { |cond| cond[:block].replace([]) }
73
+ return unless @else_block
74
+ #
75
+ # apply the else clause if none of the conditions were true
76
+ if any_true
77
+ @else_block.replace([])
78
+ elsif @else_block
79
+ @else_block.replace(@else_block.process(env).reverse)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def eval_conditional_blocks(env)
86
+ #
87
+ # evaluate each expression until a true one is found, false blocks
88
+ # are cleared from the document.
89
+ until @conditions.empty?
90
+ condition = @conditions.shift
91
+ conditon_expr = condition[:condition_expr]
92
+ predicate = condition[:predicate]
93
+ block = condition[:block]
94
+ #
95
+ # fetch value optionally calling a predicate method
96
+ value = conditon_expr.evaluate(env.context)
97
+ value = value.public_send(predicate) if predicate
98
+ #
99
+ if truthy?(value)
100
+ block.replace(block.process(env).reverse)
101
+ break true
102
+ else
103
+ block.replace([])
104
+ end
63
105
  end
64
106
  end
65
107
 
66
108
  def truthy?(value)
67
109
  case value
68
- when Array;
110
+ when Array
69
111
  !value.empty?
70
112
  else
71
- !!value
113
+ value ? true : false
72
114
  end
73
115
  end
74
116
  end
@@ -2,8 +2,13 @@ module Sablon
2
2
  module Parser
3
3
  class MailMerge
4
4
  class MergeField
5
+ attr_accessor :block_reference_count
5
6
  KEY_PATTERN = /^\s*MERGEFIELD\s+([^ ]+)\s+\\\*\s+MERGEFORMAT\s*$/
6
7
 
8
+ def initialize
9
+ @block_reference_count = 0
10
+ end
11
+
7
12
  def valid?
8
13
  expression
9
14
  end
@@ -14,6 +19,16 @@ module Sablon
14
19
 
15
20
  private
16
21
 
22
+ # removes all nodes associated with the merge field if the reference
23
+ # count is less than or equal to 1
24
+ def remove_or_decrement_ref(*nodes)
25
+ if @block_reference_count > 1
26
+ @block_reference_count -= 1
27
+ else
28
+ nodes.each(&:remove)
29
+ end
30
+ end
31
+
17
32
  def replace_field_display(node, content, env)
18
33
  paragraph = node.ancestors(".//w:p").first
19
34
  display_node = get_display_node(node)
@@ -28,6 +43,7 @@ module Sablon
28
43
 
29
44
  class ComplexField < MergeField
30
45
  def initialize(nodes)
46
+ super()
31
47
  @nodes = nodes
32
48
  @raw_expression = @nodes.flat_map {|n| n.search(".//w:instrText").map(&:content) }.join
33
49
  end
@@ -41,8 +57,14 @@ module Sablon
41
57
  (@nodes - [pattern_node]).each(&:remove)
42
58
  end
43
59
 
60
+ # removes only the merge field in question
44
61
  def remove
45
- @nodes.each(&:remove)
62
+ remove_or_decrement_ref(*@nodes)
63
+ end
64
+
65
+ def remove_parent(selector)
66
+ node = @nodes.first
67
+ remove_or_decrement_ref(node.ancestors(selector).first)
46
68
  end
47
69
 
48
70
  def ancestors(*args)
@@ -58,6 +80,7 @@ module Sablon
58
80
  end
59
81
 
60
82
  private
83
+
61
84
  def pattern_node
62
85
  separate_node.next_element
63
86
  end
@@ -69,6 +92,7 @@ module Sablon
69
92
 
70
93
  class SimpleField < MergeField
71
94
  def initialize(node)
95
+ super()
72
96
  @node = node
73
97
  @raw_expression = @node["w:instr"]
74
98
  end
@@ -79,8 +103,14 @@ module Sablon
79
103
  @node.replace(@node.children)
80
104
  end
81
105
 
106
+ # removes only the merge field in question
82
107
  def remove
83
- @node.remove
108
+ remove_or_decrement_ref(@node)
109
+ end
110
+
111
+ # removes the parent node containing the merge field
112
+ def remove_parent(selector)
113
+ remove_or_decrement_ref(@node.ancestors(selector).first)
84
114
  end
85
115
 
86
116
  def ancestors(*args)
@@ -93,6 +123,7 @@ module Sablon
93
123
  alias_method :end_node, :start_node
94
124
 
95
125
  private
126
+
96
127
  def remove_extra_runs!
97
128
  @node.search(".//w:r")[1..-1].each(&:remove)
98
129
  end
@@ -1,7 +1,61 @@
1
- # -*- coding: utf-8 -*-
1
+ require 'sablon/processor/document/blocks'
2
+ require 'sablon/processor/document/field_handlers'
3
+ require 'sablon/processor/document/operation_construction'
4
+
2
5
  module Sablon
3
6
  module Processor
7
+ # This class manages processing of the XML portions of a word document
8
+ # that can contain mailmerge fields
4
9
  class Document
10
+ class << self
11
+ # Adds a new handler to the OperationConstruction class. The handler
12
+ # passed in should be an instance of the Handler class or implement
13
+ # the same interface. Handlers cannot be replaced by this method,
14
+ # instead the `replace_field_handler` method should be used which
15
+ # internally removes the existing hander and registers the one passed
16
+ # in. The name 'default' is special and will be called if no other
17
+ # handlers can use the provided field.
18
+ def register_field_handler(name, handler)
19
+ name = name.to_sym
20
+ if field_handlers[name] || (name == :default && !default_field_handler.nil?)
21
+ msg = "Handler named: '#{name}' already exists. Use `replace_field_handler` instead."
22
+ raise ArgumentError, msg
23
+ end
24
+ #
25
+ if name == :default
26
+ @default_field_handler = handler
27
+ else
28
+ field_handlers[name] = handler
29
+ end
30
+ end
31
+
32
+ # Removes a handler from the hash and returns it
33
+ def remove_field_handler(name)
34
+ name = name.to_sym
35
+ if name == :default
36
+ handler = @default_field_handler
37
+ @default_field_handler = nil
38
+ handler
39
+ else
40
+ field_handlers.delete(name)
41
+ end
42
+ end
43
+
44
+ # Replaces an existing handler
45
+ def replace_field_handler(name, handler)
46
+ remove_field_handler(name)
47
+ register_field_handler(name, handler)
48
+ end
49
+
50
+ def field_handlers
51
+ @field_handlers ||= {}
52
+ end
53
+
54
+ def default_field_handler
55
+ @default_field_handler ||= nil
56
+ end
57
+ end
58
+
5
59
  def self.process(xml_node, env)
6
60
  processor = new(parser)
7
61
  processor.manipulate xml_node, env
@@ -27,7 +81,9 @@ module Sablon
27
81
  private
28
82
 
29
83
  def build_operations(fields)
30
- OperationConstruction.new(fields).operations
84
+ OperationConstruction.new(fields,
85
+ self.class.field_handlers.values,
86
+ self.class.default_field_handler).operations
31
87
  end
32
88
 
33
89
  def cleanup(xml_node)
@@ -35,199 +91,19 @@ module Sablon
35
91
  end
36
92
 
37
93
  def fill_empty_table_cells(xml_node)
38
- xml_node.xpath("//w:tc[count(*[name() = 'w:p'])=0 or not(*)]").each do |blank_cell|
39
- filler = Nokogiri::XML::Node.new("w:p", xml_node.document)
94
+ selector = "//w:tc[count(*[name() = 'w:p'])=0 or not(*)]"
95
+ xml_node.xpath(selector).each do |blank_cell|
96
+ filler = Nokogiri::XML::Node.new('w:p', xml_node.document)
40
97
  blank_cell.add_child filler
41
98
  end
42
99
  end
43
100
 
44
- class Block < Struct.new(:start_field, :end_field)
45
- def self.enclosed_by(start_field, end_field)
46
- @blocks ||= [ImageBlock, RowBlock, ParagraphBlock, InlineParagraphBlock]
47
- block_class = @blocks.detect { |klass| klass.encloses?(start_field, end_field) }
48
- block_class.new start_field, end_field
49
- end
50
-
51
- def process(env)
52
- replaced_node = Nokogiri::XML::Node.new("tmp", start_node.document)
53
- replaced_node.children = Nokogiri::XML::NodeSet.new(start_node.document, body.map(&:dup))
54
- Processor::Document.process replaced_node, env
55
- replaced_node.children
56
- end
57
-
58
- def replace(content)
59
- content.each { |n| start_node.add_next_sibling n }
60
- remove_control_elements
61
- end
62
-
63
- def remove_control_elements
64
- body.each(&:remove)
65
- start_node.remove
66
- end_node.remove
67
- end
68
-
69
- def body
70
- return @body if defined?(@body)
71
- @body = []
72
- node = start_node
73
- while (node = node.next_element) && node != end_node
74
- @body << node
75
- end
76
- @body
77
- end
78
-
79
- def start_node
80
- @start_node ||= self.class.parent(start_field).first
81
- end
82
-
83
- def end_node
84
- @end_node ||= self.class.parent(end_field).first
85
- end
86
-
87
- def self.encloses?(start_field, end_field)
88
- parent(start_field).any? && parent(end_field).any?
89
- end
90
- end
91
-
92
- class RowBlock < Block
93
- def self.parent(node)
94
- node.ancestors ".//w:tr"
95
- end
96
-
97
- def self.encloses?(start_field, end_field)
98
- super && parent(start_field) != parent(end_field)
99
- end
100
- end
101
-
102
- class ParagraphBlock < Block
103
- def self.parent(node)
104
- node.ancestors ".//w:p"
105
- end
106
-
107
- def self.encloses?(start_field, end_field)
108
- super && parent(start_field) != parent(end_field)
109
- end
110
- end
111
-
112
- class ImageBlock < ParagraphBlock
113
- def self.parent(node)
114
- node.ancestors(".//w:p").first
115
- end
116
-
117
- def self.encloses?(start_field, end_field)
118
- start_field.expression.start_with?('@')
119
- end
120
-
121
- def replace(image)
122
- #
123
- if image
124
- nodes_between_fields.each do |node|
125
- pic_prop = node.at_xpath('.//pic:cNvPr', pic: 'http://schemas.openxmlformats.org/drawingml/2006/picture')
126
- pic_prop.attributes['name'].value = image.name if pic_prop
127
- blip = node.at_xpath('.//a:blip', a: 'http://schemas.openxmlformats.org/drawingml/2006/main')
128
- blip.attributes['embed'].value = image.local_rid if blip
129
- end
130
- end
131
- #
132
- start_field.remove
133
- end_field.remove
134
- end
135
-
136
- private
137
-
138
- # Collects all nodes between the two nodes provided into an array.
139
- # Each entry in the array should be a paragraph tag.
140
- # https://stackoverflow.com/a/820776
141
- def nodes_between_fields
142
- first = self.class.parent(start_field)
143
- last = self.class.parent(end_field)
144
- #
145
- result = [first]
146
- until first == last
147
- first = first.next
148
- result << first
149
- end
150
- result
151
- end
152
- end
153
-
154
- class InlineParagraphBlock < Block
155
- def self.parent(node)
156
- node.ancestors ".//w:p"
157
- end
158
-
159
- def remove_control_elements
160
- body.each(&:remove)
161
- start_field.remove
162
- end_field.remove
163
- end
164
-
165
- def start_node
166
- @start_node ||= start_field.end_node
167
- end
168
-
169
- def end_node
170
- @end_node ||= end_field.start_node
171
- end
172
-
173
- def self.encloses?(start_field, end_field)
174
- super && parent(start_field) == parent(end_field)
175
- end
176
- end
177
-
178
- class OperationConstruction
179
- def initialize(fields)
180
- @fields = fields
181
- @operations = []
182
- end
183
-
184
- def operations
185
- while @fields.any?
186
- @operations << consume(true)
187
- end
188
- @operations.compact
189
- end
190
-
191
- def consume(allow_insertion)
192
- @field = @fields.shift
193
- return unless @field
194
- case @field.expression
195
- when /^=/
196
- if allow_insertion
197
- Statement::Insertion.new(Expression.parse(@field.expression[1..-1]), @field)
198
- end
199
- when /([^ ]+):each\(([^ ]+)\)/
200
- block = consume_block("#{$1}:endEach")
201
- Statement::Loop.new(Expression.parse($1), $2, block)
202
- when /([^ ]+):if\(([^)]+)\)/
203
- block = consume_block("#{$1}:endIf")
204
- Statement::Condition.new(Expression.parse($1), block, $2)
205
- when /([^ ]+):if/
206
- block = consume_block("#{$1}:endIf")
207
- Statement::Condition.new(Expression.parse($1), block)
208
- when /^@([^ ]+):start/
209
- block = consume_block("@#{$1}:end")
210
- Statement::Image.new(Expression.parse($1), block)
211
- when /^comment$/
212
- block = consume_block("endComment")
213
- Statement::Comment.new(block)
214
- end
215
- end
216
-
217
- def consume_block(end_expression)
218
- start_field = end_field = @field
219
- while end_field && end_field.expression != end_expression
220
- consume(false)
221
- end_field = @field
222
- end
223
-
224
- if end_field
225
- Block.enclosed_by start_field, end_field
226
- else
227
- raise TemplateError, "Could not find end field for «#{start_field.expression}». Was looking for «#{end_expression}»"
228
- end
229
- end
230
- end
101
+ # register "builtin" handlers
102
+ register_field_handler :insertion, InsertionHandler.new
103
+ register_field_handler :each_loop, EachLoopHandler.new
104
+ register_field_handler :conditional, ConditionalHandler.new
105
+ register_field_handler :image, ImageHandler.new
106
+ register_field_handler :comment, CommentHandler.new
231
107
  end
232
108
  end
233
109
  end