Skip to content

Commit 3015b85

Browse files
sofiaromoralesfranklinschd-ronnqvist
authored andcommitted
Include manually curated non-symbol nodes in the Topics section regardless of the source language of the symbol (#757)
If the node to which the reference belongs is a non symbol node, render the topic in the topic section independently of the source language of the curation. By default, Articles and other non-symbol nodes have the source language set to 'Swift.' Prior to this fix, when curating an Article within a symbol with ObjC availability, the curated article was being omitted from the Topics section. With this fix, we check whether it's an article and maintain the curation, regardless of whether we're linking to a node with a different source language. rdar://118461894 * Added multiple tests to validate that the correct filter behaviour happens when linking to external articles with the link resolver. * Refactored tests to validate filtering functionality for all available language variants. --------- Co-authored-by: Franklin Schrans <[email protected]> Co-authored-by: David Rönnqvist <[email protected]>
1 parent f265a9f commit 3015b85

File tree

4 files changed

+208
-7
lines changed

4 files changed

+208
-7
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2523,22 +2523,41 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
25232523
throw ContextError.notFound(reference.url)
25242524
}
25252525

2526-
/// Returns the set of languages the entity corresponding to the given reference is available in.
2527-
///
2528-
/// - Precondition: The entity associated with the given reference must be registered in the context.
2529-
public func sourceLanguages(for reference: ResolvedTopicReference) -> Set<SourceLanguage> {
2526+
private func knownEntityValue<Result>(
2527+
reference: ResolvedTopicReference,
2528+
valueInLocalEntity: (DocumentationNode) -> Result
2529+
) -> Result {
25302530
do {
25312531
// Look up the entity without its fragment. The documentation context does not keep track of page sections
25322532
// as nodes, and page sections are considered to be available in the same languages as the page they're
25332533
// defined in.
25342534
let referenceWithoutFragment = reference.withFragment(nil)
2535-
return try entity(with: referenceWithoutFragment).availableSourceLanguages
2535+
return try valueInLocalEntity(entity(with: referenceWithoutFragment))
25362536
} catch ContextError.notFound {
25372537
preconditionFailure("Reference does not have an associated documentation node.")
25382538
} catch {
2539-
fatalError("Unexpected error when retrieving source languages: \(error)")
2539+
fatalError("Unexpected error when retrieving entity: \(error)")
25402540
}
25412541
}
2542+
2543+
/// Returns the set of languages the entity corresponding to the given reference is available in.
2544+
///
2545+
/// - Precondition: The entity associated with the given reference must be registered in the context.
2546+
public func sourceLanguages(for reference: ResolvedTopicReference) -> Set<SourceLanguage> {
2547+
knownEntityValue(
2548+
reference: reference,
2549+
valueInLocalEntity: \.availableSourceLanguages
2550+
// valueInExternalEntity: \.sourceLanguages
2551+
)
2552+
}
2553+
2554+
/// Returns whether the given reference corresponds to a symbol.
2555+
func isSymbol(reference: ResolvedTopicReference) -> Bool {
2556+
knownEntityValue(
2557+
reference: reference,
2558+
valueInLocalEntity: { node in node.kind.isSymbol }
2559+
)
2560+
}
25422561

25432562
// MARK: - Relationship queries
25442563

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,13 @@ public struct RenderNodeTranslator: SemanticVisitor {
10401040
return true
10411041
}
10421042

1043+
guard context.isSymbol(reference: reference) else {
1044+
// If the reference corresponds to any kind except Symbol
1045+
// (e.g., Article, Tutorial, SampleCode...), allow the topic
1046+
// to appear independently of the source language it belongs to.
1047+
return true
1048+
}
1049+
10431050
let referenceSourceLanguageIDs = Set(context.sourceLanguages(for: reference).map(\.id))
10441051

10451052
let availableSourceLanguageTraits = Set(availableTraits.compactMap(\.interfaceLanguage))
@@ -1469,7 +1476,7 @@ public struct RenderNodeTranslator: SemanticVisitor {
14691476

14701477
var sections = [TaskGroupRenderSection]()
14711478
if let topics = topics, !topics.taskGroups.isEmpty {
1472-
// Allowed traits should be all traits except the reverse of the objc/swift pairing
1479+
// Allowed symbol traits should be all traits except the reverse of the objc/swift pairing
14731480
sections.append(
14741481
contentsOf: renderGroups(
14751482
topics,

Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,4 +912,97 @@ Document @1:1-1:35
912912
let finalURL = urlGenerator.presentationURLForReference(linkReference)
913913
XCTAssertEqual(finalURL.absoluteString, "https://example.com/example/externally/resolved/path#67890")
914914
}
915+
916+
func testExternalArticlesAreIncludedInAllVariantsTopicsSection() throws {
917+
let externalResolver = TestMultiResultExternalReferenceResolver()
918+
externalResolver.bundleIdentifier = "com.test.external"
919+
920+
externalResolver.entitiesToReturn["/path/to/external/swiftArticle"] = .success(
921+
.init(
922+
referencePath: "/path/to/external/swiftArticle",
923+
title: "SwiftArticle",
924+
kind: .article,
925+
language: .swift
926+
)
927+
)
928+
929+
externalResolver.entitiesToReturn["/path/to/external/objCArticle"] = .success(
930+
.init(
931+
referencePath: "/path/to/external/objCArticle",
932+
title: "ObjCArticle",
933+
kind: .article,
934+
language: .objectiveC
935+
)
936+
)
937+
938+
externalResolver.entitiesToReturn["/path/to/external/swiftSymbol"] = .success(
939+
.init(
940+
referencePath: "/path/to/external/swiftSymbol",
941+
title: "SwiftSymbol",
942+
kind: .class,
943+
language: .swift
944+
)
945+
)
946+
947+
externalResolver.entitiesToReturn["/path/to/external/objCSymbol"] = .success(
948+
.init(
949+
referencePath: "/path/to/external/objCSymbol",
950+
title: "ObjCSymbol",
951+
kind: .class,
952+
language: .objectiveC
953+
)
954+
)
955+
956+
let (_, bundle, context) = try testBundleAndContext(
957+
copying: "MixedLanguageFramework",
958+
externalResolvers: [externalResolver.bundleIdentifier: externalResolver]
959+
) { url in
960+
let mixedLanguageFrameworkExtension = """
961+
# ``MixedLanguageFramework``
962+
963+
This symbol has a Swift and Objective-C variant.
964+
965+
## Topics
966+
967+
### External Reference
968+
969+
- <doc://com.test.external/path/to/external/swiftArticle>
970+
- <doc://com.test.external/path/to/external/swiftSymbol>
971+
- <doc://com.test.external/path/to/external/objCArticle>
972+
- <doc://com.test.external/path/to/external/objCSymbol>
973+
"""
974+
try mixedLanguageFrameworkExtension.write(to: url.appendingPathComponent("/MixedLanguageFramework.md"), atomically: true, encoding: .utf8)
975+
}
976+
let converter = DocumentationNodeConverter(bundle: bundle, context: context)
977+
let mixedLanguageFrameworkReference = ResolvedTopicReference(
978+
bundleIdentifier: bundle.identifier,
979+
path: "/documentation/MixedLanguageFramework",
980+
sourceLanguage: .swift
981+
)
982+
let node = try context.entity(with: mixedLanguageFrameworkReference)
983+
let fileURL = try XCTUnwrap(context.documentURL(for: node.reference))
984+
let renderNode = try converter.convert(node, at: fileURL)
985+
// Topic identifiers in the Swift variant of the `MixedLanguageFramework` symbol
986+
let swiftTopicIDs = renderNode.topicSections.flatMap(\.identifiers)
987+
988+
let data = try renderNode.encodeToJSON()
989+
let variantRenderNode = try RenderNodeVariantOverridesApplier()
990+
.applyVariantOverrides(in: data, for: [.interfaceLanguage("occ")])
991+
let objCRenderNode = try RenderJSONDecoder.makeDecoder().decode(RenderNode.self, from: variantRenderNode)
992+
// Topic identifiers in the ObjC variant of the `MixedLanguageFramework` symbol
993+
let objCTopicIDs = objCRenderNode.topicSections.flatMap(\.identifiers)
994+
995+
// Verify that external articles are included in the Topics section of both symbol
996+
// variants regardless of their perceived language.
997+
XCTAssertTrue(swiftTopicIDs.contains("doc://com.test.external/path/to/external/swiftArticle"))
998+
XCTAssertTrue(swiftTopicIDs.contains("doc://com.test.external/path/to/external/objCArticle"))
999+
XCTAssertTrue(objCTopicIDs.contains("doc://com.test.external/path/to/external/swiftArticle"))
1000+
XCTAssertTrue(objCTopicIDs.contains("doc://com.test.external/path/to/external/objCArticle"))
1001+
// Verify that external language specific symbols are dropped from the Topics section in the
1002+
// variants for languages where the symbol isn't available.
1003+
XCTAssertFalse(swiftTopicIDs.contains("doc://com.test.external/path/to/external/objCSymbol"))
1004+
XCTAssertTrue(swiftTopicIDs.contains("doc://com.test.external/path/to/external/swiftSymbol"))
1005+
XCTAssertTrue(objCTopicIDs.contains("doc://com.test.external/path/to/external/objCSymbol"))
1006+
XCTAssertFalse(objCTopicIDs.contains("doc://com.test.external/path/to/external/swiftSymbol"))
1007+
}
9151008
}

Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,88 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase {
888888
)
889889
}
890890

891+
func testArticlesAreIncludedInAllVariantsTopicsSection() throws {
892+
let outputConsumer = try renderNodeConsumer(
893+
for: "MixedLanguageFramework",
894+
configureBundle: { bundleURL in
895+
try """
896+
# ObjCArticle
897+
898+
@Metadata {
899+
@SupportedLanguage(objc)
900+
}
901+
902+
This article has Objective-C as the source language.
903+
904+
## Topics
905+
""".write(to: bundleURL.appendingPathComponent("ObjCArticle.md"), atomically: true, encoding: .utf8)
906+
try """
907+
# SwiftArticle
908+
909+
@Metadata {
910+
@SupportedLanguage(swift)
911+
}
912+
913+
This article has Swift as the source language.
914+
""".write(to: bundleURL.appendingPathComponent("SwiftArticle.md"), atomically: true, encoding: .utf8)
915+
try """
916+
# ``MixedLanguageFramework``
917+
918+
This symbol has a Swift and Objective-C variant.
919+
920+
## Topics
921+
922+
- <doc:ObjCArticle>
923+
- <doc:SwiftArticle>
924+
- ``_MixedLanguageFrameworkVersionNumber``
925+
- ``SwiftOnlyStruct``
926+
927+
""".write(to: bundleURL.appendingPathComponent("MixedLanguageFramework.md"), atomically: true, encoding: .utf8)
928+
}
929+
)
930+
assertIsAvailableInLanguages(
931+
try outputConsumer.renderNode(
932+
withTitle: "ObjCArticle"
933+
),
934+
languages: ["occ"],
935+
defaultLanguage: .objectiveC
936+
)
937+
assertIsAvailableInLanguages(
938+
try outputConsumer.renderNode(
939+
withTitle: "_MixedLanguageFrameworkVersionNumber"
940+
),
941+
languages: ["occ"],
942+
defaultLanguage: .objectiveC
943+
)
944+
945+
let renderNode = try outputConsumer.renderNode(withIdentifier: "MixedLanguageFramework")
946+
947+
// Topic identifiers in the Swift variant of the `MixedLanguageFramework` symbol
948+
let swiftTopicIDs = renderNode.topicSections.flatMap(\.identifiers)
949+
950+
let data = try renderNode.encodeToJSON()
951+
let variantRenderNode = try RenderNodeVariantOverridesApplier()
952+
.applyVariantOverrides(in: data, for: [.interfaceLanguage("occ")])
953+
let objCRenderNode = try RenderJSONDecoder.makeDecoder().decode(RenderNode.self, from: variantRenderNode)
954+
// Topic identifiers in the ObjC variant of the `MixedLanguageFramework` symbol
955+
let objCTopicIDs = objCRenderNode.topicSections.flatMap(\.identifiers)
956+
957+
958+
// Verify that articles are included in the Topics section of both symbol
959+
// variants regardless of their perceived language.
960+
XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/ObjCArticle"))
961+
XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftArticle"))
962+
XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftArticle"))
963+
XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/ObjCArticle"))
964+
965+
// Verify that language specific symbols are dropped from the Topics section in the
966+
// variants for languages where the symbol isn't available.
967+
XCTAssertTrue(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct"))
968+
XCTAssertFalse(swiftTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber"))
969+
XCTAssertTrue(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber"))
970+
XCTAssertFalse(objCTopicIDs.contains("doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct"))
971+
}
972+
891973
func renderNodeApplyingObjectiveCVariantOverrides(to renderNode: RenderNode) throws -> RenderNode {
892974
return try renderNodeApplying(variant: "occ", to: renderNode)
893975
}

0 commit comments

Comments
 (0)