extension PureYAML.Parsing.Scanner { func scanDirectiveOrDocumentMarker(_ state: inout State) throws -> Bool { if state.flowDepth > 0, state.reader.mark.column == 1, isDocumentMarkerHere(state) { // A `---` or `...` document marker at column 0 is recognized regardless // of flow nesting (scanner.c:908-924 keys only on `mark.column == 0`), // but a flow collection may not span a document boundary. libyaml reports // the unterminated flow at the parser layer; reject it here. throw PureYAML.Parsing.ParseError.unexpectedToken( expected: "flow collection close before document marker", actual: "document marker", line: state.reader.mark.line, column: state.reader.mark.column, ) } guard state.indentation.last == 0, state.flowDepth == 0 else { return false } if state.reader.peek() == "%" { // A directive section is only valid at the start of the stream or right // after a `...` document-end suffix. Once a document has started (via // `---`), a `%YAML`/`%TAG` line that itself introduces a new explicit // document (its next line is a `---`/`...` marker) begins a second // directive section that the YAML grammar's l-document-suffix rule // forbids without an intervening `...`. This is the MUS6/01 case // (`%YAML`/`---`/`%YAML`/`---`), distinct from a `%`-prefixed line that // is part of the current document body. if state.hasDocumentStartMarker, isDirectiveLineHere(state), directiveLineIntroducesDocument(state) { throw PureYAML.Parsing.ParseError.directiveWithoutDocument(line: state.reader.mark.line) } // `%` introduces a directive only in directive position: before any // document content and before the `---` that starts a document. After // content, after `---`, or anywhere inside a node, `%` is plain text, // so defer to ordinary token scanning by returning false. This mirrors // the C scanner, which only enters `yaml_parser_scan_directive` from // the start-of-line directive context, never inside a node or scalar. guard !state.hasDocumentContent, !state.hasDocumentStartMarker else { return false } try scanDirective(&state) return true } if state.reader.peek() == "#" { return false } if startsWith("---", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) { scanDocumentMarker(.documentStart, state: &state) resetForDocumentStart(&state) return true } if startsWith("...", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) { // A `...` end marker terminates the directive section it follows just // as a `---` would, except that no document was started, so a pending // directive here was never attached to a document and is rejected. if state.hasPendingDirective { throw PureYAML.Parsing.ParseError.directiveWithoutDocument(line: state.reader.mark.line) } scanDocumentMarker(.documentEnd, state: &state) resetForDocumentEnd(&state) return true } return false } /// True when, after the directive line under the reader, the following line at /// column 0 is a `---`/`...` document marker, i.e. the directive line opens a new /// explicit document. Used to tell a genuine second directive section (MUS6/01) /// apart from a `%`-prefixed line that belongs to the current document body. func directiveLineIntroducesDocument(_ state: State) -> Bool { var probe = state.reader while let character = probe.peek(), !isLineBreak(character) { probe.advance() } guard isLineBreak(probe.peek()) else { return false } probe.advance() guard probe.mark.column == 1 else { return false } return (startsWith3(probe, "-") || startsWith3(probe, ".")) && isIndicatorBoundary(probe.peek(offset: 3)) } private func startsWith3(_ reader: PureYAML.Parsing.Reader, _ character: Character) -> Bool { reader.peek(offset: 0) == character && reader.peek(offset: 1) == character && reader.peek(offset: 2) == character } /// True when the reader sits on a recognized directive line (`%YAML` or `%TAG`). /// Reserved/unknown `%...` directives are not treated as a hard error here; only /// the two grammar-significant directives gate the document-suffix rule. func isDirectiveLineHere(_ state: State) -> Bool { startsWith("%YAML", state.reader) || startsWith("%TAG", state.reader) } /// True when the reader sits on a `---` or `...` document marker, i.e. three /// identical marker characters followed by a blank, line break, or end of input. func isDocumentMarkerHere(_ state: State) -> Bool { (startsWith("---", state.reader) || startsWith("...", state.reader)) && isIndicatorBoundary(state.reader.peek(offset: 3)) } func scanDocumentMarker( _ kind: PureYAML.Parsing.TokenKind, state: inout State, ) { let start = state.reader.mark state.reader.advance() state.reader.advance() state.reader.advance() state.append(kind, mark: start, endMark: state.reader.mark) // A `---` start marker may be followed by the document's root node on the // same line (`--- foo`, `--- |`, `--- [1, 2]`), matching libyaml, which scans // the marker as a token and keeps scanning the line. Skip the inline spaces // and, when real content follows, hand it to the ordinary token scanner; a // trailing comment or empty remainder is skipped as before. A `...` end // marker carries no node, so its line (only an optional comment) is skipped. guard case .documentStart = kind else { skipLine(&state) return } skipSeparation(&state) guard let character = state.reader.peek(), !isLineBreak(character), character != "#" else { skipLine(&state) return } state.isAtLineStart = false } func resetForDocumentStart(_ state: inout State) { state.hasDocumentContent = false state.hasDocumentStartMarker = true state.isDocumentClosed = false state.sawYAMLDirective = false state.hasPendingDirective = false resetBlockIndent(&state) } func resetForDocumentEnd(_ state: inout State) { state.hasDocumentContent = false state.hasDocumentStartMarker = false state.isDocumentClosed = true state.sawYAMLDirective = false state.hasPendingDirective = false resetBlockIndent(&state) } /// Mirror libyaml unrolling the block indent to -1 at a document boundary so a /// new document's root scalar starts with no enclosing block collection. func resetBlockIndent(_ state: inout State) { state.blockIndent = -1 state.blockIndents.removeAll(keepingCapacity: true) } func scanDirective(_ state: inout State) throws { let start = state.reader.mark let line = state.reader.consume { character in !isLineBreak(character) } let fields = directiveFields(line) guard let name = fields.first else { return } switch name { case "%YAML": try scanYAMLDirective(fields: fields, start: start, state: &state) case "%TAG": scanTagDirective(fields: fields, state: &state) default: // Reserved or unknown directives are ignored with a warning per the // spec; PureYAML accepts and discards them rather than erroring. break } state.hasPendingDirective = true } /// Splits a directive line into its `%name` and parameter fields, dropping /// any trailing comment. Fields are separated by spaces or tabs, matching the /// C scanner's blank handling around directive values. private func directiveFields(_ line: String) -> [String] { var fields: [String] = [] var current = "" for character in line { // A `#` at a field boundary (i.e. preceded by whitespace or the line // start) begins a trailing comment that ends the directive's fields. if character == "#", current.isEmpty { break } if character == " " || character == "\t" { if !current.isEmpty { fields.append(current) current = "" } continue } current.append(character) } if !current.isEmpty { fields.append(current) } return fields } private func scanYAMLDirective( fields: [String], start: PureYAML.Parsing.Mark, state: inout State, ) throws { // A `%YAML` directive carries exactly one version field of the form // `major.minor`. A duplicate within one document, extra trailing fields, // or an unsupported major version is an error; a higher 1.x minor is // accepted (treated as 1.2 with a warning) so future minors keep parsing. guard !state.sawYAMLDirective else { throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line) } guard fields.count == 2, isSupportedYAMLVersion(fields[1]) else { throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line) } state.sawYAMLDirective = true } private func isSupportedYAMLVersion(_ version: String) -> Bool { let components = version.split(separator: ".", omittingEmptySubsequences: false) guard components.count == 2 else { return false } let major = components[0] let minor = components[1] guard !major.isEmpty, !minor.isEmpty else { return false } guard major.allSatisfy(\.isNumber), minor.allSatisfy(\.isNumber) else { return false } return major == "1" } private func scanTagDirective( fields: [String], state: inout State, ) { guard fields.count >= 3 else { return } state.tagHandles[fields[1]] = fields[2] } }