Skip to content

Commit 6ed76cc

Browse files
authored
Added support for rendering thematic breaks with unit tests (#865)
Markdown supports thematic breaks (aka horizontal rules) that are represented by placing three or more hyphens, asterisks, or underscores on a separate line, surrounded by blank lines. This PR adds support for thematic breaks within Swift DocC, adding a thematicBreak element in the render JSON. [Minor fix]: also fixed a consistent typo in RenderContentCompilerTests.
1 parent 851b40f commit 6ed76cc

File tree

8 files changed

+210
-16
lines changed

8 files changed

+210
-16
lines changed

Sources/SwiftDocC/Indexing/RenderBlockContent+TextIndexing.swift

Lines changed: 3 additions & 1 deletion
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 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -77,6 +77,8 @@ extension RenderBlockContent: TextIndexing {
7777
.joined(separator: " ")
7878
case .video(let video):
7979
return video.metadata?.rawIndexableTextContent(references: references) ?? ""
80+
case .thematicBreak:
81+
return ""
8082
default:
8183
fatalError("unknown RenderBlockContent case in rawIndexableTextContent")
8284
}

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

Lines changed: 11 additions & 3 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 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -76,7 +76,10 @@ public enum RenderBlockContent: Equatable {
7676

7777
/// A video with an optional caption.
7878
case video(Video)
79-
79+
80+
/// An authored thematic break between block elements.
81+
case thematicBreak
82+
8083
// Warning: If you add a new case to this enum, make sure to handle it in the Codable
8184
// conformance at the bottom of this file, and in the `rawIndexableTextContent` method in
8285
// RenderBlockContent+TextIndexing.swift!
@@ -776,11 +779,13 @@ extension RenderBlockContent: Codable {
776779
metadata: container.decodeIfPresent(RenderContentMetadata.self, forKey: .metadata)
777780
)
778781
)
782+
case .thematicBreak:
783+
self = .thematicBreak
779784
}
780785
}
781786

782787
private enum BlockType: String, Codable {
783-
case paragraph, aside, codeListing, heading, orderedList, unorderedList, step, endpointExample, dictionaryExample, table, termList, row, small, tabNavigator, links, video
788+
case paragraph, aside, codeListing, heading, orderedList, unorderedList, step, endpointExample, dictionaryExample, table, termList, row, small, tabNavigator, links, video, thematicBreak
784789
}
785790

786791
private var type: BlockType {
@@ -801,6 +806,7 @@ extension RenderBlockContent: Codable {
801806
case .tabNavigator: return .tabNavigator
802807
case .links: return .links
803808
case .video: return .video
809+
case .thematicBreak: return .thematicBreak
804810
default: fatalError("unknown RenderBlockContent case in type property")
805811
}
806812
}
@@ -862,6 +868,8 @@ extension RenderBlockContent: Codable {
862868
case .video(let video):
863869
try container.encode(video.identifier, forKey: .identifier)
864870
try container.encodeIfPresent(video.metadata, forKey: .metadata)
871+
case .thematicBreak:
872+
break
865873
default:
866874
fatalError("unknown RenderBlockContent case in encode method")
867875
}

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 5 additions & 1 deletion
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-2023 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -376,6 +376,10 @@ struct RenderContentCompiler: MarkupVisitor {
376376
content: content
377377
))]
378378
}
379+
380+
mutating func visitThematicBreak(_ thematicBreak: ThematicBreak) -> [RenderContent] {
381+
return [RenderBlockContent.thematicBreak]
382+
}
379383

380384
func defaultVisit(_ markup: Markup) -> [RenderContent] {
381385
return []

Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift

Lines changed: 5 additions & 1 deletion
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-2023 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 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
@@ -171,6 +171,10 @@ struct MarkupReferenceResolver: MarkupRewriter {
171171

172172
return symbolLink
173173
}
174+
175+
mutating func visitThematicBreak(_ thematicBreak: ThematicBreak) -> Markup? {
176+
return thematicBreak
177+
}
174178

175179
mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> Markup? {
176180
switch blockDirective.name {

Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"openapi": "3.0.0",
33
"info": {
44
"description": "Render Node API",
5-
"version": "0.4.0",
5+
"version": "0.4.1",
66
"title": "Render Node API"
77
},
88
"paths": { },
@@ -437,6 +437,9 @@
437437
{
438438
"$ref": "#/components/schemas/Video"
439439
},
440+
{
441+
"$ref": "#/components/schemas/ThematicBreak"
442+
},
440443
{
441444
"$ref": "#/components/schemas/Aside"
442445
},
@@ -695,6 +698,20 @@
695698
}
696699
}
697700
},
701+
"ThematicBreak": {
702+
"type": "object",
703+
"required": [
704+
"type",
705+
],
706+
"properties": {
707+
"type": {
708+
"type": "string",
709+
"enum": [
710+
"thematicBreak"
711+
]
712+
}
713+
}
714+
},
698715
"Aside": {
699716
"type": "object",
700717
"required": [

Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3452,4 +3452,27 @@ Document
34523452
])
34533453
}
34543454
}
3455+
3456+
func testThematicBreak() throws {
3457+
let source = """
3458+
3459+
---
3460+
3461+
"""
3462+
3463+
let markup = Document(parsing: source, options: .parseBlockDirectives)
3464+
3465+
XCTAssertEqual(markup.childCount, 1)
3466+
3467+
let (bundle, context) = try testBundleAndContext()
3468+
3469+
var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift))
3470+
3471+
let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent])
3472+
let expectedContent: [RenderBlockContent] = [
3473+
.thematicBreak
3474+
]
3475+
3476+
XCTAssertEqual(expectedContent, renderContent)
3477+
}
34553478
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 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+
13+
@testable import SwiftDocC
14+
import Markdown
15+
import XCTest
16+
17+
class RenderBlockContent_ThematicBreakTests: XCTestCase {
18+
func testThematicBreakCodability() throws {
19+
try assertRoundTripCoding(RenderBlockContent.thematicBreak)
20+
}
21+
22+
func testThematicBreakIndexable() throws {
23+
let thematicBreak = RenderBlockContent.thematicBreak
24+
XCTAssertEqual("", thematicBreak.rawIndexableTextContent(references: [:]))
25+
}
26+
27+
// MARK: - Thematic Break Markdown Variants
28+
func testThematicBreakVariants() throws {
29+
let source = """
30+
31+
---
32+
***
33+
___
34+
35+
"""
36+
37+
let markup = Document(parsing: source, options: .parseBlockDirectives)
38+
39+
XCTAssertEqual(markup.childCount, 3)
40+
41+
let (bundle, context) = try testBundleAndContext()
42+
43+
var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift))
44+
45+
let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent])
46+
let expectedContent: [RenderBlockContent] = [
47+
.thematicBreak,
48+
.thematicBreak,
49+
.thematicBreak
50+
]
51+
52+
XCTAssertEqual(expectedContent, renderContent)
53+
}
54+
55+
func testThematicBreakVariantsWithSpaces() throws {
56+
let source = """
57+
58+
- - -
59+
* * *
60+
_ _ _
61+
62+
"""
63+
64+
let markup = Document(parsing: source, options: .parseBlockDirectives)
65+
66+
XCTAssertEqual(markup.childCount, 3)
67+
68+
let (bundle, context) = try testBundleAndContext()
69+
70+
var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift))
71+
72+
let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent])
73+
let expectedContent: [RenderBlockContent] = [
74+
.thematicBreak,
75+
.thematicBreak,
76+
.thematicBreak
77+
]
78+
79+
XCTAssertEqual(expectedContent, renderContent)
80+
}
81+
82+
func testThematicBreakMoreThanThreeCharacters() throws {
83+
let source = """
84+
85+
----
86+
*****
87+
______
88+
- - - - - -
89+
* * * * *
90+
_ _ _ _ _ _ _ _
91+
92+
"""
93+
94+
let markup = Document(parsing: source, options: .parseBlockDirectives)
95+
96+
XCTAssertEqual(markup.childCount, 6)
97+
98+
let (bundle, context) = try testBundleAndContext()
99+
100+
var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift))
101+
102+
let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent])
103+
let expectedContent: [RenderBlockContent] = [
104+
.thematicBreak, .thematicBreak, .thematicBreak, .thematicBreak, .thematicBreak, .thematicBreak
105+
]
106+
107+
XCTAssertEqual(expectedContent, renderContent)
108+
}
109+
}

0 commit comments

Comments
 (0)