agda2lagda-0.2023.6.9: src/Options.hs
module Options
( Options(..)
, options
, Format(..)
, Language(..)
, languageToHighlighter
, getTarget
, self
, vocalizeOptions
)
where
import qualified Data.List as List
import Options.Applicative
import Options.Applicative.Help.Pretty (vcat, pretty) -- , (<$$>))
import System.Directory (doesDirectoryExist)
import System.FilePath
(splitExtension, addExtension, takeFileName, hasTrailingPathSeparator, (</>))
import Util
import Version
-- import Debug.Trace (traceShowId)
self :: String
self = "agda2lagda"
extensionMap :: [(String,(Language,[(Format,String)]))]
extensionMap =
[ (".agda" , (Agda , [ (LaTeX , ".lagda.tex")
, (Markdown , ".lagda.md" ) ]))
, (".hs" , (Haskell , [ (LaTeX , ".lhs" )
, (Markdown , ".lhs" ) ]))
]
-- | Map extensions to formats.
formatMap :: [(String, Format)]
formatMap =
[ (".tex", LaTeX)
, (".md", Markdown)
, (".markdown", Markdown)
]
-- | From the name of the output file, determine the format.
formatFromFilename :: String -> Maybe Format
formatFromFilename file =
case filter (\ (ext, _) -> ext `List.isSuffixOf` file) formatMap of
-- Only determine format if we have exactly one hit
[ (_, fmt) ] -> Just fmt
_ -> Nothing
-- | Format of the output.
data Format
= LaTeX -- ^ Latex literate text.
| Markdown -- ^ Markdown literate text.
deriving (Eq, Show)
-- formatToLongOption :: Format -> String
-- formatToLongOption = map toLower . show
data Language
= Agda
| Haskell
deriving (Eq, Show)
-- | Get markdown highlighter identifier (e.g. "agda", "haskell") for language.
languageToHighlighter :: Language -> String
languageToHighlighter = map toLower . show
-- * Option parsing and handling
data Options = Options
{ optVerbose :: Bool
, optDryRun :: Bool
, optForce :: Bool
, optFormat :: Maybe Format
-- , optLatex :: Bool
-- , optMarkdown :: Bool
, optOutput :: Maybe FilePath
, optInput :: Maybe FilePath
} deriving Show
inferFormatFromOutput :: Options -> Options
inferFormatFromOutput opts
| Nothing <- optFormat opts, Just out <- optOutput opts = opts { optFormat = formatFromFilename out }
| otherwise = opts
-- | Default format to 'LaTeX'.
getFormat :: Options -> Format
getFormat = fromMaybe LaTeX . optFormat
options :: IO Options
options = inferFormatFromOutput <$> do
execParser $
info (helper <*> versionOption <*> numericVersionOption <*> programOptions)
(header "Translates Agda/Haskell text into literate Agda/Haskell text, turning line comments into ordinary text and code into TeX code blocks."
<> footerDoc (Just foot))
where
versionOption =
infoOption (unwords versionWords)
$ long "version"
<> short 'V'
<> hidden
<> help "Show version info."
versionWords = concat
[ [ self, "version", version ]
, [ "(Halloween edition)" | ".11.1" `List.isSuffixOf` version ]
]
numericVersionOption =
infoOption version
$ long "numeric-version"
<> hidden
<> help "Show just version number."
-- Newline at the end:
-- <> helpDoc (Just $ pretty "Show just version number." <$$> pretty "")
programOptions = Options
<$> oVerbose
<*> oDryRun
<*> oForce
<*> oFormat
<*> oOutput
<*> (oStdin <|> oInput)
oForce =
switch
$ long "force"
<> short 'f'
<> help "Overwrite existing output files."
oDryRun =
switch
$ long "dry-run"
<> hidden
<> help "Do not write any output files, write to standard output."
oVerbose =
switch
$ long "verbose"
<> short 'v'
<> hidden
<> help "Comment on what is happening."
oFormat =
flag Nothing (Just Markdown)
$ long "markdown"
<> help "Produce Markdown instead of LaTeX."
-- oLaTeX =
-- switch
-- $ long (formatToLongOption LaTeX)
-- <> help (concat ["Generate LaTeX (default if --", formatToLongOption Markdown, " is not given)."])
-- oMarkdown =
-- switch
-- $ long (formatToLongOption Markdown)
-- <> help "Generate Markdown (possibly inferred from OUT file extension)."
-- oLaTeX =
-- optional $ strOption
-- $ long "latex"
-- <> value ""
-- <> noArgError (ShowHelpText Nothing)
-- <> metavar "LATEXFILE"
-- <> action "file"
-- <> help "Generate LaTeX (if LATEXFILE is not given, it is computed from INFILE and OUT)."
oOutput =
optional $ strOption
$ long "output"
<> short 'o'
<> metavar "OUT"
<> action "directory"
<> help "Name of output file or directory. Ending '.md' or '.markdown' determines Markdown output."
oStdin =
flag' Nothing
$ long "stdin"
<> help "Read from standard input."
oInput :: Parser (Maybe FilePath)
oInput = dashToStdin <$> do
strArgument
$ metavar "INFILE"
<> action "file"
<> help "The input file containing the Agda/Haskell text."
where
-- dash is interpreted as stdin
dashToStdin = \case
"-" -> Nothing
file -> Just file
foot = vcat $ map pretty $ concat
[ [ "Unless explicitly given via -o, the name of the output file is computed by replacing the extension of the input file, according to the following rules:"
, "" ]
, extensionMap >>= \ (src, (_language, rest)) ->
rest <&> \ (format, tgt) ->
-- let o = "--" ++ formatToLongOption format ++ ":" in
let o = show format in
List.intercalate "\t" [ "", src, "--" ++ o ++ "-->", tgt ]
, [ ""
, "If the path OUT given via -o ends in '.md' or '.markdown', '--markdown' is assumed."
, "If the path OUT given via -o is a directory, that's where the output file will be placed."
, ""
, unwords [ "Example:", self, "path/to/file.agda" ]
, ""
, "This will write file path/to/file.lagda.tex unless it already exists."
, "To overwrite existing files, use option -f."
, ""
, unwords [ "Example:", self, "path/to/file.hs", "-o out/dir" ]
, ""
, "This places the output in file out/dir/file.lhs."
, ""
, unwords [ "Example:", self, "path/to/file.hs", "-o new/dir/" ]
, ""
, "This places the output in file new/dir/file.lhs, creating directories new/ and new/dir/ if necessary."
]
]
type Target = (Language, Format, Maybe FilePath)
vocalizeOptions :: Options -> Target -> String
vocalizeOptions Options{..} (language, format, target) = unlines $ map concat
[ [ "Plan:" ]
, [ "- Read " ++ show language ++ " code from "
, maybe "standard input" ("file " ++) optInput
, "."
]
, [ "- Write " ++ show format ++ " to "
, maybe "standard output" (\ f -> "file " ++ f ++ unlessNewer) target
, "."
]
]
where
unlessNewer
| optForce = ""
| otherwise = " (only if output is not newer than input)"
getTarget :: Options -> IO Target
getTarget opts@Options{..} = do
let language = maybe Agda fst m
outFile <- if optDryRun then return Nothing else do
traverse dirOrFile optOutput <&> \ mout ->
if | Just (File out) <- mout -> Just out
| Just (_, (base, tgt)) <- m -> Just $ addDir mout $ addExtension base tgt
| otherwise -> Nothing
return (language, format, outFile)
where
format = getFormat opts
-- Resolve Language, basename, and extension from input file.
m :: Maybe (Language, (FilePath, FilePath))
m = do
inp <- optInput
let (base, src) = splitExtension inp
(language, rest) <- lookup src extensionMap
ext <- lookup format rest
Just (language, (base, ext))
addDir (Just (Dir dir)) = (dir </>) . takeFileName
addDir _ = id
data DirOrFile = Dir FilePath | File FilePath
dirOrFile :: FilePath -> IO DirOrFile
dirOrFile path
| hasTrailingPathSeparator path = return $ Dir path
| otherwise = doesDirectoryExist path <&> \case
True -> Dir path
False -> File path