From 045f48348dd7c2ee0c9368173b43cee5b13f7851 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Mon, 17 Nov 2025 09:38:23 -0500 Subject: [PATCH] feat: Fixes generation of html document, it now builds a latex document in the build directory that it then converts to html. Need to update tests. --- Sources/CommandClient/CommandClient.swift | 49 +++---- Sources/PandocClient/PandocClient+Run.swift | 137 +++++++++++++++----- Sources/PandocClient/PandocClient.swift | 4 +- 3 files changed, 131 insertions(+), 59 deletions(-) diff --git a/Sources/CommandClient/CommandClient.swift b/Sources/CommandClient/CommandClient.swift index f283ff8..b43ce89 100644 --- a/Sources/CommandClient/CommandClient.swift +++ b/Sources/CommandClient/CommandClient.swift @@ -4,10 +4,10 @@ import DependenciesMacros import Foundation import ShellClient -public extension DependencyValues { +extension DependencyValues { /// Runs shell commands. - var commandClient: CommandClient { + public var commandClient: CommandClient { get { self[CommandClient.self] } set { self[CommandClient.self] = newValue } } @@ -67,12 +67,13 @@ public struct CommandClient: Sendable { in workingDirectory: String? = nil, _ arguments: [String] ) async throws { - try await runCommand(.init( - arguments: arguments, - quiet: quiet, - shell: shell, - workingDirectory: workingDirectory - )) + try await runCommand( + .init( + arguments: arguments, + quiet: quiet, + shell: shell, + workingDirectory: workingDirectory + )) } /// Runs a shell command. @@ -161,19 +162,21 @@ extension CommandClient: DependencyKey { .init { options in @Dependency(\.asyncShellClient) var shellClient if !options.quiet { - try await shellClient.foreground(.init( - shell: .init(options.shell), - environment: environment, - in: options.workingDirectory, - options.arguments - )) + try await shellClient.foreground( + .init( + shell: .init(options.shell), + environment: environment, + in: options.workingDirectory, + options.arguments + )) } else { - try await shellClient.background(.init( - shell: .init(options.shell), - environment: environment, - in: options.workingDirectory, - options.arguments - )) + try await shellClient.background( + .init( + shell: .init(options.shell), + environment: environment, + in: options.workingDirectory, + options.arguments + )) } } } @@ -184,12 +187,12 @@ extension CommandClient: DependencyKey { } @_spi(Internal) -public extension CommandClient { +extension CommandClient { /// Create a command client that can capture the arguments / options. /// /// This is used for testing. - static func capturing(_ client: CapturingClient) -> Self { + public static func capturing(_ client: CapturingClient) -> Self { .init { options in await client.set(options) } @@ -198,7 +201,7 @@ public extension CommandClient { /// Captures the arguments / options passed into the command client's run commands. /// @dynamicMemberLookup - actor CapturingClient: Sendable { + public actor CapturingClient: Sendable { public private(set) var options: RunCommandOptions? public init() {} diff --git a/Sources/PandocClient/PandocClient+Run.swift b/Sources/PandocClient/PandocClient+Run.swift index 8836342..f76e511 100644 --- a/Sources/PandocClient/PandocClient+Run.swift +++ b/Sources/PandocClient/PandocClient+Run.swift @@ -12,20 +12,67 @@ extension PandocClient.RunOptions { ) async throws -> String { @Dependency(\.commandClient) var commandClient @Dependency(\.logger) var logger + + // return try await commandClient.run(logging: loggingOptions, quiet: quiet, shell: shell) { + let ensuredOptions = try await self.ensuredOptions(fileType) + + let projectDirectory = self.projectDirectory ?? environment["PWD"] + + guard let projectDirectory else { + throw ProjectDirectoryNotSpecified() + } + + try await buildProject(fileType, projectDirectory, ensuredOptions) + + let outputDirectory = self.outputDirectory ?? projectDirectory + let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)" + + let arguments = ensuredOptions.makeArguments( + fileType: fileType, + outputPath: outputPath, + projectDirectory: projectDirectory + ) + + logger.debug("Pandoc arguments: \(arguments)") + return try await runCommand(arguments, outputPath) + + // return (arguments, outputPath) + // } + } + + /// Runs a shell command with the given arguments, returning the passed in output path + /// so the command can be chained, if needed. + /// + @_spi(Internal) + @discardableResult + public 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). + /// + @_spi(Internal) + public func buildProject( + _ fileType: PandocClient.FileType, + _ projectDirectory: String, + _ ensuredOptions: EnsuredPandocOptions + ) async throws { + @Dependency(\.logger) var logger @Dependency(\.playbookClient) var playbookClient - return try await commandClient.run(logging: loggingOptions, quiet: quiet, shell: shell) { - let ensuredOptions = try await self.ensuredOptions(fileType) - - let projectDirectory = self.projectDirectory ?? environment["PWD"] - - guard let projectDirectory else { - throw ProjectDirectoryNotSpecified() - } - - if shouldBuildProject { - logger.debug("Building project...") - try await playbookClient.run.buildProject(.init( + if shouldBuildProject { + logger.debug("Building project...") + try await playbookClient.run.buildProject( + .init( projectDirectory: projectDirectory, shared: .init( extraOptions: nil, @@ -34,20 +81,21 @@ extension PandocClient.RunOptions { quiet: quiet, shell: shell ) - )) - } + ) + ) + } - let outputDirectory = self.outputDirectory ?? projectDirectory - let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)" - - let arguments = ensuredOptions.makeArguments( + // Build latex file pre-html, so that we can properly convert the latex document + // into an html document. + if fileType == .html { + logger.debug("Building latex, pre-html conversion...") + let outputPath = "\(ensuredOptions.buildDirectory)/Report.tex" + let arguments = ensuredOptions.preHtmlLatexOptions.makeArguments( + fileType: .latex, outputPath: outputPath, projectDirectory: projectDirectory ) - - logger.debug("Pandoc arguments: \(arguments)") - - return (arguments, outputPath) + try await runCommand(arguments, outputPath) } } @@ -80,6 +128,8 @@ extension PandocClient.FileType { @_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] @@ -97,14 +147,29 @@ public struct EnsuredPandocOptions: Equatable, Sendable { return outputFileName } + public var preHtmlLatexOptions: Self { + .init( + buildDirectory: buildDirectory, + extraOptions: extraOptions, + files: files, + includeInHeader: includeInHeader, + outputFileName: Self.latexFilename, + outputFileType: .latex, + pdfEngine: nil + ) + } + func makeArguments( + fileType: PandocClient.FileType, outputPath: String, projectDirectory: String ) -> [String] { var arguments = [PandocClient.Constants.pandocCommand] - arguments += includeInHeader.map { - "--include-in-header=\(projectDirectory)/\(buildDirectory)/\($0)" + if fileType != .html { + arguments += includeInHeader.map { + "--include-in-header=\(projectDirectory)/\(buildDirectory)/\($0)" + } } if let pdfEngine { @@ -117,8 +182,12 @@ public struct EnsuredPandocOptions: Equatable, Sendable { arguments.append(contentsOf: extraOptions) } - arguments += files.map { - "\(projectDirectory)/\(buildDirectory)/\($0)" + if fileType != .html { + arguments += files.map { + "\(projectDirectory)/\(buildDirectory)/\($0)" + } + } else { + arguments.append("\(projectDirectory)/\(buildDirectory)/\(Self.latexFilename)") } return arguments @@ -145,15 +214,15 @@ public func ensurePandocOptions( } @_spi(Internal) -public extension PandocClient.FileType { - func parsePdfEngine( +extension PandocClient.FileType { + public func parsePdfEngine( _ configuration: Configuration.Generate?, _ defaults: Configuration.Generate ) -> String? { switch self { case .html, .latex: return nil - case let .pdf(engine: engine): + case .pdf(let engine): if let engine { return engine } else if let engine = configuration?.pdfEngine { @@ -168,8 +237,8 @@ public extension PandocClient.FileType { } @_spi(Internal) -public extension PandocClient.RunOptions { - func parseFiles( +extension PandocClient.RunOptions { + public func parseFiles( _ configuration: Configuration.Generate?, _ defaults: Configuration.Generate ) -> [String] { @@ -187,7 +256,7 @@ public extension PandocClient.RunOptions { } } - func parseIncludeInHeader( + public func parseIncludeInHeader( _ configuration: Configuration.Generate?, _ defaults: Configuration.Generate ) -> [String] { @@ -205,7 +274,7 @@ public extension PandocClient.RunOptions { } } - func parseOutputFileName( + public func parseOutputFileName( _ configuration: Configuration.Generate?, _ defaults: Configuration.Generate ) -> String { @@ -223,7 +292,7 @@ public extension PandocClient.RunOptions { } } - func parseBuildDirectory( + public func parseBuildDirectory( _ configuration: Configuration.Generate?, _ defaults: Configuration.Generate ) -> String { diff --git a/Sources/PandocClient/PandocClient.swift b/Sources/PandocClient/PandocClient.swift index c79089d..b8be760 100644 --- a/Sources/PandocClient/PandocClient.swift +++ b/Sources/PandocClient/PandocClient.swift @@ -4,14 +4,14 @@ import Dependencies import DependenciesMacros import Foundation -public extension DependencyValues { +extension DependencyValues { /// Represents interactions with the `pandoc` command line application. /// /// The `pandoc` command line application is used to generate the final output /// documents from a home performance assessment project. /// - var pandocClient: PandocClient { + public var pandocClient: PandocClient { get { self[PandocClient.self] } set { self[PandocClient.self] = newValue } }