Skip to content

Commit 1c4b479

Browse files
Add authoring support for @Row and @column directives (swiftlang#378)
Adds support for the @Row and @column directive as described here: https://forums.swift.org/t/supporting-more-dynamic-content-in-swift-docc-reference-documentation/59527#row-9. Dependencies: - swiftlang/swift-docc-render#409 Example: @Row(numberOfColumns: 3) { @column(size: 1) { ![A sloth hanging off of a tree.](sloth-in-tree.png) } @column(size: 2) { SlothCreator provides models and utilities for creating, tracking, and caring for sloths. The framework provides structures to model an individual ``Sloth``, and identify them by key characteristics, including their ``name`` and special supernatural ``power``. } } Resolves rdar://97739637
1 parent db190fe commit 1c4b479

25 files changed

+836
-19
lines changed

Sources/SwiftDocC/Indexing/RenderBlockContent+TextIndexing.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ extension RenderBlockContent: TextIndexing {
5858
return $0.term.inlineContent.rawIndexableTextContent(references: references)
5959
+ ( definition.isEmpty ? "" : " \(definition)" )
6060
}.joined(separator: " ")
61+
case .row(let row):
62+
return row.columns.map { column in
63+
return column.content.rawIndexableTextContent(references: references)
64+
}.joined(separator: " ")
6165
default:
6266
fatalError("unknown RenderBlockContent case in rawIndexableTextContent")
6367
}

Sources/SwiftDocC/Model/DocumentationMarkup.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,17 @@ struct DocumentationMarkup {
142142
abstractSection = AbstractSection(paragraph: firstParagraph)
143143
return
144144
} else if let directive = child as? BlockDirective {
145-
// Found deprecation notice in the abstract.
146145
if directive.name == DeprecationSummary.directiveName {
146+
// Found deprecation notice in the abstract.
147147
deprecation = MarkupContainer(directive.children)
148+
return
149+
} else if directive.name == Comment.directiveName || directive.name == Metadata.directiveName {
150+
// These directives don't affect content so they shouldn't break us out of
151+
// the automatic abstract section.
152+
return
153+
} else {
154+
currentSection = .discussion
148155
}
149-
// Skip other block like @Comment and so on.
150-
return
151156
} else if let _ = child as? HTMLBlock {
152157
// Skip HTMLBlock comment.
153158
return

Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public enum RenderBlockContent: Equatable {
6060
case termList(TermList)
6161
/// A table that contains a list of row data.
6262
case table(Table)
63+
64+
case row(Row)
6365

6466
// Warning: If you add a new case to this enum, make sure to handle it in the Codable
6567
// conformance at the bottom of this file, and in the `rawIndexableTextContent` method in
@@ -403,6 +405,28 @@ public enum RenderBlockContent: Equatable {
403405
/// The definition in the term-list item.
404406
public let definition: Definition
405407
}
408+
409+
/// A row in a grid-based layout system that describes a collection of columns.
410+
public struct Row: Codable, Equatable {
411+
/// The number of columns that should be rendered in this row.
412+
///
413+
/// This may be different then the count of ``columns`` array. For example, there may be
414+
/// individual columns that span multiple columns (specified with the column's
415+
/// ``Column/size`` property) or the row could be not fully filled with columns.
416+
public let numberOfColumns: Int
417+
418+
/// The columns that should be rendered in this row.
419+
public let columns: [Column]
420+
421+
/// A column with a row in a grid-based layout system.
422+
public struct Column: Codable, Equatable {
423+
/// The number of columns in the parent row this column should span.
424+
public let size: Int
425+
426+
/// The content that should be rendered in this column.
427+
public let content: [RenderBlockContent]
428+
}
429+
}
406430
}
407431

408432
// Codable conformance
@@ -412,6 +436,7 @@ extension RenderBlockContent: Codable {
412436
case inlineContent, content, caption, style, name, syntax, code, level, text, items, media, runtimePreview, anchor, summary, example, metadata
413437
case request, response
414438
case header, rows
439+
case numberOfColumns, columns
415440
}
416441

417442
public init(from decoder: Decoder) throws {
@@ -457,11 +482,18 @@ extension RenderBlockContent: Codable {
457482
))
458483
case .termList:
459484
self = try .termList(.init(items: container.decode([TermListItem].self, forKey: .items)))
485+
case .row:
486+
self = try .row(
487+
Row(
488+
numberOfColumns: container.decode(Int.self, forKey: .numberOfColumns),
489+
columns: container.decode([Row.Column].self, forKey: .columns)
490+
)
491+
)
460492
}
461493
}
462494

463495
private enum BlockType: String, Codable {
464-
case paragraph, aside, codeListing, heading, orderedList, unorderedList, step, endpointExample, dictionaryExample, table, termList
496+
case paragraph, aside, codeListing, heading, orderedList, unorderedList, step, endpointExample, dictionaryExample, table, termList, row
465497
}
466498

467499
private var type: BlockType {
@@ -477,6 +509,7 @@ extension RenderBlockContent: Codable {
477509
case .dictionaryExample: return .dictionaryExample
478510
case .table: return .table
479511
case .termList: return .termList
512+
case .row: return .row
480513
default: fatalError("unknown RenderBlockContent case in type property")
481514
}
482515
}
@@ -523,6 +556,9 @@ extension RenderBlockContent: Codable {
523556
try container.encodeIfPresent(t.metadata, forKey: .metadata)
524557
case .termList(items: let l):
525558
try container.encode(l.items, forKey: .items)
559+
case .row(let row):
560+
try container.encode(row.numberOfColumns, forKey: .numberOfColumns)
561+
try container.encode(row.columns, forKey: .columns)
526562
default:
527563
fatalError("unknown RenderBlockContent case in encode method")
528564
}

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,11 @@ struct RenderContentCompiler: MarkupVisitor {
237237
return docCommentContent + [code]
238238
}
239239
default:
240-
return []
240+
guard let renderableDirective = DirectiveIndex.shared.renderableDirectives[blockDirective.name] else {
241+
return []
242+
}
243+
244+
return renderableDirective.render(blockDirective, with: &self)
241245
}
242246
}
243247

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
import Foundation
12+
import Markdown
13+
14+
/// A directive that can be directly rendered within markup content.
15+
///
16+
/// This protocol is used by the `RenderContentCompiler` to render arbitrary directives
17+
/// that conform to renderable.
18+
protocol RenderableDirectiveConvertible: AutomaticDirectiveConvertible {
19+
func render(with contentCompiler: inout RenderContentCompiler) -> [RenderContent]
20+
}
21+
22+
extension RenderableDirectiveConvertible {
23+
static func render(
24+
_ blockDirective: BlockDirective,
25+
with contentCompiler: inout RenderContentCompiler
26+
) -> [RenderContent] {
27+
guard let directive = Self.init(
28+
from: blockDirective,
29+
for: contentCompiler.bundle,
30+
in: contentCompiler.context
31+
) else {
32+
return []
33+
}
34+
35+
return directive.render(with: &contentCompiler)
36+
}
37+
}
38+
39+
struct AnyRenderableDirectiveConvertibleType {
40+
var underlyingType: RenderableDirectiveConvertible.Type
41+
42+
func render(
43+
_ blockDirective: BlockDirective,
44+
with contentCompiler: inout RenderContentCompiler
45+
) -> [RenderContent] {
46+
return underlyingType.render(blockDirective, with: &contentCompiler)
47+
}
48+
49+
var directiveName: String {
50+
return underlyingType.directiveName
51+
}
52+
}

Sources/SwiftDocC/Model/Rendering/RenderSection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public enum RenderSectionKind: String, Codable {
1717
case hero, intro, tasks, assessments, volume, contentAndMedia, contentAndMediaGroup, callToAction, tile, articleBody, resources
1818

1919
// Symbol render sections
20-
case discussion, content, taskGroup, relationships, declarations, parameters, sampleDownload
20+
case discussion, content, taskGroup, relationships, declarations, parameters, sampleDownload, row
2121

2222
// Rest symbol sections
2323
case restParameters, restResponses, restBody, restEndpoint, properties

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/AutomaticDirectiveConvertible.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ extension AutomaticDirectiveConvertible {
168168

169169
Semantic.Analyses.HasOnlyKnownDirectives<Self>(
170170
severityIfFound: .warning,
171-
allowedDirectives: reflectedDirective.childDirectives.map(\.name)
171+
allowedDirectives: reflectedDirective.childDirectives.map(\.name),
172+
allowsStructuredMarkup: reflectedDirective.allowsStructuredMarkup
172173
)
173174
.analyze(
174175
directive,

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/ChildMarkdownWrapper.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ protocol _ChildMarkupProtocol {
1616

1717
var index: Int? { get }
1818

19+
var supportsStructuredMarkup: Bool { get }
20+
1921
func setProperty<T>(
2022
on containingDirective: T,
2123
named propertyName: String,
@@ -61,6 +63,10 @@ public struct ChildMarkup<Value>: _ChildMarkupProtocol {
6163

6264
var numberOfParagraphs: _ChildMarkupParagraphs
6365

66+
/// Returns true if the child markup can contain structured markup content like
67+
/// rows and columns.
68+
var supportsStructuredMarkup: Bool
69+
6470
public var wrappedValue: Value {
6571
get {
6672
parsedValue!
@@ -89,21 +95,28 @@ public struct ChildMarkup<Value>: _ChildMarkupProtocol {
8995
}
9096

9197
extension ChildMarkup where Value == MarkupContainer {
92-
init(numberOfParagraphs: _ChildMarkupParagraphs = .oneOrMore, index: Int? = nil) {
98+
init(
99+
numberOfParagraphs: _ChildMarkupParagraphs = .oneOrMore,
100+
index: Int? = nil,
101+
supportsStructure: Bool = false
102+
) {
93103
self.parsedValue = MarkupContainer()
94104
self.numberOfParagraphs = numberOfParagraphs
95105
self.index = index
106+
self.supportsStructuredMarkup = supportsStructure
96107
}
97108
}
98109

99110
extension ChildMarkup where Value == Optional<MarkupContainer> {
100111
init(
101112
value: Value,
102113
numberOfParagraphs: _ChildMarkupParagraphs = .zeroOrMore,
103-
index: Int? = nil
114+
index: Int? = nil,
115+
supportsStructure: Bool = false
104116
) {
105117
self.parsedValue = value
106118
self.numberOfParagraphs = numberOfParagraphs
107119
self.index = index
120+
self.supportsStructuredMarkup = supportsStructure
108121
}
109122
}

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveIndex.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct DirectiveIndex {
1616
Redirect.self,
1717
Snippet.self,
1818
DeprecationSummary.self,
19+
Row.self,
1920
]
2021

2122
private static let topLevelTutorialDirectives: [AutomaticDirectiveConvertible.Type] = [
@@ -39,6 +40,8 @@ struct DirectiveIndex {
3940

4041
let indexedDirectives: [String : DirectiveMirror.ReflectedDirective]
4142

43+
let renderableDirectives: [String : AnyRenderableDirectiveConvertibleType]
44+
4245
static let shared = DirectiveIndex()
4346

4447
private init() {
@@ -78,6 +81,14 @@ struct DirectiveIndex {
7881
}
7982

8083
self.indexedDirectives = indexedDirectives
84+
85+
self.renderableDirectives = indexedDirectives.compactMapValues { directive in
86+
guard let renderableDirective = directive.type as? RenderableDirectiveConvertible.Type else {
87+
return nil
88+
}
89+
90+
return AnyRenderableDirectiveConvertibleType(underlyingType: renderableDirective)
91+
}
8192
}
8293

8394
func reflection(

Sources/SwiftDocC/Semantics/DirectiveInfrastructure/DirectiveMirror.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ extension DirectiveMirror {
240240
}
241241
}
242242

243+
var allowsStructuredMarkup: Bool {
244+
switch childMarkupSupport {
245+
case .supportsMarkup(let markupRequirements):
246+
return markupRequirements.first?.markup.supportsStructuredMarkup ?? false
247+
case .disallowsMarkup:
248+
return false
249+
}
250+
}
251+
243252
var requiresMarkup: Bool {
244253
switch childMarkupSupport {
245254
case .supportsMarkup(let markupRequirements):
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
import Markdown
12+
13+
/// A directive convertible that contains markup.
14+
protocol MarkupContaining: DirectiveConvertible {
15+
/// The markup contained by this directive.
16+
///
17+
/// This property does not necessarily return the markup contained only by this directive, it may
18+
/// be the concatenated markup contained by all of this directive's directive children.
19+
var childMarkup: [Markup] { get }
20+
}

Sources/SwiftDocC/Semantics/General Purpose Analyses/HasOnlyKnownDirectives.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,24 @@ extension Semantic.Analyses {
1717
let severityIfFound: DiagnosticSeverity?
1818
let allowedDirectives: [String]
1919
let allowsMarkup: Bool
20-
public init(severityIfFound: DiagnosticSeverity?, allowedDirectives: [String], allowsMarkup: Bool = true) {
20+
public init(
21+
severityIfFound: DiagnosticSeverity?,
22+
allowedDirectives: [String],
23+
allowsMarkup: Bool = true,
24+
allowsStructuredMarkup: Bool = false
25+
) {
2126
self.severityIfFound = severityIfFound
22-
self.allowedDirectives = allowedDirectives
27+
var allowedDirectives = allowedDirectives
2328
/* Comments are always allowed because they are ignored. */
2429
+ [Comment.directiveName]
30+
31+
if allowsStructuredMarkup {
32+
allowedDirectives += DirectiveIndex.shared.renderableDirectives.values.map {
33+
return $0.directiveName
34+
}
35+
}
36+
self.allowedDirectives = allowedDirectives
37+
2538
self.allowsMarkup = allowsMarkup
2639
}
2740

0 commit comments

Comments
 (0)