Skip to content

Add support for options directive #368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 15, 2022
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
40 changes: 40 additions & 0 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {

/// The graph of all the documentation content and their relationships to each other.
var topicGraph = TopicGraph()

/// User-provided global options for this documentation conversion.
var options: Options?

/// 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`.
public var shouldStoreManuallyCuratedReferences: Bool = false {
Expand Down Expand Up @@ -1981,6 +1984,43 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate {
let (technologies, tutorials, tutorialArticles, allArticles) = result
var (otherArticles, rootPageArticles) = splitArticles(allArticles)

let globalOptions = (allArticles + uncuratedDocumentationExtensions.values.flatMap { $0 }).compactMap { article in
return article.value.options[.global]
}

if globalOptions.count > 1 {
let extraGlobalOptionsProblems = globalOptions.map { extraOptionsDirective -> Problem in
let diagnostic = Diagnostic(
source: extraOptionsDirective.originalMarkup.nameLocation?.source,
severity: .warning,
range: extraOptionsDirective.originalMarkup.range,
identifier: "org.swift.docc.DuplicateGlobalOptions",
summary: "Duplicate \(extraOptionsDirective.scope) \(Options.directiveName.singleQuoted) directive",
explanation: """
A DocC catalog can only contain a single \(Options.directiveName.singleQuoted) \
directive with the \(extraOptionsDirective.scope.rawValue.singleQuoted) scope.
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is a warning, it would be good to describe which one of these global options will be dropped, if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They're all dropped currently but I can update the diagnostic to clarify that. Picking a single one deterministically is somewhat non-trivial and didn't seem worth it to me since it's an unsupported workflow.

"""
)

guard let range = extraOptionsDirective.originalMarkup.range else {
return Problem(diagnostic: diagnostic)
}

let solution = Solution(
summary: "Remove extraneous \(extraOptionsDirective.scope) \(Options.directiveName.singleQuoted) directive",
replacements: [
Replacement(range: range, replacement: "")
]
)

return Problem(diagnostic: diagnostic, possibleSolutions: [solution])
}

diagnosticEngine.emit(extraGlobalOptionsProblems)
} else {
options = globalOptions.first
}

if LinkResolutionMigrationConfiguration.shouldSetUpHierarchyBasedLinkResolver {
hierarchyBasedLinkResolver = hierarchyBasedResolver
hierarchyBasedResolver.addMappingForRoots(bundle: bundle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ enum GeneratedDocumentationTopics {
availableSourceLanguages: automaticCurationSourceLanguages,
name: DocumentationNode.Name.conceptual(title: title),
markup: Document(parsing: ""),
semantic: Article(markup: nil, metadata: nil, redirects: nil)
semantic: Article(markup: nil, metadata: nil, redirects: nil, options: [:])
)

let collectionTaskGroups = try AutomaticCuration.topics(for: temporaryCollectionNode, withTrait: nil, context: context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ public struct AutomaticCuration {
renderContext: RenderContext?,
renderer: DocumentationContentRenderer
) throws -> TaskGroup? {
if let automaticSeeAlsoOption = node.options?.automaticSeeAlsoBehavior
?? context.options?.automaticSeeAlsoBehavior
{
guard automaticSeeAlsoOption == .siblingPages else {
return nil
}
}

// First try getting the canonical path from a render context, default to the documentation context
guard let canonicalPath = renderContext?.store.content(for: node.reference)?.canonicalPath ?? context.pathsTo(node.reference).first,
!canonicalPath.isEmpty else {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftDocC/Model/DocumentationMarkup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ struct DocumentationMarkup {
// Found deprecation notice in the abstract.
deprecation = MarkupContainer(directive.children)
return
} else if directive.name == Comment.directiveName || directive.name == Metadata.directiveName {
} else if directive.name == Comment.directiveName || directive.name == Metadata.directiveName || directive.name == Options.directiveName {
// These directives don't affect content so they shouldn't break us out of
// the automatic abstract section.
return
Expand Down
15 changes: 15 additions & 0 deletions Sources/SwiftDocC/Model/DocumentationNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public struct DocumentationNode {

/// If true, the node was created implicitly and should not generally be rendered as a page of documentation.
public var isVirtual: Bool

/// The authored options for this node.
///
/// Allows for control of settings such as automatic see also generation.
public var options: Options?

/// A discrete unit of documentation
struct DocumentationChunk {
Expand Down Expand Up @@ -137,6 +142,13 @@ public struct DocumentationNode {
self.platformNames = platformNames
self.docChunks = [DocumentationChunk(source: .sourceCode(location: nil), markup: markup)]
self.isVirtual = isVirtual

if let article = semantic as? Article {
self.options = article.options[.local]
} else {
self.options = nil
}

updateAnchorSections()
}

Expand Down Expand Up @@ -351,6 +363,8 @@ public struct DocumentationNode {
)
}

options = documentationExtension?.options[.local]

updateAnchorSections()
}

Expand Down Expand Up @@ -603,6 +617,7 @@ public struct DocumentationNode {
self.docChunks = [DocumentationChunk(source: .documentationExtension, markup: articleMarkup)]
self.markup = articleMarkup
self.isVirtual = false
self.options = article.options[.local]

updateAnchorSections()
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftDocC/Model/Rendering/RenderNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ public struct RenderNode: VariantContainer {
/// The variants of the primary content sections of the node, which are the main sections of a reference documentation node.
public var primaryContentSectionsVariants: [VariantCollection<CodableContentSection?>] = []

/// The visual style that should be used when rendering this page's Topics section.
public var topicSectionsStyle: TopicsSectionStyle

/// The default Topics sections of this documentation node, which contain links to useful related documentation nodes.
public var topicSections: [TaskGroupRenderSection] {
get { getVariantDefaultValue(keyPath: \.topicSectionsVariants) }
Expand Down Expand Up @@ -234,6 +237,7 @@ public struct RenderNode: VariantContainer {
public init(identifier: ResolvedTopicReference, kind: Kind) {
self.identifier = identifier
self.kind = kind
self.topicSectionsStyle = .list
}

// MARK: Tutorials nodes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Foundation
extension RenderNode: Codable {
private enum CodingKeys: CodingKey {
case schemaVersion, identifier, sections, references, metadata, kind, hierarchy
case abstract, topicSections, defaultImplementationsSections, primaryContentSections, relationshipsSections, declarationSections, seeAlsoSections, returnsSection, parametersSection, sampleCodeDownload, downloadNotAvailableSummary, deprecationSummary, diffAvailability, interfaceLanguage, variants, variantOverrides
case abstract, topicSections, topicSectionsStyle, defaultImplementationsSections, primaryContentSections, relationshipsSections, declarationSections, seeAlsoSections, returnsSection, parametersSection, sampleCodeDownload, downloadNotAvailableSummary, deprecationSummary, diffAvailability, interfaceLanguage, variants, variantOverrides
}

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

primaryContentSectionsVariants = try container.decodeVariantCollectionArrayIfPresent(
ofValueType: CodableContentSection?.self,
Expand Down Expand Up @@ -79,6 +80,9 @@ extension RenderNode: Codable {
try container.encode(metadata, forKey: .metadata)
try container.encode(kind, forKey: .kind)
try container.encode(hierarchy, forKey: .hierarchy)
if topicSectionsStyle != .list {
try container.encode(topicSectionsStyle, forKey: .topicSectionsStyle)
}

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

Expand Down
53 changes: 47 additions & 6 deletions Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -703,12 +703,15 @@ public struct RenderNodeTranslator: SemanticVisitor {
return sections
} ?? .init(defaultValue: [])


if node.topicSections.isEmpty {
// Set an eyebrow for articles
node.metadata.roleHeading = "Article"
node.topicSectionsStyle = topicsSectionStyle(for: documentationNode)

if shouldCreateAutomaticRoleHeading(for: documentationNode) {
if node.topicSections.isEmpty {
// Set an eyebrow for articles
node.metadata.roleHeading = "Article"
}
node.metadata.role = contentRenderer.roleForArticle(article, nodeKind: documentationNode.kind).rawValue
}
node.metadata.role = contentRenderer.roleForArticle(article, nodeKind: documentationNode.kind).rawValue

node.seeAlsoSectionsVariants = VariantCollection<[TaskGroupRenderSection]>(
from: documentationNode.availableVariantTraits,
Expand Down Expand Up @@ -1038,6 +1041,39 @@ public struct RenderNodeTranslator: SemanticVisitor {
return reference
}

private func shouldCreateAutomaticRoleHeading(for node: DocumentationNode) -> Bool {
var shouldCreateAutomaticRoleHeading = true
if let automaticTitleHeadingOption = node.options?.automaticTitleHeadingBehavior
?? context.options?.automaticTitleHeadingBehavior
{
shouldCreateAutomaticRoleHeading = automaticTitleHeadingOption == .pageKind
}

return shouldCreateAutomaticRoleHeading
}

private func topicsSectionStyle(for node: DocumentationNode) -> RenderNode.TopicsSectionStyle {
let topicsVisualStyleOption: TopicsVisualStyle.Style
if let topicsSectionStyleOption = node.options?.topicsVisualStyle
?? context.options?.topicsVisualStyle
{
topicsVisualStyleOption = topicsSectionStyleOption
} else {
topicsVisualStyleOption = .list
}

switch topicsVisualStyleOption {
case .list:
return .list
case .compactGrid:
return .compactGrid
case .detailedGrid:
return .detailedGrid
case .hidden:
return .hidden
}
}

public mutating func visitSymbol(_ symbol: Symbol) -> RenderTree? {
let documentationNode = try! context.entity(with: identifier)

Expand Down Expand Up @@ -1095,10 +1131,13 @@ public struct RenderNodeTranslator: SemanticVisitor {

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

if shouldCreateAutomaticRoleHeading(for: documentationNode) {
node.metadata.roleHeadingVariants = VariantCollection<String?>(from: symbol.roleHeadingVariants)
}

node.metadata.symbolKindVariants = VariantCollection<String?>(from: symbol.kindVariants) { _, kindVariants in
kindVariants.identifier.renderingIdentifier
} ?? .init(defaultValue: nil)
Expand Down Expand Up @@ -1308,6 +1347,8 @@ public struct RenderNodeTranslator: SemanticVisitor {
return sections
} ?? .init(defaultValue: [])

node.topicSectionsStyle = topicsSectionStyle(for: documentationNode)

node.defaultImplementationsSectionsVariants = VariantCollection<[TaskGroupRenderSection]>(
from: symbol.defaultImplementationsVariants,
symbol.relationshipsVariants
Expand Down
30 changes: 30 additions & 0 deletions Sources/SwiftDocC/Model/Rendering/TopicsSectionStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2022 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
*/

extension RenderNode {
/// The rendering style of the topics section.
public enum TopicsSectionStyle: String, Codable {
/// A list of the page's topics, including their full declaration and abstract.
case list

/// A grid of items based on the card image for each page.
///
/// Includes each page’s title and card image but excludes their abstracts.
case compactGrid

/// A grid of items based on the card image for each page.
///
/// Unlike ``compactGrid``, this style includes the abstract for each page.
case detailedGrid

/// Do not show child pages anywhere on the page.
case hidden
}
}
68 changes: 65 additions & 3 deletions Sources/SwiftDocC/Semantics/Article/Article.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
let markup: Markup?
/// An optional container for metadata that's unrelated to the article's content.
private(set) var metadata: Metadata?
/// An optional container for options that are unrelated to the article's content.
private(set) var options: [Options.Scope : Options]
/// An optional list of previously known locations for this article.
private(set) public var redirects: [Redirect]?

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

self.markup = markup
self.options = options
self.metadata = metadata
self.redirects = redirects
self.discussion = markupModel?.discussionSection
Expand All @@ -46,7 +49,7 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
}

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

var optionalMetadata = metadata.first

let options: [Options]
(options, remainder) = remainder.categorize { child -> Options? in
guard let childDirective = child as? BlockDirective, childDirective.name == Options.directiveName else {
return nil
}
return Options(
from: childDirective,
source: source,
for: bundle,
in: context,
problems: &problems
)
}

let allCategorizedOptions = Dictionary(grouping: options, by: \.scope)

for (scope, options) in allCategorizedOptions {
let extraOptions = options.dropFirst()
guard !extraOptions.isEmpty else {
continue
}

let extraOptionsProblems = extraOptions.map { extraOptionsDirective -> Problem in
let diagnostic = Diagnostic(
source: source,
severity: .warning,
range: extraOptionsDirective.originalMarkup.range,
identifier: "org.swift.docc.HasAtMostOne<\(Article.self), \(Options.self), \(scope)>.DuplicateChildren",
summary: "Duplicate \(scope) \(Options.directiveName.singleQuoted) directive",
explanation: """
An article can only contain a single \(Options.directiveName.singleQuoted) \
directive with the \(scope.rawValue.singleQuoted) scope.
Comment on lines +176 to +177
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here regarding explaining which will be dropped.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The first one will be kept and all others dropped. I updated the diagnostics to only apply to those that are dropped.

"""
)

guard let range = extraOptionsDirective.originalMarkup.range else {
return Problem(diagnostic: diagnostic)
}

let solution = Solution(
summary: "Remove extraneous \(scope) \(Options.directiveName.singleQuoted) directive",
replacements: [
Replacement(range: range, replacement: "")
]
)

return Problem(diagnostic: diagnostic, possibleSolutions: [solution])
}

problems.append(contentsOf: extraOptionsProblems)
}

let relevantCategorizedOptions = allCategorizedOptions.compactMapValues(\.first)

let isDocumentationExtension = title.child(at: 0) is AnyLink
if !isDocumentationExtension, let metadata = optionalMetadata, let displayName = metadata.displayName {
let diagnosticSummary = """
Expand All @@ -164,7 +221,12 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected,
optionalMetadata = Metadata(originalMarkup: metadata.originalMarkup, documentationExtension: metadata.documentationOptions, technologyRoot: metadata.technologyRoot, displayName: nil)
}

self.init(markup: markup, metadata: optionalMetadata, redirects: redirects.isEmpty ? nil : redirects)
self.init(
markup: markup,
metadata: optionalMetadata,
redirects: redirects.isEmpty ? nil : redirects,
options: relevantCategorizedOptions
)
}

/// Visit the article using a semantic visitor and return the result of visiting the article.
Expand Down
Loading