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