antares 0.1.0 → 0.2.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.
@@ -0,0 +1,430 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antares
4
+ Bracket = Struct.new(:open_line, :open_column, :close_line, :close_column,
5
+ :depth, :kind, keyword_init: true)
6
+ Region = Struct.new(:start_line, :end_line, :kind, :label,
7
+ :start_column, :end_column, keyword_init: true)
8
+
9
+ class Structure
10
+ OPEN = {"(" => ")", "[" => "]", "{" => "}"}.freeze
11
+ CLOSE = OPEN.invert.freeze
12
+ REGION_MARKER = /\A\s*(?:\#|\/\/|\/\*+|<!--)\s*\#?\s*(end)?region\b/i
13
+ PROVIDER_METHODS = %i[fold_regions brackets bracket_at context_at selection_ranges edit].freeze
14
+ Line = Struct.new(:text, :tokens, :brackets, :indent, :blank,
15
+ :comment, :marker, :label, keyword_init: true)
16
+
17
+ @providers = {}
18
+ @providers_lock = Mutex.new
19
+
20
+ class << self
21
+ def register(language, provider)
22
+ raise ArgumentError, "provider must respond to new" unless provider.respond_to?(:new)
23
+
24
+ @providers_lock.synchronize { @providers[language_key(language)] = provider }
25
+ provider
26
+ end
27
+
28
+ def unregister(language)
29
+ @providers_lock.synchronize { @providers.delete(language_key(language)) }
30
+ end
31
+
32
+ def build(language:, **arguments)
33
+ provider = language && @providers_lock.synchronize { @providers[language_key(language)] }
34
+ structure = (provider || self).new(**arguments)
35
+ missing = PROVIDER_METHODS.reject { |method| structure.respond_to?(method) }
36
+ raise Error, "structure provider is missing #{missing.join(', ')}" unless missing.empty?
37
+
38
+ structure
39
+ end
40
+
41
+ private
42
+
43
+ def language_key(language)
44
+ raise ArgumentError, "language must be a String or Symbol" unless language.is_a?(String) || language.is_a?(Symbol)
45
+
46
+ value = language.to_s.downcase
47
+ raise ArgumentError, "language must be nonempty" if value.empty? || value.include?("\0")
48
+
49
+ value.freeze
50
+ end
51
+ end
52
+
53
+ def initialize(lines:, line_count:, tokens_for:, tokens_in:, stabilize: nil)
54
+ @lines = lines
55
+ @line_count = line_count
56
+ @tokens_for = tokens_for
57
+ @tokens_in = tokens_in
58
+ @stabilize = stabilize
59
+ @line_data = []
60
+ @dirty_from = 0
61
+ @brackets_dirty = true
62
+ @folds_dirty = true
63
+ @fold_brackets_dirty = true
64
+ @derived_dirty = true
65
+ @derived_regions = [].freeze
66
+ @fold_start_lines = {}.freeze
67
+ @pending_old_lines = nil
68
+ @token_kinds = {}
69
+ end
70
+
71
+ def fold_regions(range = nil)
72
+ refresh(folds: true)
73
+ select_range(@fold_regions, range) { |region| [region.start_line, region.end_line] }
74
+ end
75
+
76
+ def brackets(range = nil)
77
+ refresh(brackets: true)
78
+ select_range(@brackets, range) { |bracket| [bracket.open_line, bracket.close_line] }
79
+ end
80
+
81
+ def bracket_at(line, column)
82
+ validate_position(line, column, brackets: true)
83
+ @bracket_positions[[line, column]]
84
+ end
85
+
86
+ def context_at(line)
87
+ validate_line(line)
88
+ refresh(folds: true)
89
+ @fold_regions.select { |region| region.start_line <= line && line <= region.end_line }
90
+ .sort_by { |region| [region.start_line, -region.end_line] }
91
+ end
92
+
93
+ def selection_ranges(line, column)
94
+ validate_position(line, column, brackets: true, folds: true)
95
+ ranges = []
96
+ token = selection_spans(@line_data.fetch(line)).find { |span| span[0] <= column && column < span[1] }
97
+ ranges << selection_region(line, token) if token
98
+ containing_brackets(line, column).each do |bracket|
99
+ ranges << Region.new(start_line: bracket.open_line, end_line: bracket.close_line,
100
+ start_column: bracket.open_column + 1, end_column: bracket.close_column,
101
+ kind: :block, label: "inside #{bracket.kind}")
102
+ ranges << Region.new(start_line: bracket.open_line, end_line: bracket.close_line,
103
+ start_column: bracket.open_column, end_column: bracket.close_column + 1,
104
+ kind: :block, label: bracket.kind)
105
+ end
106
+ ranges << Region.new(start_line: line, end_line: line, start_column: 0,
107
+ end_column: @line_data.fetch(line).text.delete_suffix("\n").length,
108
+ kind: :line, label: @line_data.fetch(line).label)
109
+ ranges.concat(context_at(line).reverse)
110
+ ranges.uniq { |region| [region.start_line, region.start_column, region.end_line, region.end_column] }
111
+ end
112
+
113
+ # The provider must already reflect the edit, matching Highlighter#edit.
114
+ def edit(from_line:, removed:, inserted:)
115
+ if @pending_old_lines || removed != inserted
116
+ @folds_dirty = @fold_brackets_dirty = @derived_dirty = true
117
+ @pending_old_lines = nil
118
+ else
119
+ @pending_old_lines = [from_line, @line_data.slice(from_line, removed) || []]
120
+ end
121
+ @line_data[from_line, removed] = Array.new(inserted)
122
+ @dirty_from = [@dirty_from || from_line, from_line].min
123
+ @brackets_dirty = true
124
+ if removed != inserted
125
+ @folds_dirty = true
126
+ @fold_brackets_dirty = true
127
+ end
128
+ self
129
+ end
130
+
131
+ private
132
+
133
+ def refresh(brackets: false, folds: false)
134
+ count = line_count
135
+ if @line_data.length != count
136
+ @brackets_dirty = @folds_dirty = @fold_brackets_dirty = @derived_dirty = true
137
+ @line_data.fill(nil, @line_data.length...count) if @line_data.length < count
138
+ @line_data.slice!(count..) if @line_data.length > count
139
+ end
140
+ refresh_lines(count) if @dirty_from
141
+ rebuild_brackets if brackets && @brackets_dirty
142
+ if folds && @folds_dirty
143
+ rebuild_brackets if @fold_brackets_dirty && @brackets_dirty
144
+ rebuild_folds
145
+ end
146
+ self
147
+ end
148
+
149
+ def refresh_lines(count)
150
+ start = @dirty_from
151
+ if start.zero? && @line_data.none?
152
+ @tokens_in.call(0...count).each_with_index { |tokens, index| @line_data[index] = scan_line(index, tokens) }
153
+ @dirty_from = nil
154
+ return
155
+ end
156
+ finish = @stabilize ? @stabilize.call(start) : count
157
+ unless finish.is_a?(Integer) && finish >= start && finish <= count
158
+ raise ArgumentError, "stabilize must return a line boundary inside the document"
159
+ end
160
+ index = start
161
+ while index < finish
162
+ tokens = @tokens_for.call(index)
163
+ old = old_line(index)
164
+ updated = scan_line(index, tokens)
165
+ if bracket_boundary_changed?(old, updated)
166
+ @fold_brackets_dirty = @folds_dirty = @derived_dirty = true
167
+ end
168
+ if derived_line_changed?(old, updated, index)
169
+ @folds_dirty = @derived_dirty = true
170
+ end
171
+ @line_data[index] = updated
172
+ index += 1
173
+ end
174
+ @pending_old_lines = nil
175
+ @dirty_from = nil
176
+ end
177
+
178
+ def scan_line(index, tokens)
179
+ text = @lines.call(index)
180
+ column = 0
181
+ brackets = []
182
+ significant = false
183
+ comment = true
184
+ tokens.each do |type, value|
185
+ finish = column + value.length
186
+ token_kind = @token_kinds[type] ||= classify_token(type.qualname)
187
+ if comment && !value.strip.empty?
188
+ significant = true
189
+ comment = token_kind == :comment
190
+ end
191
+ if token_kind == :punctuation
192
+ value.each_char.with_index { |character, offset| brackets << [character, column + offset] if OPEN.key?(character) || CLOSE.key?(character) }
193
+ end
194
+ column = finish
195
+ end
196
+ stripped = text.strip
197
+ comment &&= significant
198
+ marker = comment && (match = REGION_MARKER.match(text)) ? (match[1] ? :close : :open) : nil
199
+ Line.new(text: text, tokens: tokens, brackets: brackets.freeze,
200
+ indent: indentation(text), blank: stripped.empty?, comment: comment,
201
+ marker: marker, label: stripped.freeze).freeze
202
+ end
203
+
204
+ def rebuild_brackets
205
+ @brackets = build_brackets.freeze
206
+ @bracket_positions = {}
207
+ @brackets.each do |bracket|
208
+ @bracket_positions[[bracket.open_line, bracket.open_column]] = bracket
209
+ @bracket_positions[[bracket.close_line, bracket.close_column]] = bracket
210
+ end
211
+ @brackets_dirty = false
212
+ end
213
+
214
+ def rebuild_folds
215
+ if @derived_dirty
216
+ @derived_regions = derived_regions.freeze
217
+ end
218
+ regions = bracket_regions + @derived_regions
219
+ @fold_regions = regions.uniq { |region| [region.start_line, region.end_line, region.kind] }
220
+ .sort_by { |region| [region.start_line, -region.end_line, region.kind.to_s] }.freeze
221
+ @fold_start_lines = @fold_regions.each_with_object({}) do |region, starts|
222
+ starts[region.start_line] = true
223
+ end.freeze
224
+ @derived_dirty = false
225
+ @folds_dirty = @fold_brackets_dirty = false
226
+ end
227
+
228
+ def old_line(index)
229
+ return @line_data[index] unless @pending_old_lines
230
+
231
+ first, lines = @pending_old_lines
232
+ index >= first && index < first + lines.length ? lines[index - first] : @line_data[index]
233
+ end
234
+
235
+ def derived_line_changed?(old, updated, index)
236
+ return true unless old
237
+ return false if old.equal?(updated)
238
+ return true unless old.indent == updated.indent && old.blank == updated.blank &&
239
+ old.comment == updated.comment && old.marker == updated.marker
240
+
241
+ old.label != updated.label && @fold_start_lines.key?(index)
242
+ end
243
+
244
+ def bracket_boundary_changed?(old, updated)
245
+ return true unless old
246
+ return false if old.brackets == updated.brackets
247
+
248
+ !locally_balanced?(old.brackets) || !locally_balanced?(updated.brackets)
249
+ end
250
+
251
+ def locally_balanced?(brackets)
252
+ stack = []
253
+ brackets.each do |character, _column|
254
+ if OPEN.key?(character)
255
+ stack << character
256
+ elsif stack.last == CLOSE.fetch(character)
257
+ stack.pop
258
+ else
259
+ return false
260
+ end
261
+ end
262
+ stack.empty?
263
+ end
264
+
265
+ def build_brackets
266
+ stack = []
267
+ pairs = []
268
+ @line_data.each_with_index do |line, line_index|
269
+ line.brackets.each do |character, column|
270
+ if OPEN.key?(character)
271
+ stack << [character, line_index, column, stack.length]
272
+ elsif stack.last&.first == CLOSE.fetch(character)
273
+ open, open_line, open_column, depth = stack.pop
274
+ pairs << Bracket.new(open_line: open_line, open_column: open_column,
275
+ close_line: line_index, close_column: column, depth: depth,
276
+ kind: "#{open}#{character}").freeze
277
+ else
278
+ stack.pop
279
+ end
280
+ end
281
+ end
282
+ pairs.sort_by { |bracket| [bracket.open_line, bracket.open_column] }
283
+ end
284
+
285
+ def bracket_regions
286
+ @brackets.filter_map do |bracket|
287
+ next if bracket.open_line == bracket.close_line
288
+ Region.new(start_line: bracket.open_line, end_line: bracket.close_line,
289
+ kind: :block, label: @line_data.fetch(bracket.open_line).label).freeze
290
+ end
291
+ end
292
+
293
+ def derived_regions
294
+ regions = []
295
+ indentation = []
296
+ markers = []
297
+ comment_start = nil
298
+ previous = nil
299
+ bracket_starts = @brackets.each_with_object({}) do |bracket, starts|
300
+ starts[bracket.open_line] = true if bracket.open_line < bracket.close_line
301
+ end
302
+ @line_data.each_with_index do |line, index|
303
+ if line.comment && !line.marker
304
+ comment_start ||= index
305
+ else
306
+ add_region(regions, comment_start, index - 1, :comment, @line_data.fetch(comment_start).label) if comment_start
307
+ comment_start = nil
308
+ end
309
+ markers << [index, line.label] if line.marker == :open
310
+ if line.marker == :close && markers.any?
311
+ start_line, label = markers.pop
312
+ add_region(regions, start_line, index, :region, label)
313
+ end
314
+ unless line.blank
315
+ while indentation.last && line.indent < indentation.last[2]
316
+ start_line, label, = indentation.pop
317
+ add_region(regions, start_line, previous[0], :block, label)
318
+ end
319
+ if previous && line.indent > previous[1] && !previous[3] && !bracket_starts[previous[0]]
320
+ indentation << [previous[0], previous[2], line.indent]
321
+ end
322
+ previous = [index, line.indent, line.label, line.comment]
323
+ end
324
+ end
325
+ add_region(regions, comment_start, @line_data.length - 1, :comment,
326
+ @line_data.fetch(comment_start).label) if comment_start
327
+ indentation.reverse_each do |start_line, label, _|
328
+ add_region(regions, start_line, previous[0], :block, label)
329
+ end if previous
330
+ regions
331
+ end
332
+
333
+ def add_region(regions, start_line, end_line, kind, label)
334
+ return unless start_line && end_line > start_line
335
+ regions << Region.new(start_line: start_line, end_line: end_line,
336
+ kind: kind, label: label).freeze
337
+ end
338
+
339
+ def containing_brackets(line, column)
340
+ @brackets.select do |bracket|
341
+ ([bracket.open_line, bracket.open_column] <=> [line, column]) <= 0 &&
342
+ ([line, column] <=> [bracket.close_line, bracket.close_column]) <= 0
343
+ end.sort_by { |bracket| -bracket.depth }
344
+ end
345
+
346
+ def selection_region(line, span)
347
+ Region.new(start_line: line, end_line: line, start_column: span[0],
348
+ end_column: span[1], kind: span[2], label: span[3])
349
+ end
350
+
351
+ def selection_spans(line)
352
+ column = 0
353
+ line.tokens.filter_map do |type, value|
354
+ span = nil
355
+ finish = column + value.length
356
+ unless value.strip.empty?
357
+ token_kind = @token_kinds[type] ||= classify_token(type.qualname)
358
+ kind = token_kind == :comment ? :comment : (token_kind == :string ? :string : :token)
359
+ span_finish = value.end_with?("\n") ? finish - 1 : finish
360
+ span = [column, span_finish, kind, value.delete_suffix("\n")] if span_finish > column
361
+ end
362
+ column = finish
363
+ span
364
+ end
365
+ end
366
+
367
+ def select_range(values, range)
368
+ return values.dup unless range
369
+ first, last = range_bounds(range)
370
+ values.select do |value|
371
+ value_first, value_last = yield(value)
372
+ value_last >= first && value_first <= last
373
+ end
374
+ end
375
+
376
+ def range_bounds(range)
377
+ raise ArgumentError, "range must have integer bounds" unless range.is_a?(Range) && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
378
+ count = line_count
379
+ last = range.exclude_end? ? range.end - 1 : range.end
380
+ endpoints = [range.begin, range.end]
381
+ outside = endpoints.any?(&:negative?) || endpoints.any? { |value| value > count }
382
+ outside ||= !range.exclude_end? && endpoints.include?(count)
383
+ raise RangeError, "range outside document" if outside
384
+ [range.begin, last]
385
+ end
386
+
387
+ def validate_position(line, column, brackets: false, folds: false)
388
+ validate_line(line)
389
+ raise ArgumentError, "column must be a nonnegative integer" unless column.is_a?(Integer) && column >= 0
390
+ refresh(brackets: brackets, folds: folds)
391
+ text = @line_data.fetch(line).text
392
+ last = text.end_with?("\n") ? text.length - 1 : text.length
393
+ raise RangeError, "column outside line" if column > last
394
+ end
395
+
396
+ def validate_line(line)
397
+ raise RangeError, "line outside document" unless line.is_a?(Integer) && line >= 0 && line < line_count
398
+ end
399
+
400
+ def line_count
401
+ value = @line_count.call
402
+ raise ArgumentError, "line_count must return a nonnegative integer" unless value.is_a?(Integer) && value >= 0
403
+ value
404
+ end
405
+
406
+ def indentation(text)
407
+ column = 0
408
+ text.each_char do |character|
409
+ case character
410
+ when " " then column += 1
411
+ when "\t" then column += 8 - (column % 8)
412
+ else break
413
+ end
414
+ end
415
+ column
416
+ end
417
+
418
+ def comment_token?(name) = name == "Comment" || name.start_with?("Comment.")
419
+ def string_token?(name) = name == "Literal.String" || name.start_with?("Literal.String.")
420
+ def punctuation_token?(name) = name == "Punctuation" || name.start_with?("Punctuation.")
421
+
422
+ def classify_token(name)
423
+ return :comment if comment_token?(name)
424
+ return :string if string_token?(name)
425
+ return :punctuation if punctuation_token?(name)
426
+
427
+ :token
428
+ end
429
+ end
430
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module Antares
6
+ class TMLanguageLexer < Rouge::Lexer
7
+ MAX_SOURCE_BYTES = 8 * 1024 * 1024
8
+ MAX_SECONDS = 0.2
9
+ MAX_INCLUDE_DEPTH = 64
10
+ MAX_CONTEXT_DEPTH = 256
11
+ MAX_NULL_STEPS = 32
12
+ Context = Struct.new(:patterns, :ending, :token, :delimiter_token, :end_captures,
13
+ :apply_end_last, keyword_init: true)
14
+
15
+ class << self
16
+ attr_reader :definition
17
+
18
+ def build(definition)
19
+ Class.new(self).tap do |lexer|
20
+ lexer.instance_variable_set(:@definition, definition)
21
+ lexer.define_singleton_method(:tag) { definition.tag }
22
+ end.new
23
+ end
24
+ end
25
+
26
+ def initialize(options = {})
27
+ super
28
+ @max_source_bytes = Integer(self.options.fetch("max_bytes", MAX_SOURCE_BYTES))
29
+ @max_seconds = Float(self.options.fetch("max_seconds", MAX_SECONDS))
30
+ raise ArgumentError, "max_bytes must be positive" unless @max_source_bytes.positive?
31
+ raise ArgumentError, "max_seconds must be positive and finite" unless @max_seconds.positive? && @max_seconds.finite?
32
+ rescue ArgumentError, TypeError
33
+ raise ArgumentError, "invalid tmLanguage lexer limits"
34
+ end
35
+
36
+ def reset!
37
+ @expanded = {}
38
+ end
39
+
40
+ def stream_tokens(source, &emit)
41
+ raise ResourceLimitError, "source exceeds #{@max_source_bytes} bytes" if source.bytesize > @max_source_bytes
42
+
43
+ Timeout.timeout(@max_seconds, ResourceLimitError) { tokenize(source, &emit) }
44
+ end
45
+
46
+ private
47
+
48
+ def tokenize(source, &emit)
49
+ definition = self.class.definition
50
+ contexts = [Context.new(patterns: definition.patterns, token: Rouge::Token::Tokens::Text)]
51
+ position = 0
52
+ null_steps = 0
53
+ while position < source.length
54
+ context = contexts.last
55
+ ending = context.ending&.match(source, position)
56
+ rule, matching = next_rule(expanded(context.patterns), source, position)
57
+ use_ending = ending && (!matching || ending.begin(0) < matching.begin(0) ||
58
+ (ending.begin(0) == matching.begin(0) && !context.apply_end_last))
59
+ boundary = use_ending ? ending&.begin(0) : matching&.begin(0)
60
+ unless boundary
61
+ emit.call(context.token, source[position..])
62
+ break
63
+ end
64
+ if boundary > position
65
+ emit.call(context.token, source[position...boundary])
66
+ position = boundary
67
+ null_steps = 0
68
+ next
69
+ end
70
+
71
+ before = position
72
+ if use_ending
73
+ emit_match(source, ending, context.delimiter_token || context.token,
74
+ context.end_captures || {}, &emit)
75
+ position = ending.end(0)
76
+ contexts.pop
77
+ elsif rule.is_a?(Grammar::MatchRule)
78
+ raise GrammarError, "match regex consumed no input" if matching.end(0) == position
79
+
80
+ emit_match(source, matching, rule.token || context.token, rule.captures, &emit)
81
+ position = matching.end(0)
82
+ else
83
+ emit_match(source, matching, rule.token || context.token, rule.begin_captures, &emit)
84
+ position = matching.end(0)
85
+ raise ResourceLimitError, "grammar context nesting exceeds #{MAX_CONTEXT_DEPTH}" if contexts.length >= MAX_CONTEXT_DEPTH
86
+
87
+ contexts << Context.new(patterns: rule.patterns,
88
+ ending: end_regexp(rule, matching),
89
+ token: rule.content_token || rule.token || context.token,
90
+ delimiter_token: rule.token || context.token,
91
+ end_captures: rule.end_captures,
92
+ apply_end_last: rule.apply_end_last)
93
+ end
94
+ null_steps = position == before ? null_steps + 1 : 0
95
+ raise GrammarError, "grammar made too many zero-width transitions" if null_steps > MAX_NULL_STEPS
96
+ end
97
+ end
98
+
99
+ def next_rule(patterns, source, position)
100
+ selected_rule = selected_match = nil
101
+ patterns.each do |rule|
102
+ matching = rule.regexp.match(source, position)
103
+ next unless matching
104
+ next if selected_match && selected_match.begin(0) <= matching.begin(0)
105
+
106
+ selected_rule = rule
107
+ selected_match = matching
108
+ end
109
+ [selected_rule, selected_match]
110
+ end
111
+
112
+ def expanded(patterns)
113
+ @expanded[patterns.object_id] ||= begin
114
+ cycle = [false]
115
+ count = [0]
116
+ rules = expand_patterns(patterns, {patterns.object_id => true}, 0, cycle, count).freeze
117
+ raise GrammarError, "recursive include contains no matching rules" if rules.empty? && cycle.first
118
+
119
+ rules
120
+ end
121
+ end
122
+
123
+ def expand_patterns(patterns, active, depth, cycle, count)
124
+ raise ResourceLimitError, "grammar include nesting exceeds #{MAX_INCLUDE_DEPTH}" if depth >= MAX_INCLUDE_DEPTH
125
+
126
+ patterns.flat_map { |rule| expand_rule(rule, active, depth, cycle, count) }
127
+ end
128
+
129
+ def expand_rule(rule, active, depth, cycle, count)
130
+ raise ResourceLimitError, "grammar include nesting exceeds #{MAX_INCLUDE_DEPTH}" if depth >= MAX_INCLUDE_DEPTH
131
+
132
+ if rule.is_a?(Grammar::MatchRule) || rule.is_a?(Grammar::BeginRule)
133
+ count[0] += 1
134
+ raise ResourceLimitError, "expanded grammar exceeds #{Grammar::MAX_RULES} rules" if count[0] > Grammar::MAX_RULES
135
+
136
+ return [rule]
137
+ end
138
+ return expand_patterns(rule.patterns, active, depth + 1, cycle, count) if rule.is_a?(Grammar::GroupRule)
139
+
140
+ target = if %w[$self $base].include?(rule.target)
141
+ self.class.definition.patterns
142
+ else
143
+ self.class.definition.repository.fetch(rule.target.delete_prefix("#"))
144
+ end
145
+ target_patterns = target.is_a?(Array) ? target : (target.patterns if target.is_a?(Grammar::GroupRule))
146
+ return expand_rule(target, active, depth + 1, cycle, count) unless target_patterns
147
+
148
+ key = target_patterns.object_id
149
+ if active[key]
150
+ cycle[0] = true
151
+ return []
152
+ end
153
+ expand_patterns(target_patterns, active.merge(key => true), depth + 1, cycle, count)
154
+ rescue KeyError
155
+ raise GrammarError, "unknown repository include #{rule.target.inspect}"
156
+ end
157
+
158
+ def end_regexp(rule, matching)
159
+ return rule.ending unless rule.dynamic_end
160
+
161
+ source = rule.end_source.gsub(/(?<!\\)\\([1-9])/) do
162
+ index = Regexp.last_match(1).to_i
163
+ capture = matching[index]
164
+ raise GrammarError, "end capture #{index} did not participate in begin regex" unless capture
165
+
166
+ Regexp.escape(capture)
167
+ end
168
+ raise ResourceLimitError, "expanded end regex exceeds #{Grammar::MAX_REGEX_BYTES} bytes" if source.bytesize > Grammar::MAX_REGEX_BYTES
169
+
170
+ Regexp.new(source)
171
+ rescue RegexpError, IndexError => error
172
+ raise GrammarError, "invalid expanded end regex: #{error.message.byteslice(0, 256)}"
173
+ end
174
+
175
+ def emit_match(source, matching, base_token, captures)
176
+ first = matching.begin(0)
177
+ last = matching.end(0)
178
+ ranges = captures.filter_map do |index, token|
179
+ start = matching.begin(index)
180
+ finish = matching.end(index)
181
+ [start, finish, token, index] if start && finish && finish > start
182
+ rescue IndexError
183
+ raise GrammarError, "capture #{index} does not exist in regex"
184
+ end
185
+ boundaries = ([first, last] + ranges.flat_map { |range| range.first(2) }).uniq.sort
186
+ boundaries.each_cons(2) do |start, finish|
187
+ range = ranges.select { |candidate| candidate[0] <= start && finish <= candidate[1] }
188
+ .min_by { |candidate| [candidate[1] - candidate[0], -candidate[3]] }
189
+ yield(range ? range[2] : base_token, source[start...finish])
190
+ end
191
+ end
192
+ end
193
+ end