Skip to content

Commit 56ba4eb

Browse files
authored
Add a helper function for rendering availability information as HTML (#1376)
rdar://163326857
1 parent 4876d1c commit 56ba4eb

File tree

4 files changed

+194
-1
lines changed

4 files changed

+194
-1
lines changed

Sources/DocCHTML/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ See https://swift.org/LICENSE.txt for license information
99

1010
add_library(DocCHTML STATIC
1111
LinkProvider.swift
12+
MarkdownRenderer+Availability.swift
1213
MarkdownRenderer.swift
1314
WordBreak.swift
1415
XMLNode+element.swift)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
#if canImport(FoundationXML)
12+
// TODO: Consider other HTML rendering options as a future improvement (rdar://165755530)
13+
package import FoundationXML
14+
#else
15+
package import Foundation
16+
#endif
17+
18+
package extension MarkdownRenderer {
19+
/// Information about the versions that a piece of API is available for a given platform.
20+
struct AvailabilityInfo {
21+
/// The name of the platform that this information applies to.
22+
package var name: String
23+
/// The pre-formatted version string that describes the version that this API was introduced in for this platform.
24+
package var introduced: String?
25+
/// The pre-formatted version string that describes the version that this API was deprecated in for this platform.
26+
package var deprecated: String?
27+
/// A Boolean value indicating if the platform is currently in beta.
28+
package var isBeta: Bool
29+
30+
package init(name: String, introduced: String? = nil, deprecated: String? = nil, isBeta: Bool) {
31+
self.name = name
32+
self.introduced = introduced
33+
self.deprecated = deprecated
34+
self.isBeta = isBeta
35+
}
36+
}
37+
38+
/// Creates an HTML element that describes the versions that a piece of API is available for the platforms described in the given availability information.
39+
func availability(_ info: [AvailabilityInfo]) -> XMLNode {
40+
let items: [XMLNode] = info.map {
41+
var text = $0.name
42+
43+
let description: String
44+
if let introduced = $0.introduced {
45+
if let deprecated = $0.deprecated{
46+
text += " \(introduced)\(deprecated)"
47+
description = "Introduced in \($0.name) \(introduced) and deprecated in \($0.name) \(deprecated)"
48+
} else {
49+
text += " \(introduced)+"
50+
description = "Available on \(introduced) and later"
51+
}
52+
} else {
53+
description = "Available on \($0.name)"
54+
}
55+
56+
var attributes = [
57+
"role": "text",
58+
"aria-label": "\(text), \(description)",
59+
"title": description
60+
]
61+
if $0.isBeta {
62+
attributes["class"] = "beta"
63+
} else if $0.deprecated != nil {
64+
attributes["class"] = "deprecated"
65+
}
66+
67+
return .element(named: "li", children: [.text(text)], attributes: goal == .richness ? attributes : [:])
68+
}
69+
70+
return .element(
71+
named: "ul",
72+
children: items,
73+
attributes: ["id": "availability"]
74+
)
75+
}
76+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
#if canImport(FoundationXML)
12+
// TODO: Consider other HTML rendering options as a future improvement (rdar://165755530)
13+
import FoundationXML
14+
import FoundationEssentials
15+
#else
16+
import Foundation
17+
#endif
18+
19+
import Testing
20+
import DocCHTML
21+
import Markdown
22+
23+
struct MarkdownRenderer_PageElementsTests {
24+
@Test(arguments: RenderGoal.allCases)
25+
func testRenderAvailability(goal: RenderGoal) {
26+
let availability = makeRenderer(goal: goal).availability([
27+
.init(name: "First", introduced: "1.2", deprecated: "3.4", isBeta: false),
28+
.init(name: "Second", introduced: "1.2.3", isBeta: false),
29+
.init(name: "Third", introduced: "4.5", isBeta: true),
30+
])
31+
switch goal {
32+
case .richness:
33+
availability.assertMatches(prettyFormatted: true, expectedXMLString: """
34+
<ul id="availability">
35+
<li aria-label="First 1.2–3.4, Introduced in First 1.2 and deprecated in First 3.4" class="deprecated" role="text" title="Introduced in First 1.2 and deprecated in First 3.4">First 1.2–3.4</li>
36+
<li aria-label="Second 1.2.3+, Available on 1.2.3 and later" role="text" title="Available on 1.2.3 and later">Second 1.2.3+</li>
37+
<li aria-label="Third 4.5+, Available on 4.5 and later" class="beta" role="text" title="Available on 4.5 and later">Third 4.5+</li>
38+
</ul>
39+
""")
40+
case .conciseness:
41+
availability.assertMatches(prettyFormatted: true, expectedXMLString: """
42+
<ul id="availability">
43+
<li>First 1.2–3.4</li>
44+
<li>Second 1.2.3+</li>
45+
<li>Third 4.5+</li>
46+
</ul>
47+
""")
48+
}
49+
}
50+
51+
// MARK: -
52+
53+
private func makeRenderer(
54+
goal: RenderGoal,
55+
elementsToReturn: [LinkedElement] = [],
56+
pathsToReturn: [String: URL] = [:],
57+
assetsToReturn: [String: LinkedAsset] = [:],
58+
fallbackLinkTextsToReturn: [String: String] = [:]
59+
) -> MarkdownRenderer<some LinkProvider> {
60+
let path = URL(string: "/documentation/ModuleName/Something/ThisPage/index.html")!
61+
62+
var elementsByURL = [
63+
path: LinkedElement(
64+
path: path,
65+
names: .single( .symbol("ThisPage") ),
66+
subheadings: .single( .symbol([
67+
.init(text: "class ", kind: .decorator),
68+
.init(text: "ThisPage", kind: .identifier),
69+
])),
70+
abstract: nil
71+
)
72+
]
73+
for element in elementsToReturn {
74+
elementsByURL[element.path] = element
75+
}
76+
77+
return MarkdownRenderer(path: path, goal: goal, linkProvider: MultiValueLinkProvider(
78+
elementsToReturn: elementsByURL,
79+
pathsToReturn: pathsToReturn,
80+
assetsToReturn: assetsToReturn,
81+
fallbackLinkTextsToReturn: fallbackLinkTextsToReturn
82+
))
83+
}
84+
85+
private func parseMarkup(string: String) -> [any Markup] {
86+
let document = Document(parsing: string, options: [.parseBlockDirectives, .parseSymbolLinks])
87+
return Array(document.children)
88+
}
89+
}
90+
91+
struct MultiValueLinkProvider: LinkProvider {
92+
var elementsToReturn: [URL: LinkedElement]
93+
func element(for path: URL) -> LinkedElement? {
94+
elementsToReturn[path]
95+
}
96+
97+
var pathsToReturn: [String: URL]
98+
func pathForSymbolID(_ usr: String) -> URL? {
99+
pathsToReturn[usr]
100+
}
101+
102+
var assetsToReturn: [String: LinkedAsset]
103+
func assetNamed(_ assetName: String) -> LinkedAsset? {
104+
assetsToReturn[assetName]
105+
}
106+
107+
var fallbackLinkTextsToReturn: [String: String]
108+
func fallbackLinkText(linkString: String) -> String {
109+
fallbackLinkTextsToReturn[linkString] ?? linkString
110+
}
111+
}
112+
113+
extension RenderGoal: CaseIterable {
114+
package static var allCases: [RenderGoal] {
115+
[.richness, .conciseness]
116+
}
117+
}

Tests/DocCHTMLTests/MarkdownRendererTests.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import Foundation
1818

1919
import Testing
2020
import DocCHTML
21-
@testable import SwiftDocC
2221
import Markdown
2322

2423
struct MarkdownRendererTests {

0 commit comments

Comments
 (0)