diff --git a/Sources/Markdown/Base/Markup.swift b/Sources/Markdown/Base/Markup.swift index 23c72e78..e26ff1ef 100644 --- a/Sources/Markdown/Base/Markup.swift +++ b/Sources/Markdown/Base/Markup.swift @@ -71,6 +71,10 @@ func makeMarkup(_ data: _MarkupData) -> Markup { return SymbolLink(data) case .inlineAttributes: return InlineAttributes(data) + case .doxygenDiscussion: + return DoxygenDiscussion(data) + case .doxygenNote: + return DoxygenNote(data) case .doxygenParam: return DoxygenParameter(data) case .doxygenReturns: diff --git a/Sources/Markdown/Base/RawMarkup.swift b/Sources/Markdown/Base/RawMarkup.swift index b001a318..f928b8fd 100644 --- a/Sources/Markdown/Base/RawMarkup.swift +++ b/Sources/Markdown/Base/RawMarkup.swift @@ -52,6 +52,8 @@ enum RawMarkupData: Equatable { case tableRow case tableCell(colspan: UInt, rowspan: UInt) + case doxygenDiscussion + case doxygenNote case doxygenParam(name: String) case doxygenReturns } @@ -334,6 +336,14 @@ final class RawMarkup: ManagedBuffer { return .create(data: .tableCell(colspan: colspan, rowspan: rowspan), parsedRange: parsedRange, children: children) } + static func doxygenDiscussion(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { + return .create(data: .doxygenDiscussion, parsedRange: parsedRange, children: children) + } + + static func doxygenNote(parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { + return .create(data: .doxygenNote, parsedRange: parsedRange, children: children) + } + static func doxygenParam(name: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup { return .create(data: .doxygenParam(name: name), parsedRange: parsedRange, children: children) } diff --git a/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenDiscussion.swift b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenDiscussion.swift new file mode 100644 index 00000000..6f72fdc4 --- /dev/null +++ b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenDiscussion.swift @@ -0,0 +1,56 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation + +/// A parsed Doxygen `\discussion` command. +/// +/// The Doxygen support in Swift-Markdown parses `\discussion` commands of the form +/// `\discussion description`, where `description` continues until the next blank +/// line or parsed command. +/// +/// ```markdown +/// \discussion This object can give other objects in your program magical powers. +/// ``` +public struct DoxygenDiscussion: BlockContainer { + public var _data: _MarkupData + + init(_ raw: RawMarkup) throws { + guard case .doxygenDiscussion = raw.data else { + throw RawMarkup.Error.concreteConversionError(from: raw, to: DoxygenDiscussion.self) + } + let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0)) + self.init(_MarkupData(absoluteRaw)) + } + + init(_ data: _MarkupData) { + self._data = data + } + + public func accept(_ visitor: inout V) -> V.Result { + return visitor.visitDoxygenDiscussion(self) + } +} + +public extension DoxygenDiscussion { + /// Create a new Doxygen discussion definition. + /// + /// - Parameter children: Block child elements. + init(children: Children) where Children.Element == BlockMarkup { + try! self.init(.doxygenDiscussion(parsedRange: nil, children.map({ $0.raw.markup }))) + } + + /// Create a new Doxygen discussion definition. + /// + /// - Parameter children: Block child elements. + init(children: BlockMarkup...) { + self.init(children: children) + } +} diff --git a/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenNote.swift b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenNote.swift new file mode 100644 index 00000000..df9e5eac --- /dev/null +++ b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenNote.swift @@ -0,0 +1,56 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation + +/// A parsed Doxygen `\note` command. +/// +/// The Doxygen support in Swift-Markdown parses `\note` commands of the form +/// `\note description`, where `description` continues until the next blank +/// line or parsed command. +/// +/// ```markdown +/// \note This method is only meant to be called an odd number of times. +/// ``` +public struct DoxygenNote: BlockContainer { + public var _data: _MarkupData + + init(_ raw: RawMarkup) throws { + guard case .doxygenNote = raw.data else { + throw RawMarkup.Error.concreteConversionError(from: raw, to: DoxygenNote.self) + } + let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0)) + self.init(_MarkupData(absoluteRaw)) + } + + init(_ data: _MarkupData) { + self._data = data + } + + public func accept(_ visitor: inout V) -> V.Result { + return visitor.visitDoxygenNote(self) + } +} + +public extension DoxygenNote { + /// Create a new Doxygen note definition. + /// + /// - Parameter children: Block child elements. + init(children: Children) where Children.Element == BlockMarkup { + try! self.init(.doxygenNote(parsedRange: nil, children.map({ $0.raw.markup }))) + } + + /// Create a new Doxygen note definition. + /// + /// - Parameter children: Block child elements. + init(children: BlockMarkup...) { + self.init(children: children) + } +} diff --git a/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift index 5a6cbd57..2a014b3f 100644 --- a/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift +++ b/Sources/Markdown/Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift @@ -40,17 +40,15 @@ public struct DoxygenReturns: BlockContainer { } public extension DoxygenReturns { - /// Create a new Doxygen parameter definition. + /// Create a new Doxygen returns definition. /// - /// - Parameter name: The name of the parameter being described. /// - Parameter children: Block child elements. init(children: Children) where Children.Element == BlockMarkup { try! self.init(.doxygenReturns(parsedRange: nil, children.map({ $0.raw.markup }))) } - /// Create a new Doxygen parameter definition. + /// Create a new Doxygen returns definition. /// - /// - Parameter name: The name of the parameter being described. /// - Parameter children: Block child elements. init(children: BlockMarkup...) { self.init(children: children) diff --git a/Sources/Markdown/CMakeLists.txt b/Sources/Markdown/CMakeLists.txt index c23aa1cc..53e1054f 100644 --- a/Sources/Markdown/CMakeLists.txt +++ b/Sources/Markdown/CMakeLists.txt @@ -17,6 +17,8 @@ add_library(Markdown Base/MarkupData.swift Base/PlainTextConvertibleMarkup.swift Base/RawMarkup.swift + "Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenDiscussion.swift" + "Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenNote.swift" "Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenParameter.swift" "Block Nodes/Block Container Blocks/Doxygen Commands/DoxygenReturns.swift" "Block Nodes/Block Container Blocks/BlockDirective.swift" diff --git a/Sources/Markdown/Parser/BlockDirectiveParser.swift b/Sources/Markdown/Parser/BlockDirectiveParser.swift index 40dd3ae5..bae40ac0 100644 --- a/Sources/Markdown/Parser/BlockDirectiveParser.swift +++ b/Sources/Markdown/Parser/BlockDirectiveParser.swift @@ -224,11 +224,17 @@ struct PendingBlockDirective { struct PendingDoxygenCommand { enum CommandKind { + case discussion + case note case param(name: Substring) case returns var debugDescription: String { switch self { + case .discussion: + return "'discussion'" + case .note: + return "'note'" case .param(name: let name): return "'param' Argument: '\(name)'" case .returns: @@ -745,6 +751,10 @@ private enum ParseContainer: CustomStringConvertible { let children = ParseContainer.lineRun(lines, isInCodeFence: false) .convertToRawMarkup(ranges: &ranges, parent: self, options: options) switch pendingDoxygenCommand.kind { + case .discussion: + return [.doxygenDiscussion(parsedRange: range, children)] + case .note: + return [.doxygenNote(parsedRange: range, children)] case .param(let name): return [.doxygenParam(name: String(name), parsedRange: range, children)] case .returns: @@ -873,7 +883,12 @@ struct ParseContainerStack { }) else { return nil } remainder.lexWhitespace() + let kind: PendingDoxygenCommand.CommandKind switch name.text.lowercased() { + case "discussion": + kind = .discussion + case "note": + kind = .note case "param": guard let paramName = remainder.lex(until: { ch in if ch.isWhitespace { @@ -883,26 +898,21 @@ struct ParseContainerStack { } }) else { return nil } remainder.lexWhitespace() - var pendingCommand = PendingDoxygenCommand( - atLocation: at.range!.lowerBound, - atSignIndentation: indent?.text.count ?? 0, - nameLocation: name.range!.lowerBound, - kind: .param(name: paramName.text), - endLocation: name.range!.upperBound) - pendingCommand.addLine(remainder) - return (pendingCommand, remainder) + kind = .param(name: paramName.text) case "return", "returns", "result": - var pendingCommand = PendingDoxygenCommand( - atLocation: at.range!.lowerBound, - atSignIndentation: indent?.text.count ?? 0, - nameLocation: name.range!.lowerBound, - kind: .returns, - endLocation: name.range!.upperBound) - pendingCommand.addLine(remainder) - return (pendingCommand, remainder) + kind = .returns default: return nil } + + var pendingCommand = PendingDoxygenCommand( + atLocation: at.range!.lowerBound, + atSignIndentation: indent?.text.count ?? 0, + nameLocation: name.range!.lowerBound, + kind: kind, + endLocation: name.range!.upperBound) + pendingCommand.addLine(remainder) + return (pendingCommand, remainder) } /// Accept a trimmed line, opening new block directives as indicated by the source, diff --git a/Sources/Markdown/Visitor/MarkupVisitor.swift b/Sources/Markdown/Visitor/MarkupVisitor.swift index 48ec9622..f979e42d 100644 --- a/Sources/Markdown/Visitor/MarkupVisitor.swift +++ b/Sources/Markdown/Visitor/MarkupVisitor.swift @@ -275,6 +275,22 @@ public protocol MarkupVisitor { */ mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result + /** + Visit a `DoxygenDiscussion` element and return the result. + + - parameter doxygenDiscussion: A `DoxygenDiscussion` element. + - returns: The result of the visit. + */ + mutating func visitDoxygenDiscussion(_ doxygenDiscussion: DoxygenDiscussion) -> Result + + /** + Visit a `DoxygenNote` element and return the result. + + - parameter doxygenNote: A `DoxygenNote` element. + - returns: The result of the visit. + */ + mutating func visitDoxygenNote(_ doxygenNote: DoxygenNote) -> Result + /** Visit a `DoxygenParam` element and return the result. @@ -389,6 +405,12 @@ extension MarkupVisitor { public mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> Result { return defaultVisit(attributes) } + public mutating func visitDoxygenDiscussion(_ doxygenDiscussion: DoxygenDiscussion) -> Result { + return defaultVisit(doxygenDiscussion) + } + public mutating func visitDoxygenNote(_ doxygenNote: DoxygenNote) -> Result { + return defaultVisit(doxygenNote) + } public mutating func visitDoxygenParameter(_ doxygenParam: DoxygenParameter) -> Result { return defaultVisit(doxygenParam) } diff --git a/Tests/MarkdownTests/Parsing/DoxygenCommandParserTests.swift b/Tests/MarkdownTests/Parsing/DoxygenCommandParserTests.swift index 7ce4f534..80ca5bbb 100644 --- a/Tests/MarkdownTests/Parsing/DoxygenCommandParserTests.swift +++ b/Tests/MarkdownTests/Parsing/DoxygenCommandParserTests.swift @@ -14,6 +14,42 @@ import XCTest class DoxygenCommandParserTests: XCTestCase { let parseOptions: ParseOptions = [.parseMinimalDoxygen, .parseBlockDirectives] + func testParseDiscussion() { + func assertValidParse(source: String) { + let document = Document(parsing: source, options: parseOptions) + XCTAssert(document.child(at: 0) is DoxygenDiscussion) + + let expectedDump = """ + Document + └─ DoxygenDiscussion + └─ Paragraph + └─ Text "The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + assertValidParse(source: "@discussion The thing.") + assertValidParse(source: #"\discussion The thing."#) + } + + func testParseNote() { + func assertValidParse(source: String) { + let document = Document(parsing: source, options: parseOptions) + XCTAssert(document.child(at: 0) is DoxygenNote) + + let expectedDump = """ + Document + └─ DoxygenNote + └─ Paragraph + └─ Text "The thing." + """ + XCTAssertEqual(document.debugDescription(), expectedDump) + } + + assertValidParse(source: "@note The thing.") + assertValidParse(source: #"\note The thing."#) + } + func testParseParam() throws { let source = """ @param thing The thing.