htx 0.0.2 → 0.0.7
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 +9 -0
- data/VERSION +1 -0
- data/lib/htx/malformed_template_error.rb +25 -0
- data/lib/htx/template.rb +340 -0
- data/lib/htx/version.rb +5 -0
- data/lib/htx.rb +17 -203
- metadata +24 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc1adc5f7b7b12da438f6c96c5a81b3585a8f9a8b761d54eee3895b5195898ec
|
4
|
+
data.tar.gz: cfc75dd48759fdb2151db74de71ce9c7ae07a73e07d20af415e96a0a37c07bda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/htx/template.rb
ADDED
@@ -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
|
data/lib/htx/version.rb
ADDED
data/lib/htx.rb
CHANGED
@@ -1,220 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require('
|
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
|
-
|
9
|
-
|
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
|
-
#
|
140
|
-
# inserted.
|
14
|
+
# Convenience method to create a new Template instance and compile it.
|
141
15
|
#
|
142
|
-
def
|
143
|
-
|
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
|
-
#
|
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
|
-
|
160
|
-
|
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
|
171
|
-
|
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
|
-
|
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.
|
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:
|
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.
|
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
|
@@ -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:
|
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:
|
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:
|
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.
|
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: []
|