htx 0.0.5 → 0.0.8
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 +4 -4
- data/LICENSE +1 -1
- data/README.md +10 -15
- data/VERSION +1 -0
- data/lib/htx/malformed_template_error.rb +25 -0
- data/lib/htx/template.rb +439 -0
- data/lib/htx/text_parser.rb +231 -0
- data/lib/htx/version.rb +5 -0
- data/lib/htx.rb +19 -237
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b83c07872c223b61b939883d9f2f6bc45a1cecc380c4b21701d04a606e7ae381
|
4
|
+
data.tar.gz: bfbf43071b20b13be3895bf4e358617765f39f276ed1d37c91c93f09c024b48a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0dc4f9f8fad2b18266ca93a562fa1565361b330744e8f02134341c1a5982f0821a9b008975adcff7840626d85f05bca223dd433705799c227217584dab1f970c
|
7
|
+
data.tar.gz: 58756df107105643831511c4faff3f7ccdc96fdcd318d7f65833cb642b4c08be60ab2e2f9cc6ffb999a9980a63a8a15a9b0b8b94c0aab8a4f8447ee61909c5a4
|
data/LICENSE
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright 2019-
|
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
@@ -20,29 +20,24 @@ gem install htx
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
To compile an HTX template, pass a name (conventionally the path of the template file) and template
|
24
|
-
strings to the `HTX.compile` method:
|
23
|
+
To compile an HTX template, pass a name (conventionally the path of the template file) and template content
|
24
|
+
as strings to the `HTX.compile` method (all other arguments are optional):
|
25
25
|
|
26
26
|
```ruby
|
27
|
-
path = '/
|
27
|
+
path = '/components/crew.htx'
|
28
28
|
template = File.read(File.join('some/asset/dir', path))
|
29
29
|
|
30
30
|
HTX.compile(path, template)
|
31
31
|
|
32
|
-
#
|
32
|
+
# window['/components/crew.htx'] = function(htx) {
|
33
|
+
# // ...
|
34
|
+
# }
|
35
|
+
|
33
36
|
HTX.compile(path, template, assign_to: 'myTemplates')
|
34
37
|
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# ...
|
39
|
-
# }
|
40
|
-
#
|
41
|
-
# If `assign_to` is specified:
|
42
|
-
#
|
43
|
-
# myTemplates['/components/people.htx'] = function(htx) {
|
44
|
-
# // ...
|
45
|
-
# }
|
38
|
+
# myTemplates['/components/crew.htx'] = function(htx) {
|
39
|
+
# // ...
|
40
|
+
# }
|
46
41
|
```
|
47
42
|
|
48
43
|
## Contributing
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.8
|
@@ -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
|
data/lib/htx/template.rb
ADDED
@@ -0,0 +1,439 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require('nokogiri')
|
4
|
+
|
5
|
+
module HTX
|
6
|
+
class Template
|
7
|
+
ELEMENT = 1 << 0
|
8
|
+
CHILDLESS = 1 << 1
|
9
|
+
XMLNS = 1 << 2
|
10
|
+
FLAG_BITS = 3
|
11
|
+
|
12
|
+
INDENT_DEFAULT = ' '
|
13
|
+
CONTENT_TAG = 'htx-content'
|
14
|
+
DYNAMIC_KEY_ATTR = 'htx-key'
|
15
|
+
DEFAULT_XMLNS = {
|
16
|
+
'math' => 'http://www.w3.org/1998/Math/MathML',
|
17
|
+
'svg' => 'http://www.w3.org/2000/svg',
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
INDENT_VALID = /^( +|\t+)$/.freeze
|
21
|
+
INDENT_GUESS = /^( +|\t+)(?=\S)/.freeze
|
22
|
+
INDENT_REGEX = /\n(?=[^\n])/.freeze
|
23
|
+
|
24
|
+
NO_SEMICOLON_BEGIN = /\A\s*[\n;}]/.freeze
|
25
|
+
NO_SEMICOLON_END = /(\A|[\n;{}][^\S\n]*)\z/.freeze
|
26
|
+
|
27
|
+
NEWLINE_BEGIN = /\A\s*\n/.freeze
|
28
|
+
NEWLINE_END = /\n[^\S\n]*\z/.freeze
|
29
|
+
NEWLINE_END_OPTIONAL = /\n?[^\S\n]*\z/.freeze
|
30
|
+
|
31
|
+
WHITESPACE_BEGIN = /\A\s/.freeze
|
32
|
+
NON_WHITESPACE = /\S/.freeze
|
33
|
+
|
34
|
+
##
|
35
|
+
# Returns false. In the near future when support for the <:> tag has been dropped (in favor of
|
36
|
+
# <htx-content>), will return true if Nokogiri's HTML5 parser is available. To use it now, monkey patch
|
37
|
+
# this method to return true.
|
38
|
+
#
|
39
|
+
def self.html5_parser?
|
40
|
+
false # !!defined?(Nokogiri::HTML5)
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Returns Nokogiri's HTML5 parser if available and enabled, and Nokogiri's regular HTML parser
|
45
|
+
# otherwise.
|
46
|
+
#
|
47
|
+
def self.nokogiri_parser
|
48
|
+
html5_parser? ? Nokogiri::HTML5 : Nokogiri::HTML
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Creates a new instance.
|
53
|
+
#
|
54
|
+
# * +name+ - Template name. Conventionally the path of the template file.
|
55
|
+
# * +content+ - Template content.
|
56
|
+
#
|
57
|
+
def initialize(name, content)
|
58
|
+
@name = name
|
59
|
+
@content = content
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Compiles the HTX template.
|
64
|
+
#
|
65
|
+
# * +assign_to+ - JavaScript object to assign the template function to (default: <tt>window</tt>).
|
66
|
+
# * +indent+ - DEPRECATED. Indentation amount (number) or string (must be only spaces or tabs but not
|
67
|
+
# both) to use for indentation of compiled output (default: indentation of first indented line of
|
68
|
+
# uncompiled template).
|
69
|
+
#
|
70
|
+
def compile(assign_to: nil, indent: (indent_omitted = true; nil))
|
71
|
+
@assign_to = assign_to || 'window'
|
72
|
+
@indent =
|
73
|
+
if indent.kind_of?(Numeric)
|
74
|
+
' ' * indent
|
75
|
+
elsif indent && !indent.match?(INDENT_VALID)
|
76
|
+
raise("Invalid indent value #{indent.inspect}: only spaces and tabs (but not both) are allowed")
|
77
|
+
else
|
78
|
+
indent || @content[INDENT_GUESS] || INDENT_DEFAULT
|
79
|
+
end
|
80
|
+
|
81
|
+
warn('The indent: option for HTX template compilation is deprecated.') unless indent_omitted
|
82
|
+
|
83
|
+
@static_key = 0
|
84
|
+
@close_count = 0
|
85
|
+
@whitespace_buff = nil
|
86
|
+
@statement_buff = +''
|
87
|
+
@compiled = +''
|
88
|
+
|
89
|
+
doc = self.class.nokogiri_parser.fragment(@content)
|
90
|
+
preprocess(doc)
|
91
|
+
process(doc)
|
92
|
+
|
93
|
+
@compiled
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
##
|
99
|
+
# Removes comment nodes and merges any adjoining text nodes that result from such removals.
|
100
|
+
#
|
101
|
+
# * +node+ - Nokogiri node to preprocess.
|
102
|
+
#
|
103
|
+
def preprocess(node)
|
104
|
+
if node.text?
|
105
|
+
if node.parent&.fragment? && node.blank?
|
106
|
+
node.remove
|
107
|
+
elsif (prev_node = node.previous)&.text?
|
108
|
+
prev_node.content += node.content
|
109
|
+
node.remove
|
110
|
+
end
|
111
|
+
elsif node.comment?
|
112
|
+
if node.previous&.text? && node.next&.text? && node.next.content.match?(NEWLINE_BEGIN)
|
113
|
+
content = node.previous.content.sub!(NEWLINE_END_OPTIONAL, '')
|
114
|
+
content.empty? ? node.previous.remove : node.previous.content = content
|
115
|
+
end
|
116
|
+
|
117
|
+
node.remove
|
118
|
+
end
|
119
|
+
|
120
|
+
node.children.each do |child|
|
121
|
+
preprocess(child)
|
122
|
+
end
|
123
|
+
|
124
|
+
if node.fragment?
|
125
|
+
children = node.children
|
126
|
+
root, root2 = children[0..1]
|
127
|
+
|
128
|
+
if (child = children.find(&:text?))
|
129
|
+
raise(MalformedTemplateError.new('text nodes are not allowed at root level', @name, child))
|
130
|
+
elsif !root
|
131
|
+
raise(MalformedTemplateError.new('a root node is required', @name))
|
132
|
+
elsif root2
|
133
|
+
raise(MalformedTemplateError.new("root node already defined on line #{root.line}", @name, root2))
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Processes a DOM node's descendents.
|
140
|
+
#
|
141
|
+
# * +node+ - Nokogiri node to process.
|
142
|
+
#
|
143
|
+
def process(node, xmlns: false)
|
144
|
+
if node.fragment?
|
145
|
+
process_fragment_node(node)
|
146
|
+
elsif node.element?
|
147
|
+
process_element_node(node, xmlns: xmlns)
|
148
|
+
elsif node.text?
|
149
|
+
process_text_node(node)
|
150
|
+
else
|
151
|
+
raise(MalformedTemplateError.new("unrecognized node type #{node.class}", @name, node))
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Processes a document fragment node.
|
157
|
+
#
|
158
|
+
# * +node+ - Nokogiri node to process.
|
159
|
+
#
|
160
|
+
def process_fragment_node(node)
|
161
|
+
append("#{@assign_to}['#{@name}'] = function(htx) {")
|
162
|
+
@whitespace_buff = "\n"
|
163
|
+
|
164
|
+
node.children.each do |child|
|
165
|
+
process(child)
|
166
|
+
end
|
167
|
+
|
168
|
+
append("\n}\n",)
|
169
|
+
flush
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Processes an element node.
|
174
|
+
#
|
175
|
+
# * +node+ - Nokogiri node to process.
|
176
|
+
# * +xmlns+ - True if node is the descendent of a node with an xmlns attribute.
|
177
|
+
#
|
178
|
+
def process_element_node(node, xmlns: false)
|
179
|
+
children = node.children
|
180
|
+
childless = children.empty? || (children.size == 1 && self.class.formatting_node?(children.first))
|
181
|
+
dynamic_key = self.class.attribute_value(node.attr(DYNAMIC_KEY_ATTR))
|
182
|
+
attributes = self.class.process_attributes(node)
|
183
|
+
xmlns ||= !!self.class.namespace(node)
|
184
|
+
|
185
|
+
if self.class.htx_content_node?(node)
|
186
|
+
if node.name != CONTENT_TAG
|
187
|
+
warn("#{@name}:#{node.line}: The <#{node.name}> tag has been deprecated. Use <#{CONTENT_TAG}> "\
|
188
|
+
"for identical functionality.")
|
189
|
+
end
|
190
|
+
|
191
|
+
if attributes.size > 0
|
192
|
+
raise(MalformedTemplateError.new("<#{node.name}> tags may not have attributes other than "\
|
193
|
+
"#{DYNAMIC_KEY_ATTR}", @name, node))
|
194
|
+
elsif (child = children.find { |n| !n.text? })
|
195
|
+
raise(MalformedTemplateError.new("<#{node.name}> tags may not contain child tags", @name, child))
|
196
|
+
end
|
197
|
+
|
198
|
+
process_text_node(
|
199
|
+
children.first || Nokogiri::XML::Text.new('', node.document),
|
200
|
+
dynamic_key: dynamic_key,
|
201
|
+
)
|
202
|
+
else
|
203
|
+
append_htx_node(
|
204
|
+
"'#{self.class.tag_name(node.name)}'",
|
205
|
+
*attributes,
|
206
|
+
dynamic_key,
|
207
|
+
ELEMENT | (childless ? CHILDLESS : 0) | (xmlns ? XMLNS : 0),
|
208
|
+
)
|
209
|
+
|
210
|
+
unless childless
|
211
|
+
children.each do |child|
|
212
|
+
process(child, xmlns: xmlns)
|
213
|
+
end
|
214
|
+
|
215
|
+
@close_count += 1
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Processes a text node.
|
222
|
+
#
|
223
|
+
# * +node+ - Nokogiri node to process.
|
224
|
+
# * +dynamic_key+ - Dynamic key of the parent if it's an <htx-content> node.
|
225
|
+
#
|
226
|
+
def process_text_node(node, dynamic_key: nil)
|
227
|
+
content = node.content
|
228
|
+
|
229
|
+
if node.blank?
|
230
|
+
if !content.include?("\n")
|
231
|
+
append_htx_node("`#{content}`")
|
232
|
+
elsif node.next
|
233
|
+
append(content)
|
234
|
+
else
|
235
|
+
@whitespace_buff = content[NEWLINE_END]
|
236
|
+
end
|
237
|
+
else
|
238
|
+
htx_content_node = self.class.htx_content_node?(node.parent)
|
239
|
+
parser = TextParser.new(content, statement_allowed: !htx_content_node)
|
240
|
+
parser.parse
|
241
|
+
|
242
|
+
append(parser.leading) unless htx_content_node
|
243
|
+
|
244
|
+
if parser.statement?
|
245
|
+
append(indent(parser.content))
|
246
|
+
elsif parser.raw?
|
247
|
+
append_htx_node(indent(parser.content), dynamic_key)
|
248
|
+
else
|
249
|
+
append_htx_node(parser.content, dynamic_key)
|
250
|
+
end
|
251
|
+
|
252
|
+
unless htx_content_node
|
253
|
+
append(parser.trailing)
|
254
|
+
@whitespace_buff = parser.whitespace_buff
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
##
|
260
|
+
# Appends a string to the compiled template function string with appropriate punctuation and/or
|
261
|
+
# whitespace inserted.
|
262
|
+
#
|
263
|
+
# * +text+ - String to append to the compiled template string.
|
264
|
+
#
|
265
|
+
def append(text)
|
266
|
+
return @compiled if text.nil? || text.empty?
|
267
|
+
|
268
|
+
if @close_count > 0
|
269
|
+
close_count = @close_count
|
270
|
+
@close_count = 0
|
271
|
+
|
272
|
+
append("htx.close(#{close_count unless close_count == 1})")
|
273
|
+
end
|
274
|
+
|
275
|
+
if @whitespace_buff
|
276
|
+
@statement_buff << @whitespace_buff
|
277
|
+
@whitespace_buff = nil
|
278
|
+
confirmed_newline = true
|
279
|
+
end
|
280
|
+
|
281
|
+
if (confirmed_newline || @statement_buff.match?(NEWLINE_END)) && !text.match?(NEWLINE_BEGIN)
|
282
|
+
@statement_buff << @indent
|
283
|
+
elsif !@statement_buff.match?(NO_SEMICOLON_END) && !text.match?(NO_SEMICOLON_BEGIN)
|
284
|
+
@statement_buff << ";#{' ' unless text.match?(WHITESPACE_BEGIN)}"
|
285
|
+
end
|
286
|
+
|
287
|
+
flush if text.match?(NON_WHITESPACE)
|
288
|
+
@statement_buff << text
|
289
|
+
|
290
|
+
@compiled
|
291
|
+
end
|
292
|
+
|
293
|
+
##
|
294
|
+
# Appends an +htx.node+ call to the compiled template function string.
|
295
|
+
#
|
296
|
+
# * +args+ - Arguments to use for the +htx.node+ call (any +nil+ ones are removed).
|
297
|
+
#
|
298
|
+
def append_htx_node(*args)
|
299
|
+
return if args.first.nil?
|
300
|
+
|
301
|
+
args.compact!
|
302
|
+
args << 0 unless args.last.kind_of?(Integer)
|
303
|
+
args[-1] |= (@static_key += 1) << FLAG_BITS
|
304
|
+
|
305
|
+
append("htx.node(#{args.join(', ')})")
|
306
|
+
end
|
307
|
+
|
308
|
+
##
|
309
|
+
# Flushes statement buffer.
|
310
|
+
#
|
311
|
+
def flush
|
312
|
+
@compiled << @statement_buff
|
313
|
+
@statement_buff.clear
|
314
|
+
|
315
|
+
@compiled
|
316
|
+
end
|
317
|
+
|
318
|
+
##
|
319
|
+
# Indents each line of a string (except the first).
|
320
|
+
#
|
321
|
+
# * +text+ - String of lines to indent.
|
322
|
+
#
|
323
|
+
def indent(text)
|
324
|
+
text.gsub!(INDENT_REGEX, "\\0#{@indent}")
|
325
|
+
text
|
326
|
+
end
|
327
|
+
|
328
|
+
##
|
329
|
+
# Returns true if the node is whitespace containing at least one newline.
|
330
|
+
#
|
331
|
+
# * +node+ - Nokogiri node to check.
|
332
|
+
#
|
333
|
+
def self.formatting_node?(node)
|
334
|
+
node.blank? && node.content.include?("\n")
|
335
|
+
end
|
336
|
+
|
337
|
+
##
|
338
|
+
# Returns true if the node is an <htx-content> node (or one of its now-deprecated names).
|
339
|
+
#
|
340
|
+
# * +node+ - Nokogiri node to check.
|
341
|
+
#
|
342
|
+
def self.htx_content_node?(node)
|
343
|
+
node && (node.name == CONTENT_TAG || node.name == 'htx-text' || node.name == ':')
|
344
|
+
end
|
345
|
+
|
346
|
+
##
|
347
|
+
# Processes a node's attributes returning a flat array of attribute names and values.
|
348
|
+
#
|
349
|
+
# * +node+ - Nokogiri node to process the attributes of.
|
350
|
+
#
|
351
|
+
def self.process_attributes(node)
|
352
|
+
attributes = []
|
353
|
+
|
354
|
+
if !node.attribute('xmlns') && (xmlns = namespace(node))
|
355
|
+
attributes.push(
|
356
|
+
attribute_name('xmlns'),
|
357
|
+
attribute_value(xmlns)
|
358
|
+
)
|
359
|
+
end
|
360
|
+
|
361
|
+
node.attribute_nodes.each.with_object(attributes) do |attribute, attributes|
|
362
|
+
next if attribute.node_name == DYNAMIC_KEY_ATTR
|
363
|
+
|
364
|
+
attributes.push(
|
365
|
+
attribute_name(attribute.node_name),
|
366
|
+
attribute_value(attribute.value)
|
367
|
+
)
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
##
|
372
|
+
#
|
373
|
+
#
|
374
|
+
def self.namespace(node)
|
375
|
+
node.namespace&.href || DEFAULT_XMLNS[node.name]
|
376
|
+
end
|
377
|
+
|
378
|
+
##
|
379
|
+
# Returns the given text if the HTML5 parser is in use, or looks up the value in the tag map to get the
|
380
|
+
# correctly-cased version, falling back to the supplied text if no mapping exists.
|
381
|
+
#
|
382
|
+
# * +text+ - Tag name as returned by Nokogiri.
|
383
|
+
#
|
384
|
+
def self.tag_name(text)
|
385
|
+
html5_parser? ? text : (TAG_MAP[text] || text)
|
386
|
+
end
|
387
|
+
|
388
|
+
##
|
389
|
+
# Returns the given text if the HTML5 parser is in use, or looks up the value in the attribute map to
|
390
|
+
# get the correctly-cased version, falling back to the supplied text if no mapping exists.
|
391
|
+
#
|
392
|
+
# * +text+ - Attribute name as returned by Nokogiri.
|
393
|
+
#
|
394
|
+
def self.attribute_name(text)
|
395
|
+
"'#{html5_parser? ? text : (ATTR_MAP[text] || text)}'"
|
396
|
+
end
|
397
|
+
|
398
|
+
##
|
399
|
+
# Returns the processed value of an attribute.
|
400
|
+
#
|
401
|
+
# * +text+ - Attribute value as returned by Nokogiri.
|
402
|
+
#
|
403
|
+
def self.attribute_value(text)
|
404
|
+
text ? TextParser.new(text, statement_allowed: false).parse : nil
|
405
|
+
end
|
406
|
+
|
407
|
+
# The Nokogiri HTML parser downcases all tag and attribute names, but SVG tags and attributes are case
|
408
|
+
# sensitive and often mix cased. These maps are used to restore the correct case of such tags and
|
409
|
+
# attributes.
|
410
|
+
#
|
411
|
+
# Note: Nokogiri's newer HTML5 parser resulting from the Nokogumbo merge fixes this issue, but it is
|
412
|
+
# currently not available for JRuby. It also does not parse <:> as a tag, which is why it's been
|
413
|
+
# deprecated in favor of <htx-content>. Once support for <:> has been completely removed, the HTML5
|
414
|
+
# parser will be used for regular Ruby and this tag and attribute mapping hack reserved for JRuby (and
|
415
|
+
# any other potential environments where the HTML5 parser is not available).
|
416
|
+
|
417
|
+
# Source: https://developer.mozilla.org/en-US/docs/Web/SVG/Element
|
418
|
+
TAG_MAP = %w[
|
419
|
+
animateMotion animateTransform clipPath feBlend feColorMatrix feComponentTransfer feComposite
|
420
|
+
feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow feFlood feFuncA
|
421
|
+
feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight
|
422
|
+
feSpecularLighting feSpotLight feTile feTurbulence foreignObject linearGradient radialGradient
|
423
|
+
textPath
|
424
|
+
].map { |tag| [tag.downcase, tag] }.to_h.freeze
|
425
|
+
|
426
|
+
# Source: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
|
427
|
+
ATTR_MAP = %w[
|
428
|
+
attributeName attributeType baseFrequency baseProfile calcMode clipPathUnits contentScriptType
|
429
|
+
contentStyleType diffuseConstant edgeMode filterRes filterUnits glyphRef gradientTransform
|
430
|
+
gradientUnits kernelMatrix kernelUnitLength keyPoints keySplines keyTimes lengthAdjust
|
431
|
+
limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits numOctaves
|
432
|
+
pathLength patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ
|
433
|
+
preserveAlpha preserveAspectRatio primitiveUnits refX refY referrerPolicy repeatCount repeatDur
|
434
|
+
requiredExtensions requiredFeatures specularConstant specularExponent spreadMethod startOffset
|
435
|
+
stdDeviation stitchTiles surfaceScale systemLanguage tableValues targetX targetY textLength viewBox
|
436
|
+
viewTarget xChannelSelector yChannelSelector zoomAndPan
|
437
|
+
].map { |attr| [attr.downcase, attr] }.to_h.freeze
|
438
|
+
end
|
439
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require('strscan')
|
4
|
+
|
5
|
+
module HTX
|
6
|
+
class TextParser
|
7
|
+
LEADING = /\A((?:[^\S\n]*\n)+)?((?:[^\S\n])+)?(?=\S)/.freeze
|
8
|
+
TRAILING = /([\S\s]*?)(\s*?)(\n[^\S\n]*)?\z/.freeze
|
9
|
+
|
10
|
+
NOT_ESCAPED = /(?<=^|[^\\])(?:\\\\)*/.freeze
|
11
|
+
OF_INTEREST = /#{NOT_ESCAPED}(?<chars>[`'"]|\${)|(?<chars>{|}|\/\/|\/\*|\*\/|\n[^\S\n]*(?=\S))/.freeze
|
12
|
+
TERMINATOR = {
|
13
|
+
'\'' => '\'',
|
14
|
+
'"' => '"',
|
15
|
+
'//' => "\n",
|
16
|
+
'/*' => '*/',
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
TEXT = /\S|\A[^\S\n]+\z/.freeze
|
20
|
+
IDENTIFIER = /[_$a-zA-Z][_$a-zA-Z0-9]*/.freeze
|
21
|
+
ASSIGNMENT = /\s*(\+|&|\||\^|\/|\*\*|<<|&&|\?\?|\|\||\*|%|-|>>>)?=/.freeze
|
22
|
+
STATEMENT = /[{}]|(^|\s)#{IDENTIFIER}(\.#{IDENTIFIER})*(#{ASSIGNMENT}|\+\+|--|\[|\()/.freeze
|
23
|
+
|
24
|
+
attr_reader(:type, :content, :leading, :trailing, :whitespace_buff)
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates a new instance.
|
28
|
+
#
|
29
|
+
# * +text+ - Text to parse.
|
30
|
+
# * +statement_allowed+ - True if statements are allowed; false if text is always a template or raw (
|
31
|
+
# single interpolation).
|
32
|
+
#
|
33
|
+
def initialize(text, statement_allowed:)
|
34
|
+
@text = text
|
35
|
+
@statement_allowed = statement_allowed
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Returns true if parsed text is a statement.
|
40
|
+
#
|
41
|
+
def statement?
|
42
|
+
@type == :statement
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns true if parsed text is a single interpolation.
|
47
|
+
#
|
48
|
+
def raw?
|
49
|
+
@type == :raw
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Returns true if parsed text is a template.
|
54
|
+
#
|
55
|
+
def template?
|
56
|
+
@type == :template
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Parses text.
|
61
|
+
#
|
62
|
+
def parse
|
63
|
+
@type = nil
|
64
|
+
@content = +''
|
65
|
+
|
66
|
+
@first_indent = nil
|
67
|
+
@min_indent = nil
|
68
|
+
@last_indent = nil
|
69
|
+
|
70
|
+
@buffer = +''
|
71
|
+
@stack = []
|
72
|
+
curlies = []
|
73
|
+
ignore_end = nil
|
74
|
+
|
75
|
+
@has_text = false
|
76
|
+
@is_statement = false
|
77
|
+
@template_count = 0
|
78
|
+
@interpolation_count = 0
|
79
|
+
|
80
|
+
scanner = StringScanner.new(@text)
|
81
|
+
|
82
|
+
scanner.scan(LEADING)
|
83
|
+
@leading_newlines = scanner[1]
|
84
|
+
@leading_indent = @first_indent = scanner[2]
|
85
|
+
@leading = scanner[0] if @leading_newlines || @leading_indent
|
86
|
+
|
87
|
+
until scanner.eos?
|
88
|
+
if (scanned = scanner.scan_until(OF_INTEREST))
|
89
|
+
ignore = @stack.last == :ignore
|
90
|
+
template = @stack.last == :template
|
91
|
+
interpolation = @stack.last == :interpolation
|
92
|
+
can_ignore = (@stack.empty? && @statement_allowed) || interpolation
|
93
|
+
can_template = @stack.empty? || interpolation
|
94
|
+
can_interpolate = @stack.empty? || template
|
95
|
+
|
96
|
+
chars = scanner[:chars]
|
97
|
+
@buffer << scanned.chomp!(chars)
|
98
|
+
|
99
|
+
if chars[0] == "\n"
|
100
|
+
indent = chars.delete_prefix("\n")
|
101
|
+
|
102
|
+
if !@last_indent
|
103
|
+
@last_indent = indent
|
104
|
+
else
|
105
|
+
@min_indent = @last_indent if !@min_indent || @last_indent.size < @min_indent.size
|
106
|
+
@last_indent = indent
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if can_ignore && (ignore_end = TERMINATOR[chars])
|
111
|
+
push_state(:ignore)
|
112
|
+
@buffer << chars
|
113
|
+
elsif ignore && chars == ignore_end
|
114
|
+
@buffer << chars
|
115
|
+
pop_state
|
116
|
+
ignore_end = nil
|
117
|
+
elsif can_template && chars == '`'
|
118
|
+
push_state(:template)
|
119
|
+
@buffer << chars
|
120
|
+
elsif template && chars == '`'
|
121
|
+
@buffer << chars
|
122
|
+
pop_state
|
123
|
+
elsif can_interpolate && chars == '${'
|
124
|
+
push_state(:interpolation)
|
125
|
+
curlies << 1
|
126
|
+
@buffer << chars
|
127
|
+
elsif interpolation && (curlies[-1] += (chars == '{' && 1) || (chars == '}' && -1) || 0) == 0
|
128
|
+
@buffer << chars
|
129
|
+
curlies.pop
|
130
|
+
pop_state
|
131
|
+
else
|
132
|
+
@buffer << chars
|
133
|
+
end
|
134
|
+
else
|
135
|
+
scanner.scan(TRAILING)
|
136
|
+
|
137
|
+
@buffer << scanner[1]
|
138
|
+
@trailing = scanner[2] == '' ? nil : scanner[2]
|
139
|
+
@whitespace_buff = scanner[3]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
flush(@stack.last)
|
144
|
+
finalize
|
145
|
+
|
146
|
+
@content
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
##
|
152
|
+
# Determines type (statement, raw, or template) and adjust formatting accordingly. Called at the end of
|
153
|
+
# parsing.
|
154
|
+
#
|
155
|
+
def finalize
|
156
|
+
if @is_statement
|
157
|
+
@type = :statement
|
158
|
+
@content.insert(0, @leading) && @leading = nil if @leading
|
159
|
+
@content.insert(-1, @trailing) && @trailing = nil if @trailing
|
160
|
+
elsif !@has_text && @template_count == 0 && @interpolation_count == 1
|
161
|
+
@type = :raw
|
162
|
+
@content.delete_prefix!('${')
|
163
|
+
@content.delete_suffix!('}')
|
164
|
+
|
165
|
+
if @content.empty?
|
166
|
+
@type = :template
|
167
|
+
@content = '``'
|
168
|
+
end
|
169
|
+
else
|
170
|
+
@type = :template
|
171
|
+
|
172
|
+
if !@has_text && @template_count == 1 && @interpolation_count == 0
|
173
|
+
@quoted = true
|
174
|
+
@outdent = @min_indent
|
175
|
+
@content.delete_prefix!('`')
|
176
|
+
@content.delete_suffix!('`')
|
177
|
+
else
|
178
|
+
@outdent = [@first_indent, @min_indent, @last_indent].compact.min
|
179
|
+
end
|
180
|
+
|
181
|
+
@content.gsub!(/(?<=\n)([^\S\n]+$|#{@outdent})/, '') if @outdent && !@outdent.empty?
|
182
|
+
|
183
|
+
unless @quoted
|
184
|
+
@content.insert(0, @leading) && @leading = nil if @leading && !@leading_newlines
|
185
|
+
@content.insert(-1, @trailing) && @trailing = nil if @trailing && !@trailing.include?("\n")
|
186
|
+
end
|
187
|
+
|
188
|
+
@content.insert(0, '`').insert(-1, '`')
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Pushes a state onto the stack during parsing.
|
194
|
+
#
|
195
|
+
# * +state+ - State to push onto the stack.
|
196
|
+
#
|
197
|
+
def push_state(state)
|
198
|
+
flush if @stack.empty?
|
199
|
+
@stack << state
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Pops a state from the stack during parsing.
|
204
|
+
#
|
205
|
+
def pop_state
|
206
|
+
state = @stack.pop
|
207
|
+
|
208
|
+
if @stack.empty?
|
209
|
+
flush(state)
|
210
|
+
|
211
|
+
@interpolation_count += 1 if state == :interpolation
|
212
|
+
@template_count += 1 if state == :template
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# Flushes buffer during parsing and performs statement detection if appropriate.
|
218
|
+
#
|
219
|
+
def flush(state = nil)
|
220
|
+
return if @buffer.empty?
|
221
|
+
|
222
|
+
if !state || state == :text
|
223
|
+
@has_text ||= @buffer.match?(TEXT)
|
224
|
+
@is_statement ||= @buffer.match?(STATEMENT) if @statement_allowed
|
225
|
+
end
|
226
|
+
|
227
|
+
@content << @buffer
|
228
|
+
@buffer.clear
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/lib/htx/version.rb
ADDED
data/lib/htx.rb
CHANGED
@@ -1,256 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require('
|
3
|
+
require('htx/malformed_template_error')
|
4
|
+
require('htx/template')
|
5
|
+
require('htx/text_parser')
|
6
|
+
require('htx/version')
|
4
7
|
|
5
8
|
##
|
6
9
|
# A Ruby compiler for HTX templates.
|
7
10
|
#
|
8
|
-
|
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
|
-
|
11
|
+
module HTX
|
37
12
|
EMPTY_HASH = {}.freeze
|
38
13
|
|
39
14
|
##
|
40
|
-
# Convenience method to create a new instance and
|
41
|
-
#
|
42
|
-
def self.compile(name, template, options = EMPTY_HASH)
|
43
|
-
new(name, template).compile(options)
|
44
|
-
end
|
45
|
-
|
46
|
-
##
|
47
|
-
# Creates a new HTX instance. Conventionally the path of the template file is used for the name, but it
|
48
|
-
# can be anything.
|
49
|
-
#
|
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.
|
61
|
-
#
|
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
|
86
|
-
|
87
|
-
process(doc)
|
88
|
-
@compiled.rstrip!
|
89
|
-
|
90
|
-
<<~EOS
|
91
|
-
#{options[:assign_to] || 'window'}['#{@name}'] = function(htx) {
|
92
|
-
#{@indent}#{@compiled}
|
93
|
-
}
|
94
|
-
EOS
|
95
|
-
end
|
96
|
-
|
97
|
-
private
|
98
|
-
|
99
|
-
##
|
100
|
-
# Processes a DOM node's descendents.
|
15
|
+
# Convenience method to create a new Template instance and compile it.
|
101
16
|
#
|
102
|
-
|
103
|
-
|
104
|
-
|
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.
|
17
|
+
# * +name+ - Template name. Conventionally the path of the template file.
|
18
|
+
# * +content+ - Template content.
|
19
|
+
# * +options+ - Options to be passed to Template#compile.
|
163
20
|
#
|
164
|
-
def
|
165
|
-
|
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
|
21
|
+
def self.compile(name, content, options = EMPTY_HASH)
|
22
|
+
Template.new(name, content).compile(**options)
|
176
23
|
end
|
177
24
|
|
178
25
|
##
|
179
|
-
#
|
26
|
+
# DEPRECATED. Use HTX::Template.new instead. HTX was formerly a class that would be instantiated for
|
27
|
+
# compilation. This method allows HTX.new calls to continue working (but support will be removed in the
|
28
|
+
# near future).
|
180
29
|
#
|
181
|
-
|
182
|
-
|
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.
|
30
|
+
# * +name+ - Template name. Conventionally the path of the template file.
|
31
|
+
# * +content+ - Template content.
|
191
32
|
#
|
192
|
-
def
|
193
|
-
|
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
|
33
|
+
def self.new(name, content)
|
34
|
+
warn('HTX.new is deprecated. Please use HTX::Template.new instead.')
|
209
35
|
|
210
|
-
|
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
|
36
|
+
Template.new(name, content)
|
233
37
|
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
38
|
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.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Pickens
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-31 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.
|
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.
|
26
|
+
version: '1.13'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -68,7 +68,12 @@ 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/text_parser.rb
|
76
|
+
- lib/htx/version.rb
|
72
77
|
homepage: https://github.com/npickens/htx
|
73
78
|
licenses:
|
74
79
|
- MIT
|
@@ -84,14 +89,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
89
|
requirements:
|
85
90
|
- - ">="
|
86
91
|
- !ruby/object:Gem::Version
|
87
|
-
version: 2.
|
92
|
+
version: 2.6.0
|
88
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
94
|
requirements:
|
90
95
|
- - ">="
|
91
96
|
- !ruby/object:Gem::Version
|
92
97
|
version: '0'
|
93
98
|
requirements: []
|
94
|
-
rubygems_version: 3.2.
|
99
|
+
rubygems_version: 3.2.32
|
95
100
|
signing_key:
|
96
101
|
specification_version: 4
|
97
102
|
summary: A Ruby compiler for HTX templates.
|