sablon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 975fe24c74bf896987909e597c4600087b4aee50
4
+ data.tar.gz: c884b29cb782ed4e30125e3020e80f52af34a92e
5
+ SHA512:
6
+ metadata.gz: aec7e1fa1c03628473f4ea9122ea44c64dedf6041d62a891e21267f12456cca58751cb43be982b588937bd8051ed4358e56ab97cebff85127f19f089a28e058a
7
+ data.tar.gz: 024a467e6a943623413deb38b2e2422a234b53d24ba1b7985778ba6d38ae66443af6df6c52321ec5ae5312c8eb195023701def63f40d7e027fa0dcb90f5dbf5f
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+
16
+ /test/sandbox/*
17
+ !/test/sandbox/.gitkeep
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sablon.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Yves Senn
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Sablon
2
+
3
+ [![Build Status](https://travis-ci.org/senny/sablon.svg?branch=master)](https://travis-ci.org/senny/sablon)
4
+
5
+ Is a document template processor based on `docx`. Tags are represented using
6
+ MailMerge fields.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'sablon'
14
+ ```
15
+
16
+
17
+ ## Usage
18
+
19
+ ```ruby
20
+ require "sablon"
21
+ template = Sablon.template(File.expand_path("~/Desktop/template.docx"))
22
+ context = {
23
+ title: "Fabulous Document",
24
+ technologies: ["Ruby", "Markdown", "ODF"]
25
+ }
26
+ template.render_to_file File.expand_path("~/Desktop/output.docx"), context
27
+ ```
28
+
29
+
30
+ ### Writing Templates
31
+
32
+ Sablon templates are normal word documents (`.docx`) sprinkled with MergeFields
33
+ to perform operations. The following section will use the notation `«=title»` to
34
+ refer to [Word MailMerge](http://en.wikipedia.org/wiki/Mail_merge) fields.
35
+
36
+ #### Inserting content
37
+
38
+ The most basic operation is to insert content. The contents of a context
39
+ variable can be inserted using a field like:
40
+
41
+ ```
42
+ «=title»
43
+ ```
44
+
45
+ It's also possible to call a method on a context object using:
46
+
47
+ ```
48
+ «=post.title»
49
+ ```
50
+
51
+
52
+ #### Conditionals
53
+
54
+ Sablon can render parts of the template conditonally based on the value of a
55
+ context variable. Conditional fields are inserted around the content.
56
+
57
+ ```
58
+ «technologies:if»
59
+ ... arbitrary document markup ...
60
+ «technologies:endIf»
61
+ ```
62
+
63
+ 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
+
66
+
67
+ #### Loops
68
+
69
+ Loops repeat parts of the document.
70
+
71
+ ```
72
+ «technologies:each(technology)»
73
+ ... arbitrary document markup ...
74
+ ... use `technology` to refer to the current item ...
75
+ «technologies:endEach»
76
+ ```
77
+
78
+ Loops can be used to repeat table rows or list enumerations. The fields need to
79
+ be placed in within table cells or enumeration items enclosing the rows or items
80
+ to repeat. Have a look at the
81
+ [example template](test/fixtures/sablon_sample.docx) for more details.
82
+
83
+
84
+ #### Nesting
85
+
86
+ It is possible to nest loops and conditionals.
87
+
88
+
89
+ ### Examples
90
+
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).
94
+
95
+
96
+ ## Contributing
97
+
98
+ 1. Fork it ( https://github.com/[my-github-username]/sablon/fork )
99
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
100
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
101
+ 4. Push to the branch (`git push origin my-new-feature`)
102
+ 5. Create a new Pull Request
103
+
104
+ ## Inspiration
105
+
106
+ The following projects address a similar goal and inspired the work on Sablon:
107
+
108
+ * [ruby-docx-templater](https://github.com/jawspeak/ruby-docx-templater)
109
+ * [docx_mailmerge](https://github.com/annaswims/docx_mailmerge)
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = FileList['test/**/*_test.rb']
6
+ t.libs.push 'test'
7
+ t.verbose = true
8
+ end
9
+
10
+ task default: :test
data/lib/sablon.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "sablon/version"
2
+ require "sablon/template"
3
+ require "sablon/processor"
4
+ require "sablon/processor/section_properties"
5
+ require "sablon/parser/mail_merge"
6
+ require "sablon/operations"
7
+ require 'zip'
8
+ require 'nokogiri'
9
+
10
+ module Sablon
11
+ def self.template(path)
12
+ Template.new(path)
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ module Sablon
2
+ module Statement
3
+ class Insertion < Struct.new(:expr, :field)
4
+ def evaluate(context)
5
+ field.replace(expr.evaluate(context))
6
+ end
7
+ end
8
+
9
+ class Loop < Struct.new(:list_expr, :iterator_name, :block)
10
+ def evaluate(context)
11
+ content = list_expr.evaluate(context).flat_map do |item|
12
+ iteration_context = context.merge(iterator_name => item)
13
+ block.process(iteration_context)
14
+ end
15
+ block.replace(content.reverse)
16
+ end
17
+ end
18
+
19
+ class Condition < Struct.new(:conditon_expr, :block)
20
+ def evaluate(context)
21
+ if truthy?(conditon_expr.evaluate(context))
22
+ block.replace(block.process(context).reverse)
23
+ else
24
+ block.replace([])
25
+ end
26
+ end
27
+
28
+ def truthy?(value)
29
+ case value
30
+ when Array;
31
+ !value.empty?
32
+ else
33
+ !!value
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module Expression
40
+ class Variable < Struct.new(:name)
41
+ def evaluate(context)
42
+ context[name]
43
+ end
44
+ end
45
+
46
+ class SimpleMethodCall < Struct.new(:receiver, :method)
47
+ def evaluate(context)
48
+ receiver.evaluate(context).public_send method
49
+ end
50
+ end
51
+
52
+ def self.parse(expression)
53
+ if expression.include?(".")
54
+ parts = expression.split(".")
55
+ SimpleMethodCall.new(Variable.new(parts.first), parts.last)
56
+ else
57
+ Variable.new(expression)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,86 @@
1
+ module Sablon
2
+ module Parser
3
+ class MailMerge
4
+ class MergeField
5
+ KEY_PATTERN = /^\s*MERGEFIELD ([^ ]+)\s+\\\* MERGEFORMAT\s*$/
6
+ def expression
7
+ $1 if @raw_expression =~ KEY_PATTERN
8
+ end
9
+
10
+ private
11
+ def replace_field_display(node, text)
12
+ display_node = node.search(".//w:t").first
13
+ text.scan(/[^\n]+|\n/).reverse.each do |part|
14
+ if part == "\n"
15
+ display_node.add_next_sibling Nokogiri::XML::Node.new "w:br", display_node.document
16
+ else
17
+ text_part = display_node.dup
18
+ text_part.content = part
19
+ display_node.add_next_sibling text_part
20
+ end
21
+ end
22
+ display_node.remove
23
+ end
24
+ end
25
+
26
+ class ComplexField < MergeField
27
+ def initialize(nodes)
28
+ @nodes = nodes
29
+ @raw_expression = @nodes.flat_map {|n| n.search(".//w:instrText").map(&:content) }.join
30
+ end
31
+
32
+ def replace(value)
33
+ replace_field_display(pattern_node, value)
34
+ (@nodes - [pattern_node]).each(&:remove)
35
+ end
36
+
37
+ def ancestors(*args)
38
+ @nodes.first.ancestors(*args)
39
+ end
40
+
41
+ private
42
+ def pattern_node
43
+ separate_node.next_element
44
+ end
45
+
46
+ def separate_node
47
+ @nodes.detect {|n| !n.search(".//w:fldChar[@w:fldCharType='separate']").empty? }
48
+ end
49
+ end
50
+
51
+ class SimpleField < MergeField
52
+ def initialize(node)
53
+ @node = node
54
+ @raw_expression = @node["w:instr"]
55
+ end
56
+
57
+ def replace(value)
58
+ replace_field_display(@node, value)
59
+ @node.replace(@node.children)
60
+ end
61
+
62
+ def ancestors(*args)
63
+ @node.ancestors(*args)
64
+ end
65
+ end
66
+
67
+ def parse_fields(xml)
68
+ fields = []
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)
80
+ end
81
+ end
82
+ fields
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,135 @@
1
+ module Sablon
2
+ class Processor
3
+ def self.process(xml_node, context, properties = {})
4
+ processor = new(parser)
5
+ processor.manipulate xml_node, context
6
+ processor.write_properties xml_node, properties if properties.any?
7
+ xml_node
8
+ end
9
+
10
+ def self.parser
11
+ @parser ||= Sablon::Parser::MailMerge.new
12
+ end
13
+
14
+ def initialize(parser)
15
+ @parser = parser
16
+ end
17
+
18
+ def manipulate(xml_node, context)
19
+ operations = build_operations(@parser.parse_fields(xml_node))
20
+ operations.each do |step|
21
+ step.evaluate context
22
+ end
23
+ xml_node
24
+ end
25
+
26
+ def write_properties(xml_node, properties)
27
+ if properties.key? :start_page_number
28
+ section_properties = SectionProperties.from_document(xml_node)
29
+ section_properties.start_page_number = properties[:start_page_number]
30
+ end
31
+ end
32
+
33
+ private
34
+ def build_operations(fields)
35
+ OperationConstruction.new(fields).operations
36
+ end
37
+
38
+ class Block < Struct.new(:start_field, :end_field)
39
+ def self.enclosed_by(start_field, end_field)
40
+ @blocks ||= [RowBlock, ParagraphBlock]
41
+ block_class = @blocks.detect { |klass| klass.possible?(start_field) && klass.possible?(end_field) }
42
+ block_class.new start_field, end_field
43
+ end
44
+
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
51
+ end
52
+
53
+ def replace(content)
54
+ content.each { |n| start_node.add_next_sibling n }
55
+
56
+ body.each &:remove
57
+ start_node.remove
58
+ end_node.remove
59
+ end
60
+
61
+ def body
62
+ return @body if defined?(@body)
63
+ @body = []
64
+ node = start_node
65
+ while (node = node.next_element) && node != end_node
66
+ @body << node
67
+ end
68
+ @body
69
+ end
70
+
71
+ def start_node
72
+ @start_node ||= self.class.parent(start_field).first
73
+ end
74
+
75
+ def end_node
76
+ @end_node ||= self.class.parent(end_field).first
77
+ end
78
+
79
+ def self.possible?(node)
80
+ parent(node).any?
81
+ end
82
+ end
83
+
84
+ class RowBlock < Block
85
+ def self.parent(node)
86
+ node.ancestors ".//w:tr"
87
+ end
88
+ end
89
+
90
+ class ParagraphBlock < Block
91
+ def self.parent(node)
92
+ node.ancestors ".//w:p"
93
+ end
94
+ end
95
+
96
+ class OperationConstruction
97
+ def initialize(fields)
98
+ @fields = fields
99
+ @operations = []
100
+ end
101
+
102
+ def operations
103
+ while @fields.any?
104
+ @operations << consume(true)
105
+ end
106
+ @operations.compact
107
+ end
108
+
109
+ def consume(allow_insertion)
110
+ @field = @fields.shift
111
+ case @field.expression
112
+ when /^=/
113
+ if allow_insertion
114
+ Statement::Insertion.new(Expression.parse(@field.expression[1..-1]), @field)
115
+ end
116
+ when /([^ ]+):each\(([^ ]+)\)/
117
+ block = consume_block("#{$1}:endEach")
118
+ Statement::Loop.new(Expression.parse($1), $2, block)
119
+ when /([^ ]+):if/
120
+ block = consume_block("#{$1}:endIf")
121
+ Statement::Condition.new(Expression.parse($1), block)
122
+ end
123
+ end
124
+
125
+ def consume_block(end_expression)
126
+ start_field = end_field = @field
127
+ while end_field && end_field.expression != end_expression
128
+ @operations << consume(false)
129
+ end_field = @field
130
+ end
131
+ Block.enclosed_by start_field, end_field
132
+ end
133
+ end
134
+ end
135
+ end