Skip to content

Commit

Permalink
Adds self to Match#captures
Browse files Browse the repository at this point in the history
A common point of confusion for some Citrus users is that they are not
able to retrieve a match by calling its own name on itself. This commit
adds self to the internal @captures Hash in the first position.

Also, Match#to_a and Match#[] were added for convenience when retrieving
captures and submatches.
  • Loading branch information
mjackson committed Jan 11, 2011
1 parent 1e61125 commit cbdce44
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 85 deletions.
213 changes: 132 additions & 81 deletions lib/citrus.rb
Expand Up @@ -1232,14 +1232,17 @@ def initialize(string, events=[])

while events[0].elide?
elisions.unshift(events.shift)
events = events.slice(0, events.length - 2)
events.slice!(-2, events.length)
end

events[0].extend_match(self)

elisions.each do |rule|
rule.extend_match(self)
end
else
# Create a default stream of events for the given string.
events = [Rule.for(string), CLOSE, string.length]
end

@events = events
Expand All @@ -1256,93 +1259,21 @@ def length
# Returns a hash of capture names to arrays of matches with that name,
# in the order they appeared in the input.
def captures
@captures ||= begin
captures = Hash.new {|hash, key| hash[key] = [] }
stack = []
offset = 0
close = false
index = 0
last_length = nil
capture = true
count = 0

while index < @events.size
event = @events[index]

if close
start = stack.pop

if Rule === start
rule = start
os = stack.pop
start = stack.pop

match = Match.new(@string.slice(os, event), @events[start..index])

# We can lookup immediate submatches by their index.
if stack.size == 1
captures[count] = match
count += 1
end

# We can lookup matches that were created by proxy by the name of
# the rule they are proxy for.
captures[rule.rule_name] << match if Proxy === rule

# We can lookup matches that were created by rules with labels by
# that label.
captures[rule.label] << match if rule.label

capture = true
end

last_length = event unless last_length

close = false
elsif event == CLOSE
close = true
else
stack << index

# We can calculate the offset of this rule event by adding back the
# last match length.
if last_length
offset += last_length
last_length = nil
end

if capture && stack.size != 1
stack << offset
stack << event
# We should not create captures when traversing the portion of the
# event stream that is masked by a proxy in the original rule
# definition.
capture = false if Proxy === event
end
end

index += 1
end

captures
end
process_events! unless @captures
@captures
end

# Returns an array of all immediate submatches of this match.
def matches
@matches ||= (0...captures.size).map {|n| captures[n] }.flatten
process_events! unless @matches
@matches
end

# A shortcut for retrieving the first immediate submatch of this match.
def first
captures[0]
matches.first
end

# The default value for a match is its string value. This method is
# overridden in most cases to be more meaningful according to the desired
# interpretation.
alias_method :value, :to_s

# Allows methods of this match's string to be called directly and provides
# a convenient interface for retrieving the first match with a given name.
def method_missing(sym, *args, &block)
Expand All @@ -1359,6 +1290,32 @@ def to_s

alias_method :to_str, :to_s

# The default value for a match is its string value. This method is
# overridden in most cases to be more meaningful according to the desired
# interpretation.
alias_method :value, :to_s

# Returns this match plus all sub #matches in an array.
def to_a
[captures[0]] + matches
end

alias_method :to_ary, :to_a

# Returns the capture at the given +key+. If it is an Integer (and an
# optional length) or a Range, the result of #to_a with the same arguments
# is returned. Otherwise, the value at +key+ in #captures is returned.
def [](key, *args)
case key
when Integer, Range
to_a[key, *args]
else
captures[key]
end
end

alias_method :fetch, :[]

def ==(other)
case other
when String
Expand Down Expand Up @@ -1398,9 +1355,7 @@ def dump(indent=' ')
string = @string.slice(os, event)
lines[start] = "#{space}#{string.inspect} rule=#{rule}, offset=#{os}, length=#{event}"

unless last_length
last_length = event
end
last_length = event unless last_length

close = false
elsif event == CLOSE
Expand All @@ -1421,6 +1376,102 @@ def dump(indent=' ')

puts lines.compact.join("\n")
end

private

# Setup both @captures and @matches instance variables.
def process_events!
# @captures should automatically convert String keys to Symbols when
# fetching, and return an empty Array for all other unknown keys.
@captures = Hash.new {|hash, key| String === key ? hash[key.to_sym] : [] }
@matches = []

capture!(@events[0], self)

stack = []
offset = 0
close = false
index = 0
last_length = nil
capture = true

while index < @events.size
event = @events[index]

if close
start = stack.pop

if Rule === start
rule = start
os = stack.pop
start = stack.pop

match = Match.new(@string.slice(os, event), @events[start..index])
capture!(rule, match)

@matches << match if stack.size == 1

capture = true
end

last_length = event unless last_length

close = false
elsif event == CLOSE
close = true
else
stack << index

# We can calculate the offset of this rule event by adding back the
# last match length.
if last_length
offset += last_length
last_length = nil
end

if capture && stack.size != 1
stack << offset
stack << event

# We should not create captures when traversing a portion of the
# event stream that is masked by a proxy in the original rule
# definition.
capture = false if Proxy === event
end
end

index += 1
end

# Add numeric indices to @captures.
@captures[0] = self

@matches.each_with_index do |match, index|
@captures[index + 1] = match
end
end

def capture!(rule, match)
# We can lookup matches that were created by proxy by the name of
# the rule they are proxy for.
if Proxy === rule
if @captures.key?(rule.rule_name)
@captures[rule.rule_name] << match
else
@captures[rule.rule_name] = [match]
end
end

# We can lookup matches that were created by rules with labels by
# that label.
if rule.label
if @captures.key?(rule.label)
@captures[rule.label] << match
else
@captures[rule.label] = [match]
end
end
end
end
end

Expand Down
6 changes: 3 additions & 3 deletions lib/citrus/file.rb
Expand Up @@ -9,7 +9,7 @@ def module_segments
end

def module_namespace
module_segments[0..-2].inject(Object) do |namespace, constant|
module_segments[0...-1].inject(Object) do |namespace, constant|
constant.empty? ? namespace : namespace.const_get(constant)
end
end
Expand Down Expand Up @@ -311,8 +311,8 @@ def value

rule :star do
all(/[0-9]*/, '*', /[0-9]*/, :space) { |rule|
min = captures[0] == '' ? 0 : captures[0].to_i
max = captures[2] == '' ? Infinity : captures[2].to_i
min = captures[1] == '' ? 0 : captures[1].to_i
max = captures[3] == '' ? Infinity : captures[3].to_i
Repeat.new(rule, min, max)
}
end
Expand Down
1 change: 0 additions & 1 deletion test/match_test.rb
Expand Up @@ -25,7 +25,6 @@ def test_matches
b = Rule.for('b')
c = Rule.for('c')
s = Rule.for([ a, b, c ])
s.name = 's'
r = Repeat.new(s, 0, Infinity)

events = [
Expand Down

0 comments on commit cbdce44

Please sign in to comment.