packages feed

brassica-0.3.0: cli/Main.hs

{-# LANGUAGE LambdaCase   #-}
{-# LANGUAGE RecordWildCards #-}

module Main where

import Control.Exception (Exception)

import Conduit
import qualified Data.ByteString as B
import Data.Text (pack, unpack, snoc, Text)
import Data.Text.Encoding (decodeUtf8)
import Options.Applicative

import Brassica.SoundChange
import Brassica.SoundChange.Frontend.Internal
import Server

main :: IO ()
main = execParser opts >>= \case
    Server -> serve
    Options{..} -> do
        changesText <-
            case rules of
                FromFile rulesFile -> unpack . decodeUtf8 <$> B.readFile rulesFile
                FromEval s -> pure s

        case parseSoundChanges changesText of
            Left err ->
                putStrLn $ errorBundlePretty err
            Right scs ->
                case expandSoundChanges scs of
                    Left err -> putStrLn $ case err of
                        (NotFound s) -> "Could not find category: " ++ s
                        InvalidBaseValue -> "Invalid value used as base grapheme in feature definition"
                        MismatchedLengths -> "Mismatched lengths in feature definition"
                    Right rules' ->
                        withSourceFileIf inWordsFile $ \inC ->
                        withSinkFileIf outWordsFile $ \outC ->
                        runConduit $
                            inC
                            .| processWords (incrFor wordsFormat) rules' wordsFormat outMode
                            .| outC

  where
    opts = info (args <**> helper <**> simpleVersioner "v0.3.0") fullDesc

    args = batchArgs <|> serverArgs
    serverArgs = flag' Server (long "server" <> help "Run server (for internal use only)")
    batchArgs = Options
        <$> asum
            [ FromEval <$> strOption
                (long "eval" <> short 'e' <> help "Literal sound change(s) to evaluate (newline-separated, as in rules file)")
            , FromFile <$> strArgument
                (metavar "RULES" <> help "File containing sound changes")
            ]
        <*> flag Raw MDF
            (long "mdf" <> help "Parse input words in MDF format")
        <*> (asum
                [ flag' (const ReportRulesApplied)
                    (long "report" <> help "Report rules applied rather than outputting words")
                , flag' (ApplyRules NoHighlight MDFOutput)
                    (long "mdf-out" <> help "With --mdf, output MDF dictionary")
                , flag' (ApplyRules NoHighlight MDFOutputWithEtymons)
                    (long "etymons" <> help "With --mdf, output MDF dictionary with etymologies")
                , flag' (ApplyRules NoHighlight WordsWithProtoOutput)
                    (long "show-input" <> help "Output an input→output wordlist")
                , flag' (ApplyRules DifferentToInput WordsOnlyOutput)
                    (long "show-changed" <> help "Add [+] after all words different to input")
                , flag
                    (ApplyRules NoHighlight WordsOnlyOutput)
                    (ApplyRules NoHighlight WordsOnlyOutput)
                    (long "wordlist" <> help "Output only a list of the derived words (default)")
                ]
            <*> strOption
                (long "separator" <> short 's' <> value "/" <> help "Separator between multiple results (default: /)"))
        <*> optional (strOption
            (long "in" <> short 'i' <> help "File containing input words (if not specified will read from stdin)"))
        <*> optional (strOption
            (long "out" <> short 'o' <> help "File to which output words should be written (if not specified will write to stdout)"))

    incrFor Raw = True
    incrFor MDF = False

    -- duplicated in paradigm builder CLI
    withSourceFileIf :: Maybe FilePath -> (ConduitM i B.ByteString IO () -> IO a) -> IO a
    withSourceFileIf = maybe ($ stdinC) withSourceFile

    withSinkFileIf :: Maybe FilePath -> (ConduitM B.ByteString o IO () -> IO a) -> IO a
    withSinkFileIf = maybe ($ stdoutC) withSinkFile

data Rules = FromFile String | FromEval String
    deriving (Show)

data Options = Options
    { rules :: Rules
    , wordsFormat :: InputLexiconFormat
    , outMode :: ApplicationMode
    , inWordsFile :: Maybe String
    , outWordsFile :: Maybe String
    }
    | Server
    deriving (Show)

processWords
    :: (MonadIO m, MonadThrow m)
    => Bool  -- split into lines?
    -> SoundChanges Expanded [Grapheme]
    -> InputLexiconFormat
    -> ApplicationMode
    -> ConduitT B.ByteString B.ByteString m ()
processWords incr rules wordsFormat outMode =
    decodeUtf8C
    .| (if incr then linesUnboundedC else mapC id)
    .| mapC (processApplicationOutput . evolve . unpack . (`snoc` '\n'))
    .| throwOnLeft
    .| encodeUtf8C
  where
    evolve ws = parseTokeniseAndApplyRules parFmap rules ws wordsFormat outMode Nothing

    throwOnLeft :: (MonadThrow m, Exception e) => ConduitT (Either e r) r m ()
    throwOnLeft = awaitForever $ \case
        Left e -> throwM e
        Right r -> yield r

    processApplicationOutput :: ApplicationOutput PWord (Statement Expanded [Grapheme]) -> Either ParseException Text
    processApplicationOutput (HighlightedWords cs) = Right $ pack $ detokeniseWords' highlight cs
    processApplicationOutput (AppliedRulesTable is) = Right $ pack $ unlines $ reportAsText plaintext' <$> is
    processApplicationOutput (ParseError e) = Left $ ParseException $ errorBundlePretty e

    highlight (w, False) = concatWithBoundary w
    highlight (w, True) = concatWithBoundary w ++ " [+]"

newtype ParseException = ParseException String
    deriving Show

instance Exception ParseException