vimeta-0.3.0.0: src/Vimeta/Core/Format.hs
-- |
--
-- Copyright:
-- This file is part of the package vimeta. It is subject to the
-- license terms in the LICENSE file found in the top-level
-- directory of this distribution and at:
--
-- https://github.com/pjones/vimeta
--
-- No part of this package, including this file, may be copied,
-- modified, propagated, or distributed except according to the terms
-- contained in the LICENSE file.
--
-- License: BSD-2-Clause
module Vimeta.Core.Format
( FormatTable,
fromFormatString,
formatYear,
formatFullDate,
)
where
import Data.Time (Day (..), defaultTimeLocale, formatTime)
import Relude.Extra.Map
import System.Process.Internals (translate)
import Text.Parsec hiding ((<|>))
-- | Mapping of format characters to their possible replacement text.
type FormatTable = Map Char (Maybe Text)
-- | Syntax tree for format strings.
data Replacement
= -- | Replace the given character.
Replace Char
| -- | Conditional section.
Condition [(Text, Replacement)]
| -- | End of input (or condition).
EndOfInput
-- | Parser type.
type Parser a = ParsecT Text () (Reader FormatTable) a
-- | Replace format characters prefixed with a @%@ with the
-- replacement text found in the given 'Map'.
fromFormatString ::
-- | Format character mapping.
FormatTable ->
-- | Name of format string.
String ->
-- | Input text.
Text ->
-- | Output text or error.
Either String Text
fromFormatString table name input =
case runReader (runParserT parseFormatString () name input) table of
Left e -> Left (show e)
Right t -> Right t
-- | Format a 'Day' using the XML schema notation.
formatFullDate :: Maybe Day -> Maybe Text
formatFullDate = formatDay "%Y-%m-%dT00:00:00Z"
-- | Format a 'Day' displaying just the year.
formatYear :: Maybe Day -> Maybe Text
formatYear = formatDay "%Y"
formatDay :: String -> Maybe Day -> Maybe Text
formatDay fmt d = toText . formatTime defaultTimeLocale fmt <$> d
parseFormatString :: Parser Text
parseFormatString = manyTill go eof >>= renderFormatString
where
go = findFormatCharacter >>= mkReplacement
-- | Render a format string syntax table as a 'Text' value.
renderFormatString :: [(Text, Replacement)] -> Parser Text
renderFormatString rs = do
table <- ask
return (mconcat $ map (render table) rs)
where
escape :: Text -> Text
escape = toText . translate . toString
findChar :: FormatTable -> Char -> Text
findChar t c = fromMaybe "" $ join (lookup c t)
render :: FormatTable -> (Text, Replacement) -> Text
render tbl (txt, Replace c) = txt <> escape (findChar tbl c)
render tbl (txt, Condition c) = txt <> renderCondition tbl c
render _ (txt, EndOfInput) = txt
renderCondition :: FormatTable -> [(Text, Replacement)] -> Text
renderCondition tbl conds =
if all (checkCondition tbl) conds
then mconcat $ map (render tbl) conds
else mempty
checkCondition :: FormatTable -> (Text, Replacement) -> Bool
checkCondition tbl (_, Replace c) = isJust (join $ lookup c tbl)
checkCondition tbl (_, Condition c) = all (checkCondition tbl) c
checkCondition _ (_, EndOfInput) = True
-- | Location a format character preceded by a @'%'@ character.
-- Returns the text leading up to the format character and the
-- character itself.
findFormatCharacter :: Parser (Text, Maybe Char)
findFormatCharacter = do
beforeText <- toText <$> manyTill anyChar (try eofOrFormatChar)
formatChar <- try $ (Just <$> anyChar) <|> return Nothing
return (beforeText, formatChar)
where
eofOrFormatChar :: Parser ()
eofOrFormatChar = eof <|> void (char '%')
-- | Translate the output from 'findFormatCharacter' into a syntax node.
mkReplacement :: (Text, Maybe Char) -> Parser (Text, Replacement)
mkReplacement (beforeText, formatChar) =
case formatChar of
Nothing -> return (beforeText, EndOfInput)
Just '{' -> (beforeText,) <$> (Condition <$> parseConditional)
Just c -> return (beforeText, Replace c)
-- | Parse a conditional section out of a format string.
parseConditional :: Parser [(Text, Replacement)]
parseConditional = do
(beforeText, formatChar) <- findFormatCharacter
case formatChar of
-- Reached the end of the format string.
Nothing -> unexpected "end of format string, expected `%}'"
-- Start another conditional.
Just '{' -> do
other <- parseConditional
return [(beforeText, Condition other)]
-- End this conditional.
Just '}' -> return [(beforeText, EndOfInput)]
-- Add this replacement to the list, fetch the next one.
Just c -> do
next <- parseConditional
return ((beforeText, Replace c) : next)