packages feed

brassica-0.0.3: src/Brassica/MDF.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ViewPatterns #-}

{-| This module contains types and functions for working with the MDF
  dictionary format, used by programs such as [SIL Toolbox](https://software.sil.org/toolbox/).
  For more on the MDF format, refer to e.g.
  [Coward & Grimes (2000), /Making Dictionaries: A guide to lexicography and the Multi-Dictionary Formatter/](http://downloads.sil.org/legacy/shoebox/MDF_2000.pdf).
-}
module Brassica.MDF
       (
       -- * MDF files
         MDF(..)
       , MDFLanguage(..)
       , fieldLangs
       -- * Parsing
       , parseMDFRaw
       , parseMDFWithTokenisation
       -- ** Re-export
       , errorBundlePretty
       -- * Conversion
       , componentiseMDF
       , componentiseMDFWordsOnly
       , duplicateEtymologies
       ) where

import Control.Category ((>>>))
import Data.Char (isSpace)
import Data.Void (Void)

import qualified Data.Map as M
import Text.Megaparsec
import Text.Megaparsec.Char

import Brassica.SoundChange.Tokenise
import Brassica.SoundChange.Types (Grapheme, PWord)
import Data.Maybe (fromMaybe)

-- | An MDF (Multi-Dictionary Formatter) file, represented as a list
-- of (field marker, whitespace, field value) tuples. The field marker
-- is represented excluding its initial slash; whitespace after the
-- field marker is also stored, allowing the original MDF file to be
-- precisely recovered. Field values should includes all whitespace to
-- the next marker. All field values are stored as 'String's, with the
-- exception of 'Vernacular' fields, which have type @v@.
--
-- For instance, the following MDF file:
--
-- > \lx kapa
-- > \ps n
-- > \ge parent
-- > \se sakapa
-- > \ge father
--
-- Could be stored as:
--
-- > MDF [ ("lx", " ", Right "kapa\n")
-- >     , ("ps", " ", Left "n\n")
-- >     , ("ge", " ", Left "parent\n")
-- >     , ("se", " ", Right "sakapa\n")
-- >     , ("ge", " ", Left "father")
-- >     ]
newtype MDF v = MDF { unMDF :: [(String, String, Either String v)] }
    deriving (Show, Functor)

type Parser = Parsec Void String

sc :: Parser String
sc = fmap (fromMaybe "") $ optional $ takeWhile1P (Just "white space") isSpace

parseToSlash :: Parser String
parseToSlash = takeWhileP (Just "field value") (/= '\\')

entry :: Parser v -> Parser (String, String, Either String v)
entry pv = do
    _ <- char '\\'
    marker <- takeWhile1P (Just "field name") (not . isSpace)
    s <- sc
    value <- case M.lookup marker fieldLangs of
        Just Vernacular -> Right <$> pv
        _ -> Left <$> parseToSlash
    pure (marker, s, value)

-- | Parse an MDF file to an 'MDF', storing the 'Vernacular' fields as 'String's.
parseMDFRaw :: String -> Either (ParseErrorBundle String Void) (MDF String)
parseMDFRaw = runParser (fmap MDF $ sc *> many (entry parseToSlash) <* eof) ""

-- | Parse an MDF file to an 'MDF', parsing the 'Vernacular' fields
-- into 'Component's in the process.
parseMDFWithTokenisation
    :: [Grapheme]
    -> String
    -> Either (ParseErrorBundle String Void) (MDF [Component PWord])
parseMDFWithTokenisation (sortByDescendingLength -> gs) =
    runParser (fmap MDF $ sc *> p <* eof) ""
  where
    p = many $ entry $ componentsParser $ wordParser "\\" gs

-- | Convert an 'MDF' to a list of 'Component's representing the same
-- textual content. Vernacular field values are left as is; everything
-- else is treated as a 'Separator', so that it is not disturbed by
-- operations such as rule application or rendering to text.
componentiseMDF :: MDF [Component a] -> [Component a]
componentiseMDF = unMDF >>> concatMap \case
    (m, s, Left v) -> [Separator ('\\':m ++ s ++ v)]
    (m, s, Right v) -> Separator ('\\':m ++ s) : v

-- | As with 'componentiseMDF', but the resulting 'Component's contain
-- the contents of 'Vernacular' fields only; all else is
-- discarded. The first parameter specifies the 'Separator' to insert
-- after each vernacular field.
componentiseMDFWordsOnly :: MDF [Component a] -> [Component a]
componentiseMDFWordsOnly = unMDF >>> concatMap \case
    (_, _, Right v) -> v
    _ -> []

-- | Add etymological fields to an 'MDF' by duplicating the values in
-- @\lx@, @\se@ and @\ge@ fields. e.g.:
--
-- > \lx kapa
-- > \ps n
-- > \ge parent
-- > \se sakapa
-- > \ge father
--
-- Would become:
--
-- > \lx kapa
-- > \ps n
-- > \ge parent
-- > \et kapa
-- > \eg parent
-- > \se sakapa
-- > \ge father
-- > \et sakapa
-- > \eg father
--
-- This can be helpful when applying sound changes to an MDF file: the
-- vernacular words can be copied as etymologies, and then the sound
-- changes can be applied leaving the etymologies as is.
duplicateEtymologies
    :: (v -> String)
    -- ^ Function to convert from vernacular field values to
    -- strings. Can also be used to preprocess the value of the
    -- resulting @\et@ fields, e.g. by prepending @*@ or similar.
    -> MDF v -> MDF v
duplicateEtymologies typeset = MDF . go Nothing Nothing . unMDF
  where
    mkEt word gloss = word' gloss'
      where
        word' = case word of
            Just et -> (("et", " ", Left $ typeset et) :)
            Nothing -> id
        gloss' = case gloss of
            Just eg -> [("eg", " ", Left eg)]
            Nothing -> []

    go word gloss [] = mkEt word gloss
    go word _ (f@("ge", _, Left gloss'):fs)                  -- store gloss field for future etymology
        = f : go word (Just gloss') fs
    go word gloss (f@(m, _, Right word'):fs)                 -- add etymology & store word if word or subentry field reached
        | m == "lx" || m == "se"
        = mkEt word gloss ++ f : go (Just word') Nothing fs
    go word gloss (f@("dt", _, _):fs)                        -- add etymology if date (usually final field in entry) reached
        = mkEt word gloss ++ f : go Nothing Nothing fs
    go word gloss (f:fs) = f : go word gloss fs
    

-- | The designated language of an MDF field.
data MDFLanguage = English | National | Regional | Vernacular | Other
    deriving (Eq, Show)

-- | A 'M.Map' from the most common field markers to the language of
-- their values.
--
-- (Note: This is currently hardcoded in the source code, based on the
-- values in the MDF definitions from SIL Toolbox. There’s probably a
-- more principled way of defining this, but hardcoding should suffice
-- for now.)
fieldLangs :: M.Map String MDFLanguage
fieldLangs = M.fromList
    [ ("1d" , Vernacular) , ("1e" , Vernacular) , ("1i" , Vernacular)
    , ("1p" , Vernacular) , ("1s" , Vernacular) , ("2d" , Vernacular)
    , ("2p" , Vernacular) , ("2s" , Vernacular) , ("3d" , Vernacular)
    , ("3p" , Vernacular) , ("3s" , Vernacular) , ("4d" , Vernacular)
    , ("4p" , Vernacular) , ("4s" , Vernacular) , ("a"  , Vernacular)
    , ("an" , Vernacular) , ("bb" , English)    , ("bw" , English)
    , ("ce" , English)    , ("cf" , Vernacular) , ("cn" , National)
    , ("cr" , National)   , ("de" , English)    , ("dn" , National)
    , ("dr" , Regional)   , ("dt" , Other)      , ("dv" , Vernacular)
    , ("ec" , English)    , ("ee" , English)    , ("eg" , English)
    , ("en" , National)   , ("er" , Regional)   , ("es" , English)
    , ("et" , Other)  {- defined as vernacular in SIL Toolbox, but by
                         definition it's really a different language -}
    , ("ev" , Vernacular) , ("ge" , English)
    , ("gn" , National)   , ("gr" , Regional)   , ("gv" , Vernacular)
    , ("hm" , English)    , ("is" , English)    , ("lc" , Vernacular)
    , ("le" , English)    , ("lf" , English)    , ("ln" , National)
    , ("lr" , Regional)   , ("lt" , English)    , ("lv" , Vernacular)
    , ("lx" , Vernacular) , ("mn" , Vernacular) , ("mr" , Vernacular)
    , ("na" , English)    , ("nd" , English)    , ("ng" , English)
    , ("np" , English)    , ("nq" , English)    , ("ns" , English)
    , ("nt" , English)    , ("oe" , English)    , ("on" , National)
    , ("or" , Regional)   , ("ov" , Vernacular) , ("pc" , English)
    , ("pd" , English)    , ("pde", English)    , ("pdl", English)
    , ("pdn", National)   , ("pdr", Regional)   , ("pdv", Vernacular)
    , ("ph" , Other)      , ("pl" , Vernacular) , ("pn" , National)
    , ("ps" , English)    , ("rd" , Vernacular) , ("re" , English)
    , ("rf" , English)    , ("rn" , National)   , ("rr" , Regional)
    , ("sc" , English)    , ("sd" , English)    , ("se" , Vernacular)
    , ("sg" , Vernacular) , ("sn" , English)    , ("so" , English)
    , ("st" , English)    , ("sy" , Vernacular) , ("tb" , English)
    , ("th" , Vernacular) , ("u"  , Vernacular) , ("ue" , English)
    , ("un" , National)   , ("ur" , Regional)   , ("uv" , Vernacular)
    , ("va" , Vernacular) , ("ve" , English)    , ("vn" , National)
    , ("vr" , Regional)   , ("we" , English)    , ("wn" , National)
    , ("wr" , Regional)   , ("xe" , English)    , ("xn" , National)
    , ("xr" , Regional)   , ("xv" , Vernacular)
    ]