Skip to content

Commit 59175af

Browse files
RenderBlockContent.Aside Names (#1361)
* Introduce a new stored property on `RenderBlockContent.Aside` called `name` to stored the name or title of aside blocks. This allows DocC to independently save and control the styling and the name of asides. Previously DocC conflated the style of the aside block (e.g. colors or other styling) with the name of the aside. Although it isn't currently possible to author an aside with a different style and name, this change allows the `Aside` model itself to support that one day. Aside now has four available initializers: * init(style: AsideStyle, content: [RenderBlockContent]) * init(name: String, content: [RenderBlockContent]) * init(style: AsideStyle, name: String, content: [RenderBlockContent]) * init(asideKind: Markdown.Aside.Kind, content: [RenderBlockContent]) Most often DocC will use the fourth initializer to create aside blocks from markdown, via RenderContentCompiler. This initializer, creating an aside from a Swift Markdown Aside.Kind, also contains special logic to determine a capitalized name for the aside that was previously implemented in AsideStyle. Simplify the `RenderBlockContent.AsideStyle` structure to act as if it were an enum of the known styles supported by DocC Render: note, tip, experiment, important, or warning. (Since this is public API, this change retains AsideStyle as a structure with a string raw value.) Also simplify the JSON encoding and decoding of Aside and AsideStyle structures to save and read the style and name in a natural way. * Correct Swift syntax error that cropped up in macOS CI * Improve the documentation comments for the RenderBlockContent.Aside and AsideStyle initializers. * Update Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift Co-authored-by: David Rönnqvist <ronnqvist@apple.com> * Simplify RenderBlockContent.AsideStyle.init(rawValue:) * Update Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift Co-authored-by: David Rönnqvist <ronnqvist@apple.com> * When decoding JSON, AsideStyle#init(from:) calls init(rawValue:) to handle uppercased and other invalid style values. Fix the logic for decoding RenderBlockContent asides, to retain both the name and style separately when they are different in JSON. This is different than the previous behavior, which overrode the style with the name. * Cleanup: Update copywrite dates, remove stray debug print statements and whitespace, and simplify how we express some expected data in a unit test. * Deprecate RenderBlockContent.AsideStyle.displayName * Simplify some test code in SemaToRenderNodeTests.swift and RenderBlockContent+AsideTests.swift * Refactor RenderBlockContent_AsideTests.decodeAsideRenderBlock to return a non-optional Aside by implementing the same logic the #require uses. And remove #require from all the call sites. * Remove use of deprecated AsideStyle.displayName at one call site. * Update Tests/SwiftDocCTests/Rendering/RenderBlockContent+AsideTests.swift Co-authored-by: David Rönnqvist <ronnqvist@apple.com> * Refactor RenderBlockContent_AsideTests.decodeAsideRenderBlock again to avoid throwing an exception manually, and to record the source location when decoding an unexpected block type. --------- Co-authored-by: David Rönnqvist <ronnqvist@apple.com>
1 parent 8b49f1b commit 59175af

File tree

7 files changed

+907
-201
lines changed

7 files changed

+907
-201
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2024 Apple Inc. and the Swift project authors
4+
Copyright (c) 2024-2025 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -68,7 +68,7 @@ extension RenderBlockContent.Paragraph {
6868

6969
extension RenderBlockContent.Aside {
7070
func capitalizingFirstWord() -> RenderBlockContent.Aside {
71-
return .init(style: self.style, content: self.content.capitalizingFirstWord())
71+
return .init(style: self.style, name: self.name, content: self.content.capitalizingFirstWord())
7272
}
7373
}
7474

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

Lines changed: 136 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2025 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -103,17 +103,95 @@ public enum RenderBlockContent: Equatable {
103103
}
104104

105105
/// An aside block.
106-
public struct Aside: Equatable {
106+
public struct Aside: Codable, Equatable {
107+
107108
/// The style of this aside block.
108109
public var style: AsideStyle
109110

111+
/// The name of this aside block.
112+
public var name: String
113+
110114
/// The content inside this aside block.
111115
public var content: [RenderBlockContent]
112116

117+
/// Creates an aside from an aside style and block content.
118+
///
119+
/// The new aside will have a name set to the capitalized style.
120+
///
121+
/// - Parameters:
122+
/// - style: The style of this aside
123+
/// - content: The block content to display in the aside
113124
public init(style: AsideStyle, content: [RenderBlockContent]) {
114125
self.style = style
126+
self.name = style.rawValue.capitalized
115127
self.content = content
116128
}
129+
130+
/// Creates an aside from a name and block content.
131+
///
132+
/// The new aside will have a style set to the lowercased name.
133+
///
134+
/// - Parameters:
135+
/// - name: The name of the aside.
136+
/// - content: The block content to display in the aside
137+
///
138+
/// > Note:
139+
/// > If the lowercased name doesn't match one of the aside styles supported
140+
/// > by DocC Render (one of note, tip, experiment, important, or warning) this will
141+
/// > set the style to be note.
142+
public init(name: String, content: [RenderBlockContent]) {
143+
self.style = .init(rawValue: name)
144+
self.name = name
145+
self.content = content
146+
}
147+
148+
/// Creates an aside from an aside style, name and block content.
149+
///
150+
/// - Parameters:
151+
/// - style: The style of the aside
152+
/// - name: The name of the aside
153+
/// - content: The block content to display in the aside
154+
public init(style: AsideStyle, name: String, content: [RenderBlockContent]) {
155+
self.style = style
156+
self.name = name
157+
self.content = content
158+
}
159+
160+
/// Creates an aside from a Swift Markdown aside kind and block content.
161+
///
162+
/// The new aside will have a name and style based on the display name of the
163+
/// Swift Markdown aside kind.
164+
///
165+
/// - Parameters:
166+
/// - asideKind: The Swift Markdown aside kind
167+
/// - content: The block content to display in the aside
168+
///
169+
/// > Note:
170+
/// > If the Swift Markdown aside kind is unknown, then the new aside will
171+
/// > have a name and style set to the Swift Markdown aside kind,
172+
/// > capitalized if necessary.
173+
public init(asideKind: Markdown.Aside.Kind, content: [RenderBlockContent]) {
174+
let name: String
175+
if let knownDisplayName = Self.knownDisplayNames[asideKind.rawValue.lowercased()] {
176+
name = knownDisplayName
177+
} else if asideKind.rawValue.contains(where: \.isUppercase) {
178+
// Assume the content has specific and intentional capitalization.
179+
name = asideKind.rawValue
180+
} else {
181+
// Avoid an all lower case display name.
182+
name = asideKind.rawValue.capitalized
183+
}
184+
185+
self.init(
186+
style: .init(asideKind: asideKind),
187+
name: name,
188+
content: content
189+
)
190+
}
191+
192+
private static let knownDisplayNames: [String: String] = Dictionary(
193+
uniqueKeysWithValues: Markdown.Aside.Kind.allCases.map { ($0.rawValue.lowercased(), $0.displayName) }
194+
)
117195
}
118196

119197
/// A block of sample code.
@@ -515,10 +593,7 @@ public enum RenderBlockContent: Equatable {
515593

516594
/// A type the describes an aside style.
517595
public struct AsideStyle: Codable, Equatable {
518-
private static let knownDisplayNames: [String: String] = Dictionary(
519-
uniqueKeysWithValues: Markdown.Aside.Kind.allCases.map { ($0.rawValue.lowercased(), $0.displayName) }
520-
)
521-
596+
522597
/// Returns a Boolean value indicating whether two aside styles are equal.
523598
///
524599
/// The comparison uses ``rawValue`` and is case-insensitive.
@@ -529,75 +604,77 @@ public enum RenderBlockContent: Equatable {
529604
public static func ==(lhs: AsideStyle, rhs: AsideStyle) -> Bool {
530605
lhs.rawValue.caseInsensitiveCompare(rhs.rawValue) == .orderedSame
531606
}
532-
607+
533608
/// The underlying raw string value.
534609
public var rawValue: String
535610

536611
/// The heading text to use when rendering this style of aside.
612+
@available(*, deprecated, message: "Use 'Aside.name' instead. This deprecated API will be removed after 6.4 is released.")
537613
public var displayName: String {
538-
if let value = Self.knownDisplayNames[rawValue.lowercased()] {
539-
return value
540-
} else if rawValue.contains(where: \.isUppercase) {
541-
// If any character is upper-cased, assume the content has
542-
// specific casing and return the raw value.
543-
return rawValue
544-
} else {
545-
return rawValue.capitalized
546-
}
614+
return rawValue.capitalized
547615
}
548616

549-
/// The style of aside to use when rendering.
617+
/// Creates an aside style.
618+
///
619+
/// The new aside style's underlying raw string value will be lowercased.
620+
///
621+
/// - Parameters:
622+
/// - rawValue: The underlying raw string value.
550623
///
551-
/// DocC Render currently has five styles of asides: Note, Tip, Experiment, Important, and Warning. Asides
552-
/// of these styles can emit their own style into the output, but other styles need to be rendered as one of
553-
/// these five styles. This property maps aside styles to the render style used in the output.
554-
var renderKind: String {
555-
switch rawValue.lowercased() {
556-
case let lowercasedRawValue
557-
where [
558-
"important",
559-
"warning",
560-
"experiment",
561-
"tip"
562-
].contains(lowercasedRawValue):
563-
return lowercasedRawValue
624+
/// > Note:
625+
/// > If the lowercased raw value doesn't match one of the aside styles supported
626+
/// > by DocC Render (one of note, tip, experiment, important, or warning) the
627+
/// > new aside style's raw value will be set to note.
628+
public init(rawValue: String) {
629+
let lowercased = rawValue.lowercased()
630+
switch lowercased {
631+
case "important", "warning", "experiment", "tip":
632+
self.rawValue = lowercased
564633
default:
565-
return "note"
634+
self.rawValue = "note"
566635
}
567636
}
568637

569-
/// Creates an aside type for the specified aside kind.
570-
/// - Parameter asideKind: The aside kind that provides the display name.
638+
/// Creates an aside style from a Swift Markdown aside kind.
639+
///
640+
/// The new aside style's underlying raw string value will be the
641+
/// markdown aside kind's raw value.
642+
///
643+
/// - Parameters:
644+
/// - rawValue: The Swift Markdown aside kind
645+
///
646+
/// > Note:
647+
/// > If the lowercased raw value doesn't match one of the aside styles supported
648+
/// > by DocC Render (one of note, tip, experiment, important, or warning) the
649+
/// > new aside style's raw value will be set to note.
571650
public init(asideKind: Markdown.Aside.Kind) {
572-
self.rawValue = asideKind.rawValue
573-
}
574-
575-
/// Creates an aside style for the specified raw value.
576-
/// - Parameter rawValue: The heading text to use when rendering this style of aside.
577-
public init(rawValue: String) {
578-
self.rawValue = rawValue
651+
self.init(rawValue: asideKind.rawValue)
579652
}
580-
653+
581654
/// Creates an aside style with the specified display name.
582655
/// - Parameter displayName: The heading text to use when rendering this style of aside.
656+
@available(*, deprecated, renamed: "init(rawValue:)", message: "Use 'init(rawValue:)' instead. This deprecated API will be removed after 6.4 is released.")
583657
public init(displayName: String) {
584-
self.rawValue = Self.knownDisplayNames.first(where: { $0.value == displayName })?.key ?? displayName
658+
self.init(rawValue: displayName)
585659
}
586660

587661
/// Encodes the aside style into the specified encoder.
588662
/// - Parameter encoder: The encoder to write data to.
589663
public func encode(to encoder: any Encoder) throws {
590-
// For backwards compatibility, encode only the display name and
591-
// not a key-value pair.
592664
var container = encoder.singleValueContainer()
593665
try container.encode(rawValue)
594666
}
595-
596-
/// Creates an aside style by decoding the specified decoder.
667+
668+
/// Creates an aside style by decoding from the specified decoder.
597669
/// - Parameter decoder: The decoder to read data from.
670+
///
671+
/// > Note:
672+
/// > If the lowercased raw value doesn't match one of the aside styles supported
673+
/// > by DocC Render (one of note, tip, experiment, important, or warning) the
674+
/// > new aside style's raw value will be set to note.
598675
public init(from decoder: any Decoder) throws {
599676
let container = try decoder.singleValueContainer()
600-
self.rawValue = try container.decode(String.self)
677+
self.init(rawValue: try container.decode(String.self))
601678
}
602679
}
603680

@@ -930,11 +1007,17 @@ extension RenderBlockContent: Codable {
9301007
case .paragraph:
9311008
self = try .paragraph(.init(inlineContent: container.decode([RenderInlineContent].self, forKey: .inlineContent)))
9321009
case .aside:
933-
var style = try container.decode(AsideStyle.self, forKey: .style)
934-
if let displayName = try container.decodeIfPresent(String.self, forKey: .name) {
935-
style = AsideStyle(displayName: displayName)
1010+
let aside: Aside
1011+
let content = try container.decode([RenderBlockContent].self, forKey: .content)
1012+
let style = try container.decode(AsideStyle.self, forKey: .style)
1013+
if let name = try container.decodeIfPresent(String.self, forKey: .name) {
1014+
// Retain both the style and name, if both are present.
1015+
aside = .init(style: style, name: name, content: content)
1016+
} else {
1017+
// Or if the name is not specified, set the name based on the style.
1018+
aside = .init(style: style, content: content)
9361019
}
937-
self = try .aside(.init(style: style, content: container.decode([RenderBlockContent].self, forKey: .content)))
1020+
self = .aside(aside)
9381021
case .codeListing:
9391022
let copy = FeatureFlags.current.isExperimentalCodeBlockAnnotationsEnabled
9401023
let options: CodeBlockOptions?
@@ -1049,8 +1132,8 @@ extension RenderBlockContent: Codable {
10491132
case .paragraph(let p):
10501133
try container.encode(p.inlineContent, forKey: .inlineContent)
10511134
case .aside(let a):
1052-
try container.encode(a.style.renderKind, forKey: .style)
1053-
try container.encode(a.style.displayName, forKey: .name)
1135+
try container.encode(a.style, forKey: .style)
1136+
try container.encode(a.name, forKey: .name)
10541137
try container.encode(a.content, forKey: .content)
10551138
case .codeListing(let l):
10561139
try container.encode(l.syntax, forKey: .syntax)

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ struct RenderContentCompiler: MarkupVisitor {
3636
let aside = Aside(blockQuote)
3737

3838
let newAside = RenderBlockContent.Aside(
39-
style: RenderBlockContent.AsideStyle(asideKind: aside.kind),
39+
asideKind: aside.kind,
4040
content: aside.content.reduce(into: [], { result, child in result.append(contentsOf: visit(child))}) as! [RenderBlockContent]
4141
)
42-
42+
4343
return [RenderBlockContent.aside(newAside.capitalizingFirstWord())]
4444
}
4545

@@ -390,7 +390,7 @@ struct RenderContentCompiler: MarkupVisitor {
390390
}
391391
}
392392
return [RenderBlockContent.aside(.init(
393-
style: .init(asideKind: .note),
393+
asideKind: .note,
394394
content: content
395395
))]
396396
}

0 commit comments

Comments
 (0)