sablon 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,134 @@
1
+ module Sablon
2
+ module Processor
3
+ class Document
4
+ class Block
5
+ attr_accessor :start_field, :end_field
6
+
7
+ def self.enclosed_by(start_field, end_field)
8
+ @blocks ||= [ImageBlock, RowBlock, ParagraphBlock, InlineParagraphBlock]
9
+ block_class = @blocks.detect { |klass| klass.encloses?(start_field, end_field) }
10
+ block_class.new start_field, end_field
11
+ end
12
+
13
+ def self.encloses?(start_field, end_field)
14
+ parent(start_field) && parent(end_field)
15
+ end
16
+
17
+ def self.parent(node)
18
+ node.ancestors(parent_selector).first
19
+ end
20
+
21
+ def self.parent_selector
22
+ './/w:p'
23
+ end
24
+
25
+ def initialize(start_field, end_field)
26
+ @start_field = start_field
27
+ @end_field = end_field
28
+
29
+ # update reference counts for control fields
30
+ @start_field.block_reference_count += 1
31
+ @end_field.block_reference_count += 1
32
+ end
33
+
34
+ def process(env)
35
+ replaced_node = Nokogiri::XML::Node.new("tmp", start_node.document)
36
+ replaced_node.children = Nokogiri::XML::NodeSet.new(start_node.document, body.map(&:dup))
37
+ Processor::Document.process replaced_node, env
38
+ replaced_node.children
39
+ end
40
+
41
+ def replace(content)
42
+ content.each { |n| start_node.add_next_sibling n }
43
+ remove_control_elements
44
+ end
45
+
46
+ def remove_control_elements
47
+ body.each(&:remove)
48
+ # we only want to remove the start and end nodes if they belong
49
+ # to a single block.
50
+ start_field.remove_parent(self.class.parent_selector)
51
+ end_field.remove_parent(self.class.parent_selector)
52
+ end
53
+
54
+ def body
55
+ return @body if defined?(@body)
56
+ @body = []
57
+ node = start_node
58
+ while (node = node.next_element) && node != end_node
59
+ @body << node
60
+ end
61
+ @body
62
+ end
63
+
64
+ def start_node
65
+ @start_node ||= self.class.parent(start_field)
66
+ end
67
+
68
+ def end_node
69
+ @end_node ||= self.class.parent(end_field)
70
+ end
71
+ end
72
+
73
+ class RowBlock < Block
74
+ def self.parent_selector
75
+ './/w:tr'
76
+ end
77
+
78
+ def self.encloses?(start_field, end_field)
79
+ super && parent(start_field) != parent(end_field)
80
+ end
81
+ end
82
+
83
+ class ParagraphBlock < Block
84
+ def self.encloses?(start_field, end_field)
85
+ super && parent(start_field) != parent(end_field)
86
+ end
87
+ end
88
+
89
+ class ImageBlock < ParagraphBlock
90
+ def self.encloses?(start_field, end_field)
91
+ start_field.expression.start_with?('@')
92
+ end
93
+
94
+ def replace(image)
95
+ # we need to include the start and end nodes incase the image is
96
+ # inline with the merge fields
97
+ nodes = [start_node] + body + [end_node]
98
+ #
99
+ if image
100
+ nodes.each do |node|
101
+ pic_prop = node.at_xpath('.//pic:cNvPr', pic: 'http://schemas.openxmlformats.org/drawingml/2006/picture')
102
+ pic_prop.attributes['name'].value = image.name if pic_prop
103
+ blip = node.at_xpath('.//a:blip', a: 'http://schemas.openxmlformats.org/drawingml/2006/main')
104
+ blip.attributes['embed'].value = image.local_rid if blip
105
+ end
106
+ end
107
+ #
108
+ start_field.remove
109
+ end_field.remove
110
+ end
111
+ end
112
+
113
+ class InlineParagraphBlock < Block
114
+ def self.encloses?(start_field, end_field)
115
+ super && parent(start_field) == parent(end_field)
116
+ end
117
+
118
+ def remove_control_elements
119
+ body.each(&:remove)
120
+ start_field.remove
121
+ end_field.remove
122
+ end
123
+
124
+ def start_node
125
+ @start_node ||= start_field.end_node
126
+ end
127
+
128
+ def end_node
129
+ @end_node ||= end_field.start_node
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,117 @@
1
+ module Sablon
2
+ module Processor
3
+ class Document
4
+ # This class is used to setup field handlers to process different
5
+ # merge field expressions based on the expression text. The #handles?
6
+ # and #build_statement methods form the standard FieldHandler API and can
7
+ # be implemented however they are needed to be as long as the call
8
+ # signature stays the same.
9
+ class FieldHandler
10
+ # Used when registering processors. The pattern tells the handler
11
+ # what expression text to search for.
12
+ def initialize(pattern)
13
+ @pattern = pattern
14
+ end
15
+
16
+ # Returns a non-nil value if the field expression matches the pattern
17
+ def handles?(field)
18
+ field.expression.match(@pattern)
19
+ end
20
+
21
+ # Uses the provided arguments to construct a Statement object.
22
+ # The constructor is an instance of the OperationConstruction class,
23
+ # the field is the current merge field being evaluated and the options
24
+ # hash defines any other parameters passed in during
25
+ # OperationConstruction#consume. Currently the only option passed is
26
+ # `:allow_insertion`.
27
+ def build_statement(constructor, field, options = {}); end
28
+ end
29
+
30
+ # Handles simple text insertion
31
+ class InsertionHandler < FieldHandler
32
+ def initialize
33
+ super(/^=/)
34
+ end
35
+
36
+ def build_statement(_constructor, field, options = {})
37
+ return unless options[:allow_insertion]
38
+ #
39
+ expr = Expression.parse(field.expression.gsub(/^=/, ''))
40
+ Statement::Insertion.new(expr, field)
41
+ end
42
+ end
43
+
44
+ # Handles each loops in the template
45
+ class EachLoopHandler < FieldHandler
46
+ def initialize
47
+ super(/([^ ]+):each\(([^ ]+)\)/)
48
+ end
49
+
50
+ def build_statement(constructor, field, _options = {})
51
+ expr_name, item_name = field.expression.match(@pattern).to_a[1..2]
52
+ block = constructor.consume_block("#{expr_name}:endEach")
53
+ Statement::Loop.new(Expression.parse(expr_name), item_name, block)
54
+ end
55
+ end
56
+
57
+ # Handles conditional blocks in the template
58
+ class ConditionalHandler < FieldHandler
59
+ def initialize
60
+ super(/([^ ]+):if(?:\(([^)]+)\))?/)
61
+ end
62
+
63
+ def build_statement(constructor, field, _options = {})
64
+ expr_name = field.expression.match(@pattern).to_a[1]
65
+ args = [
66
+ # end expression (first arg)
67
+ "#{expr_name}:endIf",
68
+ # sub block patterns to check for
69
+ /(#{expr_name}):els[iI]f(?:\(([^)]+)\))?/,
70
+ /(#{expr_name}):else/
71
+ ]
72
+ blocks = process_blocks(constructor.consume_multi_block(*args))
73
+ Statement::Condition.new(blocks)
74
+ end
75
+
76
+ private
77
+
78
+ # Processes the main expression from each start field block to
79
+ # simplify usage in Statement
80
+ def process_blocks(blocks)
81
+ blocks.map do |block|
82
+ pattern = /([^ ]+):(?:if|els[iI]f|else)(?:\(([^)]+)\))?/
83
+ expr, pred = block.start_field.expression.match(pattern).to_a[1..2]
84
+ expr = Expression.parse(expr)
85
+ #
86
+ { condition_expr: expr, predicate: pred, block: block }
87
+ end
88
+ end
89
+ end
90
+
91
+ # Handles image insertion fields
92
+ class ImageHandler < FieldHandler
93
+ def initialize
94
+ super(/^@([^ ]+):start/)
95
+ end
96
+
97
+ def build_statement(constructor, field, _options = {})
98
+ expr_name = field.expression.match(@pattern).to_a[1]
99
+ block = constructor.consume_block("@#{expr_name}:end")
100
+ Statement::Image.new(Expression.parse(expr_name), block)
101
+ end
102
+ end
103
+
104
+ # Handles comment blocks in the template
105
+ class CommentHandler < FieldHandler
106
+ def initialize
107
+ super(/^comment$/)
108
+ end
109
+
110
+ def build_statement(constructor, _field, _options = {})
111
+ block = constructor.consume_block('endComment')
112
+ Statement::Comment.new(block)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,71 @@
1
+ module Sablon
2
+ module Processor
3
+ class Document
4
+ class OperationConstruction
5
+ def initialize(fields, field_handlers, default_handler)
6
+ @fields = fields
7
+ @field_handlers = field_handlers
8
+ @default_handler = default_handler
9
+ @operations = []
10
+ end
11
+
12
+ def operations
13
+ @operations << consume(true) while @fields.any?
14
+ @operations.compact
15
+ end
16
+
17
+ def consume(allow_insertion)
18
+ return unless (@field = @fields.shift)
19
+ #
20
+ # step over provided handlers to see if any can process the field
21
+ handler = @field_handlers.detect(proc { @default_handler }) do |fh|
22
+ fh.handles?(@field)
23
+ end
24
+ return if handler.nil?
25
+ #
26
+ # process and return
27
+ handler.build_statement(self, @field, allow_insertion: allow_insertion)
28
+ end
29
+
30
+ def consume_block(end_expression)
31
+ start_field = end_field = @field
32
+ while end_field && end_field.expression != end_expression
33
+ consume(false)
34
+ end_field = @field
35
+ end
36
+
37
+ unless end_field
38
+ raise TemplateError, "Could not find end field for «#{start_field.expression}». Was looking for «#{end_expression}»"
39
+ end
40
+ Block.enclosed_by(start_field, end_field) if end_field
41
+ end
42
+
43
+ # Creates multiple blocks based on the sub expression patterns supplied
44
+ # while searching for the end expresion. The start and end fields
45
+ # of adjacent blocks are shared. For example in an if-else-endif
46
+ # block the else field is the end for the if clause block and the
47
+ # start of the else clause block.
48
+ def consume_multi_block(end_expression, *sub_expr_patterns)
49
+ start_field = end_field = @field
50
+ blocks = []
51
+ while end_field && end_field.expression != end_expression
52
+ consume(false)
53
+ break unless (end_field = @field)
54
+ if sub_expr_patterns.any? { |pat| end_field.expression =~ pat }
55
+ blocks << Block.enclosed_by(start_field, end_field)
56
+ start_field = end_field
57
+ end
58
+ end
59
+
60
+ # raise error if no final end field
61
+ unless end_field
62
+ raise TemplateError, "Could not find end field for «#{start_field.expression}». Was looking for «#{end_expression}»"
63
+ end
64
+
65
+ # add final block and return
66
+ blocks << Block.enclosed_by(start_field, end_field)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,3 +1,3 @@
1
1
  module Sablon
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,106 @@
1
+ require 'test_helper'
2
+
3
+ class SablonCustomFieldHandlerTest < Sablon::TestCase
4
+ #
5
+ # This class supports more advanced conditional expressions and crafted
6
+ # by @moritzgloeckl in PR #73
7
+ class OperatorCondition < Sablon::Statement::Condition
8
+ def eval_conditional_blocks(env)
9
+ #
10
+ # evaluate each expression until a true one is found, false blocks
11
+ # are cleared from the document.
12
+ until @conditions.empty?
13
+ condition = @conditions.shift
14
+ conditon_expr = condition[:condition_expr]
15
+ predicate = condition[:predicate]
16
+ block = condition[:block]
17
+ #
18
+ # determine valeu of conditional expression + predicate
19
+ value = eval_condition_expr(conditon_expr, predicate, env.context)
20
+ #
21
+ # manipulate block based on truthy-ness of value
22
+ if truthy?(value)
23
+ block.replace(block.process(env).reverse)
24
+ break true
25
+ else
26
+ block.replace([])
27
+ end
28
+ end
29
+ end
30
+
31
+ def eval_condition_expr(conditon_expr, predicate, context)
32
+ value = conditon_expr.evaluate(context)
33
+ #
34
+ if predicate.to_s =~ /^[!=]=/
35
+ operator = predicate[0..1]
36
+ cmpr_val = predicate[2..-1].tr("'", '')
37
+ compare_values(value.to_s, cmpr_val, operator)
38
+ elsif predicate
39
+ value.public_send(predicate)
40
+ else
41
+ value
42
+ end
43
+ end
44
+
45
+ def compare_values(value_a, value_b, operator)
46
+ case operator
47
+ when '!='
48
+ value_a != value_b
49
+ when '=='
50
+ value_a == value_b
51
+ end
52
+ end
53
+ end
54
+
55
+ # Handles conditional blocks in the template that use an operator
56
+ class OperatorConditionalHandler < Sablon::Processor::Document::ConditionalHandler
57
+ def build_statement(constructor, field, _options = {})
58
+ expr_name = field.expression.match(@pattern).to_a[1]
59
+ args = [
60
+ # end expression (first arg)
61
+ "#{expr_name}:endIf",
62
+ # sub block patterns to check for
63
+ /(#{expr_name}):els[iI]f(?:\(([^)]+)\))?/,
64
+ /(#{expr_name}):else/
65
+ ]
66
+ blocks = process_blocks(constructor.consume_multi_block(*args))
67
+ OperatorCondition.new(blocks)
68
+ end
69
+ end
70
+
71
+ def setup
72
+ super
73
+ @base_path = Pathname.new(File.expand_path('../', __FILE__))
74
+ @template_path = @base_path + 'fixtures/custom_field_handlers_template.docx'
75
+ @output_path = @base_path + 'sandbox/custom_field_handlers.docx'
76
+ @sample_path = @base_path + 'fixtures/custom_field_handlers_sample.docx'
77
+ #
78
+ # register new handlers to allow insertion without equals sign and
79
+ # advanced conditionals
80
+ klass = Sablon::Processor::Document
81
+ @orig_conditional_handler = klass.remove_field_handler :conditional
82
+ klass.register_field_handler :default, klass.field_handlers[:insertion]
83
+ klass.register_field_handler :conditional, OperatorConditionalHandler.new
84
+ end
85
+
86
+ def teardown
87
+ # remove extra handlers
88
+ Sablon::Processor::Document.remove_field_handler :default
89
+ Sablon::Processor::Document.replace_field_handler :conditional, @orig_conditional_handler
90
+ end
91
+
92
+ def test_generate_document_from_template
93
+ template = Sablon.template @template_path
94
+ context = {
95
+ normal_field: 'success1',
96
+ no_leading_equals: 'success2',
97
+ inside_if_no_op: 'success3',
98
+ no_operator: OpenStruct.new(test: 'success4'),
99
+ equals_operator: 'test',
100
+ inside_if_equals_op: 'success5'
101
+ }
102
+ #
103
+ template.render_to_file @output_path, context
104
+ assert_docx_equal @sample_path, @output_path
105
+ end
106
+ end
@@ -0,0 +1,40 @@
1
+ <w:p>
2
+ <w:r><w:t>Before</w:t></w:r>
3
+ <w:r><w:t xml:space="preserve"> </w:t></w:r>
4
+ <w:fldSimple w:instr=" MERGEFIELD object:if(method_a) \* MERGEFORMAT ">
5
+ <w:r>
6
+ <w:rPr><w:noProof/></w:rPr>
7
+ <w:t>«object:if(method_a)»</w:t>
8
+ </w:r>
9
+ </w:fldSimple>
10
+ <w:r>
11
+ <w:t>Method A was true</w:t>
12
+ </w:r>
13
+ <w:fldSimple w:instr=" MERGEFIELD object:elsif(method_b) \* MERGEFORMAT ">
14
+ <w:r>
15
+ <w:rPr><w:noProof/></w:rPr>
16
+ <w:t>«object:elsif(method_b)»</w:t>
17
+ </w:r>
18
+ </w:fldSimple>
19
+ <w:r>
20
+ <w:t>Method B was true</w:t>
21
+ </w:r>
22
+ <w:fldSimple w:instr=" MERGEFIELD object:else \* MERGEFORMAT ">
23
+ <w:r>
24
+ <w:rPr><w:noProof/></w:rPr>
25
+ <w:t>«object:else»</w:t>
26
+ </w:r>
27
+ </w:fldSimple>
28
+ <w:r>
29
+ <w:t>Method A and B were false</w:t>
30
+ </w:r>
31
+ <w:fldSimple w:instr=" MERGEFIELD object:endIf \* MERGEFORMAT ">
32
+ <w:r>
33
+ <w:rPr><w:noProof/></w:rPr>
34
+ <w:t>object:endIf»</w:t>
35
+ </w:r>
36
+ </w:fldSimple>
37
+ <w:r>
38
+ <w:t>After</w:t>
39
+ </w:r>
40
+ </w:p>