Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ extension LinkDestinationSummary {
identifier: .init(referenceURL.absoluteString),
titleVariants: titleVariants,
abstractVariants: abstractVariants,
url: relativePresentationURL.absoluteString,
url: absolutePresentationURL?.absoluteString ?? relativePresentationURL.absoluteString,
kind: kind,
required: false,
role: role,
Expand Down
32 changes: 31 additions & 1 deletion Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public struct LinkDestinationSummary: Codable, Equatable {
/// The relative presentation URL for this element.
public let relativePresentationURL: URL

/// The absolute presentation URL for this element, or `nil` if only the _relative_ presentation URL is known.
///
/// - Note: The absolute presentation URL (if one exists) and the relative presentation URL will always have the same path and fragment components.
let absolutePresentationURL: URL?

/// The resolved topic reference URL to this element.
public var referenceURL: URL

Expand Down Expand Up @@ -359,6 +364,7 @@ public struct LinkDestinationSummary: Codable, Equatable {
self.kind = kind
self.language = language
self.relativePresentationURL = relativePresentationURL
self.absolutePresentationURL = nil
self.referenceURL = referenceURL
self.title = title
self.abstract = abstract
Expand Down Expand Up @@ -763,7 +769,9 @@ extension LinkDestinationSummary {
} catch {
kind = try container.decode(DocumentationNode.Kind.self, forKey: .kind)
}
relativePresentationURL = try container.decode(URL.self, forKey: .relativePresentationURL)
let decodedURL = try container.decode(URL.self, forKey: .relativePresentationURL)
(relativePresentationURL, absolutePresentationURL) = Self.checkIfDecodedURLWasAbsolute(decodedURL)

Comment on lines +772 to +774
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add some tests specifically for the decoding logic?

referenceURL = try container.decode(URL.self, forKey: .referenceURL)
title = try container.decode(String.self, forKey: .title)
abstract = try container.decodeIfPresent(Abstract.self, forKey: .abstract)
Expand Down Expand Up @@ -808,6 +816,28 @@ extension LinkDestinationSummary {

variants = try container.decodeIfPresent([Variant].self, forKey: .variants) ?? []
}

private static func checkIfDecodedURLWasAbsolute(_ decodedURL: URL) -> (relative: URL, absolute: URL?) {
guard decodedURL.isAbsoluteWebURL,
var components = URLComponents(url: decodedURL, resolvingAgainstBaseURL: false)
else {
// If the decoded URL isn't an absolute web URL that's valid according to RFC 3986, then treat it as relative.
return (relative: decodedURL, absolute: nil)
}

// Remove the scheme, user, port, and host to create a relative URL.
components.scheme = nil
components.user = nil
components.host = nil
components.port = nil

guard let relativeURL = components.url else {
// If we can't create a relative URL that's valid according to RFC 3986, then treat the original as relative.
return (relative: decodedURL, absolute: nil)
}

return (relative: relativeURL, absolute: decodedURL)
}
}

extension LinkDestinationSummary.Variant {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1379,4 +1379,62 @@ class ExternalReferenceResolverTests: XCTestCase {
XCTAssertEqual(externalLinkCount, 2, "Did not resolve the 2 expected external links.")
}

func testExternalReferenceWithAbsolutePresentationURL() async throws {
class Resolver: ExternalDocumentationSource {
let bundleID: DocumentationBundle.Identifier = "com.example.test"

func resolve(_ reference: TopicReference) -> TopicReferenceResolutionResult {
.success(ResolvedTopicReference(bundleID: bundleID, path: "/path/to/something", sourceLanguage: .swift))
}

var entityToReturn: LinkDestinationSummary
init(entityToReturn: LinkDestinationSummary) {
self.entityToReturn = entityToReturn
}

func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity {
entityToReturn
}
}

let catalog = Folder(name: "unit-test.docc", content: [
TextFile(name: "Root.md", utf8Content: """
# Root

Link to an external page: <doc://com.example.test/something>
"""),
])

// Only decoded link summaries support absolute presentation URLs.
let externalEntity = try JSONDecoder().decode(LinkDestinationSummary.self, from: Data("""
{
"path": "https://com.example/path/to/something",
"title": "Something",
"kind": "org.swift.docc.kind.article",
"referenceURL": "doc://com.example.test/path/to/something",
"language": "swift",
"availableLanguages": [
"swift"
]
}
""".utf8))
XCTAssertEqual(externalEntity.relativePresentationURL.absoluteString, "/path/to/something")
XCTAssertEqual(externalEntity.absolutePresentationURL?.absoluteString, "https://com.example/path/to/something")

let resolver = Resolver(entityToReturn: externalEntity)

var configuration = DocumentationContext.Configuration()
configuration.externalDocumentationConfiguration.sources = [resolver.bundleID: resolver]
let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration)

XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))")

// Check the curation on the root page
let rootNode = try context.entity(with: XCTUnwrap(context.soleRootModuleReference))
let converter = DocumentationNodeConverter(context: context)

let renderNode = converter.convert(rootNode)
let externalTopicReference = try XCTUnwrap(renderNode.references.values.first as? TopicRenderReference)
XCTAssertEqual(externalTopicReference.url, "https://com.example/path/to/something")
}
}