1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
|
/*
This source file is part of the Swift.org open source project
Copyright (c) 2021-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
@available(*, deprecated, message: "Use 'DocumentationContext.InputProvider' instead. This deprecated API will be removed after 6.2 is released")
extension LocalFileSystemDataProvider: DocumentationWorkspaceDataProvider {
public func bundles(options: BundleDiscoveryOptions) throws -> [DocumentationBundle] {
var bundles = try bundlesInTree(fileSystem, options: options)
guard case .directory(let rootDirectory) = fileSystem else {
preconditionFailure("Expected directory object at path '\(fileSystem.url.absoluteString)'.")
}
// If no bundles were found in the root directory, assume that the directory itself is a bundle.
if bundles.isEmpty && self.allowArbitraryCatalogDirectories {
bundles.append(try createBundle(rootDirectory, rootDirectory.children, options: options))
}
return bundles
}
/// Recursively traverses the file system, searching for documentation bundles.
///
/// - Parameters:
/// - root: The directory in which to search for documentation bundles.
/// - options: Configuration that controls how the provider discovers documentation bundles.
/// - Throws: A ``WorkspaceError`` if one of the found documentation bundle directories is an invalid documentation bundle.
/// - Returns: A list of all the bundles that the provider discovered in the file system.
private func bundlesInTree(_ root: FSNode, options: BundleDiscoveryOptions) throws -> [DocumentationBundle] {
var bundles: [DocumentationBundle] = []
guard case .directory(let rootDirectory) = root else {
preconditionFailure("Expected directory object at path '\(root.url.absoluteString)'.")
}
if DocumentationBundleFileTypes.isDocumentationCatalog(rootDirectory.url) {
bundles.append(try createBundle(rootDirectory, rootDirectory.children, options: options))
} else {
// Recursively descend when the current root directory isn't a documentation bundle.
for child in rootDirectory.children {
if case .directory = child {
try bundles.append(contentsOf: bundlesInTree(child, options: options))
}
}
}
return bundles
}
/// Creates a documentation bundle from the content in a given documentation bundle directory.
/// - Parameters:
/// - directory: The documentation bundle directory.
/// - bundleChildren: The top-level files and directories in the documentation bundle directory.
/// - options: Configuration that controls how the provider discovers documentation bundles.
/// - Throws: A ``WorkspaceError`` if the content is an invalid documentation bundle or
/// a ``DocumentationBundle/PropertyListError`` error if the bundle's Info.plist file is invalid.
/// - Returns: The new documentation bundle.
private func createBundle(_ directory: FSNode.Directory, _ bundleChildren: [FSNode], options: BundleDiscoveryOptions) throws -> DocumentationBundle {
let infoPlistData: Data?
if let infoPlistRef = findInfoPlist(bundleChildren) {
infoPlistData = try contentsOfURL(infoPlistRef.url)
} else {
infoPlistData = nil
}
let info = try DocumentationBundle.Info(
from: infoPlistData,
bundleDiscoveryOptions: options,
derivedDisplayName: directory.url.deletingPathExtension().lastPathComponent
)
let markupFiles = findMarkupFiles(bundleChildren, recursive: true).map { $0.url }
let miscResources = findNonMarkupFiles(bundleChildren, recursive: true).map { $0.url }
let symbolGraphFiles = findSymbolGraphFiles(bundleChildren, recursive: true).map { $0.url } + options.additionalSymbolGraphFiles
let customHeader = findCustomHeader(bundleChildren)?.url
let customFooter = findCustomFooter(bundleChildren)?.url
let themeSettings = findThemeSettings(bundleChildren)?.url
return DocumentationBundle(
info: info,
symbolGraphURLs: symbolGraphFiles,
markupURLs: markupFiles,
miscResourceURLs: miscResources,
customHeader: customHeader,
customFooter: customFooter,
themeSettings: themeSettings
)
}
/// Performs a shallow search for the first Info.plist file in the given list of files and directories.
/// - Parameter bundleChildren: The list of files and directories to check.
/// - Returns: The first Info.plist file, or `nil` if none of the files is an Info.plist file.
private func findInfoPlist(_ bundleChildren: [FSNode]) -> FSNode.File? {
return bundleChildren.firstFile { DocumentationBundleFileTypes.isInfoPlistFile($0.url) }
}
/// Finds all the symbol-graph files in the given list of files and directories.
/// - Parameters:
/// - bundleChildren: The list of files and directories to check.
/// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories.
/// - Returns: A list of all the symbol-graph files.
private func findSymbolGraphFiles(_ bundleChildren: [FSNode], recursive: Bool) -> [FSNode.File] {
return bundleChildren.files(recursive: recursive) { DocumentationBundleFileTypes.isSymbolGraphFile($0.url) }
}
/// Finds all the markup files in the given list of files and directories.
/// - Parameters:
/// - bundleChildren: The list of files and directories to check.
/// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories.
/// - Returns: A list of all the markup files.
private func findMarkupFiles(_ bundleChildren: [FSNode], recursive: Bool) -> [FSNode.File] {
return bundleChildren.files(recursive: recursive) { DocumentationBundleFileTypes.isMarkupFile($0.url) }
}
/// Finds all the non-markup files in the given list of files and directories.
/// - Parameters:
/// - bundleChildren: The list of files and directories to check.
/// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories.
/// - Returns: A list of all the non-markup files.
private func findNonMarkupFiles(_ bundleChildren: [FSNode], recursive: Bool) -> [FSNode.File] {
bundleChildren.files(recursive: recursive) { !DocumentationBundleFileTypes.isMarkupFile($0.url) && !DocumentationBundleFileTypes.isSymbolGraphFile($0.url) }
}
private func findCustomHeader(_ bundleChildren: [FSNode]) -> FSNode.File? {
return bundleChildren.firstFile { DocumentationBundleFileTypes.isCustomHeader($0.url) }
}
private func findCustomFooter(_ bundleChildren: [FSNode]) -> FSNode.File? {
return bundleChildren.firstFile { DocumentationBundleFileTypes.isCustomFooter($0.url) }
}
private func findThemeSettings(_ bundleChildren: [FSNode]) -> FSNode.File? {
return bundleChildren.firstFile { DocumentationBundleFileTypes.isThemeSettingsFile($0.url) }
}
}
@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.
/// - Throws: Any error that the predicate closure raises.
/// - Returns: The first file that matches the predicate.
func firstFile(where predicate: (FSNode.File) throws -> Bool) rethrows -> FSNode.File? {
for case .file(let file) in self where try predicate(file) {
return file
}
return nil
}
/// Returns all the files that match s given predicate.
/// - Parameters:
/// - recursive: If `true`, this function will recursively check the files of all directories in the array. If `false`, it will ignore all directories in the array.
/// - predicate: A closure that takes a file as its argument and returns a Boolean value indicating whether the file should be included in the returned array.
/// - Throws: Any error that the predicate closure raises.
/// - Returns: The first file that matches the predicate.
func files(recursive: Bool, where predicate: (FSNode.File) throws -> Bool) rethrows -> [FSNode.File] {
var matches: [FSNode.File] = []
for node in self {
switch node {
case .directory(let directory):
guard recursive else { break }
try matches.append(contentsOf: directory.children.files(recursive: true, where: predicate))
case .file(let file) where try predicate(file):
matches.append(file)
case .file:
break
}
}
return matches
}
}
|