sablon 0.2.1 → 0.3.0

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: 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