Skip to content

Commit dcaacda

Browse files
committed
Merge branch 'main' into async-context-init
2 parents aadd434 + c7660bb commit dcaacda

File tree

2 files changed

+122
-11
lines changed

2 files changed

+122
-11
lines changed

Sources/SwiftDocC/Infrastructure/DocumentationCurator.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,22 @@ struct DocumentationCurator {
9090
}
9191

9292
// Try extracting an article from the cache
93-
let articleFilename = unresolved.topicURL.components.path.components(separatedBy: "/").last!
94-
let sourceArticlePath = NodeURLGenerator.Path.article(bundleName: bundle.displayName, articleName: articleFilename).stringValue
95-
93+
let sourceArticlePath: String = {
94+
let path = unresolved.topicURL.components.path.removingLeadingSlash
95+
96+
// The article path can either be written as
97+
// - "ArticleName"
98+
// - "CatalogName/ArticleName"
99+
// - "documentation/CatalogName/ArticleName"
100+
switch path.components(separatedBy: "/").count {
101+
case 0,1:
102+
return NodeURLGenerator.Path.article(bundleName: bundle.displayName, articleName: path).stringValue
103+
case 2:
104+
return "\(NodeURLGenerator.Path.documentationFolder)/\(path)"
105+
default:
106+
return path.prependingLeadingSlash
107+
}
108+
}()
96109
let reference = ResolvedTopicReference(
97110
bundleID: resolved.bundleID,
98111
path: sourceArticlePath,
@@ -115,6 +128,7 @@ struct DocumentationCurator {
115128
context.topicGraph.addNode(curatedNode)
116129

117130
// Move the article from the article cache to the documentation
131+
let articleFilename = reference.url.pathComponents.last!
118132
context.linkResolver.localResolver.addArticle(filename: articleFilename, reference: reference, anchorSections: documentationNode.anchorSections)
119133

120134
context.documentationCache[reference] = documentationNode

Tests/SwiftDocCTests/Infrastructure/DocumentationCuratorTests.swift

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class DocumentationCuratorTests: XCTestCase {
3030
func testCrawl() async throws {
3131
let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests")
3232

33-
var crawler = DocumentationCurator.init(in: context, bundle: bundle)
33+
var crawler = DocumentationCurator(in: context, bundle: bundle)
3434
let mykit = try context.entity(with: ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/MyKit", sourceLanguage: .swift))
3535

3636
var symbolsWithCustomCuration = [ResolvedTopicReference]()
@@ -303,7 +303,7 @@ class DocumentationCuratorTests: XCTestCase {
303303
""".write(to: url.appendingPathComponent("Root.md"), atomically: true, encoding: .utf8)
304304
}
305305

306-
let crawler = DocumentationCurator.init(in: context, bundle: bundle)
306+
let crawler = DocumentationCurator(in: context, bundle: bundle)
307307
XCTAssert(context.problems.isEmpty, "Expected no problems. Found: \(context.problems.map(\.diagnostic.summary))")
308308

309309
guard let moduleNode = context.documentationCache["SourceLocations"],
@@ -316,11 +316,109 @@ class DocumentationCuratorTests: XCTestCase {
316316

317317
XCTAssertEqual(root.path, "/documentation/Root")
318318
XCTAssertEqual(crawler.problems.count, 0)
319-
319+
}
320+
321+
func testCuratorDoesNotRelateNodesWhenArticleLinksContainExtraPathComponents() throws {
322+
let (bundle, context) = try loadBundle(catalog:
323+
Folder(name: "CatalogName.docc", content: [
324+
TextFile(name: "Root.md", utf8Content: """
325+
# Root
326+
327+
@Metadata {
328+
@TechnologyRoot
329+
}
330+
331+
Add an API Collection of indirection to more easily detect the failed curation.
332+
333+
## Topics
334+
- <doc:API-Collection>
335+
"""),
336+
337+
TextFile(name: "API-Collection.md", utf8Content: """
338+
# Some API Collection
339+
340+
Fail to curate all 4 articles because of extra incorrect path components.
341+
342+
## Topics
343+
344+
### No links will resolve in this section
345+
346+
- <doc:WrongModuleName/First>
347+
- <doc:documentation/WrongModuleName/Second>
348+
- <doc:documentation/CatalogName/ExtraPathComponent/Third>
349+
- <doc:CatalogName/ExtraPathComponent/Forth>
350+
"""),
351+
352+
TextFile(name: "First.md", utf8Content: "# First"),
353+
TextFile(name: "Second.md", utf8Content: "# Second"),
354+
TextFile(name: "Third.md", utf8Content: "# Third"),
355+
TextFile(name: "Forth.md", utf8Content: "# Forth"),
356+
])
357+
)
358+
let (linkResolutionProblems, otherProblems) = context.problems.categorize(where: { $0.diagnostic.identifier == "org.swift.docc.unresolvedTopicReference" })
359+
XCTAssert(otherProblems.isEmpty, "Unexpected problems: \(otherProblems.map(\.diagnostic.summary).sorted())")
360+
361+
XCTAssertEqual(
362+
linkResolutionProblems.map(\.diagnostic.source?.lastPathComponent),
363+
["API-Collection.md", "API-Collection.md", "API-Collection.md", "API-Collection.md"],
364+
"Every unresolved link is in the API collection"
365+
)
366+
XCTAssertEqual(
367+
linkResolutionProblems.map({ $0.diagnostic.range?.lowerBound.line }), [9, 10, 11, 12],
368+
"There should be one warning about an unresolved reference for each link in the API collection's top"
369+
)
370+
371+
let rootReference = try XCTUnwrap(context.soleRootModuleReference)
372+
373+
for articleName in ["First", "Second", "Third", "Forth"] {
374+
let reference = try XCTUnwrap(context.documentationCache.allReferences.first(where: { $0.lastPathComponent == articleName }))
375+
XCTAssertEqual(
376+
context.topicGraph.nodeWithReference(reference)?.shouldAutoCurateInCanonicalLocation, true,
377+
"Article '\(articleName)' isn't (successfully) manually curated and should therefore automatically curate."
378+
)
379+
XCTAssertEqual(
380+
context.topicGraph.reverseEdges[reference]?.map(\.path), [rootReference.path],
381+
"Article '\(articleName)' should only have a reverse edge to the root page where it will be automatically curated."
382+
)
383+
}
384+
385+
let apiCollectionReference = try XCTUnwrap(context.documentationCache.allReferences.first(where: { $0.lastPathComponent == "API-Collection" }))
386+
let apiCollectionSemantic = try XCTUnwrap(try context.entity(with: apiCollectionReference).semantic as? Article)
387+
XCTAssertEqual(apiCollectionSemantic.topics?.taskGroups.count, 1, "The API Collection has one topic section")
388+
let topicSection = try XCTUnwrap(apiCollectionSemantic.topics?.taskGroups.first)
389+
XCTAssertEqual(topicSection.links.map(\.destination), [
390+
// All these links are the same as they were authored which means that they didn't resolve.
391+
"doc:WrongModuleName/First",
392+
"doc:documentation/WrongModuleName/Second",
393+
"doc:documentation/CatalogName/ExtraPathComponent/Third",
394+
"doc:CatalogName/ExtraPathComponent/Forth",
395+
])
396+
397+
let rootPage = try context.entity(with: rootReference)
398+
let renderer = DocumentationNodeConverter(bundle: bundle, context: context)
399+
let renderNode = renderer.convert(rootPage)
400+
401+
XCTAssertEqual(renderNode.topicSections.map(\.title), [
402+
nil, // An unnamed topic section
403+
"Articles", // The automatic topic section
404+
])
405+
XCTAssertEqual(renderNode.topicSections.map { $0.identifiers.sorted() }, [
406+
// The unnamed topic section curates the API collection
407+
[
408+
"doc://CatalogName/documentation/CatalogName/API-Collection"
409+
],
410+
// The automatic "Articles" section curates all 4 articles
411+
[
412+
"doc://CatalogName/documentation/CatalogName/First",
413+
"doc://CatalogName/documentation/CatalogName/Forth",
414+
"doc://CatalogName/documentation/CatalogName/Second",
415+
"doc://CatalogName/documentation/CatalogName/Third",
416+
],
417+
])
320418
}
321419

322420
func testModuleUnderAncestorOfTechnologyRoot() async throws {
323-
let (_, bundle, context) = try await testBundleAndContext(copying: "SourceLocations") { url in
421+
let (_, _, context) = try await testBundleAndContext(copying: "SourceLocations") { url in
324422
try """
325423
# Root with ancestor curating a module
326424
@@ -347,7 +445,6 @@ class DocumentationCuratorTests: XCTestCase {
347445
""".write(to: url.appendingPathComponent("Ancestor.md"), atomically: true, encoding: .utf8)
348446
}
349447

350-
let _ = DocumentationCurator.init(in: context, bundle: bundle)
351448
XCTAssert(context.problems.isEmpty, "Expected no problems. Found: \(context.problems.map(\.diagnostic.summary))")
352449

353450
guard let moduleNode = context.documentationCache["SourceLocations"],
@@ -364,7 +461,7 @@ class DocumentationCuratorTests: XCTestCase {
364461
func testSymbolLinkResolving() async throws {
365462
let (bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests")
366463

367-
let crawler = DocumentationCurator.init(in: context, bundle: bundle)
464+
let crawler = DocumentationCurator(in: context, bundle: bundle)
368465

369466
// Resolve top-level symbol in module parent
370467
do {
@@ -417,7 +514,7 @@ class DocumentationCuratorTests: XCTestCase {
417514
func testLinkResolving() async throws {
418515
let (sourceRoot, bundle, context) = try await testBundleAndContext(named: "LegacyBundle_DoNotUseInNewTests")
419516

420-
var crawler = DocumentationCurator.init(in: context, bundle: bundle)
517+
var crawler = DocumentationCurator(in: context, bundle: bundle)
421518

422519
// Resolve and curate an article in module root (absolute link)
423520
do {
@@ -510,7 +607,7 @@ class DocumentationCuratorTests: XCTestCase {
510607
""".write(to: root.appendingPathComponent("documentation").appendingPathComponent("api-collection.md"), atomically: true, encoding: .utf8)
511608
}
512609

513-
var crawler = DocumentationCurator.init(in: context, bundle: bundle)
610+
var crawler = DocumentationCurator(in: context, bundle: bundle)
514611
let reference = ResolvedTopicReference(bundleID: "org.swift.docc.example", path: "/documentation/SideKit", sourceLanguage: .swift)
515612

516613
try crawler.crawlChildren(of: reference, prepareForCuration: {_ in }) { (_, _) in }

0 commit comments

Comments
 (0)