A line that begins with a document marker ('---' or '...') followed by a
boundary is forbidden inside a multi-line scalar, since it is ambiguous
with a document boundary. A multi-line quoted scalar folded such a line
into its value instead of rejecting it.
scanQuotedScalar now checks, on crossing each line break, whether the next
line begins a document marker, and rejects the scalar if so. The check
requires the marker at the line start with a following boundary, so inline
dashes ('a --- b'), '---' with no boundary ('---b'), an indented '---',
and dots not at a line start stay ordinary content.
Measured against the official YAML test-suite, parse-level conformance
rises from 268/333 (80.5%) to 270/333 (81.1%) with no regression.
167 lines
6.7 KiB
Swift
167 lines
6.7 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
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|