1
+ //===----------------------------------------------------------------------===//
2
+ //
3
+ // This source file is part of the Swift.org open source project
4
+ //
5
+ // Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6
+ // Licensed under Apache License v2.0 with Runtime Library Exception
7
+ //
8
+ // See https://swift.org/LICENSE.txt for license information
9
+ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10
+ //
11
+ //===----------------------------------------------------------------------===//
12
+
1
13
import Foundation
2
14
import Markdown
3
15
16
+ /// Extracts parameter documentation from a markdown string.
17
+ ///
18
+ /// The parameter extraction implementation is almost ported from the implementation in the Swift compiler codebase.
19
+ ///
20
+ /// The problem with doing that in the Swift compiler codebase is that once you parse a the comment as markdown into
21
+ /// a `Document` you cannot easily convert it back into markdown (we'd need to write our own markdown formatter).
22
+ /// Besides, `cmark` doesn't handle Doxygen commands.
23
+ ///
24
+ /// We considered using `swift-docc` but we faced some problems with it:
25
+ ///
26
+ /// 1. We would need to refactor existing use of `swift-docc` in SourceKit-LSP to reuse some of that logic here besides
27
+ /// providing the required arguments.
28
+ /// 2. The result returned by DocC can't be directly converted to markdown, we'd need to provide our own DocC markdown renderer.
29
+ ///
30
+ /// Implementing this using `swift-markdown` allows us to easily parse the comment, process it, convert it back to markdown.
31
+ /// It also provides minimal parsing for Doxygen commands (we're only interested in `\param`) allowing us to use the same
32
+ /// implementation for Clang-based declarations.
33
+ ///
34
+ /// Although this approach involves code duplication, it's simple enough for the initial implementation. We should consider
35
+ /// `swift-docc` in the future.
4
36
private struct ParametersDocumentationExtractor {
5
- private var parameters = [ String: String] ( )
37
+ struct Parameter {
38
+ let name : String
39
+ let documentation : String
40
+ }
6
41
7
42
/// Extracts parameter documentation from a markdown string.
8
43
///
9
44
/// - Returns: A tuple containing the extracted parameters and the remaining markdown.
10
- mutating func extract( from markdown: String ) -> ( parameters: [ String : String ] , remaining: String ) {
45
+ func extract( from markdown: String ) -> ( parameters: [ String : String ] , remaining: String ) {
11
46
let document = Document ( parsing: markdown, options: [ . parseBlockDirectives, . parseMinimalDoxygen] )
12
47
13
- var remainingBlocks = [ any BlockMarkup ] ( )
48
+ var parameters : [ String : String ] = [ : ]
49
+ var remainingBlocks : [ any BlockMarkup ] = [ ]
14
50
15
51
for block in document. blockChildren {
16
52
switch block {
17
53
case let unorderedList as UnorderedList :
18
- if let newUnorderedList = extract ( from: unorderedList) {
54
+ let ( newUnorderedList, params) = extract ( from: unorderedList)
55
+ if let newUnorderedList {
19
56
remainingBlocks. append ( newUnorderedList)
20
57
}
58
+
59
+ for param in params {
60
+ parameters [ param. name] = param. documentation
61
+ }
62
+
21
63
case let doxygenParameter as DoxygenParameter :
22
- extract ( from: doxygenParameter)
64
+ let param = extract ( from: doxygenParameter)
65
+ parameters [ param. name] = param. documentation
66
+
23
67
default :
24
68
remainingBlocks. append ( block)
25
69
}
@@ -31,29 +75,35 @@ private struct ParametersDocumentationExtractor {
31
75
}
32
76
33
77
/// Extracts parameter documentation from a Doxygen parameter command.
34
- private mutating func extract( from doxygenParameter: DoxygenParameter ) {
35
- parameters [ doxygenParameter. name] = Document ( doxygenParameter. blockChildren) . format ( )
78
+ private func extract( from doxygenParameter: DoxygenParameter ) -> Parameter {
79
+ return Parameter (
80
+ name: doxygenParameter. name,
81
+ documentation: Document ( doxygenParameter. blockChildren) . format ( ) ,
82
+ )
36
83
}
37
84
38
85
/// Extracts parameter documentation from an unordered list.
39
86
///
40
87
/// - Returns: A new UnorderedList with the items that were not added to the parameters if any.
41
- private mutating func extract( from unorderedList: UnorderedList ) -> UnorderedList ? {
42
- var newItems = [ ListItem] ( )
88
+ private func extract( from unorderedList: UnorderedList ) -> ( remaining: UnorderedList ? , parameters: [ Parameter ] ) {
89
+ var parameters : [ Parameter ] = [ ]
90
+ var newItems : [ ListItem ] = [ ]
43
91
44
92
for item in unorderedList. listItems {
45
- if extractSingle ( from: item) || extractOutline ( from: item) {
46
- continue
93
+ if let param = extractSingle ( from: item) {
94
+ parameters. append ( param)
95
+ } else if let params = extractOutline ( from: item) {
96
+ parameters. append ( contentsOf: params)
97
+ } else {
98
+ newItems. append ( item)
47
99
}
48
-
49
- newItems. append ( item)
50
100
}
51
101
52
102
if newItems. isEmpty {
53
- return nil
103
+ return ( remaining : nil , parameters : parameters )
54
104
}
55
105
56
- return UnorderedList ( newItems)
106
+ return ( remaining : UnorderedList ( newItems) , parameters : parameters )
57
107
}
58
108
59
109
/// Parameter documentation from a `Parameters:` outline.
@@ -65,33 +115,24 @@ private struct ParametersDocumentationExtractor {
65
115
/// ```
66
116
///
67
117
/// - Returns: True if the list item has parameter outline documentation, false otherwise.
68
- private mutating func extractOutline( from listItem: ListItem ) -> Bool {
118
+ private func extractOutline( from listItem: ListItem ) -> [ Parameter ] ? {
69
119
guard let firstChild = listItem. child ( at: 0 ) as? Paragraph ,
70
120
let headingText = firstChild. child ( at: 0 ) as? Text
71
121
else {
72
- return false
122
+ return nil
73
123
}
74
124
75
- let parametersPrefix = " parameters: "
76
- let headingContent = headingText. string. trimmingCharacters ( in: . whitespaces)
77
-
78
- guard headingContent. lowercased ( ) . hasPrefix ( parametersPrefix) else {
79
- return false
125
+ guard headingText. string. trimmingCharacters ( in: . whitespaces) . lowercased ( ) . hasPrefix ( " parameters: " ) else {
126
+ return nil
80
127
}
81
128
82
- for child in listItem. children {
129
+ return listItem. children. flatMap { child in
83
130
guard let nestedList = child as? UnorderedList else {
84
- continue
131
+ return [ ] as [ Parameter ]
85
132
}
86
133
87
- for nestedItem in nestedList. listItems {
88
- if let parameter = extractOutlineItem ( from: nestedItem) {
89
- parameters [ parameter. name] = parameter. documentation
90
- }
91
- }
134
+ return nestedList. listItems. compactMap ( extractOutlineItem)
92
135
}
93
-
94
- return true
95
136
}
96
137
97
138
/// Extracts parameter documentation from a single parameter.
@@ -102,40 +143,34 @@ private struct ParametersDocumentationExtractor {
102
143
/// ```
103
144
///
104
145
/// - Returns: True if the list item has single parameter documentation, false otherwise.
105
- private mutating func extractSingle( from listItem: ListItem ) -> Bool {
146
+ private func extractSingle( from listItem: ListItem ) -> Parameter ? {
106
147
guard let paragraph = listItem. child ( at: 0 ) as? Paragraph ,
107
148
let paragraphText = paragraph. child ( at: 0 ) as? Text
108
149
else {
109
- return false
150
+ return nil
110
151
}
111
152
112
153
let parameterPrefix = " parameter "
113
154
let paragraphContent = paragraphText. string
114
155
115
156
guard paragraphContent. count >= parameterPrefix. count else {
116
- return false
157
+ return nil
117
158
}
118
159
119
160
let prefixEnd = paragraphContent. index ( paragraphContent. startIndex, offsetBy: parameterPrefix. count)
120
161
let potentialMatch = paragraphContent [ ..< prefixEnd] . lowercased ( )
121
162
122
163
guard potentialMatch == parameterPrefix else {
123
- return false
164
+ return nil
124
165
}
125
166
126
167
let remainingContent = String ( paragraphContent [ prefixEnd... ] ) . trimmingCharacters ( in: . whitespaces)
127
168
128
- guard let parameter = extractParam ( firstTextContent: remainingContent, listItem: listItem) else {
129
- return false
130
- }
131
-
132
- parameters [ parameter. name] = parameter. documentation
133
-
134
- return true
169
+ return extractParam ( firstTextContent: remainingContent, listItem: listItem)
135
170
}
136
171
137
172
/// Extracts a parameter field from a list item (used for parameter outline items)
138
- private func extractOutlineItem( from listItem: ListItem ) -> ( name : String , documentation : String ) ? {
173
+ private func extractOutlineItem( from listItem: ListItem ) -> Parameter ? {
139
174
guard let paragraph = listItem. child ( at: 0 ) as? Paragraph else {
140
175
return nil
141
176
}
@@ -157,7 +192,7 @@ private struct ParametersDocumentationExtractor {
157
192
private func extractParam(
158
193
firstTextContent: String ,
159
194
listItem: ListItem
160
- ) -> ( name : String , documentation : String ) ? {
195
+ ) -> Parameter ? {
161
196
guard let paragraph = listItem. child ( at: 0 ) as? Paragraph else {
162
197
return nil
163
198
}
@@ -178,7 +213,7 @@ private struct ParametersDocumentationExtractor {
178
213
let remainingChildren = [ Paragraph ( remainingParagraphChildren) ] + listItem. blockChildren. dropFirst ( )
179
214
let documentation = Document ( remainingChildren) . format ( )
180
215
181
- return ( name, documentation)
216
+ return Parameter ( name: name , documentation : documentation)
182
217
}
183
218
}
184
219
@@ -187,6 +222,6 @@ private struct ParametersDocumentationExtractor {
187
222
/// - Parameter markdown: The markdown text to extract parameters from
188
223
/// - Returns: A tuple containing the extracted parameters dictionary and the remaining markdown text
189
224
package func extractParametersDocumentation( from markdown: String ) -> ( [ String : String ] , String ) {
190
- var extractor = ParametersDocumentationExtractor ( )
225
+ let extractor = ParametersDocumentationExtractor ( )
191
226
return extractor. extract ( from: markdown)
192
227
}
0 commit comments