mustermann 1.0.1 → 1.0.2.rc1

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