janeway-jsonpath 0.6.0 → 1.1.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/README.md +1 -1
- data/lib/janeway/ast/array_slice_selector.rb +6 -90
- data/lib/janeway/ast/binary_operator.rb +57 -51
- data/lib/janeway/ast/child_segment.rb +3 -5
- data/lib/janeway/ast/current_node.rb +2 -15
- data/lib/janeway/ast/descendant_segment.rb +0 -3
- data/lib/janeway/ast/expression.rb +35 -5
- data/lib/janeway/ast/function.rb +15 -10
- data/lib/janeway/ast/name_selector.rb +12 -7
- data/lib/janeway/ast/root_node.rb +2 -15
- data/lib/janeway/ast/selector.rb +5 -6
- data/lib/janeway/ast/unary_operator.rb +12 -2
- data/lib/janeway/ast/wildcard_selector.rb +2 -2
- data/lib/janeway/enumerator.rb +14 -7
- data/lib/janeway/error.rb +6 -1
- data/lib/janeway/functions/count.rb +1 -8
- data/lib/janeway/functions/length.rb +1 -12
- data/lib/janeway/functions/match.rb +3 -8
- data/lib/janeway/functions/search.rb +3 -8
- data/lib/janeway/functions/value.rb +1 -7
- data/lib/janeway/functions.rb +75 -2
- data/lib/janeway/interpreter.rb +50 -35
- data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +10 -13
- data/lib/janeway/interpreters/array_slice_selector_interpreter.rb +56 -13
- data/lib/janeway/interpreters/binary_operator_interpreter.rb +18 -13
- data/lib/janeway/interpreters/descendant_segment_interpreter.rb +32 -13
- data/lib/janeway/interpreters/filter_selector_interpreter.rb +26 -34
- data/lib/janeway/interpreters/function_interpreter.rb +5 -1
- data/lib/janeway/interpreters/index_selector_delete_if.rb +1 -0
- data/lib/janeway/interpreters/index_selector_interpreter.rb +3 -1
- data/lib/janeway/interpreters/iteration_helper.rb +19 -5
- data/lib/janeway/interpreters/root_node_delete_if.rb +57 -16
- data/lib/janeway/interpreters/tree_constructor.rb +0 -20
- data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +16 -0
- data/lib/janeway/interpreters/yielder.rb +8 -2
- data/lib/janeway/lexer.rb +66 -77
- data/lib/janeway/location.rb +3 -1
- data/lib/janeway/normalized_path.rb +23 -7
- data/lib/janeway/parser.rb +46 -34
- data/lib/janeway/query.rb +48 -5
- data/lib/janeway/token.rb +2 -5
- data/lib/janeway/version.rb +1 -1
- data/lib/janeway.rb +16 -3
- metadata +3 -10
- data/lib/janeway/interpreters/array_slice_selector_deleter.rb +0 -41
- data/lib/janeway/interpreters/child_segment_deleter.rb +0 -19
- data/lib/janeway/interpreters/filter_selector_deleter.rb +0 -65
- data/lib/janeway/interpreters/index_selector_deleter.rb +0 -26
- data/lib/janeway/interpreters/name_selector_deleter.rb +0 -27
- data/lib/janeway/interpreters/root_node_deleter.rb +0 -34
- data/lib/janeway/interpreters/wildcard_selector_deleter.rb +0 -32
|
@@ -22,6 +22,10 @@ module Janeway
|
|
|
22
22
|
def initialize(function)
|
|
23
23
|
super
|
|
24
24
|
@params = function.parameters.map { |param| TreeConstructor.ast_node_to_interpreter(param) }
|
|
25
|
+
# Build the function body from the registry. This is the point at
|
|
26
|
+
# which per-instance specialization happens — e.g. match/search
|
|
27
|
+
# compile a literal regex once, capturing it in the closure.
|
|
28
|
+
@body = Functions::REGISTRY.fetch(function.name)[:build].call(function.parameters)
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
# @param input [Array, Hash] the results of processing so far
|
|
@@ -30,7 +34,7 @@ module Janeway
|
|
|
30
34
|
# @param _path [Array<String>] elements of normalized path to the current input
|
|
31
35
|
def interpret(input, _parent, root, _path)
|
|
32
36
|
params = interpret_function_parameters(@params, input, root)
|
|
33
|
-
|
|
37
|
+
@body.call(*params)
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
# Evaluate the expressions in the parameter list to make the parameter values
|
|
@@ -28,6 +28,7 @@ module Janeway
|
|
|
28
28
|
|
|
29
29
|
index = selector.value
|
|
30
30
|
result = input.fetch(index) # raises IndexError if no such index
|
|
31
|
+
index += input.size if index.negative? # yield positive index for the normalize path
|
|
31
32
|
return unless @yield_proc.call(input[index], input, path + [index])
|
|
32
33
|
|
|
33
34
|
input.delete_at(index) # returns nil if deleted value is nil, or if no value was deleted
|
|
@@ -31,7 +31,9 @@ module Janeway
|
|
|
31
31
|
result = input.fetch(selector.value) # raises IndexError if no such index
|
|
32
32
|
return [result] unless @next
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
index = selector.value
|
|
35
|
+
index += input.size if index.negative?
|
|
36
|
+
@next.interpret(result, input, root, path + [index])
|
|
35
37
|
rescue IndexError
|
|
36
38
|
[]
|
|
37
39
|
end
|
|
@@ -6,21 +6,28 @@ module Janeway
|
|
|
6
6
|
module Interpreters
|
|
7
7
|
# Mixin for interpreter classes that yield to a block
|
|
8
8
|
module IterationHelper
|
|
9
|
+
# Sentinel block used by Interpreter#make_deleter to turn a DeleteIf
|
|
10
|
+
# into an unconditional deleter. `equal?`-comparable so future fast
|
|
11
|
+
# paths (bulk clear) can detect it.
|
|
12
|
+
PASS_ALL = proc { true }.freeze
|
|
9
13
|
# Returns a Proc that yields the correct number of parameters to a block
|
|
10
14
|
#
|
|
11
15
|
# block.arity is -1 when no block is given, and an enumerator is being returned
|
|
12
16
|
# @return [Proc] which takes 3 parameters
|
|
13
17
|
def make_yield_proc(&block)
|
|
18
|
+
# Bind the block locally so the returned proc captures a local, not the
|
|
19
|
+
# containing instance's @block ivar (avoids an ivar read per yield).
|
|
20
|
+
b = block
|
|
14
21
|
if block.arity.negative?
|
|
15
22
|
# Yield just the value to an enumerator, to enable instance method calls on
|
|
16
23
|
# matched values like this: enum.delete_if(&:even?)
|
|
17
|
-
proc { |value, _parent, _path|
|
|
24
|
+
proc { |value, _parent, _path| b.call(value) }
|
|
18
25
|
elsif block.arity > 3
|
|
19
26
|
# Only do the work of constructing the normalized path when it is actually used
|
|
20
|
-
proc { |value, parent, path|
|
|
27
|
+
proc { |value, parent, path| b.call(value, parent, path.last, normalized_path(path)) }
|
|
21
28
|
else
|
|
22
29
|
# block arity is 1, 2 or 3. Send all 3 parameters regardless.
|
|
23
|
-
proc { |value, parent, path|
|
|
30
|
+
proc { |value, parent, path| b.call(value, parent, path.last) }
|
|
24
31
|
end
|
|
25
32
|
end
|
|
26
33
|
|
|
@@ -37,8 +44,15 @@ module Janeway
|
|
|
37
44
|
def normalized_path(components)
|
|
38
45
|
# First component is the root identifer, the remaining components are
|
|
39
46
|
# all index selectors or name selectors.
|
|
40
|
-
#
|
|
41
|
-
|
|
47
|
+
# Build the result string in a single pass — avoids `components[1..]`
|
|
48
|
+
# (slice allocation) plus `.map { ... }.join` (intermediate Array).
|
|
49
|
+
result = +'$'
|
|
50
|
+
i = 1
|
|
51
|
+
while i < components.size
|
|
52
|
+
result << NormalizedPath.normalize(components[i])
|
|
53
|
+
i += 1
|
|
54
|
+
end
|
|
55
|
+
result
|
|
42
56
|
end
|
|
43
57
|
end
|
|
44
58
|
end
|
|
@@ -1,33 +1,74 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'root_node_interpreter'
|
|
4
|
+
require_relative 'iteration_helper'
|
|
4
5
|
|
|
5
6
|
module Janeway
|
|
6
7
|
module Interpreters
|
|
7
|
-
#
|
|
8
|
-
class
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
8
|
+
# Delete values from the root node for which the block returns a truthy value.
|
|
9
|
+
class RootNodeDeleteIf < RootNodeInterpreter
|
|
10
|
+
include IterationHelper
|
|
11
|
+
|
|
12
|
+
# @param node [AST::RootNode]
|
|
13
|
+
def initialize(node, &block)
|
|
14
|
+
super(node)
|
|
15
|
+
@block = block
|
|
16
|
+
|
|
17
|
+
# Make a proc that yields the correct number of values to a block
|
|
18
|
+
@yield_proc = make_yield_proc(&block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Delete values from the root container for which the block returns truthy.
|
|
12
22
|
#
|
|
13
23
|
# @param _input [Array, Hash] the results of processing so far
|
|
14
24
|
# @param _parent [Array, Hash] parent of the input object
|
|
15
25
|
# @param root [Array, Hash] the entire input
|
|
16
26
|
# @param _path [Array<String>] elements of normalized path to the current input
|
|
17
27
|
# @return [Array] deleted elements
|
|
18
|
-
def interpret(_input, _parent, root, _path)
|
|
28
|
+
def interpret(_input, _parent, root, _path = nil)
|
|
19
29
|
case root
|
|
20
|
-
when Array
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
when Array then maybe_delete_array_values(root)
|
|
31
|
+
when Hash then maybe_delete_hash_values(root)
|
|
32
|
+
else []
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# @param input [Array]
|
|
39
|
+
def maybe_delete_array_values(input)
|
|
40
|
+
# Fast path: unconditional delete (from Interpreter#make_deleter).
|
|
41
|
+
if @block.equal?(IterationHelper::PASS_ALL)
|
|
42
|
+
results = input.dup
|
|
43
|
+
input.clear
|
|
44
|
+
return results
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
results = []
|
|
48
|
+
(input.size - 1).downto(0).each do |i|
|
|
49
|
+
next unless @yield_proc.call(input[i], input, ['$', i])
|
|
50
|
+
|
|
51
|
+
results << input.delete_at(i)
|
|
52
|
+
end
|
|
53
|
+
results.reverse
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @param input [Hash]
|
|
57
|
+
def maybe_delete_hash_values(input)
|
|
58
|
+
# Fast path: unconditional delete (from Interpreter#make_deleter).
|
|
59
|
+
if @block.equal?(IterationHelper::PASS_ALL)
|
|
60
|
+
results = input.values
|
|
61
|
+
input.clear
|
|
62
|
+
return results
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
results = []
|
|
66
|
+
input.each do |key, value|
|
|
67
|
+
next unless @yield_proc.call(value, input, ['$', key])
|
|
68
|
+
|
|
69
|
+
results << input.delete(key)
|
|
30
70
|
end
|
|
71
|
+
results
|
|
31
72
|
end
|
|
32
73
|
end
|
|
33
74
|
end
|
|
@@ -44,26 +44,6 @@ module Janeway
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Return a Deleter interpreter for the given AST node.
|
|
48
|
-
# This interpreter deletes matched values.
|
|
49
|
-
#
|
|
50
|
-
# @param expr [AST::Expression]
|
|
51
|
-
# @return [Interprteters::Base]
|
|
52
|
-
def self.ast_node_to_deleter(expr)
|
|
53
|
-
case expr
|
|
54
|
-
when AST::IndexSelector then IndexSelectorDeleter.new(expr)
|
|
55
|
-
when AST::ArraySliceSelector then ArraySliceSelectorDeleter.new(expr)
|
|
56
|
-
when AST::NameSelector then NameSelectorDeleter.new(expr)
|
|
57
|
-
when AST::FilterSelector then FilterSelectorDeleter.new(expr)
|
|
58
|
-
when AST::WildcardSelector then WildcardSelectorDeleter.new(expr)
|
|
59
|
-
when AST::ChildSegment then ChildSegmentDeleter.new(expr)
|
|
60
|
-
when AST::RootNode then RootNodeDeleter.new(expr)
|
|
61
|
-
when nil then nil # caller has no @next node
|
|
62
|
-
else
|
|
63
|
-
raise "Unknown AST expression: #{expr.inspect}"
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
47
|
# Return a DeleteIf interpreter for the given AST node.
|
|
68
48
|
# This interpreter deletes matched values, but only after
|
|
69
49
|
# yielding to a block that returns a truthy value.
|
|
@@ -36,6 +36,15 @@ module Janeway
|
|
|
36
36
|
# @param input [Array]
|
|
37
37
|
# @param path [Array]
|
|
38
38
|
def maybe_delete_array_values(input, path)
|
|
39
|
+
# Fast path: unconditional delete (from Interpreter#make_deleter).
|
|
40
|
+
# Bulk clear is O(n) with much smaller constants than reverse-iterate
|
|
41
|
+
# + delete_at + per-element proc call.
|
|
42
|
+
if @block.equal?(IterationHelper::PASS_ALL)
|
|
43
|
+
results = input.dup
|
|
44
|
+
input.clear
|
|
45
|
+
return results
|
|
46
|
+
end
|
|
47
|
+
|
|
39
48
|
results = []
|
|
40
49
|
(input.size - 1).downto(0).each do |i|
|
|
41
50
|
next unless @yield_proc.yield(input[i], input, path + [i])
|
|
@@ -48,6 +57,13 @@ module Janeway
|
|
|
48
57
|
# @param input [Hash]
|
|
49
58
|
# @param path [Array]
|
|
50
59
|
def maybe_delete_hash_values(input, path)
|
|
60
|
+
# Fast path: unconditional delete (from Interpreter#make_deleter).
|
|
61
|
+
if @block.equal?(IterationHelper::PASS_ALL)
|
|
62
|
+
results = input.values
|
|
63
|
+
input.clear
|
|
64
|
+
return results
|
|
65
|
+
end
|
|
66
|
+
|
|
51
67
|
results = []
|
|
52
68
|
input.each do |key, value|
|
|
53
69
|
next unless @yield_proc.yield(value, input, path + [key])
|
|
@@ -19,6 +19,12 @@ module Janeway
|
|
|
19
19
|
@yield_proc = make_yield_proc(&block)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Frozen empty array — Yielder is only used by Enumerator#each and
|
|
23
|
+
# #find_paths, both of which discard the traversal's return value
|
|
24
|
+
# (results are surfaced via yield instead). Returning a shared frozen
|
|
25
|
+
# array avoids allocating a wrapper array per leaf.
|
|
26
|
+
EMPTY_RESULT = [].freeze
|
|
27
|
+
|
|
22
28
|
# Yield each input value
|
|
23
29
|
#
|
|
24
30
|
# @param input [Array, Hash] the results of processing so far
|
|
@@ -26,10 +32,10 @@ module Janeway
|
|
|
26
32
|
# @param _root [Array, Hash] the entire input
|
|
27
33
|
# @param path [Array<String, Integer>] components of normalized path to the current input
|
|
28
34
|
# @yieldparam [Object] matched value
|
|
29
|
-
# @return [
|
|
35
|
+
# @return [Array] frozen empty array — see EMPTY_RESULT
|
|
30
36
|
def interpret(input, parent, _root, path)
|
|
31
37
|
@yield_proc.call(input, parent, path)
|
|
32
|
-
|
|
38
|
+
EMPTY_RESULT
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
# Dummy method from Interpreters::Base, allow child segment interpreter to disable the
|
data/lib/janeway/lexer.rb
CHANGED
|
@@ -38,9 +38,23 @@ module Janeway
|
|
|
38
38
|
TWO_CHAR_LEX_FIRST = TWO_CHAR_LEX.map { |lexeme| lexeme[0] }.freeze
|
|
39
39
|
ONE_OR_TWO_CHAR_LEX = ONE_CHAR_LEX & TWO_CHAR_LEX.map { |str| str[0] }.freeze
|
|
40
40
|
|
|
41
|
+
# O(1)-lookup companions to the arrays above. Hash#include? and the reverse
|
|
42
|
+
# lookup avoid the linear scan that .include? / .key would do per token.
|
|
43
|
+
LEXEME_TO_TYPE = OPERATORS.invert.freeze
|
|
44
|
+
ONE_CHAR_LEX_SET = ONE_CHAR_LEX.each_with_object({}) { |c, h| h[c] = true }.freeze
|
|
45
|
+
TWO_CHAR_LEX_SET = TWO_CHAR_LEX.each_with_object({}) { |c, h| h[c] = true }.freeze
|
|
46
|
+
TWO_CHAR_LEX_FIRST_SET = TWO_CHAR_LEX_FIRST.each_with_object({}) { |c, h| h[c] = true }.freeze
|
|
47
|
+
ONE_OR_TWO_CHAR_LEX_SET = ONE_OR_TWO_CHAR_LEX.each_with_object({}) { |c, h| h[c] = true }.freeze
|
|
48
|
+
|
|
41
49
|
WHITESPACE = " \t\n\r"
|
|
42
50
|
KEYWORD = %w[true false null].freeze
|
|
51
|
+
KEYWORD_SET = KEYWORD.each_with_object({}) { |k, h| h[k] = true }.freeze
|
|
43
52
|
FUNCTIONS = %w[length count match search value].freeze
|
|
53
|
+
FUNCTIONS_SET = FUNCTIONS.each_with_object({}) { |k, h| h[k] = true }.freeze
|
|
54
|
+
|
|
55
|
+
# Hoisted from lex_delimited_string. Given a delimiter, the value is the "other" delimiter.
|
|
56
|
+
OTHER_DELIMITER = { "'" => '"', '"' => "'" }.freeze
|
|
57
|
+
QUOTE_LEX_SET = { "'" => true, '"' => true }.freeze
|
|
44
58
|
|
|
45
59
|
# faster to check membership in a string than an array of char (benchmarked ruby 3.1.2)
|
|
46
60
|
ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
@@ -72,6 +86,7 @@ module Janeway
|
|
|
72
86
|
end
|
|
73
87
|
|
|
74
88
|
def start_tokenization
|
|
89
|
+
raise err('JSONPath query is empty') if @source.empty?
|
|
75
90
|
if WHITESPACE.include?(@source[0]) || WHITESPACE.include?(@source[-1])
|
|
76
91
|
raise err('JSONPath query may not start or end with whitespace')
|
|
77
92
|
end
|
|
@@ -88,18 +103,18 @@ module Janeway
|
|
|
88
103
|
return if WHITESPACE.include?(c)
|
|
89
104
|
|
|
90
105
|
token =
|
|
91
|
-
if
|
|
106
|
+
if ONE_OR_TWO_CHAR_LEX_SET.include?(c)
|
|
92
107
|
token_from_one_or_two_char_lex(c)
|
|
93
|
-
elsif
|
|
108
|
+
elsif ONE_CHAR_LEX_SET.include?(c)
|
|
94
109
|
token_from_one_char_lex(c)
|
|
95
|
-
elsif
|
|
110
|
+
elsif TWO_CHAR_LEX_FIRST_SET.include?(c)
|
|
96
111
|
token_from_two_char_lex(c)
|
|
97
|
-
elsif
|
|
112
|
+
elsif QUOTE_LEX_SET.include?(c)
|
|
98
113
|
lex_delimited_string(c)
|
|
99
114
|
elsif digit?(c)
|
|
100
115
|
lex_number
|
|
101
116
|
elsif name_first_char?(c)
|
|
102
|
-
lex_member_name_shorthand(ignore_keywords:
|
|
117
|
+
lex_member_name_shorthand(ignore_keywords: previous_token_type == :dot)
|
|
103
118
|
end
|
|
104
119
|
raise err("Unknown character: #{c.inspect}") unless token
|
|
105
120
|
|
|
@@ -126,20 +141,20 @@ module Janeway
|
|
|
126
141
|
raise err("Operator #{lexeme.inspect} must not be followed by whitespace")
|
|
127
142
|
end
|
|
128
143
|
|
|
129
|
-
Token.new(
|
|
144
|
+
Token.new(LEXEME_TO_TYPE[lexeme], lexeme, nil, current_location)
|
|
130
145
|
end
|
|
131
146
|
|
|
132
147
|
# Consumes an operator that could be either 1 or 2 chars in length
|
|
133
148
|
# @return [Token]
|
|
134
149
|
def token_from_one_or_two_char_lex(lexeme)
|
|
135
|
-
next_two_chars =
|
|
136
|
-
if
|
|
150
|
+
next_two_chars = lexeme + lookahead
|
|
151
|
+
if TWO_CHAR_LEX_SET.include?(next_two_chars)
|
|
137
152
|
consume
|
|
138
153
|
if next_two_chars == '..' && WHITESPACE.include?(lookahead)
|
|
139
154
|
raise err("Operator #{next_two_chars.inspect} must not be followed by whitespace")
|
|
140
155
|
end
|
|
141
156
|
|
|
142
|
-
Token.new(
|
|
157
|
+
Token.new(LEXEME_TO_TYPE[next_two_chars], next_two_chars, nil, current_location)
|
|
143
158
|
else
|
|
144
159
|
token_from_one_char_lex(lexeme)
|
|
145
160
|
end
|
|
@@ -148,11 +163,11 @@ module Janeway
|
|
|
148
163
|
# Consumes a 2 char operator
|
|
149
164
|
# @return [Token]
|
|
150
165
|
def token_from_two_char_lex(lexeme)
|
|
151
|
-
next_two_chars =
|
|
152
|
-
raise err("Unknown operator \"#{lexeme}\"") unless
|
|
166
|
+
next_two_chars = lexeme + lookahead
|
|
167
|
+
raise err("Unknown operator \"#{lexeme}\"") unless TWO_CHAR_LEX_SET.include?(next_two_chars)
|
|
153
168
|
|
|
154
169
|
consume
|
|
155
|
-
Token.new(
|
|
170
|
+
Token.new(LEXEME_TO_TYPE[next_two_chars], next_two_chars, nil, current_location)
|
|
156
171
|
end
|
|
157
172
|
|
|
158
173
|
def consume
|
|
@@ -165,12 +180,24 @@ module Janeway
|
|
|
165
180
|
consume while digit?(lookahead)
|
|
166
181
|
end
|
|
167
182
|
|
|
183
|
+
# @return [String] source substring for the lexeme currently being built
|
|
184
|
+
def current_lexeme
|
|
185
|
+
source[lexeme_start_p..(next_p - 1)]
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Type of the most-recently-emitted token, or nil if none.
|
|
189
|
+
# Named lookback for the two context-sensitive lexing decisions
|
|
190
|
+
# (identifier-in-brackets rejection; keyword ignore after `.`).
|
|
191
|
+
# @return [Symbol, nil]
|
|
192
|
+
def previous_token_type
|
|
193
|
+
@tokens.last&.type
|
|
194
|
+
end
|
|
195
|
+
|
|
168
196
|
# @param delimiter [String] eg. ' or "
|
|
169
197
|
# @return [Token] string token
|
|
170
198
|
def lex_delimited_string(delimiter)
|
|
171
|
-
allowed_delimiters = %w[' "]
|
|
172
199
|
# the "other" delimiter char, which is not currently being treated as a delimiter
|
|
173
|
-
non_delimiter =
|
|
200
|
+
non_delimiter = OTHER_DELIMITER[delimiter]
|
|
174
201
|
|
|
175
202
|
literal_chars = []
|
|
176
203
|
while lookahead != delimiter && source_uncompleted?
|
|
@@ -189,7 +216,7 @@ module Janeway
|
|
|
189
216
|
end
|
|
190
217
|
elsif unescaped?(next_char)
|
|
191
218
|
consume
|
|
192
|
-
elsif
|
|
219
|
+
elsif QUOTE_LEX_SET.include?(next_char) && next_char != delimiter
|
|
193
220
|
consume
|
|
194
221
|
else
|
|
195
222
|
raise err("invalid character #{next_char.inspect}")
|
|
@@ -203,7 +230,7 @@ module Janeway
|
|
|
203
230
|
literal = literal_chars.join
|
|
204
231
|
|
|
205
232
|
# lexeme value includes delimiters and literal escape characters
|
|
206
|
-
lexeme =
|
|
233
|
+
lexeme = current_lexeme
|
|
207
234
|
|
|
208
235
|
Token.new(:string, lexeme, literal, current_location)
|
|
209
236
|
end
|
|
@@ -284,10 +311,11 @@ module Janeway
|
|
|
284
311
|
# @param low_surrogate_hex [String] string of hex digits, eg. "DE09"
|
|
285
312
|
# @return [String] UTF-8 string containing a single multi-byte unicode character, eg. "😉"
|
|
286
313
|
def convert_surrogate_pair_to_codepoint(high_surrogate_hex, low_surrogate_hex)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
314
|
+
# Callers guarantee 4-char inputs (consume_four_hex_digits followed by
|
|
315
|
+
# high_surrogate?/low_surrogate? which pre-check size). The old defensive
|
|
316
|
+
# guard here referenced a non-existent `hex_string` local — dead code
|
|
317
|
+
# that would NameError if ever reached.
|
|
318
|
+
#
|
|
291
319
|
# Calculate the code point from the surrogate pair values
|
|
292
320
|
# algorithm from https://russellcottrell.com/greek/utilities/SurrogatePairCalculator.htm
|
|
293
321
|
high = high_surrogate_hex.hex
|
|
@@ -316,20 +344,19 @@ module Janeway
|
|
|
316
344
|
#
|
|
317
345
|
# @return [String]
|
|
318
346
|
def consume_four_hex_digits
|
|
319
|
-
hex_digits =
|
|
347
|
+
hex_digits = String.new(capacity: 4)
|
|
320
348
|
4.times do
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
when
|
|
349
|
+
c = consume
|
|
350
|
+
hex_digits << c
|
|
351
|
+
case c.ord
|
|
352
|
+
when 0x30..0x39 then next # '0'..'9'
|
|
353
|
+
when 0x41..0x46 then next # 'A'..'F'
|
|
325
354
|
when 0x61..0x66 then next # 'a'..'f'
|
|
326
355
|
else
|
|
327
|
-
raise err("Invalid unicode escape sequence: \\u#{hex_digits
|
|
356
|
+
raise err("Invalid unicode escape sequence: \\u#{hex_digits}")
|
|
328
357
|
end
|
|
329
358
|
end
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
hex_digits.join
|
|
359
|
+
hex_digits
|
|
333
360
|
end
|
|
334
361
|
|
|
335
362
|
# Consume a numeric string. May be an integer, fractional, or exponent.
|
|
@@ -352,13 +379,13 @@ module Janeway
|
|
|
352
379
|
consume # "+" / "-"
|
|
353
380
|
end
|
|
354
381
|
unless digit?(lookahead)
|
|
355
|
-
lexeme =
|
|
382
|
+
lexeme = current_lexeme
|
|
356
383
|
raise err("Exponent 'e' must be followed by number: #{lexeme.inspect}")
|
|
357
384
|
end
|
|
358
385
|
consume_digits
|
|
359
386
|
end
|
|
360
387
|
|
|
361
|
-
lexeme =
|
|
388
|
+
lexeme = current_lexeme
|
|
362
389
|
if lexeme.start_with?('0') && lexeme.size > 1
|
|
363
390
|
raise err("Number may not start with leading zero: #{lexeme.inspect}")
|
|
364
391
|
end
|
|
@@ -374,36 +401,6 @@ module Janeway
|
|
|
374
401
|
|
|
375
402
|
# Consume an alphanumeric string.
|
|
376
403
|
# If `ignore_keywords`, the result is always an :identifier token.
|
|
377
|
-
# Otherwise, keywords and function names will be recognized and tokenized as those types.
|
|
378
|
-
#
|
|
379
|
-
# @param ignore_keywords [Boolean]
|
|
380
|
-
def lex_identifier(ignore_keywords: false)
|
|
381
|
-
consume while alpha_numeric?(lookahead)
|
|
382
|
-
|
|
383
|
-
identifier = source[lexeme_start_p..(next_p - 1)]
|
|
384
|
-
type =
|
|
385
|
-
if KEYWORD.include?(identifier) && !ignore_keywords
|
|
386
|
-
identifier.to_sym
|
|
387
|
-
elsif FUNCTIONS.include?(identifier) && !ignore_keywords
|
|
388
|
-
:function
|
|
389
|
-
else
|
|
390
|
-
:identifier
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
Token.new(type, identifier, identifier, current_location)
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Parse an identifier string which is not within delimiters.
|
|
397
|
-
# The standard set of unicode code points are allowed.
|
|
398
|
-
# No character escapes are allowed.
|
|
399
|
-
# Keywords and function names are ignored in this context.
|
|
400
|
-
# @return [Token]
|
|
401
|
-
def lex_unescaped_identifier
|
|
402
|
-
consume while unescaped?(lookahead)
|
|
403
|
-
identifier = source[lexeme_start_p..(next_p - 1)]
|
|
404
|
-
Token.new(:identifier, identifier, identifier, current_location)
|
|
405
|
-
end
|
|
406
|
-
|
|
407
404
|
# Return true if string matches the definition of "unescaped" from RFC9535:
|
|
408
405
|
# unescaped = %x20-21 / ; see RFC 8259
|
|
409
406
|
# ; omit 0x22 "
|
|
@@ -427,19 +424,6 @@ module Janeway
|
|
|
427
424
|
end
|
|
428
425
|
end
|
|
429
426
|
|
|
430
|
-
def escapable?(char)
|
|
431
|
-
case char.ord
|
|
432
|
-
when 0x62 then true # backspace
|
|
433
|
-
when 0x66 then true # form feed
|
|
434
|
-
when 0x6E then true # line feed
|
|
435
|
-
when 0x72 then true # carriage return
|
|
436
|
-
when 0x74 then true # horizontal tab
|
|
437
|
-
when 0x2F then true # slash
|
|
438
|
-
when 0x5C then true # backslash
|
|
439
|
-
else false
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
|
|
443
427
|
# True if character is suitable as the first character in a name selector
|
|
444
428
|
# using shorthand notation (ie. no bracket notation.)
|
|
445
429
|
#
|
|
@@ -470,6 +454,7 @@ module Janeway
|
|
|
470
454
|
end
|
|
471
455
|
|
|
472
456
|
# Lex a member name that is found within dot notation.
|
|
457
|
+
# This name is not delimited and allows a subset of the characters that can appear in a delimited string.
|
|
473
458
|
#
|
|
474
459
|
# Recognize keywords and given them the correct type.
|
|
475
460
|
# @see https://www.rfc-editor.org/rfc/rfc9535.html#section-2.5.1.1-3
|
|
@@ -477,12 +462,16 @@ module Janeway
|
|
|
477
462
|
# @param ignore_keywords [Boolean]
|
|
478
463
|
# @return [Token]
|
|
479
464
|
def lex_member_name_shorthand(ignore_keywords: false)
|
|
465
|
+
# Abort if name is preceded by child_start. Catches non-delimited identifiers in brackets,
|
|
466
|
+
# eg. $["key"] is allowed, but $[key] is not
|
|
467
|
+
raise err('Identifier within brackets must be surrounded by quotes') if previous_token_type == :child_start
|
|
468
|
+
|
|
480
469
|
consume while name_char?(lookahead)
|
|
481
|
-
identifier =
|
|
470
|
+
identifier = current_lexeme
|
|
482
471
|
type =
|
|
483
|
-
if
|
|
472
|
+
if KEYWORD_SET.include?(identifier) && !ignore_keywords
|
|
484
473
|
identifier.to_sym
|
|
485
|
-
elsif
|
|
474
|
+
elsif FUNCTIONS_SET.include?(identifier) && !ignore_keywords
|
|
486
475
|
:function
|
|
487
476
|
else
|
|
488
477
|
:identifier
|
data/lib/janeway/location.rb
CHANGED
|
@@ -11,6 +11,15 @@ module Janeway
|
|
|
11
11
|
# Characters that do not need escaping, defined by hexadecimal range
|
|
12
12
|
NORMAL_UNESCAPED_RANGES = [(0x20..0x26), (0x28..0x5B), (0x5D..0xD7FF), (0xE000..0x10FFFF)].freeze
|
|
13
13
|
|
|
14
|
+
# Fast-path check for `escape`: a single anchored regex covering the same
|
|
15
|
+
# ranges as NORMAL_UNESCAPED_RANGES. Avoids allocating a `chars` array and
|
|
16
|
+
# scanning four Range objects per character on the common all-normal case.
|
|
17
|
+
NORMAL_UNESCAPED_REGEX = Regexp.new(
|
|
18
|
+
'\A[' +
|
|
19
|
+
NORMAL_UNESCAPED_RANGES.map { |r| "\\u{#{r.min.to_s(16)}}-\\u{#{r.max.to_s(16)}}" }.join +
|
|
20
|
+
']*\z'
|
|
21
|
+
).freeze
|
|
22
|
+
|
|
14
23
|
def self.normalize(value)
|
|
15
24
|
case value
|
|
16
25
|
when String then normalize_name(value)
|
|
@@ -33,8 +42,8 @@ module Janeway
|
|
|
33
42
|
end
|
|
34
43
|
|
|
35
44
|
def self.escape(str)
|
|
36
|
-
# Common case, all chars are normal.
|
|
37
|
-
return str if
|
|
45
|
+
# Common case, all chars are normal — one regex match, no allocations.
|
|
46
|
+
return str if NORMAL_UNESCAPED_REGEX.match?(str)
|
|
38
47
|
|
|
39
48
|
# Some escaping must be done
|
|
40
49
|
str.chars.map { |char| escape_char(char) }.join
|
|
@@ -46,11 +55,18 @@ module Janeway
|
|
|
46
55
|
def self.escape_char(char)
|
|
47
56
|
# Character ranges defined by https://www.rfc-editor.org/rfc/rfc9535.html#section-2.7-8
|
|
48
57
|
case char.ord
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
# normal-unescaped range
|
|
59
|
+
when 0x20..0x26, 0x28..0x5B, 0x5D..0xD7FF, 0xE000..0x10FFFF then char # unescaped
|
|
60
|
+
|
|
61
|
+
# normal-escapable range
|
|
62
|
+
when 0x08 then '\\b' # backspace
|
|
63
|
+
when 0x0C then '\\f' # form feed
|
|
64
|
+
when 0x0A then '\\n' # line feed / newline
|
|
65
|
+
when 0x0D then '\\r' # carriage return
|
|
66
|
+
when 0x09 then '\\t' # horizontal tab
|
|
67
|
+
when 0x27 then '\\\'' # apostrophe
|
|
68
|
+
when 0x5C then '\\\\' # backslash
|
|
69
|
+
|
|
54
70
|
else # normal-hexchar range
|
|
55
71
|
hex_encode_char(char)
|
|
56
72
|
end
|