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