htx 0.0.5 → 0.0.6

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
  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.