summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcchalmers <>2015-04-11 19:32:00 (GMT)
committerhdiff <hdiff@hdiff.luite.com>2015-04-11 19:32:00 (GMT)
commitff4a43bba0aac4bdcc29b1166dc378827ea9c688 (patch)
treed064e5049e96e2e22e37c8e3fd3544ea6e9c0d94
version 0.0.1.00.0.1.0
-rw-r--r--LICENSE30
-rw-r--r--README.md14
-rw-r--r--Setup.hs2
-rw-r--r--src/System/Texrunner.hs109
-rw-r--r--src/System/Texrunner/Online.hs207
-rw-r--r--src/System/Texrunner/Parse.hs324
-rw-r--r--tests/Tests.hs9
-rw-r--r--tests/Tex/LogParse.hs160
-rw-r--r--tests/Tex/PDF.hs64
-rw-r--r--texrunner.cabal62
10 files changed, 981 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..06cd9de
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2014, Chris Chalmers
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of Chris Chalmers nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..07ca5c2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,14 @@
+texrunner
+=========
+
+texrunner provides a (hopefully) convenient interface to Tex including
+functions for running Tex, parsing log files.
+
+The online module allows retrieving results of commands, such as
+dimensions of an `hbox`, using Tex's interactive features.
+
+![hbox dimensions recovered from texrunner](https://rawgit.com/cchalmers/texrunner/master/diagrams/hbox.svg)
+
+This package is a work in progress and likely contains lots of bugs.
+Eventually I hope it will be good enough for general use but for now
+it's only suitable for simple things.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
index 0000000..9a994af
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/src/System/Texrunner.hs b/src/System/Texrunner.hs
new file mode 100644
index 0000000..0f659c5
--- /dev/null
+++ b/src/System/Texrunner.hs
@@ -0,0 +1,109 @@
+----------------------------------------------------------------------------
+-- |
+-- Module : System.Texrunner
+-- Copyright : (c) 2014 Christopher Chalmers
+-- License : BSD-style (see LICENSE)
+-- Maintainer : c.chalmers@me.com
+--
+-- Functions for running Tex.
+--
+-----------------------------------------------------------------------------
+
+module System.Texrunner
+ ( runTex
+ , runTex'
+ , prettyPrintLog
+ ) where
+
+import Control.Applicative
+import qualified Data.ByteString.Char8 as C8 hiding (concatMap)
+import Data.ByteString.Lazy.Char8 as LC8 hiding (concatMap)
+import Data.Maybe
+
+import System.Directory
+import System.Environment
+import System.Exit
+import System.FilePath
+import System.IO
+import System.IO.Temp
+import System.Process
+
+import System.Texrunner.Parse
+
+-- | Same as 'runTex'' but runs Tex in a temporary system directory.
+runTex :: String -- ^ Tex command
+ -> [String] -- ^ Additional arguments
+ -> [FilePath] -- ^ Additional Tex input paths
+ -> ByteString -- ^ Source Tex file
+ -> IO (ExitCode, TexLog, Maybe ByteString)
+runTex command args extras source =
+ withSystemTempDirectory "texrunner." $ \path ->
+ runTex' path command args extras source
+
+-- | Run Tex program in the given directory. Additional Tex inputs are
+-- for filepaths to things like images that Tex can refer to.
+runTex' :: FilePath -- ^ Directory to run Tex in
+ -> String -- ^ Tex command
+ -> [String] -- ^ Additional arguments
+ -> [FilePath] -- ^ Additional Tex inputs
+ -> ByteString -- ^ Source Tex file
+ -> IO (ExitCode, TexLog, Maybe ByteString)
+runTex' path command args extras source = do
+
+ LC8.writeFile (path </> "texrunner.tex") source
+
+ environment <- extraTexInputs (path:extras) <$> getEnvironment
+
+ let p = (proc command ("texrunner.tex" : args))
+ { cwd = Just path
+ , std_in = CreatePipe
+ , std_out = CreatePipe
+ , env = Just environment
+ }
+
+ (Just inH, Just outH, _, pHandle) <- createProcess p
+
+ hClose inH
+ a <- C8.hGetContents outH -- backup log
+
+ hClose outH
+ exitC <- waitForProcess pHandle
+
+ pdfExists <- doesFileExist (path </> "texrunner.pdf")
+ pdfFile <- if pdfExists
+ then Just <$> LC8.readFile (path </> "texrunner.pdf")
+ else return Nothing
+
+ logExists <- doesFileExist (path </> "texrunner.log")
+ logFile <- if logExists
+ then Just <$> C8.readFile (path </> "texrunner.log")
+ else return Nothing
+
+ -- pdfFile <- optional $ LC8.readFile (path </> "texrunner.pdf")
+ -- logFile <- optional $ C8.readFile (path </> "texrunner.log")
+
+ return (exitC, parseLog $ fromMaybe a logFile, pdfFile)
+
+-- | Add a list of paths to the tex
+extraTexInputs :: [FilePath] -> [(String,String)] -> [(String,String)]
+extraTexInputs [] = id
+extraTexInputs inputss = alter f "TEXINPUTS"
+ where
+ f Nothing = Just inputs
+ f (Just x) = Just (inputs ++ [searchPathSeparator] ++ x)
+ --
+ inputs = concatMap (++ [searchPathSeparator]) inputss
+ -- inputs = intercalate [searchPathSeparator] inputss
+
+-- Alter can be used to insert, delete or update an element. Similar to alter
+-- in Data.Map.
+alter :: Eq k => (Maybe a -> Maybe a) -> k -> [(k,a)] -> [(k,a)]
+alter f k = go
+ where
+ go [] = maybeToList ((,) k <$> f Nothing)
+ go ((k',x):xs)
+ | k' == k = case f (Just x) of
+ Just x' -> (k',x') : xs
+ Nothing -> xs
+ | otherwise = (k',x) : go xs
+
diff --git a/src/System/Texrunner/Online.hs b/src/System/Texrunner/Online.hs
new file mode 100644
index 0000000..81c5cea
--- /dev/null
+++ b/src/System/Texrunner/Online.hs
@@ -0,0 +1,207 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+----------------------------------------------------------------------------
+-- |
+-- Module : System.Texrunner.Online
+-- Copyright : (c) 2015 Christopher Chalmers
+-- License : BSD-style (see LICENSE)
+-- Maintainer : c.chalmers@me.com
+--
+-- Functions for running and parsing using Tex's online interface. This is
+-- mostly used for getting measurements like hbox dimensions and textwidth.
+--
+-- Tex's online interface is basically running the command line. You can
+-- see it by running @pdflatex@ without any arguments. The contents can
+-- be writen line by and tex can give feedback though stdout, which gets
+-- parsed in by this module. This is the only way I know to get info
+-- like hbox sizes. Please let me know if you know a better way.
+--
+-----------------------------------------------------------------------------
+
+module System.Texrunner.Online
+ ( OnlineTex
+ -- * Running Tex online
+ , runOnlineTex
+
+ , runOnlineTex'
+ -- * Interaction
+ , hbox
+ , hsize
+ , showthe
+ , onlineTexParser
+ , texPutStrLn
+
+ -- * Low level
+ -- | These functions allow give you direct access to the iostreams
+ -- with tex. The implementation is likely to change in the future
+ -- and using them directly is not recommended.
+ , TexStreams
+ , getInStream
+ , getOutStream
+ , clearUnblocking
+ ) where
+
+import Control.Applicative
+import Control.Monad.Reader
+import qualified Data.Attoparsec.ByteString as A
+import Data.ByteString.Char8 (ByteString)
+import qualified Data.ByteString.Char8 as C8
+import qualified Data.ByteString.Lazy.Char8 as LC8
+import Data.List (find)
+import Data.Maybe
+import Data.Monoid
+import qualified Data.Traversable as T
+
+import System.Directory
+import System.FilePath
+import System.IO
+import System.IO.Streams as Streams
+import System.IO.Streams.Attoparsec
+import System.IO.Temp
+import System.Process as P (runInteractiveProcess)
+
+import System.Texrunner.Parse
+
+-- | Type for dealing with Tex's pipping interface, the current streams
+-- are availble though the `MonadReader` instance.
+newtype OnlineTex a = OnlineTex {runOnlineTexT :: ReaderT TexStreams IO a}
+ deriving (Functor, Applicative, Monad, MonadIO, MonadReader TexStreams)
+
+-- Run a tex process, disguarding the resulting PDF.
+runOnlineTex :: String -- ^ tex command
+ -> [String] -- ^ tex command arguments
+ -> ByteString -- ^ preamble
+ -> OnlineTex a -- ^ Online Tex to be Run
+ -> IO a
+runOnlineTex command args preamble process =
+ (\(a,_,_) -> a) <$> runOnlineTex' command args preamble process
+
+-- Run a tex process, keeping the resulting PDF. The OnlineTex must receive
+-- the terminating control sequence (\bye, \end{document}, \stoptext).
+runOnlineTex' :: String
+ -> [String]
+ -> ByteString
+ -> OnlineTex a
+ -> IO (a, TexLog, Maybe LC8.ByteString)
+runOnlineTex' command args preamble process =
+ withSystemTempDirectory "onlinetex." $ \path -> do
+ (outS, inS, h) <- mkTexHandles path Nothing command args preamble
+ a <- flip runReaderT (outS, inS) . runOnlineTexT $ process
+
+ write Nothing outS
+ _ <- waitForProcess h
+
+ -- it's normally texput.pdf but some (Context) choose random names
+ pdfPath <- find ((==".pdf") . takeExtension) <$> getDirectoryContents path
+ pdfFile <- T.mapM (LC8.readFile . (path </>)) pdfPath
+
+ logPath <- find ((==".log") . takeExtension) <$> getDirectoryContents path
+ logFile <- T.mapM (C8.readFile . (path </>)) logPath
+
+ return (a, parseLog $ fromMaybe "" logFile, pdfFile)
+
+-- | Get the dimensions of a hbox.
+hbox :: Fractional n => ByteString -> OnlineTex (Box n)
+hbox str = do
+ clearUnblocking
+ texPutStrLn $ "\\setbox0=\\hbox{" <> str <> "}\n\\showbox0\n"
+ onlineTexParser parseBox
+
+-- | Parse result from @\showthe@.
+showthe :: Fractional n => ByteString -> OnlineTex n
+showthe str = do
+ clearUnblocking
+ texPutStrLn $ "\\showthe" <> str
+ onlineTexParser parseUnit
+
+-- | Dimensions from filling the current line.
+hsize :: Fractional n => OnlineTex n
+hsize = boxWidth <$> hbox "\\line{\\hfill}"
+
+-- | Run an Attoparsec parser on Tex's output.
+onlineTexParser :: A.Parser a -> OnlineTex a
+onlineTexParser p = getInStream >>= liftIO . parseFromStream p
+ -- TODO: have a timeout
+
+texPutStrLn :: ByteString -> OnlineTex ()
+texPutStrLn a = getOutStream >>= liftIO . write (Just $ C8.append a "\n")
+
+-- * Internal
+-- These functions should be used with caution.
+
+type TexStreams = (OutputStream ByteString, InputStream ByteString)
+
+-- | Get the output stream to read tex's output.
+getOutStream :: OnlineTex (OutputStream ByteString)
+getOutStream = reader fst
+
+-- | Get the input stream to give text to tex.
+getInStream :: OnlineTex (InputStream ByteString)
+getInStream = reader snd
+
+-- | Clear any output tex has already given.
+clearUnblocking :: OnlineTex ()
+clearUnblocking = getInStream >>= void . liftIO . Streams.read
+
+-- | Uses a surface to open an interface with Tex,
+mkTexHandles :: FilePath
+ -> Maybe [(String, String)]
+ -> String
+ -> [String]
+ -> ByteString
+ -> IO (OutputStream ByteString,
+ InputStream ByteString,
+ ProcessHandle)
+mkTexHandles dir env command args preamble = do
+
+ -- Tex doesn't send anything to stderr
+ (outStream, inStream, _, h) <- runInteractiveProcess'
+ command
+ args
+ (Just dir)
+ env
+
+ -- inStream <- debugStream inStream'
+
+ -- commands to get Tex to play nice
+ write (Just $ "\\tracingonline=1" -- \showbox is echoed to stdout
+ <> "\\showboxdepth=1" -- show boxes one deep
+ <> "\\showboxbreadth=1"
+ <> "\\scrollmode\n" -- don't pause after showing something
+ ) outStream
+ write (Just preamble) outStream
+
+ return (outStream, inStream, h)
+
+-- Adapted from io-streams. Sets input handle to line buffering.
+runInteractiveProcess'
+ :: FilePath -- ^ Filename of the executable (see 'proc' for details)
+ -> [String] -- ^ Arguments to pass to the executable
+ -> Maybe FilePath -- ^ Optional path to the working directory
+ -> Maybe [(String,String)] -- ^ Optional environment (otherwise inherit)
+ -> IO (OutputStream ByteString,
+ InputStream ByteString,
+ InputStream ByteString,
+ ProcessHandle)
+runInteractiveProcess' cmd args wd env = do
+ (hin, hout, herr, ph) <- P.runInteractiveProcess cmd args wd env
+
+ -- it is possible to flush using write (Just "") but this seems nicer
+ -- is there a better way?
+ hSetBuffering hin LineBuffering
+
+ sIn <- Streams.handleToOutputStream hin >>=
+ Streams.atEndOfOutput (hClose hin) >>=
+ Streams.lockingOutputStream
+ sOut <- Streams.handleToInputStream hout >>=
+ Streams.atEndOfInput (hClose hout) >>=
+ Streams.lockingInputStream
+ sErr <- Streams.handleToInputStream herr >>=
+ Streams.atEndOfInput (hClose herr) >>=
+ Streams.lockingInputStream
+
+ return (sIn, sOut, sErr, ph)
+
+-- debugStream :: InputStream ByteString -> IO (InputStream ByteString)
+-- debugStream = debugInput id "tex" Streams.stdout
diff --git a/src/System/Texrunner/Parse.hs b/src/System/Texrunner/Parse.hs
new file mode 100644
index 0000000..a02b31e
--- /dev/null
+++ b/src/System/Texrunner/Parse.hs
@@ -0,0 +1,324 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+
+----------------------------------------------------------------------------
+-- |
+-- Module : System.Texrunner.Parse
+-- Copyright : (c) 2015 Christopher Chalmers
+-- License : BSD-style (see LICENSE)
+-- Maintainer : c.chalmers@me.com
+--
+-- Functions for parsing Tex output and logs. This log is parser is
+-- experimental and largely untested. Please make an issue for any logs
+-- that aren't parsed properly.
+--
+-----------------------------------------------------------------------------
+
+module System.Texrunner.Parse
+ ( -- * Box
+ Box (..)
+ , parseBox
+ -- * Errors
+ , TexLog (..)
+ , TexInfo (..)
+ , TexError (..)
+ , TexError' (..)
+ , someError
+ , badBox
+ , parseUnit
+ , parseLog
+ , prettyPrintLog
+ ) where
+
+import Control.Applicative
+import Data.Attoparsec.ByteString.Char8 as A
+import Data.ByteString.Char8 (ByteString, cons, pack)
+import qualified Data.ByteString.Char8 as B
+import Data.Maybe
+import Data.Monoid
+
+------------------------------------------------------------------------
+-- Boxes
+------------------------------------------------------------------------
+
+-- | Data type for holding dimensions of a hbox. It is likely the
+-- internal representation will change to allow nested boxes in the
+-- future.
+data Box n = Box
+ { boxHeight :: n
+ , boxDepth :: n
+ , boxWidth :: n
+ } deriving Show
+
+int :: Parser Int
+int = decimal
+
+parseBox :: Fractional n => Parser (Box n)
+parseBox = do
+ A.skipWhile (/='\\') <* char '\\'
+ parseSingle <|> parseBox
+ where
+ parseSingle = do
+ _ <- "box" *> int <* "=\n\\hbox("
+ h <- rational <* char '+'
+ d <- rational <* ")x"
+ w <- rational
+ --
+ return $ Box (pt2bp h) (pt2bp d) (pt2bp w)
+
+parseUnit :: Fractional n => Parser n
+parseUnit = do
+ A.skipWhile (/='>') <* char '>'
+ skipSpace
+ fmap pt2bp rational <|> parseUnit
+
+pt2bp :: Fractional n => n -> n
+pt2bp = (/1.00374)
+
+------------------------------------------------------------------------
+-- Logs
+------------------------------------------------------------------------
+
+-- Everything's done using ByteString because io-streams' attoparsec module
+-- only has a ByteString function. It's very likely this will all change to
+-- Text in the future.
+
+data TexLog = TexLog
+ { texInfo :: TexInfo
+ , numPages :: Maybe Int
+ , texErrors :: [TexError]
+ -- , rawLog :: ByteString
+ } deriving Show
+
+data TexInfo = TexInfo
+ { texCommand :: Maybe ByteString
+ , texVersion :: Maybe ByteString
+ , texDistribution :: Maybe ByteString
+ -- , texDate :: Maybe Date
+ }
+ deriving Show
+
+-- Make shift way to parse a log by combining it in this way.
+instance Monoid TexLog where
+ mempty = TexLog (TexInfo Nothing Nothing Nothing) Nothing []
+ TexLog prog pages1 errors1 `mappend` TexLog _ pages2 errors2 =
+ case (pages1,pages2) of
+ (Just a,_) -> TexLog prog (Just a) (errors1 ++ errors2)
+ (_,b) -> TexLog prog b (errors1 ++ errors2)
+
+infoParser :: Parser TexInfo
+infoParser
+ = TexInfo
+ <$> optional ("This is" *> takeTill (== ',') <* anyChar)
+ <*> optional (" Version " *> takeTill (== ' ') <* anyChar)
+ <*> optional (char '(' *> takeTill (== ')') <* anyChar)
+ -- <*> Nothing
+
+logFile :: Parser TexLog
+logFile = mconcat <$> many logLine
+ where
+ logLine = do
+ info <- infoParser
+ pages <- optional nPages
+ errors <- maybeToList <$> optional someError
+ _ <- restOfLine
+ return $ TexLog info pages errors
+
+-- thisIs :: Parser TexVersion
+
+parseLog :: ByteString -> TexLog
+parseLog = (\(Right a) -> a) . parseOnly logFile
+-- the parse should never fail (I think)
+
+prettyPrintLog :: TexLog -> ByteString
+prettyPrintLog (TexLog {..}) =
+ fromMaybe "unknown program" (texCommand texInfo)
+ <> maybe "" (" version " <>) (texVersion texInfo)
+ <> maybe "" (" " <>) (texDistribution texInfo)
+ <> "\n"
+ <> maybe "" ((<> "pages\n") . pack . show) numPages
+ <> B.unlines (map (pack . show) texErrors)
+
+------------------------------------------------------------------------
+-- Errors
+------------------------------------------------------------------------
+
+-- | An error from tex with possible line number.
+data TexError = TexError
+ { errorLine :: Maybe Int
+ , error' :: TexError'
+ }
+ deriving Show
+
+instance Eq TexError where
+ TexError _ a == TexError _ b = a == b
+
+-- | A subset of possible error Tex can throw.
+data TexError'
+ = UndefinedControlSequence ByteString
+ | MissingNumber
+ | Missing Char
+ | IllegalUnit -- (Maybe Char) (Maybe Char)
+ | PackageError String String
+ | LatexError ByteString
+ | BadBox ByteString
+ | EmergencyStop
+ | ParagraphEnded
+ | TooMany ByteString
+ | DimensionTooLarge
+ | TooManyErrors
+ | NumberTooBig
+ | ExtraBrace
+ | FatalError ByteString
+ | UnknownError ByteString
+ deriving (Show, Read, Eq)
+
+-- Parse any line begining with "! ". Any unknown errors are returned as 'UnknownError'.
+someError :: Parser TexError
+someError = mark *> errors
+ where
+ -- in context exclamation mark isn't always at the begining
+ mark = "! " <|> (notChar '\n' *> mark)
+ errors = undefinedControlSequence
+ <|> illegalUnit
+ <|> missingNumber
+ <|> missing
+ <|> latexError
+ <|> emergencyStop
+ <|> extraBrace
+ <|> paragraphEnded
+ <|> numberTooBig
+ <|> tooMany
+ <|> dimentionTooLarge
+ <|> tooManyErrors
+ <|> fatalError
+ <|> TexError Nothing <$> UnknownError <$> restOfLine
+
+noteStar :: Parser ()
+noteStar = skipSpace *> "<*>" *> skipSpace
+
+toBeReadAgain :: Parser Char
+toBeReadAgain = do
+ skipSpace
+ _ <- "<to be read again>"
+ skipSpace
+ anyChar
+
+-- insertedText :: Parser ByteString
+-- insertedText = do
+-- skipSpace
+-- _ <- "<inserted text>"
+-- skipSpace
+-- restOfLine
+
+------------------------------------------------------------------------
+-- Error parsers
+------------------------------------------------------------------------
+
+undefinedControlSequence :: Parser TexError
+undefinedControlSequence = do
+ _ <- "Undefined control sequence"
+
+ _ <- optional $ do -- for context log
+ skipSpace
+ _ <- "system"
+ let skipLines = line <|> restOfLine *> skipLines
+ skipLines
+
+ _ <- optional noteStar
+ skipSpace
+ l <- optional line
+ skipSpace
+ cs <- finalControlSequence
+ return $ TexError l (UndefinedControlSequence cs)
+
+finalControlSequence :: Parser ByteString
+finalControlSequence = last <$> many1 controlSequence
+ where
+ controlSequence = cons '\\' <$>
+ (char '\\' *> takeTill (\x -> isSpace x || x=='\\'))
+
+illegalUnit :: Parser TexError
+illegalUnit = do
+ _ <- "Illegal unit of measure (pt inserted)"
+ _ <- optional toBeReadAgain
+ _ <- optional toBeReadAgain
+
+ return $ TexError Nothing IllegalUnit
+
+missingNumber :: Parser TexError
+missingNumber = do
+ _ <- "Missing number, treated as zero"
+ _ <- optional toBeReadAgain
+ _ <- optional noteStar
+ return $ TexError Nothing MissingNumber
+
+badBox :: Parser TexError
+badBox = do
+ s <- choice ["Underfull", "Overfull", "Tight", "Loose"]
+ _ <- " \\hbox " *> char '(' *> takeTill (==')') <* char ')'
+ l <- optional line
+ return $ TexError l (BadBox s)
+
+missing :: Parser TexError
+missing = do
+ c <- "Missing " *> anyChar <* " inserted"
+ l <- optional line
+ return $ TexError l (Missing c)
+
+line :: Parser Int
+line = " detected at line " *> decimal
+ <|> "l." *> decimal
+
+emergencyStop :: Parser TexError
+emergencyStop = "Emergency stop"
+ *> return (TexError Nothing EmergencyStop)
+
+fatalError :: Parser TexError
+fatalError = TexError Nothing <$> FatalError <$> (" ==> Fatal error occurred, " *> restOfLine)
+
+-- line 8058 tex.web
+extraBrace :: Parser TexError
+extraBrace = "Argument of" *> return (TexError Nothing ExtraBrace)
+
+tooMany :: Parser TexError
+tooMany = TexError Nothing <$> TooMany <$> ("Too Many " *> takeTill (=='\''))
+
+tooManyErrors :: Parser TexError
+tooManyErrors = "That makes 100 errors; please try again"
+ *> return (TexError Nothing TooManyErrors)
+
+dimentionTooLarge :: Parser TexError
+dimentionTooLarge = "Dimension too large"
+ *> return (TexError Nothing DimensionTooLarge)
+
+-- line 8075 tex.web
+paragraphEnded :: Parser TexError
+paragraphEnded = do
+ _ <- "Paragraph ended before "
+ _ <- takeTill isSpace
+ _ <- toBeReadAgain
+ l <- optional line
+ return $ TexError l ParagraphEnded
+
+numberTooBig :: Parser TexError
+numberTooBig = "Number too big"
+ *> return (TexError Nothing NumberTooBig)
+
+-- Latex errors
+
+latexError :: Parser TexError
+latexError = TexError Nothing <$> LatexError <$> ("Latex Error: " *> restOfLine)
+
+-- Pages
+
+nPages :: Parser Int
+nPages = "Output written on "
+ *> skipWhile (/= '(') *> char '('
+ *> decimal
+
+-- Utilities
+
+restOfLine :: Parser ByteString
+restOfLine = takeTill (=='\n') <* char '\n'
+
diff --git a/tests/Tests.hs b/tests/Tests.hs
new file mode 100644
index 0000000..bf8a543
--- /dev/null
+++ b/tests/Tests.hs
@@ -0,0 +1,9 @@
+module Main (main) where
+
+import Test.Framework (defaultMain)
+import qualified Tex.PDF
+import qualified Tex.LogParse
+
+main :: IO ()
+main = defaultMain (Tex.PDF.tests ++ Tex.LogParse.tests)
+
diff --git a/tests/Tex/LogParse.hs b/tests/Tex/LogParse.hs
new file mode 100644
index 0000000..5cef0b2
--- /dev/null
+++ b/tests/Tex/LogParse.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+module Tex.LogParse where
+
+import Data.ByteString.Lazy.Char8 as B (unlines, ByteString, writeFile)
+import Data.Monoid
+
+import Test.HUnit
+import Test.Framework.Providers.HUnit
+import Test.Framework as F
+
+import System.Texrunner
+import System.Texrunner.Parse
+import Control.Lens
+import Data.Foldable
+
+tests = texTests ++ latexTests ++ contextTests
+
+texTests = [checkErrors "tex error parse" tex]
+latexTests = [checkErrors "latex error parse" latex]
+contextTests = [checkErrors "context error parse" context]
+
+withHead :: Monad m => [a] -> (a -> m ()) -> m ()
+withHead (a:_) f = f a
+withHead _ _ = return ()
+
+tex e code = testCase ("tex" ++ show e) $ do
+ (exitCode, texLog, mPDF) <- runTex "pdftex" [] [] code
+ take 1 (map error' (texErrors texLog)) @?= [e]
+
+latexHeader, latexBye :: ByteString
+latexHeader = B.unlines
+ [ "\\documentclass{article}"
+ , "\\begin{document}"
+ ]
+latexBye = "\\end{document}"
+
+latex e code = testCase ("latex" ++ show e) $ do
+ (exitCode, texLog, mPDF) <- runTex "pdflatex" [] [] (latexHeader <> code)
+ head (map error' $ texErrors texLog) @?= e
+
+contextHeader, contextBye :: ByteString
+contextHeader = "\\starttext"
+contextBye = "\\stoptext"
+
+context e code = testCase ("context" ++ show e) $ do
+ (exitCode, texLog, mPDF) <- runTex "context" [] [] (contextHeader <> code)
+ take 1 (map error' (texErrors texLog)) @?= [e]
+ -- head (map error' $ texErrors texLog) @?= e
+ -- assertBool ("context" ++ show e) $ texLog `containsError` e
+
+-- Generating tex sample tex files -------------------------------------
+
+-- plain tex
+
+genTexFiles :: IO ()
+genTexFiles = for_ labeledErrors mkFile
+ where
+ mkFile (nm, (_err, xs)) = ifor_ xs $ \i x -> do
+ let doc = latexHeader <> x <> latexBye
+ name | length xs == 1 = nm
+ | otherwise = nm <> "-" <> show (i+1)
+ B.writeFile ("tests/samples/tex/" <> name <> ".tex") doc
+
+-- latex
+
+-- pdflatex -draftmode --interaction=nonstopmode $i
+
+genLatexFiles :: IO ()
+genLatexFiles = for_ labeledErrors mkFile
+ where
+ mkFile (nm, (_err, xs)) = ifor_ xs $ \i x -> do
+ let doc = latexHeader <> x <> latexBye
+ name | length xs == 1 = nm
+ | otherwise = nm <> "-" <> show (i+1)
+ B.writeFile ("tests/samples/latex/" <> name <> ".tex") doc
+
+-- context tex
+
+genContextFiles :: IO ()
+genContextFiles = for_ labeledErrors mkFile
+ where
+ mkFile (nm, (_err, xs)) = ifor_ xs $ \i x -> do
+ let doc = contextHeader <> x <> contextBye
+ name | length xs == 1 = nm
+ | otherwise = nm <> "-" <> show (i+1)
+ B.writeFile ("tests/samples/context/" <> name <> ".tex") doc
+
+labeledErrors =
+ [ ("missing-dollar", missingDollar)
+ , ("dimention-too-large", dimensionTooLarge)
+ , ("illegal-unit", illegalUnit)
+ , ("missing-number", missingNumber)
+ , ("undefined-control-sequence", undefinedControlSequence)
+ ]
+
+-- Checking error parsing ----------------------------------------------
+
+containsError :: TexLog -> TexError -> Bool
+containsError log (TexError _ err) = err `elem` map error' (texErrors log)
+
+checkError :: (TexError' -> ByteString -> F.Test) -> (TexError', [ByteString]) -> F.Test
+checkError f (e, codes) = testGroup (show e) $ map (f e) codes
+
+checkErrors :: TestName -> (TexError' -> ByteString -> F.Test) -> F.Test
+checkErrors name f = testGroup name $ map (checkError f) texErrs
+
+-- Sample errors -------------------------------------------------------
+
+texErrs =
+ [ missingDollar
+ , dimensionTooLarge
+ , illegalUnit
+ , missingNumber
+ , undefinedControlSequence
+ ]
+
+missingDollar = (,) (Missing '$')
+ [ "$x+1=2\n\n"
+ , "$$x+1=2\n\n"
+ ]
+
+dimensionTooLarge = (,) DimensionTooLarge
+ [ "\\hskip100000em"
+ ]
+
+illegalUnit = (,) IllegalUnit
+ [ "\\hskip1cn"
+ ]
+
+missingNumber = (,) MissingNumber
+ [ "\\hskip hi"
+ ]
+
+undefinedControlSequence = (,) (UndefinedControlSequence "\\hobx")
+ [ "\\hobx"
+ ]
+
+
+
+--
+-- missingDollarExample2= "x_1"
+--
+-- missingDollarExample3= "
+--
+-- numberTooBig = "10000000000"
+--
+-- overfull = "\\hbox to 1em{overfill box}"
+--
+-- underfill = "\\hbox to 20em{underfill box}"
+--
+-- illegalUnit = "\\hskip{1cn}"
+--
+-- undefinedControlSequence = "\\hobx"
+--
+-- missingNumber = "\\hskip"
+--
+--
+-- missingDollarTest = (texPutStrLn missingDollarExample, MissingDollar)
+--
diff --git a/tests/Tex/PDF.hs b/tests/Tex/PDF.hs
new file mode 100644
index 0000000..1e9e9aa
--- /dev/null
+++ b/tests/Tex/PDF.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+module Tex.PDF where
+
+import Data.ByteString.Lazy.Char8 as B
+import Data.Maybe
+import System.Exit
+
+import Test.HUnit
+import Test.Framework.Providers.HUnit
+
+import System.Texrunner
+import System.Texrunner.Online
+
+tests = [tex, latex, context, texOnline, latexOnline, contextOnline]
+texTests = [tex, texOnline]
+latexTests = [latex, latexOnline]
+contextTests = [context, contextOnline]
+
+texDocument :: ByteString
+texDocument = "hi\\bye"
+
+latexDocument :: ByteString
+latexDocument = B.unlines
+ [ "\\documentclass{article}"
+ , "\\begin{document}"
+ , "hi"
+ , "\\end{document}"
+ ]
+
+contextDocument :: ByteString
+contextDocument = B.unlines
+ [ "\\starttext"
+ , "hi"
+ , "\\stoptext"
+ ]
+
+tex = testRunTeX "pdftex" [] texDocument
+latex = testRunTeX "pdflatex" [] latexDocument
+context = testRunTeX "context" ["--once"] contextDocument
+
+testRunTeX command args document = testCase command $ do
+ (exitCode, _, mPDF) <- runTex command args [] document
+ exitCode @?= ExitSuccess
+ assertBool "pdf found" $ isJust mPDF
+
+
+-- online
+
+testOnlineTeX command args document = testCase (command ++ "Online") $ do
+ ((), _, mPDF) <- runOnlineTex' command args "" (texPutStrLn $ toStrict document)
+ assertBool "pdf found" $ isJust mPDF
+
+texOnline = testOnlineTeX "pdftex" [] texDocument
+latexOnline = testOnlineTeX "pdflatex" [] latexDocument
+contextOnline = testOnlineTeX "context" ["--pipe"] contextDocument
+
+
+
+
+-- tests to make:
+-- * texinputs for files in cwd
+-- * pdf made online
+
diff --git a/texrunner.cabal b/texrunner.cabal
new file mode 100644
index 0000000..2efb4e6
--- /dev/null
+++ b/texrunner.cabal
@@ -0,0 +1,62 @@
+name: texrunner
+version: 0.0.1.0
+synopsis: Functions for running Tex from Haskell.
+description:
+ texrunner is an interface to tex that attempts to parse errors and
+ can parse tex in online mode to retrive hbox sizes.
+ .
+ This package should be considered very experimental. Eventually I hope
+ it will be good enough for general use but for now it's only suitable
+ for simple things.
+license: BSD3
+license-file: LICENSE
+bug-reports: http://github.com/cchalmers/texrunner/issues
+author: Christopher Chalmers
+maintainer: c.chalmers@me.com
+copyright: 2015 Christopher Chalmers
+category: System
+build-type: Simple
+extra-source-files: README.md
+cabal-version: >=1.10
+source-repository head
+ type: git
+ location: http://github.com/cchalmers/texrunner
+
+library
+ exposed-modules:
+ System.Texrunner
+ System.Texrunner.Online
+ System.Texrunner.Parse
+ build-depends:
+ base >=4.6 && <4.9,
+ bytestring >=0.10 && <1.0,
+ filepath,
+ directory >=1.2 && <2.0,
+ temporary >=1.2 && <2.0,
+ process >=1.2 && <2.0,
+ mtl >=2.1 && <3.0,
+ attoparsec >=0.10 && <1.0,
+ io-streams >=1.1 && <2.0
+ hs-source-dirs: src
+ ghc-options: -Wall
+ default-language: Haskell2010
+
+test-suite tests
+ type: exitcode-stdio-1.0
+ hs-source-dirs: tests
+ main-is: Tests.hs
+ ghc-options: -Wall
+ default-language: Haskell2010
+ other-modules:
+ Tex.PDF
+ Tex.LogParse
+
+ build-depends:
+ base,
+ lens,
+ test-framework,
+ test-framework-hunit,
+ HUnit,
+ bytestring,
+ texrunner
+