htx 0.0.2 → 0.0.7

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