htx 0.0.5 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|