trenni 3.8.0 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -67,7 +67,7 @@
67
67
  include template "trenni/template.rl";
68
68
  }%%
69
69
 
70
- require_relative '../parse_error'
70
+ require_relative '../error'
71
71
 
72
72
  module Trenni
73
73
  module Fallback
@@ -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 'parse_error'
23
+ require_relative 'error'
24
24
 
25
25
  # Methods on the following classes may be replaced by native implementations:
26
26
  require_relative 'tag'
@@ -8,6 +8,7 @@ if defined? Trenni::Native
8
8
  else
9
9
  require_relative 'fallback/markup'
10
10
  require_relative 'fallback/template'
11
+ require_relative 'fallback/query'
11
12
 
12
13
  Trenni::Parsers = Trenni::Fallback
13
14
  end
@@ -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
@@ -30,7 +30,11 @@ module Trenni
30
30
  end
31
31
 
32
32
  def self.to_quoted_string(string)
33
- '"' + string.gsub('"', '\\"').gsub(/\r/, "\\r").gsub(/\n/, "\\n") + '"'
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}.strip
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("::", "").gsub(/([A-Z]+)/){"_" + $1.downcase}.sub(/^_+/, "")
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
@@ -21,6 +21,7 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Trenni
24
+ # This class is superceeded by `Trenni::Reference`.
24
25
  class URI
25
26
  def initialize(path, query_string, fragment, parameters)
26
27
  @path = path
@@ -21,5 +21,5 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Trenni
24
- VERSION = "3.8.0"
24
+ VERSION = "3.9.0"
25
25
  end
@@ -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("General String") do |times|
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("Code String") do |times|
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