Compare commits
10
Commits
dda2fc15b2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54c7502f0f | ||
|
|
eb374d20f5 | ||
|
|
57f4f9f165 | ||
|
|
f016950ac7 | ||
|
|
f4dab3e354 | ||
|
|
6b49db5801 | ||
|
|
b0457388dc | ||
|
|
8abe1d1bc2 | ||
|
|
9dd9313829 | ||
|
|
fa850d5017 |
+10
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
# Run basic refactoring first
|
||||
/agent haskell-refactoring-expert "Please refactor the Main.hs file"
|
||||
|
||||
# Then apply advanced patterns
|
||||
/agent haskell-higher-order "Please improve the error handling patterns"
|
||||
stack build # build
|
||||
stack test # run tests
|
||||
stack exec docster -- -pdf file.md # convert to PDF
|
||||
stack exec docster -- -html file.md # convert to HTML
|
||||
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
|
||||
```bash
|
||||
# Just structural cleanup
|
||||
/agent haskell-refactoring-expert "Split this large module"
|
||||
## Structure
|
||||
|
||||
# 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:
|
||||
- **haskell-refactoring-expert**: Add more structural patterns, linting rules
|
||||
- **haskell-higher-order**: Add more advanced patterns (free monads, effect systems)
|
||||
1. Parse Markdown via Pandoc AST
|
||||
2. Walk the AST, find Mermaid code blocks
|
||||
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:
|
||||
- **haskell-performance**: Optimization-focused refactoring
|
||||
- **haskell-testing**: Test-driven refactoring and property-based testing
|
||||
- **haskell-domain**: Domain modeling and type design
|
||||
Key functions in `Main.hs`:
|
||||
- `transformDoc` — AST walker
|
||||
- `processMermaidBlock` — calls `mmdc`, returns image reference
|
||||
- `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
@@ -4,7 +4,7 @@
|
||||
module Main (main) where
|
||||
|
||||
import Docster.Types (DocsterError(..))
|
||||
import Docster.Compiler (compileToPDF, compileToHTML)
|
||||
import Docster.Compiler (compileToPDF, compileToHTML, compileToDOCX)
|
||||
import System.Environment (getArgs)
|
||||
import Control.Exception (throwIO)
|
||||
|
||||
@@ -12,7 +12,8 @@ import Control.Exception (throwIO)
|
||||
parseArgs :: [String] -> Either DocsterError (IO ())
|
||||
parseArgs ["-pdf", path] = Right (compileToPDF 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 :: IO ()
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+79
-46
@@ -1,17 +1,20 @@
|
||||
{-# 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
|
||||
, compileToHTML
|
||||
, compileToDOCX
|
||||
) where
|
||||
|
||||
import Docster.Types
|
||||
( DocsterError(..), OutputFormat(..), SourceDir(..), OutputDir(..), OutputPath(..)
|
||||
, DiagramConfig(..), computeOutputDir, ensureOutputDir
|
||||
)
|
||||
import Text.Pandoc.Writers ()
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import Docster.Transform (transformDocument)
|
||||
import Docster.LaTeX (latexTemplate)
|
||||
import Text.Pandoc
|
||||
@@ -30,6 +33,7 @@ import Control.Monad.Trans.Reader (ReaderT, runReaderT, asks)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.Char (ord)
|
||||
|
||||
-- | Success indicator for user feedback
|
||||
successEmoji :: Text
|
||||
@@ -38,8 +42,6 @@ successEmoji = "✅"
|
||||
-- | Compilation context for pipeline operations
|
||||
data CompilationContext = CompilationContext
|
||||
{ ccStrategy :: CompilationStrategy
|
||||
, ccSourceDir :: SourceDir
|
||||
, ccOutputDir :: OutputDir
|
||||
, ccInputPath :: FilePath
|
||||
, ccOutputPath :: FilePath
|
||||
, ccDocName :: Text
|
||||
@@ -54,10 +56,10 @@ type CompilationM = ReaderT CompilationContext (ExceptT DocsterError IO)
|
||||
data CompilationStrategy = CompilationStrategy
|
||||
{ -- | Format for diagram configuration
|
||||
csOutputFormat :: OutputFormat
|
||||
-- | Pandoc writer function
|
||||
, 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
|
||||
}
|
||||
@@ -66,8 +68,14 @@ data CompilationStrategy = CompilationStrategy
|
||||
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
|
||||
}
|
||||
|
||||
@@ -75,11 +83,32 @@ pdfStrategy = CompilationStrategy
|
||||
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
|
||||
}
|
||||
|
||||
-- | 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
|
||||
parseLatexErrors :: Text -> Text
|
||||
parseLatexErrors logContent =
|
||||
@@ -143,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
|
||||
@@ -155,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
|
||||
@@ -193,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]
|
||||
@@ -212,11 +235,30 @@ liftEitherM action = do
|
||||
Left err -> lift $ throwE err
|
||||
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
|
||||
readContent :: CompilationM Text
|
||||
readContent = do
|
||||
inputPath <- asks ccInputPath
|
||||
liftIO $ TIO.readFile inputPath
|
||||
liftIO $ stripAnsiCodes <$> TIO.readFile inputPath
|
||||
|
||||
-- | Pipeline step: Parse markdown content into Pandoc AST
|
||||
parseDocument :: Text -> CompilationM Pandoc
|
||||
@@ -231,18 +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
|
||||
strategy <- asks ccStrategy
|
||||
liftEitherM $ generateOutput strategy pandoc
|
||||
|
||||
-- | Pipeline step: Process output and write to file
|
||||
processOutput :: Text -> CompilationM ()
|
||||
processOutput output = do
|
||||
-- | Pipeline step: Write output and post-process (format-specific)
|
||||
writeAndProcessOutput :: Pandoc -> CompilationM ()
|
||||
writeAndProcessOutput pandoc = do
|
||||
strategy <- asks ccStrategy
|
||||
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 ()
|
||||
@@ -256,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 sourceDir outputDir inputPath outputPath docName readerOptions config
|
||||
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
|
||||
|
||||
@@ -269,15 +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)
|
||||
Right output -> Right output
|
||||
|
||||
|
||||
-- | Compile markdown to PDF using XeLaTeX
|
||||
compileToPDF :: FilePath -> IO ()
|
||||
@@ -287,6 +316,10 @@ compileToPDF = compileWithFormat pdfStrategy "pdf"
|
||||
compileToHTML :: FilePath -> IO ()
|
||||
compileToHTML = compileWithFormat htmlStrategy "html"
|
||||
|
||||
-- | Compile markdown to DOCX
|
||||
compileToDOCX :: FilePath -> IO ()
|
||||
compileToDOCX = compileWithFormat docxStrategy "docx"
|
||||
|
||||
-- | Higher-order function to compile with any format strategy
|
||||
compileWithFormat :: CompilationStrategy -> String -> FilePath -> IO ()
|
||||
compileWithFormat strategy extension path = do
|
||||
|
||||
+37
-9
@@ -9,15 +9,12 @@ module Docster.LaTeX
|
||||
import Data.Text (Text)
|
||||
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 bodyContent = T.unlines
|
||||
[ "\\documentclass{article}"
|
||||
, "\\usepackage[utf8]{inputenc}"
|
||||
-- Packages
|
||||
, "\\usepackage{fontspec}"
|
||||
, "\\setmainfont{DejaVu Serif}[Scale=1.0]"
|
||||
, "\\setsansfont{DejaVu Sans}[Scale=1.0]"
|
||||
, "\\setmonofont{DejaVu Sans Mono}[Scale=0.85]"
|
||||
, "\\usepackage{graphicx}"
|
||||
, "\\usepackage{adjustbox}"
|
||||
, "\\usepackage{geometry}"
|
||||
@@ -25,16 +22,46 @@ latexTemplate bodyContent = T.unlines
|
||||
, "\\usepackage{booktabs}"
|
||||
, "\\usepackage{array}"
|
||||
, "\\usepackage{calc}"
|
||||
, "\\geometry{margin=1in}"
|
||||
, "\\usepackage{hyperref}"
|
||||
, "\\usepackage{enumitem}"
|
||||
, "\\usepackage{amsmath}"
|
||||
, "\\usepackage{amssymb}"
|
||||
, "\\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=\\\\\\{\\}}"
|
||||
, "\\newenvironment{Shaded}{}{}"
|
||||
, "\\newenvironment{Shaded}{\\begin{snugshade}}{\\end{snugshade}}"
|
||||
, "\\definecolor{shadecolor}{HTML}{F5F5F5}"
|
||||
, syntaxHighlightingCommands
|
||||
-- Pandoc helpers
|
||||
, "\\providecommand{\\tightlist}{%"
|
||||
, " \\setlength{\\itemsep}{0pt}\\setlength{\\parskip}{0pt}}"
|
||||
, "\\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}"
|
||||
, "\\makeatother"
|
||||
, "\\setkeys{Gin}{width=\\maxwidth,height=\\maxheight,keepaspectratio}"
|
||||
, "\\providecommand{\\pandocbounded}[1]{#1}"
|
||||
, "\\begin{document}"
|
||||
, bodyContent
|
||||
, "\\end{document}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,8 +36,9 @@ transformDocument config docName (Pandoc meta blocks) = do
|
||||
Left err -> return $ Left err
|
||||
Right (newBlocks, _finalState) ->
|
||||
case dcOutputFormat config of
|
||||
PDF -> return $ Right $ substituteUnicodeSymbols (Pandoc meta newBlocks)
|
||||
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
|
||||
|
||||
Binary file not shown.
@@ -45,7 +45,7 @@ data DocsterError
|
||||
instance Exception DocsterError
|
||||
|
||||
-- | Output format for document generation
|
||||
data OutputFormat = PDF | HTML
|
||||
data OutputFormat = PDF | HTML | DOCX
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | Type-safe wrapper for source directory paths
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user