From 505b285990764758a05b96535ea9352f39cd9e2f Mon Sep 17 00:00:00 2001 From: Emily Chen Date: Mon, 25 Mar 2024 15:53:58 +0000 Subject: [PATCH] Added support for rendering thematic breaks with unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typo in RenderContentCompilerTests resolve some PR feedback Update Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift Co-authored-by: David Rönnqvist add tests with multiple variants of thematic breaks Don't enforce unique filenames for documentation extensions (#863) * Allow documentation extensions to have the same name Since documentation extensions' filenames have no impact on the URL of any pages, there's no need to enforce unique filenames for them. DocC currently uses only the filename / last path component of articles to determine if there is a duplicate article. This change excludes doc extensions from that check to allow documentation extensions to have the same filename. rdar://117174884 * Add test to ensure doc extensions can have the same filename Checks that doc extensions with the same filename do not produce a warning and DocC no longer drops the content from one of the files. Update the year in license headers Bump patch version --- .../RenderBlockContent+TextIndexing.swift | 4 +- .../Infrastructure/DocumentationContext.swift | 15 ++- .../Content/RenderBlockContent.swift | 14 ++- .../Rendering/RenderContentCompiler.swift | 6 +- .../Semantics/MarkupReferenceResolver.swift | 6 +- .../Resources/RenderNode.spec.json | 19 ++- .../DocumentationContextTests.swift | 67 ++++++++++- .../Model/SemaToRenderNodeTests.swift | 23 ++++ ...enderBlockContent_ThematicBreakTests.swift | 109 ++++++++++++++++++ .../RenderContentCompilerTests.swift | 45 ++++++-- 10 files changed, 285 insertions(+), 23 deletions(-) create mode 100644 Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift diff --git a/Sources/SwiftDocC/Indexing/RenderBlockContent+TextIndexing.swift b/Sources/SwiftDocC/Indexing/RenderBlockContent+TextIndexing.swift index 9ad594e461..1ab125f3f0 100644 --- a/Sources/SwiftDocC/Indexing/RenderBlockContent+TextIndexing.swift +++ b/Sources/SwiftDocC/Indexing/RenderBlockContent+TextIndexing.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2024 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 @@ -77,6 +77,8 @@ extension RenderBlockContent: TextIndexing { .joined(separator: " ") case .video(let video): return video.metadata?.rawIndexableTextContent(references: references) ?? "" + case .thematicBreak: + return "" default: fatalError("unknown RenderBlockContent case in rawIndexableTextContent") } diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index e2b5c922d5..3af131f06e 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -854,7 +854,11 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { let path = NodeURLGenerator.pathForSemantic(analyzed, source: url, bundle: bundle) let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: path, sourceLanguage: .swift) - if let firstFoundAtURL = references[reference] { + // Since documentation extensions' filenames have no impact on the URL of pages, there is no need to enforce unique filenames for them. + // At this point we consider all articles with an H1 containing link a "documentation extension." + let isDocumentationExtension = (analyzed as? Article)?.title?.child(at: 0) is AnyLink + + if let firstFoundAtURL = references[reference], !isDocumentationExtension { let problem = Problem( diagnostic: Diagnostic( source: url, @@ -874,7 +878,9 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { continue } - references[reference] = url + if !isDocumentationExtension { + references[reference] = url + } /* Add all topic graph nodes up front before resolution starts, because @@ -907,9 +913,8 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { let result = SemanticResult(value: article, source: url, topicGraphNode: topicGraphNode) // Separate articles that look like documentation extension files from other articles, so that the documentation extension files can be matched up with a symbol. - // At this point we consider all articles with an H1 containing link "documentation extension" - some links might not resolve in the final documentation hierarchy - // and we will emit warnings for those later on when we finalize the bundle discovery phase. - if result.value.title?.child(at: 0) is AnyLink { + // Some links might not resolve in the final documentation hierarchy and we will emit warnings for those later on when we finalize the bundle discovery phase. + if isDocumentationExtension { documentationExtensions.append(result) // Warn for an incorrect root page metadata directive. diff --git a/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift b/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift index c93c7be128..d144d9c238 100644 --- a/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift +++ b/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021 Apple Inc. and the Swift project authors + Copyright (c) 2021-2024 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 @@ -76,7 +76,10 @@ public enum RenderBlockContent: Equatable { /// A video with an optional caption. case video(Video) - + + /// An authored thematic break between block elements. + case thematicBreak + // Warning: If you add a new case to this enum, make sure to handle it in the Codable // conformance at the bottom of this file, and in the `rawIndexableTextContent` method in // RenderBlockContent+TextIndexing.swift! @@ -776,11 +779,13 @@ extension RenderBlockContent: Codable { metadata: container.decodeIfPresent(RenderContentMetadata.self, forKey: .metadata) ) ) + case .thematicBreak: + self = .thematicBreak } } private enum BlockType: String, Codable { - case paragraph, aside, codeListing, heading, orderedList, unorderedList, step, endpointExample, dictionaryExample, table, termList, row, small, tabNavigator, links, video + case paragraph, aside, codeListing, heading, orderedList, unorderedList, step, endpointExample, dictionaryExample, table, termList, row, small, tabNavigator, links, video, thematicBreak } private var type: BlockType { @@ -801,6 +806,7 @@ extension RenderBlockContent: Codable { case .tabNavigator: return .tabNavigator case .links: return .links case .video: return .video + case .thematicBreak: return .thematicBreak default: fatalError("unknown RenderBlockContent case in type property") } } @@ -862,6 +868,8 @@ extension RenderBlockContent: Codable { case .video(let video): try container.encode(video.identifier, forKey: .identifier) try container.encodeIfPresent(video.metadata, forKey: .metadata) + case .thematicBreak: + break default: fatalError("unknown RenderBlockContent case in encode method") } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift b/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift index 1a26f01c3a..1973130bae 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2023 Apple Inc. and the Swift project authors + Copyright (c) 2021-2024 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 @@ -376,6 +376,10 @@ struct RenderContentCompiler: MarkupVisitor { content: content ))] } + + mutating func visitThematicBreak(_ thematicBreak: ThematicBreak) -> [RenderContent] { + return [RenderBlockContent.thematicBreak] + } func defaultVisit(_ markup: Markup) -> [RenderContent] { return [] diff --git a/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift b/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift index 1cad08719d..b4be584a01 100644 --- a/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift +++ b/Sources/SwiftDocC/Semantics/MarkupReferenceResolver.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2023 Apple Inc. and the Swift project authors + Copyright (c) 2021-2024 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 @@ -171,6 +171,10 @@ struct MarkupReferenceResolver: MarkupRewriter { return symbolLink } + + mutating func visitThematicBreak(_ thematicBreak: ThematicBreak) -> Markup? { + return thematicBreak + } mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> Markup? { switch blockDirective.name { diff --git a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json index 539a7081cd..2ed9cb1bda 100644 --- a/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json +++ b/Sources/SwiftDocC/SwiftDocC.docc/Resources/RenderNode.spec.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "description": "Render Node API", - "version": "0.4.0", + "version": "0.4.1", "title": "Render Node API" }, "paths": { }, @@ -437,6 +437,9 @@ { "$ref": "#/components/schemas/Video" }, + { + "$ref": "#/components/schemas/ThematicBreak" + }, { "$ref": "#/components/schemas/Aside" }, @@ -695,6 +698,20 @@ } } }, + "ThematicBreak": { + "type": "object", + "required": [ + "type", + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "thematicBreak" + ] + } + } + }, "Aside": { "type": "object", "required": [ diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift index 2a4f1e3b45..929f4c7eae 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -667,6 +667,68 @@ class DocumentationContextTests: XCTestCase { let localizedSummarySecond = try XCTUnwrap(problemWithDuplicateReference[1].diagnostic.summary) XCTAssertEqual(localizedSummarySecond, "Redeclaration of \'overview.md\'; this file will be skipped") } + + func testUsesMultipleDocExtensionFilesWithSameName() throws { + + // Generate 2 different symbols with the same name. + let someSymbol = makeSymbol(name: "MyEnum", identifier: "someEnumSymbol-id", kind: .init(rawValue: "enum"), pathComponents: ["SomeDirectory", "MyEnum"]) + let anotherSymbol = makeSymbol(name: "MyEnum", identifier: "anotherEnumSymbol-id", kind: .init(rawValue: "enum"), pathComponents: ["AnotherDirectory", "MyEnum"]) + let symbols: [SymbolGraph.Symbol] = [someSymbol, anotherSymbol] + + // Create a catalog with doc extension files with the same filename for each symbol. + let tempURL = try createTempFolder(content: [ + Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: makeSymbolGraph( + moduleName: "ModuleName", + symbols: symbols + )), + + Folder(name: "SomeDirectory", content: [ + TextFile(name: "MyEnum.md", utf8Content: + """ + # ``SomeDirectory/MyEnum`` + + A documentation extension for my enum. + """ + ) + ]), + + Folder(name: "AnotherDirectory", content: [ + TextFile(name: "MyEnum.md", utf8Content: + """ + # ``AnotherDirectory/MyEnum`` + + A documentation extension for an unrelated enum. + """ + ) + ]), + + // An unrelated article that happens to have the same filename + TextFile(name: "MyEnum.md", utf8Content: + """ + # MyEnum + + Here is a regular article about MyEnum. + """ + ) + ]) + ]) + + let (_, _, context) = try loadBundle(from: tempURL) + + // Since documentation extensions' filenames have no impact on the URL of pages, we should not see warnings enforcing unique filenames for them. + let problemWithDuplicateReference = context.problems.filter { $0.diagnostic.identifier == "org.swift.docc.DuplicateReference" } + XCTAssertEqual(problemWithDuplicateReference.count, 0) + + // Ensure the content from both documentation extensions was used. + let someEnumNode = try XCTUnwrap(context.documentationCache["someEnumSymbol-id"]) + let someEnumSymbol = try XCTUnwrap(someEnumNode.semantic as? Symbol) + XCTAssertEqual(someEnumSymbol.abstract?.plainText, "A documentation extension for my enum.", "The abstract should be from the symbol's documentation extension.") + + let anotherEnumNode = try XCTUnwrap(context.documentationCache["anotherEnumSymbol-id"]) + let anotherEnumSymbol = try XCTUnwrap(anotherEnumNode.semantic as? Symbol) + XCTAssertEqual(anotherEnumSymbol.abstract?.plainText, "A documentation extension for an unrelated enum.", "The abstract should be from the symbol's documentation extension.") + } func testGraphChecks() throws { let workspace = DocumentationWorkspace() @@ -4457,12 +4519,13 @@ let expected = """ private func makeSymbol( name: String = "SymbolName", identifier: String, - kind: SymbolGraph.Symbol.KindIdentifier + kind: SymbolGraph.Symbol.KindIdentifier, + pathComponents: [String]? = nil ) -> SymbolGraph.Symbol { return SymbolGraph.Symbol( identifier: .init(precise: identifier, interfaceLanguage: SourceLanguage.swift.id), names: .init(title: name, navigator: nil, subHeading: nil, prose: nil), - pathComponents: [name], + pathComponents: pathComponents ?? [name], docComment: nil, accessLevel: .public, kind: .init(parsedIdentifier: kind, displayName: "Kind Display Name"), diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift index d48b825e90..6f9f2f4a91 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift @@ -3452,4 +3452,27 @@ Document ]) } } + + func testThematicBreak() throws { + let source = """ + + --- + + """ + + let markup = Document(parsing: source, options: .parseBlockDirectives) + + XCTAssertEqual(markup.childCount, 1) + + let (bundle, context) = try testBundleAndContext() + + var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift)) + + let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) + let expectedContent: [RenderBlockContent] = [ + .thematicBreak + ] + + XCTAssertEqual(expectedContent, renderContent) + } } diff --git a/Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift b/Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift new file mode 100644 index 0000000000..0cb5d9a819 --- /dev/null +++ b/Tests/SwiftDocCTests/Rendering/RenderBlockContent_ThematicBreakTests.swift @@ -0,0 +1,109 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2024 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 +*/ + +import Foundation + +@testable import SwiftDocC +import Markdown +import XCTest + +class RenderBlockContent_ThematicBreakTests: XCTestCase { + func testThematicBreakCodability() throws { + try assertRoundTripCoding(RenderBlockContent.thematicBreak) + } + + func testThematicBreakIndexable() throws { + let thematicBreak = RenderBlockContent.thematicBreak + XCTAssertEqual("", thematicBreak.rawIndexableTextContent(references: [:])) + } + + // MARK: - Thematic Break Markdown Variants + func testThematicBreakVariants() throws { + let source = """ + + --- + *** + ___ + + """ + + let markup = Document(parsing: source, options: .parseBlockDirectives) + + XCTAssertEqual(markup.childCount, 3) + + let (bundle, context) = try testBundleAndContext() + + var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift)) + + let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) + let expectedContent: [RenderBlockContent] = [ + .thematicBreak, + .thematicBreak, + .thematicBreak + ] + + XCTAssertEqual(expectedContent, renderContent) + } + + func testThematicBreakVariantsWithSpaces() throws { + let source = """ + + - - - + * * * + _ _ _ + + """ + + let markup = Document(parsing: source, options: .parseBlockDirectives) + + XCTAssertEqual(markup.childCount, 3) + + let (bundle, context) = try testBundleAndContext() + + var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift)) + + let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) + let expectedContent: [RenderBlockContent] = [ + .thematicBreak, + .thematicBreak, + .thematicBreak + ] + + XCTAssertEqual(expectedContent, renderContent) + } + + func testThematicBreakMoreThanThreeCharacters() throws { + let source = """ + + ---- + ***** + ______ + - - - - - - + * * * * * + _ _ _ _ _ _ _ _ + + """ + + let markup = Document(parsing: source, options: .parseBlockDirectives) + + XCTAssertEqual(markup.childCount, 6) + + let (bundle, context) = try testBundleAndContext() + + var contentTranslator = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/TestThematicBreak", sourceLanguage: .swift)) + + let renderContent = try XCTUnwrap(markup.children.reduce(into: [], { result, item in result.append(contentsOf: contentTranslator.visit(item))}) as? [RenderBlockContent]) + let expectedContent: [RenderBlockContent] = [ + .thematicBreak, .thematicBreak, .thematicBreak, .thematicBreak, .thematicBreak, .thematicBreak + ] + + XCTAssertEqual(expectedContent, renderContent) + } +} diff --git a/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift b/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift index 1e22135663..5014c65611 100644 --- a/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift +++ b/Tests/SwiftDocCTests/Rendering/RenderContentCompilerTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2023 Apple Inc. and the Swift project authors + Copyright (c) 2023-2024 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 @@ -62,7 +62,7 @@ class RenderContentCompilerTests: XCTestCase { do { guard case let .paragraph(paragraph) = result[0] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let link = RenderInlineContent.reference( @@ -75,7 +75,7 @@ class RenderContentCompilerTests: XCTestCase { } do { guard case let .paragraph(paragraph) = result[1] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let text = RenderInlineContent.text("Custom Title") @@ -83,7 +83,7 @@ class RenderContentCompilerTests: XCTestCase { } do { guard case let .paragraph(paragraph) = result[2] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let link = RenderInlineContent.reference( @@ -95,7 +95,7 @@ class RenderContentCompilerTests: XCTestCase { } do { guard case let .paragraph(paragraph) = result[3] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let link = RenderInlineContent.reference( @@ -111,7 +111,7 @@ class RenderContentCompilerTests: XCTestCase { } do { guard case let .paragraph(paragraph) = result[4] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let text = RenderInlineContent.text("doc:UNRESOVLED") @@ -119,7 +119,7 @@ class RenderContentCompilerTests: XCTestCase { } do { guard case let .paragraph(paragraph) = result[5] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let link = RenderInlineContent.reference( @@ -181,7 +181,7 @@ class RenderContentCompilerTests: XCTestCase { XCTAssertEqual(result.count, 6) do { guard case let .paragraph(paragraph) = result[0] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let text = RenderInlineContent.text("\n") @@ -189,11 +189,38 @@ class RenderContentCompilerTests: XCTestCase { } do { guard case let .paragraph(paragraph) = result[1] as? RenderBlockContent else { - XCTFail("RenderCotent result is not the expected RenderBlockContent.paragraph(Paragraph)") + XCTFail("RenderContent result is not the expected RenderBlockContent.paragraph(Paragraph)") return } let text = RenderInlineContent.text("\n") XCTAssertEqual(paragraph.inlineContent[1], text) } } + + func testThematicBreak() throws { + let (bundle, context) = try testBundleAndContext() + var compiler = RenderContentCompiler(context: context, bundle: bundle, identifier: ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/path", fragment: nil, sourceLanguage: .swift)) + + + let source = #""" + + --- + + """# + let document = Document(parsing: source) + let expectedDump = #""" + Document + └─ ThematicBreak + """# + XCTAssertEqual(document.debugDescription(), expectedDump) + let result = document.children.flatMap { compiler.visit($0) } + XCTAssertEqual(result.count, 1) + do { + let thematicBreak = RenderBlockContent.thematicBreak + + let documentThematicBreak = try XCTUnwrap(result[0] as? RenderBlockContent) + + XCTAssertEqual(documentThematicBreak, thematicBreak) + } + } }