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, diff --git a/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift b/Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift index 4c6fac3500..55bb272813 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 plainTextDeclaration = summary.plainTextDeclaration { + return plainTextDeclaration } if let variant = summary.variants.first(where: { $0.traits.contains(.interfaceLanguage(symbolID.interfaceLanguage)) }), - let fragments = variant.declarationFragments ?? summary.declarationFragments + let plainTextDeclaration = variant.plainTextDeclaration ?? summary.plainTextDeclaration { - return fragments.plainTextDeclaration() + return plainTextDeclaration } } 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 { @@ -177,7 +171,8 @@ 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: navigatorDeclarationFragments) for variant in variants { let traits = variant.traits @@ -187,9 +182,12 @@ 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.navigatorDeclarationFragments { + navigatorTitleVariants.variants.append(.init(traits: traits, patch: [.replace(value: navigatorTitle)])) + } } return TopicRenderReference( @@ -201,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/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/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 0a53b58c3d..a092359f28 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -135,11 +135,32 @@ 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 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] - /// The fragments for this symbol's declaration, 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? + @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 navigator fragments. + public let navigatorDeclarationFragments: DeclarationFragments? + /// Any previous URLs for this element. /// /// A web server can use this list of URLs to redirect to the current URL. @@ -193,11 +214,30 @@ 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 declaration of the variant or `nil` if the declaration 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 plain text declaration but the variant doesn't, this property will be `Optional.some(nil)`. + public let plainTextDeclaration: VariantValue + + /// 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 + @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 navigatorDeclarationFragments: 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,7 +255,9 @@ 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. - /// - declarationFragments: The declaration of the variant or `nil` if the declaration 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 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, @@ -225,7 +267,9 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: VariantValue = nil, taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, - declarationFragments: VariantValue = nil + plainTextDeclaration: VariantValue = nil, + subheadingDeclarationFragments: VariantValue = nil, + navigatorDeclarationFragments: VariantValue = nil ) { self.traits = traits self.kind = kind @@ -235,10 +279,12 @@ public struct LinkDestinationSummary: Codable, Equatable { self.abstract = abstract self.taskGroups = taskGroups self.usr = usr - self.declarationFragments = declarationFragments + self.plainTextDeclaration = plainTextDeclaration + self.subheadingDeclarationFragments = subheadingDeclarationFragments + self.navigatorDeclarationFragments = navigatorDeclarationFragments } - @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: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, @@ -248,7 +294,9 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: VariantValue = nil, taskGroups: VariantValue<[LinkDestinationSummary.TaskGroup]?> = nil, usr: VariantValue = nil, + plainTextDeclaration: VariantValue = nil, declarationFragments: VariantValue = nil, + navigatorDeclarationFragments: VariantValue = nil, topicImages: VariantValue<[TopicImage]?> = nil ) { self.init( @@ -260,7 +308,9 @@ public struct LinkDestinationSummary: Codable, Equatable { abstract: abstract, taskGroups: taskGroups, usr: usr, - declarationFragments: declarationFragments + plainTextDeclaration: plainTextDeclaration, + subheadingDeclarationFragments: declarationFragments, + navigatorDeclarationFragments: navigatorDeclarationFragments ) } } @@ -281,7 +331,9 @@ 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. - /// - declarationFragments: The fragments for this symbol's declaration, 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, 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. @@ -296,7 +348,9 @@ public struct LinkDestinationSummary: Codable, Equatable { platforms: [LinkDestinationSummary.PlatformAvailability]? = nil, taskGroups: [LinkDestinationSummary.TaskGroup]? = nil, usr: String? = nil, - declarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + plainTextDeclaration: String? = nil, + subheadingDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, + navigatorDeclarationFragments: LinkDestinationSummary.DeclarationFragments? = nil, redirects: [URL]? = nil, topicImages: [TopicImage]? = nil, references: [any RenderReference]? = nil, @@ -312,12 +366,54 @@ public struct LinkDestinationSummary: Codable, Equatable { self.platforms = platforms self.taskGroups = taskGroups self.usr = usr - self.declarationFragments = declarationFragments + self.plainTextDeclaration = plainTextDeclaration + self.subheadingDeclarationFragments = subheadingDeclarationFragments + 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: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, + 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?, + navigatorDeclarationFragments: 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, + navigatorDeclarationFragments: navigatorDeclarationFragments, + redirects: redirects, + topicImages: topicImages, + references: references, + variants: variants + ) + } } // MARK: - Accessing the externally linkable elements @@ -442,7 +538,7 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: nil, - declarationFragments: nil, + subheadingDeclarationFragments: nil, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -466,24 +562,37 @@ extension LinkDestinationSummary { let abstract = renderSymbolAbstract(symbol.abstractVariants[summaryTrait] ?? symbol.abstract) let usr = symbol.externalIDVariants[summaryTrait] ?? symbol.externalID - let declaration = (symbol.declarationVariants[summaryTrait] ?? symbol.declaration).renderDeclarationTokens() + let plainTextDeclaration = symbol.plainTextDeclaration(for: summaryTrait) let language = documentationNode.sourceLanguage - + // 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 navigatorDeclarationFragments = renderNode.metadata.navigatorTitleVariants.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 { 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? { return main == variant ? nil : variant } + let plainTextDeclarationVariant = symbol.plainTextDeclaration(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 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) }), @@ -493,7 +602,9 @@ extension LinkDestinationSummary { abstract: nilIfEqual(main: abstract, variant: abstractVariant), taskGroups: nilIfEqual(main: taskGroups, variant: taskGroupVariants[variantTraits]), usr: nil, // The symbol variant uses the same USR - declarationFragments: nilIfEqual(main: declaration, variant: declarationVariant) + plainTextDeclaration: nilIfEqual(main: plainTextDeclaration, variant: plainTextDeclarationVariant), + subheadingDeclarationFragments: nilIfEqual(main: subheadingDeclarationFragments, variant: subheadingDeclarationFragmentsVariant), + navigatorDeclarationFragments: nilIfEqual(main: navigatorDeclarationFragments, variant: navigatorDeclarationFragmentsVariant) ) } @@ -512,7 +623,9 @@ extension LinkDestinationSummary { platforms: platforms, taskGroups: taskGroups, usr: usr, - declarationFragments: declaration, + plainTextDeclaration: plainTextDeclaration, + subheadingDeclarationFragments: subheadingDeclarationFragments, + navigatorDeclarationFragments: navigatorDeclarationFragments, redirects: redirects, topicImages: topicImages.nilIfEmpty, references: references.nilIfEmpty, @@ -580,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 @@ -594,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 + case kind, referenceURL, title, abstract, language, taskGroups, usr, availableLanguages, platforms, redirects, topicImages, references, variants, plainTextDeclaration case relativePresentationURL = "path" - case declarationFragments = "fragments" + case subheadingDeclarationFragments = "fragments" + case navigatorDeclarationFragments = "navigatorFragments" } public func encode(to encoder: any Encoder) throws { @@ -626,7 +740,9 @@ extension LinkDestinationSummary { try container.encodeIfPresent(platforms, forKey: .platforms) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) try container.encodeIfPresent(usr, forKey: .usr) - try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) + try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .subheadingDeclarationFragments) + 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) @@ -682,7 +798,9 @@ 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) - declarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .declarationFragments) + plainTextDeclaration = try container.decodeIfPresent(String.self, forKey: .plainTextDeclaration) + subheadingDeclarationFragments = try container.decodeIfPresent(DeclarationFragments.self, forKey: .subheadingDeclarationFragments) + 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 @@ -695,9 +813,10 @@ 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, plainTextDeclaration case relativePresentationURL = "path" case declarationFragments = "fragments" + case navigatorDeclarationFragments = "navigatorFragments" } public func encode(to encoder: any Encoder) throws { @@ -721,7 +840,9 @@ extension LinkDestinationSummary.Variant { } } try container.encodeIfPresent(usr, forKey: .usr) - try container.encodeIfPresent(declarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(plainTextDeclaration, forKey: .plainTextDeclaration) + try container.encodeIfPresent(subheadingDeclarationFragments, forKey: .declarationFragments) + try container.encodeIfPresent(navigatorDeclarationFragments, forKey: .navigatorDeclarationFragments) try container.encodeIfPresent(taskGroups, forKey: .taskGroups) } @@ -762,7 +883,10 @@ 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) - declarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) + plainTextDeclaration = try container.decodeIfPresent(String?.self, forKey: .plainTextDeclaration) + subheadingDeclarationFragments = try container.decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .declarationFragments) + navigatorDeclarationFragments = try container + .decodeIfPresent(LinkDestinationSummary.DeclarationFragments?.self, forKey: .navigatorDeclarationFragments) taskGroups = try container.decodeIfPresent([LinkDestinationSummary.TaskGroup]?.self, forKey: .taskGroups) } } @@ -787,7 +911,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 } @@ -871,3 +995,10 @@ private extension Collection { isEmpty ? nil : self } } + +private extension Symbol { + 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..a07d1c18a2 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/LinkableEntities.json @@ -68,12 +68,21 @@ "usr": { "type": "string" }, + "plainTextDeclaration": { + "type": "string" + }, "fragments": { "type": "array", "items": { "$ref": "#/components/schemas/DeclarationToken" } }, + "navigatorFragments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarationToken" + } + }, "topicImages": { "type": "array", "items": { @@ -158,6 +167,10 @@ "type": "string", "nullable": true }, + "plainTextDeclaration": { + "type": "string", + "nullable": true + }, "fragments": { "type": "array", "items": { @@ -165,6 +178,13 @@ }, "nullable": true }, + "navigatorFragments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeclarationToken" + }, + "nullable": true + }, "taskGroups": { "type": "array", "items": { diff --git a/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift b/Tests/SwiftDocCTests/Indexing/ExternalRenderNodeTests.swift index d0602e48ca..d2c790caa0 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,49 @@ 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)] + ) + ) + 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)] ) ) @@ -132,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 ) ] ) @@ -152,12 +200,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,21 +268,114 @@ class ExternalRenderNodeTests: XCTestCase { XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.swift.id]?.count(where: \.isExternal), 0) XCTAssertEqual(renderIndex.interfaceLanguages[SourceLanguage.objectiveC.id]?.count(where: \.isExternal), 0) + + 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 = (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(externalTopLevelNodes(for: .swift)) XCTAssertEqual(swiftExternalNodes.count, 2) + + let objcExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .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 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 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(externalTopLevelNodes(for: .swift)) + let objcExternalNodes = try XCTUnwrap(externalTopLevelNodes(for: .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: [ @@ -299,7 +442,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"]) } @@ -324,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/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/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 7ea89aa72c..d02463b820 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 } @@ -92,6 +93,8 @@ class TestMultiResultExternalReferenceResolver: ExternalDocumentationSource { title: entityInfo.title, availableLanguages: [entityInfo.language], platforms: entityInfo.platforms, + subheadingDeclarationFragments: entityInfo.declarationFragments?.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 a51ef8b102..5606958101 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -113,7 +113,9 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(pageSummary.platforms, renderNode.metadata.platforms) XCTAssertEqual(pageSummary.redirects, nil) XCTAssertNil(pageSummary.usr, "Only symbols have USRs") - XCTAssertNil(pageSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(pageSummary.plainTextDeclaration, "Only symbols have a plain text declaration") + XCTAssertNil(pageSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") + 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") @@ -131,7 +133,9 @@ class LinkDestinationSummaryTests: XCTestCase { URL(string: "old/path/to/this/landmark")!, ]) XCTAssertNil(sectionSummary.usr, "Only symbols have USRs") - XCTAssertNil(sectionSummary.declarationFragments, "Only symbols have declaration fragments") + XCTAssertNil(sectionSummary.plainTextDeclaration, "Only symbols have a plain text declaration") + XCTAssertNil(sectionSummary.subheadingDeclarationFragments, "Only symbols have subheading declaration fragments") + 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(" "), @@ -180,11 +184,15 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "class MyClass") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "MyClass", kind: .identifier, identifier: nil), ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "MyClassNavigator", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -219,13 +227,17 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ProtocolP") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "protocol MyProtocol : Hashable") + 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"), ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "MyProtocol", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -250,7 +262,8 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "myFunction", kind: .identifier, identifier: nil), @@ -261,6 +274,7 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) + XCTAssertNil(summary.navigatorDeclarationFragments, "This symbol doesn't have a navigator title") XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -285,13 +299,24 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit14globalFunction_11consideringy10Foundation4DataV_SitF") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "func globalFunction(_: Data, considering: Int)") + 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: "_", kind: .identifier, 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) + ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .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), @@ -342,7 +367,8 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages, [.swift]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "s:5MyKit0A5ClassC10myFunctionyyF") - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "func myFunction(for name...)") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "func", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "myFunction", kind: .identifier, identifier: nil), @@ -353,7 +379,8 @@ class LinkDestinationSummaryTests: XCTestCase { .init(text: "...", kind: .text, identifier: nil), .init(text: ")", kind: .text, identifier: nil) ]) - + XCTAssertNil(summary.navigatorDeclarationFragments, "This symbol doesn't have a navigator title") + XCTAssertEqual(summary.topicImages, [ TopicImage( type: .card, @@ -454,12 +481,15 @@ class LinkDestinationSummaryTests: XCTestCase { XCTAssertEqual(summary.availableLanguages.sorted(), [.swift, .objectiveC]) XCTAssertEqual(summary.platforms, renderNode.metadata.platforms) XCTAssertEqual(summary.usr, "c:objc(cs)Bar") - - XCTAssertEqual(summary.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "class Bar") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil) ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "Bar", kind: .identifier, identifier: nil) + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -468,14 +498,18 @@ class LinkDestinationSummaryTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) - XCTAssertEqual(variant.declarationFragments, [ + XCTAssertEqual(variant.plainTextDeclaration, "@interface Bar : NSObject") + XCTAssertEqual(variant.subheadingDeclarationFragments, [ .init(text: "@interface", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "Bar", kind: .identifier, identifier: nil), .init(text: " : ", kind: .text, identifier: nil), .init(text: "NSObject", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "c:objc(cs)NSObject"), ]) - + XCTAssertEqual(variant.navigatorDeclarationFragments, [ + .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) @@ -514,23 +548,23 @@ 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.declarationFragments, [ + XCTAssertEqual(summary.plainTextDeclaration, "class func myStringFunction(_ string: String) throws -> String") + XCTAssertEqual(summary.subheadingDeclarationFragments, [ .init(text: "class", kind: .keyword, identifier: nil), .init(text: " ", kind: .text, identifier: nil), .init(text: "func", kind: .keyword, identifier: nil), .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), .init(text: " -> ", kind: .text, identifier: nil), .init(text: "String", kind: .typeIdentifier, identifier: nil, preciseIdentifier: "s:SS") ]) + XCTAssertEqual(summary.navigatorDeclarationFragments, [ + .init(text: "myStringFunction:error: (navigator title)", kind: .identifier, identifier: nil), + ]) XCTAssertNil(summary.topicImages) XCTAssertNil(summary.references) @@ -540,20 +574,13 @@ class LinkDestinationSummaryTests: XCTestCase { // Check variant content that is different XCTAssertEqual(variant.language, .objectiveC) XCTAssertEqual(variant.title, "myStringFunction: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) + XCTAssertEqual(variant.plainTextDeclaration, "+ (NSString *) myStringFunction: (NSString *)string error: (NSError **)error;") + XCTAssertEqual(variant.subheadingDeclarationFragments, [ + .init(text: "+ ", kind: .text, identifier: nil), + .init(text: "myStringFunction:error:", kind: .identifier, identifier: nil) ]) - + 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) XCTAssertEqual(variant.usr, nil) @@ -686,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/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/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/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..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", @@ -195,7 +201,7 @@ }, { "kind" : "text", - "spelling" : " *)string" + "spelling" : " *)string " }, { "kind" : "identifier", @@ -366,46 +372,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 +458,7 @@ "text" : "This is the foo's description." } ] - }, + } }, { "accessLevel" : "public", @@ -570,7 +543,7 @@ { "kind" : "typeIdentifier", "spelling" : "NSString", - "preciseIdentifier": "c:@T@NSInteger", + "preciseIdentifier": "c:@T@NSInteger" }, { "kind": "text", @@ -630,7 +603,7 @@ "kind" : "identifier", "spelling" : "first" } - ], + ] }, "pathComponents" : [ "Foo", @@ -677,7 +650,7 @@ "kind" : "identifier", "spelling" : "fourth" } - ], + ] }, "pathComponents" : [ "Foo", @@ -724,7 +697,7 @@ "kind" : "identifier", "spelling" : "second" } - ], + ] }, "pathComponents" : [ "Foo", @@ -771,7 +744,7 @@ "kind" : "identifier", "spelling" : "third" } - ], + ] }, "pathComponents" : [ "Foo", 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,