The element-content markup dispatch ran up to five literal string comparisons (</, <!--, <![CDATA[, <?, <!DOCTYPE) before the start-tag branch won, a per-element cost the parse profile indicted as Reader.matchesBytes. Branch instead on the single byte after '<' on the byte fast path: '/' end tag, '?' processing instruction, '!' a declaration, otherwise a start tag. The Character path keeps the equivalent matches()-based dispatch for a streaming source or spliced entity text, and a '<!' that matches none of comment/CDATA/DOCTYPE still falls through to the start-tag scanner with the same expectedName error. Measured parse ~9.5x -> ~9.0x slower than libxml2 on the 20k-item corpus; the byte and Character paths are held identical by ParserPathDifferentialTests over its adversarial corpus, and the Xalan, W3C, and RELAX NG suites stay green. No spec behavior changed.
68 lines
3.2 KiB
Swift
68 lines
3.2 KiB
Swift
/// Content markup dispatch, in its own file to keep the reader's primary type
|
|
/// body under the length cap. Behavior is part of EventReader proper.
|
|
extension PureXML.Parsing.EventReader {
|
|
/// At a '<' in element content, dispatches to the matching markup scanner.
|
|
/// The caller has confirmed the lead is '<' and set the event start. Shared
|
|
/// by the byte and Character dispatch paths so the two cannot diverge.
|
|
///
|
|
/// On the byte fast path the single byte after '<' selects the branch ('/'
|
|
/// end tag, '?' PI, '!' a declaration, otherwise a start tag), so an
|
|
/// element no longer runs up to five literal string comparisons before the
|
|
/// start-tag branch wins. The Character path below is the exact fallback for
|
|
/// a streaming source or spliced entity text, and a '<!' that matches none
|
|
/// of comment/CDATA/DOCTYPE still falls through to the start-tag scanner,
|
|
/// which reports the same `expectedName` error as before.
|
|
mutating func scanContentMarkup() throws -> Event {
|
|
switch reader.peekByte(1) {
|
|
case 0x2F: // '/'
|
|
try scanEndTag()
|
|
case 0x3F: // '?'
|
|
try scanContentProcessingInstruction()
|
|
case 0x21: // '!'
|
|
try scanContentDeclaration()
|
|
case .some:
|
|
try scanStartTag()
|
|
case nil:
|
|
try scanContentMarkupViaCharacters()
|
|
}
|
|
}
|
|
|
|
/// The Character-path markup dispatch, used off the byte fast path (a
|
|
/// streaming source or spliced entity text). Equivalent to the byte switch
|
|
/// in `scanContentMarkup`.
|
|
private mutating func scanContentMarkupViaCharacters() throws -> Event {
|
|
if reader.matches("</") { return try scanEndTag() }
|
|
if reader.matches("<?") { return try scanContentProcessingInstruction() }
|
|
if reader.matches("<!") { return try scanContentDeclaration() }
|
|
return try scanStartTag()
|
|
}
|
|
|
|
/// Scans a `<!...` construct in element content: a comment, a CDATA
|
|
/// section, or a rejected DOCTYPE. A `<!` matching none of these falls
|
|
/// through to the start-tag scanner, which reports the same `expectedName`
|
|
/// error as before.
|
|
private mutating func scanContentDeclaration() throws -> Event {
|
|
if reader.matches("<!--") {
|
|
recordEmptyElementContent("a comment")
|
|
return try scanComment()
|
|
}
|
|
if reader.matches("<![CDATA[") { return try scanCDATA() }
|
|
if reader.matches("<!DOCTYPE") {
|
|
throw PureXML.Parsing.ParseError.unsupportedDoctype(reader.mark)
|
|
}
|
|
return try scanStartTag()
|
|
}
|
|
|
|
/// Scans a processing instruction in element content, rejecting the
|
|
/// reserved `xml` target (legal only as the document's leading declaration).
|
|
private mutating func scanContentProcessingInstruction() throws -> Event {
|
|
let mark = reader.mark
|
|
recordEmptyElementContent("a processing instruction")
|
|
let instruction = try scanProcessingInstruction()
|
|
if instruction.target.lowercased() == "xml" {
|
|
throw PureXML.Parsing.ParseError.reservedProcessingInstructionTarget(mark)
|
|
}
|
|
return .processingInstruction(target: instruction.target, data: instruction.data)
|
|
}
|
|
}
|