htx 0.0.1 → 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: 94e1883a59d09cce6170061c9e0602a3430da8ca800163ba2fe8cc9d10b316b4
4
- data.tar.gz: a26e6aa3a874e40569cd62204aa698a6bdec5b02a78dc35ea993d0a11ff8f0f2
3
+ metadata.gz: 6bb09e36ecc0e6a70df4fed4552ae20a9512eadd3ac57d86bd88fa278c8d6029
4
+ data.tar.gz: e45e67362bc5422faa7df5330c5bd4940d4973c8ab0303cdd8207f0d7149efed
5
5
  SHA512:
6
- metadata.gz: 004dca83de5a65ba167eb345d21a1a8c1d771efbe946fc746cfb470d4ca783bad57830ced8ba6f1a7ce47e9e04abc3ee06a6ebf02fc9d5a0eee2892983dbf136
7
- data.tar.gz: ab814533c1f9ead9fa8daa2ef46959a2a8893c21e3705ee9863fbcaeed90ec0e70f835d36420fe1ade8c1a0bf9c33478987e76d07cbb20c154a4da26d3ef7492
6
+ metadata.gz: 9eaea052f3b131de348f9ad20de712d55e180820d184a54356935aeacc3f2a7116266f2a4a6ff6e725ca44052a6c15350debcacd250b0f60be5ed0bb586aca26
7
+ data.tar.gz: f0f3ca8fd975d7bbf3b8d26c65d62cda86caaf245dd8143be571a5a9b28088e2274fb89a04a5fba9c567ef549f1d9d5dbaad8fcbc9db1ee346f395cd863097df
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2019 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/README.md CHANGED
@@ -29,11 +29,20 @@ template = File.read(File.join('some/asset/dir', path))
29
29
 
30
30
  HTX.compile(path, template)
31
31
 
32
+ # Or to attach to a custom object instead of `window`:
33
+ HTX.compile(path, template, assign_to: 'myTemplates')
34
+
32
35
  # Result:
33
36
  #
34
37
  # window['/my/hot/template.htx'] = function(htx) {
35
38
  # ...
36
39
  # }
40
+ #
41
+ # If `assign_to` is specified:
42
+ #
43
+ # myTemplates['/components/people.htx'] = function(htx) {
44
+ # // ...
45
+ # }
37
46
  ```
38
47
 
39
48
  ## Contributing
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,190 +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.1'
10
-
11
- CHILDLESS = 0b01
12
- TEXT_NODE = 0b10
13
- FLAG_BITS = 2
14
-
15
- DYNAMIC_KEY_ATTR = 'htx-key'
16
-
17
- LEADING_WHITESPACE = /\A *\n */.freeze
18
- TRAILING_WHITESPACE = /\n *\z/.freeze
19
- NON_BLANK_NON_FIRST_LINE = /(?<=\n) *(?=\S)/.freeze
20
- NEWLINE_NON_BLANK = /\n(?=[^\n]+)/.freeze
21
-
22
- END_STATEMENT_END = /(;|\n|\{|\}) *\z/.freeze
23
- BEGIN_STATEMENT_END = /\A *(;|\{|\n|\})/.freeze
24
- END_WHITESPACE = /\s\z/.freeze
25
- BEGIN_WHITESPACE = /\A\s/.freeze
26
-
27
- RAW_VALUE = /\A\s*\${([\S\s]*)}\s*\z/.freeze
28
- TEMPLATE_STRING = /\A\s*`([\S\s]*)`\s*\z/.freeze
29
- INTERPOLATION = /\$\\?{([^}]*})?/.freeze
30
- HTML_ENTITY = /&([a-zA-Z]+|#\d+|x[0-9a-fA-F]+);/.freeze
31
- NON_CONTROL_STATEMENT = /#{INTERPOLATION}|(#{HTML_ENTITY})/.freeze
32
- CONTROL_STATEMENT = /[{}();]/.freeze
33
- CLOSE_STATEMENT = /;?\s*htx\.close\((\d*)\);?(\s*)\z/.freeze
34
-
35
- ##
36
- # Compiles an HTX template and assigns it the given name (conventionally the path of the template file is
37
- # used, but it can be anything).
38
- #
39
- def self.compile(name, template)
40
- doc = Nokogiri::HTML::DocumentFragment.parse(template)
41
- js = ''.dup
42
-
43
- process(doc, js, static_key: 0)
44
- js.rstrip!
45
-
46
- <<~EOS
47
- window['#{name}'] = function(htx) {
48
- #{js}
49
- }
50
- EOS
51
- end
52
-
53
- ##
54
- # Processes a DOM node.
55
- #
56
- def self.process(base, js, options = {})
57
- base.children.each do |node|
58
- next if node.comment?
59
-
60
- dynamic_key = process_value(node.attr(DYNAMIC_KEY_ATTR), :attr)
61
-
62
- if node.text? || node.name == ':'
63
- text = (node.text? ? node : node.children).text
64
-
65
- if (value = process_value(text))
66
- append(js,
67
- "#{indent(text[LEADING_WHITESPACE])}"\
68
- "htx.node(#{[
69
- value,
70
- dynamic_key,
71
- ((options[:static_key] += 1) << FLAG_BITS) | TEXT_NODE,
72
- ].compact.join(', ')})"\
73
- "#{indent(text[TRAILING_WHITESPACE])}"
74
- )
75
- else
76
- append(js, indent(text))
77
- end
78
- else
79
- attrs = node.attributes.inject([]) do |attrs, (_, attr)|
80
- next attrs if attr.name == DYNAMIC_KEY_ATTR
81
-
82
- attrs << "'#{ATTR_MAP[attr.name] || attr.name}'"
83
- attrs << process_value(attr.value, :attr)
84
- end
85
-
86
- append(js, "htx.node(#{[
87
- "'#{TAG_MAP[node.name] || node.name}'",
88
- attrs,
89
- dynamic_key,
90
- ((options[:static_key] += 1) << FLAG_BITS) | (node.children.empty? ? CHILDLESS : 0),
91
- ].compact.flatten.join(', ')})")
92
-
93
- unless node.children.empty?
94
- process(node, js, options)
95
-
96
- count = ''
97
- js.sub!(CLOSE_STATEMENT) do
98
- count = $1 == '' ? 2 : $1.to_i + 1
99
- $2
100
- end
101
-
102
- append(js, "htx.close(#{count})")
103
- end
104
- end
105
- end
106
- end
10
+ module HTX
11
+ EMPTY_HASH = {}.freeze
107
12
 
108
13
  ##
109
- # Appends a string to the compiled template function string with appropriate punctuation and/or
110
- # whitespace inserted.
14
+ # Convenience method to create a new Template instance and compile it.
111
15
  #
112
- def self.append(js, text)
113
- if js == ''
114
- # Do nothing.
115
- elsif js !~ END_STATEMENT_END && text !~ BEGIN_STATEMENT_END
116
- js << '; '
117
- elsif js !~ END_WHITESPACE && text !~ BEGIN_WHITESPACE
118
- js << ' '
119
- elsif js[-1] == "\n"
120
- js << ' '
121
- end
122
-
123
- js << text
16
+ def self.compile(name, template, options = EMPTY_HASH)
17
+ Template.new(name, template).compile(**options)
124
18
  end
125
19
 
126
20
  ##
127
- # Indents each line of a string (except the first).
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).
128
24
  #
129
- def self.indent(text)
130
- return '' unless text
131
-
132
- text.gsub!(NEWLINE_NON_BLANK, "\n ")
133
- text
134
- end
135
-
136
- ##
137
- # Processes, formats, and encodes an attribute or text node value. Returns nil if the value is determined
138
- # to be a control statement.
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.
139
28
  #
140
- def self.process_value(text, is_attr = false)
141
- return nil if text.nil? || (!is_attr && text.strip == '')
29
+ def self.new(name, content)
30
+ warn('HTX.new is deprecated. Please use HTX::Template.new instead.')
142
31
 
143
- if (value = text[RAW_VALUE, 1])
144
- # Entire text is enclosed in ${...}.
145
- value.strip!
146
- quote = false
147
- elsif (value = text[TEMPLATE_STRING, 1])
148
- # Entire text is enclosed in backticks (template string).
149
- quote = true
150
- elsif is_attr || text.gsub(NON_CONTROL_STATEMENT, '') !~ CONTROL_STATEMENT
151
- # Text is an attribute value or doesn't match control statement pattern.
152
- value = text.dup
153
- quote = true
154
- else
155
- return nil
156
- end
157
-
158
- # Strip one leading and trailing newline (and attached spaces) and perform outdent. Outdent amount
159
- # calculation ignores everything before the first newline in its search for the least-indented line.
160
- outdent = value.scan(NON_BLANK_NON_FIRST_LINE).min
161
- value.gsub!(/#{LEADING_WHITESPACE}|#{TRAILING_WHITESPACE}|^#{outdent}/, '')
162
- value.insert(0, '`').insert(-1, '`') if quote
163
-
164
- # Ensure any Unicode characters get converted to Unicode escape sequences. Also note that since Nokogiri
165
- # converts HTML entities to Unicode characters, this causes them to be properly passed to
166
- # `document.createTextNode` calls as Unicode escape sequences rather than (incorrectly) as HTML
167
- # entities.
168
- value.encode('ascii', fallback: ->(c) { "\\u#{c.ord.to_s(16).rjust(4, '0')}" })
32
+ Template.new(name, content)
169
33
  end
170
-
171
- # The Nokogiri HTML parser downcases all tag and attribute names, but SVG tags and attributes are case
172
- # sensitive and often mix cased. These maps are used to restore the correct case of such tags and
173
- # attributes.
174
- TAG_MAP = %w[
175
- animateMotion animateTransform clipPath feBlend feColorMatrix feComponentTransfer feComposite
176
- feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB
177
- feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight
178
- feSpecularLighting feSpotLight feTile feTurbulence foreignObject linearGradient radialGradient textPath
179
- ].map { |tag| [tag.downcase, tag] }.to_h.freeze
180
-
181
- ATTR_MAP = %w[
182
- attributeName baseFrequency calcMode clipPathUnits diffuseConstant edgeMode filterUnits
183
- gradientTransform gradientUnits kernelMatrix kernelUnitLength keyPoints keySplines keyTimes lengthAdjust
184
- limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits numOctaves pathLength
185
- patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ preserveAlpha
186
- preserveAspectRatio primitiveUnits refX refY repeatCount repeatDur requiredExtensions specularConstant
187
- specularExponent spreadMethod startOffset stdDeviation stitchTiles surfaceScale systemLanguage
188
- tableValues targetX targetY textLength viewBox xChannelSelector yChannelSelector zoomAndPan
189
- ].map { |attr| [attr.downcase, attr] }.to_h.freeze
190
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.1
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Pickens
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-03 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
@@ -42,27 +42,37 @@ dependencies:
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 5.11.2
48
+ - - "<"
46
49
  - !ruby/object:Gem::Version
47
- version: '5.11'
50
+ version: 6.0.0
48
51
  type: :development
49
52
  prerelease: false
50
53
  version_requirements: !ruby/object:Gem::Requirement
51
54
  requirements:
52
- - - "~>"
55
+ - - ">="
53
56
  - !ruby/object:Gem::Version
54
- version: '5.11'
57
+ version: 5.11.2
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: 6.0.0
55
61
  description: HTX is a full-featured HTML template system that is simple, lightweight,
56
62
  and highly performant. This library is a Ruby implementation of the HTX template
57
63
  compiler--it converts HTX templates to their compiled JavaScript form.
58
- email:
64
+ email:
59
65
  executables: []
60
66
  extensions: []
61
67
  extra_rdoc_files: []
62
68
  files:
63
69
  - LICENSE
64
70
  - README.md
71
+ - VERSION
65
72
  - lib/htx.rb
73
+ - lib/htx/malformed_template_error.rb
74
+ - lib/htx/template.rb
75
+ - lib/htx/version.rb
66
76
  homepage: https://github.com/npickens/htx
67
77
  licenses:
68
78
  - MIT
@@ -70,7 +80,7 @@ metadata:
70
80
  allowed_push_host: https://rubygems.org
71
81
  homepage_uri: https://github.com/npickens/htx
72
82
  source_code_uri: https://github.com/npickens/htx
73
- post_install_message:
83
+ post_install_message:
74
84
  rdoc_options: []
75
85
  require_paths:
76
86
  - lib
@@ -78,15 +88,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
88
  requirements:
79
89
  - - ">="
80
90
  - !ruby/object:Gem::Version
81
- version: '0'
91
+ version: 2.6.0
82
92
  required_rubygems_version: !ruby/object:Gem::Requirement
83
93
  requirements:
84
94
  - - ">="
85
95
  - !ruby/object:Gem::Version
86
96
  version: '0'
87
97
  requirements: []
88
- rubygems_version: 3.0.3
89
- signing_key:
98
+ rubygems_version: 3.2.32
99
+ signing_key:
90
100
  specification_version: 4
91
101
  summary: A Ruby compiler for HTX templates.
92
102
  test_files: []