feat(parsing): port libyaml directive scanner to Swift
Adds the %YAML and %TAG directive scanner, the final scanner piece. Ports fetch_directive, scan_directive, scan_directive_name, scan_version_directive_value/number, and scan_tag_directive_value, reusing the already-ported tag handle/URI scanners. Adds versionDirective and tagDirective token kinds. The libyaml scanner is now fully ported: stream, document markers, directives, block + flow structure, plain/quoted/block scalars, anchors, aliases, and tags. 319 tests, no regression.
This commit is contained in:
parent
ed1904534c
commit
ab05580518
131
Sources/Parsing/LibYAML.Scanner.Directive.swift
Normal file
131
Sources/Parsing/LibYAML.Scanner.Directive.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user