htx 0.0.6 → 0.0.9
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/README.md +10 -15
- data/VERSION +1 -1
- data/lib/htx/template.rb +291 -183
- data/lib/htx/text_parser.rb +233 -0
- data/lib/htx/version.rb +1 -1
- data/lib/htx.rb +9 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 395620b3af20b4da17e4413632418481ee28d6d0fe6b46427af240475087efce
|
4
|
+
data.tar.gz: bc15b61294daaf218b3a3d5a106de7c9279a7d9e8cb7ebd01002fcd618491b1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83805b10f30d1846a187b458579a0748339a3956a4123226b7b9ebdb6dcff6cae3dbcd94913c1bbab2c91186ef3f46642f30a4ed9c3f30b072b3efe2f2ceb1ca
|
7
|
+
data.tar.gz: 26e48419a9a719f1b8b582ef9e525ffcc37affdcb279f0cba95c6fe4cd2108cc6ec66e9ba4d3216a0ef02aad506d56c4743b059916ba78409f0dc672a760a1fc
|
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
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.9
|
data/lib/htx/template.rb
CHANGED
@@ -4,42 +4,36 @@ require('nokogiri')
|
|
4
4
|
|
5
5
|
module HTX
|
6
6
|
class Template
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
FLAG_BITS
|
7
|
+
ELEMENT = 1 << 0
|
8
|
+
CHILDLESS = 1 << 1
|
9
|
+
XMLNS = 1 << 2
|
10
|
+
FLAG_BITS = 3
|
11
11
|
|
12
12
|
INDENT_DEFAULT = ' '
|
13
|
-
|
13
|
+
CONTENT_TAG = 'htx-content'
|
14
14
|
DYNAMIC_KEY_ATTR = 'htx-key'
|
15
|
-
|
16
15
|
DEFAULT_XMLNS = {
|
17
16
|
'math' => 'http://www.w3.org/1998/Math/MathML',
|
18
17
|
'svg' => 'http://www.w3.org/2000/svg',
|
19
18
|
}.freeze
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
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
|
39
33
|
|
40
34
|
##
|
41
35
|
# Returns false. In the near future when support for the <:> tag has been dropped (in favor of
|
42
|
-
# <htx-
|
36
|
+
# <htx-content>), will return true if Nokogiri's HTML5 parser is available. To use it now, monkey patch
|
43
37
|
# this method to return true.
|
44
38
|
#
|
45
39
|
def self.html5_parser?
|
@@ -51,15 +45,14 @@ module HTX
|
|
51
45
|
# otherwise.
|
52
46
|
#
|
53
47
|
def self.nokogiri_parser
|
54
|
-
html5_parser? ? Nokogiri::HTML5
|
48
|
+
html5_parser? ? Nokogiri::HTML5 : Nokogiri::HTML
|
55
49
|
end
|
56
50
|
|
57
51
|
##
|
58
|
-
# Creates a new
|
52
|
+
# Creates a new instance.
|
59
53
|
#
|
60
|
-
# * +name+ -
|
61
|
-
#
|
62
|
-
# * +content+ - Template content string.
|
54
|
+
# * +name+ - Template name. Conventionally the path of the template file.
|
55
|
+
# * +content+ - Template content.
|
63
56
|
#
|
64
57
|
def initialize(name, content)
|
65
58
|
@name = name
|
@@ -69,107 +62,196 @@ module HTX
|
|
69
62
|
##
|
70
63
|
# Compiles the HTX template.
|
71
64
|
#
|
72
|
-
# * +
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
def compile(
|
78
|
-
|
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
|
-
|
65
|
+
# * +assign_to+ - JavaScript object to assign the template function to (default: +window+).
|
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'
|
93
72
|
@indent =
|
94
73
|
if indent.kind_of?(Numeric)
|
95
74
|
' ' * indent
|
96
|
-
elsif indent.
|
97
|
-
raise("Invalid indent value #{indent.inspect}: only spaces and tabs are allowed")
|
75
|
+
elsif indent && !indent.match?(INDENT_VALID)
|
76
|
+
raise("Invalid indent value #{indent.inspect}: only spaces and tabs (but not both) are allowed")
|
98
77
|
else
|
99
78
|
indent || @content[INDENT_GUESS] || INDENT_DEFAULT
|
100
79
|
end
|
101
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)
|
102
91
|
process(doc)
|
103
|
-
@compiled.rstrip!
|
104
92
|
|
105
|
-
|
106
|
-
#{assign_to}['#{@name}'] = function(htx) {
|
107
|
-
#{@indent}#{@compiled}
|
108
|
-
}
|
109
|
-
EOS
|
93
|
+
@compiled
|
110
94
|
end
|
111
95
|
|
112
96
|
private
|
113
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
|
+
|
114
138
|
##
|
115
139
|
# Processes a DOM node's descendents.
|
116
140
|
#
|
117
|
-
# * +
|
141
|
+
# * +node+ - Nokogiri node to process.
|
118
142
|
#
|
119
|
-
def process(
|
120
|
-
|
121
|
-
|
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
|
122
154
|
|
123
|
-
|
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"
|
124
163
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
'for identical functionality.')
|
129
|
-
end
|
164
|
+
node.children.each do |child|
|
165
|
+
process(child)
|
166
|
+
end
|
130
167
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
end
|
168
|
+
append("\n}\n",)
|
169
|
+
flush
|
170
|
+
end
|
135
171
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
172
|
+
##
|
173
|
+
# Processes an element node.
|
174
|
+
#
|
175
|
+
# * +node+ - Nokogiri node to process.
|
176
|
+
# * +xmlns+ - True if node is the descendant 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)
|
150
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)
|
151
234
|
else
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
end
|
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
|
173
255
|
end
|
174
256
|
end
|
175
257
|
end
|
@@ -181,17 +263,52 @@ module HTX
|
|
181
263
|
# * +text+ - String to append to the compiled template string.
|
182
264
|
#
|
183
265
|
def append(text)
|
184
|
-
if
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
@compiled << @indent
|
266
|
+
return 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})")
|
192
273
|
end
|
193
274
|
|
194
|
-
@
|
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
|
+
end
|
290
|
+
|
291
|
+
##
|
292
|
+
# Appends an +htx.node+ call to the compiled template function string.
|
293
|
+
#
|
294
|
+
# * +args+ - Arguments to use for the +htx.node+ call (any +nil+ ones are removed).
|
295
|
+
#
|
296
|
+
def append_htx_node(*args)
|
297
|
+
return if args.first.nil?
|
298
|
+
|
299
|
+
args.compact!
|
300
|
+
args << 0 unless args.last.kind_of?(Integer)
|
301
|
+
args[-1] |= (@static_key += 1) << FLAG_BITS
|
302
|
+
|
303
|
+
append("htx.node(#{args.join(', ')})")
|
304
|
+
end
|
305
|
+
|
306
|
+
##
|
307
|
+
# Flushes statement buffer.
|
308
|
+
#
|
309
|
+
def flush
|
310
|
+
@compiled << @statement_buff
|
311
|
+
@statement_buff.clear
|
195
312
|
end
|
196
313
|
|
197
314
|
##
|
@@ -200,98 +317,89 @@ module HTX
|
|
200
317
|
# * +text+ - String of lines to indent.
|
201
318
|
#
|
202
319
|
def indent(text)
|
203
|
-
|
204
|
-
|
205
|
-
text.gsub!(NEWLINE_NON_BLANK, "\n#{@indent}")
|
320
|
+
text.gsub!(INDENT_REGEX, "\\0#{@indent}")
|
206
321
|
text
|
207
322
|
end
|
208
323
|
|
209
324
|
##
|
210
|
-
#
|
211
|
-
#
|
212
|
-
#
|
213
|
-
#
|
214
|
-
|
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')}" })
|
325
|
+
# Returns true if the node is whitespace containing at least one newline.
|
326
|
+
#
|
327
|
+
# * +node+ - Nokogiri node to check.
|
328
|
+
#
|
329
|
+
def self.formatting_node?(node)
|
330
|
+
node.blank? && node.content.include?("\n")
|
245
331
|
end
|
246
332
|
|
247
333
|
##
|
248
|
-
#
|
249
|
-
# boolean indicating whether or not an xmlns attribute is present.
|
334
|
+
# Returns true if the node is an <htx-content> node (or one of its now-deprecated names).
|
250
335
|
#
|
251
|
-
#
|
252
|
-
# will be automatically added since it is required for those elements to render properly.
|
336
|
+
# * +node+ - Nokogiri node to check.
|
253
337
|
#
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
attrs = []
|
258
|
-
xmlns = !!node.attributes['xmlns']
|
259
|
-
|
260
|
-
if !xmlns && DEFAULT_XMLNS[node.name]
|
261
|
-
xmlns = true
|
338
|
+
def self.htx_content_node?(node)
|
339
|
+
node && (node.name == CONTENT_TAG || node.name == 'htx-text' || node.name == ':')
|
340
|
+
end
|
262
341
|
|
263
|
-
|
264
|
-
|
342
|
+
##
|
343
|
+
# Processes a node's attributes returning a flat array of attribute names and values.
|
344
|
+
#
|
345
|
+
# * +node+ - Nokogiri node to process the attributes of.
|
346
|
+
#
|
347
|
+
def self.process_attributes(node)
|
348
|
+
attributes = []
|
349
|
+
|
350
|
+
if !node.attribute('xmlns') && (xmlns = namespace(node))
|
351
|
+
attributes.push(
|
352
|
+
attribute_name('xmlns'),
|
353
|
+
attribute_value(xmlns)
|
354
|
+
)
|
265
355
|
end
|
266
356
|
|
267
|
-
node.
|
268
|
-
next if
|
357
|
+
node.attribute_nodes.each.with_object(attributes) do |attribute, attributes|
|
358
|
+
next if attribute.node_name == DYNAMIC_KEY_ATTR
|
269
359
|
|
270
|
-
|
271
|
-
|
360
|
+
attributes.push(
|
361
|
+
attribute_name(attribute.node_name),
|
362
|
+
attribute_value(attribute.value)
|
363
|
+
)
|
272
364
|
end
|
365
|
+
end
|
273
366
|
|
274
|
-
|
367
|
+
##
|
368
|
+
# Returns namespace URL of a Nokogiri node.
|
369
|
+
#
|
370
|
+
# * +node+ - Nokogiri node to get the namespace of.
|
371
|
+
#
|
372
|
+
def self.namespace(node)
|
373
|
+
node.namespace&.href || DEFAULT_XMLNS[node.name]
|
275
374
|
end
|
276
375
|
|
277
376
|
##
|
278
377
|
# Returns the given text if the HTML5 parser is in use, or looks up the value in the tag map to get the
|
279
378
|
# correctly-cased version, falling back to the supplied text if no mapping exists.
|
280
379
|
#
|
281
|
-
# * +text+ - Tag name as returned by Nokogiri
|
380
|
+
# * +text+ - Tag name as returned by Nokogiri.
|
282
381
|
#
|
283
|
-
def tag_name(text)
|
284
|
-
|
382
|
+
def self.tag_name(text)
|
383
|
+
html5_parser? ? text : (TAG_MAP[text] || text)
|
285
384
|
end
|
286
385
|
|
287
386
|
##
|
288
387
|
# Returns the given text if the HTML5 parser is in use, or looks up the value in the attribute map to
|
289
388
|
# get the correctly-cased version, falling back to the supplied text if no mapping exists.
|
290
389
|
#
|
291
|
-
# * +text+ - Attribute name as returned by Nokogiri
|
390
|
+
# * +text+ - Attribute name as returned by Nokogiri.
|
391
|
+
#
|
392
|
+
def self.attribute_name(text)
|
393
|
+
"'#{html5_parser? ? text : (ATTR_MAP[text] || text)}'"
|
394
|
+
end
|
395
|
+
|
396
|
+
##
|
397
|
+
# Returns the processed value of an attribute.
|
398
|
+
#
|
399
|
+
# * +text+ - Attribute value as returned by Nokogiri.
|
292
400
|
#
|
293
|
-
def
|
294
|
-
|
401
|
+
def self.attribute_value(text)
|
402
|
+
text ? TextParser.new(text, statement_allowed: false).parse : nil
|
295
403
|
end
|
296
404
|
|
297
405
|
# The Nokogiri HTML parser downcases all tag and attribute names, but SVG tags and attributes are case
|
@@ -300,9 +408,9 @@ module HTX
|
|
300
408
|
#
|
301
409
|
# Note: Nokogiri's newer HTML5 parser resulting from the Nokogumbo merge fixes this issue, but it is
|
302
410
|
# 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-
|
304
|
-
# will be used for regular Ruby and this tag and attribute mapping hack reserved for JRuby (and
|
305
|
-
# other potential environments where the HTML5 parser is not available).
|
411
|
+
# deprecated in favor of <htx-content>. Once support for <:> has been completely removed, the HTML5
|
412
|
+
# parser will be used for regular Ruby and this tag and attribute mapping hack reserved for JRuby (and
|
413
|
+
# any other potential environments where the HTML5 parser is not available).
|
306
414
|
|
307
415
|
# Source: https://developer.mozilla.org/en-US/docs/Web/SVG/Element
|
308
416
|
TAG_MAP = %w[
|
@@ -0,0 +1,233 @@
|
|
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
|
+
# * +state+ - Last state that was parsed.
|
220
|
+
#
|
221
|
+
def flush(state = nil)
|
222
|
+
return if @buffer.empty?
|
223
|
+
|
224
|
+
if !state || state == :text
|
225
|
+
@has_text ||= @buffer.match?(TEXT)
|
226
|
+
@is_statement ||= @buffer.match?(STATEMENT) if @statement_allowed
|
227
|
+
end
|
228
|
+
|
229
|
+
@content << @buffer
|
230
|
+
@buffer.clear
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
data/lib/htx/version.rb
CHANGED
data/lib/htx.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require('htx/malformed_template_error')
|
4
4
|
require('htx/template')
|
5
|
+
require('htx/text_parser')
|
5
6
|
require('htx/version')
|
6
7
|
|
7
8
|
##
|
@@ -13,8 +14,12 @@ module HTX
|
|
13
14
|
##
|
14
15
|
# Convenience method to create a new Template instance and compile it.
|
15
16
|
#
|
16
|
-
|
17
|
-
|
17
|
+
# * +name+ - Template name. Conventionally the path of the template file.
|
18
|
+
# * +content+ - Template content.
|
19
|
+
# * +options+ - Options to be passed to Template#compile.
|
20
|
+
#
|
21
|
+
def self.compile(name, content, options = EMPTY_HASH)
|
22
|
+
Template.new(name, content).compile(**options)
|
18
23
|
end
|
19
24
|
|
20
25
|
##
|
@@ -22,9 +27,8 @@ module HTX
|
|
22
27
|
# compilation. This method allows HTX.new calls to continue working (but support will be removed in the
|
23
28
|
# near future).
|
24
29
|
#
|
25
|
-
# * +name+ -
|
26
|
-
#
|
27
|
-
# * +content+ - Template content string.
|
30
|
+
# * +name+ - Template name. Conventionally the path of the template file.
|
31
|
+
# * +content+ - Template content.
|
28
32
|
#
|
29
33
|
def self.new(name, content)
|
30
34
|
warn('HTX.new is deprecated. Please use HTX::Template.new instead.')
|
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.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Pickens
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-03-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -72,6 +72,7 @@ files:
|
|
72
72
|
- lib/htx.rb
|
73
73
|
- lib/htx/malformed_template_error.rb
|
74
74
|
- lib/htx/template.rb
|
75
|
+
- lib/htx/text_parser.rb
|
75
76
|
- lib/htx/version.rb
|
76
77
|
homepage: https://github.com/npickens/htx
|
77
78
|
licenses:
|