Skip to content

Commit 7db36ad

Browse files
authored
Support decoding link summaries with absolute paths to external pages (#1334)
rdar://149470919
1 parent 8beb703 commit 7db36ad

File tree

3 files changed

+90
-2
lines changed

3 files changed

+90
-2
lines changed

Sources/SwiftDocC/Infrastructure/Link Resolution/ExternalPathHierarchyResolver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ extension LinkDestinationSummary {
192192
identifier: .init(referenceURL.absoluteString),
193193
titleVariants: titleVariants,
194194
abstractVariants: abstractVariants,
195-
url: relativePresentationURL.absoluteString,
195+
url: absolutePresentationURL?.absoluteString ?? relativePresentationURL.absoluteString,
196196
kind: kind,
197197
required: false,
198198
role: role,

Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ public struct LinkDestinationSummary: Codable, Equatable {
8383
/// The relative presentation URL for this element.
8484
public let relativePresentationURL: URL
8585

86+
/// The absolute presentation URL for this element, or `nil` if only the _relative_ presentation URL is known.
87+
///
88+
/// - Note: The absolute presentation URL (if one exists) and the relative presentation URL will always have the same path and fragment components.
89+
let absolutePresentationURL: URL?
90+
8691
/// The resolved topic reference URL to this element.
8792
public var referenceURL: URL
8893

@@ -359,6 +364,7 @@ public struct LinkDestinationSummary: Codable, Equatable {
359364
self.kind = kind
360365
self.language = language
361366
self.relativePresentationURL = relativePresentationURL
367+
self.absolutePresentationURL = nil
362368
self.referenceURL = referenceURL
363369
self.title = title
364370
self.abstract = abstract
@@ -763,7 +769,9 @@ extension LinkDestinationSummary {
763769
} catch {
764770
kind = try container.decode(DocumentationNode.Kind.self, forKey: .kind)
765771
}
766-
relativePresentationURL = try container.decode(URL.self, forKey: .relativePresentationURL)
772+
let decodedURL = try container.decode(URL.self, forKey: .relativePresentationURL)
773+
(relativePresentationURL, absolutePresentationURL) = Self.checkIfDecodedURLWasAbsolute(decodedURL)
774+
767775
referenceURL = try container.decode(URL.self, forKey: .referenceURL)
768776
title = try container.decode(String.self, forKey: .title)
769777
abstract = try container.decodeIfPresent(Abstract.self, forKey: .abstract)
@@ -808,6 +816,28 @@ extension LinkDestinationSummary {
808816

809817
variants = try container.decodeIfPresent([Variant].self, forKey: .variants) ?? []
810818
}
819+
820+
private static func checkIfDecodedURLWasAbsolute(_ decodedURL: URL) -> (relative: URL, absolute: URL?) {
821+
guard decodedURL.isAbsoluteWebURL,
822+
var components = URLComponents(url: decodedURL, resolvingAgainstBaseURL: false)
823+
else {
824+
// If the decoded URL isn't an absolute web URL that's valid according to RFC 3986, then treat it as relative.
825+
return (relative: decodedURL, absolute: nil)
826+
}
827+
828+
// Remove the scheme, user, port, and host to create a relative URL.
829+
components.scheme = nil
830+
components.user = nil
831+
components.host = nil
832+
components.port = nil
833+
834+
guard let relativeURL = components.url else {
835+
// If we can't create a relative URL that's valid according to RFC 3986, then treat the original as relative.
836+
return (relative: decodedURL, absolute: nil)
837+
}
838+
839+
return (relative: relativeURL, absolute: decodedURL)
840+
}
811841
}
812842

813843
extension LinkDestinationSummary.Variant {

Tests/SwiftDocCTests/Infrastructure/ExternalReferenceResolverTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,4 +1379,62 @@ class ExternalReferenceResolverTests: XCTestCase {
13791379
XCTAssertEqual(externalLinkCount, 2, "Did not resolve the 2 expected external links.")
13801380
}
13811381

1382+
func testExternalReferenceWithAbsolutePresentationURL() async throws {
1383+
class Resolver: ExternalDocumentationSource {
1384+
let bundleID: DocumentationBundle.Identifier = "com.example.test"
1385+
1386+
func resolve(_ reference: TopicReference) -> TopicReferenceResolutionResult {
1387+
.success(ResolvedTopicReference(bundleID: bundleID, path: "/path/to/something", sourceLanguage: .swift))
1388+
}
1389+
1390+
var entityToReturn: LinkDestinationSummary
1391+
init(entityToReturn: LinkDestinationSummary) {
1392+
self.entityToReturn = entityToReturn
1393+
}
1394+
1395+
func entity(with reference: ResolvedTopicReference) -> LinkResolver.ExternalEntity {
1396+
entityToReturn
1397+
}
1398+
}
1399+
1400+
let catalog = Folder(name: "unit-test.docc", content: [
1401+
TextFile(name: "Root.md", utf8Content: """
1402+
# Root
1403+
1404+
Link to an external page: <doc://com.example.test/something>
1405+
"""),
1406+
])
1407+
1408+
// Only decoded link summaries support absolute presentation URLs.
1409+
let externalEntity = try JSONDecoder().decode(LinkDestinationSummary.self, from: Data("""
1410+
{
1411+
"path": "https://com.example/path/to/something",
1412+
"title": "Something",
1413+
"kind": "org.swift.docc.kind.article",
1414+
"referenceURL": "doc://com.example.test/path/to/something",
1415+
"language": "swift",
1416+
"availableLanguages": [
1417+
"swift"
1418+
]
1419+
}
1420+
""".utf8))
1421+
XCTAssertEqual(externalEntity.relativePresentationURL.absoluteString, "/path/to/something")
1422+
XCTAssertEqual(externalEntity.absolutePresentationURL?.absoluteString, "https://com.example/path/to/something")
1423+
1424+
let resolver = Resolver(entityToReturn: externalEntity)
1425+
1426+
var configuration = DocumentationContext.Configuration()
1427+
configuration.externalDocumentationConfiguration.sources = [resolver.bundleID: resolver]
1428+
let (_, context) = try await loadBundle(catalog: catalog, configuration: configuration)
1429+
1430+
XCTAssert(context.problems.isEmpty, "Unexpected problems: \(context.problems.map(\.diagnostic.summary))")
1431+
1432+
// Check the curation on the root page
1433+
let rootNode = try context.entity(with: XCTUnwrap(context.soleRootModuleReference))
1434+
let converter = DocumentationNodeConverter(context: context)
1435+
1436+
let renderNode = converter.convert(rootNode)
1437+
let externalTopicReference = try XCTUnwrap(renderNode.references.values.first as? TopicRenderReference)
1438+
XCTAssertEqual(externalTopicReference.url, "https://com.example/path/to/something")
1439+
}
13821440
}

0 commit comments

Comments
 (0)