/// The start-tag attribute scanners, in their own file to keep the reader's /// type body under the length cap. Behavior is part of EventReader proper. extension PureXML.Parsing.EventReader { mutating func scanAttributes() throws -> [PureXML.Model.Attribute] { var attributes: [PureXML.Model.Attribute] = [] // Reuses the stored scratch set (see its declaration) so an element // does not allocate fresh Set storage just to check its attribute // names; clearing keeps the capacity. `scanAttributes` cannot re-enter // itself (nothing inside an attribute scan starts another start tag), // so the shared storage never sees two interleaved scans. attributeNamesSeen.removeAll(keepingCapacity: true) while true { reader.skipSpace() // Byte dispatch keeps the buffer empty so scanName() reaches its // byte fast path; the Character peek is the off-fast-path fallback. if let byte = reader.peekByte() { if byte == 0x3E || byte == 0x2F { return attributes } // '>' or '/' } else { guard let next = reader.peek(), next != ">", next != "/" else { return attributes } } let mark = reader.mark let name = try scanName() reader.skipSpace() guard reader.consume("=") else { throw PureXML.Parsing.ParseError.expectedEquals(reader.mark) } reader.skipSpace() let value = try scanAttributeValue() guard attributeNamesSeen.insert(name.description).inserted else { if recovering { // Keep the element: record the duplicate and drop the later // occurrence rather than failing the whole start tag. diagnostics.append(PureXML.Parsing.Diagnostic(PureXML.Parsing.ParseError.duplicateAttribute(name: name.description, mark))) continue } throw PureXML.Parsing.ParseError.duplicateAttribute(name: name.description, mark) } attributes.append(PureXML.Model.Attribute(name: name, value: value)) // A following attribute needs whitespace before it. Check the raw // byte when on the fast path to avoid buffering the next name char. if let byte = reader.peekByte() { // '>', '/', '?', or whitespace may legally follow a value; any // other byte means a missing separator before the next attribute. let isSeparator = byte == 0x3E || byte == 0x2F || byte == 0x3F || byte == 0x20 || byte == 0x09 || byte == 0x0A || byte == 0x0D if !isSeparator { throw PureXML.Parsing.ParseError.missingSpaceBeforeAttribute(reader.mark) } } else if let next = reader.peek(), next != ">", next != "/", next != "?", !next.isWhitespace { throw PureXML.Parsing.ParseError.missingSpaceBeforeAttribute(reader.mark) } } } mutating func scanAttributeValue() throws -> String { let mark = reader.mark let (quoteByte, quoteCharacter) = try consumeAttributeValueOpenQuote() var raw = "" var length = 0 // Whether any '&' was seen: without one the value has no references, so // the decode pass (and its full re-scan for '&') is skipped. The // scanners visit every byte already, so this costs nothing extra. var sawAmpersand = false while true { // Bulk byte runs first: plain ASCII value text (the common case) // skips the per-character machinery; the run stops before the // quote, '<', CR, or any byte the Character path must inspect. if let run = reader.attributeRunBytes(quote: quoteByte, sawAmpersand: &sawAmpersand) { length += run.utf8.count try checkContent(length, mark) raw += run continue } // Byte-level check before the Character peek: whenever // `attributeRunBytes` stopped because the next byte is the closing // quote (its only non-empty-input stopping reason besides '<', CR, // or a control/non-ASCII byte the Character path must inspect), // this avoids buffering that quote as a Character only to compare // and discard it a moment later. if reader.peekByte() == quoteByte { break } guard let character = reader.peek(), character != quoteCharacter else { break } length += 1 try checkContent(length, mark) if character == "<" { throw PureXML.Parsing.ParseError.rawLessThanInAttribute(reader.mark) } guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else { throw PureXML.Parsing.ParseError.invalidCharacter(reader.mark) } if character == "&" { sawAmpersand = true } // 3.3.3: a literal whitespace character in an attribute value // normalizes to a space; this happens before reference decoding, // so whitespace that arrives via a character reference survives. raw.append(character.isXMLWhitespace ? " " : character) reader.advance() } // The loop above only exits (breaks) when the next position is nil (end // of input) or the closing quote: there is no third case. try consumeAttributeValueCloseQuote(byte: quoteByte, character: quoteCharacter) // No '&' means no references to decode: the normalized run is the value. guard sawAmpersand else { return raw } return try decodeReferences(raw, at: mark) } /// Detects and consumes the attribute value's opening quote, returning /// both its byte and `Character` forms: `quoteByte` drives /// `attributeRunBytes`'s bulk scan and the closing-quote byte check; /// `quoteCharacter` is the fallback for wherever the byte fast path does /// not apply (a streaming source with no owned byte storage, or a /// lookahead buffer some earlier scan already populated). Byte fast path: /// `peekByte()` only returns non-nil when the lookahead buffer is empty, /// so `reader.consume(_:)` below reaches its own byte fast path /// (`matchesBytes`) directly, with no Character buffering at all. private mutating func consumeAttributeValueOpenQuote() throws -> (byte: UInt8, character: Character) { if let byte = reader.peekByte(), byte == 0x22 || byte == 0x27 { reader.consume(byte == 0x22 ? "\"" : "'") return (byte, byte == 0x22 ? "\"" : "'") } guard let quote = reader.peek(), quote == "\"" || quote == "'" else { throw PureXML.Parsing.ParseError.unquotedAttributeValue(reader.mark) } reader.advance() return (quote == "\"" ? 0x22 : 0x27, quote) } /// Consumes the attribute value's closing quote. Called only once the /// value-scanning loop has already confirmed (via a peek that consumed /// nothing) the next position is either the quote or end of input, so /// whichever check fired that break already buffered nothing new; /// re-checking the byte first, then falling back to the already-buffered /// Character peek, reaches the same verdict `reader.consume(String(quote))` /// would, without that function's fresh `String` allocation and double /// `Character` iteration on every single attribute value in the document. private mutating func consumeAttributeValueCloseQuote(byte quoteByte: UInt8, character quoteCharacter: Character) throws { if reader.peekByte() == quoteByte { reader.advance() return } guard reader.peek() == quoteCharacter else { throw PureXML.Parsing.ParseError.unexpectedEndOfInput(reader.mark) } reader.advance() } }