Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/DocCHTML/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions Sources/DocCHTML/MarkdownRenderer+Declaration.swift
Original file line number Diff line number Diff line change
@@ -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`<pre><code>` HTML element hierarchy that represents the symbol's language-specific declarations.
///
/// When the renderer has a ``RenderGoal/richness`` goal, it creates a `<span>` 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment in #1382 - can you explain why the concise version only displays info about the primary language? Maybe paste 2 examples for richness and conciseness above in the doc comments and then a quick sentence about why the concise version is sooo concise :)

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading this pattern in three or four functions now makes me wonder if you could extract the general behavior into a single place, and use a protocol, closure or some other means of calling the individual functions like _declarationTokens or _singleTaskGroupElements but maybe that would be difficult and not worth the effort?

// 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 <span> 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])
}
}
}
}
153 changes: 153 additions & 0 deletions Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: """
<pre id="declaration">
<code>
<span class="token-keyword">func</span>
<span class="token-identifier">doSomething</span>
(<span class="token-externalParam">with</span>
<span class="token-internalParam">first</span>
: <a class="token-typeIdentifier" href="../../firstparametervalue/index.html">FirstParameterValue</a>
, <span class="token-externalParam">and</span>
<span class="token-internalParam">second</span>
: <a class="token-typeIdentifier" href="../../secondparametervalue/index.html">SecondParameterValue</a>
) <span class="token-keyword">throws</span>
-&gt; <a class="token-typeIdentifier" href="../../returnvalue/index.html">ReturnValue</a>
</code>
</pre>
""")
case .conciseness:
declaration.assertMatches(prettyFormatted: true, expectedXMLString: """
<pre>
<code>func doSomething(with first: FirstParameterValue, and second: SecondParameterValue) throws-&gt; ReturnValue</code>
</pre>
""")
}
}

@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: """
<pre id="declaration">
<code class="swift-only">
<span class="token-keyword">func</span>
<span class="token-identifier">doSomething</span>
(<span class="token-externalParam">with</span>
<span class="token-internalParam">first</span>
: <a class="token-typeIdentifier" href="../../firstparametervalue/index.html">FirstParameterValue</a>
, <span class="token-externalParam">and</span>
<span class="token-internalParam">second</span>
: <a class="token-typeIdentifier" href="../../secondparametervalue/index.html">SecondParameterValue</a>
) <span class="token-keyword">throws</span>
-&gt; <a class="token-typeIdentifier" href="../../returnvalue/index.html">ReturnValue</a>
</code>
<code class="occ-only">- (<a class="token-typeIdentifier" href="../../returnvalue/index.html">ReturnValue</a>
) <span class="token-identifier">doSomethingWithFirst</span>
: (<a class="token-typeIdentifier" href="../../firstparametervalue/index.html">FirstParameterValue</a>
) <span class="token-internalParam">first</span>
<span class="token-identifier">andSecond</span>
: (<a class="token-typeIdentifier" href="../../secondparametervalue/index.html">SecondParameterValue</a>
) <span class="token-internalParam">second</span>
<span class="token-identifier">error</span>
: (<a class="token-typeIdentifier" href="../../../foundation/nserror/index.html">NSError</a>
**) <span class="token-internalParam">error</span>
;</code>
</pre>
""")

case .conciseness:
declaration.assertMatches(prettyFormatted: true, expectedXMLString: """
<pre>
<code>func doSomething(with first: FirstParameterValue, and second: SecondParameterValue) throws-&gt; ReturnValue</code>
</pre>
""")
}
}

// MARK: -

private func makeRenderer(
Expand Down