From 6b49db5801126fdbc2693cb24b2a69b326fc4c39 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Thu, 30 Apr 2026 18:13:00 +0100 Subject: [PATCH] refactor(Compiler): eliminate Maybe Text indirection in CompilationStrategy - csWriter now writes files directly (WriterOptions -> Pandoc -> FilePath -> IO (Either DocsterError ())) - csPostProcess no longer takes text content (String -> IO (Either DocsterError ())) - Each strategy owns its complete output logic (PDF/HTML/DOCX) - Remove generateOutputFile (eliminated unused CompilationStrategy parameter) - Pipeline: generateOutputM >>= processOutput => writeAndProcessOutput (1 step) - Pandoc 3.7 compatibility: writeDocx returns ByteString instead of () .gitignore: exclude stack/cabal config and generated files --- .gitignore | 9 ++++ src/Docster/Compiler.hs | 114 +++++++++++++++------------------------- 2 files changed, 52 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 33fedcc..fc410f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ dist-newstyle dist-newstyle /.stack-work/ +/.stack-root/ +/.cabal-config/ *.mmd *.png *.svg @@ -10,3 +12,10 @@ dist-newstyle dist-newstyle output/ *.log +cabal.project +lts-24-34.yaml +stack-setup-2.yaml +analytics-charts.md +architecture-deep-dive.md +devcontainer.org +root.json diff --git a/src/Docster/Compiler.hs b/src/Docster/Compiler.hs index 1c7fa9b..9bb0d02 100644 --- a/src/Docster/Compiler.hs +++ b/src/Docster/Compiler.hs @@ -47,7 +47,6 @@ data CompilationContext = CompilationContext , ccDocName :: Text , ccReaderOptions :: ReaderOptions , ccConfig :: DiagramConfig - , ccWritesFile :: Bool } -- | Monad stack for compilation pipeline @@ -57,44 +56,57 @@ type CompilationM = ReaderT CompilationContext (ExceptT DocsterError IO) data CompilationStrategy = CompilationStrategy { -- | Format for diagram configuration csOutputFormat :: OutputFormat - -- | Pandoc writer function (returns Text for HTML/PDF, unused for DOCX) - , csWriter :: WriterOptions -> Pandoc -> PandocIO Text - -- | Post-processing function for the generated content - , csProcessOutput :: String -> Text -> IO (Either DocsterError ()) + -- | Pandoc writer: writes output directly to the given file path + , csWriter :: WriterOptions -> Pandoc -> FilePath -> IO (Either DocsterError ()) + -- | Post-processing after write (PDF→xelatex, HTML→open browser, DOCX→noop) + , csPostProcess :: String -> IO (Either DocsterError ()) -- | Success message formatter , csSuccessMessage :: String -> Text - -- | True for formats where writer writes a file directly (DOCX) - , csWritesFile :: Bool } -- | PDF compilation strategy pdfStrategy :: CompilationStrategy pdfStrategy = CompilationStrategy { csOutputFormat = PDF - , csWriter = writeLaTeX - , csProcessOutput = processPDFOutput + , csWriter = \opts doc path -> do + result <- runIO (writeLaTeX opts doc) + case result of + Left err -> return $ Left $ FileError $ "LaTeX write failed: " <> T.pack (show err) + Right latex -> do + TIO.writeFile path (latexTemplate latex) + return $ Right () + , csPostProcess = processPDFOutput , csSuccessMessage = \path -> successEmoji <> " PDF written to " <> T.pack path - , csWritesFile = False } -- | HTML compilation strategy htmlStrategy :: CompilationStrategy htmlStrategy = CompilationStrategy { csOutputFormat = HTML - , csWriter = writeHtml5String - , csProcessOutput = processHTMLOutput + , csWriter = \opts doc path -> do + result <- runIO (writeHtml5String opts doc) + case result of + Left err -> return $ Left $ FileError $ "HTML write failed: " <> T.pack (show err) + Right html -> do + TIO.writeFile path html + return $ Right () + , csPostProcess = processHTMLOutput , csSuccessMessage = \path -> successEmoji <> " HTML written to " <> T.pack path - , csWritesFile = False } -- | DOCX compilation strategy (Pandoc writes file directly) docxStrategy :: CompilationStrategy docxStrategy = CompilationStrategy { csOutputFormat = DOCX - , csWriter = \_ _ -> return "" -- unused: writeDocx writes file directly - , csProcessOutput = \_ _ -> return $ Right () -- no post-processing needed + , csWriter = \opts doc path -> do + result <- runIO (writeDocx opts doc) + case result of + Left err -> return $ Left $ FileError $ "DOCX generation failed: " <> T.pack (show err) + Right docxBS -> do + BSL.writeFile path docxBS + return $ Right () + , csPostProcess = \_ -> return $ Right () -- no post-processing needed , csSuccessMessage = \path -> successEmoji <> " DOCX written to " <> T.pack path - , csWritesFile = True } -- | Parse LaTeX log content to extract meaningful error messages @@ -160,11 +172,10 @@ extractFatalErrors = mapMaybe extractFatal | "! " `T.isPrefixOf` line && not ("Missing character:" `T.isInfixOf` line) = Just $ T.drop 2 line | otherwise = Nothing --- | Process PDF output: LaTeX template application and direct XeLaTeX compilation -processPDFOutput :: String -> Text -> IO (Either DocsterError ()) -processPDFOutput outputPath latexOutput = do - let completeLatex = latexTemplate latexOutput - logOutputPath = replaceExtension outputPath "log" +-- | Process PDF output: direct XeLaTeX compilation (LaTeX already written by csWriter) +processPDFOutput :: String -> IO (Either DocsterError ()) +processPDFOutput outputPath = do + let logOutputPath = replaceExtension outputPath "log" -- Use temporary directory for LaTeX compilation withSystemTempDirectory "docster-latex" $ \tempDir -> do @@ -172,9 +183,6 @@ processPDFOutput outputPath latexOutput = do pdfFile = tempDir "document.pdf" logFile = tempDir "document.log" - -- Write LaTeX content to temporary file - TIO.writeFile texFile completeLatex - -- Run XeLaTeX compilation (exitCode, _stdout, stderr) <- readProcessWithExitCode "xelatex" [ "-output-directory=" <> tempDir @@ -210,11 +218,9 @@ processPDFOutput outputPath latexOutput = do errorSummary <> "\n\n" <> "Full LaTeX log written to: " <> T.pack logOutputPath --- | Process HTML output: file writing and browser opening -processHTMLOutput :: String -> Text -> IO (Either DocsterError ()) -processHTMLOutput outputPath html = do - TIO.writeFile outputPath html - +-- | Process HTML output: open browser (HTML already written by csWriter) +processHTMLOutput :: String -> IO (Either DocsterError ()) +processHTMLOutput outputPath = do -- Open the generated HTML file in browser for verification putStrLn $ "🌐 Opening " <> outputPath <> " in browser for error checking..." void $ callProcess "open" [outputPath] @@ -267,28 +273,13 @@ transformDocumentM pandoc = do docName <- asks ccDocName liftEitherM $ transformDocument config docName pandoc --- | Pipeline step: Generate output using format-specific writer -generateOutputM :: Pandoc -> CompilationM Text -generateOutputM pandoc = do +-- | Pipeline step: Write output and post-process (format-specific) +writeAndProcessOutput :: Pandoc -> CompilationM () +writeAndProcessOutput pandoc = do strategy <- asks ccStrategy - writesFile <- asks ccWritesFile - if writesFile - then do - outputPath <- asks ccOutputPath - _ <- liftIO $ generateOutputFile outputPath pandoc - return "" -- placeholder, won't be used - else liftEitherM $ generateOutput strategy pandoc - --- | Pipeline step: Process output and write to file -processOutput :: Text -> CompilationM () -processOutput output = do - strategy <- asks ccStrategy - writesFile <- asks ccWritesFile - if writesFile - then return () -- file already written by writer - else do - outputPath <- asks ccOutputPath - liftEitherM $ csProcessOutput strategy outputPath output + outputPath <- asks ccOutputPath + liftEitherM $ csWriter strategy def pandoc outputPath + liftEitherM $ (csPostProcess strategy) outputPath -- | Pipeline step: Print success message printSuccess :: CompilationM () @@ -302,8 +293,8 @@ compileWithStrategy :: CompilationStrategy -> SourceDir -> OutputDir -> Text -> compileWithStrategy strategy sourceDir outputDir docName (OutputPath inputPath) (OutputPath outputPath) = do let readerOptions = def { readerExtensions = getDefaultExtensions "markdown" } config = DiagramConfig sourceDir outputDir (csOutputFormat strategy) - context = CompilationContext strategy inputPath outputPath docName readerOptions config (csWritesFile strategy) - pipeline = readContent >>= parseDocument >>= transformDocumentM >>= generateOutputM >>= processOutput >> printSuccess + context = CompilationContext strategy inputPath outputPath docName readerOptions config + pipeline = readContent >>= parseDocument >>= transformDocumentM >>= writeAndProcessOutput >> printSuccess runExceptT $ runReaderT pipeline context @@ -315,26 +306,7 @@ parseMarkdown readerOptions content = do Left err -> Left $ FileError $ "Failed to parse markdown: " <> T.pack (show err) Right pandoc -> Right pandoc --- | Generate output using the strategy's writer with error handling -generateOutput :: CompilationStrategy -> Pandoc -> IO (Either DocsterError Text) -generateOutput strategy transformed = do - result <- runIO $ csWriter strategy def transformed - return $ case result of - Left err -> Left $ case csOutputFormat strategy of - PDF -> PDFGenerationError $ "LaTeX generation failed: " <> T.pack (show err) - HTML -> FileError $ "HTML generation failed: " <> T.pack (show err) - DOCX -> FileError $ "DOCX generation failed: " <> T.pack (show err) - Right output -> Right output --- | Generate output file directly (for DOCX which writes to file) -generateOutputFile :: CompilationStrategy -> FilePath -> Pandoc -> IO (Either DocsterError ()) -generateOutputFile outputPath pandoc = do - result <- runIO $ writeDocx def pandoc - case result of - Left err -> return $ Left $ FileError $ "DOCX generation failed: " <> T.pack (show err) - Right docxBS -> do - BSL.writeFile outputPath docxBS - return $ Right () -- | Compile markdown to PDF using XeLaTeX compileToPDF :: FilePath -> IO ()