Skip to content

Commit 247b298

Browse files
add initial support for Doxygen \param command
rdar://69835334
1 parent 8acb2b0 commit 247b298

File tree

9 files changed

+548
-14
lines changed

9 files changed

+548
-14
lines changed

Sources/Markdown/Base/Markup.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ func makeMarkup(_ data: _MarkupData) -> Markup {
7171
return SymbolLink(data)
7272
case .inlineAttributes:
7373
return InlineAttributes(data)
74+
case .doxygenParam:
75+
return DoxygenParam(data)
7476
}
7577
}
7678

Sources/Markdown/Base/RawMarkup.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ enum RawMarkupData: Equatable {
5151
case tableBody
5252
case tableRow
5353
case tableCell(colspan: UInt, rowspan: UInt)
54+
55+
case doxygenParam(name: String)
5456
}
5557

5658
extension RawMarkupData {
@@ -330,6 +332,10 @@ final class RawMarkup: ManagedBuffer<RawMarkupHeader, RawMarkup> {
330332
static func tableCell(parsedRange: SourceRange?, colspan: UInt, rowspan: UInt, _ children: [RawMarkup]) -> RawMarkup {
331333
return .create(data: .tableCell(colspan: colspan, rowspan: rowspan), parsedRange: parsedRange, children: children)
332334
}
335+
336+
static func doxygenParam(name: String, parsedRange: SourceRange?, _ children: [RawMarkup]) -> RawMarkup {
337+
return .create(data: .doxygenParam(name: name), parsedRange: parsedRange, children: children)
338+
}
333339
}
334340

335341
fileprivate extension Sequence where Element == RawMarkup {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2023 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
/// A parsed Doxygen `\param` command.
14+
///
15+
/// The Doxygen support in Swift-Markdown parses `\param` commands of the form
16+
/// `\param name description`, where `description` extends until the next blank line or the next
17+
/// parsed command. For example, the following input will return two `DoxygenParam` instances:
18+
///
19+
/// ```markdown
20+
/// \param coordinate The coordinate used to center the transformation.
21+
/// \param matrix The transformation matrix that describes the transformation.
22+
/// For more information about transformation matrices, refer to the Transformation
23+
/// documentation.
24+
/// ```
25+
public struct DoxygenParam: BlockContainer {
26+
public var _data: _MarkupData
27+
28+
init(_ raw: RawMarkup) throws {
29+
guard case .doxygenParam = raw.data else {
30+
throw RawMarkup.Error.concreteConversionError(from: raw, to: BlockDirective.self)
31+
}
32+
let absoluteRaw = AbsoluteRawMarkup(markup: raw, metadata: MarkupMetadata(id: .newRoot(), indexInParent: 0))
33+
self.init(_MarkupData(absoluteRaw))
34+
}
35+
36+
init(_ data: _MarkupData) {
37+
self._data = data
38+
}
39+
40+
public func accept<V: MarkupVisitor>(_ visitor: inout V) -> V.Result {
41+
return visitor.visitDoxygenParam(self)
42+
}
43+
}
44+
45+
public extension DoxygenParam {
46+
/// Create a new Doxygen parameter definition.
47+
///
48+
/// - Parameter name: The name of the parameter being described.
49+
/// - Parameter children: Block child elements.
50+
init<Children: Sequence>(name: String, children: Children) where Children.Element == BlockMarkup {
51+
try! self.init(.doxygenParam(name: name, parsedRange: nil, children.map({ $0.raw.markup })))
52+
}
53+
54+
/// Create a new Doxygen parameter definition.
55+
///
56+
/// - Parameter name: The name of the parameter being described.
57+
/// - Parameter children: Block child elements.
58+
init(name: String, children: BlockMarkup...) {
59+
self.init(name: name, children: children)
60+
}
61+
62+
/// The name of the parameter being described.
63+
var name: String {
64+
get {
65+
guard case let .doxygenParam(name) = _data.raw.markup.data else {
66+
fatalError("\(self) markup wrapped unexpected \(_data.raw)")
67+
}
68+
return name
69+
}
70+
set {
71+
_data = _data.replacingSelf(.doxygenParam(
72+
name: newValue,
73+
parsedRange: nil,
74+
_data.raw.markup.copyChildren()
75+
))
76+
}
77+
}
78+
}

Sources/Markdown/Parser/BlockDirectiveParser.swift

Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,31 @@ struct PendingBlockDirective {
223223
}
224224
}
225225

226+
struct PendingDoxygenCommand {
227+
enum CommandKind {
228+
case param(name: Substring)
229+
230+
var debugDescription: String {
231+
switch self {
232+
case .param(name: let name):
233+
return "'param' Argument: '\(name)'"
234+
}
235+
}
236+
}
237+
238+
var atLocation: SourceLocation
239+
240+
var nameLocation: SourceLocation
241+
242+
var kind: CommandKind
243+
244+
var endLocation: SourceLocation
245+
246+
mutating func addLine(_ line: TrimmedLine) {
247+
endLocation = SourceLocation(line: line.lineNumber ?? 0, column: line.untrimmedText.count + 1, source: line.source)
248+
}
249+
}
250+
226251
struct TrimmedLine {
227252
/// A successful result of scanning for a prefix on a ``TrimmedLine``.
228253
struct Lex: Equatable {
@@ -459,8 +484,11 @@ private enum ParseContainer: CustomStringConvertible {
459484
/// A block directive container, which can contain other block directives or runs of lines.
460485
case blockDirective(PendingBlockDirective, [ParseContainer])
461486

462-
init<TrimmedLines: Sequence>(parsingHierarchyFrom trimmedLines: TrimmedLines) where TrimmedLines.Element == TrimmedLine {
463-
self = ParseContainerStack(parsingHierarchyFrom: trimmedLines).top
487+
/// A Doxygen command, which can contain arbitrary markup (but not block directives).
488+
case doxygenCommand(PendingDoxygenCommand, [TrimmedLine])
489+
490+
init<TrimmedLines: Sequence>(parsingHierarchyFrom trimmedLines: TrimmedLines, options: ParseOptions) where TrimmedLines.Element == TrimmedLine {
491+
self = ParseContainerStack(parsingHierarchyFrom: trimmedLines, options: options).top
464492
}
465493

466494
var children: [ParseContainer] {
@@ -471,6 +499,8 @@ private enum ParseContainer: CustomStringConvertible {
471499
return children
472500
case .lineRun:
473501
return []
502+
case .doxygenCommand:
503+
return []
474504
}
475505
}
476506

@@ -545,6 +575,15 @@ private enum ParseContainer: CustomStringConvertible {
545575
indent -= 4
546576
}
547577
print(children: children)
578+
case .doxygenCommand(let pendingDoxygenCommand, let lines):
579+
print("* Doxygen command \(pendingDoxygenCommand.kind.debugDescription)")
580+
queueNewline()
581+
indent += 4
582+
for line in lines {
583+
print(line.text.debugDescription)
584+
queueNewline()
585+
}
586+
indent -= 4
548587
}
549588
}
550589
}
@@ -565,6 +604,9 @@ private enum ParseContainer: CustomStringConvertible {
565604
case .blockDirective(var pendingBlockDirective, let children):
566605
pendingBlockDirective.updateIndentation(for: line)
567606
self = .blockDirective(pendingBlockDirective, children)
607+
case .doxygenCommand:
608+
var newParent: ParseContainer? = nil
609+
parent?.updateIndentation(under: &newParent, for: line)
568610
}
569611
}
570612

@@ -609,6 +651,8 @@ private enum ParseContainer: CustomStringConvertible {
609651
return parent?.indentationAdjustment(under: nil) ?? 0
610652
case .blockDirective(let pendingBlockDirective, _):
611653
return pendingBlockDirective.indentationColumnCount
654+
case .doxygenCommand:
655+
return parent?.indentationAdjustment(under: nil) ?? 0
612656
}
613657
}
614658

@@ -677,6 +721,15 @@ private enum ParseContainer: CustomStringConvertible {
677721
}),
678722
parsedRange: pendingBlockDirective.atLocation..<pendingBlockDirective.endLocation,
679723
children)]
724+
case let .doxygenCommand(pendingDoxygenCommand, lines):
725+
let range = pendingDoxygenCommand.atLocation..<pendingDoxygenCommand.endLocation
726+
ranges.add(range)
727+
let children = ParseContainer.lineRun(lines, isInCodeFence: false)
728+
.convertToRawMarkup(ranges: &ranges, parent: self, options: options)
729+
switch pendingDoxygenCommand.kind {
730+
case .param(let name):
731+
return [.doxygenParam(name: String(name), parsedRange: range, children)]
732+
}
680733
}
681734
}
682735
}
@@ -689,8 +742,11 @@ struct ParseContainerStack {
689742
/// The stack of containers to be incrementally folded into a hierarchy.
690743
private var stack: [ParseContainer]
691744

692-
init<TrimmedLines: Sequence>(parsingHierarchyFrom trimmedLines: TrimmedLines) where TrimmedLines.Element == TrimmedLine {
745+
private let options: ParseOptions
746+
747+
init<TrimmedLines: Sequence>(parsingHierarchyFrom trimmedLines: TrimmedLines, options: ParseOptions) where TrimmedLines.Element == TrimmedLine {
693748
self.stack = [.root([])]
749+
self.options = options
694750
for line in trimmedLines {
695751
accept(line)
696752
}
@@ -708,6 +764,20 @@ struct ParseContainerStack {
708764
} != nil
709765
}
710766

767+
private var canParseDoxygenCommand: Bool {
768+
guard options.contains(.parseMinimalDoxygen) else { return false }
769+
770+
guard !isInBlockDirective else { return false }
771+
772+
if case .blockDirective = top {
773+
return false
774+
} else if case .lineRun(_, isInCodeFence: let codeFence) = top {
775+
return !codeFence
776+
} else {
777+
return true
778+
}
779+
}
780+
711781
private func isCodeFenceOrIndentedCodeBlock(on line: TrimmedLine) -> Bool {
712782
// Check if this line is indented 4 or more spaces relative to the current
713783
// indentation adjustment.
@@ -760,23 +830,85 @@ struct ParseContainerStack {
760830
return pendingBlockDirective
761831
}
762832

833+
private func parseDoxygenCommandOpening(on line: TrimmedLine) -> (pendingCommand: PendingDoxygenCommand, remainder: TrimmedLine)? {
834+
guard canParseDoxygenCommand else { return nil }
835+
guard !isCodeFenceOrIndentedCodeBlock(on: line) else { return nil }
836+
837+
var remainder = line
838+
guard let at = remainder.lex(until: { ch in
839+
switch ch {
840+
case "@", "\\":
841+
return .continue
842+
default:
843+
return .stop
844+
}
845+
}) else { return nil }
846+
guard let name = remainder.lex(until: { ch in
847+
if ch.isWhitespace {
848+
return .stop
849+
} else {
850+
return .continue
851+
}
852+
}) else { return nil }
853+
remainder.lexWhitespace()
854+
855+
switch name.text.lowercased() {
856+
case "param":
857+
guard let paramName = remainder.lex(until: { ch in
858+
if ch.isWhitespace {
859+
return .stop
860+
} else {
861+
return .continue
862+
}
863+
}) else { return nil }
864+
remainder.lexWhitespace()
865+
var pendingCommand = PendingDoxygenCommand(
866+
atLocation: at.range!.lowerBound,
867+
nameLocation: name.range!.lowerBound,
868+
kind: .param(name: paramName.text),
869+
endLocation: name.range!.upperBound)
870+
pendingCommand.addLine(remainder)
871+
return (pendingCommand, remainder)
872+
default:
873+
return nil
874+
}
875+
}
876+
763877
/// Accept a trimmed line, opening new block directives as indicated by the source,
764878
/// closing a block directive if applicable, or adding the line to a run of lines to be parsed
765879
/// as Markdown later.
766880
private mutating func accept(_ line: TrimmedLine) {
767-
if line.isEmptyOrAllWhitespace,
768-
case let .blockDirective(pendingBlockDirective, _) = top {
769-
switch pendingBlockDirective.parseState {
770-
case .argumentsStart,
771-
.contentsStart,
772-
.done:
773-
closeTop()
881+
if line.isEmptyOrAllWhitespace {
882+
switch top {
883+
case let .blockDirective(pendingBlockDirective, _):
884+
switch pendingBlockDirective.parseState {
885+
case .argumentsStart,
886+
.contentsStart,
887+
.done:
888+
closeTop()
774889

890+
default:
891+
break
892+
}
893+
case .doxygenCommand:
894+
closeTop()
775895
default:
776896
break
777897
}
778898
}
779899

900+
// If we can parse a Doxygen command from this line, start one and skip everything else.
901+
if let result = parseDoxygenCommandOpening(on: line) {
902+
switch top {
903+
case .root:
904+
break
905+
default:
906+
closeTop()
907+
}
908+
push(.doxygenCommand(result.pendingCommand, [result.remainder]))
909+
return
910+
}
911+
780912
// If we're inside a block directive, check to see whether we need to update its
781913
// indentation calculation to account for its content.
782914
updateIndentation(for: line)
@@ -822,7 +954,7 @@ struct ParseContainerStack {
822954
switch top {
823955
case .root:
824956
push(.blockDirective(newBlockDirective, []))
825-
case .lineRun:
957+
case .lineRun, .doxygenCommand:
826958
closeTop()
827959
push(.blockDirective(newBlockDirective, []))
828960
case .blockDirective(let previousBlockDirective, _):
@@ -848,11 +980,16 @@ struct ParseContainerStack {
848980
} else {
849981
switch top {
850982
case .root:
851-
push(.lineRun([line], isInCodeFence: false))
983+
push(.lineRun([line], isInCodeFence: line.isProbablyCodeFence))
852984
case .lineRun(var lines, let isInCodeFence):
853985
pop()
854986
lines.append(line)
855987
push(.lineRun(lines, isInCodeFence: isInCodeFence != line.isProbablyCodeFence))
988+
case .doxygenCommand(var pendingDoxygenCommand, var lines):
989+
pop()
990+
lines.append(line)
991+
pendingDoxygenCommand.addLine(line)
992+
push(.doxygenCommand(pendingDoxygenCommand, lines))
856993
case .blockDirective(var pendingBlockDirective, let children):
857994
// A pending block directive can accept this line if it is in the middle of
858995
// parsing arguments text (to allow indentation to align arguments) or
@@ -923,6 +1060,8 @@ struct ParseContainerStack {
9231060
push(.blockDirective(pendingBlockDirective, children))
9241061
case .lineRun:
9251062
fatalError("Line runs cannot have children")
1063+
case .doxygenCommand:
1064+
fatalError("Doxygen commands cannot have children")
9261065
}
9271066
}
9281067

@@ -985,7 +1124,7 @@ struct BlockDirectiveParser {
9851124
// Phase 1: Categorize the lines into a hierarchy of block containers by parsing the prefix
9861125
// of the line, opening and closing block directives appropriately, and folding elements
9871126
// into a root document.
988-
let rootContainer = ParseContainer(parsingHierarchyFrom: trimmedLines)
1127+
let rootContainer = ParseContainer(parsingHierarchyFrom: trimmedLines, options: options)
9891128

9901129
// Phase 2: Convert the hierarchy of block containers into a real ``Document``.
9911130
// This is where the CommonMark parser is called upon to parse runs of lines of content,

Sources/Markdown/Parser/ParseOptions.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ public struct ParseOptions: OptionSet {
2424

2525
/// Disable converting straight quotes to curly, --- to em dashes, -- to en dashes during parsing
2626
public static let disableSmartOpts = ParseOptions(rawValue: 1 << 2)
27+
28+
/// Parse a limited set of Doxygen commands. Requires ``parseBlockDirectives``.
29+
public static let parseMinimalDoxygen = ParseOptions(rawValue: 1 << 3)
2730
}
2831

0 commit comments

Comments
 (0)