Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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?
Copy link
Contributor

Choose a reason for hiding this comment

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

@d-ronnqvist Could this property be made public to match the rest of this struct? It doesn't make sense to me that a client of DocC's API could access all the other properties in this structure except this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does make sense to have API that only DocC can use but that clients can't.

In this case I'm not sure yet if this property fits into the long-term design of linking between documentation and unless we have an explicit need for making it public (in which case I would probably say we should only make it SPI) I would really prefer to not commit to its long term existence.

This type (LinkDestinationSummary) is used for both documentation from other documentation sources (resolved via a separate process) and documentation from other DocC targets (resolved via --dependency arguments). DocC itself doesn't produce any summaries with absolute presentation URLs but we can't make any guarantees about what other documentation systems do.

At this point I would rather not have behaviors of other documentation systems impact the design for DocC's own external documentation information.


/// 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)

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")
}
}