htx 0.0.1
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 +7 -0
- data/LICENSE +16 -0
- data/README.md +46 -0
- data/lib/htx.rb +190 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 94e1883a59d09cce6170061c9e0602a3430da8ca800163ba2fe8cc9d10b316b4
|
4
|
+
data.tar.gz: a26e6aa3a874e40569cd62204aa698a6bdec5b02a78dc35ea993d0a11ff8f0f2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 004dca83de5a65ba167eb345d21a1a8c1d771efbe946fc746cfb470d4ca783bad57830ced8ba6f1a7ce47e9e04abc3ee06a6ebf02fc9d5a0eee2892983dbf136
|
7
|
+
data.tar.gz: ab814533c1f9ead9fa8daa2ef46959a2a8893c21e3705ee9863fbcaeed90ec0e70f835d36420fe1ade8c1a0bf9c33478987e76d07cbb20c154a4da26d3ef7492
|
data/LICENSE
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Copyright 2019 Nate Pickens
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
4
|
+
documentation files (the "Software"), to deal in the Software without restriction, including without
|
5
|
+
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
6
|
+
the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
|
7
|
+
conditions:
|
8
|
+
|
9
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
10
|
+
portions of the Software.
|
11
|
+
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
13
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
14
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
15
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
16
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# HTX Ruby Compiler
|
2
|
+
|
3
|
+
HTX templates are compiled to JavaScript before being used. This library provides a Ruby implementation of
|
4
|
+
the compiler. For more information on HTX, see
|
5
|
+
[https://github.com/npickens/htx](https://github.com/npickens/htx).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem('htx')
|
13
|
+
```
|
14
|
+
|
15
|
+
Or install manually on the command line:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
gem install htx
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
To compile an HTX template, pass a name (conventionally the path of the template file) and template as
|
24
|
+
strings to the `HTX.compile` method:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
path = '/my/hot/template.htx'
|
28
|
+
template = File.read(File.join('some/asset/dir', path))
|
29
|
+
|
30
|
+
HTX.compile(path, template)
|
31
|
+
|
32
|
+
# Result:
|
33
|
+
#
|
34
|
+
# window['/my/hot/template.htx'] = function(htx) {
|
35
|
+
# ...
|
36
|
+
# }
|
37
|
+
```
|
38
|
+
|
39
|
+
## Contributing
|
40
|
+
|
41
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/npickens/htx.
|
42
|
+
|
43
|
+
## License
|
44
|
+
|
45
|
+
The gem is available as open source under the terms of the
|
46
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/lib/htx.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require('nokogiri')
|
4
|
+
|
5
|
+
##
|
6
|
+
# A Ruby compiler for HTX templates.
|
7
|
+
#
|
8
|
+
class HTX
|
9
|
+
VERSION = '0.0.1'
|
10
|
+
|
11
|
+
CHILDLESS = 0b01
|
12
|
+
TEXT_NODE = 0b10
|
13
|
+
FLAG_BITS = 2
|
14
|
+
|
15
|
+
DYNAMIC_KEY_ATTR = 'htx-key'
|
16
|
+
|
17
|
+
LEADING_WHITESPACE = /\A *\n */.freeze
|
18
|
+
TRAILING_WHITESPACE = /\n *\z/.freeze
|
19
|
+
NON_BLANK_NON_FIRST_LINE = /(?<=\n) *(?=\S)/.freeze
|
20
|
+
NEWLINE_NON_BLANK = /\n(?=[^\n]+)/.freeze
|
21
|
+
|
22
|
+
END_STATEMENT_END = /(;|\n|\{|\}) *\z/.freeze
|
23
|
+
BEGIN_STATEMENT_END = /\A *(;|\{|\n|\})/.freeze
|
24
|
+
END_WHITESPACE = /\s\z/.freeze
|
25
|
+
BEGIN_WHITESPACE = /\A\s/.freeze
|
26
|
+
|
27
|
+
RAW_VALUE = /\A\s*\${([\S\s]*)}\s*\z/.freeze
|
28
|
+
TEMPLATE_STRING = /\A\s*`([\S\s]*)`\s*\z/.freeze
|
29
|
+
INTERPOLATION = /\$\\?{([^}]*})?/.freeze
|
30
|
+
HTML_ENTITY = /&([a-zA-Z]+|#\d+|x[0-9a-fA-F]+);/.freeze
|
31
|
+
NON_CONTROL_STATEMENT = /#{INTERPOLATION}|(#{HTML_ENTITY})/.freeze
|
32
|
+
CONTROL_STATEMENT = /[{}();]/.freeze
|
33
|
+
CLOSE_STATEMENT = /;?\s*htx\.close\((\d*)\);?(\s*)\z/.freeze
|
34
|
+
|
35
|
+
##
|
36
|
+
# Compiles an HTX template and assigns it the given name (conventionally the path of the template file is
|
37
|
+
# used, but it can be anything).
|
38
|
+
#
|
39
|
+
def self.compile(name, template)
|
40
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(template)
|
41
|
+
js = ''.dup
|
42
|
+
|
43
|
+
process(doc, js, static_key: 0)
|
44
|
+
js.rstrip!
|
45
|
+
|
46
|
+
<<~EOS
|
47
|
+
window['#{name}'] = function(htx) {
|
48
|
+
#{js}
|
49
|
+
}
|
50
|
+
EOS
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Processes a DOM node.
|
55
|
+
#
|
56
|
+
def self.process(base, js, options = {})
|
57
|
+
base.children.each do |node|
|
58
|
+
next if node.comment?
|
59
|
+
|
60
|
+
dynamic_key = process_value(node.attr(DYNAMIC_KEY_ATTR), :attr)
|
61
|
+
|
62
|
+
if node.text? || node.name == ':'
|
63
|
+
text = (node.text? ? node : node.children).text
|
64
|
+
|
65
|
+
if (value = process_value(text))
|
66
|
+
append(js,
|
67
|
+
"#{indent(text[LEADING_WHITESPACE])}"\
|
68
|
+
"htx.node(#{[
|
69
|
+
value,
|
70
|
+
dynamic_key,
|
71
|
+
((options[:static_key] += 1) << FLAG_BITS) | TEXT_NODE,
|
72
|
+
].compact.join(', ')})"\
|
73
|
+
"#{indent(text[TRAILING_WHITESPACE])}"
|
74
|
+
)
|
75
|
+
else
|
76
|
+
append(js, indent(text))
|
77
|
+
end
|
78
|
+
else
|
79
|
+
attrs = node.attributes.inject([]) do |attrs, (_, attr)|
|
80
|
+
next attrs if attr.name == DYNAMIC_KEY_ATTR
|
81
|
+
|
82
|
+
attrs << "'#{ATTR_MAP[attr.name] || attr.name}'"
|
83
|
+
attrs << process_value(attr.value, :attr)
|
84
|
+
end
|
85
|
+
|
86
|
+
append(js, "htx.node(#{[
|
87
|
+
"'#{TAG_MAP[node.name] || node.name}'",
|
88
|
+
attrs,
|
89
|
+
dynamic_key,
|
90
|
+
((options[:static_key] += 1) << FLAG_BITS) | (node.children.empty? ? CHILDLESS : 0),
|
91
|
+
].compact.flatten.join(', ')})")
|
92
|
+
|
93
|
+
unless node.children.empty?
|
94
|
+
process(node, js, options)
|
95
|
+
|
96
|
+
count = ''
|
97
|
+
js.sub!(CLOSE_STATEMENT) do
|
98
|
+
count = $1 == '' ? 2 : $1.to_i + 1
|
99
|
+
$2
|
100
|
+
end
|
101
|
+
|
102
|
+
append(js, "htx.close(#{count})")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Appends a string to the compiled template function string with appropriate punctuation and/or
|
110
|
+
# whitespace inserted.
|
111
|
+
#
|
112
|
+
def self.append(js, text)
|
113
|
+
if js == ''
|
114
|
+
# Do nothing.
|
115
|
+
elsif js !~ END_STATEMENT_END && text !~ BEGIN_STATEMENT_END
|
116
|
+
js << '; '
|
117
|
+
elsif js !~ END_WHITESPACE && text !~ BEGIN_WHITESPACE
|
118
|
+
js << ' '
|
119
|
+
elsif js[-1] == "\n"
|
120
|
+
js << ' '
|
121
|
+
end
|
122
|
+
|
123
|
+
js << text
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Indents each line of a string (except the first).
|
128
|
+
#
|
129
|
+
def self.indent(text)
|
130
|
+
return '' unless text
|
131
|
+
|
132
|
+
text.gsub!(NEWLINE_NON_BLANK, "\n ")
|
133
|
+
text
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Processes, formats, and encodes an attribute or text node value. Returns nil if the value is determined
|
138
|
+
# to be a control statement.
|
139
|
+
#
|
140
|
+
def self.process_value(text, is_attr = false)
|
141
|
+
return nil if text.nil? || (!is_attr && text.strip == '')
|
142
|
+
|
143
|
+
if (value = text[RAW_VALUE, 1])
|
144
|
+
# Entire text is enclosed in ${...}.
|
145
|
+
value.strip!
|
146
|
+
quote = false
|
147
|
+
elsif (value = text[TEMPLATE_STRING, 1])
|
148
|
+
# Entire text is enclosed in backticks (template string).
|
149
|
+
quote = true
|
150
|
+
elsif is_attr || text.gsub(NON_CONTROL_STATEMENT, '') !~ CONTROL_STATEMENT
|
151
|
+
# Text is an attribute value or doesn't match control statement pattern.
|
152
|
+
value = text.dup
|
153
|
+
quote = true
|
154
|
+
else
|
155
|
+
return nil
|
156
|
+
end
|
157
|
+
|
158
|
+
# Strip one leading and trailing newline (and attached spaces) and perform outdent. Outdent amount
|
159
|
+
# calculation ignores everything before the first newline in its search for the least-indented line.
|
160
|
+
outdent = value.scan(NON_BLANK_NON_FIRST_LINE).min
|
161
|
+
value.gsub!(/#{LEADING_WHITESPACE}|#{TRAILING_WHITESPACE}|^#{outdent}/, '')
|
162
|
+
value.insert(0, '`').insert(-1, '`') if quote
|
163
|
+
|
164
|
+
# Ensure any Unicode characters get converted to Unicode escape sequences. Also note that since Nokogiri
|
165
|
+
# converts HTML entities to Unicode characters, this causes them to be properly passed to
|
166
|
+
# `document.createTextNode` calls as Unicode escape sequences rather than (incorrectly) as HTML
|
167
|
+
# entities.
|
168
|
+
value.encode('ascii', fallback: ->(c) { "\\u#{c.ord.to_s(16).rjust(4, '0')}" })
|
169
|
+
end
|
170
|
+
|
171
|
+
# The Nokogiri HTML parser downcases all tag and attribute names, but SVG tags and attributes are case
|
172
|
+
# sensitive and often mix cased. These maps are used to restore the correct case of such tags and
|
173
|
+
# attributes.
|
174
|
+
TAG_MAP = %w[
|
175
|
+
animateMotion animateTransform clipPath feBlend feColorMatrix feComponentTransfer feComposite
|
176
|
+
feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB
|
177
|
+
feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight
|
178
|
+
feSpecularLighting feSpotLight feTile feTurbulence foreignObject linearGradient radialGradient textPath
|
179
|
+
].map { |tag| [tag.downcase, tag] }.to_h.freeze
|
180
|
+
|
181
|
+
ATTR_MAP = %w[
|
182
|
+
attributeName baseFrequency calcMode clipPathUnits diffuseConstant edgeMode filterUnits
|
183
|
+
gradientTransform gradientUnits kernelMatrix kernelUnitLength keyPoints keySplines keyTimes lengthAdjust
|
184
|
+
limitingConeAngle markerHeight markerUnits markerWidth maskContentUnits maskUnits numOctaves pathLength
|
185
|
+
patternContentUnits patternTransform patternUnits pointsAtX pointsAtY pointsAtZ preserveAlpha
|
186
|
+
preserveAspectRatio primitiveUnits refX refY repeatCount repeatDur requiredExtensions specularConstant
|
187
|
+
specularExponent spreadMethod startOffset stdDeviation stitchTiles surfaceScale systemLanguage
|
188
|
+
tableValues targetX targetY textLength viewBox xChannelSelector yChannelSelector zoomAndPan
|
189
|
+
].map { |attr| [attr.downcase, attr] }.to_h.freeze
|
190
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: htx
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nate Pickens
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.11'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.11'
|
55
|
+
description: HTX is a full-featured HTML template system that is simple, lightweight,
|
56
|
+
and highly performant. This library is a Ruby implementation of the HTX template
|
57
|
+
compiler--it converts HTX templates to their compiled JavaScript form.
|
58
|
+
email:
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- LICENSE
|
64
|
+
- README.md
|
65
|
+
- lib/htx.rb
|
66
|
+
homepage: https://github.com/npickens/htx
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata:
|
70
|
+
allowed_push_host: https://rubygems.org
|
71
|
+
homepage_uri: https://github.com/npickens/htx
|
72
|
+
source_code_uri: https://github.com/npickens/htx
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubygems_version: 3.0.3
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: A Ruby compiler for HTX templates.
|
92
|
+
test_files: []
|