]>>]>', ast.inspect
144 | end
145 |
146 | private
147 |
148 | # returns the numid attribute from paragraphs
149 | def get_numpr_prop_from_ast(ast, key)
150 | values = []
151 | ast.grep(Sablon::HTMLConverter::ListParagraph).each do |para|
152 | numpr = para.instance_variable_get('@properties')[:numPr]
153 | numpr.each { |val| values.push(val[key]) if val[key] }
154 | end
155 | values
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/test/fixtures/html/html_test_content.html:
--------------------------------------------------------------------------------
1 | Sablon HTML insertion
2 |
3 | Text
4 |
5 |
6 | Lorem ipsum dolor sit
7 | amet, consectetur adipiscing elit.
8 | Suspendisse a tempus turpis. Duis urna justo,
9 | vehicula vitae ultricies vel, congue at sem. Fusce turpis
10 | turpis, aliquet id pulvinar aliquam, iaculis non elit. Nulla feugiat
11 | lectus nulla, in dictum ipsum cursus ac. Quisque at odio neque.
12 | Sed ac tortor iaculis, bibendum leo ut, malesuada velit. Donec iaculis
13 | sed urna eget pharetra. Praesent ornare fermentum turpis, placerat
14 | iaculis urna bibendum vitae. Nunc in quam consequat, tristique tellus in,
15 | commodo turpis. Curabitur ullamcorper odio purus, lobortis egestas magna
16 | laoreet vitae. Nunc fringilla velit ante, eu aliquam nisi cursus vitae.
17 | Suspendisse sit amet dui egestas, volutpat
18 | nisi vel, mattis justo. Nullam pellentesque, ipsum eget blandit pharetra,
19 | augue elit aliquam mauris, vel mollis nisl augue ut
20 | ipsum.
21 |
22 |
23 | HTML Entities
24 |
25 | All HTML entities should get passed through to the final doc
26 | Less Than: <
27 | Ampersand: &
28 | Percent: %
29 | One Quarter: ¼
30 |
31 |
32 |
33 | Hyper Links
34 |
35 |
43 |
44 | Lists
45 |
46 |
47 | -
48 | Vestibulum
49 |
50 | - ante ipsum primis
51 |
52 |
53 | -
54 | in faucibus orci luctus
55 |
56 | - et ultrices posuere cubilia Curae;
57 |
58 | - Aliquam vel dolor
59 | - sed sem maximus
60 |
61 |
62 | -
63 | fermentum in non odio.
64 |
65 | - Fusce hendrerit ornare mollis.
66 |
67 |
68 | - Nunc scelerisque nibh nec turpis tempor pulvinar.
69 |
70 |
71 | - Donec eros turpis,
72 | -
73 | aliquet vel volutpat sit amet,
74 |
75 | - semper eu purus.
76 | -
77 | Proin ac erat nec urna efficitur vulputate.
78 |
79 | - Quisque varius convallis ultricies.
80 | - Nullam vel fermentum eros.
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | Pellentesque nulla leo, auctor ornare erat sed, rhoncus congue diam.
89 | Duis non porttitor nulla, ut eleifend enim. Pellentesque non tempor sem.
90 |
91 |
92 | Mauris auctor egestas arcu,
93 |
94 |
95 | - id venenatis nibh dignissim id.
96 | - In non placerat metus.
97 |
98 |
99 |
100 | - Nunc sed consequat metus.
101 | - Nulla consectetur lorem consequat,
102 | - malesuada dui at, lacinia lectus.
103 |
104 |
105 |
106 | - Aliquam efficitur
107 | - lorem a mauris feugiat,
108 | - at semper eros pellentesque.
109 |
110 |
111 |
112 | Nunc lacus diam, consectetur ut odio sit amet, placerat pharetra erat.
113 | Sed commodo ut sem id congue. Sed eget neque elit. Curabitur at erat tortor.
114 | Maecenas eget sapien vitae est sagittis accumsan et nec orci. Integer
115 | luctus at nisl eget venenatis. Nunc nunc eros, consectetur at tortor et,
116 | tristique ultrices elit. Nulla in turpis nibh.
117 |
118 |
119 |
120 | -
121 | Nam consectetur
122 |
123 | - venenatis tempor.
124 |
125 |
126 | -
127 | Aenean
128 |
129 | - blandit
130 |
131 | - porttitor massa,
132 |
133 | - non efficitur
134 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | - Duis faucibus nunc nec venenatis faucibus.
145 | - Aliquam erat volutpat.
146 |
147 |
148 | Quisque non neque ut lacus eleifend volutpat quis sed lacus.
149 |
Praesent ultrices purus eu quam elementum, sit amet faucibus elit
150 | interdum. In lectus orci,
elementum quis dictum ac, porta ac ante.
151 | Fusce tempus ac mauris id cursus. Phasellus a erat nulla. Mauris dolor orci,
152 | malesuada auctor dignissim non, posuere nec odio. Etiam hendrerit
153 | justo nec diam ullamcorper, nec blandit elit sodales.
154 |
155 |
156 |
157 |
158 | Ut eget auctor enim.
159 | Quisque id
160 | neque eu nibh feugiat imperdiet
161 | id ut dui. Ut auctor libero eget
162 | massa tristique pharetra. Cras tincidunt finibus sapien, ut maximus
163 | tortor tempor at. Proin pulvinar
164 | pretium justo vitae malesuada. Suspendisse porta purus eget tortor
165 | tincidunt vestibulum. Maecenas id egestas purus, quis vulputate
166 | lacus. Quisque non
167 | eleifend est.
168 |
169 |
170 |
171 | - Item 1
172 | - Item 2
173 |
174 | - Nested 1
175 | -
176 | Nested 2
177 |
178 | - Nested 2.1
179 | - Nested 2.2
180 | - Nested 2.3
181 |
182 |
183 |
184 | - Item 3
185 |
186 |
187 |
188 | Tables
189 |
190 |
191 | Table 1: Example
192 |
193 | | Head Cell 1 |
194 | Head Cell 2 |
195 |
196 |
197 | | Data Cell 1 |
198 | Data Cell 2 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 | Table 1: Example With Formatting
210 |
211 |
212 |
213 | | Head Cell 1 |
214 | Head Cell 2 |
215 |
216 |
217 |
218 |
219 | | Data Cell 1 |
220 | Data Cell 2 |
221 |
222 |
223 |
224 |
225 | | Data Cell 3 |
226 | Data Cell 4 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | | Above paragraph tag In paragraph below paragraph tag |
234 |
235 |
236 |
237 | - Item A
238 | - Item B
239 |
240 | GitHub
241 | |
242 |
243 |
244 |
245 |
246 |
247 | - Item 1
248 | - Item 2
249 |
250 | - Item 2a
251 | - Item 2b
252 |
253 |
254 |
255 | |
256 |
257 |
258 |
259 | Sub table header
260 | | A | B |
261 | | C | D |
262 |
263 | |
264 |
265 |
266 |
--------------------------------------------------------------------------------
/test/sablon_test.rb:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | require "test_helper"
3 | require "support/xml_snippets"
4 |
5 | class SablonTest < Sablon::TestCase
6 | include XMLSnippets
7 |
8 | def setup
9 | super
10 | @base_path = Pathname.new(File.expand_path("../", __FILE__))
11 | @template_path = @base_path + "fixtures/cv_template.docx"
12 | @output_path = @base_path + "sandbox/cv.docx"
13 | @sample_path = @base_path + "fixtures/cv_sample.docx"
14 | end
15 |
16 | def test_generate_document_from_template
17 | template = Sablon.template @template_path
18 |
19 | skill = Struct.new(:index, :label, :rating)
20 | position = Struct.new(:when, :where, :tasks, :description)
21 | language = Struct.new(:name, :skill)
22 | education = Struct.new(:when, :where, :what)
23 | referee = Struct.new(:name, :company, :position, :phone)
24 |
25 | context = {
26 | current_time: '15.04.2015 14:57',
27 | metadata: { generator: "Sablon" },
28 | title: "Resume",
29 | person: OpenStruct.new("first_name" => "Ronald", "last_name" => "Anderson",
30 | "phone" => "630-384-2975",
31 | "email" => "ron.anderson@gmail.com",
32 | "address" => {
33 | "street" => "1009 Fraggle Drive",
34 | "municipality" => "Wheaton",
35 | "province_zip" => "IL 60187"}),
36 | skills: [skill.new("1.", "Java", "★" * 5),
37 | skill.new("2.", "Ruby", "★" * 3),
38 | skill.new("3.", "Python", "★" * 1),
39 | skill.new("4.", "XML, XSLT, JSP"),
40 | skill.new("5.", "automated testing", "★" * 3),
41 | ],
42 | education: [
43 | education.new("2005 – 2008", "Birmingham University", "Degree: BSc Hons Computer Science. 2:1 Attained."),
44 | education.new("2003 – 2005", "Yale Sixth Form College, Bristol.", "3 A Levels - Mathematics (A), Science (A), History (B)"),
45 | education.new("1997 – 2003", "Berry High School, Bristol.", "11 GCSE’s – 5 As, 5 Bs, 1 C")
46 | ],
47 | certifications: [],
48 | career: [position.new("February 2013 - Present", "Apps Limited", [],
49 | "Ruby on Rails Web Developer for this retail merchandising company."),
50 | position.new("June 2010 - December 2012", "Digital Design Limited",
51 | ["Ongoing ASP.NET website development using C#.",
52 | "Developed CRM web application using SQL Server 2008.",
53 | "SQL Server Reporting.",
54 | "Helped junior developers gain understanding of C# and .NET framework and apply this accordingly."],
55 | "Software Engineer for this financial services provider."),
56 | position.new("June 2008 - June 2010", "Development Consultancy Limited",
57 | ["Development of new features and testing of functionality.",
58 | "Assisted in development and documentation of several ASP.NET based applications.",
59 | "Web application maintenance.",
60 | "Ensured development was signed off prior to unit testing.",
61 | "Liaised with various service providers."])
62 | ],
63 | languages: [language.new("English", "native speaker"),
64 | language.new("German", "fluent"),
65 | language.new("French", "basics"),
66 | ],
67 | about_me: Sablon.content(:html, "I am fond of writing short stories and poems in my spare time,
and have won several literary contests in pursuit of my passion."),
68 | activities: ["Writing", "Photography", "Traveling"],
69 | referees: [
70 | referee.new("Mary P. Larsen", "Strongbod",
71 | "Organizational development consultant", "509-471-9365"),
72 | referee.new("Jeanne P. Eldridge", "Widdmann",
73 | "Information designer", "530-376-1628")
74 | ]
75 | }
76 |
77 | properties = {
78 | start_page_number: 7
79 | }
80 |
81 | template.render_to_file @output_path, context, properties
82 |
83 | assert_docx_equal @sample_path, @output_path
84 | end
85 | end
86 |
87 | class SablonConditionalsTest < Sablon::TestCase
88 | include XMLSnippets
89 |
90 | def setup
91 | super
92 | @base_path = Pathname.new(File.expand_path("../", __FILE__))
93 | @template_path = @base_path + "fixtures/conditionals_template.docx"
94 | @output_path = @base_path + "sandbox/conditionals.docx"
95 | @sample_path = @base_path + "fixtures/conditionals_sample.docx"
96 | end
97 |
98 | def test_generate_document_from_template
99 | template = Sablon.template @template_path
100 | context = {
101 | paragraph: true,
102 | inline: true,
103 | table: true,
104 | table_inline: true,
105 | object: OpenStruct.new(true_method: true, false_method: false),
106 | success_content: '✓',
107 | fail_content: '✗',
108 | content: 'Some Content',
109 | block_content: Sablon.content(:html, 'HTML paragraph injected
')
110 | }
111 | #
112 | template.render_to_file @output_path, context
113 | assert_docx_equal @sample_path, @output_path
114 | end
115 | end
116 |
117 | class SablonLoopsTest < Sablon::TestCase
118 | include XMLSnippets
119 |
120 | def setup
121 | super
122 | @base_path = Pathname.new(File.expand_path("../", __FILE__))
123 | @template_path = @base_path + "fixtures/loops_template.docx"
124 | @output_path = @base_path + "sandbox/loops.docx"
125 | @sample_path = @base_path + "fixtures/loops_sample.docx"
126 | end
127 |
128 | def test_generate_document_from_template
129 | template = Sablon.template @template_path
130 | context = {
131 | fruits: %w[Apple Blueberry Cranberry Date].map { |i| { name: i } },
132 | cars: %w[Silverado Serria Ram Tundra].map { |i| { name: i } }
133 | }
134 |
135 | template.render_to_file @output_path, context
136 | assert_docx_equal @sample_path, @output_path
137 | end
138 | end
139 |
140 | class SablonImagesTest < Sablon::TestCase
141 | def setup
142 | super
143 | @base_path = Pathname.new(File.expand_path("../", __FILE__))
144 | @template_path = @base_path + "fixtures/images_template.docx"
145 | @output_path = @base_path + "sandbox/images.docx"
146 | @sample_path = @base_path + "fixtures/images_sample.docx"
147 | @image_fixtures = @base_path + "fixtures/images"
148 | end
149 |
150 | def test_generate_document_from_template
151 | template = Sablon.template @template_path
152 | #
153 | # setup two image contents to allow quick reuse
154 | r2d2 = Sablon.content(:image, @image_fixtures.join('r2d2.jpg').to_s, properties: {height: '1cm', width: '1cm'})
155 | c3po = Sablon.content(:image, @image_fixtures.join('c3po.jpg'))
156 | darth = Sablon.content(:image, @image_fixtures.join('darth_vader.jpg'))
157 | #
158 | im_data = StringIO.new(IO.binread(@image_fixtures.join('clone.jpg')))
159 | trooper = Sablon.content(:image, im_data, filename: 'clone.jpg', properties: {height: '1cm', width: '4cm'})
160 | #
161 | # with the following context setup all trooper should be reused and
162 | # only a single file added to media. R2D2 should get duplicated in the
163 | # media folder because it is used in two different context keys as
164 | # separate instances. Darth Vader should not be duplicated because
165 | # the key "unused_darth" doesn't appear in the template
166 | context = {
167 | items: [
168 | { title: 'C-3PO', image: c3po },
169 | { title: 'R2-D2', image: r2d2 },
170 | { title: 'Darth Vader', 'image:image' => @image_fixtures.join('darth_vader.jpg') },
171 | { title: 'Storm Trooper', image: trooper }
172 | ],
173 | 'image:r2d2' => @image_fixtures.join('r2d2.jpg'),
174 | 'unused_darth' => darth,
175 | trooper: trooper
176 | }
177 |
178 | template.render_to_file @output_path, context
179 | assert_docx_equal @sample_path, @output_path
180 |
181 | # try to render a document with an image that has no extension
182 | trooper = Sablon.content(:image, im_data, filename: 'clone')
183 | context = { items: [], trooper: trooper }
184 | e = assert_raises ArgumentError do
185 | template.render_to_file @output_path, context
186 | end
187 | assert_equal "Filename: 'clone' has no discernable extension", e.message
188 | end
189 | end
190 |
191 | class SablonSvgImagesTest < Sablon::TestCase
192 | def setup
193 | super
194 | @base_path = Pathname.new(File.expand_path("../", __FILE__))
195 | @template_path = @base_path + "fixtures/svg_images_template.docx"
196 | @output_path = @base_path + "sandbox/svg_images.docx"
197 | @sample_path = @base_path + "fixtures/svg_images_sample.docx"
198 | @image_fixtures = @base_path + "fixtures/images"
199 | end
200 |
201 | def test_generate_document_from_template
202 | template = Sablon.template @template_path
203 |
204 | context = {
205 | svg_sample: Sablon.content(:image, @image_fixtures.join('svg_sample.svg'))
206 | }
207 |
208 | template.render_to_file @output_path, context
209 | assert_docx_equal @sample_path, @output_path
210 | end
211 | end
212 |
--------------------------------------------------------------------------------
/lib/sablon/content.rb:
--------------------------------------------------------------------------------
1 | require 'open-uri'
2 |
3 | module Sablon
4 | module Content
5 | class << self
6 | def wrap(value)
7 | case value
8 | when Sablon::Content
9 | value
10 | else
11 | if type = type_wrapping(value)
12 | type.new(value)
13 | else
14 | raise ArgumentError, "Could not find Sablon content type to wrap #{value.inspect}"
15 | end
16 | end
17 | end
18 |
19 | def make(type_id, *args)
20 | if types.key?(type_id)
21 | types[type_id].new(*args)
22 | else
23 | raise ArgumentError, "Could not find Sablon content type with id '#{type_id}'"
24 | end
25 | end
26 |
27 | def register(content_type)
28 | types[content_type.id] = content_type
29 | end
30 |
31 | def remove(content_type_or_id)
32 | types.delete_if {|k,v| k == content_type_or_id || v == content_type_or_id }
33 | end
34 |
35 | private
36 | def type_wrapping(value)
37 | types.values.reverse.detect { |type| type.wraps?(value) }
38 | end
39 |
40 | def types
41 | @types ||= {}
42 | end
43 | end
44 |
45 | # Handles simple text replacement of fields in the template
46 | class String < Struct.new(:string)
47 | include Sablon::Content
48 | def self.id; :string end
49 | def self.wraps?(value)
50 | value.respond_to?(:to_s)
51 | end
52 |
53 | def initialize(value)
54 | super value.to_s
55 | end
56 |
57 | def append_to(paragraph, display_node, env)
58 | string.scan(/[^\n]+|\n/).reverse.each do |part|
59 | if part == "\n"
60 | display_node.add_next_sibling Nokogiri::XML::Node.new "w:br", display_node.document
61 | else
62 | text_part = display_node.dup
63 | text_part.content = part
64 | display_node.add_next_sibling text_part
65 | end
66 | end
67 | end
68 | end
69 |
70 | # handles direct addition of WordML to the document template
71 | class WordML < Struct.new(:xml)
72 | include Sablon::Content
73 | def self.id; :word_ml end
74 | def self.wraps?(value) false end
75 |
76 | def initialize(value)
77 | super Nokogiri::XML.fragment(value)
78 | end
79 |
80 | def append_to(paragraph, display_node, env)
81 | # if all nodes are inline then add them to the existing paragraph
82 | # otherwise replace the paragraph with the new content.
83 | if all_inline?
84 | pr_tag = display_node.parent.at_xpath('./w:rPr')
85 | add_siblings_to(display_node.parent, pr_tag)
86 | display_node.parent.remove
87 | else
88 | add_siblings_to(paragraph)
89 | paragraph.remove
90 | end
91 | end
92 |
93 | # This allows proper equality checks with other WordML content objects.
94 | # Due to the fact the `xml` attribute is a live Nokogiri object
95 | # the default `==` comparison returns false unless it is the exact
96 | # same object being compared. This method instead checks if the XML
97 | # being added to the document is the same when the `other` object is
98 | # an instance of the WordML content class.
99 | def ==(other)
100 | if other.class == self.class
101 | xml.to_s == other.xml.to_s
102 | else
103 | super
104 | end
105 | end
106 |
107 | private
108 |
109 | # Returns `true` if all of the xml nodes to be inserted are
110 | def all_inline?
111 | (xml.children.map(&:node_name) - inline_tags).empty?
112 | end
113 |
114 | # Array of tags allowed to be a child of the w:p XML tag as defined
115 | # by the Open XML specification
116 | def inline_tags
117 | %w[w:bdo w:bookmarkEnd w:bookmarkStart w:commentRangeEnd
118 | w:commentRangeStart w:customXml
119 | w:customXmlDelRangeEnd w:customXmlDelRangeStart
120 | w:customXmlInsRangeEnd w:customXmlInsRangeStart
121 | w:customXmlMoveFromRangeEnd w:customXmlMoveFromRangeStart
122 | w:customXmlMoveToRangeEnd w:customXmlMoveToRangeStart
123 | w:del w:dir w:fldSimple w:hyperlink w:ins w:moveFrom
124 | w:moveFromRangeEnd w:moveFromRangeStart w:moveTo
125 | w:moveToRangeEnd w:moveToRangeStart m:oMath m:oMathPara
126 | w:pPr w:proofErr w:r w:sdt w:smartTag]
127 | end
128 |
129 | # Adds the XML to be inserted in the document as siblings to the
130 | # node passed in. Run properties are merged here because of namespace
131 | # issues when working with a document fragment
132 | def add_siblings_to(node, rpr_tag = nil)
133 | # Since Nokogiri 1.11.0 adding siblings is only possible for nodes
134 | # with a parent because the parent is used as the context node for
135 | # parsing markup.
136 | if !node.parent.nil?
137 | xml.children.reverse.each do |child|
138 | node.add_next_sibling child
139 | # merge properties
140 | next unless rpr_tag
141 | merge_rpr_tags(child, rpr_tag.children)
142 | end
143 | end
144 | end
145 |
146 | # Merges the provided properties into the run properties of the
147 | # node passed in. Properties are only added if they are not already
148 | # defined on the node itself.
149 | def merge_rpr_tags(node, props)
150 | # first assert that all child runs (w:r tags) have a w:rPr tag
151 | node.xpath('.//w:r').each do |child|
152 | child.prepend_child '' unless child.at_xpath('./w:rPr')
153 | end
154 | #
155 | # merge run props, only adding them if they aren't already defined
156 | node.xpath('.//w:rPr').each do |pr_tag|
157 | existing = pr_tag.children.map(&:node_name)
158 | props.map { |pr| pr_tag << pr unless existing.include? pr.node_name }
159 | end
160 | end
161 | end
162 |
163 | # Handles conversion of HTML -> WordML and addition into template
164 | class HTML < Struct.new(:html_content)
165 | include Sablon::Content
166 | def self.id; :html end
167 | def self.wraps?(value) false end
168 |
169 | def initialize(value)
170 | super value
171 | end
172 |
173 | def append_to(paragraph, display_node, env)
174 | converter = HTMLConverter.new
175 | word_ml = WordML.new(converter.process(html_content, env))
176 | word_ml.append_to(paragraph, display_node, env)
177 | end
178 | end
179 |
180 | # Handles reading image data and inserting it into the document
181 | class Image < Struct.new(:name, :data, :properties)
182 | attr_reader :rid_by_file
183 | attr_accessor :local_rid
184 |
185 | def self.id; :image end
186 | def self.wraps?(value) false end
187 |
188 | def inspect
189 | "#"
190 | end
191 |
192 | def initialize(source, attributes = {})
193 | attributes = Hash[attributes.map { |k, v| [k.to_s, v] }]
194 | # If the source object is readable, use it as such otherwise open
195 | # and read the content
196 | if source.respond_to?(:read)
197 | name, img_data = process_readable(source, attributes)
198 | else
199 | name = File.basename(source)
200 | img_data = IO.binread(source)
201 | end
202 | #
203 | super name, img_data
204 | @attributes = attributes
205 | @properties = @attributes.fetch("properties", {})
206 |
207 | # rId's are separate for each XML file but I want to be able
208 | # to reuse the actual image file itself.
209 | @rid_by_file = {}
210 | end
211 |
212 | def width
213 | return unless (width_str = @properties[:width])
214 | convert_to_emu(width_str)
215 | end
216 |
217 | def height
218 | return unless (height_str = @properties[:height])
219 | convert_to_emu(height_str)
220 | end
221 |
222 | def append_to(paragraph, display_node, env) end
223 |
224 | private
225 |
226 | # Reads the data and attempts to find a filename from either the
227 | # attributes hash or a #filename method on the source object itself.
228 | # A filename is required inorder for MS Word to know the content type.
229 | def process_readable(source, attributes)
230 | if attributes['filename']
231 | name = attributes['filename']
232 | elsif source.respond_to?(:filename)
233 | name = source.filename
234 | else
235 | begin
236 | name = File.basename(source)
237 | rescue TypeError
238 | raise ArgumentError, "Error: Could not determine filename from source, try: `Sablon.content(readable_obj, filename: '...')`"
239 | end
240 | end
241 | #
242 | [File.basename(name), source.read]
243 | end
244 |
245 | # Convert centimeters or inches to Word specific emu format
246 | def convert_to_emu(dim_str)
247 | value, unit = dim_str.match(/(^\.?\d+\.?\d*)(\w+)/).to_a[1..-1]
248 | value = value.to_f
249 |
250 | if unit == "cm"
251 | value = value * 360000
252 | elsif unit == "in"
253 | value = value * 914400
254 | else
255 | throw ArgumentError, "Unsupported unit '#{unit}', only 'cm' and 'in' are permitted."
256 | end
257 |
258 | value.round()
259 | end
260 | end
261 |
262 | register Sablon::Content::String
263 | register Sablon::Content::WordML
264 | register Sablon::Content::HTML
265 | register Sablon::Content::Image
266 | end
267 | end
268 |
--------------------------------------------------------------------------------
/test/content_test.rb:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | require "test_helper"
3 |
4 | module XmlContentTestSetup
5 | def setup
6 | super
7 | @template_text = 'templateAFTER'
8 | #
9 | @document = Nokogiri::XML(doc_wrapper(@template_text))
10 | @paragraph = @document.xpath('//w:p').first
11 | @node = @paragraph.xpath('.//w:r').first.at_xpath('./w:t')
12 | @env = Sablon::Environment.new(nil)
13 | end
14 |
15 | private
16 |
17 | def doc_wrapper(content)
18 | doc = <<-XML.gsub(/^\s+|\n/, '')
19 |
20 |
21 | %s
22 |
23 |
24 | XML
25 | format(doc, content: content)
26 | end
27 |
28 | def assert_xml_equal(expected, document)
29 | expected = Nokogiri::XML(doc_wrapper(expected)).to_xml(indent: 0, save_with: 0)
30 | assert_equal expected, document.to_xml(indent: 0, save_with: 0)
31 | end
32 | end
33 |
34 | class ContentTest < Sablon::TestCase
35 | def test_can_build_content_objects
36 | content = Sablon.content(:string, "a string")
37 | assert_instance_of Sablon::Content::String, content
38 | end
39 |
40 | def test_raises_error_when_building_non_registered_type
41 | e = assert_raises ArgumentError do
42 | Sablon.content :nope, "this should not work"
43 | end
44 | assert_equal "Could not find Sablon content type with id 'nope'", e.message
45 | end
46 |
47 | def test_wraps_string_objects
48 | content = Sablon::Content.wrap(67)
49 | assert_instance_of Sablon::Content::String, content
50 | assert_equal "67", content.string
51 | end
52 |
53 | def test_raises_an_error_if_no_wrapping_type_was_found
54 | Sablon::Content.remove Sablon::Content::String
55 |
56 | e = assert_raises ArgumentError do
57 | Sablon::Content.wrap(43)
58 | end
59 | assert_equal "Could not find Sablon content type to wrap 43", e.message
60 | ensure
61 | Sablon::Content.register Sablon::Content::String
62 | end
63 |
64 | def test_does_not_wrap_content_objects
65 | original_content = Sablon.content(:word_ml, "")
66 | content = Sablon::Content.wrap(original_content)
67 | assert_instance_of Sablon::Content::WordML, content
68 | assert_equal original_content.object_id, content.object_id
69 | end
70 | end
71 |
72 | class CustomContentTest < Sablon::TestCase
73 | class MyContent < Struct.new(:numeric)
74 | include Sablon::Content
75 | def self.id; :custom end
76 | def self.wraps?(value); Numeric === value end
77 |
78 | def append_to(paragraph, display_node)
79 | end
80 | end
81 |
82 | def setup
83 | Sablon::Content.register MyContent
84 | end
85 |
86 | def teardown
87 | Sablon::Content.remove MyContent
88 | end
89 |
90 | def test_can_build_custom_content
91 | content = Sablon.content(:custom, 42)
92 | assert_instance_of MyContent, content
93 | end
94 |
95 | def test_wraps_custom_content
96 | content = Sablon::Content.wrap(31)
97 | assert_instance_of MyContent, content
98 | assert_equal 31, content.numeric
99 | end
100 | end
101 |
102 | class ContentStringTest < Sablon::TestCase
103 | include XmlContentTestSetup
104 |
105 | def test_single_line_string
106 | Sablon.content(:string, 'a normal string').append_to @paragraph, @node, @env
107 |
108 | output = <<-XML.strip
109 | templatea normal stringAFTER
110 | XML
111 | assert_xml_equal output, @document
112 | end
113 |
114 | def test_numeric_string
115 | Sablon.content(:string, 42).append_to @paragraph, @node, @env
116 |
117 | output = <<-XML.strip
118 | template42AFTER
119 | XML
120 | assert_xml_equal output, @document
121 | end
122 |
123 | def test_string_with_newlines
124 | Sablon.content(:string, "a\nmultiline\n\nstring").append_to @paragraph, @node, @env
125 |
126 | output = <<-XML.gsub(/\s/, '')
127 |
128 |
129 | template
130 | a
131 |
132 | multiline
133 |
134 |
135 | string
136 |
137 | AFTER
138 | XML
139 |
140 | assert_xml_equal output, @document
141 | end
142 |
143 | def test_blank_string
144 | Sablon.content(:string, '').append_to @paragraph, @node, @env
145 |
146 | assert_xml_equal @template_text, @document
147 | end
148 | end
149 |
150 | class ContentWordMLTest < Sablon::TestCase
151 | include XmlContentTestSetup
152 |
153 | def test_blank_word_ml
154 | # blank strings in word_ml are an odd corner case, they get treated
155 | # as inline so the paragraph is retained but the display node is still
156 | # removed with nothing being inserted in it's place. Nokogiri automatically
157 | # collapsed the empty tag into a form.
158 | Sablon.content(:word_ml, '').append_to @paragraph, @node, @env
159 | assert_xml_equal "AFTER", @document
160 | end
161 |
162 | def test_plain_text_word_ml
163 | # text isn't a valid child element of a w:p tag, so the whole paragraph
164 | # gets replaced.
165 | Sablon.content(:word_ml, "test").append_to @paragraph, @node, @env
166 | assert_xml_equal "testAFTER", @document
167 | end
168 |
169 | def test_inserts_paragraph_word_ml_into_the_document
170 | @word_ml = 'a '
171 | Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
172 |
173 | output = <<-XML.gsub(/^\s+|\n/, '')
174 |
175 | a
176 |
177 | AFTER
178 | XML
179 |
180 | assert_xml_equal output, @document
181 | end
182 |
183 | def test_inserts_inline_word_ml_into_the_document
184 | @word_ml = 'inline text '
185 | Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
186 |
187 | output = <<-XML.gsub(/^\s+|\n/, '')
188 |
189 | inline text
190 |
191 | AFTER
192 | XML
193 |
194 | assert_xml_equal output, @document
195 | end
196 |
197 | def test_inserting_word_ml_multiple_times_into_same_paragraph
198 | @word_ml = 'inline text '
199 | Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
200 | @word_ml = 'inline text2 '
201 | Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
202 | @word_ml = 'inline text3 '
203 | Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
204 |
205 | # Only a single insertion should work because the node that we insert
206 | # the content after contains a merge field that needs removed. That means
207 | # in the next two appends the @node variable doesn't exist on the document
208 | # tree
209 | output = <<-XML.gsub(/^\s+|\n/, '')
210 |
211 | inline text
212 |
213 | AFTER
214 | XML
215 |
216 | assert_xml_equal output, @document
217 | end
218 |
219 | def test_inserting_multiple_runs_into_same_paragraph
220 | @word_ml = <<-XML.gsub(/^\s+|\n/, '')
221 | inline text
222 | inline text2
223 | inline text3
224 | XML
225 | Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
226 |
227 | # This works because all three runs are added as a single insertion
228 | # event
229 | output = <<-XML.gsub(/^\s+|\n/, '')
230 |
231 | inline text
232 | inline text2
233 | inline text3
234 |
235 | AFTER
236 | XML
237 |
238 | assert_xml_equal output, @document
239 | end
240 | end
241 |
242 | class ContentImageTest < Sablon::TestCase
243 | def setup
244 | base_path = Pathname.new(File.expand_path('../', __FILE__))
245 | fixture_dir = base_path.join('fixtures')
246 | @image_path = fixture_dir.join('images', 'r2d2.jpg')
247 | @expected = Sablon::Content::Image.new(@image_path.to_s)
248 | end
249 |
250 | def test_inspect
251 | assert_equal '#', @expected.inspect
252 | #
253 | # set some rid's and retest
254 | @expected.rid_by_file['word/test.xml'] = 'rId1'
255 | assert_includes [
256 | '#"rId1"}>',
257 | '# "rId1"}>'
258 | ], @expected.inspect
259 | end
260 |
261 | def test_wraps_image_from_string_path
262 | #
263 | tested = Sablon.content(:image, @image_path.to_s)
264 | assert_equal @expected, tested
265 | end
266 |
267 | def test_wraps_image_from_readable_object_that_can_be_basenamed
268 | tested = Sablon.content(:image, open(@image_path.to_s, 'rb'))
269 | assert_equal @expected, tested
270 | end
271 |
272 | def test_wraps_image_from_readable_object_with_filename_supplied
273 | data = StringIO.new(IO.binread(@image_path.to_s))
274 | tested = Sablon.content(:image, data, filename: File.basename(@image_path))
275 | assert_equal @expected, tested
276 | end
277 |
278 | def test_wraps_readable_object_that_responds_to_filename
279 | readable = Struct.new(:data, :filename) { alias read data }
280 | #
281 | readable = readable.new(IO.binread(@image_path.to_s), File.basename(@image_path))
282 | tested = Sablon.content(:image, readable)
283 | assert_equal @expected, tested
284 | end
285 |
286 | def test_raises_error_when_no_filename
287 | data = StringIO.new(IO.binread(@image_path.to_s))
288 | #
289 | assert_raises ArgumentError do
290 | Sablon.content(:image, data)
291 | end
292 | end
293 |
294 | def test_width_conversion
295 | img = Sablon.content(:image, @image_path.to_s, properties: {width: '1.0cm'})
296 | assert_equal 360000, img.width
297 | assert_nil img.height
298 | end
299 |
300 | def test_height_conversion
301 | img = Sablon.content(:image, @image_path.to_s, properties: {height: '1.0in'})
302 | assert_nil img.width
303 | assert_equal 914400, img.height
304 | end
305 |
306 | def test_invalid_unit_conversion
307 | img = Sablon.content(:image, @image_path.to_s, properties: {width: '100px'})
308 | assert_raises ArgumentError do
309 | img.width
310 | end
311 | end
312 | end
313 |
--------------------------------------------------------------------------------