This commit is contained in:
@@ -0,0 +1,866 @@
|
||||
# frozen_string_literal: true
|
||||
require_relative '../parseexception'
|
||||
require_relative '../undefinednamespaceexception'
|
||||
require_relative '../security'
|
||||
require_relative '../source'
|
||||
require 'set'
|
||||
require "strscan"
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
unless [].respond_to?(:tally)
|
||||
module EnumerableTally
|
||||
refine Enumerable do
|
||||
def tally
|
||||
counts = {}
|
||||
each do |item|
|
||||
counts[item] ||= 0
|
||||
counts[item] += 1
|
||||
end
|
||||
counts
|
||||
end
|
||||
end
|
||||
end
|
||||
using EnumerableTally
|
||||
end
|
||||
|
||||
if StringScanner::Version < "3.0.8"
|
||||
module StringScannerCaptures
|
||||
refine StringScanner do
|
||||
def captures
|
||||
values_at(*(1...size))
|
||||
end
|
||||
end
|
||||
end
|
||||
using StringScannerCaptures
|
||||
end
|
||||
|
||||
# = Using the Pull Parser
|
||||
# <em>This API is experimental, and subject to change.</em>
|
||||
# parser = PullParser.new( "<a>text<b att='val'/>txet</a>" )
|
||||
# while parser.has_next?
|
||||
# res = parser.next
|
||||
# puts res[1]['att'] if res.start_tag? and res[0] == 'b'
|
||||
# end
|
||||
# See the PullEvent class for information on the content of the results.
|
||||
# The data is identical to the arguments passed for the various events to
|
||||
# the StreamListener API.
|
||||
#
|
||||
# Notice that:
|
||||
# parser = PullParser.new( "<a>BAD DOCUMENT" )
|
||||
# while parser.has_next?
|
||||
# res = parser.next
|
||||
# raise res[1] if res.error?
|
||||
# end
|
||||
#
|
||||
# Nat Price gave me some good ideas for the API.
|
||||
class BaseParser
|
||||
LETTER = '[:alpha:]'
|
||||
DIGIT = '[:digit:]'
|
||||
|
||||
COMBININGCHAR = '' # TODO
|
||||
EXTENDER = '' # TODO
|
||||
|
||||
NCNAME_STR= "[#{LETTER}_][-[:alnum:]._#{COMBININGCHAR}#{EXTENDER}]*"
|
||||
QNAME_STR= "(?:(#{NCNAME_STR}):)?(#{NCNAME_STR})"
|
||||
QNAME = /(#{QNAME_STR})/
|
||||
|
||||
# Just for backward compatibility. For example, kramdown uses this.
|
||||
# It's not used in REXML.
|
||||
UNAME_STR= "(?:#{NCNAME_STR}:)?#{NCNAME_STR}"
|
||||
|
||||
NAMECHAR = '[\-\w\.:]'
|
||||
NAME = "([\\w:]#{NAMECHAR}*)"
|
||||
NMTOKEN = "(?:#{NAMECHAR})+"
|
||||
NMTOKENS = "#{NMTOKEN}(\\s+#{NMTOKEN})*"
|
||||
REFERENCE = "&(?:#{NAME};|#\\d+;|#x[0-9a-fA-F]+;)"
|
||||
REFERENCE_RE = /#{REFERENCE}/
|
||||
|
||||
DOCTYPE_START = /\A\s*<!DOCTYPE\s/um
|
||||
DOCTYPE_END = /\A\s*\]\s*>/um
|
||||
ATTRIBUTE_PATTERN = /\s*(#{QNAME_STR})\s*=\s*(["'])(.*?)\4/um
|
||||
COMMENT_START = /\A<!--/u
|
||||
COMMENT_PATTERN = /<!--(.*?)-->/um
|
||||
CDATA_START = /\A<!\[CDATA\[/u
|
||||
CDATA_END = /\A\s*\]\s*>/um
|
||||
CDATA_PATTERN = /<!\[CDATA\[(.*?)\]\]>/um
|
||||
XMLDECL_START = /\A<\?xml\s/u;
|
||||
XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>/um
|
||||
INSTRUCTION_START = /\A<\?/u
|
||||
INSTRUCTION_PATTERN = /<\?#{NAME}(\s+.*?)?\?>/um
|
||||
TAG_MATCH = /\A<((?>#{QNAME_STR}))/um
|
||||
CLOSE_MATCH = /\A\s*<\/(#{QNAME_STR})\s*>/um
|
||||
|
||||
VERSION = /\bversion\s*=\s*["'](.*?)['"]/um
|
||||
ENCODING = /\bencoding\s*=\s*["'](.*?)['"]/um
|
||||
STANDALONE = /\bstandalone\s*=\s*["'](.*?)['"]/um
|
||||
|
||||
ENTITY_START = /\A\s*<!ENTITY/
|
||||
ELEMENTDECL_START = /\A\s*<!ELEMENT/um
|
||||
ELEMENTDECL_PATTERN = /\A\s*(<!ELEMENT.*?)>/um
|
||||
SYSTEMENTITY = /\A\s*(%.*?;)\s*$/um
|
||||
ENUMERATION = "\\(\\s*#{NMTOKEN}(?:\\s*\\|\\s*#{NMTOKEN})*\\s*\\)"
|
||||
NOTATIONTYPE = "NOTATION\\s+\\(\\s*#{NAME}(?:\\s*\\|\\s*#{NAME})*\\s*\\)"
|
||||
ENUMERATEDTYPE = "(?:(?:#{NOTATIONTYPE})|(?:#{ENUMERATION}))"
|
||||
ATTTYPE = "(CDATA|ID|IDREF|IDREFS|ENTITY|ENTITIES|NMTOKEN|NMTOKENS|#{ENUMERATEDTYPE})"
|
||||
ATTVALUE = "(?:\"((?:[^<&\"]|#{REFERENCE})*)\")|(?:'((?:[^<&']|#{REFERENCE})*)')"
|
||||
DEFAULTDECL = "(#REQUIRED|#IMPLIED|(?:(#FIXED\\s+)?#{ATTVALUE}))"
|
||||
ATTDEF = "\\s+#{NAME}\\s+#{ATTTYPE}\\s+#{DEFAULTDECL}"
|
||||
ATTDEF_RE = /#{ATTDEF}/
|
||||
ATTLISTDECL_START = /\A\s*<!ATTLIST/um
|
||||
ATTLISTDECL_PATTERN = /\A\s*<!ATTLIST\s+#{NAME}(?:#{ATTDEF})*\s*>/um
|
||||
|
||||
TEXT_PATTERN = /\A([^<]*)/um
|
||||
|
||||
# Entity constants
|
||||
PUBIDCHAR = "\x20\x0D\x0Aa-zA-Z0-9\\-()+,./:=?;!*@$_%#"
|
||||
SYSTEMLITERAL = %Q{((?:"[^"]*")|(?:'[^']*'))}
|
||||
PUBIDLITERAL = %Q{("[#{PUBIDCHAR}']*"|'[#{PUBIDCHAR}]*')}
|
||||
EXTERNALID = "(?:(?:(SYSTEM)\\s+#{SYSTEMLITERAL})|(?:(PUBLIC)\\s+#{PUBIDLITERAL}\\s+#{SYSTEMLITERAL}))"
|
||||
NDATADECL = "\\s+NDATA\\s+#{NAME}"
|
||||
PEREFERENCE = "%#{NAME};"
|
||||
ENTITYVALUE = %Q{((?:"(?:[^%&"]|#{PEREFERENCE}|#{REFERENCE})*")|(?:'([^%&']|#{PEREFERENCE}|#{REFERENCE})*'))}
|
||||
PEDEF = "(?:#{ENTITYVALUE}|#{EXTERNALID})"
|
||||
ENTITYDEF = "(?:#{ENTITYVALUE}|(?:#{EXTERNALID}(#{NDATADECL})?))"
|
||||
PEDECL = "<!ENTITY\\s+(%)\\s+#{NAME}\\s+#{PEDEF}\\s*>"
|
||||
GEDECL = "<!ENTITY\\s+#{NAME}\\s+#{ENTITYDEF}\\s*>"
|
||||
ENTITYDECL = /\s*(?:#{GEDECL})|\s*(?:#{PEDECL})/um
|
||||
|
||||
NOTATIONDECL_START = /\A\s*<!NOTATION/um
|
||||
EXTERNAL_ID_PUBLIC = /\A\s*PUBLIC\s+#{PUBIDLITERAL}\s+#{SYSTEMLITERAL}\s*/um
|
||||
EXTERNAL_ID_SYSTEM = /\A\s*SYSTEM\s+#{SYSTEMLITERAL}\s*/um
|
||||
PUBLIC_ID = /\A\s*PUBLIC\s+#{PUBIDLITERAL}\s*/um
|
||||
|
||||
EREFERENCE = /&(?!#{NAME};)/
|
||||
|
||||
DEFAULT_ENTITIES = {
|
||||
'gt' => [/>/, '>', '>', />/],
|
||||
'lt' => [/</, '<', '<', /</],
|
||||
'quot' => [/"/, '"', '"', /"/],
|
||||
"apos" => [/'/, "'", "'", /'/]
|
||||
}
|
||||
|
||||
module Private
|
||||
PEREFERENCE_PATTERN = /#{PEREFERENCE}/um
|
||||
TAG_PATTERN = /((?>#{QNAME_STR}))\s*/um
|
||||
CLOSE_PATTERN = /(#{QNAME_STR})\s*>/um
|
||||
ATTLISTDECL_END = /\s+#{NAME}(?:#{ATTDEF})*\s*>/um
|
||||
NAME_PATTERN = /#{NAME}/um
|
||||
GEDECL_PATTERN = "\\s+#{NAME}\\s+#{ENTITYDEF}\\s*>"
|
||||
PEDECL_PATTERN = "\\s+(%)\\s+#{NAME}\\s+#{PEDEF}\\s*>"
|
||||
ENTITYDECL_PATTERN = /(?:#{GEDECL_PATTERN})|(?:#{PEDECL_PATTERN})/um
|
||||
CARRIAGE_RETURN_NEWLINE_PATTERN = /\r\n?/
|
||||
CHARACTER_REFERENCES = /&#((?:\d+)|(?:x[a-fA-F0-9]+));/
|
||||
DEFAULT_ENTITIES_PATTERNS = {}
|
||||
default_entities = ['gt', 'lt', 'quot', 'apos', 'amp']
|
||||
default_entities.each do |term|
|
||||
DEFAULT_ENTITIES_PATTERNS[term] = /&#{term};/
|
||||
end
|
||||
XML_PREFIXED_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
|
||||
end
|
||||
private_constant :Private
|
||||
|
||||
def initialize( source )
|
||||
self.stream = source
|
||||
@listeners = []
|
||||
@prefixes = Set.new
|
||||
@entity_expansion_count = 0
|
||||
@entity_expansion_limit = Security.entity_expansion_limit
|
||||
@entity_expansion_text_limit = Security.entity_expansion_text_limit
|
||||
@source.ensure_buffer
|
||||
end
|
||||
|
||||
def add_listener( listener )
|
||||
@listeners << listener
|
||||
end
|
||||
|
||||
attr_reader :source
|
||||
attr_reader :entity_expansion_count
|
||||
attr_writer :entity_expansion_limit
|
||||
attr_writer :entity_expansion_text_limit
|
||||
|
||||
def stream=( source )
|
||||
@source = SourceFactory.create_from( source )
|
||||
reset
|
||||
end
|
||||
|
||||
def reset
|
||||
@closed = nil
|
||||
@have_root = false
|
||||
@document_status = nil
|
||||
@tags = []
|
||||
@stack = []
|
||||
@entities = []
|
||||
@namespaces = {"xml" => Private::XML_PREFIXED_NAMESPACE}
|
||||
@namespaces_restore_stack = []
|
||||
end
|
||||
|
||||
def position
|
||||
if @source.respond_to? :position
|
||||
@source.position
|
||||
else
|
||||
# FIXME
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if there are no more events
|
||||
def empty?
|
||||
return (@source.empty? and @stack.empty?)
|
||||
end
|
||||
|
||||
# Returns true if there are more events. Synonymous with !empty?
|
||||
def has_next?
|
||||
return !(@source.empty? and @stack.empty?)
|
||||
end
|
||||
|
||||
# Push an event back on the head of the stream. This method
|
||||
# has (theoretically) infinite depth.
|
||||
def unshift token
|
||||
@stack.unshift(token)
|
||||
end
|
||||
|
||||
# Peek at the +depth+ event in the stack. The first element on the stack
|
||||
# is at depth 0. If +depth+ is -1, will parse to the end of the input
|
||||
# stream and return the last event, which is always :end_document.
|
||||
# Be aware that this causes the stream to be parsed up to the +depth+
|
||||
# event, so you can effectively pre-parse the entire document (pull the
|
||||
# entire thing into memory) using this method.
|
||||
def peek depth=0
|
||||
raise %Q[Illegal argument "#{depth}"] if depth < -1
|
||||
temp = []
|
||||
if depth == -1
|
||||
temp.push(pull()) until empty?
|
||||
else
|
||||
while @stack.size+temp.size < depth+1
|
||||
temp.push(pull())
|
||||
end
|
||||
end
|
||||
@stack += temp if temp.size > 0
|
||||
@stack[depth]
|
||||
end
|
||||
|
||||
# Returns the next event. This is a +PullEvent+ object.
|
||||
def pull
|
||||
@source.drop_parsed_content
|
||||
|
||||
pull_event.tap do |event|
|
||||
@listeners.each do |listener|
|
||||
listener.receive event
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pull_event
|
||||
if @closed
|
||||
x, @closed = @closed, nil
|
||||
return [ :end_element, x ]
|
||||
end
|
||||
if empty?
|
||||
if @document_status == :in_doctype
|
||||
raise ParseException.new("Malformed DOCTYPE: unclosed", @source)
|
||||
end
|
||||
unless @tags.empty?
|
||||
path = "/" + @tags.join("/")
|
||||
raise ParseException.new("Missing end tag for '#{path}'", @source)
|
||||
end
|
||||
return [ :end_document ]
|
||||
end
|
||||
return @stack.shift if @stack.size > 0
|
||||
#STDERR.puts @source.encoding
|
||||
#STDERR.puts "BUFFER = #{@source.buffer.inspect}"
|
||||
|
||||
@source.ensure_buffer
|
||||
if @document_status == nil
|
||||
start_position = @source.position
|
||||
if @source.match?("<?", true)
|
||||
return process_instruction
|
||||
elsif @source.match?("<!", true)
|
||||
if @source.match?("--", true)
|
||||
md = @source.match(/(.*?)-->/um, true)
|
||||
if md.nil?
|
||||
raise REXML::ParseException.new("Unclosed comment", @source)
|
||||
end
|
||||
if /--|-\z/.match?(md[1])
|
||||
raise REXML::ParseException.new("Malformed comment", @source)
|
||||
end
|
||||
return [ :comment, md[1] ]
|
||||
elsif @source.match?("DOCTYPE", true)
|
||||
base_error_message = "Malformed DOCTYPE"
|
||||
unless @source.match?(/\s+/um, true)
|
||||
if @source.match?(">")
|
||||
message = "#{base_error_message}: name is missing"
|
||||
else
|
||||
message = "#{base_error_message}: invalid name"
|
||||
end
|
||||
@source.position = start_position
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
name = parse_name(base_error_message)
|
||||
if @source.match?(/\s*\[/um, true)
|
||||
id = [nil, nil, nil]
|
||||
@document_status = :in_doctype
|
||||
elsif @source.match?(/\s*>/um, true)
|
||||
id = [nil, nil, nil]
|
||||
@document_status = :after_doctype
|
||||
@source.ensure_buffer
|
||||
else
|
||||
id = parse_id(base_error_message,
|
||||
accept_external_id: true,
|
||||
accept_public_id: false)
|
||||
if id[0] == "SYSTEM"
|
||||
# For backward compatibility
|
||||
id[1], id[2] = id[2], nil
|
||||
end
|
||||
if @source.match?(/\s*\[/um, true)
|
||||
@document_status = :in_doctype
|
||||
elsif @source.match?(/\s*>/um, true)
|
||||
@document_status = :after_doctype
|
||||
@source.ensure_buffer
|
||||
else
|
||||
message = "#{base_error_message}: garbage after external ID"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
end
|
||||
args = [:start_doctype, name, *id]
|
||||
if @document_status == :after_doctype
|
||||
@source.match?(/\s*/um, true)
|
||||
@stack << [ :end_doctype ]
|
||||
end
|
||||
return args
|
||||
else
|
||||
message = "Invalid XML"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
end
|
||||
end
|
||||
if @document_status == :in_doctype
|
||||
@source.match?(/\s*/um, true) # skip spaces
|
||||
start_position = @source.position
|
||||
if @source.match?("<!", true)
|
||||
if @source.match?("ELEMENT", true)
|
||||
md = @source.match(/(.*?)>/um, true)
|
||||
raise REXML::ParseException.new( "Bad ELEMENT declaration!", @source ) if md.nil?
|
||||
return [ :elementdecl, "<!ELEMENT" + md[1] ]
|
||||
elsif @source.match?("ENTITY", true)
|
||||
match_data = @source.match(Private::ENTITYDECL_PATTERN, true)
|
||||
unless match_data
|
||||
raise REXML::ParseException.new("Malformed entity declaration", @source)
|
||||
end
|
||||
match = [:entitydecl, *match_data.captures.compact]
|
||||
ref = false
|
||||
if match[1] == '%'
|
||||
ref = true
|
||||
match.delete_at 1
|
||||
end
|
||||
# Now we have to sort out what kind of entity reference this is
|
||||
if match[2] == 'SYSTEM'
|
||||
# External reference
|
||||
match[3] = match[3][1..-2] # PUBID
|
||||
match.delete_at(4) if match.size > 4 # Chop out NDATA decl
|
||||
# match is [ :entity, name, SYSTEM, pubid(, ndata)? ]
|
||||
elsif match[2] == 'PUBLIC'
|
||||
# External reference
|
||||
match[3] = match[3][1..-2] # PUBID
|
||||
match[4] = match[4][1..-2] # HREF
|
||||
match.delete_at(5) if match.size > 5 # Chop out NDATA decl
|
||||
# match is [ :entity, name, PUBLIC, pubid, href(, ndata)? ]
|
||||
elsif Private::PEREFERENCE_PATTERN.match?(match[2])
|
||||
raise REXML::ParseException.new("Parameter entity references forbidden in internal subset: #{match[2]}", @source)
|
||||
else
|
||||
match[2] = match[2][1..-2]
|
||||
match.pop if match.size == 4
|
||||
# match is [ :entity, name, value ]
|
||||
end
|
||||
match << '%' if ref
|
||||
return match
|
||||
elsif @source.match?("ATTLIST", true)
|
||||
md = @source.match(Private::ATTLISTDECL_END, true)
|
||||
raise REXML::ParseException.new( "Bad ATTLIST declaration!", @source ) if md.nil?
|
||||
element = md[1]
|
||||
contents = md[0]
|
||||
|
||||
pairs = {}
|
||||
values = md[0].strip.scan( ATTDEF_RE )
|
||||
values.each do |attdef|
|
||||
unless attdef[3] == "#IMPLIED"
|
||||
attdef.compact!
|
||||
val = attdef[3]
|
||||
val = attdef[4] if val == "#FIXED "
|
||||
pairs[attdef[0]] = val
|
||||
if attdef[0] =~ /^xmlns:(.*)/
|
||||
@namespaces[$1] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
return [ :attlistdecl, element, pairs, contents ]
|
||||
elsif @source.match?("NOTATION", true)
|
||||
base_error_message = "Malformed notation declaration"
|
||||
unless @source.match?(/\s+/um, true)
|
||||
if @source.match?(">")
|
||||
message = "#{base_error_message}: name is missing"
|
||||
else
|
||||
message = "#{base_error_message}: invalid name"
|
||||
end
|
||||
@source.position = start_position
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
name = parse_name(base_error_message)
|
||||
id = parse_id(base_error_message,
|
||||
accept_external_id: true,
|
||||
accept_public_id: true)
|
||||
unless @source.match?(/\s*>/um, true)
|
||||
message = "#{base_error_message}: garbage before end >"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
return [:notationdecl, name, *id]
|
||||
elsif md = @source.match(/--(.*?)-->/um, true)
|
||||
case md[1]
|
||||
when /--/, /-\z/
|
||||
raise REXML::ParseException.new("Malformed comment", @source)
|
||||
end
|
||||
return [ :comment, md[1] ] if md
|
||||
end
|
||||
elsif match = @source.match(/(%.*?;)\s*/um, true)
|
||||
return [ :externalentity, match[1] ]
|
||||
elsif @source.match?(/\]\s*>/um, true)
|
||||
@document_status = :after_doctype
|
||||
return [ :end_doctype ]
|
||||
end
|
||||
if @document_status == :in_doctype
|
||||
raise ParseException.new("Malformed DOCTYPE: invalid declaration", @source)
|
||||
end
|
||||
end
|
||||
if @document_status == :after_doctype
|
||||
@source.match?(/\s*/um, true)
|
||||
end
|
||||
begin
|
||||
start_position = @source.position
|
||||
if @source.match?("<", true)
|
||||
# :text's read_until may remain only "<" in buffer. In the
|
||||
# case, buffer is empty here. So we need to fill buffer
|
||||
# here explicitly.
|
||||
@source.ensure_buffer
|
||||
if @source.match?("/", true)
|
||||
@namespaces_restore_stack.pop
|
||||
last_tag = @tags.pop
|
||||
md = @source.match(Private::CLOSE_PATTERN, true)
|
||||
if md and !last_tag
|
||||
message = "Unexpected top-level end tag (got '#{md[1]}')"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
if md.nil? or last_tag != md[1]
|
||||
message = "Missing end tag for '#{last_tag}'"
|
||||
message += " (got '#{md[1]}')" if md
|
||||
@source.position = start_position if md.nil?
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
return [ :end_element, last_tag ]
|
||||
elsif @source.match?("!", true)
|
||||
md = @source.match(/([^>]*>)/um)
|
||||
#STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}"
|
||||
raise REXML::ParseException.new("Malformed node", @source) unless md
|
||||
if md[0][0] == ?-
|
||||
md = @source.match(/--(.*?)-->/um, true)
|
||||
|
||||
if md.nil? || /--|-\z/.match?(md[1])
|
||||
raise REXML::ParseException.new("Malformed comment", @source)
|
||||
end
|
||||
|
||||
return [ :comment, md[1] ]
|
||||
else
|
||||
md = @source.match(/\[CDATA\[(.*?)\]\]>/um, true)
|
||||
return [ :cdata, md[1] ] if md
|
||||
end
|
||||
raise REXML::ParseException.new( "Declarations can only occur "+
|
||||
"in the doctype declaration.", @source)
|
||||
elsif @source.match?("?", true)
|
||||
return process_instruction
|
||||
else
|
||||
# Get the next tag
|
||||
md = @source.match(Private::TAG_PATTERN, true)
|
||||
unless md
|
||||
@source.position = start_position
|
||||
raise REXML::ParseException.new("malformed XML: missing tag start", @source)
|
||||
end
|
||||
tag = md[1]
|
||||
@document_status = :in_element
|
||||
@prefixes.clear
|
||||
@prefixes << md[2] if md[2]
|
||||
push_namespaces_restore
|
||||
attributes, closed = parse_attributes(@prefixes)
|
||||
# Verify that all of the prefixes have been defined
|
||||
for prefix in @prefixes
|
||||
unless @namespaces.key?(prefix)
|
||||
raise UndefinedNamespaceException.new(prefix,@source,self)
|
||||
end
|
||||
end
|
||||
|
||||
if closed
|
||||
@closed = tag
|
||||
pop_namespaces_restore
|
||||
else
|
||||
if @tags.empty? and @have_root
|
||||
raise ParseException.new("Malformed XML: Extra tag at the end of the document (got '<#{tag}')", @source)
|
||||
end
|
||||
@tags.push( tag )
|
||||
end
|
||||
@have_root = true
|
||||
return [ :start_element, tag, attributes ]
|
||||
end
|
||||
else
|
||||
text = @source.read_until("<")
|
||||
if text.chomp!("<")
|
||||
@source.position -= "<".bytesize
|
||||
end
|
||||
if @tags.empty?
|
||||
unless /\A\s*\z/.match?(text)
|
||||
if @have_root
|
||||
raise ParseException.new("Malformed XML: Extra content at the end of the document (got '#{text}')", @source)
|
||||
else
|
||||
raise ParseException.new("Malformed XML: Content at the start of the document (got '#{text}')", @source)
|
||||
end
|
||||
end
|
||||
return pull_event if @have_root
|
||||
end
|
||||
return [ :text, text ]
|
||||
end
|
||||
rescue REXML::UndefinedNamespaceException
|
||||
raise
|
||||
rescue REXML::ParseException
|
||||
raise
|
||||
rescue => error
|
||||
raise REXML::ParseException.new( "Exception parsing",
|
||||
@source, self, (error ? error : $!) )
|
||||
end
|
||||
return [ :dummy ]
|
||||
end
|
||||
private :pull_event
|
||||
|
||||
def entity( reference, entities )
|
||||
return unless entities
|
||||
|
||||
value = entities[ reference ]
|
||||
return if value.nil?
|
||||
|
||||
record_entity_expansion
|
||||
unnormalize( value, entities )
|
||||
end
|
||||
|
||||
# Escapes all possible entities
|
||||
def normalize( input, entities=nil, entity_filter=nil )
|
||||
copy = input.clone
|
||||
# Doing it like this rather than in a loop improves the speed
|
||||
copy.gsub!( EREFERENCE, '&' )
|
||||
entities.each do |key, value|
|
||||
copy.gsub!( value, "&#{key};" ) unless entity_filter and
|
||||
entity_filter.include?(entity)
|
||||
end if entities
|
||||
copy.gsub!( EREFERENCE, '&' )
|
||||
DEFAULT_ENTITIES.each do |key, value|
|
||||
copy.gsub!( value[3], value[1] )
|
||||
end
|
||||
copy
|
||||
end
|
||||
|
||||
# Unescapes all possible entities
|
||||
def unnormalize( string, entities=nil, filter=nil )
|
||||
if string.include?("\r")
|
||||
rv = string.gsub( Private::CARRIAGE_RETURN_NEWLINE_PATTERN, "\n" )
|
||||
else
|
||||
rv = string.dup
|
||||
end
|
||||
matches = rv.scan( REFERENCE_RE )
|
||||
return rv if matches.size == 0
|
||||
rv.gsub!( Private::CHARACTER_REFERENCES ) {
|
||||
m=$1
|
||||
if m.start_with?("x")
|
||||
code_point = Integer(m[1..-1], 16)
|
||||
else
|
||||
code_point = Integer(m, 10)
|
||||
end
|
||||
[code_point].pack('U*')
|
||||
}
|
||||
matches.collect!{|x|x[0]}.compact!
|
||||
if filter
|
||||
matches.reject! do |entity_reference|
|
||||
filter.include?(entity_reference)
|
||||
end
|
||||
end
|
||||
if matches.size > 0
|
||||
matches.tally.each do |entity_reference, n|
|
||||
entity_expansion_count_before = @entity_expansion_count
|
||||
entity_value = entity( entity_reference, entities )
|
||||
if entity_value
|
||||
if n > 1
|
||||
entity_expansion_count_delta =
|
||||
@entity_expansion_count - entity_expansion_count_before
|
||||
record_entity_expansion(entity_expansion_count_delta * (n - 1))
|
||||
end
|
||||
re = Private::DEFAULT_ENTITIES_PATTERNS[entity_reference] || /&#{entity_reference};/
|
||||
rv.gsub!( re, entity_value )
|
||||
if rv.bytesize > @entity_expansion_text_limit
|
||||
raise "entity expansion has grown too large"
|
||||
end
|
||||
else
|
||||
er = DEFAULT_ENTITIES[entity_reference]
|
||||
rv.gsub!( er[0], er[2] ) if er
|
||||
end
|
||||
end
|
||||
rv.gsub!( Private::DEFAULT_ENTITIES_PATTERNS['amp'], '&' )
|
||||
end
|
||||
rv
|
||||
end
|
||||
|
||||
private
|
||||
def add_namespace(prefix, uri)
|
||||
@namespaces_restore_stack.last[prefix] = @namespaces[prefix]
|
||||
if uri.nil?
|
||||
@namespaces.delete(prefix)
|
||||
else
|
||||
@namespaces[prefix] = uri
|
||||
end
|
||||
end
|
||||
|
||||
def push_namespaces_restore
|
||||
namespaces_restore = {}
|
||||
@namespaces_restore_stack.push(namespaces_restore)
|
||||
namespaces_restore
|
||||
end
|
||||
|
||||
def pop_namespaces_restore
|
||||
namespaces_restore = @namespaces_restore_stack.pop
|
||||
namespaces_restore.each do |prefix, uri|
|
||||
if uri.nil?
|
||||
@namespaces.delete(prefix)
|
||||
else
|
||||
@namespaces[prefix] = uri
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def record_entity_expansion(delta=1)
|
||||
@entity_expansion_count += delta
|
||||
if @entity_expansion_count > @entity_expansion_limit
|
||||
raise "number of entity expansions exceeded, processing aborted."
|
||||
end
|
||||
end
|
||||
|
||||
def need_source_encoding_update?(xml_declaration_encoding)
|
||||
return false if xml_declaration_encoding.nil?
|
||||
return false if /\AUTF-16\z/i =~ xml_declaration_encoding
|
||||
true
|
||||
end
|
||||
|
||||
def parse_name(base_error_message)
|
||||
md = @source.match(Private::NAME_PATTERN, true)
|
||||
unless md
|
||||
if @source.match?(/\S/um)
|
||||
message = "#{base_error_message}: invalid name"
|
||||
else
|
||||
message = "#{base_error_message}: name is missing"
|
||||
end
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
md[0]
|
||||
end
|
||||
|
||||
def parse_id(base_error_message,
|
||||
accept_external_id:,
|
||||
accept_public_id:)
|
||||
if accept_external_id and (md = @source.match(EXTERNAL_ID_PUBLIC, true))
|
||||
pubid = system = nil
|
||||
pubid_literal = md[1]
|
||||
pubid = pubid_literal[1..-2] if pubid_literal # Remove quote
|
||||
system_literal = md[2]
|
||||
system = system_literal[1..-2] if system_literal # Remove quote
|
||||
["PUBLIC", pubid, system]
|
||||
elsif accept_public_id and (md = @source.match(PUBLIC_ID, true))
|
||||
pubid = system = nil
|
||||
pubid_literal = md[1]
|
||||
pubid = pubid_literal[1..-2] if pubid_literal # Remove quote
|
||||
["PUBLIC", pubid, nil]
|
||||
elsif accept_external_id and (md = @source.match(EXTERNAL_ID_SYSTEM, true))
|
||||
system = nil
|
||||
system_literal = md[1]
|
||||
system = system_literal[1..-2] if system_literal # Remove quote
|
||||
["SYSTEM", nil, system]
|
||||
else
|
||||
details = parse_id_invalid_details(accept_external_id: accept_external_id,
|
||||
accept_public_id: accept_public_id)
|
||||
message = "#{base_error_message}: #{details}"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_id_invalid_details(accept_external_id:,
|
||||
accept_public_id:)
|
||||
public = /\A\s*PUBLIC/um
|
||||
system = /\A\s*SYSTEM/um
|
||||
if (accept_external_id or accept_public_id) and @source.match?(/#{public}/um)
|
||||
if @source.match?(/#{public}(?:\s+[^'"]|\s*[\[>])/um)
|
||||
return "public ID literal is missing"
|
||||
end
|
||||
unless @source.match?(/#{public}\s+#{PUBIDLITERAL}/um)
|
||||
return "invalid public ID literal"
|
||||
end
|
||||
if accept_public_id
|
||||
if @source.match?(/#{public}\s+#{PUBIDLITERAL}\s+[^'"]/um)
|
||||
return "system ID literal is missing"
|
||||
end
|
||||
unless @source.match?(/#{public}\s+#{PUBIDLITERAL}\s+#{SYSTEMLITERAL}/um)
|
||||
return "invalid system literal"
|
||||
end
|
||||
"garbage after system literal"
|
||||
else
|
||||
"garbage after public ID literal"
|
||||
end
|
||||
elsif accept_external_id and @source.match?(/#{system}/um)
|
||||
if @source.match?(/#{system}(?:\s+[^'"]|\s*[\[>])/um)
|
||||
return "system literal is missing"
|
||||
end
|
||||
unless @source.match?(/#{system}\s+#{SYSTEMLITERAL}/um)
|
||||
return "invalid system literal"
|
||||
end
|
||||
"garbage after system literal"
|
||||
else
|
||||
unless @source.match?(/\A\s*(?:PUBLIC|SYSTEM)\s/um)
|
||||
return "invalid ID type"
|
||||
end
|
||||
"ID type is missing"
|
||||
end
|
||||
end
|
||||
|
||||
def process_instruction
|
||||
name = parse_name("Malformed XML: Invalid processing instruction node")
|
||||
if @source.match?(/\s+/um, true)
|
||||
match_data = @source.match(/(.*?)\?>/um, true)
|
||||
unless match_data
|
||||
raise ParseException.new("Malformed XML: Unclosed processing instruction", @source)
|
||||
end
|
||||
content = match_data[1]
|
||||
else
|
||||
content = nil
|
||||
unless @source.match?("?>", true)
|
||||
raise ParseException.new("Malformed XML: Unclosed processing instruction", @source)
|
||||
end
|
||||
end
|
||||
if name == "xml"
|
||||
if @document_status
|
||||
raise ParseException.new("Malformed XML: XML declaration is not at the start", @source)
|
||||
end
|
||||
version = VERSION.match(content)
|
||||
version = version[1] unless version.nil?
|
||||
encoding = ENCODING.match(content)
|
||||
encoding = encoding[1] unless encoding.nil?
|
||||
if need_source_encoding_update?(encoding)
|
||||
@source.encoding = encoding
|
||||
end
|
||||
if encoding.nil? and /\AUTF-16(?:BE|LE)\z/i =~ @source.encoding
|
||||
encoding = "UTF-16"
|
||||
end
|
||||
standalone = STANDALONE.match(content)
|
||||
standalone = standalone[1] unless standalone.nil?
|
||||
return [ :xmldecl, version, encoding, standalone ]
|
||||
end
|
||||
[:processing_instruction, name, content]
|
||||
end
|
||||
|
||||
def parse_attributes(prefixes)
|
||||
attributes = {}
|
||||
expanded_names = {}
|
||||
closed = false
|
||||
while true
|
||||
if @source.match?(">", true)
|
||||
return attributes, closed
|
||||
elsif @source.match?("/>", true)
|
||||
closed = true
|
||||
return attributes, closed
|
||||
elsif match = @source.match(QNAME, true)
|
||||
name = match[1]
|
||||
prefix = match[2]
|
||||
local_part = match[3]
|
||||
|
||||
unless @source.match?(/\s*=\s*/um, true)
|
||||
message = "Missing attribute equal: <#{name}>"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
unless match = @source.match(/(['"])/, true)
|
||||
message = "Missing attribute value start quote: <#{name}>"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
quote = match[1]
|
||||
start_position = @source.position
|
||||
value = @source.read_until(quote)
|
||||
unless value.chomp!(quote)
|
||||
@source.position = start_position
|
||||
message = "Missing attribute value end quote: <#{name}>: <#{quote}>"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
@source.match?(/\s*/um, true)
|
||||
if prefix == "xmlns"
|
||||
if local_part == "xml"
|
||||
if value != Private::XML_PREFIXED_NAMESPACE
|
||||
msg = "The 'xml' prefix must not be bound to any other namespace "+
|
||||
"(http://www.w3.org/TR/REC-xml-names/#ns-decl)"
|
||||
raise REXML::ParseException.new( msg, @source, self )
|
||||
end
|
||||
elsif local_part == "xmlns"
|
||||
msg = "The 'xmlns' prefix must not be declared "+
|
||||
"(http://www.w3.org/TR/REC-xml-names/#ns-decl)"
|
||||
raise REXML::ParseException.new( msg, @source, self)
|
||||
end
|
||||
add_namespace(local_part, value)
|
||||
elsif prefix
|
||||
prefixes << prefix unless prefix == "xml"
|
||||
end
|
||||
|
||||
if attributes[name]
|
||||
msg = "Duplicate attribute #{name.inspect}"
|
||||
raise REXML::ParseException.new(msg, @source, self)
|
||||
end
|
||||
|
||||
unless prefix == "xmlns"
|
||||
uri = @namespaces[prefix]
|
||||
expanded_name = [uri, local_part]
|
||||
existing_prefix = expanded_names[expanded_name]
|
||||
if existing_prefix
|
||||
message = "Namespace conflict in adding attribute " +
|
||||
"\"#{local_part}\": " +
|
||||
"Prefix \"#{existing_prefix}\" = \"#{uri}\" and " +
|
||||
"prefix \"#{prefix}\" = \"#{uri}\""
|
||||
raise REXML::ParseException.new(message, @source, self)
|
||||
end
|
||||
expanded_names[expanded_name] = prefix
|
||||
end
|
||||
|
||||
attributes[name] = value
|
||||
else
|
||||
message = "Invalid attribute name: <#{@source.buffer.split(%r{[/>\s]}).first}>"
|
||||
raise REXML::ParseException.new(message, @source)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
=begin
|
||||
case event[0]
|
||||
when :start_element
|
||||
when :text
|
||||
when :end_element
|
||||
when :processing_instruction
|
||||
when :cdata
|
||||
when :comment
|
||||
when :xmldecl
|
||||
when :start_doctype
|
||||
when :end_doctype
|
||||
when :externalentity
|
||||
when :elementdecl
|
||||
when :entity
|
||||
when :attlistdecl
|
||||
when :notationdecl
|
||||
when :end_doctype
|
||||
end
|
||||
=end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: false
|
||||
require_relative 'streamparser'
|
||||
require_relative 'baseparser'
|
||||
require_relative '../light/node'
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
class LightParser
|
||||
def initialize stream
|
||||
@stream = stream
|
||||
@parser = REXML::Parsers::BaseParser.new( stream )
|
||||
end
|
||||
|
||||
def add_listener( listener )
|
||||
@parser.add_listener( listener )
|
||||
end
|
||||
|
||||
def rewind
|
||||
@stream.rewind
|
||||
@parser.stream = @stream
|
||||
end
|
||||
|
||||
def parse
|
||||
root = context = [ :document ]
|
||||
while true
|
||||
event = @parser.pull
|
||||
case event[0]
|
||||
when :end_document
|
||||
break
|
||||
when :start_element, :start_doctype
|
||||
new_node = event
|
||||
context << new_node
|
||||
new_node[1,0] = [context]
|
||||
context = new_node
|
||||
when :end_element, :end_doctype
|
||||
context = context[1]
|
||||
else
|
||||
new_node = event
|
||||
context << new_node
|
||||
new_node[1,0] = [context]
|
||||
end
|
||||
end
|
||||
root
|
||||
end
|
||||
end
|
||||
|
||||
# An element is an array. The array contains:
|
||||
# 0 The parent element
|
||||
# 1 The tag name
|
||||
# 2 A hash of attributes
|
||||
# 3..-1 The child elements
|
||||
# An element is an array of size > 3
|
||||
# Text is a String
|
||||
# PIs are [ :processing_instruction, target, data ]
|
||||
# Comments are [ :comment, data ]
|
||||
# DocTypes are DocType structs
|
||||
# The root is an array with XMLDecls, Text, DocType, Array, Text
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,213 @@
|
||||
# frozen_string_literal: false
|
||||
require 'forwardable'
|
||||
|
||||
require_relative '../parseexception'
|
||||
require_relative 'baseparser'
|
||||
require_relative '../xmltokens'
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
# = Using the Pull Parser
|
||||
# <em>This API is experimental, and subject to change.</em>
|
||||
# parser = PullParser.new( "<a>text<b att='val'/>txet</a>" )
|
||||
# while parser.has_next?
|
||||
# res = parser.next
|
||||
# puts res[1]['att'] if res.start_tag? and res[0] == 'b'
|
||||
# end
|
||||
# See the PullEvent class for information on the content of the results.
|
||||
# The data is identical to the arguments passed for the various events to
|
||||
# the StreamListener API.
|
||||
#
|
||||
# Notice that:
|
||||
# parser = PullParser.new( "<a>BAD DOCUMENT" )
|
||||
# while parser.has_next?
|
||||
# res = parser.next
|
||||
# raise res[1] if res.error?
|
||||
# end
|
||||
#
|
||||
# Nat Price gave me some good ideas for the API.
|
||||
class PullParser
|
||||
include XMLTokens
|
||||
extend Forwardable
|
||||
|
||||
def_delegators( :@parser, :has_next? )
|
||||
def_delegators( :@parser, :entity )
|
||||
def_delegators( :@parser, :empty? )
|
||||
def_delegators( :@parser, :source )
|
||||
|
||||
def initialize stream
|
||||
@entities = {}
|
||||
@listeners = nil
|
||||
@parser = BaseParser.new( stream )
|
||||
@my_stack = []
|
||||
end
|
||||
|
||||
def add_listener( listener )
|
||||
@listeners = [] unless @listeners
|
||||
@listeners << listener
|
||||
end
|
||||
|
||||
def entity_expansion_count
|
||||
@parser.entity_expansion_count
|
||||
end
|
||||
|
||||
def entity_expansion_limit=( limit )
|
||||
@parser.entity_expansion_limit = limit
|
||||
end
|
||||
|
||||
def entity_expansion_text_limit=( limit )
|
||||
@parser.entity_expansion_text_limit = limit
|
||||
end
|
||||
|
||||
def each
|
||||
while has_next?
|
||||
yield self.pull
|
||||
end
|
||||
end
|
||||
|
||||
def peek depth=0
|
||||
if @my_stack.length <= depth
|
||||
(depth - @my_stack.length + 1).times {
|
||||
e = PullEvent.new(@parser.pull)
|
||||
@my_stack.push(e)
|
||||
}
|
||||
end
|
||||
@my_stack[depth]
|
||||
end
|
||||
|
||||
def pull
|
||||
return @my_stack.shift if @my_stack.length > 0
|
||||
|
||||
event = @parser.pull
|
||||
case event[0]
|
||||
when :entitydecl
|
||||
@entities[ event[1] ] =
|
||||
event[2] unless event[2] =~ /PUBLIC|SYSTEM/
|
||||
when :text
|
||||
unnormalized = @parser.unnormalize( event[1], @entities )
|
||||
event << unnormalized
|
||||
end
|
||||
PullEvent.new( event )
|
||||
end
|
||||
|
||||
def unshift token
|
||||
@my_stack.unshift token
|
||||
end
|
||||
|
||||
def reset
|
||||
@parser.reset
|
||||
end
|
||||
end
|
||||
|
||||
# A parsing event. The contents of the event are accessed as an +Array?,
|
||||
# and the type is given either by the ...? methods, or by accessing the
|
||||
# +type+ accessor. The contents of this object vary from event to event,
|
||||
# but are identical to the arguments passed to +StreamListener+s for each
|
||||
# event.
|
||||
class PullEvent
|
||||
# The type of this event. Will be one of :tag_start, :tag_end, :text,
|
||||
# :processing_instruction, :comment, :doctype, :attlistdecl, :entitydecl,
|
||||
# :notationdecl, :entity, :cdata, :xmldecl, or :error.
|
||||
def initialize(arg)
|
||||
@contents = arg
|
||||
end
|
||||
|
||||
def []( start, endd=nil)
|
||||
if start.kind_of? Range
|
||||
@contents.slice( start.begin+1 .. start.end )
|
||||
elsif start.kind_of? Numeric
|
||||
if endd.nil?
|
||||
@contents.slice( start+1 )
|
||||
else
|
||||
@contents.slice( start+1, endd )
|
||||
end
|
||||
else
|
||||
raise "Illegal argument #{start.inspect} (#{start.class})"
|
||||
end
|
||||
end
|
||||
|
||||
def event_type
|
||||
@contents[0]
|
||||
end
|
||||
|
||||
# Content: [ String tag_name, Hash attributes ]
|
||||
def start_element?
|
||||
@contents[0] == :start_element
|
||||
end
|
||||
|
||||
# Content: [ String tag_name ]
|
||||
def end_element?
|
||||
@contents[0] == :end_element
|
||||
end
|
||||
|
||||
# Content: [ String raw_text, String unnormalized_text ]
|
||||
def text?
|
||||
@contents[0] == :text
|
||||
end
|
||||
|
||||
# Content: [ String text ]
|
||||
def instruction?
|
||||
@contents[0] == :processing_instruction
|
||||
end
|
||||
|
||||
# Content: [ String text ]
|
||||
def comment?
|
||||
@contents[0] == :comment
|
||||
end
|
||||
|
||||
# Content: [ String name, String pub_sys, String long_name, String uri ]
|
||||
def doctype?
|
||||
@contents[0] == :start_doctype
|
||||
end
|
||||
|
||||
# Content: [ String text ]
|
||||
def attlistdecl?
|
||||
@contents[0] == :attlistdecl
|
||||
end
|
||||
|
||||
# Content: [ String text ]
|
||||
def elementdecl?
|
||||
@contents[0] == :elementdecl
|
||||
end
|
||||
|
||||
# Due to the wonders of DTDs, an entity declaration can be just about
|
||||
# anything. There's no way to normalize it; you'll have to interpret the
|
||||
# content yourself. However, the following is true:
|
||||
#
|
||||
# * If the entity declaration is an internal entity:
|
||||
# [ String name, String value ]
|
||||
# Content: [ String text ]
|
||||
def entitydecl?
|
||||
@contents[0] == :entitydecl
|
||||
end
|
||||
|
||||
# Content: [ String text ]
|
||||
def notationdecl?
|
||||
@contents[0] == :notationdecl
|
||||
end
|
||||
|
||||
# Content: [ String text ]
|
||||
def entity?
|
||||
@contents[0] == :entity
|
||||
end
|
||||
|
||||
# Content: [ String text ]
|
||||
def cdata?
|
||||
@contents[0] == :cdata
|
||||
end
|
||||
|
||||
# Content: [ String version, String encoding, String standalone ]
|
||||
def xmldecl?
|
||||
@contents[0] == :xmldecl
|
||||
end
|
||||
|
||||
def error?
|
||||
@contents[0] == :error
|
||||
end
|
||||
|
||||
def inspect
|
||||
@contents[0].to_s + ": " + @contents[1..-1].inspect
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,270 @@
|
||||
# frozen_string_literal: false
|
||||
require_relative 'baseparser'
|
||||
require_relative '../parseexception'
|
||||
require_relative '../namespace'
|
||||
require_relative '../text'
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
# SAX2Parser
|
||||
class SAX2Parser
|
||||
def initialize source
|
||||
@parser = BaseParser.new(source)
|
||||
@listeners = []
|
||||
@procs = []
|
||||
@namespace_stack = []
|
||||
@has_listeners = false
|
||||
@tag_stack = []
|
||||
@entities = {}
|
||||
end
|
||||
|
||||
def source
|
||||
@parser.source
|
||||
end
|
||||
|
||||
def entity_expansion_count
|
||||
@parser.entity_expansion_count
|
||||
end
|
||||
|
||||
def entity_expansion_limit=( limit )
|
||||
@parser.entity_expansion_limit = limit
|
||||
end
|
||||
|
||||
def entity_expansion_text_limit=( limit )
|
||||
@parser.entity_expansion_text_limit = limit
|
||||
end
|
||||
|
||||
def add_listener( listener )
|
||||
@parser.add_listener( listener )
|
||||
end
|
||||
|
||||
# Listen arguments:
|
||||
#
|
||||
# Symbol, Array, Block
|
||||
# Listen to Symbol events on Array elements
|
||||
# Symbol, Block
|
||||
# Listen to Symbol events
|
||||
# Array, Listener
|
||||
# Listen to all events on Array elements
|
||||
# Array, Block
|
||||
# Listen to :start_element events on Array elements
|
||||
# Listener
|
||||
# Listen to All events
|
||||
#
|
||||
# Symbol can be one of: :start_element, :end_element,
|
||||
# :start_prefix_mapping, :end_prefix_mapping, :characters,
|
||||
# :processing_instruction, :doctype, :attlistdecl, :elementdecl,
|
||||
# :entitydecl, :notationdecl, :cdata, :xmldecl, :comment
|
||||
#
|
||||
# There is an additional symbol that can be listened for: :progress.
|
||||
# This will be called for every event generated, passing in the current
|
||||
# stream position.
|
||||
#
|
||||
# Array contains regular expressions or strings which will be matched
|
||||
# against fully qualified element names.
|
||||
#
|
||||
# Listener must implement the methods in SAX2Listener
|
||||
#
|
||||
# Block will be passed the same arguments as a SAX2Listener method would
|
||||
# be, where the method name is the same as the matched Symbol.
|
||||
# See the SAX2Listener for more information.
|
||||
def listen( *args, &blok )
|
||||
if args[0].kind_of? Symbol
|
||||
if args.size == 2
|
||||
args[1].each { |match| @procs << [args[0], match, blok] }
|
||||
else
|
||||
add( [args[0], nil, blok] )
|
||||
end
|
||||
elsif args[0].kind_of? Array
|
||||
if args.size == 2
|
||||
args[0].each { |match| add( [nil, match, args[1]] ) }
|
||||
else
|
||||
args[0].each { |match| add( [ :start_element, match, blok ] ) }
|
||||
end
|
||||
else
|
||||
add([nil, nil, args[0]])
|
||||
end
|
||||
end
|
||||
|
||||
def deafen( listener=nil, &blok )
|
||||
if listener
|
||||
@listeners.delete_if {|item| item[-1] == listener }
|
||||
@has_listeners = false if @listeners.size == 0
|
||||
else
|
||||
@procs.delete_if {|item| item[-1] == blok }
|
||||
end
|
||||
end
|
||||
|
||||
def parse
|
||||
@procs.each { |sym,match,block| block.call if sym == :start_document }
|
||||
@listeners.each { |sym,match,block|
|
||||
block.start_document if sym == :start_document or sym.nil?
|
||||
}
|
||||
context = []
|
||||
while true
|
||||
event = @parser.pull
|
||||
case event[0]
|
||||
when :end_document
|
||||
handle( :end_document )
|
||||
break
|
||||
when :start_doctype
|
||||
handle( :doctype, *event[1..-1])
|
||||
when :end_doctype
|
||||
context = context[1]
|
||||
when :start_element
|
||||
@tag_stack.push(event[1])
|
||||
# find the observers for namespaces
|
||||
procs = get_procs( :start_prefix_mapping, event[1] )
|
||||
listeners = get_listeners( :start_prefix_mapping, event[1] )
|
||||
if procs or listeners
|
||||
# break out the namespace declarations
|
||||
# The attributes live in event[2]
|
||||
event[2].each {|n, v| event[2][n] = @parser.normalize(v)}
|
||||
nsdecl = event[2].find_all { |n, value| n =~ /^xmlns(:|$)/ }
|
||||
nsdecl.collect! { |n, value| [ n[6..-1], value ] }
|
||||
@namespace_stack.push({})
|
||||
nsdecl.each do |n,v|
|
||||
@namespace_stack[-1][n] = v
|
||||
# notify observers of namespaces
|
||||
procs.each { |ob| ob.call( n, v ) } if procs
|
||||
listeners.each { |ob| ob.start_prefix_mapping(n, v) } if listeners
|
||||
end
|
||||
end
|
||||
event[1] =~ Namespace::NAMESPLIT
|
||||
prefix = $1
|
||||
local = $2
|
||||
uri = get_namespace(prefix)
|
||||
# find the observers for start_element
|
||||
procs = get_procs( :start_element, event[1] )
|
||||
listeners = get_listeners( :start_element, event[1] )
|
||||
# notify observers
|
||||
procs.each { |ob| ob.call( uri, local, event[1], event[2] ) } if procs
|
||||
listeners.each { |ob|
|
||||
ob.start_element( uri, local, event[1], event[2] )
|
||||
} if listeners
|
||||
when :end_element
|
||||
@tag_stack.pop
|
||||
event[1] =~ Namespace::NAMESPLIT
|
||||
prefix = $1
|
||||
local = $2
|
||||
uri = get_namespace(prefix)
|
||||
# find the observers for start_element
|
||||
procs = get_procs( :end_element, event[1] )
|
||||
listeners = get_listeners( :end_element, event[1] )
|
||||
# notify observers
|
||||
procs.each { |ob| ob.call( uri, local, event[1] ) } if procs
|
||||
listeners.each { |ob|
|
||||
ob.end_element( uri, local, event[1] )
|
||||
} if listeners
|
||||
|
||||
namespace_mapping = @namespace_stack.pop
|
||||
# find the observers for namespaces
|
||||
procs = get_procs( :end_prefix_mapping, event[1] )
|
||||
listeners = get_listeners( :end_prefix_mapping, event[1] )
|
||||
if procs or listeners
|
||||
namespace_mapping.each do |ns_prefix, ns_uri|
|
||||
# notify observers of namespaces
|
||||
procs.each { |ob| ob.call( ns_prefix ) } if procs
|
||||
listeners.each { |ob| ob.end_prefix_mapping(ns_prefix) } if listeners
|
||||
end
|
||||
end
|
||||
when :text
|
||||
unnormalized = @parser.unnormalize( event[1], @entities )
|
||||
handle( :characters, unnormalized )
|
||||
when :entitydecl
|
||||
handle_entitydecl( event )
|
||||
when :processing_instruction, :comment, :attlistdecl,
|
||||
:elementdecl, :cdata, :notationdecl, :xmldecl
|
||||
handle( *event )
|
||||
end
|
||||
handle( :progress, @parser.position )
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def handle( symbol, *arguments )
|
||||
tag = @tag_stack[-1]
|
||||
procs = get_procs( symbol, tag )
|
||||
listeners = get_listeners( symbol, tag )
|
||||
# notify observers
|
||||
procs.each { |ob| ob.call( *arguments ) } if procs
|
||||
listeners.each { |l|
|
||||
l.send( symbol.to_s, *arguments )
|
||||
} if listeners
|
||||
end
|
||||
|
||||
def handle_entitydecl( event )
|
||||
@entities[ event[1] ] = event[2] if event.size == 3
|
||||
parameter_reference_p = false
|
||||
case event[2]
|
||||
when "SYSTEM"
|
||||
if event.size == 5
|
||||
if event.last == "%"
|
||||
parameter_reference_p = true
|
||||
else
|
||||
event[4, 0] = "NDATA"
|
||||
end
|
||||
end
|
||||
when "PUBLIC"
|
||||
if event.size == 6
|
||||
if event.last == "%"
|
||||
parameter_reference_p = true
|
||||
else
|
||||
event[5, 0] = "NDATA"
|
||||
end
|
||||
end
|
||||
else
|
||||
parameter_reference_p = (event.size == 4)
|
||||
end
|
||||
event[1, 0] = event.pop if parameter_reference_p
|
||||
handle( event[0], event[1..-1] )
|
||||
end
|
||||
|
||||
# The following methods are duplicates, but it is faster than using
|
||||
# a helper
|
||||
def get_procs( symbol, name )
|
||||
return nil if @procs.size == 0
|
||||
@procs.find_all do |sym, match, block|
|
||||
(
|
||||
(sym.nil? or symbol == sym) and
|
||||
((name.nil? and match.nil?) or match.nil? or (
|
||||
(name == match) or
|
||||
(match.kind_of? Regexp and name =~ match)
|
||||
)
|
||||
)
|
||||
)
|
||||
end.collect{|x| x[-1]}
|
||||
end
|
||||
def get_listeners( symbol, name )
|
||||
return nil if @listeners.size == 0
|
||||
@listeners.find_all do |sym, match, block|
|
||||
(
|
||||
(sym.nil? or symbol == sym) and
|
||||
((name.nil? and match.nil?) or match.nil? or (
|
||||
(name == match) or
|
||||
(match.kind_of? Regexp and name =~ match)
|
||||
)
|
||||
)
|
||||
)
|
||||
end.collect{|x| x[-1]}
|
||||
end
|
||||
|
||||
def add( pair )
|
||||
if pair[-1].respond_to? :call
|
||||
@procs << pair unless @procs.include? pair
|
||||
else
|
||||
@listeners << pair unless @listeners.include? pair
|
||||
@has_listeners = true
|
||||
end
|
||||
end
|
||||
|
||||
def get_namespace( prefix )
|
||||
return nil if @namespace_stack.empty?
|
||||
|
||||
uris = (@namespace_stack.find_all { |ns| not ns[prefix].nil? }) ||
|
||||
(@namespace_stack.find { |ns| not ns[nil].nil? })
|
||||
uris[-1][prefix] unless uris.nil? or 0 == uris.size
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: false
|
||||
require_relative "baseparser"
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
class StreamParser
|
||||
def initialize source, listener
|
||||
@listener = listener
|
||||
@parser = BaseParser.new( source )
|
||||
@entities = {}
|
||||
end
|
||||
|
||||
def add_listener( listener )
|
||||
@parser.add_listener( listener )
|
||||
end
|
||||
|
||||
def entity_expansion_count
|
||||
@parser.entity_expansion_count
|
||||
end
|
||||
|
||||
def entity_expansion_limit=( limit )
|
||||
@parser.entity_expansion_limit = limit
|
||||
end
|
||||
|
||||
def entity_expansion_text_limit=( limit )
|
||||
@parser.entity_expansion_text_limit = limit
|
||||
end
|
||||
|
||||
def parse
|
||||
# entity string
|
||||
while true
|
||||
event = @parser.pull
|
||||
case event[0]
|
||||
when :end_document
|
||||
return
|
||||
when :start_element
|
||||
attrs = event[2].each do |n, v|
|
||||
event[2][n] = @parser.unnormalize( v )
|
||||
end
|
||||
@listener.tag_start( event[1], attrs )
|
||||
when :end_element
|
||||
@listener.tag_end( event[1] )
|
||||
when :text
|
||||
unnormalized = @parser.unnormalize( event[1], @entities )
|
||||
@listener.text( unnormalized )
|
||||
when :processing_instruction
|
||||
@listener.instruction( *event[1,2] )
|
||||
when :start_doctype
|
||||
@listener.doctype( *event[1..-1] )
|
||||
when :end_doctype
|
||||
# FIXME: remove this condition for milestone:3.2
|
||||
@listener.doctype_end if @listener.respond_to? :doctype_end
|
||||
when :comment, :attlistdecl, :cdata, :xmldecl, :elementdecl
|
||||
@listener.send( event[0].to_s, *event[1..-1] )
|
||||
when :entitydecl, :notationdecl
|
||||
@entities[ event[1] ] = event[2] if event.size == 3
|
||||
@listener.send( event[0].to_s, event[1..-1] )
|
||||
when :externalentity
|
||||
entity_reference = event[1]
|
||||
content = entity_reference.gsub(/\A%|;\z/, "")
|
||||
@listener.entity(content)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,89 @@
|
||||
# frozen_string_literal: false
|
||||
require_relative '../validation/validationexception'
|
||||
require_relative '../undefinednamespaceexception'
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
class TreeParser
|
||||
def initialize( source, build_context = Document.new )
|
||||
@build_context = build_context
|
||||
@parser = Parsers::BaseParser.new( source )
|
||||
end
|
||||
|
||||
def add_listener( listener )
|
||||
@parser.add_listener( listener )
|
||||
end
|
||||
|
||||
def parse
|
||||
entities = nil
|
||||
begin
|
||||
while true
|
||||
event = @parser.pull
|
||||
#STDERR.puts "TREEPARSER GOT #{event.inspect}"
|
||||
case event[0]
|
||||
when :end_document
|
||||
return
|
||||
when :start_element
|
||||
el = @build_context = @build_context.add_element( event[1] )
|
||||
event[2].each do |key, value|
|
||||
el.attributes[key]=Attribute.new(key,value,self)
|
||||
end
|
||||
when :end_element
|
||||
@build_context = @build_context.parent
|
||||
when :text
|
||||
if @build_context[-1].instance_of? Text
|
||||
@build_context[-1] << event[1]
|
||||
else
|
||||
@build_context.add(
|
||||
Text.new(event[1], @build_context.whitespace, nil, true)
|
||||
) unless (
|
||||
@build_context.ignore_whitespace_nodes and
|
||||
event[1].strip.size==0
|
||||
)
|
||||
end
|
||||
when :comment
|
||||
c = Comment.new( event[1] )
|
||||
@build_context.add( c )
|
||||
when :cdata
|
||||
c = CData.new( event[1] )
|
||||
@build_context.add( c )
|
||||
when :processing_instruction
|
||||
@build_context.add( Instruction.new( event[1], event[2] ) )
|
||||
when :end_doctype
|
||||
entities.each { |k,v| entities[k] = @build_context.entities[k].value }
|
||||
@build_context = @build_context.parent
|
||||
when :start_doctype
|
||||
doctype = DocType.new( event[1..-1], @build_context )
|
||||
@build_context = doctype
|
||||
entities = {}
|
||||
when :attlistdecl
|
||||
n = AttlistDecl.new( event[1..-1] )
|
||||
@build_context.add( n )
|
||||
when :externalentity
|
||||
n = ExternalEntity.new( event[1] )
|
||||
@build_context.add( n )
|
||||
when :elementdecl
|
||||
n = ElementDecl.new( event[1] )
|
||||
@build_context.add(n)
|
||||
when :entitydecl
|
||||
entities[ event[1] ] = event[2] unless event[2] =~ /PUBLIC|SYSTEM/
|
||||
@build_context.add(Entity.new(event))
|
||||
when :notationdecl
|
||||
n = NotationDecl.new( *event[1..-1] )
|
||||
@build_context.add( n )
|
||||
when :xmldecl
|
||||
x = XMLDecl.new( event[1], event[2], event[3] )
|
||||
@build_context.add( x )
|
||||
end
|
||||
end
|
||||
rescue REXML::Validation::ValidationException
|
||||
raise
|
||||
rescue REXML::ParseException
|
||||
raise
|
||||
rescue
|
||||
raise ParseException.new( $!.message, @parser.source, @parser, $! )
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: false
|
||||
require_relative 'streamparser'
|
||||
require_relative 'baseparser'
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
class UltraLightParser
|
||||
def initialize stream
|
||||
@stream = stream
|
||||
@parser = REXML::Parsers::BaseParser.new( stream )
|
||||
end
|
||||
|
||||
def add_listener( listener )
|
||||
@parser.add_listener( listener )
|
||||
end
|
||||
|
||||
def rewind
|
||||
@stream.rewind
|
||||
@parser.stream = @stream
|
||||
end
|
||||
|
||||
def parse
|
||||
root = context = []
|
||||
while true
|
||||
event = @parser.pull
|
||||
case event[0]
|
||||
when :end_document
|
||||
break
|
||||
when :end_doctype
|
||||
context = context[1]
|
||||
when :start_element, :start_doctype
|
||||
context << event
|
||||
event[1,0] = [context]
|
||||
context = event
|
||||
when :end_element
|
||||
context = context[1]
|
||||
else
|
||||
context << event
|
||||
end
|
||||
end
|
||||
root
|
||||
end
|
||||
end
|
||||
|
||||
# An element is an array. The array contains:
|
||||
# 0 The parent element
|
||||
# 1 The tag name
|
||||
# 2 A hash of attributes
|
||||
# 3..-1 The child elements
|
||||
# An element is an array of size > 3
|
||||
# Text is a String
|
||||
# PIs are [ :processing_instruction, target, data ]
|
||||
# Comments are [ :comment, data ]
|
||||
# DocTypes are DocType structs
|
||||
# The root is an array with XMLDecls, Text, DocType, Array, Text
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,739 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
require_relative '../namespace'
|
||||
require_relative '../xmltokens'
|
||||
|
||||
module REXML
|
||||
module Parsers
|
||||
# You don't want to use this class. Really. Use XPath, which is a wrapper
|
||||
# for this class. Believe me. You don't want to poke around in here.
|
||||
# There is strange, dark magic at work in this code. Beware. Go back! Go
|
||||
# back while you still can!
|
||||
class XPathParser
|
||||
include XMLTokens
|
||||
LITERAL = /^'([^']*)'|^"([^"]*)"/u
|
||||
|
||||
def namespaces=( namespaces )
|
||||
Functions::namespace_context = namespaces
|
||||
@namespaces = namespaces
|
||||
end
|
||||
|
||||
def parse path
|
||||
path = path.dup
|
||||
path.gsub!(/([\(\[])\s+/, '\1') # Strip ignorable spaces
|
||||
path.gsub!( /\s+([\]\)])/, '\1')
|
||||
parsed = []
|
||||
rest = OrExpr(path, parsed)
|
||||
if rest
|
||||
unless rest.strip.empty?
|
||||
raise ParseException.new("Garbage component exists at the end: " +
|
||||
"<#{rest}>: <#{path}>")
|
||||
end
|
||||
end
|
||||
parsed
|
||||
end
|
||||
|
||||
def predicate path
|
||||
parsed = []
|
||||
Predicate( "[#{path}]", parsed )
|
||||
parsed
|
||||
end
|
||||
|
||||
def abbreviate(path_or_parsed)
|
||||
if path_or_parsed.kind_of?(String)
|
||||
parsed = parse(path_or_parsed)
|
||||
else
|
||||
parsed = path_or_parsed
|
||||
end
|
||||
components = []
|
||||
component = nil
|
||||
while parsed.size > 0
|
||||
op = parsed.shift
|
||||
case op
|
||||
when :node
|
||||
component << "node()"
|
||||
when :attribute
|
||||
component = "@"
|
||||
components << component
|
||||
when :child
|
||||
component = ""
|
||||
components << component
|
||||
when :descendant_or_self
|
||||
next_op = parsed[0]
|
||||
if next_op == :node
|
||||
parsed.shift
|
||||
component = ""
|
||||
components << component
|
||||
else
|
||||
component = "descendant-or-self::"
|
||||
components << component
|
||||
end
|
||||
when :self
|
||||
next_op = parsed[0]
|
||||
if next_op == :node
|
||||
parsed.shift
|
||||
components << "."
|
||||
else
|
||||
component = "self::"
|
||||
components << component
|
||||
end
|
||||
when :parent
|
||||
next_op = parsed[0]
|
||||
if next_op == :node
|
||||
parsed.shift
|
||||
components << ".."
|
||||
else
|
||||
component = "parent::"
|
||||
components << component
|
||||
end
|
||||
when :any
|
||||
component << "*"
|
||||
when :text
|
||||
component << "text()"
|
||||
when :following, :following_sibling,
|
||||
:ancestor, :ancestor_or_self, :descendant,
|
||||
:namespace, :preceding, :preceding_sibling
|
||||
component = op.to_s.tr("_", "-") << "::"
|
||||
components << component
|
||||
when :qname
|
||||
prefix = parsed.shift
|
||||
name = parsed.shift
|
||||
component << prefix+":" if prefix.size > 0
|
||||
component << name
|
||||
when :predicate
|
||||
component << '['
|
||||
component << predicate_to_path(parsed.shift) {|x| abbreviate(x)}
|
||||
component << ']'
|
||||
when :document
|
||||
components << ""
|
||||
when :function
|
||||
component << parsed.shift
|
||||
component << "( "
|
||||
component << predicate_to_path(parsed.shift[0]) {|x| abbreviate(x)}
|
||||
component << " )"
|
||||
when :literal
|
||||
component << quote_literal(parsed.shift)
|
||||
else
|
||||
component << "UNKNOWN("
|
||||
component << op.inspect
|
||||
component << ")"
|
||||
end
|
||||
end
|
||||
case components
|
||||
when [""]
|
||||
"/"
|
||||
when ["", ""]
|
||||
"//"
|
||||
else
|
||||
components.join("/")
|
||||
end
|
||||
end
|
||||
|
||||
def expand(path_or_parsed)
|
||||
if path_or_parsed.kind_of?(String)
|
||||
parsed = parse(path_or_parsed)
|
||||
else
|
||||
parsed = path_or_parsed
|
||||
end
|
||||
path = ""
|
||||
document = false
|
||||
while parsed.size > 0
|
||||
op = parsed.shift
|
||||
case op
|
||||
when :node
|
||||
path << "node()"
|
||||
when :attribute, :child, :following, :following_sibling,
|
||||
:ancestor, :ancestor_or_self, :descendant, :descendant_or_self,
|
||||
:namespace, :preceding, :preceding_sibling, :self, :parent
|
||||
path << "/" unless path.size == 0
|
||||
path << op.to_s.tr("_", "-")
|
||||
path << "::"
|
||||
when :any
|
||||
path << "*"
|
||||
when :qname
|
||||
prefix = parsed.shift
|
||||
name = parsed.shift
|
||||
path << prefix+":" if prefix.size > 0
|
||||
path << name
|
||||
when :predicate
|
||||
path << '['
|
||||
path << predicate_to_path( parsed.shift ) { |x| expand(x) }
|
||||
path << ']'
|
||||
when :document
|
||||
document = true
|
||||
else
|
||||
path << "UNKNOWN("
|
||||
path << op.inspect
|
||||
path << ")"
|
||||
end
|
||||
end
|
||||
path = "/"+path if document
|
||||
path
|
||||
end
|
||||
|
||||
def predicate_to_path(parsed, &block)
|
||||
path = ""
|
||||
case parsed[0]
|
||||
when :and, :or, :mult, :plus, :minus, :neq, :eq, :lt, :gt, :lteq, :gteq, :div, :mod, :union
|
||||
op = parsed.shift
|
||||
case op
|
||||
when :eq
|
||||
op = "="
|
||||
when :lt
|
||||
op = "<"
|
||||
when :gt
|
||||
op = ">"
|
||||
when :lteq
|
||||
op = "<="
|
||||
when :gteq
|
||||
op = ">="
|
||||
when :neq
|
||||
op = "!="
|
||||
when :union
|
||||
op = "|"
|
||||
end
|
||||
left = predicate_to_path( parsed.shift, &block )
|
||||
right = predicate_to_path( parsed.shift, &block )
|
||||
path << left
|
||||
path << " "
|
||||
path << op.to_s
|
||||
path << " "
|
||||
path << right
|
||||
when :function
|
||||
parsed.shift
|
||||
name = parsed.shift
|
||||
path << name
|
||||
path << "("
|
||||
parsed.shift.each_with_index do |argument, i|
|
||||
path << ", " if i > 0
|
||||
path << predicate_to_path(argument, &block)
|
||||
end
|
||||
path << ")"
|
||||
when :literal
|
||||
parsed.shift
|
||||
path << quote_literal(parsed.shift)
|
||||
else
|
||||
path << yield( parsed )
|
||||
end
|
||||
return path.squeeze(" ")
|
||||
end
|
||||
# For backward compatibility
|
||||
alias_method :preciate_to_string, :predicate_to_path
|
||||
|
||||
private
|
||||
def quote_literal( literal )
|
||||
case literal
|
||||
when String
|
||||
# XPath 1.0 does not support escape characters.
|
||||
# Assumes literal does not contain both single and double quotes.
|
||||
if literal.include?("'")
|
||||
"\"#{literal}\""
|
||||
else
|
||||
"'#{literal}'"
|
||||
end
|
||||
else
|
||||
literal.inspect
|
||||
end
|
||||
end
|
||||
|
||||
#LocationPath
|
||||
# | RelativeLocationPath
|
||||
# | '/' RelativeLocationPath?
|
||||
# | '//' RelativeLocationPath
|
||||
def LocationPath path, parsed
|
||||
path = path.lstrip
|
||||
if path[0] == ?/
|
||||
parsed << :document
|
||||
if path[1] == ?/
|
||||
parsed << :descendant_or_self
|
||||
parsed << :node
|
||||
path = path[2..-1]
|
||||
else
|
||||
path = path[1..-1]
|
||||
end
|
||||
end
|
||||
return RelativeLocationPath( path, parsed ) if path.size > 0
|
||||
end
|
||||
|
||||
#RelativeLocationPath
|
||||
# | Step
|
||||
# | (AXIS_NAME '::' | '@' | '') AxisSpecifier
|
||||
# NodeTest
|
||||
# Predicate
|
||||
# | '.' | '..' AbbreviatedStep
|
||||
# | RelativeLocationPath '/' Step
|
||||
# | RelativeLocationPath '//' Step
|
||||
AXIS = /^(ancestor|ancestor-or-self|attribute|child|descendant|descendant-or-self|following|following-sibling|namespace|parent|preceding|preceding-sibling|self)::/
|
||||
def RelativeLocationPath path, parsed
|
||||
loop do
|
||||
original_path = path
|
||||
path = path.lstrip
|
||||
|
||||
return original_path if path.empty?
|
||||
|
||||
# (axis or @ or <child::>) nodetest predicate >
|
||||
# OR > / Step
|
||||
# (. or ..) >
|
||||
if path[0] == ?.
|
||||
if path[1] == ?.
|
||||
parsed << :parent
|
||||
parsed << :node
|
||||
path = path[2..-1]
|
||||
else
|
||||
parsed << :self
|
||||
parsed << :node
|
||||
path = path[1..-1]
|
||||
end
|
||||
else
|
||||
path_before_axis_specifier = path
|
||||
parsed_not_abberviated = []
|
||||
if path[0] == ?@
|
||||
parsed_not_abberviated << :attribute
|
||||
path = path[1..-1]
|
||||
# Goto Nodetest
|
||||
elsif path =~ AXIS
|
||||
parsed_not_abberviated << $1.tr('-','_').intern
|
||||
path = $'
|
||||
# Goto Nodetest
|
||||
else
|
||||
parsed_not_abberviated << :child
|
||||
end
|
||||
|
||||
path_before_node_test = path
|
||||
path = NodeTest(path, parsed_not_abberviated)
|
||||
if path == path_before_node_test
|
||||
return path_before_axis_specifier
|
||||
end
|
||||
path = Predicate(path, parsed_not_abberviated)
|
||||
|
||||
parsed.concat(parsed_not_abberviated)
|
||||
end
|
||||
|
||||
original_path = path
|
||||
path = path.lstrip
|
||||
return original_path if path.empty?
|
||||
|
||||
return original_path if path[0] != ?/
|
||||
|
||||
if path[1] == ?/
|
||||
parsed << :descendant_or_self
|
||||
parsed << :node
|
||||
path = path[2..-1]
|
||||
else
|
||||
path = path[1..-1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a 1-1 map of the nodeset
|
||||
# The contents of the resulting array are either:
|
||||
# true/false, if a positive match
|
||||
# String, if a name match
|
||||
#NodeTest
|
||||
# | ('*' | NCNAME ':' '*' | QNAME) NameTest
|
||||
# | '*' ':' NCNAME NameTest since XPath 2.0
|
||||
# | NODE_TYPE '(' ')' NodeType
|
||||
# | PI '(' LITERAL ')' PI
|
||||
# | '[' expr ']' Predicate
|
||||
PREFIX_WILDCARD = /^\*:(#{NCNAME_STR})/u
|
||||
LOCAL_NAME_WILDCARD = /^(#{NCNAME_STR}):\*/u
|
||||
QNAME = Namespace::NAMESPLIT
|
||||
NODE_TYPE = /^(comment|text|node)\(\s*\)/m
|
||||
PI = /^processing-instruction\(/
|
||||
def NodeTest path, parsed
|
||||
original_path = path
|
||||
path = path.lstrip
|
||||
case path
|
||||
when PREFIX_WILDCARD
|
||||
prefix = nil
|
||||
name = $1
|
||||
path = $'
|
||||
parsed << :qname
|
||||
parsed << prefix
|
||||
parsed << name
|
||||
when /^\*/
|
||||
path = $'
|
||||
parsed << :any
|
||||
when NODE_TYPE
|
||||
type = $1
|
||||
path = $'
|
||||
parsed << type.tr('-', '_').intern
|
||||
when PI
|
||||
path = $'
|
||||
literal = nil
|
||||
if path =~ /^\s*\)/
|
||||
path = $'
|
||||
else
|
||||
path =~ LITERAL
|
||||
literal = $1
|
||||
path = $'
|
||||
raise ParseException.new("Missing ')' after processing instruction") if path[0] != ?)
|
||||
path = path[1..-1]
|
||||
end
|
||||
parsed << :processing_instruction
|
||||
parsed << (literal || '')
|
||||
when LOCAL_NAME_WILDCARD
|
||||
prefix = $1
|
||||
path = $'
|
||||
parsed << :namespace
|
||||
parsed << prefix
|
||||
when QNAME
|
||||
prefix = $1
|
||||
name = $2
|
||||
path = $'
|
||||
prefix = "" unless prefix
|
||||
parsed << :qname
|
||||
parsed << prefix
|
||||
parsed << name
|
||||
else
|
||||
path = original_path
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
# Filters the supplied nodeset on the predicate(s)
|
||||
def Predicate path, parsed
|
||||
original_path = path
|
||||
path = path.lstrip
|
||||
return original_path unless path[0] == ?[
|
||||
predicates = []
|
||||
while path[0] == ?[
|
||||
path, expr = get_group(path)
|
||||
predicates << expr[1..-2] if expr
|
||||
end
|
||||
predicates.each{ |pred|
|
||||
preds = []
|
||||
parsed << :predicate
|
||||
parsed << preds
|
||||
OrExpr(pred, preds)
|
||||
}
|
||||
path
|
||||
end
|
||||
|
||||
# The following return arrays of true/false, a 1-1 mapping of the
|
||||
# supplied nodeset, except for axe(), which returns a filtered
|
||||
# nodeset
|
||||
|
||||
#| OrExpr S 'or' S AndExpr
|
||||
#| AndExpr
|
||||
def OrExpr path, parsed
|
||||
n = []
|
||||
rest = AndExpr( path, n )
|
||||
if rest != path
|
||||
while rest =~ /^\s*( or )/
|
||||
n = [ :or, n, [] ]
|
||||
rest = AndExpr( $', n[-1] )
|
||||
end
|
||||
end
|
||||
if parsed.size == 0 and n.size != 0
|
||||
parsed.replace(n)
|
||||
elsif n.size > 0
|
||||
parsed << n
|
||||
end
|
||||
rest
|
||||
end
|
||||
|
||||
#| AndExpr S 'and' S EqualityExpr
|
||||
#| EqualityExpr
|
||||
def AndExpr path, parsed
|
||||
n = []
|
||||
rest = EqualityExpr( path, n )
|
||||
if rest != path
|
||||
while rest =~ /^\s*( and )/
|
||||
n = [ :and, n, [] ]
|
||||
rest = EqualityExpr( $', n[-1] )
|
||||
end
|
||||
end
|
||||
if parsed.size == 0 and n.size != 0
|
||||
parsed.replace(n)
|
||||
elsif n.size > 0
|
||||
parsed << n
|
||||
end
|
||||
rest
|
||||
end
|
||||
|
||||
#| EqualityExpr ('=' | '!=') RelationalExpr
|
||||
#| RelationalExpr
|
||||
def EqualityExpr path, parsed
|
||||
n = []
|
||||
rest = RelationalExpr( path, n )
|
||||
if rest != path
|
||||
while rest =~ /^\s*(!?=)\s*/
|
||||
if $1[0] == ?!
|
||||
n = [ :neq, n, [] ]
|
||||
else
|
||||
n = [ :eq, n, [] ]
|
||||
end
|
||||
rest = RelationalExpr( $', n[-1] )
|
||||
end
|
||||
end
|
||||
if parsed.size == 0 and n.size != 0
|
||||
parsed.replace(n)
|
||||
elsif n.size > 0
|
||||
parsed << n
|
||||
end
|
||||
rest
|
||||
end
|
||||
|
||||
#| RelationalExpr ('<' | '>' | '<=' | '>=') AdditiveExpr
|
||||
#| AdditiveExpr
|
||||
def RelationalExpr path, parsed
|
||||
n = []
|
||||
rest = AdditiveExpr( path, n )
|
||||
if rest != path
|
||||
while rest =~ /^\s*([<>]=?)\s*/
|
||||
if $1[0] == ?<
|
||||
sym = "lt"
|
||||
else
|
||||
sym = "gt"
|
||||
end
|
||||
sym << "eq" if $1[-1] == ?=
|
||||
n = [ sym.intern, n, [] ]
|
||||
rest = AdditiveExpr( $', n[-1] )
|
||||
end
|
||||
end
|
||||
if parsed.size == 0 and n.size != 0
|
||||
parsed.replace(n)
|
||||
elsif n.size > 0
|
||||
parsed << n
|
||||
end
|
||||
rest
|
||||
end
|
||||
|
||||
#| AdditiveExpr ('+' | '-') MultiplicativeExpr
|
||||
#| MultiplicativeExpr
|
||||
def AdditiveExpr path, parsed
|
||||
n = []
|
||||
rest = MultiplicativeExpr( path, n )
|
||||
if rest != path
|
||||
while rest =~ /^\s*(\+|-)\s*/
|
||||
if $1[0] == ?+
|
||||
n = [ :plus, n, [] ]
|
||||
else
|
||||
n = [ :minus, n, [] ]
|
||||
end
|
||||
rest = MultiplicativeExpr( $', n[-1] )
|
||||
end
|
||||
end
|
||||
if parsed.size == 0 and n.size != 0
|
||||
parsed.replace(n)
|
||||
elsif n.size > 0
|
||||
parsed << n
|
||||
end
|
||||
rest
|
||||
end
|
||||
|
||||
#| MultiplicativeExpr ('*' | S ('div' | 'mod') S) UnaryExpr
|
||||
#| UnaryExpr
|
||||
def MultiplicativeExpr path, parsed
|
||||
n = []
|
||||
rest = UnaryExpr( path, n )
|
||||
if rest != path
|
||||
while rest =~ /^\s*(\*| div | mod )\s*/
|
||||
if $1[0] == ?*
|
||||
n = [ :mult, n, [] ]
|
||||
elsif $1.include?( "div" )
|
||||
n = [ :div, n, [] ]
|
||||
else
|
||||
n = [ :mod, n, [] ]
|
||||
end
|
||||
rest = UnaryExpr( $', n[-1] )
|
||||
end
|
||||
end
|
||||
if parsed.size == 0 and n.size != 0
|
||||
parsed.replace(n)
|
||||
elsif n.size > 0
|
||||
parsed << n
|
||||
end
|
||||
rest
|
||||
end
|
||||
|
||||
#| '-' UnaryExpr
|
||||
#| UnionExpr
|
||||
def UnaryExpr path, parsed
|
||||
path =~ /^(\-*)/
|
||||
path = $'
|
||||
if $1 and (($1.size % 2) != 0)
|
||||
mult = -1
|
||||
else
|
||||
mult = 1
|
||||
end
|
||||
parsed << :neg if mult < 0
|
||||
|
||||
n = []
|
||||
path = UnionExpr( path, n )
|
||||
parsed.concat( n )
|
||||
path
|
||||
end
|
||||
|
||||
#| UnionExpr '|' PathExpr
|
||||
#| PathExpr
|
||||
def UnionExpr path, parsed
|
||||
n = []
|
||||
rest = PathExpr( path, n )
|
||||
if rest != path
|
||||
while rest =~ /^\s*(\|)\s*/
|
||||
n = [ :union, n, [] ]
|
||||
rest = PathExpr( $', n[-1] )
|
||||
end
|
||||
end
|
||||
if parsed.size == 0 and n.size != 0
|
||||
parsed.replace( n )
|
||||
elsif n.size > 0
|
||||
parsed << n
|
||||
end
|
||||
rest
|
||||
end
|
||||
|
||||
#| LocationPath
|
||||
#| FilterExpr ('/' | '//') RelativeLocationPath
|
||||
def PathExpr path, parsed
|
||||
path = path.lstrip
|
||||
n = []
|
||||
rest = FilterExpr( path, n )
|
||||
if rest != path
|
||||
if rest and rest[0] == ?/
|
||||
rest = RelativeLocationPath(rest, n)
|
||||
parsed.concat(n)
|
||||
return rest
|
||||
end
|
||||
end
|
||||
rest = LocationPath(rest, n) if rest =~ /\A[\/\.\@\[\w*]/
|
||||
parsed.concat(n)
|
||||
return rest
|
||||
end
|
||||
|
||||
#| FilterExpr Predicate
|
||||
#| PrimaryExpr
|
||||
def FilterExpr path, parsed
|
||||
n = []
|
||||
path_before_primary_expr = path
|
||||
path = PrimaryExpr(path, n)
|
||||
return path_before_primary_expr if path == path_before_primary_expr
|
||||
path = Predicate(path, n)
|
||||
parsed.concat(n)
|
||||
path
|
||||
end
|
||||
|
||||
#| VARIABLE_REFERENCE
|
||||
#| '(' expr ')'
|
||||
#| LITERAL
|
||||
#| NUMBER
|
||||
#| FunctionCall
|
||||
VARIABLE_REFERENCE = /^\$(#{NAME_STR})/u
|
||||
NUMBER = /^(\d*\.?\d+)/
|
||||
NT = /^comment|text|processing-instruction|node$/
|
||||
def PrimaryExpr path, parsed
|
||||
case path
|
||||
when VARIABLE_REFERENCE
|
||||
varname = $1
|
||||
path = $'
|
||||
parsed << :variable
|
||||
parsed << varname
|
||||
#arry << @variables[ varname ]
|
||||
when /^(\w[-\w]*)(?:\()/
|
||||
fname = $1
|
||||
tmp = $'
|
||||
return path if fname =~ NT
|
||||
path = tmp
|
||||
parsed << :function
|
||||
parsed << fname
|
||||
path = FunctionCall(path, parsed)
|
||||
when NUMBER
|
||||
varname = $1.nil? ? $2 : $1
|
||||
path = $'
|
||||
parsed << :literal
|
||||
parsed << (varname.include?('.') ? varname.to_f : varname.to_i)
|
||||
when LITERAL
|
||||
varname = $1.nil? ? $2 : $1
|
||||
path = $'
|
||||
parsed << :literal
|
||||
parsed << varname
|
||||
when /^\(/ #/
|
||||
path, contents = get_group(path)
|
||||
contents = contents[1..-2]
|
||||
n = []
|
||||
OrExpr( contents, n )
|
||||
parsed.concat(n)
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
#| FUNCTION_NAME '(' ( expr ( ',' expr )* )? ')'
|
||||
def FunctionCall rest, parsed
|
||||
path, arguments = parse_args(rest)
|
||||
argset = []
|
||||
for argument in arguments
|
||||
args = []
|
||||
OrExpr( argument, args )
|
||||
argset << args
|
||||
end
|
||||
parsed << argset
|
||||
path
|
||||
end
|
||||
|
||||
# get_group( '[foo]bar' ) -> ['bar', '[foo]']
|
||||
def get_group string
|
||||
ind = 0
|
||||
depth = 0
|
||||
st = string[0,1]
|
||||
en = (st == "(" ? ")" : "]")
|
||||
begin
|
||||
case string[ind,1]
|
||||
when st
|
||||
depth += 1
|
||||
when en
|
||||
depth -= 1
|
||||
end
|
||||
ind += 1
|
||||
end while depth > 0 and ind < string.length
|
||||
return nil unless depth==0
|
||||
[string[ind..-1], string[0..ind-1]]
|
||||
end
|
||||
|
||||
def parse_args( string )
|
||||
arguments = []
|
||||
ind = 0
|
||||
inquot = false
|
||||
inapos = false
|
||||
depth = 1
|
||||
begin
|
||||
case string[ind]
|
||||
when ?"
|
||||
inquot = !inquot unless inapos
|
||||
when ?'
|
||||
inapos = !inapos unless inquot
|
||||
else
|
||||
unless inquot or inapos
|
||||
case string[ind]
|
||||
when ?(
|
||||
depth += 1
|
||||
if depth == 1
|
||||
string = string[1..-1]
|
||||
ind -= 1
|
||||
end
|
||||
when ?)
|
||||
depth -= 1
|
||||
if depth == 0
|
||||
s = string[0,ind].strip
|
||||
arguments << s unless s == ""
|
||||
string = string[ind+1..-1]
|
||||
end
|
||||
when ?,
|
||||
if depth == 1
|
||||
s = string[0,ind].strip
|
||||
arguments << s unless s == ""
|
||||
string = string[ind+1..-1]
|
||||
ind = -1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ind += 1
|
||||
end while depth > 0 and ind < string.length
|
||||
return nil unless depth==0
|
||||
[string,arguments]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user