packages feed

mole-0.0.3: src/Data/Mole/Builder/Html.hs

module Data.Mole.Builder.Html
    ( htmlBuilder
    ) where


import           Control.Monad

import           Data.Map (Map)
import qualified Data.Map as M

import qualified Data.Set as S

import qualified Data.Text          as T
import qualified Data.Text.Encoding as T

import           Data.Maybe
import           Data.CSS.Syntax.Tokens

import           Text.HTML.TagSoup

import           Data.Mole.Types
import           Data.Mole.Builder.Stylesheet



data Transformer = Transformer
    { tagSelector       :: String -> Bool
    , attributeName     :: String
    , depExtractor      :: String -> [AssetId]
    , attributeRenderer :: Map AssetId String -> String -> Either Error String
    }

extractSingleAsset :: String -> [AssetId]
extractSingleAsset x = [AssetId x]

renderSingleAttribute :: Map AssetId String -> String -> Either Error String
renderSingleAttribute m v =
    case M.lookup (AssetId v) m of
        Just v' -> Right v'
        Nothing -> case M.lookup (AssetId $ tail v) m of
            Just v'' -> Right v''
            Nothing -> Left (UndeclaredDependency (AssetId v))

extractStylesheetAssets :: String -> [AssetId]
extractStylesheetAssets v =
    let Right tokens = tokenize (T.pack v)
    in catMaybes $ (flip map) tokens $ \t ->
            case t of
                (Url x) -> Just $ urlAssetId (T.unpack x)
                _       -> Nothing

renderStylesheetAssets :: Map AssetId String -> String -> Either Error String
renderStylesheetAssets m v = do
    let Right tokens = tokenize (T.pack v)
    newTokens <- forM tokens $ \t -> case t of
        (Url x) -> case M.lookup (urlAssetId (T.unpack x)) m of
            Nothing -> Left (UndeclaredDependency (AssetId (T.unpack x)))
            Just v -> Right (Url $ T.pack $ reconstructUrl (T.unpack x) v)
        _ -> return t

    return $ T.unpack $ serialize newTokens

tagTransformers :: [Transformer]
tagTransformers =
    [ Transformer ("link"==)   "href"    extractSingleAsset renderSingleAttribute
    , Transformer ("img"==)    "src"     extractSingleAsset renderSingleAttribute
    , Transformer ("script"==) "src"     extractSingleAsset renderSingleAttribute
    , Transformer ("a"==)      "href"    extractSingleAsset renderSingleAttribute
    , Transformer ("meta"==)   "content" extractSingleAsset renderSingleAttribute
    , Transformer (const True) "style"   extractStylesheetAssets renderStylesheetAssets
    ]

tagTransformersFor :: String -> [Transformer]
tagTransformersFor tag = filter (\t -> tagSelector t tag) tagTransformers


toInlineStyleDep :: [AssetId] -> [Tag String] -> [AssetId]
toInlineStyleDep acc [] = acc
toInlineStyleDep acc ((TagOpen "style" _):(TagText text):(TagClose "style"):xs)
    = toInlineStyleDep (acc ++ extractStylesheetAssets text) xs
toInlineStyleDep acc (x:xs) = toInlineStyleDep acc xs

renderInlineStyles :: Map AssetId String -> [Tag String] -> Either Error [Tag String]
renderInlineStyles m [] = return []
renderInlineStyles m ((TagOpen "style" attrs):(TagText text):(TagClose "style"):xs) = do
    text' <- renderStylesheetAssets m text
    rest  <- renderInlineStyles m xs
    return $ (TagOpen "style" attrs) : (TagText text') : (TagClose "style") : rest
renderInlineStyles m (x:xs) = do
    xs' <- renderInlineStyles m xs
    return $ x:xs'

htmlBuilder :: String -> String -> Handle -> AssetId -> IO Builder
htmlBuilder pubId src _ _ = do
    body <- readFile src
    let tags = parseTags body
    let deps = concatMap toDep tags
    let inlineStyleDeps = toInlineStyleDep [] tags
    return $ Builder (S.singleton src) (S.fromList (deps ++ inlineStyleDeps)) (render tags)

  where
    toDep :: Tag String -> [AssetId]
    toDep (TagOpen tag attrs) =
        concatMap (\t -> case lookup (attributeName t) attrs of
            Nothing -> []
            Just v ->  depExtractor t v
        ) (tagTransformersFor tag)
    toDep _ = []

    render :: [Tag String] -> Map AssetId String -> Either Error Result
    render tags m = do
        t' <- forM tags $ \t -> do
            insertResult m t

        t'' <- renderInlineStyles m t'

        let body = T.encodeUtf8 $ T.pack $ renderTags t''
        return $ Result pubId $ Just (body, "text/html")

    insertResult :: Map AssetId String -> Tag String -> Either Error (Tag String)
    insertResult m t@(TagOpen tag attrs) = do
        let tfs = tagTransformersFor tag
        let f at tf = mapM (overrideAttr tf m) at

        TagOpen <$> (pure tag) <*> foldM f attrs tfs

    insertResult _ t = pure t

    overrideAttr :: Transformer -> Map AssetId String -> (String,String) -> Either Error (String,String)
    overrideAttr tf m (k,v)
        | k == attributeName tf = attributeRenderer tf m v >>= \v' -> Right (k, v')
        | otherwise = Right (k,v)