From 832f1eb42b0f79d694c5e061626f115345783541 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:01:45 +0100 Subject: [PATCH 1/4] Add the abbreviated declaration fragments to 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]. As part of `LinkDestinationSummary`, we store the full declaration fragments of the symbol [3][4]. However, currently `OutOfProcessReferenceResolver` is using these full declaration fragments to populate `TopicRenderReference.fragmentsVariants` [5], which expects the abbreviated declaration fragments [6]. These abbreviated declaration fragments are meant to be used in the Topics section and the navigation index in order to reduce verbosity and improve readability by removing any declaration fragments non-essential to the core of the symbol declaration. These abbreviated declaration fragments are not captured as part of `LinkDestinationSummary` or `OutOfProcessReferenceResolver.ResolvedInformation`. To start capturing this information, this commit modifies `LinkDestinationSummary` to add a new optional field `subHeadingDeclarationFragments` which stores the abbreviated declaration fragments from `renderNode.metadata.fragmentsVariants` (abbreviated declaration fragments are derived from the `subHeading` of the symbol [7], further processed during the render node transformation phase, and finally stored as `renderNode.metadata.fragmentsVariants` [8]). This will enable us to use them in `OutOfProcessReferenceResolver.ResolvedInformation` to initialise `TopicRenderReference.fragmentsVariants` with the expected value. The final goal is for a symbol's representation in the Topics section and the navigation index to look the same regardless of whether the symbol is a local or external one. Fixes rdar://156488052. Alternatives considered: ------------------------ Considered modifying `LinkDestinationSummary.declarationFragments` instead of adding a new optional field, however the full declaration fragments are used to determine the full name [9] of the symbol for diagnostic reporting [10]. By introducing a new field we also ensure this is a non-breaking change. [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/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift#L140-L141 [4]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift#L445 [5]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Infrastructure/External%20Data/OutOfProcessReferenceResolver.swift#L169 [6]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Model/Rendering/References/TopicRenderReference.swift#L50-L53 [7]: https://github.com/swiftlang/swift-docc-symbolkit/blob/ebe89c7da4cf03ded04cd708f3399087c6f2dad7/Sources/SymbolKit/SymbolGraph/Symbol/Names.swift#L28-L31 [8]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift#L1304 [9]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Infrastructure/Link%20Resolution/ExternalPathHierarchyResolver.swift#L61-L63 [10]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Infrastructure/Link%20Resolution/PathHierarchy%2BError.swift#L115-L117 --- .../LinkTargets/LinkDestinationSummary.swift | 41 +++++++++++++++++-- .../LinkDestinationSummaryTests.swift | 25 +++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 0a901bab5..5302e4c36 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -139,7 +139,12 @@ public struct LinkDestinationSummary: Codable, Equatable { public typealias DeclarationFragments = [DeclarationRenderSection.Token] /// The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. public let declarationFragments: DeclarationFragments? - + + /// The abbreviated fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. + /// + /// They are used for displaying in contexts where the full declaration fragments would be too verbose, like in the Topics section or the navigation index. + public let subHeadingDeclarationFragments: DeclarationFragments? + /// Any previous URLs for this element. /// /// A web server can use this list of URLs to redirect to the current URL. @@ -197,7 +202,13 @@ public struct LinkDestinationSummary: Codable, Equatable { /// /// 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 of the variant or `nil` if the declaration is the same as the summarized element. + /// + /// They are used for displaying in contexts where the full declaration fragments would be too verbose, like in the Topics section or the navigation index. + /// If the summarized element has an abbreviated declaration but the variant doesn't, this property will be `Optional.some(nil)`. + public let subHeadingDeclarationFragments: 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)`. @@ -215,6 +226,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. /// - declarationFragments: The declaration of the variant or `nil` if the declaration is the same as the summarized element. + /// - subHeadingDeclarationFragments: The abbreviated declaration of the variant or `nil` if the declaration is the same as the summarized element. /// - topicImages: Images that are used to represent the summarized element or `nil` if the images are the same as the summarized element. public init( traits: [RenderNode.Variant.Trait], @@ -226,6 +238,7 @@ public struct LinkDestinationSummary: Codable, Equatable { taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, declarationFragments: VariantValue = nil, + subHeadingDeclarationFragments: VariantValue = nil, topicImages: VariantValue<[TopicImage]?> = nil ) { self.traits = traits @@ -237,6 +250,7 @@ public struct LinkDestinationSummary: Codable, Equatable { self.taskGroups = taskGroups self.usr = usr self.declarationFragments = declarationFragments + self.subHeadingDeclarationFragments = subHeadingDeclarationFragments self.topicImages = topicImages } } @@ -258,6 +272,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. /// - declarationFragments: The fragments for this symbol's declaration, or `nil` if the summarized element isn't a symbol. + /// - subHeadingDeclarationFragments: 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. @@ -273,6 +288,7 @@ public struct LinkDestinationSummary: Codable, Equatable { taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, usr: String? = nil, declarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + subHeadingDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, references: [any RenderReference]? = nil, @@ -289,6 +305,7 @@ public struct LinkDestinationSummary: Codable, Equatable { self.taskGroups = taskGroups self.usr = usr self.declarationFragments = declarationFragments + self.subHeadingDeclarationFragments = subHeadingDeclarationFragments self.redirects = redirects self.topicImages = topicImages self.references = references @@ -444,7 +461,11 @@ extension LinkDestinationSummary { let usr = symbol.externalIDVariants[summaryTrait] ?? symbol.externalID let declaration = (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() let language = documentationNode.sourceLanguage - + // Use the render metadata declaration fragments as the subheading declaration fragments. + // These have been derived from the symbol's original subheading declaration fragments as part of the rendering step. + // They are an abbreviated version of the declaration for display in Topic sections, the navigator, etc.. + let subHeadingDeclaration = renderNode.metadata.fragmentsVariants.value(for: language) + 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 { @@ -460,6 +481,11 @@ extension LinkDestinationSummary { } let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage(interfaceLanguage)] + + // Use the render metadata declaration fragments as the subheading declaration fragments. + // These have been derived from the symbol's original subheading declaration fragments as part of the rendering step. + // They are an abbreviated version of the declaration for display in Topic sections, the navigator, etc.. + let subHeadingDeclarationVariant = renderNode.metadata.fragmentsVariants.value(for: variantTraits) return Variant( traits: variantTraits, kind: nilIfEqual(main: kind, variant: symbol.kindVariants[trait].map { DocumentationNode.kind(forKind: $0.identifier) }), @@ -470,6 +496,7 @@ extension LinkDestinationSummary { taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant), + subHeadingDeclarationFragments: nilIfEqual(main: subHeadingDeclaration, variant: subHeadingDeclarationVariant), topicImages: nil // The symbol variant doesn't currently have their own images ) } @@ -490,6 +517,7 @@ extension LinkDestinationSummary { taskGroups: taskGroups, usr: usr, declarationFragments: declaration, + subHeadingDeclarationFragments: subHeadingDeclaration, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -574,6 +602,7 @@ extension LinkDestinationSummary { case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants case relativePresentationURL = "path" case declarationFragments = "fragments" + case subHeadingDeclarationFragments = "subheadingFragments" } public func encode(to encoder: any Encoder) throws { @@ -589,6 +618,7 @@ extension LinkDestinationSummary { try container.encodeIfPresent(taskGroups, forKey: .taskGroups) try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(subHeadingDeclarationFragments, forKey: .subHeadingDeclarationFragments) try container.encodeIfPresent(redirects, forKey: .redirects) try container.encodeIfPresent(topicImages, forKey: .topicImages) try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references) @@ -626,6 +656,7 @@ extension LinkDestinationSummary { taskGroups = try container.decodeIfPresent([TaskGroup].self, forKey: .taskGroups) usr = try container.decodeIfPresent(String.self, forKey: .usr) declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments) + subHeadingDeclarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .subHeadingDeclarationFragments) 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 @@ -641,6 +672,7 @@ extension LinkDestinationSummary.Variant { case traits, kind, title, abstract, language, usr, taskGroups, topicImages case relativePresentationURL = "path" case declarationFragments = "fragments" + case subHeadingDeclarationFragments = "subheadingFragments" } public func encode(to encoder: any Encoder) throws { @@ -653,6 +685,7 @@ extension LinkDestinationSummary.Variant { try container.encodeIfPresent(language?.id, forKey: .language) try container.encodeIfPresent(usr, forKey: .usr) try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(subHeadingDeclarationFragments, forKey: .subHeadingDeclarationFragments) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) try container.encodeIfPresent(topicImages, forKey: .topicImages) } @@ -692,6 +725,8 @@ extension LinkDestinationSummary.Variant { abstract = try container.decodeIfPresent(LinkDestinationSummary.Abstract?.self, forKey: .abstract) usr = try container.decodeIfPresent(String?.self, forKey: .usr) declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) + subHeadingDeclarationFragments = try container + .decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .subHeadingDeclarationFragments) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) topicImages = try container.decodeIfPresent([TopicImage]?.self, forKey: .topicImages) } diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index 572f2ea2a..0c360d739 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -189,6 +189,11 @@ class ExternalLinkableTests: XCTestCase { .init(text: " ", kind: .text, identifier: nil), .init(text: "MyClass", kind: .identifier, identifier: nil), ]) + XCTAssertEqual(summary.subHeadingDeclarationFragments, [ + .init(text: "class", kind: .keyword, identifier: nil), + .init(text: " ", kind: .text, identifier: nil), + .init(text: "MyClass", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -230,6 +235,13 @@ class ExternalLinkableTests: XCTestCase { .init(text: " : ", kind: .text, identifier: nil), .init(text: "Hashable", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "p:hPP"), ]) + XCTAssertEqual(summary.subHeadingDeclarationFragments, [ + .init(text: "protocol", kind: .keyword, identifier: nil), + .init(text: " ", kind: .text, identifier: nil), + .init(text: "MyProtocol", kind: .identifier, identifier: nil), + .init(text: " : ", kind: .text, identifier: nil), + .init(text: "Hashable", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "p:hPP"), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -265,6 +277,7 @@ class ExternalLinkableTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) + XCTAssertNil(summary.subHeadingDeclarationFragments) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -303,6 +316,18 @@ class ExternalLinkableTests: XCTestCase { .init(text: "Int", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:Si"), .init(text: ")", kind: .text, identifier: nil) ]) + XCTAssertEqual(summary.subHeadingDeclarationFragments, [ + .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) From 86b1ff83894534d544f95275549f5ae13532a730 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:43:54 +0100 Subject: [PATCH 2/4] Use the subheading fragments to init the external entity `LinkDestinationSummary` now provides information for both the full and abbreviated declaration fragments. This commit adds the abbreviated declaration fragments to its counterpart type `OutOfProcessReferenceResolver.ResolvedInformation` [1], and uses them to initialise the external entity's `TopicRenderReference`. This is because the `TopicRenderReference` expects the abbreviated declaration fragments [2]. This results in the abbreviated declaration fragments correctly being used in the Topics and navigation index, rather than the full declaration fragments. Fixes rdar://156488052. [1]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Infrastructure/External%20Data/OutOfProcessReferenceResolver.swift#L559-L562 [2]: https://github.com/swiftlang/swift-docc/blob/1b4a1850dd2785a8ebabded139ae0af3551bb029/Sources/SwiftDocC/Model/Rendering/References/TopicRenderReference.swift#L50-L53 --- .../OutOfProcessReferenceResolver.swift | 30 +++++++++++++++---- .../OutOfProcessReferenceResolverTests.swift | 22 ++++++++++---- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift index fa3666edd..9f88fea72 100644 --- a/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift +++ b/Sources/SwiftDocC/Infrastructure/External Data/OutOfProcessReferenceResolver.swift @@ -166,7 +166,8 @@ public class OutOfProcessReferenceResolver: ExternalDocumentationSource, GlobalE url: resolvedInformation.url.path, kind: kind, role: role, - fragments: resolvedInformation.declarationFragments?.declarationFragments.map { DeclarationRenderSection.Token(fragment: $0, identifier: nil) }, + fragments: resolvedInformation.subHeadingDeclarationFragments?.declarationFragments + .map { DeclarationRenderSection.Token(fragment: $0, identifier: nil) }, isBeta: resolvedInformation.isBeta, isDeprecated: (resolvedInformation.platforms ?? []).contains(where: { $0.deprecated != nil }), images: resolvedInformation.topicImages ?? [] @@ -182,7 +183,7 @@ public class OutOfProcessReferenceResolver: ExternalDocumentationSource, GlobalE .init(traits: variant.traits, patch: [.replace(value: [.text(abstract)])]) ) } - if let declarationFragments = variant.declarationFragments { + if let declarationFragments = variant.subHeadingDeclarationFragments { renderReference.fragmentsVariants.variants.append( .init(traits: variant.traits, patch: [.replace(value: declarationFragments?.declarationFragments.map { DeclarationRenderSection.Token(fragment: $0, identifier: nil) })]) ) @@ -577,7 +578,11 @@ extension OutOfProcessReferenceResolver { public let platforms: [PlatformAvailability]? /// Information about the resolved declaration fragments, if any. public let declarationFragments: DeclarationFragments? - + /// Information about the resolved abbreviated declaration fragments, if any. + /// + /// They are used for displaying in contexts where the full declaration fragments would be too verbose, like in the Topics section or the navigation index. + public let subHeadingDeclarationFragments: DeclarationFragments? + // We use the real types here because they're Codable and don't have public member-wise initializers. /// Platform availability for a resolved symbol reference. @@ -620,6 +625,7 @@ extension OutOfProcessReferenceResolver { /// - 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. + /// - subHeadingDeclarationFragments: The abbreviated resolved declaration fragments, if any. /// - 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. @@ -632,6 +638,7 @@ extension OutOfProcessReferenceResolver { availableLanguages: Set, platforms: [PlatformAvailability]? = nil, declarationFragments: DeclarationFragments? = nil, + subHeadingDeclarationFragments: DeclarationFragments? = nil, topicImages: [TopicImage]? = nil, references: [any RenderReference]? = nil, variants: [Variant]? = nil @@ -644,6 +651,7 @@ extension OutOfProcessReferenceResolver { self.availableLanguages = availableLanguages self.platforms = platforms self.declarationFragments = declarationFragments + self.subHeadingDeclarationFragments = subHeadingDeclarationFragments self.topicImages = topicImages self.references = references self.variants = variants @@ -675,7 +683,12 @@ extension OutOfProcessReferenceResolver { /// /// If the resolver information has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. public let declarationFragments: VariantValue - + /// The abbreviated declaration fragments of the variant or `nil` if the declaration is the same as the resolved information. + /// + /// They are used for displaying in contexts where the full declaration fragments would be too verbose, like in the Topics section or the navigation index. + /// If the resolver information has a declaration but the variant doesn't, this property will be `Optional.some(nil)`. + public let subHeadingDeclarationFragments: VariantValue + /// Creates a new resolved information variant with the values that are different from the resolved information values. /// /// - Parameters: @@ -686,6 +699,7 @@ extension OutOfProcessReferenceResolver { /// - abstract: The resolved (plain text) abstract. /// - language: The resolved language. /// - declarationFragments: The resolved declaration fragments, if any. + /// - subHeadingDeclarationFragments: The resolved abbreviated declaration fragments, if any. public init( traits: [RenderNode.Variant.Trait], kind: VariantValue = nil, @@ -693,7 +707,8 @@ extension OutOfProcessReferenceResolver { title: VariantValue = nil, abstract: VariantValue = nil, language: VariantValue = nil, - declarationFragments: VariantValue = nil + declarationFragments: VariantValue = nil, + subHeadingDeclarationFragments: VariantValue = nil ) { self.traits = traits self.kind = kind @@ -702,6 +717,7 @@ extension OutOfProcessReferenceResolver { self.abstract = abstract self.language = language self.declarationFragments = declarationFragments + self.subHeadingDeclarationFragments = subHeadingDeclarationFragments } } } @@ -717,6 +733,7 @@ extension OutOfProcessReferenceResolver.ResolvedInformation { case availableLanguages case platforms case declarationFragments + case subHeadingDeclarationFragments case topicImages case references case variants @@ -733,6 +750,8 @@ extension OutOfProcessReferenceResolver.ResolvedInformation { availableLanguages = try container.decode(Set.self, forKey: .availableLanguages) platforms = try container.decodeIfPresent([OutOfProcessReferenceResolver.ResolvedInformation.PlatformAvailability].self, forKey: .platforms) declarationFragments = try container.decodeIfPresent(OutOfProcessReferenceResolver.ResolvedInformation.DeclarationFragments.self, forKey: .declarationFragments) + subHeadingDeclarationFragments = try container + .decodeIfPresent(OutOfProcessReferenceResolver.ResolvedInformation.DeclarationFragments.self, forKey: .subHeadingDeclarationFragments) topicImages = try container.decodeIfPresent([TopicImage].self, forKey: .topicImages) references = try container.decodeIfPresent([CodableRenderReference].self, forKey: .references).map { decodedReferences in decodedReferences.map(\.reference) @@ -752,6 +771,7 @@ extension OutOfProcessReferenceResolver.ResolvedInformation { try container.encode(self.availableLanguages, forKey: .availableLanguages) try container.encodeIfPresent(self.platforms, forKey: .platforms) try container.encodeIfPresent(self.declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(self.subHeadingDeclarationFragments, forKey: .subHeadingDeclarationFragments) try container.encodeIfPresent(self.topicImages, forKey: .topicImages) try container.encodeIfPresent(references?.map { CodableRenderReference($0) }, forKey: .references) try container.encodeIfPresent(self.variants, forKey: .variants) diff --git a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift index d96a0216f..4bcd99e04 100644 --- a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift @@ -70,6 +70,9 @@ class OutOfProcessReferenceResolverTests: XCTestCase { declarationFragments: .init(declarationFragments: [ .init(kind: .text, spelling: "declaration fragment", preciseIdentifier: nil) ]), + subHeadingDeclarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "subheading declaration fragment", preciseIdentifier: nil) + ]), topicImages: nil, references: nil, variants: [ @@ -82,7 +85,10 @@ class OutOfProcessReferenceResolverTests: XCTestCase { language: .init(name: "Language Name 2", id: "com.test.another-language.id"), declarationFragments: .init(declarationFragments: [ .init(kind: .text, spelling: "variant declaration fragment", preciseIdentifier: nil) - ]) + ]), + subHeadingDeclarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "variant subheading declaration fragment", preciseIdentifier: nil) + ]), ) ] ) @@ -120,7 +126,7 @@ class OutOfProcessReferenceResolverTests: XCTestCase { XCTAssertEqual(availableSourceLanguages[1], expectedLanguages[1]) XCTAssertEqual(availableSourceLanguages[2], expectedLanguages[2]) - XCTAssertEqual(entity.topicRenderReference.fragments, [.init(text: "declaration fragment", kind: .text, preciseIdentifier: nil)]) + XCTAssertEqual(entity.topicRenderReference.fragments, [.init(text: "subheading declaration fragment", kind: .text, preciseIdentifier: nil)]) let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage("com.test.another-language.id")] XCTAssertEqual(entity.topicRenderReference.titleVariants.value(for: variantTraits), "Resolved Variant Title") @@ -129,7 +135,7 @@ class OutOfProcessReferenceResolverTests: XCTestCase { let fragmentVariant = try XCTUnwrap(entity.topicRenderReference.fragmentsVariants.variants.first(where: { $0.traits == variantTraits })) XCTAssertEqual(fragmentVariant.patch.map(\.operation), [.replace]) if case .replace(let variantFragment) = fragmentVariant.patch.first { - XCTAssertEqual(variantFragment, [.init(text: "variant declaration fragment", kind: .text, preciseIdentifier: nil)]) + XCTAssertEqual(variantFragment, [.init(text: "variant subheading declaration fragment", kind: .text, preciseIdentifier: nil)]) } else { XCTFail("Unexpected fragments variant patch") } @@ -226,6 +232,9 @@ class OutOfProcessReferenceResolverTests: XCTestCase { declarationFragments: .init(declarationFragments: [ .init(kind: .text, spelling: "declaration fragment", preciseIdentifier: nil) ]), + subHeadingDeclarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "subheading declaration fragment", preciseIdentifier: nil) + ]), topicImages: [ TopicImage( type: .card, @@ -260,6 +269,9 @@ class OutOfProcessReferenceResolverTests: XCTestCase { language: .init(name: "Language Name 2", id: "com.test.another-language.id"), declarationFragments: .init(declarationFragments: [ .init(kind: .text, spelling: "variant declaration fragment", preciseIdentifier: nil) + ]), + subHeadingDeclarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "variant subheading declaration fragment", preciseIdentifier: nil) ]) ) ] @@ -288,7 +300,7 @@ class OutOfProcessReferenceResolverTests: XCTestCase { XCTAssertEqual(availableSourceLanguages[1], expectedLanguages[1]) XCTAssertEqual(availableSourceLanguages[2], expectedLanguages[2]) - XCTAssertEqual(entity.topicRenderReference.fragments, [.init(text: "declaration fragment", kind: .text, preciseIdentifier: nil)]) + XCTAssertEqual(entity.topicRenderReference.fragments, [.init(text: "subheading declaration fragment", kind: .text, preciseIdentifier: nil)]) let variantTraits = [RenderNode.Variant.Trait.interfaceLanguage("com.test.another-language.id")] XCTAssertEqual(entity.topicRenderReference.titleVariants.value(for: variantTraits), "Resolved Variant Title") @@ -297,7 +309,7 @@ class OutOfProcessReferenceResolverTests: XCTestCase { let fragmentVariant = try XCTUnwrap(entity.topicRenderReference.fragmentsVariants.variants.first(where: { $0.traits == variantTraits })) XCTAssertEqual(fragmentVariant.patch.map(\.operation), [.replace]) if case .replace(let variantFragment) = fragmentVariant.patch.first { - XCTAssertEqual(variantFragment, [.init(text: "variant declaration fragment", kind: .text, preciseIdentifier: nil)]) + XCTAssertEqual(variantFragment, [.init(text: "variant subheading declaration fragment", kind: .text, preciseIdentifier: nil)]) } else { XCTFail("Unexpected fragments variant patch") } From 79c9844eb24eb26d8381b9980c3d12782b0efbd8 Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:44:35 +0100 Subject: [PATCH 3/4] 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 | 10 +++---- .../Indexing/ExternalRenderNodeTests.swift | 29 +++++++++++++------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift index c23a850f7..dec5ca8e0 100644 --- a/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift +++ b/Sources/SwiftDocC/Infrastructure/Link Resolution/LinkResolver+NavigatorIndex.swift @@ -117,7 +117,8 @@ struct NavigatorExternalRenderNode: NavigatorIndexableRenderNodeRepresentation { externalID: renderNode.externalIdentifier.identifier, role: renderNode.role, symbolKind: renderNode.symbolKind?.identifier, - images: renderNode.images + images: renderNode.images, + fragments: renderNode.fragmentsVariants.value(for: traits) ) } } @@ -130,19 +131,16 @@ struct ExternalRenderNodeMetadataRepresentation: NavigatorIndexableRenderMetadat var role: String? var symbolKind: String? var images: [TopicImage] + 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 -} \ No newline at end of file +} diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift index 43243e342..c24a392e4 100644 --- a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift @@ -13,7 +13,7 @@ import XCTest @_spi(ExternalLinks) @testable import SwiftDocC class ExternalRenderNodeTests: XCTestCase { - func generateExternalResover() -> TestMultiResultExternalReferenceResolver { + func generateExternalResolver() -> TestMultiResultExternalReferenceResolver { let externalResolver = TestMultiResultExternalReferenceResolver() externalResolver.bundleID = "com.test.external" externalResolver.entitiesToReturn["/path/to/external/swiftArticle"] = .success( @@ -37,7 +37,12 @@ class ExternalRenderNodeTests: XCTestCase { referencePath: "/path/to/external/swiftSymbol", title: "SwiftSymbol", kind: .class, - language: .swift + 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) + ]) ) ) externalResolver.entitiesToReturn["/path/to/external/objCSymbol"] = .success( @@ -45,7 +50,11 @@ class ExternalRenderNodeTests: XCTestCase { referencePath: "/path/to/external/objCSymbol", title: "ObjCSymbol", kind: .function, - language: .objectiveC + language: .objectiveC, + declarationFragments: .init(declarationFragments: [ + .init(kind: .text, spelling: "- ", preciseIdentifier: nil), + .init(kind: .identifier, spelling: "ObjCSymbol", preciseIdentifier: nil), + ]) ) ) return externalResolver @@ -53,7 +62,7 @@ class ExternalRenderNodeTests: XCTestCase { func testExternalRenderNode() throws { - let externalResolver = generateExternalResover() + let externalResolver = generateExternalResolver() let (_, bundle, context) = try testBundleAndContext( copying: "MixedLanguageFramework", externalResolvers: [externalResolver.bundleID: externalResolver] @@ -146,16 +155,18 @@ class ExternalRenderNodeTests: XCTestCase { ) XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.title, swiftTitle) XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.navigatorTitle, navigatorTitle) + XCTAssertEqual(swiftNavigatorExternalRenderNode.metadata.fragments, fragments) let objcNavigatorExternalRenderNode = try XCTUnwrap( NavigatorExternalRenderNode(renderNode: externalRenderNode, trait: .interfaceLanguage("objc")) ) XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.title, occTitle) XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.navigatorTitle, occNavigatorTitle) + XCTAssertEqual(objcNavigatorExternalRenderNode.metadata.fragments, occFragments) } func testNavigatorWithExternalNodes() throws { - let externalResolver = generateExternalResover() + let externalResolver = generateExternalResolver() let (_, bundle, context) = try testBundleAndContext( copying: "MixedLanguageFramework", externalResolvers: [externalResolver.bundleID: externalResolver] @@ -204,14 +215,14 @@ class ExternalRenderNodeTests: XCTestCase { let occExternalNodes = renderIndex.interfaceLanguages["occ"]?.first { $0.path == "/documentation/mixedlanguageframework" }?.children?.filter { $0.path?.contains("/path/to/external") ?? false } ?? [] XCTAssertEqual(swiftExternalNodes.count, 2) XCTAssertEqual(occExternalNodes.count, 2) - XCTAssertEqual(swiftExternalNodes.map(\.title), ["SwiftArticle", "SwiftSymbol"]) - XCTAssertEqual(occExternalNodes.map(\.title), ["ObjCArticle", "ObjCSymbol"]) + XCTAssertEqual(swiftExternalNodes.map(\.title), ["SwiftArticle", "class SwiftSymbol"]) + XCTAssertEqual(occExternalNodes.map(\.title), ["ObjCArticle", "- ObjCSymbol"]) XCTAssert(swiftExternalNodes.allSatisfy(\.isExternal)) XCTAssert(occExternalNodes.allSatisfy(\.isExternal)) } func testNavigatorWithExternalNodesOnlyAddsCuratedNodesToNavigator() throws { - let externalResolver = generateExternalResover() + let externalResolver = generateExternalResolver() let (_, bundle, context) = try testBundleAndContext( copying: "MixedLanguageFramework", @@ -265,7 +276,7 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(swiftExternalNodes.count, 1) XCTAssertEqual(occExternalNodes.count, 1) XCTAssertEqual(swiftExternalNodes.map(\.title), ["SwiftArticle"]) - XCTAssertEqual(occExternalNodes.map(\.title), ["ObjCSymbol"]) + XCTAssertEqual(occExternalNodes.map(\.title), ["- ObjCSymbol"]) XCTAssert(swiftExternalNodes.allSatisfy(\.isExternal)) XCTAssert(occExternalNodes.allSatisfy(\.isExternal)) } From 2785db22134821aaf685074c5afe6729cb48105f Mon Sep 17 00:00:00 2001 From: Andrea Fernandez Buitrago <15234535+anferbui@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:34:12 +0100 Subject: [PATCH 4/4] fixup! Use the subheading fragments to init the external entity --- .../OutOfProcessReferenceResolverTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift index 4bcd99e04..47c973253 100644 --- a/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/OutOfProcessReferenceResolverTests.swift @@ -88,7 +88,7 @@ class OutOfProcessReferenceResolverTests: XCTestCase { ]), subHeadingDeclarationFragments: .init(declarationFragments: [ .init(kind: .text, spelling: "variant subheading declaration fragment", preciseIdentifier: nil) - ]), + ]) ) ] )