packages feed

brassica-0.3.0: src/Brassica/SFM/SFM.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}

{-| This module implements basic support for the SIL Standard Format
Marker (SFM) format, used by dictionary software such as
[FieldWorks](https://software.sil.org/fieldworks/). This format forms
the basis of standards such as Multi-Dictionary Formatter (MDF),
implemented here in 'Brassica.SFM.MDF'.
-}
module Brassica.SFM.SFM
       ( -- * Linear SFM documents
         Field(..)
       , SFM
       , parseSFM
       , exactPrintField
       , exactPrintSFM
       , stripSourcePos
         -- * Hierarchies
       , Hierarchy
       , SFMTree(..)
       , toTree
       , fromTree
       , mapField
       , searchField
       ) where

import Data.Char (isSpace)
import Data.Maybe (fromMaybe)
import Data.Void (Void)

import Text.Megaparsec
import Text.Megaparsec.Char

import qualified Data.Map as M

-- | A single field of an SFM file.
data Field = Field
    { fieldMarker :: String
    -- ^ The field marker, ommitting the initial backslash
    , fieldWhitespace :: String
    -- ^ Whitespace after the field marker
    , fieldSourcePos :: Maybe SourcePos
    -- ^ Optionally, a Megaparsec 'SourcePos' marking the start of the value
    , fieldValue :: String
    -- ^ The value of the field, including all whitespace until the next marker
    } deriving (Show)

-- | An SFM file, being a list of fields.
type SFM = [Field]

-- | Set the 'fieldSourcePos' of a 'Field' to 'Nothing'. Useful for
-- making debug output shorter.
stripSourcePos :: Field -> Field
stripSourcePos f = f { fieldSourcePos = Nothing }

type Parser = Parsec Void String

sc :: Parser String
sc = fmap (fromMaybe "") $ optional $
    takeWhile1P (Just "white space") ((&&) <$> isSpace <*> (/='\n'))

-- | Parse until the next backslash at the beginning of a line.
parseFieldValue :: Parser String
parseFieldValue = do
    val <- takeWhileP Nothing (/= '\n')
    observing (char '\n') >>= \case
        Left _ -> val <$ eof
        Right _ -> do
            let val' = val++"\n"
            -- parse more lines if no following backslash
            (notFollowedBy (char '\\') *> ((val'++) <$> parseFieldValue))
                <|> pure val'

entry :: Parser (String, String, SourcePos, String)
entry = do
    _ <- char '\\'
    marker <- takeWhile1P (Just "field name") (not . isSpace)
    s <- sc
    ps <- getSourcePos
    value <- parseFieldValue
    pure (marker, s, ps, value)

-- | Parse an SFM file to an 'SFM'.
parseSFM
    :: String  -- ^ Name of source file
    -> String  -- ^ Input SFM data to parse
    -> Either (ParseErrorBundle String Void) SFM
parseSFM = runParser (sc *> many (toField <$> entry) <* eof)
  where
    toField (f, s, p, v) = Field f s (Just p) v

-- | Print a single field as 'String'.
exactPrintField :: Field -> String
exactPrintField f = '\\' : fieldMarker f ++ fieldWhitespace f ++ fieldValue f

-- | Given an 'SFM', reconstruct the original file. A trivial wrapper
-- around 'exactPrintField'.
exactPrintSFM :: SFM -> String
exactPrintSFM = concatMap exactPrintField

-- | Rose tree describing a hierarchical SFM document.
data SFMTree
    = Root [SFMTree]             -- ^ Root node
    | Filled Field [SFMTree]     -- ^ A 'Field' with zero or more children.
    | Missing String [SFMTree]
    -- ^ A missing level of the hierarchy: a marker which is inferred
    -- from the presence of its children.
    deriving (Show)

-- | The hierarchy underlying an SFM document, defined as a map from
-- field names to their parents. Unlisted fields are treated as roots.
type Hierarchy = M.Map String String

-- | Returns the full hierarchy of a marker, starting with its
-- immediate parent and finishing with the root.
hierarchyFor :: Hierarchy -> String -> [String]
hierarchyFor h = go
  where
    go :: String -> [String]
    go m = case M.lookup m h of
        Just m' -> m' : go m'
        Nothing -> []

(<+:>) :: SFMTree -> SFMTree -> SFMTree
(Root s) <+:> t = Root (s ++ [t])
(Filled f s) <+:> t = Filled f (s ++ [t])
(Missing f s) <+:> t = Missing f (s ++ [t])

-- | Use a 'Hierarchy' to generate a tree structure from an 'SFM'
-- document. Fields are converted to 'Filled' nodes, containing as
-- many following nodes as possible, until the next node which is at
-- the same level of the hierarchy or lower. 'Missing' nodes are
-- created for any missing levels of the hierarchy.
toTree :: Hierarchy -> SFM -> SFMTree
toTree h = fst . go (Root [])
  where
    go :: SFMTree -> SFM -> (SFMTree, SFM)
    go t [] = (t, [])
    go s@(Root _) (f:fs) =
        let (subtree, fs') = go (Filled f []) fs
        in go (s <+:> subtree) fs'
    go s (f:fs) =
        let parentMarker = case s of
                Filled (Field{fieldMarker=m}) _ -> m
                Missing m _ -> m
            hierarchy = hierarchyFor h (fieldMarker f)
        in case break (==parentMarker) hierarchy of
            -- this marker is unrelated to parentMarker, need to
            -- rewind back up the tree and try again
            (_, []) -> (s, f:fs)

            -- otherwise this marker belongs somewhere under
            -- parentMarker

            -- if it is an immediate child, add the subtree directly
            ([], _) ->
                let (subtree, fs') = go (Filled f []) fs
                in go (s <+:> subtree) fs'

            -- otherwise, recurse into the hierarchy
            (m:_, _) ->
                let (subtree, fs') = go (Missing m []) (f:fs)
                in go (s <+:> subtree) fs'

-- | Inverse of 'toTree': convert an 'SFMTree' back into a linear
-- 'SFM' document.
fromTree :: SFMTree -> SFM
fromTree (Root s) = concatMap fromTree s
fromTree (Filled f s) = f : concatMap fromTree s
fromTree (Missing _ s) = concatMap fromTree s

-- | Map a function over all the 'Field's in an 'SFMTree'.
mapField :: (Field -> Field) -> SFMTree -> SFMTree
mapField g (Root s) = Root $ mapField g <$> s
mapField g (Filled f s) = Filled (g f) $ mapField g <$> s
mapField g (Missing m s) = Missing m $ mapField g <$> s

-- | Depth-first search for fields under an 'SFMTree'.
searchField :: (Field -> Maybe a) -> SFMTree -> [a]
searchField p (Root ts) = searchField p =<< ts
searchField p (Filled f ts)
    | Just a <- p f = a : (searchField p =<< ts)
    | otherwise     =      searchField p =<< ts
searchField p (Missing _ ts) = searchField p =<< ts