packages feed

anansi-0.3: src/Main.hs

-- Copyright (C) 2010 John Millikin <jmillikin@gmail.com>
-- 
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- any later version.
-- 
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
-- 
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Anansi
import Paths_anansi (version)

import Prelude hiding (FilePath)
import Control.Monad.Writer
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TLIO
import Data.Text.Lazy.Encoding (encodeUtf8)
import qualified Data.ByteString.Lazy as BL
import Data.Version (showVersion)

import System.Console.GetOpt
import System.Directory
import System.Environment
import System.Exit
import System.FilePath (FilePath)
import qualified System.FilePath.CurrentOS as FP
import System.IO hiding (withFile, FilePath)

data Output = Tangle | Weave
	deriving (Eq)

data Option
	= OptionOutput Output
	| OptionOutputPath FilePath
	| OptionLoom TL.Text
	| OptionNoLines
	| OptionVersion
	deriving (Eq)

optionInfo :: [OptDescr Option]
optionInfo =
	[ Option ['t'] ["tangle"] (NoArg (OptionOutput Tangle))
	  "Generate tangled source code (default)"
	, Option ['w'] ["weave"] (NoArg (OptionOutput Weave))
	  "Generate woven markup"
	, Option ['o'] ["out", "output"] (ReqArg (OptionOutputPath . FP.fromString) "PATH")
	  "Output path (directory for tangle, file for weave)"
	, Option ['l'] ["loom"] (ReqArg (OptionLoom . TL.pack) "NAME")
	  "Which loom should be used to weave output"
	, Option [] ["noline"] (NoArg OptionNoLines)
	  "Disable generating #line declarations in tangled code"
	, Option [] ["version"] (NoArg OptionVersion) ""
	]

usage :: String -> String
usage name = "Usage: " ++ name ++ " [OPTION...]"

getOutput :: [Option] -> Output
getOutput [] = Tangle
getOutput (x:xs) = case x of
	OptionOutput o -> o
	_ -> getOutput xs

getPath :: [Option] -> FilePath
getPath [] = ""
getPath (x:xs) = case x of
	OptionOutputPath p -> p
	_ -> getPath xs

withFile :: FilePath -> (Handle -> IO a) -> IO a
withFile path io = if FP.null path
	then io stdout
	else withBinaryFile (FP.toString path) WriteMode io

loomMap :: [(TL.Text, Loom)]
loomMap = [(loomName l, l) | l <- looms]

getLoom :: [Option] -> Loom
getLoom [] = loomLaTeX
getLoom (x:xs) = case x of
	OptionLoom name -> case lookup name loomMap of
		Just loom -> loom
		Nothing -> error $ "Unknown loom: " ++ show name
	_ -> getLoom xs

getEnableLines :: [Option] -> Bool
getEnableLines [] = True
getEnableLines (x:xs) = case x of
	OptionNoLines -> False
	_ -> getEnableLines xs

main :: IO ()
main = do
	args <- getArgs
	let (options, inputs, errors) = getOpt Permute optionInfo args
	unless (null errors) $ do
		name <- getProgName
		hPutStrLn stderr $ concat errors
		hPutStrLn stderr $ usageInfo (usage name) optionInfo
		exitFailure
	
	when (OptionVersion `elem` options) $ do
		putStrLn ("anansi_" ++ showVersion version)
		exitSuccess
	
	let path = getPath options
	let loom = getLoom options
	let enableLines = getEnableLines options
	
	parsed <- parseInputs inputs
	case parsed of
		Left err -> do
			hPutStrLn stderr (formatError err)
			exitFailure
		Right blocks -> case getOutput options of
			Tangle -> case path of
				"" -> tangle debugTangle enableLines blocks
				_ -> tangle (realTangle path) enableLines blocks
			Weave -> let
				texts = execWriter $ loomWeave loom blocks
				in withFile path $ \h -> BL.hPut h $ encodeUtf8 texts

debugTangle :: FilePath -> TL.Text -> IO ()
debugTangle path text = do
	let strPath = FP.toString path
	putStr "\n"
	putStrLn strPath
	putStrLn $ replicate (fromIntegral (length strPath)) '='
	TLIO.putStr text

realTangle :: FilePath -> FilePath -> TL.Text -> IO ()
realTangle root path text = do
	let fullpath = FP.append root path
	createDirectoryIfMissing True (FP.toString (FP.parent fullpath))
	let bytes = encodeUtf8 text
	withBinaryFile (FP.toString fullpath) ReadWriteMode $ \h -> do
		equal <- fileContentsEqual h bytes
		unless equal $ do
			hSetFileSize h 0
			BL.hPut h bytes

fileContentsEqual :: Handle -> BL.ByteString -> IO Bool
fileContentsEqual h bytes = do
	hSeek h SeekFromEnd 0
	size <- hTell h
	hSeek h AbsoluteSeek 0
	
	if size /= toInteger (BL.length bytes)
		then return False
		else do
			-- FIXME: 'Int' overflow?
			contents <- BL.hGet h (fromInteger size)
			hSeek h AbsoluteSeek 0
			return $ bytes == contents

parseInputs :: [String] -> IO (Either ParseError [Block])
parseInputs inputs = do
	eithers <- mapM (parseFile . FP.fromString) inputs
	return $ case catEithers eithers of
		Left err -> Left err
		Right bs -> Right $ concat bs

formatError :: ParseError -> String
formatError err = concat [filename, ":", line, ": error: ", message] where
	pos = parseErrorPosition err
	filename = FP.toString (positionFile pos)
	line = show $ positionLine pos
	message = TL.unpack $ parseErrorMessage err

catEithers :: [Either e a] -> Either e [a]
catEithers = cat' [] where
	cat' acc [] = Right $ reverse acc
	cat' acc (e:es) = case e of
		Left err -> Left err
		Right x -> cat' (x : acc) es