|
| 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 import Markdown |
| 19 | +package import DocCCommon |
| 20 | + |
| 21 | +package extension MarkdownRenderer { |
| 22 | + /// Information about a specific parameter for a piece of API. |
| 23 | + struct ParameterInfo { |
| 24 | + /// The name of the parameter. |
| 25 | + package var name: String |
| 26 | + /// The markdown content that describes the parameter. |
| 27 | + package var content: [any Markup] |
| 28 | + |
| 29 | + package init(name: String, content: [any Markup]) { |
| 30 | + self.name = name |
| 31 | + self.content = content |
| 32 | + } |
| 33 | + } |
| 34 | + |
| 35 | + /// Creates a "parameters" section that describes all the parameters for a symbol. |
| 36 | + /// |
| 37 | + /// If each language representation of the API has their own language-specific parameters, pass each language representation's parameter information. |
| 38 | + /// |
| 39 | + /// If the API has the _same_ parameters in all language representations, only pass the parameters for one language. |
| 40 | + /// This produces a "parameters" section that doesn't hide any parameters for any of the languages (same as if the symbol only had one language representation) |
| 41 | + func parameters(_ info: [SourceLanguage: [ParameterInfo]]) -> [XMLNode] { |
| 42 | + let info = RenderHelpers.sortedLanguageSpecificValues(info) |
| 43 | + guard info.contains(where: { _, parameters in !parameters.isEmpty }) else { |
| 44 | + // Don't create a section if there are no parameters to describe. |
| 45 | + return [] |
| 46 | + } |
| 47 | + |
| 48 | + let items: [XMLElement] = switch info.count { |
| 49 | + case 1: |
| 50 | + [_singleLanguageParameters(info.first!.value)] |
| 51 | + |
| 52 | + case 2: |
| 53 | + [_dualLanguageParameters(primary: info.first!, secondary: info.last!)] |
| 54 | + |
| 55 | + default: |
| 56 | + // In practice DocC only encounters one or two different languages. If there would be a third one, |
| 57 | + // produce correct looking pages that may include duplicated markup by not trying to share parameters across languages. |
| 58 | + info.map { language, info in |
| 59 | + .element( |
| 60 | + named: "dl", |
| 61 | + children: _singleLanguageParameterItems(info), |
| 62 | + attributes: ["class": "\(language.id)-only"] |
| 63 | + ) |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + return selfReferencingSection(named: "Parameters", content: items) |
| 68 | + } |
| 69 | + |
| 70 | + private func _singleLanguageParameters(_ parameterInfo: [ParameterInfo]) -> XMLElement { |
| 71 | + .element(named: "dl", children: _singleLanguageParameterItems(parameterInfo)) |
| 72 | + } |
| 73 | + |
| 74 | + private func _singleLanguageParameterItems(_ parameterInfo: [ParameterInfo]) -> [XMLElement] { |
| 75 | + // When there's only a single language representation, create a list of `<dt>` and `<dd>` HTML elements ("terms" and "definitions" in a "description list" (`<dl> HTML element`) |
| 76 | + var items: [XMLElement] = [] |
| 77 | + items.reserveCapacity(parameterInfo.count * 2) |
| 78 | + for parameter in parameterInfo { |
| 79 | + // name |
| 80 | + items.append( |
| 81 | + .element(named: "dt", children: [ |
| 82 | + .element(named: "code", children: [.text(parameter.name)]) |
| 83 | + ]) |
| 84 | + ) |
| 85 | + // description |
| 86 | + items.append( |
| 87 | + .element(named: "dd", children: parameter.content.map { visit($0) }) |
| 88 | + ) |
| 89 | + } |
| 90 | + |
| 91 | + return items |
| 92 | + } |
| 93 | + |
| 94 | + private func _dualLanguageParameters( |
| 95 | + primary: (key: SourceLanguage, value: [ParameterInfo]), |
| 96 | + secondary: (key: SourceLanguage, value: [ParameterInfo]) |
| 97 | + ) -> XMLElement { |
| 98 | + // "Shadow" the parameters with more descriptive tuple labels |
| 99 | + let primary = (language: primary.key, parameters: primary.value) |
| 100 | + let secondary = (language: secondary.key, parameters: secondary.value) |
| 101 | + |
| 102 | + // When there are exactly two language representations, which is very common, |
| 103 | + // avoid duplication and only create `<dt>` and `<dd>` HTML elements _once_ if the parameter exist in both language representations. |
| 104 | + |
| 105 | + // Start by rendering the primary language's parameter, then update that list with information about language-specific parameters. |
| 106 | + var items = _singleLanguageParameterItems(primary.parameters) |
| 107 | + |
| 108 | + // Find all the inserted and deleted parameters. |
| 109 | + // This assumes that parameters appear in the same _order_ in each language representation, which is true in practice. |
| 110 | + // If that assumption is wrong, it will produce correct looking results but some repeated markup. |
| 111 | + // TODO: Consider adding a debug assertion that verifies the order and a test that verifies the output of out-of-order parameter names. |
| 112 | + let differences = secondary.parameters.difference(from: primary.parameters, by: { $0.name == $1.name }) |
| 113 | + |
| 114 | + // Track which parameters _only_ exist in the primary language in order to insert the secondary languages's _unique_ parameters in the right locations. |
| 115 | + var primaryOnlyIndices = Set<Int>() |
| 116 | + |
| 117 | + // Add a "class" attribute to the parameters that only exist in the secondary language representation. |
| 118 | + // Through CSS, the rendered page can show and hide HTML elements that only apply to a specific language representation. |
| 119 | + for case let .remove(offset, _, _) in differences.removals { |
| 120 | + // This item only exists in the primary parameters |
| 121 | + primaryOnlyIndices.insert(offset) |
| 122 | + let index = offset * 2 |
| 123 | + // Mark those items as only being applying to the first language |
| 124 | + items[index ].addAttributes(["class": "\(primary.language.id)-only"]) |
| 125 | + items[index + 1].addAttributes(["class": "\(primary.language.id)-only"]) |
| 126 | + } |
| 127 | + |
| 128 | + // Insert parameter that only exists in the secondary language representation. |
| 129 | + for case let .insert(offset, parameter, _) in differences.insertions { |
| 130 | + // Account for any primary-only parameters that appear before this (times 2 because each parameter has a `<dt>` and `<dd>` HTML element) |
| 131 | + let index = (offset + primaryOnlyIndices.count(where: { $0 < offset })) * 2 |
| 132 | + items.insert(contentsOf: [ |
| 133 | + // Name |
| 134 | + .element(named: "dt", children: [ |
| 135 | + .element(named: "code", children: [.text(parameter.name)]) |
| 136 | + ], attributes: ["class": "\(secondary.language.id)-only"]), |
| 137 | + // Description |
| 138 | + .element(named: "dd", children: parameter.content.map { visit($0) }, attributes: ["class": "\(secondary.language.id)-only"]) |
| 139 | + ], at: index) |
| 140 | + } |
| 141 | + |
| 142 | + return .element(named: "dl", children: items) |
| 143 | + } |
| 144 | +} |
0 commit comments