feat: Adds generate commands that call to pandoc to generate pdf, latex, and html files from a project.
This commit is contained in:
223
Sources/CliClient/CliClient+PandocCommands.swift
Normal file
223
Sources/CliClient/CliClient+PandocCommands.swift
Normal file
@@ -0,0 +1,223 @@
|
||||
import ConfigurationClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import ShellClient
|
||||
|
||||
// TODO: Need to parse / ensure that header includes and files are in the build directory, not sure
|
||||
// exactly how to handle if they're not, but it seems reasonable to potentially allow files that are
|
||||
// outside of the build directory to be included.
|
||||
|
||||
public extension CliClient {
|
||||
|
||||
@discardableResult
|
||||
func runPandocCommand(
|
||||
_ options: PandocOptions,
|
||||
logging loggingOptions: LoggingOptions
|
||||
) async throws -> String {
|
||||
try await withLogger(loggingOptions) {
|
||||
@Dependency(\.configurationClient) var configurationClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let configuration = try await configurationClient.findAndLoad()
|
||||
logger.trace("Configuration: \(configuration)")
|
||||
|
||||
let ensuredOptions = try await ensurePandocOptions(
|
||||
configuration: configuration,
|
||||
options: options
|
||||
)
|
||||
|
||||
let projectDirectory = options.projectDirectory ?? ProcessInfo.processInfo.environment["PWD"]
|
||||
guard let projectDirectory else {
|
||||
throw CliClientError.generate(.projectDirectoryNotSpecified)
|
||||
}
|
||||
|
||||
let outputDirectory = options.outputDirectory ?? projectDirectory
|
||||
let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)"
|
||||
|
||||
var arguments = [
|
||||
"pandoc"
|
||||
]
|
||||
arguments += ensuredOptions.includeInHeader.map {
|
||||
"--include-in-header=\(projectDirectory)/\(ensuredOptions.buildDirectory)/\($0)"
|
||||
}
|
||||
|
||||
if let pdfEngine = ensuredOptions.pdfEngine {
|
||||
arguments.append("--pdf-engine=\(pdfEngine)")
|
||||
}
|
||||
|
||||
arguments.append("--output=\(outputPath)")
|
||||
arguments += ensuredOptions.files.map {
|
||||
"\(projectDirectory)/\(ensuredOptions.buildDirectory)/\($0)"
|
||||
}
|
||||
|
||||
if options.shouldBuild {
|
||||
logger.trace("Building project...")
|
||||
try await runPlaybookCommand(
|
||||
.init(
|
||||
arguments: [
|
||||
"--tags", "build-project",
|
||||
"--extra-vars", "project_dir=\(projectDirectory)"
|
||||
],
|
||||
configuration: configuration,
|
||||
quiet: options.quiet,
|
||||
shell: options.shell
|
||||
),
|
||||
logging: loggingOptions
|
||||
)
|
||||
}
|
||||
|
||||
logger.trace("Running pandoc with arguments: \(arguments)")
|
||||
|
||||
try await runCommand(
|
||||
quiet: options.quiet,
|
||||
shell: options.shell.orDefault,
|
||||
arguments
|
||||
)
|
||||
|
||||
return outputPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public struct EnsuredPandocOptions: Equatable, Sendable {
|
||||
public let buildDirectory: String
|
||||
public let files: [String]
|
||||
public let includeInHeader: [String]
|
||||
public let outputFileName: String
|
||||
public let outputFileType: CliClient.PandocOptions.FileType
|
||||
public let pdfEngine: String?
|
||||
|
||||
public var ensuredExtensionFileName: String {
|
||||
let extensionString: String
|
||||
|
||||
switch outputFileType {
|
||||
case .html:
|
||||
extensionString = ".html"
|
||||
case .latex:
|
||||
extensionString = ".tex"
|
||||
case .pdf:
|
||||
extensionString = ".pdf"
|
||||
}
|
||||
|
||||
if !outputFileName.hasSuffix(extensionString) {
|
||||
return outputFileName + extensionString
|
||||
}
|
||||
return outputFileName
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public func ensurePandocOptions(
|
||||
configuration: Configuration,
|
||||
options: CliClient.PandocOptions
|
||||
) async throws -> EnsuredPandocOptions {
|
||||
let defaults = Configuration.Generate.default
|
||||
let pdfEngine = parsePdfEngine(configuration.generate, defaults, options)
|
||||
|
||||
return .init(
|
||||
buildDirectory: parseBuildDirectory(configuration.generate, defaults, options),
|
||||
files: parseFiles(configuration.generate, defaults, options),
|
||||
includeInHeader: parseIncludeInHeader(configuration.generate, defaults, options),
|
||||
outputFileName: parseOutputFileName(configuration.generate, defaults, options),
|
||||
outputFileType: options.outputFileType,
|
||||
pdfEngine: pdfEngine
|
||||
)
|
||||
}
|
||||
|
||||
private func parsePdfEngine(
|
||||
_ configuration: Configuration.Generate?,
|
||||
_ defaults: Configuration.Generate,
|
||||
_ options: CliClient.PandocOptions
|
||||
) -> String? {
|
||||
switch options.outputFileType {
|
||||
case .html, .latex:
|
||||
return nil
|
||||
case let .pdf(engine: engine):
|
||||
if let engine {
|
||||
return engine
|
||||
} else if let engine = configuration?.pdfEngine {
|
||||
return engine
|
||||
} else if let engine = defaults.pdfEngine {
|
||||
return engine
|
||||
} else {
|
||||
return "xelatex"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func parseFiles(
|
||||
_ configuration: Configuration.Generate?,
|
||||
_ defaults: Configuration.Generate,
|
||||
_ options: CliClient.PandocOptions
|
||||
) -> [String] {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
if let files = options.files {
|
||||
return files
|
||||
} else if let files = configuration?.files {
|
||||
return files
|
||||
} else if let files = defaults.files {
|
||||
return files
|
||||
} else {
|
||||
logger.warning("Files not specified, this could lead to errors.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func parseIncludeInHeader(
|
||||
_ configuration: Configuration.Generate?,
|
||||
_ defaults: Configuration.Generate,
|
||||
_ options: CliClient.PandocOptions
|
||||
) -> [String] {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
if let files = options.includeInHeader {
|
||||
return files
|
||||
} else if let files = configuration?.includeInHeader {
|
||||
return files
|
||||
} else if let files = defaults.includeInHeader {
|
||||
return files
|
||||
} else {
|
||||
logger.warning("Include in header files not specified, this could lead to errors.")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private func parseOutputFileName(
|
||||
_ configuration: Configuration.Generate?,
|
||||
_ defaults: Configuration.Generate,
|
||||
_ options: CliClient.PandocOptions
|
||||
) -> String {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
if let output = options.outputFileName {
|
||||
return output
|
||||
} else if let output = configuration?.outputFileName {
|
||||
return output
|
||||
} else if let output = defaults.outputFileName {
|
||||
return output
|
||||
} else {
|
||||
logger.warning("Output file name not specified, this could lead to errors.")
|
||||
return "Report"
|
||||
}
|
||||
}
|
||||
|
||||
private func parseBuildDirectory(
|
||||
_ configuration: Configuration.Generate?,
|
||||
_ defaults: Configuration.Generate,
|
||||
_ options: CliClient.PandocOptions
|
||||
) -> String {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
if let output = options.buildDirectory {
|
||||
return output
|
||||
} else if let output = configuration?.buildDirectory {
|
||||
return output
|
||||
} else if let output = defaults.buildDirectory {
|
||||
return output
|
||||
} else {
|
||||
logger.warning("Output file name not specified, this could lead to errors.")
|
||||
return ".build"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,12 @@ public enum CliClientError: Error {
|
||||
case brewfileNotFound
|
||||
case encodingError
|
||||
case playbookDirectoryNotFound
|
||||
case generate(GenerateError)
|
||||
case templateDirectoryNotFound
|
||||
case templateDirectoryOrRepoNotSpecified
|
||||
case vaultFileNotFound
|
||||
|
||||
public enum GenerateError: Sendable {
|
||||
case projectDirectoryNotSpecified
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,52 @@ public struct CliClient: Sendable {
|
||||
|
||||
public extension CliClient {
|
||||
|
||||
struct PandocOptions: Equatable, Sendable {
|
||||
|
||||
let buildDirectory: String?
|
||||
let files: [String]?
|
||||
let includeInHeader: [String]?
|
||||
let outputDirectory: String?
|
||||
let outputFileName: String?
|
||||
let outputFileType: FileType
|
||||
let projectDirectory: String?
|
||||
let quiet: Bool
|
||||
let shell: String?
|
||||
let shouldBuild: Bool
|
||||
|
||||
public init(
|
||||
buildDirectory: String? = nil,
|
||||
files: [String]? = nil,
|
||||
includeInHeader: [String]? = nil,
|
||||
outputDirectory: String? = nil,
|
||||
outputFileName: String? = nil,
|
||||
outputFileType: FileType,
|
||||
projectDirectory: String?,
|
||||
quiet: Bool,
|
||||
shell: String? = nil,
|
||||
shouldBuild: Bool
|
||||
) {
|
||||
self.buildDirectory = buildDirectory
|
||||
self.files = files
|
||||
self.includeInHeader = includeInHeader
|
||||
self.outputDirectory = outputDirectory
|
||||
self.outputFileName = outputFileName
|
||||
self.outputFileType = outputFileType
|
||||
self.projectDirectory = projectDirectory
|
||||
self.quiet = quiet
|
||||
self.shell = shell
|
||||
self.shouldBuild = shouldBuild
|
||||
}
|
||||
|
||||
// swiftlint:disable nesting
|
||||
public enum FileType: Equatable, Sendable {
|
||||
case html
|
||||
case latex
|
||||
case pdf(engine: String?)
|
||||
}
|
||||
// swiftlint:enable nesting
|
||||
}
|
||||
|
||||
struct GenerateJsonOptions: Equatable, Sendable {
|
||||
let templateDirectory: String?
|
||||
let templateRepo: String?
|
||||
|
||||
Reference in New Issue
Block a user