diff --git a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift index 03d7b834c..d000cb091 100644 --- a/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift +++ b/Sources/SwiftDocC/Infrastructure/DocumentationContext.swift @@ -829,7 +829,13 @@ public class DocumentationContext { insertLandmarks(tutorialArticle.landmarks, from: topicGraphNode, source: url) } else if let article = analyzed as? Article { - + // If the article contains any `@SupportedLanguage` directives in the metadata, + // include those languages in the set of source languages for the reference. + let reference = if let supportedLanguages = article.supportedLanguages { + reference.withSourceLanguages(supportedLanguages) + } else { + reference + } // Here we create a topic graph node with the prepared data but we don't add it to the topic graph just yet // because we don't know where in the hierarchy the article belongs, we will add it later when crawling the manual curation via Topics task groups. let topicGraphNode = TopicGraph.Node(reference: reference, kind: .article, source: .file(url: url), title: article.title!.plainText) @@ -1883,17 +1889,7 @@ public class DocumentationContext { let path = NodeURLGenerator.pathForSemantic(article.value, source: article.source, bundle: bundle) // Use the languages specified by the `@SupportedLanguage` directives if present. - let availableSourceLanguages = article.value - .metadata - .flatMap { metadata in - let languages = Set( - metadata.supportedLanguages - .map(\.language) - ) - - return languages.isEmpty ? nil : languages - } - ?? availableSourceLanguages + let availableSourceLanguages = article.value.supportedLanguages ?? availableSourceLanguages // If available source languages are provided and it contains Swift, use Swift as the default language of // the article. diff --git a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift index 00cd12c11..5bf02e416 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderNodeTranslator.swift @@ -630,7 +630,7 @@ public struct RenderNodeTranslator: SemanticVisitor { node.hierarchyVariants = hierarchyVariants // Emit variants only if we're not compiling an article-only catalog to prevent renderers from - // advertising the page as "Swift", which is the language DocC assigns to pages in article only pages. + // advertising the page as "Swift", which is the language DocC assigns to pages in article only catalogs. // (github.com/swiftlang/swift-docc/issues/240). if let topLevelModule = context.soleRootModuleReference, try! context.entity(with: topLevelModule).kind.isSymbol diff --git a/Sources/SwiftDocC/Semantics/Article/Article.swift b/Sources/SwiftDocC/Semantics/Article/Article.swift index aec17835f..42195c74c 100644 --- a/Sources/SwiftDocC/Semantics/Article/Article.swift +++ b/Sources/SwiftDocC/Semantics/Article/Article.swift @@ -66,6 +66,18 @@ public final class Article: Semantic, MarkupConvertible, Abstracted, Redirected, return abstractSection?.paragraph } + /// The list of supported languages for the article, if present. + /// + /// This information is available via `@SupportedLanguage` in the `@Metadata` directive. + public var supportedLanguages: Set? { + guard let metadata = self.metadata else { + return nil + } + + let langs = metadata.supportedLanguages.map(\.language) + return langs.isEmpty ? nil : Set(langs) + } + /// An optional custom deprecation summary for a deprecated symbol. private(set) public var deprecationSummary: MarkupContainer? diff --git a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift index 907c19050..4f172ea2a 100644 --- a/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift +++ b/Sources/SwiftDocC/Semantics/Metadata/Metadata.swift @@ -105,7 +105,7 @@ public final class Metadata: Semantic, AutomaticDirectiveConvertible { func validate(source: URL?, problems: inout [Problem]) -> Bool { // Check that something is configured in the metadata block - if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty && callToAction == nil && availability.isEmpty && pageKind == nil && pageColor == nil && titleHeading == nil && redirects == nil && alternateRepresentations.isEmpty { + if documentationOptions == nil && technologyRoot == nil && displayName == nil && pageImages.isEmpty && customMetadata.isEmpty && callToAction == nil && availability.isEmpty && pageKind == nil && pageColor == nil && supportedLanguages.isEmpty && titleHeading == nil && redirects == nil && alternateRepresentations.isEmpty { let diagnostic = Diagnostic( source: source, severity: .information, diff --git a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift index acc5b9d50..10d0f8ebe 100644 --- a/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift +++ b/Tests/SwiftDocCTests/Infrastructure/DocumentationContext/DocumentationContextTests.swift @@ -5773,6 +5773,44 @@ let expected = """ XCTAssertEqual(solution.replacements.first?.replacement, "") } + func testSupportedLanguageDirectiveInNonRootArticleWithSymbols() async throws { + let catalog = Folder(name: "unit-test.docc", content: [ + TextFile(name: "Root.md", utf8Content: """ + # Root + + @Metadata { + @TechnologyRoot + @SupportedLanguage(objc) + @SupportedLanguage(data) + } + + ## Topics + + - + """), + + TextFile(name: "Article.md", utf8Content: """ + # Article + + @Metadata { + @SupportedLanguage(objc) + @SupportedLanguage(data) + } + """), + ]) + + let (bundle, context) = try await loadBundle(catalog: catalog) + + XCTAssert(context.problems.isEmpty, "Unexpected problems:\n\(context.problems.map(\.diagnostic.summary).joined(separator: "\n"))") + + do { + let reference = ResolvedTopicReference(bundleID: bundle.id, path: "/documentation/unit-test/Article", sourceLanguage: .swift) + let node = try context.entity(with: reference) + + let supportedLanguages = try XCTUnwrap((node.semantic as? Article)?.metadata?.supportedLanguages) + XCTAssertEqual(supportedLanguages.map(\.language), [.objectiveC, .data]) + } + } } func assertEqualDumps(_ lhs: String, _ rhs: String, file: StaticString = #filePath, line: UInt = #line) { diff --git a/Tests/SwiftDocCTests/Semantics/ArticleTests.swift b/Tests/SwiftDocCTests/Semantics/ArticleTests.swift index 18968e24c..84c032f05 100644 --- a/Tests/SwiftDocCTests/Semantics/ArticleTests.swift +++ b/Tests/SwiftDocCTests/Semantics/ArticleTests.swift @@ -249,4 +249,26 @@ class ArticleTests: XCTestCase { XCTAssertNil(semantic.metadata?.pageKind) XCTAssertNil(semantic.metadata?.titleHeading) } + + func testSupportedLanguageDirective() async throws { + let source = """ + # Root + + @Metadata { + @SupportedLanguage(swift) + @SupportedLanguage(objc) + @SupportedLanguage(data) + } + """ + let document = Document(parsing: source, options: [.parseBlockDirectives]) + let (bundle, _) = try await testBundleAndContext() + var problems = [Problem]() + let article = Article(from: document, source: nil, for: bundle, problems: &problems) + + XCTAssert(problems.isEmpty, "Unexpectedly found problems: \(DiagnosticConsoleWriter.formattedDescription(for: problems))") + + XCTAssertNotNil(article) + XCTAssertNotNil(article?.metadata, "Article should have a metadata container since the markup has a @Metadata directive") + XCTAssertEqual(article?.metadata?.supportedLanguages.map(\.language), [.swift, .objectiveC, .data]) + } }