extension PureYAML.Parsing.LibYAML.Scanner { private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass private typealias Token = PureYAML.Parsing.LibYAML.Token private typealias Mark = PureYAML.Parsing.Mark /// The longest version number libyaml accepts (`MAX_NUMBER_LENGTH`). private var maxVersionNumberLength: Int { 9 } /// Port of `yaml_parser_fetch_directive`: reset the indentation and simple keys, /// then scan a `%YAML` or `%TAG` directive token. mutating func fetchDirective() throws { indentEngine.unrollIndent(column: -1, mark: reader.mark) try removeSimpleKey() simpleKeyAllowed = false let token = try scanDirective() indentEngine.enqueue(token) } /// Port of `yaml_parser_scan_directive`, extended to YAML 1.2 §6.8.1: a `%YAML` /// or `%TAG` directive produces its token; any other (reserved) directive is /// ignored with a warning, producing a `reservedDirective` token whose content /// the parser drops, rather than libyaml's hard rejection. The reserved token /// still marks the document as a directives document, so it must be followed by /// a `---`. private mutating func scanDirective() throws -> Token { let startMark = reader.mark reader.skip() let name = try scanDirectiveName(startMark: startMark) if name == "YAML" { let version = try scanVersionDirectiveValue(startMark: startMark) let token = Token(type: .versionDirective(major: version.major, minor: version.minor), startMark: startMark, endMark: reader.mark) try consumeDirectiveTail(startMark: startMark) return token } if name == "TAG" { let value = try scanTagDirectiveValue(startMark: startMark) let token = Token(type: .tagDirective(handle: value.handle, prefix: value.prefix), startMark: startMark, endMark: reader.mark) try consumeDirectiveTail(startMark: startMark) return token } skipReservedDirectiveLine() return Token(type: .reservedDirective, startMark: startMark, endMark: reader.mark) } /// Consume a reserved directive's parameters through the end of the line; the /// whole line is ignored per YAML 1.2 §6.8.1. private mutating func skipReservedDirectiveLine() { while !Classify.isBreakZ(reader.peek()) { reader.skip() } if Classify.isBreak(reader.peek()) { reader.skipLine() } } /// Eat trailing blanks and an optional comment, require a line break, eat it. private mutating func consumeDirectiveTail(startMark: Mark) throws { while Classify.isBlank(reader.peek()) { reader.skip() } if reader.peek() == "#" { while !Classify.isBreakZ(reader.peek()) { reader.skip() } } guard Classify.isBreakZ(reader.peek()) else { throw directiveError("while scanning a directive", "did not find expected comment or line break", startMark) } if Classify.isBreak(reader.peek()) { reader.skipLine() } } /// Port of `yaml_parser_scan_directive_name`: the alphanumeric name after `%`, /// which must be non-empty and followed by a blank or line break. private mutating func scanDirectiveName(startMark: Mark) throws -> String { var name = "" while let character = reader.peek(), Classify.isAlpha(character) { name.unicodeScalars.append(character) reader.skip() } guard !name.isEmpty else { throw directiveError("while scanning a directive", "could not find expected directive name", startMark) } guard Classify.isBlankZ(reader.peek()) else { throw directiveError("while scanning a directive", "found unexpected non-alphabetical character", startMark) } return name } /// Port of `yaml_parser_scan_version_directive_value`: `major.minor`. private mutating func scanVersionDirectiveValue(startMark: Mark) throws -> (major: Int, minor: Int) { while Classify.isBlank(reader.peek()) { reader.skip() } let major = try scanVersionDirectiveNumber(startMark: startMark) guard reader.peek() == "." else { throw directiveError("while scanning a %YAML directive", "did not find expected digit or '.' character", startMark) } reader.skip() let minor = try scanVersionDirectiveNumber(startMark: startMark) // The version must be followed by separation; a comment needs a space // before its `#`, so `%YAML 1.1#...` (no space) is malformed. libyaml // accepts it, treating the `#` as an adjacent comment. guard Classify.isBlankZ(reader.peek()) else { throw directiveError("while scanning a %YAML directive", "did not find expected digit or '.' character", startMark) } return (major, minor) } /// Port of `yaml_parser_scan_version_directive_number`: a bounded run of digits. private mutating func scanVersionDirectiveNumber(startMark: Mark) throws -> Int { var value = 0 var length = 0 while let character = reader.peek(), Classify.isDigit(character) { length += 1 guard length <= maxVersionNumberLength else { throw directiveError("while scanning a %YAML directive", "found extremely long version number", startMark) } value = value * 10 + Int(character.value - Unicode.Scalar("0").value) reader.skip() } guard length > 0 else { throw directiveError("while scanning a %YAML directive", "did not find expected version number", startMark) } return value } /// Port of `yaml_parser_scan_tag_directive_value`: `!handle! prefix`. private mutating func scanTagDirectiveValue(startMark: Mark) throws -> (handle: String, prefix: String) { while Classify.isBlank(reader.peek()) { reader.skip() } let handle = try scanTagHandle(directive: true, startMark: startMark) guard Classify.isBlank(reader.peek()) else { throw directiveError("while scanning a %TAG directive", "did not find expected whitespace", startMark) } while Classify.isBlank(reader.peek()) { reader.skip() } let prefix = try scanTagURI(uriChar: true, directive: true, head: "", startMark: startMark) guard Classify.isBlankZ(reader.peek()) else { throw directiveError("while scanning a %TAG directive", "did not find expected whitespace or line break", startMark) } return (handle, prefix) } private func directiveError(_ context: String, _ problem: String, _ mark: Mark) -> PureYAML.Parsing.LibYAML.ScannerError { PureYAML.Parsing.LibYAML.ScannerError(context: context, problem: problem, mark: mark) } }