sablon 0.0.22 → 0.1.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.
@@ -52,13 +52,8 @@ module Sablon
52
52
  # Checking that the current tag is an allowed child of the parent_tag.
53
53
  # If the parent tag is nil then a block level tag is required.
54
54
  def validate_structure(parent, child)
55
- if parent.ast_class == Root && child.type == :inline
56
- msg = "#{child.name} needs to be wrapped in a block level tag."
57
- elsif parent && !parent.allowed_child?(child)
58
- msg = "#{child.name} is not a valid child element of #{parent.name}."
59
- else
60
- return
61
- end
55
+ return unless parent && !parent.allowed_child?(child)
56
+ msg = "#{child.name} is not a valid child element of #{parent.name}."
62
57
  raise ContextError, "Invalid HTML structure: #{msg}"
63
58
  end
64
59
 
@@ -0,0 +1,91 @@
1
+ module Sablon
2
+ class HTMLConverter
3
+ # Manages the properties for an AST node, includes factory methods
4
+ # for easy use at calling sites.
5
+ class NodeProperties
6
+ attr_reader :transferred_properties
7
+
8
+ def self.paragraph(properties)
9
+ new('w:pPr', properties, Paragraph::PROPERTIES)
10
+ end
11
+
12
+ def self.table(properties)
13
+ new('w:tblPr', properties, Table::PROPERTIES)
14
+ end
15
+
16
+ def self.table_row(properties)
17
+ new('w:trPr', properties, TableRow::PROPERTIES)
18
+ end
19
+
20
+ def self.table_cell(properties)
21
+ new('w:tcPr', properties, TableCell::PROPERTIES)
22
+ end
23
+
24
+ def self.run(properties)
25
+ new('w:rPr', properties, Run::PROPERTIES)
26
+ end
27
+
28
+ def initialize(tagname, properties, whitelist)
29
+ @tagname = tagname
30
+ filter_properties(properties, whitelist)
31
+ end
32
+
33
+ def inspect
34
+ @properties.map { |k, v| v ? "#{k}=#{v}" : k }.join(';')
35
+ end
36
+
37
+ def [](key)
38
+ @properties[key]
39
+ end
40
+
41
+ def []=(key, value)
42
+ @properties[key] = value
43
+ end
44
+
45
+ def to_docx
46
+ "<#{@tagname}>#{properties_word_ml}</#{@tagname}>" unless @properties.empty?
47
+ end
48
+
49
+ private
50
+
51
+ # processes properties adding those on the whitelist to the
52
+ # properties instance variable and those not to the transferred_properties
53
+ # isntance variable
54
+ def filter_properties(properties, whitelist)
55
+ @transferred_properties = {}
56
+ @properties = {}
57
+ #
58
+ properties.each do |key, value|
59
+ if whitelist.include? key.to_s
60
+ @properties[key] = value
61
+ else
62
+ @transferred_properties[key] = value
63
+ end
64
+ end
65
+ end
66
+
67
+ # processes attributes defined on the node into wordML property syntax
68
+ def properties_word_ml
69
+ @properties.map { |k, v| transform_attr(k, v) }.join
70
+ end
71
+
72
+ # properties that have a list as the value get nested in tags and
73
+ # each entry in the list is transformed. When a value is a hash the
74
+ # keys in the hash are used to explicitly build the XML tag attributes.
75
+ def transform_attr(key, value)
76
+ if value.is_a? Array
77
+ sub_attrs = value.map do |sub_prop|
78
+ sub_prop.map { |k, v| transform_attr(k, v) }
79
+ end
80
+ "<w:#{key}>#{sub_attrs.join}</w:#{key}>"
81
+ elsif value.is_a? Hash
82
+ props = value.map { |k, v| format('w:%s="%s"', k, v) if v }
83
+ "<w:#{key} #{props.compact.join(' ')} />"
84
+ else
85
+ value = format('w:val="%s" ', value) if value
86
+ "<w:#{key} #{value}/>"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,47 @@
1
+ module Sablon
2
+ # Handles storing referenced relationships in the document.xml file and
3
+ # writing them to the document.xml.rels file
4
+ class Relationship
5
+ attr_accessor :relationships
6
+
7
+ def initialize
8
+ @relationships = []
9
+ end
10
+
11
+ def add_found_relationships(content, output_stream)
12
+ output_stream.put_next_entry('word/_rels/document.xml.rels')
13
+ #
14
+ unless @relationships.empty?
15
+ rels_doc = Nokogiri::XML(content)
16
+ rels_doc_root = rels_doc.root
17
+ # convert new rels to nodes
18
+ node_set = convert_relationships_to_node_set(rels_doc)
19
+ @relationships = []
20
+ # add new nodes to XML content
21
+ rels_doc_root.last_element_child.after(node_set)
22
+ content = rels_doc.to_xml(indent: 0, save_with: 0)
23
+ end
24
+ #
25
+ output_stream.write(content)
26
+ end
27
+
28
+ private
29
+
30
+ # Builds a set of Relationship XML nodes from the stored relationships
31
+ def convert_relationships_to_node_set(doc)
32
+ node_set = Nokogiri::XML::NodeSet.new(doc)
33
+ @relationships.each do |relationship|
34
+ rel_tag = "<Relationship#{relationship_attributes(relationship)}/>"
35
+ node_set << Nokogiri::XML.fragment(rel_tag).children.first
36
+ end
37
+ #
38
+ node_set
39
+ end
40
+
41
+ # Builds the attribute string for the relationship XML node
42
+ def relationship_attributes(relationship)
43
+ return '' if relationship.nil? || relationship.empty?
44
+ ' ' + relationship.map { |k, v| %(#{k}="#{v}") }.join(' ')
45
+ end
46
+ end
47
+ end
@@ -19,11 +19,14 @@ module Sablon
19
19
  private
20
20
 
21
21
  def render(context, properties = {})
22
+ created_dirs = []
23
+ relations_file_content = nil
22
24
  env = Sablon::Environment.new(self, context)
23
25
  Zip.sort_entries = true # required to process document.xml before numbering.xml
24
26
  Zip::OutputStream.write_buffer(StringIO.new) do |out|
25
27
  Zip::File.open(@path).each do |entry|
26
28
  entry_name = entry.name
29
+ created_dirs = create_dirs_in_zipfile(created_dirs, entry_name, out)
27
30
  out.put_next_entry(entry_name)
28
31
  content = entry.get_input_stream.read
29
32
  if entry_name == 'word/document.xml'
@@ -32,11 +35,38 @@ module Sablon
32
35
  out.write(process(Processor::Document, content, env))
33
36
  elsif entry_name == 'word/numbering.xml'
34
37
  out.write(process(Processor::Numbering, content, env))
38
+ elsif entry_name == 'word/_rels/document.xml.rels'
39
+ relations_file_content = content
35
40
  else
36
41
  out.write(content)
37
42
  end
38
43
  end
44
+ if relations_file_content
45
+ env.relationship.add_found_relationships(relations_file_content, out)
46
+ end
47
+ end
48
+ end
49
+
50
+ # creates directories of the unzipped docx file in the newly created docx file e.g. in case of
51
+ # word/_rels/document.xml.rels it creates word/ and _rels directories to apply recursive zipping.
52
+ # This is a hack to fix the issue of getting a corrupted file when any referencing between the
53
+ # xml files happen like in the case of implementing hyperlinks
54
+ #
55
+ def create_dirs_in_zipfile(previous_created_dirs, entry_name, output_stream)
56
+ created_dirs = previous_created_dirs
57
+ entry_name_tokens = entry_name.split('/')
58
+ entry_name_tokens.pop()
59
+ if entry_name_tokens.length > 1
60
+ prev_dir = ''
61
+ entry_name_tokens.each do |dir_name|
62
+ prev_dir += dir_name + '/'
63
+ unless created_dirs.include? prev_dir
64
+ output_stream.put_next_entry(prev_dir)
65
+ created_dirs << prev_dir
66
+ end
67
+ end
39
68
  end
69
+ created_dirs
40
70
  end
41
71
 
42
72
  # process the sablon xml template with the given +context+.
@@ -1,3 +1,3 @@
1
1
  module Sablon
2
- VERSION = "0.0.22"
2
+ VERSION = "0.1.0"
3
3
  end
data/test/content_test.rb CHANGED
@@ -1,6 +1,36 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  require "test_helper"
3
3
 
4
+ module XmlContentTestSetup
5
+ def setup
6
+ super
7
+ @template_text = '<w:p><w:r><w:t>template</w:t></w:r></w:p><w:p>AFTER</w:p>'
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
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
20
+ <w:body>
21
+ %<content>s
22
+ </w:body>
23
+ </w:document>
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
+
4
34
  class ContentTest < Sablon::TestCase
5
35
  def test_can_build_content_objects
6
36
  content = Sablon.content(:string, "a string")
@@ -69,30 +99,14 @@ class CustomContentTest < Sablon::TestCase
69
99
  end
70
100
  end
71
101
 
72
- module ContentTestSetup
73
- def setup
74
- super
75
- @template_text = '<w:p><span>template</span></w:p><w:p>AFTER</w:p>'
76
- @document = Nokogiri::XML.fragment(@template_text)
77
- @paragraph = @document.children.first
78
- @node = @document.css("span").first
79
- @env = Sablon::Environment.new(nil)
80
- end
81
-
82
- private
83
- def assert_xml_equal(expected, document)
84
- assert_equal expected, document.to_xml(indent: 0, save_with: 0)
85
- end
86
- end
87
-
88
102
  class ContentStringTest < Sablon::TestCase
89
- include ContentTestSetup
103
+ include XmlContentTestSetup
90
104
 
91
105
  def test_single_line_string
92
- Sablon.content(:string, "a normal string").append_to @paragraph, @node, @env
106
+ Sablon.content(:string, 'a normal string').append_to @paragraph, @node, @env
93
107
 
94
108
  output = <<-XML.strip
95
- <w:p><span>template</span><span>a normal string</span></w:p><w:p>AFTER</w:p>
109
+ <w:p><w:r><w:t>template</w:t><w:t>a normal string</w:t></w:r></w:p><w:p>AFTER</w:p>
96
110
  XML
97
111
  assert_xml_equal output, @document
98
112
  end
@@ -101,7 +115,7 @@ class ContentStringTest < Sablon::TestCase
101
115
  Sablon.content(:string, 42).append_to @paragraph, @node, @env
102
116
 
103
117
  output = <<-XML.strip
104
- <w:p><span>template</span><span>42</span></w:p><w:p>AFTER</w:p>
118
+ <w:p><w:r><w:t>template</w:t><w:t>42</w:t></w:r></w:p><w:p>AFTER</w:p>
105
119
  XML
106
120
  assert_xml_equal output, @document
107
121
  end
@@ -109,53 +123,118 @@ class ContentStringTest < Sablon::TestCase
109
123
  def test_string_with_newlines
110
124
  Sablon.content(:string, "a\nmultiline\n\nstring").append_to @paragraph, @node, @env
111
125
 
112
- output = <<-XML.strip.gsub("\n", "")
113
- <w:p>
114
- <span>template</span>
115
- <span>a</span>
116
- <w:br/>
117
- <span>multiline</span>
118
- <w:br/>
119
- <w:br/>
120
- <span>string</span>
121
- </w:p>
122
- <w:p>AFTER</w:p>
126
+ output = <<-XML.gsub(/\s/, '')
127
+ <w:p>
128
+ <w:r>
129
+ <w:t>template</w:t>
130
+ <w:t>a</w:t>
131
+ <w:br/>
132
+ <w:t>multiline</w:t>
133
+ <w:br/>
134
+ <w:br/>
135
+ <w:t>string</w:t>
136
+ </w:r>
137
+ </w:p><w:p>AFTER</w:p>
123
138
  XML
124
139
 
125
140
  assert_xml_equal output, @document
126
141
  end
127
142
 
128
143
  def test_blank_string
129
- Sablon.content(:string, "").append_to @paragraph, @node, @env
144
+ Sablon.content(:string, '').append_to @paragraph, @node, @env
130
145
 
131
146
  assert_xml_equal @template_text, @document
132
147
  end
133
148
  end
134
149
 
135
150
  class ContentWordMLTest < Sablon::TestCase
136
- include ContentTestSetup
151
+ include XmlContentTestSetup
137
152
 
138
153
  def test_blank_word_ml
139
- Sablon.content(:word_ml, "").append_to @paragraph, @node, @env
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 <w:p></w:P> tag into a <w:/p> form.
158
+ Sablon.content(:word_ml, '').append_to @paragraph, @node, @env
159
+ assert_xml_equal "<w:p/><w:p>AFTER</w:p>", @document
160
+ end
140
161
 
141
- assert_xml_equal "<w:p>AFTER</w:p>", @document
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 "test<w:p>AFTER</w:p>", @document
142
167
  end
143
168
 
144
- def test_inserts_word_ml_into_the_document
169
+ def test_inserts_paragraph_word_ml_into_the_document
145
170
  @word_ml = '<w:p><w:r><w:t xml:space="preserve">a </w:t></w:r></w:p>'
146
171
  Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
147
172
 
148
- output = <<-XML.strip.gsub("\n", "")
149
- <w:p>
150
- <w:r><w:t xml:space=\"preserve\">a </w:t></w:r>
151
- </w:p>
152
- <w:p>AFTER</w:p>
173
+ output = <<-XML.gsub(/^\s+|\n/, '')
174
+ <w:p>
175
+ <w:r><w:t xml:space=\"preserve\">a </w:t></w:r>
176
+ </w:p>
177
+ <w:p>AFTER</w:p>
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 = '<w:r><w:t xml:space="preserve">inline text </w:t></w:r>'
185
+ Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
186
+
187
+ output = <<-XML.gsub(/^\s+|\n/, '')
188
+ <w:p>
189
+ <w:r><w:t xml:space="preserve">inline text </w:t></w:r>
190
+ </w:p>
191
+ <w:p>AFTER</w:p>
153
192
  XML
154
193
 
155
194
  assert_xml_equal output, @document
156
195
  end
157
196
 
158
197
  def test_inserting_word_ml_multiple_times_into_same_paragraph
159
- skip "Content::WordML currently removes the paragraph..."
198
+ @word_ml = '<w:r><w:t xml:space="preserve">inline text </w:t></w:r>'
199
+ Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
200
+ @word_ml = '<w:r><w:t xml:space="preserve">inline text2 </w:t></w:r>'
201
+ Sablon.content(:word_ml, @word_ml).append_to @paragraph, @node, @env
202
+ @word_ml = '<w:r><w:t xml:space="preserve">inline text3 </w:t></w:r>'
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 afer 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
+ <w:p>
211
+ <w:r><w:t xml:space="preserve">inline text </w:t></w:r>
212
+ </w:p>
213
+ <w:p>AFTER</w:p>
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
+ <w:r><w:t xml:space="preserve">inline text </w:t></w:r>
222
+ <w:r><w:t xml:space="preserve">inline text2 </w:t></w:r>
223
+ <w:r><w:t xml:space="preserve">inline text3 </w:t></w:r>
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
+ <w:p>
231
+ <w:r><w:t xml:space="preserve">inline text </w:t></w:r>
232
+ <w:r><w:t xml:space="preserve">inline text2 </w:t></w:r>
233
+ <w:r><w:t xml:space="preserve">inline text3 </w:t></w:r>
234
+ </w:p>
235
+ <w:p>AFTER</w:p>
236
+ XML
237
+
238
+ assert_xml_equal output, @document
160
239
  end
161
240
  end
@@ -30,9 +30,20 @@
30
30
  </div>
31
31
 
32
32
 
33
+ <h2>Hyper Links</h2>
34
+
35
+ <div>
36
+ <a href="http://www.google.com">Hyperlink</a>
37
+ <br/>
38
+ <span style="font-style: bold"><a href="http://www.google.com">Hyperlink with bold style</a></span>
39
+ <br/>
40
+ <u><a href="http://www.google.com" style="color: #1022DD">Hyperlink with color and underline</a></u>
41
+ <br/>
42
+ </div>
43
+
33
44
  <h2>Lists</h2>
34
45
 
35
- <ol>
46
+ <ol>
36
47
  <li>
37
48
  Vestibulum&nbsp;
38
49
  <ol>
@@ -78,23 +89,23 @@
78
89
  Duis non porttitor nulla, ut eleifend enim. Pellentesque non tempor sem.
79
90
  </div>
80
91
 
81
- <div>Mauris auctor egestas arcu,&nbsp;</div>
92
+ <div>Mauris auctor egestas arcu,&nbsp;</div>
82
93
 
83
94
  <ol>
84
- <li>id venenatis nibh dignissim id.&nbsp;</li>
85
- <li>In non placerat metus.&nbsp;</li>
95
+ <li>id venenatis nibh dignissim id.&nbsp;</li>
96
+ <li>In non placerat metus.&nbsp;</li>
86
97
  </ol>
87
98
 
88
- <ul>
89
- <li>Nunc sed consequat metus.&nbsp;</li>
90
- <li>Nulla consectetur lorem consequat,&nbsp;</li>
91
- <li>malesuada dui at, lacinia lectus.&nbsp;</li>
99
+ <ul>
100
+ <li>Nunc sed consequat metus.&nbsp;</li>
101
+ <li>Nulla consectetur lorem consequat,&nbsp;</li>
102
+ <li>malesuada dui at, lacinia lectus.&nbsp;</li>
92
103
  </ul>
93
104
 
94
- <ol>
95
- <li>Aliquam efficitur&nbsp;</li>
96
- <li>lorem a mauris feugiat,&nbsp;</li>
97
- <li>at semper eros pellentesque.&nbsp;</li>
105
+ <ol>
106
+ <li>Aliquam efficitur&nbsp;</li>
107
+ <li>lorem a mauris feugiat,&nbsp;</li>
108
+ <li>at semper eros pellentesque.&nbsp;</li>
98
109
  </ol>
99
110
 
100
111
  <div>
@@ -105,7 +116,7 @@
105
116
  tristique ultrices elit. Nulla in turpis nibh.
106
117
  </div>
107
118
 
108
- <ul>
119
+ <ul>
109
120
  <li>
110
121
  Nam consectetur&nbsp;
111
122
  <ul>
@@ -133,14 +144,14 @@
133
144
  <li>Duis faucibus nunc nec venenatis faucibus.&nbsp;</li>
134
145
  <li>Aliquam erat volutpat.&nbsp;</li>
135
146
  </ul>
136
- <div style="border: 5px double #FF00FF">
147
+ <div style="border: 5px double #FF00FF">
137
148
  <strong>Quisque non neque ut lacus eleifend volutpat quis sed lacus.
138
149
  <br />Praesent ultrices purus eu quam elementum, sit amet faucibus elit
139
150
  interdum. In lectus orci,<br /> elementum quis dictum ac, porta ac ante.
140
151
  Fusce tempus ac mauris id cursus. Phasellus a erat nulla. <em>Mauris dolor orci</em>,
141
152
  malesuada auctor dignissim non, <u>posuere nec odio</u>. Etiam hendrerit
142
153
  justo nec diam ullamcorper, nec blandit elit sodales.</strong>
143
- </div>
154
+ </div>
144
155
 
145
156
 
146
157
  <div style="text-align: both; background-color: #EAFEDA; vertical-align: top">
@@ -172,3 +183,83 @@
172
183
  </ul>
173
184
  <li>Item 3</li>
174
185
  </ul>
186
+
187
+
188
+ <h2>Tables</h2>
189
+
190
+ <table>
191
+ <caption>Table 1: Example</caption>
192
+ <tr>
193
+ <th>Head Cell 1</th>
194
+ <th>Head Cell 2</th>
195
+ </tr>
196
+ <tr>
197
+ <td>Data Cell 1</td>
198
+ <td>Data Cell 2</td>
199
+ </tr>
200
+ </table>
201
+
202
+ <p>
203
+ <br/>
204
+ <br/>
205
+ </p>
206
+
207
+ <table style="border: 1px solid #FF0000">
208
+ <caption style="caption-side: bottom; highlight: cyan; text-align: center">
209
+ Table 1: Example With Formatting
210
+ </caption>
211
+ <thead>
212
+ <tr style="border: 1px solid #FF00FF">
213
+ <th>Head Cell 1</th>
214
+ <th>Head Cell 2</th>
215
+ </tr>
216
+ </thead>
217
+ <tbody>
218
+ <tr style="border: 1px solid #0000FF">
219
+ <td style="color: #FFAA22">Data Cell 1</td>
220
+ <td style="background-color: #123456">Data Cell 2</td>
221
+ </tr>
222
+ </tbody>
223
+ <tfoot>
224
+ <tr>
225
+ <td>Data Cell 3</td>
226
+ <td>Data Cell 4</td>
227
+ </tr>
228
+ </tfoot>
229
+ </table>
230
+
231
+ <table style="border: 1px solid #000000; width: 4000dxa">
232
+ <tr>
233
+ <td>Above paragraph tag<p>In paragraph</p>below paragraph tag</td>
234
+
235
+ <td>
236
+ <ul>
237
+ <li>Item A</li>
238
+ <li>Item B</li>
239
+ </ul>
240
+ <a href="http://www.github.com" style="color: #0000dd">GitHub</a>
241
+ </td>
242
+ </tr>
243
+
244
+ <tr>
245
+ <td>
246
+ <ol>
247
+ <li>Item 1</li>
248
+ <li>Item 2
249
+ <ol>
250
+ <li>Item 2a</li>
251
+ <li>Item 2b</li>
252
+ </ol>
253
+ </li>
254
+ </ol>
255
+ </td>
256
+
257
+ <td>
258
+ <table style="border: 1px solid #FF0000">
259
+ <caption style="text-align: center">Sub table header</caption>
260
+ <tr><td>A</td><td>B</td></tr>
261
+ <tr><td>C</td><td>D</td></tr>
262
+ </table>
263
+ </td>
264
+ </tr>
265
+ </table>