trenni 1.4.5 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://secure.travis-ci.org/ioquatix/trenni.
|
11
|
-
[![Code Climate](https://codeclimate.com/github/ioquatix/trenni.
|
10
|
+
[![Build Status](https://secure.travis-ci.org/ioquatix/trenni.svg)](http://travis-ci.org/ioquatix/trenni)
|
11
|
+
[![Code Climate](https://codeclimate.com/github/ioquatix/trenni.svg)](https://codeclimate.com/github/ioquatix/trenni)
|
12
12
|
[![Coverage Status](https://coveralls.io/repos/ioquatix/trenni/badge.svg)](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
|