sablon 0.0.18 → 0.0.19.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -3
- data/Gemfile.lock +5 -5
- data/README.md +33 -1
- data/lib/sablon.rb +8 -3
- data/lib/sablon/content.rb +17 -0
- data/lib/sablon/html/ast.rb +130 -0
- data/lib/sablon/html/converter.rb +133 -0
- data/lib/sablon/html/visitor.rb +26 -0
- data/lib/sablon/numbering.rb +28 -0
- data/lib/sablon/processor/document.rb +193 -0
- data/lib/sablon/processor/numbering.rb +47 -0
- data/lib/sablon/processor/section_properties.rb +1 -1
- data/lib/sablon/template.rb +8 -4
- data/lib/sablon/version.rb +1 -1
- data/test/fixtures/html_sample.docx +0 -0
- data/test/fixtures/insertion_template.docx +0 -0
- data/test/fixtures/insertion_template_no_styles.docx +0 -0
- data/test/html/converter_test.rb +303 -0
- data/test/html_test.rb +45 -0
- data/test/{processor_test.rb → processor/document_test.rb} +2 -2
- data/test/test_helper.rb +4 -0
- metadata +22 -7
- data/lib/sablon/processor.rb +0 -191
data/test/html_test.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "test_helper"
|
3
|
+
require "support/xml_snippets"
|
4
|
+
|
5
|
+
class SablonHTMLTest < Sablon::TestCase
|
6
|
+
include Sablon::Test::Assertions
|
7
|
+
|
8
|
+
def setup
|
9
|
+
super
|
10
|
+
@base_path = Pathname.new(File.expand_path("../", __FILE__))
|
11
|
+
|
12
|
+
@sample_path = @base_path + "fixtures/html_sample.docx"
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_generate_document_from_template_with_styles_and_html
|
16
|
+
template_path = @base_path + "fixtures/insertion_template.docx"
|
17
|
+
output_path = @base_path + "sandbox/html.docx"
|
18
|
+
template = Sablon.template template_path
|
19
|
+
context = {'html:content' => content}
|
20
|
+
template.render_to_file output_path, context
|
21
|
+
|
22
|
+
assert_docx_equal @sample_path, output_path
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_generate_document_from_template_without_styles_and_html
|
26
|
+
template_path = @base_path + "fixtures/insertion_template_no_styles.docx"
|
27
|
+
output_path = @base_path + "sandbox/html_no_styles.docx"
|
28
|
+
template = Sablon.template template_path
|
29
|
+
context = {'html:content' => content}
|
30
|
+
|
31
|
+
e = assert_raises(ArgumentError) do
|
32
|
+
template.render_to_file output_path, context
|
33
|
+
end
|
34
|
+
assert_equal 'Could not find w:abstractNum definition for style: "ListNumber"', e.message
|
35
|
+
|
36
|
+
skip 'implement default styles'
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def content
|
41
|
+
<<-HTML
|
42
|
+
<div>Lorem <strong>ipsum</strong> <em>dolor</em> <strong>sit</strong> <em>amet</em>, <strong>consectetur adipiscing elit</strong>. <em>Suspendisse a tempus turpis</em>. Duis urna justo, vehicula vitae ultricies vel, congue at sem. Fusce turpis turpis, aliquet id pulvinar aliquam, iaculis non elit. Nulla feugiat lectus nulla, in dictum ipsum cursus ac. Quisque at odio neque. Sed ac tortor iaculis, bibendum leo ut, malesuada velit. Donec iaculis sed urna eget pharetra. Praesent ornare fermentum turpis, placerat iaculis urna bibendum vitae. Nunc in quam consequat, tristique tellus in, commodo turpis. Curabitur ullamcorper odio purus, lobortis egestas magna laoreet vitae. Nunc fringilla velit ante, eu aliquam nisi cursus vitae. Suspendisse sit amet dui egestas, volutpat nisi vel, mattis justo. Nullam pellentesque, ipsum eget blandit pharetra, augue elit aliquam mauris, vel mollis nisl augue ut ipsum.</div><ol><li>Vestibulum <ol><li>ante ipsum primis </li></ol></li><li>in faucibus orci luctus <ol><li>et ultrices posuere cubilia Curae; <ol><li>Aliquam vel dolor </li><li>sed sem maximus </li></ol></li><li>fermentum in non odio. <ol><li>Fusce hendrerit ornare mollis. </li></ol></li><li>Nunc scelerisque nibh nec turpis tempor pulvinar. </li></ol></li><li>Donec eros turpis, </li><li>aliquet vel volutpat sit amet, <ol><li>semper eu purus. </li><li>Proin ac erat nec urna efficitur vulputate. <ol><li>Quisque varius convallis ultricies. </li><li>Nullam vel fermentum eros. </li></ol></li></ol></li></ol><div>Pellentesque nulla leo, auctor ornare erat sed, rhoncus congue diam. Duis non porttitor nulla, ut eleifend enim. Pellentesque non tempor sem.</div><div>Mauris auctor egestas arcu, </div><ol><li>id venenatis nibh dignissim id. </li><li>In non placerat metus. </li></ol><ul><li>Nunc sed consequat metus. </li><li>Nulla consectetur lorem consequat, </li><li>malesuada dui at, lacinia lectus. </li></ul><ol><li>Aliquam efficitur </li><li>lorem a mauris feugiat, </li><li>at semper eros pellentesque. </li></ol><div>Nunc lacus diam, consectetur ut odio sit amet, placerat pharetra erat. Sed commodo ut sem id congue. Sed eget neque elit. Curabitur at erat tortor. Maecenas eget sapien vitae est sagittis accumsan et nec orci. Integer luctus at nisl eget venenatis. Nunc nunc eros, consectetur at tortor et, tristique ultrices elit. Nulla in turpis nibh.</div><ul><li>Nam consectetur <ul><li>venenatis tempor. </li></ul></li><li>Aenean <ul><li>blandit<ul><li>porttitor massa, <ul><li>non efficitur <ul><li>metus. </li></ul></li></ul></li></ul></li></ul></li><li>Duis faucibus nunc nec venenatis faucibus. </li><li>Aliquam erat volutpat. </li></ul><div><strong>Quisque non neque ut lacus eleifend volutpat quis sed lacus. Praesent ultrices purus eu quam elementum, sit amet faucibus elit interdum. In lectus orci, elementum quis dictum ac, porta ac ante. Fusce tempus ac mauris id cursus. Phasellus a erat nulla. Mauris dolor orci, malesuada auctor dignissim non, posuere nec odio. Etiam hendrerit justo nec diam ullamcorper, nec blandit elit sodales.</strong></div>
|
43
|
+
HTML
|
44
|
+
end
|
45
|
+
end
|
@@ -3,13 +3,13 @@ require "test_helper"
|
|
3
3
|
require "support/document_xml_helper"
|
4
4
|
require "support/xml_snippets"
|
5
5
|
|
6
|
-
class
|
6
|
+
class ProcessorDocumentTest < Sablon::TestCase
|
7
7
|
include DocumentXMLHelper
|
8
8
|
include XMLSnippets
|
9
9
|
|
10
10
|
def setup
|
11
11
|
super
|
12
|
-
@processor = Sablon::Processor
|
12
|
+
@processor = Sablon::Processor::Document
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_simple_field_replacement
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sablon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.19.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yves Senn
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -129,9 +129,14 @@ files:
|
|
129
129
|
- lib/sablon.rb
|
130
130
|
- lib/sablon/content.rb
|
131
131
|
- lib/sablon/context.rb
|
132
|
+
- lib/sablon/html/ast.rb
|
133
|
+
- lib/sablon/html/converter.rb
|
134
|
+
- lib/sablon/html/visitor.rb
|
135
|
+
- lib/sablon/numbering.rb
|
132
136
|
- lib/sablon/operations.rb
|
133
137
|
- lib/sablon/parser/mail_merge.rb
|
134
|
-
- lib/sablon/processor.rb
|
138
|
+
- lib/sablon/processor/document.rb
|
139
|
+
- lib/sablon/processor/numbering.rb
|
135
140
|
- lib/sablon/processor/section_properties.rb
|
136
141
|
- lib/sablon/redcarpet/render/word_ml.rb
|
137
142
|
- lib/sablon/template.rb
|
@@ -151,6 +156,9 @@ files:
|
|
151
156
|
- test/fixtures/conditionals_template.docx
|
152
157
|
- test/fixtures/cv_sample.docx
|
153
158
|
- test/fixtures/cv_template.docx
|
159
|
+
- test/fixtures/html_sample.docx
|
160
|
+
- test/fixtures/insertion_template.docx
|
161
|
+
- test/fixtures/insertion_template_no_styles.docx
|
154
162
|
- test/fixtures/recipe_context.json
|
155
163
|
- test/fixtures/recipe_sample.docx
|
156
164
|
- test/fixtures/recipe_template.docx
|
@@ -169,8 +177,10 @@ files:
|
|
169
177
|
- test/fixtures/xml/simple_fields.xml
|
170
178
|
- test/fixtures/xml/table_multi_row_loop.xml
|
171
179
|
- test/fixtures/xml/table_row_loop.xml
|
180
|
+
- test/html/converter_test.rb
|
181
|
+
- test/html_test.rb
|
172
182
|
- test/mail_merge_parser_test.rb
|
173
|
-
- test/
|
183
|
+
- test/processor/document_test.rb
|
174
184
|
- test/redcarpet_render_word_ml_test.rb
|
175
185
|
- test/sablon_test.rb
|
176
186
|
- test/sandbox/.gitkeep
|
@@ -193,9 +203,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
193
203
|
version: '0'
|
194
204
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
195
205
|
requirements:
|
196
|
-
- - "
|
206
|
+
- - ">"
|
197
207
|
- !ruby/object:Gem::Version
|
198
|
-
version:
|
208
|
+
version: 1.3.1
|
199
209
|
requirements: []
|
200
210
|
rubyforge_project:
|
201
211
|
rubygems_version: 2.4.5.1
|
@@ -211,6 +221,9 @@ test_files:
|
|
211
221
|
- test/fixtures/conditionals_template.docx
|
212
222
|
- test/fixtures/cv_sample.docx
|
213
223
|
- test/fixtures/cv_template.docx
|
224
|
+
- test/fixtures/html_sample.docx
|
225
|
+
- test/fixtures/insertion_template.docx
|
226
|
+
- test/fixtures/insertion_template_no_styles.docx
|
214
227
|
- test/fixtures/recipe_context.json
|
215
228
|
- test/fixtures/recipe_sample.docx
|
216
229
|
- test/fixtures/recipe_template.docx
|
@@ -229,8 +242,10 @@ test_files:
|
|
229
242
|
- test/fixtures/xml/simple_fields.xml
|
230
243
|
- test/fixtures/xml/table_multi_row_loop.xml
|
231
244
|
- test/fixtures/xml/table_row_loop.xml
|
245
|
+
- test/html/converter_test.rb
|
246
|
+
- test/html_test.rb
|
232
247
|
- test/mail_merge_parser_test.rb
|
233
|
-
- test/
|
248
|
+
- test/processor/document_test.rb
|
234
249
|
- test/redcarpet_render_word_ml_test.rb
|
235
250
|
- test/sablon_test.rb
|
236
251
|
- test/sandbox/.gitkeep
|
data/lib/sablon/processor.rb
DELETED
@@ -1,191 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
module Sablon
|
3
|
-
class Processor
|
4
|
-
def self.process(xml_node, context, properties = {})
|
5
|
-
processor = new(parser)
|
6
|
-
processor.manipulate xml_node, Sablon::Context.transform(context)
|
7
|
-
processor.write_properties xml_node, properties if properties.any?
|
8
|
-
xml_node
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.parser
|
12
|
-
@parser ||= Sablon::Parser::MailMerge.new
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize(parser)
|
16
|
-
@parser = parser
|
17
|
-
end
|
18
|
-
|
19
|
-
def manipulate(xml_node, context)
|
20
|
-
operations = build_operations(@parser.parse_fields(xml_node))
|
21
|
-
operations.each do |step|
|
22
|
-
step.evaluate context
|
23
|
-
end
|
24
|
-
cleanup(xml_node)
|
25
|
-
xml_node
|
26
|
-
end
|
27
|
-
|
28
|
-
def write_properties(xml_node, properties)
|
29
|
-
if start_page_number = properties[:start_page_number] || properties["start_page_number"]
|
30
|
-
section_properties = SectionProperties.from_document(xml_node)
|
31
|
-
section_properties.start_page_number = start_page_number
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
def build_operations(fields)
|
37
|
-
OperationConstruction.new(fields).operations
|
38
|
-
end
|
39
|
-
|
40
|
-
def cleanup(xml_node)
|
41
|
-
fill_empty_table_cells xml_node
|
42
|
-
end
|
43
|
-
|
44
|
-
def fill_empty_table_cells(xml_node)
|
45
|
-
xml_node.xpath("//w:tc[count(*[name() = 'w:p'])=0 or not(*)]").each do |blank_cell|
|
46
|
-
filler = Nokogiri::XML::Node.new("w:p", xml_node.document)
|
47
|
-
blank_cell.add_child filler
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class Block < Struct.new(:start_field, :end_field)
|
52
|
-
def self.enclosed_by(start_field, end_field)
|
53
|
-
@blocks ||= [RowBlock, ParagraphBlock, InlineParagraphBlock]
|
54
|
-
block_class = @blocks.detect { |klass| klass.encloses?(start_field, end_field) }
|
55
|
-
block_class.new start_field, end_field
|
56
|
-
end
|
57
|
-
|
58
|
-
def process(context)
|
59
|
-
replaced_node = Nokogiri::XML::Node.new("tmp", start_node.document)
|
60
|
-
replaced_node.children = Nokogiri::XML::NodeSet.new(start_node.document, body.map(&:dup))
|
61
|
-
Processor.process replaced_node, context
|
62
|
-
replaced_node.children
|
63
|
-
end
|
64
|
-
|
65
|
-
def replace(content)
|
66
|
-
content.each { |n| start_node.add_next_sibling n }
|
67
|
-
remove_control_elements
|
68
|
-
end
|
69
|
-
|
70
|
-
def remove_control_elements
|
71
|
-
body.each &:remove
|
72
|
-
start_node.remove
|
73
|
-
end_node.remove
|
74
|
-
end
|
75
|
-
|
76
|
-
def body
|
77
|
-
return @body if defined?(@body)
|
78
|
-
@body = []
|
79
|
-
node = start_node
|
80
|
-
while (node = node.next_element) && node != end_node
|
81
|
-
@body << node
|
82
|
-
end
|
83
|
-
@body
|
84
|
-
end
|
85
|
-
|
86
|
-
def start_node
|
87
|
-
@start_node ||= self.class.parent(start_field).first
|
88
|
-
end
|
89
|
-
|
90
|
-
def end_node
|
91
|
-
@end_node ||= self.class.parent(end_field).first
|
92
|
-
end
|
93
|
-
|
94
|
-
def self.encloses?(start_field, end_field)
|
95
|
-
parent(start_field).any? && parent(end_field).any?
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
class RowBlock < Block
|
100
|
-
def self.parent(node)
|
101
|
-
node.ancestors ".//w:tr"
|
102
|
-
end
|
103
|
-
|
104
|
-
def self.encloses?(start_field, end_field)
|
105
|
-
super && parent(start_field) != parent(end_field)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
class ParagraphBlock < Block
|
110
|
-
def self.parent(node)
|
111
|
-
node.ancestors ".//w:p"
|
112
|
-
end
|
113
|
-
|
114
|
-
def self.encloses?(start_field, end_field)
|
115
|
-
super && parent(start_field) != parent(end_field)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
class InlineParagraphBlock < Block
|
120
|
-
def self.parent(node)
|
121
|
-
node.ancestors ".//w:p"
|
122
|
-
end
|
123
|
-
|
124
|
-
def remove_control_elements
|
125
|
-
body.each &:remove
|
126
|
-
start_field.remove
|
127
|
-
end_field.remove
|
128
|
-
end
|
129
|
-
|
130
|
-
def start_node
|
131
|
-
@start_node ||= start_field.end_node
|
132
|
-
end
|
133
|
-
|
134
|
-
def end_node
|
135
|
-
@end_node ||= end_field.start_node
|
136
|
-
end
|
137
|
-
|
138
|
-
def self.encloses?(start_field, end_field)
|
139
|
-
super && parent(start_field) == parent(end_field)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
class OperationConstruction
|
144
|
-
def initialize(fields)
|
145
|
-
@fields = fields
|
146
|
-
@operations = []
|
147
|
-
end
|
148
|
-
|
149
|
-
def operations
|
150
|
-
while @fields.any?
|
151
|
-
@operations << consume(true)
|
152
|
-
end
|
153
|
-
@operations.compact
|
154
|
-
end
|
155
|
-
|
156
|
-
def consume(allow_insertion)
|
157
|
-
@field = @fields.shift
|
158
|
-
return unless @field
|
159
|
-
case @field.expression
|
160
|
-
when /^=/
|
161
|
-
if allow_insertion
|
162
|
-
Statement::Insertion.new(Expression.parse(@field.expression[1..-1]), @field)
|
163
|
-
end
|
164
|
-
when /([^ ]+):each\(([^ ]+)\)/
|
165
|
-
block = consume_block("#{$1}:endEach")
|
166
|
-
Statement::Loop.new(Expression.parse($1), $2, block)
|
167
|
-
when /([^ ]+):if\(([^)]+)\)/
|
168
|
-
block = consume_block("#{$1}:endIf")
|
169
|
-
Statement::Condition.new(Expression.parse($1), block, $2)
|
170
|
-
when /([^ ]+):if/
|
171
|
-
block = consume_block("#{$1}:endIf")
|
172
|
-
Statement::Condition.new(Expression.parse($1), block)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def consume_block(end_expression)
|
177
|
-
start_field = end_field = @field
|
178
|
-
while end_field && end_field.expression != end_expression
|
179
|
-
consume(false)
|
180
|
-
end_field = @field
|
181
|
-
end
|
182
|
-
|
183
|
-
if end_field
|
184
|
-
Block.enclosed_by start_field, end_field
|
185
|
-
else
|
186
|
-
raise TemplateError, "Could not find end field for «#{start_field.expression}». Was looking for «#{end_expression}»"
|
187
|
-
end
|
188
|
-
end
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|