From 2ec78bd8bf0670746b2a325a3590171ddd6cf51a Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:10:58 +0100 Subject: [PATCH 01/11] Clarify doc comment for `OutOfProcessReferenceResolver.declarationFragments` These declaration fragments are directly used to populate `TopicRenderReference.fragmentsVariants` [1], which is described as "The abbreviated declaration of the symbol to display in links" [2]. This change clarifies that the declaration fragments are expected to be the abbreviated declaration fragments. [1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Infrastructure/External%20Data/OutOfProcessReferenceResolver.swift#L169 [2]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Model/Rendering/References/TopicRenderReference.swift#L50-L53 --- ...ProcessReferenceResolver+DeprecatedCommunication.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift index e95a3a01ff..153a9aa72d 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver+DeprecatedCommunication.swift @@ -162,6 +162,8 @@ extension OutOfProcessReferenceResolver { /// Information about the platforms and their versions where the resolved node is available, if any. public let platforms: [PlatformAvailability]? /// Information about the resolved declaration fragments, if any. + /// + /// This is expected to be an abbreviated declaration for the symbol. public let declarationFragments: DeclarationFragments? // We use the real types here because they're Codable and don't have public member-wise initializers. @@ -205,7 +207,7 @@ extension OutOfProcessReferenceResolver { /// - language: The resolved language. /// - availableLanguages: The languages where the resolved node is available. /// - platforms: The platforms and their versions where the resolved node is available, if any. - /// - declarationFragments: The resolved declaration fragments, if any. + /// - declarationFragments: The resolved declaration fragments, if any. This is expected to be an abbreviated declaration for the symbol. /// - topicImages: Images that are used to represent the summarized element. /// - references: References used in the content of the summarized element. /// - variants: The variants of content for this resolver information. @@ -259,6 +261,8 @@ extension OutOfProcessReferenceResolver { public let language: VariantValue /// The declaration fragments of the variant or `nil` if the declaration is the same as the resolved information. /// + /// This is expected to be an abbreviated declaration for the symbol. + /// /// If the resolver information has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. public let declarationFragments: VariantValue @@ -271,7 +275,7 @@ extension OutOfProcessReferenceResolver { /// - title: The resolved title /// - abstract: The resolved (plain text) abstract. /// - language: The resolved language. - /// - declarationFragments: The resolved declaration fragments, if any. + /// - declarationFragments: The resolved declaration fragments, if any. This is expected to be an abbreviated declaration for the symbol. public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, From 8086accecfabbeaf729d71ab2e3d031122dc024c Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:53:06 +0100 Subject: [PATCH 02/11] Add full (symbol) name to LinkDestinationSummary The only use-case for storing the full declaration fragments in `LinkDestinationSummary` rather than the shortened declaration fragments intended for display, is to derive the full name of the symbol in diagnostics [1] [2]. We have a number of tests [3] which verify that the diagnostic emitted when a symbol is referenced locally is the same as when the symbol is referenced externally. However, we want to stop storing the full declaration fragments in favour of the shortened ones because: - the full fragments are never displayed when referenced externally, as they are too long - the full fragments take up additionally storage space, and encoding/decoding time, to only be used to derive the full name of the symbol This commit updates the logic such that, we pre-derive the full name of the symbol from its declaration fragments, and then store that as part of `LinkDestinationSummary`, so that in a subsequent commit we can stop storing the full declaration fragments and store only the shortened ones instead. The diagnostic logic has also been updated to use the new field rather than the full declaration fragments. [1]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Infrastructure/Link%20Resolution/ExternalPathHierarchyResolver.swift#L61-L63 [2]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Infrastructure/Link%20Resolution/PathHierarchy%2BError.swift#L115-L117 [3]: https://github.com/swiftlang/swift-docc/blob/9413c599817abc9dd4f8feeed57a998b19a05a6f/Tests/SwiftDocCTests/Infrastructure/ExternalPathHierarchyResolverTests.swift#L907-L918 --- .../ExternalPathHierarchyResolver.swift | 14 ++---- .../LinkTargets/LinkDestinationSummary.swift | 43 +++++++++++++++++-- .../LinkDestinationSummaryTests.swift | 14 +++++- .../clang/MixedLanguageFramework.symbols.json | 2 +- 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index 4c6fac3500..836938d500 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift @@ -58,13 +58,13 @@ final class ExternalPathHierarchyResolver { return collidingNode.name } if let symbolID = collidingNode.symbol?.identifier { - if symbolID.interfaceLanguage == summary.language.id, let fragments = summary.declarationFragments { - return fragments.plainTextDeclaration() + if symbolID.interfaceLanguage == summary.language.id, let fullName = summary.fullName { + return fullName } if let variant = summary.variants.first(where: { $0.traits.contains(.interfaceLanguage(symbolID.interfaceLanguage)) }), - let fragments = variant.declarationFragments ?? summary.declarationFragments + let fullName = variant.fullName ?? summary.fullName { - return fragments.plainTextDeclaration() + return fullName } } return summary.title @@ -153,12 +153,6 @@ final class ExternalPathHierarchyResolver { } } -private extension Sequence { - func plainTextDeclaration() -> String { - return self.map(\.text).joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ") - } -} - // MARK: ExternalEntity extension LinkDestinationSummary { diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 0a53b58c3d..5c94435d0a 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -135,6 +135,9 @@ public struct LinkDestinationSummary: Codable, Equatable { /// The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. public let usr: String? + /// The full name of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. + public let fullName: String? + /// The rendered fragments of a symbol's declaration. public typealias DeclarationFragments = [DeclarationRenderSection.Token] /// The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. @@ -193,6 +196,11 @@ public struct LinkDestinationSummary: Codable, Equatable { /// If the summarized element has a precise symbol identifier but the variant doesn't, this property will be `Optional.some(nil)`. public let usr: VariantValue + /// The full name of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. + /// + /// If the summarized element has a full name but the variant doesn't, this property will be `Optional.some(nil)`. + public let fullName: VariantValue + /// The declaration of the variant or `nil` if the declaration is the same as the summarized element. /// /// If the summarized element has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. @@ -215,6 +223,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - abstract: The abstract of the variant or `nil` if the abstract is the same as the summarized element. /// - taskGroups: The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element. /// - usr: The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element. + /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. /// - declarationFragments: The declaration of the variant or `nil` if the declaration is the same as the summarized element. public init( traits: [RenderNode.Variant.Trait], @@ -225,6 +234,7 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: VariantValue = nil, taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, + fullName: VariantValue = nil, declarationFragments: VariantValue = nil ) { self.traits = traits @@ -235,10 +245,11 @@ public struct LinkDestinationSummary: Codable, Equatable { self.abstract = abstract self.taskGroups = taskGroups self.usr = usr + self.fullName = fullName self.declarationFragments = declarationFragments } - @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:declarationFragments:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:declarationFragments:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") + @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -248,6 +259,7 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: VariantValue = nil, taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, + fullName: VariantValue = nil, declarationFragments: VariantValue = nil, topicImages: VariantValue<[TopicImage]?> = nil ) { @@ -260,6 +272,7 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: abstract, taskGroups: taskGroups, usr: usr, + fullName: fullName, declarationFragments: declarationFragments ) } @@ -281,6 +294,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - platforms: Information about the platforms for which the summarized element is available. /// - taskGroups: The reference URLs of the summarized element's children, grouped by their task groups. /// - usr: The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. + /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. /// - declarationFragments: The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. /// - redirects: Any previous URLs for this element, or `nil` if this element has no previous URLs. /// - topicImages: Images that are used to represent the summarized element, or `nil` if this element has no topic images. @@ -296,6 +310,7 @@ public struct LinkDestinationSummary: Codable, Equatable { platforms: [LinkDestinationSummary.PlatformAvailability]? = nil, taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, usr: String? = nil, + fullName: String? = nil, declarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, @@ -312,6 +327,7 @@ public struct LinkDestinationSummary: Codable, Equatable { self.platforms = platforms self.taskGroups = taskGroups self.usr = usr + self.fullName = fullName self.declarationFragments = declarationFragments self.redirects = redirects self.topicImages = topicImages @@ -466,6 +482,7 @@ extension LinkDestinationSummary { let abstract = renderSymbolAbstract(symbol.abstractVariants[summaryTrait] ?? symbol.abstract) let usr = symbol.externalIDVariants[summaryTrait] ?? symbol.externalID + let fullName = symbol.fullName(for: summaryTrait) let declaration = (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() let language = documentationNode.sourceLanguage @@ -483,6 +500,7 @@ extension LinkDestinationSummary { return main == variant ? nil : variant } + let fullNameVariant = symbol.fullName(for: trait) let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage(interfaceLanguage)] return Variant( traits: variantTraits, @@ -493,6 +511,7 @@ extension LinkDestinationSummary { abstract: nilIfEqual(main: abstract, variant: abstractVariant), taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR + fullName: nilIfEqual(main: fullName, variant: fullNameVariant), declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant) ) } @@ -512,6 +531,7 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: usr, + fullName: fullName, declarationFragments: declaration, redirects: redirects, topicImages: topicImages.nilIfEmpty, @@ -594,7 +614,7 @@ extension LinkDestinationSummary { // Add Codable methods—which include an initializer—in an extension so that it doesn't override the member-wise initializer. extension LinkDestinationSummary { enum CodingKeys: String, CodingKey { - case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants + case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, fullName case relativePresentationURL = "path" case declarationFragments = "fragments" } @@ -626,6 +646,7 @@ extension LinkDestinationSummary { try container.encodeIfPresent(platforms, forKey: .platforms) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) try container.encodeIfPresent(usr, forKey: .usr) + try container.encodeIfPresent(fullName, forKey: .fullName) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) try container.encodeIfPresent(redirects, forKey: .redirects) try container.encodeIfPresent(topicImages, forKey: .topicImages) @@ -682,6 +703,7 @@ extension LinkDestinationSummary { platforms = try container.decodeIfPresent([AvailabilityRenderItem].self, forKey: .platforms) taskGroups = try container.decodeIfPresent([TaskGroup].self, forKey: .taskGroups) usr = try container.decodeIfPresent(String.self, forKey: .usr) + fullName = try container.decodeIfPresent(String.self, forKey: .fullName) declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments) redirects = try container.decodeIfPresent([URL].self, forKey: .redirects) topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) @@ -695,7 +717,7 @@ extension LinkDestinationSummary { extension LinkDestinationSummary.Variant { enum CodingKeys: String, CodingKey { - case traits, kind, title, abstract, language, usr, taskGroups + case traits, kind, title, abstract, language, usr, taskGroups, fullName case relativePresentationURL = "path" case declarationFragments = "fragments" } @@ -721,6 +743,7 @@ extension LinkDestinationSummary.Variant { } } try container.encodeIfPresent(usr, forKey: .usr) + try container.encodeIfPresent(fullName, forKey: .fullName) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) } @@ -762,6 +785,7 @@ extension LinkDestinationSummary.Variant { title = try container.decodeIfPresent(String.self, forKey: .title) abstract = try container.decodeIfPresent(LinkDestinationSummary.Abstract?.self, forKey: .abstract) usr = try container.decodeIfPresent(String?.self, forKey: .usr) + fullName = try container.decodeIfPresent(String?.self, forKey: .fullName) declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) } @@ -871,3 +895,16 @@ private extension Collection { isEmpty ? nil : self } } + +private extension Sequence { + func plainTextDeclaration() -> String { + return self.map(\.text).joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ") + } +} + +private extension Symbol { + func fullName(for trait: DocumentationDataVariantsTrait) -> String? { + let fullDeclaration = (self.declarationVariants[trait] ?? self.declaration).renderDeclarationTokens() + return fullDeclaration?.plainTextDeclaration() + } +} diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index a51ef8b102..64dff9b090 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -113,6 +113,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(pageSummary.platforms, renderNode.metadata.platforms) XCTAssertEqual(pageSummary.redirects, nil) XCTAssertNil(pageSummary.usr, "Only symbols have USRs") + XCTAssertNil(pageSummary.fullName, "Only symbols have full names") XCTAssertNil(pageSummary.declarationFragments, "Only symbols have declaration fragments") XCTAssertNil(pageSummary.abstract, "There is no text to use as an abstract for the tutorial page") XCTAssertNil(pageSummary.topicImages, "The tutorial page doesn't have any topic images") @@ -131,6 +132,7 @@ class LinkDestinationSummaryTests: XCTestCase { URL(string: "old/path/to/this/landmark")!, ]) XCTAssertNil(sectionSummary.usr, "Only symbols have USRs") + XCTAssertNil(sectionSummary.fullName, "Only symbols have full names") XCTAssertNil(sectionSummary.declarationFragments, "Only symbols have declaration fragments") XCTAssertEqual(sectionSummary.abstract, [ .text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt"), @@ -180,6 +182,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC") + XCTAssertEqual(summary.fullName, "class MyClass") XCTAssertEqual(summary.declarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -219,6 +222,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ProtocolP") + XCTAssertEqual(summary.fullName, "protocol MyProtocol : Hashable") XCTAssertEqual(summary.declarationFragments, [ .init(text: "protocol", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -250,6 +254,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") + XCTAssertEqual(summary.fullName, "func myFunction(for name...)") XCTAssertEqual(summary.declarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -285,6 +290,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit14globalFunction_11consideringy10Foundation4DataV_SitF") + XCTAssertEqual(summary.fullName, "func globalFunction(_: Data, considering: Int)") XCTAssertEqual(summary.declarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -342,6 +348,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") + XCTAssertEqual(summary.fullName, "func myFunction(for name...)") XCTAssertEqual(summary.declarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -454,7 +461,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages.sorted(), [.swift, .objectiveC]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar") - + XCTAssertEqual(summary.fullName, "class Bar") XCTAssertEqual(summary.declarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -468,6 +475,7 @@ class LinkDestinationSummaryTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) + XCTAssertEqual(variant.fullName, "@interface Bar : NSObject") XCTAssertEqual(variant.declarationFragments, [ .init(text: "@interface", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -514,6 +522,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages.sorted(), [.swift, .objectiveC]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar(cm)myStringFunction:error:") + XCTAssertEqual(summary.fullName, "class func myStringFunction(_ string: String) throws -> String") XCTAssertEqual(summary.declarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -540,6 +549,7 @@ class LinkDestinationSummaryTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) XCTAssertEqual(variant.title, "myStringFunction:error:") + XCTAssertEqual(variant.fullName, "+ (NSString *) myStringFunction: (NSString *)string error: (NSError **)error;") XCTAssertEqual(variant.declarationFragments, [ .init(text: "+ (", kind: .text, identifier: nil), .init(text: "NSString", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSString"), @@ -547,7 +557,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "myStringFunction", kind: .identifier, identifier: nil), .init(text: ": (", kind: .text, identifier: nil), .init(text: "NSString", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSString"), - .init(text: " *)string", kind: .text, identifier: nil), + .init(text: " *)string ", kind: .text, identifier: nil), .init(text: "error", kind: .identifier, identifier: nil), .init(text: ": (", kind: .text, identifier: nil), .init(text: "NSError", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSError"), diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json index ec1390b35a..410641ea3a 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json @@ -195,7 +195,7 @@ }, { "kind" : "text", - "spelling" : " *)string" + "spelling" : " *)string " }, { "kind" : "identifier", From 12a7e1d0f519addea356a8baaec8d0bc0d9c7511 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:26:54 +0100 Subject: [PATCH 03/11] Use the abbreviated declaration fragments in LinkDestinationSummary LinkDestinationSummary contains a summary of an element that you can link to from outside the documentation bundle. [1] This information is meant to be used by a server to provide information to an out-of-process resolver to resolve links to external entities, so that the partner implementation of `LinkDestinationSummary` is `OutOfProcessReferenceResolver.ResolvedInformation` [2]. However, currently `OutOfProcessReferenceResolver.ResolvedInformation. declarationFragments` is expecting the abbreviated declaration fragments, but we are storing the full fragments instead. [3] Instead, we should be storing the abbreviated declaration fragments, which are stored as the `subHeading` of the symbol. [4] This subheading is further processed during the render node transformation phase [5], and stored as `renderNode.metadata.fragmentsVariants`. This commit modifies `LinkDestinationSummary` such that its declaration fragments are the abbreviated declaration fragments from `renderNode.metadata.fragmentsVariants` rather than the full declaration fragments. The final result will be that declaration fragments for external links will behave the same as local links when referencing them in the Topics section. They will both now use the abbreviated declaration fragments. [1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift#L66 [2]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Infrastructure/External%20Data/OutOfProcessReferenceResolver.swift#L558-L562 [3]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift#L445 [4]: https://github.com/swiftlang/swift-docc-symbolkit/blob/ebe89c7da4cf03ded04cd708f3399087c6f2dad7/Sources/SymbolKit/SymbolGraph/Symbol/Names.swift#L28-L31 --- .../LinkTargets/LinkDestinationSummary.swift | 30 +++++++++--- .../LinkDestinationSummaryTests.swift | 19 +------ .../SemaToRenderNodeMultiLanguageTests.swift | 2 +- .../clang/MixedLanguageFramework.symbols.json | 49 +++---------------- 4 files changed, 33 insertions(+), 67 deletions(-) diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 5c94435d0a..09dd02d4b6 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -140,7 +140,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// The rendered fragments of a symbol's declaration. public typealias DeclarationFragments = [DeclarationRenderSection.Token] - /// The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. + /// The abbreviated fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. public let declarationFragments: DeclarationFragments? /// Any previous URLs for this element. @@ -201,7 +201,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// If the summarized element has a full name but the variant doesn't, this property will be `Optional.some(nil)`. public let fullName: VariantValue - /// The declaration of the variant or `nil` if the declaration is the same as the summarized element. + /// The abbreviated declaration of the variant or `nil` if the declaration is the same as the summarized element. /// /// If the summarized element has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. public let declarationFragments: VariantValue @@ -224,7 +224,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element. /// - usr: The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element. /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. - /// - declarationFragments: The declaration of the variant or `nil` if the declaration is the same as the summarized element. + /// - declarationFragments: The abbreviated declaration of the variant or `nil` if the declaration is the same as the summarized element. public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -295,7 +295,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The reference URLs of the summarized element's children, grouped by their task groups. /// - usr: The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. - /// - declarationFragments: The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. + /// - declarationFragments: The abbreviated fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. /// - redirects: Any previous URLs for this element, or `nil` if this element has no previous URLs. /// - topicImages: Images that are used to represent the summarized element, or `nil` if this element has no topic images. /// - references: References used in the content of the summarized element, or `nil` if this element has no references to other content. @@ -483,17 +483,22 @@ extension LinkDestinationSummary { let abstract = renderSymbolAbstract(symbol.abstractVariants[summaryTrait] ?? symbol.abstract) let usr = symbol.externalIDVariants[summaryTrait] ?? symbol.externalID let fullName = symbol.fullName(for: summaryTrait) - let declaration = (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() let language = documentationNode.sourceLanguage - + // Use the abbreviated declaration fragments instead of the full declaration fragments. + // These have been derived from the symbol's subheading declaration fragments as part of rendering. + // We only want an abbreviated version of the declaration in the link summary (for display in Topic sections, the navigator, etc.). + // Otherwise, the declaration would be too verbose. + // + // However if no abbreviated declaration fragments are available, use the full declaration fragments instead. + // In this case, they are assumed to be the same. + let declaration = renderNode.metadata.fragmentsVariants.value(for: language) ?? (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() + let variants: [Variant] = documentationNode.availableVariantTraits.compactMap { trait in // Skip the variant for the summarized elements source language. guard let interfaceLanguage = trait.interfaceLanguage, interfaceLanguage != documentationNode.sourceLanguage.id else { return nil } - let declarationVariant = symbol.declarationVariants[trait]?.renderDeclarationTokens() - let abstractVariant: Variant.VariantValue = symbol.abstractVariants[trait].map { renderSymbolAbstract($0) } func nilIfEqual(main: Value, variant: Value?) -> Value? { @@ -502,6 +507,15 @@ extension LinkDestinationSummary { let fullNameVariant = symbol.fullName(for: trait) let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage(interfaceLanguage)] + + // Use the abbreviated declaration fragments instead of the full declaration fragments. + // These have been derived from the symbol's subheading declaration fragments as part of rendering. + // We only want an abbreviated version of the declaration in the link summary (for display in Topic sections, the navigator, etc.). + // Otherwise, the declaration would be too verbose. + // + // However if no abbreviated declaration fragments are available, use the full declaration fragments instead. + // In this case, they are assumed to be the same. + let declarationVariant = renderNode.metadata.fragmentsVariants.value(for: variantTraits) ?? symbol.declarationVariants[trait]?.renderDeclarationTokens() return Variant( traits: variantTraits, kind: nilIfEqual(main: kind, variant: symbol.kindVariants[trait].map { DocumentationNode.kind(forKind: $0.identifier) }), diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index 64dff9b090..cba65bbf74 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -296,8 +296,6 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " ", kind: .text, identifier: nil), .init(text: "globalFunction", kind: .identifier, identifier: nil), .init(text: "(", kind: .text, identifier: nil), - .init(text: "_", kind: .identifier, identifier: nil), - .init(text: ": ", kind: .text, identifier: nil), .init(text: "Data", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:10Foundation4DataV"), .init(text: ", ", kind: .text, identifier: nil), .init(text: "considering", kind: .identifier, identifier: nil), @@ -530,10 +528,6 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " ", kind: .text, identifier: nil), .init(text: "myStringFunction", kind: .identifier, identifier: nil), .init(text: "(", kind: .text, identifier: nil), - .init(text: "_", kind: .externalParam, identifier: nil), - .init(text: " ", kind: .text, identifier: nil), - .init(text: "string", kind: .internalParam, identifier: nil), - .init(text: ": ", kind: .text, identifier: nil), .init(text: "String", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:SS"), .init(text: ") ", kind: .text, identifier: nil), .init(text: "throws", kind: .keyword, identifier: nil), @@ -551,17 +545,8 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(variant.title, "myStringFunction:error:") XCTAssertEqual(variant.fullName, "+ (NSString *) myStringFunction: (NSString *)string error: (NSError **)error;") XCTAssertEqual(variant.declarationFragments, [ - .init(text: "+ (", kind: .text, identifier: nil), - .init(text: "NSString", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSString"), - .init(text: " *) ", kind: .text, identifier: nil), - .init(text: "myStringFunction", kind: .identifier, identifier: nil), - .init(text: ": (", kind: .text, identifier: nil), - .init(text: "NSString", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSString"), - .init(text: " *)string ", kind: .text, identifier: nil), - .init(text: "error", kind: .identifier, identifier: nil), - .init(text: ": (", kind: .text, identifier: nil), - .init(text: "NSError", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSError"), - .init(text: " **)error;", kind: .text, identifier: nil) + .init(text: "+ ", kind: .text, identifier: nil), + .init(text: "myStringFunction:error:", kind: .identifier, identifier: nil) ]) // Check variant content that is the same as the summarized element diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift index 02638bd029..e4acc7156a 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeMultiLanguageTests.swift @@ -556,7 +556,7 @@ class SemaToRenderNodeMixedLanguageTests: XCTestCase { "myStringFunction:error:", ], referenceFragments: [ - "typedef enum Foo : NSString {\n ...\n} Foo;", + "+ myStringFunction:error:", ], failureMessage: { fieldName in "Objective-C variant of 'MyArticle' article has unexpected content for '\(fieldName)'." diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json index 410641ea3a..762bec59b2 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json @@ -366,46 +366,13 @@ } ], "subHeading" : [ - { - "kind" : "keyword", - "spelling" : "typedef" - }, { "kind" : "text", - "spelling" : " " - }, - { - "kind" : "keyword", - "spelling" : "enum" - }, - { - "kind" : "text", - "spelling" : " " + "spelling" : "+ " }, { "kind" : "identifier", - "spelling" : "Foo" - }, - { - "kind" : "text", - "spelling" : " : " - }, - { - "kind" : "typeIdentifier", - "spelling" : "NSString", - "preciseIdentifier": "c:@T@NSInteger" - }, - { - "kind": "text", - "spelling": " {\n ...\n} " - }, - { - "kind": "identifier", - "spelling": "Foo" - }, - { - "kind": "text", - "spelling": ";" + "spelling" : "myStringFunction:error:" } ] }, @@ -485,7 +452,7 @@ "text" : "This is the foo's description." } ] - }, + } }, { "accessLevel" : "public", @@ -570,7 +537,7 @@ { "kind" : "typeIdentifier", "spelling" : "NSString", - "preciseIdentifier": "c:@T@NSInteger", + "preciseIdentifier": "c:@T@NSInteger" }, { "kind": "text", @@ -630,7 +597,7 @@ "kind" : "identifier", "spelling" : "first" } - ], + ] }, "pathComponents" : [ "Foo", @@ -677,7 +644,7 @@ "kind" : "identifier", "spelling" : "fourth" } - ], + ] }, "pathComponents" : [ "Foo", @@ -724,7 +691,7 @@ "kind" : "identifier", "spelling" : "second" } - ], + ] }, "pathComponents" : [ "Foo", @@ -771,7 +738,7 @@ "kind" : "identifier", "spelling" : "third" } - ], + ] }, "pathComponents" : [ "Foo", From 51e76114ae3d58cd1047179d706ea0b4c6f7d13a Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:31:34 +0100 Subject: [PATCH 04/11] Populate declaration fragments in navigator metadata for external links Now that the declaration fragments will be the abbreviated declaration fragments from `LinkDestinationSummary`, we can propagate those to the navigator metadata for them to be used to inform the title of the navigator item [1]. Fixes rdar://156488052. [1]: https://github.com/swiftlang/swift-docc/blob/65aaf926ec079ddbd40f29540d4180a70af99e5e/Sources/SwiftDocC/Indexing/Navigator/RenderNode%2BNavigatorIndex.swift#L140 --- .../LinkResolver+NavigatorIndex.swift | 15 +++-- .../Indexing/ExternalRenderNodeTests.swift | 57 +++++++++++++++---- .../TestExternalReferenceResolvers.swift | 1 + 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift index 8fa9575bd5..2bb01f5abe 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift @@ -69,6 +69,13 @@ package struct ExternalRenderNode { topicRenderReference.navigatorTitleVariants } + /// The variants of the abbreviated declaration of the symbol to display in links and fall-back to in navigation. + /// + /// This value is `nil` if the referenced page is not a symbol. + var fragmentsVariants: VariantCollection<[DeclarationRenderSection.Token]?> { + topicRenderReference.fragmentsVariants + } + /// Author provided images that represent this page. var images: [TopicImage] { entity.topicImages ?? [] @@ -129,7 +136,8 @@ struct NavigatorExternalRenderNode: NavigatorIndexableRenderNodeRepresentation { role: renderNode.role, symbolKind: renderNode.symbolKind?.renderingIdentifier, images: renderNode.images, - isBeta: renderNode.isBeta + isBeta: renderNode.isBeta, + fragments: renderNode.fragmentsVariants.value(for: traits) ) } } @@ -143,19 +151,16 @@ struct ExternalRenderNodeMetadataRepresentation: NavigatorIndexableRenderMetadat var symbolKind: String? var images: [TopicImage] var isBeta: Bool + var fragments: [DeclarationRenderSection.Token]? // Values that we have insufficient information to derive. // These are needed to conform to the navigator indexable metadata protocol. // - // The fragments that we get as part of the external link are the full declaration fragments. - // These are too verbose for the navigator, so instead of using them, we rely on the title, navigator title and symbol kind instead. - // // The role heading is used to identify Property Lists. // The value being missing is used for computing the final navigator title. // // The platforms are used for generating the availability index, // but doesn't affect how the node is rendered in the sidebar. - var fragments: [DeclarationRenderSection.Token]? = nil var roleHeading: String? = nil var platforms: [AvailabilityRenderItem]? = nil } diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift index d0602e48ca..d48cd5ece8 100644 --- a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift @@ -41,6 +41,11 @@ class ExternalRenderNodeTests: XCTestCase { title: "SwiftSymbol", kind: .class, language: .swift, + declarationFragments: .init(declarationFragments: [ + .init(kind: .keyword, spelling: "class", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "SwiftSymbol", preciseIdentifier: nil) + ]), platforms: [.init(name: "iOS", introduced: nil, isBeta: true)] ) ) @@ -50,6 +55,13 @@ class ExternalRenderNodeTests: XCTestCase { title: "ObjCSymbol", kind: .function, language: .objectiveC, + declarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "- ", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "void", preciseIdentifier: nil), + .init(kind: .text, spelling: ") ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "ObjCSymbol", preciseIdentifier: nil) + ]), platforms: [.init(name: "macOS", introduced: nil, isBeta: false)] ) ) @@ -152,12 +164,14 @@ class ExternalRenderNodeTests: XCTestCase { ) XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.title, swiftTitle) XCTAssertFalse(swiftNavigatorExternalRenderNode.metadata.isBeta) - + XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.fragments, swiftFragments) + let objcNavigatorExternalRenderNode = try XCTUnwrap( NavigatorExternalRenderNode(renderNode: externalRenderNode, trait: .interfaceLanguage(SourceLanguage.objectiveC.id)) ) XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.title, objcTitle) XCTAssertFalse(objcNavigatorExternalRenderNode.metadata.isBeta) + XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.fragments, objcFragments) } func testNavigatorWithExternalNodes() async throws { @@ -218,19 +232,38 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.count(where: \.isExternal), 0) XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) + + func externalNodes(by language: SourceLanguage) -> [RenderIndex.Node]? { + renderIndex.interfaceLanguages[language.id]?.first?.children?.filter(\.isExternal) + } + // Verify that the curated external links are part of the index. - let swiftExternalNodes = (renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.first?.children?.filter(\.isExternal) ?? []).sorted(by: \.title) - let objcExternalNodes = (renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.first?.children?.filter(\.isExternal) ?? []).sorted(by: \.title) + let swiftExternalNodes = try XCTUnwrap(externalNodes(by: .swift)) XCTAssertEqual(swiftExternalNodes.count, 2) + + let objcExternalNodes = try XCTUnwrap(externalNodes(by: .objectiveC)) XCTAssertEqual(objcExternalNodes.count, 2) - XCTAssertEqual(swiftExternalNodes.map(\.title), ["SwiftArticle", "SwiftSymbol"]) - XCTAssertEqual(objcExternalNodes.map(\.title), ["ObjCArticle", "ObjCSymbol"]) - XCTAssert(swiftExternalNodes.first?.isBeta == false) - XCTAssert(swiftExternalNodes.last?.isBeta == true) - XCTAssert(objcExternalNodes.first?.isBeta == true) - XCTAssert(objcExternalNodes.last?.isBeta == false) - XCTAssertEqual(swiftExternalNodes.map(\.type), ["article", "class"]) - XCTAssertEqual(objcExternalNodes.map(\.type), ["article", "func"]) + + let swiftArticleExternalNode = try XCTUnwrap(swiftExternalNodes.first(where: { $0.path == "/path/to/external/swiftarticle" })) + let swiftSymbolExternalNode = try XCTUnwrap(swiftExternalNodes.first(where: { $0.path == "/path/to/external/swiftsymbol" })) + let objcArticleExternalNode = try XCTUnwrap(objcExternalNodes.first(where: { $0.path == "/path/to/external/objcarticle" })) + let objcSymbolExternalNode = try XCTUnwrap(objcExternalNodes.first(where: { $0.path == "/path/to/external/objcsymbol" })) + + XCTAssertEqual(swiftArticleExternalNode.title, "SwiftArticle") + XCTAssertEqual(swiftArticleExternalNode.isBeta, false) + XCTAssertEqual(swiftArticleExternalNode.type, "article") + + XCTAssertEqual(swiftSymbolExternalNode.title, "SwiftSymbol") // Classes don't use declaration fragments in their navigator title + XCTAssertEqual(swiftSymbolExternalNode.isBeta, true) + XCTAssertEqual(swiftSymbolExternalNode.type, "class") + + XCTAssertEqual(objcArticleExternalNode.title, "ObjCArticle") + XCTAssertEqual(objcArticleExternalNode.isBeta, true) + XCTAssertEqual(objcArticleExternalNode.type, "article") + + XCTAssertEqual(objcSymbolExternalNode.title, "- (void) ObjCSymbol") + XCTAssertEqual(objcSymbolExternalNode.isBeta, false) + XCTAssertEqual(objcSymbolExternalNode.type, "func") } func testNavigatorWithExternalNodesOnlyAddsCuratedNodesToNavigator() async throws { @@ -299,7 +332,7 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(swiftExternalNodes.count, 1) XCTAssertEqual(objcExternalNodes.count, 1) XCTAssertEqual(swiftExternalNodes.map(\.title), ["SwiftArticle"]) - XCTAssertEqual(objcExternalNodes.map(\.title), ["ObjCSymbol"]) + XCTAssertEqual(objcExternalNodes.map(\.title), ["- (void) ObjCSymbol"]) XCTAssertEqual(swiftExternalNodes.map(\.type), ["article"]) XCTAssertEqual(objcExternalNodes.map(\.type), ["func"]) } diff --git a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift index 7ea89aa72c..c8cf21c922 100644 --- a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift +++ b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift @@ -92,6 +92,7 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { title: entityInfo.title, availableLanguages: [entityInfo.language], platforms: entityInfo.platforms, + declarationFragments: entityInfo.declarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, topicImages: entityInfo.topicImages?.map(\.0), references: entityInfo.topicImages?.map { topicImage, altText in ImageReference(identifier: topicImage.identifier, altText: altText, imageAsset: assetsToReturn[topicImage.identifier.identifier] ?? .init()) From 078469aa8cebf341f73ce305dbc00497f08ac426 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:54:38 +0100 Subject: [PATCH 05/11] Add navigator title to LinkDestinationSummary The navigator title, which contains abbreviated fragments of a symbol's declaration for display in navigation [1], was missing from LinkDestinationSummary. The navigator title is used here [2], but the information wasn't being populated anywhere [3]. This commit captures the navigator title information from the render metadata, and stores it as part of `LinkDestinationSummary`. While the original information comes from the symbol graph [4], the `DocumentationContentRenderer` has some special logic to sanitise the declaration fragments, particularly for Swift [5]. In order to ensure that we have parity between internal and external navigator titles, used the render metadata rather than the original information from the symbol graph. This mirrors what we're already doing with the shortened declaration fragments, which also are processed with the same logic. Finally, `LinkDestinationSummary.makeTopicRenderReference()` has been updated to populate the navigator title variants, such that after this commit navigator titles will be correctly populated for external links in the navigator. Fixes rdar://156488184. [1]: https://github.com/swiftlang/swift-docc/blob/257c62d1ef11efc023667fda89c0cd4419a8afa3/Sources/SwiftDocC/Model/Rendering/References/TopicRenderReference.swift#L60-L63 [2]: https://github.com/swiftlang/swift-docc/blob/257c62d1ef11efc023667fda89c0cd4419a8afa3/Sources/SwiftDocC/Indexing/Navigator/RenderNode%2BNavigatorIndex.swift#L153 [3]: https://github.com/swiftlang/swift-docc/blob/257c62d1ef11efc023667fda89c0cd4419a8afa3/Sources/SwiftDocC/Infrastructure/Link%20Resolution/ExternalPathHierarchyResolver.swift#L225 [4]: https://github.com/swiftlang/swift-docc-symbolkit/blob/3fc0c6880f712ce38137d0752f24b568fce1522e/Sources/SymbolKit/SymbolGraph/Symbol/Names.swift#L23-L26 [5]: https://github.com/swiftlang/swift-docc/blob/257c62d1ef11efc023667fda89c0cd4419a8afa3/Sources/SwiftDocC/Model/Rendering/DocumentationContentRenderer.swift#L87-L105 --- .../ExternalPathHierarchyResolver.swift | 6 ++- .../LinkTargets/LinkDestinationSummary.swift | 44 ++++++++++++++----- .../Indexing/RenderIndexTests.swift | 2 +- .../LinkDestinationSummaryTests.swift | 38 ++++++++++++++-- .../clang/MixedLanguageFramework.symbols.json | 6 +++ 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index 836938d500..d901e08579 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift @@ -172,6 +172,7 @@ extension LinkDestinationSummary { var titleVariants = VariantCollection(defaultValue: title) var abstractVariants = VariantCollection(defaultValue: abstract ?? []) var fragmentVariants = VariantCollection(defaultValue: declarationFragments) + var navigatorTitleVariants = VariantCollection(defaultValue: navigatorTitle) for variant in variants { let traits = variant.traits @@ -184,6 +185,9 @@ extension LinkDestinationSummary { if let fragment = variant.declarationFragments { fragmentVariants.variants.append(.init(traits: traits, patch: [.replace(value: fragment)])) } + if let navigatorTitle = variant.navigatorTitle { + navigatorTitleVariants.variants.append(.init(traits: traits, patch: [.replace(value: navigatorTitle)])) + } } return TopicRenderReference( @@ -195,7 +199,7 @@ extension LinkDestinationSummary { required: false, role: role, fragmentsVariants: fragmentVariants, - navigatorTitleVariants: .init(defaultValue: nil), + navigatorTitleVariants: navigatorTitleVariants, estimatedTime: nil, conformance: nil, isBeta: isBeta, diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 09dd02d4b6..1f9c87ccdf 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -140,9 +140,12 @@ public struct LinkDestinationSummary: Codable, Equatable { /// The rendered fragments of a symbol's declaration. public typealias DeclarationFragments = [DeclarationRenderSection.Token] - /// The abbreviated fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. + /// The abbreviated fragments for this symbol's declaration, to display in links, or `nil` if the summarized element isn't a symbol. public let declarationFragments: DeclarationFragments? + /// The abbreviated fragments for this symbol's declaration, to display in navigation, or `nil` if the summarized element isn't a symbol. + public let navigatorTitle: DeclarationFragments? + /// Any previous URLs for this element. /// /// A web server can use this list of URLs to redirect to the current URL. @@ -201,11 +204,16 @@ public struct LinkDestinationSummary: Codable, Equatable { /// If the summarized element has a full name but the variant doesn't, this property will be `Optional.some(nil)`. public let fullName: VariantValue - /// The abbreviated declaration of the variant or `nil` if the declaration is the same as the summarized element. + /// The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. /// /// If the summarized element has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. public let declarationFragments: VariantValue + /// The abbreviated declaration for this symbol's declaration, to display in navigation, or `nil` if the navigator title is the same as the summarized element. + /// + /// If the summarized element has a navigator title but the variant doesn't, this property will be `Optional.some(nil)`. + public let navigatorTitle: VariantValue + /// Images that are used to represent the summarized element or `nil` if the images are the same as the summarized element. /// /// If the summarized element has an image but the variant doesn't, this property will be `Optional.some(nil)`. @@ -224,7 +232,8 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element. /// - usr: The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element. /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. - /// - declarationFragments: The abbreviated declaration of the variant or `nil` if the declaration is the same as the summarized element. + /// - declarationFragments: The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. + /// - navigatorTitle: The abbreviated declaration for this symbol's declaration, to display in navigation, or `nil` if the declaration is the same as the summarized element. public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -235,7 +244,8 @@ public struct LinkDestinationSummary: Codable, Equatable { taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, fullName: VariantValue = nil, - declarationFragments: VariantValue = nil + declarationFragments: VariantValue = nil, + navigatorTitle: VariantValue = nil ) { self.traits = traits self.kind = kind @@ -247,9 +257,10 @@ public struct LinkDestinationSummary: Codable, Equatable { self.usr = usr self.fullName = fullName self.declarationFragments = declarationFragments + self.navigatorTitle = navigatorTitle } - @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") + @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:navigatorTitle:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:navigatorTitle:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -261,6 +272,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: VariantValue = nil, fullName: VariantValue = nil, declarationFragments: VariantValue = nil, + navigatorTitle: VariantValue = nil, topicImages: VariantValue<[TopicImage]?> = nil ) { self.init( @@ -273,7 +285,8 @@ public struct LinkDestinationSummary: Codable, Equatable { taskGroups: taskGroups, usr: usr, fullName: fullName, - declarationFragments: declarationFragments + declarationFragments: declarationFragments, + navigatorTitle: navigatorTitle ) } } @@ -295,7 +308,8 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The reference URLs of the summarized element's children, grouped by their task groups. /// - usr: The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. - /// - declarationFragments: The abbreviated fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. + /// - declarationFragments: The abbreviated fragments for this symbol's declaration, to display in links, or `nil` if the summarized element isn't a symbol. + /// - navigatorTitle: The abbreviated fragments for this symbol's declaration, to display in navigation, or `nil` if the summarized element isn't a symbol. /// - redirects: Any previous URLs for this element, or `nil` if this element has no previous URLs. /// - topicImages: Images that are used to represent the summarized element, or `nil` if this element has no topic images. /// - references: References used in the content of the summarized element, or `nil` if this element has no references to other content. @@ -312,6 +326,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: String? = nil, fullName: String? = nil, declarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + navigatorTitle: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, references: [any RenderReference]? = nil, @@ -329,6 +344,7 @@ public struct LinkDestinationSummary: Codable, Equatable { self.usr = usr self.fullName = fullName self.declarationFragments = declarationFragments + self.navigatorTitle = navigatorTitle self.redirects = redirects self.topicImages = topicImages self.references = references @@ -492,6 +508,7 @@ extension LinkDestinationSummary { // However if no abbreviated declaration fragments are available, use the full declaration fragments instead. // In this case, they are assumed to be the same. let declaration = renderNode.metadata.fragmentsVariants.value(for: language) ?? (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() + let navigatorTitle = renderNode.metadata.navigatorTitleVariants.value(for: language) let variants: [Variant] = documentationNode.availableVariantTraits.compactMap { trait in // Skip the variant for the summarized elements source language. @@ -516,6 +533,7 @@ extension LinkDestinationSummary { // However if no abbreviated declaration fragments are available, use the full declaration fragments instead. // In this case, they are assumed to be the same. let declarationVariant = renderNode.metadata.fragmentsVariants.value(for: variantTraits) ?? symbol.declarationVariants[trait]?.renderDeclarationTokens() + let navigatorTitleVariant = renderNode.metadata.navigatorTitleVariants.value(for: variantTraits) return Variant( traits: variantTraits, kind: nilIfEqual(main: kind, variant: symbol.kindVariants[trait].map { DocumentationNode.kind(forKind: $0.identifier) }), @@ -526,7 +544,8 @@ extension LinkDestinationSummary { taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR fullName: nilIfEqual(main: fullName, variant: fullNameVariant), - declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant) + declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant), + navigatorTitle: nilIfEqual(main: navigatorTitle, variant: navigatorTitleVariant) ) } @@ -547,6 +566,7 @@ extension LinkDestinationSummary { usr: usr, fullName: fullName, declarationFragments: declaration, + navigatorTitle: navigatorTitle, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -628,7 +648,7 @@ extension LinkDestinationSummary { // Add Codable methods—which include an initializer—in an extension so that it doesn't override the member-wise initializer. extension LinkDestinationSummary { enum CodingKeys: String, CodingKey { - case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, fullName + case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, fullName, navigatorTitle case relativePresentationURL = "path" case declarationFragments = "fragments" } @@ -662,6 +682,7 @@ extension LinkDestinationSummary { try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(fullName, forKey: .fullName) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) try container.encodeIfPresent(redirects, forKey: .redirects) try container.encodeIfPresent(topicImages, forKey: .topicImages) try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references) @@ -719,6 +740,7 @@ extension LinkDestinationSummary { usr = try container.decodeIfPresent(String.self, forKey: .usr) fullName = try container.decodeIfPresent(String.self, forKey: .fullName) declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments) + navigatorTitle = try container.decodeIfPresent(DeclarationFragments.self, forKey: .navigatorTitle) redirects = try container.decodeIfPresent([URL].self, forKey: .redirects) topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) references = try container.decodeIfPresent([CodableRenderReference].self, forKey: .references).map { decodedReferences in @@ -731,7 +753,7 @@ extension LinkDestinationSummary { extension LinkDestinationSummary.Variant { enum CodingKeys: String, CodingKey { - case traits, kind, title, abstract, language, usr, taskGroups, fullName + case traits, kind, title, abstract, language, usr, taskGroups, fullName, navigatorTitle case relativePresentationURL = "path" case declarationFragments = "fragments" } @@ -759,6 +781,7 @@ extension LinkDestinationSummary.Variant { try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(fullName, forKey: .fullName) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) } @@ -801,6 +824,7 @@ extension LinkDestinationSummary.Variant { usr = try container.decodeIfPresent(String?.self, forKey: .usr) fullName = try container.decodeIfPresent(String?.self, forKey: .fullName) declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) + navigatorTitle = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .navigatorTitle) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) } } diff --git a/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift b/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift index 0481fb8237..acc19fa5a6 100644 --- a/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift +++ b/Tests/SwiftDocCTests/Indexing/RenderIndexTests.swift @@ -89,7 +89,7 @@ final class RenderIndexTests: XCTestCase { }, { "path": "/documentation/mixedlanguageframework/bar", - "title": "Bar", + "title": "Bar (objective c)", "type": "class", "children": [ { diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index cba65bbf74..d697b02a28 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -115,6 +115,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertNil(pageSummary.usr, "Only symbols have USRs") XCTAssertNil(pageSummary.fullName, "Only symbols have full names") XCTAssertNil(pageSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(pageSummary.navigatorTitle, "Only symbols have navigator titles") XCTAssertNil(pageSummary.abstract, "There is no text to use as an abstract for the tutorial page") XCTAssertNil(pageSummary.topicImages, "The tutorial page doesn't have any topic images") XCTAssertNil(pageSummary.references, "Since the tutorial page doesn't have any topic images it also doesn't have any references") @@ -134,6 +135,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertNil(sectionSummary.usr, "Only symbols have USRs") XCTAssertNil(sectionSummary.fullName, "Only symbols have full names") XCTAssertNil(sectionSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(sectionSummary.navigatorTitle, "Only symbols have navigator titles") XCTAssertEqual(sectionSummary.abstract, [ .text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt"), .text(" "), @@ -188,6 +190,9 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " ", kind: .text, identifier: nil), .init(text: "MyClass", kind: .identifier, identifier: nil), ]) + XCTAssertEqual(summary.navigatorTitle, [ + .init(text: "MyClassNavigator", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -230,6 +235,9 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " : ", kind: .text, identifier: nil), .init(text: "Hashable", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "p:hPP"), ]) + XCTAssertEqual(summary.navigatorTitle, [ + .init(text: "MyProtocol", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -266,6 +274,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) + XCTAssertNil(summary.navigatorTitle, "This symbol doesn't have a navigator title") XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -303,6 +312,18 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "Int", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:Si"), .init(text: ")", kind: .text, identifier: nil) ]) + XCTAssertEqual(summary.navigatorTitle, [ + .init(text: "func", kind: .keyword, identifier: nil), + .init(text: " ", kind: .text, identifier: nil), + .init(text: "globalFunction", kind: .identifier, identifier: nil), + .init(text: "(", kind: .text, identifier: nil), + .init(text: "Data", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:10Foundation4DataV"), + .init(text: ", ", kind: .text, identifier: nil), + .init(text: "considering", kind: .identifier, identifier: nil), + .init(text: ": ", kind: .text, identifier: nil), + .init(text: "Int", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:Si"), + .init(text: ")", kind: .text, identifier: nil) + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -358,7 +379,8 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) - + XCTAssertNil(summary.navigatorTitle, "This symbol doesn't have a navigator title") + XCTAssertEqual(summary.topicImages, [ TopicImage( type: .card, @@ -465,6 +487,9 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil) ]) + XCTAssertEqual(summary.navigatorTitle, [ + .init(text: "Bar", kind: .identifier, identifier: nil) + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -481,7 +506,10 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " : ", kind: .text, identifier: nil), .init(text: "NSObject", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSObject"), ]) - + XCTAssertEqual(variant.navigatorTitle, [ + .init(text: "Bar (objective c)", kind: .identifier, identifier: nil), + ]) + // Check variant content that is the same as the summarized element XCTAssertEqual(variant.title, nil) XCTAssertEqual(variant.abstract, nil) @@ -534,6 +562,9 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " -> ", kind: .text, identifier: nil), .init(text: "String", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:SS") ]) + XCTAssertEqual(summary.navigatorTitle, [ + .init(text: "myStringFunction:error: (navigator title)", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -548,7 +579,8 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "+ ", kind: .text, identifier: nil), .init(text: "myStringFunction:error:", kind: .identifier, identifier: nil) ]) - + XCTAssertEqual(variant.navigatorTitle, .none, "Navigator title is the same across variants") + // Check variant content that is the same as the summarized element XCTAssertEqual(variant.abstract, nil) XCTAssertEqual(variant.usr, nil) diff --git a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json index 762bec59b2..a845cec637 100644 --- a/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json +++ b/Tests/SwiftDocCTests/Test Bundles/MixedLanguageFramework.docc/symbol-graphs/clang/MixedLanguageFramework.symbols.json @@ -136,6 +136,12 @@ }, "names" : { "title" : "Bar", + "navigator": [ + { + "kind": "identifier", + "spelling": "Bar (objective c)" + } + ], "subHeading" : [ { "kind" : "keyword", From 7cddb9ee9279b88f6d68c0dd474346c9b86f3f3c Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:12:06 +0100 Subject: [PATCH 06/11] Add navigator title tests for external links in the navigator Now that the navigator title is supported in external references, we can add some tests which verify this logic is working as expected. Added some tests which verify that the navigator title is used as expected for both Swift and Objective C symbols. --- .../Indexing/ExternalRenderNodeTests.swift | 110 ++++++++++++++++++ .../TestExternalReferenceResolvers.swift | 2 + 2 files changed, 112 insertions(+) diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift index d48cd5ece8..fdcc490a5b 100644 --- a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift @@ -65,6 +65,42 @@ class ExternalRenderNodeTests: XCTestCase { platforms: [.init(name: "macOS", introduced: nil, isBeta: false)] ) ) + externalResolver.entitiesToReturn["/path/to/external/navigatorTitleSwiftSymbol"] = .success( + .init( + referencePath: "/path/to/external/navigatorTitleSwiftSymbol", + title: "NavigatorTitleSwiftSymbol (title)", + kind: .class, + language: .swift, + declarationFragments: .init(declarationFragments: [ + .init(kind: .keyword, spelling: "class", preciseIdentifier: nil), + .init(kind: .text, spelling: " ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "NavigatorTitleSwiftSymbol", preciseIdentifier: nil) + ]), + navigatorTitle: .init(declarationFragments: [ + .init(kind: .identifier, spelling: "NavigatorTitleSwiftSymbol (navigator title)", preciseIdentifier: nil) + ]), + platforms: [.init(name: "iOS", introduced: nil, isBeta: true)] + ) + ) + externalResolver.entitiesToReturn["/path/to/external/navigatorTitleObjCSymbol"] = .success( + .init( + referencePath: "/path/to/external/navigatorTitleObjCSymbol", + title: "NavigatorTitleObjCSymbol (title)", + kind: .function, + language: .objectiveC, + declarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "- ", preciseIdentifier: nil), + .init(kind: .text, spelling: "(", preciseIdentifier: nil), + .init(kind: .typeIdentifier, spelling: "void", preciseIdentifier: nil), + .init(kind: .text, spelling: ") ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "ObjCSymbol", preciseIdentifier: nil) + ]), + navigatorTitle: .init(declarationFragments: [ + .init(kind: .identifier, spelling: "NavigatorTitleObjCSymbol (navigator title)", preciseIdentifier: nil) + ]), + platforms: [.init(name: "macOS", introduced: nil, isBeta: false)] + ) + ) return externalResolver } @@ -266,6 +302,80 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(objcSymbolExternalNode.type, "func") } + func testNavigatorWithExternalNodesWithNavigatorTitle() async throws { + let catalog = Folder(name: "ModuleName.docc", content: [ + Folder(name: "swift", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .swift, kind: .class, pathComponents: ["SomeClass"]) + ])) + ]), + Folder(name: "clang", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph(moduleName: "ModuleName", symbols: [ + makeSymbol(id: "some-symbol-id", language: .objectiveC, kind: .class, pathComponents: ["TLASomeClass"]) + ])) + ]), + + InfoPlist(identifier: "some.custom.identifier"), + + TextFile(name: "ModuleName.md", utf8Content: """ + # ``ModuleName`` + + Curate a few external language-specific symbols and articles + + ## Topics + + ### External Reference + + - + - + """), + ]) + + var configuration = DocumentationContext.Configuration() + let externalResolver = generateExternalResolver() + configuration.externalDocumentationConfiguration.sources[externalResolver.bundleID] = externalResolver + let (bundle, context) = try await loadBundle(catalog: catalog, configuration: configuration) + XCTAssert(context.problems.isEmpty, "Encountered unexpected problems: \(context.problems.map(\.diagnostic.summary))") + + let renderContext = RenderContext(documentationContext: context, bundle: bundle) + let converter = DocumentationContextConverter(bundle: bundle, context: context, renderContext: renderContext) + let targetURL = try createTemporaryDirectory() + let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: bundle.id.rawValue, sortRootChildrenByName: true, groupByLanguage: true) + builder.setup() + for externalLink in context.externalCache { + let externalRenderNode = ExternalRenderNode(externalEntity: externalLink.value, bundleIdentifier: bundle.id) + try builder.index(renderNode: externalRenderNode) + } + for identifier in context.knownPages { + let entity = try context.entity(with: identifier) + let renderNode = try XCTUnwrap(converter.renderNode(for: entity)) + try builder.index(renderNode: renderNode) + } + builder.finalize() + let renderIndex = try RenderIndex.fromURL(targetURL.appendingPathComponent("index.json")) + + // Verify that there are no uncurated external links at the top level + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.count(where: \.isExternal), 0) + XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) + + func externalNodes(by language: SourceLanguage) -> [RenderIndex.Node]? { + renderIndex.interfaceLanguages[language.id]?.first?.children?.filter(\.isExternal) + } + + // Verify that the curated external links are part of the index. + let swiftExternalNodes = try XCTUnwrap(externalNodes(by: .swift)) + let objcExternalNodes = try XCTUnwrap(externalNodes(by: .objectiveC)) + + XCTAssertEqual(swiftExternalNodes.count, 1) + XCTAssertEqual(objcExternalNodes.count, 1) + + let swiftSymbolExternalNode = try XCTUnwrap(swiftExternalNodes.first) + let objcSymbolExternalNode = try XCTUnwrap(objcExternalNodes.first) + + XCTAssertEqual(swiftSymbolExternalNode.title, "NavigatorTitleSwiftSymbol (title)") // Swift types prefer not using the navigator title where possible + XCTAssertEqual(objcSymbolExternalNode.title, "NavigatorTitleObjCSymbol (navigator title)") // Objective C types prefer using the navigator title where possible + } + func testNavigatorWithExternalNodesOnlyAddsCuratedNodesToNavigator() async throws { let catalog = Folder(name: "ModuleName.docc", content: [ Folder(name: "swift", content: [ diff --git a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift index c8cf21c922..3f1d629a19 100644 --- a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift +++ b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift @@ -28,6 +28,7 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { var kind = DocumentationNode.Kind.article var language = SourceLanguage.swift var declarationFragments: SymbolGraph.Symbol.DeclarationFragments? = nil + var navigatorTitle: SymbolGraph.Symbol.DeclarationFragments? = nil var topicImages: [(TopicImage, alt: String)]? = nil var platforms: [AvailabilityRenderItem]? = nil } @@ -93,6 +94,7 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { availableLanguages: [entityInfo.language], platforms: entityInfo.platforms, declarationFragments: entityInfo.declarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + navigatorTitle: entityInfo.navigatorTitle?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, topicImages: entityInfo.topicImages?.map(\.0), references: entityInfo.topicImages?.map { topicImage, altText in ImageReference(identifier: topicImage.identifier, altText: altText, imageAsset: assetsToReturn[topicImage.identifier.identifier] ?? .init()) From 04867f13ded853aa75313476e8c1ce924f7834f8 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:20:15 +0100 Subject: [PATCH 07/11] Rename `fullName` to `plainTextDeclaration` in LinkDestinationSummary `plainTextDeclaration` is clearer and makes it harder to confuse it with `title`. It is also closer to how the value is derived. Also updates the JSON schema in `LinkableEntities.json` to add the new `plainTextDeclaration` property to the `LinkDestinationSummary` and `LinkDestinationSummaryVariant` schemas. --- .../ExternalPathHierarchyResolver.swift | 8 +-- .../LinkTargets/LinkDestinationSummary.swift | 60 +++++++++---------- .../Resources/LinkableEntities.json | 7 +++ .../LinkDestinationSummaryTests.swift | 22 +++---- 4 files changed, 49 insertions(+), 48 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index d901e08579..fd4e45bbde 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift @@ -58,13 +58,13 @@ final class ExternalPathHierarchyResolver { return collidingNode.name } if let symbolID = collidingNode.symbol?.identifier { - if symbolID.interfaceLanguage == summary.language.id, let fullName = summary.fullName { - return fullName + if symbolID.interfaceLanguage == summary.language.id, let plainTextDeclaration = summary.plainTextDeclaration { + return plainTextDeclaration } if let variant = summary.variants.first(where: { $0.traits.contains(.interfaceLanguage(symbolID.interfaceLanguage)) }), - let fullName = variant.fullName ?? summary.fullName + let plainTextDeclaration = variant.plainTextDeclaration ?? summary.plainTextDeclaration { - return fullName + return plainTextDeclaration } } return summary.title diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 1f9c87ccdf..6895bf5a5f 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -135,8 +135,8 @@ public struct LinkDestinationSummary: Codable, Equatable { /// The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. public let usr: String? - /// The full name of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. - public let fullName: String? + /// The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. + public let plainTextDeclaration: String? /// The rendered fragments of a symbol's declaration. public typealias DeclarationFragments = [DeclarationRenderSection.Token] @@ -199,10 +199,10 @@ public struct LinkDestinationSummary: Codable, Equatable { /// If the summarized element has a precise symbol identifier but the variant doesn't, this property will be `Optional.some(nil)`. public let usr: VariantValue - /// The full name of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. + /// The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. /// - /// If the summarized element has a full name but the variant doesn't, this property will be `Optional.some(nil)`. - public let fullName: VariantValue + /// If the summarized element has a plain text declaration but the variant doesn't, this property will be `Optional.some(nil)`. + public let plainTextDeclaration: VariantValue /// The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. /// @@ -231,7 +231,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - abstract: The abstract of the variant or `nil` if the abstract is the same as the summarized element. /// - taskGroups: The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element. /// - usr: The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element. - /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. + /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. /// - declarationFragments: The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. /// - navigatorTitle: The abbreviated declaration for this symbol's declaration, to display in navigation, or `nil` if the declaration is the same as the summarized element. public init( @@ -243,7 +243,7 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: VariantValue = nil, taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, - fullName: VariantValue = nil, + plainTextDeclaration: VariantValue = nil, declarationFragments: VariantValue = nil, navigatorTitle: VariantValue = nil ) { @@ -255,12 +255,12 @@ public struct LinkDestinationSummary: Codable, Equatable { self.abstract = abstract self.taskGroups = taskGroups self.usr = usr - self.fullName = fullName + self.plainTextDeclaration = plainTextDeclaration self.declarationFragments = declarationFragments self.navigatorTitle = navigatorTitle } - @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:navigatorTitle:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:fullName:declarationFragments:navigatorTitle:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") + @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:declarationFragments:navigatorTitle:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:declarationFragments:navigatorTitle:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -270,7 +270,7 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: VariantValue = nil, taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, - fullName: VariantValue = nil, + plainTextDeclaration: VariantValue = nil, declarationFragments: VariantValue = nil, navigatorTitle: VariantValue = nil, topicImages: VariantValue<[TopicImage]?> = nil @@ -284,7 +284,7 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: abstract, taskGroups: taskGroups, usr: usr, - fullName: fullName, + plainTextDeclaration: plainTextDeclaration, declarationFragments: declarationFragments, navigatorTitle: navigatorTitle ) @@ -307,7 +307,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - platforms: Information about the platforms for which the summarized element is available. /// - taskGroups: The reference URLs of the summarized element's children, grouped by their task groups. /// - usr: The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. - /// - fullName: The full name of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. + /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. /// - declarationFragments: The abbreviated fragments for this symbol's declaration, to display in links, or `nil` if the summarized element isn't a symbol. /// - navigatorTitle: The abbreviated fragments for this symbol's declaration, to display in navigation, or `nil` if the summarized element isn't a symbol. /// - redirects: Any previous URLs for this element, or `nil` if this element has no previous URLs. @@ -324,7 +324,7 @@ public struct LinkDestinationSummary: Codable, Equatable { platforms: [LinkDestinationSummary.PlatformAvailability]? = nil, taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, usr: String? = nil, - fullName: String? = nil, + plainTextDeclaration: String? = nil, declarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, navigatorTitle: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, @@ -342,7 +342,7 @@ public struct LinkDestinationSummary: Codable, Equatable { self.platforms = platforms self.taskGroups = taskGroups self.usr = usr - self.fullName = fullName + self.plainTextDeclaration = plainTextDeclaration self.declarationFragments = declarationFragments self.navigatorTitle = navigatorTitle self.redirects = redirects @@ -498,7 +498,7 @@ extension LinkDestinationSummary { let abstract = renderSymbolAbstract(symbol.abstractVariants[summaryTrait] ?? symbol.abstract) let usr = symbol.externalIDVariants[summaryTrait] ?? symbol.externalID - let fullName = symbol.fullName(for: summaryTrait) + let plainTextDeclaration = symbol.plainTextDeclaration(for: summaryTrait) let language = documentationNode.sourceLanguage // Use the abbreviated declaration fragments instead of the full declaration fragments. // These have been derived from the symbol's subheading declaration fragments as part of rendering. @@ -522,7 +522,7 @@ extension LinkDestinationSummary { return main == variant ? nil : variant } - let fullNameVariant = symbol.fullName(for: trait) + let plainTextDeclarationVariant = symbol.plainTextDeclaration(for: trait) let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage(interfaceLanguage)] // Use the abbreviated declaration fragments instead of the full declaration fragments. @@ -543,7 +543,7 @@ extension LinkDestinationSummary { abstract: nilIfEqual(main: abstract, variant: abstractVariant), taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR - fullName: nilIfEqual(main: fullName, variant: fullNameVariant), + plainTextDeclaration: nilIfEqual(main: plainTextDeclaration, variant: plainTextDeclarationVariant), declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant), navigatorTitle: nilIfEqual(main: navigatorTitle, variant: navigatorTitleVariant) ) @@ -564,7 +564,7 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: usr, - fullName: fullName, + plainTextDeclaration: plainTextDeclaration, declarationFragments: declaration, navigatorTitle: navigatorTitle, redirects: redirects, @@ -648,7 +648,7 @@ extension LinkDestinationSummary { // Add Codable methods—which include an initializer—in an extension so that it doesn't override the member-wise initializer. extension LinkDestinationSummary { enum CodingKeys: String, CodingKey { - case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, fullName, navigatorTitle + case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, plainTextDeclaration, navigatorTitle case relativePresentationURL = "path" case declarationFragments = "fragments" } @@ -680,7 +680,7 @@ extension LinkDestinationSummary { try container.encodeIfPresent(platforms, forKey: .platforms) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) try container.encodeIfPresent(usr, forKey: .usr) - try container.encodeIfPresent(fullName, forKey: .fullName) + try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) try container.encodeIfPresent(redirects, forKey: .redirects) @@ -738,7 +738,7 @@ extension LinkDestinationSummary { platforms = try container.decodeIfPresent([AvailabilityRenderItem].self, forKey: .platforms) taskGroups = try container.decodeIfPresent([TaskGroup].self, forKey: .taskGroups) usr = try container.decodeIfPresent(String.self, forKey: .usr) - fullName = try container.decodeIfPresent(String.self, forKey: .fullName) + plainTextDeclaration = try container.decodeIfPresent(String.self, forKey: .plainTextDeclaration) declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments) navigatorTitle = try container.decodeIfPresent(DeclarationFragments.self, forKey: .navigatorTitle) redirects = try container.decodeIfPresent([URL].self, forKey: .redirects) @@ -753,7 +753,7 @@ extension LinkDestinationSummary { extension LinkDestinationSummary.Variant { enum CodingKeys: String, CodingKey { - case traits, kind, title, abstract, language, usr, taskGroups, fullName, navigatorTitle + case traits, kind, title, abstract, language, usr, taskGroups, plainTextDeclaration, navigatorTitle case relativePresentationURL = "path" case declarationFragments = "fragments" } @@ -779,7 +779,7 @@ extension LinkDestinationSummary.Variant { } } try container.encodeIfPresent(usr, forKey: .usr) - try container.encodeIfPresent(fullName, forKey: .fullName) + try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) @@ -822,7 +822,7 @@ extension LinkDestinationSummary.Variant { title = try container.decodeIfPresent(String.self, forKey: .title) abstract = try container.decodeIfPresent(LinkDestinationSummary.Abstract?.self, forKey: .abstract) usr = try container.decodeIfPresent(String?.self, forKey: .usr) - fullName = try container.decodeIfPresent(String?.self, forKey: .fullName) + plainTextDeclaration = try container.decodeIfPresent(String?.self, forKey: .plainTextDeclaration) declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) navigatorTitle = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .navigatorTitle) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) @@ -934,15 +934,9 @@ private extension Collection { } } -private extension Sequence { - func plainTextDeclaration() -> String { - return self.map(\.text).joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ") - } -} - private extension Symbol { - func fullName(for trait: DocumentationDataVariantsTrait) -> String? { - let fullDeclaration = (self.declarationVariants[trait] ?? self.declaration).renderDeclarationTokens() - return fullDeclaration?.plainTextDeclaration() + func plainTextDeclaration(for trait: DocumentationDataVariantsTrait) -> String? { + guard let fullDeclaration = (self.declarationVariants[trait] ?? self.declaration).mainRenderFragments() else { return nil } + return fullDeclaration.declarationFragments.map(\.spelling).joined().split(whereSeparator: { $0.isWhitespace || $0.isNewline }).joined(separator: " ") } } diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json index 6f156a7423..02f111476e 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json @@ -68,6 +68,9 @@ "usr": { "type": "string" }, + "plainTextDeclaration": { + "type": "string" + }, "fragments": { "type": "array", "items": { @@ -158,6 +161,10 @@ "type": "string", "nullable": true }, + "plainTextDeclaration": { + "type": "string", + "nullable": true + }, "fragments": { "type": "array", "items": { diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index d697b02a28..b8846daeaf 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -113,7 +113,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(pageSummary.platforms, renderNode.metadata.platforms) XCTAssertEqual(pageSummary.redirects, nil) XCTAssertNil(pageSummary.usr, "Only symbols have USRs") - XCTAssertNil(pageSummary.fullName, "Only symbols have full names") + XCTAssertNil(pageSummary.plainTextDeclaration, "Only symbols have a plain text declaration") XCTAssertNil(pageSummary.declarationFragments, "Only symbols have declaration fragments") XCTAssertNil(pageSummary.navigatorTitle, "Only symbols have navigator titles") XCTAssertNil(pageSummary.abstract, "There is no text to use as an abstract for the tutorial page") @@ -133,7 +133,7 @@ class LinkDestinationSummaryTests: XCTestCase { URL(string: "old/path/to/this/landmark")!, ]) XCTAssertNil(sectionSummary.usr, "Only symbols have USRs") - XCTAssertNil(sectionSummary.fullName, "Only symbols have full names") + XCTAssertNil(sectionSummary.plainTextDeclaration, "Only symbols have a plain text declaration") XCTAssertNil(sectionSummary.declarationFragments, "Only symbols have declaration fragments") XCTAssertNil(sectionSummary.navigatorTitle, "Only symbols have navigator titles") XCTAssertEqual(sectionSummary.abstract, [ @@ -184,7 +184,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC") - XCTAssertEqual(summary.fullName, "class MyClass") + XCTAssertEqual(summary.plainTextDeclaration, "class MyClass") XCTAssertEqual(summary.declarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -227,7 +227,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ProtocolP") - XCTAssertEqual(summary.fullName, "protocol MyProtocol : Hashable") + XCTAssertEqual(summary.plainTextDeclaration, "protocol MyProtocol : Hashable") XCTAssertEqual(summary.declarationFragments, [ .init(text: "protocol", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -262,7 +262,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") - XCTAssertEqual(summary.fullName, "func myFunction(for name...)") + XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") XCTAssertEqual(summary.declarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -299,7 +299,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit14globalFunction_11consideringy10Foundation4DataV_SitF") - XCTAssertEqual(summary.fullName, "func globalFunction(_: Data, considering: Int)") + XCTAssertEqual(summary.plainTextDeclaration, "func globalFunction(_: Data, considering: Int)") XCTAssertEqual(summary.declarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -367,7 +367,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") - XCTAssertEqual(summary.fullName, "func myFunction(for name...)") + XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") XCTAssertEqual(summary.declarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -481,7 +481,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages.sorted(), [.swift, .objectiveC]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar") - XCTAssertEqual(summary.fullName, "class Bar") + XCTAssertEqual(summary.plainTextDeclaration, "class Bar") XCTAssertEqual(summary.declarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -498,7 +498,7 @@ class LinkDestinationSummaryTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) - XCTAssertEqual(variant.fullName, "@interface Bar : NSObject") + XCTAssertEqual(variant.plainTextDeclaration, "@interface Bar : NSObject") XCTAssertEqual(variant.declarationFragments, [ .init(text: "@interface", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -548,7 +548,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages.sorted(), [.swift, .objectiveC]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar(cm)myStringFunction:error:") - XCTAssertEqual(summary.fullName, "class func myStringFunction(_ string: String) throws -> String") + XCTAssertEqual(summary.plainTextDeclaration, "class func myStringFunction(_ string: String) throws -> String") XCTAssertEqual(summary.declarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), @@ -574,7 +574,7 @@ class LinkDestinationSummaryTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) XCTAssertEqual(variant.title, "myStringFunction:error:") - XCTAssertEqual(variant.fullName, "+ (NSString *) myStringFunction: (NSString *)string error: (NSError **)error;") + XCTAssertEqual(variant.plainTextDeclaration, "+ (NSString *) myStringFunction: (NSString *)string error: (NSError **)error;") XCTAssertEqual(variant.declarationFragments, [ .init(text: "+ ", kind: .text, identifier: nil), .init(text: "myStringFunction:error:", kind: .identifier, identifier: nil) From fbc7a8a222c2795a9a7315d4431d05083a85cd78 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:12:22 +0100 Subject: [PATCH 08/11] Rename to `subheadingDeclarationFragments` for clarity `declarationFragments` was not descriptive enough about the contents of the property, so renamed to `subheadingDeclarationFragments` and deprecated `declarationFragments` for backwards compatibility. The JSON encoding key is kept the same (`fragments`) for backwards compatibility. --- .../ExternalPathHierarchyResolver.swift | 4 +- .../LinkTargets/LinkDestinationSummary.swift | 121 +++++++++++++----- .../Indexing/ExternalRenderNodeTests.swift | 8 +- .../ExternalReferenceResolverTests.swift | 2 +- .../TestExternalReferenceResolvers.swift | 2 +- .../LinkDestinationSummaryTests.swift | 24 ++-- ...OutOfProcessReferenceResolverV2Tests.swift | 10 +- .../ConvertActionTests.swift | 2 +- 8 files changed, 113 insertions(+), 60 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index fd4e45bbde..cb3b9db182 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift @@ -171,7 +171,7 @@ extension LinkDestinationSummary { var titleVariants = VariantCollection(defaultValue: title) var abstractVariants = VariantCollection(defaultValue: abstract ?? []) - var fragmentVariants = VariantCollection(defaultValue: declarationFragments) + var fragmentVariants = VariantCollection(defaultValue: subheadingDeclarationFragments) var navigatorTitleVariants = VariantCollection(defaultValue: navigatorTitle) for variant in variants { @@ -182,7 +182,7 @@ extension LinkDestinationSummary { if let abstract = variant.abstract { abstractVariants.variants.append(.init(traits: traits, patch: [.replace(value: abstract ?? [])])) } - if let fragment = variant.declarationFragments { + if let fragment = variant.subheadingDeclarationFragments { fragmentVariants.variants.append(.init(traits: traits, patch: [.replace(value: fragment)])) } if let navigatorTitle = variant.navigatorTitle { diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 6895bf5a5f..a5d4165bbd 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -140,10 +140,25 @@ public struct LinkDestinationSummary: Codable, Equatable { /// The rendered fragments of a symbol's declaration. public typealias DeclarationFragments = [DeclarationRenderSection.Token] - /// The abbreviated fragments for this symbol's declaration, to display in links, or `nil` if the summarized element isn't a symbol. - public let declarationFragments: DeclarationFragments? + /// The simplified "subheading" declaration fragments for this symbol, or `nil` if the summarized element isn't a symbol. + /// + /// These subheading fragments are suitable to use to refer to a symbol that's linked to in a topic group. + /// + /// - Note: The subheading fragments do not represent the symbol's full declaration. + /// Different overloads may have indistinguishable subheading fragments. + public let subheadingDeclarationFragments: DeclarationFragments? - /// The abbreviated fragments for this symbol's declaration, to display in navigation, or `nil` if the summarized element isn't a symbol. + @available(*, deprecated, renamed: "subheadingDeclarationFragments", message: "Use 'subheadingDeclarationFragments' instead. This deprecated API will be removed after 6.3 is released.") + public var declarationFragments: DeclarationFragments? { + subheadingDeclarationFragments + } + + /// The simplified "navigator" declaration fragments for this symbol, or `nil` if the summarized element isn't a symbol. + /// + /// These navigator fragments are suitable to use to refer to a symbol that's linked to in a navigator. + /// + /// - Note: The navigator title does not represent the symbol's full declaration. + /// Different overloads may have indistinguishable subheading fragments. public let navigatorTitle: DeclarationFragments? /// Any previous URLs for this element. @@ -204,12 +219,21 @@ public struct LinkDestinationSummary: Codable, Equatable { /// If the summarized element has a plain text declaration but the variant doesn't, this property will be `Optional.some(nil)`. public let plainTextDeclaration: VariantValue - /// The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. + /// The simplified "subheading" declaration fragments for this symbol, or `nil` if the declaration is the same as the summarized element. + /// + /// These subheading fragments are suitable to use to refer to a symbol that's linked to in a topic group. /// /// If the summarized element has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. - public let declarationFragments: VariantValue + public let subheadingDeclarationFragments: VariantValue - /// The abbreviated declaration for this symbol's declaration, to display in navigation, or `nil` if the navigator title is the same as the summarized element. + @available(*, deprecated, renamed: "subheadingDeclarationFragments", message: "Use 'subheadingDeclarationFragments' instead. This deprecated API will be removed after 6.3 is released.") + public var declarationFragments: VariantValue { + subheadingDeclarationFragments + } + + /// The simplified "navigator" declaration fragments for this symbol, or `nil` if the navigator title is the same as the summarized element. + /// + /// These navigator fragments are suitable to use to refer to a symbol that's linked to in a navigator. /// /// If the summarized element has a navigator title but the variant doesn't, this property will be `Optional.some(nil)`. public let navigatorTitle: VariantValue @@ -232,7 +256,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element. /// - usr: The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element. /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. - /// - declarationFragments: The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. + /// - subheadingDeclarationFragments: The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. /// - navigatorTitle: The abbreviated declaration for this symbol's declaration, to display in navigation, or `nil` if the declaration is the same as the summarized element. public init( traits: [RenderNode.Variant.Trait], @@ -244,7 +268,7 @@ public struct LinkDestinationSummary: Codable, Equatable { taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, plainTextDeclaration: VariantValue = nil, - declarationFragments: VariantValue = nil, + subheadingDeclarationFragments: VariantValue = nil, navigatorTitle: VariantValue = nil ) { self.traits = traits @@ -256,11 +280,11 @@ public struct LinkDestinationSummary: Codable, Equatable { self.taskGroups = taskGroups self.usr = usr self.plainTextDeclaration = plainTextDeclaration - self.declarationFragments = declarationFragments + self.subheadingDeclarationFragments = subheadingDeclarationFragments self.navigatorTitle = navigatorTitle } - @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:declarationFragments:navigatorTitle:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:declarationFragments:navigatorTitle:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") + @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -285,7 +309,7 @@ public struct LinkDestinationSummary: Codable, Equatable { taskGroups: taskGroups, usr: usr, plainTextDeclaration: plainTextDeclaration, - declarationFragments: declarationFragments, + subheadingDeclarationFragments: declarationFragments, navigatorTitle: navigatorTitle ) } @@ -308,7 +332,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The reference URLs of the summarized element's children, grouped by their task groups. /// - usr: The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. - /// - declarationFragments: The abbreviated fragments for this symbol's declaration, to display in links, or `nil` if the summarized element isn't a symbol. + /// - subheadingDeclarationFragments: The simplified "subheading" fragments for this symbol, or `nil` if the summarized element isn't a symbol. /// - navigatorTitle: The abbreviated fragments for this symbol's declaration, to display in navigation, or `nil` if the summarized element isn't a symbol. /// - redirects: Any previous URLs for this element, or `nil` if this element has no previous URLs. /// - topicImages: Images that are used to represent the summarized element, or `nil` if this element has no topic images. @@ -325,7 +349,7 @@ public struct LinkDestinationSummary: Codable, Equatable { taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, usr: String? = nil, plainTextDeclaration: String? = nil, - declarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + subheadingDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, navigatorTitle: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, @@ -343,13 +367,53 @@ public struct LinkDestinationSummary: Codable, Equatable { self.taskGroups = taskGroups self.usr = usr self.plainTextDeclaration = plainTextDeclaration - self.declarationFragments = declarationFragments + self.subheadingDeclarationFragments = subheadingDeclarationFragments self.navigatorTitle = navigatorTitle self.redirects = redirects self.topicImages = topicImages self.references = references self.variants = variants } + + @available(*, deprecated, renamed: "init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:redirects:topicImages:references:variants:)", message: "Use `init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:redirects:topicImages:references:variants:)` instead. This property will be removed after 6.3 is released") + public init( + kind: DocumentationNode.Kind, + language: SourceLanguage, + relativePresentationURL: URL, + referenceURL: URL, title: String, + abstract: LinkDestinationSummary.Abstract? = nil, + availableLanguages: Set, + platforms: [LinkDestinationSummary.PlatformAvailability]? = nil, + taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, + usr: String? = nil, + plainTextDeclaration: String? = nil, + declarationFragments: LinkDestinationSummary.DeclarationFragments?, + navigatorTitle: LinkDestinationSummary.DeclarationFragments? = nil, + redirects: [URL]? = nil, + topicImages: [TopicImage]? = nil, + references: [any RenderReference]? = nil, + variants: [LinkDestinationSummary.Variant] + ) { + self.init( + kind: kind, + language: language, + relativePresentationURL: relativePresentationURL, + referenceURL: referenceURL, + title: title, + abstract: abstract, + availableLanguages: availableLanguages, + platforms: platforms, + taskGroups: taskGroups, + usr: usr, + plainTextDeclaration: plainTextDeclaration, + subheadingDeclarationFragments: declarationFragments, + navigatorTitle: navigatorTitle, + redirects: redirects, + topicImages: topicImages, + references: references, + variants: variants + ) + } } // MARK: - Accessing the externally linkable elements @@ -474,7 +538,7 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: nil, - declarationFragments: nil, + subheadingDeclarationFragments: nil, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -500,14 +564,9 @@ extension LinkDestinationSummary { let usr = symbol.externalIDVariants[summaryTrait] ?? symbol.externalID let plainTextDeclaration = symbol.plainTextDeclaration(for: summaryTrait) let language = documentationNode.sourceLanguage - // Use the abbreviated declaration fragments instead of the full declaration fragments. - // These have been derived from the symbol's subheading declaration fragments as part of rendering. - // We only want an abbreviated version of the declaration in the link summary (for display in Topic sections, the navigator, etc.). - // Otherwise, the declaration would be too verbose. - // - // However if no abbreviated declaration fragments are available, use the full declaration fragments instead. + // If no abbreviated declaration fragments are available, use the full declaration fragments instead. // In this case, they are assumed to be the same. - let declaration = renderNode.metadata.fragmentsVariants.value(for: language) ?? (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() + let subheadingDeclarationFragments = renderNode.metadata.fragmentsVariants.value(for: language) ?? (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() let navigatorTitle = renderNode.metadata.navigatorTitleVariants.value(for: language) let variants: [Variant] = documentationNode.availableVariantTraits.compactMap { trait in @@ -544,7 +603,7 @@ extension LinkDestinationSummary { taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR plainTextDeclaration: nilIfEqual(main: plainTextDeclaration, variant: plainTextDeclarationVariant), - declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant), + subheadingDeclarationFragments: nilIfEqual(main: subheadingDeclarationFragments, variant: declarationVariant), navigatorTitle: nilIfEqual(main: navigatorTitle, variant: navigatorTitleVariant) ) } @@ -565,7 +624,7 @@ extension LinkDestinationSummary { taskGroups: taskGroups, usr: usr, plainTextDeclaration: plainTextDeclaration, - declarationFragments: declaration, + subheadingDeclarationFragments: subheadingDeclarationFragments, navigatorTitle: navigatorTitle, redirects: redirects, topicImages: topicImages.nilIfEmpty, @@ -634,7 +693,7 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: [], // Landmarks have no children usr: nil, // Only symbols have a USR - declarationFragments: nil, // Only symbols have declarations + subheadingDeclarationFragments: nil, // Only symbols have declarations redirects: (landmark as? (any Redirected))?.redirects?.map { $0.oldPath }, topicImages: nil, // Landmarks doesn't have topic images references: nil, // Landmarks have no references, since only topic image references is currently supported @@ -650,7 +709,7 @@ extension LinkDestinationSummary { enum CodingKeys: String, CodingKey { case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, plainTextDeclaration, navigatorTitle case relativePresentationURL = "path" - case declarationFragments = "fragments" + case subheadingDeclarationFragments = "fragments" } public func encode(to encoder: any Encoder) throws { @@ -681,7 +740,7 @@ extension LinkDestinationSummary { try container.encodeIfPresent(taskGroups, forKey: .taskGroups) try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) - try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .subheadingDeclarationFragments) try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) try container.encodeIfPresent(redirects, forKey: .redirects) try container.encodeIfPresent(topicImages, forKey: .topicImages) @@ -739,7 +798,7 @@ extension LinkDestinationSummary { taskGroups = try container.decodeIfPresent([TaskGroup].self, forKey: .taskGroups) usr = try container.decodeIfPresent(String.self, forKey: .usr) plainTextDeclaration = try container.decodeIfPresent(String.self, forKey: .plainTextDeclaration) - declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments) + subheadingDeclarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .subheadingDeclarationFragments) navigatorTitle = try container.decodeIfPresent(DeclarationFragments.self, forKey: .navigatorTitle) redirects = try container.decodeIfPresent([URL].self, forKey: .redirects) topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) @@ -780,7 +839,7 @@ extension LinkDestinationSummary.Variant { } try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) - try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .declarationFragments) try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) } @@ -823,7 +882,7 @@ extension LinkDestinationSummary.Variant { abstract = try container.decodeIfPresent(LinkDestinationSummary.Abstract?.self, forKey: .abstract) usr = try container.decodeIfPresent(String?.self, forKey: .usr) plainTextDeclaration = try container.decodeIfPresent(String?.self, forKey: .plainTextDeclaration) - declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) + subheadingDeclarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) navigatorTitle = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .navigatorTitle) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) } @@ -849,7 +908,7 @@ extension LinkDestinationSummary { guard lhs.availableLanguages == rhs.availableLanguages else { return false } guard lhs.platforms == rhs.platforms else { return false } guard lhs.taskGroups == rhs.taskGroups else { return false } - guard lhs.declarationFragments == rhs.declarationFragments else { return false } + guard lhs.subheadingDeclarationFragments == rhs.subheadingDeclarationFragments else { return false } guard lhs.redirects == rhs.redirects else { return false } guard lhs.topicImages == rhs.topicImages else { return false } guard lhs.variants == rhs.variants else { return false } diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift index fdcc490a5b..a284bc12c1 100644 --- a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift @@ -180,13 +180,13 @@ class ExternalRenderNodeTests: XCTestCase { title: swiftTitle, availableLanguages: [.swift, .objectiveC], usr: "some-unique-symbol-id", - declarationFragments: swiftFragments, + subheadingDeclarationFragments: swiftFragments, variants: [ .init( traits: [.interfaceLanguage(SourceLanguage.objectiveC.id)], language: .objectiveC, title: objcTitle, - declarationFragments: objcFragments + subheadingDeclarationFragments: objcFragments ) ] ) @@ -467,13 +467,13 @@ class ExternalRenderNodeTests: XCTestCase { availableLanguages: [.swift, .objectiveC], platforms: [.init(name: "Platform name", introduced: "1.2.3", isBeta: true)], usr: "some-unique-symbol-id", - declarationFragments: swiftFragments, + subheadingDeclarationFragments: swiftFragments, variants: [ .init( traits: [.interfaceLanguage(SourceLanguage.objectiveC.id)], language: .objectiveC, title: objcTitle, - declarationFragments: objcFragments + subheadingDeclarationFragments: objcFragments ) ] ) diff --git a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift index 6e2526397e..84cb5e7a11 100644 --- a/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift @@ -45,7 +45,7 @@ class ExternalReferenceResolverTests: XCTestCase { referenceURL: reference.url, title: resolvedEntityTitle, availableLanguages: [resolvedEntityLanguage], - declarationFragments: resolvedEntityDeclarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + subheadingDeclarationFragments: resolvedEntityDeclarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, variants: [] ) } diff --git a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift index 3f1d629a19..f1f190afbf 100644 --- a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift +++ b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift @@ -93,7 +93,7 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { title: entityInfo.title, availableLanguages: [entityInfo.language], platforms: entityInfo.platforms, - declarationFragments: entityInfo.declarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + subheadingDeclarationFragments: entityInfo.declarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, navigatorTitle: entityInfo.navigatorTitle?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, topicImages: entityInfo.topicImages?.map(\.0), references: entityInfo.topicImages?.map { topicImage, altText in diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index b8846daeaf..0a17eebe5c 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -114,7 +114,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(pageSummary.redirects, nil) XCTAssertNil(pageSummary.usr, "Only symbols have USRs") XCTAssertNil(pageSummary.plainTextDeclaration, "Only symbols have a plain text declaration") - XCTAssertNil(pageSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(pageSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") XCTAssertNil(pageSummary.navigatorTitle, "Only symbols have navigator titles") XCTAssertNil(pageSummary.abstract, "There is no text to use as an abstract for the tutorial page") XCTAssertNil(pageSummary.topicImages, "The tutorial page doesn't have any topic images") @@ -134,7 +134,7 @@ class LinkDestinationSummaryTests: XCTestCase { ]) XCTAssertNil(sectionSummary.usr, "Only symbols have USRs") XCTAssertNil(sectionSummary.plainTextDeclaration, "Only symbols have a plain text declaration") - XCTAssertNil(sectionSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(sectionSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") XCTAssertNil(sectionSummary.navigatorTitle, "Only symbols have navigator titles") XCTAssertEqual(sectionSummary.abstract, [ .text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt"), @@ -185,7 +185,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC") XCTAssertEqual(summary.plainTextDeclaration, "class MyClass") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "MyClass", kind: .identifier, identifier: nil), @@ -228,7 +228,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ProtocolP") XCTAssertEqual(summary.plainTextDeclaration, "protocol MyProtocol : Hashable") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "protocol", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "MyProtocol", kind: .identifier, identifier: nil), @@ -263,7 +263,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "myFunction", kind: .identifier, identifier: nil), @@ -300,7 +300,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit14globalFunction_11consideringy10Foundation4DataV_SitF") XCTAssertEqual(summary.plainTextDeclaration, "func globalFunction(_: Data, considering: Int)") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "globalFunction", kind: .identifier, identifier: nil), @@ -368,7 +368,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "myFunction", kind: .identifier, identifier: nil), @@ -482,7 +482,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar") XCTAssertEqual(summary.plainTextDeclaration, "class Bar") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil) @@ -499,7 +499,7 @@ class LinkDestinationSummaryTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) XCTAssertEqual(variant.plainTextDeclaration, "@interface Bar : NSObject") - XCTAssertEqual(variant.declarationFragments, [ + XCTAssertEqual(variant.subheadingDeclarationFragments, [ .init(text: "@interface", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil), @@ -549,7 +549,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar(cm)myStringFunction:error:") XCTAssertEqual(summary.plainTextDeclaration, "class func myStringFunction(_ string: String) throws -> String") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "func", kind: .keyword, identifier: nil), @@ -575,7 +575,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(variant.language, .objectiveC) XCTAssertEqual(variant.title, "myStringFunction:error:") XCTAssertEqual(variant.plainTextDeclaration, "+ (NSString *) myStringFunction: (NSString *)string error: (NSError **)error;") - XCTAssertEqual(variant.declarationFragments, [ + XCTAssertEqual(variant.subheadingDeclarationFragments, [ .init(text: "+ ", kind: .text, identifier: nil), .init(text: "myStringFunction:error:", kind: .identifier, identifier: nil) ]) @@ -713,7 +713,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(decoded.title, "ClassName") XCTAssertEqual(decoded.abstract?.plainText, "A brief explanation of my class.") XCTAssertEqual(decoded.relativePresentationURL.absoluteString, "documentation/MyKit/ClassName") - XCTAssertEqual(decoded.declarationFragments, [ + XCTAssertEqual(decoded.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "ClassName", kind: .identifier, identifier: nil), diff --git a/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift b/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift index dcd51bf89e..8a8e4ccc7c 100644 --- a/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift +++ b/Tests/SwiftDocCTests/OutOfProcessReferenceResolverV2Tests.swift @@ -85,7 +85,7 @@ class OutOfProcessReferenceResolverV2Tests: XCTestCase { .init(name: "secondOS", introduced: "4.5.6", isBeta: false), ], usr: "some-unique-symbol-id", - declarationFragments: .init([ + subheadingDeclarationFragments: .init([ .init(text: "struct", kind: .keyword, preciseIdentifier: nil), .init(text: " ", kind: .text, preciseIdentifier: nil), .init(text: "declaration fragment", kind: .identifier, preciseIdentifier: nil), @@ -127,7 +127,7 @@ class OutOfProcessReferenceResolverV2Tests: XCTestCase { .text(" and a link: "), .reference(identifier: linkedVariantReference, isActive: true, overridingTitle: nil, overridingTitleInlineContent: nil) ], - declarationFragments: .init([ + subheadingDeclarationFragments: .init([ .init(text: "variant declaration fragment", kind: .text, preciseIdentifier: nil) ]) ) @@ -341,12 +341,6 @@ class OutOfProcessReferenceResolverV2Tests: XCTestCase { .text("."), ], availableLanguages: [.swift], - platforms: nil, - taskGroups: nil, - usr: nil, - declarationFragments: nil, - redirects: nil, - topicImages: nil, references: [originalLinkedImage, originalLinkedTopic], variants: [] ) diff --git a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift index 85fc99fa13..72f42ba478 100644 --- a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift @@ -3218,7 +3218,7 @@ private extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: usr, - declarationFragments: nil, + subheadingDeclarationFragments: nil, redirects: redirects, topicImages: topicImages, references: references, From 56672dae7fd7cf9239395c6e4fbcbdbbda0ce225 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:24:06 +0100 Subject: [PATCH 09/11] Rename `navigatorTitle` to `navigatorDeclarationFragments` Renamed the property in `LinkDestinationSummary` which stores the declaration fragments for the navigator to `navigatorDeclarationFragments`, to follow the same naming convention as `subheadingDeclarationFragments` which fulfils a very similar purpose. The documentation comments mirror each other, with one explaining that it's intended for use in topic groups, and the other that it's intended for use in navigators. --- .../ExternalPathHierarchyResolver.swift | 4 +- .../LinkTargets/LinkDestinationSummary.swift | 59 ++++++++++--------- .../Resources/LinkableEntities.json | 13 ++++ .../TestExternalReferenceResolvers.swift | 2 +- .../LinkDestinationSummaryTests.swift | 22 +++---- 5 files changed, 58 insertions(+), 42 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index cb3b9db182..55bb272813 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift @@ -172,7 +172,7 @@ extension LinkDestinationSummary { var titleVariants = VariantCollection(defaultValue: title) var abstractVariants = VariantCollection(defaultValue: abstract ?? []) var fragmentVariants = VariantCollection(defaultValue: subheadingDeclarationFragments) - var navigatorTitleVariants = VariantCollection(defaultValue: navigatorTitle) + var navigatorTitleVariants = VariantCollection(defaultValue: navigatorDeclarationFragments) for variant in variants { let traits = variant.traits @@ -185,7 +185,7 @@ extension LinkDestinationSummary { if let fragment = variant.subheadingDeclarationFragments { fragmentVariants.variants.append(.init(traits: traits, patch: [.replace(value: fragment)])) } - if let navigatorTitle = variant.navigatorTitle { + if let navigatorTitle = variant.navigatorDeclarationFragments { navigatorTitleVariants.variants.append(.init(traits: traits, patch: [.replace(value: navigatorTitle)])) } } diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index a5d4165bbd..786de94efa 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -159,7 +159,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// /// - Note: The navigator title does not represent the symbol's full declaration. /// Different overloads may have indistinguishable subheading fragments. - public let navigatorTitle: DeclarationFragments? + public let navigatorDeclarationFragments: DeclarationFragments? /// Any previous URLs for this element. /// @@ -236,7 +236,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// These navigator fragments are suitable to use to refer to a symbol that's linked to in a navigator. /// /// If the summarized element has a navigator title but the variant doesn't, this property will be `Optional.some(nil)`. - public let navigatorTitle: VariantValue + public let navigatorDeclarationFragments: VariantValue /// Images that are used to represent the summarized element or `nil` if the images are the same as the summarized element. /// @@ -256,8 +256,8 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The taskGroups of the variant or `nil` if the taskGroups is the same as the summarized element. /// - usr: The precise symbol identifier of the variant or `nil` if the precise symbol identifier is the same as the summarized element. /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the precise symbol identifier is the same as the summarized element. - /// - subheadingDeclarationFragments: The abbreviated declaration of the variant, to display in links, or `nil` if the declaration is the same as the summarized element. - /// - navigatorTitle: The abbreviated declaration for this symbol's declaration, to display in navigation, or `nil` if the declaration is the same as the summarized element. + /// - subheadingDeclarationFragments: The simplified "subheading" declaration fragments for this symbol, to display in topic groups, or `nil` if the declaration is the same as the summarized element. + /// - navigatorDeclarationFragments: The simplified "navigator" declaration fragments for this symbol, to display in navigation, or `nil` if the declaration is the same as the summarized element. public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -269,7 +269,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: VariantValue = nil, plainTextDeclaration: VariantValue = nil, subheadingDeclarationFragments: VariantValue = nil, - navigatorTitle: VariantValue = nil + navigatorDeclarationFragments: VariantValue = nil ) { self.traits = traits self.kind = kind @@ -281,10 +281,10 @@ public struct LinkDestinationSummary: Codable, Equatable { self.usr = usr self.plainTextDeclaration = plainTextDeclaration self.subheadingDeclarationFragments = subheadingDeclarationFragments - self.navigatorTitle = navigatorTitle + self.navigatorDeclarationFragments = navigatorDeclarationFragments } - @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") + @available(*, deprecated, renamed: "init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:)", message: "Use `init(traits:kind:language:relativePresentationURL:title:abstract:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:)` instead. `TopicRenderReference` doesn't support variant specific topic images. This property will be removed after 6.3 is released") public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -296,7 +296,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: VariantValue = nil, plainTextDeclaration: VariantValue = nil, declarationFragments: VariantValue = nil, - navigatorTitle: VariantValue = nil, + navigatorDeclarationFragments: VariantValue = nil, topicImages: VariantValue<[TopicImage]?> = nil ) { self.init( @@ -310,7 +310,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: usr, plainTextDeclaration: plainTextDeclaration, subheadingDeclarationFragments: declarationFragments, - navigatorTitle: navigatorTitle + navigatorDeclarationFragments: navigatorDeclarationFragments ) } } @@ -332,8 +332,8 @@ public struct LinkDestinationSummary: Codable, Equatable { /// - taskGroups: The reference URLs of the summarized element's children, grouped by their task groups. /// - usr: The unique, precise identifier for this symbol that you use to reference it across different systems, or `nil` if the summarized element isn't a symbol. /// - plainTextDeclaration: The plain text declaration of this symbol, derived from its full declaration fragments, or `nil` if the summarized element isn't a symbol. - /// - subheadingDeclarationFragments: The simplified "subheading" fragments for this symbol, or `nil` if the summarized element isn't a symbol. - /// - navigatorTitle: The abbreviated fragments for this symbol's declaration, to display in navigation, or `nil` if the summarized element isn't a symbol. + /// - subheadingDeclarationFragments: The simplified "subheading" fragments for this symbol, to display in topic groups, or `nil` if the summarized element isn't a symbol. + /// - navigatorDeclarationFragments: The simplified "subheading" declaration fragments for this symbol, to display in navigation, or `nil` if the summarized element isn't a symbol. /// - redirects: Any previous URLs for this element, or `nil` if this element has no previous URLs. /// - topicImages: Images that are used to represent the summarized element, or `nil` if this element has no topic images. /// - references: References used in the content of the summarized element, or `nil` if this element has no references to other content. @@ -350,7 +350,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: String? = nil, plainTextDeclaration: String? = nil, subheadingDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, - navigatorTitle: LinkDestinationSummary.DeclarationFragments? = nil, + navigatorDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, references: [any RenderReference]? = nil, @@ -368,14 +368,14 @@ public struct LinkDestinationSummary: Codable, Equatable { self.usr = usr self.plainTextDeclaration = plainTextDeclaration self.subheadingDeclarationFragments = subheadingDeclarationFragments - self.navigatorTitle = navigatorTitle + self.navigatorDeclarationFragments = navigatorDeclarationFragments self.redirects = redirects self.topicImages = topicImages self.references = references self.variants = variants } - @available(*, deprecated, renamed: "init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:redirects:topicImages:references:variants:)", message: "Use `init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorTitle:redirects:topicImages:references:variants:)` instead. This property will be removed after 6.3 is released") + @available(*, deprecated, renamed: "init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:redirects:topicImages:references:variants:)", message: "Use `init(kind:language:relativePresentationURL:referenceURL:title:abstract:availableLanguages:platforms:taskGroups:usr:plainTextDeclaration:subheadingDeclarationFragments:navigatorDeclarationFragments:redirects:topicImages:references:variants:)` instead. This property will be removed after 6.3 is released") public init( kind: DocumentationNode.Kind, language: SourceLanguage, @@ -388,7 +388,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: String? = nil, plainTextDeclaration: String? = nil, declarationFragments: LinkDestinationSummary.DeclarationFragments?, - navigatorTitle: LinkDestinationSummary.DeclarationFragments? = nil, + navigatorDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, references: [any RenderReference]? = nil, @@ -407,7 +407,7 @@ public struct LinkDestinationSummary: Codable, Equatable { usr: usr, plainTextDeclaration: plainTextDeclaration, subheadingDeclarationFragments: declarationFragments, - navigatorTitle: navigatorTitle, + navigatorDeclarationFragments: navigatorDeclarationFragments, redirects: redirects, topicImages: topicImages, references: references, @@ -567,7 +567,7 @@ extension LinkDestinationSummary { // If no abbreviated declaration fragments are available, use the full declaration fragments instead. // In this case, they are assumed to be the same. let subheadingDeclarationFragments = renderNode.metadata.fragmentsVariants.value(for: language) ?? (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() - let navigatorTitle = renderNode.metadata.navigatorTitleVariants.value(for: language) + let navigatorDeclarationFragments = renderNode.metadata.navigatorTitleVariants.value(for: language) let variants: [Variant] = documentationNode.availableVariantTraits.compactMap { trait in // Skip the variant for the summarized elements source language. @@ -591,8 +591,8 @@ extension LinkDestinationSummary { // // However if no abbreviated declaration fragments are available, use the full declaration fragments instead. // In this case, they are assumed to be the same. - let declarationVariant = renderNode.metadata.fragmentsVariants.value(for: variantTraits) ?? symbol.declarationVariants[trait]?.renderDeclarationTokens() - let navigatorTitleVariant = renderNode.metadata.navigatorTitleVariants.value(for: variantTraits) + let subheadingDeclarationFragmentsVariant = renderNode.metadata.fragmentsVariants.value(for: variantTraits) ?? symbol.declarationVariants[trait]?.renderDeclarationTokens() + let navigatorDeclarationFragmentsVariant = renderNode.metadata.navigatorTitleVariants.value(for: variantTraits) return Variant( traits: variantTraits, kind: nilIfEqual(main: kind, variant: symbol.kindVariants[trait].map { DocumentationNode.kind(forKind: $0.identifier) }), @@ -603,8 +603,8 @@ extension LinkDestinationSummary { taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR plainTextDeclaration: nilIfEqual(main: plainTextDeclaration, variant: plainTextDeclarationVariant), - subheadingDeclarationFragments: nilIfEqual(main: subheadingDeclarationFragments, variant: declarationVariant), - navigatorTitle: nilIfEqual(main: navigatorTitle, variant: navigatorTitleVariant) + subheadingDeclarationFragments: nilIfEqual(main: subheadingDeclarationFragments, variant: subheadingDeclarationFragmentsVariant), + navigatorDeclarationFragments: nilIfEqual(main: navigatorDeclarationFragments, variant: navigatorDeclarationFragmentsVariant) ) } @@ -625,7 +625,7 @@ extension LinkDestinationSummary { usr: usr, plainTextDeclaration: plainTextDeclaration, subheadingDeclarationFragments: subheadingDeclarationFragments, - navigatorTitle: navigatorTitle, + navigatorDeclarationFragments: navigatorDeclarationFragments, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -707,9 +707,10 @@ extension LinkDestinationSummary { // Add Codable methods—which include an initializer—in an extension so that it doesn't override the member-wise initializer. extension LinkDestinationSummary { enum CodingKeys: String, CodingKey { - case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, plainTextDeclaration, navigatorTitle + case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, plainTextDeclaration case relativePresentationURL = "path" case subheadingDeclarationFragments = "fragments" + case navigatorDeclarationFragments = "navigatorFragments" } public func encode(to encoder: any Encoder) throws { @@ -741,7 +742,7 @@ extension LinkDestinationSummary { try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .subheadingDeclarationFragments) - try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) + try container.encodeIfPresent(navigatorDeclarationFragments, forKey: .navigatorDeclarationFragments) try container.encodeIfPresent(redirects, forKey: .redirects) try container.encodeIfPresent(topicImages, forKey: .topicImages) try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references) @@ -799,7 +800,7 @@ extension LinkDestinationSummary { usr = try container.decodeIfPresent(String.self, forKey: .usr) plainTextDeclaration = try container.decodeIfPresent(String.self, forKey: .plainTextDeclaration) subheadingDeclarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .subheadingDeclarationFragments) - navigatorTitle = try container.decodeIfPresent(DeclarationFragments.self, forKey: .navigatorTitle) + navigatorDeclarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .navigatorDeclarationFragments) redirects = try container.decodeIfPresent([URL].self, forKey: .redirects) topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) references = try container.decodeIfPresent([CodableRenderReference].self, forKey: .references).map { decodedReferences in @@ -812,9 +813,10 @@ extension LinkDestinationSummary { extension LinkDestinationSummary.Variant { enum CodingKeys: String, CodingKey { - case traits, kind, title, abstract, language, usr, taskGroups, plainTextDeclaration, navigatorTitle + case traits, kind, title, abstract, language, usr, taskGroups, plainTextDeclaration case relativePresentationURL = "path" case declarationFragments = "fragments" + case navigatorDeclarationFragments = "navigatorFragments" } public func encode(to encoder: any Encoder) throws { @@ -840,7 +842,7 @@ extension LinkDestinationSummary.Variant { try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .declarationFragments) - try container.encodeIfPresent(navigatorTitle, forKey: .navigatorTitle) + try container.encodeIfPresent(navigatorDeclarationFragments, forKey: .navigatorDeclarationFragments) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) } @@ -883,7 +885,8 @@ extension LinkDestinationSummary.Variant { usr = try container.decodeIfPresent(String?.self, forKey: .usr) plainTextDeclaration = try container.decodeIfPresent(String?.self, forKey: .plainTextDeclaration) subheadingDeclarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) - navigatorTitle = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .navigatorTitle) + navigatorDeclarationFragments = try container + .decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .navigatorDeclarationFragments) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) } } diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json index 02f111476e..a07d1c18a2 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json @@ -77,6 +77,12 @@ "$ref": "#/components/schemas/DeclarationToken" } }, + "navigatorFragments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarationToken" + } + }, "topicImages": { "type": "array", "items": { @@ -172,6 +178,13 @@ }, "nullable": true }, + "navigatorFragments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarationToken" + }, + "nullable": true + }, "taskGroups": { "type": "array", "items": { diff --git a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift index f1f190afbf..d02463b820 100644 --- a/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift +++ b/Tests/SwiftDocCTests/Infrastructure/TestExternalReferenceResolvers.swift @@ -94,7 +94,7 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { availableLanguages: [entityInfo.language], platforms: entityInfo.platforms, subheadingDeclarationFragments: entityInfo.declarationFragments?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, - navigatorTitle: entityInfo.navigatorTitle?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, + navigatorDeclarationFragments: entityInfo.navigatorTitle?.declarationFragments.map { .init(fragment: $0, identifier: nil) }, topicImages: entityInfo.topicImages?.map(\.0), references: entityInfo.topicImages?.map { topicImage, altText in ImageReference(identifier: topicImage.identifier, altText: altText, imageAsset: assetsToReturn[topicImage.identifier.identifier] ?? .init()) diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index 0a17eebe5c..5606958101 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -115,7 +115,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertNil(pageSummary.usr, "Only symbols have USRs") XCTAssertNil(pageSummary.plainTextDeclaration, "Only symbols have a plain text declaration") XCTAssertNil(pageSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") - XCTAssertNil(pageSummary.navigatorTitle, "Only symbols have navigator titles") + XCTAssertNil(pageSummary.navigatorDeclarationFragments, "Only symbols have navigator titles") XCTAssertNil(pageSummary.abstract, "There is no text to use as an abstract for the tutorial page") XCTAssertNil(pageSummary.topicImages, "The tutorial page doesn't have any topic images") XCTAssertNil(pageSummary.references, "Since the tutorial page doesn't have any topic images it also doesn't have any references") @@ -135,7 +135,7 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertNil(sectionSummary.usr, "Only symbols have USRs") XCTAssertNil(sectionSummary.plainTextDeclaration, "Only symbols have a plain text declaration") XCTAssertNil(sectionSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") - XCTAssertNil(sectionSummary.navigatorTitle, "Only symbols have navigator titles") + XCTAssertNil(sectionSummary.navigatorDeclarationFragments, "Only symbols have navigator titles") XCTAssertEqual(sectionSummary.abstract, [ .text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt"), .text(" "), @@ -190,7 +190,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " ", kind: .text, identifier: nil), .init(text: "MyClass", kind: .identifier, identifier: nil), ]) - XCTAssertEqual(summary.navigatorTitle, [ + XCTAssertEqual(summary.navigatorDeclarationFragments, [ .init(text: "MyClassNavigator", kind: .identifier, identifier: nil), ]) XCTAssertNil(summary.topicImages) @@ -235,7 +235,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " : ", kind: .text, identifier: nil), .init(text: "Hashable", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "p:hPP"), ]) - XCTAssertEqual(summary.navigatorTitle, [ + XCTAssertEqual(summary.navigatorDeclarationFragments, [ .init(text: "MyProtocol", kind: .identifier, identifier: nil), ]) XCTAssertNil(summary.topicImages) @@ -274,7 +274,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) - XCTAssertNil(summary.navigatorTitle, "This symbol doesn't have a navigator title") + XCTAssertNil(summary.navigatorDeclarationFragments, "This symbol doesn't have a navigator title") XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -312,7 +312,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "Int", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:Si"), .init(text: ")", kind: .text, identifier: nil) ]) - XCTAssertEqual(summary.navigatorTitle, [ + XCTAssertEqual(summary.navigatorDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "globalFunction", kind: .identifier, identifier: nil), @@ -379,7 +379,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) - XCTAssertNil(summary.navigatorTitle, "This symbol doesn't have a navigator title") + XCTAssertNil(summary.navigatorDeclarationFragments, "This symbol doesn't have a navigator title") XCTAssertEqual(summary.topicImages, [ TopicImage( @@ -487,7 +487,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil) ]) - XCTAssertEqual(summary.navigatorTitle, [ + XCTAssertEqual(summary.navigatorDeclarationFragments, [ .init(text: "Bar", kind: .identifier, identifier: nil) ]) XCTAssertNil(summary.topicImages) @@ -506,7 +506,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " : ", kind: .text, identifier: nil), .init(text: "NSObject", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSObject"), ]) - XCTAssertEqual(variant.navigatorTitle, [ + XCTAssertEqual(variant.navigatorDeclarationFragments, [ .init(text: "Bar (objective c)", kind: .identifier, identifier: nil), ]) @@ -562,7 +562,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: " -> ", kind: .text, identifier: nil), .init(text: "String", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:SS") ]) - XCTAssertEqual(summary.navigatorTitle, [ + XCTAssertEqual(summary.navigatorDeclarationFragments, [ .init(text: "myStringFunction:error: (navigator title)", kind: .identifier, identifier: nil), ]) XCTAssertNil(summary.topicImages) @@ -579,7 +579,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "+ ", kind: .text, identifier: nil), .init(text: "myStringFunction:error:", kind: .identifier, identifier: nil) ]) - XCTAssertEqual(variant.navigatorTitle, .none, "Navigator title is the same across variants") + XCTAssertEqual(variant.navigatorDeclarationFragments, .none, "Navigator title is the same across variants") // Check variant content that is the same as the summarized element XCTAssertEqual(variant.abstract, nil) From 427e27ce8fcb20a65590c2328b758eb81c89379d Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:26:28 +0100 Subject: [PATCH 10/11] Rename helper function for readability Clarifies the function name so that it clearly states that only top level nodes are returned, as well as improving the grammar at the call site. --- .../Indexing/ExternalRenderNodeTests.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift index a284bc12c1..d2c790caa0 100644 --- a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift @@ -269,15 +269,15 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) - func externalNodes(by language: SourceLanguage) -> [RenderIndex.Node]? { + func externalTopLevelNodes(for language: SourceLanguage) -> [RenderIndex.Node]? { renderIndex.interfaceLanguages[language.id]?.first?.children?.filter(\.isExternal) } // Verify that the curated external links are part of the index. - let swiftExternalNodes = try XCTUnwrap(externalNodes(by: .swift)) + let swiftExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .swift)) XCTAssertEqual(swiftExternalNodes.count, 2) - let objcExternalNodes = try XCTUnwrap(externalNodes(by: .objectiveC)) + let objcExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .objectiveC)) XCTAssertEqual(objcExternalNodes.count, 2) let swiftArticleExternalNode = try XCTUnwrap(swiftExternalNodes.first(where: { $0.path == "/path/to/external/swiftarticle" })) @@ -358,13 +358,13 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.count(where: \.isExternal), 0) XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) - func externalNodes(by language: SourceLanguage) -> [RenderIndex.Node]? { + func externalTopLevelNodes(for language: SourceLanguage) -> [RenderIndex.Node]? { renderIndex.interfaceLanguages[language.id]?.first?.children?.filter(\.isExternal) } // Verify that the curated external links are part of the index. - let swiftExternalNodes = try XCTUnwrap(externalNodes(by: .swift)) - let objcExternalNodes = try XCTUnwrap(externalNodes(by: .objectiveC)) + let swiftExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .swift)) + let objcExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .objectiveC)) XCTAssertEqual(swiftExternalNodes.count, 1) XCTAssertEqual(objcExternalNodes.count, 1) From edd24c2e458090ab9a46ebc3a02a8975b1079db7 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:32:19 +0100 Subject: [PATCH 11/11] Fix minor issue in comment --- Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 786de94efa..a092359f28 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -158,7 +158,7 @@ public struct LinkDestinationSummary: Codable, Equatable { /// These navigator fragments are suitable to use to refer to a symbol that's linked to in a navigator. /// /// - Note: The navigator title does not represent the symbol's full declaration. - /// Different overloads may have indistinguishable subheading fragments. + /// Different overloads may have indistinguishable navigator fragments. public let navigatorDeclarationFragments: DeclarationFragments? /// Any previous URLs for this element.