htx 0.0.7 → 0.0.8
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 +286 -187
- data/lib/htx/text_parser.rb +231 -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: b83c07872c223b61b939883d9f2f6bc45a1cecc380c4b21701d04a606e7ae381
|
4
|
+
data.tar.gz: bfbf43071b20b13be3895bf4e358617765f39f276ed1d37c91c93f09c024b48a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0dc4f9f8fad2b18266ca93a562fa1565361b330744e8f02134341c1a5982f0821a9b008975adcff7840626d85f05bca223dd433705799c227217584dab1f970c
|
7
|
+
data.tar.gz: 58756df107105643831511c4faff3f7ccdc96fdcd318d7f65833cb642b4c08be60ab2e2f9cc6ffb999a9980a63a8a15a9b0b8b94c0aab8a4f8447ee61909c5a4
|
data/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.8
|
data/lib/htx/template.rb
CHANGED
@@ -4,39 +4,32 @@ require('nokogiri')
|
|
4
4
|
|
5
5
|
module HTX
|
6
6
|
class Template
|
7
|
-
ELEMENT =
|
8
|
-
CHILDLESS =
|
9
|
-
XMLNS =
|
7
|
+
ELEMENT = 1 << 0
|
8
|
+
CHILDLESS = 1 << 1
|
9
|
+
XMLNS = 1 << 2
|
10
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
|
-
UNESCAPED_BACKTICK = /(?<!\\)((\\\\)*)`/.freeze
|
39
|
-
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
|
40
33
|
|
41
34
|
##
|
42
35
|
# Returns false. In the near future when support for the <:> tag has been dropped (in favor of
|
@@ -52,15 +45,14 @@ module HTX
|
|
52
45
|
# otherwise.
|
53
46
|
#
|
54
47
|
def self.nokogiri_parser
|
55
|
-
html5_parser? ? Nokogiri::HTML5
|
48
|
+
html5_parser? ? Nokogiri::HTML5 : Nokogiri::HTML
|
56
49
|
end
|
57
50
|
|
58
51
|
##
|
59
|
-
# Creates a new
|
52
|
+
# Creates a new instance.
|
60
53
|
#
|
61
|
-
# * +name+ -
|
62
|
-
#
|
63
|
-
# * +content+ - Template content string.
|
54
|
+
# * +name+ - Template name. Conventionally the path of the template file.
|
55
|
+
# * +content+ - Template content.
|
64
56
|
#
|
65
57
|
def initialize(name, content)
|
66
58
|
@name = name
|
@@ -70,113 +62,196 @@ module HTX
|
|
70
62
|
##
|
71
63
|
# Compiles the HTX template.
|
72
64
|
#
|
73
|
-
# * +
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
def compile(
|
79
|
-
|
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
|
-
|
65
|
+
# * +assign_to+ - JavaScript object to assign the template function to (default: <tt>window</tt>).
|
66
|
+
# * +indent+ - DEPRECATED. Indentation amount (number) or string (must be only spaces or tabs but not
|
67
|
+
# both) to use for indentation of compiled output (default: indentation of first indented line of
|
68
|
+
# uncompiled template).
|
69
|
+
#
|
70
|
+
def compile(assign_to: nil, indent: (indent_omitted = true; nil))
|
71
|
+
@assign_to = assign_to || 'window'
|
94
72
|
@indent =
|
95
73
|
if indent.kind_of?(Numeric)
|
96
74
|
' ' * indent
|
97
|
-
elsif indent.
|
98
|
-
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")
|
99
77
|
else
|
100
78
|
indent || @content[INDENT_GUESS] || INDENT_DEFAULT
|
101
79
|
end
|
102
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)
|
103
91
|
process(doc)
|
104
|
-
@compiled.rstrip!
|
105
92
|
|
106
|
-
|
107
|
-
#{assign_to}['#{@name}'] = function(htx) {
|
108
|
-
#{@indent}#{@compiled}
|
109
|
-
}
|
110
|
-
EOS
|
93
|
+
@compiled
|
111
94
|
end
|
112
95
|
|
113
96
|
private
|
114
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
|
+
|
115
138
|
##
|
116
139
|
# Processes a DOM node's descendents.
|
117
140
|
#
|
118
|
-
# * +
|
141
|
+
# * +node+ - Nokogiri node to process.
|
119
142
|
#
|
120
|
-
def process(
|
121
|
-
|
122
|
-
|
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
|
123
154
|
|
124
|
-
|
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"
|
125
163
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
"<#{CONTENT_TAG}> for identical functionality.")
|
130
|
-
end
|
164
|
+
node.children.each do |child|
|
165
|
+
process(child)
|
166
|
+
end
|
131
167
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
end
|
168
|
+
append("\n}\n",)
|
169
|
+
flush
|
170
|
+
end
|
136
171
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
172
|
+
##
|
173
|
+
# Processes an element node.
|
174
|
+
#
|
175
|
+
# * +node+ - Nokogiri node to process.
|
176
|
+
# * +xmlns+ - True if node is the descendent of a node with an xmlns attribute.
|
177
|
+
#
|
178
|
+
def process_element_node(node, xmlns: false)
|
179
|
+
children = node.children
|
180
|
+
childless = children.empty? || (children.size == 1 && self.class.formatting_node?(children.first))
|
181
|
+
dynamic_key = self.class.attribute_value(node.attr(DYNAMIC_KEY_ATTR))
|
182
|
+
attributes = self.class.process_attributes(node)
|
183
|
+
xmlns ||= !!self.class.namespace(node)
|
184
|
+
|
185
|
+
if self.class.htx_content_node?(node)
|
186
|
+
if node.name != CONTENT_TAG
|
187
|
+
warn("#{@name}:#{node.line}: The <#{node.name}> tag has been deprecated. Use <#{CONTENT_TAG}> "\
|
188
|
+
"for identical functionality.")
|
189
|
+
end
|
141
190
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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)
|
156
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)
|
157
234
|
else
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
append("htx.close(#{count})")
|
179
|
-
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
|
180
255
|
end
|
181
256
|
end
|
182
257
|
end
|
@@ -188,121 +263,145 @@ module HTX
|
|
188
263
|
# * +text+ - String to append to the compiled template string.
|
189
264
|
#
|
190
265
|
def append(text)
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
@compiled << @indent
|
266
|
+
return @compiled if text.nil? || text.empty?
|
267
|
+
|
268
|
+
if @close_count > 0
|
269
|
+
close_count = @close_count
|
270
|
+
@close_count = 0
|
271
|
+
|
272
|
+
append("htx.close(#{close_count unless close_count == 1})")
|
199
273
|
end
|
200
274
|
|
201
|
-
@
|
275
|
+
if @whitespace_buff
|
276
|
+
@statement_buff << @whitespace_buff
|
277
|
+
@whitespace_buff = nil
|
278
|
+
confirmed_newline = true
|
279
|
+
end
|
280
|
+
|
281
|
+
if (confirmed_newline || @statement_buff.match?(NEWLINE_END)) && !text.match?(NEWLINE_BEGIN)
|
282
|
+
@statement_buff << @indent
|
283
|
+
elsif !@statement_buff.match?(NO_SEMICOLON_END) && !text.match?(NO_SEMICOLON_BEGIN)
|
284
|
+
@statement_buff << ";#{' ' unless text.match?(WHITESPACE_BEGIN)}"
|
285
|
+
end
|
286
|
+
|
287
|
+
flush if text.match?(NON_WHITESPACE)
|
288
|
+
@statement_buff << text
|
289
|
+
|
290
|
+
@compiled
|
202
291
|
end
|
203
292
|
|
204
293
|
##
|
205
|
-
#
|
294
|
+
# Appends an +htx.node+ call to the compiled template function string.
|
206
295
|
#
|
207
|
-
# * +
|
296
|
+
# * +args+ - Arguments to use for the +htx.node+ call (any +nil+ ones are removed).
|
208
297
|
#
|
209
|
-
def
|
210
|
-
return
|
298
|
+
def append_htx_node(*args)
|
299
|
+
return if args.first.nil?
|
211
300
|
|
212
|
-
|
213
|
-
|
301
|
+
args.compact!
|
302
|
+
args << 0 unless args.last.kind_of?(Integer)
|
303
|
+
args[-1] |= (@static_key += 1) << FLAG_BITS
|
304
|
+
|
305
|
+
append("htx.node(#{args.join(', ')})")
|
214
306
|
end
|
215
307
|
|
216
308
|
##
|
217
|
-
#
|
218
|
-
#
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
309
|
+
# Flushes statement buffer.
|
310
|
+
#
|
311
|
+
def flush
|
312
|
+
@compiled << @statement_buff
|
313
|
+
@statement_buff.clear
|
243
314
|
|
244
|
-
|
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')}" })
|
315
|
+
@compiled
|
256
316
|
end
|
257
317
|
|
258
318
|
##
|
259
|
-
#
|
260
|
-
# boolean indicating whether or not an xmlns attribute is present.
|
319
|
+
# Indents each line of a string (except the first).
|
261
320
|
#
|
262
|
-
#
|
263
|
-
# will be automatically added since it is required for those elements to render properly.
|
321
|
+
# * +text+ - String of lines to indent.
|
264
322
|
#
|
265
|
-
|
323
|
+
def indent(text)
|
324
|
+
text.gsub!(INDENT_REGEX, "\\0#{@indent}")
|
325
|
+
text
|
326
|
+
end
|
327
|
+
|
328
|
+
##
|
329
|
+
# Returns true if the node is whitespace containing at least one newline.
|
266
330
|
#
|
267
|
-
|
268
|
-
|
269
|
-
|
331
|
+
# * +node+ - Nokogiri node to check.
|
332
|
+
#
|
333
|
+
def self.formatting_node?(node)
|
334
|
+
node.blank? && node.content.include?("\n")
|
335
|
+
end
|
270
336
|
|
271
|
-
|
272
|
-
|
337
|
+
##
|
338
|
+
# Returns true if the node is an <htx-content> node (or one of its now-deprecated names).
|
339
|
+
#
|
340
|
+
# * +node+ - Nokogiri node to check.
|
341
|
+
#
|
342
|
+
def self.htx_content_node?(node)
|
343
|
+
node && (node.name == CONTENT_TAG || node.name == 'htx-text' || node.name == ':')
|
344
|
+
end
|
273
345
|
|
274
|
-
|
275
|
-
|
346
|
+
##
|
347
|
+
# Processes a node's attributes returning a flat array of attribute names and values.
|
348
|
+
#
|
349
|
+
# * +node+ - Nokogiri node to process the attributes of.
|
350
|
+
#
|
351
|
+
def self.process_attributes(node)
|
352
|
+
attributes = []
|
353
|
+
|
354
|
+
if !node.attribute('xmlns') && (xmlns = namespace(node))
|
355
|
+
attributes.push(
|
356
|
+
attribute_name('xmlns'),
|
357
|
+
attribute_value(xmlns)
|
358
|
+
)
|
276
359
|
end
|
277
360
|
|
278
|
-
node.
|
279
|
-
next if
|
361
|
+
node.attribute_nodes.each.with_object(attributes) do |attribute, attributes|
|
362
|
+
next if attribute.node_name == DYNAMIC_KEY_ATTR
|
280
363
|
|
281
|
-
|
282
|
-
|
364
|
+
attributes.push(
|
365
|
+
attribute_name(attribute.node_name),
|
366
|
+
attribute_value(attribute.value)
|
367
|
+
)
|
283
368
|
end
|
369
|
+
end
|
284
370
|
|
285
|
-
|
371
|
+
##
|
372
|
+
#
|
373
|
+
#
|
374
|
+
def self.namespace(node)
|
375
|
+
node.namespace&.href || DEFAULT_XMLNS[node.name]
|
286
376
|
end
|
287
377
|
|
288
378
|
##
|
289
379
|
# Returns the given text if the HTML5 parser is in use, or looks up the value in the tag map to get the
|
290
380
|
# correctly-cased version, falling back to the supplied text if no mapping exists.
|
291
381
|
#
|
292
|
-
# * +text+ - Tag name as returned by Nokogiri
|
382
|
+
# * +text+ - Tag name as returned by Nokogiri.
|
293
383
|
#
|
294
|
-
def tag_name(text)
|
295
|
-
|
384
|
+
def self.tag_name(text)
|
385
|
+
html5_parser? ? text : (TAG_MAP[text] || text)
|
296
386
|
end
|
297
387
|
|
298
388
|
##
|
299
389
|
# Returns the given text if the HTML5 parser is in use, or looks up the value in the attribute map to
|
300
390
|
# get the correctly-cased version, falling back to the supplied text if no mapping exists.
|
301
391
|
#
|
302
|
-
# * +text+ - Attribute name as returned by Nokogiri
|
392
|
+
# * +text+ - Attribute name as returned by Nokogiri.
|
393
|
+
#
|
394
|
+
def self.attribute_name(text)
|
395
|
+
"'#{html5_parser? ? text : (ATTR_MAP[text] || text)}'"
|
396
|
+
end
|
397
|
+
|
398
|
+
##
|
399
|
+
# Returns the processed value of an attribute.
|
400
|
+
#
|
401
|
+
# * +text+ - Attribute value as returned by Nokogiri.
|
303
402
|
#
|
304
|
-
def
|
305
|
-
|
403
|
+
def self.attribute_value(text)
|
404
|
+
text ? TextParser.new(text, statement_allowed: false).parse : nil
|
306
405
|
end
|
307
406
|
|
308
407
|
# The Nokogiri HTML parser downcases all tag and attribute names, but SVG tags and attributes are case
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require('strscan')
|
4
|
+
|
5
|
+
module HTX
|
6
|
+
class TextParser
|
7
|
+
LEADING = /\A((?:[^\S\n]*\n)+)?((?:[^\S\n])+)?(?=\S)/.freeze
|
8
|
+
TRAILING = /([\S\s]*?)(\s*?)(\n[^\S\n]*)?\z/.freeze
|
9
|
+
|
10
|
+
NOT_ESCAPED = /(?<=^|[^\\])(?:\\\\)*/.freeze
|
11
|
+
OF_INTEREST = /#{NOT_ESCAPED}(?<chars>[`'"]|\${)|(?<chars>{|}|\/\/|\/\*|\*\/|\n[^\S\n]*(?=\S))/.freeze
|
12
|
+
TERMINATOR = {
|
13
|
+
'\'' => '\'',
|
14
|
+
'"' => '"',
|
15
|
+
'//' => "\n",
|
16
|
+
'/*' => '*/',
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
TEXT = /\S|\A[^\S\n]+\z/.freeze
|
20
|
+
IDENTIFIER = /[_$a-zA-Z][_$a-zA-Z0-9]*/.freeze
|
21
|
+
ASSIGNMENT = /\s*(\+|&|\||\^|\/|\*\*|<<|&&|\?\?|\|\||\*|%|-|>>>)?=/.freeze
|
22
|
+
STATEMENT = /[{}]|(^|\s)#{IDENTIFIER}(\.#{IDENTIFIER})*(#{ASSIGNMENT}|\+\+|--|\[|\()/.freeze
|
23
|
+
|
24
|
+
attr_reader(:type, :content, :leading, :trailing, :whitespace_buff)
|
25
|
+
|
26
|
+
##
|
27
|
+
# Creates a new instance.
|
28
|
+
#
|
29
|
+
# * +text+ - Text to parse.
|
30
|
+
# * +statement_allowed+ - True if statements are allowed; false if text is always a template or raw (
|
31
|
+
# single interpolation).
|
32
|
+
#
|
33
|
+
def initialize(text, statement_allowed:)
|
34
|
+
@text = text
|
35
|
+
@statement_allowed = statement_allowed
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Returns true if parsed text is a statement.
|
40
|
+
#
|
41
|
+
def statement?
|
42
|
+
@type == :statement
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Returns true if parsed text is a single interpolation.
|
47
|
+
#
|
48
|
+
def raw?
|
49
|
+
@type == :raw
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Returns true if parsed text is a template.
|
54
|
+
#
|
55
|
+
def template?
|
56
|
+
@type == :template
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Parses text.
|
61
|
+
#
|
62
|
+
def parse
|
63
|
+
@type = nil
|
64
|
+
@content = +''
|
65
|
+
|
66
|
+
@first_indent = nil
|
67
|
+
@min_indent = nil
|
68
|
+
@last_indent = nil
|
69
|
+
|
70
|
+
@buffer = +''
|
71
|
+
@stack = []
|
72
|
+
curlies = []
|
73
|
+
ignore_end = nil
|
74
|
+
|
75
|
+
@has_text = false
|
76
|
+
@is_statement = false
|
77
|
+
@template_count = 0
|
78
|
+
@interpolation_count = 0
|
79
|
+
|
80
|
+
scanner = StringScanner.new(@text)
|
81
|
+
|
82
|
+
scanner.scan(LEADING)
|
83
|
+
@leading_newlines = scanner[1]
|
84
|
+
@leading_indent = @first_indent = scanner[2]
|
85
|
+
@leading = scanner[0] if @leading_newlines || @leading_indent
|
86
|
+
|
87
|
+
until scanner.eos?
|
88
|
+
if (scanned = scanner.scan_until(OF_INTEREST))
|
89
|
+
ignore = @stack.last == :ignore
|
90
|
+
template = @stack.last == :template
|
91
|
+
interpolation = @stack.last == :interpolation
|
92
|
+
can_ignore = (@stack.empty? && @statement_allowed) || interpolation
|
93
|
+
can_template = @stack.empty? || interpolation
|
94
|
+
can_interpolate = @stack.empty? || template
|
95
|
+
|
96
|
+
chars = scanner[:chars]
|
97
|
+
@buffer << scanned.chomp!(chars)
|
98
|
+
|
99
|
+
if chars[0] == "\n"
|
100
|
+
indent = chars.delete_prefix("\n")
|
101
|
+
|
102
|
+
if !@last_indent
|
103
|
+
@last_indent = indent
|
104
|
+
else
|
105
|
+
@min_indent = @last_indent if !@min_indent || @last_indent.size < @min_indent.size
|
106
|
+
@last_indent = indent
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
if can_ignore && (ignore_end = TERMINATOR[chars])
|
111
|
+
push_state(:ignore)
|
112
|
+
@buffer << chars
|
113
|
+
elsif ignore && chars == ignore_end
|
114
|
+
@buffer << chars
|
115
|
+
pop_state
|
116
|
+
ignore_end = nil
|
117
|
+
elsif can_template && chars == '`'
|
118
|
+
push_state(:template)
|
119
|
+
@buffer << chars
|
120
|
+
elsif template && chars == '`'
|
121
|
+
@buffer << chars
|
122
|
+
pop_state
|
123
|
+
elsif can_interpolate && chars == '${'
|
124
|
+
push_state(:interpolation)
|
125
|
+
curlies << 1
|
126
|
+
@buffer << chars
|
127
|
+
elsif interpolation && (curlies[-1] += (chars == '{' && 1) || (chars == '}' && -1) || 0) == 0
|
128
|
+
@buffer << chars
|
129
|
+
curlies.pop
|
130
|
+
pop_state
|
131
|
+
else
|
132
|
+
@buffer << chars
|
133
|
+
end
|
134
|
+
else
|
135
|
+
scanner.scan(TRAILING)
|
136
|
+
|
137
|
+
@buffer << scanner[1]
|
138
|
+
@trailing = scanner[2] == '' ? nil : scanner[2]
|
139
|
+
@whitespace_buff = scanner[3]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
flush(@stack.last)
|
144
|
+
finalize
|
145
|
+
|
146
|
+
@content
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
##
|
152
|
+
# Determines type (statement, raw, or template) and adjust formatting accordingly. Called at the end of
|
153
|
+
# parsing.
|
154
|
+
#
|
155
|
+
def finalize
|
156
|
+
if @is_statement
|
157
|
+
@type = :statement
|
158
|
+
@content.insert(0, @leading) && @leading = nil if @leading
|
159
|
+
@content.insert(-1, @trailing) && @trailing = nil if @trailing
|
160
|
+
elsif !@has_text && @template_count == 0 && @interpolation_count == 1
|
161
|
+
@type = :raw
|
162
|
+
@content.delete_prefix!('${')
|
163
|
+
@content.delete_suffix!('}')
|
164
|
+
|
165
|
+
if @content.empty?
|
166
|
+
@type = :template
|
167
|
+
@content = '``'
|
168
|
+
end
|
169
|
+
else
|
170
|
+
@type = :template
|
171
|
+
|
172
|
+
if !@has_text && @template_count == 1 && @interpolation_count == 0
|
173
|
+
@quoted = true
|
174
|
+
@outdent = @min_indent
|
175
|
+
@content.delete_prefix!('`')
|
176
|
+
@content.delete_suffix!('`')
|
177
|
+
else
|
178
|
+
@outdent = [@first_indent, @min_indent, @last_indent].compact.min
|
179
|
+
end
|
180
|
+
|
181
|
+
@content.gsub!(/(?<=\n)([^\S\n]+$|#{@outdent})/, '') if @outdent && !@outdent.empty?
|
182
|
+
|
183
|
+
unless @quoted
|
184
|
+
@content.insert(0, @leading) && @leading = nil if @leading && !@leading_newlines
|
185
|
+
@content.insert(-1, @trailing) && @trailing = nil if @trailing && !@trailing.include?("\n")
|
186
|
+
end
|
187
|
+
|
188
|
+
@content.insert(0, '`').insert(-1, '`')
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
##
|
193
|
+
# Pushes a state onto the stack during parsing.
|
194
|
+
#
|
195
|
+
# * +state+ - State to push onto the stack.
|
196
|
+
#
|
197
|
+
def push_state(state)
|
198
|
+
flush if @stack.empty?
|
199
|
+
@stack << state
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Pops a state from the stack during parsing.
|
204
|
+
#
|
205
|
+
def pop_state
|
206
|
+
state = @stack.pop
|
207
|
+
|
208
|
+
if @stack.empty?
|
209
|
+
flush(state)
|
210
|
+
|
211
|
+
@interpolation_count += 1 if state == :interpolation
|
212
|
+
@template_count += 1 if state == :template
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
##
|
217
|
+
# Flushes buffer during parsing and performs statement detection if appropriate.
|
218
|
+
#
|
219
|
+
def flush(state = nil)
|
220
|
+
return if @buffer.empty?
|
221
|
+
|
222
|
+
if !state || state == :text
|
223
|
+
@has_text ||= @buffer.match?(TEXT)
|
224
|
+
@is_statement ||= @buffer.match?(STATEMENT) if @statement_allowed
|
225
|
+
end
|
226
|
+
|
227
|
+
@content << @buffer
|
228
|
+
@buffer.clear
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
data/lib/htx/version.rb
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.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nate Pickens
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 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:
|