packages feed

fluent-syntax-1.0.0: src/Language/Fluent/TH.hs

{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE ExplicitForAll #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Language.Fluent.TH (fluent, messageIdentifiers) where

import Data.Char (toLower, toUpper)
import Data.String (IsString, fromString)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.IO qualified as Text
import Data.Traversable (for)
import Language.Fluent.AST
import Language.Fluent.Parser (parse, parseResource, resource)
import Language.Haskell.TH.Lib
    ( DecsQ
    , appE
    , litE
    , normalB
    , sigD
    , stringL
    , valD
    , varE
    , varP
    )
import Language.Haskell.TH.Quote (QuasiQuoter (..))
import Language.Haskell.TH.Syntax
    ( Lift
    , Quasi (qAddDependentFile)
    , lift
    , makeRelativeToProject
    , mkName
    , runIO
    )
import Prelude

-- | Parses a 'Resource'.
-- Strips the indentation shared by every line.
fluent :: QuasiQuoter
fluent =
    QuasiQuoter
        { quoteExp = either fail lift . parse resource . dedent . Text.pack
        , quotePat = const $ fail "a Fluent Resource is not a pattern"
        , quoteType = const $ fail "a Fluent Resource is not a type"
        , quoteDec = const $ fail "a Fluent Resource is not a declaration"
        }
  where
    dedent :: Text -> Text
    dedent written = Text.unlines $ Text.drop shared <$> lines'
      where
        lines' = Text.lines written
        shared = minimum . (maxBound :) $ do
            line <- lines'
            let (indent, rest) = Text.span (== ' ') line
            [Text.length indent | not . Text.null $ rest]

-- | Declares a constant for every message identifier in the given Fluent file.
--
-- > messageIdentifiers "en.ftl"
--
-- declares, for the message @text-field-intro@,
--
-- > textFieldIntro :: (IsString s) => s
-- > textFieldIntro = "text-field-intro"
messageIdentifiers :: FilePath -> DecsQ
messageIdentifiers path = do
    path <- makeRelativeToProject path
    qAddDependentFile path
    contents <- runIO $ Text.readFile path
    Resource{entries} <- either fail pure $ parseResource contents
    mconcat <$> for entries \case
        (MessageEntry Message{..}) -> declare id
        _ -> pure []
  where
    declare :: Identifier -> DecsQ
    declare (Identifier identifier@(mkName . escape . camel -> name)) =
        sequence
            [ sigD name [t|forall s. (IsString s) => s|]
            , valD (varP name) (normalB $ varE 'fromString `appE` (litE . stringL . Text.unpack) identifier) []
            ]
    camel :: Text -> String
    camel =
        Text.unpack
            . mconcat
            . zipWith ($) (firstChar toLower : repeat (firstChar toUpper))
            . filter (not . Text.null)
            . Text.splitOn "-"
    firstChar :: (Char -> Char) -> Text -> Text
    firstChar f = maybe Text.empty (\(c, cs) -> Text.cons (f c) cs) . Text.uncons
    escape :: String -> String
    escape name
        | name `elem` keywords = name <> "'"
        | otherwise = name
    keywords :: [String]
    keywords =
        [ "case"
        , "class"
        , "data"
        , "default"
        , "deriving"
        , "do"
        , "else"
        , "foreign"
        , "if"
        , "import"
        , "in"
        , "infix"
        , "infixl"
        , "infixr"
        , "instance"
        , "let"
        , "module"
        , "newtype"
        , "of"
        , "then"
        , "type"
        , "where"
        ]

deriving stock instance Lift Resource

deriving stock instance Lift Entry

deriving stock instance Lift Message

deriving stock instance Lift Term

deriving stock instance Lift Comment

deriving stock instance Lift Attribute

deriving stock instance Lift Pattern

deriving stock instance Lift PatternElement

deriving stock instance Lift Placeable

deriving stock instance Lift Expression

deriving stock instance Lift SelectExpression

deriving stock instance Lift InlineExpression

deriving stock instance Lift AttributeAccessor

deriving stock instance Lift Variant

deriving stock instance Lift VariantKey

deriving stock instance Lift VariantList

deriving stock instance Lift CallArguments

deriving stock instance Lift NamedArgument

deriving stock instance Lift Identifier

deriving stock instance Lift NumberLiteral

deriving stock instance Lift StringLiteral