sablon 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +38 -0
- data/lib/sablon.rb +11 -0
- data/lib/sablon/content.rb +30 -0
- data/lib/sablon/context.rb +28 -0
- data/lib/sablon/operations.rb +10 -1
- data/lib/sablon/parser/mail_merge.rb +7 -14
- data/lib/sablon/processor.rb +1 -2
- data/lib/sablon/template.rb +12 -3
- data/lib/sablon/version.rb +1 -1
- data/test/content_test.rb +92 -0
- data/test/context_test.rb +15 -0
- data/test/executable_test.rb +0 -2
- data/test/expression_test.rb +0 -7
- data/test/fixtures/sablon_sample.docx +0 -0
- data/test/fixtures/sablon_template.docx +0 -0
- data/test/fixtures/shopping_list_context.json +2 -1
- data/test/fixtures/shopping_list_sample.docx +0 -0
- data/test/fixtures/shopping_list_template.docx +0 -0
- data/test/fixtures/xml/about_me_snippet.xml +1 -0
- data/test/fixtures/xml/complex_field.xml +24 -22
- data/test/fixtures/xml/edited_complex_field.xml +28 -26
- data/test/fixtures/xml/simple_field.xml +10 -8
- data/test/fixtures/xml/simple_fields.xml +14 -12
- data/test/mail_merge_parser_test.rb +48 -104
- data/test/processor_test.rb +6 -0
- data/test/sablon_test.rb +3 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 249d5af8b120f806c35a208e2804915c575b23e7
|
4
|
+
data.tar.gz: e7a006f1fcd0bbd7202d25681578c4fb327ed49b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39bc20fe127fbd6d8bc2737af0e5c1e559182815ce94798ac5c488c66029829148665befdafee6fec0dab3c1b9a241ec6326600dedd6518b8eb5e697e10ed2d3
|
7
|
+
data.tar.gz: ceb09efa9b30aa2a6ac69ef199f2f8f8452db76b9272c5940c9c10b437459bbd4fbff370d62adbba5a3bacda4eb8f79aa0d1abf2856176ef2e33a73e02f4ee89
|
data/README.md
CHANGED
@@ -61,6 +61,44 @@ This works for chained method calls and nested hash lookup as well:
|
|
61
61
|
«=buyer.address.street»
|
62
62
|
```
|
63
63
|
|
64
|
+
##### WordProcessingML
|
65
|
+
|
66
|
+
Generally Sablon tries to reuse the formatting defined in the template. However,
|
67
|
+
there are situations where more fine grained control is needed. Imagine you need
|
68
|
+
to insert a body of text containing different formats. If you can't decide the
|
69
|
+
format ahead of processing time (in the template) you can insert
|
70
|
+
[WordProcessingML](http://en.wikipedia.org/wiki/Microsoft_Office_XML_formats)
|
71
|
+
directly.
|
72
|
+
|
73
|
+
The template can use a simple insertion operation like so:
|
74
|
+
|
75
|
+
```
|
76
|
+
«=long_description»
|
77
|
+
```
|
78
|
+
|
79
|
+
The thing that changes is the context passed when processing the template:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
word_processing_ml = <<-XML
|
83
|
+
<w:p>
|
84
|
+
<w:r w:rsidRPr="00B97C39">
|
85
|
+
<w:rPr>
|
86
|
+
<w:b />
|
87
|
+
</w:rPr>
|
88
|
+
<w:t>this is bold text</w:t>
|
89
|
+
</w:r>
|
90
|
+
</w:p>
|
91
|
+
XML
|
92
|
+
context = {
|
93
|
+
long_description: Sablon.word_ml(word_processing_ml)
|
94
|
+
}
|
95
|
+
template.render_to_file File.expand_path("~/Desktop/output.docx"), context
|
96
|
+
```
|
97
|
+
|
98
|
+
**IMPORTANT:** This feature is very much *experimental*. Currently, this only
|
99
|
+
works if the insertion is the only thing inside the template paragraph. Other
|
100
|
+
content is discarded!
|
101
|
+
|
64
102
|
#### Conditionals
|
65
103
|
|
66
104
|
Sablon can render parts of the template conditonally based on the value of a
|
data/lib/sablon.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
require "sablon/version"
|
2
|
+
require "sablon/context"
|
2
3
|
require "sablon/template"
|
3
4
|
require "sablon/processor"
|
4
5
|
require "sablon/processor/section_properties"
|
5
6
|
require "sablon/parser/mail_merge"
|
6
7
|
require "sablon/operations"
|
8
|
+
require "sablon/content"
|
9
|
+
|
7
10
|
require 'zip'
|
8
11
|
require 'nokogiri'
|
9
12
|
|
@@ -14,4 +17,12 @@ module Sablon
|
|
14
17
|
def self.template(path)
|
15
18
|
Template.new(path)
|
16
19
|
end
|
20
|
+
|
21
|
+
def self.word_ml(xml)
|
22
|
+
Sablon::Content::WordML.new(xml)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.string(object)
|
26
|
+
Sablon::Content::String.new(object.to_s)
|
27
|
+
end
|
17
28
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Sablon
|
2
|
+
module Content
|
3
|
+
class String < Struct.new(:string)
|
4
|
+
include Sablon::Content
|
5
|
+
|
6
|
+
def append_to(paragraph, display_node)
|
7
|
+
string.scan(/[^\n]+|\n/).reverse.each do |part|
|
8
|
+
if part == "\n"
|
9
|
+
display_node.add_next_sibling Nokogiri::XML::Node.new "w:br", display_node.document
|
10
|
+
else
|
11
|
+
text_part = display_node.dup
|
12
|
+
text_part.content = part
|
13
|
+
display_node.add_next_sibling text_part
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class WordML < Struct.new(:xml)
|
20
|
+
include Sablon::Content
|
21
|
+
|
22
|
+
def append_to(paragraph, display_node)
|
23
|
+
Nokogiri::XML.fragment(xml).children.reverse.each do |child|
|
24
|
+
paragraph.add_next_sibling child
|
25
|
+
end
|
26
|
+
paragraph.remove
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Sablon
|
2
|
+
module Context
|
3
|
+
def self.transform(hash)
|
4
|
+
transform_hash(hash)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.transform_hash(hash)
|
8
|
+
Hash[hash.map{|k,v| transform_pair(k.to_s, v) }]
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.transform_pair(key, value)
|
12
|
+
if key =~ /\Awordml:(.+)\z/
|
13
|
+
[$1, Sablon.word_ml(value)]
|
14
|
+
else
|
15
|
+
transform_standard_key(key, value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.transform_standard_key(key, value)
|
20
|
+
case value
|
21
|
+
when Hash
|
22
|
+
[key, transform_hash(value)]
|
23
|
+
else
|
24
|
+
[key, value]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/sablon/operations.rb
CHANGED
@@ -3,7 +3,16 @@ module Sablon
|
|
3
3
|
module Statement
|
4
4
|
class Insertion < Struct.new(:expr, :field)
|
5
5
|
def evaluate(context)
|
6
|
-
field.replace(expr.evaluate(context))
|
6
|
+
field.replace(wrap_content(expr.evaluate(context)))
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def wrap_content(value)
|
11
|
+
if value.is_a?(Sablon::Content)
|
12
|
+
value
|
13
|
+
else
|
14
|
+
Sablon.string(value)
|
15
|
+
end
|
7
16
|
end
|
8
17
|
end
|
9
18
|
|
@@ -8,17 +8,10 @@ module Sablon
|
|
8
8
|
end
|
9
9
|
|
10
10
|
private
|
11
|
-
def replace_field_display(node,
|
11
|
+
def replace_field_display(node, content)
|
12
|
+
paragraph = node.ancestors(".//w:p").first
|
12
13
|
display_node = node.search(".//w:t").first
|
13
|
-
|
14
|
-
if part == "\n"
|
15
|
-
display_node.add_next_sibling Nokogiri::XML::Node.new "w:br", display_node.document
|
16
|
-
else
|
17
|
-
text_part = display_node.dup
|
18
|
-
text_part.content = part
|
19
|
-
display_node.add_next_sibling text_part
|
20
|
-
end
|
21
|
-
end
|
14
|
+
content.append_to(paragraph, display_node)
|
22
15
|
display_node.remove
|
23
16
|
end
|
24
17
|
end
|
@@ -29,8 +22,8 @@ module Sablon
|
|
29
22
|
@raw_expression = @nodes.flat_map {|n| n.search(".//w:instrText").map(&:content) }.join
|
30
23
|
end
|
31
24
|
|
32
|
-
def replace(
|
33
|
-
replace_field_display(pattern_node,
|
25
|
+
def replace(content)
|
26
|
+
replace_field_display(pattern_node, content)
|
34
27
|
(@nodes - [pattern_node]).each(&:remove)
|
35
28
|
end
|
36
29
|
|
@@ -54,8 +47,8 @@ module Sablon
|
|
54
47
|
@raw_expression = @node["w:instr"]
|
55
48
|
end
|
56
49
|
|
57
|
-
def replace(
|
58
|
-
replace_field_display(@node,
|
50
|
+
def replace(content)
|
51
|
+
replace_field_display(@node, content)
|
59
52
|
@node.replace(@node.children)
|
60
53
|
end
|
61
54
|
|
data/lib/sablon/processor.rb
CHANGED
@@ -3,8 +3,7 @@ module Sablon
|
|
3
3
|
class Processor
|
4
4
|
def self.process(xml_node, context, properties = {})
|
5
5
|
processor = new(parser)
|
6
|
-
|
7
|
-
processor.manipulate xml_node, stringified_context
|
6
|
+
processor.manipulate xml_node, Sablon::Context.transform(context)
|
8
7
|
processor.write_properties xml_node, properties if properties.any?
|
9
8
|
xml_node
|
10
9
|
end
|
data/lib/sablon/template.rb
CHANGED
@@ -6,7 +6,7 @@ module Sablon
|
|
6
6
|
|
7
7
|
# Same as +render_to_string+ but writes the processed template to +output_path+.
|
8
8
|
def render_to_file(output_path, context, properties = {})
|
9
|
-
File.open(output_path, '
|
9
|
+
File.open(output_path, 'wb') do |f|
|
10
10
|
f.write render_to_string(context, properties)
|
11
11
|
end
|
12
12
|
end
|
@@ -24,14 +24,23 @@ module Sablon
|
|
24
24
|
out.put_next_entry(entry_name)
|
25
25
|
content = entry.get_input_stream.read
|
26
26
|
if entry_name == 'word/document.xml'
|
27
|
-
out.write(
|
27
|
+
out.write(process(content, context, properties))
|
28
28
|
elsif entry_name =~ /word\/header\d*\.xml/ || entry_name =~ /word\/footer\d*\.xml/
|
29
|
-
out.write(
|
29
|
+
out.write(process(content, context))
|
30
30
|
else
|
31
31
|
out.write(content)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
|
+
|
37
|
+
# process the sablon xml template with the given +context+.
|
38
|
+
#
|
39
|
+
# IMPORTANT: Open Office does not ignore whitespace around tags.
|
40
|
+
# We need to render the xml without indent and whitespace.
|
41
|
+
def process(content, context, *args)
|
42
|
+
document = Nokogiri::XML(content)
|
43
|
+
Processor.process(document, context, *args).to_xml(indent: 0, save_with: 0)
|
44
|
+
end
|
36
45
|
end
|
37
46
|
end
|
data/lib/sablon/version.rb
CHANGED
@@ -0,0 +1,92 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
module ContentTestSetup
|
5
|
+
def setup
|
6
|
+
super
|
7
|
+
@template_text = '<w:p><span>template</span></w:p><w:p>AFTER</w:p>'
|
8
|
+
@document = Nokogiri::XML.fragment(@template_text)
|
9
|
+
@paragraph = @document.children.first
|
10
|
+
@node = @document.css("span").first
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def assert_xml_equal(expected, document)
|
15
|
+
assert_equal expected, document.to_xml(indent: 0, save_with: 0)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ContentStringTest < Sablon::TestCase
|
20
|
+
include ContentTestSetup
|
21
|
+
|
22
|
+
def test_single_line_string
|
23
|
+
Sablon.string("a normal string").append_to @paragraph, @node
|
24
|
+
|
25
|
+
output = <<-XML.strip
|
26
|
+
<w:p><span>template</span><span>a normal string</span></w:p><w:p>AFTER</w:p>
|
27
|
+
XML
|
28
|
+
assert_xml_equal output, @document
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_numeric_string
|
32
|
+
Sablon.string(42).append_to @paragraph, @node
|
33
|
+
|
34
|
+
output = <<-XML.strip
|
35
|
+
<w:p><span>template</span><span>42</span></w:p><w:p>AFTER</w:p>
|
36
|
+
XML
|
37
|
+
assert_xml_equal output, @document
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_string_with_newlines
|
41
|
+
Sablon.string("a\nmultiline\n\nstring").append_to @paragraph, @node
|
42
|
+
|
43
|
+
output = <<-XML.strip.gsub("\n", "")
|
44
|
+
<w:p>
|
45
|
+
<span>template</span>
|
46
|
+
<span>a</span>
|
47
|
+
<w:br/>
|
48
|
+
<span>multiline</span>
|
49
|
+
<w:br/>
|
50
|
+
<w:br/>
|
51
|
+
<span>string</span>
|
52
|
+
</w:p>
|
53
|
+
<w:p>AFTER</w:p>
|
54
|
+
XML
|
55
|
+
|
56
|
+
assert_xml_equal output, @document
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_blank_string
|
60
|
+
Sablon.string("").append_to @paragraph, @node
|
61
|
+
|
62
|
+
assert_xml_equal @template_text, @document
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ContentWordMLTest < Sablon::TestCase
|
67
|
+
include ContentTestSetup
|
68
|
+
|
69
|
+
def test_blank_word_ml
|
70
|
+
Sablon.word_ml("").append_to @paragraph, @node
|
71
|
+
|
72
|
+
assert_xml_equal "<w:p>AFTER</w:p>", @document
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_inserts_word_ml_into_the_document
|
76
|
+
@word_ml = '<w:p><w:r><w:t xml:space="preserve">a </w:t></w:r></w:p>'
|
77
|
+
Sablon.word_ml(@word_ml).append_to @paragraph, @node
|
78
|
+
|
79
|
+
output = <<-XML.strip.gsub("\n", "")
|
80
|
+
<w:p>
|
81
|
+
<w:r><w:t xml:space=\"preserve\">a </w:t></w:r>
|
82
|
+
</w:p>
|
83
|
+
<w:p>AFTER</w:p>
|
84
|
+
XML
|
85
|
+
|
86
|
+
assert_xml_equal output, @document
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_inserting_word_ml_multiple_times_into_same_paragraph
|
90
|
+
skip "Content::WordML currently removes the paragraph..."
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
class ContextTest < Sablon::TestCase
|
5
|
+
def test_converts_symbol_keys_to_string_keys
|
6
|
+
transformed = Sablon::Context.transform({a: 1, b: {c: 2, "d" => 3}})
|
7
|
+
assert_equal({"a"=>1, "b"=>{"c" =>2, "d"=>3}}, transformed)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_recognizes_wordml_keys
|
11
|
+
transformed = Sablon::Context.transform({"wordml:mykey" => "<w:p><w:p>", "otherkey" => "<nope>"})
|
12
|
+
assert_equal({ "mykey"=>Sablon.word_ml("<w:p><w:p>"),
|
13
|
+
"otherkey"=>"<nope>"}, transformed)
|
14
|
+
end
|
15
|
+
end
|
data/test/executable_test.rb
CHANGED
@@ -10,9 +10,7 @@ class ExecutableTest < Sablon::TestCase
|
|
10
10
|
@template_path = @base_path + "fixtures/shopping_list_template.docx"
|
11
11
|
@context_path = @base_path + "fixtures/shopping_list_context.json"
|
12
12
|
@executable_path = @base_path + '../bin/sablon'
|
13
|
-
end
|
14
13
|
|
15
|
-
def teardown
|
16
14
|
@output_path.delete if @output_path.exist?
|
17
15
|
end
|
18
16
|
|
data/test/expression_test.rb
CHANGED
@@ -29,13 +29,6 @@ class LookupOrMethodCallTest < Sablon::TestCase
|
|
29
29
|
assert_equal "Jack", expr.evaluate("user" => user)
|
30
30
|
end
|
31
31
|
|
32
|
-
def test_calls_perform_lookup_on_hash_with_symbol_keys
|
33
|
-
skip
|
34
|
-
user = {first_name: "Jack"}
|
35
|
-
expr = Sablon::Expression.parse("user.first_name")
|
36
|
-
assert_equal "Jack", expr.evaluate("user" => user)
|
37
|
-
end
|
38
|
-
|
39
32
|
def test_inspect
|
40
33
|
expr = Sablon::Expression.parse("user.first_name")
|
41
34
|
assert_equal "«user.first_name»", expr.inspect
|
Binary file
|
Binary file
|
@@ -4,7 +4,8 @@
|
|
4
4
|
"author": {
|
5
5
|
"first_name": "John",
|
6
6
|
"last_name": "Doe"
|
7
|
-
}
|
7
|
+
},
|
8
|
+
"wordml:description": "<w:p><w:r><w:t xml:space=\"preserve\">A list of items to buy at the next </w:t></w:r><w:r><w:rPr><w:b /></w:rPr><w:t>Shop</w:t></w:r><w:r><w:t xml:space=\"preserve\">.</w:t></w:r></w:p>"
|
8
9
|
},
|
9
10
|
"items": [
|
10
11
|
{"name": "Milk", "amount": "1L"},
|
Binary file
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
<w:p><w:r><w:t xml:space="preserve">I </w:t></w:r><w:r><w:rPr><w:i /></w:rPr><w:t>am</w:t></w:r><w:r><w:t xml:space="preserve"> </w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">a </w:t></w:r><w:r><w:rPr><w:b /></w:rPr><w:t>Software Developer</w:t></w:r></w:p>
|
@@ -1,22 +1,24 @@
|
|
1
|
-
<w:
|
2
|
-
<w:r
|
3
|
-
<w:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
<w:
|
8
|
-
|
9
|
-
</w:
|
10
|
-
|
11
|
-
<w:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
<w:
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
<w:
|
20
|
-
|
21
|
-
|
22
|
-
|
1
|
+
<w:p>
|
2
|
+
<w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
|
3
|
+
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
4
|
+
<w:rPr><w:b/></w:rPr>
|
5
|
+
<w:fldChar w:fldCharType="begin"/>
|
6
|
+
</w:r>
|
7
|
+
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
8
|
+
<w:rPr><w:b/></w:rPr>
|
9
|
+
<w:instrText xml:space="preserve"> MERGEFIELD =last_name \* MERGEFORMAT </w:instrText>
|
10
|
+
</w:r>
|
11
|
+
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
12
|
+
<w:rPr><w:b/></w:rPr>
|
13
|
+
<w:fldChar w:fldCharType="separate"/>
|
14
|
+
</w:r>
|
15
|
+
<w:r w:rsidR="004B49F0">
|
16
|
+
<w:rPr><w:b/><w:noProof/></w:rPr>
|
17
|
+
<w:t>«=last_name»</w:t>
|
18
|
+
</w:r>
|
19
|
+
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
20
|
+
<w:rPr><w:b/></w:rPr>
|
21
|
+
<w:fldChar w:fldCharType="end"/>
|
22
|
+
</w:r>
|
23
|
+
<w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
|
24
|
+
</w:p>
|
@@ -1,26 +1,28 @@
|
|
1
|
-
<w:
|
2
|
-
<w:
|
3
|
-
</w:
|
4
|
-
|
5
|
-
<w:
|
6
|
-
|
7
|
-
|
8
|
-
<w:
|
9
|
-
</w:
|
10
|
-
|
11
|
-
<w:
|
12
|
-
|
13
|
-
|
14
|
-
<w:
|
15
|
-
</w:
|
16
|
-
|
17
|
-
<w:
|
18
|
-
|
19
|
-
|
20
|
-
<w:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
<w:
|
25
|
-
|
26
|
-
|
1
|
+
<w:p>
|
2
|
+
<w:r>
|
3
|
+
<w:t xml:space="preserve">Hello! My Name is </w:t>
|
4
|
+
</w:r>
|
5
|
+
<w:r w:rsidR="003C4780">
|
6
|
+
<w:fldChar w:fldCharType="begin" />
|
7
|
+
</w:r>
|
8
|
+
<w:r w:rsidR="003C4780">
|
9
|
+
<w:instrText xml:space="preserve"> MERGEFIELD </w:instrText>
|
10
|
+
</w:r>
|
11
|
+
<w:r w:rsidR="003A4504">
|
12
|
+
<w:instrText>=</w:instrText>
|
13
|
+
</w:r>
|
14
|
+
<w:r w:rsidR="003C4780">
|
15
|
+
<w:instrText xml:space="preserve">first_name \* MERGEFORMAT </w:instrText>
|
16
|
+
</w:r>
|
17
|
+
<w:r w:rsidR="003C4780">
|
18
|
+
<w:fldChar w:fldCharType="separate" />
|
19
|
+
</w:r>
|
20
|
+
<w:r w:rsidR="00441382">
|
21
|
+
<w:rPr><w:noProof /></w:rPr>
|
22
|
+
<w:t>«=person.first_name»</w:t>
|
23
|
+
</w:r>
|
24
|
+
<w:r w:rsidR="003C4780">
|
25
|
+
<w:fldChar w:fldCharType="end" />
|
26
|
+
</w:r>
|
27
|
+
<w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
|
28
|
+
</w:p>
|
@@ -1,8 +1,10 @@
|
|
1
|
-
<w:
|
2
|
-
<w:
|
3
|
-
<w:
|
4
|
-
<w:
|
5
|
-
|
6
|
-
|
7
|
-
</w:
|
8
|
-
|
1
|
+
<w:p>
|
2
|
+
<w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
|
3
|
+
<w:fldSimple w:instr=" MERGEFIELD =first_name \* MERGEFORMAT ">
|
4
|
+
<w:r w:rsidR="004B49F0">
|
5
|
+
<w:rPr><w:noProof/></w:rPr>
|
6
|
+
<w:t>«=first_name»</w:t>
|
7
|
+
</w:r>
|
8
|
+
</w:fldSimple>
|
9
|
+
<w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
|
10
|
+
</w:p>
|
@@ -1,12 +1,14 @@
|
|
1
|
-
<w:
|
2
|
-
<w:
|
3
|
-
<w:
|
4
|
-
|
5
|
-
|
6
|
-
</w:
|
7
|
-
|
8
|
-
<w:
|
9
|
-
<w:
|
10
|
-
|
11
|
-
|
12
|
-
</w:
|
1
|
+
<w:p>
|
2
|
+
<w:fldSimple w:instr=" MERGEFIELD =first_name \* MERGEFORMAT ">
|
3
|
+
<w:r w:rsidR="004B49F0">
|
4
|
+
<w:rPr><w:noProof/></w:rPr>
|
5
|
+
<w:t>«=first_name»</w:t>
|
6
|
+
</w:r>
|
7
|
+
</w:fldSimple>
|
8
|
+
<w:fldSimple w:instr=" MERGEFIELD =last_name \* MERGEFORMAT ">
|
9
|
+
<w:r w:rsidR="004B49F0">
|
10
|
+
<w:rPr><w:noProof/></w:rPr>
|
11
|
+
<w:t>«=last_name»</w:t>
|
12
|
+
</w:r>
|
13
|
+
</w:fldSimple>
|
14
|
+
</w:p>
|
@@ -19,7 +19,7 @@ module MailMergeParser
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def body_xml
|
22
|
-
document.search(".//w:body").children.map(&:to_xml).join
|
22
|
+
document.search(".//w:body").children.map(&:to_xml).map(&:strip).join
|
23
23
|
end
|
24
24
|
|
25
25
|
def document
|
@@ -35,59 +35,28 @@ module MailMergeParser
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def test_replace
|
38
|
-
field.replace("Hello")
|
39
|
-
xml = <<-xml
|
40
|
-
|
41
|
-
<w:rPr><w:noProof/></w:rPr>
|
42
|
-
<w:t>Hello</w:t>
|
43
|
-
</w:r>
|
44
|
-
xml
|
45
|
-
assert_equal body_xml.strip, xml.strip
|
46
|
-
end
|
47
|
-
|
48
|
-
def test_replace_with_newlines
|
49
|
-
field.replace("First\nSecond\n\nThird")
|
50
|
-
xml = <<-xml
|
51
|
-
<w:r w:rsidR=\"004B49F0\">
|
52
|
-
<w:rPr><w:noProof/></w:rPr>
|
53
|
-
<w:t>First</w:t><w:br/><w:t>Second</w:t><w:br/><w:br/><w:t>Third</w:t>
|
54
|
-
</w:r>
|
55
|
-
xml
|
56
|
-
assert_equal body_xml.strip, xml.strip
|
57
|
-
end
|
58
|
-
|
59
|
-
def test_replace_with_nil
|
60
|
-
field.replace(nil)
|
61
|
-
xml = <<-xml
|
38
|
+
field.replace(Sablon.string("Hello"))
|
39
|
+
xml = <<-xml.strip
|
40
|
+
<w:p>
|
62
41
|
<w:r w:rsidR=\"004B49F0\">
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
assert_equal body_xml.gsub(/^\s+$/,'').strip, xml.strip
|
68
|
-
end
|
69
|
-
|
70
|
-
def test_replace_with_numeric
|
71
|
-
field.replace(45)
|
72
|
-
xml = <<-xml
|
73
|
-
<w:r w:rsidR=\"004B49F0\">
|
74
|
-
<w:rPr><w:noProof/></w:rPr>
|
75
|
-
<w:t>45</w:t>
|
76
|
-
</w:r>
|
42
|
+
<w:rPr><w:noProof/></w:rPr>
|
43
|
+
<w:t>Hello</w:t>
|
44
|
+
</w:r>
|
45
|
+
</w:p>
|
77
46
|
xml
|
78
|
-
|
47
|
+
assert_equal xml, body_xml
|
79
48
|
end
|
80
49
|
|
81
50
|
private
|
82
51
|
|
83
52
|
def xml
|
84
|
-
xml = <<-xml
|
85
|
-
<w:fldSimple w:instr=" MERGEFIELD =first_name \\* MERGEFORMAT ">
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
</w:fldSimple>
|
53
|
+
xml = <<-xml.strip
|
54
|
+
<w:p><w:fldSimple w:instr=" MERGEFIELD =first_name \\* MERGEFORMAT ">
|
55
|
+
<w:r w:rsidR="004B49F0">
|
56
|
+
<w:rPr><w:noProof/></w:rPr>
|
57
|
+
<w:t>«=first_name»</w:t>
|
58
|
+
</w:r>
|
59
|
+
</w:fldSimple></w:p>
|
91
60
|
xml
|
92
61
|
wrap(xml)
|
93
62
|
end
|
@@ -101,75 +70,50 @@ xml
|
|
101
70
|
end
|
102
71
|
|
103
72
|
def test_replace
|
104
|
-
field.replace("Hello")
|
105
|
-
xml = <<-xml
|
106
|
-
<w:
|
107
|
-
<w:rPr>
|
108
|
-
<w:b/>
|
109
|
-
<w:noProof/>
|
110
|
-
</w:rPr>
|
111
|
-
<w:t>Hello</w:t>
|
112
|
-
</w:r>
|
113
|
-
xml
|
114
|
-
assert_equal body_xml.strip, xml.strip
|
115
|
-
end
|
73
|
+
field.replace(Sablon.string("Hello"))
|
74
|
+
xml = <<-xml.strip
|
75
|
+
<w:p>
|
116
76
|
|
117
|
-
def test_replace_with_newlines
|
118
|
-
field.replace("First\nSecond\n\nThird")
|
119
|
-
xml = <<-xml
|
120
|
-
<w:r w:rsidR="004B49F0">
|
121
|
-
<w:rPr>
|
122
|
-
<w:b/>
|
123
|
-
<w:noProof/>
|
124
|
-
</w:rPr>
|
125
|
-
<w:t>First</w:t><w:br/><w:t>Second</w:t><w:br/><w:br/><w:t>Third</w:t>
|
126
|
-
</w:r>
|
127
|
-
xml
|
128
|
-
assert_equal body_xml.strip, xml.strip
|
129
|
-
end
|
130
|
-
|
131
|
-
def test_replace_with_nil
|
132
|
-
field.replace(nil)
|
133
77
|
|
134
|
-
xml = <<-xml
|
135
78
|
<w:r w:rsidR="004B49F0">
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
79
|
+
<w:rPr>
|
80
|
+
<w:b/>
|
81
|
+
<w:noProof/>
|
82
|
+
</w:rPr>
|
83
|
+
<w:t>Hello</w:t>
|
141
84
|
</w:r>
|
85
|
+
</w:p>
|
142
86
|
xml
|
143
|
-
assert_equal body_xml.
|
87
|
+
assert_equal body_xml.strip, xml.strip
|
144
88
|
end
|
145
89
|
|
146
90
|
private
|
147
91
|
|
148
92
|
def xml
|
149
|
-
xml = <<-xml
|
150
|
-
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
151
|
-
|
152
|
-
|
93
|
+
xml = <<-xml.strip
|
94
|
+
<w:p><w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
95
|
+
<w:rPr><w:b/></w:rPr>
|
96
|
+
<w:fldChar w:fldCharType="begin"/>
|
153
97
|
</w:r>
|
154
98
|
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
155
|
-
|
156
|
-
|
99
|
+
<w:rPr><w:b/></w:rPr>
|
100
|
+
<w:instrText xml:space="preserve"> MERGEFIELD =last_name \\* MERGEFORMAT </w:instrText>
|
157
101
|
</w:r>
|
158
102
|
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
159
|
-
|
160
|
-
|
103
|
+
<w:rPr><w:b/></w:rPr>
|
104
|
+
<w:fldChar w:fldCharType="separate"/>
|
161
105
|
</w:r>
|
162
106
|
<w:r w:rsidR="004B49F0">
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
107
|
+
<w:rPr>
|
108
|
+
<w:b/>
|
109
|
+
<w:noProof/>
|
110
|
+
</w:rPr>
|
111
|
+
<w:t>«=last_name»</w:t>
|
168
112
|
</w:r>
|
169
113
|
<w:r w:rsidR="00BE47B1" w:rsidRPr="00BE47B1">
|
170
|
-
|
171
|
-
|
172
|
-
</w:r>
|
114
|
+
<w:rPr><w:b/></w:rPr>
|
115
|
+
<w:fldChar w:fldCharType="end"/>
|
116
|
+
</w:r></w:p>
|
173
117
|
xml
|
174
118
|
wrap(xml)
|
175
119
|
end
|
@@ -240,12 +184,12 @@ xml
|
|
240
184
|
|
241
185
|
def xml
|
242
186
|
xml = <<-xml
|
243
|
-
<w:fldSimple w:instr=" MERGEFIELD =title \\* MERGEFORMAT ">
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
</w:fldSimple>
|
187
|
+
<w:p><w:fldSimple w:instr=" MERGEFIELD =title \\* MERGEFORMAT ">
|
188
|
+
<w:r w:rsidR="004B49F0">
|
189
|
+
<w:rPr><w:noProof/></w:rPr>
|
190
|
+
<w:t>«=title»</w:t>
|
191
|
+
</w:r>
|
192
|
+
</w:fldSimple></w:p>
|
249
193
|
xml
|
250
194
|
wrap(xml)
|
251
195
|
end
|
data/test/processor_test.rb
CHANGED
@@ -17,12 +17,14 @@ class ProcessorTest < Sablon::TestCase
|
|
17
17
|
|
18
18
|
assert_equal "Hello! My Name is Jack , nice to meet you.", text(result)
|
19
19
|
assert_xml_equal <<-document, result
|
20
|
+
<w:p>
|
20
21
|
<w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
|
21
22
|
<w:r w:rsidR="004B49F0">
|
22
23
|
<w:rPr><w:noProof/></w:rPr>
|
23
24
|
<w:t>Jack</w:t>
|
24
25
|
</w:r>
|
25
26
|
<w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
|
27
|
+
</w:p>
|
26
28
|
document
|
27
29
|
end
|
28
30
|
|
@@ -36,12 +38,14 @@ class ProcessorTest < Sablon::TestCase
|
|
36
38
|
|
37
39
|
assert_equal "Hello! My Name is Zane , nice to meet you.", text(result)
|
38
40
|
assert_xml_equal <<-document, result
|
41
|
+
<w:p>
|
39
42
|
<w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
|
40
43
|
<w:r w:rsidR="004B49F0">
|
41
44
|
<w:rPr><w:b/><w:noProof/></w:rPr>
|
42
45
|
<w:t>Zane</w:t>
|
43
46
|
</w:r>
|
44
47
|
<w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
|
48
|
+
</w:p>
|
45
49
|
document
|
46
50
|
end
|
47
51
|
|
@@ -50,12 +54,14 @@ class ProcessorTest < Sablon::TestCase
|
|
50
54
|
|
51
55
|
assert_equal "Hello! My Name is Daniel , nice to meet you.", text(result)
|
52
56
|
assert_xml_equal <<-document, result
|
57
|
+
<w:p>
|
53
58
|
<w:r><w:t xml:space="preserve">Hello! My Name is </w:t></w:r>
|
54
59
|
<w:r w:rsidR="00441382">
|
55
60
|
<w:rPr><w:noProof/></w:rPr>
|
56
61
|
<w:t>Daniel</w:t>
|
57
62
|
</w:r>
|
58
63
|
<w:r w:rsidR="00BE47B1"><w:t xml:space="preserve">, nice to meet you.</w:t></w:r>
|
64
|
+
</w:p>
|
59
65
|
document
|
60
66
|
end
|
61
67
|
|
data/test/sablon_test.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
require "test_helper"
|
3
|
+
require "support/xml_snippets"
|
3
4
|
|
4
5
|
class SablonTest < Sablon::TestCase
|
5
6
|
include Sablon::Test::Assertions
|
7
|
+
include XMLSnippets
|
6
8
|
|
7
9
|
def setup
|
8
10
|
super
|
@@ -21,6 +23,7 @@ class SablonTest < Sablon::TestCase
|
|
21
23
|
author: "Yves Senn",
|
22
24
|
title: "Letter of application",
|
23
25
|
person: person,
|
26
|
+
about_me: Sablon.word_ml(snippet("about_me_snippet").strip),
|
24
27
|
items: [item.new("1.", "Ruby", "★" * 5), item.new("2.", "Java", "★" * 1), item.new("3.", "Python", "★" * 3)],
|
25
28
|
career: [position.new("1999 - 2006", "Junior Java Engineer", "Lorem ipsum dolor\nsit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."),
|
26
29
|
position.new("2006 - 2013", "Senior Ruby Developer", "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo."),
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sablon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yves Senn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-04-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -111,6 +111,8 @@ files:
|
|
111
111
|
- Rakefile
|
112
112
|
- bin/sablon
|
113
113
|
- lib/sablon.rb
|
114
|
+
- lib/sablon/content.rb
|
115
|
+
- lib/sablon/context.rb
|
114
116
|
- lib/sablon/operations.rb
|
115
117
|
- lib/sablon/parser/mail_merge.rb
|
116
118
|
- lib/sablon/processor.rb
|
@@ -122,6 +124,8 @@ files:
|
|
122
124
|
- misc/output.png
|
123
125
|
- misc/template.png
|
124
126
|
- sablon.gemspec
|
127
|
+
- test/content_test.rb
|
128
|
+
- test/context_test.rb
|
125
129
|
- test/executable_test.rb
|
126
130
|
- test/expression_test.rb
|
127
131
|
- test/fixtures/sablon_sample.docx
|
@@ -129,6 +133,7 @@ files:
|
|
129
133
|
- test/fixtures/shopping_list_context.json
|
130
134
|
- test/fixtures/shopping_list_sample.docx
|
131
135
|
- test/fixtures/shopping_list_template.docx
|
136
|
+
- test/fixtures/xml/about_me_snippet.xml
|
132
137
|
- test/fixtures/xml/complex_field.xml
|
133
138
|
- test/fixtures/xml/conditional.xml
|
134
139
|
- test/fixtures/xml/conditional_with_predicate.xml
|
@@ -175,6 +180,8 @@ signing_key:
|
|
175
180
|
specification_version: 4
|
176
181
|
summary: docx tempalte processor
|
177
182
|
test_files:
|
183
|
+
- test/content_test.rb
|
184
|
+
- test/context_test.rb
|
178
185
|
- test/executable_test.rb
|
179
186
|
- test/expression_test.rb
|
180
187
|
- test/fixtures/sablon_sample.docx
|
@@ -182,6 +189,7 @@ test_files:
|
|
182
189
|
- test/fixtures/shopping_list_context.json
|
183
190
|
- test/fixtures/shopping_list_sample.docx
|
184
191
|
- test/fixtures/shopping_list_template.docx
|
192
|
+
- test/fixtures/xml/about_me_snippet.xml
|
185
193
|
- test/fixtures/xml/complex_field.xml
|
186
194
|
- test/fixtures/xml/conditional.xml
|
187
195
|
- test/fixtures/xml/conditional_with_predicate.xml
|