class REXML::Parsers::BaseParser
Using the Pull Parser¶ ↑
This API is experimental, and subject to change.
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.
Constants
- ATTDEF
- ATTDEF_RE
- ATTLISTDECL_PATTERN
- ATTLISTDECL_START
- ATTRIBUTE_PATTERN
- ATTTYPE
- ATTVALUE
- CDATA_END
- CDATA_PATTERN
- CDATA_START
- CLOSE_MATCH
- COMBININGCHAR
- COMMENT_PATTERN
- COMMENT_START
- DEFAULTDECL
- DEFAULT_ENTITIES
- DIGIT
- DOCTYPE_END
- DOCTYPE_START
- ELEMENTDECL_PATTERN
- ELEMENTDECL_START
- ENCODING
- ENTITYDECL
- ENTITYDEF
- ENTITYVALUE
- ENTITY_START
- ENUMERATEDTYPE
- ENUMERATION
- EREFERENCE
- EXTENDER
- EXTERNALID
- EXTERNAL_ID_PUBLIC
- EXTERNAL_ID_SYSTEM
- GEDECL
- INSTRUCTION_PATTERN
- INSTRUCTION_START
- LETTER
- NAME
- NAMECHAR
- NCNAME_STR
- NDATADECL
- NMTOKEN
- NMTOKENS
- NOTATIONDECL_START
- NOTATIONTYPE
- PEDECL
- PEDEF
- PEREFERENCE
- PUBIDCHAR
Entityconstants- PUBIDLITERAL
- PUBLIC_ID
- QNAME
- QNAME_STR
- REFERENCE
- REFERENCE_RE
- STANDALONE
- SYSTEMENTITY
- SYSTEMLITERAL
- TAG_MATCH
- TEXT_PATTERN
- UNAME_STR
Just for backward compatibility. For example, kramdown uses this. It's not used in
REXML.- VERSION
- XMLDECL_PATTERN
- XMLDECL_START
Attributes
Public Class Methods
# File lib/rexml/parsers/baseparser.rb, line 164 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 @version = nil end
Public Instance Methods
# File lib/rexml/parsers/baseparser.rb, line 175 def add_listener( listener ) @listeners << listener end
Returns true if there are no more events
# File lib/rexml/parsers/baseparser.rb, line 210 def empty? (@source.empty? and @stack.empty?) end
# File lib/rexml/parsers/baseparser.rb, line 537 def entity( reference, entities ) return unless entities value = entities[ reference ] return if value.nil? record_entity_expansion unnormalize( value, entities ) end
Returns true if there are more events. Synonymous with !empty?
# File lib/rexml/parsers/baseparser.rb, line 215 def has_next? !(@source.empty? and @stack.empty?) end
Escapes all possible entities
# File lib/rexml/parsers/baseparser.rb, line 548 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
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.
# File lib/rexml/parsers/baseparser.rb, line 231 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
# File lib/rexml/parsers/baseparser.rb, line 200 def position if @source.respond_to? :position @source.position else # FIXME 0 end end
Returns the next event. This is a PullEvent object.
# File lib/rexml/parsers/baseparser.rb, line 246 def pull @source.drop_parsed_content pull_event.tap do |event| @listeners.each do |listener| listener.receive event end end end
# File lib/rexml/parsers/baseparser.rb, line 189 def reset @closed = nil @have_root = false @document_status = nil @tags = [] @stack = [] @entities = [] @namespaces = {"xml" => Private::XML_PREFIXED_NAMESPACE} @namespaces_restore_stack = [] end
# File lib/rexml/parsers/baseparser.rb, line 184 def stream=( source ) @source = SourceFactory.create_from( source ) reset end
Unescapes all possible entities
# File lib/rexml/parsers/baseparser.rb, line 564 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
Push an event back on the head of the stream. This method has (theoretically) infinite depth.
# File lib/rexml/parsers/baseparser.rb, line 221 def unshift token @stack.unshift(token) end
Private Instance Methods
# File lib/rexml/parsers/baseparser.rb, line 613 def add_namespace(prefix, uri) @namespaces_restore_stack.last[prefix] = @namespaces[prefix] if uri.nil? @namespaces.delete(prefix) else @namespaces[prefix] = uri end end
# File lib/rexml/parsers/baseparser.rb, line 646 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
# File lib/rexml/parsers/baseparser.rb, line 652 def normalize_xml_declaration_encoding(xml_declaration_encoding) /\AUTF-16(?:BE|LE)\z/i.match?(xml_declaration_encoding) ? "UTF-16" : nil end
# File lib/rexml/parsers/baseparser.rb, line 849 def parse_attribute_value_with_equal(name) unless @source.match?(Private::EQUAL_PATTERN, true) message = "Missing attribute equal: <#{name}>" raise REXML::ParseException.new(message, @source) end unless quote = scan_quote message = "Missing attribute value start quote: <#{name}>" raise REXML::ParseException.new(message, @source) end 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 value end
# File lib/rexml/parsers/baseparser.rb, line 868 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] value = parse_attribute_value_with_equal(name) @source.skip_spaces 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
# File lib/rexml/parsers/baseparser.rb, line 669 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
# File lib/rexml/parsers/baseparser.rb, line 697 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
# File lib/rexml/parsers/baseparser.rb, line 656 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
# File lib/rexml/parsers/baseparser.rb, line 628 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
# File lib/rexml/parsers/baseparser.rb, line 735 def process_comment text = @source.read_until("-->") unless text.chomp!("-->") raise REXML::ParseException.new("Unclosed comment: Missing end '-->'", @source) end if text.include? "--" or text.end_with?("-") raise REXML::ParseException.new("Malformed comment", @source) end text end
# File lib/rexml/parsers/baseparser.rb, line 747 def process_instruction name = parse_name("Malformed XML: Invalid processing instruction node") if name == "xml" xml_declaration else # PITarget if @source.skip_spaces # e.g. <?name content?> start_position = @source.position content = @source.read_until("?>") unless content.chomp!("?>") @source.position = start_position raise ParseException.new("Malformed XML: Unclosed processing instruction: <#{name}>", @source) end else # e.g. <?name?> content = nil unless @source.match?("?>", true) raise ParseException.new("Malformed XML: Unclosed processing instruction: <#{name}>", @source) end end [:processing_instruction, name, content] end end
# File lib/rexml/parsers/baseparser.rb, line 256 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 unless @document_status == :in_element raise ParseException.new("Malformed XML: No root element", @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) return [ :comment, process_comment ] elsif @source.match?("DOCTYPE", true) base_error_message = "Malformed DOCTYPE" unless @source.skip_spaces 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) @source.skip_spaces if @source.match?("[", true) id = [nil, nil, nil] @document_status = :in_doctype elsif @source.match?(">", 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 @source.skip_spaces if @source.match?("[", true) @document_status = :in_doctype elsif @source.match?(">", 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.skip_spaces @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.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 = "<!ATTLIST" + 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.skip_spaces 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) @source.skip_spaces unless @source.match?(">", true) message = "#{base_error_message}: garbage before end >" raise REXML::ParseException.new(message, @source) end return [:notationdecl, name, *id] elsif @source.match?("--", true) return [ :comment, process_comment ] else raise REXML::ParseException.new("Malformed node: Started with '<!' but not a comment nor ELEMENT,ENTITY,ATTLIST,NOTATION", @source) 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 ] else raise ParseException.new("Malformed DOCTYPE: invalid declaration", @source) end end if @document_status == :after_doctype @source.skip_spaces 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) #STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}" if @source.match?("--", true) return [ :comment, process_comment ] elsif @source.match?("[CDATA[", true) text = @source.read_until("]]>") if text.chomp!("]]>") return [ :cdata, text ] else raise REXML::ParseException.new("Malformed CDATA: Missing end ']]>'", @source) end else raise REXML::ParseException.new("Malformed node: Started with '<!' but not a comment nor CDATA", @source) end 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 # NOTE: The end of the method never runs, because it is unreachable. # All branches of code above have explicit unconditional return or raise statements. end
# File lib/rexml/parsers/baseparser.rb, line 622 def push_namespaces_restore namespaces_restore = {} @namespaces_restore_stack.push(namespaces_restore) namespaces_restore end
# File lib/rexml/parsers/baseparser.rb, line 639 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
# File lib/rexml/parsers/baseparser.rb, line 831 def scan_quote @source.match(/(['"])/, true)&.[](1) end
# File lib/rexml/parsers/baseparser.rb, line 769 def xml_declaration unless @version.nil? raise ParseException.new("Malformed XML: XML declaration is duplicated", @source) end if @document_status raise ParseException.new("Malformed XML: XML declaration is not at the start", @source) end unless @source.skip_spaces raise ParseException.new("Malformed XML: XML declaration misses spaces before version", @source) end unless @source.match?("version", true) raise ParseException.new("Malformed XML: XML declaration misses version", @source) end @version = parse_attribute_value_with_equal("xml") unless @source.skip_spaces unless @source.match?("?>", true) raise ParseException.new("Malformed XML: Unclosed XML declaration", @source) end encoding = normalize_xml_declaration_encoding(@source.encoding) return [ :xmldecl, @version, encoding, nil ] # e.g. <?xml version="1.0"?> end if @source.match?("encoding", true) encoding = parse_attribute_value_with_equal("xml") unless @source.skip_spaces unless @source.match?("?>", true) raise ParseException.new("Malformed XML: Unclosed XML declaration", @source) end if need_source_encoding_update?(encoding) @source.encoding = encoding end encoding ||= normalize_xml_declaration_encoding(@source.encoding) return [ :xmldecl, @version, encoding, nil ] # e.g. <?xml version="1.1" encoding="UTF-8"?> end end if @source.match?("standalone", true) standalone = parse_attribute_value_with_equal("xml") case standalone when "yes", "no" else raise ParseException.new("Malformed XML: XML declaration standalone is not yes or no : <#{standalone}>", @source) end end @source.skip_spaces unless @source.match?("?>", true) raise ParseException.new("Malformed XML: Unclosed XML declaration", @source) end if need_source_encoding_update?(encoding) @source.encoding = encoding end encoding ||= normalize_xml_declaration_encoding(@source.encoding) # e.g. <?xml version="1.0" ?> # <?xml version="1.1" encoding="UTF-8" ?> # <?xml version="1.1" standalone="yes"?> # <?xml version="1.1" encoding="UTF-8" standalone="yes" ?> [ :xmldecl, @version, encoding, standalone ] end