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+
113import Foundation
214import Markdown
315
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.
436private struct ParametersDocumentationExtractor {
5- private var parameters = [ String: String] ( )
37+ struct Parameter {
38+ let name : String
39+ let documentation : String
40+ }
641
742 /// Extracts parameter documentation from a markdown string.
843 ///
944 /// - 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 ) {
1146 let document = Document ( parsing: markdown, options: [ . parseBlockDirectives, . parseMinimalDoxygen] )
1247
13- var remainingBlocks = [ any BlockMarkup ] ( )
48+ var parameters : [ String : String ] = [ : ]
49+ var remainingBlocks : [ any BlockMarkup ] = [ ]
1450
1551 for block in document. blockChildren {
1652 switch block {
1753 case let unorderedList as UnorderedList :
18- if let newUnorderedList = extract ( from: unorderedList) {
54+ let ( newUnorderedList, params) = extract ( from: unorderedList)
55+ if let newUnorderedList {
1956 remainingBlocks. append ( newUnorderedList)
2057 }
58+
59+ for param in params {
60+ parameters [ param. name] = param. documentation
61+ }
62+
2163 case let doxygenParameter as DoxygenParameter :
22- extract ( from: doxygenParameter)
64+ let param = extract ( from: doxygenParameter)
65+ parameters [ param. name] = param. documentation
66+
2367 default :
2468 remainingBlocks. append ( block)
2569 }
@@ -31,29 +75,35 @@ private struct ParametersDocumentationExtractor {
3175 }
3276
3377 /// 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+ )
3683 }
3784
3885 /// Extracts parameter documentation from an unordered list.
3986 ///
4087 /// - 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 ] = [ ]
4391
4492 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)
4799 }
48-
49- newItems. append ( item)
50100 }
51101
52102 if newItems. isEmpty {
53- return nil
103+ return ( remaining : nil , parameters : parameters )
54104 }
55105
56- return UnorderedList ( newItems)
106+ return ( remaining : UnorderedList ( newItems) , parameters : parameters )
57107 }
58108
59109 /// Parameter documentation from a `Parameters:` outline.
@@ -65,33 +115,24 @@ private struct ParametersDocumentationExtractor {
65115 /// ```
66116 ///
67117 /// - 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 ] ? {
69119 guard let firstChild = listItem. child ( at: 0 ) as? Paragraph ,
70120 let headingText = firstChild. child ( at: 0 ) as? Text
71121 else {
72- return false
122+ return nil
73123 }
74124
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
80127 }
81128
82- for child in listItem. children {
129+ return listItem. children. flatMap { child in
83130 guard let nestedList = child as? UnorderedList else {
84- continue
131+ return [ ] as [ Parameter ]
85132 }
86133
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)
92135 }
93-
94- return true
95136 }
96137
97138 /// Extracts parameter documentation from a single parameter.
@@ -102,40 +143,34 @@ private struct ParametersDocumentationExtractor {
102143 /// ```
103144 ///
104145 /// - 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 ? {
106147 guard let paragraph = listItem. child ( at: 0 ) as? Paragraph ,
107148 let paragraphText = paragraph. child ( at: 0 ) as? Text
108149 else {
109- return false
150+ return nil
110151 }
111152
112153 let parameterPrefix = " parameter "
113154 let paragraphContent = paragraphText. string
114155
115156 guard paragraphContent. count >= parameterPrefix. count else {
116- return false
157+ return nil
117158 }
118159
119160 let prefixEnd = paragraphContent. index ( paragraphContent. startIndex, offsetBy: parameterPrefix. count)
120161 let potentialMatch = paragraphContent [ ..< prefixEnd] . lowercased ( )
121162
122163 guard potentialMatch == parameterPrefix else {
123- return false
164+ return nil
124165 }
125166
126167 let remainingContent = String ( paragraphContent [ prefixEnd... ] ) . trimmingCharacters ( in: . whitespaces)
127168
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)
135170 }
136171
137172 /// 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 ? {
139174 guard let paragraph = listItem. child ( at: 0 ) as? Paragraph else {
140175 return nil
141176 }
@@ -157,7 +192,7 @@ private struct ParametersDocumentationExtractor {
157192 private func extractParam(
158193 firstTextContent: String ,
159194 listItem: ListItem
160- ) -> ( name : String , documentation : String ) ? {
195+ ) -> Parameter ? {
161196 guard let paragraph = listItem. child ( at: 0 ) as? Paragraph else {
162197 return nil
163198 }
@@ -178,7 +213,7 @@ private struct ParametersDocumentationExtractor {
178213 let remainingChildren = [ Paragraph ( remainingParagraphChildren) ] + listItem. blockChildren. dropFirst ( )
179214 let documentation = Document ( remainingChildren) . format ( )
180215
181- return ( name, documentation)
216+ return Parameter ( name: name , documentation : documentation)
182217 }
183218}
184219
@@ -187,6 +222,6 @@ private struct ParametersDocumentationExtractor {
187222/// - Parameter markdown: The markdown text to extract parameters from
188223/// - Returns: A tuple containing the extracted parameters dictionary and the remaining markdown text
189224package func extractParametersDocumentation( from markdown: String ) -> ( [ String : String ] , String ) {
190- var extractor = ParametersDocumentationExtractor ( )
225+ let extractor = ParametersDocumentationExtractor ( )
191226 return extractor. extract ( from: markdown)
192227}
0 commit comments