File: TaskGroup.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (243 lines) | stat: -rw-r--r-- 9,388 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
 This source file is part of the Swift.org open source project

 Copyright (c) 2021 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
import Markdown

/**
 A rewriter that extracts topic links for unordered list items.
 */
struct ExtractLinks: MarkupRewriter {
    enum Mode {
        case linksDirective
        case taskGroup
    }
    
    var links = [AnyLink]()
    var problems = [Problem]()
    var mode = Mode.taskGroup
        
    /// Creates a warning with a suggestion to remove all paragraph elements but the first.
    private func problemForTrailingContent(_ paragraph: Paragraph) -> Problem {
        let range = paragraph.range ?? paragraph.firstChildRange()
        // An unexpected non-link list item found, suggest to remove it
        let trailingContent = Document(Paragraph(paragraph.inlineChildren.dropFirst()))
        let replacements = trailingContent.children.range.map({ [Replacement(range: $0, replacement: "")] }) ?? []
        
        let diagnostic: Diagnostic
        switch mode {
        case .taskGroup:
            diagnostic = Diagnostic(
                source: range?.source,
                severity: .warning,
                range: range,
                identifier: "org.swift.docc.ExtraneousTaskGroupItemContent",
                summary: "Extraneous content found after a link in task group list item"
            )
        case .linksDirective:
            diagnostic = Diagnostic(
                source: range?.source,
                severity: .warning,
                range: range,
                identifier: "org.swift.docc.ExtraneousLinksDirectiveItemContent",
                summary: "Extraneous content found after a link",
                explanation: "\(Links.directiveName.singleQuoted) can only contain a bulleted list of documentation links"
            )
        }
        
        return .init(diagnostic: diagnostic, possibleSolutions: [
            Solution(summary: "Remove extraneous content", replacements: replacements)
        ])
    }
    
    private func problemForNonLinkContent(_ item: ListItem) -> Problem {
        let range = item.range ?? item.firstChildRange()
        let replacements = range.map({ [Replacement(range: $0, replacement: "")] }) ?? []
        
        let diagnostic: Diagnostic
        switch mode {
        case .taskGroup:
            diagnostic = Diagnostic(
                source: range?.source,
                severity: .warning,
                range: range,
                identifier: "org.swift.docc.UnexpectedTaskGroupItem",
                summary: "Only links are allowed in task group list items"
            )
        case .linksDirective:
            diagnostic = Diagnostic(
                source: range?.source,
                severity: .warning,
                range: range,
                identifier: "org.swift.docc.UnexpectedLinksDirectiveListItem",
                summary: "Only documentation links are allowed in \(Links.directiveName.singleQuoted) list items"
            )
        }
        
        return .init(diagnostic: diagnostic, possibleSolutions: [
            Solution(summary: "Remove non-link item", replacements: replacements)
        ])
    }
    
    mutating func visitUnorderedList(_ unorderedList: UnorderedList) -> Markup? {
        let remainingItems = unorderedList.children.map { $0 as! ListItem }
            .filter { item -> Bool in
                guard item.childCount == 1 else { return true }
                
                guard let paragraph = item.child(at: 0) as? Paragraph,
                    paragraph.childCount >= 1 else { return true }
                
                // Check for trailing invalid content.
                let containsInvalidContent = paragraph.children.dropFirst().contains { child in
                    let isComment = child is InlineHTML
                    var isSpace = false
                    if let text = child as? Text {
                        isSpace = text.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                    }
                    return !(isComment || isSpace)
                }
                
                switch paragraph.child(at: 0) {
                    case let link as Link:
                        // Topic link
                        guard let url = link.destination.flatMap(URL.init(string:)) else {
                            return true
                        }
                    
                        switch mode {
                        case .linksDirective:
                            // The 'Links' directive only supports `doc:` links.
                            guard ResolvedTopicReference.urlHasResolvedTopicScheme(url) else {
                                problems.append(problemForNonLinkContent(item))
                                return true
                            }
                        case .taskGroup:
                            guard let scheme = url.scheme,
                                  TaskGroup.allowedSchemes.contains(scheme) else { return true }
                        }
                        links.append(link)
                        
                        // Warn if there is a trailing content after the link
                        if containsInvalidContent {
                            
                            problems.append(problemForTrailingContent(paragraph))
                        }
                        return false
                    case let link as SymbolLink:
                        // Symbol link
                        links.append(link)
                        
                        // Warn if there is a trailing content after the link
                        if containsInvalidContent {
                            problems.append(problemForTrailingContent(paragraph))
                        }
                        return false
                    default:
                        problems.append(problemForNonLinkContent(item))
                        return true
                }
        }
        guard !remainingItems.isEmpty else {
            return nil
        }
        return UnorderedList(remainingItems)
    }
}

/**
 A collection of curated child topics.
 */
public struct TaskGroup {
    /// The schemes for links to external content supported in task groups.
    static let allowedExternalSchemes = ["http", "https"]
    /// The schemes for links that is supported in task groups.
    static let allowedSchemes = allowedExternalSchemes + [ResolvedTopicReference.urlScheme]
    
    /// The title heading of the group.
    public var heading: Heading?
    
    /// The group's original contents, excluding its delimiting heading.
    public var originalContent: [Markup]
    
    /// The group's remaining content after stripping topic links.
    public var content: [Markup] {
        var extractor = ExtractLinks()
        return originalContent.compactMap {
            extractor.visit($0)
        }
    }
    
    /**
     The curated child topic links in this group.
     
     - Note: Links must be at the top level and have the `doc:` URL scheme.
     */
    public var links: [AnyLink] {
        var extractor = ExtractLinks()
        for child in originalContent {
            _ = extractor.visit(child)
        }
        return extractor.links
    }
    
    /// An optional abstract for the task group.
    public var abstract: AbstractSection? {
        if let firstParagraph = originalContent.mapFirst(where: { $0 as? Paragraph }) {
            return AbstractSection(paragraph: firstParagraph)
        }
        return nil
    }
    
    /// An optional discussion section for the task group.
    public var discussion: DiscussionSection? {
        guard originalContent.count > 1 else {
            // There must be more than 1 element to contain both a discussion and links list
            return nil
        }
        
        var discussionChildren = originalContent
            .prefix(while: { !($0 is UnorderedList) })
            .filter({ !($0 is BlockDirective) })
        
        // Drop the abstract
        if discussionChildren.first is Paragraph {
            discussionChildren.removeFirst()
        }
        
        guard !discussionChildren.isEmpty else { return nil }

        return DiscussionSection(content: Array(discussionChildren))
    }
    
    /// Creates a new task group with a given heading and content.
    /// - Parameters:
    ///   - heading: The heading for this task group.
    ///   - content: The content, excluding the title, for this task group.
    public init(heading: Heading?, content: [Markup]) {
        self.heading = heading
        self.originalContent = content
    }
    
    var directives: [String: [BlockDirective]] {
        .init(grouping: originalContent.compactMap { $0 as? BlockDirective }, by: \.name)
    }
}

extension TaskGroup {
    /// Validates the task group links markdown and return the problems, if any.
    func problemsForGroupLinks() -> [Problem] {
        var extractor = ExtractLinks()
        for child in originalContent {
            _ = extractor.visit(child)
        }
        return extractor.problems
    }
}