File: Merge.swift

package info (click to toggle)
swiftlang 6.1.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,791,532 kB
  • sloc: cpp: 9,901,743; ansic: 2,201,431; asm: 1,091,827; python: 308,252; objc: 82,166; f90: 80,126; lisp: 38,358; pascal: 25,559; sh: 20,429; ml: 5,058; perl: 4,745; makefile: 4,484; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (185 lines) | stat: -rw-r--r-- 9,018 bytes parent folder | download
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 {}