extension PureYAML.Parsing.TokenEventParser { mutating func parseMappingPair() throws -> PureYAML.Parsing.NodeResult { // A subsequent pair's key may carry an anchor or tag (`&a key: …`). The first pair's // properties are consumed when the mapping is entered; consuming here covers the rest, and // is a no-op (`.none`) when there are none. let keyProperties = try consumeProperties() _ = try expect("mapping key") { $0.isMappingKey } let key = try parseNode(properties: keyProperties) // An explicit key (`? key`) with no following `:` has an implicit null value, so several // `? key` entries in a row (a set, for instance) each map to null rather than being an error. guard case .mappingValue = cursor.current?.kind else { return try emitNullMappingValue(at: key.endMark) } let valueIndicator = try expect("mapping value") { kind in if case .mappingValue = kind { return true } return false } let properties = try consumeProperties() try enforceValuePropertyIndented(properties, valueIndicator: valueIndicator) if cursor.current?.kind.isIndent == true { return try parseIndentedMappingValue(properties: properties) } if properties == .none, try isCommentInterruptedScalarValue(after: valueIndicator) { return try rejectCommentInterruptedScalar() } if cursor.current?.kind.isMappingKey == true, hasNodeOnSameLine(after: valueIndicator) { return try parseBlockMapping( properties: properties, includeIndentedContinuation: true, minimumColumn: cursor.current?.mark.column, ) } if properties != .none || hasNodeOnSameLine(after: valueIndicator) { return try parseNode(properties: properties) } if cursor.current?.kind.isBlockEntry == true { return try parseBlockSequence( properties: properties, allowMappingKeyTerminator: true, ) } return try emitNullMappingValue(at: valueIndicator.endMark) } /// Parses a mapping value that begins on an indented line below the `:`. A leading block entry /// is a block sequence; anything else is a node. The closing dedent is consumed unless the value /// ended on a sibling mapping key, which the surrounding mapping continues from. mutating func parseIndentedMappingValue( properties: PureYAML.Parsing.NodeProperties, ) throws -> PureYAML.Parsing.NodeResult { _ = try expect("indent") { $0.isIndent } let result: PureYAML.Parsing.NodeResult = if cursor.current?.kind.isBlockEntry == true { try parseBlockSequence(properties: properties, allowMappingKeyTerminator: true) } else { try parseNode(properties: properties) } if cursor.current?.kind.isDedent == true { _ = try expect("dedent") { $0.isDedent } } else if cursor.current?.kind.isMappingKey != true { _ = try expect("dedent") { $0.isDedent } } return result } /// A value's anchor or tag on a line below the `:` must be indented past the key. A properly /// indented property is preceded by an indent token and consumed inside the indented value, so a /// property consumed here on a line below the `:` sits at the key's level and is malformed. The /// last consumed property is `cursor.previous`; checking it catches both a single under-indented /// property (`seq:` then `&anchor`) and a second one after an inline first (`key: &x` then a /// `!!tag` in column 1). func enforceValuePropertyIndented( _ properties: PureYAML.Parsing.NodeProperties, valueIndicator: PureYAML.Parsing.Token, ) throws { guard properties != .none, let lastProperty = cursor.previous, lastProperty.mark.line != valueIndicator.mark.line else { return } throw PureYAML.Parsing.ParseError.unexpectedIndentation(line: lastProperty.mark.line) } /// Emit an empty (null) scalar as a mapping value and return its node result, used where a /// mapping entry has no value: an explicit key with no `:`, or a `key:` with nothing after it. mutating func emitNullMappingValue(at mark: PureYAML.Parsing.Mark) throws -> PureYAML.Parsing.NodeResult { try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark)) return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark) } /// Rejects a plain scalar that the scanner split into a value-line scalar, an indent, and a /// further scalar. The scanner folds a genuine multi-line plain scalar into a single token, so /// this token shape arises only when a comment broke its continuation: a comment cannot /// interrupt a multi-line plain scalar, so the construct is malformed. mutating func rejectCommentInterruptedScalar() throws -> PureYAML.Parsing.NodeResult { let first = try expect("plain scalar") { kind in if case .scalar(_, .plain) = kind { return true } return false } throw PureYAML.Parsing.ParseError.commentInMultilineScalar(line: first.mark.line) } mutating func consumeProperties() throws -> PureYAML.Parsing.NodeProperties { var properties = PureYAML.Parsing.NodeProperties.none while let token = cursor.current { switch token.kind { case let .anchor(anchor): properties.anchor = anchor properties.mark = properties.mark ?? token.mark _ = try cursor.advance() case let .tag(tag): properties.tag = tag properties.mark = properties.mark ?? token.mark _ = try cursor.advance() default: return properties } } return properties } func decodeScalar( _ value: String, style: PureYAML.Parsing.ScalarStyle, line: Int, ) throws -> String { switch style { case .doubleQuoted: try scalarParser.parseDoubleQuoted("\"\(value)\"", line: line) case .folded, .literal: value case .plain: value case .singleQuoted: try scalarParser.parseSingleQuoted("'\(value)'", line: line) } } func decodedScalar(from token: PureYAML.Parsing.Token) throws -> String { guard case let .scalar(value, style) = token.kind else { throw unexpectedToken(expected: "scalar") } return try decodeScalar(value, style: style, line: token.mark.line) } func hasNodeOnSameLine(after token: PureYAML.Parsing.Token) -> Bool { guard let current = cursor.current, !current.kind.isTerminator else { return false } return current.mark.line == token.mark.line } mutating func isCommentInterruptedScalarValue(after token: PureYAML.Parsing.Token) throws -> Bool { guard case .scalar(_, .plain) = cursor.current?.kind else { return false } guard cursor.current?.mark.line == token.mark.line else { return false } guard try cursor.peek()?.kind.isIndent == true else { return false } guard try isScalar(cursor.peek(offset: 2)) else { return false } return true } mutating func isCommentInterruptedScalar() throws -> Bool { guard case .scalar(_, .plain) = cursor.current?.kind else { return false } guard try cursor.peek()?.kind.isIndent == true else { return false } guard try isScalar(cursor.peek(offset: 2)) else { return false } return true } func isScalar(_ token: PureYAML.Parsing.Token?) -> Bool { guard case .scalar = token?.kind else { return false } return true } mutating func consumeDedents(atOrAbove minimumColumn: Int) throws { while let width = dedentWidth(cursor.current), width >= minimumColumn { _ = try expect("dedent") { $0.isDedent } } } mutating func consumeMappingSiblingBoundaryDedents(minimumColumn: Int) throws { while try canConsumeMappingSiblingBoundaryDedent(minimumColumn: minimumColumn) { _ = try expect("dedent") { $0.isDedent } } } mutating func canConsumeMappingSiblingBoundaryDedent(minimumColumn: Int) throws -> Bool { guard let width = dedentWidth(cursor.current), let next = try cursor.peek(), next.kind.isMappingKey, next.mark.column >= minimumColumn else { return false } return width == next.mark.column - 1 } mutating func consumeMappingSiblingBoundaryReindent(minimumColumn: Int) throws { guard let dedent = dedentWidth(cursor.current), dedent < minimumColumn, try indentWidth(cursor.peek()) == minimumColumn, let mappingKey = try cursor.peek(offset: 2), mappingKey.kind.isMappingKey, mappingKey.mark.column == minimumColumn + 1 else { return } _ = try expect("dedent") { $0.isDedent } _ = try expect("indent") { $0.isIndent } } func dedentWidth(_ token: PureYAML.Parsing.Token?) -> Int? { guard case let .dedent(width) = token?.kind else { return nil } return width } func indentWidth(_ token: PureYAML.Parsing.Token?) -> Int? { guard case let .indent(width) = token?.kind else { return nil } return width } func canContinueBlockMapping(minimumColumn: Int?) -> Bool { guard let current = cursor.current, current.kind.isMappingKey || startsNodeProperties(current.kind) else { return false } guard let minimumColumn else { return true } return current.mark.column >= minimumColumn } /// Whether a token kind begins a node's properties (an anchor or a tag), which a mapping key may /// carry, so the next pair's key is `&a key: …` rather than a bare key. func startsNodeProperties(_ kind: PureYAML.Parsing.TokenKind) -> Bool { switch kind { case .anchor, .tag: true default: false } } func canEndBlockMapping( at token: PureYAML.Parsing.Token?, minimumColumn: Int? = nil, ) -> Bool { guard let token else { return true } return token.kind.isBlockEntry || token.kind.isDedent || token.kind.isDocumentEnd || token.kind.isDocumentStart || token.kind.isFlowTerminator || isLowerColumnMappingKey(token, minimumColumn: minimumColumn) || token.kind.isStreamEnd } func isLowerColumnMappingKey( _ token: PureYAML.Parsing.Token?, minimumColumn: Int?, ) -> Bool { guard let token, token.kind.isMappingKey, let minimumColumn else { return false } return token.mark.column < minimumColumn } mutating func expect( _ expected: String, matching: (PureYAML.Parsing.TokenKind) -> Bool, ) throws -> PureYAML.Parsing.Token { guard let token = cursor.current else { let mark = cursor.previous?.endMark ?? .start throw PureYAML.Parsing.ParseError.unexpectedToken( expected: expected, actual: "end of tokens", line: mark.line, column: mark.column, ) } guard matching(token.kind) else { throw PureYAML.Parsing.ParseError.unexpectedToken( expected: expected, actual: token.kind.description, line: token.mark.line, column: token.mark.column, ) } _ = try cursor.advance() return token } func unexpectedToken(expected: String) -> PureYAML.Parsing.ParseError { guard let token = cursor.current else { let mark = cursor.previous?.endMark ?? .start return PureYAML.Parsing.ParseError.unexpectedToken( expected: expected, actual: "end of tokens", line: mark.line, column: mark.column, ) } return PureYAML.Parsing.ParseError.unexpectedToken( expected: expected, actual: token.kind.description, line: token.mark.line, column: token.mark.column, ) } }