sablon 0.0.18 → 0.0.19.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -3
- data/Gemfile.lock +5 -5
- data/README.md +33 -1
- data/lib/sablon.rb +8 -3
- data/lib/sablon/content.rb +17 -0
- data/lib/sablon/html/ast.rb +130 -0
- data/lib/sablon/html/converter.rb +133 -0
- data/lib/sablon/html/visitor.rb +26 -0
- data/lib/sablon/numbering.rb +28 -0
- data/lib/sablon/processor/document.rb +193 -0
- data/lib/sablon/processor/numbering.rb +47 -0
- data/lib/sablon/processor/section_properties.rb +1 -1
- data/lib/sablon/template.rb +8 -4
- data/lib/sablon/version.rb +1 -1
- data/test/fixtures/html_sample.docx +0 -0
- data/test/fixtures/insertion_template.docx +0 -0
- data/test/fixtures/insertion_template_no_styles.docx +0 -0
- data/test/html/converter_test.rb +303 -0
- data/test/html_test.rb +45 -0
- data/test/{processor_test.rb → processor/document_test.rb} +2 -2
- data/test/test_helper.rb +4 -0
- metadata +22 -7
- data/lib/sablon/processor.rb +0 -191
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a08176b5a9b4646da6f24b4aff58ac09dcbc7cd
|
4
|
+
data.tar.gz: f2549ddf6142a9ffb02dde86b6a13d9eb38cfbbd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f54003471153af06f163732d84eeab5768e7430f744702062d6868a9ca9f1be6847875b141f7e69ed6e5d4c62957d2f8853dc058696d2bf6234beee9b74a6fd
|
7
|
+
data.tar.gz: 25731c1257863f69c9b68586041a35e0c2ca85b1d85e01bc6c8c14a2ec569920aa57658a40fc0d7f002681181eba59c1b71e4c0e37f17c3b83157b42f8f5512d
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
sablon (0.0.
|
4
|
+
sablon (0.0.19.beta1)
|
5
5
|
nokogiri (>= 1.6.0)
|
6
6
|
redcarpet (>= 3.2)
|
7
7
|
rubyzip (>= 1.1)
|
@@ -9,12 +9,12 @@ PATH
|
|
9
9
|
GEM
|
10
10
|
remote: https://rubygems.org/
|
11
11
|
specs:
|
12
|
-
|
12
|
+
mini_portile2 (2.0.0)
|
13
13
|
minitest (5.8.0)
|
14
|
-
nokogiri (1.6.
|
15
|
-
|
14
|
+
nokogiri (1.6.7.1)
|
15
|
+
mini_portile2 (~> 2.0.0.rc2)
|
16
16
|
rake (10.4.2)
|
17
|
-
redcarpet (3.3.
|
17
|
+
redcarpet (3.3.3)
|
18
18
|
rubyzip (1.1.7)
|
19
19
|
xml-simple (1.1.5)
|
20
20
|
|
data/README.md
CHANGED
@@ -100,7 +100,7 @@ IMPORTANT: This feature is very much *experimental*. Currently, the insertion
|
|
100
100
|
will replace the containing paragraph. This means that other content in the same
|
101
101
|
paragraph is discarded.
|
102
102
|
|
103
|
-
##### Markdown
|
103
|
+
##### Markdown [experimental]
|
104
104
|
|
105
105
|
Similar to WordProcessingML it's possible to use markdown while processing the
|
106
106
|
tempalte. You don't need to modify your templates, a simple insertion operation
|
@@ -138,10 +138,42 @@ etc. according to the header level. Ordered lists will use the style
|
|
138
138
|
supported. Normal text paragraphs use the style `Paragraph`. It's not necessary
|
139
139
|
to have that style in the template. Word will fall back to using the `Normal` style.
|
140
140
|
|
141
|
+
IMPORTANT: This feature is very much *experimental*. Currently, the insertion
|
142
|
+
will replace the containing paragraph. This means that other content in the same
|
143
|
+
paragraph is discarded. In the near future this feature will most likely be
|
144
|
+
implemented on top of HTML insertion.
|
145
|
+
|
146
|
+
##### HTML [experimental]
|
147
|
+
|
148
|
+
Similar to WordProcessingML it's possible to use html as input while processing the
|
149
|
+
tempalte. You don't need to modify your templates, a simple insertion operation
|
150
|
+
is sufficient:
|
151
|
+
|
152
|
+
```
|
153
|
+
«=article.body»
|
154
|
+
```
|
155
|
+
|
156
|
+
To use HTML insertion prepare the context like so:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
html_body = <<-HTML
|
160
|
+
<div>This text can contain <em>additional formatting</em>
|
161
|
+
according to the <strong>Markdown</strongstrong> specification.</div>
|
162
|
+
HTML
|
163
|
+
context = {
|
164
|
+
article: { html_body: Sablon.content(:html, html_body) }
|
165
|
+
}
|
166
|
+
template.render_to_file File.expand_path("~/Desktop/output.docx"), context
|
167
|
+
```
|
168
|
+
|
169
|
+
Currently HTML insertion is very limited and strongly focused on the HTML
|
170
|
+
generated by [Trix editor](https://github.com/basecamp/trix).
|
171
|
+
|
141
172
|
IMPORTANT: This feature is very much *experimental*. Currently, the insertion
|
142
173
|
will replace the containing paragraph. This means that other content in the same
|
143
174
|
paragraph is discarded.
|
144
175
|
|
176
|
+
|
145
177
|
#### Conditionals
|
146
178
|
|
147
179
|
Sablon can render parts of the template conditonally based on the value of a
|
data/lib/sablon.rb
CHANGED
@@ -1,14 +1,19 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'zip'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
1
5
|
require "sablon/version"
|
6
|
+
require "sablon/numbering"
|
2
7
|
require "sablon/context"
|
3
8
|
require "sablon/template"
|
4
|
-
require "sablon/processor"
|
9
|
+
require "sablon/processor/document"
|
5
10
|
require "sablon/processor/section_properties"
|
11
|
+
require "sablon/processor/numbering"
|
6
12
|
require "sablon/parser/mail_merge"
|
7
13
|
require "sablon/operations"
|
14
|
+
require "sablon/html/converter"
|
8
15
|
require "sablon/content"
|
9
16
|
|
10
|
-
require 'zip'
|
11
|
-
require 'nokogiri'
|
12
17
|
require 'redcarpet'
|
13
18
|
require "sablon/redcarpet/render/word_ml"
|
14
19
|
|
data/lib/sablon/content.rb
CHANGED
@@ -93,8 +93,25 @@ module Sablon
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
+
class HTML < Struct.new(:word_ml)
|
97
|
+
include Sablon::Content
|
98
|
+
def self.id; :html end
|
99
|
+
def self.wraps?(value) false end
|
100
|
+
|
101
|
+
def initialize(html)
|
102
|
+
converter = HTMLConverter.new
|
103
|
+
word_ml = Sablon.content(:word_ml, converter.process(html))
|
104
|
+
super word_ml
|
105
|
+
end
|
106
|
+
|
107
|
+
def append_to(*args)
|
108
|
+
word_ml.append_to(*args)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
96
112
|
register Sablon::Content::String
|
97
113
|
register Sablon::Content::WordML
|
98
114
|
register Sablon::Content::Markdown
|
115
|
+
register Sablon::Content::HTML
|
99
116
|
end
|
100
117
|
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Sablon
|
2
|
+
class HTMLConverter
|
3
|
+
class Node
|
4
|
+
def accept(visitor)
|
5
|
+
visitor.visit(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.node_name
|
9
|
+
@node_name ||= name.split('::').last
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Collection < Node
|
14
|
+
attr_reader :nodes
|
15
|
+
def initialize(nodes)
|
16
|
+
@nodes = nodes
|
17
|
+
end
|
18
|
+
|
19
|
+
def accept(visitor)
|
20
|
+
super
|
21
|
+
@nodes.each do |node|
|
22
|
+
node.accept(visitor)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_docx
|
27
|
+
nodes.map(&:to_docx).join
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Root < Collection
|
32
|
+
def to_a
|
33
|
+
nodes
|
34
|
+
end
|
35
|
+
|
36
|
+
def grep(pattern)
|
37
|
+
visitor = GrepVisitor.new(pattern)
|
38
|
+
accept(visitor)
|
39
|
+
visitor.result
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Paragraph < Node
|
44
|
+
attr_accessor :style, :runs
|
45
|
+
def initialize(style, runs)
|
46
|
+
@style, @runs = style, runs
|
47
|
+
end
|
48
|
+
|
49
|
+
PATTERN = <<-XML.gsub("\n", "")
|
50
|
+
<w:p>
|
51
|
+
<w:pPr>
|
52
|
+
<w:pStyle w:val="%s" />
|
53
|
+
%s
|
54
|
+
</w:pPr>
|
55
|
+
%s
|
56
|
+
</w:p>
|
57
|
+
XML
|
58
|
+
|
59
|
+
def to_docx
|
60
|
+
PATTERN % [style, ppr_docx, runs.to_docx]
|
61
|
+
end
|
62
|
+
|
63
|
+
def accept(visitor)
|
64
|
+
super
|
65
|
+
runs.accept(visitor)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def ppr_docx
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class ListParagraph < Paragraph
|
74
|
+
LIST_STYLE = <<-XML.gsub("\n", "")
|
75
|
+
<w:numPr>
|
76
|
+
<w:ilvl w:val="%s" />
|
77
|
+
<w:numId w:val="%s" />
|
78
|
+
</w:numPr>
|
79
|
+
XML
|
80
|
+
attr_accessor :numid, :ilvl
|
81
|
+
def initialize(style, runs, numid, ilvl)
|
82
|
+
super style, runs
|
83
|
+
@numid = numid
|
84
|
+
@ilvl = ilvl
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def ppr_docx
|
89
|
+
LIST_STYLE % [@ilvl, numid]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Text < Node
|
94
|
+
attr_reader :string
|
95
|
+
def initialize(string)
|
96
|
+
@string = string
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_docx
|
100
|
+
"<w:r>#{style_docx}<w:t xml:space=\"preserve\">#{normalized_string}</w:t></w:r>"
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
def style_docx
|
105
|
+
end
|
106
|
+
|
107
|
+
def normalized_string
|
108
|
+
string.tr("\u00A0", ' ')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class Bold < Text
|
113
|
+
def style_docx
|
114
|
+
'<w:rPr><w:b /></w:rPr>'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Italic < Text
|
119
|
+
def style_docx
|
120
|
+
'<w:rPr><w:i /></w:rPr>'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class Newline < Node
|
125
|
+
def to_docx
|
126
|
+
"<w:r><w:br/></w:r>"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require "sablon/html/ast"
|
2
|
+
require "sablon/html/visitor"
|
3
|
+
|
4
|
+
module Sablon
|
5
|
+
class HTMLConverter
|
6
|
+
class ASTBuilder
|
7
|
+
Layer = Struct.new(:items, :ilvl)
|
8
|
+
|
9
|
+
def initialize(nodes)
|
10
|
+
@layers = [Layer.new(nodes, false)]
|
11
|
+
@root = Root.new([])
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_ast
|
15
|
+
@root
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_layer(ilvl: false)
|
19
|
+
@layers.push Layer.new([], ilvl)
|
20
|
+
end
|
21
|
+
|
22
|
+
def next
|
23
|
+
current_layer.items.shift
|
24
|
+
end
|
25
|
+
|
26
|
+
def push(node)
|
27
|
+
@layers.last.items.push node
|
28
|
+
end
|
29
|
+
|
30
|
+
def push_all(nodes)
|
31
|
+
nodes.each(&method(:push))
|
32
|
+
end
|
33
|
+
|
34
|
+
def done?
|
35
|
+
!current_layer.items.any?
|
36
|
+
end
|
37
|
+
|
38
|
+
def nested?
|
39
|
+
ilvl > 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def ilvl
|
43
|
+
@layers.select { |layer| layer.ilvl }.size - 1
|
44
|
+
end
|
45
|
+
|
46
|
+
def emit(node)
|
47
|
+
@root.nodes << node
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def current_layer
|
52
|
+
if @layers.any?
|
53
|
+
last_layer = @layers.last
|
54
|
+
if last_layer.items.any?
|
55
|
+
last_layer
|
56
|
+
else
|
57
|
+
@layers.pop
|
58
|
+
current_layer
|
59
|
+
end
|
60
|
+
else
|
61
|
+
Layer.new([], false)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def process(input)
|
67
|
+
processed_ast(input).to_docx
|
68
|
+
end
|
69
|
+
|
70
|
+
def processed_ast(input)
|
71
|
+
ast = build_ast(input)
|
72
|
+
ast
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_ast(input)
|
76
|
+
doc = Nokogiri::HTML.fragment(input)
|
77
|
+
@builder = ASTBuilder.new(doc.children)
|
78
|
+
|
79
|
+
while !@builder.done?
|
80
|
+
ast_next_paragraph
|
81
|
+
end
|
82
|
+
@builder.to_ast
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
def ast_next_paragraph
|
87
|
+
node = @builder.next
|
88
|
+
if node.name == 'div' || node.name == 'p'
|
89
|
+
@builder.new_layer
|
90
|
+
@builder.emit Paragraph.new('Paragraph', text(node.children))
|
91
|
+
elsif node.name == 'ul'
|
92
|
+
@builder.new_layer ilvl: true
|
93
|
+
unless @builder.nested?
|
94
|
+
@definition = Sablon::Numbering.instance.register('ListBullet')
|
95
|
+
end
|
96
|
+
@builder.push_all(node.children)
|
97
|
+
elsif node.name == 'ol'
|
98
|
+
@builder.new_layer ilvl: true
|
99
|
+
unless @builder.nested?
|
100
|
+
@definition = Sablon::Numbering.instance.register('ListNumber')
|
101
|
+
end
|
102
|
+
@builder.push_all(node.children)
|
103
|
+
elsif node.name == 'li'
|
104
|
+
@builder.new_layer
|
105
|
+
@builder.emit ListParagraph.new(@definition.style, text(node.children), @definition.numid, @builder.ilvl)
|
106
|
+
elsif node.text?
|
107
|
+
# SKIP?
|
108
|
+
else
|
109
|
+
raise ArgumentError, "Don't know how to handle node: #{node.inspect}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def text(nodes)
|
114
|
+
runs = nodes.map do |node|
|
115
|
+
if node.text?
|
116
|
+
Text.new(node.text)
|
117
|
+
elsif node.name == 'br'
|
118
|
+
Newline.new
|
119
|
+
elsif node.name == 'strong'
|
120
|
+
Bold.new(node.text)
|
121
|
+
elsif node.name == 'em'
|
122
|
+
Italic.new(node.text)
|
123
|
+
elsif ['ul', 'ol', 'p', 'div'].include?(node.name)
|
124
|
+
@builder.push(node)
|
125
|
+
nil
|
126
|
+
else
|
127
|
+
raise ArgumentError, "Don't know how to handle node: #{node.inspect}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
Collection.new(runs.compact)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Sablon
|
2
|
+
class HTMLConverter
|
3
|
+
class Visitor
|
4
|
+
def visit(node)
|
5
|
+
method_name = "visit_#{node.class.node_name}"
|
6
|
+
if respond_to? method_name
|
7
|
+
public_send method_name, node
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class GrepVisitor
|
13
|
+
attr_reader :result
|
14
|
+
def initialize(pattern)
|
15
|
+
@pattern = pattern
|
16
|
+
@result = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit(node)
|
20
|
+
if @pattern === node
|
21
|
+
@result << node
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Sablon
|
2
|
+
class Numbering
|
3
|
+
include Singleton
|
4
|
+
attr_reader :definitions
|
5
|
+
|
6
|
+
Definition = Struct.new(:numid, :style) do
|
7
|
+
def inspect
|
8
|
+
"#<Numbering #{numid}:#{style}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
reset!
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset!
|
17
|
+
@numid = 1000
|
18
|
+
@definitions = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def register(style)
|
22
|
+
@numid += 1
|
23
|
+
definition = Definition.new(@numid, style)
|
24
|
+
@definitions << definition
|
25
|
+
definition
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|