sablon 0.0.21 → 0.0.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -3
  3. data/Gemfile.lock +9 -9
  4. data/README.md +120 -11
  5. data/lib/sablon.rb +7 -1
  6. data/lib/sablon/configuration/configuration.rb +165 -0
  7. data/lib/sablon/configuration/html_tag.rb +99 -0
  8. data/lib/sablon/content.rb +12 -9
  9. data/lib/sablon/context.rb +27 -20
  10. data/lib/sablon/environment.rb +31 -0
  11. data/lib/sablon/html/ast.rb +290 -75
  12. data/lib/sablon/html/ast_builder.rb +90 -0
  13. data/lib/sablon/html/converter.rb +3 -123
  14. data/lib/sablon/numbering.rb +0 -5
  15. data/lib/sablon/operations.rb +11 -11
  16. data/lib/sablon/parser/mail_merge.rb +7 -6
  17. data/lib/sablon/processor/document.rb +9 -9
  18. data/lib/sablon/processor/numbering.rb +4 -4
  19. data/lib/sablon/template.rb +5 -4
  20. data/lib/sablon/version.rb +1 -1
  21. data/sablon.gemspec +3 -3
  22. data/test/configuration_test.rb +122 -0
  23. data/test/content_test.rb +7 -6
  24. data/test/context_test.rb +11 -11
  25. data/test/environment_test.rb +27 -0
  26. data/test/expression_test.rb +2 -2
  27. data/test/fixtures/html/html_test_content.html +174 -0
  28. data/test/fixtures/html_sample.docx +0 -0
  29. data/test/fixtures/xml/comment_block_and_comment_as_key.xml +31 -0
  30. data/test/html/ast_builder_test.rb +65 -0
  31. data/test/html/ast_test.rb +117 -0
  32. data/test/html/converter_test.rb +386 -87
  33. data/test/html/node_properties_test.rb +113 -0
  34. data/test/html_test.rb +10 -10
  35. data/test/mail_merge_parser_test.rb +3 -2
  36. data/test/processor/document_test.rb +20 -2
  37. data/test/section_properties_test.rb +1 -1
  38. data/test/support/html_snippets.rb +9 -0
  39. data/test/test_helper.rb +0 -1
  40. metadata +27 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b83974c57d78d5a2b9ae7f1fc15f8527b3d349bf
4
- data.tar.gz: 1d8283d8c781bb7e3810316cd71ef60f4e53cf70
3
+ metadata.gz: 8dce87aaca368f43d657f2ff7fe7249e6f46ddee
4
+ data.tar.gz: 8fddd1630ba575d38c6ab790d82ee33e88c33b65
5
5
  SHA512:
6
- metadata.gz: aacf3306315f9d9cc76c82de9a43cbf491ffccc5af4434e1409c6a7f513ef13133868201ba5cf702c478c585182fd46d8232a2c8e45ae3b50675cdc957f4b7a6
7
- data.tar.gz: bf82feaafe41009c8f1e4b95a3dbbf31159da473fcfda2cad055c37bfe3ba600d7529aabba8bf3574353bbb81be9f9e9b751b7aa8e5e93b8ef8df740790db6e6
6
+ metadata.gz: 8f9b5dfcec4a943d674e4144cfa9d545a99340d14592b4e6aa09e19199ae363f919d182473004787ca43e7153ffa1bd48bdfd828d83bb3474ee811feb301c8ba
7
+ data.tar.gz: 592faea43070727caf1608ce097aa10fc43f0821c0614a658c7a4af3474a37e501d90115d2d5ad54cdf6303250ed2a7ba2cb21ab6949d79d0cef4fabbe6bf26f
data/.travis.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.8
4
- - 2.2.4
5
- - 2.3.0
3
+ - 2.1
4
+ - 2.2
5
+ - 2.3
6
+ - 2.4
data/Gemfile.lock CHANGED
@@ -1,18 +1,18 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sablon (0.0.21)
4
+ sablon (0.0.22)
5
5
  nokogiri (>= 1.6.0)
6
- rubyzip (>= 1.1)
6
+ rubyzip (>= 1.1.1)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- mini_portile2 (2.1.0)
12
- minitest (5.8.0)
13
- nokogiri (1.7.1)
14
- mini_portile2 (~> 2.1.0)
15
- rake (10.4.2)
11
+ mini_portile2 (2.3.0)
12
+ minitest (5.10.3)
13
+ nokogiri (1.8.1)
14
+ mini_portile2 (~> 2.3.0)
15
+ rake (12.3.0)
16
16
  rubyzip (1.2.1)
17
17
  xml-simple (1.1.5)
18
18
 
@@ -22,9 +22,9 @@ PLATFORMS
22
22
  DEPENDENCIES
23
23
  bundler (>= 1.6)
24
24
  minitest (~> 5.4)
25
- rake (~> 10.0)
25
+ rake (~> 12.0)
26
26
  sablon!
27
27
  xml-simple
28
28
 
29
29
  BUNDLED WITH
30
- 1.14.5
30
+ 1.16.0
data/README.md CHANGED
@@ -8,6 +8,28 @@ and efficient.
8
8
 
9
9
  *Note: Sablon is still in early development. Please report if you encounter any issues along the way.*
10
10
 
11
+ #### Table of Contents
12
+ * [Installation](#installation)
13
+ * [Usage](#usage)
14
+ * [Writing Templates](#writing-templates)
15
+ * [Content Insertion](#content-insertion)
16
+ * [WordProcessingML](#wordprocessingml)
17
+ * [HTML](#html)
18
+ * [Conditionals](#conditionals)
19
+ * [Loops](#loops)
20
+ * [Nesting](#nesting)
21
+ * [Comments](#comments)
22
+ * [Configuration (Beta)](#configuration-beta)
23
+ * [Customizing HTML Tag Conversion](#customizing-html-tag-conversion)
24
+ * [Customizing CSS Style Conversion](#customizing-css-style-conversion)
25
+ * [Executable](#executable)
26
+ * [Examples](#examples)
27
+ * [Using a Ruby script](#using-a-ruby-script)
28
+ * [Using the sablon executable](#using-the-sablon-executable)
29
+ * [Contributing](#contributing)
30
+ * [Inspiration](#inspiration)
31
+
32
+
11
33
  ## Installation
12
34
 
13
35
  Add this line to your application's Gemfile:
@@ -102,14 +124,13 @@ IMPORTANT: This feature is very much *experimental*. Currently, the insertion
102
124
  will replace the containing paragraph. This means that other content in the same
103
125
  paragraph is discarded.
104
126
 
105
- ##### HTML [experimental]
127
+ ##### HTML
106
128
 
107
- Similar to WordProcessingML it's possible to use html as input while processing the
108
- tempalte. You don't need to modify your templates, a simple insertion operation
129
+ Similar to WordProcessingML it's possible to use html as input while processing the template. You don't need to modify your templates, a simple insertion operation
109
130
  is sufficient:
110
131
 
111
132
  ```
112
- «=article.body»
133
+ «=article»
113
134
  ```
114
135
 
115
136
  To use HTML insertion prepare the context like so:
@@ -118,24 +139,40 @@ To use HTML insertion prepare the context like so:
118
139
  html_body = <<-HTML
119
140
  <div>This text can contain <em>additional formatting</em>
120
141
  according to the <strong>HTML</strong> specification.</div>
142
+ <p style="text-align: right; background-color: #FFFF00">Right aligned
143
+ content with a yellow background color</p>
144
+ <div><span style="color: #123456">Inline styles</span> are possible as well</div>
121
145
  HTML
122
146
  context = {
123
- article: { html_body: Sablon.content(:html, html_body) }
147
+ article: Sablon.content(:html, html_body) }
148
+ # alternative method using special key format
149
+ # 'html:article' => html_body
124
150
  }
125
151
  template.render_to_file File.expand_path("~/Desktop/output.docx"), context
126
152
  ```
127
153
 
128
- Currently HTML insertion is very limited and strongly focused on the HTML
129
- generated by [Trix editor](https://github.com/basecamp/trix).
154
+ Currently, HTML insertion is somewhat limited. It is recommended that the block level tags such as `p` and `div` are not nested within each other, otherwise the final document may not generate as anticipated. List tags (`ul` and `ol`) and inline tags (`span`, `b`, `em`, etc.) can be nested as deeply as needed.
130
155
 
131
- IMPORTANT: This feature is very much *experimental*. Currently, the insertion
132
- will replace the containing paragraph. This means that other content in the same
133
- paragraph is discarded.
156
+ Not all tags are supported. Currently supported tags are defined in [configuration.rb](lib/sablon/configuration/configuration.rb) for paragraphs in method `prepare_paragraph` and for text runs in `prepare_run`.
157
+
158
+ Basic conversion of CSS inline styles into matching WordML properties in supported through the `style=" ... "` attribute in the HTML markup. Not all possible styles are supported and only a small subset of CSS styles have a direct WordML equivalent. Styles are passed onto nested elements. The currently supported styles are also defined in [configuration.rb](lib/sablon/configuration/configuration.rb) in method `process_style`. Simple toggle properties that aren't directly supported can be added using the `text-decoration: ` style attribute with the proper WordML tag name as the value. Paragraph and Run property reference can be found at:
159
+ * http://officeopenxml.com/WPparagraphProperties.php
160
+ * http://officeopenxml.com/WPtextFormatting.php
161
+
162
+ If you wish to write out your HTML code in an indented human readable fashion, or you are pulling content from the ERB templating engine in rails the following regular expression can help eliminate extraneous whitespace in the final document.
163
+ ```ruby
164
+ # combine all white space
165
+ html_str = html_str.gsub(/\s+/, ' ')
166
+ # clear any white space between block level tags and other content
167
+ html_str.gsub(%r{\s*<(/?(?:h\d|div|p|br|ul|ol|li).*?)>\s*}, '<\1>')
168
+ ```
169
+
170
+ IMPORTANT: Currently, the insertion will replace the containing paragraph. This means that other content in the same paragraph is discarded.
134
171
 
135
172
 
136
173
  #### Conditionals
137
174
 
138
- Sablon can render parts of the template conditonally based on the value of a
175
+ Sablon can render parts of the template conditionally based on the value of a
139
176
  context variable. Conditional fields are inserted around the content.
140
177
 
141
178
  ```
@@ -189,6 +226,78 @@ styles for HTML insertion.
189
226
  «endComment»
190
227
  ```
191
228
 
229
+ ### Configuration (Beta)
230
+
231
+ The Sablon::Configuration singleton is a new feature that allows the end user to customize HTML parsing to their needs without needing to fork and edit the source code of the gem. This API is still in a beta state and may be subject to change as future needs are identified beyond HTML conversion.
232
+
233
+ The example below show how to expose the configuration instance:
234
+ ```ruby
235
+ Sablon.configure do |config|
236
+ # manipulate config object
237
+ end
238
+ ```
239
+
240
+ The default set of registered HTML tags and CSS property conversions are defined in [configuration.rb](lib/sablon/configuration/configuration.rb).
241
+
242
+ #### Customizing HTML Tag Conversion
243
+
244
+ Any HTML tag can be added using the configuration object even if it needs a custom AST class to handle conversion logic. Simple inline tags that only modify the style of text (i.e. the already supported `<b>` tag) can be added without an AST class as shown below:
245
+ ```ruby
246
+ Sablon.configure do |config|
247
+ config.register_html_tag(:bgcyan, :inline, properties: { highlight: 'cyan' })
248
+ end
249
+ ```
250
+ The above tag simply adds a background color to text using the `<w:highlight w:val="cyan" />` property.
251
+
252
+
253
+ More complex business logic can be supported by adding a new class under the `Sablon::HTMLConverter` namespace. The new class will likely subclass `Sablon::HTMLConverter::Node` or `Sablon::HTMLConverter::Collection` depending on the needed behavior. The current AST classes serve as additional examples and can be found in [ast.rb](/lib/sablon/html/ast.rb). When registering a new HTML tag that uses a custom AST class the class must be passed in either by name using a lowercased and underscored symbol or the class object itself.
254
+
255
+ The block below shows how to register a new HTML tag that adds the following AST class: `Sablon::HTMLConverter::InstrText`.
256
+ ```ruby
257
+ module Sablon
258
+ class HTMLConverter
259
+ class InstrText < Node
260
+ # implementation details ...
261
+ end
262
+ end
263
+ end
264
+ # register tag
265
+ Sablon.configure do |config|
266
+ config.register_html_tag(:bgcyan, :inline, ast_class: :instr_text)
267
+ end
268
+ ```
269
+
270
+ Existing tags can be overwritten using the `config.register_html_tag` method or removed entirely using `config.remove_html_tag`.
271
+ ```ruby
272
+ # remove tag
273
+ Sablon.configure do |config|
274
+ # remove support for the span tag
275
+ config.remove_html_tag(:span)
276
+ end
277
+ ```
278
+
279
+
280
+ #### Customizing CSS Style Conversion
281
+
282
+ The conversion of CSS stored in an element's `style="..."` attribute can be customized using the configuration object as well. Adding a new style conversion or overriding an existing one is done using the `config.register_style_converter` method. It accepts three arguments the name of the AST node (as a lowercased and underscored symbol) the style applies to, the name of the CSS property (needs to be a string in most cases) and a lambda that accepts a single argument, the property value. The example below shows how to add a new style that sets the `<w:highlight />` property.
283
+ ```ruby
284
+ # add style conversion
285
+ Sablon.configure do |config|
286
+ # register new conversion for the Sablon::HTMLConverter::Run AST class.
287
+ converter = lambda { |v| return 'highlight', v }
288
+ config.register_style_converter(:run, 'custom-highlight', converter)
289
+ end
290
+ ```
291
+
292
+ Existing conversions can be overwritten using the `config.register_style_converter` method or removed entirely using `config.remove_style_converter`.
293
+ ```ruby
294
+ # remove tag
295
+ Sablon.configure do |config|
296
+ # remove support for conversion of font-size for the Run AST class
297
+ config.remove_style_converter(:run, 'font-size')
298
+ end
299
+ ```
300
+
192
301
  ### Executable
193
302
 
194
303
  The `sablon` executable can be used to process templates on the command-line.
data/lib/sablon.rb CHANGED
@@ -1,10 +1,12 @@
1
- require 'singleton'
2
1
  require 'zip'
3
2
  require 'nokogiri'
4
3
 
5
4
  require "sablon/version"
5
+ require "sablon/configuration/configuration"
6
+
6
7
  require "sablon/numbering"
7
8
  require "sablon/context"
9
+ require "sablon/environment"
8
10
  require "sablon/template"
9
11
  require "sablon/processor/document"
10
12
  require "sablon/processor/section_properties"
@@ -18,6 +20,10 @@ module Sablon
18
20
  class TemplateError < ArgumentError; end
19
21
  class ContextError < ArgumentError; end
20
22
 
23
+ def self.configure
24
+ yield(Configuration.instance) if block_given?
25
+ end
26
+
21
27
  def self.template(path)
22
28
  Template.new(path)
23
29
  end
@@ -0,0 +1,165 @@
1
+ require 'singleton'
2
+ require 'sablon/configuration/html_tag'
3
+
4
+ module Sablon
5
+ # Handles storing configuration data for the sablon module
6
+ class Configuration
7
+ include Singleton
8
+
9
+ attr_accessor :permitted_html_tags, :defined_style_conversions
10
+
11
+ def initialize
12
+ initialize_html_tags
13
+ initialize_css_style_conversion
14
+ end
15
+
16
+ # Adds a new tag to the permitted tags hash or replaces an existing one
17
+ def register_html_tag(tag_name, type = :inline, **options)
18
+ tag = HTMLTag.new(tag_name, type, **options)
19
+ @permitted_html_tags[tag.name] = tag
20
+ end
21
+
22
+ # Removes a tag from the permitted tgs hash, returning it
23
+ def remove_html_tag(tag_name)
24
+ @permitted_html_tags.delete(tag_name)
25
+ end
26
+
27
+ # Adds a new style property converter for the specified ast class and
28
+ # CSS property name. The ast_class variable should be the class name
29
+ # in lowercased snakecase as a symbol, i.e. MyClass -> :my_class.
30
+ # The converter passed in must be a proc that accepts
31
+ # a single argument (the value) and returns two values: the WordML property
32
+ # name and its value. The converted property value can be a string, hash
33
+ # or array.
34
+ def register_style_converter(ast_node, prop_name, converter)
35
+ # create a new ast node hash if needed
36
+ unless @defined_style_conversions[ast_node]
37
+ @defined_style_conversions[ast_node] = {}
38
+ end
39
+ # add the style converter to the node's hash
40
+ @defined_style_conversions[ast_node][prop_name] = converter
41
+ end
42
+
43
+ # Deletes a CSS converter from the hash by specifying the AST class
44
+ # in lowercased snake case and the property name.
45
+ def remove_style_converter(ast_node, prop_name)
46
+ @defined_style_conversions[ast_node].delete(prop_name)
47
+ end
48
+
49
+ private
50
+
51
+ # Defines all of the initial HTML tags to be used by HTMLconverter
52
+ def initialize_html_tags
53
+ @permitted_html_tags = {}
54
+ tags = {
55
+ # special tag used for elements with no parent, i.e. top level
56
+ '#document-fragment' => { type: :block, ast_class: :root, allowed_children: :_block },
57
+
58
+ # block level tags
59
+ div: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Normal' }, allowed_children: :_inline },
60
+ p: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Paragraph' }, allowed_children: :_inline },
61
+ h1: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading1' }, allowed_children: :_inline },
62
+ h2: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading2' }, allowed_children: :_inline },
63
+ h3: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading3' }, allowed_children: :_inline },
64
+ h4: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading4' }, allowed_children: :_inline },
65
+ h5: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading5' }, allowed_children: :_inline },
66
+ h6: { type: :block, ast_class: :paragraph, properties: { pStyle: 'Heading6' }, allowed_children: :_inline },
67
+ ol: { type: :block, ast_class: :list, properties: { pStyle: 'ListNumber' }, allowed_children: %i[ol li] },
68
+ ul: { type: :block, ast_class: :list, properties: { pStyle: 'ListBullet' }, allowed_children: %i[ul li] },
69
+ li: { type: :block, ast_class: :list_paragraph },
70
+
71
+ # inline style tags
72
+ span: { type: :inline, ast_class: nil, properties: {} },
73
+ strong: { type: :inline, ast_class: nil, properties: { b: nil } },
74
+ b: { type: :inline, ast_class: nil, properties: { b: nil } },
75
+ em: { type: :inline, ast_class: nil, properties: { i: nil } },
76
+ i: { type: :inline, ast_class: nil, properties: { i: nil } },
77
+ u: { type: :inline, ast_class: nil, properties: { u: 'single' } },
78
+ s: { type: :inline, ast_class: nil, properties: { strike: 'true' } },
79
+ sub: { type: :inline, ast_class: nil, properties: { vertAlign: 'subscript' } },
80
+ sup: { type: :inline, ast_class: nil, properties: { vertAlign: 'superscript' } },
81
+
82
+ # inline content tags
83
+ text: { type: :inline, ast_class: :run, properties: {}, allowed_children: [] },
84
+ br: { type: :inline, ast_class: :newline, properties: {}, allowed_children: [] }
85
+ }
86
+ # add all tags to the config object
87
+ tags.each do |tag_name, settings|
88
+ type = settings.delete(:type)
89
+ register_html_tag(tag_name, type, **settings)
90
+ end
91
+ end
92
+
93
+ # Defines an initial set of CSS -> WordML conversion lambdas stored in
94
+ # a nested hash structure where the first key is the AST class and the
95
+ # second is the conversion lambda
96
+ def initialize_css_style_conversion
97
+ @defined_style_conversions = {
98
+ # styles shared or common logic across all node types go here.
99
+ # Special conversion lambdas such as :_border can be
100
+ # defined here for reuse across several AST nodes. Care must
101
+ # be taken to avoid possible naming conflicts, hence the underscore.
102
+ # AST class keys should be stored with their names converted from
103
+ # camelcase to lowercased snakecase, i.e. TestCase = test_case
104
+ node: {
105
+ 'background-color' => lambda { |v|
106
+ return 'shd', { val: 'clear', fill: v.delete('#') }
107
+ },
108
+ _border: lambda { |v|
109
+ props = { sz: 2, val: 'single', color: '000000' }
110
+ vals = v.split
111
+ vals[1] = 'single' if vals[1] == 'solid'
112
+ #
113
+ props[:sz] = @defined_style_conversions[:node][:_sz].call(vals[0])
114
+ props[:val] = vals[1] if vals[1]
115
+ props[:color] = vals[2].delete('#') if vals[2]
116
+ #
117
+ return props
118
+ },
119
+ _sz: lambda { |v|
120
+ return nil unless v
121
+ (2 * Float(v.gsub(/[^\d.]/, '')).ceil).to_s
122
+ },
123
+ 'text-align' => ->(v) { return 'jc', v }
124
+ },
125
+ # Styles specific to the Paragraph AST class
126
+ paragraph: {
127
+ 'border' => lambda { |v|
128
+ props = @defined_style_conversions[:node][:_border].call(v)
129
+ #
130
+ return 'pBdr', [
131
+ { top: props }, { bottom: props },
132
+ { left: props }, { right: props }
133
+ ]
134
+ },
135
+ 'vertical-align' => ->(v) { return 'textAlignment', v }
136
+ },
137
+ # Styles specific to a run of text
138
+ run: {
139
+ 'color' => ->(v) { return 'color', v.delete('#') },
140
+ 'font-size' => lambda { |v|
141
+ return 'sz', @defined_style_conversions[:node][:_sz].call(v)
142
+ },
143
+ 'font-style' => lambda { |v|
144
+ return 'b', nil if v =~ /bold/
145
+ return 'i', nil if v =~ /italic/
146
+ },
147
+ 'font-weight' => ->(v) { return 'b', nil if v =~ /bold/ },
148
+ 'text-decoration' => lambda { |v|
149
+ supported = %w[line-through underline]
150
+ props = v.split
151
+ return props[0], 'true' unless supported.include? props[0]
152
+ return 'strike', 'true' if props[0] == 'line-through'
153
+ return 'u', 'single' if props.length == 1
154
+ return 'u', { val: props[1], color: 'auto' } if props.length == 2
155
+ return 'u', { val: props[1], color: props[2].delete('#') }
156
+ },
157
+ 'vertical-align' => lambda { |v|
158
+ return 'vertAlign', 'subscript' if v =~ /sub/
159
+ return 'vertAlign', 'superscript' if v =~ /super/
160
+ }
161
+ }
162
+ }
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,99 @@
1
+ module Sablon
2
+ class Configuration
3
+ # Stores the information for a single HTML tag. This information
4
+ # is used by the HTMLConverter. An optional AST class can be defined,
5
+ # and if so conversion stops there and it is assumed the AST class
6
+ # will handle any child nodes unless the element is a block level tag.
7
+ # In the case of a block level tag the child nodes are processed by the
8
+ # AST builder again. If the AST class is omitted it is assumed the node
9
+ # should be "passed through" only transferring it's properties onto
10
+ # children. A block level tag must have an AST class associated with
11
+ # it. The block and inline status of tags is not affected by CSS.
12
+ # Permitted child tags are specified using the :allowed_children optional
13
+ # arg. The default value is [:_inline, :ul, :ol]. :_inline is a special
14
+ # reference to all inline type tags, :_block is equivalent for block
15
+ # type tags.
16
+ #
17
+ # == Parameters
18
+ # * name - symbol or string of the HTML element tag name
19
+ # * type - The type of HTML tag needs to be :inline or :block
20
+ # * ast_class - class instance or symbol, the AST class or it's name
21
+ # used to process the HTML node
22
+ # * options - collects all other keyword arguments, Current kwargs are
23
+ # `:properties`, `:attributes` and `:allowed_children`.
24
+ #
25
+ # Example
26
+ # HTMLTag.new(:div, :block, ast_class: Sablon::HTMLConverter::Paragraph,
27
+ # properties: { pStyle: 'Normal' })
28
+ class HTMLTag
29
+ attr_reader :name, :type, :ast_class, :attributes, :properties,
30
+ :allowed_children
31
+
32
+ # Setup HTML tag information
33
+ def initialize(name, type, ast_class: nil, **options)
34
+ # Set basic params converting some args to symbols for consistency
35
+ @name = name.to_sym
36
+ @type = type.to_sym
37
+ @ast_class = nil
38
+ # use self.ast_class to trigger setter method
39
+ self.ast_class = ast_class if ast_class
40
+
41
+ # Ensure block level tags have an AST class
42
+ if @type == :block && @ast_class.nil?
43
+ raise ArgumentError, "Block level tag #{name} must have an AST class."
44
+ end
45
+
46
+ # Set attributes from optinos hash, currently unused during AST generation
47
+ @attributes = options.fetch(:attributes, {})
48
+ # WordML properties defined by the tag, i.e. <w:b /> for the <b> tag,
49
+ # etc. All the keys need to be symbols to avoid getting reparsed
50
+ # with the element's CSS attributes.
51
+ @properties = options.fetch(:properties, {})
52
+ @properties = Hash[@properties.map { |k, v| [k.to_sym, v] }]
53
+ # Set permitted child tags or tag groups
54
+ self.allowed_children = options[:allowed_children]
55
+ end
56
+
57
+ # checks if the given tag is a permitted child element
58
+ def allowed_child?(tag)
59
+ if @allowed_children.include?(tag.name)
60
+ true
61
+ elsif @allowed_children.include?(:_inline) && tag.type == :inline
62
+ true
63
+ elsif @allowed_children.include?(:_block) && tag.type == :block
64
+ true
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def allowed_children=(value)
73
+ if value.nil?
74
+ @allowed_children = %i[_inline ol ul]
75
+ return
76
+ else
77
+ value = [value] unless value.is_a? Array
78
+ end
79
+ @allowed_children = value.map(&:to_sym)
80
+ end
81
+
82
+ # converts a string or symbol to a class defined under
83
+ # Sablon::HTMLConverter
84
+ def ast_class=(value)
85
+ if value.is_a? Class
86
+ @ast_class = value
87
+ return
88
+ else
89
+ value = value.to_s
90
+ end
91
+ # camel case the word and get class, similar logic to
92
+ # ActiveSupport::Inflector.constantize but refactored to be specific
93
+ # to the HTMLConverter class
94
+ value.gsub!(/(?:^|_)([a-z])/) { Regexp.last_match[1].capitalize }
95
+ @ast_class = Sablon::HTMLConverter.const_get(value)
96
+ end
97
+ end
98
+ end
99
+ end