Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c530437
Initial HTML output proof of concept
d-ronnqvist Aug 27, 2025
ca6e4f2
Merge branch 'main' into output-html
d-ronnqvist Sep 28, 2025
b15fcf1
Start adding core rendering tests
d-ronnqvist Sep 28, 2025
4e119f6
Extract returning link and asset information
d-ronnqvist Sep 28, 2025
2d9bd2d
Avoid encoding <source> elements when they're not needed
d-ronnqvist Sep 28, 2025
654a118
Move core HTML rendering to its own target
d-ronnqvist Sep 28, 2025
f5eda36
Add helper to insert word breaks into symbol names
d-ronnqvist Sep 28, 2025
0e1d6cd
Insert word breaks in symbol titles for better wrapping
d-ronnqvist Oct 4, 2025
661f585
Update table test to verify formatted cell content
d-ronnqvist Oct 4, 2025
6c552d6
Add test for rendering page breadcrumbs
d-ronnqvist Oct 4, 2025
bae2f01
Move word breaking to new type
d-ronnqvist Oct 4, 2025
005191c
Sort Swift before other language specific symbol names
d-ronnqvist Oct 4, 2025
5cd607b
Use new breadcrumbs function when rendering pages
d-ronnqvist Oct 4, 2025
2746aaf
Add test for rendering page availability
d-ronnqvist Oct 4, 2025
8ab20b6
Use new availability function when rendering pages
d-ronnqvist Oct 4, 2025
9ce783a
Move breadcrumbs and availability helpers to separate files
d-ronnqvist Oct 5, 2025
fb0c415
Add test for rendering symbol parameters
d-ronnqvist Oct 11, 2025
9fb2588
Use new parameters function when rendering pages.
d-ronnqvist Oct 11, 2025
29d717c
Make HTML.MarkupRenderer APIs non mutating
d-ronnqvist Oct 11, 2025
145efc4
Move HTML.LinkProvider (and its method return types) to its own file
d-ronnqvist Oct 11, 2025
fa943e1
Add subheadings and abstract to HTML.LinkedElement (for topics sections)
d-ronnqvist Oct 11, 2025
e9febe0
Avoid emitting multiple subheading fragments of the same kind in a row
d-ronnqvist Oct 11, 2025
cf0d4ff
Merge branch 'main' into output-html
d-ronnqvist Oct 11, 2025
d1202a5
Add test for rendering symbol declarations
d-ronnqvist Oct 11, 2025
51c88d2
Use new declaration function when rendering pages.
d-ronnqvist Oct 11, 2025
81018e3
Merge branch 'main' into output-html
d-ronnqvist Nov 20, 2025
711a07f
Add DocC prefix to new HTML target
d-ronnqvist Nov 20, 2025
f1ff73a
Add CMake list for the new DocCHTML target
d-ronnqvist Nov 20, 2025
999291f
Use Swift 6 language mode for new DocCHTML target
d-ronnqvist Nov 20, 2025
ed290c0
Use Swift Testing for DocCHTMLTests
d-ronnqvist Nov 20, 2025
22395a9
Pass the current pages name rather than look it up for the breadcrumbs
d-ronnqvist Nov 21, 2025
746a961
Add a CLI feature flag for adding content to the HTML template files
d-ronnqvist Nov 26, 2025
bffc1c5
Rename MarkupRender to MarkdownRenderer
d-ronnqvist Nov 26, 2025
75f4515
Remove the (JSON) RenderContext parameter
d-ronnqvist Nov 26, 2025
4307f29
Display a nicer link text as a fallback for unresolved links
d-ronnqvist Nov 26, 2025
3e8731f
Merge branch 'main' into output-html
d-ronnqvist Nov 26, 2025
de5289f
Prefer SourceLanguage type over string identifiers for languages
d-ronnqvist Nov 26, 2025
cb318b5
Support creating a minimal/concise HTML content for machine consumption
d-ronnqvist Nov 26, 2025
e3131c1
Remove duplicate declaration in HTML output
d-ronnqvist Nov 26, 2025
81ae2cc
Add test for rendering return sections declarations
d-ronnqvist Nov 26, 2025
af95717
Add an article and a method to the full HTML content consumer test
d-ronnqvist Nov 26, 2025
f7303da
Omit language class from single-language breadcrumbs
d-ronnqvist Nov 26, 2025
8d28071
Fix missing symbol breadcrumbs to container pages
d-ronnqvist Nov 26, 2025
c6db56d
Add test for rendering Topics sections
d-ronnqvist Nov 27, 2025
5d771d9
Update HTML content test to include authored Topics and SeeAlso sections
d-ronnqvist Nov 27, 2025
9b401a3
Use thematic breaks instead of element classes to separate sections
d-ronnqvist Nov 27, 2025
aa48c60
Add declaration parameter to makeSymbol(...) helper
d-ronnqvist Nov 27, 2025
5890088
Simplify HTML element for the "eyebrow"
d-ronnqvist Nov 27, 2025
c25e8fa
Simplify creation of Discussion/Overview section
d-ronnqvist Nov 27, 2025
3bff6ad
Make headings link to themselves by default
d-ronnqvist Nov 27, 2025
e5a822e
Include description meta element in the HTML
d-ronnqvist Nov 27, 2025
f5e0bc5
Simplify breadcrumbs and eyebrow HTML
d-ronnqvist Nov 27, 2025
61381a6
Don't include an empty declaration for module pages
d-ronnqvist Nov 27, 2025
1ef2628
Use a paragraph for the article title in a topic section item
d-ronnqvist Nov 27, 2025
4ac4a27
Use a minimal/concise version of the HTML content
d-ronnqvist Nov 27, 2025
893917e
Rename 'RenderGoal.quality' to 'RenderGoal.richness'
d-ronnqvist Nov 27, 2025
f4bbc8e
Remove unused code and unused parameters
d-ronnqvist Nov 28, 2025
815e88c
Don't count the "index.html" component when determining a relative path
d-ronnqvist Nov 28, 2025
43018c8
Add FIXME about future changes once other PRs are merged
d-ronnqvist Nov 28, 2025
4c60f0c
Share logic with the JSON file writer for creating files and directories
d-ronnqvist Nov 28, 2025
7225dd9
Include Mentioned In links in the HTML content
d-ronnqvist Nov 28, 2025
8fe0e0b
Further minimize the concise HTML by avoiding most <section> elements
d-ronnqvist Nov 28, 2025
49cb629
Avoid creating sections with only a heading in the HTML output
d-ronnqvist Nov 28, 2025
2df6b8d
Lowercase links in the HTML to avoid making assumptions about a case …
d-ronnqvist Nov 28, 2025
f746d75
Fix links in Mentioned In section
d-ronnqvist Nov 28, 2025
f5f2923
Fix trailing comma in parameters for compatibility with Swift 6.0
d-ronnqvist Nov 28, 2025
5468d48
Update how FoundationXML is imported
d-ronnqvist Nov 28, 2025
de766b9
Update XML string comparisons to account for formatting differences a…
d-ronnqvist Dec 1, 2025
baa3736
Merge branch 'main' into output-html
d-ronnqvist Dec 1, 2025
48607e2
Prefer using SourceLanguage values over string identifiers
d-ronnqvist Dec 1, 2025
011b1ab
Update file names to match already renamed type
d-ronnqvist Dec 2, 2025
cd0ecb2
Update Windows CMake file lists
d-ronnqvist Dec 3, 2025
90a2430
Copy over Windows CMake workaround from other target's CMake file list
d-ronnqvist Dec 3, 2025
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
23 changes: 23 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ let package = Package(
name: "SwiftDocC",
dependencies: [
.target(name: "DocCCommon"),
.target(name: "DocCHTML"),
.product(name: "Markdown", package: "swift-markdown"),
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
.product(name: "CLMDB", package: "swift-lmdb"),
Expand Down Expand Up @@ -122,6 +123,7 @@ let package = Package(
// This target shouldn't have any local dependencies so that all other targets can depend on it.
// We can add dependencies on SymbolKit and Markdown here but they're not needed yet.
],
exclude: ["CMakeLists.txt"],
swiftSettings: [.swiftLanguageMode(.v6)]
),

Expand All @@ -134,6 +136,27 @@ let package = Package(
swiftSettings: [.swiftLanguageMode(.v6)]
),

.target(
name: "DocCHTML",
dependencies: [
.target(name: "DocCCommon"),
.product(name: "Markdown", package: "swift-markdown"),
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
],
exclude: ["CMakeLists.txt"],
swiftSettings: [.swiftLanguageMode(.v6)]
),
.testTarget(
name: "DocCHTMLTests",
dependencies: [
.target(name: "DocCHTML"),
.target(name: "SwiftDocC"),
.product(name: "Markdown", package: "swift-markdown"),
.target(name: "SwiftDocCTestUtilities"),
],
swiftSettings: [.swiftLanguageMode(.v6)]
),

// Test app for SwiftDocCUtilities
.executableTarget(
name: "signal-test-app",
Expand Down
1 change: 1 addition & 0 deletions Sources/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ See https://swift.org/LICENSE.txt for license information
#]]

add_subdirectory(DocCCommon)
add_subdirectory(DocCHTML)
add_subdirectory(SwiftDocC)
add_subdirectory(SwiftDocCUtilities)
add_subdirectory(docc)
29 changes: 29 additions & 0 deletions Sources/DocCHTML/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#[[
This source file is part of the Swift open source project

Copyright © 2014 - 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
#]]

add_library(DocCHTML STATIC
LinkProvider.swift
MarkdownRenderer+Availability.swift
MarkdownRenderer+Breadcrumbs.swift
MarkdownRenderer+Declaration.swift
MarkdownRenderer+Parameters.swift
MarkdownRenderer+Returns.swift
MarkdownRenderer+Topics.swift
MarkdownRenderer.swift
WordBreak.swift
XMLNode+element.swift)
target_link_libraries(DocCHTML PRIVATE
DocCCommon)
target_link_libraries(DocCHTML PUBLIC
SwiftMarkdown::Markdown
DocC::SymbolKit)
# FIXME(compnerd) workaround leaking dependencies
target_link_libraries(SwiftDocC PUBLIC
libcmark-gfm
libcmark-gfm-extensions)
115 changes: 115 additions & 0 deletions Sources/DocCHTML/LinkProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
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
*/

package import Foundation
package import Markdown
package import DocCCommon

/// A type that provides information about other pages, and on-page elements, that the rendered page references.
package protocol LinkProvider {
/// Provide information about another page or on-page element, or `nil` if the other page can't be found.
func element(for path: URL) -> LinkedElement?

/// Provide the path for a symbol based on its unique identifier, or `nil` if the other symbol with that identifier can't be found.
func pathForSymbolID(_ usr: String) -> URL?

/// Provide information about an asset, or `nil` if the asset can't be found.
func assetNamed(_ assetName: String) -> LinkedAsset?

/// Fallback link text for a link string that the provider couldn't provide any information for.
func fallbackLinkText(linkString: String) -> String
}

package struct LinkedElement {
/// The path within the output archive to the linked element.
package var path: URL
/// The names of the linked element, for display when the element is referenced in inline content.
///
/// Articles, headings, tutorials, and similar pages have a ``Names/single/conceptual(_:)`` name.
/// Symbols can either have a ``Names/single/symbol(_:)`` name or have different names for each language representation (``Names/languageSpecificSymbol``).
package var names: Names
/// The subheadings of the linked element, for display when the element is referenced in either a Topics section, See Also section, or in a `@Links` directive.
///
/// Articles, headings, tutorials, and similar pages have a ``Names/single/conceptual(_:)`` name.
/// Symbols can either have a ``Names/single/symbol(_:)`` name or have different names for each language representation (``Names/languageSpecificSymbol``).
package var subheadings: Subheadings
/// The abstract of the page—to be displayed in either a Topics section, See Also section, or in a `@Links` directive—or `nil` if the linked element doesn't have an abstract.
package var abstract: Paragraph?

package init(path: URL, names: Names, subheadings: Subheadings, abstract: Paragraph?) {
self.path = path
self.names = names
self.subheadings = subheadings
self.abstract = abstract
}

/// The single name or language-specific names to use when referring to a linked element in inline content.
package enum Names {
/// This element has the same name in all language representations
case single(Name)
/// This element is a symbol with different names in different languages.
///
/// Because `@DisplayName` applies to all language representations, these language specific names are always the symbol's subheading declaration and should display in a monospaced font.
case languageSpecificSymbol([SourceLanguage: String])
}
package enum Name {
/// The name refers to an article, heading, or custom `@DisplayName` and should display as regular text.
case conceptual(String)
/// The name refers to a symbol's subheading declaration and should display in a monospaced font.
case symbol(String)
}

/// The single subheading or language-specific subheadings to use when referring to a linked element in either a Topics section, See Also section, or in a `@Links` directive.
package enum Subheadings {
/// This element has the same name in all language representations
case single(Subheading)
/// This element is a symbol with different names in different languages.
///
/// Because `@DisplayName` applies to all language representations, these language specific names are always the symbol's subheading declaration and should display in a monospaced font.
case languageSpecificSymbol([SourceLanguage: [SymbolNameFragment]])
}
package enum Subheading {
/// The name refers to an article, heading, or custom `@DisplayName` and should display as regular text.
case conceptual(String)
/// The name refers to a symbol's subheading declaration and should display in a monospaced font.
case symbol([SymbolNameFragment])
}

/// A fragment in a symbol's name
package struct SymbolNameFragment {
/// The textual spelling of this fragment
package var text: String
/// The kind of fragment
package var kind: Kind

/// The display kind of a single symbol name fragment
package enum Kind: String {
case identifier, decorator
}

package init(text: String, kind: Kind) {
self.text = text
self.kind = kind
}
}
}

package struct LinkedAsset {
/// The path within the output archive to each image variant, by their light/dark style.
package var images: [ColorStyle: [Int /* display scale*/: URL]]

package init(images: [ColorStyle : [Int : URL]]) {
self.images = images
}

package enum ColorStyle: String {
case light, dark
}
}
70 changes: 70 additions & 0 deletions Sources/DocCHTML/MarkdownRenderer+Availability.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
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)
// FIXME: See if we can avoid depending on XMLNode/XMLParser to avoid needing to import FoundationXML
package import FoundationXML
#else
package import Foundation
#endif

package extension MarkdownRenderer {
struct AvailabilityInfo {
package var name: String
package var introduced, deprecated: String?
package var isBeta: Bool

package init(name: String, introduced: String? = nil, deprecated: String? = nil, isBeta: Bool) {
self.name = name
self.introduced = introduced
self.deprecated = deprecated
self.isBeta = isBeta
}
}

/// Creates an HTML element for the availability information.
func availability(_ info: [AvailabilityInfo]) -> XMLNode {
let items: [XMLNode] = info.map {
var text = $0.name

let description: String
if let introduced = $0.introduced {
if let deprecated = $0.deprecated{
text += " \(introduced)–\(deprecated)"
description = "Introduced in \($0.name) \(introduced) and deprecated in \($0.name) \(deprecated)"
} else {
text += " \(introduced)+"
description = "Available on \(introduced) and later"
}
} else {
description = "Available on \($0.name)"
}

var attributes = [
"role": "text",
"aria-label": "\(text), \(description)",
"title": description
]
if $0.isBeta {
attributes["class"] = "beta"
} else if $0.deprecated != nil {
attributes["class"] = "deprecated"
}

return .element(named: "li", children: [.text(text)], attributes: goal == .richness ? attributes : [:])
}

return .element(
named: "ul",
children: items,
attributes: ["id": "availability"]
)
}
}
72 changes: 72 additions & 0 deletions Sources/DocCHTML/MarkdownRenderer+Breadcrumbs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
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)
// FIXME: See if we can avoid depending on XMLNode/XMLParser to avoid needing to import FoundationXML
package import FoundationXML
package import FoundationEssentials
#else
package import Foundation
#endif

package extension MarkdownRenderer {
/// Creates an HTML element for the breadcrumbs leading up to the renderer's reference.
func breadcrumbs(references: [URL], currentPageNames: LinkedElement.Names) -> XMLNode {
// Breadcrumbs handle symbols differently than most elements, so there's no point in sharing _this_ code
func nameElements(for names: LinkedElement.Names) -> [XMLNode] {
switch names {
// Breadcrumbs display both symbolic names and conceptual in a default style
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)]
} else {
names.map { language, name in
.element(named: "span", children: [
.text(name) // Breadcrumbs display symbol names in a default style (no "code voice")
], 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)]
} ?? []
}
}
}

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)])
])
}
}

// The current page doesn't display as a link
items.append(
.element(named: "li", children: nameElements(for: currentPageNames))
)
let list = XMLNode.element(named: "ul", children: items)

return switch goal {
case .conciseness:
list
case .richness:
.element(named: "nav", children: [list], attributes: ["id": "breadcrumbs"])
}
}
}
Loading