diff --git a/Sources/DocCHTML/CMakeLists.txt b/Sources/DocCHTML/CMakeLists.txt index 531ccff0b..67798d1ab 100644 --- a/Sources/DocCHTML/CMakeLists.txt +++ b/Sources/DocCHTML/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(DocCHTML STATIC LinkProvider.swift MarkdownRenderer+Availability.swift MarkdownRenderer+Breadcrumbs.swift + MarkdownRenderer+Declaration.swift MarkdownRenderer+Parameters.swift MarkdownRenderer.swift WordBreak.swift diff --git a/Sources/DocCHTML/MarkdownRenderer+Declaration.swift b/Sources/DocCHTML/MarkdownRenderer+Declaration.swift new file mode 100644 index 000000000..c38897263 --- /dev/null +++ b/Sources/DocCHTML/MarkdownRenderer+Declaration.swift @@ -0,0 +1,75 @@ +/* + 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 import DocCCommon +package import SymbolKit + +package extension MarkdownRenderer { + + typealias DeclarationFragment = SymbolGraph.Symbol.DeclarationFragments.Fragment + + /// Creates a`
` HTML element hierarchy that represents the symbol's language-specific declarations.
+ ///
+ /// When the renderer has a ``RenderGoal/richness`` goal, it creates a `` element for each declaration fragment so that to enable syntax highlighting.
+ ///
+ /// When the renderer has a ``RenderGoal/conciseness`` goal, it joins the different fragments into string.
+ func declaration(_ fragmentsByLanguage: [SourceLanguage: [DeclarationFragment]]) -> XMLElement {
+ let fragmentsByLanguage = RenderHelpers.sortedLanguageSpecificValues(fragmentsByLanguage)
+
+ guard goal == .richness else {
+ // If the goal is conciseness, display only the primary language's plain text declaration
+ let plainTextDeclaration: [XMLNode] = fragmentsByLanguage.first.map { _, fragments in
+ [.element(named: "code", children: [.text(fragments.map(\.spelling).joined())])]
+ } ?? []
+ return .element(named: "pre", children: plainTextDeclaration)
+ }
+
+ let declarations: [XMLElement] = if fragmentsByLanguage.count == 1 {
+ // If there's only a single language there's no need to mark anything as language specific.
+ [XMLNode.element(named: "code", children: _declarationTokens(for: fragmentsByLanguage.first!.value))]
+ } else {
+ fragmentsByLanguage.map { language, fragments in
+ XMLNode.element(named: "code", children: _declarationTokens(for: fragments), attributes: ["class": "\(language.id)-only"])
+ }
+ }
+ return .element(named: "pre", children: declarations, attributes: ["id": "declaration"])
+ }
+
+ private func _declarationTokens(for fragments: [DeclarationFragment]) -> [XMLNode] {
+ // TODO: Pretty print declarations for Swift and Objective-C by placing attributes and parameters on their own lines (rdar://165918402)
+ fragments.map { fragment in
+ let elementClass = "token-\(fragment.kind.rawValue)"
+
+ if fragment.kind == .typeIdentifier,
+ let symbolID = fragment.preciseIdentifier,
+ let reference = linkProvider.pathForSymbolID(symbolID)
+ {
+ // If the token refers to a symbol that the `linkProvider` is aware of, make that fragment a link to that symbol.
+ return .element(named: "a", children: [.text(fragment.spelling)], attributes: [
+ "href": path(to: reference),
+ "class": elementClass
+ ])
+ } else if fragment.kind == .text {
+ // ???: Does text also need a element or can that be avoided?
+ return .text(fragment.spelling)
+ } else {
+ // The declaration element is expected to scroll, so individual fragments don't need to contain explicit word breaks.
+ return .element(named: "span", children: [.text(fragment.spelling)], attributes: ["class": elementClass])
+ }
+ }
+ }
+}
diff --git a/Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift b/Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift
index 2ab9c7cd5..5907ed10a 100644
--- a/Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift
+++ b/Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift
@@ -269,6 +269,159 @@ struct MarkdownRenderer_PageElementsTests {
""")
}
+ @Test(arguments: RenderGoal.allCases)
+ func testRenderSwiftDeclaration(goal: RenderGoal) {
+ let symbolPaths = [
+ "first-parameter-symbol-id": URL(string: "/documentation/ModuleName/FirstParameterValue/index.html")!,
+ "second-parameter-symbol-id": URL(string: "/documentation/ModuleName/SecondParameterValue/index.html")!,
+ "return-value-symbol-id": URL(string: "/documentation/ModuleName/ReturnValue/index.html")!,
+ ]
+
+ let declaration = makeRenderer(goal: goal, pathsToReturn: symbolPaths).declaration([
+ .swift: [
+ .init(kind: .keyword, spelling: "func", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .identifier, spelling: "doSomething", preciseIdentifier: nil),
+ .init(kind: .text, spelling: "(", preciseIdentifier: nil),
+ .init(kind: .externalParameter, spelling: "with", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .internalParameter, spelling: "first", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ": ", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "FirstParameterValue", preciseIdentifier: "first-parameter-symbol-id"),
+ .init(kind: .text, spelling: ", ", preciseIdentifier: nil),
+ .init(kind: .externalParameter, spelling: "and", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .internalParameter, spelling: "second", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ": ", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "SecondParameterValue", preciseIdentifier: "second-parameter-symbol-id"),
+ .init(kind: .text, spelling: ") ", preciseIdentifier: nil),
+ .init(kind: .keyword, spelling: "throws", preciseIdentifier: nil),
+ .init(kind: .text, spelling: "-> ", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "ReturnValue", preciseIdentifier: "return-value-symbol-id"),
+ ]
+ ])
+ switch goal {
+ case .richness:
+ declaration.assertMatches(prettyFormatted: true, expectedXMLString: """
+
+
+ func
+ doSomething
+ (with
+ first
+ : FirstParameterValue
+ , and
+ second
+ : SecondParameterValue
+ ) throws
+ -> ReturnValue
+
+
+ """)
+ case .conciseness:
+ declaration.assertMatches(prettyFormatted: true, expectedXMLString: """
+
+ func doSomething(with first: FirstParameterValue, and second: SecondParameterValue) throws-> ReturnValue
+
+ """)
+ }
+ }
+
+ @Test(arguments: RenderGoal.allCases)
+ func testRenderLanguageSpecificDeclarations(goal: RenderGoal) {
+ let symbolPaths = [
+ "first-parameter-symbol-id": URL(string: "/documentation/ModuleName/FirstParameterValue/index.html")!,
+ "second-parameter-symbol-id": URL(string: "/documentation/ModuleName/SecondParameterValue/index.html")!,
+ "return-value-symbol-id": URL(string: "/documentation/ModuleName/ReturnValue/index.html")!,
+ "error-parameter-symbol-id": URL(string: "/documentation/Foundation/NSError/index.html")!,
+ ]
+
+ let declaration = makeRenderer(goal: goal, pathsToReturn: symbolPaths).declaration([
+ .swift: [
+ .init(kind: .keyword, spelling: "func", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .identifier, spelling: "doSomething", preciseIdentifier: nil),
+ .init(kind: .text, spelling: "(", preciseIdentifier: nil),
+ .init(kind: .externalParameter, spelling: "with", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .internalParameter, spelling: "first", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ": ", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "FirstParameterValue", preciseIdentifier: "first-parameter-symbol-id"),
+ .init(kind: .text, spelling: ", ", preciseIdentifier: nil),
+ .init(kind: .externalParameter, spelling: "and", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .internalParameter, spelling: "second", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ": ", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "SecondParameterValue", preciseIdentifier: "second-parameter-symbol-id"),
+ .init(kind: .text, spelling: ") ", preciseIdentifier: nil),
+ .init(kind: .keyword, spelling: "throws", preciseIdentifier: nil),
+ .init(kind: .text, spelling: "-> ", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "ReturnValue", preciseIdentifier: "return-value-symbol-id"),
+ ],
+
+ .objectiveC: [
+ .init(kind: .text, spelling: "- (", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "ReturnValue", preciseIdentifier: "return-value-symbol-id"),
+ .init(kind: .text, spelling: ") ", preciseIdentifier: nil),
+ .init(kind: .identifier, spelling: "doSomethingWithFirst", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ": (", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "FirstParameterValue", preciseIdentifier: "first-parameter-symbol-id"),
+ .init(kind: .text, spelling: ") ", preciseIdentifier: nil),
+ .init(kind: .internalParameter, spelling: "first", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .identifier, spelling: "andSecond", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ": (", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "SecondParameterValue", preciseIdentifier: "second-parameter-symbol-id"),
+ .init(kind: .text, spelling: ") ", preciseIdentifier: nil),
+ .init(kind: .internalParameter, spelling: "second", preciseIdentifier: nil),
+ .init(kind: .text, spelling: " ", preciseIdentifier: nil),
+ .init(kind: .identifier, spelling: "error", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ": (", preciseIdentifier: nil),
+ .init(kind: .typeIdentifier, spelling: "NSError", preciseIdentifier: "error-parameter-symbol-id"),
+ .init(kind: .text, spelling: " **) ", preciseIdentifier: nil),
+ .init(kind: .internalParameter, spelling: "error", preciseIdentifier: nil),
+ .init(kind: .text, spelling: ";", preciseIdentifier: nil),
+ ]
+ ])
+ switch goal {
+ case .richness:
+ declaration.assertMatches(prettyFormatted: true, expectedXMLString: """
+
+
+ func
+ doSomething
+ (with
+ first
+ : FirstParameterValue
+ , and
+ second
+ : SecondParameterValue
+ ) throws
+ -> ReturnValue
+
+ - (ReturnValue
+ ) doSomethingWithFirst
+ : (FirstParameterValue
+ ) first
+ andSecond
+ : (SecondParameterValue
+ ) second
+ error
+ : (NSError
+ **) error
+ ;
+
+ """)
+
+ case .conciseness:
+ declaration.assertMatches(prettyFormatted: true, expectedXMLString: """
+
+ func doSomething(with first: FirstParameterValue, and second: SecondParameterValue) throws-> ReturnValue
+
+ """)
+ }
+ }
+
// MARK: -
private func makeRenderer(