From cd3f5ba8fbd48781fa90b53be0458ddabd32b565 Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Wed, 4 Dec 2024 16:27:11 -0800 Subject: [PATCH] Revert "Deprecate `FileSystemProvider` and `RenderNodeProvider` (#1108)" (#1116) This reverts commit d82da96f1bebda563c58de90d544b60ba8d95bda. --- .../Navigator/NavigatorIndex+Ext.swift | 1 - .../Indexing/Navigator/NavigatorIndex.swift | 74 ++-------- .../Workspace/FileSystemProvider.swift | 4 +- ...leSystemDataProvider+BundleDiscovery.swift | 1 - .../LocalFileSystemDataProvider.swift | 1 - .../FileManagerProtocol+FilesSequence.swift | 72 ---------- .../Action/Actions/Convert/Indexer.swift | 2 +- .../Action/Actions/IndexAction.swift | 11 +- .../TransformForStaticHostingAction.swift | 3 +- .../IndexAction+CommandInitialization.swift | 8 +- .../ArgumentParsing/Subcommands/Index.swift | 6 +- .../StaticHostableTransformer.swift | 132 ++++++++++++++---- .../ConvertActionTests.swift | 14 +- .../IndexActionTests.swift | 9 +- .../StaticHostableTransformerTests.swift | 8 +- .../Utility/TestFileSystemTests.swift | 22 --- 16 files changed, 147 insertions(+), 221 deletions(-) delete mode 100644 Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift index 2ed5ea9d08..9ca01bf78d 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex+Ext.swift @@ -14,7 +14,6 @@ import Foundation This class provides a simple way to transform a `FileSystemProvider` into a `RenderNodeProvider` to feed an index builder. The data from the disk is fetched and processed in an efficient way to build a navigator index. */ -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public class FileSystemRenderNodeProvider: RenderNodeProvider { /// The internal `FileSystemProvider` reference. diff --git a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift index 503d4e0611..1ec1ddddbf 100644 --- a/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift +++ b/Sources/SwiftDocC/Indexing/Navigator/NavigatorIndex.swift @@ -12,7 +12,6 @@ import Foundation import Crypto /// A protocol to provide data to be indexed. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public protocol RenderNodeProvider { /// Get an instance of `RenderNode` to be processed by the index. /// - Note: Returning `nil` will end the indexing process. @@ -22,6 +21,8 @@ public protocol RenderNodeProvider { func getProblems() -> [Problem] } + + /** A `NavigatorIndex` contains all the necessary information to display the data inside a navigator. The data ranges from the tree to the necessary pieces of information to filter the content and perform actions in a fast way. @@ -478,12 +479,8 @@ extension NavigatorIndex { open class Builder { /// The data provider. - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") public let renderNodeProvider: RenderNodeProvider? - /// The documentation archive to build an index from. - public let archiveURL: URL? - /// The output URL. public let outputURL: URL @@ -557,6 +554,7 @@ extension NavigatorIndex { /// Indicates if the page title should be used instead of the navigator title. private let usePageTitle: Bool + /// Maps the icon render references in the navigator items created by this builder /// to their image references. /// @@ -564,31 +562,18 @@ extension NavigatorIndex { /// for any custom icons used in this navigator index. var iconReferences = [String : ImageReference]() + /// Create a new a builder with the given data provider and output URL. /// - Parameters: - /// - archiveURL: The location of the documentation archive that the builder builds an navigator index for. + /// - renderNodeProvider: The `RenderNode` provider to use. /// - outputURL: The location where the builder will write the the built navigator index. /// - bundleIdentifier: The bundle identifier of the documentation that the builder builds a navigator index for. /// - sortRootChildrenByName: Configure the builder to sort root's children by name. /// - groupByLanguage: Configure the builder to group the entries by language. /// - writePathsOnDisk: Configure the builder to write each navigator item's path components to the location. /// - usePageTitle: Configure the builder to use the "page title" instead of the "navigator title" as the title for each entry. - public init(archiveURL: URL? = nil, outputURL: URL, bundleIdentifier: String, sortRootChildrenByName: Bool = false, groupByLanguage: Bool = false, writePathsOnDisk: Bool = true, usePageTitle: Bool = false) { - self.archiveURL = archiveURL - self.renderNodeProvider = nil - self.outputURL = outputURL - self.bundleIdentifier = bundleIdentifier - self.sortRootChildrenByName = sortRootChildrenByName - self.groupByLanguage = groupByLanguage - self.writePathsOnDisk = writePathsOnDisk - self.usePageTitle = usePageTitle - } - - @available(*, deprecated, renamed: "init(archiveURL:outputURL:bundleIdentifier:sortRootChildrenByName:groupByLanguage:writePathsOnDisk:usePageTitle:)", message: "Use 'init(archiveURL:outputURL:bundleIdentifier:sortRootChildrenByName:groupByLanguage:writePathsOnDisk:usePageTitle:)' instead. This deprecated API will be removed after 6.2 is released") - @_disfavoredOverload public init(renderNodeProvider: RenderNodeProvider? = nil, outputURL: URL, bundleIdentifier: String, sortRootChildrenByName: Bool = false, groupByLanguage: Bool = false, writePathsOnDisk: Bool = true, usePageTitle: Bool = false) { self.renderNodeProvider = renderNodeProvider - self.archiveURL = nil self.outputURL = outputURL self.bundleIdentifier = bundleIdentifier self.sortRootChildrenByName = sortRootChildrenByName @@ -1260,43 +1245,13 @@ extension NavigatorIndex { problems.append(problem) } - - /// Build the index using the render nodes files in the provided documentation archive. - /// - Returns: A list containing all the errors encountered during indexing. - /// - Precondition: Either ``archiveURL`` or ``renderNodeProvider`` is set. + /** + Build the index using the passed instance of `RenderNodeProvider` if available. + - Returns: A list containing all the problems encountered during indexing. + - Note: If a provider is not available, this method would generate a fatal error. + */ public func build() -> [Problem] { - if let archiveURL { - return _build(archiveURL: archiveURL) - } else { - return (self as _DeprecatedRenderNodeProviderAccess)._legacyBuild() - } - } - - // After 6.2 is released, move this into `build()`. - private func _build(archiveURL: URL) -> [Problem] { - setup() - - let dataDirectory = archiveURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName, isDirectory: true) - for file in FileManager.default.recursiveFiles(startingPoint: dataDirectory) where file.pathExtension.lowercased() == "json" { - do { - let data = try Data(contentsOf: file) - let renderNode = try RenderNode.decode(fromJSON: data) - try index(renderNode: renderNode) - } catch { - problems.append(error.problem(source: file, - severity: .warning, - summaryPrefix: "RenderNode indexing process failed")) - } - } - - finalize() - - return problems - } - - @available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released") - fileprivate func _legacyBuild() -> [Problem] { - precondition(renderNodeProvider != nil, "Calling `build()` without an `archiveURL` or `renderNodeProvider` set is not permitted.") + precondition(renderNodeProvider != nil, "Calling build without a renderNodeProvider set is not permitted.") setup() @@ -1319,6 +1274,7 @@ extension NavigatorIndex { return availabilityIDs[Int(availabilityID)] } } + } fileprivate extension Error { @@ -1387,9 +1343,3 @@ enum PathHasher: String { } } } - -private protocol _DeprecatedRenderNodeProviderAccess { - // This private function accesses the deprecated RenderNodeProvider - func _legacyBuild() -> [Problem] -} -extension NavigatorIndex.Builder: _DeprecatedRenderNodeProviderAccess {} diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift index 085343cea6..dc0d5ad148 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/FileSystemProvider.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + 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 @@ -11,14 +11,12 @@ import Foundation /// A type that vends a tree of virtual filesystem objects. -@available(*, deprecated, message: "Use 'FileManagerProtocol.recursiveFiles(startingPoint:)' instead. This deprecated API will be removed after 6.2 is released.") public protocol FileSystemProvider { /// The organization of the files that this provider provides. var fileSystem: FSNode { get } } /// An element in a virtual filesystem. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public enum FSNode { /// A file in a filesystem. case file(File) diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift index 9101f51ec1..7eff159b16 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider+BundleDiscovery.swift @@ -142,7 +142,6 @@ extension LocalFileSystemDataProvider: DocumentationWorkspaceDataProvider { } } -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") fileprivate extension [FSNode] { /// Returns the first file that matches a given predicate. /// - Parameter predicate: A closure that takes a file as its argument and returns a Boolean value indicating whether the file should be returned from this function. diff --git a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift index d0ea8e313b..568c3f9e70 100644 --- a/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift +++ b/Sources/SwiftDocC/Infrastructure/Workspace/LocalFileSystemDataProvider.swift @@ -11,7 +11,6 @@ import Foundation /// A type that provides documentation bundles that it discovers by traversing the local file system. -@available(*, deprecated, message: "This deprecated API will be removed after 6.2 is released.") public struct LocalFileSystemDataProvider: FileSystemProvider { public var identifier: String = UUID().uuidString diff --git a/Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift b/Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift deleted file mode 100644 index 1579dd826b..0000000000 --- a/Sources/SwiftDocC/Utility/FileManagerProtocol+FilesSequence.swift +++ /dev/null @@ -1,72 +0,0 @@ -/* - 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 - -extension FileManagerProtocol { - /// Returns a sequence of all the files in the directory structure from the starting point. - /// - Parameters: - /// - startingPoint: The file or directory that's the top of the directory structure that the file manager traverses. - /// - options: Options for how the file manager enumerates the contents of directories. Defaults to `.skipsHiddenFiles`. - /// - Returns: A sequence of the files in the directory structure. - package func recursiveFiles(startingPoint: URL, options: FileManager.DirectoryEnumerationOptions = .skipsHiddenFiles) -> some Sequence { - IteratorSequence(FilesIterator(fileManager: self, startingPoint: startingPoint, options: options)) - } -} - -/// An iterator that traverses the directory structure and returns the files in breadth-first order. -private struct FilesIterator: IteratorProtocol { - /// The file manager that the iterator uses to traverse the directory structure. - var fileManager: FileManager - var options: Foundation.FileManager.DirectoryEnumerationOptions - - private var foundFiles: [URL] - private var foundDirectories: [URL] - - init(fileManager: FileManager, startingPoint: URL, options: Foundation.FileManager.DirectoryEnumerationOptions) { - self.fileManager = fileManager - self.options = options - - // Check if the starting point is a file or a directory. - if fileManager.directoryExists(atPath: startingPoint.path) { - foundFiles = [] - foundDirectories = [startingPoint] - } else { - foundFiles = [startingPoint] - foundDirectories = [] - } - } - - mutating func next() -> URL? { - // If the iterator has already found some files, return those first - if !foundFiles.isEmpty { - return foundFiles.removeFirst() - } - - // Otherwise, check the next found directory and add its contents - guard !foundDirectories.isEmpty else { - // Traversed the entire directory structure - return nil - } - - let directory = foundDirectories.removeFirst() - guard let (newFiles, newDirectories) = try? fileManager.contentsOfDirectory(at: directory, options: options) else { - // The iterator protocol doesn't have a mechanism for raising errors. If an error occurs we - return nil - } - - foundFiles.append(contentsOf: newFiles) - foundDirectories.append(contentsOf: newDirectories) - - // Iterate again after adding new found files and directories. - // This enables the iterator do recurse multiple layers of directories until it finds a file. - return next() - } -} diff --git a/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift b/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift index 1097395d89..c8be4857f3 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/Convert/Indexer.swift @@ -36,7 +36,7 @@ extension ConvertAction { init(outputURL: URL, bundleID: DocumentationBundle.Identifier) throws { let indexURL = outputURL.appendingPathComponent("index", isDirectory: true) indexBuilder = Synchronized( - NavigatorIndex.Builder( + NavigatorIndex.Builder(renderNodeProvider: nil, outputURL: indexURL, bundleIdentifier: bundleID.rawValue, sortRootChildrenByName: true, diff --git a/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift index b1cd729aac..4062acc4ce 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/IndexAction.swift @@ -13,21 +13,21 @@ import SwiftDocC /// An action that creates an index of a documentation bundle. public struct IndexAction: AsyncAction { - let archiveURL: URL + let rootURL: URL let outputURL: URL let bundleIdentifier: String var diagnosticEngine: DiagnosticEngine /// Initializes the action with the given validated options, creates or uses the given action workspace & context. - public init(archiveURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) { + public init(documentationBundleURL: URL, outputURL: URL, bundleIdentifier: String, diagnosticEngine: DiagnosticEngine = .init()) throws { // Initialize the action context. - self.archiveURL = archiveURL + self.rootURL = documentationBundleURL self.outputURL = outputURL self.bundleIdentifier = bundleIdentifier self.diagnosticEngine = diagnosticEngine - self.diagnosticEngine.add(DiagnosticConsoleWriter(formattingOptions: [], baseURL: archiveURL)) + self.diagnosticEngine.add(DiagnosticConsoleWriter(formattingOptions: [], baseURL: documentationBundleURL)) } /// Converts each eligible file from the source documentation bundle, @@ -40,7 +40,8 @@ public struct IndexAction: AsyncAction { } private func buildIndex() throws -> [Problem] { - let indexBuilder = NavigatorIndex.Builder(archiveURL: archiveURL, + let dataProvider = try LocalFileSystemDataProvider(rootURL: rootURL) + let indexBuilder = NavigatorIndex.Builder(renderNodeProvider: FileSystemRenderNodeProvider(fileSystemProvider: dataProvider), outputURL: outputURL, bundleIdentifier: bundleIdentifier, sortRootChildrenByName: true, diff --git a/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift b/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift index cb68f0cb56..68628831fe 100644 --- a/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift +++ b/Sources/SwiftDocCUtilities/Action/Actions/TransformForStaticHostingAction.swift @@ -92,7 +92,8 @@ struct TransformForStaticHostingAction: AsyncAction { ) // Create a StaticHostableTransformer targeted at the archive data folder - let transformer = StaticHostableTransformer(dataDirectory: rootURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName), fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) + let dataProvider = try LocalFileSystemDataProvider(rootURL: rootURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName)) + let transformer = StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) try transformer.transform() } diff --git a/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift b/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift index 1cdbb8f6f3..4d3ba70d3d 100644 --- a/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift +++ b/Sources/SwiftDocCUtilities/ArgumentParsing/ActionExtensions/IndexAction+CommandInitialization.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2021-2024 Apple Inc. and the Swift project authors + 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 @@ -12,10 +12,10 @@ import Foundation extension IndexAction { /// Initializes ``IndexAction`` from the options in the ``Index`` command. - init(fromIndexCommand index: Docc.Index) { + init(fromIndexCommand index: Docc.Index) throws { // Initialize the `IndexAction` from the options provided by the `Index` command - self.init( - archiveURL: index.documentationArchive.urlOrFallback, + try self.init( + documentationBundleURL: index.documentationBundle.urlOrFallback, outputURL: index.outputURL, bundleIdentifier: index.bundleIdentifier) } diff --git a/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift b/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift index bbad44b44f..273486325d 100644 --- a/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift +++ b/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Index.swift @@ -21,7 +21,7 @@ extension Docc { /// The user-provided path to a `.doccarchive` documentation archive. @OptionGroup() - public var documentationArchive: DocCArchiveOption + public var documentationBundle: DocCArchiveOption /// The user-provided bundle name to use for the produced index. @Option(help: "The bundle name for the index.") @@ -33,11 +33,11 @@ extension Docc { /// The path to the directory that all build output should be placed in. public var outputURL: URL { - documentationArchive.urlOrFallback.appendingPathComponent("index", isDirectory: true) + documentationBundle.urlOrFallback.appendingPathComponent("index", isDirectory: true) } public func run() async throws { - let indexAction = IndexAction(fromIndexCommand: self) + let indexAction = try IndexAction(fromIndexCommand: self) try await indexAction.performAndHandleResult() } } diff --git a/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift b/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift index ed3a97e466..fcb6ac1de9 100644 --- a/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift +++ b/Sources/SwiftDocCUtilities/Transformers/StaticHostableTransformer.swift @@ -32,50 +32,122 @@ enum StaticHostableTransformerError: DescribedError { /// Navigates the contents of a FileSystemProvider pointing at the data folder of a `.doccarchive` to emit a static hostable website. struct StaticHostableTransformer { - /// The data directory to create static hostable files for. - private let dataDirectory: URL - /// The directory to write the static hostable files in. + + /// The internal `FileSystemProvider` reference. + /// This should be the data folder of an archive. + private let dataProvider: FileSystemProvider + + /// Where the output will be written. private let outputURL: URL - /// The index.html contents to write for each static hostable file. + + /// The index.html file to be used. private let indexHTMLData: Data - /// The file manager used to create directories and files. + private let fileManager: FileManagerProtocol - /// Initialize with a dataProvider to the source doccarchive. + /// Initialise with a dataProvider to the source doccarchive. /// - Parameters: - /// - dataDirectory: The data directory to create static hostable files for. - /// - fileManager: The file manager used to create directories and files. - /// - outputURL: The output directory where the transformer will write the static hostable files in. - /// - indexHTMLData: Data representing the index.html content that the static - init(dataDirectory: URL, fileManager: FileManagerProtocol, outputURL: URL, indexHTMLData: Data) { - self.dataDirectory = dataDirectory.standardizedFileURL + /// - dataProvider: Should point to the data folder in a docc archive. + /// - fileManager: The FileManager to use for file processes. + /// - outputURL: The folder where the output will be placed + /// - indexHTMLData: Data representing the index.html to be written in the transformed folder structure. + init(dataProvider: FileSystemProvider, fileManager: FileManagerProtocol, outputURL: URL, indexHTMLData: Data) { + self.dataProvider = dataProvider self.fileManager = fileManager - self.outputURL = outputURL.standardizedFileURL + self.outputURL = outputURL self.indexHTMLData = indexHTMLData } /// Creates a static hostable version of the documentation in the data folder of an archive pointed to by the `dataProvider` func transform() throws { - for file in fileManager.recursiveFiles(startingPoint: dataDirectory) where file.pathExtension.lowercased() == "json" { - // For each "/relative/something.json" file, create a "/relative/something/index.html" file. - - guard let relativeFileURL = file.relative(to: dataDirectory) else { - // Our `URL.relative(to:)` extension only return `nil` if the URLComponents aren't valid. - continue - } - - let outputDirectoryURL = outputURL.appendingPathComponent( - relativeFileURL.deletingPathExtension().path, // A directory with the same base name as the file - isDirectory: true - ) - // Ensure that the intermediate directories exist - if !fileManager.fileExists(atPath: outputDirectoryURL.path) { - try fileManager.createDirectory(at: outputDirectoryURL, withIntermediateDirectories: true, attributes: [:]) + let node = dataProvider.fileSystem + + // We should be starting at the data folder of a .doccarchive. + switch node { + case .directory(let dir): + try transformDirectoryContents(directoryRoot: outputURL, relativeSubPath: "", directoryContents: dir.children) + case .file(let file): + throw StaticHostableTransformerError.dataProviderDoesNotReferenceValidInput(url: file.url) + } + } + + + /// Create a directory at the provided URL + /// + private func createDirectory(url: URL) throws { + if !fileManager.fileExists(atPath: url.path) { + try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: [:]) + } + } + + /// Transforms the contents of a given directory + /// - Parameters: + /// - root: The root output URL + /// - directory: The relative path (to the root) of the directory for which then content will processed. + /// - nodes: The directory contents + /// - Returns: An array of problems that may have occurred during processing + private func transformDirectoryContents(directoryRoot: URL, relativeSubPath: String, directoryContents: [FSNode]) throws { + + for node in directoryContents { + switch node { + case .directory(let dir): + try transformDirectory(directoryRoot: directoryRoot, currentDirectoryNode: dir, directorySubPath: relativeSubPath) + case .file(let file): + let outputURL = directoryRoot.appendingPathComponent(relativeSubPath) + try transformFile(file: file, outputURL: outputURL) } - - try fileManager.createFile(at: outputDirectoryURL.appendingPathComponent("index.html"), contents: indexHTMLData) } + + } + + /// Transform the given directory + /// - Parameters: + /// - root: The root output URL + /// - dir: The FSNode that represents the directory + /// - currentDirectory: The relative path (to the root) of the directory that will contain this directory + private func transformDirectory(directoryRoot: URL, currentDirectoryNode: FSNode.Directory, directorySubPath: String) throws { + + // Create the path for the new directory + var newDirectory = directorySubPath + let newPathComponent = currentDirectoryNode.url.lastPathComponent + + // We need to ensure the new directory component, if not empty, ends with / + if !newDirectory.isEmpty && !newDirectory.hasSuffix("/") { + newDirectory += "/" + } + newDirectory += newPathComponent + + + // Create the HTML output directory + + let htmlOutputURL = directoryRoot.appendingPathComponent(newDirectory) + try createDirectory(url: htmlOutputURL) + + // Process the directory contents + try transformDirectoryContents(directoryRoot: directoryRoot, relativeSubPath: newDirectory, directoryContents: currentDirectoryNode.children) + + } + + /// Transform the given File + /// - Parameters: + /// - file: The FSNode that represents the file + /// - outputURL: The directory the need to be placed in + private func transformFile(file: FSNode.File, outputURL: URL) throws { + + // For JSON files we need to create an associated index.html in a sub-folder of the same name. + guard file.url.pathExtension.lowercased() == "json" else { return } + + let dirURL = file.url.deletingPathExtension() + let newDir = dirURL.lastPathComponent + let newDirURL = outputURL.appendingPathComponent(newDir) + + if !fileManager.fileExists(atPath: newDirURL.path) { + try fileManager.createDirectory(at: newDirURL, withIntermediateDirectories: true, attributes: [:]) + } + + let fileURL = newDirURL.appendingPathComponent("index.html") + try self.indexHTMLData.write(to: fileURL) } } diff --git a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift index 3adc98c49d..c0f1b1d2a5 100644 --- a/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/ConvertActionTests.swift @@ -1946,8 +1946,8 @@ class ConvertActionTests: XCTestCase { // Run just the index command over the built documentation - let indexAction = IndexAction( - archiveURL: targetURL, + let indexAction = try IndexAction( + documentationBundleURL: targetURL, outputURL: indexURL, bundleIdentifier: indexFromConvertAction.bundleIdentifier ) @@ -2257,7 +2257,7 @@ class ConvertActionTests: XCTestCase { let targetDirectory = URL(fileURLWithPath: testDataProvider.currentDirectoryPath) .appendingPathComponent("target", isDirectory: true) - let action = try ConvertAction( + var action = try ConvertAction( documentationBundleURL: bundle.absoluteURL, outOfProcessResolver: nil, analyze: true, @@ -2291,7 +2291,7 @@ class ConvertActionTests: XCTestCase { // TODO: Support TestFileSystem in DiagnosticFileWriter let diagnosticFile = try createTemporaryDirectory().appendingPathComponent("test-diagnostics.json") - let action = try ConvertAction( + var action = try ConvertAction( documentationBundleURL: bundle.absoluteURL, outOfProcessResolver: nil, analyze: true, @@ -2363,7 +2363,7 @@ class ConvertActionTests: XCTestCase { let digestFileURL = targetDirectory .appendingPathComponent("diagnostics.json") - let action = try ConvertAction( + var action = try ConvertAction( documentationBundleURL: bundle.absoluteURL, outOfProcessResolver: nil, analyze: false, @@ -2394,7 +2394,7 @@ class ConvertActionTests: XCTestCase { let targetDirectory = temporaryDirectory.appendingPathComponent("target", isDirectory: true) - let action = try ConvertAction( + var action = try ConvertAction( documentationBundleURL: catalogURL, outOfProcessResolver: nil, analyze: false, @@ -2530,7 +2530,7 @@ class ConvertActionTests: XCTestCase { let targetDirectory = temporaryDirectory.appendingPathComponent("target.doccarchive", isDirectory: true) - let action = try ConvertAction( + var action = try ConvertAction( documentationBundleURL: catalogURL, outOfProcessResolver: nil, analyze: false, diff --git a/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift b/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift index f97644ad31..5bd3303ad0 100644 --- a/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/IndexActionTests.swift @@ -50,8 +50,8 @@ class IndexActionTests: XCTestCase { let engine = DiagnosticEngine(filterLevel: .warning) - let indexAction = IndexAction( - archiveURL: targetBundleURL, + let indexAction = try IndexAction( + documentationBundleURL: targetBundleURL, outputURL: indexURL, bundleIdentifier: bundleIdentifier, diagnosticEngine: engine @@ -61,7 +61,6 @@ class IndexActionTests: XCTestCase { let index = try NavigatorIndex.readNavigatorIndex(url: indexURL) resultIndexDumps.insert(index.navigatorTree.root.dumpTree()) - XCTAssert(engine.problems.isEmpty, "Unexpected problems:\n\(engine.problems.map(\.diagnostic.summary).joined(separator: "\n"))") XCTAssertTrue(engine.problems.isEmpty, "Indexing bundle at \(targetURL) resulted in unexpected issues") } @@ -92,8 +91,8 @@ class IndexActionTests: XCTestCase { let bundleIdentifier = "org.swift.docc.example" let indexURL = targetURL.appendingPathComponent("index") let engine = DiagnosticEngine(filterLevel: .warning) - let indexAction = IndexAction( - archiveURL: targetBundleURL, + let indexAction = try IndexAction( + documentationBundleURL: targetBundleURL, outputURL: indexURL, bundleIdentifier: bundleIdentifier, diagnosticEngine: engine diff --git a/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift b/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift index 53260b4100..edce10b200 100644 --- a/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/StaticHostableTransformerTests.swift @@ -55,7 +55,8 @@ class StaticHostableTransformerTests: StaticHostingBaseTests { let indexHTMLData = try StaticHostableTransformer.indexHTMLData(in: testTemplateURL, with: basePath, fileManager: fileManager) let dataURL = targetBundleURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName) - let transformer = StaticHostableTransformer(dataDirectory: dataURL, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) + let dataProvider = try LocalFileSystemDataProvider(rootURL: dataURL) + let transformer = StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) try transformer.transform() @@ -143,6 +144,7 @@ class StaticHostableTransformerTests: StaticHostingBaseTests { _ = try await action.perform(logHandle: .none) let dataURL = targetBundleURL.appendingPathComponent(NodeURLGenerator.Path.dataFolderName) + let dataProvider = try LocalFileSystemDataProvider(rootURL: dataURL) let testTemplateURL = try createTemporaryDirectory().appendingPathComponent("testTemplate") try Folder.testHTMLTemplateDirectory.write(to: testTemplateURL) @@ -158,10 +160,10 @@ class StaticHostableTransformerTests: StaticHostingBaseTests { let fileManager = FileManager.default for (basePath, testValue) in basePaths { - let outputURL = try createTemporaryDirectory().appendingPathComponent("output/") + let outputURL = try createTemporaryDirectory().appendingPathComponent("output") let indexHTMLData = try StaticHostableTransformer.indexHTMLData(in: testTemplateURL, with: basePath, fileManager: FileManager.default) - let transformer = StaticHostableTransformer(dataDirectory: dataURL, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) + let transformer = StaticHostableTransformer(dataProvider: dataProvider, fileManager: fileManager, outputURL: outputURL, indexHTMLData: indexHTMLData) try transformer.transform() diff --git a/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift b/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift index 0108e846ef..af4318698c 100644 --- a/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift +++ b/Tests/SwiftDocCUtilitiesTests/Utility/TestFileSystemTests.swift @@ -133,18 +133,6 @@ class TestFileSystemTests: XCTestCase { │ ╰─ myfile2.txt ╰─ tmp/ """) - - let filesIterator = fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")) - XCTAssertEqual(filesIterator.prefix(2).map(\.path).sorted(), [ - // Shallow files first - "/copy/myfile1.txt", - "/copy/myfile2.txt", - ]) - XCTAssertEqual(filesIterator.dropFirst(2).map(\.path).sorted(), [ - // Deeper files after - "/main/nested/myfile1.txt", - "/main/nested/myfile2.txt", - ]) } @@ -187,10 +175,6 @@ class TestFileSystemTests: XCTestCase { │ ╰─ myfile2.txt ╰─ tmp/ """) - - XCTAssertEqual(fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")).map(\.lastPathComponent), [ - "myfile2.txt", - ]) } func testRemoveFolders() throws { @@ -255,10 +239,6 @@ class TestFileSystemTests: XCTestCase { │ ╰─ myfile2.txt ╰─ tmp/ """) - - XCTAssertEqual(fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")).map(\.lastPathComponent).sorted(), [ - "myfile1.txt", "myfile2.txt", - ]) } func testCreateDeeplyNestedDirectory() throws { @@ -277,8 +257,6 @@ class TestFileSystemTests: XCTestCase { │ ╰─ six/ ╰─ tmp/ """) - - XCTAssertEqual(fs.recursiveFiles(startingPoint: URL(fileURLWithPath: "/")).map(\.lastPathComponent), [], "Only directories. No files.") } func testFileExists() throws {