Skip to content

Commit 3099317

Browse files
Add support for @Options directive (#368)
- Adds a new `@Options` directive for holding other directives describing how content on the local page should be autogenerated and rendered. This directive functions like the existing `@Metadata` directive but is used specifically for "option directives" that effect the way content is rendered on the page (and not the content itself). Any directive that can be used in `@Options` is called an "option directive". The `@Options` directive accepts a `scope` parameter of either `local` or `global`. A `local` scoped `@Options` affects only the current page while a `global` scoped one affects the entire DocC catalog. Example: @options(scope: global) { @AutomaticTitleHeading(disabled) } @options { @AutomaticSeeAlso(disabled) @TopicsVisualStyle(detailedGrid) } `@Options` is described on the Swift forums here: https://forums.swift.org/t/supporting-more-types-of-documentation-with-swift-docc/59725#options-1 ------------------------------------------------------------------------------- - Adds a new `@AutomaticTitleHeading` option directive for customizing the way a page-title’s heading (also known as an eyebrow or kicker) is automatically generated. `@AutomaticTitleHeading` accepts an unnamed parameter of either `pageKind` or `disabled`. - `pageKind`: A page-title heading will be automatically added based on the page’s kind. This is Swift-DocC’s current default if no automatic subheading generation style is specified. - `disabled`: A page-title heading will not be automatically created for the page. `@AutomaticTitleHeading` is described on the Swift forums here: https://forums.swift.org/t/supporting-more-types-of-documentation-with-swift-docc/59725#automatictitleheading-4 ------------------------------------------------------------------------------- - Adds a new `@AutomaticSeeAlso` option directive for customizing the way See Also sections are automatically generated on a given page. `@AutomaticSeeAlso` accepts an unnamed parameter containing one of the following: - `siblingPages`: The current list of auto-generated “See Also” items based on sibling items. This is Swift-DocC’s current default if an automatic see also style is not specified. - `disabled`: Do not automatically generate any See Also items. `@AutomaticSeeAlso` is described on the Swift forums here: https://forums.swift.org/t/supporting-more-types-of-documentation-with-swift-docc/59725#automaticseealso-14 ------------------------------------------------------------------------------- - Adds a new `@TopicsVisualStyle` option directive for customizing the way Topics sections are rendered on a given page. `@TopicsVisualStyle` accepts an unnamed parameter containing one of the following: - `list`: The current list of topics we render on page, including their abstracts. This is Swift-DocC’s current default if a topics style is not specified. - `compactGrid`: A grid of items based on the card image for each page. Includes each page’s title and card image but excludes their abstracts. - `detailedGrid`: A grid of items based on the card image for each page, includes the abstract for each page. - `hidden`: Don’t show child pages anywhere on the page. `@TopicsVisualStyle` is described on the Swift forums here: https://forums.swift.org/t/supporting-more-types-of-documentation-with-swift-docc/59725#topicsvisualstyle-18 ------------------------------------------------------------------------------- Resolves rdar://97737541
1 parent 1c4b479 commit 3099317

31 files changed

+1075
-32
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationContext.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
127127

128128
/// The graph of all the documentation content and their relationships to each other.
129129
var topicGraph = TopicGraph()
130+
131+
/// User-provided global options for this documentation conversion.
132+
var options: Options?
130133

131134
/// A value to control whether the set of manually curated references found during bundle registration should be stored. Defaults to `false`. Setting this property to `false` clears any stored references from `manuallyCuratedReferences`.
132135
public var shouldStoreManuallyCuratedReferences: Bool = false {
@@ -1981,6 +1984,43 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
19811984
let (technologies, tutorials, tutorialArticles, allArticles) = result
19821985
var (otherArticles, rootPageArticles) = splitArticles(allArticles)
19831986

1987+
let globalOptions = (allArticles + uncuratedDocumentationExtensions.values.flatMap { $0 }).compactMap { article in
1988+
return article.value.options[.global]
1989+
}
1990+
1991+
if globalOptions.count > 1 {
1992+
let extraGlobalOptionsProblems = globalOptions.map { extraOptionsDirective -> Problem in
1993+
let diagnostic = Diagnostic(
1994+
source: extraOptionsDirective.originalMarkup.nameLocation?.source,
1995+
severity: .warning,
1996+
range: extraOptionsDirective.originalMarkup.range,
1997+
identifier: "org.swift.docc.DuplicateGlobalOptions",
1998+
summary: "Duplicate \(extraOptionsDirective.scope) \(Options.directiveName.singleQuoted) directive",
1999+
explanation: """
2000+
A DocC catalog can only contain a single \(Options.directiveName.singleQuoted) \
2001+
directive with the \(extraOptionsDirective.scope.rawValue.singleQuoted) scope.
2002+
"""
2003+
)
2004+
2005+
guard let range = extraOptionsDirective.originalMarkup.range else {
2006+
return Problem(diagnostic: diagnostic)
2007+
}
2008+
2009+
let solution = Solution(
2010+
summary: "Remove extraneous \(extraOptionsDirective.scope) \(Options.directiveName.singleQuoted) directive",
2011+
replacements: [
2012+
Replacement(range: range, replacement: "")
2013+
]
2014+
)
2015+
2016+
return Problem(diagnostic: diagnostic, possibleSolutions: [solution])
2017+
}
2018+
2019+
diagnosticEngine.emit(extraGlobalOptionsProblems)
2020+
} else {
2021+
options = globalOptions.first
2022+
}
2023+
19842024
if LinkResolutionMigrationConfiguration.shouldSetUpHierarchyBasedLinkResolver {
19852025
hierarchyBasedLinkResolver = hierarchyBasedResolver
19862026
hierarchyBasedResolver.addMappingForRoots(bundle: bundle)

Sources/SwiftDocC/Infrastructure/Symbol Graph/GeneratedDocumentationTopics.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ enum GeneratedDocumentationTopics {
185185
availableSourceLanguages: automaticCurationSourceLanguages,
186186
name: DocumentationNode.Name.conceptual(title: title),
187187
markup: Document(parsing: ""),
188-
semantic: Article(markup: nil, metadata: nil, redirects: nil)
188+
semantic: Article(markup: nil, metadata: nil, redirects: nil, options: [:])
189189
)
190190

191191
let collectionTaskGroups = try AutomaticCuration.topics(for: temporaryCollectionNode, withTrait: nil, context: context)

Sources/SwiftDocC/Infrastructure/Topic Graph/AutomaticCuration.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ public struct AutomaticCuration {
123123
renderContext: RenderContext?,
124124
renderer: DocumentationContentRenderer
125125
) throws -> TaskGroup? {
126+
if let automaticSeeAlsoOption = node.options?.automaticSeeAlsoBehavior
127+
?? context.options?.automaticSeeAlsoBehavior
128+
{
129+
guard automaticSeeAlsoOption == .siblingPages else {
130+
return nil
131+
}
132+
}
133+
126134
// First try getting the canonical path from a render context, default to the documentation context
127135
guard let canonicalPath = renderContext?.store.content(for: node.reference)?.canonicalPath ?? context.pathsTo(node.reference).first,
128136
!canonicalPath.isEmpty else {

Sources/SwiftDocC/Model/DocumentationMarkup.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ struct DocumentationMarkup {
146146
// Found deprecation notice in the abstract.
147147
deprecation = MarkupContainer(directive.children)
148148
return
149-
} else if directive.name == Comment.directiveName || directive.name == Metadata.directiveName {
149+
} else if directive.name == Comment.directiveName || directive.name == Metadata.directiveName || directive.name == Options.directiveName {
150150
// These directives don't affect content so they shouldn't break us out of
151151
// the automatic abstract section.
152152
return

Sources/SwiftDocC/Model/DocumentationNode.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public struct DocumentationNode {
6161

6262
/// If true, the node was created implicitly and should not generally be rendered as a page of documentation.
6363
public var isVirtual: Bool
64+
65+
/// The authored options for this node.
66+
///
67+
/// Allows for control of settings such as automatic see also generation.
68+
public var options: Options?
6469

6570
/// A discrete unit of documentation
6671
struct DocumentationChunk {
@@ -137,6 +142,13 @@ public struct DocumentationNode {
137142
self.platformNames = platformNames
138143
self.docChunks = [DocumentationChunk(source: .sourceCode(location: nil), markup: markup)]
139144
self.isVirtual = isVirtual
145+
146+
if let article = semantic as? Article {
147+
self.options = article.options[.local]
148+
} else {
149+
self.options = nil
150+
}
151+
140152
updateAnchorSections()
141153
}
142154

@@ -351,6 +363,8 @@ public struct DocumentationNode {
351363
)
352364
}
353365

366+
options = documentationExtension?.options[.local]
367+
354368
updateAnchorSections()
355369
}
356370

@@ -603,6 +617,7 @@ public struct DocumentationNode {
603617
self.docChunks = [DocumentationChunk(source: .documentationExtension, markup: articleMarkup)]
604618
self.markup = articleMarkup
605619
self.isVirtual = false
620+
self.options = article.options[.local]
606621

607622
updateAnchorSections()
608623
}

Sources/SwiftDocC/Model/Rendering/RenderNode.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ public struct RenderNode: VariantContainer {
162162
/// The variants of the primary content sections of the node, which are the main sections of a reference documentation node.
163163
public var primaryContentSectionsVariants: [VariantCollection<CodableContentSection?>] = []
164164

165+
/// The visual style that should be used when rendering this page's Topics section.
166+
public var topicSectionsStyle: TopicsSectionStyle
167+
165168
/// The default Topics sections of this documentation node, which contain links to useful related documentation nodes.
166169
public var topicSections: [TaskGroupRenderSection] {
167170
get { getVariantDefaultValue(keyPath: \.topicSectionsVariants) }
@@ -234,6 +237,7 @@ public struct RenderNode: VariantContainer {
234237
public init(identifier: ResolvedTopicReference, kind: Kind) {
235238
self.identifier = identifier
236239
self.kind = kind
240+
self.topicSectionsStyle = .list
237241
}
238242

239243
// MARK: Tutorials nodes

Sources/SwiftDocC/Model/Rendering/RenderNode/RenderNode+Codable.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Foundation
1313
extension RenderNode: Codable {
1414
private enum CodingKeys: CodingKey {
1515
case schemaVersion, identifier, sections, references, metadata, kind, hierarchy
16-
case abstract, topicSections, defaultImplementationsSections, primaryContentSections, relationshipsSections, declarationSections, seeAlsoSections, returnsSection, parametersSection, sampleCodeDownload, downloadNotAvailableSummary, deprecationSummary, diffAvailability, interfaceLanguage, variants, variantOverrides
16+
case abstract, topicSections, topicSectionsStyle, defaultImplementationsSections, primaryContentSections, relationshipsSections, declarationSections, seeAlsoSections, returnsSection, parametersSection, sampleCodeDownload, downloadNotAvailableSummary, deprecationSummary, diffAvailability, interfaceLanguage, variants, variantOverrides
1717
}
1818

1919
public init(from decoder: Decoder) throws {
@@ -26,6 +26,7 @@ extension RenderNode: Codable {
2626
metadata = try container.decode(RenderMetadata.self, forKey: .metadata)
2727
kind = try container.decode(Kind.self, forKey: .kind)
2828
hierarchy = try container.decodeIfPresent(RenderHierarchy.self, forKey: .hierarchy)
29+
topicSectionsStyle = try container.decodeIfPresent(TopicsSectionStyle.self, forKey: .topicSectionsStyle) ?? .list
2930

3031
primaryContentSectionsVariants = try container.decodeVariantCollectionArrayIfPresent(
3132
ofValueType: CodableContentSection?.self,
@@ -79,6 +80,9 @@ extension RenderNode: Codable {
7980
try container.encode(metadata, forKey: .metadata)
8081
try container.encode(kind, forKey: .kind)
8182
try container.encode(hierarchy, forKey: .hierarchy)
83+
if topicSectionsStyle != .list {
84+
try container.encode(topicSectionsStyle, forKey: .topicSectionsStyle)
85+
}
8286

8387
try container.encodeVariantCollection(abstractVariants, forKey: .abstract, encoder: encoder)
8488

Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -703,12 +703,15 @@ public struct RenderNodeTranslator: SemanticVisitor {
703703
return sections
704704
} ?? .init(defaultValue: [])
705705

706-
707-
if node.topicSections.isEmpty {
708-
// Set an eyebrow for articles
709-
node.metadata.roleHeading = "Article"
706+
node.topicSectionsStyle = topicsSectionStyle(for: documentationNode)
707+
708+
if shouldCreateAutomaticRoleHeading(for: documentationNode) {
709+
if node.topicSections.isEmpty {
710+
// Set an eyebrow for articles
711+
node.metadata.roleHeading = "Article"
712+
}
713+
node.metadata.role = contentRenderer.roleForArticle(article, nodeKind: documentationNode.kind).rawValue
710714
}
711-
node.metadata.role = contentRenderer.roleForArticle(article, nodeKind: documentationNode.kind).rawValue
712715

713716
node.seeAlsoSectionsVariants = VariantCollection<[TaskGroupRenderSection]>(
714717
from: documentationNode.availableVariantTraits,
@@ -1038,6 +1041,39 @@ public struct RenderNodeTranslator: SemanticVisitor {
10381041
return reference
10391042
}
10401043

1044+
private func shouldCreateAutomaticRoleHeading(for node: DocumentationNode) -> Bool {
1045+
var shouldCreateAutomaticRoleHeading = true
1046+
if let automaticTitleHeadingOption = node.options?.automaticTitleHeadingBehavior
1047+
?? context.options?.automaticTitleHeadingBehavior
1048+
{
1049+
shouldCreateAutomaticRoleHeading = automaticTitleHeadingOption == .pageKind
1050+
}
1051+
1052+
return shouldCreateAutomaticRoleHeading
1053+
}
1054+
1055+
private func topicsSectionStyle(for node: DocumentationNode) -> RenderNode.TopicsSectionStyle {
1056+
let topicsVisualStyleOption: TopicsVisualStyle.Style
1057+
if let topicsSectionStyleOption = node.options?.topicsVisualStyle
1058+
?? context.options?.topicsVisualStyle
1059+
{
1060+
topicsVisualStyleOption = topicsSectionStyleOption
1061+
} else {
1062+
topicsVisualStyleOption = .list
1063+
}
1064+
1065+
switch topicsVisualStyleOption {
1066+
case .list:
1067+
return .list
1068+
case .compactGrid:
1069+
return .compactGrid
1070+
case .detailedGrid:
1071+
return .detailedGrid
1072+
case .hidden:
1073+
return .hidden
1074+
}
1075+
}
1076+
10411077
public mutating func visitSymbol(_ symbol: Symbol) -> RenderTree? {
10421078
let documentationNode = try! context.entity(with: identifier)
10431079

@@ -1095,10 +1131,13 @@ public struct RenderNodeTranslator: SemanticVisitor {
10951131

10961132
node.metadata.requiredVariants = VariantCollection<Bool>(from: symbol.isRequiredVariants) ?? .init(defaultValue: false)
10971133
node.metadata.role = contentRenderer.role(for: documentationNode.kind).rawValue
1098-
node.metadata.roleHeadingVariants = VariantCollection<String?>(from: symbol.roleHeadingVariants)
10991134
node.metadata.titleVariants = VariantCollection<String?>(from: symbol.titleVariants)
11001135
node.metadata.externalIDVariants = VariantCollection<String?>(from: symbol.externalIDVariants)
11011136

1137+
if shouldCreateAutomaticRoleHeading(for: documentationNode) {
1138+
node.metadata.roleHeadingVariants = VariantCollection<String?>(from: symbol.roleHeadingVariants)
1139+
}
1140+
11021141
node.metadata.symbolKindVariants = VariantCollection<String?>(from: symbol.kindVariants) { _, kindVariants in
11031142
kindVariants.identifier.renderingIdentifier
11041143
} ?? .init(defaultValue: nil)
@@ -1308,6 +1347,8 @@ public struct RenderNodeTranslator: SemanticVisitor {
13081347
return sections
13091348
} ?? .init(defaultValue: [])
13101349

1350+
node.topicSectionsStyle = topicsSectionStyle(for: documentationNode)
1351+
13111352
node.defaultImplementationsSectionsVariants = VariantCollection<[TaskGroupRenderSection]>(
13121353
from: symbol.defaultImplementationsVariants,
13131354
symbol.relationshipsVariants
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2022 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+
extension RenderNode {
12+
/// The rendering style of the topics section.
13+
public enum TopicsSectionStyle: String, Codable {
14+
/// A list of the page's topics, including their full declaration and abstract.
15+
case list
16+
17+
/// A grid of items based on the card image for each page.
18+
///
19+
/// Includes each page’s title and card image but excludes their abstracts.
20+
case compactGrid
21+
22+
/// A grid of items based on the card image for each page.
23+
///
24+
/// Unlike ``compactGrid``, this style includes the abstract for each page.
25+
case detailedGrid
26+
27+
/// Do not show child pages anywhere on the page.
28+
case hidden
29+
}
30+
}

Sources/SwiftDocC/Semantics/Article/Article.swift

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
2121
let markup: Markup?
2222
/// An optional container for metadata that's unrelated to the article's content.
2323
private(set) var metadata: Metadata?
24+
/// An optional container for options that are unrelated to the article's content.
25+
private(set) var options: [Options.Scope : Options]
2426
/// An optional list of previously known locations for this article.
2527
private(set) public var redirects: [Redirect]?
2628

@@ -30,10 +32,11 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
3032
/// - markup: The markup that makes up this article's content.
3133
/// - metadata: An optional container for metadata that's unrelated to the article's content.
3234
/// - redirects: An optional list of previously known locations for this article.
33-
init(markup: Markup?, metadata: Metadata?, redirects: [Redirect]?) {
35+
init(markup: Markup?, metadata: Metadata?, redirects: [Redirect]?, options: [Options.Scope : Options]) {
3436
let markupModel = markup.map { DocumentationMarkup(markup: $0) }
3537

3638
self.markup = markup
39+
self.options = options
3740
self.metadata = metadata
3841
self.redirects = redirects
3942
self.discussion = markupModel?.discussionSection
@@ -46,7 +49,7 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
4649
}
4750

4851
convenience init(title: Heading?, abstractSection: AbstractSection?, discussion: DiscussionSection?, topics: TopicsSection?, seeAlso: SeeAlsoSection?, deprecationSummary: MarkupContainer?, metadata: Metadata?, redirects: [Redirect]?, automaticTaskGroups: [AutomaticTaskGroupSection]? = nil) {
49-
self.init(markup: nil, metadata: metadata, redirects: redirects)
52+
self.init(markup: nil, metadata: metadata, redirects: redirects, options: [:])
5053
self.title = title
5154
self.abstractSection = abstractSection
5255
self.discussion = discussion
@@ -139,7 +142,61 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
139142
}
140143

141144
var optionalMetadata = metadata.first
145+
146+
let options: [Options]
147+
(options, remainder) = remainder.categorize { child -> Options? in
148+
guard let childDirective = child as? BlockDirective, childDirective.name == Options.directiveName else {
149+
return nil
150+
}
151+
return Options(
152+
from: childDirective,
153+
source: source,
154+
for: bundle,
155+
in: context,
156+
problems: &problems
157+
)
158+
}
159+
160+
let allCategorizedOptions = Dictionary(grouping: options, by: \.scope)
161+
162+
for (scope, options) in allCategorizedOptions {
163+
let extraOptions = options.dropFirst()
164+
guard !extraOptions.isEmpty else {
165+
continue
166+
}
167+
168+
let extraOptionsProblems = extraOptions.map { extraOptionsDirective -> Problem in
169+
let diagnostic = Diagnostic(
170+
source: source,
171+
severity: .warning,
172+
range: extraOptionsDirective.originalMarkup.range,
173+
identifier: "org.swift.docc.HasAtMostOne<\(Article.self), \(Options.self), \(scope)>.DuplicateChildren",
174+
summary: "Duplicate \(scope) \(Options.directiveName.singleQuoted) directive",
175+
explanation: """
176+
An article can only contain a single \(Options.directiveName.singleQuoted) \
177+
directive with the \(scope.rawValue.singleQuoted) scope.
178+
"""
179+
)
142180

181+
guard let range = extraOptionsDirective.originalMarkup.range else {
182+
return Problem(diagnostic: diagnostic)
183+
}
184+
185+
let solution = Solution(
186+
summary: "Remove extraneous \(scope) \(Options.directiveName.singleQuoted) directive",
187+
replacements: [
188+
Replacement(range: range, replacement: "")
189+
]
190+
)
191+
192+
return Problem(diagnostic: diagnostic, possibleSolutions: [solution])
193+
}
194+
195+
problems.append(contentsOf: extraOptionsProblems)
196+
}
197+
198+
let relevantCategorizedOptions = allCategorizedOptions.compactMapValues(\.first)
199+
143200
let isDocumentationExtension = title.child(at: 0) is AnyLink
144201
if !isDocumentationExtension, let metadata = optionalMetadata, let displayName = metadata.displayName {
145202
let diagnosticSummary = """
@@ -164,7 +221,12 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
164221
optionalMetadata = Metadata(originalMarkup: metadata.originalMarkup, documentationExtension: metadata.documentationOptions, technologyRoot: metadata.technologyRoot, displayName: nil)
165222
}
166223

167-
self.init(markup: markup, metadata: optionalMetadata, redirects: redirects.isEmpty ? nil : redirects)
224+
self.init(
225+
markup: markup,
226+
metadata: optionalMetadata,
227+
redirects: redirects.isEmpty ? nil : redirects,
228+
options: relevantCategorizedOptions
229+
)
168230
}
169231

170232
/// Visit the article using a semantic visitor and return the result of visiting the article.

0 commit comments

Comments
 (0)