extension PureYAML.Parsing.Scanner { func scanDirectiveOrDocumentMarker(_ state: inout State) throws -> Bool { guard state.indentation.last == 0 else { return false } if state.reader.peek() == "%" { try scanDirective(&state) return true } if state.reader.peek() == "#" { return false } if startsWith("---", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) { try scanDocumentMarker(.documentStart, state: &state) resetForDocumentStart(&state) return true } if startsWith("...", state.reader), isIndicatorBoundary(state.reader.peek(offset: 3)) { guard !state.isDocumentClosed else { throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: state.reader.mark.line) } // A `...` ends a document, so a directive still awaiting its `---` never gets one. try enforceDirectiveHasDocument(state) try scanDocumentMarker(.documentEnd, state: &state) resetForDocumentEnd(&state) return true } // Content after a document-end marker (`...`) begins a new bare document, so reopen the // stream rather than rejecting it. if state.isDocumentClosed { state.isDocumentClosed = false } return false } func scanDocumentMarker( _ kind: PureYAML.Parsing.TokenKind, state: inout State, ) throws { let start = state.reader.mark state.reader.advance() state.reader.advance() state.reader.advance() state.append(kind, mark: start, endMark: state.reader.mark) while state.reader.peek() == " " { state.reader.advance() } if let next = state.reader.peek(), !isLineBreak(next), next != "#" { // The marker's own line carries real content. A document start may carry the root node // here (e.g. `--- >`, `--- foo`, `--- {a: 1}`), so leave it for the main loop to scan. if case .documentStart = kind { state.isAtLineStart = false return } // A document end (`...`) takes no same-line node; only a comment or blank may follow it. throw PureYAML.Parsing.ParseError.contentAfterDocumentEnd(line: state.reader.mark.line) } skipLine(&state) } func resetForDocumentStart(_ state: inout State) { state.hasDocumentContent = false state.hasDocumentStartMarker = true state.isDocumentClosed = false state.hasYAMLDirective = false state.pendingDirectiveLine = nil } /// A directive must be followed by an explicit `---` document start. Called at a document end /// and at stream end, where a still-pending directive means none ever came. func enforceDirectiveHasDocument(_ state: State) throws { if let line = state.pendingDirectiveLine { throw PureYAML.Parsing.ParseError.directiveWithoutDocument(line: line) } } func resetForDocumentEnd(_ state: inout State) { state.hasDocumentContent = false state.hasDocumentStartMarker = false state.isDocumentClosed = true state.hasYAMLDirective = false } func scanDirective(_ state: inout State) throws { let start = state.reader.mark let line = state.reader.consume { character in !isLineBreak(character) } let parts = line.split(separator: " ").map(String.init) guard let name = parts.first else { return } // A directive after a document-end marker (`...`) begins the next document's directive // section, so it reopens the stream for a new document rather than being rejected. if state.isDocumentClosed { state.isDocumentClosed = false } guard !state.hasDocumentContent, !state.hasDocumentStartMarker else { throw PureYAML.Parsing.ParseError.unsupportedDirective(name: name, line: start.line) } // Every directive must be followed by an explicit `---`; record that one is now awaiting it. state.pendingDirectiveLine = start.line switch name { case "%YAML": guard parts.indices.contains(1), parts[1] == "1.1" || parts[1] == "1.2" else { throw PureYAML.Parsing.ParseError.incompatibleYAMLDirective(line: start.line) } // `%YAML` takes exactly one parameter, the version; only a comment may follow it. guard parts.count == 2 || parts[2].hasPrefix("#") else { throw PureYAML.Parsing.ParseError.malformedDirective(name: name, line: start.line) } // At most one `%YAML` directive is allowed per document. guard !state.hasYAMLDirective else { throw PureYAML.Parsing.ParseError.duplicateDirective(name: name, line: start.line) } state.hasYAMLDirective = true case "%TAG": guard parts.count >= 3 else { return } state.tagHandles[parts[1]] = parts[2] default: // A YAML processor ignores a directive it does not recognize (the spec calls for a // warning and otherwise no effect), rather than rejecting the document. The line has // already been consumed, so there is nothing more to do. return } } }