trenni 2.1.0 → 3.0.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.
data/lib/trenni/markup.rb CHANGED
@@ -18,47 +18,46 @@
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_relative 'substitutions'
21
+ require 'cgi'
22
22
 
23
23
  module Trenni
24
24
  # A wrapper which indicates that `value` can be appended to the output buffer without any changes.
25
25
  module Markup
26
- # Generates a string suitable for concatenating with the output buffer.
27
- def self.escape(value)
26
+ # Converts special characters `<`, `>`, `&`, and `"` into their equivalent entities.
27
+ # @return [String] May return the original string if no changes were made.
28
+ def self.escape_string(string)
29
+ CGI.escape_html(string)
30
+ end
31
+
32
+ # Appends a string to the output buffer, escaping if if necessary.
33
+ def self.append(buffer, value)
28
34
  if value.is_a? Markup
29
- value
35
+ buffer << value
30
36
  elsif value
31
- MarkupString.new(value.to_s)
32
- else
33
- # String#<< won't accept nil, so we return an empty string, thus ensuring a fixed point function:
34
- EMPTY
37
+ buffer << self.escape_string(value.to_s)
35
38
  end
36
39
  end
37
-
38
- def escape(value)
39
- Markup.escape(value)
40
- end
41
40
  end
42
41
 
43
42
  # Initialized from text which is escaped to use HTML entities.
44
43
  class MarkupString < String
45
44
  include Markup
46
45
 
47
- # This is only casually related to HTML, it's just enough so that it would not be mis-interpreted by `Trenni::Parser`.
48
- ESCAPE = Substitutions.new("&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "\"" => "&quot;")
49
-
50
- # Convert ESCAPE characters into their corresponding entities.
46
+ # @param string [String] the string value itself.
47
+ # @param escape [Boolean] whether or not to escape the string.
51
48
  def initialize(string = nil, escape = true)
52
49
  if string
53
- super(string)
50
+ if escape
51
+ string = Markup.escape_string(string)
52
+ end
54
53
 
55
- # self.replace CGI.escapeHTML(self)
56
- ESCAPE.gsub!(self) if escape
54
+ super(string)
57
55
  else
58
56
  super()
59
57
  end
60
58
  end
61
59
 
60
+ # Generate a valid MarkupString withot any escaping.
62
61
  def self.raw(string)
63
62
  self.new(string, false)
64
63
  end
@@ -69,10 +68,4 @@ module Trenni
69
68
  MarkupString.new(JSON.dump(value), false)
70
69
  end
71
70
  end
72
-
73
- def self.MarkupString(value)
74
- Markup.escape(value)
75
- end
76
-
77
- Markup::EMPTY = String.new.extend(Markup).freeze
78
71
  end
data/lib/trenni/native.rb CHANGED
@@ -20,9 +20,12 @@
20
20
 
21
21
  require_relative 'parse_error'
22
22
 
23
+ # Methods on the following classes may be replaced by native implementations:
24
+ require_relative 'tag'
25
+
23
26
  begin
24
27
  # Load native code:
25
28
  require_relative 'trenni'
26
29
  rescue LoadError
27
30
  warn "Could not load native implementation: #{$!}" if $VERBOSE
28
- end
31
+ end unless ENV['TRENNI_PREFER_FALLBACK']
@@ -2,7 +2,7 @@
2
2
  require_relative 'native'
3
3
  require_relative 'parse_delegate'
4
4
 
5
- if defined? Trenni::Native and !ENV['TRENNI_PREFER_FALLBACK']
5
+ if defined? Trenni::Native
6
6
  Trenni::Parsers = Trenni::Native
7
7
  else
8
8
  require_relative 'fallback/markup'
data/lib/trenni/tag.rb ADDED
@@ -0,0 +1,130 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'markup'
22
+
23
+ module Trenni
24
+ # This represents an individual SGML tag, e.g. <a>, </a> or <a />, with attributes. Attribute values must be escaped.
25
+ Tag = Struct.new(:name, :closed, :attributes) do
26
+ include Trenni::Markup
27
+
28
+ def self.split(qualified_name)
29
+ if i = qualified_name.index(':')
30
+ return qualified_name.slice(0...i), qualified_name.slice(i+1..-1)
31
+ else
32
+ return nil, qualified_name
33
+ end
34
+ end
35
+
36
+ def self.closed(name, **attributes)
37
+ self.new(name, true, attributes)
38
+ end
39
+
40
+ def self.opened(name, **attributes)
41
+ self.new(name, false, attributes)
42
+ end
43
+
44
+ def [] key
45
+ attributes[key]
46
+ end
47
+
48
+ alias to_hash attributes
49
+
50
+ def to_s(content = nil)
51
+ self.class.format_tag(name, attributes, content || !closed)
52
+ end
53
+
54
+ alias to_str to_s
55
+
56
+ def self_closed?
57
+ closed
58
+ end
59
+
60
+ def write_opening_tag(buffer)
61
+ buffer << '<' << name
62
+
63
+ self.class.append_attributes(buffer, attributes, nil)
64
+
65
+ if self_closed?
66
+ buffer << '/>'
67
+ else
68
+ buffer << '>'
69
+ end
70
+ end
71
+
72
+ def write_closing_tag(buffer)
73
+ buffer << '</' << name << '>'
74
+ end
75
+
76
+ def write(buffer, content = nil)
77
+ self.class.append_tag(buffer, name, attributes, content || !closed)
78
+ end
79
+
80
+ def self.format_tag(name, attributes, content)
81
+ buffer = String.new.force_encoding(name.encoding)
82
+
83
+ self.append_tag(buffer, name, attributes, content)
84
+
85
+ return buffer
86
+ end
87
+
88
+ def self.append_tag(buffer, name, attributes, content)
89
+ buffer << '<' << name.to_s
90
+
91
+ self.append_attributes(buffer, attributes, nil)
92
+
93
+ if !content
94
+ buffer << '/>'
95
+ else
96
+ buffer << '>'
97
+ unless content == true
98
+ Markup.append(buffer, content)
99
+ end
100
+ buffer << '</' << name.to_s << '>'
101
+ end
102
+
103
+ return nil
104
+ end
105
+
106
+ # Convert a set of attributes into a string suitable for use within a <tag>.
107
+ def self.append_attributes(buffer, attributes, prefix)
108
+ attributes.each do |key, value|
109
+ next unless value
110
+
111
+ attribute_key = prefix ? "#{prefix}-#{key}" : key
112
+
113
+ case value
114
+ when Hash
115
+ self.append_attributes(buffer, value, attribute_key)
116
+ when Array
117
+ self.append_attributes(buffer, value, attribute_key)
118
+ when TrueClass
119
+ buffer << ' ' << attribute_key.to_s
120
+ else
121
+ buffer << ' ' << attribute_key.to_s << '="'
122
+ Markup.append(buffer, value)
123
+ buffer << '"'
124
+ end
125
+ end
126
+
127
+ return nil
128
+ end
129
+ end
130
+ end
@@ -59,9 +59,8 @@ module Trenni
59
59
  end
60
60
 
61
61
  class Assembler
62
- def initialize(filter: String, encoding: Encoding::UTF_8)
62
+ def initialize(encoding: Encoding::UTF_8)
63
63
  @code = String.new.force_encoding(encoding)
64
- @filter = filter
65
64
  end
66
65
 
67
66
  attr :code
@@ -83,20 +82,27 @@ module Trenni
83
82
  # Output a string interpolation.
84
83
  def expression(text)
85
84
  # Double brackets are required here to handle expressions like #{foo rescue "bar"}.
86
- @code << "#{OUT}<<#{@filter}((#{text}));"
85
+ @code << "#{OUT}<<String(#{text});"
87
86
  end
88
87
  end
89
88
 
90
- def self.load_file(path, **options)
91
- self.new(FileBuffer.new(path), **options)
89
+ def self.load_file(path, *args)
90
+ self.new(FileBuffer.new(path), *args).freeze
92
91
  end
93
92
 
94
- def initialize(buffer, filter: String)
93
+ def initialize(buffer)
95
94
  @buffer = buffer
96
- @filter = filter
97
95
  end
98
-
99
- def to_string(scope = Object.new, output = output_buffer)
96
+
97
+ def freeze
98
+ return self if frozen?
99
+
100
+ to_proc
101
+
102
+ super
103
+ end
104
+
105
+ def to_string(scope = Object.new, output = nil)
100
106
  output ||= output_buffer
101
107
 
102
108
  scope.instance_exec(output, &to_proc)
@@ -105,9 +111,9 @@ module Trenni
105
111
  def to_buffer(scope)
106
112
  Buffer.new(to_string(scope), path: @buffer.path)
107
113
  end
108
-
114
+
109
115
  def to_proc(scope = nil)
110
- @compiled_proc ||= eval("proc{|#{OUT}|;#{code};#{OUT}}", scope, @buffer.path)
116
+ @compiled_proc ||= eval("proc{|#{OUT}|;#{code};#{OUT}}", scope, @buffer.path).freeze
111
117
  end
112
118
 
113
119
  protected
@@ -120,8 +126,12 @@ module Trenni
120
126
  @code ||= compile!
121
127
  end
122
128
 
129
+ def make_assembler
130
+ Assembler.new
131
+ end
132
+
123
133
  def compile!
124
- assembler = Assembler.new(filter: @filter)
134
+ assembler = make_assembler
125
135
 
126
136
  Parsers.parse_template(@buffer, assembler)
127
137
 
@@ -130,11 +140,21 @@ module Trenni
130
140
  end
131
141
 
132
142
  class MarkupTemplate < Template
133
- def initialize(buffer, filter: MarkupString)
134
- super
143
+ class Assembler < Template::Assembler
144
+ # Output a string interpolation.
145
+ def expression(text)
146
+ @code << "Markup.append(#{OUT},(#{text}));"
147
+ end
148
+ end
149
+
150
+ protected
151
+
152
+ # We need an assembler which builds specific `Markup.append` sequences.
153
+ def make_assembler
154
+ Assembler.new
135
155
  end
136
156
 
137
- # The output of the markup template is encoded markup (e.g. with entities, tags, etc)
157
+ # The output of the markup template is encoded markup (e.g. with entities, tags, etc).
138
158
  def output_buffer
139
159
  MarkupString.new.force_encoding(code.encoding)
140
160
  end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Trenni
22
- VERSION = "2.1.0"
22
+ VERSION = "3.0.0"
23
23
  end
@@ -0,0 +1,29 @@
1
+
2
+ if ENV['COVERAGE']
3
+ begin
4
+ require 'simplecov'
5
+
6
+ SimpleCov.start do
7
+ add_filter "/spec/"
8
+ end
9
+
10
+ if ENV['TRAVIS']
11
+ require 'coveralls'
12
+ Coveralls.wear!
13
+ end
14
+ rescue LoadError
15
+ warn "Could not load simplecov: #{$!}"
16
+ end
17
+ end
18
+
19
+ require "bundler/setup"
20
+ require "trenni"
21
+
22
+ RSpec.configure do |config|
23
+ # Enable flags like --only-failures and --next-failure
24
+ config.example_status_persistence_file_path = ".rspec_status"
25
+
26
+ config.expect_with :rspec do |c|
27
+ c.syntax = :expect
28
+ end
29
+ end
@@ -23,7 +23,7 @@
23
23
  require 'trenni'
24
24
 
25
25
  module Trenni::BuilderSpec
26
- describe 'Trenni::Builder#tag_attributes' do
26
+ describe 'Trenni::Builder#tag' do
27
27
  subject {Trenni::Builder.new}
28
28
 
29
29
  it "should format nested attributes" do
@@ -115,6 +115,11 @@ RSpec.describe "<foo bar=\"20\" baz>Hello World</foo>" do
115
115
  expect(events[4][1].encoding).to be == subject.encoding
116
116
  expect(events[5][1].encoding).to be == subject.encoding
117
117
  end
118
+
119
+ it "should track entities" do
120
+ expect(events[1][2]).to be_kind_of Trenni::Markup
121
+ expect(events[4][1]).to be_kind_of Trenni::Markup
122
+ end
118
123
  end
119
124
 
120
125
  RSpec.describe "<test><![CDATA[Hello World]]></test>" do
@@ -150,10 +155,25 @@ RSpec.describe "<p attr=\"foo&amp;bar\">&quot;</p>" do
150
155
  let(:template_buffer) {Trenni::Buffer(template_text)}
151
156
  let(:template) {Trenni::MarkupTemplate.new(template_buffer)}
152
157
 
158
+ it "should parse empty attributes" do
159
+ expect(events).to be == [
160
+ [:open_tag_begin, "p", 1],
161
+ [:attribute, "attr", "foo&bar"],
162
+ [:open_tag_end, false],
163
+ [:text, "\""],
164
+ [:close_tag, "p", 30]
165
+ ]
166
+ end
167
+
153
168
  it "generates same output as input" do
154
169
  result = template.to_string(self)
155
170
  expect(result).to be == subject
156
171
  end
172
+
173
+ it "should track entities" do
174
+ expect(events[1][2]).to_not be_kind_of Trenni::Markup
175
+ expect(events[3][1]).to_not be_kind_of Trenni::Markup
176
+ end
157
177
  end
158
178
 
159
179
  RSpec.shared_examples "valid markup file" do |base|
@@ -0,0 +1,26 @@
1
+
2
+ require 'benchmark/ips'
3
+ require 'trenni/markup'
4
+
5
+ RSpec.describe Trenni::Markup do
6
+ let(:code_string) {'javascript:if (foo < bar) {alert("Hello World")}'}
7
+ let(:general_string) {"a" * code_string.size}
8
+
9
+ it "should be fast to parse large documents" do
10
+ Benchmark.ips do |x|
11
+ x.report("General String") do |times|
12
+ while (times -= 1) >= 0
13
+ Trenni::Markup.escape_string(general_string)
14
+ end
15
+ end
16
+
17
+ x.report("Code String") do |times|
18
+ while (times -= 1) >= 0
19
+ Trenni::Markup.escape_string(code_string)
20
+ end
21
+ end
22
+
23
+ x.compare!
24
+ end
25
+ end
26
+ end
@@ -43,6 +43,8 @@ RSpec.describe Trenni::MarkupString do
43
43
  end
44
44
 
45
45
  it "should convert nil to empty string" do
46
- expect(Trenni::MarkupString(nil)).to be == ""
46
+ Trenni::Markup.append(subject, nil)
47
+
48
+ expect(subject).to be_empty
47
49
  end
48
50
  end