/// Text and processing-instruction scanning, split from the reader body to /// keep it under the length caps. Behavior is part of EventReader proper. extension PureXML.Parsing.EventReader { /// Returns the next text event, or nil after splicing: a reference to /// an entity whose replacement (directly or transitively) contains /// markup is validated, budgeted, and pushed back into the reader so /// it is reparsed as content (4.4.2 Included), elements in replacement /// text become elements in the stream, not character data. mutating func scanText() throws -> Event? { let mark = reader.mark var raw = "" var length = 0 // Whether any '&' was seen: without one the text has no references // and no markup-entity boundary, so the whole decode/split/findings // pass is skipped. The scanners visit every byte already, so this // costs nothing beyond a compare they were doing anyway. var sawAmpersand = false while true { // Bulk byte runs first: plain ASCII content (the bulk of any // document) skips the per-character machinery entirely; the // run never spans '<', CR, ']', or invalid bytes, so the // character path below keeps exact error marks. if let run = reader.contentRunBytes(sawAmpersand: &sawAmpersand) { length += run.utf8.count try checkContent(length, mark) raw += run continue } // Detect the closing '<' on the byte path so the buffer stays // empty: a buffered '<' would force the next element's dispatch, // matches(), and name scan onto the slow Character path. if reader.peekByte() == 0x3C { break } guard let character = reader.peek(), character != "<" else { break } length += 1 try checkContent(length, mark) if character == ">", raw.hasSuffix("]]") { throw ParseError.cdataCloseInContent(reader.mark) } guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else { throw ParseError.invalidCharacter(reader.mark) } if character == "&" { sawAmpersand = true } raw.append(character) reader.advance() } guard sawAmpersand else { return .characters(raw) } let split = EntityDecoder.splitAtMarkupEntity(raw, entities: referencableEntities) if let split { let replacement = try EntityDecoder.includeForContent( split.name, entities: referencableEntities, budget: &entityBudget, at: mark, ) reader.inject(replacement + split.remainder) let prefix = try decodeReferences(split.prefix, at: mark) return prefix.isEmpty ? nil : .characters(prefix) } let decoded = try decodeReferences(raw, at: mark) recordReferenceContentFindings(raw: raw, decoded: decoded) return .characters(decoded) } mutating func scanProcessingInstruction() throws -> (target: String, data: String) { let mark = reader.mark reader.consume("` is not well-formed. if let next = reader.peek(), !next.isWhitespace, !reader.matches("?>") { throw ParseError.unexpectedCharacter(next, reader.mark) } reader.skipSpace() var data = "" var length = 0 while !reader.matches("?>") { guard let character = reader.advance() else { throw ParseError.unterminatedTag(mark) } length += 1 try checkContent(length, mark) guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else { throw ParseError.invalidCharacter(reader.mark) } data.append(character) } reader.consume("?>") return (target.description, data) } }