diff --git a/Sources/SwiftDocC/Checker/Checkers/TopicsSectionWithoutSubheading.swift b/Sources/SwiftDocC/Checker/Checkers/TopicsSectionWithoutSubheading.swift deleted file mode 100644 index c5a5c9f50f..0000000000 --- a/Sources/SwiftDocC/Checker/Checkers/TopicsSectionWithoutSubheading.swift +++ /dev/null @@ -1,52 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 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 -import Markdown - -/** - A Topics section should have at least one subheading. - */ -public struct TopicsSectionWithoutSubheading: Checker { - public var problems = [Problem]() - - private var sourceFile: URL? - - /// Creates a new checker that detects Topics sections without subheadings. - /// - /// - Parameter sourceFile: The URL to the documentation file that the checker checks. - public init(sourceFile: URL?) { - self.sourceFile = sourceFile - } - - public mutating func visitDocument(_ document: Document) -> () { - let headings = document.children.compactMap { $0 as? Heading } - for (index, heading) in headings.enumerated() { - guard heading.isTopicsSection, isMissingSubheading(heading, remainingHeadings: headings.dropFirst(index + 1)) else { - continue - } - - let explanation = """ - A Topics section requires at least one topic, represented by a level-3 subheading. A Topics section without topics won’t render any content.” - """ - - let diagnostic = Diagnostic(source: sourceFile, severity: .warning, range: heading.range, identifier: "org.swift.docc.TopicsSectionWithoutSubheading", summary: "Missing required subheading for Topics section.", explanation: explanation) - problems.append(Problem(diagnostic: diagnostic)) - } - } - - private func isMissingSubheading(_ heading: Heading, remainingHeadings: ArraySlice) -> Bool { - if let nextHeading = remainingHeadings.first { - return nextHeading.level <= heading.level - } - - return true - } -} diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index 57a54f541c..9597fe8ab5 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -400,7 +400,6 @@ public class DocumentationContext: DocumentationContextDataProviderDelegate { MissingAbstract(sourceFile: source).any(), NonOverviewHeadingChecker(sourceFile: source).any(), SeeAlsoInTopicsHeadingChecker(sourceFile: source).any(), - TopicsSectionWithoutSubheading(sourceFile: source).any(), ]) checker.visit(document) diagnosticEngine.emit(checker.problems) diff --git a/Sources/SwiftDocC/Model/DocumentationMarkup.swift b/Sources/SwiftDocC/Model/DocumentationMarkup.swift index da47867501..1846b068ca 100644 --- a/Sources/SwiftDocC/Model/DocumentationMarkup.swift +++ b/Sources/SwiftDocC/Model/DocumentationMarkup.swift @@ -247,6 +247,14 @@ struct DocumentationMarkup { topicsFirstTaskGroupIndex = index } } + // The first topic group in a topic section is allowed to be "anonymous", or without + // an H3 heading. We account for this by treating both UnorderedLists and Paragraphs as + // valid children indicating the start of a task group. + else if child is UnorderedList { + topicsFirstTaskGroupIndex = index + } else if child is Paragraph { + topicsFirstTaskGroupIndex = index + } if topicsIndex == nil { topicsIndex = index } diff --git a/Tests/SwiftDocCTests/Checker/Checkers/TopicsSectionWithoutSubheadingTests.swift b/Tests/SwiftDocCTests/Checker/Checkers/TopicsSectionWithoutSubheadingTests.swift deleted file mode 100644 index ae26aacc8c..0000000000 --- a/Tests/SwiftDocCTests/Checker/Checkers/TopicsSectionWithoutSubheadingTests.swift +++ /dev/null @@ -1,104 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 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 XCTest -@testable import SwiftDocC -import Markdown - -class TopicsSectionWithoutSubheadingTests: XCTestCase { - func testEmptyDocument() { - let checker = visitDocument(Document()) - XCTAssertTrue(checker.problems.isEmpty) - } - - func testTopicsSectionHasSubheading() { - let markupSource = """ -# Title - -Testing One - -## Topics - -### Test 2 - -Testing Two - -# Test -""" - let checker = visitSource(markupSource) - XCTAssertTrue(checker.problems.isEmpty) - } - - func testTopicsSectionHasNoSubheading() { - let markupSource = """ -# Title - -Abstract. - -## Topics - -## Information - -### Topic B -""" - - let document = Document(parsing: markupSource) - let checker = visitDocument(document) - XCTAssertEqual(1, checker.problems.count) - - let problem = checker.problems[0] - XCTAssertTrue(problem.possibleSolutions.isEmpty) - - let noSubheadingHeading = document.child(at: 2)! as! Heading - let diagnostic = problem.diagnostic - XCTAssertEqual("org.swift.docc.TopicsSectionWithoutSubheading", diagnostic.identifier) - XCTAssertEqual(noSubheadingHeading.range, diagnostic.range) - } - - func testTopicsSectionIsFinalHeading() { - let markupSource = """ -# Title - -Abstract. - -## User - -## Information - -## Topics -""" - - let document = Document(parsing: markupSource) - let checker = visitDocument(document) - XCTAssertEqual(1, checker.problems.count) - - let problem = checker.problems[0] - XCTAssertTrue(problem.possibleSolutions.isEmpty) - - let noSubheadingHeading = document.child(at: 4)! as! Heading - let diagnostic = problem.diagnostic - XCTAssertEqual("org.swift.docc.TopicsSectionWithoutSubheading", diagnostic.identifier) - XCTAssertEqual(noSubheadingHeading.range, diagnostic.range) - } -} - -extension TopicsSectionWithoutSubheadingTests { - func visitSource(_ source: String) -> TopicsSectionWithoutSubheading { - let document = Document(parsing: source) - return visitDocument(document) - } - - func visitDocument(_ document: Document) -> TopicsSectionWithoutSubheading { - var checker = TopicsSectionWithoutSubheading(sourceFile: nil) - checker.visit(document) - - return checker - } -} diff --git a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift index 139356c14e..ff1c76b6d8 100644 --- a/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift +++ b/Tests/SwiftDocCTests/Model/SemaToRenderNodeTests.swift @@ -3174,4 +3174,45 @@ Document ] ) } + + func testTopicsSectionWithSingleAnonymousTopicGroup() throws { + let (_, bundle, context) = try testBundleAndContext( + copying: "TestBundle", + configureBundle: { url in + try """ + # Article + + Abstract. + + ## Topics + + - ``MyKit/MyProtocol`` + - ``MyKit/MyClass`` + + """.write(to: url.appendingPathComponent("article.md"), atomically: true, encoding: .utf8) + } + ) + + let articleReference = ResolvedTopicReference( + bundleIdentifier: bundle.identifier, + path: "/documentation/Test-Bundle/article", + sourceLanguage: .swift + ) + + let articleNode = try context.entity(with: articleReference) + + var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: articleNode.reference, source: nil) + let articleRenderNode = try XCTUnwrap(translator.visit(articleNode.semantic) as? RenderNode) + + XCTAssertEqual( + articleRenderNode.topicSections.flatMap { taskGroup in + [taskGroup.title] + taskGroup.identifiers + }, + [ + nil, + "doc://org.swift.docc.example/documentation/MyKit/MyProtocol", + "doc://org.swift.docc.example/documentation/MyKit/MyClass", + ] + ) + } }