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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a32d344154e7d3212cf75d5360c2f593122bd082
4
- data.tar.gz: 2224e7a3f29c836876bae9aa12cb812c91feb2fd
3
+ metadata.gz: 4a08176b5a9b4646da6f24b4aff58ac09dcbc7cd
4
+ data.tar.gz: f2549ddf6142a9ffb02dde86b6a13d9eb38cfbbd
5
5
  SHA512:
6
- metadata.gz: 711140b3b7ea9723188d5937d79d8fc8be86a76ccad7b9d3ae1c60c268da0ed072b129c9eef4447183c150bcdec7c98c08d1735836a67b1c9109dd500b3b7d0a
7
- data.tar.gz: 04000b91fb2cd8e4941a5b1bb073bf22f73a8d1de5214f5b9b6eff8a25942bc60946126235fffa457dd14feac8fb9b13a1472878848ec89d33606ad803f6a0e8
6
+ metadata.gz: 1f54003471153af06f163732d84eeab5768e7430f744702062d6868a9ca9f1be6847875b141f7e69ed6e5d4c62957d2f8853dc058696d2bf6234beee9b74a6fd
7
+ data.tar.gz: 25731c1257863f69c9b68586041a35e0c2ca85b1d85e01bc6c8c14a2ec569920aa57658a40fc0d7f002681181eba59c1b71e4c0e37f17c3b83157b42f8f5512d
data/.travis.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
- - 2.0.0
5
- - 2.1.2
3
+ - 2.1.8
4
+ - 2.2.4
5
+ - 2.3.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sablon (0.0.18)
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
- mini_portile (0.6.2)
12
+ mini_portile2 (2.0.0)
13
13
  minitest (5.8.0)
14
- nokogiri (1.6.6.2)
15
- mini_portile (~> 0.6.0)
14
+ nokogiri (1.6.7.1)
15
+ mini_portile2 (~> 2.0.0.rc2)
16
16
  rake (10.4.2)
17
- redcarpet (3.3.2)
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
 
@@ -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