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.
@@ -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>