Skip to content

Commit e3e8c91

Browse files
committed
Merge branch 'main' into jed/doxygen-discussion-note
2 parents 323675b + 90a56b8 commit e3e8c91

File tree

55 files changed

+2849
-540
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2849
-540
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 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+
import SymbolKit
13+
14+
/// A type that writes the auto-generated curation into documentation extension files.
15+
public struct GeneratedCurationWriter {
16+
let context: DocumentationContext
17+
let catalogURL: URL?
18+
let outputURL: URL
19+
let linkResolver: PathHierarchyBasedLinkResolver
20+
21+
public init(
22+
context: DocumentationContext,
23+
catalogURL: URL?,
24+
outputURL: URL
25+
) {
26+
self.context = context
27+
28+
self.catalogURL = catalogURL
29+
self.outputURL = outputURL
30+
31+
self.linkResolver = context.linkResolver.localResolver
32+
}
33+
34+
/// Generates the markdown representation of the auto-generated curation for a given symbol reference.
35+
///
36+
/// - Parameters:
37+
/// - reference: The symbol reference to generate curation text for.
38+
/// - Returns: The auto-generated curation text, or `nil` if this reference has no auto-generated curation.
39+
func defaultCurationText(for reference: ResolvedTopicReference) -> String? {
40+
guard let node = context.documentationCache[reference],
41+
let symbol = node.semantic as? Symbol,
42+
let automaticTopics = try? AutomaticCuration.topics(for: node, withTraits: [], context: context),
43+
!automaticTopics.isEmpty
44+
else {
45+
return nil
46+
}
47+
48+
let relativeLinks = linkResolver.disambiguatedRelativeLinksForDescendants(of: reference)
49+
50+
// Top-level curation has a few special behaviors regarding symbols with different representations in multiple languages.
51+
let isForTopLevelCuration = symbol.kind.identifier == .module
52+
53+
var text = ""
54+
for taskGroup in automaticTopics {
55+
if isForTopLevelCuration, let firstReference = taskGroup.references.first, context.documentationCache[firstReference]?.symbol?.kind.identifier == .typeProperty {
56+
// Skip type properties in top-level curation. It's not clear what's the right place for these symbols are since they exist in
57+
// different places in different source languages (which documentation extensions don't yet have a way of representing).
58+
continue
59+
}
60+
61+
let links: [(link: String, comment: String?)] = taskGroup.references.compactMap { (curatedReference: ResolvedTopicReference) -> (String, String?)? in
62+
guard let linkInfo = relativeLinks[curatedReference] else { return nil }
63+
// 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.
64+
var commentText: String?
65+
if linkInfo.hasDisambiguation {
66+
commentText = context.documentationCache[curatedReference]?.symbol?.declarationFragments?.map(\.spelling)
67+
// Replace sequences of whitespace and newlines with a single space
68+
.joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ")
69+
}
70+
71+
return ("\n- ``\(linkInfo.link)``", commentText.map { " <!-- \($0) -->" })
72+
}
73+
74+
guard !links.isEmpty else { continue }
75+
76+
text.append("\n\n### \(taskGroup.title ?? "<!-- This auto-generated topic has no title -->")\n")
77+
78+
// Calculate the longest link to nicely align all the comments
79+
let longestLink = links.map(\.link.count).max()! // `links` are non-empty so it's safe to force-unwrap `.max()` here
80+
for (link, comment) in links {
81+
if let comment = comment {
82+
text.append(link.padding(toLength: longestLink, withPad: " ", startingAt: 0))
83+
text.append(comment)
84+
} else {
85+
text.append(link)
86+
}
87+
}
88+
}
89+
90+
guard !text.isEmpty else { return nil }
91+
92+
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. -->"
93+
94+
// Add "## Topics" to the curation text unless the symbol already had some manual curation.
95+
let hasAnyManualCuration = symbol.topics?.taskGroups.isEmpty == false
96+
if !hasAnyManualCuration {
97+
prefix.append("\n\n## Topics")
98+
}
99+
return "\(prefix)\(text)\n"
100+
}
101+
102+
enum Error: DescribedError {
103+
case symbolLinkNotFound(TopicReferenceResolutionErrorInfo)
104+
105+
var errorDescription: String {
106+
switch self {
107+
case .symbolLinkNotFound(let errorInfo):
108+
var errorMessage = "'--from-symbol <symbol-link>' not found: \(errorInfo.message)"
109+
for solution in errorInfo.solutions {
110+
errorMessage.append("\n\(solution.summary.replacingOccurrences(of: "\n", with: ""))")
111+
}
112+
return errorMessage
113+
}
114+
}
115+
}
116+
117+
/// Generates documentation extension content with a markdown representation of DocC's auto-generated curation.
118+
/// - Parameters:
119+
/// - symbolLink: A link to the symbol whose sub hierarchy the curation writer will descend.
120+
/// - depthLimit: The depth limit of how far the curation writer will descend from its starting point symbol.
121+
/// - Returns: A collection of file URLs and their markdown content.
122+
public func generateDefaultCurationContents(fromSymbol symbolLink: String? = nil, depthLimit: Int? = nil) throws -> [URL: String] {
123+
// Used in documentation extension page titles to reference symbols that don't already have a documentation extension file.
124+
let allAbsoluteLinks = linkResolver.pathHierarchy.disambiguatedAbsoluteLinks()
125+
126+
guard var curationCrawlRoot = linkResolver.modules().first else {
127+
return [:]
128+
}
129+
130+
if let symbolLink = symbolLink {
131+
switch context.linkResolver.resolve(UnresolvedTopicReference(topicURL: .init(symbolPath: symbolLink)), in: curationCrawlRoot, fromSymbolLink: true, context: context) {
132+
case .success(let foundSymbol):
133+
curationCrawlRoot = foundSymbol
134+
case .failure(_, let errorInfo):
135+
throw Error.symbolLinkNotFound(errorInfo)
136+
}
137+
}
138+
139+
var contentsToWrite = [URL: String]()
140+
for (usr, reference) in context.symbolIndex {
141+
// Filter out symbols that aren't in the specified sub hierarchy.
142+
if symbolLink != nil || depthLimit != nil {
143+
guard reference == curationCrawlRoot || context.pathsTo(reference).contains(where: { path in path.suffix(depthLimit ?? .max).contains(curationCrawlRoot)}) else {
144+
continue
145+
}
146+
}
147+
148+
guard let absoluteLink = allAbsoluteLinks[usr], let curationText = defaultCurationText(for: reference) else { continue }
149+
if let catalogURL = catalogURL, let existingURL = context.documentationExtensionURL(for: reference) {
150+
let updatedFileURL: URL
151+
if catalogURL == outputURL {
152+
updatedFileURL = existingURL
153+
} else {
154+
var url = outputURL
155+
let relativeComponents = existingURL.standardizedFileURL.pathComponents.dropFirst(catalogURL.standardizedFileURL.pathComponents.count)
156+
for component in relativeComponents.dropLast() {
157+
url.appendPathComponent(component, isDirectory: true)
158+
}
159+
url.appendPathComponent(relativeComponents.last!, isDirectory: false)
160+
updatedFileURL = url
161+
}
162+
// Append to the end of the file. See if we can avoid reading the existing contents on disk.
163+
var contents = try String(contentsOf: existingURL)
164+
contents.append("\n")
165+
contents.append(curationText)
166+
contentsToWrite[updatedFileURL] = contents
167+
} else {
168+
let relativeReferencePath = reference.url.pathComponents.dropFirst(2).joined(separator: "/")
169+
let fileName = urlReadablePath("/" + relativeReferencePath)
170+
let newFileURL = NodeURLGenerator.fileSafeURL(outputURL.appendingPathComponent("\(fileName).md"))
171+
172+
let contents = """
173+
# ``\(absoluteLink)``
174+
175+
\(curationText)
176+
"""
177+
contentsToWrite[newFileURL] = contents
178+
}
179+
}
180+
181+
return contentsToWrite
182+
}
183+
}

Sources/SwiftDocC/Infrastructure/Bundle Assets/DataAssetManager.swift

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -12,17 +12,6 @@ import Foundation
1212

1313
/// A container for a collection of data. Each data can have multiple variants.
1414
struct DataAssetManager {
15-
enum Error: DescribedError {
16-
case invalidImageAsset(URL)
17-
18-
var errorDescription: String {
19-
switch self {
20-
case .invalidImageAsset(let url):
21-
return "The dimensions of the image at \(url.path.singleQuoted) could not be computed because the file is not a valid image."
22-
}
23-
}
24-
}
25-
2615
var storage = [String: DataAsset]()
2716

2817
// A "file name with no extension" to "file name with extension" index
@@ -113,12 +102,10 @@ struct DataAssetManager {
113102
return (reference: dataReference, traits: traitCollection, metadata: metadata)
114103
}
115104

116-
/**
117-
Registers a collection of data and determines their trait collection.
118-
119-
Data objects which have a file name ending with '~dark' are associated to their light variant.
120-
- Throws: Will throw `Error.invalidImageAsset(URL)` if fails to read the size of an image asset (e.g. the file is corrupt).
121-
*/
105+
106+
/// Registers a collection of data and determines their trait collection.
107+
///
108+
/// Data objects which have a file name ending with '~dark' are associated to their light variant.
122109
mutating func register<Datas: Collection>(data datas: Datas, dataProvider: DocumentationContextDataProvider? = nil, bundle documentationBundle: DocumentationBundle? = nil) throws where Datas.Element == URL {
123110
for dataURL in datas {
124111
let meta = try referenceMetaInformationForDataURL(dataURL, dataProvider: dataProvider, bundle: documentationBundle)

Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -92,6 +92,8 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
9292
/// The source repository where the documentation's sources are hosted.
9393
var sourceRepository: SourceRepository?
9494

95+
var experimentalModifyCatalogWithGeneratedCuration: Bool
96+
9597
/// The identifiers and access level requirements for symbols that have an expanded version of their documentation page if the requirements are met
9698
var symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil
9799

@@ -139,7 +141,8 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
139141
sourceRepository: SourceRepository? = nil,
140142
isCancelled: Synchronized<Bool>? = nil,
141143
diagnosticEngine: DiagnosticEngine = .init(),
142-
symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil
144+
symbolIdentifiersWithExpandedDocumentation: [String: ConvertRequest.ExpandedDocumentationRequirements]? = nil,
145+
experimentalModifyCatalogWithGeneratedCuration: Bool = false
143146
) {
144147
self.rootURL = documentationBundleURL
145148
self.emitDigest = emitDigest
@@ -156,6 +159,7 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
156159
self.isCancelled = isCancelled
157160
self.diagnosticEngine = diagnosticEngine
158161
self.symbolIdentifiersWithExpandedDocumentation = symbolIdentifiersWithExpandedDocumentation
162+
self.experimentalModifyCatalogWithGeneratedCuration = experimentalModifyCatalogWithGeneratedCuration
159163

160164
// Inject current platform versions if provided
161165
if let currentPlatforms = currentPlatforms {
@@ -246,6 +250,16 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
246250
// For now, we only support one bundle.
247251
let bundle = bundles.first!
248252

253+
if experimentalModifyCatalogWithGeneratedCuration, let catalogURL = rootURL {
254+
let writer = GeneratedCurationWriter(context: context, catalogURL: catalogURL, outputURL: catalogURL)
255+
let curation = try writer.generateDefaultCurationContents()
256+
for (url, updatedContent) in curation {
257+
guard let data = updatedContent.data(using: .utf8) else { continue }
258+
try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)
259+
try? data.write(to: url, options: .atomic)
260+
}
261+
}
262+
249263
guard !context.problems.containsErrors else {
250264
if emitDigest {
251265
try outputConsumer.consume(problems: context.problems)

Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,29 @@ struct DocumentationCurator {
229229
(childDocumentationNode.kind == .article || childDocumentationNode.kind.isSymbol || childDocumentationNode.kind == .tutorial || childDocumentationNode.kind == .tutorialArticle) else {
230230
continue
231231
}
232-
233-
guard childDocumentationNode.kind != .module else {
232+
233+
234+
// If a module has a path with a manual technology root there shouldn't
235+
// be an error messsage. This is an if statement instead of a guard because
236+
// when there's no warning we still curate the node
237+
if childDocumentationNode.kind == .module {
238+
239+
func isTechnologyRoot(_ reference: ResolvedTopicReference) -> Bool {
240+
guard let node = context.topicGraph.nodeWithReference(reference) else {return false}
241+
return node.kind == .module && documentationNode.kind.isSymbol == false
242+
}
243+
244+
245+
let hasTechnologyRoot = isTechnologyRoot(nodeReference) || context.pathsTo(nodeReference).contains { path in
246+
guard let root = path.first else {return false}
247+
return isTechnologyRoot(root)
248+
}
249+
250+
if !hasTechnologyRoot {
234251
problems.append(Problem(diagnostic: Diagnostic(source: source(), severity: .warning, range: range(), identifier: "org.swift.docc.ModuleCuration", summary: "Linking to \((link.destination ?? "").singleQuoted) from a Topics group in \(nodeReference.absoluteString.singleQuoted) isn't allowed", explanation: "The former is a module, and modules only exist at the root"), possibleSolutions: []))
235252
continue
253+
}
254+
236255
}
237256

238257
// Verify we are not creating a graph cyclic relationship.

0 commit comments

Comments
 (0)