Skip to content

Commit 87318ef

Browse files
Support resolving external content with topic images (#469)
* Support resolving external content with topic images rdar://100501774 * Apply suggestions from code review Co-authored-by: Ethan Kusters <[email protected]> * Address additional code review feedback * Don't create an DataAssetManager for assets from the external content * Test resolving two external topics with images on the same page --------- Co-authored-by: Ethan Kusters <[email protected]>
1 parent 39e23fb commit 87318ef

31 files changed

+1497
-71
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
223223
/// unresolvable asset references using a fallback resolver (if one is set for the reference's bundle identifier.)
224224
public var fallbackAssetResolvers = [BundleIdentifier: FallbackAssetResolver]()
225225

226+
// This protocol only exist to workaround a limitation. It should be removed when it's no longer needed.
227+
// FIXME: https://github.com/apple/swift-docc/issues/468
228+
public var _externalAssetResolvers = [BundleIdentifier: _ExternalAssetResolver]()
229+
226230
/// All the symbol references that have been resolved from external sources.
227231
///
228232
/// This is tracked to exclude external symbols from the build output. Information about external references is still included for the local pages that makes the external reference.
@@ -2655,6 +2659,12 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
26552659
return externallyResolvedAsset
26562660
}
26572661

2662+
if let externalAssetResolver = _externalAssetResolvers[bundleIdentifier],
2663+
let externallyResolvedAsset = externalAssetResolver._resolveExternalAsset(named: name, bundleIdentifier: bundleIdentifier) {
2664+
// Don't create a new DataAssetManager for the external bundle.
2665+
return externallyResolvedAsset
2666+
}
2667+
26582668
// If no fallbackAssetResolver is set, try to treat it as external media link
26592669
if let externalMediaLink = URL(string: name),
26602670
externalMediaLink.isAbsoluteWebURL {
@@ -2679,7 +2689,15 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
26792689
/// - Returns: The best matching storage key if it was found, otherwise `nil`.
26802690
public func identifier(forAssetName name: String, in parent: ResolvedTopicReference) -> String? {
26812691
let bundleIdentifier = parent.bundleIdentifier
2682-
return assetManagers[bundleIdentifier]?.bestKey(forAssetName: name)
2692+
if let assetManager = assetManagers[bundleIdentifier] {
2693+
return assetManager.bestKey(forAssetName: name)
2694+
} else {
2695+
if _externalAssetResolvers[bundleIdentifier]?._resolveExternalAsset(named: name, bundleIdentifier: parent.bundleIdentifier) != nil {
2696+
return name
2697+
} else {
2698+
return nil
2699+
}
2700+
}
26832701
}
26842702

26852703
/// Attempt to resolve an unresolved code listing.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2023 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+
13+
/// An asset resolver that can be used to resolve assets that couldn't be resolved locally.
14+
public protocol _ExternalAssetResolver {
15+
// This protocol only exist so that externally resolved media references can be returned separately from the external
16+
// content that is known to reference them. See details in OutOfProcessReferenceResolver.addImagesAndCacheMediaReferences(to:from:)
17+
// We should remove it when that's no longer necessary.
18+
// FIXME: https://github.com/apple/swift-docc/issues/468
19+
20+
/// Attempts to resolve an asset that couldn't be resolved externally given its name and the bundle it's apart of.
21+
func _resolveExternalAsset(named assetName: String, bundleIdentifier: String) -> DataAsset?
22+
}

Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift

Lines changed: 125 additions & 4 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-2022 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
@@ -172,7 +172,7 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR
172172
name = .conceptual(title: resolvedInformation.title)
173173
}
174174

175-
return DocumentationNode(
175+
var node = DocumentationNode(
176176
reference: reference,
177177
kind: resolvedInformation.kind,
178178
sourceLanguage: resolvedInformation.language,
@@ -182,6 +182,10 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR
182182
semantic: maybeSymbol,
183183
platformNames: resolvedInformation.platformNames
184184
)
185+
186+
addImagesAndCacheMediaReferences(to: &node, from: resolvedInformation)
187+
188+
return node
185189
}
186190

187191
/// Returns the web URL for the external topic.
@@ -247,7 +251,7 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR
247251
variants: resolvedInformation.variants
248252
)! // This entity was resolved from a symbol USR and is known to be a symbol.
249253

250-
return DocumentationNode(
254+
var node = DocumentationNode(
251255
reference: reference,
252256
kind: resolvedInformation.kind,
253257
sourceLanguage: resolvedInformation.language,
@@ -257,6 +261,49 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR
257261
semantic: symbol,
258262
platformNames: resolvedInformation.platformNames
259263
)
264+
265+
addImagesAndCacheMediaReferences(to: &node, from: resolvedInformation)
266+
267+
return node
268+
}
269+
270+
private func addImagesAndCacheMediaReferences(to node: inout DocumentationNode, from resolvedInformation: ResolvedInformation) {
271+
// Because DocC renders external content in the local context, external media also needs to be added to the local context.
272+
// If the external content was treated as pre-rendered then this wouldn't be necessary. (rdar://78718811)
273+
// FIXME: https://github.com/apple/swift-docc/issues/468
274+
275+
// `@PageImage` directives isn't meant to be created in code like this but it's the only way to associate topic images
276+
// with a render node.
277+
278+
if let topicImages = resolvedInformation.topicImages, !topicImages.isEmpty {
279+
let metadata = node.metadata ?? Metadata(originalMarkup: BlockDirective(name: "Metadata", children: []), documentationExtension: nil, technologyRoot: nil, displayName: nil)
280+
281+
metadata.pageImages = topicImages.map { topicImage in
282+
let purpose: PageImage.Purpose
283+
switch topicImage.type {
284+
case .card: purpose = .card
285+
case .icon: purpose = .icon
286+
}
287+
288+
return PageImage._make(
289+
purpose: purpose,
290+
source: ResourceReference(bundleIdentifier: node.reference.bundleIdentifier, path: topicImage.identifier.identifier),
291+
alt: (resolvedInformation.references?.first(where: { $0.identifier == topicImage.identifier }) as? ImageReference)?.altText
292+
)
293+
}
294+
295+
node.metadata = metadata
296+
}
297+
298+
// Since the DocumentationNode doesn't have any media references, the external media references can't be returned with the node.
299+
// Instead they are added to the cache so that they are returned from later requests.
300+
301+
for reference in (resolvedInformation.references ?? []) {
302+
guard let mediaReference = reference as? MediaReference else { continue }
303+
304+
assetCache[.init(assetName: mediaReference.identifier.identifier, bundleIdentifier: node.reference.bundleIdentifier)] = mediaReference.asset
305+
}
306+
260307
}
261308

262309
/// Returns the web URL for the external symbol.
@@ -400,6 +447,14 @@ public class OutOfProcessReferenceResolver: ExternalReferenceResolver, FallbackR
400447
}
401448
}
402449

450+
extension OutOfProcessReferenceResolver: _ExternalAssetResolver {
451+
public func _resolveExternalAsset(named assetName: String, bundleIdentifier: String) -> DataAsset? {
452+
// We don't want to make additional requests for these external assets.
453+
// If they were already resolved, return them from the cache.
454+
return assetCache[AssetReference(assetName: assetName, bundleIdentifier: bundleIdentifier)]
455+
}
456+
}
457+
403458
private protocol ExternalLinkResolving {
404459
func sendAndWait<Request: Codable & CustomStringConvertible, Response: Codable>(request: Request?) throws -> Response
405460
}
@@ -689,6 +744,10 @@ extension OutOfProcessReferenceResolver {
689744

690745
/// A type used to transfer information about a resolved reference to DocC from from a reference resolver in another executable.
691746
public struct ResolvedInformation: Codable {
747+
// This type is duplicating the information from LinkDestinationSummary with some minor differences.
748+
// Changes generally need to be made in both places. It would be good to replace this with LinkDestinationSummary.
749+
// FIXME: https://github.com/apple/swift-docc/issues/468
750+
692751
/// Information about the resolved kind.
693752
public let kind: DocumentationNode.Kind
694753
/// Information about the resolved URL.
@@ -719,8 +778,14 @@ extension OutOfProcessReferenceResolver {
719778
return platforms.map { platforms in Set(platforms.compactMap { $0.name }) }
720779
}
721780

781+
/// Images that are used to represent the summarized element.
782+
public var topicImages: [TopicImage]?
783+
784+
/// References used in the content of the summarized element.
785+
public var references: [RenderReference]?
786+
722787
/// The variants of content (kind, url, title, abstract, language, declaration) for this resolver information.
723-
public let variants: [Variant]?
788+
public var variants: [Variant]?
724789

725790
/// Creates a new resolved information value with all its values.
726791
///
@@ -743,6 +808,8 @@ extension OutOfProcessReferenceResolver {
743808
availableLanguages: Set<SourceLanguage>,
744809
platforms: [PlatformAvailability]?,
745810
declarationFragments: DeclarationFragments?,
811+
topicImages: [TopicImage]?,
812+
references: [RenderReference]?,
746813
variants: [Variant]? = nil
747814
) {
748815
self.kind = kind
@@ -753,6 +820,8 @@ extension OutOfProcessReferenceResolver {
753820
self.availableLanguages = availableLanguages
754821
self.platforms = platforms
755822
self.declarationFragments = declarationFragments
823+
self.topicImages = topicImages
824+
self.references = references
756825
self.variants = variants
757826
}
758827

@@ -889,3 +958,55 @@ extension OutOfProcessReferenceResolver {
889958
)
890959
}
891960
}
961+
962+
extension OutOfProcessReferenceResolver.ResolvedInformation {
963+
enum CodingKeys: CodingKey {
964+
case kind
965+
case url
966+
case title
967+
case abstract
968+
case language
969+
case availableLanguages
970+
case platforms
971+
case declarationFragments
972+
case topicImages
973+
case references
974+
case variants
975+
}
976+
977+
public init(from decoder: Decoder) throws {
978+
let container = try decoder.container(keyedBy: CodingKeys.self)
979+
980+
kind = try container.decode(DocumentationNode.Kind.self, forKey: .kind)
981+
url = try container.decode(URL.self, forKey: .url)
982+
title = try container.decode(String.self, forKey: .title)
983+
abstract = try container.decode(String.self, forKey: .abstract)
984+
language = try container.decode(SourceLanguage.self, forKey: .language)
985+
availableLanguages = try container.decode(Set<SourceLanguage>.self, forKey: .availableLanguages)
986+
platforms = try container.decodeIfPresent([OutOfProcessReferenceResolver.ResolvedInformation.PlatformAvailability].self, forKey: .platforms)
987+
declarationFragments = try container.decodeIfPresent(OutOfProcessReferenceResolver.ResolvedInformation.DeclarationFragments.self, forKey: .declarationFragments)
988+
topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages)
989+
references = try container.decodeIfPresent([CodableRenderReference].self, forKey: .references).map { decodedReferences in
990+
decodedReferences.map(\.reference)
991+
}
992+
variants = try container.decodeIfPresent([OutOfProcessReferenceResolver.ResolvedInformation.Variant].self, forKey: .variants)
993+
994+
}
995+
996+
public func encode(to encoder: Encoder) throws {
997+
var container = encoder.container(keyedBy: CodingKeys.self)
998+
999+
try container.encode(self.kind, forKey: .kind)
1000+
try container.encode(self.url, forKey: .url)
1001+
try container.encode(self.title, forKey: .title)
1002+
try container.encode(self.abstract, forKey: .abstract)
1003+
try container.encode(self.language, forKey: .language)
1004+
try container.encode(self.availableLanguages, forKey: .availableLanguages)
1005+
try container.encodeIfPresent(self.platforms, forKey: .platforms)
1006+
try container.encodeIfPresent(self.declarationFragments, forKey: .declarationFragments)
1007+
try container.encodeIfPresent(self.topicImages, forKey: .topicImages)
1008+
try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references)
1009+
try container.encodeIfPresent(self.variants, forKey: .variants)
1010+
}
1011+
}
1012+

0 commit comments

Comments
 (0)