Stage 3 of the faithful libyaml scanner port: the token producer. Translates scan_to_next_token, fetch_more_tokens/fetch_next_token dispatch, and the fetchers for stream start/end, document indicators, block entry, key, value (with the simple-key splice and roll-indent), plus scan_plain_scalar with its line-break folding. Reuses the ported indent engine, simple-key mechanism, character classes, and reader. Constructs not yet ported (flow collections, quoted scalars, anchors, tags, directives, block scalars) throw an explicit 'not yet ported' error rather than mis-scan; the port is not yet wired to the public API. Milestone: the ported scanner tokenizes '- a:' followed by a same-column 'b:' as siblings (one BLOCK-MAPPING-START), matching libyaml, fixing the original block-mapping bug by construction; a more-indented value still nests. Unit-tested for the sibling case, the nested case, a simple pair, and multi-line plain-scalar folding. 305 tests, no regression.
222 lines
8.6 KiB
Swift
222 lines
8.6 KiB
Swift
extension PureYAML.Parsing.LibYAML.Scanner {
|
|
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
|
|
private typealias Token = PureYAML.Parsing.LibYAML.Token
|
|
private typealias ScannerError = PureYAML.Parsing.LibYAML.ScannerError
|
|
|
|
/// Produce every token for the input, mirroring repeated calls to libyaml's
|
|
/// `yaml_parser_fetch_next_token` until STREAM-END. Tokens are never dequeued,
|
|
/// so `tokens_parsed` stays 0 and `QUEUE_INSERT` positions are absolute, which
|
|
/// matches the simple-key splice.
|
|
mutating func scanAll() throws -> [PureYAML.Parsing.LibYAML.Token] {
|
|
while !streamEndProduced {
|
|
try fetchNextToken()
|
|
}
|
|
return indentEngine.tokens
|
|
}
|
|
|
|
/// Port of `yaml_parser_scan_to_next_token`: skip a leading BOM, spaces (and
|
|
/// tabs only in flow context or where a simple key is not allowed), a comment,
|
|
/// and a single line break, which re-allows a simple key in the block context.
|
|
private mutating func scanToNextToken() {
|
|
while true {
|
|
if reader.column == 0, reader.peek() == "\u{FEFF}" {
|
|
reader.skip()
|
|
}
|
|
while reader.peek() == " " || ((flowLevel > 0 || !simpleKeyAllowed) && reader.peek() == "\t") {
|
|
reader.skip()
|
|
}
|
|
if reader.peek() == "#" {
|
|
while !Classify.isBreakZ(reader.peek()) {
|
|
reader.skip()
|
|
}
|
|
}
|
|
if Classify.isBreak(reader.peek()) {
|
|
reader.skipLine()
|
|
if flowLevel == 0 {
|
|
simpleKeyAllowed = true
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_next_token`: emit STREAM-START first, otherwise
|
|
/// skip to the next token, expire stale simple keys, unroll the indentation to
|
|
/// the current column, and dispatch on the leading character. Constructs not
|
|
/// yet ported throw an explicit error rather than mis-scan.
|
|
mutating func fetchNextToken() throws {
|
|
if !streamStartProduced {
|
|
fetchStreamStart()
|
|
return
|
|
}
|
|
|
|
scanToNextToken()
|
|
mark = reader.mark
|
|
try staleSimpleKeys()
|
|
indentEngine.unrollIndent(column: reader.column, mark: reader.mark)
|
|
mark = reader.mark
|
|
try dispatchToken()
|
|
}
|
|
|
|
/// Dispatch on the leading character: stream end, directives and document
|
|
/// markers at column 0, then the indicators.
|
|
private mutating func dispatchToken() throws {
|
|
let character = reader.peek()
|
|
if Classify.isZ(character) {
|
|
fetchStreamEnd()
|
|
return
|
|
}
|
|
if reader.column == 0, character == "%" {
|
|
throw notPorted("directive")
|
|
}
|
|
if reader.column == 0, isDocumentMarker("-") {
|
|
fetchDocumentIndicator(.documentStart)
|
|
return
|
|
}
|
|
if reader.column == 0, isDocumentMarker(".") {
|
|
fetchDocumentIndicator(.documentEnd)
|
|
return
|
|
}
|
|
guard let character else {
|
|
return
|
|
}
|
|
try dispatchIndicator(character)
|
|
}
|
|
|
|
/// Dispatch the block-structure indicators and plain scalars; unported
|
|
/// constructs throw an explicit error.
|
|
private mutating func dispatchIndicator(_ character: Unicode.Scalar) throws {
|
|
if let unported = unportedConstruct(character) {
|
|
throw notPorted(unported)
|
|
}
|
|
switch character {
|
|
case "-" where Classify.isBlankZ(reader.peek(1)):
|
|
fetchBlockEntry()
|
|
case "?" where flowLevel > 0 || Classify.isBlankZ(reader.peek(1)):
|
|
fetchKey()
|
|
case ":" where flowLevel > 0 || Classify.isBlankZ(reader.peek(1)):
|
|
try fetchValue()
|
|
default:
|
|
try fetchPlainScalar()
|
|
}
|
|
}
|
|
|
|
/// The constructs whose fetchers are not yet ported, named for the error.
|
|
private func unportedConstruct(_ character: Unicode.Scalar) -> String? {
|
|
switch character {
|
|
case "[", "]", "{", "}", ",": "flow collection"
|
|
case "*", "&": "anchor or alias"
|
|
case "!": "tag"
|
|
case "|" where flowLevel == 0, ">" where flowLevel == 0: "block scalar"
|
|
case "'", "\"": "flow scalar"
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_stream_start`.
|
|
private mutating func fetchStreamStart() {
|
|
// indent is already -1 and simpleKeys already holds the block-context slot.
|
|
simpleKeyAllowed = true
|
|
streamStartProduced = true
|
|
indentEngine.enqueue(Token(type: .streamStart, startMark: reader.mark, endMark: reader.mark))
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_stream_end`.
|
|
private mutating func fetchStreamEnd() {
|
|
if reader.column != 0 {
|
|
// Force a new line in the mark, as libyaml does.
|
|
mark = PureYAML.Parsing.Mark(line: reader.line + 1, column: 0, index: reader.index)
|
|
}
|
|
indentEngine.unrollIndent(column: -1, mark: mark)
|
|
// remove_simple_key cannot throw here: a required key would already have
|
|
// been resolved before stream end in the block + plain core.
|
|
simpleKeys[simpleKeys.count - 1].possible = false
|
|
simpleKeyAllowed = false
|
|
streamEndProduced = true
|
|
indentEngine.enqueue(Token(type: .streamEnd, startMark: mark, endMark: mark))
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_document_indicator`.
|
|
private mutating func fetchDocumentIndicator(_ type: PureYAML.Parsing.LibYAML.TokenType) {
|
|
indentEngine.unrollIndent(column: -1, mark: reader.mark)
|
|
simpleKeys[simpleKeys.count - 1].possible = false
|
|
simpleKeyAllowed = false
|
|
let startMark = reader.mark
|
|
reader.skip()
|
|
reader.skip()
|
|
reader.skip()
|
|
indentEngine.enqueue(Token(type: type, startMark: startMark, endMark: reader.mark))
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_block_entry`.
|
|
private mutating func fetchBlockEntry() {
|
|
if flowLevel == 0 {
|
|
indentEngine.rollIndent(column: reader.column, number: -1, type: .blockSequenceStart, mark: reader.mark)
|
|
}
|
|
simpleKeys[simpleKeys.count - 1].possible = false
|
|
simpleKeyAllowed = true
|
|
let startMark = reader.mark
|
|
reader.skip()
|
|
indentEngine.enqueue(Token(type: .blockEntry, startMark: startMark, endMark: reader.mark))
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_key`.
|
|
private mutating func fetchKey() {
|
|
if flowLevel == 0 {
|
|
indentEngine.rollIndent(column: reader.column, number: -1, type: .blockMappingStart, mark: reader.mark)
|
|
}
|
|
simpleKeys[simpleKeys.count - 1].possible = false
|
|
simpleKeyAllowed = flowLevel == 0
|
|
let startMark = reader.mark
|
|
reader.skip()
|
|
indentEngine.enqueue(Token(type: .key, startMark: startMark, endMark: reader.mark))
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_value`, including the simple-key splice: a
|
|
/// possible simple key inserts its `KEY` token at the recorded position and
|
|
/// rolls the block mapping indent to the key's column.
|
|
private mutating func fetchValue() throws {
|
|
let top = simpleKeys.count - 1
|
|
if simpleKeys[top].possible {
|
|
let key = simpleKeys[top]
|
|
indentEngine.insert(Token(type: .key, startMark: key.mark, endMark: key.mark), atTokenNumber: key.tokenNumber)
|
|
indentEngine.rollIndent(column: key.mark.column, number: key.tokenNumber, type: .blockMappingStart, mark: key.mark)
|
|
simpleKeys[top].possible = false
|
|
simpleKeyAllowed = false
|
|
} else {
|
|
if flowLevel == 0 {
|
|
indentEngine.rollIndent(column: reader.column, number: -1, type: .blockMappingStart, mark: reader.mark)
|
|
}
|
|
simpleKeyAllowed = flowLevel == 0
|
|
}
|
|
let startMark = reader.mark
|
|
reader.skip()
|
|
indentEngine.enqueue(Token(type: .value, startMark: startMark, endMark: reader.mark))
|
|
}
|
|
|
|
/// Port of `yaml_parser_fetch_plain_scalar`.
|
|
private mutating func fetchPlainScalar() throws {
|
|
mark = reader.mark
|
|
try saveSimpleKey()
|
|
simpleKeyAllowed = false
|
|
let token = try scanPlainScalar()
|
|
indentEngine.enqueue(token)
|
|
}
|
|
|
|
private func isDocumentMarker(_ character: Unicode.Scalar) -> Bool {
|
|
reader.peek(0) == character
|
|
&& reader.peek(1) == character
|
|
&& reader.peek(2) == character
|
|
&& Classify.isBlankZ(reader.peek(3))
|
|
}
|
|
|
|
private func notPorted(_ what: String) -> ScannerError {
|
|
ScannerError(
|
|
context: "while scanning a token",
|
|
problem: "construct not yet ported: \(what)",
|
|
mark: reader.mark,
|
|
)
|
|
}
|
|
}
|