Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions Sources/SwiftDocC/Infrastructure/DocumentationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,7 @@ public class DocumentationContext {
let (url, analyzed) = analyzedDocument

let path = NodeURLGenerator.pathForSemantic(analyzed, source: url, bundle: inputs)
let reference = ResolvedTopicReference(bundleID: inputs.id, path: path, sourceLanguage: .swift)
var reference = ResolvedTopicReference(bundleID: inputs.id, path: path, sourceLanguage: .swift)

// 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."
Expand Down Expand Up @@ -811,7 +811,11 @@ 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.
if let supportedLanguages = article.supportedLanguages {
reference = reference.withSourceLanguages(supportedLanguages)
}
// 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)
Expand Down Expand Up @@ -1842,17 +1846,7 @@ public class DocumentationContext {
let path = NodeURLGenerator.pathForSemantic(article.value, source: article.source, bundle: inputs)

// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Sources/SwiftDocC/Semantics/Article/Article.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<SourceLanguage>? {
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?

Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftDocC/Semantics/Metadata/Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
71 changes: 71 additions & 0 deletions Tests/SwiftDocCTests/Indexing/NavigatorIndexTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,77 @@ Root
"""
)
}

// Bug: rdar://160284853
// The supported languages of an article need to be stored in the resolved
// topic reference that eventually gets serialised into the render node,
// which the navigator uses. This must be done when creating the topic
// graph node, rather than updating the set of supported languages when
// registering the article. If a catalog contains more than one module, any
// articles present are not registered in the documentation cache, since it
// is not possible to determine what module it is belongs to. This test
// ensures that in such cases, the supported languages information is
// correctly included in the render node, and that the navigator is built
// correctly.
func testSupportedLanguageDirectiveForStandaloneArticles() async throws {
let catalog = Folder(name: "unit-test.docc", content: [
InfoPlist(identifier: testBundleIdentifier),
TextFile(name: "UnitTest.md", utf8Content: """
# UnitTest

@Metadata {
@TechnologyRoot
@SupportedLanguage(data)
}

## Topics

- <doc:Article>
- ``Foo``
"""),
TextFile(name: "Article.md", utf8Content: """
# Article

Just a random article.
"""),
// The correct way to configure a catalog is to have a single root module. If multiple modules,
// are present, it is not possible to determine which module an article is supposed to be
// registered with. We include multiple modules to prevent registering the articles in the
// documentation cache, to test if the supported languages are attached prior to registration.
JSONFile(name: "Foo.symbols.json", content: makeSymbolGraph(moduleName: "Foo", symbols: [
makeSymbol(id: "some-symbol", language: SourceLanguage.data, kind: .class, pathComponents: ["SomeSymbol"]),
]))
])

let (_, context) = try await loadBundle(catalog: catalog)

let renderContext = RenderContext(documentationContext: context)
let converter = DocumentationContextConverter(context: context, renderContext: renderContext)

let targetURL = try createTemporaryDirectory()
let builder = NavigatorIndex.Builder(outputURL: targetURL, bundleIdentifier: testBundleIdentifier)
builder.setup()

for identifier in context.knownPages {
let entity = try context.entity(with: identifier)
let renderNode = try XCTUnwrap(converter.renderNode(for: entity))
try builder.index(renderNode: renderNode)
}

builder.finalize()

let navigatorIndex = try XCTUnwrap(builder.navigatorIndex)

let expectedNavigator = """
[Root]
┗╸UnitTest
┣╸Article
┗╸Foo
┣╸Classes
┗╸SomeSymbol
"""
XCTAssertEqual(navigatorIndex.navigatorTree.root.dumpTree(), expectedNavigator)
}

func testNavigatorIndexUsingPageTitleGeneration() async throws {
let (_, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5772,6 +5772,48 @@ let expected = """
XCTAssertEqual(solution.replacements.first?.replacement, "")
}

func testSupportedLanguageDirectiveForStandaloneArticles() async throws {
let catalog = Folder(name: "unit-test.docc", content: [
TextFile(name: "Root.md", utf8Content: """
# Root

@Metadata {
@TechnologyRoot
@SupportedLanguage(objc)
@SupportedLanguage(data)
}

## Topics

- <doc:Article>
"""),
TextFile(name: "Article.md", utf8Content: """
# Article

@Metadata {
@SupportedLanguage(objc)
@SupportedLanguage(data)
}
"""),
// The correct way to configure a catalog is to have a single root module. If multiple modules,
// are present, it is not possible to determine which module an article is supposed to be
// registered with. We include multiple modules to prevent registering the articles in the
// documentation cache, to test if the supported languages are attached prior to registration.
JSONFile(name: "Foo.symbols.json", content: makeSymbolGraph(moduleName: "Foo")),
])

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: .data)
// Find the topic graph node for the article
let node = context.topicGraph.nodes.first { $0.key == reference }?.value
// Ensure that the reference within the topic graph node contains the supported languages
XCTAssertEqual(node?.reference.sourceLanguages, [.objectiveC, .data])
}
}
}

func assertEqualDumps(_ lhs: String, _ rhs: String, file: StaticString = #filePath, line: UInt = #line) {
Expand Down
22 changes: 22 additions & 0 deletions Tests/SwiftDocCTests/Semantics/ArticleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
}