packages feed

mjml-0.1.0.0: test/Main.hs

-- | Render every fixture and compare it with the HTML MJML itself produced.
--
-- @fixtures/NAME.mjml@ is the input and @fixtures/NAME.html@ MJML 5's output,
-- regenerated by @scripts/update-fixtures.mjs@. Arguments restrict the run to
-- fixtures whose name contains one of them; @--verbose@ prints each result.
module Main (main) where

import Control.Monad (forM, unless, when)
import Data.Char (isAlphaNum, isAscii)
import Data.List (isInfixOf, isSuffixOf, sort)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import System.Directory (doesFileExist, listDirectory)
import System.Environment (getArgs)
import System.Exit (exitFailure)
import System.FilePath (dropExtension, (</>))

import HtmlCompare (compareHtml)
import Text.Mjml (MjmlError (..), defaultOptions, renderText)


fixtureDir :: FilePath
fixtureDir = "test/fixtures"


main :: IO ()
main = do
    args <- getArgs
    let verbose = "--verbose" `elem` args
        filters = filter (/= "--verbose") args
    names <-
        sort . map dropExtension . filter (".mjml" `isSuffixOf`) <$> listDirectory fixtureDir
    let selected = [n | n <- names, null filters || any (`isInfixOf` n) filters]
    results <- forM selected $ \n -> do
        let html = fixtureDir </> n <> ".html"
        present <- doesFileExist html
        if not present
            then pure Nothing
            else do
                source <- T.readFile (fixtureDir </> n <> ".mjml")
                expected <- T.readFile html
                let outcome = case renderText defaultOptions source of
                        Left (MjmlError e) -> Just ("render error: " <> e)
                        Right out -> compareHtml (normalizeIds expected) (normalizeIds out)
                case outcome of
                    Nothing -> when verbose (putStrLn ("ok   " <> n))
                    Just why -> do
                        putStrLn ("FAIL " <> n)
                        when (verbose || length selected <= 3) (T.putStrLn why)
                pure (Just (n, outcome))
    let ran = [r | Just r <- results]
        failed = [n | (n, Just _) <- ran]
    putStrLn (show (length ran - length failed) <> "/" <> show (length ran) <> " fixtures match MJML")
    unless (null failed) exitFailure


-- | Generated identifiers are random in MJML; both sides use zeros.
--
-- As in mrml's fixture script: an identifier a @for@ attribute points at
-- (accordion, navbar) becomes @00000000@ in both its @for@ and its @id@, and a
-- carousel's identifier becomes @00000000@ wherever it names a class or radio.
normalizeIds :: Text -> Text
normalizeIds = normalizeCarouselIds . normalizeForIds


normalizeForIds :: Text -> Text
normalizeForIds t = foldr replaceId t ids
  where
    ids =
        [ v
        | chunk <- drop 1 (T.splitOn "for=\"" t)
        , let v = T.takeWhile (/= '"') chunk
        , not (T.null v)
        , T.all isAsciiAlphaNum v
        ]
    replaceId v =
        T.replace ("for=\"" <> v <> "\"") "for=\"00000000\""
            . T.replace ("id=\"" <> v <> "\"") "id=\"00000000\""


normalizeCarouselIds :: Text -> Text
normalizeCarouselIds t = foldr replaceId t ids
  where
    ids =
        [ v
        | chunk <- drop 1 (T.splitOn "mj-carousel-" t)
        , let v = T.takeWhile isAsciiAlphaNum chunk
        , T.length v >= 12 && T.length v <= 16
        , T.take 1 (T.drop (T.length v) chunk) == "-"
        ]
    replaceId v =
        T.replace ("mj-carousel-" <> v <> "-") "mj-carousel-00000000-"
            . T.replace ("mj-carousel-radio-" <> v) "mj-carousel-radio-00000000"


isAsciiAlphaNum :: Char -> Bool
isAsciiAlphaNum c = isAscii c && isAlphaNum c