trenni 1.4.5 → 1.5.0
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/Gemfile +3 -3
- data/README.md +15 -3
- data/Rakefile +5 -1
- data/benchmark/io_vs_string.rb +80 -0
- data/lib/trenni.rb +2 -2
- data/lib/trenni/builder.rb +27 -38
- data/lib/trenni/parser.rb +28 -39
- data/lib/trenni/template.rb +28 -21
- data/lib/trenni/version.rb +1 -1
- data/spec/trenni/builder_spec.rb +46 -13
- data/spec/trenni/parser_spec.rb +45 -19
- data/spec/trenni/template_spec.rb +20 -2
- data/spec/trenni/template_spec/buffer.trenni +8 -0
- data/spec/trenni/template_spec/capture.trenni +4 -0
- data/spec/trenni/{escaped.trenni → template_spec/escaped.trenni} +0 -0
- data/spec/trenni/{large.trenni → template_spec/large.trenni} +0 -0
- data/spec/trenni/template_spec/nested.trenni +1 -0
- metadata +14 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ff8d7a73ba2ef5c153eff38d0869b2d6cb53a91
|
4
|
+
data.tar.gz: 5a8a5f044ea099e6d69fc17d1481f47f2a678f47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 505a3e2d978b00b89764a085402d94d5d15c6c8230ed91f4f6f5970d6c5752f86af98d176e9be7bbd40d9b294ad35d6e13b08194d4f846706444bc5a916f13ac
|
7
|
+
data.tar.gz: a08e384c2cf0487b9567e972b701f8b5d70b0b8d737e579bc7c76fc9586228657e1cee554d099ffc45b23d6150f7ad0a8a5bf3c976cfc4e39166c3e7cd023000
|
data/Gemfile
CHANGED
@@ -4,10 +4,10 @@ source 'https://rubygems.org'
|
|
4
4
|
gemspec
|
5
5
|
|
6
6
|
group :development do
|
7
|
-
gem
|
7
|
+
gem 'pry'
|
8
|
+
gem 'ruby-prof', platforms: [:mri]
|
8
9
|
end
|
9
10
|
|
10
11
|
group :test do
|
11
|
-
gem '
|
12
|
-
gem 'coveralls', require: false
|
12
|
+
gem 'coveralls', platforms: [:mri]
|
13
13
|
end
|
data/README.md
CHANGED
@@ -7,10 +7,20 @@ generally get the best possible performance.
|
|
7
7
|
In addition, Trenni includes an SGML/XML builder to assist with the generation
|
8
8
|
of pleasantly formatted markup.
|
9
9
|
|
10
|
-
[](http://travis-ci.org/ioquatix/trenni)
|
11
|
+
[](https://codeclimate.com/github/ioquatix/trenni)
|
12
12
|
[](https://coveralls.io/r/ioquatix/trenni)
|
13
13
|
|
14
|
+
## Motivation
|
15
|
+
|
16
|
+
Trenni was designed for [Utopia](https://github.com/ioquatix/utopia). When I originally looked at template engines, I was surprised by the level of complexity and the effort involved in processing a template to produce useful output. In particular, many template engines generate an AST and walk over it to generate output (e.g. ERB, at least at the time I last checked). This is exceedingly slow in Ruby.
|
17
|
+
|
18
|
+
At the time (around 2008?) I was playing around with [ramaze](https://github.com/Ramaze/ramaze) and found a template engine I really liked the design of, called [ezamar](https://github.com/manveru/ezamar). The template compilation process actually generates Ruby code which can then be compiled and executed efficiently. Another engine, by the same author, [nagoro](https://github.com/manveru/nagoro), also provided some inspiration.
|
19
|
+
|
20
|
+
More recently I was doing some investigation regarding using `eval` for executing the code. The problem is that it's [not possible to replace the binding](http://stackoverflow.com/questions/27391909/replace-evalcode-string-binding-with-lambda/27392437) of a `Proc` once it's created, so template engines that evaluate code in a given binding cannot use a compiled proc, they must parse the code every time. By using a `Proc` we can generate a Ruby function which *can* be compiled to a faster representation by the VM.
|
21
|
+
|
22
|
+
In addition, I wanted a simple parser and builder for HTML style markup. These are used heavily by Utopia for implementing it's tag based evaluation. The (event based) `Trenni::Parser` is designed so that some day it could be easily written in C. `Trenni::Builder` is a simple and efficient way to generate markup, it's not particularly notable, except that it doesn't use `method_missing` to [implement normal behaviour](https://github.com/sparklemotion/nokogiri/blob/b6679e928924529b56dcc0f3164224c040d14555/lib/nokogiri/xml/builder.rb#L355) which is [sort of slow](http://franck.verrot.fr/blog/2015/07/12/benchmarking-ruby-method-missing-and-define-method/).
|
23
|
+
|
14
24
|
## Installation
|
15
25
|
|
16
26
|
Add this line to your application's Gemfile:
|
@@ -62,6 +72,8 @@ Trenni can help construct XML/HTML using a simple DSL:
|
|
62
72
|
|
63
73
|
There is a [language-trenni](https://atom.io/packages/language-trenni) package for the [Atom text editor](https://atom.io). It provides syntax highlighting and integration when Trenni is used with the [utopia web framework](https://github.com/ioquatix/utopia).
|
64
74
|
|
75
|
+
[Trenni Formatters](https://github.com/ioquatix/trenni-formatters) is a separate gem that uses `Trenni::Builder` to generate HTML forms easily.
|
76
|
+
|
65
77
|
## Contributing
|
66
78
|
|
67
79
|
1. Fork it
|
@@ -74,7 +86,7 @@ There is a [language-trenni](https://atom.io/packages/language-trenni) package f
|
|
74
86
|
|
75
87
|
Released under the MIT license.
|
76
88
|
|
77
|
-
Copyright, 2012, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
89
|
+
Copyright, 2012, 2016, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
78
90
|
|
79
91
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
80
92
|
of this software and associated documentation files (the "Software"), to deal
|
data/Rakefile
CHANGED
@@ -2,7 +2,11 @@ require "bundler/gem_tasks"
|
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
|
4
4
|
RSpec::Core::RakeTask.new(:spec) do |task|
|
5
|
-
|
5
|
+
begin
|
6
|
+
require('simplecov/version')
|
7
|
+
task.rspec_opts = %w{--require simplecov} if ENV['COVERAGE']
|
8
|
+
rescue LoadError
|
9
|
+
end
|
6
10
|
end
|
7
11
|
|
8
12
|
task :default => :spec
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
require 'benchmark/ips'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
puts "Ruby #{RUBY_VERSION} at #{Time.now}"
|
6
|
+
puts
|
7
|
+
|
8
|
+
Benchmark.ips do |x|
|
9
|
+
|
10
|
+
# These two tests look at the cost of simply writing to a buffer.
|
11
|
+
|
12
|
+
x.report("String (Amortized)") do |i|
|
13
|
+
buffer = String.new
|
14
|
+
|
15
|
+
i.times do
|
16
|
+
buffer << "String #{i}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
x.report("StringIO (Amortized)") do |i|
|
21
|
+
buffer = StringIO.new
|
22
|
+
|
23
|
+
i.times do
|
24
|
+
buffer << "String #{i}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
x.compare!
|
29
|
+
end
|
30
|
+
|
31
|
+
# Calculating -------------------------------------
|
32
|
+
# String (Amortized) 91666 i/100ms
|
33
|
+
# StringIO (Amortized) 69531 i/100ms
|
34
|
+
# -------------------------------------------------
|
35
|
+
# String (Amortized) 2856024.9 (±8.8%) i/s - 14208230 in 5.017309s
|
36
|
+
# StringIO (Amortized) 2424863.3 (±7.0%) i/s - 12098394 in 5.013982s
|
37
|
+
#
|
38
|
+
# Comparison:
|
39
|
+
# String (Amortized): 2856024.9 i/s
|
40
|
+
# StringIO (Amortized): 2424863.3 i/s - 1.18x slower
|
41
|
+
|
42
|
+
# Adjust N to consider the cost of allocation vs the cost of appending.
|
43
|
+
N = 5
|
44
|
+
|
45
|
+
Benchmark.ips do |x|
|
46
|
+
# These next two tests consider that multiple writes may be done per buffer allocation.
|
47
|
+
|
48
|
+
x.report("String") do |i|
|
49
|
+
i.times do
|
50
|
+
buffer = String.new
|
51
|
+
|
52
|
+
N.times do
|
53
|
+
buffer << "String #{i}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
x.report("StringIO") do |i|
|
59
|
+
i.times do
|
60
|
+
buffer = StringIO.new
|
61
|
+
|
62
|
+
N.times do
|
63
|
+
buffer << "String #{i}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
x.compare!
|
69
|
+
end
|
70
|
+
|
71
|
+
# Calculating -------------------------------------
|
72
|
+
# String 36822 i/100ms
|
73
|
+
# StringIO 32471 i/100ms
|
74
|
+
# -------------------------------------------------
|
75
|
+
# String 445143.2 (±5.0%) i/s - 2246142 in 5.059017s
|
76
|
+
# StringIO 328469.2 (±4.1%) i/s - 1656021 in 5.049919s
|
77
|
+
#
|
78
|
+
# Comparison:
|
79
|
+
# String: 445143.2 i/s
|
80
|
+
# StringIO: 328469.2 i/s - 1.36x slower
|
data/lib/trenni.rb
CHANGED
data/lib/trenni/builder.rb
CHANGED
@@ -18,7 +18,7 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
21
|
+
require_relative 'strings'
|
22
22
|
|
23
23
|
module Trenni
|
24
24
|
INSTRUCT_ATTRIBUTES = [
|
@@ -27,7 +27,7 @@ module Trenni
|
|
27
27
|
].freeze
|
28
28
|
|
29
29
|
class Builder
|
30
|
-
|
30
|
+
DEFAULT_INDENTATION = "\t".freeze
|
31
31
|
|
32
32
|
# A helper to generate fragments of markup.
|
33
33
|
def self.fragment(builder = nil, &block)
|
@@ -40,18 +40,21 @@ module Trenni
|
|
40
40
|
|
41
41
|
yield builder
|
42
42
|
|
43
|
-
return builder.output
|
43
|
+
return builder.output
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
def initialize(
|
48
|
-
@strict =
|
47
|
+
def initialize(strict: false, indent: true, indentation: DEFAULT_INDENTATION, escape: false, output: String.new)
|
48
|
+
@strict = strict
|
49
49
|
|
50
|
-
@
|
51
|
-
@indentation = options[:indentation] || INDENT
|
52
|
-
@indent = options.fetch(:indent, true)
|
50
|
+
@indent = indent
|
53
51
|
|
54
|
-
@
|
52
|
+
@indentation = indentation
|
53
|
+
# This field gets togged in #inline so we keep track of it separately from @indentation.
|
54
|
+
|
55
|
+
@escape = escape
|
56
|
+
|
57
|
+
@output = output
|
55
58
|
|
56
59
|
@level = [0]
|
57
60
|
@children = [0]
|
@@ -70,24 +73,11 @@ module Trenni
|
|
70
73
|
def instruct(attributes = nil)
|
71
74
|
attributes ||= INSTRUCT_ATTRIBUTES
|
72
75
|
|
73
|
-
@output
|
76
|
+
@output << "<?xml#{tag_attributes(attributes)}?>\n"
|
74
77
|
end
|
75
78
|
|
76
79
|
def doctype(attributes = 'html')
|
77
|
-
|
78
|
-
text = ''
|
79
|
-
attributes.each do |value|
|
80
|
-
if value.match(/[\s"]/)
|
81
|
-
value = '"' + value.gsub('"', '"') + '"'
|
82
|
-
end
|
83
|
-
|
84
|
-
text += ' ' + value
|
85
|
-
end
|
86
|
-
else
|
87
|
-
text = ' ' + attributes
|
88
|
-
end
|
89
|
-
|
90
|
-
@output.puts "<!DOCTYPE#{text}>"
|
80
|
+
@output << "<!DOCTYPE #{attributes}>\n"
|
91
81
|
end
|
92
82
|
|
93
83
|
# Begin a block tag.
|
@@ -112,18 +102,17 @@ module Trenni
|
|
112
102
|
|
113
103
|
# Append pre-existing markup:
|
114
104
|
def append(data)
|
105
|
+
return unless data
|
106
|
+
|
115
107
|
# The parent has one more child:
|
116
108
|
@level[-1] += 1
|
117
109
|
|
118
110
|
if @indent
|
119
|
-
|
120
|
-
|
121
|
-
lines.each_with_index do |line, i|
|
122
|
-
@output.puts if i > 0
|
123
|
-
@output.write indentation + line
|
111
|
+
data.each_line.with_index do |line, i|
|
112
|
+
@output << indentation << line
|
124
113
|
end
|
125
114
|
else
|
126
|
-
@output
|
115
|
+
@output << data
|
127
116
|
end
|
128
117
|
end
|
129
118
|
|
@@ -156,12 +145,12 @@ module Trenni
|
|
156
145
|
def full_tag(name, attributes, indent_outer, indent_inner, &block)
|
157
146
|
if block_given?
|
158
147
|
if indent_outer
|
159
|
-
@output
|
160
|
-
@output
|
148
|
+
@output << "\n" if @level.last > 0
|
149
|
+
@output << indentation
|
161
150
|
end
|
162
151
|
|
163
|
-
@output
|
164
|
-
@output
|
152
|
+
@output << "<#{name}#{tag_attributes(attributes)}>"
|
153
|
+
@output << "\n" if indent_inner
|
165
154
|
|
166
155
|
# The parent has one more child:
|
167
156
|
@level[-1] += 1
|
@@ -173,16 +162,16 @@ module Trenni
|
|
173
162
|
children = @level.pop
|
174
163
|
|
175
164
|
if indent_inner
|
176
|
-
@output
|
177
|
-
@output
|
165
|
+
@output << "\n" if children > 0
|
166
|
+
@output << indentation
|
178
167
|
end
|
179
168
|
|
180
|
-
@output
|
169
|
+
@output << "</#{name}>"
|
181
170
|
else
|
182
171
|
# The parent has one more child:
|
183
172
|
@level[-1] += 1
|
184
173
|
|
185
|
-
@output
|
174
|
+
@output << indentation + "<#{name}#{tag_attributes(attributes)}/>"
|
186
175
|
end
|
187
176
|
end
|
188
177
|
end
|
data/lib/trenni/parser.rb
CHANGED
@@ -54,7 +54,7 @@ module Trenni
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def to_s
|
57
|
-
"
|
57
|
+
":#{self.line_number}"
|
58
58
|
end
|
59
59
|
|
60
60
|
# The line that contains the @offset (base 0 indexing).
|
@@ -74,21 +74,6 @@ module Trenni
|
|
74
74
|
end
|
75
75
|
|
76
76
|
attr :line_text
|
77
|
-
|
78
|
-
def to_hash
|
79
|
-
{
|
80
|
-
:line_number => self.line_number,
|
81
|
-
:line_offset => self.line_range.min,
|
82
|
-
:character_offset => self.line_offset,
|
83
|
-
:text => self.line_text.chomp
|
84
|
-
}
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def self.line_at_offset(input, input_offset)
|
89
|
-
warn "#{self.class}::line_at_offset is deprecated, use Location.new(input, input_offset) directly!"
|
90
|
-
|
91
|
-
Location.new(input, input_offset).to_hash rescue nil
|
92
77
|
end
|
93
78
|
|
94
79
|
class ParseError < StandardError
|
@@ -108,13 +93,14 @@ module Trenni
|
|
108
93
|
@delegate = delegate
|
109
94
|
# The delegate must respond to:
|
110
95
|
# .begin_parse(scanner)
|
111
|
-
# .text(
|
112
|
-
# .cdata(
|
113
|
-
# .attribute(name,
|
96
|
+
# .text(escaped_data)
|
97
|
+
# .cdata(unescaped_data)
|
98
|
+
# .attribute(name, value_or_true)
|
114
99
|
# .begin_tag(name, :opened or :closed)
|
115
100
|
# .end_tag(begin_tag_type, :opened or :closed)
|
116
|
-
# .
|
117
|
-
# .
|
101
|
+
# .doctype(doctype_attributes)
|
102
|
+
# .comment(comment_text)
|
103
|
+
# .instruction(instruction_text)
|
118
104
|
end
|
119
105
|
|
120
106
|
def parse(string)
|
@@ -148,8 +134,10 @@ module Trenni
|
|
148
134
|
scan_tag_normal(scanner, CLOSED_TAG)
|
149
135
|
elsif scanner.scan(/!\[CDATA\[/)
|
150
136
|
scan_tag_cdata(scanner)
|
151
|
-
elsif scanner.scan(
|
137
|
+
elsif scanner.scan(/!--/)
|
152
138
|
scan_tag_comment(scanner)
|
139
|
+
elsif scanner.scan(/!DOCTYPE/)
|
140
|
+
scan_doctype(scanner)
|
153
141
|
elsif scanner.scan(/\?/)
|
154
142
|
scan_tag_instruction(scanner)
|
155
143
|
else
|
@@ -161,9 +149,10 @@ module Trenni
|
|
161
149
|
def scan_attributes(scanner)
|
162
150
|
# Parse an attribute in the form of key="value" or key.
|
163
151
|
while scanner.scan(/\s*([^\s=\/>]+)/um)
|
164
|
-
name = scanner[1]
|
152
|
+
name = scanner[1].freeze
|
165
153
|
if scanner.scan(/=((['"])(.*?)\2)/um)
|
166
|
-
|
154
|
+
value = scanner[3].freeze
|
155
|
+
@delegate.attribute(name, value)
|
167
156
|
else
|
168
157
|
@delegate.attribute(name, true)
|
169
158
|
end
|
@@ -172,7 +161,7 @@ module Trenni
|
|
172
161
|
|
173
162
|
def scan_tag_normal(scanner, begin_tag_type = OPENED_TAG)
|
174
163
|
if scanner.scan(/[^\s\/>]+/)
|
175
|
-
@delegate.begin_tag(scanner.matched, begin_tag_type)
|
164
|
+
@delegate.begin_tag(scanner.matched.freeze, begin_tag_type)
|
176
165
|
|
177
166
|
scanner.scan(/\s*/)
|
178
167
|
self.scan_attributes(scanner)
|
@@ -193,34 +182,34 @@ module Trenni
|
|
193
182
|
raise ParseError.new("Invalid tag!", scanner)
|
194
183
|
end
|
195
184
|
end
|
196
|
-
|
185
|
+
|
186
|
+
def scan_doctype(scanner)
|
187
|
+
if scanner.scan_until(/(.*?)>/)
|
188
|
+
@delegate.doctype(scanner[1].strip.freeze)
|
189
|
+
else
|
190
|
+
raise ParseError.new("DOCTYPE is not closed!", scanner)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
197
194
|
def scan_tag_cdata(scanner)
|
198
195
|
if scanner.scan_until(/(.*?)\]\]>/m)
|
199
|
-
@delegate.cdata(scanner[1])
|
196
|
+
@delegate.cdata(scanner[1].freeze)
|
200
197
|
else
|
201
198
|
raise ParseError.new("CDATA segment is not closed!", scanner)
|
202
199
|
end
|
203
200
|
end
|
204
201
|
|
205
202
|
def scan_tag_comment(scanner)
|
206
|
-
if scanner.
|
207
|
-
|
208
|
-
@delegate.comment("--" + scanner[1] + "--")
|
209
|
-
else
|
210
|
-
raise ParseError.new("Comment is not closed!", scanner)
|
211
|
-
end
|
203
|
+
if scanner.scan_until(/(.*?)-->/m)
|
204
|
+
@delegate.comment(scanner[1].freeze)
|
212
205
|
else
|
213
|
-
|
214
|
-
@delegate.comment(scanner[1])
|
215
|
-
else
|
216
|
-
raise ParseError.new("Comment is not closed!", scanner)
|
217
|
-
end
|
206
|
+
raise ParseError.new("Comment is not closed!", scanner)
|
218
207
|
end
|
219
208
|
end
|
220
209
|
|
221
210
|
def scan_tag_instruction(scanner)
|
222
211
|
if scanner.scan_until(/(.*)\?>/)
|
223
|
-
@delegate.instruction(scanner[1])
|
212
|
+
@delegate.instruction(scanner[1].freeze)
|
224
213
|
end
|
225
214
|
end
|
226
215
|
end
|
data/lib/trenni/template.rb
CHANGED
@@ -48,24 +48,26 @@ module Trenni
|
|
48
48
|
|
49
49
|
attr :parts
|
50
50
|
|
51
|
+
# Output raw text to the template.
|
51
52
|
def text(text)
|
52
|
-
|
53
|
-
|
54
|
-
@parts << "#{OUT} << %q@#{text}@ ; "
|
53
|
+
@parts << "#{OUT}<<#{text.dump};"
|
55
54
|
end
|
56
55
|
|
56
|
+
# Output a ruby expression (or part of).
|
57
57
|
def expression(text)
|
58
|
-
@parts << "#{text}
|
58
|
+
@parts << "#{text};"
|
59
59
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
|
61
|
+
# Output a string interpolation.
|
62
|
+
def interpolation(text)
|
63
|
+
@parts << "#{OUT}<<(#{text});"
|
63
64
|
end
|
64
65
|
|
65
|
-
|
66
|
-
|
66
|
+
CODE_PREFIX = "#{OUT}=[];".freeze
|
67
|
+
CODE_POSTFIX = "#{OUT}".freeze
|
67
68
|
|
68
|
-
|
69
|
+
def code
|
70
|
+
return [CODE_PREFIX, *@parts, CODE_POSTFIX].join
|
69
71
|
end
|
70
72
|
end
|
71
73
|
|
@@ -126,7 +128,7 @@ module Trenni
|
|
126
128
|
end
|
127
129
|
|
128
130
|
if level == 0
|
129
|
-
@callback.
|
131
|
+
@callback.interpolation(code)
|
130
132
|
else
|
131
133
|
raise StandardError.new "Could not find end of expression #{self}!"
|
132
134
|
end
|
@@ -140,16 +142,20 @@ module Trenni
|
|
140
142
|
end
|
141
143
|
end
|
142
144
|
|
143
|
-
def self.
|
144
|
-
|
145
|
+
def self.load_file(path)
|
146
|
+
self.new(File.read(path), path)
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.load(io, path = io.inspect)
|
150
|
+
self.new(io.read, path)
|
145
151
|
end
|
146
152
|
|
147
|
-
def initialize(
|
148
|
-
@
|
149
|
-
@
|
153
|
+
def initialize(text, path = '<Trenni>')
|
154
|
+
@text = text
|
155
|
+
@path = path
|
150
156
|
end
|
151
157
|
|
152
|
-
def to_string(scope =
|
158
|
+
def to_string(scope = Object.new)
|
153
159
|
to_array(scope).join
|
154
160
|
end
|
155
161
|
|
@@ -159,15 +165,16 @@ module Trenni
|
|
159
165
|
|
160
166
|
def to_array(scope)
|
161
167
|
if Binding === scope
|
162
|
-
|
168
|
+
# Slow code path, evaluate the code string in the given binding (scope).
|
169
|
+
eval(code, scope, @path)
|
163
170
|
else
|
164
|
-
#
|
171
|
+
# Faster code path, use instance_eval on a compiled Proc.
|
165
172
|
scope.instance_eval(&to_proc)
|
166
173
|
end
|
167
174
|
end
|
168
175
|
|
169
176
|
def to_proc
|
170
|
-
@compiled_proc ||= eval("proc{\n#{code}\n}", binding, @
|
177
|
+
@compiled_proc ||= eval("proc{\n#{code}\n}", binding, @path, 0)
|
171
178
|
end
|
172
179
|
|
173
180
|
protected
|
@@ -178,7 +185,7 @@ module Trenni
|
|
178
185
|
|
179
186
|
def compile!
|
180
187
|
buffer = Buffer.new
|
181
|
-
scanner = Scanner.new(buffer, @
|
188
|
+
scanner = Scanner.new(buffer, @text)
|
182
189
|
|
183
190
|
scanner.parse
|
184
191
|
|
data/lib/trenni/version.rb
CHANGED
data/spec/trenni/builder_spec.rb
CHANGED
@@ -23,20 +23,53 @@
|
|
23
23
|
require 'trenni'
|
24
24
|
|
25
25
|
module Trenni::BuilderSpec
|
26
|
+
describe 'Trenni::Builder#fragment' do
|
27
|
+
let(:builder) {Trenni::Builder.new}
|
28
|
+
|
29
|
+
it "should use an existing builder" do
|
30
|
+
result = Trenni::Builder.fragment do |builder|
|
31
|
+
end
|
32
|
+
|
33
|
+
expect(result).to_not be == nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should use an existing builder" do
|
37
|
+
expect(Trenni::Builder).to receive(:new).and_call_original
|
38
|
+
|
39
|
+
result = Trenni::Builder.fragment(builder) do |builder|
|
40
|
+
end
|
41
|
+
|
42
|
+
expect(result).to be == nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
26
46
|
describe Trenni::Builder do
|
47
|
+
it 'should be able to append nil' do
|
48
|
+
expect{subject.append(nil)}.to_not raise_error(TypeError)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should append existing markup' do
|
52
|
+
subject.tag("outer") do
|
53
|
+
subject.append("<inner>\n\t<nested/>\n</inner>")
|
54
|
+
end
|
55
|
+
|
56
|
+
expect(subject.output).to be == "<outer>\n\t<inner>\n\t\t<nested/>\n\t</inner>\n</outer>"
|
57
|
+
end
|
58
|
+
|
27
59
|
it "should produce valid xml output" do
|
28
|
-
builder = Trenni::Builder.new(:
|
60
|
+
builder = Trenni::Builder.new(indent: false)
|
29
61
|
|
30
62
|
builder.instruct
|
31
63
|
builder.tag('foo', 'bar' => 'baz') do
|
32
|
-
builder.text(
|
64
|
+
builder.text('apples and oranges')
|
65
|
+
builder.tag('baz')
|
33
66
|
end
|
34
67
|
|
35
|
-
expect(builder.output
|
68
|
+
expect(builder.output).to be == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<foo bar=\"baz\">apples and oranges<baz/></foo>"
|
36
69
|
end
|
37
70
|
|
38
71
|
it "should produce valid html" do
|
39
|
-
builder = Trenni::Builder.new(:
|
72
|
+
builder = Trenni::Builder.new(indent: true)
|
40
73
|
|
41
74
|
builder.doctype
|
42
75
|
builder.tag('html') do
|
@@ -49,7 +82,7 @@ module Trenni::BuilderSpec
|
|
49
82
|
end
|
50
83
|
end
|
51
84
|
|
52
|
-
expect(builder.output
|
85
|
+
expect(builder.output).to be == "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<title>Hello World</title>\n\t</head>\n\t<body>\n\t</body>\n</html>"
|
53
86
|
end
|
54
87
|
|
55
88
|
it "should indent self-closing tag correctly" do
|
@@ -57,7 +90,7 @@ module Trenni::BuilderSpec
|
|
57
90
|
|
58
91
|
builder.tag('foo') { builder.tag('bar') }
|
59
92
|
|
60
|
-
expect(builder.output
|
93
|
+
expect(builder.output).to be == "<foo>\n\t<bar/>\n</foo>"
|
61
94
|
end
|
62
95
|
|
63
96
|
it "should produce inline html" do
|
@@ -71,7 +104,7 @@ module Trenni::BuilderSpec
|
|
71
104
|
builder.text "World!"
|
72
105
|
end
|
73
106
|
|
74
|
-
expect(builder.output
|
107
|
+
expect(builder.output).to be == "<div><strong>Hello</strong>World!</div>"
|
75
108
|
end
|
76
109
|
|
77
110
|
it "escapes attributes and text correctly" do
|
@@ -81,7 +114,7 @@ module Trenni::BuilderSpec
|
|
81
114
|
builder.text %Q{if x < 10}
|
82
115
|
end
|
83
116
|
|
84
|
-
expect(builder.output
|
117
|
+
expect(builder.output).to be == %Q{<foo bar=""Hello World"">if x < 10</foo>}
|
85
118
|
end
|
86
119
|
|
87
120
|
it "should support strict attributes" do
|
@@ -89,7 +122,7 @@ module Trenni::BuilderSpec
|
|
89
122
|
|
90
123
|
builder.tag :option, :required => true
|
91
124
|
|
92
|
-
expect(builder.output
|
125
|
+
expect(builder.output).to be == %Q{<option required="required"/>}
|
93
126
|
end
|
94
127
|
|
95
128
|
it "should support compact attributes" do
|
@@ -97,7 +130,7 @@ module Trenni::BuilderSpec
|
|
97
130
|
|
98
131
|
builder.tag :option, :required => true
|
99
132
|
|
100
|
-
expect(builder.output
|
133
|
+
expect(builder.output).to be == %Q{<option required/>}
|
101
134
|
end
|
102
135
|
|
103
136
|
it "should output without changing escaped characters" do
|
@@ -105,17 +138,17 @@ module Trenni::BuilderSpec
|
|
105
138
|
|
106
139
|
builder.tag "section", :'data-text' => 'foo\nbar'
|
107
140
|
|
108
|
-
expect(builder.output
|
141
|
+
expect(builder.output).to be == '<section data-text="foo\nbar"/>'
|
109
142
|
end
|
110
143
|
|
111
144
|
it "should order attributes as specified" do
|
112
145
|
builder = Trenni::Builder.new(:strict => true)
|
113
146
|
builder.tag :t, [[:a, 10], [:b, 20]]
|
114
|
-
expect(builder.output
|
147
|
+
expect(builder.output).to be == %Q{<t a="10" b="20"/>}
|
115
148
|
|
116
149
|
builder = Trenni::Builder.new(:strict => true)
|
117
150
|
builder.tag :t, :b => 20, :a => 10
|
118
|
-
expect(builder.output
|
151
|
+
expect(builder.output).to be == %Q{<t b="20" a="10"/>}
|
119
152
|
end
|
120
153
|
end
|
121
154
|
end
|
data/spec/trenni/parser_spec.rb
CHANGED
@@ -40,11 +40,44 @@ module Trenni::ParserSpec
|
|
40
40
|
end
|
41
41
|
|
42
42
|
describe Trenni::Parser do
|
43
|
+
let(:delegate) {ParserDelegate.new}
|
44
|
+
let(:parser) {Trenni::Parser.new(delegate)}
|
45
|
+
|
46
|
+
it "should parse self-closing tags correctly" do
|
47
|
+
parser.parse("<br/>")
|
48
|
+
|
49
|
+
expect(delegate.events).to be == [
|
50
|
+
[:begin_tag, "br", :opened],
|
51
|
+
[:finish_tag, :opened, :closed],
|
52
|
+
]
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should parse doctype correctly" do
|
56
|
+
parser.parse("<!DOCTYPE html>")
|
57
|
+
|
58
|
+
expect(delegate.events).to be == [
|
59
|
+
[:doctype, "html"]
|
60
|
+
]
|
61
|
+
end
|
62
|
+
|
63
|
+
it "Should parse instruction correctly" do
|
64
|
+
parser.parse("<?foo=bar?>")
|
65
|
+
|
66
|
+
expect(delegate.events).to be == [
|
67
|
+
[:instruction, "foo=bar"]
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should parse comment correctly" do
|
72
|
+
parser.parse(%Q{<!--comment-->})
|
73
|
+
|
74
|
+
expect(delegate.events).to be == [
|
75
|
+
[:comment, "comment"]
|
76
|
+
]
|
77
|
+
end
|
78
|
+
|
43
79
|
it "should parse markup correctly" do
|
44
|
-
|
45
|
-
scanner = Trenni::Parser.new(delegate)
|
46
|
-
|
47
|
-
scanner.parse(%Q{<foo bar="20" baz>Hello World</foo>})
|
80
|
+
parser.parse(%Q{<foo bar="20" baz>Hello World</foo>})
|
48
81
|
|
49
82
|
expected_events = [
|
50
83
|
[:begin_tag, "foo", :opened],
|
@@ -60,10 +93,7 @@ module Trenni::ParserSpec
|
|
60
93
|
end
|
61
94
|
|
62
95
|
it "should parse CDATA correctly" do
|
63
|
-
|
64
|
-
scanner = Trenni::Parser.new(delegate)
|
65
|
-
|
66
|
-
scanner.parse(%Q{<test><![CDATA[Hello World]]></test>})
|
96
|
+
parser.parse(%Q{<test><![CDATA[Hello World]]></test>})
|
67
97
|
|
68
98
|
expected_events = [
|
69
99
|
[:begin_tag, "test", :opened],
|
@@ -77,21 +107,20 @@ module Trenni::ParserSpec
|
|
77
107
|
end
|
78
108
|
|
79
109
|
it "should generate errors on incorrect input" do
|
80
|
-
|
81
|
-
scanner = Trenni::Parser.new(delegate)
|
82
|
-
|
83
|
-
expect{scanner.parse(%Q{<foo})}.to raise_error Trenni::Parser::ParseError
|
110
|
+
expect{parser.parse(%Q{<foo})}.to raise_error Trenni::Parser::ParseError
|
84
111
|
|
85
|
-
expect{
|
112
|
+
expect{parser.parse(%Q{<foo bar=>})}.to raise_error Trenni::Parser::ParseError
|
86
113
|
|
87
|
-
expect{
|
114
|
+
expect{parser.parse(%Q{<foo bar="" baz>})}.to_not raise_error
|
88
115
|
end
|
89
116
|
|
90
117
|
it "should know about line numbers" do
|
91
118
|
data = %Q{Hello\nWorld\nFoo\nBar!}
|
92
119
|
|
93
120
|
location = Trenni::Parser::Location.new(data, 7)
|
94
|
-
|
121
|
+
|
122
|
+
expect(location.to_i).to be == 7
|
123
|
+
expect(location.to_s).to be == ":2"
|
95
124
|
expect(location.line_text).to be == "World"
|
96
125
|
|
97
126
|
expect(location.line_number).to be == 2
|
@@ -100,11 +129,8 @@ module Trenni::ParserSpec
|
|
100
129
|
end
|
101
130
|
|
102
131
|
it "should know about line numbers when input contains multi-byte characters" do
|
103
|
-
delegate = ParserDelegate.new
|
104
|
-
scanner = Trenni::Parser.new(delegate)
|
105
|
-
|
106
132
|
data = %Q{<p>\nこんにちは\nWorld\n<p}
|
107
|
-
error =
|
133
|
+
error = parser.parse(data) rescue $!
|
108
134
|
|
109
135
|
expect(error).to be_kind_of Trenni::Parser::ParseError
|
110
136
|
expect(error.location.line_number).to be == 4
|
@@ -27,6 +27,24 @@ require 'benchmark'
|
|
27
27
|
|
28
28
|
module Trenni::TemplateSpec
|
29
29
|
describe Trenni::Template do
|
30
|
+
let(:capture_template) {Trenni::Template.load_file File.expand_path('template_spec/capture.trenni', __dir__)}
|
31
|
+
|
32
|
+
it "should be able to capture output" do
|
33
|
+
expect(capture_template.to_string.strip).to be == 'TEST TEST TEST'
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:buffer_template) {Trenni::Template.load_file File.expand_path('template_spec/buffer.trenni', __dir__)}
|
37
|
+
|
38
|
+
it "should be able to fetch output buffer" do
|
39
|
+
expect(buffer_template.to_string).to be == 'test'
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:nested_template) {Trenni::Template.load_file File.expand_path('template_spec/nested.trenni', __dir__)}
|
43
|
+
|
44
|
+
it "should be able to handle nested interpolations" do
|
45
|
+
expect(nested_template.to_string).to be == "Hello world!"
|
46
|
+
end
|
47
|
+
|
30
48
|
it "should process list of items" do
|
31
49
|
template = Trenni::Template.new('<?r items.each do |item| ?>#{item}<?r end ?>')
|
32
50
|
|
@@ -35,7 +53,7 @@ module Trenni::TemplateSpec
|
|
35
53
|
expect(template.to_string(binding)).to be == "1234"
|
36
54
|
end
|
37
55
|
|
38
|
-
let(:large_template) {Trenni::Template.
|
56
|
+
let(:large_template) {Trenni::Template.load_file File.expand_path('template_spec/large.trenni', __dir__)}
|
39
57
|
|
40
58
|
it "should have better performance using instance" do
|
41
59
|
n = 1_000
|
@@ -54,7 +72,7 @@ module Trenni::TemplateSpec
|
|
54
72
|
expect(object_time).to be < binding_time
|
55
73
|
end
|
56
74
|
|
57
|
-
let(:escaped_template) {Trenni::Template.
|
75
|
+
let(:escaped_template) {Trenni::Template.load_file File.expand_path('template_spec/escaped.trenni', __dir__)}
|
58
76
|
|
59
77
|
it "should process escaped characters" do
|
60
78
|
expect(escaped_template.to_string).to be ==
|
File without changes
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
#{{text: "Hello #{'world'}!"}[:text]}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trenni
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -69,6 +69,7 @@ files:
|
|
69
69
|
- Gemfile
|
70
70
|
- README.md
|
71
71
|
- Rakefile
|
72
|
+
- benchmark/io_vs_string.rb
|
72
73
|
- lib/trenni.rb
|
73
74
|
- lib/trenni/builder.rb
|
74
75
|
- lib/trenni/parser.rb
|
@@ -76,11 +77,14 @@ files:
|
|
76
77
|
- lib/trenni/template.rb
|
77
78
|
- lib/trenni/version.rb
|
78
79
|
- spec/trenni/builder_spec.rb
|
79
|
-
- spec/trenni/escaped.trenni
|
80
|
-
- spec/trenni/large.trenni
|
81
80
|
- spec/trenni/parser_spec.rb
|
82
81
|
- spec/trenni/strings_spec.rb
|
83
82
|
- spec/trenni/template_spec.rb
|
83
|
+
- spec/trenni/template_spec/buffer.trenni
|
84
|
+
- spec/trenni/template_spec/capture.trenni
|
85
|
+
- spec/trenni/template_spec/escaped.trenni
|
86
|
+
- spec/trenni/template_spec/large.trenni
|
87
|
+
- spec/trenni/template_spec/nested.trenni
|
84
88
|
- trenni.gemspec
|
85
89
|
homepage: https://github.com/ioquatix/trenni
|
86
90
|
licenses: []
|
@@ -101,14 +105,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
105
|
version: '0'
|
102
106
|
requirements: []
|
103
107
|
rubyforge_project:
|
104
|
-
rubygems_version: 2.
|
108
|
+
rubygems_version: 2.5.2
|
105
109
|
signing_key:
|
106
110
|
specification_version: 4
|
107
111
|
summary: A fast native templating system that compiles directly to Ruby code.
|
108
112
|
test_files:
|
109
113
|
- spec/trenni/builder_spec.rb
|
110
|
-
- spec/trenni/escaped.trenni
|
111
|
-
- spec/trenni/large.trenni
|
112
114
|
- spec/trenni/parser_spec.rb
|
113
115
|
- spec/trenni/strings_spec.rb
|
114
116
|
- spec/trenni/template_spec.rb
|
117
|
+
- spec/trenni/template_spec/buffer.trenni
|
118
|
+
- spec/trenni/template_spec/capture.trenni
|
119
|
+
- spec/trenni/template_spec/escaped.trenni
|
120
|
+
- spec/trenni/template_spec/large.trenni
|
121
|
+
- spec/trenni/template_spec/nested.trenni
|