trenni 3.8.0 → 3.9.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/.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
|