329 lines
9.5 KiB
Swift
329 lines
9.5 KiB
Swift
import CommandClient
|
|
import ConfigurationClient
|
|
import Dependencies
|
|
import Foundation
|
|
import PlaybookClient
|
|
|
|
extension PandocClient.RunOptions {
|
|
|
|
/// Runs a pandoc conversion on the project generating the given file type.
|
|
///
|
|
/// - Parameters:
|
|
/// - fileType: The file type to convert to.
|
|
/// - environment: The environment variables.
|
|
///
|
|
/// - Returns: File path to the converted output file.
|
|
func run(
|
|
_ fileType: PandocClient.FileType,
|
|
_ environment: [String: String]
|
|
) async throws -> String {
|
|
@Dependency(\.logger) var logger
|
|
|
|
let ensuredOptions = try await self.ensuredOptions(fileType)
|
|
|
|
let projectDirectory = self.projectDirectory ?? environment["PWD"]
|
|
|
|
guard let projectDirectory else {
|
|
throw ProjectDirectoryNotSpecified()
|
|
}
|
|
|
|
try await buildProject(projectDirectory, ensuredOptions)
|
|
|
|
let outputDirectory = self.outputDirectory ?? projectDirectory
|
|
let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredFilename)"
|
|
|
|
let arguments = ensuredOptions.makeArguments(
|
|
outputPath: outputPath,
|
|
projectDirectory: projectDirectory
|
|
)
|
|
|
|
logger.debug("Pandoc arguments: \(arguments)")
|
|
return try await runCommand(arguments, outputPath)
|
|
|
|
}
|
|
|
|
/// Runs a shell command with the given arguments, returning the passed in output path
|
|
/// so the command can be chained, if needed.
|
|
///
|
|
@discardableResult
|
|
func runCommand(
|
|
_ arguments: [String],
|
|
_ outputPath: String
|
|
) async throws -> String {
|
|
@Dependency(\.commandClient) var commandClient
|
|
@Dependency(\.logger) var logger
|
|
logger.debug("Running shell command with arguments: \(arguments)")
|
|
return try await commandClient.run(logging: loggingOptions, quiet: quiet, shell: shell) {
|
|
(arguments, outputPath)
|
|
}
|
|
}
|
|
|
|
/// Build the project if necessary, before running the shell command that builds the final
|
|
/// output file(s).
|
|
///
|
|
func buildProject(
|
|
_ projectDirectory: String,
|
|
_ ensuredOptions: EnsuredPandocOptions
|
|
) async throws {
|
|
@Dependency(\.logger) var logger
|
|
@Dependency(\.playbookClient) var playbookClient
|
|
|
|
if shouldBuildProject {
|
|
logger.debug("Building project...")
|
|
try await playbookClient.run.buildProject(
|
|
.init(
|
|
projectDirectory: projectDirectory,
|
|
shared: .init(
|
|
extraOptions: nil,
|
|
inventoryFilePath: nil,
|
|
loggingOptions: loggingOptions,
|
|
quiet: quiet,
|
|
shell: shell
|
|
)
|
|
)
|
|
)
|
|
}
|
|
|
|
// Build latex file pre-html, so that we can properly convert the latex document
|
|
// into an html document.
|
|
if ensuredOptions.outputFileType == .html {
|
|
logger.debug("Building latex, pre-html conversion...")
|
|
let outputPath = "\(ensuredOptions.buildDirectory)/\(EnsuredPandocOptions.latexFilename)"
|
|
let arguments = ensuredOptions.preHtmlLatexOptions.makeArguments(
|
|
outputPath: outputPath,
|
|
projectDirectory: projectDirectory
|
|
)
|
|
try await runCommand(arguments, outputPath)
|
|
}
|
|
}
|
|
|
|
/// Generates the ensured/parsed options for a pandoc conversion.
|
|
///
|
|
/// - Parameter fileType: The file type we're converting to.
|
|
///
|
|
/// - Returns: The ensured options.
|
|
func ensuredOptions(
|
|
_ fileType: PandocClient.FileType
|
|
) async throws -> EnsuredPandocOptions {
|
|
@Dependency(\.configurationClient) var configurationClient
|
|
@Dependency(\.logger) var logger
|
|
|
|
let configuration = try await configurationClient.findAndLoad()
|
|
logger.debug("Configuration: \(configuration)")
|
|
|
|
return try await ensurePandocOptions(
|
|
configuration: configuration,
|
|
fileType: fileType,
|
|
options: self
|
|
)
|
|
}
|
|
}
|
|
|
|
extension PandocClient.FileType {
|
|
/// Represents the appropriate file extension for a file type.
|
|
var fileExtension: String {
|
|
switch self {
|
|
case .html: return "html"
|
|
case .latex: return "tex"
|
|
case .pdf: return "pdf"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents pandoc options that get parsed based on the given run options, configuration, etc.
|
|
///
|
|
/// This set's potentially optional values into real values that are required for pandoc to run
|
|
/// properly and convert files for the given file type conversion.
|
|
@_spi(Internal)
|
|
public struct EnsuredPandocOptions: Equatable, Sendable {
|
|
public static let latexFilename = "Report.tex"
|
|
|
|
public let buildDirectory: String
|
|
public let extraOptions: [String]?
|
|
public let files: [String]
|
|
public let includeInHeader: [String]
|
|
public let outputFileName: String
|
|
public let outputFileType: PandocClient.FileType
|
|
public let pdfEngine: String?
|
|
|
|
/// Ensures the output filename includes the file extension, so that pandoc
|
|
/// can properly convert the files.
|
|
public var ensuredFilename: String {
|
|
let extensionString = ".\(outputFileType.fileExtension)"
|
|
|
|
if !outputFileName.hasSuffix(extensionString) {
|
|
return outputFileName + extensionString
|
|
}
|
|
return outputFileName
|
|
}
|
|
|
|
/// Generates the options required for building the latex file that is needed
|
|
/// to convert the project to an html output file.
|
|
var preHtmlLatexOptions: Self {
|
|
.init(
|
|
buildDirectory: buildDirectory,
|
|
extraOptions: extraOptions,
|
|
files: files,
|
|
includeInHeader: includeInHeader,
|
|
outputFileName: Self.latexFilename,
|
|
outputFileType: .latex,
|
|
pdfEngine: nil
|
|
)
|
|
}
|
|
|
|
/// Generate arguments for the pandoc shell command based on the parsed options
|
|
/// for a given conversion.
|
|
///
|
|
func makeArguments(
|
|
outputPath: String,
|
|
projectDirectory: String
|
|
) -> [String] {
|
|
var arguments = [PandocClient.Constants.pandocCommand]
|
|
|
|
if outputFileType != .html {
|
|
arguments += includeInHeader.map {
|
|
"--include-in-header=\(projectDirectory)/\(buildDirectory)/\($0)"
|
|
}
|
|
}
|
|
|
|
if let pdfEngine {
|
|
arguments.append("--pdf-engine=\(pdfEngine)")
|
|
}
|
|
|
|
arguments.append("--output=\(outputPath)")
|
|
|
|
if let extraOptions {
|
|
arguments.append(contentsOf: extraOptions)
|
|
}
|
|
|
|
if outputFileType != .html {
|
|
arguments += files.map {
|
|
"\(projectDirectory)/\(buildDirectory)/\($0)"
|
|
}
|
|
} else {
|
|
arguments.append("\(projectDirectory)/\(buildDirectory)/\(Self.latexFilename)")
|
|
}
|
|
|
|
return arguments
|
|
}
|
|
}
|
|
|
|
@_spi(Internal)
|
|
public func ensurePandocOptions(
|
|
configuration: Configuration,
|
|
fileType: PandocClient.FileType,
|
|
options: PandocClient.RunOptions
|
|
) async throws -> EnsuredPandocOptions {
|
|
let defaults = Configuration.Generate.default
|
|
|
|
return .init(
|
|
buildDirectory: options.parseBuildDirectory(configuration.generate, defaults),
|
|
extraOptions: options.extraOptions,
|
|
files: options.parseFiles(configuration.generate, defaults),
|
|
includeInHeader: options.parseIncludeInHeader(configuration.generate, defaults),
|
|
outputFileName: options.parseOutputFileName(configuration.generate, defaults),
|
|
outputFileType: fileType,
|
|
pdfEngine: fileType.parsePdfEngine(configuration.generate, defaults)
|
|
)
|
|
}
|
|
|
|
@_spi(Internal)
|
|
extension PandocClient.FileType {
|
|
public func parsePdfEngine(
|
|
_ configuration: Configuration.Generate?,
|
|
_ defaults: Configuration.Generate
|
|
) -> String? {
|
|
switch self {
|
|
case .html, .latex:
|
|
return nil
|
|
case .pdf(let engine):
|
|
if let engine {
|
|
return engine
|
|
} else if let engine = configuration?.pdfEngine {
|
|
return engine
|
|
} else if let engine = defaults.pdfEngine {
|
|
return engine
|
|
} else {
|
|
return PandocClient.Constants.defaultPdfEngine
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@_spi(Internal)
|
|
extension PandocClient.RunOptions {
|
|
public func parseFiles(
|
|
_ configuration: Configuration.Generate?,
|
|
_ defaults: Configuration.Generate
|
|
) -> [String] {
|
|
@Dependency(\.logger) var logger
|
|
|
|
if let files = 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 []
|
|
}
|
|
}
|
|
|
|
public func parseIncludeInHeader(
|
|
_ configuration: Configuration.Generate?,
|
|
_ defaults: Configuration.Generate
|
|
) -> [String] {
|
|
@Dependency(\.logger) var logger
|
|
|
|
if let files = 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 []
|
|
}
|
|
}
|
|
|
|
public func parseOutputFileName(
|
|
_ configuration: Configuration.Generate?,
|
|
_ defaults: Configuration.Generate
|
|
) -> String {
|
|
@Dependency(\.logger) var logger
|
|
|
|
if let output = 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 PandocClient.Constants.defaultOutputFileName
|
|
}
|
|
}
|
|
|
|
public func parseBuildDirectory(
|
|
_ configuration: Configuration.Generate?,
|
|
_ defaults: Configuration.Generate
|
|
) -> String {
|
|
@Dependency(\.logger) var logger
|
|
|
|
if let output = 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"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ProjectDirectoryNotSpecified: Error {}
|