sablon 0.0.22 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>