Skip to content

Commit cbca9dc

Browse files
committed
Fix language availability for article topics
For multi-language articles, only displays the APIs available in the language of the currently-selected language. For example, this fix makes DocC omit a Swift-only API from the Objective-C variant of an article that curates it. rdar://90159299
1 parent 315f1e2 commit cbca9dc

File tree

9 files changed

+301
-53
lines changed

9 files changed

+301
-53
lines changed

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -622,54 +622,75 @@ public struct RenderNodeTranslator: SemanticVisitor {
622622
node.primaryContentSections.append(ContentRenderSection(kind: .content, content: discussionContent, heading: title))
623623
}
624624

625-
if let topics = article.topics, !topics.taskGroups.isEmpty {
626-
// Don't set an eyebrow as collections and groups don't have one; append the authored Topics section
627-
node.topicSections.append(
628-
contentsOf: renderGroups(
629-
topics,
630-
allowExternalLinks: false,
631-
allowedTraits: documentationNode.availableVariantTraits,
632-
contentCompiler: &contentCompiler
625+
node.topicSectionsVariants = VariantCollection<[TaskGroupRenderSection]>(
626+
from: documentationNode.availableVariantTraits,
627+
fallbackDefaultValue: []
628+
) { trait in
629+
var sections = [TaskGroupRenderSection]()
630+
631+
if let topics = article.topics, !topics.taskGroups.isEmpty {
632+
// Don't set an eyebrow as collections and groups don't have one; append the authored Topics section
633+
sections.append(
634+
contentsOf: renderGroups(
635+
topics,
636+
allowExternalLinks: false,
637+
allowedTraits: [trait],
638+
contentCompiler: &contentCompiler
639+
)
633640
)
634-
)
635-
}
636-
637-
// Place "top" rendering preference automatic task groups
638-
// after any user-defined task groups but before automatic curation.
639-
if !article.automaticTaskGroups.isEmpty {
640-
node.topicSections.append(contentsOf: renderAutomaticTaskGroupsSection(article.automaticTaskGroups.filter({ $0.renderPositionPreference == .top }), contentCompiler: &contentCompiler))
641-
}
642-
643-
// If there are no manually curated topics, and no automatic groups, try generating automatic groups by child kind.
644-
if (article.topics == nil || article.topics?.taskGroups.isEmpty == true) &&
645-
article.automaticTaskGroups.isEmpty {
646-
// If there are no authored child topics in docs or markdown,
647-
// inspect the topic graph, find this node's children, and
648-
// for the ones found curate them automatically in task groups.
649-
// Automatic groups are named after the child's kind, e.g.
650-
// "Methods", "Variables", etc.
651-
let alreadyCurated = Set(node.topicSections.flatMap { $0.identifiers })
652-
let groups = try! AutomaticCuration.topics(for: documentationNode, withTrait: nil, context: context)
653-
.compactMap({ group -> AutomaticCuration.TaskGroup? in
654-
// Remove references that have been already curated.
655-
let newReferences = group.references.filter { !alreadyCurated.contains($0.absoluteString) }
656-
// Remove groups that have no uncurated references
657-
guard !newReferences.isEmpty else { return nil }
658-
659-
return (title: group.title, references: newReferences)
660-
})
641+
}
661642

662-
// Collect all child topic references.
663-
contentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references))
664-
// Add the final groups to the node.
665-
node.topicSections.append(contentsOf: groups.map(TaskGroupRenderSection.init(taskGroup:)))
666-
}
643+
// Place "top" rendering preference automatic task groups
644+
// after any user-defined task groups but before automatic curation.
645+
if !article.automaticTaskGroups.isEmpty {
646+
sections.append(
647+
contentsOf: renderAutomaticTaskGroupsSection(
648+
article.automaticTaskGroups.filter { $0.renderPositionPreference == .top },
649+
contentCompiler: &contentCompiler
650+
)
651+
)
652+
}
653+
654+
// If there are no manually curated topics, and no automatic groups, try generating automatic groups by
655+
// child kind.
656+
if (article.topics == nil || article.topics?.taskGroups.isEmpty == true) &&
657+
article.automaticTaskGroups.isEmpty {
658+
// If there are no authored child topics in docs or markdown,
659+
// inspect the topic graph, find this node's children, and
660+
// for the ones found curate them automatically in task groups.
661+
// Automatic groups are named after the child's kind, e.g.
662+
// "Methods", "Variables", etc.
663+
let alreadyCurated = Set(node.topicSections.flatMap { $0.identifiers })
664+
let groups = try! AutomaticCuration.topics(for: documentationNode, withTrait: nil, context: context)
665+
.compactMap({ group -> AutomaticCuration.TaskGroup? in
666+
// Remove references that have been already curated.
667+
let newReferences = group.references.filter { !alreadyCurated.contains($0.absoluteString) }
668+
// Remove groups that have no uncurated references
669+
guard !newReferences.isEmpty else { return nil }
670+
671+
return (title: group.title, references: newReferences)
672+
})
673+
674+
// Collect all child topic references.
675+
contentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references))
676+
// Add the final groups to the node.
677+
sections.append(contentsOf: groups.map(TaskGroupRenderSection.init(taskGroup:)))
678+
}
679+
680+
// Place "bottom" rendering preference automatic task groups
681+
// after any user-defined task groups but before automatic curation.
682+
if !article.automaticTaskGroups.isEmpty {
683+
sections.append(
684+
contentsOf: renderAutomaticTaskGroupsSection(
685+
article.automaticTaskGroups.filter { $0.renderPositionPreference == .bottom },
686+
contentCompiler: &contentCompiler
687+
)
688+
)
689+
}
690+
691+
return sections
692+
} ?? .init(defaultValue: [])
667693

668-
// Place "bottom" rendering preference automatic task groups
669-
// after any user-defined task groups but before automatic curation.
670-
if !article.automaticTaskGroups.isEmpty {
671-
node.topicSections.append(contentsOf: renderAutomaticTaskGroupsSection(article.automaticTaskGroups.filter({ $0.renderPositionPreference == .bottom }), contentCompiler: &contentCompiler))
672-
}
673694

674695
if node.topicSections.isEmpty {
675696
// Set an eyebrow for articles

Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Coding.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,19 @@ extension KeyedEncodingContainer {
3939
)
4040
}
4141

42-
/// Encodes the given variant collection.
42+
/// Encodes the given variant collection for its non-empty values.
4343
mutating func encodeVariantCollectionIfNotEmpty<Value>(
4444
_ variantCollection: VariantCollection<Value>,
4545
forKey key: Key,
4646
encoder: Encoder
4747
) throws where Value: Collection {
4848
try encodeIfNotEmpty(variantCollection.defaultValue, forKey: key)
4949

50-
variantCollection.addVariantsToEncoder(
50+
variantCollection.mapValues { value in
51+
// Encode `nil` if the value is empty, so that when the patch is applied, it effectively
52+
// removes the default value.
53+
value.isEmpty ? nil : value
54+
}.addVariantsToEncoder(
5155
encoder,
5256

5357
// Add the key to the encoder's coding path, since the coding path refers to the value's parent.

Sources/SwiftDocC/Semantics/Article/Article.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
7575
private(set) public var abstractSection: AbstractSection?
7676

7777
/// The Topic curation section of the article.
78-
private(set) public var topics: TopicsSection?
78+
internal(set) public var topics: TopicsSection?
7979

8080
/// The See Also section of the article.
8181
private(set) public var seeAlso: SeeAlsoSection?

Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ final class RenderIndexTests: XCTestCase {
121121
"path": "/documentation/mixedlanguageframework/article",
122122
"type": "article"
123123
},
124+
{
125+
"title": "APICollection",
126+
"path": "/documentation/mixedlanguageframework/apicollection",
127+
"type": "symbol",
128+
"children": [
129+
{
130+
"title": "Objective-C–only APIs",
131+
"type": "groupMarker"
132+
},
133+
{
134+
"title": "_MixedLanguageFrameworkVersionNumber",
135+
"path": "/documentation/mixedlanguageframework/_mixedlanguageframeworkversionnumber",
136+
"type": "var"
137+
}
138+
]
139+
},
124140
{
125141
"title": "Classes",
126142
"type": "groupMarker"
@@ -262,6 +278,33 @@ final class RenderIndexTests: XCTestCase {
262278
"path": "/documentation/mixedlanguageframework/article",
263279
"type": "article"
264280
},
281+
{
282+
"path": "/documentation/mixedlanguageframework/apicollection",
283+
"title": "APICollection",
284+
"type": "symbol",
285+
"children": [
286+
{
287+
"title": "Swift-only APIs",
288+
"type": "groupMarker"
289+
},
290+
{
291+
"path": "/documentation/mixedlanguageframework/swiftonlystruct",
292+
"title": "SwiftOnlyStruct",
293+
"type": "struct",
294+
"children": [
295+
{
296+
"title": "Instance Methods",
297+
"type": "groupMarker"
298+
},
299+
{
300+
"title": "func tada()",
301+
"path": "/documentation/mixedlanguageframework/swiftonlystruct/tada()",
302+
"type": "method"
303+
}
304+
]
305+
}
306+
]
307+
},
265308
{
266309
"title": "Classes",
267310
"type": "groupMarker"

Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,9 @@ class AutomaticCurationTests: XCTestCase {
379379

380380
"Structures",
381381
"/documentation/MixedLanguageFramework/Foo-swift.struct",
382-
"/documentation/MixedLanguageFramework/SwiftOnlyStruct",
382+
383+
// SwiftOnlyStruct is manually curated.
384+
// "/documentation/MixedLanguageFramework/SwiftOnlyStruct",
383385
]
384386
)
385387

@@ -398,7 +400,10 @@ class AutomaticCurationTests: XCTestCase {
398400
"/documentation/MixedLanguageFramework/Bar",
399401

400402
"Variables",
401-
"/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber",
403+
404+
// _MixedLanguageFrameworkVersionNumber is manually curated.
405+
// "/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber",
406+
402407
"/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionString",
403408

404409
// 'MixedLanguageFramework/Foo-occ.typealias' is manually curated in a task group titled "Custom" under 'MixedLanguageFramework/Bar/myStringFunction:error:'

Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase {
125125
"c:objc(cs)Bar",
126126
"c:objc(cs)Bar(cm)myStringFunction:error:",
127127
"Article",
128+
"APICollection",
128129
"MixedLanguageFramework Tutorials",
129130
"Tutorial Article",
130131
"Tutorial",
@@ -153,10 +154,12 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase {
153154
"doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/TutorialArticle",
154155
"doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/Tutorial",
155156
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Article",
157+
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/APICollection",
156158
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Bar",
157159
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Foo-swift.struct",
158160
],
159161
referenceTitles: [
162+
"APICollection",
160163
"Article",
161164
"Bar",
162165
"Foo",
@@ -197,11 +200,13 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase {
197200
"doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/TutorialArticle",
198201
"doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/Tutorial",
199202
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Article",
203+
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/APICollection",
200204
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Bar",
201205
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionString",
202206
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Foo-swift.struct",
203207
],
204208
referenceTitles: [
209+
"APICollection",
205210
"Article",
206211
"Bar",
207212
"Foo",
@@ -440,7 +445,67 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase {
440445
"typedef enum Foo : NSString {\n ...\n} Foo;",
441446
],
442447
failureMessage: { fieldName in
443-
"Swift variant of 'MyArticle' article has unexpected content for '\(fieldName)'."
448+
"Objective-C variant of 'MyArticle' article has unexpected content for '\(fieldName)'."
449+
}
450+
)
451+
}
452+
453+
func testAPICollectionInMixedLanguageFramework() throws {
454+
enableFeatureFlag(\.isExperimentalObjectiveCSupportEnabled)
455+
456+
let outputConsumer = try mixedLanguageFrameworkConsumer()
457+
458+
let articleRenderNode = try outputConsumer.renderNode(withTitle: "APICollection")
459+
460+
assertExpectedContent(
461+
articleRenderNode,
462+
sourceLanguage: "swift",
463+
title: "APICollection",
464+
navigatorTitle: nil,
465+
abstract: "This is an API collection.",
466+
declarationTokens: nil,
467+
discussionSection: nil,
468+
topicSectionIdentifiers: [
469+
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct"
470+
],
471+
referenceTitles: [
472+
"Article",
473+
"MixedLanguageFramework",
474+
"SwiftOnlyStruct",
475+
"_MixedLanguageFrameworkVersionNumber",
476+
],
477+
referenceFragments: [
478+
"struct SwiftOnlyStruct",
479+
],
480+
failureMessage: { fieldName in
481+
"Swift variant of 'APICollection' article has unexpected content for '\(fieldName)'."
482+
}
483+
)
484+
485+
let objectiveCVariantNode = try renderNodeApplyingObjectiveCVariantOverrides(to: articleRenderNode)
486+
487+
assertExpectedContent(
488+
objectiveCVariantNode,
489+
sourceLanguage: "occ",
490+
title: "APICollection",
491+
navigatorTitle: nil,
492+
abstract: "This is an API collection.",
493+
declarationTokens: nil,
494+
discussionSection: nil,
495+
topicSectionIdentifiers: [
496+
"doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber"
497+
],
498+
referenceTitles: [
499+
"Article",
500+
"MixedLanguageFramework",
501+
"SwiftOnlyStruct",
502+
"_MixedLanguageFrameworkVersionNumber",
503+
],
504+
referenceFragments: [
505+
"struct SwiftOnlyStruct",
506+
],
507+
failureMessage: { fieldName in
508+
"Objective-C variant of 'MyArticle' article has unexpected content for '\(fieldName)'."
444509
}
445510
)
446511
}

0 commit comments

Comments
 (0)