Compare commits

..
10 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
mostalive 8abe1d1bc2 resolve compiler warnings 2026-04-30 17:33:27 +01:00
mostalive 9dd9313829 Add docx export 2026-04-30 17:27:59 +01:00
mostalive fa850d5017 write agents.md in qwen's own words 2026-04-30 17:27:37 +01:00
12 changed files with 183 additions and 173 deletions
+10
View File
@@ -1,6 +1,8 @@
dist-newstyle dist-newstyle
dist-newstyle dist-newstyle
/.stack-work/ /.stack-work/
/.stack-root/
/.cabal-config/
*.mmd *.mmd
*.png *.png
*.svg *.svg
@@ -10,3 +12,11 @@ dist-newstyle
dist-newstyle dist-newstyle
output/ output/
*.log *.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 -pdf path/to/file.md
docster -html 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 ## Installation
+36 -103
View File
@@ -1,115 +1,48 @@
# Claude Code Agents # Docster — Project Guide
This project uses specialized Claude Code agents for different types of Haskell refactoring. Each agent has focused expertise to provide targeted improvements. Docster is a Haskell CLI tool: Markdown + embedded Mermaid diagrams → PDF, HTML, or DOCX.
## Available Agents ## Quick Commands
### haskell-refactoring-expert
**Purpose**: Basic code quality and structural improvements
**Expertise**:
- Type consistency (String vs Text vs ByteString)
- Module organization and file splitting (>150 lines)
- Naming conventions and clarity
- Dependency management
- Basic code structure improvements
**When to use**:
- Inconsistent type usage across the codebase
- Large files that need module organization
- Poor naming or unclear function responsibilities
- Mixed concerns in single modules
**Example**: Converting a 300-line Main.hs into proper module hierarchy
### haskell-higher-order
**Purpose**: Advanced functional programming patterns and architectural refactoring
**Expertise**:
- Monad transformer patterns (ExceptT, ReaderT, StateT)
- Pipeline composition with monadic operators
- Higher-order abstractions and strategy patterns
- Effect management and pure/IO separation
- Functional design patterns
**When to use**:
- Nested case statements handling Either values in IO
- Duplicated functions that differ only in specific steps
- Manual threading of configuration or state
- Imperative-style code that could be more functional
- Complex error handling that needs cleanup
**Example**: Converting nested Either/IO handling to ExceptT pipelines
## Agent Boundaries and Trade-offs
### Complementary Design
These agents are designed to work **sequentially**:
1. **First pass**: `haskell-refactoring-expert` for structural cleanup
2. **Second pass**: `haskell-higher-order` for functional patterns
### Why Separate Agents?
**Benefits**:
- **Focused expertise**: Each agent has deep knowledge in its domain
- **Clear boundaries**: Easy to know which agent to use
- **Manageable complexity**: Avoids instruction bloat in single agent
- **Progressive enhancement**: Apply increasingly sophisticated refactoring
- **Composability**: Can run both agents or just one as needed
**Trade-offs**:
- **Coordination overhead**: Need to run multiple agents
- **Context switching**: Each agent analyzes code independently
- **Potential overlap**: Some patterns might fit both agents
### Decision Framework
**Use haskell-refactoring-expert when you have**:
- ❌ Mixed String/Text types
- ❌ Large monolithic files (>150 lines)
- ❌ Unclear naming or responsibilities
- ❌ Basic structural issues
**Use haskell-higher-order when you have**:
- ❌ Nested error handling (Either in IO)
- ❌ Duplicated function structures
- ❌ Manual state/config threading
- ❌ Imperative-style patterns
**Use both agents when**:
- ❌ You want comprehensive refactoring
- ❌ Code has both structural and architectural issues
- ❌ You're doing major codebase improvements
## Usage Patterns
### Sequential Refactoring
```bash ```bash
# Run basic refactoring first stack build # build
/agent haskell-refactoring-expert "Please refactor the Main.hs file" stack test # run tests
stack exec docster -- -pdf file.md # convert to PDF
# Then apply advanced patterns stack exec docster -- -html file.md # convert to HTML
/agent haskell-higher-order "Please improve the error handling patterns" stack exec docster -- -docx file.md # convert to DOCX
stack exec docster -- -pdf sample.md # test with a single file
stack clean # clean build artifacts
``` ```
### Targeted Improvements ## Structure
```bash
# Just structural cleanup
/agent haskell-refactoring-expert "Split this large module"
# Just functional patterns ```
/agent haskell-higher-order "Convert these nested cases to monadic style" docster.cabal # package definition
stack.yaml # GHC 9.12.2, lts-24.34
app/Main.hs # everything — entry point + all logic (~70 lines)
test/ # HSpec tests (TransformSpec.hs)
``` ```
## Evolution Strategy ## How It Works
These agents can evolve independently: 1. Parse Markdown via Pandoc AST
- **haskell-refactoring-expert**: Add more structural patterns, linting rules 2. Walk the AST, find Mermaid code blocks
- **haskell-higher-order**: Add more advanced patterns (free monads, effect systems) 3. Run `mmdc` (mermaid-cli) to render each block → SVG (for HTML) or high-res PNG (for PDF)
4. Replace code blocks with image references in the AST
5. Compile final output via Pandoc (LaTeX/XeLaTeX for PDF, native for HTML/DOCX)
New specialized agents could be added: Key functions in `Main.hs`:
- **haskell-performance**: Optimization-focused refactoring - `transformDoc` — AST walker
- **haskell-testing**: Test-driven refactoring and property-based testing - `processMermaidBlock` — calls `mmdc`, returns image reference
- **haskell-domain**: Domain modeling and type design - `compileToPDF` / `compileToHTML` / `compileToDOCX` — final Pandoc compilation
The key is maintaining clear boundaries and complementary functionality. ## Dependencies
**System**: TeX Live (for PDF), `npm install -g @mermaid-js/mermaid-cli`
**Haskell**: Pandoc library, Stack manages GHC automatically
## Common Gotchas
- **Text vs String**: Codebase mixes `Data.Text` and `String`. Use `T.pack`/`T.unpack` for conversions.
- **PDF needs LaTeX**: BasicTeX/TinyTeX + `tlmgr` for missing packages.
- **mmdc in PATH**: `mermaid-cli` must be globally installed and on PATH.
+3 -2
View File
@@ -4,7 +4,7 @@
module Main (main) where module Main (main) where
import Docster.Types (DocsterError(..)) import Docster.Types (DocsterError(..))
import Docster.Compiler (compileToPDF, compileToHTML) import Docster.Compiler (compileToPDF, compileToHTML, compileToDOCX)
import System.Environment (getArgs) import System.Environment (getArgs)
import Control.Exception (throwIO) import Control.Exception (throwIO)
@@ -12,7 +12,8 @@ import Control.Exception (throwIO)
parseArgs :: [String] -> Either DocsterError (IO ()) parseArgs :: [String] -> Either DocsterError (IO ())
parseArgs ["-pdf", path] = Right (compileToPDF path) parseArgs ["-pdf", path] = Right (compileToPDF path)
parseArgs ["-html", path] = Right (compileToHTML path) parseArgs ["-html", path] = Right (compileToHTML path)
parseArgs _ = Left $ InvalidUsage "Usage: docster -pdf|-html <file.md>" parseArgs ["-docx", path] = Right (compileToDOCX path)
parseArgs _ = Left $ InvalidUsage "Usage: docster -pdf|-html|-docx <file.md>"
-- | Main entry point - parse arguments and execute appropriate action -- | Main entry point - parse arguments and execute appropriate action
main :: IO () main :: IO ()
+4 -4
View File
@@ -1,9 +1,9 @@
cabal-version: 3.0 cabal-version: 3.0
name: docster name: docster
version: 0.1.0.0 version: 0.1.0.0
synopsis: A self-contained CLI tool that converts Markdown with Mermaid diagrams to PDF/HTML 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 or HTML files 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 using Pandoc and Mermaid CLI. It automatically renders Mermaid code blocks to SVG (HTML) or PNG (PDF/DOCX)
and embeds them in the output. and embeds them in the output.
homepage: https://github.com/yourusername/docster homepage: https://github.com/yourusername/docster
license: BSD-3-Clause license: BSD-3-Clause
@@ -41,7 +41,7 @@ library
process >=1.6 && <1.7, process >=1.6 && <1.7,
hashable >=1.4 && <1.6, hashable >=1.4 && <1.6,
containers >=0.6 && <0.8, containers >=0.6 && <0.8,
pandoc >=3.0 && <3.2, pandoc >=3.0 && <3.8,
pandoc-types >=1.23 && <1.25, pandoc-types >=1.23 && <1.25,
bytestring >=0.11 && <0.13, bytestring >=0.11 && <0.13,
temporary >=1.3 && <1.4, temporary >=1.3 && <1.4,
+79 -46
View File
@@ -1,17 +1,20 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
-- | Document compilation functionality for PDF and HTML output -- | Document compilation functionality for PDF, HTML, and DOCX output
module Docster.Compiler module Docster.Compiler
( -- * Compilation Functions ( -- * Compilation Functions
compileToPDF compileToPDF
, compileToHTML , compileToHTML
, compileToDOCX
) where ) where
import Docster.Types import Docster.Types
( DocsterError(..), OutputFormat(..), SourceDir(..), OutputDir(..), OutputPath(..) ( DocsterError(..), OutputFormat(..), SourceDir(..), OutputDir(..), OutputPath(..)
, DiagramConfig(..), computeOutputDir, ensureOutputDir , DiagramConfig(..), computeOutputDir, ensureOutputDir
) )
import Text.Pandoc.Writers ()
import qualified Data.ByteString.Lazy as BSL
import Docster.Transform (transformDocument) import Docster.Transform (transformDocument)
import Docster.LaTeX (latexTemplate) import Docster.LaTeX (latexTemplate)
import Text.Pandoc import Text.Pandoc
@@ -30,6 +33,7 @@ import Control.Monad.Trans.Reader (ReaderT, runReaderT, asks)
import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Class (lift)
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.Maybe (mapMaybe) import Data.Maybe (mapMaybe)
import Data.Char (ord)
-- | Success indicator for user feedback -- | Success indicator for user feedback
successEmoji :: Text successEmoji :: Text
@@ -38,8 +42,6 @@ successEmoji = "✅"
-- | Compilation context for pipeline operations -- | Compilation context for pipeline operations
data CompilationContext = CompilationContext data CompilationContext = CompilationContext
{ ccStrategy :: CompilationStrategy { ccStrategy :: CompilationStrategy
, ccSourceDir :: SourceDir
, ccOutputDir :: OutputDir
, ccInputPath :: FilePath , ccInputPath :: FilePath
, ccOutputPath :: FilePath , ccOutputPath :: FilePath
, ccDocName :: Text , ccDocName :: Text
@@ -54,10 +56,10 @@ type CompilationM = ReaderT CompilationContext (ExceptT DocsterError IO)
data CompilationStrategy = CompilationStrategy data CompilationStrategy = CompilationStrategy
{ -- | Format for diagram configuration { -- | Format for diagram configuration
csOutputFormat :: OutputFormat csOutputFormat :: OutputFormat
-- | Pandoc writer function -- | Pandoc writer: writes output directly to the given file path
, csWriter :: WriterOptions -> Pandoc -> PandocIO Text , csWriter :: WriterOptions -> Pandoc -> FilePath -> IO (Either DocsterError ())
-- | Post-processing function for the generated content -- | Post-processing after write (PDF→xelatex, HTML→open browser, DOCX→noop)
, csProcessOutput :: String -> Text -> IO (Either DocsterError ()) , csPostProcess :: String -> IO (Either DocsterError ())
-- | Success message formatter -- | Success message formatter
, csSuccessMessage :: String -> Text , csSuccessMessage :: String -> Text
} }
@@ -66,8 +68,14 @@ data CompilationStrategy = CompilationStrategy
pdfStrategy :: CompilationStrategy pdfStrategy :: CompilationStrategy
pdfStrategy = CompilationStrategy pdfStrategy = CompilationStrategy
{ csOutputFormat = PDF { csOutputFormat = PDF
, csWriter = writeLaTeX , csWriter = \opts doc path -> do
, csProcessOutput = processPDFOutput 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 , csSuccessMessage = \path -> successEmoji <> " PDF written to " <> T.pack path
} }
@@ -75,11 +83,32 @@ pdfStrategy = CompilationStrategy
htmlStrategy :: CompilationStrategy htmlStrategy :: CompilationStrategy
htmlStrategy = CompilationStrategy htmlStrategy = CompilationStrategy
{ csOutputFormat = HTML { csOutputFormat = HTML
, csWriter = writeHtml5String , csWriter = \opts doc path -> do
, csProcessOutput = processHTMLOutput 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 , csSuccessMessage = \path -> successEmoji <> " HTML written to " <> T.pack path
} }
-- | DOCX compilation strategy (Pandoc writes file directly)
docxStrategy :: CompilationStrategy
docxStrategy = CompilationStrategy
{ csOutputFormat = DOCX
, 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
}
-- | Parse LaTeX log content to extract meaningful error messages -- | Parse LaTeX log content to extract meaningful error messages
parseLatexErrors :: Text -> Text parseLatexErrors :: Text -> Text
parseLatexErrors logContent = parseLatexErrors logContent =
@@ -143,11 +172,10 @@ extractFatalErrors = mapMaybe extractFatal
| "! " `T.isPrefixOf` line && not ("Missing character:" `T.isInfixOf` line) = Just $ T.drop 2 line | "! " `T.isPrefixOf` line && not ("Missing character:" `T.isInfixOf` line) = Just $ T.drop 2 line
| otherwise = Nothing | otherwise = Nothing
-- | Process PDF output: LaTeX template application and direct XeLaTeX compilation -- | Process PDF output: direct XeLaTeX compilation (LaTeX already written by csWriter)
processPDFOutput :: String -> Text -> IO (Either DocsterError ()) processPDFOutput :: String -> IO (Either DocsterError ())
processPDFOutput outputPath latexOutput = do processPDFOutput outputPath = do
let completeLatex = latexTemplate latexOutput let logOutputPath = replaceExtension outputPath "log"
logOutputPath = replaceExtension outputPath "log"
-- Use temporary directory for LaTeX compilation -- Use temporary directory for LaTeX compilation
withSystemTempDirectory "docster-latex" $ \tempDir -> do withSystemTempDirectory "docster-latex" $ \tempDir -> do
@@ -155,9 +183,6 @@ processPDFOutput outputPath latexOutput = do
pdfFile = tempDir </> "document.pdf" pdfFile = tempDir </> "document.pdf"
logFile = tempDir </> "document.log" logFile = tempDir </> "document.log"
-- Write LaTeX content to temporary file
TIO.writeFile texFile completeLatex
-- Run XeLaTeX compilation -- Run XeLaTeX compilation
(exitCode, _stdout, stderr) <- readProcessWithExitCode "xelatex" (exitCode, _stdout, stderr) <- readProcessWithExitCode "xelatex"
[ "-output-directory=" <> tempDir [ "-output-directory=" <> tempDir
@@ -193,11 +218,9 @@ processPDFOutput outputPath latexOutput = do
errorSummary <> "\n\n" <> errorSummary <> "\n\n" <>
"Full LaTeX log written to: " <> T.pack logOutputPath "Full LaTeX log written to: " <> T.pack logOutputPath
-- | Process HTML output: file writing and browser opening -- | Process HTML output: open browser (HTML already written by csWriter)
processHTMLOutput :: String -> Text -> IO (Either DocsterError ()) processHTMLOutput :: String -> IO (Either DocsterError ())
processHTMLOutput outputPath html = do processHTMLOutput outputPath = do
TIO.writeFile outputPath html
-- Open the generated HTML file in browser for verification -- Open the generated HTML file in browser for verification
putStrLn $ "🌐 Opening " <> outputPath <> " in browser for error checking..." putStrLn $ "🌐 Opening " <> outputPath <> " in browser for error checking..."
void $ callProcess "open" [outputPath] void $ callProcess "open" [outputPath]
@@ -212,11 +235,30 @@ liftEitherM action = do
Left err -> lift $ throwE err Left err -> lift $ throwE err
Right value -> return value Right value -> return value
-- | Strip ANSI escape sequences (CSI codes like color/style) from text.
-- These appear in copy-pasted terminal output and break LaTeX compilation.
stripAnsiCodes :: Text -> Text
stripAnsiCodes input = case T.break (== '\x1b') input of
(before, rest)
| T.null rest -> before
| otherwise -> before <> stripAnsiCodes (skipEscape (T.tail rest))
where
-- Skip an ESC sequence: ESC [ <params> <final byte>
skipEscape t
| T.null t = t
| T.head t == '[' = skipCSIParams (T.tail t)
| otherwise = T.tail t -- non-CSI escape: skip one char after ESC
-- Skip CSI parameter/intermediate bytes until final byte (0x40-0x7E)
skipCSIParams t
| T.null t = t
| let c = ord (T.head t), c >= 0x40 && c <= 0x7E = T.tail t -- final byte, consume it
| otherwise = skipCSIParams (T.tail t)
-- | Pipeline step: Read content from input file -- | Pipeline step: Read content from input file
readContent :: CompilationM Text readContent :: CompilationM Text
readContent = do readContent = do
inputPath <- asks ccInputPath inputPath <- asks ccInputPath
liftIO $ TIO.readFile inputPath liftIO $ stripAnsiCodes <$> TIO.readFile inputPath
-- | Pipeline step: Parse markdown content into Pandoc AST -- | Pipeline step: Parse markdown content into Pandoc AST
parseDocument :: Text -> CompilationM Pandoc parseDocument :: Text -> CompilationM Pandoc
@@ -231,18 +273,13 @@ transformDocumentM pandoc = do
docName <- asks ccDocName docName <- asks ccDocName
liftEitherM $ transformDocument config docName pandoc liftEitherM $ transformDocument config docName pandoc
-- | Pipeline step: Generate output using format-specific writer -- | Pipeline step: Write output and post-process (format-specific)
generateOutputM :: Pandoc -> CompilationM Text writeAndProcessOutput :: Pandoc -> CompilationM ()
generateOutputM pandoc = do writeAndProcessOutput pandoc = do
strategy <- asks ccStrategy
liftEitherM $ generateOutput strategy pandoc
-- | Pipeline step: Process output and write to file
processOutput :: Text -> CompilationM ()
processOutput output = do
strategy <- asks ccStrategy strategy <- asks ccStrategy
outputPath <- asks ccOutputPath outputPath <- asks ccOutputPath
liftEitherM $ csProcessOutput strategy outputPath output liftEitherM $ csWriter strategy def pandoc outputPath
liftEitherM $ (csPostProcess strategy) outputPath
-- | Pipeline step: Print success message -- | Pipeline step: Print success message
printSuccess :: CompilationM () printSuccess :: CompilationM ()
@@ -256,8 +293,8 @@ compileWithStrategy :: CompilationStrategy -> SourceDir -> OutputDir -> Text ->
compileWithStrategy strategy sourceDir outputDir docName (OutputPath inputPath) (OutputPath outputPath) = do compileWithStrategy strategy sourceDir outputDir docName (OutputPath inputPath) (OutputPath outputPath) = do
let readerOptions = def { readerExtensions = getDefaultExtensions "markdown" } let readerOptions = def { readerExtensions = getDefaultExtensions "markdown" }
config = DiagramConfig sourceDir outputDir (csOutputFormat strategy) config = DiagramConfig sourceDir outputDir (csOutputFormat strategy)
context = CompilationContext strategy sourceDir outputDir inputPath outputPath docName readerOptions config context = CompilationContext strategy inputPath outputPath docName readerOptions config
pipeline = readContent >>= parseDocument >>= transformDocumentM >>= generateOutputM >>= processOutput >> printSuccess pipeline = readContent >>= parseDocument >>= transformDocumentM >>= writeAndProcessOutput >> printSuccess
runExceptT $ runReaderT pipeline context runExceptT $ runReaderT pipeline context
@@ -269,15 +306,7 @@ parseMarkdown readerOptions content = do
Left err -> Left $ FileError $ "Failed to parse markdown: " <> T.pack (show err) Left err -> Left $ FileError $ "Failed to parse markdown: " <> T.pack (show err)
Right pandoc -> Right pandoc 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)
Right output -> Right output
-- | Compile markdown to PDF using XeLaTeX -- | Compile markdown to PDF using XeLaTeX
compileToPDF :: FilePath -> IO () compileToPDF :: FilePath -> IO ()
@@ -287,6 +316,10 @@ compileToPDF = compileWithFormat pdfStrategy "pdf"
compileToHTML :: FilePath -> IO () compileToHTML :: FilePath -> IO ()
compileToHTML = compileWithFormat htmlStrategy "html" compileToHTML = compileWithFormat htmlStrategy "html"
-- | Compile markdown to DOCX
compileToDOCX :: FilePath -> IO ()
compileToDOCX = compileWithFormat docxStrategy "docx"
-- | Higher-order function to compile with any format strategy -- | Higher-order function to compile with any format strategy
compileWithFormat :: CompilationStrategy -> String -> FilePath -> IO () compileWithFormat :: CompilationStrategy -> String -> FilePath -> IO ()
compileWithFormat strategy extension path = do compileWithFormat strategy extension path = do
+37 -9
View File
@@ -9,15 +9,12 @@ module Docster.LaTeX
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
-- | LaTeX template with comprehensive package support for PDF generation -- | LaTeX template with modern corporate styling for PDF generation
latexTemplate :: Text -> Text latexTemplate :: Text -> Text
latexTemplate bodyContent = T.unlines latexTemplate bodyContent = T.unlines
[ "\\documentclass{article}" [ "\\documentclass{article}"
, "\\usepackage[utf8]{inputenc}" -- Packages
, "\\usepackage{fontspec}" , "\\usepackage{fontspec}"
, "\\setmainfont{DejaVu Serif}[Scale=1.0]"
, "\\setsansfont{DejaVu Sans}[Scale=1.0]"
, "\\setmonofont{DejaVu Sans Mono}[Scale=0.85]"
, "\\usepackage{graphicx}" , "\\usepackage{graphicx}"
, "\\usepackage{adjustbox}" , "\\usepackage{adjustbox}"
, "\\usepackage{geometry}" , "\\usepackage{geometry}"
@@ -25,16 +22,46 @@ latexTemplate bodyContent = T.unlines
, "\\usepackage{booktabs}" , "\\usepackage{booktabs}"
, "\\usepackage{array}" , "\\usepackage{array}"
, "\\usepackage{calc}" , "\\usepackage{calc}"
, "\\geometry{margin=1in}"
, "\\usepackage{hyperref}"
, "\\usepackage{enumitem}" , "\\usepackage{enumitem}"
, "\\usepackage{amsmath}" , "\\usepackage{amsmath}"
, "\\usepackage{amssymb}" , "\\usepackage{amssymb}"
, "\\usepackage{fancyvrb}" , "\\usepackage{fancyvrb}"
, "\\usepackage{color}" , "\\usepackage[dvipsnames,svgnames,x11names]{xcolor}"
, "\\usepackage{titlesec}"
, "\\usepackage{fancyhdr}"
, "\\usepackage{framed}"
-- Typography: Helvetica Neue + Menlo, sans-serif default
, "\\setmainfont{Helvetica Neue}"
, "\\setsansfont{Helvetica Neue}"
, "\\setmonofont{Menlo}[Scale=0.85]"
, "\\renewcommand{\\familydefault}{\\sfdefault}"
-- Layout: wider margins, block paragraphs
, "\\geometry{left=0.9in,right=0.9in,top=1in,bottom=1in}"
, "\\setlength{\\parindent}{0pt}"
, "\\setlength{\\parskip}{0.5em}"
-- Color scheme
, "\\definecolor{accent}{HTML}{1A365D}"
, "\\definecolor{codebg}{HTML}{F5F5F5}"
-- Hyperlinks: accent-colored, no boxes
, "\\usepackage[colorlinks=true,linkcolor=accent,urlcolor=accent,citecolor=accent]{hyperref}"
-- Heading styles
, "\\titleformat{\\section}{\\Large\\bfseries\\color{accent}}{\\thesection}{1em}{}[\\vspace{2pt}\\titlerule]"
, "\\titleformat{\\subsection}{\\large\\bfseries\\color{accent}}{\\thesubsection}{1em}{}"
, "\\titleformat{\\subsubsection}{\\normalsize\\bfseries\\color{accent}}{\\thesubsubsection}{1em}{}"
, "\\titlespacing*{\\section}{0pt}{1.5em}{0.8em}"
, "\\titlespacing*{\\subsection}{0pt}{1.2em}{0.5em}"
, "\\titlespacing*{\\subsubsection}{0pt}{1em}{0.4em}"
-- Page header/footer: minimal centered page number
, "\\pagestyle{fancy}"
, "\\fancyhf{}"
, "\\renewcommand{\\headrulewidth}{0pt}"
, "\\fancyfoot[C]{\\small\\thepage}"
-- Code blocks: light gray background
, "\\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\}}" , "\\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\}}"
, "\\newenvironment{Shaded}{}{}" , "\\newenvironment{Shaded}{\\begin{snugshade}}{\\end{snugshade}}"
, "\\definecolor{shadecolor}{HTML}{F5F5F5}"
, syntaxHighlightingCommands , syntaxHighlightingCommands
-- Pandoc helpers
, "\\providecommand{\\tightlist}{%" , "\\providecommand{\\tightlist}{%"
, " \\setlength{\\itemsep}{0pt}\\setlength{\\parskip}{0pt}}" , " \\setlength{\\itemsep}{0pt}\\setlength{\\parskip}{0pt}}"
, "\\newcommand{\\real}[1]{#1}" , "\\newcommand{\\real}[1]{#1}"
@@ -47,6 +74,7 @@ latexTemplate bodyContent = T.unlines
, "\\def\\maxheight{\\ifdim\\Gin@nat@height>\\textheight\\textheight\\else\\Gin@nat@height\\fi}" , "\\def\\maxheight{\\ifdim\\Gin@nat@height>\\textheight\\textheight\\else\\Gin@nat@height\\fi}"
, "\\makeatother" , "\\makeatother"
, "\\setkeys{Gin}{width=\\maxwidth,height=\\maxheight,keepaspectratio}" , "\\setkeys{Gin}{width=\\maxwidth,height=\\maxheight,keepaspectratio}"
, "\\providecommand{\\pandocbounded}[1]{#1}"
, "\\begin{document}" , "\\begin{document}"
, bodyContent , bodyContent
, "\\end{document}" , "\\end{document}"
+3
View File
@@ -79,6 +79,8 @@ generateDiagramPaths (DiagramConfig _ (OutputDir outDir) format) (DiagramId diag
in (svgFile, T.pack $ takeFileName svgFile) in (svgFile, T.pack $ takeFileName svgFile)
PDF -> let pngFile = outDir </> diagIdStr <> ".png" PDF -> let pngFile = outDir </> diagIdStr <> ".png"
in (pngFile, T.pack pngFile) in (pngFile, T.pack pngFile)
DOCX -> let pngFile = outDir </> diagIdStr <> ".png"
in (pngFile, T.pack pngFile)
-- | Puppeteer configuration content for disabling sandbox -- | Puppeteer configuration content for disabling sandbox
puppeteerConfigContent :: Text puppeteerConfigContent :: Text
@@ -90,6 +92,7 @@ callMermaidProcess format mmdFile outputFile = do
let baseArgs = case format of let baseArgs = case format of
HTML -> ["-i", mmdFile, "-o", outputFile] HTML -> ["-i", mmdFile, "-o", outputFile]
PDF -> ["-i", mmdFile, "-o", outputFile, "--scale", "3"] PDF -> ["-i", mmdFile, "-o", outputFile, "--scale", "3"]
DOCX -> ["-i", mmdFile, "-o", outputFile]
-- Create temporary puppeteer config file -- Create temporary puppeteer config file
result <- bracket result <- bracket
+1
View File
@@ -38,6 +38,7 @@ transformDocument config docName (Pandoc meta blocks) = do
case dcOutputFormat config of case dcOutputFormat config of
PDF -> return $ Right $ substituteUnicodeSymbols (Pandoc meta newBlocks) PDF -> return $ Right $ substituteUnicodeSymbols (Pandoc meta newBlocks)
HTML -> return $ Right $ Pandoc meta newBlocks HTML -> return $ Right $ Pandoc meta newBlocks
DOCX -> return $ Right $ Pandoc meta newBlocks
-- | Process a single block with heading tracking state -- | Process a single block with heading tracking state
processBlockStateful :: DiagramConfig -> Block -> TransformM Block processBlockStateful :: DiagramConfig -> Block -> TransformM Block
Binary file not shown.
+1 -1
View File
@@ -45,7 +45,7 @@ data DocsterError
instance Exception DocsterError instance Exception DocsterError
-- | Output format for document generation -- | Output format for document generation
data OutputFormat = PDF | HTML data OutputFormat = PDF | HTML | DOCX
deriving (Show, Eq) deriving (Show, Eq)
-- | Type-safe wrapper for source directory paths -- | Type-safe wrapper for source directory paths
+6 -6
View File
@@ -4,7 +4,7 @@ module Docster.TransformSpec (spec) where
import Test.Hspec import Test.Hspec
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Data.Text (Text) import Data.Text()
import qualified Data.Text as T import qualified Data.Text as T
import Text.Pandoc.Definition (Inline(..)) import Text.Pandoc.Definition (Inline(..))
@@ -64,21 +64,21 @@ spec = do
describe "diagram naming logic" $ do describe "diagram naming logic" $ do
it "first diagram under heading has no suffix" $ it "first diagram under heading has no suffix" $
let baseName = "file_flow" 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) diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter)
in diagName `shouldBe` "file_flow" in diagName `shouldBe` "file_flow"
it "second diagram gets _1 suffix" $ it "second diagram gets _1 suffix" $
let baseName = "file_flow" let baseName = "file_flow"
counters = Map.singleton "file_flow" 1 counters = Map.singleton "file_flow" (1 :: Int)
counter = Map.findWithDefault 0 baseName counters counter = Map.findWithDefault (0 :: Int) baseName counters
diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter) diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter)
in diagName `shouldBe` "file_flow_1" in diagName `shouldBe` "file_flow_1"
it "third diagram gets _2 suffix" $ it "third diagram gets _2 suffix" $
let baseName = "file_flow" let baseName = "file_flow"
counters = Map.singleton "file_flow" 2 counters = Map.singleton "file_flow" (2 :: Int)
counter = Map.findWithDefault 0 baseName counters counter = Map.findWithDefault (0 :: Int) baseName counters
diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter) diagName = if counter == 0 then baseName else baseName <> "_" <> T.pack (show counter)
in diagName `shouldBe` "file_flow_2" in diagName `shouldBe` "file_flow_2"