Skip to content

Commit 5ad35a3

Browse files
authored
Support resolving documentation links to content in other DocC archives (#710)
This adds experimental support for resolving documentation links and symbol links to content in other DocC archives. At a high level this external link support works in two steps: 1. While building the documentation for the first module, DocC emits two files, one that describe the link hierarchy and its possible disambiguations and one that contains minimal amount of content for every linkable page. 2. Later, while building the documentation for the second module, DocC is passed the documentation archive for the first module and reads these file and uses them to resolve links to content in the first archive. > Important: There is more work needed to support this end-to-end. > > If you run this now you'll notice that the external links are 404s in the > browser unless you host both archives on the same server. rdar://114731067
1 parent ecca65a commit 5ad35a3

29 files changed

+1978
-292
lines changed

Sources/SwiftDocC/Infrastructure/ConvertOutputConsumer.swift

+4
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ public protocol ConvertOutputConsumer {
4646

4747
/// Consumes build metadata created during a conversion.
4848
func consume(buildMetadata: BuildMetadata) throws
49+
50+
/// Consumes a file representation of the local link resolution information.
51+
func consume(linkResolutionInformation: SerializableLinkResolutionInformation) throws
4952
}
5053

5154
// Default implementations that discard the documentation conversion products, for consumers that don't need these
5255
// values.
5356
public extension ConvertOutputConsumer {
5457
func consume(renderReferenceStore: RenderReferenceStore) throws {}
5558
func consume(buildMetadata: BuildMetadata) throws {}
59+
func consume(linkResolutionInformation: SerializableLinkResolutionInformation) throws {}
5660
}

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

+29-15
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
110110
}
111111
}
112112

113-
/// A link resolver that resolves references by finding them in path hierarchy.
114-
///
115-
/// The link resolver is `nil` until some documentation content is registered with the context.
116-
/// It's safe to access the link resolver during symbol registration and at later points in the registration and conversion.
117-
var hierarchyBasedLinkResolver: PathHierarchyBasedLinkResolver! = nil
113+
/// A class that resolves documentation links by orchestrating calls to other link resolver implementations.
114+
public var linkResolver = LinkResolver()
118115

119116
/// The provider of documentation bundles for this context.
120117
var dataProvider: DocumentationContextDataProvider
@@ -200,6 +197,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
200197
/// A list of non-topic links that can be resolved.
201198
var nodeAnchorSections = [ResolvedTopicReference: AnchorSection]()
202199

200+
var externalCache = [ResolvedTopicReference: LinkResolver.ExternalEntity]()
201+
203202
/// A list of all the problems that was encountered while registering and processing the documentation bundles in this context.
204203
public var problems: [Problem] {
205204
return diagnosticEngine.problems
@@ -361,7 +360,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
361360
/// - dataProvider: The provider that removed this bundle.
362361
/// - bundle: The bundle that was removed.
363362
public func dataProvider(_ dataProvider: DocumentationContextDataProvider, didRemoveBundle bundle: DocumentationBundle) throws {
364-
hierarchyBasedLinkResolver?.unregisterBundle(identifier: bundle.identifier)
363+
linkResolver.localResolver?.unregisterBundle(identifier: bundle.identifier)
365364

366365
// Purge the reference cache for this bundle and disable reference caching for
367366
// this bundle moving forward.
@@ -1153,7 +1152,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
11531152
var moduleReferences = [String: ResolvedTopicReference]()
11541153

11551154
// Build references for all symbols in all of this module's symbol graphs.
1156-
let symbolReferences = hierarchyBasedLinkResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
1155+
let symbolReferences = linkResolver.localResolver.referencesForSymbols(in: symbolGraphLoader.unifiedGraphs, bundle: bundle, context: self)
11571156

11581157
// Set the index and cache storage capacity to avoid ad-hoc storage resizing.
11591158
symbolIndex.reserveCapacity(symbolReferences.count)
@@ -1270,7 +1269,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
12701269
// Only add the symbol mapping now if the path hierarchy based resolver is the main implementation.
12711270
// If it is only used for mismatch checking then we must wait until the documentation cache code path has traversed and updated all the colliding nodes.
12721271
// Otherwise the mappings will save the unmodified references and the hierarchy based resolver won't find the expected parent nodes when resolving links.
1273-
hierarchyBasedLinkResolver.addMappingForSymbols(symbolIndex: symbolIndex)
1272+
linkResolver.localResolver.addMappingForSymbols(symbolIndex: symbolIndex)
12741273

12751274
// Track the symbols that have multiple matching documentation extension files for diagnostics.
12761275
var symbolsWithMultipleDocumentationExtensionMatches = [ResolvedTopicReference: [SemanticResult<Article>]]()
@@ -1817,7 +1816,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
18171816
topicGraph.addNode(graphNode)
18181817
documentationCache[reference] = documentation
18191818

1820-
hierarchyBasedLinkResolver.addRootArticle(article, anchorSections: documentation.anchorSections)
1819+
linkResolver.localResolver.addRootArticle(article, anchorSections: documentation.anchorSections)
18211820
for anchor in documentation.anchorSections {
18221821
nodeAnchorSections[anchor.reference] = anchor
18231822
}
@@ -1873,7 +1872,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
18731872
let graphNode = TopicGraph.Node(reference: reference, kind: .article, source: .file(url: article.source), title: title)
18741873
topicGraph.addNode(graphNode)
18751874

1876-
hierarchyBasedLinkResolver.addArticle(article, anchorSections: documentation.anchorSections)
1875+
linkResolver.localResolver.addArticle(article, anchorSections: documentation.anchorSections)
18771876
for anchor in documentation.anchorSections {
18781877
nodeAnchorSections[anchor.reference] = anchor
18791878
}
@@ -2078,6 +2077,17 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
20782077
}
20792078
}
20802079

2080+
discoveryGroup.async(queue: discoveryQueue) { [unowned self] in
2081+
do {
2082+
try linkResolver.loadExternalResolvers()
2083+
} catch {
2084+
// Pipe the error out of the dispatch queue.
2085+
discoveryError.sync({
2086+
if $0 == nil { $0 = error }
2087+
})
2088+
}
2089+
}
2090+
20812091
discoveryGroup.wait()
20822092

20832093
try shouldContinueRegistration()
@@ -2128,7 +2138,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21282138
options = globalOptions.first
21292139
}
21302140

2131-
self.hierarchyBasedLinkResolver = hierarchyBasedResolver
2141+
self.linkResolver.localResolver = hierarchyBasedResolver
21322142
hierarchyBasedResolver.addMappingForRoots(bundle: bundle)
21332143
for tutorial in tutorials {
21342144
hierarchyBasedResolver.addTutorial(tutorial)
@@ -2187,7 +2197,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
21872197
}
21882198

21892199
try shouldContinueRegistration()
2190-
var allCuratedReferences = try crawlSymbolCuration(in: hierarchyBasedLinkResolver.topLevelSymbols(), bundle: bundle)
2200+
var allCuratedReferences = try crawlSymbolCuration(in: linkResolver.localResolver.topLevelSymbols(), bundle: bundle)
21912201

21922202
// Store the list of manually curated references if doc coverage is on.
21932203
if shouldStoreManuallyCuratedReferences {
@@ -2222,7 +2232,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
22222232
// Emit warnings for any remaining uncurated files.
22232233
emitWarningsForUncuratedTopics()
22242234

2225-
hierarchyBasedLinkResolver.addAnchorForSymbols(symbolIndex: symbolIndex, documentationCache: documentationCache)
2235+
linkResolver.localResolver.addAnchorForSymbols(symbolIndex: symbolIndex, documentationCache: documentationCache)
22262236

22272237
// Fifth, resolve links in nodes that are added solely via curation
22282238
try preResolveExternalLinks(references: Array(allCuratedReferences), bundle: bundle)
@@ -2329,7 +2339,7 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
23292339
/// - Returns: An ordered list of symbol references that have been added to the topic graph automatically.
23302340
private func autoCurateSymbolsInTopicGraph(engine: DiagnosticEngine) -> [(child: ResolvedTopicReference, parent: ResolvedTopicReference)] {
23312341
var automaticallyCuratedSymbols = [(ResolvedTopicReference, ResolvedTopicReference)]()
2332-
hierarchyBasedLinkResolver.traverseSymbolAndParentPairs { reference, parentReference in
2342+
linkResolver.localResolver.traverseSymbolAndParentPairs { reference, parentReference in
23332343
guard let topicGraphNode = topicGraph.nodeWithReference(reference),
23342344
let topicGraphParentNode = topicGraph.nodeWithReference(parentReference),
23352345
// Check that the node hasn't got any parents from manual curation
@@ -2535,6 +2545,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
25352545
let referenceWithoutFragment = reference.withFragment(nil)
25362546
return try entity(with: referenceWithoutFragment).availableSourceLanguages
25372547
} catch ContextError.notFound {
2548+
if let externalEntity = externalCache[reference] {
2549+
return externalEntity.sourceLanguages
2550+
}
25382551
preconditionFailure("Reference does not have an associated documentation node.")
25392552
} catch {
25402553
fatalError("Unexpected error when retrieving source languages: \(error)")
@@ -2646,7 +2659,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
26462659
public func resolve(_ reference: TopicReference, in parent: ResolvedTopicReference, fromSymbolLink isCurrentlyResolvingSymbolLink: Bool = false) -> TopicReferenceResolutionResult {
26472660
switch reference {
26482661
case .unresolved(let unresolvedReference):
2649-
return hierarchyBasedLinkResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink, context: self)
2662+
return linkResolver.resolve(unresolvedReference, in: parent, fromSymbolLink: isCurrentlyResolvingSymbolLink, context: self)
2663+
26502664
case .resolved(let resolved):
26512665
// This reference is already resolved (either as a success or a failure), so don't change anything.
26522666
return resolved

Sources/SwiftDocC/Infrastructure/DocumentationConverter.swift

+20-1
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,20 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
330330
}
331331

332332
if emitDigest {
333-
let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode)
333+
let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode, includeTaskGroups: true)
334334
let nodeIndexingRecords = try renderNode.indexingRecords(onPage: identifier)
335335

336336
resultsGroup.async(queue: resultsSyncQueue) {
337337
assets.merge(renderNode.assetReferences, uniquingKeysWith: +)
338338
linkSummaries.append(contentsOf: nodeLinkSummaries)
339339
indexingRecords.append(contentsOf: nodeIndexingRecords)
340340
}
341+
} else if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled {
342+
let nodeLinkSummaries = entity.externallyLinkableElementSummaries(context: context, renderNode: renderNode, includeTaskGroups: false)
343+
344+
resultsGroup.async(queue: resultsSyncQueue) {
345+
linkSummaries.append(contentsOf: nodeLinkSummaries)
346+
}
341347
}
342348
} catch {
343349
recordProblem(from: error, in: &results, withIdentifier: "render-node")
@@ -362,6 +368,19 @@ public struct DocumentationConverter: DocumentationConverterProtocol {
362368
}
363369
}
364370

371+
if FeatureFlags.current.isExperimentalLinkHierarchySerializationEnabled {
372+
do {
373+
let serializableLinkInformation = try context.linkResolver.localResolver.prepareForSerialization(bundleID: bundle.identifier)
374+
try outputConsumer.consume(linkResolutionInformation: serializableLinkInformation)
375+
376+
if !emitDigest {
377+
try outputConsumer.consume(linkableElementSummaries: linkSummaries)
378+
}
379+
} catch {
380+
recordProblem(from: error, in: &conversionProblems, withIdentifier: "link-resolver")
381+
}
382+
}
383+
365384
if emitDigest {
366385
do {
367386
try outputConsumer.consume(problems: context.problems + conversionProblems)

Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ struct DocumentationCurator {
115115
context.topicGraph.addNode(curatedNode)
116116

117117
// Move the article from the article cache to the documentation
118-
context.hierarchyBasedLinkResolver.addArticle(filename: articleFilename, reference: reference, anchorSections: documentationNode.anchorSections)
118+
context.linkResolver.localResolver.addArticle(filename: articleFilename, reference: reference, anchorSections: documentationNode.anchorSections)
119119

120120
context.documentationCache[reference] = documentationNode
121121
for anchor in documentationNode.anchorSections {

0 commit comments

Comments
 (0)