PureYAML/Sources/Parsing/LibYAML.Parser.swift
Mihaela Mihaljevic f7500400ee feat(parsing): exact scalar end positions in the located parse
The scalar event now carries the scanner token's end mark, so a located
scalar's range spans the whole token (end exclusive, one past the last
character) instead of being start-anchored. This gives consumers exact
highlight widths and is the prerequisite for surgical, position-based
text edits. Only the scalar event case gained a field; the value tree,
its equality, and the event dump (which compares value and style, not
marks) are unchanged, so the canonical-event and whole-suite gates stay
green (343 tests).
2026-06-26 02:25:01 +02:00

149 lines
6.1 KiB
Swift

extension PureYAML.Parsing.LibYAML {
/// A faithful port of libyaml's `parser.c`: a one-event-at-a-time state
/// machine that consumes the scanner's token stream and produces the YAML
/// event stream. The token queue is materialized up front by the scanner, so
/// `peekToken`/`skipToken` index into a fixed array rather than pulling lazily,
/// which is equivalent because simple-key resolution lives entirely in the
/// scanner.
struct Parser {
let tokens: [Token]
var cursor = 0
var state: ParserState = .streamStart
var states: [ParserState] = []
var marks: [Mark] = []
var tagDirectives: [(handle: String, prefix: String)] = []
/// Whether a bare (implicit) document may begin here: true for the first
/// document and after an explicit `...` end marker, false otherwise. YAML
/// 1.2 allows a bare document to follow `...`, which libyaml rejects.
var allowBareDocument = true
init(_ input: String = "") throws {
var scanner = Scanner(input)
tokens = try scanner.scanAll()
}
init(tokens: [Token]) {
self.tokens = tokens
}
/// Drive the state machine until STREAM-END, collecting every event.
mutating func parse() throws -> [Event] {
var events: [Event] = []
while state != .end {
try events.append(stateMachine())
}
return events
}
/// The head of the token queue. STREAM-END is always the final token and
/// the machine halts once it has been consumed, so the clamp is never the
/// value actually parsed; it only avoids an out-of-range index.
func peekToken() -> Token {
tokens[min(cursor, tokens.count - 1)]
}
mutating func skipToken() {
cursor += 1
}
/// Port of `yaml_parser_state_machine`: dispatch on the current state. The
/// flat 24-way switch is split into document/block/flow groups to keep each
/// dispatcher's branching within the complexity budget.
mutating func stateMachine() throws -> Event {
switch state {
case .streamStart, .implicitDocumentStart, .documentStart, .documentContent, .documentEnd:
try documentStateMachine()
case .blockNode, .blockNodeOrIndentlessSequence, .flowNode,
.blockSequenceFirstEntry, .blockSequenceEntry, .indentlessSequenceEntry,
.blockMappingFirstKey, .blockMappingKey, .blockMappingValue:
try nodeStateMachine()
case .flowSequenceFirstEntry, .flowSequenceEntry, .flowSequenceEntryMappingKey,
.flowSequenceEntryMappingValue, .flowSequenceEntryMappingEnd,
.flowMappingFirstKey, .flowMappingKey, .flowMappingValue, .flowMappingEmptyValue:
try flowStateMachine()
case .end:
throw endError()
}
}
private mutating func documentStateMachine() throws -> Event {
switch state {
case .streamStart:
try parseStreamStart()
case .implicitDocumentStart:
try parseDocumentStart()
case .documentStart:
try parseDocumentStart()
case .documentContent:
try parseDocumentContent()
default:
try parseDocumentEnd()
}
}
private mutating func nodeStateMachine() throws -> Event {
switch state {
case .blockNode:
try parseNode(block: true, indentlessSequence: false)
case .blockNodeOrIndentlessSequence:
try parseNode(block: true, indentlessSequence: true)
case .flowNode:
try parseNode(block: false, indentlessSequence: false)
case .blockSequenceFirstEntry:
try parseBlockSequenceEntry(first: true)
case .blockSequenceEntry:
try parseBlockSequenceEntry(first: false)
case .indentlessSequenceEntry:
try parseIndentlessSequenceEntry()
case .blockMappingFirstKey:
try parseBlockMappingKey(first: true)
case .blockMappingKey:
try parseBlockMappingKey(first: false)
default:
try parseBlockMappingValue()
}
}
private mutating func flowStateMachine() throws -> Event {
switch state {
case .flowSequenceFirstEntry:
try parseFlowSequenceEntry(first: true)
case .flowSequenceEntry:
try parseFlowSequenceEntry(first: false)
case .flowSequenceEntryMappingKey:
try parseFlowSequenceEntryMappingKey()
case .flowSequenceEntryMappingValue:
try parseFlowSequenceEntryMappingValue()
case .flowSequenceEntryMappingEnd:
try parseFlowSequenceEntryMappingEnd()
case .flowMappingFirstKey:
try parseFlowMappingKey(first: true)
case .flowMappingKey:
try parseFlowMappingKey(first: false)
case .flowMappingValue:
try parseFlowMappingValue(empty: false)
default:
try parseFlowMappingValue(empty: true)
}
}
private func endError() -> ParserError {
ParserError(context: nil, problem: "parsed past the end of the stream", mark: peekToken().startMark)
}
/// Port of `yaml_parser_process_empty_scalar`: an empty plain scalar event
/// stands in for an absent node.
func processEmptyScalar(at mark: Mark) -> Event {
.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark, endMark: mark)
}
}
}
extension PureYAML.Parsing.LibYAML.Parser {
typealias Token = PureYAML.Parsing.LibYAML.Token
typealias Mark = PureYAML.Parsing.Mark
typealias Event = PureYAML.Parsing.Event
typealias ParserError = PureYAML.Parsing.LibYAML.ParserError
typealias ParserState = PureYAML.Parsing.LibYAML.ParserState
}