trenni 3.8.0 → 3.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +6 -0
- data/ext/trenni/markup.c +85 -85
- data/ext/trenni/markup.rl +11 -11
- data/ext/trenni/query.c +619 -0
- data/ext/trenni/query.h +6 -0
- data/ext/trenni/query.rl +82 -0
- data/ext/trenni/tag.c +8 -6
- data/ext/trenni/template.c +57 -57
- data/ext/trenni/template.rl +4 -4
- data/ext/trenni/trenni.c +9 -1
- data/ext/trenni/trenni.h +8 -3
- data/lib/trenni.rb +2 -0
- data/lib/trenni/{parse_error.rb → error.rb} +4 -1
- data/lib/trenni/fallback/markup.rb +1622 -1576
- data/lib/trenni/fallback/markup.rl +2 -2
- data/lib/trenni/fallback/query.rb +565 -0
- data/lib/trenni/fallback/query.rl +105 -0
- data/lib/trenni/fallback/template.rb +756 -748
- data/lib/trenni/fallback/template.rl +1 -1
- data/lib/trenni/native.rb +1 -1
- data/lib/trenni/parsers.rb +1 -0
- data/lib/trenni/query.rb +94 -0
- data/lib/trenni/reference.rb +125 -0
- data/lib/trenni/strings.rb +15 -4
- data/lib/trenni/uri.rb +1 -0
- data/lib/trenni/version.rb +1 -1
- data/parsers/trenni/query.rl +23 -0
- data/spec/trenni/markup_performance_spec.rb +14 -2
- data/spec/trenni/parsers_performance_spec.rb +31 -0
- data/spec/trenni/query_spec.rb +51 -0
- data/spec/trenni/reference_spec.rb +87 -0
- data/trenni.gemspec +2 -0
- metadata +32 -5
- data/.simplecov +0 -9
data/lib/trenni/native.rb
CHANGED
@@ -20,7 +20,7 @@
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
21
|
# THE SOFTWARE.
|
22
22
|
|
23
|
-
require_relative '
|
23
|
+
require_relative 'error'
|
24
24
|
|
25
25
|
# Methods on the following classes may be replaced by native implementations:
|
26
26
|
require_relative 'tag'
|
data/lib/trenni/parsers.rb
CHANGED
data/lib/trenni/query.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
|
23
|
+
require_relative 'buffer'
|
24
|
+
|
25
|
+
require 'uri'
|
26
|
+
|
27
|
+
module Trenni
|
28
|
+
class Query < Hash
|
29
|
+
def parse(buffer)
|
30
|
+
Parsers.parse_query(buffer, Delegate.new(self))
|
31
|
+
end
|
32
|
+
|
33
|
+
class Delegate
|
34
|
+
def initialize(top = {})
|
35
|
+
@top = top
|
36
|
+
|
37
|
+
@current = @top
|
38
|
+
@index = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def string(key, encoded)
|
42
|
+
if encoded
|
43
|
+
key = ::URI.decode_www_form_component(key)
|
44
|
+
end
|
45
|
+
|
46
|
+
index(key.to_sym)
|
47
|
+
end
|
48
|
+
|
49
|
+
def integer(key)
|
50
|
+
index(key.to_i)
|
51
|
+
end
|
52
|
+
|
53
|
+
def index(key)
|
54
|
+
if @index
|
55
|
+
@current = @current.fetch(@index) do
|
56
|
+
@current[@index] = {}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@index = key
|
61
|
+
end
|
62
|
+
|
63
|
+
def append
|
64
|
+
if @index
|
65
|
+
@current = @current.fetch(@index) do
|
66
|
+
@current[@index] = []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@index = @current.size
|
71
|
+
end
|
72
|
+
|
73
|
+
def assign(value, encoded)
|
74
|
+
if encoded
|
75
|
+
value = ::URI.decode_www_form_component(value)
|
76
|
+
end
|
77
|
+
|
78
|
+
@current[@index] = value
|
79
|
+
|
80
|
+
@current = @top
|
81
|
+
@index = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def pair
|
85
|
+
if @index
|
86
|
+
@current[@index] = true
|
87
|
+
end
|
88
|
+
|
89
|
+
@current = @top
|
90
|
+
@index = nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
|
23
|
+
require_relative 'query'
|
24
|
+
|
25
|
+
module Trenni
|
26
|
+
class Reference
|
27
|
+
def initialize(path, query = {}, fragment: nil)
|
28
|
+
@path = path.to_s
|
29
|
+
@query = query
|
30
|
+
@fragment = fragment
|
31
|
+
end
|
32
|
+
|
33
|
+
# The path component of the URI, e.g. /foo/bar/index.html
|
34
|
+
attr :path
|
35
|
+
|
36
|
+
# The query parameters.
|
37
|
+
attr :query
|
38
|
+
|
39
|
+
# A fragment identifier, the part after the '#'
|
40
|
+
attr :fragment
|
41
|
+
|
42
|
+
def append(buffer)
|
43
|
+
buffer << escape_path(@path)
|
44
|
+
|
45
|
+
unless @query.empty?
|
46
|
+
buffer << '?' << query_string
|
47
|
+
end
|
48
|
+
|
49
|
+
if @fragment
|
50
|
+
buffer << '#' << escape(@fragment)
|
51
|
+
end
|
52
|
+
|
53
|
+
return buffer
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_str
|
57
|
+
append(String.new)
|
58
|
+
end
|
59
|
+
|
60
|
+
alias to_s to_str
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar.
|
65
|
+
NON_PCHAR = /([^
|
66
|
+
a-zA-Z0-9
|
67
|
+
\-\._~
|
68
|
+
!\$&'\(\)\*\+,;=
|
69
|
+
:@\/
|
70
|
+
]+)/x.freeze
|
71
|
+
|
72
|
+
def escape_path(path)
|
73
|
+
encoding = path.encoding
|
74
|
+
path.b.gsub(NON_PCHAR) do |m|
|
75
|
+
'%' + m.unpack('H2' * m.bytesize).join('%').upcase
|
76
|
+
end.force_encoding(encoding)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Escapes a generic string, using percent encoding.
|
80
|
+
def escape(string)
|
81
|
+
encoding = string.encoding
|
82
|
+
string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
|
83
|
+
'%' + m.unpack('H2' * m.bytesize).join('%').upcase
|
84
|
+
end.force_encoding(encoding)
|
85
|
+
end
|
86
|
+
|
87
|
+
def query_string
|
88
|
+
build_nested_query(@query)
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_nested_query(value, prefix = nil)
|
92
|
+
case value
|
93
|
+
when Array
|
94
|
+
value.map { |v|
|
95
|
+
build_nested_query(v, "#{prefix}[]")
|
96
|
+
}.join("&")
|
97
|
+
when Hash
|
98
|
+
value.map { |k, v|
|
99
|
+
build_nested_query(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
|
100
|
+
}.reject(&:empty?).join('&')
|
101
|
+
when nil
|
102
|
+
prefix
|
103
|
+
else
|
104
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
105
|
+
"#{prefix}=#{escape(value.to_s)}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Generate a URI from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
|
111
|
+
def self.Reference(path = '', **parameters)
|
112
|
+
base, fragment = path.split('#', 2)
|
113
|
+
path, query_string = base.split('?', 2)
|
114
|
+
|
115
|
+
query = Query.new
|
116
|
+
|
117
|
+
if query_string
|
118
|
+
query.parse(Buffer.new(query_string))
|
119
|
+
end
|
120
|
+
|
121
|
+
query.update(parameters)
|
122
|
+
|
123
|
+
Reference.new(path, query, fragment: fragment)
|
124
|
+
end
|
125
|
+
end
|
data/lib/trenni/strings.rb
CHANGED
@@ -30,7 +30,11 @@ module Trenni
|
|
30
30
|
end
|
31
31
|
|
32
32
|
def self.to_quoted_string(string)
|
33
|
-
|
33
|
+
string = string.gsub('"', '\\"')
|
34
|
+
string.gsub!(/\r/, "\\r")
|
35
|
+
string.gsub!(/\n/, "\\n")
|
36
|
+
|
37
|
+
return "\"#{string}\""
|
34
38
|
end
|
35
39
|
|
36
40
|
# `value` must already be escaped.
|
@@ -43,11 +47,18 @@ module Trenni
|
|
43
47
|
end
|
44
48
|
|
45
49
|
def self.to_title(string)
|
46
|
-
string.gsub(/(^|[ \-_])(.)/){" " + $2.upcase}
|
50
|
+
string = string.gsub(/(^|[ \-_])(.)/){" " + $2.upcase}
|
51
|
+
string.strip!
|
52
|
+
|
53
|
+
return string
|
47
54
|
end
|
48
|
-
|
55
|
+
|
49
56
|
def self.to_snake(string)
|
50
|
-
string.gsub("::", "")
|
57
|
+
string = string.gsub("::", "")
|
58
|
+
string.gsub!(/([A-Z]+)/){"_" + $1.downcase}
|
59
|
+
string.sub!(/^_+/, "")
|
60
|
+
|
61
|
+
return string
|
51
62
|
end
|
52
63
|
end
|
53
64
|
end
|
data/lib/trenni/uri.rb
CHANGED
data/lib/trenni/version.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
%%{
|
2
|
+
machine query;
|
3
|
+
|
4
|
+
# An application/x-www-form-urlencoded parser based on the definition by WhatWG.
|
5
|
+
# Based on https://url.spec.whatwg.org/#application/x-www-form-urlencoded
|
6
|
+
pchar = any - [&=\[\]%+];
|
7
|
+
echar = pchar | ('+' | '%' xdigit xdigit) >encoded;
|
8
|
+
|
9
|
+
integer = ([0-9]+) >integer_begin %integer_end;
|
10
|
+
string = (echar+ - integer) >string_begin %string_end;
|
11
|
+
|
12
|
+
value = (echar*) >value_begin %value_end;
|
13
|
+
|
14
|
+
index = string (
|
15
|
+
'[' (integer | string) ']'
|
16
|
+
)* ('[]' %append)?;
|
17
|
+
|
18
|
+
pair = (
|
19
|
+
index ('=' value)?
|
20
|
+
) %pair;
|
21
|
+
|
22
|
+
main := ((pair '&')* pair)?;
|
23
|
+
}%%
|
@@ -9,13 +9,25 @@ RSpec.describe Trenni::Markup do
|
|
9
9
|
|
10
10
|
it "should be fast to parse large documents" do
|
11
11
|
Benchmark.ips do |x|
|
12
|
-
x.report("
|
12
|
+
x.report("CGI.escapeHTML(general_string)") do |times|
|
13
|
+
while (times -= 1) >= 0
|
14
|
+
CGI.escapeHTML(general_string)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
x.report("CGI.escapeHTML(code_string)") do |times|
|
19
|
+
while (times -= 1) >= 0
|
20
|
+
CGI.escapeHTML(code_string)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
x.report("Trenni::Markup.escape_string(general_string)") do |times|
|
13
25
|
while (times -= 1) >= 0
|
14
26
|
Trenni::Markup.escape_string(general_string)
|
15
27
|
end
|
16
28
|
end
|
17
29
|
|
18
|
-
x.report("
|
30
|
+
x.report("Trenni::Markup.escape_string(code_string)") do |times|
|
19
31
|
while (times -= 1) >= 0
|
20
32
|
Trenni::Markup.escape_string(code_string)
|
21
33
|
end
|
@@ -4,6 +4,9 @@ require 'benchmark/ips'
|
|
4
4
|
require 'trenni/parsers'
|
5
5
|
require 'trenni/entities'
|
6
6
|
|
7
|
+
require 'trenni/query'
|
8
|
+
require 'rack/utils'
|
9
|
+
|
7
10
|
require 'nokogiri'
|
8
11
|
|
9
12
|
RSpec.describe Trenni::Parsers do
|
@@ -71,4 +74,32 @@ RSpec.describe Trenni::Parsers do
|
|
71
74
|
end
|
72
75
|
end
|
73
76
|
end
|
77
|
+
|
78
|
+
describe '#parse_query' do
|
79
|
+
let(:string) {"foo=hi%20there&bar[blah]=123&bar[quux][0]=1&bar[quux][1]=2&bar[quux][2]=3"}
|
80
|
+
|
81
|
+
it "should be fast to parse large query strings" do
|
82
|
+
# query = Trenni::Query.new
|
83
|
+
# query.parse(Trenni::Buffer.new string)
|
84
|
+
# pp query
|
85
|
+
#
|
86
|
+
# pp Rack::Utils.parse_nested_query(string)
|
87
|
+
|
88
|
+
Benchmark.ips do |x|
|
89
|
+
x.report("Large (Trenni)") do |times|
|
90
|
+
while (times -= 1) >= 0
|
91
|
+
Trenni::Query.new.parse(Trenni::Buffer.new string)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
x.report("Large (Rack)") do |times|
|
96
|
+
while (times -= 1) >= 0
|
97
|
+
Rack::Utils.parse_nested_query(string)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
x.compare!
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
74
105
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
|
23
|
+
require 'trenni/query'
|
24
|
+
|
25
|
+
RSpec.describe Trenni::Query do
|
26
|
+
def parse(string)
|
27
|
+
subject.parse(Trenni::Buffer.new(string))
|
28
|
+
|
29
|
+
return subject
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can parse query string with integer key" do
|
33
|
+
expect(parse "q[0]=0").to be == {q: {0 => "0"}}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "can parse query string with mixed integer/string key" do
|
37
|
+
expect(parse "q[2d]=3d").to be == {q: {:'2d' => "3d"}}
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can parse query string appending items to array" do
|
41
|
+
expect(parse "q[]=a&q[]=b").to be == {q: ["a", "b"]}
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can decode encoded keys" do
|
45
|
+
expect(parse "hello+world=true").to be == {:"hello world" => "true"}
|
46
|
+
end
|
47
|
+
|
48
|
+
it "can decode encoded values" do
|
49
|
+
expect(parse "message=hello+world").to be == {message: "hello world"}
|
50
|
+
end
|
51
|
+
end
|