sablon 0.0.18 → 0.0.19.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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