A quoted mapping key followed by a space and then ':' ('"key" : value')
failed to parse: the scanner synthesizes the implicit mapping-key token by
checking the character immediately after the key, but a quoted scalar
stops at its closing quote, leaving the separating space, whereas a plain
scalar consumes that space into itself and ends at the colon. The quoted
path now looks past spaces for the colon, which the spec allows between an
implicit key and its value indicator.
Quoted keys with no space, quoted values, and plain keys (which already
worked with a space) are unaffected. Measured against the official YAML
test-suite, parse-level conformance rises from 238/333 (71.5%) to
240/333 (72.1%), with no regression.
318 lines
11 KiB
Swift
318 lines
11 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
/// A blank line inside a block scalar is preserved as empty content so that
|
|
/// chomping and folding can apply to it. The guard keeps blank lines outside
|
|
/// block scalars unaffected; `isAtLineStart` is true here only when the
|
|
/// previous line already ended, so a content line's own terminating break
|
|
/// does not emit a blank. Blank lines between the header and the first
|
|
/// content line precede the indent token, so they are buffered and emitted
|
|
/// once the content indentation is established.
|
|
func captureBlockScalarBlankLine(_ state: inout State) {
|
|
guard state.isAtLineStart else {
|
|
return
|
|
}
|
|
if state.blockScalarIndentation != nil {
|
|
state.append(.scalar(value: "", style: .plain))
|
|
} else if state.pendingBlockScalarIndentation {
|
|
state.pendingBlockScalarLeadingBlankLines += 1
|
|
}
|
|
}
|
|
|
|
func startsWith(
|
|
_ prefix: String,
|
|
_ reader: PureYAML.Parsing.Reader,
|
|
) -> Bool {
|
|
for (offset, character) in prefix.enumerated() {
|
|
guard reader.peek(offset: offset) == character else {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isLineBreak(_ character: Character?) -> Bool {
|
|
character == "\n" || character == "\r" || character == "\r\n"
|
|
}
|
|
|
|
func isBlockEntry(_ reader: PureYAML.Parsing.Reader) -> Bool {
|
|
reader.peek() == "-" && isIndicatorBoundary(reader.peek(offset: 1))
|
|
}
|
|
|
|
func isIndicatorBoundary(_ character: Character?) -> Bool {
|
|
guard let character else {
|
|
return true
|
|
}
|
|
return character == " " || isLineBreak(character)
|
|
}
|
|
|
|
func isMappingValueBoundary(_ character: Character?) -> Bool {
|
|
guard let character else {
|
|
return true
|
|
}
|
|
return character == " "
|
|
|| isLineBreak(character)
|
|
|| character == ","
|
|
|| character == "]"
|
|
|| character == "}"
|
|
}
|
|
|
|
func isBlockMappingValueBoundary(_ character: Character?) -> Bool {
|
|
guard let character else {
|
|
return true
|
|
}
|
|
return character == " " || isLineBreak(character)
|
|
}
|
|
|
|
func isTokenTerminator(_ character: Character) -> Bool {
|
|
character == " "
|
|
|| isLineBreak(character)
|
|
|| character == ","
|
|
|| character == "["
|
|
|| character == "]"
|
|
|| character == "{"
|
|
|| character == "}"
|
|
|| character == "#"
|
|
}
|
|
|
|
func canStartAnchorName(_ character: Character?) -> Bool {
|
|
guard let character else {
|
|
return false
|
|
}
|
|
return !isTokenTerminator(character)
|
|
&& character != "*"
|
|
&& character != "&"
|
|
&& character != "!"
|
|
&& character != "\""
|
|
&& character != "'"
|
|
}
|
|
|
|
func hasNodeOnSameLine(afterCurrentPositionIn reader: PureYAML.Parsing.Reader) -> Bool {
|
|
var offset = 0
|
|
while reader.peek(offset: offset) == " " {
|
|
offset += 1
|
|
}
|
|
guard let character = reader.peek(offset: offset) else {
|
|
return false
|
|
}
|
|
return !isLineBreak(character) && character != "#"
|
|
}
|
|
|
|
func isFlowDelimiter(_ character: Character) -> Bool {
|
|
character == ","
|
|
|| character == "["
|
|
|| character == "]"
|
|
|| character == "{"
|
|
|| character == "}"
|
|
}
|
|
|
|
func isBlockScalarHeaderCharacter(_ character: Character) -> Bool {
|
|
character == "+"
|
|
|| character == "-"
|
|
|| character.isNumber
|
|
}
|
|
|
|
func trimTrailingSpaces(_ value: String) -> String {
|
|
var end = value.endIndex
|
|
while end > value.startIndex {
|
|
let previous = value.index(before: end)
|
|
guard value[previous] == " " else {
|
|
break
|
|
}
|
|
end = previous
|
|
}
|
|
return String(value[..<end])
|
|
}
|
|
|
|
func scanPlainScalarContinuations(
|
|
after firstLine: String,
|
|
firstLineIndent: Int,
|
|
state: inout State,
|
|
) -> String {
|
|
var value = firstLine
|
|
while true {
|
|
var probe = state.reader
|
|
guard isLineBreak(probe.peek()) else {
|
|
return value
|
|
}
|
|
|
|
probe.advance()
|
|
let blankLineCount = consumeBlankLines(&probe)
|
|
|
|
_ = probe.consume { character in
|
|
character == " "
|
|
}
|
|
let indentationWidth = probe.mark.column - 1
|
|
|
|
guard !probe.isAtEnd, !isLineBreak(probe.peek()), probe.peek() != "#" else {
|
|
return value
|
|
}
|
|
|
|
// A plain scalar never spans a document marker (`---` / `...`); stop the continuation at
|
|
// one so it begins (or ends) the document rather than folding into the scalar's value.
|
|
if indentationWidth == 0, isAtDocumentMarker(probe) {
|
|
return value
|
|
}
|
|
|
|
let baseIndentation = plainScalarContinuationBaseIndentation(state: state, firstLineIndent: firstLineIndent)
|
|
guard indentationWidth > baseIndentation else {
|
|
return value
|
|
}
|
|
if isBlockEntry(probe), state.validImplicitIndentation.contains(indentationWidth) {
|
|
return value
|
|
}
|
|
|
|
var continuationState = state
|
|
continuationState.reader = probe
|
|
let continuation = scanPlainScalarContinuationLine(&continuationState)
|
|
guard !isAtMappingValueIndicator(state: continuationState) else {
|
|
return value
|
|
}
|
|
|
|
state = continuationState
|
|
if blankLineCount == 0 {
|
|
value += " "
|
|
} else {
|
|
value += String(repeating: "\n", count: blankLineCount)
|
|
}
|
|
value += continuation
|
|
}
|
|
}
|
|
|
|
/// Advance `probe` over a run of blank lines (lines that are empty or only spaces), returning
|
|
/// how many were consumed; the probe is left at the start of the next non-blank line.
|
|
func consumeBlankLines(_ probe: inout PureYAML.Parsing.Reader) -> Int {
|
|
var count = 0
|
|
while true {
|
|
var blankProbe = probe
|
|
_ = blankProbe.consume { character in
|
|
character == " "
|
|
}
|
|
guard isLineBreak(blankProbe.peek()) else {
|
|
break
|
|
}
|
|
blankProbe.advance()
|
|
probe = blankProbe
|
|
count += 1
|
|
}
|
|
return count
|
|
}
|
|
|
|
/// Whether the reader is positioned at a document marker (`---` or `...`) followed by an
|
|
/// indicator boundary, i.e. the marker that starts or ends a document.
|
|
func isAtDocumentMarker(_ reader: PureYAML.Parsing.Reader) -> Bool {
|
|
(startsWith("---", reader) || startsWith("...", reader)) && isIndicatorBoundary(reader.peek(offset: 3))
|
|
}
|
|
|
|
func plainScalarContinuationBaseIndentation(state: State, firstLineIndent: Int) -> Int {
|
|
// When the scalar begins exactly at the current indentation top, it is a block value on its
|
|
// own line, so its own indent was pushed onto the stack; its continuation lines sit at that
|
|
// same indent, and the level they must exceed is the enclosing block's: the indent below on
|
|
// the stack, or -1 at the document root, where nothing encloses the scalar and a
|
|
// continuation may sit at column 1. For an inline value the top of the stack already is the
|
|
// enclosing block's indent, so it is used directly.
|
|
if state.indentation.last == firstLineIndent {
|
|
return state.indentation.count >= 2 ? state.indentation[state.indentation.count - 2] : -1
|
|
}
|
|
return state.indentation.last ?? 0
|
|
}
|
|
|
|
func scanPlainScalarContinuationLine(_ state: inout State) -> String {
|
|
var value = ""
|
|
while let character = state.reader.peek(), !isLineBreak(character) {
|
|
if character == " ", state.reader.peek(offset: 1) == "#" {
|
|
break
|
|
}
|
|
if character == "#", value.isEmpty || value.last == " " {
|
|
break
|
|
}
|
|
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
|
|
break
|
|
}
|
|
if state.flowDepth > 0, isFlowDelimiter(character) {
|
|
break
|
|
}
|
|
value.append(character)
|
|
state.reader.advance()
|
|
}
|
|
return trimTrailingSpaces(value)
|
|
}
|
|
|
|
func skipLine(_ state: inout State) {
|
|
_ = state.reader.consume { character in
|
|
!isLineBreak(character)
|
|
}
|
|
}
|
|
|
|
func expandTag(
|
|
_ tag: String,
|
|
state: State,
|
|
) -> String {
|
|
let handles = state.tagHandles.keys.sorted { first, second in
|
|
first.count > second.count
|
|
}
|
|
for handle in handles where tag.hasPrefix(handle) {
|
|
let suffix = tag.dropFirst(handle.count)
|
|
return (state.tagHandles[handle] ?? handle) + suffix
|
|
}
|
|
return tag
|
|
}
|
|
|
|
func shouldEndPlainScalar(
|
|
_ character: Character,
|
|
state: State,
|
|
accumulated: String,
|
|
) -> Bool {
|
|
if isLineBreak(character) {
|
|
return true
|
|
}
|
|
if character == " ", state.reader.peek(offset: 1) == "#" {
|
|
return true
|
|
}
|
|
if character == "#", accumulated.isEmpty || accumulated.last == " " {
|
|
return true
|
|
}
|
|
if character == ":", shouldEndPlainScalarAtMappingValue(state: state) {
|
|
return true
|
|
}
|
|
if state.flowDepth > 0, isFlowDelimiter(character) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func shouldEndPlainScalarAtMappingValue(state: State) -> Bool {
|
|
if state.flowDepth > 0 {
|
|
return state.isExpectingFlowMappingKey
|
|
|| isMappingValueBoundary(state.reader.peek(offset: 1))
|
|
}
|
|
return isBlockMappingValueBoundary(state.reader.peek(offset: 1))
|
|
}
|
|
|
|
func isAtMappingValueIndicator(state: State) -> Bool {
|
|
state.reader.peek() == ":" && shouldEndPlainScalarAtMappingValue(state: state)
|
|
}
|
|
|
|
/// Whether a mapping-value indicator (`: ` or `:` at line end) follows, allowing spaces before
|
|
/// the colon. Used after a quoted scalar, where the reader stops at the closing quote and any
|
|
/// separating spaces remain, unlike a plain scalar that ends at the colon itself.
|
|
func isAtMappingValueIndicatorAfterSpaces(state: State) -> Bool {
|
|
var offset = 0
|
|
while state.reader.peek(offset: offset) == " " {
|
|
offset += 1
|
|
}
|
|
return state.reader.peek(offset: offset) == ":"
|
|
&& isBlockMappingValueBoundary(state.reader.peek(offset: offset + 1))
|
|
}
|
|
|
|
@discardableResult
|
|
func appendMappingKeyIfNeeded(
|
|
start: PureYAML.Parsing.Mark,
|
|
state: inout State,
|
|
) -> Bool {
|
|
guard isAtMappingValueIndicator(state: state) else {
|
|
return false
|
|
}
|
|
state.append(.mappingKey, mark: start, endMark: start)
|
|
return true
|
|
}
|
|
}
|