Skip to content
Merged
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 @@ -10,6 +10,7 @@ See https://swift.org/LICENSE.txt for license information
add_library(DocCHTML STATIC
LinkProvider.swift
MarkdownRenderer+Availability.swift
MarkdownRenderer+Breadcrumbs.swift
MarkdownRenderer.swift
WordBreak.swift
XMLNode+element.swift)
Expand Down
67 changes: 67 additions & 0 deletions Sources/DocCHTML/MarkdownRenderer+Breadcrumbs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
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
package import FoundationEssentials
#else
package import Foundation
#endif

package extension MarkdownRenderer {
/// Creates an HTML element for the breadcrumbs that lead to the renderer's current page.
func breadcrumbs(references: [URL], currentPageNames: LinkedElement.Names) -> XMLNode {
// Breadcrumbs handle symbols differently than most elements in that everything uses a default style (no "code voice")
func nameElements(for names: LinkedElement.Names) -> [XMLNode] {
switch names {
case .single(.conceptual(let name)), .single(.symbol(let name)):
return [.text(name)]

case .languageSpecificSymbol(let namesByLanguageID):
let names = RenderHelpers.sortedLanguageSpecificValues(namesByLanguageID)
return switch goal {
case .richness:
if names.count == 1 {
[.text(names.first!.value)]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we want this behavior if there is only one language-specific name? If there is a language-specific name, even if there is only one, wouldn't we want to go through the logic in the else block that applies a "class" attribute (which I assume is used for formatting)?

Copy link
Contributor Author

@d-ronnqvist d-ronnqvist Dec 5, 2025

Choose a reason for hiding this comment

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

That's a good question that I don't know if I've written down the answer to anywhere.

The idea is that the language specific elements would have classes like "swift-only" or "occ-only" that can be shown or hidden through very small CSS changes. For example, consider this HTML that represents a link to a method with different names in Swift and Objective-C:

<a href=../somemethod(with:and:)/index.html>
  <code class="swift-only">someMethod(with:and:)</code>
  <code class="occ-only">someMethodWithFirst:AndSecond:</code>
</a>

If we add this CSS to the page:

.swift-only {}
.occ-only {
    display: none;
}

the rendered page will only display the Swift title for that link. If we flip the CSS, then the rendered page will only display the Objective-C title for that link.

If the symbol has the same title on both languages, or is only available in a single language,then it's unnecessary to have 2 <code> elements, and the HTML can be simplified to just:

<a href=../somemethod(with:and:)/index.html>
  <code>someMethod(with:and:)</code>
</a>

} else {
names.map { language, name in
// Wrap the name in a span so that it can be given a language specific "class" attribute.
.element(named: "span", children: [.text(name)], attributes: ["class": "\(language.id)-only"])
}
}
case .conciseness:
// If the goal is conciseness, only display the primary language's name
names.first.map { _, name in [.text(name)] } ?? []
}
}
}

// Create links for each of the breadcrumbs
var items: [XMLNode] = references.compactMap {
linkProvider.element(for: $0).map { page in
.element(named: "li", children: [
.element(named: "a", children: nameElements(for: page.names), attributes: ["href": self.path(to: page.path)])
])
}
}

// Add the name of the current page. It doesn't display as a link because it would refer to the current page.
items.append(
.element(named: "li", children: nameElements(for: currentPageNames))
)
let list = XMLNode.element(named: "ul", children: items)

return switch goal {
case .conciseness: list // If the goal is conciseness, don't wrap the list in a `<nav>` HTML element with an "id".
case .richness: .element(named: "nav", children: [list], attributes: ["id": "breadcrumbs"])
}
}
}
62 changes: 62 additions & 0 deletions Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,68 @@ import DocCHTML
import Markdown

struct MarkdownRenderer_PageElementsTests {
@Test(arguments: RenderGoal.allCases)
func testRenderBreadcrumbs(goal: RenderGoal) {
let elements = [
LinkedElement(
path: URL(string: "/documentation/ModuleName/index.html")!,
names: .single(.symbol("ModuleName")),
subheadings: .single(.symbol([.init(text: "ModuleName", kind: .identifier)])),
abstract: nil
),
LinkedElement(
path: URL(string: "/documentation/ModuleName/Something/index.html")!,
names: .languageSpecificSymbol([
.swift: "Something",
.objectiveC: "TLASomething",
]),
subheadings: .languageSpecificSymbol([
.swift: [
.init(text: "class ", kind: .decorator),
.init(text: "Something", kind: .identifier),
],
.objectiveC: [
.init(text: "class ", kind: .decorator),
.init(text: "TLASomething", kind: .identifier),
],
]),
abstract: nil
),
]
let breadcrumbs = makeRenderer(goal: goal, elementsToReturn: elements).breadcrumbs(references: elements.map { $0.path }, currentPageNames: .single(.conceptual("ThisPage")))
switch goal {
case .richness:
breadcrumbs.assertMatches(prettyFormatted: true, expectedXMLString: """
<nav id="breadcrumbs">
<ul>
<li>
<a href="../../index.html">ModuleName</a>
</li>
<li>
<a href="../index.html">
<span class="swift-only">Something</span>
<span class="occ-only">TLASomething</span>
</a>
</li>
<li>ThisPage</li>
</ul>
</nav>
""")
case .conciseness:
breadcrumbs.assertMatches(prettyFormatted: true, expectedXMLString: """
<ul>
<li>
<a href="../../index.html">ModuleName</a>
</li>
<li>
<a href="../index.html">Something</a>
</li>
<li>ThisPage</li>
</ul>
""")
}
}

@Test(arguments: RenderGoal.allCases)
func testRenderAvailability(goal: RenderGoal) {
let availability = makeRenderer(goal: goal).availability([
Expand Down