A block sequence entry cannot follow a flow collection that closed earlier
on the same line ('- { y: z }- invalid', '- [a] - b'): the entry would
begin a sequence inline after a complete node. The scanner accepted it,
producing extra sequence items.
The scanner now records when a flow collection closes at the top level on
the current line and rejects a following block entry. The flag resets at
each line break, so a flow collection on its own line is unaffected, and a
nested block entry ('- -') has no preceding flow close, so it stays valid.
Measured against the official YAML test-suite, parse-level conformance
rises from 276/333 (82.9%) to 277/333 (83.2%) with no regression.
208 lines
5.8 KiB
Swift
208 lines
5.8 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 {
|
|
scanner.closeIndentation(&scannerBox.state)
|
|
try scanner.enforceDirectiveHasDocument(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()
|
|
scanner.captureBlockScalarBlankLine(&scannerBox.state)
|
|
scannerBox.state.isAtLineStart = true
|
|
scannerBox.state.sawContentOnLine = false
|
|
scannerBox.state.sawMappingValueOnLine = false
|
|
scannerBox.state.sawFlowCloseOnLine = false
|
|
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 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 isStreamEnd: Bool {
|
|
if case .streamEnd = self {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var isTerminator: Bool {
|
|
isDedent || isDocumentEnd || isDocumentStart || isFlowTerminator || isStreamEnd
|
|
}
|
|
}
|