diff --git a/app/Main.hs b/app/Main.hs index 32c5d2e..54e029a 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -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 " +parseArgs ["-docx", path] = Right (compileToDOCX path) +parseArgs _ = Left $ InvalidUsage "Usage: docster -pdf|-html|-docx " -- | Main entry point - parse arguments and execute appropriate action main :: IO () diff --git a/src/Docster/Compiler.hs b/src/Docster/Compiler.hs index a757345..e7220e5 100644 --- a/src/Docster/Compiler.hs +++ b/src/Docster/Compiler.hs @@ -6,12 +6,15 @@ module Docster.Compiler ( -- * Compilation Functions compileToPDF , compileToHTML + , compileToDOCX ) where import Docster.Types ( DocsterError(..), OutputFormat(..), SourceDir(..), OutputDir(..), OutputPath(..) , DiagramConfig(..), computeOutputDir, ensureOutputDir ) +import Text.Pandoc.Writers (writeDocx) +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,13 +42,12 @@ successEmoji = "✅" -- | Compilation context for pipeline operations data CompilationContext = CompilationContext { ccStrategy :: CompilationStrategy - , ccSourceDir :: SourceDir - , ccOutputDir :: OutputDir , ccInputPath :: FilePath , ccOutputPath :: FilePath , ccDocName :: Text , ccReaderOptions :: ReaderOptions , ccConfig :: DiagramConfig + , ccWritesFile :: Bool } -- | Monad stack for compilation pipeline @@ -54,12 +57,14 @@ type CompilationM = ReaderT CompilationContext (ExceptT DocsterError IO) data CompilationStrategy = CompilationStrategy { -- | Format for diagram configuration csOutputFormat :: OutputFormat - -- | Pandoc writer function + -- | 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 ()) -- | Success message formatter , csSuccessMessage :: String -> Text + -- | True for formats where writer writes a file directly (DOCX) + , csWritesFile :: Bool } -- | PDF compilation strategy @@ -69,6 +74,7 @@ pdfStrategy = CompilationStrategy , csWriter = writeLaTeX , csProcessOutput = processPDFOutput , csSuccessMessage = \path -> successEmoji <> " PDF written to " <> T.pack path + , csWritesFile = False } -- | HTML compilation strategy @@ -78,6 +84,17 @@ htmlStrategy = CompilationStrategy , csWriter = writeHtml5String , csProcessOutput = 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 + , csSuccessMessage = \path -> successEmoji <> " DOCX written to " <> T.pack path + , csWritesFile = True } -- | Parse LaTeX log content to extract meaningful error messages @@ -212,11 +229,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 [ + 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 @@ -235,14 +271,24 @@ transformDocumentM pandoc = do generateOutputM :: Pandoc -> CompilationM Text generateOutputM pandoc = do strategy <- asks ccStrategy - liftEitherM $ generateOutput strategy pandoc + 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 - outputPath <- asks ccOutputPath - liftEitherM $ csProcessOutput strategy outputPath output + writesFile <- asks ccWritesFile + if writesFile + then return () -- file already written by writer + else do + outputPath <- asks ccOutputPath + liftEitherM $ csProcessOutput strategy outputPath output -- | Pipeline step: Print success message printSuccess :: CompilationM () @@ -256,7 +302,7 @@ 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 + context = CompilationContext strategy inputPath outputPath docName readerOptions config (csWritesFile strategy) pipeline = readContent >>= parseDocument >>= transformDocumentM >>= generateOutputM >>= processOutput >> printSuccess runExceptT $ runReaderT pipeline context @@ -277,8 +323,19 @@ generateOutput strategy transformed = do 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 strategy 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 () compileToPDF = compileWithFormat pdfStrategy "pdf" @@ -287,6 +344,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 diff --git a/src/Docster/LaTeX.hs b/src/Docster/LaTeX.hs index b1cca27..d9a44ce 100644 --- a/src/Docster/LaTeX.hs +++ b/src/Docster/LaTeX.hs @@ -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}" diff --git a/src/Docster/Types.hi b/src/Docster/Types.hi new file mode 100644 index 0000000..340a0e3 Binary files /dev/null and b/src/Docster/Types.hi differ diff --git a/src/Docster/Types.hs b/src/Docster/Types.hs index f42e4cf..ef55a10 100644 --- a/src/Docster/Types.hs +++ b/src/Docster/Types.hs @@ -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