sablon 0.0.21 → 0.0.22
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.
- checksums.yaml +4 -4
- data/.travis.yml +4 -3
- data/Gemfile.lock +9 -9
- data/README.md +120 -11
- data/lib/sablon.rb +7 -1
- data/lib/sablon/configuration/configuration.rb +165 -0
- data/lib/sablon/configuration/html_tag.rb +99 -0
- data/lib/sablon/content.rb +12 -9
- data/lib/sablon/context.rb +27 -20
- data/lib/sablon/environment.rb +31 -0
- data/lib/sablon/html/ast.rb +290 -75
- data/lib/sablon/html/ast_builder.rb +90 -0
- data/lib/sablon/html/converter.rb +3 -123
- data/lib/sablon/numbering.rb +0 -5
- data/lib/sablon/operations.rb +11 -11
- data/lib/sablon/parser/mail_merge.rb +7 -6
- data/lib/sablon/processor/document.rb +9 -9
- data/lib/sablon/processor/numbering.rb +4 -4
- data/lib/sablon/template.rb +5 -4
- data/lib/sablon/version.rb +1 -1
- data/sablon.gemspec +3 -3
- data/test/configuration_test.rb +122 -0
- data/test/content_test.rb +7 -6
- data/test/context_test.rb +11 -11
- data/test/environment_test.rb +27 -0
- data/test/expression_test.rb +2 -2
- data/test/fixtures/html/html_test_content.html +174 -0
- data/test/fixtures/html_sample.docx +0 -0
- data/test/fixtures/xml/comment_block_and_comment_as_key.xml +31 -0
- data/test/html/ast_builder_test.rb +65 -0
- data/test/html/ast_test.rb +117 -0
- data/test/html/converter_test.rb +386 -87
- data/test/html/node_properties_test.rb +113 -0
- data/test/html_test.rb +10 -10
- data/test/mail_merge_parser_test.rb +3 -2
- data/test/processor/document_test.rb +20 -2
- data/test/section_properties_test.rb +1 -1
- data/test/support/html_snippets.rb +9 -0
- data/test/test_helper.rb +0 -1
- metadata +27 -7
data/lib/sablon/content.rb
CHANGED
@@ -40,6 +40,7 @@ module Sablon
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
# Handles simple text replacement of fields in the template
|
43
44
|
class String < Struct.new(:string)
|
44
45
|
include Sablon::Content
|
45
46
|
def self.id; :string end
|
@@ -51,7 +52,7 @@ module Sablon
|
|
51
52
|
super value.to_s
|
52
53
|
end
|
53
54
|
|
54
|
-
def append_to(paragraph, display_node)
|
55
|
+
def append_to(paragraph, display_node, env)
|
55
56
|
string.scan(/[^\n]+|\n/).reverse.each do |part|
|
56
57
|
if part == "\n"
|
57
58
|
display_node.add_next_sibling Nokogiri::XML::Node.new "w:br", display_node.document
|
@@ -64,12 +65,13 @@ module Sablon
|
|
64
65
|
end
|
65
66
|
end
|
66
67
|
|
68
|
+
# handles direct addition of WordML to the document template
|
67
69
|
class WordML < Struct.new(:xml)
|
68
70
|
include Sablon::Content
|
69
71
|
def self.id; :word_ml end
|
70
72
|
def self.wraps?(value) false end
|
71
73
|
|
72
|
-
def append_to(paragraph, display_node)
|
74
|
+
def append_to(paragraph, display_node, env)
|
73
75
|
Nokogiri::XML.fragment(xml).children.reverse.each do |child|
|
74
76
|
paragraph.add_next_sibling child
|
75
77
|
end
|
@@ -77,19 +79,20 @@ module Sablon
|
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
80
|
-
|
82
|
+
# Handles conversion of HTML -> WordML and addition into template
|
83
|
+
class HTML < Struct.new(:html_content)
|
81
84
|
include Sablon::Content
|
82
85
|
def self.id; :html end
|
83
86
|
def self.wraps?(value) false end
|
84
87
|
|
85
|
-
def initialize(
|
86
|
-
|
87
|
-
word_ml = Sablon.content(:word_ml, converter.process(html))
|
88
|
-
super word_ml
|
88
|
+
def initialize(value)
|
89
|
+
super value
|
89
90
|
end
|
90
91
|
|
91
|
-
def append_to(
|
92
|
-
|
92
|
+
def append_to(paragraph, display_node, env)
|
93
|
+
converter = HTMLConverter.new
|
94
|
+
word_ml = WordML.new(converter.process(html_content, env))
|
95
|
+
word_ml.append_to(paragraph, display_node, env)
|
93
96
|
end
|
94
97
|
end
|
95
98
|
|
data/lib/sablon/context.rb
CHANGED
@@ -1,31 +1,38 @@
|
|
1
1
|
module Sablon
|
2
|
+
# A context represents the user supplied arguments to render a
|
3
|
+
# template.
|
4
|
+
#
|
5
|
+
# This module contains transformation functions to turn a
|
6
|
+
# user supplied hash into a data structure suitable for rendering the
|
7
|
+
# docx template.
|
2
8
|
module Context
|
3
|
-
|
4
|
-
transform_hash(hash)
|
5
|
-
|
9
|
+
class << self
|
10
|
+
def transform_hash(hash)
|
11
|
+
Hash[hash.map { |k, v| transform_pair(k.to_s, v) }]
|
12
|
+
end
|
6
13
|
|
7
|
-
|
8
|
-
Hash[hash.map{|k,v| transform_pair(k.to_s, v) }]
|
9
|
-
end
|
14
|
+
private
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
[
|
16
|
+
def transform_standard_key(key, value)
|
17
|
+
case value
|
18
|
+
when Hash
|
19
|
+
[key, transform_hash(value)]
|
15
20
|
else
|
16
|
-
[
|
21
|
+
[key, value]
|
17
22
|
end
|
18
|
-
else
|
19
|
-
transform_standard_key(key, value)
|
20
23
|
end
|
21
|
-
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
def transform_pair(key, value)
|
26
|
+
if key =~ /\A([^:]+):(.+)\z/
|
27
|
+
if value.nil?
|
28
|
+
[Regexp.last_match[2], value]
|
29
|
+
else
|
30
|
+
key_sym = Regexp.last_match[1].to_sym
|
31
|
+
[Regexp.last_match[2], Content.make(key_sym, value)]
|
32
|
+
end
|
33
|
+
else
|
34
|
+
transform_standard_key(key, value)
|
35
|
+
end
|
29
36
|
end
|
30
37
|
end
|
31
38
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Sablon
|
2
|
+
# Combines the user supplied context and template into a single object
|
3
|
+
# to manage data during template processing.
|
4
|
+
class Environment
|
5
|
+
attr_reader :template
|
6
|
+
attr_reader :numbering
|
7
|
+
attr_reader :context
|
8
|
+
|
9
|
+
# returns a new environment with merged contexts
|
10
|
+
def alter_context(context = {})
|
11
|
+
new_context = @context.merge(context)
|
12
|
+
Environment.new(nil, new_context, self)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def initialize(template, context = {}, parent_env = nil)
|
18
|
+
# pass attributes of the supplied environment to the new one or
|
19
|
+
# create new references
|
20
|
+
if parent_env
|
21
|
+
@template = parent_env.template
|
22
|
+
@numbering = parent_env.numbering
|
23
|
+
else
|
24
|
+
@template = template
|
25
|
+
@numbering = Numbering.new
|
26
|
+
end
|
27
|
+
#
|
28
|
+
@context = Context.transform_hash(context)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/sablon/html/ast.rb
CHANGED
@@ -1,18 +1,177 @@
|
|
1
|
+
require "sablon/html/ast_builder"
|
2
|
+
|
1
3
|
module Sablon
|
2
4
|
class HTMLConverter
|
5
|
+
# A top level abstract class to handle common logic for all AST nodes
|
3
6
|
class Node
|
7
|
+
PROPERTIES = [].freeze
|
8
|
+
|
9
|
+
def self.node_name
|
10
|
+
@node_name ||= name.split('::').last
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns a hash defined on the configuration object by default. However,
|
14
|
+
# this method can be overridden by subclasses to return a different
|
15
|
+
# node's style conversion config (i.e. :run) or a hash unrelated to the
|
16
|
+
# config itself. The config object is used for all built-in classes to
|
17
|
+
# allow for end-user customization via the configuration object
|
18
|
+
def self.style_conversion
|
19
|
+
# converts camelcase to underscored
|
20
|
+
key = node_name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
|
21
|
+
Sablon::Configuration.instance.defined_style_conversions.fetch(key, {})
|
22
|
+
end
|
23
|
+
|
24
|
+
# maps the CSS style property to it's OpenXML equivalent. Not all CSS
|
25
|
+
# properties have an equivalent, nor share the same behavior when
|
26
|
+
# defined on different node types (Paragraph, Table and Run).
|
27
|
+
def self.process_properties(properties)
|
28
|
+
# process the styles as a hash and store values
|
29
|
+
style_attrs = {}
|
30
|
+
properties.each do |key, value|
|
31
|
+
unless key.is_a? Symbol
|
32
|
+
key, value = *convert_style_property(key.strip, value.strip)
|
33
|
+
end
|
34
|
+
style_attrs[key] = value if key
|
35
|
+
end
|
36
|
+
style_attrs
|
37
|
+
end
|
38
|
+
|
39
|
+
# handles conversion of a single attribute allowing recursion through
|
40
|
+
# super classes. If the key exists and conversion is succesful a
|
41
|
+
# symbol is returned to avoid conflicts with a CSS prop sharing the
|
42
|
+
# same name. Keys without a conversion class are returned as is
|
43
|
+
def self.convert_style_property(key, value)
|
44
|
+
if style_conversion.key?(key)
|
45
|
+
key, value = style_conversion[key].call(value)
|
46
|
+
key = key.to_sym if key
|
47
|
+
[key, value]
|
48
|
+
elsif self == Node
|
49
|
+
[key, value]
|
50
|
+
else
|
51
|
+
superclass.convert_style_property(key, value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(_env, _node, _properties)
|
56
|
+
@properties ||= nil
|
57
|
+
@attributes ||= {}
|
58
|
+
end
|
59
|
+
|
4
60
|
def accept(visitor)
|
5
61
|
visitor.visit(self)
|
6
62
|
end
|
7
63
|
|
8
|
-
|
9
|
-
|
64
|
+
# Simplifies usage at call sites by only requiring them to supply
|
65
|
+
# the tag name to use and any child AST nodes to render
|
66
|
+
def to_docx(tag)
|
67
|
+
prop_str = @properties.to_docx if @properties
|
68
|
+
#
|
69
|
+
"<#{tag}#{attributes_to_docx}>#{prop_str}#{children_to_docx}</#{tag}>"
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Simplifies usage at call sites
|
75
|
+
def transferred_properties
|
76
|
+
@properties.transferred_properties
|
77
|
+
end
|
78
|
+
|
79
|
+
# Gracefully handles conversion of an attributes hash into a
|
80
|
+
# string
|
81
|
+
def attributes_to_docx
|
82
|
+
return '' if @attributes.nil? || @attributes.empty?
|
83
|
+
' ' + @attributes.map { |k, v| %(#{k}="#{v}") }.join(' ')
|
84
|
+
end
|
85
|
+
|
86
|
+
# Acts like an abstract method allowing subclases full flexibility to
|
87
|
+
# define any content inside the tags.
|
88
|
+
def children_to_docx
|
89
|
+
''
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Manages the properties for an AST node
|
94
|
+
class NodeProperties
|
95
|
+
attr_reader :transferred_properties
|
96
|
+
|
97
|
+
def self.paragraph(properties)
|
98
|
+
new('w:pPr', properties, Paragraph::PROPERTIES)
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.run(properties)
|
102
|
+
new('w:rPr', properties, Run::PROPERTIES)
|
103
|
+
end
|
104
|
+
|
105
|
+
def initialize(tagname, properties, whitelist)
|
106
|
+
@tagname = tagname
|
107
|
+
filter_properties(properties, whitelist)
|
108
|
+
end
|
109
|
+
|
110
|
+
def inspect
|
111
|
+
@properties.map { |k, v| v ? "#{k}=#{v}" : k }.join(';')
|
112
|
+
end
|
113
|
+
|
114
|
+
def [](key)
|
115
|
+
@properties[key]
|
116
|
+
end
|
117
|
+
|
118
|
+
def []=(key, value)
|
119
|
+
@properties[key] = value
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_docx
|
123
|
+
"<#{@tagname}>#{properties_word_ml}</#{@tagname}>" unless @properties.empty?
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# processes properties adding those on the whitelist to the
|
129
|
+
# properties instance variable and those not to the transferred_properties
|
130
|
+
# isntance variable
|
131
|
+
def filter_properties(properties, whitelist)
|
132
|
+
@transferred_properties = {}
|
133
|
+
@properties = {}
|
134
|
+
#
|
135
|
+
properties.each do |key, value|
|
136
|
+
if whitelist.include? key.to_s
|
137
|
+
@properties[key] = value
|
138
|
+
else
|
139
|
+
@transferred_properties[key] = value
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# processes attributes defined on the node into wordML property syntax
|
145
|
+
def properties_word_ml
|
146
|
+
@properties.map { |k, v| transform_attr(k, v) }.join
|
147
|
+
end
|
148
|
+
|
149
|
+
# properties that have a list as the value get nested in tags and
|
150
|
+
# each entry in the list is transformed. When a value is a hash the
|
151
|
+
# keys in the hash are used to explicitly build the XML tag attributes.
|
152
|
+
def transform_attr(key, value)
|
153
|
+
if value.is_a? Array
|
154
|
+
sub_attrs = value.map do |sub_prop|
|
155
|
+
sub_prop.map { |k, v| transform_attr(k, v) }
|
156
|
+
end
|
157
|
+
"<w:#{key}>#{sub_attrs.join}</w:#{key}>"
|
158
|
+
elsif value.is_a? Hash
|
159
|
+
props = value.map { |k, v| format('w:%s="%s"', k, v) if v }
|
160
|
+
"<w:#{key} #{props.compact.join(' ')} />"
|
161
|
+
else
|
162
|
+
value = format('w:val="%s" ', value) if value
|
163
|
+
"<w:#{key} #{value}/>"
|
164
|
+
end
|
10
165
|
end
|
11
166
|
end
|
12
167
|
|
168
|
+
# A container for an array of AST nodes with convenience methods to
|
169
|
+
# work with the internal array as if it were a regular node
|
13
170
|
class Collection < Node
|
14
171
|
attr_reader :nodes
|
15
172
|
def initialize(nodes)
|
173
|
+
@properties ||= nil
|
174
|
+
@attributes ||= {}
|
16
175
|
@nodes = nodes
|
17
176
|
end
|
18
177
|
|
@@ -32,7 +191,18 @@ module Sablon
|
|
32
191
|
end
|
33
192
|
end
|
34
193
|
|
194
|
+
# Stores all of the AST nodes from the current fragment of HTML being
|
195
|
+
# parsed
|
35
196
|
class Root < Collection
|
197
|
+
def initialize(env, node)
|
198
|
+
# strip text nodes from the root level element, these are typically
|
199
|
+
# extra whitespace from indenting the markup
|
200
|
+
node.search('./text()').remove
|
201
|
+
|
202
|
+
# convert children from HTML to AST nodes
|
203
|
+
super(ASTBuilder.html_to_ast(env, node.children, {}))
|
204
|
+
end
|
205
|
+
|
36
206
|
def grep(pattern)
|
37
207
|
visitor = GrepVisitor.new(pattern)
|
38
208
|
accept(visitor)
|
@@ -44,24 +214,26 @@ module Sablon
|
|
44
214
|
end
|
45
215
|
end
|
46
216
|
|
217
|
+
# An AST node representing the top level content container for a word
|
218
|
+
# document. These cannot be nested within other paragraph elements
|
47
219
|
class Paragraph < Node
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
220
|
+
PROPERTIES = %w[framePr ind jc keepLines keepNext numPr
|
221
|
+
outlineLvl pBdr pStyle rPr sectPr shd spacing
|
222
|
+
tabs textAlignment].freeze
|
223
|
+
attr_accessor :runs
|
52
224
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
225
|
+
def initialize(env, node, properties)
|
226
|
+
super
|
227
|
+
properties = self.class.process_properties(properties)
|
228
|
+
@properties = NodeProperties.paragraph(properties)
|
229
|
+
#
|
230
|
+
trans_props = transferred_properties
|
231
|
+
@runs = ASTBuilder.html_to_ast(env, node.children, trans_props)
|
232
|
+
@runs = Collection.new(@runs)
|
233
|
+
end
|
62
234
|
|
63
235
|
def to_docx
|
64
|
-
|
236
|
+
super('w:p')
|
65
237
|
end
|
66
238
|
|
67
239
|
def accept(visitor)
|
@@ -70,107 +242,150 @@ XML
|
|
70
242
|
end
|
71
243
|
|
72
244
|
def inspect
|
73
|
-
"<Paragraph{#{
|
245
|
+
"<Paragraph{#{@properties[:pStyle]}}: #{runs.inspect}>"
|
74
246
|
end
|
75
247
|
|
76
248
|
private
|
77
|
-
|
249
|
+
|
250
|
+
def children_to_docx
|
251
|
+
runs.to_docx
|
78
252
|
end
|
79
253
|
end
|
80
254
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
@ilvl = ilvl
|
93
|
-
end
|
255
|
+
# Manages the child nodes of a list type tag
|
256
|
+
class List < Collection
|
257
|
+
def initialize(env, node, properties)
|
258
|
+
# intialize values
|
259
|
+
@list_tag = node.name
|
260
|
+
#
|
261
|
+
@definition = nil
|
262
|
+
if node.ancestors(".//#{@list_tag}").length.zero?
|
263
|
+
# Only register a definition when upon the first list tag encountered
|
264
|
+
@definition = env.numbering.register(properties[:pStyle])
|
265
|
+
end
|
94
266
|
|
95
|
-
|
96
|
-
|
97
|
-
LIST_STYLE % [@ilvl, numid]
|
98
|
-
end
|
99
|
-
end
|
267
|
+
# update attributes of all child nodes
|
268
|
+
transfer_node_attributes(node.children, node.attributes)
|
100
269
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
270
|
+
# Move any list tags that are a child of a list item up one level
|
271
|
+
process_child_nodes(node)
|
272
|
+
|
273
|
+
# strip text nodes from the list level element, this is typically
|
274
|
+
# extra whitespace from indenting the markup
|
275
|
+
node.search('./text()').remove
|
276
|
+
|
277
|
+
# convert children from HTML to AST nodes
|
278
|
+
super(ASTBuilder.html_to_ast(env, node.children, properties))
|
106
279
|
end
|
107
280
|
|
108
281
|
def inspect
|
109
|
-
|
110
|
-
parts << 'bold' if @bold
|
111
|
-
parts << 'italic' if @italic
|
112
|
-
parts << 'underline' if @underline
|
113
|
-
parts.join('|')
|
282
|
+
"<List: #{super}>"
|
114
283
|
end
|
115
284
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
285
|
+
private
|
286
|
+
|
287
|
+
# handles passing all attributes on the parent down to children
|
288
|
+
def transfer_node_attributes(nodes, attributes)
|
289
|
+
nodes.each do |child|
|
290
|
+
# update all attributes
|
291
|
+
merge_attributes(child, attributes)
|
292
|
+
|
293
|
+
# set attributes specific to list items
|
294
|
+
if @definition
|
295
|
+
child['pStyle'] = @definition.style
|
296
|
+
child['numId'] = @definition.numid
|
297
|
+
end
|
298
|
+
child['ilvl'] = child.ancestors(".//#{@list_tag}").length - 1
|
125
299
|
end
|
126
300
|
end
|
127
301
|
|
128
|
-
|
129
|
-
|
302
|
+
# merges parent and child attributes together, preappending the parent's
|
303
|
+
# values to allow the child node to override it if the value is already
|
304
|
+
# defined on the child node.
|
305
|
+
def merge_attributes(child, parent_attributes)
|
306
|
+
parent_attributes.each do |name, par_attr|
|
307
|
+
child_attr = child[name] ? child[name].split(';') : []
|
308
|
+
child[name] = par_attr.value.split(';').concat(child_attr).join('; ')
|
309
|
+
end
|
130
310
|
end
|
131
311
|
|
132
|
-
|
133
|
-
|
312
|
+
# moves any list tags that are a child of a list item tag up one level
|
313
|
+
# so they become a sibling instead of a child
|
314
|
+
def process_child_nodes(node)
|
315
|
+
node.xpath("./li/#{@list_tag}").each do |list|
|
316
|
+
# transfer attributes from parent now because the list tag will
|
317
|
+
# no longer be a child and won't inheirit them as usual
|
318
|
+
transfer_node_attributes(list.children, list.parent.attributes)
|
319
|
+
list.parent.add_next_sibling(list)
|
320
|
+
end
|
134
321
|
end
|
322
|
+
end
|
135
323
|
|
136
|
-
|
137
|
-
|
324
|
+
# Sets list item specific attributes registered on the node to properly
|
325
|
+
# generate a list paragraph
|
326
|
+
class ListParagraph < Paragraph
|
327
|
+
def initialize(env, node, properties)
|
328
|
+
list_props = {
|
329
|
+
pStyle: node['pStyle'],
|
330
|
+
numPr: [{ ilvl: node['ilvl'] }, { numId: node['numId'] }]
|
331
|
+
}
|
332
|
+
properties = properties.merge(list_props)
|
333
|
+
super
|
138
334
|
end
|
139
335
|
|
140
|
-
|
141
|
-
|
336
|
+
private
|
337
|
+
|
338
|
+
def transferred_properties
|
339
|
+
super
|
142
340
|
end
|
143
341
|
end
|
144
342
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
343
|
+
# Create a run of text in the document, runs cannot be nested within
|
344
|
+
# each other
|
345
|
+
class Run < Node
|
346
|
+
PROPERTIES = %w[b i caps color dstrike emboss imprint highlight outline
|
347
|
+
rStyle shadow shd smallCaps strike sz u vanish
|
348
|
+
vertAlign].freeze
|
349
|
+
|
350
|
+
def initialize(_env, node, properties)
|
351
|
+
super
|
352
|
+
properties = self.class.process_properties(properties)
|
353
|
+
@properties = NodeProperties.run(properties)
|
354
|
+
@string = node.to_s # using `text` doesn't reconvert HTML entities
|
150
355
|
end
|
151
356
|
|
152
357
|
def to_docx
|
153
|
-
|
358
|
+
super('w:r')
|
154
359
|
end
|
155
360
|
|
156
361
|
def inspect
|
157
|
-
"<
|
362
|
+
"<Run{#{@properties.inspect}}: #{@string}>"
|
158
363
|
end
|
159
364
|
|
160
365
|
private
|
161
|
-
|
162
|
-
|
366
|
+
|
367
|
+
def children_to_docx
|
368
|
+
content = @string.tr("\u00A0", ' ')
|
369
|
+
"<w:t xml:space=\"preserve\">#{content}</w:t>"
|
163
370
|
end
|
164
371
|
end
|
165
372
|
|
166
|
-
|
167
|
-
|
168
|
-
|
373
|
+
# Creates a blank line in the word document
|
374
|
+
class Newline < Run
|
375
|
+
def initialize(*)
|
376
|
+
@properties = nil
|
377
|
+
@attributes = {}
|
169
378
|
end
|
170
379
|
|
171
380
|
def inspect
|
172
381
|
"<Newline>"
|
173
382
|
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
def children_to_docx
|
387
|
+
"<w:br/>"
|
388
|
+
end
|
174
389
|
end
|
175
390
|
end
|
176
391
|
end
|