|
| 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 | +} |
0 commit comments