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 181 182 183 184 185
|
/*
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 ArgumentParser
import SwiftDocC
import Foundation
extension Docc {
/// Merge a list of documentation archives into a combined archive.
public struct Merge: AsyncParsableCommand {
public init() {}
public static var configuration = CommandConfiguration(
abstract: "Merge a list of documentation archives into a combined archive.",
usage: "docc merge <archive-path> ... [<synthesized-landing-page-options>] [--output-path <output-path>]"
)
private static let archivePathExtension = "doccarchive"
private static let catalogPathExtension = "docc"
// The file manager used to validate the input and output directories.
//
// Provided as a static variable to allow for using a different file manager in unit tests.
static var _fileManager: FileManagerProtocol = FileManager.default
// Note:
// The order of the option groups in this file is reflected in the 'docc merge --help' output.
// MARK: - Inputs & outputs
@OptionGroup(title: "Inputs & outputs")
var inputsAndOutputs: InputAndOutputOptions
struct InputAndOutputOptions: ParsableArguments {
@Argument(
help: ArgumentHelp(
"A list of paths to '.\(Merge.archivePathExtension)' documentation archive directories to combine into a combined archive.",
valueName: "archive-path"),
transform: URL.init(fileURLWithPath:))
var archives: [URL]
@Option(
help: ArgumentHelp(
"Path to a '.\(Merge.catalogPathExtension)' documentation catalog directory with content for the landing page.",
discussion: """
The documentation compiler uses this catalog content to create a landing page, and optionally additional top-level articles, for the combined archive.
Because the documentation compiler won't synthesize any landing page content, also passing a `--synthesized-landing-page-name` value has no effect.
""",
valueName: "catalog-path",
visibility: .hidden),
transform: URL.init(fileURLWithPath:))
var landingPageCatalog: URL?
@Option(
name: [.customLong("output-path"), .customShort("o")],
help: "The location where the documentation compiler writes the combined documentation archive.",
transform: URL.init(fileURLWithPath:)
)
var providedOutputURL: URL?
var outputURL: URL!
mutating func validate() throws {
let fileManager = Docc.Merge._fileManager
guard !archives.isEmpty else {
throw ValidationError("Require at least one documentation archive to merge.")
}
// Validate that the input archives exists and have the expected path extension
for archive in archives {
switch archive.pathExtension.lowercased() {
case Merge.archivePathExtension:
break // The expected path extension
case "":
throw ValidationError("Missing '\(Merge.archivePathExtension)' path extension for archive '\(archive.path)'")
default:
throw ValidationError("Path extension '\(archive.pathExtension)' is not '\(Merge.archivePathExtension)' for archive '\(archive.path)'")
}
guard fileManager.directoryExists(atPath: archive.path) else {
throw ValidationError("No directory exists at '\(archive.path)'")
}
}
// Validate that the input catalog exist and have the expected path extension
if let catalog = landingPageCatalog {
switch catalog.pathExtension.lowercased() {
case Merge.catalogPathExtension:
break // The expected path extension
case "":
throw ValidationError("Missing '\(Merge.catalogPathExtension)' path extension for catalog '\(catalog.path)'")
default:
throw ValidationError("Path extension '\(catalog.pathExtension)' is not '\(Merge.catalogPathExtension)' for catalog '\(catalog.path)'")
}
guard fileManager.directoryExists(atPath: catalog.path) else {
throw ValidationError("No directory exists at '\(catalog.path)'")
}
print("note: Using a custom landing page catalog isn't supported yet. Will synthesize a default landing page instead.")
}
// Validate that the directory above the output location exist so that the merge command doesn't need to create intermediate directories.
if let outputParent = providedOutputURL?.deletingLastPathComponent() {
// Verify that the intermediate directories exist for the output location.
guard fileManager.directoryExists(atPath: outputParent.path) else {
throw ValidationError("Missing intermediate directory at '\(outputParent.path)' for output path")
}
}
outputURL = providedOutputURL ?? URL(fileURLWithPath: fileManager.currentDirectoryPath).appendingPathComponent("Combined.\(Merge.archivePathExtension)", isDirectory: true)
}
}
@OptionGroup(title: "Synthesized landing page options")
var synthesizedLandingPageOptions: SynthesizedLandingPageOptions
struct SynthesizedLandingPageOptions: ParsableArguments {
@Option(
name: .customLong("synthesized-landing-page-name"),
help: ArgumentHelp(
"A display name for the combined archive's synthesized landing page.",
valueName: "name"
)
)
var name: String = "Documentation"
@Option(
name: .customLong("synthesized-landing-page-kind"),
help: ArgumentHelp(
"A page kind that displays as a title heading for the combined archive's synthesized landing page.",
valueName: "kind"
)
)
var kind: String = "Package"
@Option(
name: .customLong("synthesized-landing-page-topics-style"),
help: ArgumentHelp(
"The visual style of the topic section for the combined archive's synthesized landing page.",
valueName: "style"
)
)
var topicStyle: TopicsVisualStyle.Style = .detailedGrid
}
public var archives: [URL] {
get { inputsAndOutputs.archives }
set { inputsAndOutputs.archives = newValue}
}
public var landingPageCatalog: URL? {
get { inputsAndOutputs.landingPageCatalog }
set { inputsAndOutputs.landingPageCatalog = newValue}
}
public var outputURL: URL {
inputsAndOutputs.outputURL
}
public var synthesizedLandingPageName: String {
synthesizedLandingPageOptions.name
}
public var synthesizedLandingPageKind: String {
synthesizedLandingPageOptions.kind
}
public var synthesizedLandingPageTopicsStyle: TopicsVisualStyle.Style {
synthesizedLandingPageOptions.topicStyle
}
public func run() async throws {
// Initialize a `ConvertAction` from the current options in the `Convert` command.
var convertAction = MergeAction(
archives: archives,
landingPageInfo: .synthesize(.init(name: synthesizedLandingPageName, kind: synthesizedLandingPageKind, style: synthesizedLandingPageTopicsStyle)),
outputURL: outputURL,
fileManager: Self._fileManager
)
// Perform the conversion and print any warnings or errors found
try await convertAction.performAndHandleResult()
}
}
}
extension TopicsVisualStyle.Style: ExpressibleByArgument {}
|