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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 35f057654b1e83a57d1fe72969c9ee72eaf376de
4
- data.tar.gz: a96c7a44b65be07ed3dc69e20ddbb25d92ee300a
3
+ metadata.gz: 5ff8d7a73ba2ef5c153eff38d0869b2d6cb53a91
4
+ data.tar.gz: 5a8a5f044ea099e6d69fc17d1481f47f2a678f47
5
5
  SHA512:
6
- metadata.gz: cef23ac0aa45af2f913cdd6b864a5beeacb78e0b3bc1c621db0565de40a34f030df816f048341a8c582f79628b75c334e33de3580c869956c7b0884856a48e21
7
- data.tar.gz: e6a318ab7acaa6a04c5857b51caecb019936b07dcdcdd50581d0cb7879a2260ac8b1c399fdd28660cfd4cd87fa247649d89a3da54205b7bee15ede9a7e890bd4
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 "ruby-prof"
7
+ gem 'pry'
8
+ gem 'ruby-prof', platforms: [:mri]
8
9
  end
9
10
 
10
11
  group :test do
11
- gem 'simplecov'
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.png)](http://travis-ci.org/ioquatix/trenni)
11
- [![Code Climate](https://codeclimate.com/github/ioquatix/trenni.png)](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
- task.rspec_opts = ["--require", "simplecov"] if ENV['COVERAGE']
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
@@ -18,5 +18,5 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'trenni/builder'
22
- require 'trenni/template'
21
+ require_relative 'trenni/builder'
22
+ require_relative 'trenni/template'
@@ -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
- require 'trenni/strings'
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
- INDENT = "\t"
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.string
43
+ return builder.output
44
44
  end
45
45
  end
46
46
 
47
- def initialize(options = {})
48
- @strict = options[:strict]
47
+ def initialize(strict: false, indent: true, indentation: DEFAULT_INDENTATION, escape: false, output: String.new)
48
+ @strict = strict
49
49
 
50
- @output = options[:output] || StringIO.new
51
- @indentation = options[:indentation] || INDENT
52
- @indent = options.fetch(:indent, true)
50
+ @indent = indent
53
51
 
54
- @escape = options[:escape]
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.puts "<?xml#{tag_attributes(attributes)}?>"
76
+ @output << "<?xml#{tag_attributes(attributes)}?>\n"
74
77
  end
75
78
 
76
79
  def doctype(attributes = 'html')
77
- if Array === attributes
78
- text = ''
79
- attributes.each do |value|
80
- if value.match(/[\s"]/)
81
- value = '"' + value.gsub('"', '&quot;') + '"'
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
- lines = data.strip.split(/\n/)
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.write data
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.puts if @level.last > 0
160
- @output.write indentation
148
+ @output << "\n" if @level.last > 0
149
+ @output << indentation
161
150
  end
162
151
 
163
- @output.write "<#{name}#{tag_attributes(attributes)}>"
164
- @output.puts if indent_inner
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.puts if children > 0
177
- @output.write indentation
165
+ @output << "\n" if children > 0
166
+ @output << indentation
178
167
  end
179
168
 
180
- @output.write "</#{name}>"
169
+ @output << "</#{name}>"
181
170
  else
182
171
  # The parent has one more child:
183
172
  @level[-1] += 1
184
173
 
185
- @output.write indentation + "<#{name}#{tag_attributes(attributes)}/>"
174
+ @output << indentation + "<#{name}#{tag_attributes(attributes)}/>"
186
175
  end
187
176
  end
188
177
  end
@@ -54,7 +54,7 @@ module Trenni
54
54
  end
55
55
 
56
56
  def to_s
57
- "[#{self.line_number}:#{self.line_range}]"
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(escaped-data)
112
- # .cdata(unescaped-data)
113
- # .attribute(name, value-or-true)
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
- # .comment(comment-text)
117
- # .instruction(instruction-text)
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
- @delegate.attribute(name, scanner[3])
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.scan(/--/)
207
- if scanner.scan_until(/(.*?)-->/m)
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
- if scanner.scan_until(/(.*?)>/)
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
@@ -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
- text = text.gsub('\\', '\\\\\\').gsub('@', '\\@')
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
- def output(text)
62
- @parts << "#{OUT} << (#{text}) ; "
60
+
61
+ # Output a string interpolation.
62
+ def interpolation(text)
63
+ @parts << "#{OUT}<<(#{text});"
63
64
  end
64
65
 
65
- def code
66
- parts = ["#{OUT} = [] ; "] + @parts + ["#{OUT}"]
66
+ CODE_PREFIX = "#{OUT}=[];".freeze
67
+ CODE_POSTFIX = "#{OUT}".freeze
67
68
 
68
- return parts.join
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.output(code)
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.load(path)
144
- return self.new(File.read(path), path)
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(template, filename = '<Trenni>')
148
- @template = template
149
- @filename = filename
153
+ def initialize(text, path = '<Trenni>')
154
+ @text = text
155
+ @path = path
150
156
  end
151
157
 
152
- def to_string(scope = nil)
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
- eval(code, scope, @filename)
168
+ # Slow code path, evaluate the code string in the given binding (scope).
169
+ eval(code, scope, @path)
163
170
  else
164
- # This can sometimes be a bit faster:
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, @filename, 0)
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, @template)
188
+ scanner = Scanner.new(buffer, @text)
182
189
 
183
190
  scanner.parse
184
191
 
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Trenni
22
- VERSION = "1.4.5"
22
+ VERSION = "1.5.0"
23
23
  end
@@ -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(:indent => false)
60
+ builder = Trenni::Builder.new(indent: false)
29
61
 
30
62
  builder.instruct
31
63
  builder.tag('foo', 'bar' => 'baz') do
32
- builder.text("apples and oranges")
64
+ builder.text('apples and oranges')
65
+ builder.tag('baz')
33
66
  end
34
67
 
35
- expect(builder.output.string).to be == "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<foo bar=\"baz\">apples and oranges</foo>"
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(:indent => true)
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.string).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>"
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.string).to be == "<foo>\n\t<bar/>\n</foo>"
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.string).to be == "<div><strong>Hello</strong>World!</div>"
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.string).to be == %Q{<foo bar="&quot;Hello World&quot;">if x &lt; 10</foo>}
117
+ expect(builder.output).to be == %Q{<foo bar="&quot;Hello World&quot;">if x &lt; 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.string).to be == %Q{<option required="required"/>}
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.string).to be == %Q{<option required/>}
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.string).to be == '<section data-text="foo\nbar"/>'
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.string).to be == %Q{<t a="10" b="20"/>}
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.string).to be == %Q{<t b="20" a="10"/>}
151
+ expect(builder.output).to be == %Q{<t b="20" a="10"/>}
119
152
  end
120
153
  end
121
154
  end
@@ -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
- delegate = ParserDelegate.new
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
- delegate = ParserDelegate.new
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
- delegate = ParserDelegate.new
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{scanner.parse(%Q{<foo bar=>})}.to raise_error Trenni::Parser::ParseError
112
+ expect{parser.parse(%Q{<foo bar=>})}.to raise_error Trenni::Parser::ParseError
86
113
 
87
- expect{scanner.parse(%Q{<foo bar="" baz>})}.to_not raise_error
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 = scanner.parse(data) rescue $!
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.load File.join(__dir__, "large.trenni")}
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.load File.join(__dir__, "escaped.trenni")}
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 ==
@@ -0,0 +1,8 @@
1
+ <?r
2
+ def append_to_buffer(&block)
3
+ out = Trenni::Template.buffer(block.binding)
4
+ out << "test"
5
+ end
6
+
7
+ append_to_buffer {}
8
+ ?>
@@ -0,0 +1,4 @@
1
+ <?r result = Trenni::Template.capture do ?>
2
+ test test test
3
+ <?r end ?>
4
+ #{result.upcase}
@@ -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.5
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-01-12 00:00:00.000000000 Z
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.4.6
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