A block scalar that is the document's own root node (`>`, `|`, or after `---` on the same line) may begin its content at column 0: the document node's indentation is -1, so column-0 content is still more indented than its parent. The scanner read the base level as 0 and required content strictly deeper than it, so the lines fell through indentation handling and were mis-scanned as a stray plain scalar after the document, raising "expected document boundary". A root block scalar and an under-indented nested one (`- |` then unindented content) present identical indentation state at the first content line, so the two cannot be told apart there. Capture the distinction at header-scan time instead: a root block scalar has the base indentation and no mapping value, explicit key, block-entry implicit indent, or flow context introducing it on its line. Only then is its parent the document; a sequence item or mapping value keeps its parent's indent as the floor, so its under-indented content is still rejected, unchanged. Raises yaml-test-suite parse-level to 291/333 (87.4%) and value-level to 173/191 (90.6%). A regression suite covers the root cases (including a leading '#' as literal content) and the nested under-indented cases that must error. 410 tests pass.
177 lines
7.4 KiB
Swift
177 lines
7.4 KiB
Swift
extension PureYAML.Parsing.Scanner {
|
|
func scanComment(_ state: inout State) {
|
|
let start = state.reader.mark
|
|
state.reader.advance()
|
|
if state.reader.peek() == " " {
|
|
state.reader.advance()
|
|
}
|
|
let value = state.reader.consume { character in
|
|
!isLineBreak(character)
|
|
}
|
|
state.append(.comment(value), mark: start, endMark: state.reader.mark)
|
|
}
|
|
|
|
func scanQuotedScalar(
|
|
_ style: PureYAML.Parsing.ScalarStyle,
|
|
state: inout State,
|
|
) throws {
|
|
let start = state.reader.mark
|
|
let quote: Character = style == .doubleQuoted ? "\"" : "'"
|
|
state.reader.advance()
|
|
var value = ""
|
|
var previousWasEscape = false
|
|
while let character = state.reader.peek() {
|
|
if style == .singleQuoted, character == quote, state.reader.peek(offset: 1) == quote {
|
|
value.append(character)
|
|
state.reader.advance()
|
|
value.append(character)
|
|
state.reader.advance()
|
|
continue
|
|
}
|
|
if character == quote, !previousWasEscape {
|
|
break
|
|
}
|
|
value.append(character)
|
|
previousWasEscape = character == "\\" && style == .doubleQuoted && !previousWasEscape
|
|
state.reader.advance()
|
|
if character != "\\" {
|
|
previousWasEscape = false
|
|
}
|
|
// A line that begins with a document marker (`---` / `...`) is forbidden inside a
|
|
// multi-line scalar: the marker would be ambiguous with a document boundary.
|
|
if isLineBreak(character), isAtDocumentMarker(state.reader) {
|
|
throw PureYAML.Parsing.ParseError.documentMarkerInScalar(line: state.reader.mark.line)
|
|
}
|
|
}
|
|
guard state.reader.peek() == quote else {
|
|
throw PureYAML.Parsing.ParseError.unterminatedQuotedString(line: start.line)
|
|
}
|
|
state.reader.advance()
|
|
// Unlike a plain scalar (which consumes the space before a `:` into itself), a quoted scalar
|
|
// stops at its closing quote, so the reader sits at any spaces before a `:`. A space between
|
|
// an implicit key and its mapping-value indicator is allowed, so look past them.
|
|
if isAtMappingValueIndicatorAfterSpaces(state: state), canSynthesizeImplicitBlockKey(state) {
|
|
// A block implicit key must lie on a single line, so a quoted scalar that spanned a line
|
|
// break (its open and close quotes sit on different lines) cannot be one. The rule is
|
|
// block-only: a flow mapping key may span lines, so the check is gated on block context.
|
|
// An explicit key (`? ...`) carries no `:` directly after the scalar, so never reaches here.
|
|
guard start.line == state.reader.mark.line || state.flowDepth > 0 else {
|
|
throw PureYAML.Parsing.ParseError.multilineImplicitKey(line: start.line)
|
|
}
|
|
state.append(.mappingKey, mark: start, endMark: start)
|
|
}
|
|
state.append(.scalar(value: value, style: style), mark: start, endMark: state.reader.mark)
|
|
}
|
|
|
|
func scanBlockScalarHeader(
|
|
_ style: PureYAML.Parsing.BlockScalarStyle,
|
|
state: inout State,
|
|
) {
|
|
let start = state.reader.mark
|
|
var chomping = PureYAML.Parsing.BlockScalarChomping.clip
|
|
var indentationIndicator: Int?
|
|
state.reader.advance()
|
|
while let character = state.reader.peek(), isBlockScalarHeaderCharacter(character) {
|
|
if character == "-" {
|
|
chomping = .strip
|
|
} else if character == "+" {
|
|
chomping = .keep
|
|
} else if character.isNumber {
|
|
indentationIndicator = Int(String(character))
|
|
}
|
|
state.reader.advance()
|
|
}
|
|
state.append(
|
|
.blockScalarHeader(style: style, chomping: chomping),
|
|
mark: start,
|
|
endMark: state.reader.mark,
|
|
)
|
|
state.pendingBlockScalarIndentation = true
|
|
state.pendingBlockScalarIndentationIndicator = indentationIndicator
|
|
// A root block scalar (the document's own node) has no enclosing collection: it sits at the
|
|
// base indentation with no mapping value, explicit key, block-entry implicit indent, or flow
|
|
// context introducing it on this line. Only then is its parent the document (indentation -1),
|
|
// letting its content begin at column 0. A sequence item or mapping value keeps its parent's
|
|
// indent as the floor instead.
|
|
state.pendingBlockScalarIsRootNode = state.indentation == [0]
|
|
&& !state.sawMappingValueOnLine
|
|
&& !state.pendingExplicitKey
|
|
&& state.validImplicitIndentation.isEmpty
|
|
&& state.flowDepth == 0
|
|
}
|
|
|
|
func scanBlockScalarContent(_ state: inout State) {
|
|
let start = state.reader.mark
|
|
let value = state.blockScalarContentPrefix + state.reader.consume { character in
|
|
!isLineBreak(character)
|
|
}
|
|
state.blockScalarContentPrefix = ""
|
|
state.append(.scalar(value: value, style: .plain), mark: start, endMark: state.reader.mark)
|
|
}
|
|
|
|
func scanNamedToken(
|
|
kind: (String) -> PureYAML.Parsing.TokenKind,
|
|
missingNameError: (Int) -> PureYAML.Parsing.ParseError,
|
|
state: inout State,
|
|
) throws {
|
|
let start = state.reader.mark
|
|
state.reader.advance()
|
|
let name = state.reader.consume { character in
|
|
!isTokenTerminator(character)
|
|
}
|
|
guard !name.isEmpty else {
|
|
throw missingNameError(start.line)
|
|
}
|
|
state.append(kind(name), mark: start, endMark: state.reader.mark)
|
|
}
|
|
|
|
func scanTag(_ state: inout State) throws {
|
|
let start = state.reader.mark
|
|
state.reader.advance()
|
|
var value = "!"
|
|
if state.reader.peek() == "<" {
|
|
value.append(state.reader.advance() ?? "<")
|
|
var didClose = false
|
|
while let character = state.reader.peek() {
|
|
value.append(character)
|
|
state.reader.advance()
|
|
if character == ">" {
|
|
didClose = true
|
|
break
|
|
}
|
|
}
|
|
guard didClose else {
|
|
throw PureYAML.Parsing.ParseError.unterminatedTag(line: start.line)
|
|
}
|
|
} else {
|
|
value += state.reader.consume { character in
|
|
!isTokenTerminator(character)
|
|
}
|
|
value = expandTag(value, state: state)
|
|
}
|
|
state.append(.tag(value), mark: start, endMark: state.reader.mark)
|
|
}
|
|
|
|
func scanPlainScalar(_ state: inout State) {
|
|
let start = state.reader.mark
|
|
var value = ""
|
|
while let character = state.reader.peek() {
|
|
if shouldEndPlainScalar(character, state: state, accumulated: value) {
|
|
break
|
|
}
|
|
value.append(character)
|
|
state.reader.advance()
|
|
}
|
|
var trimmed = trimTrailingSpaces(value)
|
|
let isMappingKey = appendMappingKeyIfNeeded(start: start, state: &state)
|
|
if !isMappingKey {
|
|
trimmed = scanPlainScalarContinuations(
|
|
after: trimmed,
|
|
firstLineIndent: start.column - 1,
|
|
state: &state,
|
|
)
|
|
}
|
|
state.append(.scalar(value: trimmed, style: .plain), mark: start, endMark: state.reader.mark)
|
|
}
|
|
}
|