File: GeneratedCurationWriter.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 (183 lines) | stat: -rw-r--r-- 9,095 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
/*
 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
import SymbolKit

/// A type that writes the auto-generated curation into documentation extension files.
public struct GeneratedCurationWriter {
    let context: DocumentationContext
    let catalogURL: URL?
    let outputURL: URL
    let linkResolver: PathHierarchyBasedLinkResolver
    
    public init(
        context: DocumentationContext,
        catalogURL: URL?,
        outputURL: URL
    ) {
        self.context = context
        
        self.catalogURL = catalogURL
        self.outputURL = outputURL
        
        self.linkResolver = context.linkResolver.localResolver
    }
    
    /// Generates the markdown representation of the auto-generated curation for a given symbol reference.
    ///
    /// - Parameters:
    ///   - reference: The symbol reference to generate curation text for.
    /// - Returns: The auto-generated curation text, or `nil` if this reference has no auto-generated curation.
    func defaultCurationText(for reference: ResolvedTopicReference) -> String? {
        guard let node = context.documentationCache[reference],
              let symbol = node.semantic as? Symbol,
              let automaticTopics = try? AutomaticCuration.topics(for: node, withTraits: [], context: context),
              !automaticTopics.isEmpty
        else {
            return nil
        }
        
        let relativeLinks = linkResolver.disambiguatedRelativeLinksForDescendants(of: reference)
        
        // Top-level curation has a few special behaviors regarding symbols with different representations in multiple languages.
        let isForTopLevelCuration = symbol.kind.identifier == .module
        
        var text = ""
        for taskGroup in automaticTopics {
            if isForTopLevelCuration, let firstReference = taskGroup.references.first, context.documentationCache[firstReference]?.symbol?.kind.identifier == .typeProperty {
                // Skip type properties in top-level curation. It's not clear what's the right place for these symbols are since they exist in
                // different places in different source languages (which documentation extensions don't yet have a way of representing).
                continue
            }
            
            let links: [(link: String, comment: String?)] = taskGroup.references.compactMap { (curatedReference: ResolvedTopicReference) -> (String, String?)? in
                guard let linkInfo = relativeLinks[curatedReference] else { return nil }
                // If this link contains disambiguation, include a comment with the full symbol declaration to make it easier to know which symbol the link refers to.
                var commentText: String?
                if linkInfo.hasDisambiguation {
                    commentText = context.documentationCache[curatedReference]?.symbol?.declarationFragments?.map(\.spelling)
                        // Replace sequences of whitespace and newlines with a single space
                        .joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ")
                }
                
                return ("\n- ``\(linkInfo.link)``", commentText.map { " <!-- \($0) -->" })
            }
            
            guard !links.isEmpty else { continue }
            
            text.append("\n\n### \(taskGroup.title ?? "<!-- This auto-generated topic has no title -->")\n")
            
            // Calculate the longest link to nicely align all the comments
            let longestLink = links.map(\.link.count).max()! // `links` are non-empty so it's safe to force-unwrap `.max()` here
            for (link, comment) in links {
                if let comment {
                    text.append(link.padding(toLength: longestLink, withPad: " ", startingAt: 0))
                    text.append(comment)
                } else {
                    text.append(link)
                }
            }
        }
        
        guard !text.isEmpty else { return nil }
        
        var prefix = "<!-- The content below this line is auto-generated and is redundant. You should either incorporate it into your content above this line or delete it. -->"
        
        // Add "## Topics" to the curation text unless the symbol already had some manual curation.
        let hasAnyManualCuration = symbol.topics?.taskGroups.isEmpty == false
        if !hasAnyManualCuration {
            prefix.append("\n\n## Topics")
        }
        return "\(prefix)\(text)\n"
    }
    
    enum Error: DescribedError {
        case symbolLinkNotFound(TopicReferenceResolutionErrorInfo)
        
        var errorDescription: String {
            switch self {
            case .symbolLinkNotFound(let errorInfo):
                var errorMessage = "'--from-symbol <symbol-link>' not found: \(errorInfo.message)"
                for solution in errorInfo.solutions {
                    errorMessage.append("\n\(solution.summary.replacingOccurrences(of: "\n", with: ""))")
                }
                return errorMessage
            }
        }
    }
    
    /// Generates documentation extension content with a markdown representation of DocC's auto-generated curation.
    /// - Parameters:
    ///   - symbolLink: A link to the symbol whose sub hierarchy the curation writer will descend.
    ///   - depthLimit: The depth limit of how far the curation writer will descend from its starting point symbol.
    /// - Returns: A collection of file URLs and their markdown content.
    public func generateDefaultCurationContents(fromSymbol symbolLink: String? = nil, depthLimit: Int? = nil) throws -> [URL: String] {
        // Used in documentation extension page titles to reference symbols that don't already have a documentation extension file.
        let allAbsoluteLinks = linkResolver.pathHierarchy.disambiguatedAbsoluteLinks()
        
        guard var curationCrawlRoot = linkResolver.modules().first else {
            return [:]
        }
        
        if let symbolLink {
            switch context.linkResolver.resolve(UnresolvedTopicReference(topicURL: .init(symbolPath: symbolLink)), in: curationCrawlRoot, fromSymbolLink: true, context: context) {
            case .success(let foundSymbol):
                curationCrawlRoot = foundSymbol
            case .failure(_, let errorInfo):
                throw Error.symbolLinkNotFound(errorInfo)
            }
        }
        
        var contentsToWrite = [URL: String]()
        for (usr, reference) in context.documentationCache.referencesBySymbolID {
            // Filter out symbols that aren't in the specified sub hierarchy.
            if symbolLink != nil || depthLimit != nil {
                guard reference == curationCrawlRoot || context.finitePaths(to: reference).contains(where: { path in path.suffix(depthLimit ?? .max).contains(curationCrawlRoot)}) else {
                    continue
                }
            }
            
            guard let absoluteLink = allAbsoluteLinks[usr], let curationText = defaultCurationText(for: reference) else { continue }
            if let catalogURL, let existingURL = context.documentationExtensionURL(for: reference) {
                let updatedFileURL: URL
                if catalogURL == outputURL {
                    updatedFileURL = existingURL
                } else {
                    var url = outputURL
                    let relativeComponents = existingURL.standardizedFileURL.pathComponents.dropFirst(catalogURL.standardizedFileURL.pathComponents.count)
                    for component in relativeComponents.dropLast() {
                        url.appendPathComponent(component, isDirectory: true)
                    }
                    url.appendPathComponent(relativeComponents.last!, isDirectory: false)
                    updatedFileURL = url
                }
                // Append to the end of the file. See if we can avoid reading the existing contents on disk.
                var contents = try String(contentsOf: existingURL)
                contents.append("\n")
                contents.append(curationText)
                contentsToWrite[updatedFileURL] = contents
            } else {
                let relativeReferencePath = reference.url.pathComponents.dropFirst(2).joined(separator: "/")
                let fileName = urlReadablePath("/" + relativeReferencePath)
                let newFileURL = NodeURLGenerator.fileSafeURL(outputURL.appendingPathComponent("\(fileName).md"))
                
                let contents = """
                # ``\(absoluteLink)``
                
                \(curationText)
                """
                contentsToWrite[newFileURL] = contents
            }
        }
        
        return contentsToWrite
    }
}