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 { // The prefix holds any surplus indentation past the block's content level for an // otherwise-empty line (see `preserveBlockScalarSurplusOnBlankLine`); empty otherwise. state.append(.scalar(value: state.blockScalarContentPrefix, style: .plain)) state.blockScalarContentPrefix = "" } else if state.pendingBlockScalarIndentation { state.pendingBlockScalarLeadingBlankWhitespace.append(state.lastLeadingWhitespace) } } 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 } // A tab is separation white space in YAML (only indentation forbids it), so it ends an // indicator just as a space does: `---\tscalar`, `-\titem`, `?\tkey`. return character == " " || character == "\t" || isLineBreak(character) } func isMappingValueBoundary(_ character: Character?) -> Bool { guard let character else { return true } // A tab is separation white space, so it follows a `:` value indicator as a space does. return character == " " || character == "\t" || isLineBreak(character) || character == "," || character == "]" || character == "}" } func isBlockMappingValueBoundary(_ character: Character?) -> Bool { guard let character else { return true } return character == " " || character == "\t" || 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[.. 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) if isAtMappingValueIndicator(state: continuationState) { // A continuation line ending at `:` is normally a new implicit key, so the scalar // stops before it. Two cases keep folding instead, with the `:` left to be scanned as // the value indicator: a block explicit key (`? foo` then `bar :`), where the line is // still the key's content, and any flow scalar, since a flow key may span lines // (`{ multi`/`line: v }`) where a block implicit key may not. let foldsAcrossMappingValue = state.flowDepth > 0 || state.pendingExplicitKey guard foldsAcrossMappingValue else { return value } state = continuationState value += blankLineCount == 0 ? " " : String(repeating: "\n", count: blankLineCount) value += continuation 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 // A line of only white space is empty for folding, including one that holds a tab: a tab // is not indentation, but a whitespace-only line is still blank, so consume it here. _ = blankProbe.consume { character in character == " " || character == "\t" } 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 { // Leading white space beyond the line's indentation folds away in a plain scalar. A tab is // not indentation, so the indentation scan leaves it here; skip it (and any spaces after it) // so a continuation line like `\t3rd` joins as `3rd`, not `\t3rd`. while let character = state.reader.peek(), character == " " || character == "\t" { state.reader.advance() } 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 } // A comment opens after white space, a tab as well as a space, so a `#` preceded by either // ends the plain scalar: `d\t#X` is the value `d` with a trailing comment. if character == " " || character == "\t", state.reader.peek(offset: 1) == "#" { return true } if character == "#", accumulated.isEmpty || accumulated.last == " " || accumulated.last == "\t" { 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), canSynthesizeImplicitBlockKey(state) else { return false } state.append(.mappingKey, mark: start, endMark: start) return true } /// Whether a scalar followed by `:` may start an implicit mapping key here. In block style a /// line holds at most one mapping pair, so once a block-level `:` has appeared on the line a /// further key would form a forbidden inline nested mapping; in flow style several pairs per /// line are allowed, so that restriction does not apply. In flow a scalar that is the content of /// an open explicit key (`?`) is the key itself, not a new implicit key. /// Records the implicit indentation levels a block entry with an inline node introduces: the /// node's own indent (one past the dash), and the dash's indent as a sibling level so an inline /// item's plain value stops folding at a later entry that returns to it (`: -␣b` then ` - - c`). /// The sibling level is skipped under a pending explicit key (`? - x`), whose sequence key drives /// implicit indentation itself. func recordBlockEntryImplicitIndentation(start: PureYAML.Parsing.Mark, state: inout State) { guard hasNodeOnSameLine(afterCurrentPositionIn: state.reader) else { return } state.validImplicitIndentation.insert(start.column + 1) if !state.pendingExplicitKey { state.validImplicitIndentation.insert(start.column - 1) } } func canSynthesizeImplicitBlockKey(_ state: State) -> Bool { if state.flowDepth > 0 { return state.flowContexts.last?.hasExplicitKey != true } return !state.sawMappingValueOnLine } }