diff --git a/Sources/DocCHTML/CMakeLists.txt b/Sources/DocCHTML/CMakeLists.txt index 3af4f6d1f..590c395ef 100644 --- a/Sources/DocCHTML/CMakeLists.txt +++ b/Sources/DocCHTML/CMakeLists.txt @@ -9,6 +9,7 @@ See https://swift.org/LICENSE.txt for license information add_library(DocCHTML STATIC LinkProvider.swift + MarkdownRenderer+Availability.swift MarkdownRenderer.swift WordBreak.swift XMLNode+element.swift) diff --git a/Sources/DocCHTML/MarkdownRenderer+Availability.swift b/Sources/DocCHTML/MarkdownRenderer+Availability.swift new file mode 100644 index 000000000..eb302aa90 --- /dev/null +++ b/Sources/DocCHTML/MarkdownRenderer+Availability.swift @@ -0,0 +1,76 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +#if canImport(FoundationXML) +// TODO: Consider other HTML rendering options as a future improvement (rdar://165755530) +package import FoundationXML +#else +package import Foundation +#endif + +package extension MarkdownRenderer { + /// Information about the versions that a piece of API is available for a given platform. + struct AvailabilityInfo { + /// The name of the platform that this information applies to. + package var name: String + /// The pre-formatted version string that describes the version that this API was introduced in for this platform. + package var introduced: String? + /// The pre-formatted version string that describes the version that this API was deprecated in for this platform. + package var deprecated: String? + /// A Boolean value indicating if the platform is currently in beta. + package var isBeta: Bool + + package init(name: String, introduced: String? = nil, deprecated: String? = nil, isBeta: Bool) { + self.name = name + self.introduced = introduced + self.deprecated = deprecated + self.isBeta = isBeta + } + } + + /// Creates an HTML element that describes the versions that a piece of API is available for the platforms described in the given availability information. + func availability(_ info: [AvailabilityInfo]) -> XMLNode { + let items: [XMLNode] = info.map { + var text = $0.name + + let description: String + if let introduced = $0.introduced { + if let deprecated = $0.deprecated{ + text += " \(introduced)–\(deprecated)" + description = "Introduced in \($0.name) \(introduced) and deprecated in \($0.name) \(deprecated)" + } else { + text += " \(introduced)+" + description = "Available on \(introduced) and later" + } + } else { + description = "Available on \($0.name)" + } + + var attributes = [ + "role": "text", + "aria-label": "\(text), \(description)", + "title": description + ] + if $0.isBeta { + attributes["class"] = "beta" + } else if $0.deprecated != nil { + attributes["class"] = "deprecated" + } + + return .element(named: "li", children: [.text(text)], attributes: goal == .richness ? attributes : [:]) + } + + return .element( + named: "ul", + children: items, + attributes: ["id": "availability"] + ) + } +} diff --git a/Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift b/Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift new file mode 100644 index 000000000..a8a2619e3 --- /dev/null +++ b/Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift @@ -0,0 +1,117 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2025 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +#if canImport(FoundationXML) +// TODO: Consider other HTML rendering options as a future improvement (rdar://165755530) +import FoundationXML +import FoundationEssentials +#else +import Foundation +#endif + +import Testing +import DocCHTML +import Markdown + +struct MarkdownRenderer_PageElementsTests { + @Test(arguments: RenderGoal.allCases) + func testRenderAvailability(goal: RenderGoal) { + let availability = makeRenderer(goal: goal).availability([ + .init(name: "First", introduced: "1.2", deprecated: "3.4", isBeta: false), + .init(name: "Second", introduced: "1.2.3", isBeta: false), + .init(name: "Third", introduced: "4.5", isBeta: true), + ]) + switch goal { + case .richness: + availability.assertMatches(prettyFormatted: true, expectedXMLString: """ + + """) + case .conciseness: + availability.assertMatches(prettyFormatted: true, expectedXMLString: """ + + """) + } + } + + // MARK: - + + private func makeRenderer( + goal: RenderGoal, + elementsToReturn: [LinkedElement] = [], + pathsToReturn: [String: URL] = [:], + assetsToReturn: [String: LinkedAsset] = [:], + fallbackLinkTextsToReturn: [String: String] = [:] + ) -> MarkdownRenderer { + let path = URL(string: "/documentation/ModuleName/Something/ThisPage/index.html")! + + var elementsByURL = [ + path: LinkedElement( + path: path, + names: .single( .symbol("ThisPage") ), + subheadings: .single( .symbol([ + .init(text: "class ", kind: .decorator), + .init(text: "ThisPage", kind: .identifier), + ])), + abstract: nil + ) + ] + for element in elementsToReturn { + elementsByURL[element.path] = element + } + + return MarkdownRenderer(path: path, goal: goal, linkProvider: MultiValueLinkProvider( + elementsToReturn: elementsByURL, + pathsToReturn: pathsToReturn, + assetsToReturn: assetsToReturn, + fallbackLinkTextsToReturn: fallbackLinkTextsToReturn + )) + } + + private func parseMarkup(string: String) -> [any Markup] { + let document = Document(parsing: string, options: [.parseBlockDirectives, .parseSymbolLinks]) + return Array(document.children) + } +} + +struct MultiValueLinkProvider: LinkProvider { + var elementsToReturn: [URL: LinkedElement] + func element(for path: URL) -> LinkedElement? { + elementsToReturn[path] + } + + var pathsToReturn: [String: URL] + func pathForSymbolID(_ usr: String) -> URL? { + pathsToReturn[usr] + } + + var assetsToReturn: [String: LinkedAsset] + func assetNamed(_ assetName: String) -> LinkedAsset? { + assetsToReturn[assetName] + } + + var fallbackLinkTextsToReturn: [String: String] + func fallbackLinkText(linkString: String) -> String { + fallbackLinkTextsToReturn[linkString] ?? linkString + } +} + +extension RenderGoal: CaseIterable { + package static var allCases: [RenderGoal] { + [.richness, .conciseness] + } +} diff --git a/Tests/DocCHTMLTests/MarkdownRendererTests.swift b/Tests/DocCHTMLTests/MarkdownRendererTests.swift index 39c2d88eb..0b0071617 100644 --- a/Tests/DocCHTMLTests/MarkdownRendererTests.swift +++ b/Tests/DocCHTMLTests/MarkdownRendererTests.swift @@ -18,7 +18,6 @@ import Foundation import Testing import DocCHTML -@testable import SwiftDocC import Markdown struct MarkdownRendererTests {