diff --git a/Sources/Parsing/LibYAML.Scanner.Directive.swift b/Sources/Parsing/LibYAML.Scanner.Directive.swift new file mode 100644 index 0000000..9ff5d2f --- /dev/null +++ b/Sources/Parsing/LibYAML.Scanner.Directive.swift @@ -0,0 +1,131 @@ +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`. Eats the `%`, reads the directive name, + /// dispatches to the `%YAML` version value or the `%TAG` handle/prefix value, + /// then consumes the rest of the line including any comment. + private mutating func scanDirective() throws -> Token { + let startMark = reader.mark + reader.skip() + let name = try scanDirectiveName(startMark: startMark) + let token: Token + if name == "YAML" { + let version = try scanVersionDirectiveValue(startMark: startMark) + token = Token(type: .versionDirective(major: version.major, minor: version.minor), startMark: startMark, endMark: reader.mark) + } else if name == "TAG" { + let value = try scanTagDirectiveValue(startMark: startMark) + token = Token(type: .tagDirective(handle: value.handle, prefix: value.prefix), startMark: startMark, endMark: reader.mark) + } else { + throw directiveError("while scanning a directive", "found unknown directive name", startMark) + } + try consumeDirectiveTail(startMark: startMark) + return token + } + + /// 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) + 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) + } +} diff --git a/Sources/Parsing/LibYAML.Scanner.Fetch.swift b/Sources/Parsing/LibYAML.Scanner.Fetch.swift index c8bbc30..4ef95f7 100644 --- a/Sources/Parsing/LibYAML.Scanner.Fetch.swift +++ b/Sources/Parsing/LibYAML.Scanner.Fetch.swift @@ -68,7 +68,8 @@ extension PureYAML.Parsing.LibYAML.Scanner { return } if reader.column == 0, character == "%" { - throw notPorted("directive") + try fetchDirective() + return } if reader.column == 0, isDocumentMarker("-") { fetchDocumentIndicator(.documentStart) @@ -296,12 +297,4 @@ extension PureYAML.Parsing.LibYAML.Scanner { && reader.peek(2) == character && Classify.isBlankZ(reader.peek(3)) } - - private func notPorted(_ what: String) -> ScannerError { - ScannerError( - context: "while scanning a token", - problem: "construct not yet ported: \(what)", - mark: reader.mark, - ) - } } diff --git a/Sources/Parsing/LibYAML.TokenType.swift b/Sources/Parsing/LibYAML.TokenType.swift index 32ed4ac..acde7af 100644 --- a/Sources/Parsing/LibYAML.TokenType.swift +++ b/Sources/Parsing/LibYAML.TokenType.swift @@ -18,6 +18,8 @@ extension PureYAML.Parsing.LibYAML { case flowEntry case key case value + case versionDirective(major: Int, minor: Int) + case tagDirective(handle: String, prefix: String) case anchor(name: String) case alias(name: String) case tag(handle: String, suffix: String) diff --git a/Tests/LibYAMLScannerFetchTests.swift b/Tests/LibYAMLScannerFetchTests.swift index b6fce0e..7e2ed26 100644 --- a/Tests/LibYAMLScannerFetchTests.swift +++ b/Tests/LibYAMLScannerFetchTests.swift @@ -237,4 +237,28 @@ struct LibYAMLScannerFetchTests { .streamEnd, ]) } + + @Test("a YAML version directive") + func test_versionDirective() throws { + let tokens = try tokenize("%YAML 1.2\n---\nx\n") + #expect(tokens == [ + .streamStart, + .versionDirective(major: 1, minor: 2), + .documentStart, + .scalar(value: "x", style: .plain), + .streamEnd, + ]) + } + + @Test("a TAG directive maps a handle to a prefix") + func test_tagDirective() throws { + let tokens = try tokenize("%TAG !e! tag:example.com,2000:\n---\nx\n") + #expect(tokens == [ + .streamStart, + .tagDirective(handle: "!e!", prefix: "tag:example.com,2000:"), + .documentStart, + .scalar(value: "x", style: .plain), + .streamEnd, + ]) + } }