brassica-0.3.0: src/Brassica/SFM/MDF.hs
{-# LANGUAGE NamedFieldPuns #-}
{-| 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.SFM.MDF where
import Brassica.SFM.SFM
import qualified Data.Map as M
import Brassica.SoundChange.Tokenise
import Brassica.SoundChange.Types (PWord)
import Text.Megaparsec (State(..), PosState (..), ParseErrorBundle, runParser')
import Text.Megaparsec.State (initialPosState)
import Data.Void (Void)
import Data.Char (isSpace)
import Data.List (dropWhileEnd)
-- | 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. The exception is
-- @\et@, which is assigned as 'Other' rather than
-- 'Vernacular'. 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)
]
-- | Standard MDF hierarchy: with @\lx@ > @\se@ > @\ps@ > @\sn@.
-- Intended for use with 'toTree'.
mdfHierarchy :: Hierarchy
mdfHierarchy = M.fromList
[ ("1d", "ps"), ("1e", "ps"), ("1i", "ps"), ("1p", "ps"), ("1s", "ps")
, ("2d", "ps"), ("2p", "ps"), ("2s", "ps"), ("3d", "ps"), ("3p", "ps")
, ("3s", "ps"), ("4d", "ps"), ("4p", "ps"), ("4s", "ps"), ("a", "lx")
, ("an", "sn"), ("bb", "sn"), ("bw", "se"), ("ce", "cf"), ("cf", "sn")
, ("cn", "cf"), ("cr", "cf"), ("de", "sn"), ("dn", "sn"), ("dr", "sn")
, ("dt", "lx"), ("dv", "sn"), ("ec", "et"), ("ee", "sn"), ("eg", "et")
, ("en", "sn"), ("er", "sn"), ("es", "et"), ("et", "se"), ("ev", "sn")
, ("ge", "sn"), ("gn", "sn"), ("gr", "sn"), ("gv", "sn"), ("hm", "lx")
, ("is", "sn"), ("lc", "lx"), ("le", "lv"), ("lf", "sn"), ("ln", "lv")
, ("lr", "lv"), ("lt", "sn"), ("lv", "lf"), ("mn", "se"), ("mr", "se")
, ("na", "sn"), ("nd", "sn"), ("ng", "sn"), ("np", "sn"), ("nq", "sn")
, ("ns", "sn"), ("nt", "sn"), ("oe", "sn"), ("on", "sn"), ("or", "sn")
, ("ov", "sn"), ("pc", "sn"), ("pd", "ps"), ("pde", "pdl")
, ("pdl", "pd"), ("pdn", "pdl"), ("pdr", "pdl"), ("pdv", "pdl")
, ("ph", "se"), ("pl", "ps"), ("pn", "ps"), ("ps", "se"), ("rd", "ps")
, ("re", "sn"), ("rf", "sn"), ("rn", "sn"), ("rr", "sn"), ("sc", "sn")
, ("sd", "sn"), ("se", "lx"), ("sg", "ps"), ("sn", "ps"), ("so", "sn")
, ("st", "lx"), ("sy", "sn"), ("tb", "sn"), ("th", "sn"), ("u", "lx")
, ("ue", "sn"), ("un", "sn"), ("ur", "sn"), ("uv", "sn"), ("va", "sn")
, ("ve", "va"), ("vn", "va"), ("vr", "va"), ("we", "sn"), ("wn", "sn")
, ("wr", "sn"), ("xe", "xv"), ("xn", "xv"), ("xr", "xv"), ("xv", "rf")
]
-- | Alternate MDF hierarchy: with @\lx@ > @\sn@ > @\se@ > @\ps@.
-- Intended for use with 'toTree'.
mdfAlternateHierarchy :: Hierarchy
mdfAlternateHierarchy = M.fromList
[ ("1d", "ps"), ("1e", "ps"), ("1i", "ps"), ("1p", "ps"), ("1s", "ps")
, ("2d", "ps"), ("2p", "ps"), ("2s", "ps"), ("3d", "ps"), ("3p", "ps")
, ("3s", "ps"), ("4d", "ps"), ("4p", "ps"), ("4s", "ps")
, ("an", "ps"), ("bb", "ps"), ("bw", "se"), ("ce", "cf"), ("cf", "ps")
, ("cn", "cf"), ("cr", "cf"), ("de", "ps"), ("dn", "ps"), ("dr", "ps")
, ("dt", "lx"), ("dv", "ps"), ("ec", "et"), ("ee", "ps"), ("eg", "et")
, ("en", "ps"), ("er", "ps"), ("es", "et"), ("et", "se"), ("ev", "ps")
, ("ge", "ps"), ("gn", "ps"), ("gr", "ps"), ("gv", "ps"), ("hm", "lx")
, ("is", "ps"), ("lc", "lx"), ("le", "lv"), ("lf", "ps"), ("ln", "lv")
, ("lr", "lv"), ("lt", "ps"), ("lv", "lf"), ("mn", "se"), ("mr", "se")
, ("na", "ps"), ("nd", "ps"), ("ng", "ps"), ("np", "ps"), ("nq", "ps")
, ("ns", "ps"), ("nt", "ps"), ("oe", "ps"), ("on", "ps"), ("or", "ps")
, ("ov", "ps"), ("pc", "ps"), ("pd", "ps"), ("pde", "pdl")
, ("pdl", "pd"), ("pdn", "pdl"), ("pdr", "pdl"), ("pdv", "pdl")
, ("ph", "se"), ("pl", "ps"), ("pn", "ps"), ("ps", "se"), ("rd", "ps")
, ("re", "ps"), ("rf", "ps"), ("rn", "ps"), ("rr", "ps"), ("sc", "ps")
, ("sd", "ps"), ("se", "sn"), ("sg", "ps"), ("sn", "lx"), ("so", "ps")
, ("st", "lx"), ("sy", "ps"), ("tb", "ps"), ("th", "ps")
, ("ue", "ps"), ("un", "ps"), ("ur", "ps"), ("uv", "ps"), ("va", "se")
, ("ve", "va"), ("vn", "va"), ("vr", "va"), ("we", "ps"), ("wn", "ps")
, ("wr", "ps"), ("xe", "xv"), ("xn", "xv"), ("xr", "xv"), ("xv", "rf")
]
-- | Convert an 'SFM' document to a list of 'Component's representing
-- the same textual content. 'Vernacular' field values are tokenised as
-- if using 'tokeniseWords'; everything else is treated as a
-- 'Separator', so that it is not disturbed by operations such as rule
-- application or rendering to text.
tokeniseMDF
:: [String] -- ^ List of available multigraphs (as with 'tokeniseWord')
-> SFM -> Either (ParseErrorBundle String Void) [Component PWord]
tokeniseMDF gs = fmap concat . traverse (tokeniseField gs)
-- | Like 'tokeniseMDF', but for a single 'Field'.
tokeniseField :: [String] -> Field -> Either (ParseErrorBundle String Void) [Component PWord]
tokeniseField gs f = case M.lookup (fieldMarker f) fieldLangs of
Just Vernacular ->
let ps = initialPosState "" (fieldValue f)
s = State
{ stateInput = fieldValue f
, stateOffset = 0
, statePosState = case fieldSourcePos f of
Nothing -> ps
Just sp -> ps { pstateSourcePos = sp }
, stateParseErrors = []
}
in case runParser' (componentsParser $ wordParser "[" gs) s of
(_, Right cs) -> Right $ Separator ('\\' : fieldMarker f ++ fieldWhitespace f) : cs
(_, Left err) -> Left err
_ -> Right [Separator $ '\\' : fieldMarker f ++ fieldWhitespace f ++ fieldValue f]
-- | 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
:: (String -> String)
-- ^ Transformation to apply to etymologies, e.g. @('*':)@
-> SFMTree
-> SFMTree
duplicateEtymologies f = go Nothing Nothing
where
-- strategy: find each \se (implicit or explicit) with its \ge
-- and make an \et under it
go lx gl (Root ts) = Root $ go lx gl <$> ts
go _lx gl t@(Filled m@(Field { fieldMarker="lx", fieldValue }) ts) =
let lx = Just fieldValue
gl' = case searchField isGloss t of
gl'':_ -> Just gl''
_ -> gl
in Filled m $ go lx gl' <$> ts
go _lx gl t@(Filled m@(Field { fieldMarker="se", fieldValue }) ts) =
let lx = Just fieldValue
gl' = case searchField isGloss t of
gl'':_ -> Just gl''
_ -> gl
in Filled m $ ts ++ mkEt lx gl'
go lx gl (Filled m ts) = Filled m $ go lx gl <$> ts
go lx gl (Missing "se" ts) = Missing "se" $ ts ++ mkEt lx gl
go lx gl (Missing m ts) = Missing m $ go lx gl <$> ts
isGloss Field{fieldMarker,fieldValue}
| fieldMarker == "ge" = Just fieldValue
| otherwise = Nothing
mkEt :: Maybe String -> Maybe String -> [SFMTree]
mkEt Nothing _ = [] -- can't make etymology without lexeme
mkEt (Just lx) gl = pure $
Filled Field
{ fieldMarker = "et"
, fieldWhitespace = " "
, fieldSourcePos = Nothing
, fieldValue = f $ trim lx
}
$ case gl of
Nothing -> []
Just gl' ->
[ Filled Field
{ fieldMarker = "eg"
, fieldWhitespace = " "
, fieldSourcePos = Nothing
, fieldValue = gl'
-- no need to add newline here because 'gl'
-- should already have whitespace
} []
]
trim = dropWhile isSpace . dropWhileEnd isSpace