htx 0.0.5 → 0.0.6

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
  SHA256:
3
- metadata.gz: 637943258561910b726a2956a9c9e683b1fe2180ab80803a484032ba56fb3101
4
- data.tar.gz: b4c6faf31037d5a8e0550e2331e6820d21349e9ca80bed18f9a6611a8004de5b
3
+ metadata.gz: 6bb09e36ecc0e6a70df4fed4552ae20a9512eadd3ac57d86bd88fa278c8d6029
4
+ data.tar.gz: e45e67362bc5422faa7df5330c5bd4940d4973c8ab0303cdd8207f0d7149efed
5
5
  SHA512:
6
- metadata.gz: 82e119f2932d7e96c17a2a336c6bcb88a27759d6657308ac91c4f1e0b6bd4c38000f5e4e45592b2603f6e9f71e6f1d670a4458745b172b157aa429a843d32970
7
- data.tar.gz: b2583375f80f0062ea9f5a73df2c72c34b728cf3e2c7b863fc4feb93e18e94aaf81a689d45933bcac3914f71b86ce16865cecec8cca86ef728d8a038c2b15eba
6
+ metadata.gz: 9eaea052f3b131de348f9ad20de712d55e180820d184a54356935aeacc3f2a7116266f2a4a6ff6e725ca44052a6c15350debcacd250b0f60be5ed0bb586aca26
7
+ data.tar.gz: f0f3ca8fd975d7bbf3b8d26c65d62cda86caaf245dd8143be571a5a9b28088e2274fb89a04a5fba9c567ef549f1d9d5dbaad8fcbc9db1ee346f395cd863097df
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2019-2021 Nate Pickens
1
+ Copyright 2019-2022 Nate Pickens
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4
4
  documentation files (the "Software"), to deal in the Software without restriction, including without
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.6
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTX
4
+ ##
5
+ # Error class used when a problem is encountered processing a template.
6
+ #
7
+ class MalformedTemplateError < StandardError
8
+ ##
9
+ # Creates a new instance.
10
+ #
11
+ # * +message+ - Description of the error.
12
+ # * +name+ - Name of the template.
13
+ # * +node+ - Nokogiri node being processed when the error was encountered (optional).
14
+ #
15
+ def initialize(message, name, node = nil)
16
+ if node
17
+ line = node.line
18
+ line = node.parent.line if line < 1
19
+ line = nil if line == -1
20
+ end
21
+
22
+ super("Malformed template #{name}#{":#{line}" if line}: #{message}")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ require('nokogiri')
4
+
5
+ module HTX
6
+ class Template
7
+ CHILDLESS = 0b001
8
+ TEXT_NODE = 0b010
9
+ XMLNS_NODE = 0b100
10
+ FLAG_BITS = 3
11
+
12
+ INDENT_DEFAULT = ' '
13
+ TEXT_NODE_TAG = 'htx-text'
14
+ DYNAMIC_KEY_ATTR = 'htx-key'
15
+
16
+ DEFAULT_XMLNS = {
17
+ 'math' => 'http://www.w3.org/1998/Math/MathML',
18
+ 'svg' => 'http://www.w3.org/2000/svg',
19
+ }.freeze
20
+
21
+ LEADING_WHITESPACE = /\A[ \t]*\n[ \t]*/.freeze
22
+ TRAILING_WHITESPACE = /\n[ \t]*\z/.freeze
23
+ NON_BLANK_NON_FIRST_LINE = /(?<=\n)[ \t]*(?=\S)/.freeze
24
+ NEWLINE_NON_BLANK = /\n(?=[^\n])/.freeze
25
+ INDENT_GUESS = /^[ \t]+/.freeze
26
+
27
+ END_STATEMENT_END = /(;|\n|\{|\})[ \t]*\z/.freeze
28
+ BEGIN_STATEMENT_END = /\A[ \t]*(;|\{|\n|\})/.freeze
29
+ END_WHITESPACE = /\s\z/.freeze
30
+ BEGIN_WHITESPACE = /\A\s/.freeze
31
+
32
+ RAW_VALUE = /\A\s*\${([\S\s]*)}\s*\z/.freeze
33
+ TEMPLATE_STRING = /\A\s*`([\S\s]*)`\s*\z/.freeze
34
+ INTERPOLATION = /\$\\?{([^}]*})?/.freeze
35
+ HTML_ENTITY = /&([a-zA-Z]+|#\d+|x[0-9a-fA-F]+);/.freeze
36
+ NON_CONTROL_STATEMENT = /#{INTERPOLATION}|(#{HTML_ENTITY})/.freeze
37
+ CONTROL_STATEMENT = /[{}();]/.freeze
38
+ CLOSE_STATEMENT = /;?\s*htx\.close\((\d*)\);?(\s*)\z/.freeze
39
+
40
+ ##
41
+ # Returns false. In the near future when support for the <:> tag has been dropped (in favor of
42
+ # <htx-text>), will return true if Nokogiri's HTML5 parser is available. To use it now, monkey patch
43
+ # this method to return true.
44
+ #
45
+ def self.html5_parser?
46
+ false # !!defined?(Nokogiri::HTML5)
47
+ end
48
+
49
+ ##
50
+ # Returns Nokogiri's HTML5 parser if available and enabled, and Nokogiri's regular HTML parser
51
+ # otherwise.
52
+ #
53
+ def self.nokogiri_parser
54
+ html5_parser? ? Nokogiri::HTML5::DocumentFragment : Nokogiri::HTML::DocumentFragment
55
+ end
56
+
57
+ ##
58
+ # Creates a new HTX instance.
59
+ #
60
+ # * +name+ - Name of the template. Conventionally the path of the template file is used for the name,
61
+ # but it can be anything.
62
+ # * +content+ - Template content string.
63
+ #
64
+ def initialize(name, content)
65
+ @name = name
66
+ @content = content
67
+ end
68
+
69
+ ##
70
+ # Compiles the HTX template.
71
+ #
72
+ # * +indent+ - Indent output by this number of spaces if Numeric, or by this string if a String (if the
73
+ # latter, may only contain space and tab characters).
74
+ # * +assign_to+ - Assign the template function to this JavaScript object instead of the <tt>window</tt>
75
+ # object.
76
+ #
77
+ def compile(indent: nil, assign_to: 'window')
78
+ doc = self.class.nokogiri_parser.parse(@content)
79
+ root_nodes = doc.children.select { |n| n.element? || (n.text? && n.text.strip != '') }
80
+
81
+ if (text_node = root_nodes.find(&:text?))
82
+ raise(MalformedTemplateError.new('text nodes are not allowed at root level', @name, text_node))
83
+ elsif root_nodes.size == 0
84
+ raise(MalformedTemplateError.new('a root node is required', @name))
85
+ elsif root_nodes.size > 1
86
+ raise(MalformedTemplateError.new("root node already defined on line #{root_nodes[0].line}", @name,
87
+ root_nodes[1]))
88
+ end
89
+
90
+ @compiled = ''.dup
91
+ @static_key = 0
92
+
93
+ @indent =
94
+ if indent.kind_of?(Numeric)
95
+ ' ' * indent
96
+ elsif indent.kind_of?(String) && indent !~ /^[ \t]+$/
97
+ raise("Invalid indent value #{indent.inspect}: only spaces and tabs are allowed")
98
+ else
99
+ indent || @content[INDENT_GUESS] || INDENT_DEFAULT
100
+ end
101
+
102
+ process(doc)
103
+ @compiled.rstrip!
104
+
105
+ <<~EOS
106
+ #{assign_to}['#{@name}'] = function(htx) {
107
+ #{@indent}#{@compiled}
108
+ }
109
+ EOS
110
+ end
111
+
112
+ private
113
+
114
+ ##
115
+ # Processes a DOM node's descendents.
116
+ #
117
+ # * +base+ - Base Nokogiri node to start from.
118
+ #
119
+ def process(base)
120
+ base.children.each do |node|
121
+ next unless node.element? || node.text?
122
+
123
+ dynamic_key = process_value(node.attr(DYNAMIC_KEY_ATTR), :attr)
124
+
125
+ if node.text? || node.name == TEXT_NODE_TAG || node.name == ':'
126
+ if node.name == ':'
127
+ warn("#{@name}:#{node.line}: The <:> tag has been deprecated. Please use <#{TEXT_NODE_TAG}> "\
128
+ 'for identical functionality.')
129
+ end
130
+
131
+ if (non_text_node = node.children.find { |n| !n.text? })
132
+ raise(MalformedTemplateError.new('text node tags may not contain child tags', @name,
133
+ non_text_node))
134
+ end
135
+
136
+ text = (node.text? ? node : node.children).text
137
+
138
+ if (value = process_value(text))
139
+ append(
140
+ "#{indent(text[LEADING_WHITESPACE])}"\
141
+ "htx.node(#{[
142
+ value,
143
+ dynamic_key,
144
+ ((@static_key += 1) << FLAG_BITS) | TEXT_NODE,
145
+ ].compact.join(', ')})"\
146
+ "#{indent(text[TRAILING_WHITESPACE])}"
147
+ )
148
+ else
149
+ append(indent(text))
150
+ end
151
+ else
152
+ childless = node.children.empty? || (node.children.size == 1 && node.children[0].text.strip == '')
153
+ attrs, xmlns = process_attrs(node)
154
+
155
+ append("htx.node(#{[
156
+ "'#{tag_name(node.name)}'",
157
+ attrs,
158
+ dynamic_key,
159
+ ((@static_key += 1) << FLAG_BITS) | (childless ? CHILDLESS : 0) | (xmlns ? XMLNS_NODE : 0),
160
+ ].compact.flatten.join(', ')})")
161
+
162
+ unless childless
163
+ process(node)
164
+
165
+ count = ''
166
+ @compiled.sub!(CLOSE_STATEMENT) do
167
+ count = $1 == '' ? 2 : $1.to_i + 1
168
+ $2
169
+ end
170
+
171
+ append("htx.close(#{count})")
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ ##
178
+ # Appends a string to the compiled template function string with appropriate punctuation and/or
179
+ # whitespace inserted.
180
+ #
181
+ # * +text+ - String to append to the compiled template string.
182
+ #
183
+ def append(text)
184
+ if @compiled == ''
185
+ # Do nothing.
186
+ elsif @compiled !~ END_STATEMENT_END && text !~ BEGIN_STATEMENT_END
187
+ @compiled << '; '
188
+ elsif @compiled !~ END_WHITESPACE && text !~ BEGIN_WHITESPACE
189
+ @compiled << ' '
190
+ elsif @compiled[-1] == "\n"
191
+ @compiled << @indent
192
+ end
193
+
194
+ @compiled << text
195
+ end
196
+
197
+ ##
198
+ # Indents each line of a string (except the first).
199
+ #
200
+ # * +text+ - String of lines to indent.
201
+ #
202
+ def indent(text)
203
+ return '' unless text
204
+
205
+ text.gsub!(NEWLINE_NON_BLANK, "\n#{@indent}")
206
+ text
207
+ end
208
+
209
+ ##
210
+ # Processes, formats, and encodes an attribute or text node value. Returns nil if the value is
211
+ # determined to be a control statement.
212
+ #
213
+ # * +text+ - String to process.
214
+ # * +is_attr+ - Truthy if the text is an attribute value.
215
+ #
216
+ def process_value(text, is_attr = false)
217
+ return nil if text.nil? || (!is_attr && text.strip == '')
218
+
219
+ if (value = text[RAW_VALUE, 1])
220
+ # Entire text is enclosed in ${...}.
221
+ value.strip!
222
+ quote = false
223
+ elsif (value = text[TEMPLATE_STRING, 1])
224
+ # Entire text is enclosed in backticks (template string).
225
+ quote = true
226
+ elsif is_attr || text.gsub(NON_CONTROL_STATEMENT, '') !~ CONTROL_STATEMENT
227
+ # Text is an attribute value or doesn't match control statement pattern.
228
+ value = text.dup
229
+ quote = true
230
+ else
231
+ return nil
232
+ end
233
+
234
+ # Strip one leading and trailing newline (and attached spaces) and perform outdent. Outdent amount
235
+ # calculation ignores everything before the first newline in its search for the least-indented line.
236
+ outdent = value.scan(NON_BLANK_NON_FIRST_LINE).min
237
+ value.gsub!(/#{LEADING_WHITESPACE}|#{TRAILING_WHITESPACE}|^#{outdent}/, '')
238
+ value.insert(0, '`').insert(-1, '`') if quote
239
+
240
+ # Ensure any Unicode characters get converted to Unicode escape sequences. Also note that since
241
+ # Nokogiri converts HTML entities to Unicode characters, this causes them to be properly passed to
242
+ # `document.createTextNode` calls as Unicode escape sequences rather than (incorrectly) as HTML
243
+ # entities.
244
+ value.encode('ascii', fallback: ->(c) { "\\u#{c.ord.to_s(16).rjust(4, '0')}" })
245
+ end
246
+
247
+ ##
248
+ # Processes a node's attributes, returning two items: a flat array of attribute names and values, and a
249
+ # boolean indicating whether or not an xmlns attribute is present.
250
+ #
251
+ # Note: if the node is a <math> or <svg> tag without an explicit xmlns attribute set, an appropriate one
252
+ # will be automatically added since it is required for those elements to render properly.
253
+ #
254
+ # * +node+ - Nokogiri node to process for attributes.
255
+ #
256
+ def process_attrs(node)
257
+ attrs = []
258
+ xmlns = !!node.attributes['xmlns']
259
+
260
+ if !xmlns && DEFAULT_XMLNS[node.name]
261
+ xmlns = true
262
+
263
+ attrs << "'xmlns'"
264
+ attrs << process_value(DEFAULT_XMLNS[node.name], :attr)
265
+ end
266
+
267
+ node.attributes.each do |_, attr|
268
+ next if attr.name == DYNAMIC_KEY_ATTR
269
+
270
+ attrs << "'#{attr_name(attr.name)}'"
271
+ attrs << process_value(attr.value, :attr)
272
+ end
273
+
274
+ [attrs, xmlns]
275
+ end
276
+
277
+ ##
278
+ # Returns the given text if the HTML5 parser is in use, or looks up the value in the tag map to get the
279
+ # correctly-cased version, falling back to the supplied text if no mapping exists.
280
+ #
281
+ # * +text+ - Tag name as returned by Nokogiri parser.
282
+ #
283
+ def tag_name(text)
284
+ self.class.html5_parser? ? text : (TAG_MAP[text] || text)
285
+ end
286
+
287
+ ##
288
+ # Returns the given text if the HTML5 parser is in use, or looks up the value in the attribute map to
289
+ # get the correctly-cased version, falling back to the supplied text if no mapping exists.
290
+ #
291
+ # * +text+ - Attribute name as returned by Nokogiri parser.
292
+ #
293
+ def attr_name(text)
294
+ self.class.html5_parser? ? text : (ATTR_MAP[text] || text)
295
+ end
296
+
297
+ # The Nokogiri HTML parser downcases all tag and attribute names, but SVG tags and attributes are case
298
+ # sensitive and often mix cased. These maps are used to restore the correct case of such tags and
299
+ # attributes.
300
+ #
301
+ # Note: Nokogiri's newer HTML5 parser resulting from the Nokogumbo merge fixes this issue, but it is
302
+ # currently not available for JRuby. It also does not parse <:> as a tag, which is why it's been
303
+ # deprecated in favor of <htx-text>. Once support for <:> has been completely removed, the HTML5 parser
304
+ # will be used for regular Ruby and this tag and attribute mapping hack reserved for JRuby (and any
305
+ # other potential environments where the HTML5 parser is not available).
306
+
307
+ # Source: https://developer.mozilla.org/en-US/docs/Web/SVG/Element
308
+ TAG_MAP = %w[
309
+ animateMotion animateTransform clipPath feBlend feColorMatrix feComponentTransfer feComposite
310
+ feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow feFlood feFuncA
311
+ feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight
312
+ feSpecularLighting feSpotLight feTile feTurbulence foreignObject linearGradient radialGradient
313
+ textPath
314
+ ].map { |tag| [tag.downcase, tag] }.to_h.freeze
315
+
316
+ # Source: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
317
+ ATTR_MAP = %w[
318
+ attributeName attributeType baseFrequency baseProfile calcMode clipPathUnits contentScriptType
319
+ contentStyleType diffuseConstant edgeMode filterRes filterUnits glyphRef gradientTransform
320
+ gradientUnits kernelMatrix kernelUnitLength keyPoints keySplines keyTimes lengthAdjust
321
+ limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits numOctaves
322
+ pathLength patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ
323
+ preserveAlpha preserveAspectRatio primitiveUnits refX refY referrerPolicy repeatCount repeatDur
324
+ requiredExtensions requiredFeatures specularConstant specularExponent spreadMethod startOffset
325
+ stdDeviation stitchTiles surfaceScale systemLanguage tableValues targetX targetY textLength viewBox
326
+ viewTarget xChannelSelector yChannelSelector zoomAndPan
327
+ ].map { |attr| [attr.downcase, attr] }.to_h.freeze
328
+ end
329
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTX
4
+ VERSION = '0.0.6'
5
+ end
data/lib/htx.rb CHANGED
@@ -1,256 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require('nokogiri')
3
+ require('htx/malformed_template_error')
4
+ require('htx/template')
5
+ require('htx/version')
4
6
 
5
7
  ##
6
8
  # A Ruby compiler for HTX templates.
7
9
  #
8
- class HTX
9
- VERSION = '0.0.5'
10
-
11
- CHILDLESS = 0b01
12
- TEXT_NODE = 0b10
13
- FLAG_BITS = 2
14
-
15
- INDENT_DEFAULT = ' '
16
- DYNAMIC_KEY_ATTR = 'htx-key'
17
-
18
- LEADING_WHITESPACE = /\A[ \t]*\n[ \t]*/.freeze
19
- TRAILING_WHITESPACE = /\n[ \t]*\z/.freeze
20
- NON_BLANK_NON_FIRST_LINE = /(?<=\n)[ \t]*(?=\S)/.freeze
21
- NEWLINE_NON_BLANK = /\n(?=[^\n])/.freeze
22
- INDENT_GUESS = /^[ \t]+/.freeze
23
-
24
- END_STATEMENT_END = /(;|\n|\{|\})[ \t]*\z/.freeze
25
- BEGIN_STATEMENT_END = /\A[ \t]*(;|\{|\n|\})/.freeze
26
- END_WHITESPACE = /\s\z/.freeze
27
- BEGIN_WHITESPACE = /\A\s/.freeze
28
-
29
- RAW_VALUE = /\A\s*\${([\S\s]*)}\s*\z/.freeze
30
- TEMPLATE_STRING = /\A\s*`([\S\s]*)`\s*\z/.freeze
31
- INTERPOLATION = /\$\\?{([^}]*})?/.freeze
32
- HTML_ENTITY = /&([a-zA-Z]+|#\d+|x[0-9a-fA-F]+);/.freeze
33
- NON_CONTROL_STATEMENT = /#{INTERPOLATION}|(#{HTML_ENTITY})/.freeze
34
- CONTROL_STATEMENT = /[{}();]/.freeze
35
- CLOSE_STATEMENT = /;?\s*htx\.close\((\d*)\);?(\s*)\z/.freeze
36
-
10
+ module HTX
37
11
  EMPTY_HASH = {}.freeze
38
12
 
39
13
  ##
40
- # Convenience method to create a new instance and immediately call compile on it.
14
+ # Convenience method to create a new Template instance and compile it.
41
15
  #
42
16
  def self.compile(name, template, options = EMPTY_HASH)
43
- new(name, template).compile(options)
17
+ Template.new(name, template).compile(**options)
44
18
  end
45
19
 
46
20
  ##
47
- # Creates a new HTX instance. Conventionally the path of the template file is used for the name, but it
48
- # can be anything.
21
+ # DEPRECATED. Use HTX::Template.new instead. HTX was formerly a class that would be instantiated for
22
+ # compilation. This method allows HTX.new calls to continue working (but support will be removed in the
23
+ # near future).
49
24
  #
50
- def initialize(name, template)
51
- @name = name
52
- @template = template
53
- end
54
-
55
- ##
56
- # Compiles the HTX template. Options:
57
- #
58
- # * :indent - Indent output by this number of spaces if Numeric, or by this string if a String (if the
59
- # latter, may only contain space and tab characters).
60
- # * :assign_to - Assign the template function to this JavaScript object instead of the `window` object.
25
+ # * +name+ - Name of the template. Conventionally the path of the template file is used for the name,
26
+ # but it can be anything.
27
+ # * +content+ - Template content string.
61
28
  #
62
- def compile(options = EMPTY_HASH)
63
- doc = Nokogiri::HTML::DocumentFragment.parse(@template)
64
- root_nodes = doc.children.select { |n| n.element? || (n.text? && n.text.strip != '') }
65
-
66
- if (text_node = root_nodes.find(&:text?))
67
- raise(MalformedTemplateError.new('text nodes are not allowed at root level', @name, text_node))
68
- elsif root_nodes.size == 0
69
- raise(MalformedTemplateError.new('a root node is required', @name))
70
- elsif root_nodes.size > 1
71
- raise(MalformedTemplateError.new("root node already defined on line #{root_nodes[0].line}", @name,
72
- root_nodes[1]))
73
- end
74
-
75
- @compiled = ''.dup
76
- @static_key = 0
77
-
78
- @indent =
79
- if options[:indent].kind_of?(Numeric)
80
- ' ' * options[:indent]
81
- elsif options[:indent].kind_of?(String) && options[:indent] !~ /^[ \t]+$/
82
- raise("Invalid :indent value #{options[:indent].inspect}: only spaces and tabs are allowed")
83
- else
84
- options[:indent] || @template[INDENT_GUESS] || INDENT_DEFAULT
85
- end
29
+ def self.new(name, content)
30
+ warn('HTX.new is deprecated. Please use HTX::Template.new instead.')
86
31
 
87
- process(doc)
88
- @compiled.rstrip!
89
-
90
- <<~EOS
91
- #{options[:assign_to] || 'window'}['#{@name}'] = function(htx) {
92
- #{@indent}#{@compiled}
93
- }
94
- EOS
32
+ Template.new(name, content)
95
33
  end
96
-
97
- private
98
-
99
- ##
100
- # Processes a DOM node's descendents.
101
- #
102
- def process(base)
103
- base.children.each do |node|
104
- next unless node.element? || node.text?
105
-
106
- dynamic_key = process_value(node.attr(DYNAMIC_KEY_ATTR), :attr)
107
-
108
- if node.text? || node.name == ':'
109
- if (non_text_node = node.children.find { |n| !n.text? })
110
- raise(MalformedTemplateError.new('dummy tags may not contain child tags', @name, non_text_node))
111
- end
112
-
113
- text = (node.text? ? node : node.children).text
114
-
115
- if (value = process_value(text))
116
- append(
117
- "#{indent(text[LEADING_WHITESPACE])}"\
118
- "htx.node(#{[
119
- value,
120
- dynamic_key,
121
- ((@static_key += 1) << FLAG_BITS) | TEXT_NODE,
122
- ].compact.join(', ')})"\
123
- "#{indent(text[TRAILING_WHITESPACE])}"
124
- )
125
- else
126
- append(indent(text))
127
- end
128
- else
129
- attrs = node.attributes.inject([]) do |attrs, (_, attr)|
130
- next attrs if attr.name == DYNAMIC_KEY_ATTR
131
-
132
- attrs << "'#{ATTR_MAP[attr.name] || attr.name}'"
133
- attrs << process_value(attr.value, :attr)
134
- end
135
-
136
- childless = node.children.empty? || (node.children.size == 1 && node.children[0].text.strip == '')
137
-
138
- append("htx.node(#{[
139
- "'#{TAG_MAP[node.name] || node.name}'",
140
- attrs,
141
- dynamic_key,
142
- ((@static_key += 1) << FLAG_BITS) | (childless ? CHILDLESS : 0),
143
- ].compact.flatten.join(', ')})")
144
-
145
- unless childless
146
- process(node)
147
-
148
- count = ''
149
- @compiled.sub!(CLOSE_STATEMENT) do
150
- count = $1 == '' ? 2 : $1.to_i + 1
151
- $2
152
- end
153
-
154
- append("htx.close(#{count})")
155
- end
156
- end
157
- end
158
- end
159
-
160
- ##
161
- # Appends a string to the compiled template function string with appropriate punctuation and/or whitespace
162
- # inserted.
163
- #
164
- def append(text)
165
- if @compiled == ''
166
- # Do nothing.
167
- elsif @compiled !~ END_STATEMENT_END && text !~ BEGIN_STATEMENT_END
168
- @compiled << '; '
169
- elsif @compiled !~ END_WHITESPACE && text !~ BEGIN_WHITESPACE
170
- @compiled << ' '
171
- elsif @compiled[-1] == "\n"
172
- @compiled << @indent
173
- end
174
-
175
- @compiled << text
176
- end
177
-
178
- ##
179
- # Indents each line of a string (except the first).
180
- #
181
- def indent(text)
182
- return '' unless text
183
-
184
- text.gsub!(NEWLINE_NON_BLANK, "\n#{@indent}")
185
- text
186
- end
187
-
188
- ##
189
- # Processes, formats, and encodes an attribute or text node value. Returns nil if the value is determined
190
- # to be a control statement.
191
- #
192
- def process_value(text, is_attr = false)
193
- return nil if text.nil? || (!is_attr && text.strip == '')
194
-
195
- if (value = text[RAW_VALUE, 1])
196
- # Entire text is enclosed in ${...}.
197
- value.strip!
198
- quote = false
199
- elsif (value = text[TEMPLATE_STRING, 1])
200
- # Entire text is enclosed in backticks (template string).
201
- quote = true
202
- elsif is_attr || text.gsub(NON_CONTROL_STATEMENT, '') !~ CONTROL_STATEMENT
203
- # Text is an attribute value or doesn't match control statement pattern.
204
- value = text.dup
205
- quote = true
206
- else
207
- return nil
208
- end
209
-
210
- # Strip one leading and trailing newline (and attached spaces) and perform outdent. Outdent amount
211
- # calculation ignores everything before the first newline in its search for the least-indented line.
212
- outdent = value.scan(NON_BLANK_NON_FIRST_LINE).min
213
- value.gsub!(/#{LEADING_WHITESPACE}|#{TRAILING_WHITESPACE}|^#{outdent}/, '')
214
- value.insert(0, '`').insert(-1, '`') if quote
215
-
216
- # Ensure any Unicode characters get converted to Unicode escape sequences. Also note that since Nokogiri
217
- # converts HTML entities to Unicode characters, this causes them to be properly passed to
218
- # `document.createTextNode` calls as Unicode escape sequences rather than (incorrectly) as HTML
219
- # entities.
220
- value.encode('ascii', fallback: ->(c) { "\\u#{c.ord.to_s(16).rjust(4, '0')}" })
221
- end
222
-
223
- class MalformedTemplateError < StandardError
224
- def initialize(message, name, node = nil)
225
- if node
226
- line = node.line
227
- line = node.parent.line if line < 1
228
- line = nil if line == -1
229
- end
230
-
231
- super("Malformed template #{name}#{":#{line}" if line}: #{message}")
232
- end
233
- end
234
-
235
- # The Nokogiri HTML parser downcases all tag and attribute names, but SVG tags and attributes are case
236
- # sensitive and often mix cased. These maps are used to restore the correct case of such tags and
237
- # attributes.
238
- TAG_MAP = %w[
239
- animateMotion animateTransform clipPath feBlend feColorMatrix feComponentTransfer feComposite
240
- feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB
241
- feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight
242
- feSpecularLighting feSpotLight feTile feTurbulence foreignObject linearGradient radialGradient textPath
243
- ].map { |tag| [tag.downcase, tag] }.to_h.freeze
244
-
245
- ATTR_MAP = %w[
246
- allowReorder attributeName attributeType autoReverse baseFrequency baseProfile calcMode clipPathUnits
247
- contentScriptType contentStyleType diffuseConstant edgeMode externalResourcesRequired filterRes
248
- filterUnits glyphRef gradientTransform gradientUnits kernelMatrix kernelUnitLength keyPoints keySplines
249
- keyTimes lengthAdjust limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits
250
- numOctaves pathLength patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ
251
- preserveAlpha preserveAspectRatio primitiveUnits refX refY referrerPolicy repeatCount repeatDur
252
- requiredExtensions requiredFeatures specularConstant specularExponent spreadMethod startOffset
253
- stdDeviation stitchTiles surfaceScale systemLanguage tableValues targetX targetY textLength viewBox
254
- viewTarget xChannelSelector yChannelSelector zoomAndPan
255
- ].map { |attr| [attr.downcase, attr] }.to_h.freeze
256
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: htx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Pickens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-05 00:00:00.000000000 Z
11
+ date: 2022-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.10'
19
+ version: '1.13'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.10'
26
+ version: '1.13'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -68,7 +68,11 @@ extra_rdoc_files: []
68
68
  files:
69
69
  - LICENSE
70
70
  - README.md
71
+ - VERSION
71
72
  - lib/htx.rb
73
+ - lib/htx/malformed_template_error.rb
74
+ - lib/htx/template.rb
75
+ - lib/htx/version.rb
72
76
  homepage: https://github.com/npickens/htx
73
77
  licenses:
74
78
  - MIT
@@ -84,14 +88,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
88
  requirements:
85
89
  - - ">="
86
90
  - !ruby/object:Gem::Version
87
- version: 2.5.8
91
+ version: 2.6.0
88
92
  required_rubygems_version: !ruby/object:Gem::Requirement
89
93
  requirements:
90
94
  - - ">="
91
95
  - !ruby/object:Gem::Version
92
96
  version: '0'
93
97
  requirements: []
94
- rubygems_version: 3.2.3
98
+ rubygems_version: 3.2.32
95
99
  signing_key:
96
100
  specification_version: 4
97
101
  summary: A Ruby compiler for HTX templates.