Files
swift-hpa/Sources/PandocClient/PandocClient+Run.swift
Michael Housh f9ecc5ce6f
All checks were successful
CI / Run Tests (push) Successful in 1m26s
Build docker images / docker (push) Successful in 7m19s
Release / release (push) Successful in 5s
fix: Fixes generating html conversion.
2025-11-17 10:59:02 -05:00

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 {}