mjml-hamlet-0.1.0.0: src/Text/Mjml/Hamlet/Check.hs
-- | What the quasiquoter rejects before generating any code: the checks
-- MJML's validator makes, done at compile time.
--
-- Every element must be an MJML element, sit inside a parent that accepts it,
-- and carry only attributes it declares; an attribute written as a literal
-- must also have a value of the declared type. HTML is only allowed inside
-- ending tags such as @mj-text@, and text only there too.
module Text.Mjml.Hamlet.Check (
checkTemplate,
) where
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Text.Mjml (Registry)
import Text.Mjml.Hamlet.Runtime (safeUrlPrefix, urlAttributes)
import Text.Mjml.Hamlet.Syntax
import Text.Mjml.Schema (checkAttribute, checkChild, knownTag)
-- | Every problem found, in template order.
checkTemplate :: Registry -> Set Text -> [Doc] -> [(Pos, Text)]
checkTemplate reg ending = concatMap (structure Nothing)
where
-- Inside MJML structure: the parent, if there is one in the template.
structure :: Maybe Text -> Doc -> [(Pos, Text)]
structure parent = \case
DTag p name as kids
| not (knownTag reg name) ->
[ ( p
, if "mj" `T.isPrefixOf` name
then "unknown MJML element <" <> name <> ">"
else "<" <> name <> "> is HTML, which can only appear inside an ending tag such as <mj-text>, <mj-button> or <mj-raw>"
)
]
| otherwise ->
placement p parent name
<> concatMap (attribute name) as
<> if
| name `Set.member` ending -> concatMap html kids
| name `elem` ["mj-attributes", "mj-class"] -> []
| otherwise -> concatMap (structure (Just name)) kids
DText p _ ->
[ ( p
, "text can only appear inside an ending tag such as <mj-text>"
<> maybe "" (\n -> ", not directly inside <" <> n <> ">") parent
)
]
DEmbed _ _ -> []
DForall _ _ _ kids -> concatMap (structure parent) kids
DWith _ _ _ kids -> concatMap (structure parent) kids
DIf _ branches final ->
concat [concatMap (structure parent) ks | (_, _, ks) <- branches] <> maybe [] (concatMap (structure parent)) final
DMaybe _ _ _ kids nothing -> concatMap (structure parent) kids <> maybe [] (concatMap (structure parent)) nothing
DCase _ _ alts -> concat [concatMap (structure parent) ks | (_, _, ks) <- alts]
DOf p _ _ -> [(p, "$of without a matching $case")]
placement p parent child = case parent of
Just par | Just why <- checkChild reg par child -> [(p, why)]
_ -> []
attribute tag = \case
Attr p attrName value ->
maybe [] (\why -> [(p, why)]) (checkAttribute reg tag attrName (value >>= literal))
<> interpolation p attrName (maybe [] id value)
ClassAttr p ps ->
maybe [] (\why -> [(p, why)]) (checkAttribute reg tag "css-class" Nothing)
<> interpolation p "class" ps
-- Where an interpolation may go in an attribute value.
interpolation p attrName ps
| any isEmbed ps = [(p, "^{...} cannot be used in an attribute value; use #{...}")]
| not (any isInterp ps) = []
| attrName `elem` urlAttributes = case ps of
[PInterp _ _] -> []
PLit lit : _ | safeUrlPrefix lit -> []
_ ->
[ ( p
, "the URL in "
<> attrName
<> " must be a single #{...} of type SafeUrl, or start with a literal https://, http://, mailto:, /, # or ?"
)
]
| "on" `T.isPrefixOf` T.toLower attrName = [(p, "event handler attributes such as " <> attrName <> " cannot be interpolated")]
| otherwise = []
isEmbed = \case
PEmbed _ _ -> True
_ -> False
isInterp = \case
PInterp _ _ -> True
_ -> False
literal = \case
[] -> Just ""
[PLit t] -> Just t
_ -> Nothing
-- Inside an ending tag everything is HTML, which MJML copies as it is.
html = \case
DTag p name as kids
| "mj-" `T.isPrefixOf` name ->
[(p, "<" <> name <> "> inside an ending tag would be copied as HTML; close the ending tag first")]
| otherwise -> concatMap htmlAttribute as <> concatMap html kids
DForall _ _ _ kids -> concatMap html kids
DWith _ _ _ kids -> concatMap html kids
DIf _ branches final -> concat [concatMap html ks | (_, _, ks) <- branches] <> maybe [] (concatMap html) final
DMaybe _ _ _ kids nothing -> concatMap html kids <> maybe [] (concatMap html) nothing
DCase _ _ alts -> concat [concatMap html ks | (_, _, ks) <- alts]
DText _ _ -> []
DEmbed _ _ -> []
DOf p _ _ -> [(p, "$of without a matching $case")]
where
htmlAttribute = \case
Attr p attrName value -> interpolation p attrName (maybe [] id value)
ClassAttr p ps -> interpolation p "class" ps