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 @@ -12,6 +12,7 @@ add_library(DocCHTML STATIC
MarkdownRenderer+Availability.swift
MarkdownRenderer+Breadcrumbs.swift
MarkdownRenderer+Declaration.swift
MarkdownRenderer+Discussion.swift
MarkdownRenderer+Parameters.swift
MarkdownRenderer+Returns.swift
MarkdownRenderer+Topics.swift
Expand Down
40 changes: 40 additions & 0 deletions Sources/DocCHTML/MarkdownRenderer+Discussion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
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 Markdown

package extension MarkdownRenderer {

/// Creates a discussion section with the given markup.
///
/// If the markup doesn't start with a level-2 heading, the renderer will insert a level-2 heading based on the `fallbackSectionName`.
func discussion(_ markup: [any Markup], fallbackSectionName: String) -> [XMLNode] {
guard !markup.isEmpty else { return [] }
var remaining = markup[...]

let sectionName: String
// Check if the markup already contains an explicit heading
if let heading = remaining.first as? Heading, heading.level == 2 {
_ = remaining.removeFirst() // Remove the heading so that it's not rendered twice
sectionName = heading.plainText
} else {
sectionName = fallbackSectionName
}

return selfReferencingSection(named: sectionName, content: remaining.map { visit($0) })
}
}
14 changes: 14 additions & 0 deletions Sources/SwiftDocC/Model/Rendering/HTML/HTMLRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ struct HTMLRenderer {
hero.addChild(paragraph)
}

// Discussion
if let discussion = article.discussion {
articleElement.addChildren(
renderer.discussion(discussion.content, fallbackSectionName: "Overview")
)
}

return RenderedPageInfo(
content: goal == .richness ? main : articleElement,
metadata: .init(
Expand Down Expand Up @@ -223,6 +230,13 @@ struct HTMLRenderer {
hero.addChild(paragraph)
}

// Discussion
if let discussion = symbol.discussion {
articleElement.addChildren(
renderer.discussion(discussion.content, fallbackSectionName: symbol.kind.identifier.swiftSymbolCouldHaveChildren ? "Overview" : "Discussion")
)
}

return RenderedPageInfo(
content: goal == .richness ? main : articleElement,
metadata: .init(
Expand Down
73 changes: 73 additions & 0 deletions Tests/DocCHTMLTests/MarkdownRenderer+PageElementsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,79 @@ struct MarkdownRenderer_PageElementsTests {
}
}

@Test(arguments: RenderGoal.allCases)
func testEmptyDiscussionSection(goal: RenderGoal) {
let renderer = makeRenderer(goal: goal)
let discussion = renderer.discussion([], fallbackSectionName: "Fallback")
#expect(discussion.isEmpty)
}

@Test(arguments: RenderGoal.allCases)
func testDiscussionSectionWithoutHeading(goal: RenderGoal) {
let renderer = makeRenderer(goal: goal)
let discussion = renderer.discussion(parseMarkup(string: """
First paragraph

Second paragraph
"""), fallbackSectionName: "Fallback")

let commonHTML = """
<p>First paragraph</p>
<p>Second paragraph</p>
"""

switch goal {
case .richness:
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
<section id="Fallback">
<h2>
<a href="#Fallback">Fallback</a>
</h2>
\(commonHTML)
</section>
""")
case .conciseness:
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
<h2>Fallback</h2>
\(commonHTML)
""")
}
}

@Test(arguments: RenderGoal.allCases)
func testDiscussionSectionWithHeading(goal: RenderGoal) {
let renderer = makeRenderer(goal: goal)
let discussion = renderer.discussion(parseMarkup(string: """
## Some Heading

First paragraph

Second paragraph
"""), fallbackSectionName: "Fallback")

let commonHTML = """
<p>First paragraph</p>
<p>Second paragraph</p>
"""

switch goal {
case .richness:
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
<section id="Some-Heading">
<h2>
<a href="#Some-Heading">Some Heading</a>
</h2>
\(commonHTML)
</section>
""")
case .conciseness:
discussion.assertMatches(prettyFormatted: true, expectedXMLString: """
<h2>Some Heading</h2>
\(commonHTML)
""")
}
}

// MARK: -

private func makeRenderer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ final class FileWritingHTMLContentConsumerTests: XCTestCase {
<h1>someMethod(with:and:)</h1>
<p>Some in-source description of this method.</p>
</section>
<h2>Discussion</h2>
<p>Further description of this method and how to use it.</p>
</article>
</noscript>
<div id="app"></div>
Expand All @@ -264,6 +266,10 @@ final class FileWritingHTMLContentConsumerTests: XCTestCase {
<h1>Some article</h1>
<p>This is an <i>formatted</i> article.</p>
</section>
<h2>Custom discussion</h2>
<p>It explains how a developer can perform some task using <a href="../someclass/index.html"><code>SomeClass</code></a> in this module.</p>
<h3>Details</h3>
<p>This subsection describes something more detailed.</p>
</article>
</noscript>
<div id="app"></div>
Expand Down