99 lines
4.4 KiB
Swift
99 lines
4.4 KiB
Swift
/// 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("<?")
|
|
let target = try scanName()
|
|
// A PI target is an NCName under namespaces (no colon).
|
|
if target.description.contains(":") {
|
|
throw ParseError.namespaceConstraint(reason: "PI target '\(target.description)' is not a legal NCName", mark)
|
|
}
|
|
// Production 16: the target and the data must be separated by
|
|
// whitespace, so `<?target+++?>` 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)
|
|
}
|
|
}
|