PureYAML/Sources/Parsing/LibYAML.Parser.Node.swift
Mihaela Mihaljevic 521062d974 feat(parsing): port libyaml parser state machine to Swift
Ports parser.c: the one-event-at-a-time state machine that turns the
scanner's token stream into the YAML event stream. Covers stream and
document framing, directive processing with %TAG handle resolution and
default handles, the node property/tag/anchor production, block and
indentless sequences, block mappings, and flow sequences and mappings
including the single-pair `?` mapping forms.

Output verified against the libyaml reference event dumper: the empty
mapping value, %TAG resolution, and shorthand tags all match byte for
byte. 331 tests, no regression.
2026-06-24 17:05:59 +02:00

100 lines
4.6 KiB
Swift

extension PureYAML.Parsing.LibYAML.Parser {
typealias NodeProperties = PureYAML.Parsing.LibYAML.NodeProperties
/// Port of `yaml_parser_parse_node`. An ALIAS is emitted directly; otherwise
/// the node's anchor/tag properties are consumed, the tag is resolved, and the
/// content token selects a scalar, a flow/block collection start, or an empty
/// scalar standing in for a property-only node.
mutating func parseNode(block: Bool, indentlessSequence: Bool) throws -> Event {
let token = peekToken()
if case let .alias(name) = token.type {
state = states.removeLast()
skipToken()
return .alias(anchor: name, mark: token.startMark)
}
let props = consumeNodeProperties()
let tag = try props.tagHandle.map {
try resolveTag(handle: $0, suffix: props.tagSuffix ?? "", startMark: props.startMark, tagMark: props.tagMark)
}
return try dispatchNodeContent(block: block, indentlessSequence: indentlessSequence, props: props, tag: tag)
}
/// Consume `properties ::= TAG ANCHOR? | ANCHOR TAG?` ahead of the content.
private mutating func consumeNodeProperties() -> NodeProperties {
var token = peekToken()
var props = NodeProperties(anchor: nil, tagHandle: nil, tagSuffix: nil, startMark: token.startMark, tagMark: token.startMark)
if case let .anchor(name) = token.type {
props.anchor = name
skipToken()
token = peekToken()
if case let .tag(handle, suffix) = token.type {
props.tagHandle = handle
props.tagSuffix = suffix
props.tagMark = token.startMark
skipToken()
}
} else if case let .tag(handle, suffix) = token.type {
props.tagHandle = handle
props.tagSuffix = suffix
props.tagMark = token.startMark
skipToken()
token = peekToken()
if case let .anchor(name) = token.type {
props.anchor = name
skipToken()
}
}
return props
}
/// Select the event for the content token following a node's properties.
private mutating func dispatchNodeContent(block: Bool, indentlessSequence: Bool, props: NodeProperties, tag: String?) throws -> Event {
let token = peekToken()
if indentlessSequence, case .blockEntry = token.type {
state = .indentlessSequenceEntry
return .sequenceStart(anchor: props.anchor, tag: tag, style: .block, mark: props.startMark)
}
if let event = contentEvent(token: token, block: block, props: props, tag: tag) {
return event
}
return try emptyNodeOrError(token: token, block: block, props: props, tag: tag)
}
/// A scalar or a flow/block collection start, or nil if the token is neither.
private mutating func contentEvent(token: Token, block: Bool, props: NodeProperties, tag: String?) -> Event? {
switch token.type {
case let .scalar(value, style):
state = states.removeLast()
skipToken()
return .scalar(value: value, anchor: props.anchor, tag: tag, style: style, mark: props.startMark)
case .flowSequenceStart:
state = .flowSequenceFirstEntry
return .sequenceStart(anchor: props.anchor, tag: tag, style: .flow, mark: props.startMark)
case .flowMappingStart:
state = .flowMappingFirstKey
return .mappingStart(anchor: props.anchor, tag: tag, style: .flow, mark: props.startMark)
case .blockSequenceStart where block:
state = .blockSequenceFirstEntry
return .sequenceStart(anchor: props.anchor, tag: tag, style: .block, mark: props.startMark)
case .blockMappingStart where block:
state = .blockMappingFirstKey
return .mappingStart(anchor: props.anchor, tag: tag, style: .block, mark: props.startMark)
default:
return nil
}
}
/// An empty scalar when the node carried only properties, else a parse error.
private mutating func emptyNodeOrError(token: Token, block: Bool, props: NodeProperties, tag: String?) throws -> Event {
if props.anchor != nil || tag != nil {
state = states.removeLast()
return .scalar(value: "", anchor: props.anchor, tag: tag, style: .plain, mark: props.startMark)
}
throw ParserError(
context: block ? "while parsing a block node" : "while parsing a flow node",
problem: "did not find expected node content",
mark: token.startMark,
)
}
}