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.
@@ -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