Skip to content

Commit 0f91352

Browse files
authored
Add a helper function for rendering a discussion section as HTML (#1388)
* Add a helper function for rendering a discussion section as HTML rdar://163326857 * Integrate the discussion section in the HTML renderer
1 parent fd549d4 commit 0f91352

File tree

5 files changed

+134
-0
lines changed

5 files changed

+134
-0
lines changed

Sources/DocCHTML/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ add_library(DocCHTML STATIC
1212
MarkdownRenderer+Availability.swift
1313
MarkdownRenderer+Breadcrumbs.swift
1414
MarkdownRenderer+Declaration.swift
15+
MarkdownRenderer+Discussion.swift
1516
MarkdownRenderer+Parameters.swift
1617
MarkdownRenderer+Returns.swift
1718
MarkdownRenderer+Topics.swift
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
20+
package extension MarkdownRenderer {
21+
22+
/// Creates a discussion section with the given markup.
23+
///
24+
/// If the markup doesn't start with a level-2 heading, the renderer will insert a level-2 heading based on the `fallbackSectionName`.
25+
func discussion(_ markup: [any Markup], fallbackSectionName: String) -> [XMLNode] {
26+
guard !markup.isEmpty else { return [] }
27+
var remaining = markup[...]
28+
29+
let sectionName: String
30+
// Check if the markup already contains an explicit heading
31+
if let heading = remaining.first as? Heading, heading.level == 2 {
32+
_ = remaining.removeFirst() // Remove the heading so that it's not rendered twice
33+
sectionName = heading.plainText
34+
} else {
35+
sectionName = fallbackSectionName
36+
}
37+
38+
return selfReferencingSection(named: sectionName, content: remaining.map { visit($0) })
39+
}
40+
}

Sources/SwiftDocC/Model/Rendering/HTML/HTMLRenderer.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ struct HTMLRenderer {
178178
hero.addChild(paragraph)
179179
}
180180

181+
// Discussion
182+
if let discussion = article.discussion {
183+
articleElement.addChildren(
184+
renderer.discussion(discussion.content, fallbackSectionName: "Overview")
185+
)
186+
}
187+
181188
return RenderedPageInfo(
182189
content: goal == .richness ? main : articleElement,
183190
metadata: .init(
@@ -223,6 +230,13 @@ struct HTMLRenderer {
223230
hero.addChild(paragraph)
224231
}
225232

233+
// Discussion
234+
if let discussion = symbol.discussion {
235+
articleElement.addChildren(
236+
renderer.discussion(discussion.content, fallbackSectionName: symbol.kind.identifier.swiftSymbolCouldHaveChildren ? "Overview" : "Discussion")
237+
)
238+
}
239+
226240
return RenderedPageInfo(
227241
content: goal == .richness ? main : articleElement,
228242
metadata: .init(

Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,79 @@ struct MarkdownRenderer_PageElementsTests {
641641
}
642642
}
643643

644+
@Test(arguments: RenderGoal.allCases)
645+
func testEmptyDiscussionSection(goal: RenderGoal) {
646+
let renderer = makeRenderer(goal: goal)
647+
let discussion = renderer.discussion([], fallbackSectionName: "Fallback")
648+
#expect(discussion.isEmpty)
649+
}
650+
651+
@Test(arguments: RenderGoal.allCases)
652+
func testDiscussionSectionWithoutHeading(goal: RenderGoal) {
653+
let renderer = makeRenderer(goal: goal)
654+
let discussion = renderer.discussion(parseMarkup(string: """
655+
First paragraph
656+
657+
Second paragraph
658+
"""), fallbackSectionName: "Fallback")
659+
660+
let commonHTML = """
661+
<p>First paragraph</p>
662+
<p>Second paragraph</p>
663+
"""
664+
665+
switch goal {
666+
case .richness:
667+
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
668+
<section id="Fallback">
669+
<h2>
670+
<a href="#Fallback">Fallback</a>
671+
</h2>
672+
\(commonHTML)
673+
</section>
674+
""")
675+
case .conciseness:
676+
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
677+
<h2>Fallback</h2>
678+
\(commonHTML)
679+
""")
680+
}
681+
}
682+
683+
@Test(arguments: RenderGoal.allCases)
684+
func testDiscussionSectionWithHeading(goal: RenderGoal) {
685+
let renderer = makeRenderer(goal: goal)
686+
let discussion = renderer.discussion(parseMarkup(string: """
687+
## Some Heading
688+
689+
First paragraph
690+
691+
Second paragraph
692+
"""), fallbackSectionName: "Fallback")
693+
694+
let commonHTML = """
695+
<p>First paragraph</p>
696+
<p>Second paragraph</p>
697+
"""
698+
699+
switch goal {
700+
case .richness:
701+
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
702+
<section id="Some-Heading">
703+
<h2>
704+
<a href="#Some-Heading">Some Heading</a>
705+
</h2>
706+
\(commonHTML)
707+
</section>
708+
""")
709+
case .conciseness:
710+
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
711+
<h2>Some Heading</h2>
712+
\(commonHTML)
713+
""")
714+
}
715+
}
716+
644717
// MARK: -
645718

646719
private func makeRenderer(

Tests/SwiftDocCUtilitiesTests/FileWritingHTMLContentConsumerTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ final class FileWritingHTMLContentConsumerTests: XCTestCase {
242242
<h1>someMethod(with:and:)</h1>
243243
<p>Some in-source description of this method.</p>
244244
</section>
245+
<h2>Discussion</h2>
246+
<p>Further description of this method and how to use it.</p>
245247
</article>
246248
</noscript>
247249
<div id="app"></div>
@@ -264,6 +266,10 @@ final class FileWritingHTMLContentConsumerTests: XCTestCase {
264266
<h1>Some article</h1>
265267
<p>This is an <i>formatted</i> article.</p>
266268
</section>
269+
<h2>Custom discussion</h2>
270+
<p>It explains how a developer can perform some task using <a href="../someclass/index.html"><code>SomeClass</code></a> in this module.</p>
271+
<h3>Details</h3>
272+
<p>This subsection describes something more detailed.</p>
267273
</article>
268274
</noscript>
269275
<div id="app"></div>

0 commit comments

Comments
 (0)