mustermann-contrib 1.0.0.beta2
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 +7 -0
- data/README.md +1239 -0
- data/examples/highlighting.rb +35 -0
- data/highlighting.png +0 -0
- data/irb.png +0 -0
- data/lib/mustermann/cake.rb +18 -0
- data/lib/mustermann/express.rb +37 -0
- data/lib/mustermann/file_utils.rb +217 -0
- data/lib/mustermann/file_utils/glob_pattern.rb +39 -0
- data/lib/mustermann/fileutils.rb +1 -0
- data/lib/mustermann/flask.rb +198 -0
- data/lib/mustermann/grape.rb +35 -0
- data/lib/mustermann/pyramid.rb +28 -0
- data/lib/mustermann/rails.rb +46 -0
- data/lib/mustermann/shell.rb +56 -0
- data/lib/mustermann/simple.rb +50 -0
- data/lib/mustermann/string_scanner.rb +313 -0
- data/lib/mustermann/strscan.rb +1 -0
- data/lib/mustermann/template.rb +62 -0
- data/lib/mustermann/uri_template.rb +1 -0
- data/lib/mustermann/versions.rb +46 -0
- data/lib/mustermann/visualizer.rb +38 -0
- data/lib/mustermann/visualizer/highlight.rb +137 -0
- data/lib/mustermann/visualizer/highlighter.rb +37 -0
- data/lib/mustermann/visualizer/highlighter/ad_hoc.rb +94 -0
- data/lib/mustermann/visualizer/highlighter/ast.rb +102 -0
- data/lib/mustermann/visualizer/highlighter/composite.rb +45 -0
- data/lib/mustermann/visualizer/highlighter/dummy.rb +18 -0
- data/lib/mustermann/visualizer/highlighter/regular.rb +104 -0
- data/lib/mustermann/visualizer/pattern_extension.rb +68 -0
- data/lib/mustermann/visualizer/renderer/ansi.rb +23 -0
- data/lib/mustermann/visualizer/renderer/generic.rb +46 -0
- data/lib/mustermann/visualizer/renderer/hansi_template.rb +34 -0
- data/lib/mustermann/visualizer/renderer/html.rb +50 -0
- data/lib/mustermann/visualizer/renderer/sexp.rb +37 -0
- data/lib/mustermann/visualizer/tree.rb +63 -0
- data/lib/mustermann/visualizer/tree_renderer.rb +78 -0
- data/mustermann-contrib.gemspec +19 -0
- data/spec/cake_spec.rb +90 -0
- data/spec/express_spec.rb +209 -0
- data/spec/file_utils_spec.rb +119 -0
- data/spec/flask_spec.rb +361 -0
- data/spec/flask_subclass_spec.rb +368 -0
- data/spec/grape_spec.rb +747 -0
- data/spec/pattern_extension_spec.rb +49 -0
- data/spec/pyramid_spec.rb +101 -0
- data/spec/rails_spec.rb +647 -0
- data/spec/shell_spec.rb +147 -0
- data/spec/simple_spec.rb +268 -0
- data/spec/string_scanner_spec.rb +271 -0
- data/spec/template_spec.rb +841 -0
- data/spec/visualizer_spec.rb +199 -0
- data/theme.png +0 -0
- data/tree.png +0 -0
- metadata +126 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/ast/pattern'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Grape style pattern implementation.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Mustermann.new('/:foo', type: :grape) === '/bar' # => true
|
9
|
+
#
|
10
|
+
# @see Mustermann::Pattern
|
11
|
+
# @see file:README.md#grape Syntax description in the README
|
12
|
+
class Grape < AST::Pattern
|
13
|
+
register :grape
|
14
|
+
|
15
|
+
on(nil, ??, ?)) { |c| unexpected(c) }
|
16
|
+
|
17
|
+
on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
|
18
|
+
on(?:) { |c| node(:capture, constraint: "[^/\\?#\.]") { scan(/\w+/) } }
|
19
|
+
on(?\\) { |c| node(:char, expect(/./)) }
|
20
|
+
on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
|
21
|
+
on(?|) { |c| node(:or) }
|
22
|
+
|
23
|
+
on ?{ do |char|
|
24
|
+
type = scan(?+) ? :named_splat : :capture
|
25
|
+
name = expect(/[\w\.]+/)
|
26
|
+
type = :splat if type == :named_splat and name == 'splat'
|
27
|
+
expect(?})
|
28
|
+
node(type, name)
|
29
|
+
end
|
30
|
+
|
31
|
+
suffix ?? do |char, element|
|
32
|
+
node(:optional, element)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/ast/pattern'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Pyramid style pattern implementation.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Mustermann.new('/<foo>', type: :pryamid) === '/bar' # => true
|
9
|
+
#
|
10
|
+
# @see Mustermann::Pattern
|
11
|
+
# @see file:README.md#pryamid Syntax description in the README
|
12
|
+
class Pyramid < AST::Pattern
|
13
|
+
register :pyramid
|
14
|
+
|
15
|
+
on(nil, ?}) { |c| unexpected(c) }
|
16
|
+
|
17
|
+
on(?{) do |char|
|
18
|
+
name = expect(/\w+/, char: char)
|
19
|
+
constraint = read_brackets(?{, ?}) if scan(?:)
|
20
|
+
expect(?}) unless constraint
|
21
|
+
node(:capture, name, constraint: constraint)
|
22
|
+
end
|
23
|
+
|
24
|
+
on(?*) do |char|
|
25
|
+
node(:named_splat, expect(/\w+$/, char: char), convert: -> e { e.split(?/) })
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/ast/pattern'
|
3
|
+
require 'mustermann/versions'
|
4
|
+
|
5
|
+
module Mustermann
|
6
|
+
# Rails style pattern implementation.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# Mustermann.new('/:foo', type: :rails) === '/bar' # => true
|
10
|
+
#
|
11
|
+
# @see Mustermann::Pattern
|
12
|
+
# @see file:README.md#rails Syntax description in the README
|
13
|
+
class Rails < AST::Pattern
|
14
|
+
extend Versions
|
15
|
+
register :rails
|
16
|
+
|
17
|
+
# first parser, no optional parts
|
18
|
+
version('2.3') do
|
19
|
+
on(nil) { |c| unexpected(c) }
|
20
|
+
on(?*) { |c| node(:named_splat) { scan(/\w+/) } }
|
21
|
+
on(?:) { |c| node(:capture) { scan(/\w+/) } }
|
22
|
+
end
|
23
|
+
|
24
|
+
# rack-mount
|
25
|
+
version('3.0', '3.1') do
|
26
|
+
on(?)) { |c| unexpected(c) }
|
27
|
+
on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
|
28
|
+
on(?\\) { |c| node(:char, expect(/./)) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# stand-alone journey
|
32
|
+
version('3.2') do
|
33
|
+
on(?|) { |c| raise ParseError, "the implementation of | is broken in ActionDispatch, cannot compile compatible pattern" }
|
34
|
+
on(?\\) { |c| node(:char, c) }
|
35
|
+
end
|
36
|
+
|
37
|
+
# embedded journey, broken (ignored) escapes
|
38
|
+
version('4.0', '4.1') { on(?\\) { |c| read } }
|
39
|
+
|
40
|
+
# escapes got fixed in 4.2
|
41
|
+
version('4.2') { on(?\\) { |c| node(:char, expect(/./)) } }
|
42
|
+
|
43
|
+
# Rails 5.0 fixes |
|
44
|
+
version('5.0') { on(?|) { |c| node(:or) }}
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/pattern'
|
3
|
+
require 'mustermann/simple_match'
|
4
|
+
|
5
|
+
module Mustermann
|
6
|
+
# Matches strings that are identical to the pattern.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# Mustermann.new('/*.*', type: :shell) === '/bar' # => false
|
10
|
+
#
|
11
|
+
# @see Mustermann::Pattern
|
12
|
+
# @see file:README.md#shell Syntax description in the README
|
13
|
+
class Shell < Pattern
|
14
|
+
include Concat::Native
|
15
|
+
register :shell
|
16
|
+
|
17
|
+
# @!visibility private
|
18
|
+
# @return [#highlight, nil]
|
19
|
+
# highlighing logic for mustermann-visualizer,
|
20
|
+
# nil if mustermann-visualizer hasn't been loaded
|
21
|
+
def highlighter
|
22
|
+
return unless defined? Mustermann::Visualizer::Highlighter
|
23
|
+
@@highlighter ||= Mustermann::Visualizer::Highlighter.create do
|
24
|
+
on('\\') { |matched| escaped(matched, scanner.getch) }
|
25
|
+
on(/[\*\[\]]/, :special)
|
26
|
+
on("{") { nested(:union, ?{, ?}, ?,) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param (see Mustermann::Pattern#initialize)
|
31
|
+
# @return (see Mustermann::Pattern#initialize)
|
32
|
+
# @see (see Mustermann::Pattern#initialize)
|
33
|
+
def initialize(string, **options)
|
34
|
+
@flags = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
|
35
|
+
super(string, **options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param (see Mustermann::Pattern#===)
|
39
|
+
# @return (see Mustermann::Pattern#===)
|
40
|
+
# @see (see Mustermann::Pattern#===)
|
41
|
+
def ===(string)
|
42
|
+
File.fnmatch? @string, unescape(string), @flags
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param (see Mustermann::Pattern#peek_size)
|
46
|
+
# @return (see Mustermann::Pattern#peek_size)
|
47
|
+
# @see (see Mustermann::Pattern#peek_size)
|
48
|
+
def peek_size(string)
|
49
|
+
@peek_string ||= @string + "{**,/**,/**/*}"
|
50
|
+
super if File.fnmatch? @peek_string, unescape(string), @flags
|
51
|
+
end
|
52
|
+
|
53
|
+
# Used by {Mustermann::FileUtils} to not use a generic glob pattern.
|
54
|
+
alias_method :to_glob, :to_s
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/regexp_based'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Sinatra 1.3 style pattern implementation.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Mustermann.new('/:foo', type: :simple) === '/bar' # => true
|
9
|
+
#
|
10
|
+
# @see Mustermann::Pattern
|
11
|
+
# @see file:README.md#simple Syntax description in the README
|
12
|
+
class Simple < RegexpBased
|
13
|
+
register :simple
|
14
|
+
supported_options :greedy, :space_matches_plus
|
15
|
+
instance_delegate highlighter: 'self.class'
|
16
|
+
|
17
|
+
# @!visibility private
|
18
|
+
# @return [#highlight, nil]
|
19
|
+
# highlighing logic for mustermann-visualizer,
|
20
|
+
# nil if mustermann-visualizer hasn't been loaded
|
21
|
+
def self.highlighter
|
22
|
+
return unless defined? Mustermann::Visualizer::Highlighter
|
23
|
+
@highlighter ||= Mustermann::Visualizer::Highlighter.create do
|
24
|
+
on(/:(\w+)/) { |matched| element(:capture, ':') { element(:name, matched[1..-1]) } }
|
25
|
+
on("*" => :splat, "?" => :optional)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def compile(greedy: true, uri_decode: true, space_matches_plus: true, **options)
|
30
|
+
pattern = @string.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c, uri_decode, space_matches_plus) }
|
31
|
+
pattern.gsub!(/((:\w+)|\*)/) do |match|
|
32
|
+
match == "*" ? "(?<splat>.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})"
|
33
|
+
end
|
34
|
+
Regexp.new(pattern)
|
35
|
+
rescue SyntaxError, RegexpError => error
|
36
|
+
type = error.message["invalid group name"] ? CompileError : ParseError
|
37
|
+
raise type, error.message, error.backtrace
|
38
|
+
end
|
39
|
+
|
40
|
+
def encoded(char, uri_decode, space_matches_plus)
|
41
|
+
return Regexp.escape(char) unless uri_decode
|
42
|
+
parser = URI::Parser.new
|
43
|
+
encoded = Regexp.union(parser.escape(char), parser.escape(char, /./).downcase, parser.escape(char, /./).upcase)
|
44
|
+
encoded = Regexp.union(encoded, encoded('+', true, true)) if space_matches_plus and char == " "
|
45
|
+
encoded
|
46
|
+
end
|
47
|
+
|
48
|
+
private :compile, :encoded
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,313 @@
|
|
1
|
+
require 'mustermann'
|
2
|
+
require 'mustermann/pattern_cache'
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module Mustermann
|
6
|
+
# Class inspired by Ruby's StringScanner to scan an input string using multiple patterns.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# require 'mustermann/string_scanner'
|
10
|
+
# scanner = Mustermann::StringScanner.new("here is our example string")
|
11
|
+
#
|
12
|
+
# scanner.scan("here") # => "here"
|
13
|
+
# scanner.getch # => " "
|
14
|
+
#
|
15
|
+
# if scanner.scan(":verb our")
|
16
|
+
# scanner.scan(:noun, capture: :word)
|
17
|
+
# scanner[:verb] # => "is"
|
18
|
+
# scanner[:nound] # => "example"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# scanner.rest # => "string"
|
22
|
+
#
|
23
|
+
# @note
|
24
|
+
# This structure is not thread-safe, you should not scan on the same StringScanner instance concurrently.
|
25
|
+
# Even if it was thread-safe, scanning concurrently would probably lead to unwanted behaviour.
|
26
|
+
class StringScanner
|
27
|
+
# Exception raised if scan/unscan operation cannot be performed.
|
28
|
+
ScanError = Class.new(::ScanError)
|
29
|
+
PATTERN_CACHE = PatternCache.new
|
30
|
+
private_constant :PATTERN_CACHE
|
31
|
+
|
32
|
+
# Patterns created by {#scan} will be globally cached, since we assume that there is a finite number
|
33
|
+
# of different patterns used and that they are more likely to be reused than not.
|
34
|
+
# This method allows clearing the cache.
|
35
|
+
#
|
36
|
+
# @see Mustermann::PatternCache
|
37
|
+
def self.clear_cache
|
38
|
+
PATTERN_CACHE.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Integer] number of cached patterns
|
42
|
+
# @see clear_cache
|
43
|
+
# @api private
|
44
|
+
def self.cache_size
|
45
|
+
PATTERN_CACHE.size
|
46
|
+
end
|
47
|
+
|
48
|
+
# Encapsulates return values for {StringScanner#scan}, {StringScanner#check}, and friends.
|
49
|
+
# Behaves like a String (the substring which matched the pattern), but also exposes its position
|
50
|
+
# in the main string and any params parsed from it.
|
51
|
+
class ScanResult < DelegateClass(String)
|
52
|
+
# The scanner this result came from.
|
53
|
+
# @example
|
54
|
+
# require 'mustermann/string_scanner'
|
55
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
56
|
+
# scanner.scan(:name).scanner == scanner # => true
|
57
|
+
attr_reader :scanner
|
58
|
+
|
59
|
+
# @example
|
60
|
+
# require 'mustermann/string_scanner'
|
61
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
62
|
+
# scanner.scan(:name).position # => 0
|
63
|
+
# scanner.getch.position # => 3
|
64
|
+
# scanner.scan(:name).position # => 4
|
65
|
+
#
|
66
|
+
# @return [Integer] position the substring starts at
|
67
|
+
attr_reader :position
|
68
|
+
alias_method :pos, :position
|
69
|
+
|
70
|
+
# @example
|
71
|
+
# require 'mustermann/string_scanner'
|
72
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
73
|
+
# scanner.scan(:name).length # => 3
|
74
|
+
# scanner.getch.length # => 1
|
75
|
+
# scanner.scan(:name).length # => 3
|
76
|
+
#
|
77
|
+
# @return [Integer] length of the substring
|
78
|
+
attr_reader :length
|
79
|
+
|
80
|
+
# Params parsed from the substring.
|
81
|
+
# Will not include params from previous scan results.
|
82
|
+
#
|
83
|
+
# @example
|
84
|
+
# require 'mustermann/string_scanner'
|
85
|
+
# scanner = Mustermann::StringScanner.new('foo/bar')
|
86
|
+
# scanner.scan(:name).params # => { "name" => "foo" }
|
87
|
+
# scanner.getch.params # => {}
|
88
|
+
# scanner.scan(:name).params # => { "name" => "bar" }
|
89
|
+
#
|
90
|
+
# @see Mustermann::StringScanner#params
|
91
|
+
# @see Mustermann::StringScanner#[]
|
92
|
+
#
|
93
|
+
# @return [Hash] params parsed from the substring
|
94
|
+
attr_reader :params
|
95
|
+
|
96
|
+
# @api private
|
97
|
+
def initialize(scanner, position, length, params = {})
|
98
|
+
@scanner, @position, @length, @params = scanner, position, length, params
|
99
|
+
end
|
100
|
+
|
101
|
+
# @api private
|
102
|
+
# @!visibility private
|
103
|
+
def __getobj__
|
104
|
+
@__getobj__ ||= scanner.to_s[position, length]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Hash] default pattern options used for {#scan} and similar methods
|
109
|
+
# @see #initialize
|
110
|
+
attr_reader :pattern_options
|
111
|
+
|
112
|
+
# Params from all previous matches from {#scan} and {#scan_until},
|
113
|
+
# but not from {#check} and {#check_until}. Changes can be reverted
|
114
|
+
# with {#unscan} and it can be completely cleared via {#reset}.
|
115
|
+
#
|
116
|
+
# @return [Hash] current params
|
117
|
+
attr_reader :params
|
118
|
+
|
119
|
+
# @return [Integer] current scan position on the input string
|
120
|
+
attr_accessor :position
|
121
|
+
alias_method :pos, :position
|
122
|
+
alias_method :pos=, :position=
|
123
|
+
|
124
|
+
# @example with different default type
|
125
|
+
# require 'mustermann/string_scanner'
|
126
|
+
# scanner = Mustermann::StringScanner.new("foo/bar/baz", type: :shell)
|
127
|
+
# scanner.scan('*') # => "foo"
|
128
|
+
# scanner.scan('**/*') # => "/bar/baz"
|
129
|
+
#
|
130
|
+
# @param [String] string the string to scan
|
131
|
+
# @param [Hash] pattern_options default options used for {#scan}
|
132
|
+
def initialize(string = "", **pattern_options)
|
133
|
+
@pattern_options = pattern_options
|
134
|
+
@string = String(string).dup
|
135
|
+
reset
|
136
|
+
end
|
137
|
+
|
138
|
+
# Resets the {#position} to the start and clears all {#params}.
|
139
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
140
|
+
def reset
|
141
|
+
@position = 0
|
142
|
+
@params = {}
|
143
|
+
@history = []
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
# Moves the position to the end of the input string.
|
148
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
149
|
+
def terminate
|
150
|
+
track_result ScanResult.new(self, @position, size - @position)
|
151
|
+
self
|
152
|
+
end
|
153
|
+
|
154
|
+
# Checks if the given pattern matches any substring starting at the current position.
|
155
|
+
#
|
156
|
+
# If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
|
157
|
+
# from the substring into {#params}.
|
158
|
+
#
|
159
|
+
# @param (see Mustermann.new)
|
160
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
161
|
+
def scan(pattern, **options)
|
162
|
+
track_result check(pattern, **options)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Checks if the given pattern matches any substring starting at any position after the current position.
|
166
|
+
#
|
167
|
+
# If it does, it will advance the current {#position} to the end of the substring and merges any params parsed
|
168
|
+
# from the substring into {#params}.
|
169
|
+
#
|
170
|
+
# @param (see Mustermann.new)
|
171
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
172
|
+
def scan_until(pattern, **options)
|
173
|
+
result, prefix = check_until_with_prefix(pattern, **options)
|
174
|
+
track_result(prefix, result)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Reverts the last operation that advanced the position.
|
178
|
+
#
|
179
|
+
# Operations advancing the position: {#terminate}, {#scan}, {#scan_until}, {#getch}.
|
180
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
181
|
+
def unscan
|
182
|
+
raise ScanError, 'unscan failed: previous match record not exist' if @history.empty?
|
183
|
+
previous = @history[0..-2]
|
184
|
+
reset
|
185
|
+
previous.each { |r| track_result(*r) }
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
# Checks if the given pattern matches any substring starting at the current position.
|
190
|
+
#
|
191
|
+
# Does not affect {#position} or {#params}.
|
192
|
+
#
|
193
|
+
# @param (see Mustermann.new)
|
194
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
195
|
+
def check(pattern, **options)
|
196
|
+
params, length = create_pattern(pattern, **options).peek_params(rest)
|
197
|
+
ScanResult.new(self, @position, length, params) if params
|
198
|
+
end
|
199
|
+
|
200
|
+
# Checks if the given pattern matches any substring starting at any position after the current position.
|
201
|
+
#
|
202
|
+
# Does not affect {#position} or {#params}.
|
203
|
+
#
|
204
|
+
# @param (see Mustermann.new)
|
205
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match
|
206
|
+
def check_until(pattern, **options)
|
207
|
+
check_until_with_prefix(pattern, **options).first
|
208
|
+
end
|
209
|
+
|
210
|
+
def check_until_with_prefix(pattern, **options)
|
211
|
+
start = @position
|
212
|
+
@position += 1 until eos? or result = check(pattern, **options)
|
213
|
+
prefix = ScanResult.new(self, start, @position - start) if result
|
214
|
+
[result, prefix]
|
215
|
+
ensure
|
216
|
+
@position = start
|
217
|
+
end
|
218
|
+
|
219
|
+
# Reads a single character and advances the {#position} by one.
|
220
|
+
# @return [Mustermann::StringScanner::ScanResult, nil] the character, nil if at end of string
|
221
|
+
def getch
|
222
|
+
track_result ScanResult.new(self, @position, 1) unless eos?
|
223
|
+
end
|
224
|
+
|
225
|
+
# Appends the given string to the string being scanned
|
226
|
+
#
|
227
|
+
# @example
|
228
|
+
# require 'mustermann/string_scanner'
|
229
|
+
# scanner = Mustermann::StringScanner.new
|
230
|
+
# scanner << "foo"
|
231
|
+
# scanner.scan(/.+/) # => "foo"
|
232
|
+
#
|
233
|
+
# @param [String] string will be appended
|
234
|
+
# @return [Mustermann::StringScanner] the scanner itself
|
235
|
+
def <<(string)
|
236
|
+
@string << string
|
237
|
+
self
|
238
|
+
end
|
239
|
+
|
240
|
+
# @return [true, false] whether or not the end of the string has been reached
|
241
|
+
def eos?
|
242
|
+
@position >= @string.size
|
243
|
+
end
|
244
|
+
|
245
|
+
# @return [true, false] whether or not the current position is at the start of a line
|
246
|
+
def beginning_of_line?
|
247
|
+
@position == 0 or @string[@position - 1] == "\n"
|
248
|
+
end
|
249
|
+
|
250
|
+
# @return [String] outstanding string not yet matched, empty string at end of input string
|
251
|
+
def rest
|
252
|
+
@string[@position..-1] || ""
|
253
|
+
end
|
254
|
+
|
255
|
+
# @return [Integer] number of character remaining to be scanned
|
256
|
+
def rest_size
|
257
|
+
@position > size ? 0 : size - @position
|
258
|
+
end
|
259
|
+
|
260
|
+
# Allows to peek at a number of still unscanned characters without advacing the {#position}.
|
261
|
+
#
|
262
|
+
# @param [Integer] length how many characters to look at
|
263
|
+
# @return [String] the substring
|
264
|
+
def peek(length = 1)
|
265
|
+
@string[@position, length]
|
266
|
+
end
|
267
|
+
|
268
|
+
# Shorthand for accessing {#params}. Accepts symbols as keys.
|
269
|
+
def [](key)
|
270
|
+
params[key.to_s]
|
271
|
+
end
|
272
|
+
|
273
|
+
# (see #params)
|
274
|
+
def to_h
|
275
|
+
params.dup
|
276
|
+
end
|
277
|
+
|
278
|
+
# @return [String] the input string
|
279
|
+
# @see #initialize
|
280
|
+
# @see #<<
|
281
|
+
def to_s
|
282
|
+
@string.dup
|
283
|
+
end
|
284
|
+
|
285
|
+
# @return [Integer] size of the input string
|
286
|
+
def size
|
287
|
+
@string.size
|
288
|
+
end
|
289
|
+
|
290
|
+
# @!visibility private
|
291
|
+
def inspect
|
292
|
+
"#<%p %d/%d @ %p>" % [ self.class, @position, @string.size, @string ]
|
293
|
+
end
|
294
|
+
|
295
|
+
# @!visibility private
|
296
|
+
def create_pattern(pattern, **options)
|
297
|
+
PATTERN_CACHE.create_pattern(pattern, **options, **pattern_options)
|
298
|
+
end
|
299
|
+
|
300
|
+
# @!visibility private
|
301
|
+
def track_result(*results)
|
302
|
+
results.compact!
|
303
|
+
@history << results if results.any?
|
304
|
+
results.each do |result|
|
305
|
+
@params.merge! result.params
|
306
|
+
@position += result.length
|
307
|
+
end
|
308
|
+
results.last
|
309
|
+
end
|
310
|
+
|
311
|
+
private :create_pattern, :track_result, :check_until_with_prefix
|
312
|
+
end
|
313
|
+
end
|