extension PureYAML.Parsing.Scanner { /// Result of one scanning iteration over the source. Both the batch ``scan`` /// loop and the incremental ``PureYAML/Parsing/TokenCursor`` driver share the /// same step so the two paths cannot diverge (a divergence is what once let a /// tab-indentation error slip past the streaming parser while the batch scanner /// caught it). enum Step { /// Nothing remains; the caller appends stream end and stops. case endOfInput /// A line break, blank line, or comment was consumed; restart the loop. case advanced /// A directive or document marker was scanned; restart the loop. case scannedMarker /// An ordinary token was scanned. case scannedToken } /// Performs one scanning iteration: consumes a leading line break or blank line, /// scans indentation at a line start, then scans either a directive / document /// marker or one ordinary token. Resets `simpleKeyAllowed` at every block-context /// line break so a tab at the next line start is recognized as (illegal) /// indentation rather than (legal) in-line separation. func scanStep(_ state: inout State) throws -> Step { if state.reader.isAtEnd { return .endOfInput } if isLineBreak(state.reader.peek()) { state.reader.advance() state.isAtLineStart = true if state.flowDepth == 0 { state.simpleKeyAllowed = true } return .advanced } if state.isAtLineStart { // A line whose entire content is blanks (spaces and/or tabs) before the // break is a blank line: it carries no indentation and no token, matching // scanner.c's scan_to_next_token, which eats the blanks and the break // before any indent processing. Consuming it here keeps a whitespace-only // line (e.g. `· \t`) from emitting a spurious indent token or being // mis-scanned as content. if consumeBlankLineRemainder(&state) { return .advanced } try scanIndentation(&state) if state.reader.isAtEnd || isLineBreak(state.reader.peek()) { return .advanced } if try scanDirectiveOrDocumentMarker(&state) { return .scannedMarker } state.isAtLineStart = false } try scanToken(&state) return .scannedToken } /// When the reader sits on a run of blanks (spaces and/or tabs) that ends the /// line (a line break or end of input follows), consumes that run plus the line /// break and reports true: the line was blank. Otherwise leaves the reader where /// it was and reports false. Used so a whitespace-only line (e.g. `· \t`) is /// skipped rather than mis-scanned as plain-scalar content. func consumeBlankLineRemainder(_ state: inout State) -> Bool { var offset = 0 while let character = state.reader.peek(offset: offset), character == " " || character == "\t" { offset += 1 } let next = state.reader.peek(offset: offset) guard next == nil || isLineBreak(next) else { return false } for _ in 0 ..< offset { state.reader.advance() } if isLineBreak(state.reader.peek()) { state.reader.advance() if state.flowDepth == 0 { state.simpleKeyAllowed = true } } return true } }