Compare commits

..
7 Commits
Author SHA1 Message Date
mostalive 54c7502f0f fix test warnings: redundant Data.Text import, explicit Int types in Map ops 2026-04-30 21:45:42 +01:00
mostalive eb374d20f5 update synopsis and module docs to mention DOCX output 2026-04-30 21:42:39 +01:00
mostalive 57f4f9f165 add -docx command example to README 2026-04-30 21:42:11 +01:00
mostalive f016950ac7 add DOCX support to Mermaid diagram rendering
- Transform.hs: DOCX case returns blocks unchanged (no unicode substitution needed)
- Mermaid.hs: DOCX uses PNG images at normal scale (like HTML, not 3x like PDF)
2026-04-30 18:25:27 +01:00
mostalive f4dab3e354 update pandoc bounds to <3.8 (matches 3.7.0.2 used by lts-24.34) 2026-04-30 18:24:07 +01:00
mostalive 6b49db5801 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
2026-04-30 18:13:00 +01:00
mostalive b0457388dc remove unused parameter 2026-04-30 17:36:24 +01:00
7 changed files with 71 additions and 84 deletions
+10
View File
@@ -1,6 +1,8 @@
dist-newstyle
dist-newstyle
/.stack-work/
/.stack-root/
/.cabal-config/
*.mmd
*.png
*.svg
@@ -10,3 +12,11 @@ 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
.devcontainer/
root.json
+2 -1
View File
@@ -6,8 +6,9 @@ A self-contained CLI tool that converts Markdown with Mermaid diagrams into PDF
docster -pdf path/to/file.md
docster -html path/to/file.md
docster -docx path/to/file.md
Mermaid code blocks (```mermaid) will be rendered to SVG and embedded.
Mermaid code blocks (```mermaid) will be rendered to SVG (HTML) or PNG (PDF/DOCX) and embedded.
## Installation
+4 -4
View File
@@ -1,9 +1,9 @@
cabal-version: 3.0
name: docster
version: 0.1.0.0
synopsis: A self-contained CLI tool that converts Markdown with Mermaid diagrams to PDF/HTML
description: Docster converts Markdown documents containing Mermaid diagrams into PDF or HTML files
using Pandoc and Mermaid CLI. It automatically renders Mermaid code blocks to SVG
synopsis: A self-contained CLI tool that converts Markdown with Mermaid diagrams to PDF, HTML, or DOCX
description: Docster converts Markdown documents containing Mermaid diagrams into PDF, HTML, or DOCX files
using Pandoc and Mermaid CLI. It automatically renders Mermaid code blocks to SVG (HTML) or PNG (PDF/DOCX)
and embeds them in the output.
homepage: https://github.com/yourusername/docster
license: BSD-3-Clause
@@ -41,7 +41,7 @@ library
process >=1.6 && <1.7,
hashable >=1.4 && <1.6,
containers >=0.6 && <0.8,
pandoc >=3.0 && <3.2,
pandoc >=3.0 && <3.8,
pandoc-types >=1.23 && <1.25,
bytestring >=0.11 && <0.13,
temporary >=1.3 && <1.4,
+43 -71
View File
@@ -1,7 +1,7 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
-- | Document compilation functionality for PDF and HTML output
-- | Document compilation functionality for PDF, HTML, and DOCX output
module Docster.Compiler
( -- * Compilation Functions
compileToPDF
@@ -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 strategy 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
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 ()
+3
View File
@@ -79,6 +79,8 @@ generateDiagramPaths (DiagramConfig _ (OutputDir outDir) format) (DiagramId diag
in (svgFile, T.pack $ takeFileName svgFile)
PDF -> let pngFile = outDir </> diagIdStr <> ".png"
in (pngFile, T.pack pngFile)
DOCX -> let pngFile = outDir </> diagIdStr <> ".png"
in (pngFile, T.pack pngFile)
-- | Puppeteer configuration content for disabling sandbox
puppeteerConfigContent :: Text
@@ -90,6 +92,7 @@ callMermaidProcess format mmdFile outputFile = do
let baseArgs = case format of
HTML -> ["-i", mmdFile, "-o", outputFile]
PDF -> ["-i", mmdFile, "-o", outputFile, "--scale", "3"]
DOCX -> ["-i", mmdFile, "-o", outputFile]
-- Create temporary puppeteer config file
result <- bracket
+1
View File
@@ -38,6 +38,7 @@ transformDocument config docName (Pandoc meta blocks) = do
case dcOutputFormat config of
PDF -> return $ Right $ substituteUnicodeSymbols (Pandoc meta newBlocks)
HTML -> return $ Right $ Pandoc meta newBlocks
DOCX -> return $ Right $ Pandoc meta newBlocks
-- | Process a single block with heading tracking state
processBlockStateful :: DiagramConfig -> Block -> TransformM Block
+6 -6
View File
@@ -4,7 +4,7 @@ module Docster.TransformSpec (spec) where
import Test.Hspec
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import Data.Text()
import qualified Data.Text as T
import Text.Pandoc.Definition (Inline(..))
@@ -64,21 +64,21 @@ spec = do
describe "diagram naming logic" $ do
it "first diagram under heading has no suffix" $
let baseName = "file_flow"
counter = Map.findWithDefault 0 baseName Map.empty
counter = Map.findWithDefault (0 :: Int) baseName Map.empty
diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter)
in diagName `shouldBe` "file_flow"
it "second diagram gets _1 suffix" $
let baseName = "file_flow"
counters = Map.singleton "file_flow" 1
counter = Map.findWithDefault 0 baseName counters
counters = Map.singleton "file_flow" (1 :: Int)
counter = Map.findWithDefault (0 :: Int) baseName counters
diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter)
in diagName `shouldBe` "file_flow_1"
it "third diagram gets _2 suffix" $
let baseName = "file_flow"
counters = Map.singleton "file_flow" 2
counter = Map.findWithDefault 0 baseName counters
counters = Map.singleton "file_flow" (2 :: Int)
counter = Map.findWithDefault (0 :: Int) baseName counters
diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter)
in diagName `shouldBe` "file_flow_2"