summaryrefslogtreecommitdiffstats
path: root/lib/rexml/validation/relaxng.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rexml/validation/relaxng.rb')
-rw-r--r--lib/rexml/validation/relaxng.rb559
1 files changed, 559 insertions, 0 deletions
diff --git a/lib/rexml/validation/relaxng.rb b/lib/rexml/validation/relaxng.rb
new file mode 100644
index 000000000..969f51bc9
--- /dev/null
+++ b/lib/rexml/validation/relaxng.rb
@@ -0,0 +1,559 @@
+require "rexml/validation/validation"
+require "rexml/parsers/baseparser"
+
+module REXML
+ module Validation
+ # Implemented:
+ # * empty
+ # * element
+ # * attribute
+ # * text
+ # * optional
+ # * choice
+ # * oneOrMore
+ # * zeroOrMore
+ # * group
+ # * value
+ # * interleave
+ # * mixed
+ # * ref
+ # * grammar
+ # * start
+ # * define
+ #
+ # Not implemented:
+ # * data
+ # * param
+ # * include
+ # * externalRef
+ # * notAllowed
+ # * anyName
+ # * nsName
+ # * except
+ # * name
+ class RelaxNG
+ include Validator
+
+ INFINITY = 1.0 / 0.0
+ EMPTY = Event.new( nil )
+ TEXT = [:start_element, "text"]
+ attr_accessor :current
+ attr_accessor :count
+ attr_reader :references
+
+ # FIXME: Namespaces
+ def initialize source
+ parser = REXML::Parsers::BaseParser.new( source )
+
+ @count = 0
+ @references = {}
+ @root = @current = Sequence.new(self)
+ @root.previous = true
+ states = [ @current ]
+ begin
+ event = parser.pull
+ case event[0]
+ when :start_element
+ case event[1]
+ when "empty"
+ when "element", "attribute", "text", "value"
+ states[-1] << event
+ when "optional"
+ states << Optional.new( self )
+ states[-2] << states[-1]
+ when "choice"
+ states << Choice.new( self )
+ states[-2] << states[-1]
+ when "oneOrMore"
+ states << OneOrMore.new( self )
+ states[-2] << states[-1]
+ when "zeroOrMore"
+ states << ZeroOrMore.new( self )
+ states[-2] << states[-1]
+ when "group"
+ states << Sequence.new( self )
+ states[-2] << states[-1]
+ when "interleave"
+ states << Interleave.new( self )
+ states[-2] << states[-1]
+ when "mixed"
+ states << Interleave.new( self )
+ states[-2] << states[-1]
+ states[-1] << TEXT
+ when "define"
+ states << [ event[2]["name"] ]
+ when "ref"
+ states[-1] << Ref.new( event[2]["name"] )
+ when "anyName"
+ states << AnyName.new( self )
+ states[-2] << states[-1]
+ when "nsName"
+ when "except"
+ when "name"
+ when "data"
+ when "param"
+ when "include"
+ when "grammar"
+ when "start"
+ when "externalRef"
+ when "notAllowed"
+ end
+ when :end_element
+ case event[1]
+ when "element", "attribute"
+ states[-1] << event
+ when "zeroOrMore", "oneOrMore", "choice", "optional",
+ "interleave", "group", "mixed"
+ states.pop
+ when "define"
+ ref = states.pop
+ @references[ ref.shift ] = ref
+ #when "empty"
+ end
+ when :end_document
+ states[-1] << event
+ when :text
+ states[-1] << event
+ end
+ end while event[0] != :end_document
+ end
+
+ def receive event
+ validate( event )
+ end
+ end
+
+ class State
+ def initialize( context )
+ @previous = []
+ @events = []
+ @current = 0
+ @count = context.count += 1
+ @references = context.references
+ @value = false
+ end
+
+ def reset
+ return if @current == 0
+ @current = 0
+ @events.each {|s| s.reset if s.kind_of? State }
+ end
+
+ def previous=( previous )
+ @previous << previous
+ end
+
+ def next( event )
+ #print "In next with #{event.inspect}. "
+ #puts "Next (#@current) is #{@events[@current]}"
+ #p @previous
+ return @previous.pop.next( event ) if @events[@current].nil?
+ expand_ref_in( @events, @current ) if @events[@current].class == Ref
+ if ( @events[@current].kind_of? State )
+ @current += 1
+ @events[@current-1].previous = self
+ return @events[@current-1].next( event )
+ end
+ #puts "Current isn't a state"
+ if ( @events[@current].matches?(event) )
+ @current += 1
+ if @events[@current].nil?
+ #puts "#{inspect[0,5]} 1RETURNING #{@previous.inspect[0,5]}"
+ return @previous.pop
+ elsif @events[@current].kind_of? State
+ @current += 1
+ #puts "#{inspect[0,5]} 2RETURNING (#{@current-1}) #{@events[@current-1].inspect[0,5]}; on return, next is #{@events[@current]}"
+ @events[@current-1].previous = self
+ return @events[@current-1]
+ else
+ #puts "#{inspect[0,5]} RETURNING self w/ next(#@current) = #{@events[@current]}"
+ return self
+ end
+ else
+ return nil
+ end
+ end
+
+ def to_s
+ # Abbreviated:
+ self.class.name =~ /(?:::)(\w)\w+$/
+ # Full:
+ #self.class.name =~ /(?:::)(\w+)$/
+ "#$1.#@count"
+ end
+
+ def inspect
+ "< #{to_s} #{@events.collect{|e|
+ pre = e == @events[@current] ? '#' : ''
+ pre + e.inspect unless self == e
+ }.join(', ')} >"
+ end
+
+ def expected
+ return [@events[@current]]
+ end
+
+ def <<( event )
+ add_event_to_arry( @events, event )
+ end
+
+
+ protected
+ def expand_ref_in( arry, ind )
+ new_events = []
+ @references[ arry[ind].to_s ].each{ |evt|
+ add_event_to_arry(new_events,evt)
+ }
+ arry[ind,1] = new_events
+ end
+
+ def add_event_to_arry( arry, evt )
+ evt = generate_event( evt )
+ if evt.kind_of? String
+ arry[-1].event_arg = evt if arry[-1].kind_of? Event and @value
+ @value = false
+ else
+ arry << evt
+ end
+ end
+
+ def generate_event( event )
+ return event if event.kind_of? State or event.class == Ref
+ evt = nil
+ arg = nil
+ case event[0]
+ when :start_element
+ case event[1]
+ when "element"
+ evt = :start_element
+ arg = event[2]["name"]
+ when "attribute"
+ evt = :start_attribute
+ arg = event[2]["name"]
+ when "text"
+ evt = :text
+ when "value"
+ evt = :text
+ @value = true
+ end
+ when :text
+ return event[1]
+ when :end_document
+ return Event.new( event[0] )
+ else # then :end_element
+ case event[1]
+ when "element"
+ evt = :end_element
+ when "attribute"
+ evt = :end_attribute
+ end
+ end
+ return Event.new( evt, arg )
+ end
+ end
+
+
+ class Sequence < State
+ def matches?(event)
+ @events[@current].matches?( event )
+ end
+ end
+
+
+ class Optional < State
+ def next( event )
+ if @current == 0
+ rv = super
+ return rv if rv
+ @prior = @previous.pop
+ return @prior.next( event )
+ end
+ super
+ end
+
+ def matches?(event)
+ @events[@current].matches?(event) ||
+ (@current == 0 and @previous[-1].matches?(event))
+ end
+
+ def expected
+ return [ @prior.expected, @events[0] ].flatten if @current == 0
+ return [@events[@current]]
+ end
+ end
+
+
+ class ZeroOrMore < Optional
+ def next( event )
+ expand_ref_in( @events, @current ) if @events[@current].class == Ref
+ if ( @events[@current].matches?(event) )
+ @current += 1
+ if @events[@current].nil?
+ @current = 0
+ return self
+ elsif @events[@current].kind_of? State
+ @current += 1
+ @events[@current-1].previous = self
+ return @events[@current-1]
+ else
+ return self
+ end
+ else
+ @prior = @previous.pop
+ return @prior.next( event ) if @current == 0
+ return nil
+ end
+ end
+
+ def expected
+ return [ @prior.expected, @events[0] ].flatten if @current == 0
+ return [@events[@current]]
+ end
+ end
+
+
+ class OneOrMore < State
+ def initialize context
+ super
+ @ord = 0
+ end
+
+ def reset
+ super
+ @ord = 0
+ end
+
+ def next( event )
+ expand_ref_in( @events, @current ) if @events[@current].class == Ref
+ if ( @events[@current].matches?(event) )
+ @current += 1
+ @ord += 1
+ if @events[@current].nil?
+ @current = 0
+ return self
+ elsif @events[@current].kind_of? State
+ @current += 1
+ @events[@current-1].previous = self
+ return @events[@current-1]
+ else
+ return self
+ end
+ else
+ return @previous.pop.next( event ) if @current == 0 and @ord > 0
+ return nil
+ end
+ end
+
+ def matches?( event )
+ @events[@current].matches?(event) ||
+ (@current == 0 and @ord > 0 and @previous[-1].matches?(event))
+ end
+
+ def expected
+ if @current == 0 and @ord > 0
+ return [@previous[-1].expected, @events[0]].flatten
+ else
+ return [@events[@current]]
+ end
+ end
+ end
+
+
+ class Choice < State
+ def initialize context
+ super
+ @choices = []
+ end
+
+ def reset
+ super
+ @events = []
+ @choices.each { |c| c.each { |s| s.reset if s.kind_of? State } }
+ end
+
+ def <<( event )
+ add_event_to_arry( @choices, event )
+ end
+
+ def next( event )
+ # Make the choice if we haven't
+ if @events.size == 0
+ c = 0 ; max = @choices.size
+ while c < max
+ if @choices[c][0].class == Ref
+ expand_ref_in( @choices[c], 0 )
+ @choices += @choices[c]
+ @choices.delete( @choices[c] )
+ max -= 1
+ else
+ c += 1
+ end
+ end
+ @events = @choices.find { |evt| evt[0].matches? event }
+ # Remove the references
+ # Find the events
+ end
+ #puts "In next with #{event.inspect}."
+ #puts "events is #{@events.inspect}"
+ unless @events
+ @events = []
+ return nil
+ end
+ #puts "current = #@current"
+ super
+ end
+
+ def matches?( event )
+ return @events[@current].matches?( event ) if @events.size > 0
+ !@choices.find{|evt| evt[0].matches?(event)}.nil?
+ end
+
+ def expected
+ #puts "IN CHOICE EXPECTED"
+ #puts "EVENTS = #{@events.inspect}"
+ return [@events[@current]] if @events.size > 0
+ return @choices.collect do |x|
+ if x[0].kind_of? State
+ x[0].expected
+ else
+ x[0]
+ end
+ end.flatten
+ end
+
+ def inspect
+ "< #{to_s} #{@choices.collect{|e| e.collect{|f|f.to_s}.join(', ')}.join(' or ')} >"
+ end
+
+ protected
+ def add_event_to_arry( arry, evt )
+ if evt.kind_of? State or evt.class == Ref
+ arry << [evt]
+ elsif evt[0] == :text
+ if arry[-1] and
+ arry[-1][-1].kind_of?( Event ) and
+ arry[-1][-1].event_type == :text and @value
+
+ arry[-1][-1].event_arg = evt[1]
+ @value = false
+ end
+ else
+ arry << [] if evt[0] == :start_element
+ arry[-1] << generate_event( evt )
+ end
+ end
+ end
+
+
+ class Interleave < Choice
+ def initialize context
+ super
+ @choice = 0
+ end
+
+ def reset
+ @choice = 0
+ end
+
+ def next_current( event )
+ # Expand references
+ c = 0 ; max = @choices.size
+ while c < max
+ if @choices[c][0].class == Ref
+ expand_ref_in( @choices[c], 0 )
+ @choices += @choices[c]
+ @choices.delete( @choices[c] )
+ max -= 1
+ else
+ c += 1
+ end
+ end
+ @events = @choices[@choice..-1].find { |evt| evt[0].matches? event }
+ @current = 0
+ if @events
+ # reorder the choices
+ old = @choices[@choice]
+ idx = @choices.index( @events )
+ @choices[@choice] = @events
+ @choices[idx] = old
+ @choice += 1
+ end
+
+ #puts "In next with #{event.inspect}."
+ #puts "events is #{@events.inspect}"
+ @events = [] unless @events
+ end
+
+
+ def next( event )
+ # Find the next series
+ next_current(event) unless @events[@current]
+ return nil unless @events[@current]
+
+ expand_ref_in( @events, @current ) if @events[@current].class == Ref
+ #puts "In next with #{event.inspect}."
+ #puts "Next (#@current) is #{@events[@current]}"
+ if ( @events[@current].kind_of? State )
+ @current += 1
+ @events[@current-1].previous = self
+ return @events[@current-1].next( event )
+ end
+ #puts "Current isn't a state"
+ return @previous.pop.next( event ) if @events[@current].nil?
+ if ( @events[@current].matches?(event) )
+ @current += 1
+ if @events[@current].nil?
+ #puts "#{inspect[0,5]} 1RETURNING self" unless @choices[@choice].nil?
+ return self unless @choices[@choice].nil?
+ #puts "#{inspect[0,5]} 1RETURNING #{@previous[-1].inspect[0,5]}"
+ return @previous.pop
+ elsif @events[@current].kind_of? State
+ @current += 1
+ #puts "#{inspect[0,5]} 2RETURNING (#{@current-1}) #{@events[@current-1].inspect[0,5]}; on return, next is #{@events[@current]}"
+ @events[@current-1].previous = self
+ return @events[@current-1]
+ else
+ #puts "#{inspect[0,5]} RETURNING self w/ next(#@current) = #{@events[@current]}"
+ return self
+ end
+ else
+ return nil
+ end
+ end
+
+ def matches?( event )
+ return @events[@current].matches?( event ) if @events[@current]
+ !@choices[@choice..-1].find{|evt| evt[0].matches?(event)}.nil?
+ end
+
+ def expected
+ #puts "IN CHOICE EXPECTED"
+ #puts "EVENTS = #{@events.inspect}"
+ return [@events[@current]] if @events[@current]
+ return @choices[@choice..-1].collect do |x|
+ if x[0].kind_of? State
+ x[0].expected
+ else
+ x[0]
+ end
+ end.flatten
+ end
+
+ def inspect
+ "< #{to_s} #{@choices.collect{|e| e.collect{|f|f.to_s}.join(', ')}.join(' and ')} >"
+ end
+ end
+
+ class Ref
+ def initialize value
+ @value = value
+ end
+ def to_s
+ @value
+ end
+ def inspect
+ "{#{to_s}}"
+ end
+ end
+ end
+end