htx 0.0.5 → 0.0.6
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/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.
|