Three root causes: (1) a '%' begins a directive only in directive position (before document content, outside flow); everywhere else it is plain content, fixing misfires inside plain scalars, flow mappings, and block scalars. (2) %YAML accepts any 1.x (a higher minor is treated as 1.2 without error), rejects major != 1 and duplicate %YAML, and requires a following '---'; reserved and %TAG directives are accepted and ignored. A directive not followed by a document start is rejected with the new directiveWithoutDocument error. (3) An explicit '...' end marker closes the current document so following content begins a fresh document in parseStream; single-document parse still rejects multi-document input at compose time. Tests/UnsupportedYAMLGapsFixtures.swift, StreamParsingTests, ScannerTests, and ParsingErrorTests were updated in lockstep to assert the now-correct behavior (percent-after-content is plain text; '...' then content is two documents; %YAML 1.3 is valid while 2.0 and duplicate %YAML are rejected); no test was weakened and multi-document rejection for single-document parse is still covered. Flips 15 official YAML test suite cases (295/402); SF5V, 9MMA, B63P, H7TQ now correctly rejected. 6LVF and W4TN are reclassified to json-mismatch (blocked by other scalar gaps). Updates the manifest, fixtures, counts, and roadmap.
231 lines
6.2 KiB
Swift
231 lines
6.2 KiB
Swift
extension PureYAML.Parsing {
|
|
private final class TokenScannerStateBox {
|
|
var state: Scanner.State
|
|
|
|
init(state: Scanner.State) {
|
|
self.state = state
|
|
}
|
|
}
|
|
|
|
struct TokenCursor {
|
|
private var tokens: [Token]
|
|
private(set) var index: Int
|
|
private var previousToken: Token?
|
|
private var scanner: Scanner?
|
|
private var scannerBox: TokenScannerStateBox?
|
|
|
|
init(tokens: [Token]) {
|
|
self.tokens = tokens.filter { !$0.kind.isComment }
|
|
index = 0
|
|
previousToken = nil
|
|
scanner = nil
|
|
scannerBox = nil
|
|
}
|
|
|
|
init(yaml: String, scanner: Scanner = .init()) {
|
|
var state = Scanner.State(reader: Reader(yaml))
|
|
state.append(.streamStart)
|
|
tokens = state.tokens.filter { !$0.kind.isComment }
|
|
state.tokens.removeAll(keepingCapacity: true)
|
|
index = 0
|
|
previousToken = nil
|
|
self.scanner = scanner
|
|
scannerBox = TokenScannerStateBox(state: state)
|
|
}
|
|
|
|
var current: Token? {
|
|
guard tokens.indices.contains(index) else {
|
|
return nil
|
|
}
|
|
return tokens[index]
|
|
}
|
|
|
|
var previous: Token? {
|
|
previousToken
|
|
}
|
|
|
|
mutating func peek(offset: Int = 1) throws -> Token? {
|
|
try ensureAvailable(offset: offset)
|
|
let targetIndex = index + offset
|
|
guard tokens.indices.contains(targetIndex) else {
|
|
return nil
|
|
}
|
|
return tokens[targetIndex]
|
|
}
|
|
|
|
mutating func advance() throws -> Token? {
|
|
try ensureAvailable(offset: 0)
|
|
guard tokens.indices.contains(index) else {
|
|
return nil
|
|
}
|
|
let token = tokens[index]
|
|
previousToken = token
|
|
index += 1
|
|
compactConsumedTokens()
|
|
try ensureAvailable(offset: 0)
|
|
return token
|
|
}
|
|
|
|
private mutating func ensureAvailable(offset: Int) throws {
|
|
while tokens.count <= index + offset, scannerBox != nil {
|
|
try scanMoreTokens()
|
|
}
|
|
}
|
|
|
|
private mutating func scanMoreTokens() throws {
|
|
guard let scanner, let scannerBox else {
|
|
return
|
|
}
|
|
|
|
while scannerBox.state.tokens.isEmpty {
|
|
guard !scannerBox.state.reader.isAtEnd else {
|
|
if scannerBox.state.hasPendingDirective {
|
|
throw PureYAML.Parsing.ParseError.directiveWithoutDocument(
|
|
line: scannerBox.state.reader.mark.line,
|
|
)
|
|
}
|
|
scanner.closeIndentation(&scannerBox.state)
|
|
scannerBox.state.append(.streamEnd)
|
|
drainScannedTokens(from: &scannerBox.state)
|
|
self.scannerBox = nil
|
|
return
|
|
}
|
|
|
|
if scanner.isLineBreak(scannerBox.state.reader.peek()) {
|
|
scannerBox.state.reader.advance()
|
|
scannerBox.state.isAtLineStart = true
|
|
continue
|
|
}
|
|
|
|
if scannerBox.state.isAtLineStart {
|
|
try scanner.scanIndentation(&scannerBox.state)
|
|
if scannerBox.state.reader.isAtEnd {
|
|
continue
|
|
}
|
|
if scanner.isLineBreak(scannerBox.state.reader.peek()) {
|
|
continue
|
|
}
|
|
if try scanner.scanDirectiveOrDocumentMarker(&scannerBox.state) {
|
|
if !scannerBox.state.tokens.isEmpty {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
scannerBox.state.isAtLineStart = false
|
|
}
|
|
|
|
try scanner.scanToken(&scannerBox.state)
|
|
}
|
|
|
|
drainScannedTokens(from: &scannerBox.state)
|
|
}
|
|
|
|
private mutating func drainScannedTokens(from state: inout Scanner.State) {
|
|
tokens.append(contentsOf: state.tokens.filter { !$0.kind.isComment })
|
|
state.tokens.removeAll(keepingCapacity: true)
|
|
}
|
|
|
|
private mutating func compactConsumedTokens() {
|
|
guard index > 4096 else {
|
|
return
|
|
}
|
|
tokens.removeFirst(index)
|
|
index = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureYAML.Parsing.TokenKind {
|
|
var isAlias: Bool {
|
|
if case .alias = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isNodeProperty: Bool {
|
|
switch self {
|
|
case .anchor, .tag:
|
|
true
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
var isBlockEntry: Bool {
|
|
if case .blockEntry = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isComment: Bool {
|
|
if case .comment = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isDedent: Bool {
|
|
if case .dedent = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isDocumentEnd: Bool {
|
|
if case .documentEnd = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isDocumentStart: Bool {
|
|
if case .documentStart = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isFlowTerminator: Bool {
|
|
switch self {
|
|
case .flowEntry, .flowMappingEnd, .flowSequenceEnd:
|
|
true
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
var isIndent: Bool {
|
|
if case .indent = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isMappingKey: Bool {
|
|
if case .mappingKey = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isMappingValue: Bool {
|
|
if case .mappingValue = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isStreamEnd: Bool {
|
|
if case .streamEnd = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isTerminator: Bool {
|
|
isDedent || isDocumentEnd || isDocumentStart || isFlowTerminator || isStreamEnd
|
|
}
|
|
}
|