From 523dd3512c9a7e031c7970d4f0c5b29d8acd00aa Mon Sep 17 00:00:00 2001 From: Franklin Schrans Date: Fri, 11 Mar 2022 17:28:01 +0000 Subject: [PATCH 1/3] Add map APIs to variant collection types Adds map APIs to variant collection types to facilitate transforming the value contained in variant collections. --- .../Variants/VariantCollection+Variant.swift | 42 ++++++++++++++++++ .../Variants/VariantCollection.swift | 31 +++++-------- .../Variants/VariantPatchOperation.swift | 21 +++++++++ .../VariantCollection+VariantTests.swift | 44 +++++++++++++++++++ .../Variants/VariantCollectionTests.swift | 19 ++++++++ .../Variants/VariantPatchOperationTests.swift | 25 +++++++++++ 6 files changed, 162 insertions(+), 20 deletions(-) create mode 100644 Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Variant.swift create mode 100644 Tests/SwiftDocCTests/Rendering/Variants/VariantCollection+VariantTests.swift diff --git a/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Variant.swift b/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Variant.swift new file mode 100644 index 0000000000..6f0d9b4586 --- /dev/null +++ b/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Variant.swift @@ -0,0 +1,42 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation + +public extension VariantCollection { + /// A variant for a render node value. + struct Variant { + /// The traits associated with the override. + public var traits: [RenderNode.Variant.Trait] + + /// The patch to apply as part of the override. + public var patch: [VariantPatchOperation] + + /// Creates an override value for the given traits. + /// + /// - Parameters: + /// - traits: The traits associated with this override value. + /// - patch: The patch to apply as part of the override. + public init(traits: [RenderNode.Variant.Trait], patch: [VariantPatchOperation]) { + self.traits = traits + self.patch = patch + } + + /// Returns a new variant collection containing the traits of this variant collection with the values transformed by the given closure. + public func mapPatch( + _ transform: (Value) -> TransformedValue + ) -> VariantCollection.Variant { + VariantCollection.Variant( + traits: traits, + patch: patch.map { patchOperation in patchOperation.map(transform) } + ) + } + } +} diff --git a/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection.swift b/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection.swift index ee8a06780e..7225da94ca 100644 --- a/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection.swift +++ b/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection.swift @@ -91,25 +91,16 @@ public struct VariantCollection: Codable { encoder.userInfoVariantOverrides?.add(contentsOf: overrides) } -} - -public extension VariantCollection { - /// A variant for a render node value. - struct Variant { - /// The traits associated with the override. - public var traits: [RenderNode.Variant.Trait] - - /// The patch to apply as part of the override. - public var patch: [VariantPatchOperation] - - /// Creates an override value for the given traits. - /// - /// - Parameters: - /// - traits: The traits associated with this override value. - /// - patch: The patch to apply as part of the override. - public init(traits: [RenderNode.Variant.Trait], patch: [VariantPatchOperation]) { - self.traits = traits - self.patch = patch - } + + /// Returns a variant collection containing the results of calling the given transformation with each value of this variant collection. + public func mapValues( + _ transform: (Value) -> TransformedValue + ) -> VariantCollection { + VariantCollection( + defaultValue: transform(defaultValue), + variants: variants.map { variant in + variant.mapPatch(transform) + } + ) } } diff --git a/Sources/SwiftDocC/Model/Rendering/Variants/VariantPatchOperation.swift b/Sources/SwiftDocC/Model/Rendering/Variants/VariantPatchOperation.swift index a9abc3217b..523ffd1c0c 100644 --- a/Sources/SwiftDocC/Model/Rendering/Variants/VariantPatchOperation.swift +++ b/Sources/SwiftDocC/Model/Rendering/Variants/VariantPatchOperation.swift @@ -17,6 +17,9 @@ public enum VariantPatchOperation { /// - Parameter value: The value to use in the replacement. case replace(value: Value) + /// An addition operation. + /// + /// - Parameter value: The value to use in the addition. case add(value: Value) /// A removal operation. @@ -33,6 +36,24 @@ public enum VariantPatchOperation { return .remove } } + + /// Returns a new patch operation with its value transformed using the given closure. + /// + /// If the patch operation doesn't have a value—for example, if it's a removal operation—the operation is returned unmodified. + func map( + _ transform: (Value) -> TransformedValue + ) -> VariantPatchOperation { + switch self { + case .replace(let value): + return VariantPatchOperation.replace(value: transform(value)) + + case .add(let value): + return VariantPatchOperation.add(value: transform(value)) + + case .remove: + return .remove + } + } } extension VariantCollection.Variant where Value: RangeReplaceableCollection { diff --git a/Tests/SwiftDocCTests/Rendering/Variants/VariantCollection+VariantTests.swift b/Tests/SwiftDocCTests/Rendering/Variants/VariantCollection+VariantTests.swift new file mode 100644 index 0000000000..1bd8e64bea --- /dev/null +++ b/Tests/SwiftDocCTests/Rendering/Variants/VariantCollection+VariantTests.swift @@ -0,0 +1,44 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2022 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Foundation +import XCTest +@testable import SwiftDocC + +class VariantCollection_VariantTests: XCTestCase { + let testVariant = VariantCollection.Variant( + traits: [.interfaceLanguage("a")], + patch: [ + .replace(value: "replace"), + .add(value: "add"), + .remove, + ] + ) + + func testMapPatch() throws { + XCTAssertEqual( + testVariant.mapPatch { "\($0) transformed" }.patch.map(\.value), + ["replace transformed", "add transformed", nil] + ) + } +} + +private extension VariantPatchOperation { + var value: Value? { + switch self { + case let .replace(value): + return value + case let .add(value): + return value + case .remove: + return nil + } + } +} diff --git a/Tests/SwiftDocCTests/Rendering/Variants/VariantCollectionTests.swift b/Tests/SwiftDocCTests/Rendering/Variants/VariantCollectionTests.swift index 25791a3067..f16aca8028 100644 --- a/Tests/SwiftDocCTests/Rendering/Variants/VariantCollectionTests.swift +++ b/Tests/SwiftDocCTests/Rendering/Variants/VariantCollectionTests.swift @@ -74,4 +74,23 @@ class VariantCollectionTests: XCTestCase { XCTAssertEqual(value.value as! String, expectedValue) } } + + func testMapValues() { + let testCollection = testCollection.mapValues { value -> String? in + if value == "default value" { + return "default value transformed" + } + + return nil + } + + XCTAssertEqual(testCollection.defaultValue, "default value transformed") + + guard case .replace(let value)? = testCollection.variants.first?.patch.first else { + XCTFail() + return + } + + XCTAssertNil(value) + } } diff --git a/Tests/SwiftDocCTests/Rendering/Variants/VariantPatchOperationTests.swift b/Tests/SwiftDocCTests/Rendering/Variants/VariantPatchOperationTests.swift index 8b5e250076..3fead2a843 100644 --- a/Tests/SwiftDocCTests/Rendering/Variants/VariantPatchOperationTests.swift +++ b/Tests/SwiftDocCTests/Rendering/Variants/VariantPatchOperationTests.swift @@ -66,4 +66,29 @@ class VariantPatchOperationTests: XCTestCase { XCTAssertEqual(stringVariant.applyingPatchTo("A"), expectedValue) } } + + func testMap() throws { + let transform: (String) -> String = { "\($0) transformed" } + let replace = VariantPatchOperation.replace(value: "replace") + guard case .replace(let value) = replace.map(transform) else { + XCTFail("Expected replace operation") + return + } + + XCTAssertEqual(value, "replace transformed") + + let add = VariantPatchOperation.add(value: "add") + guard case .add(let value) = add.map(transform) else { + XCTFail("Expected add operation") + return + } + + XCTAssertEqual(value, "add transformed") + + let remove = VariantPatchOperation.remove.map(transform) + guard case .remove = remove else { + XCTFail("Expected remove operation") + return + } + } } From d305946378ada4316e4daae0b4567013862c0721 Mon Sep 17 00:00:00 2001 From: Franklin Schrans Date: Fri, 11 Mar 2022 17:29:33 +0000 Subject: [PATCH 2/3] 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 --- .../Rendering/RenderNodeTranslator.swift | 111 +++++++++++------- .../Variants/VariantCollection+Coding.swift | 8 +- .../SwiftDocC/Semantics/Article/Article.swift | 2 +- .../Indexing/RenderIndexTests.swift | 43 +++++++ .../AutomaticCurationTests.swift | 9 +- .../SemaToRenderNodeMultiLanguageTests.swift | 67 ++++++++++- ...derNodeTranslatorSymbolVariantsTests.swift | 98 +++++++++++++++- .../APICollection.md | 15 +++ .../MixedLanguageFramework.md | 1 + 9 files changed, 301 insertions(+), 53 deletions(-) create mode 100644 Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 1c5232e34b..cd3be5a320 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -622,54 +622,75 @@ public struct RenderNodeTranslator: SemanticVisitor { node.primaryContentSections.append(ContentRenderSection(kind: .content, content: discussionContent, heading: title)) } - if let topics = article.topics, !topics.taskGroups.isEmpty { - // Don't set an eyebrow as collections and groups don't have one; append the authored Topics section - node.topicSections.append( - contentsOf: renderGroups( - topics, - allowExternalLinks: false, - allowedTraits: documentationNode.availableVariantTraits, - contentCompiler: &contentCompiler + node.topicSectionsVariants = VariantCollection<[TaskGroupRenderSection]>( + from: documentationNode.availableVariantTraits, + fallbackDefaultValue: [] + ) { trait in + var sections = [TaskGroupRenderSection]() + + if let topics = article.topics, !topics.taskGroups.isEmpty { + // Don't set an eyebrow as collections and groups don't have one; append the authored Topics section + sections.append( + contentsOf: renderGroups( + topics, + allowExternalLinks: false, + allowedTraits: [trait], + contentCompiler: &contentCompiler + ) ) - ) - } - - // Place "top" rendering preference automatic task groups - // after any user-defined task groups but before automatic curation. - if !article.automaticTaskGroups.isEmpty { - node.topicSections.append(contentsOf: renderAutomaticTaskGroupsSection(article.automaticTaskGroups.filter({ $0.renderPositionPreference == .top }), contentCompiler: &contentCompiler)) - } - - // If there are no manually curated topics, and no automatic groups, try generating automatic groups by child kind. - if (article.topics == nil || article.topics?.taskGroups.isEmpty == true) && - article.automaticTaskGroups.isEmpty { - // If there are no authored child topics in docs or markdown, - // inspect the topic graph, find this node's children, and - // for the ones found curate them automatically in task groups. - // Automatic groups are named after the child's kind, e.g. - // "Methods", "Variables", etc. - let alreadyCurated = Set(node.topicSections.flatMap { $0.identifiers }) - let groups = try! AutomaticCuration.topics(for: documentationNode, withTrait: nil, context: context) - .compactMap({ group -> AutomaticCuration.TaskGroup? in - // Remove references that have been already curated. - let newReferences = group.references.filter { !alreadyCurated.contains($0.absoluteString) } - // Remove groups that have no uncurated references - guard !newReferences.isEmpty else { return nil } - - return (title: group.title, references: newReferences) - }) + } - // Collect all child topic references. - contentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references)) - // Add the final groups to the node. - node.topicSections.append(contentsOf: groups.map(TaskGroupRenderSection.init(taskGroup:))) - } + // Place "top" rendering preference automatic task groups + // after any user-defined task groups but before automatic curation. + if !article.automaticTaskGroups.isEmpty { + sections.append( + contentsOf: renderAutomaticTaskGroupsSection( + article.automaticTaskGroups.filter { $0.renderPositionPreference == .top }, + contentCompiler: &contentCompiler + ) + ) + } + + // If there are no manually curated topics, and no automatic groups, try generating automatic groups by + // child kind. + if (article.topics == nil || article.topics?.taskGroups.isEmpty == true) && + article.automaticTaskGroups.isEmpty { + // If there are no authored child topics in docs or markdown, + // inspect the topic graph, find this node's children, and + // for the ones found curate them automatically in task groups. + // Automatic groups are named after the child's kind, e.g. + // "Methods", "Variables", etc. + let alreadyCurated = Set(node.topicSections.flatMap { $0.identifiers }) + let groups = try! AutomaticCuration.topics(for: documentationNode, withTrait: nil, context: context) + .compactMap({ group -> AutomaticCuration.TaskGroup? in + // Remove references that have been already curated. + let newReferences = group.references.filter { !alreadyCurated.contains($0.absoluteString) } + // Remove groups that have no uncurated references + guard !newReferences.isEmpty else { return nil } + + return (title: group.title, references: newReferences) + }) + + // Collect all child topic references. + contentCompiler.collectedTopicReferences.append(contentsOf: groups.flatMap(\.references)) + // Add the final groups to the node. + sections.append(contentsOf: groups.map(TaskGroupRenderSection.init(taskGroup:))) + } + + // Place "bottom" rendering preference automatic task groups + // after any user-defined task groups but before automatic curation. + if !article.automaticTaskGroups.isEmpty { + sections.append( + contentsOf: renderAutomaticTaskGroupsSection( + article.automaticTaskGroups.filter { $0.renderPositionPreference == .bottom }, + contentCompiler: &contentCompiler + ) + ) + } + + return sections + } ?? .init(defaultValue: []) - // Place "bottom" rendering preference automatic task groups - // after any user-defined task groups but before automatic curation. - if !article.automaticTaskGroups.isEmpty { - node.topicSections.append(contentsOf: renderAutomaticTaskGroupsSection(article.automaticTaskGroups.filter({ $0.renderPositionPreference == .bottom }), contentCompiler: &contentCompiler)) - } if node.topicSections.isEmpty { // Set an eyebrow for articles diff --git a/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Coding.swift b/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Coding.swift index d465f2c54b..b1c54f3e9d 100644 --- a/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Coding.swift +++ b/Sources/SwiftDocC/Model/Rendering/Variants/VariantCollection+Coding.swift @@ -39,7 +39,7 @@ extension KeyedEncodingContainer { ) } - /// Encodes the given variant collection. + /// Encodes the given variant collection for its non-empty values. mutating func encodeVariantCollectionIfNotEmpty( _ variantCollection: VariantCollection, forKey key: Key, @@ -47,7 +47,11 @@ extension KeyedEncodingContainer { ) throws where Value: Collection { try encodeIfNotEmpty(variantCollection.defaultValue, forKey: key) - variantCollection.addVariantsToEncoder( + variantCollection.mapValues { value in + // Encode `nil` if the value is empty, so that when the patch is applied, it effectively + // removes the default value. + value.isEmpty ? nil : value + }.addVariantsToEncoder( encoder, // Add the key to the encoder's coding path, since the coding path refers to the value's parent. diff --git a/Sources/SwiftDocC/Semantics/Article/Article.swift b/Sources/SwiftDocC/Semantics/Article/Article.swift index 49c7851108..c82effb140 100644 --- a/Sources/SwiftDocC/Semantics/Article/Article.swift +++ b/Sources/SwiftDocC/Semantics/Article/Article.swift @@ -75,7 +75,7 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected, private(set) public var abstractSection: AbstractSection? /// The Topic curation section of the article. - private(set) public var topics: TopicsSection? + internal(set) public var topics: TopicsSection? /// The See Also section of the article. private(set) public var seeAlso: SeeAlsoSection? diff --git a/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift b/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift index 4c090ee2cd..692c15409e 100644 --- a/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift +++ b/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift @@ -121,6 +121,22 @@ final class RenderIndexTests: XCTestCase { "path": "/documentation/mixedlanguageframework/article", "type": "article" }, + { + "title": "APICollection", + "path": "/documentation/mixedlanguageframework/apicollection", + "type": "symbol", + "children": [ + { + "title": "Objective-C–only APIs", + "type": "groupMarker" + }, + { + "title": "_MixedLanguageFrameworkVersionNumber", + "path": "/documentation/mixedlanguageframework/_mixedlanguageframeworkversionnumber", + "type": "var" + } + ] + }, { "title": "Classes", "type": "groupMarker" @@ -262,6 +278,33 @@ final class RenderIndexTests: XCTestCase { "path": "/documentation/mixedlanguageframework/article", "type": "article" }, + { + "path": "/documentation/mixedlanguageframework/apicollection", + "title": "APICollection", + "type": "symbol", + "children": [ + { + "title": "Swift-only APIs", + "type": "groupMarker" + }, + { + "path": "/documentation/mixedlanguageframework/swiftonlystruct", + "title": "SwiftOnlyStruct", + "type": "struct", + "children": [ + { + "title": "Instance Methods", + "type": "groupMarker" + }, + { + "title": "func tada()", + "path": "/documentation/mixedlanguageframework/swiftonlystruct/tada()", + "type": "method" + } + ] + } + ] + }, { "title": "Classes", "type": "groupMarker" diff --git a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift index 7878436dc7..9c824b9a84 100644 --- a/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/AutomaticCurationTests.swift @@ -379,7 +379,9 @@ class AutomaticCurationTests: XCTestCase { "Structures", "/documentation/MixedLanguageFramework/Foo-swift.struct", - "/documentation/MixedLanguageFramework/SwiftOnlyStruct", + + // SwiftOnlyStruct is manually curated. + // "/documentation/MixedLanguageFramework/SwiftOnlyStruct", ] ) @@ -398,7 +400,10 @@ class AutomaticCurationTests: XCTestCase { "/documentation/MixedLanguageFramework/Bar", "Variables", - "/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber", + + // _MixedLanguageFrameworkVersionNumber is manually curated. + // "/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber", + "/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionString", // 'MixedLanguageFramework/Foo-occ.typealias' is manually curated in a task group titled "Custom" under 'MixedLanguageFramework/Bar/myStringFunction:error:' diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift index 5d94f93af1..1c5c008148 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift @@ -125,6 +125,7 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { "c:objc(cs)Bar", "c:objc(cs)Bar(cm)myStringFunction:error:", "Article", + "APICollection", "MixedLanguageFramework Tutorials", "Tutorial Article", "Tutorial", @@ -153,10 +154,12 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { "doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/TutorialArticle", "doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/Tutorial", "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Article", + "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/APICollection", "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Bar", "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Foo-swift.struct", ], referenceTitles: [ + "APICollection", "Article", "Bar", "Foo", @@ -197,11 +200,13 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { "doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/TutorialArticle", "doc://org.swift.MixedLanguageFramework/tutorials/MixedLanguageFramework/Tutorial", "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Article", + "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/APICollection", "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Bar", "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionString", "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Foo-swift.struct", ], referenceTitles: [ + "APICollection", "Article", "Bar", "Foo", @@ -440,7 +445,67 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { "typedef enum Foo : NSString {\n ...\n} Foo;", ], failureMessage: { fieldName in - "Swift variant of 'MyArticle' article has unexpected content for '\(fieldName)'." + "Objective-C variant of 'MyArticle' article has unexpected content for '\(fieldName)'." + } + ) + } + + func testAPICollectionInMixedLanguageFramework() throws { + enableFeatureFlag(\.isExperimentalObjectiveCSupportEnabled) + + let outputConsumer = try mixedLanguageFrameworkConsumer() + + let articleRenderNode = try outputConsumer.renderNode(withTitle: "APICollection") + + assertExpectedContent( + articleRenderNode, + sourceLanguage: "swift", + title: "APICollection", + navigatorTitle: nil, + abstract: "This is an API collection.", + declarationTokens: nil, + discussionSection: nil, + topicSectionIdentifiers: [ + "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct" + ], + referenceTitles: [ + "Article", + "MixedLanguageFramework", + "SwiftOnlyStruct", + "_MixedLanguageFrameworkVersionNumber", + ], + referenceFragments: [ + "struct SwiftOnlyStruct", + ], + failureMessage: { fieldName in + "Swift variant of 'APICollection' article has unexpected content for '\(fieldName)'." + } + ) + + let objectiveCVariantNode = try renderNodeApplyingObjectiveCVariantOverrides(to: articleRenderNode) + + assertExpectedContent( + objectiveCVariantNode, + sourceLanguage: "occ", + title: "APICollection", + navigatorTitle: nil, + abstract: "This is an API collection.", + declarationTokens: nil, + discussionSection: nil, + topicSectionIdentifiers: [ + "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber" + ], + referenceTitles: [ + "Article", + "MixedLanguageFramework", + "SwiftOnlyStruct", + "_MixedLanguageFrameworkVersionNumber", + ], + referenceFragments: [ + "struct SwiftOnlyStruct", + ], + failureMessage: { fieldName in + "Objective-C variant of 'MyArticle' article has unexpected content for '\(fieldName)'." } ) } diff --git a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorSymbolVariantsTests.swift b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorSymbolVariantsTests.swift index 2858efa6f9..e29937f953 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorSymbolVariantsTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderNodeTranslatorSymbolVariantsTests.swift @@ -680,6 +680,43 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { ) } + func testEncodesNilTopicsSectionsForVariantIfDefaultIsNonEmpty() throws { + try assertMultiVariantArticle( + configureArticle: { article in + article.automaticTaskGroups = [] + article.topics = makeTopicsSection( + taskGroupName: "Swift Task Group", + destination: "doc://org.swift.docc.example/documentation/MyKit/MyProtocol" + ) + }, + assertOriginalRenderNode: { renderNode in + XCTAssertEqual(renderNode.topicSections.count, 1) + let taskGroup = try XCTUnwrap(renderNode.topicSections.first) + XCTAssertEqual(taskGroup.title, "Swift Task Group") + + XCTAssertEqual( + taskGroup.identifiers, + ["doc://org.swift.docc.example/documentation/MyKit/MyProtocol"] + ) + }, + assertDataAfterApplyingVariant: { renderNodeData in + // What we want to validate here is that the Objective-C render JSON's `topicSections` is `null` rather + // than `[]`. Since the `RenderNode` decoder implementation encodes `[]` rather than `nil` into the + // model when the JSON value is `null` (`topicSections` is not optional in the model), we can't use it + // for this test. Instead, we decode the JSON using a proxy type that has an optional `topicSections`. + + struct RenderNodeProxy: Codable { + var topicSections: [TaskGroupRenderSection]? + } + + XCTAssertNil( + try JSONDecoder().decode(RenderNodeProxy.self, from: renderNodeData).topicSections, + "Expected topicSections to be null in the JSON because the article has no Objective-C topics." + ) + } + ) + } + func testTopicsSectionVariantsNoUserProvidedTopics() throws { try assertMultiVariantSymbol( configureSymbol: { symbol in @@ -877,7 +914,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { } private func assertMultiVariantSymbol( - configureContext: (DocumentationContext, ResolvedTopicReference) throws -> () = { _,_ in }, + configureContext: (DocumentationContext, ResolvedTopicReference) throws -> () = { _, _ in }, configureSymbol: (Symbol) throws -> () = { _ in }, configureRenderNodeTranslator: (inout RenderNodeTranslator) -> () = { _ in }, assertOriginalRenderNode: (RenderNode) throws -> (), @@ -900,6 +937,61 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { try configureSymbol(symbol) + try assertMultiLanguageSemantic( + symbol, + context: context, + bundle: bundle, + identifier: identifier, + configureRenderNodeTranslator: configureRenderNodeTranslator, + assertOriginalRenderNode: assertOriginalRenderNode, + assertAfterApplyingVariant: assertAfterApplyingVariant + ) + } + + private func assertMultiVariantArticle( + configureArticle: (Article) throws -> () = { _ in }, + configureRenderNodeTranslator: (inout RenderNodeTranslator) -> () = { _ in }, + assertOriginalRenderNode: (RenderNode) throws -> (), + assertAfterApplyingVariant: (RenderNode) throws -> () = { _ in }, + assertDataAfterApplyingVariant: (Data) throws -> () = { _ in } + ) throws { + let (_, bundle, context) = try testBundleAndContext(copying: "TestBundle") + + let identifier = ResolvedTopicReference( + bundleIdentifier: bundle.identifier, + path: "/documentation/Test-Bundle/article", + sourceLanguage: .swift + ) + + context.documentationCache[identifier]?.availableSourceLanguages = [.swift, .objectiveC] + + let node = try context.entity(with: identifier) + + let article = try XCTUnwrap(node.semantic as? Article) + + try configureArticle(article) + + try assertMultiLanguageSemantic( + article, + context: context, + bundle: bundle, + identifier: identifier, + assertOriginalRenderNode: assertOriginalRenderNode, + assertAfterApplyingVariant: assertAfterApplyingVariant, + assertDataAfterApplyingVariant: assertDataAfterApplyingVariant + ) + } + + private func assertMultiLanguageSemantic( + _ semantic: Semantic, + context: DocumentationContext, + bundle: DocumentationBundle, + identifier: ResolvedTopicReference, + configureRenderNodeTranslator: (inout RenderNodeTranslator) -> () = { _ in }, + assertOriginalRenderNode: (RenderNode) throws -> (), + assertAfterApplyingVariant: (RenderNode) throws -> (), + assertDataAfterApplyingVariant: (Data) throws -> () = { _ in } + ) throws { var translator = RenderNodeTranslator( context: context, bundle: bundle, @@ -909,7 +1001,7 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { configureRenderNodeTranslator(&translator) - let renderNode = translator.visit(symbol) as! RenderNode + let renderNode = translator.visit(semantic) as! RenderNode let data = try renderNode.encodeToJSON() @@ -918,6 +1010,8 @@ class RenderNodeTranslatorSymbolVariantsTests: XCTestCase { let variantRenderNode = try RenderNodeVariantOverridesApplier() .applyVariantOverrides(in: data, for: [.interfaceLanguage("occ")]) + try assertDataAfterApplyingVariant(variantRenderNode) + try assertAfterApplyingVariant(RenderJSONDecoder.makeDecoder().decode(RenderNode.self, from: variantRenderNode)) } diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md new file mode 100644 index 0000000000..4267a00955 --- /dev/null +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md @@ -0,0 +1,15 @@ +# APICollection + +This is an API collection. + +## Topics + +### Swift-only APIs + +- ``SwiftOnlyStruct`` + +### Objective-C–only APIs + +- + + diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/MixedLanguageFramework.md b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/MixedLanguageFramework.md index b5b5885cf4..b3e124d367 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/MixedLanguageFramework.md +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/MixedLanguageFramework.md @@ -21,5 +21,6 @@ This framework is available to both Swift and Objective-C clients. ### Articles - +- From eed8c753cc48650776ad45de4ac913f943ba30c7 Mon Sep 17 00:00:00 2001 From: Franklin Schrans Date: Fri, 11 Mar 2022 18:14:01 +0000 Subject: [PATCH 3/3] Fix language availability for articles See Alsos rdar://90159299 --- .../Rendering/RenderNodeTranslator.swift | 59 ++++++++++++------- .../SemaToRenderNodeMultiLanguageTests.swift | 18 ++++++ .../APICollection.md | 4 ++ 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index cd3be5a320..13bc48bee6 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -698,29 +698,44 @@ public struct RenderNodeTranslator: SemanticVisitor { } node.metadata.role = contentRenderer.roleForArticle(article, nodeKind: documentationNode.kind).rawValue - // Authored See Also section - if let seeAlso = article.seeAlso, !seeAlso.taskGroups.isEmpty { - node.seeAlsoSections.append( - contentsOf: renderGroups( - seeAlso, - allowExternalLinks: true, - allowedTraits: documentationNode.availableVariantTraits, - contentCompiler: &contentCompiler + node.seeAlsoSectionsVariants = VariantCollection<[TaskGroupRenderSection]>( + from: documentationNode.availableVariantTraits, + fallbackDefaultValue: [] + ) { trait in + var seeAlsoSections = [TaskGroupRenderSection]() + + // Authored See Also section + if let seeAlso = article.seeAlso, !seeAlso.taskGroups.isEmpty { + seeAlsoSections.append( + contentsOf: renderGroups( + seeAlso, + allowExternalLinks: true, + allowedTraits: [trait], + contentCompiler: &contentCompiler + ) ) - ) - } - - // Automatic See Also section - if let seeAlso = try! AutomaticCuration.seeAlso(for: documentationNode, context: context, bundle: bundle, renderContext: renderContext, renderer: contentRenderer) { - contentCompiler.collectedTopicReferences.append(contentsOf: seeAlso.references) - node.seeAlsoSections.append(TaskGroupRenderSection( - title: seeAlso.title, - abstract: nil, - discussion: nil, - identifiers: seeAlso.references.map { $0.absoluteString }, - generated: true - )) - } + } + + // Automatic See Also section + if let seeAlso = try! AutomaticCuration.seeAlso( + for: documentationNode, + context: context, + bundle: bundle, + renderContext: renderContext, + renderer: contentRenderer + ) { + contentCompiler.collectedTopicReferences.append(contentsOf: seeAlso.references) + seeAlsoSections.append(TaskGroupRenderSection( + title: seeAlso.title, + abstract: nil, + discussion: nil, + identifiers: seeAlso.references.map { $0.absoluteString }, + generated: true + )) + } + + return seeAlsoSections + } ?? .init(defaultValue: []) collectedTopicReferences.append(contentsOf: contentCompiler.collectedTopicReferences) node.references = createTopicRenderReferences() diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift index 1c5c008148..fa0d6e9c42 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift @@ -468,6 +468,10 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { topicSectionIdentifiers: [ "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct" ], + seeAlsoSectionIdentifiers: [ + "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/SwiftOnlyStruct", + "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Article", + ], referenceTitles: [ "Article", "MixedLanguageFramework", @@ -495,6 +499,9 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { topicSectionIdentifiers: [ "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/_MixedLanguageFrameworkVersionNumber" ], + seeAlsoSectionIdentifiers: [ + "doc://org.swift.MixedLanguageFramework/documentation/MixedLanguageFramework/Article", + ], referenceTitles: [ "Article", "MixedLanguageFramework", @@ -520,6 +527,7 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { declarationTokens expectedDeclarationTokens: [String]?, discussionSection expectedDiscussionSection: [String]?, topicSectionIdentifiers expectedTopicSectionIdentifiers: [String], + seeAlsoSectionIdentifiers expectedSeeAlsoSectionIdentifiers: [String]? = nil, referenceTitles expectedReferenceTitles: [String], referenceFragments expectedReferenceFragments: [String], failureMessage failureMessageForField: (_ field: String) -> String, @@ -593,6 +601,16 @@ class SemaToRenderNodeMixedLanguageTests: ExperimentalObjectiveCTestCase { line: line ) + if let expectedSeeAlsoSectionIdentifiers = expectedSeeAlsoSectionIdentifiers { + XCTAssertEqual( + renderNode.seeAlsoSections.flatMap(\.identifiers), + expectedSeeAlsoSectionIdentifiers, + failureMessageForField("see also sections identifiers"), + file: file, + line: line + ) + } + XCTAssertEqual( renderNode.references.map(\.value).compactMap { reference in (reference as? TopicRenderReference)?.title diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md index 4267a00955..1e4ecb2d11 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/APICollection.md @@ -12,4 +12,8 @@ This is an API collection. - +## See Also + +- ``SwiftOnlyStruct`` +