docstache 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +176 -0
- data/README.rdoc +24 -0
- data/Rakefile +7 -0
- data/docstache.gemspec +22 -0
- data/lib/docstache/document.rb +102 -0
- data/lib/docstache/renderer.rb +152 -0
- data/lib/docstache/version.rb +3 -0
- data/lib/docstache.rb +9 -0
- data/spec/example_input/ExampleTemplate.docx +0 -0
- data/spec/example_input/word/document.xml +1122 -0
- data/spec/integration_spec.rb +40 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/template_processor_spec.rb +222 -0
- metadata +94 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'template_processor_spec'
|
3
|
+
|
4
|
+
describe 'integration test', integration: true do
|
5
|
+
let(:data) { Docstache::TestData::DATA }
|
6
|
+
let(:base_path) { SPEC_BASE_PATH.join('example_input') }
|
7
|
+
let(:input_file) { "#{base_path}/ExampleTemplate.docx" }
|
8
|
+
let(:output_dir) { "#{base_path}/tmp" }
|
9
|
+
let(:output_file) { "#{output_dir}/IntegrationTestOutput.docx" }
|
10
|
+
before do
|
11
|
+
FileUtils.rm_rf(output_dir) if File.exist?(output_dir)
|
12
|
+
Dir.mkdir(output_dir)
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'should process in incoming docx' do
|
16
|
+
it 'generates a valid zip file (.docx)' do
|
17
|
+
Docstache::Render.new(input_file, data).generate_docx_file(output_file)
|
18
|
+
|
19
|
+
archive = Zip::File.open(output_file)
|
20
|
+
archive.close
|
21
|
+
|
22
|
+
puts "\n************************************"
|
23
|
+
puts ' >>> Only will work on mac <<<'
|
24
|
+
puts 'NOW attempting to open created file in Word.'
|
25
|
+
cmd = "open #{output_file}"
|
26
|
+
puts " will run '#{cmd}'"
|
27
|
+
puts '************************************'
|
28
|
+
|
29
|
+
system cmd
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'generates a file with the same contents as the input docx' do
|
33
|
+
input_entries = Zip::File.open(input_file) { |z| z.map(&:name) }
|
34
|
+
DocxTemplater::DocxCreator.new(input_file, data).generate_docx_file(output_file)
|
35
|
+
output_entries = Zip::File.open(output_file) { |z| z.map(&:name) }
|
36
|
+
|
37
|
+
expect(input_entries).to eq(output_entries)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'docstache'
|
3
|
+
|
4
|
+
SPEC_BASE_PATH = Pathname.new(File.expand_path(File.dirname(__FILE__)))
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
[:expect_with, :mock_with].each do |method|
|
8
|
+
config.send(method, :rspec) do |c|
|
9
|
+
c.syntax = :expect
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module Docstache
|
5
|
+
module TestData
|
6
|
+
DATA = {
|
7
|
+
teacher: 'Priya Vora',
|
8
|
+
building: 'Building #14',
|
9
|
+
classroom: 'Rm 202'.to_sym,
|
10
|
+
district: 'Washington County Public Schools',
|
11
|
+
senority: 12.25,
|
12
|
+
roster: [
|
13
|
+
{ name: 'Sally', age: 12, attendence: '100%' },
|
14
|
+
{ name: :Xiao, age: 10, attendence: '94%' },
|
15
|
+
{ name: 'Bryan', age: 13, attendence: '100%' },
|
16
|
+
{ name: 'Larry', age: 11, attendence: '90%' },
|
17
|
+
{ name: 'Kumar', age: 12, attendence: '76%' },
|
18
|
+
{ name: 'Amber', age: 11, attendence: '100%' },
|
19
|
+
{ name: 'Isaiah', age: 12, attendence: '89%' },
|
20
|
+
{ name: 'Omar', age: 12, attendence: '99%' },
|
21
|
+
{ name: 'Xi', age: 11, attendence: '20%' },
|
22
|
+
{ name: 'Noushin', age: 12, attendence: '100%' }
|
23
|
+
],
|
24
|
+
event_reports: [
|
25
|
+
{ name: 'Science Museum Field Trip', notes: 'PTA sponsored event. Spoke to Astronaut with HAM radio.' },
|
26
|
+
{ name: 'Wilderness Center Retreat', notes: '2 days hiking for charity:water fundraiser, $10,200 raised.' }
|
27
|
+
],
|
28
|
+
created_at: '11-12-03 02:01'
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe Docstache::Render do
|
34
|
+
let(:data) { Marshal.load(Marshal.dump(DocxTemplater::TestData::DATA)) } # deep copy
|
35
|
+
let(:base_path) { SPEC_BASE_PATH.join('example_input') }
|
36
|
+
let(:xml) { File.read("#{base_path}/word/document.xml") }
|
37
|
+
let(:parser) { DocxTemplater::TemplateProcessor.new(data) }
|
38
|
+
|
39
|
+
context 'valid xml' do
|
40
|
+
it 'should render and still be valid XML' do
|
41
|
+
expect(Nokogiri::XML.parse(xml)).to be_xml
|
42
|
+
out = parser.render(xml)
|
43
|
+
expect(Nokogiri::XML.parse(out)).to be_xml
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should accept non-ascii characters' do
|
47
|
+
data[:teacher] = '老师'
|
48
|
+
out = parser.render(xml)
|
49
|
+
expect(out).to include('老师')
|
50
|
+
expect(Nokogiri::XML.parse(out)).to be_xml
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should escape as necessary invalid xml characters, if told to' do
|
54
|
+
data[:building] = '23rd & A #1 floor'
|
55
|
+
data[:classroom] = '--> 201 <!--'
|
56
|
+
data[:roster][0][:name] = '<#Ai & Bo>'
|
57
|
+
out = parser.render(xml)
|
58
|
+
|
59
|
+
expect(Nokogiri::XML.parse(out)).to be_xml
|
60
|
+
expect(out).to include('23rd & A #1 floor')
|
61
|
+
expect(out).to include('--> 201 <!--')
|
62
|
+
expect(out).to include('<#Ai & Bo>')
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'not escape xml' do
|
66
|
+
let(:parser) { DocxTemplater::TemplateProcessor.new(data, false) }
|
67
|
+
it 'does not escape the xml attributes' do
|
68
|
+
data[:building] = '23rd <p>&</p> #1 floor'
|
69
|
+
out = parser.render(xml)
|
70
|
+
expect(Nokogiri::XML.parse(out)).to be_xml
|
71
|
+
expect(out).to include('23rd <p>&</p> #1 floor')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'unmatched begin and end row templates' do
|
77
|
+
it 'should not raise' do
|
78
|
+
xml = <<EOF
|
79
|
+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
80
|
+
<w:body>
|
81
|
+
<w:tbl>
|
82
|
+
<w:tr><w:tc>
|
83
|
+
<w:p>
|
84
|
+
<w:r><w:t>#BEGIN_ROW:#{:roster.to_s.upcase}#</w:t></w:r>
|
85
|
+
</w:p>
|
86
|
+
</w:tc></w:tr>
|
87
|
+
<w:tr><w:tc>
|
88
|
+
<w:p>
|
89
|
+
<w:r><w:t>#END_ROW:#{:roster.to_s.upcase}#</w:t></w:r>
|
90
|
+
</w:p>
|
91
|
+
</w:tc></w:tr>
|
92
|
+
<w:tr><w:tc>
|
93
|
+
<w:p>
|
94
|
+
<w:r><w:t>#BEGIN_ROW:#{:event_reports.to_s.upcase}#</w:t></w:r>
|
95
|
+
</w:p>
|
96
|
+
</w:tc></w:tr>
|
97
|
+
<w:tr><w:tc>
|
98
|
+
<w:p>
|
99
|
+
<w:r><w:t>#END_ROW:#{:event_reports.to_s.upcase}#</w:t></w:r>
|
100
|
+
</w:p>
|
101
|
+
</w:tc></w:tr>
|
102
|
+
</w:tbl>
|
103
|
+
</w:body>
|
104
|
+
</xml>
|
105
|
+
EOF
|
106
|
+
expect { parser.render(xml) }.to_not raise_error
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should raise an exception' do
|
110
|
+
xml = <<EOF
|
111
|
+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
112
|
+
<w:body>
|
113
|
+
<w:tbl>
|
114
|
+
<w:tr><w:tc>
|
115
|
+
<w:p>
|
116
|
+
<w:r><w:t>#BEGIN_ROW:#{:roster.to_s.upcase}#</w:t></w:r>
|
117
|
+
</w:p>
|
118
|
+
</w:tc></w:tr>
|
119
|
+
<w:tr><w:tc>
|
120
|
+
<w:p>
|
121
|
+
<w:r><w:t>#END_ROW:#{:roster.to_s.upcase}#</w:t></w:r>
|
122
|
+
</w:p>
|
123
|
+
</w:tc></w:tr>
|
124
|
+
<w:tr><w:tc>
|
125
|
+
<w:p>
|
126
|
+
<w:r><w:t>#BEGIN_ROW:#{:event_reports.to_s.upcase}#</w:t></w:r>
|
127
|
+
</w:p>
|
128
|
+
</w:tc></w:tr>
|
129
|
+
</w:tbl>
|
130
|
+
</w:body>
|
131
|
+
</xml>
|
132
|
+
EOF
|
133
|
+
expect { parser.render(xml) }.to raise_error(/#END_ROW:EVENT_REPORTS# nil: true/)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should enter no text for a nil value' do
|
138
|
+
xml = <<EOF
|
139
|
+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
140
|
+
<w:body>
|
141
|
+
<w:p>Before.$KEY$After</w:p>
|
142
|
+
</w:body>
|
143
|
+
</xml>
|
144
|
+
EOF
|
145
|
+
actual = DocxTemplater::TemplateProcessor.new(key: nil).render(xml)
|
146
|
+
expected_xml = <<EOF
|
147
|
+
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
148
|
+
<w:body>
|
149
|
+
<w:p>Before.After</w:p>
|
150
|
+
</w:body>
|
151
|
+
</xml>
|
152
|
+
EOF
|
153
|
+
expect(actual).to eq(expected_xml)
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should replace all simple keys with values' do
|
157
|
+
non_array_keys = data.reject { |_, v| v.is_a?(Array) }
|
158
|
+
non_array_keys.keys.each do |key|
|
159
|
+
expect(xml).to include("$#{key.to_s.upcase}$")
|
160
|
+
expect(xml).not_to include(data[key].to_s)
|
161
|
+
end
|
162
|
+
out = parser.render(xml)
|
163
|
+
|
164
|
+
non_array_keys.each do |key|
|
165
|
+
expect(out).not_to include("$#{key}$")
|
166
|
+
expect(out).to include(data[key].to_s)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should replace all array keys with values' do
|
171
|
+
expect(xml).to include('#BEGIN_ROW:')
|
172
|
+
expect(xml).to include('#END_ROW:')
|
173
|
+
expect(xml).to include('$EACH:')
|
174
|
+
|
175
|
+
out = parser.render(xml)
|
176
|
+
|
177
|
+
expect(out).not_to include('#BEGIN_ROW:')
|
178
|
+
expect(out).not_to include('#END_ROW:')
|
179
|
+
expect(out).not_to include('$EACH:')
|
180
|
+
|
181
|
+
[:roster, :event_reports].each do |key|
|
182
|
+
data[key].each do |row|
|
183
|
+
row.values.map(&:to_s).each do |row_value|
|
184
|
+
expect(out).to include(row_value)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'shold render students names in the same order as the data' do
|
191
|
+
out = parser.render(xml)
|
192
|
+
expect(out).to include('Sally')
|
193
|
+
expect(out).to include('Kumar')
|
194
|
+
expect(out.index('Kumar')).to be > out.index('Sally')
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'shold render event reports names in the same order as the data' do
|
198
|
+
out = parser.render(xml)
|
199
|
+
expect(out).to include('Science Museum Field Trip')
|
200
|
+
expect(out).to include('Wilderness Center Retreat')
|
201
|
+
expect(out.index('Wilderness Center Retreat')).to be > out.index('Science Museum Field Trip')
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should render 2-line event reports in same order as docx' do
|
205
|
+
event_reports_starting_at = xml.index('#BEGIN_ROW:EVENT_REPORTS#')
|
206
|
+
expect(event_reports_starting_at).to be >= 0
|
207
|
+
expect(xml.index('$EACH:NAME$', event_reports_starting_at)).to be > event_reports_starting_at
|
208
|
+
expect(xml.index('$EACH:NOTES$', event_reports_starting_at)).to be > event_reports_starting_at
|
209
|
+
expect(xml.index('$EACH:NOTES$', event_reports_starting_at)).to be > xml.index('$EACH:NAME$', event_reports_starting_at)
|
210
|
+
|
211
|
+
out = parser.render(xml)
|
212
|
+
expect(out.index('PTA sponsored event. Spoke to Astronaut with HAM radio.')).to be > out.index('Science Museum Field Trip')
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'should render sums of input data' do
|
216
|
+
expect(xml).to include('#SUM')
|
217
|
+
out = parser.render(xml)
|
218
|
+
expect(out).not_to include('#SUM')
|
219
|
+
expect(out).to include("#{data[:roster].count} Students")
|
220
|
+
expect(out).to include("#{data[:event_reports].count} Events")
|
221
|
+
end
|
222
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: docstache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Will Cosgrove
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubyzip
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
description: Integrates data into MS Word docx template files. Processing supports
|
42
|
+
loops and replacement of strings of data both outside and within loops.
|
43
|
+
email:
|
44
|
+
- will@willcosgrove.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- ".gitignore"
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.rdoc
|
53
|
+
- Rakefile
|
54
|
+
- docstache.gemspec
|
55
|
+
- lib/docstache.rb
|
56
|
+
- lib/docstache/document.rb
|
57
|
+
- lib/docstache/renderer.rb
|
58
|
+
- lib/docstache/version.rb
|
59
|
+
- spec/example_input/ExampleTemplate.docx
|
60
|
+
- spec/example_input/word/document.xml
|
61
|
+
- spec/integration_spec.rb
|
62
|
+
- spec/spec_helper.rb
|
63
|
+
- spec/template_processor_spec.rb
|
64
|
+
homepage: https://github.com/willcosgrove/docstache
|
65
|
+
licenses:
|
66
|
+
- MIT
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 2.2.2
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: Merges Hash of Data into Word docx template files using mustache syntax
|
88
|
+
test_files:
|
89
|
+
- spec/example_input/ExampleTemplate.docx
|
90
|
+
- spec/example_input/word/document.xml
|
91
|
+
- spec/integration_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
- spec/template_processor_spec.rb
|
94
|
+
has_rdoc:
|