packages feed

fluent-syntax-1.1.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 Control.Exception (displayException)
import Data.Bifunctor (first)
import Data.Char (toLower, toUpper)
import Data.List qualified as List
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Maybe (mapMaybe)
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 (parseResource, parseResourceLenient, rejectJunk)
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
    , Loc (loc_start)
    , Quasi (qAddDependentFile)
    , lift
    , location
    , makeRelativeToProject
    , mkName
    , runIO
    )
import Prelude

-- | Parses a 'Resource'.
-- Strips the indentation shared by every line.
-- Syntax errors fail the compilation.
fluent :: QuasiQuoter
fluent =
    QuasiQuoter
        { quoteExp = \quoted -> do
            start <- loc_start <$> location
            either (fail . List.intercalate "\n" . fmap displayException . NonEmpty.toList) lift $
                parseQuoted start quoted
        , 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
    parseQuoted :: (Int, Int) -> String -> Either (NonEmpty ParseError) Resource
    parseQuoted (firstLine, firstColumn) quoted =
        first (fmap locate) . rejectJunk . parseResourceLenient $ text
      where
        (indent, text) = dedent $ Text.pack quoted
        locate error =
            error
                { line = firstLine + error.line - 1
                , column = indent + error.column + if error.line == 1 then firstColumn - 1 else 0
                }

    dedent :: Text -> (Int, Text)
    dedent (Text.lines -> lines) = (shared, Text.unlines $ Text.drop shared <$> lines)
      where
        shared = minimum . (maxBound :) $ mapMaybe (Text.findIndex (/= ' ')) lines

-- | 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 ParseError

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