atomic-css-0.1.0: src/Web/Atomic/Render.hs
{-# LANGUAGE OverloadedLists #-}
module Web.Atomic.Render where
import Data.ByteString.Lazy qualified as BL
import Data.List qualified as L
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as M
import Data.Maybe (mapMaybe)
import Data.String (IsString (..))
import Data.Text (Text, intercalate, pack)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Encoding qualified as TLE
import HTMLEntities.Text qualified as HE
import Web.Atomic.Html
import Web.Atomic.Types
renderLazyText :: Html () -> TL.Text
renderLazyText = TL.fromStrict . renderText
renderLazyByteString :: Html () -> BL.ByteString
renderLazyByteString = TLE.encodeUtf8 . renderLazyText
renderText :: Html () -> Text
renderText html =
let cs = cssRulesLines $ htmlCSSRules html
in renderLines $ addCss cs $ htmlLines 2 html
where
addCss :: [Line] -> [Line] -> [Line]
addCss [] cnt = cnt
addCss cs cnt = do
styleLines cs <> (Line Newline 0 "" : cnt)
htmlLines :: Int -> Html a -> [Line]
htmlLines ind (Html _ ns) = nodesLines ind ns
nodesLines :: Int -> [Node] -> [Line]
nodesLines ind ns = mconcat $ fmap (nodeLines ind) ns
nodeLines :: Int -> Node -> [Line]
nodeLines ind (Elem e) = elementLines ind e
nodeLines _ (Text t) = [Line Inline 0 $ HE.text t]
nodeLines _ (Raw t) = [Line Newline 0 t]
elementLines :: Int -> Element -> [Line]
elementLines ind elm =
-- special rendering cases for the children
case (elm.content :: [Node]) of
[] ->
-- auto closing creates a bug in chrome. An auto-closed div
-- absorbs the next children
[line $ open <> renderAttributes (elementAttributes elm) <> ">" <> close]
[Text t] ->
-- SINGLE text node, just display it indented
[line $ open <> renderAttributes (elementAttributes elm) <> ">" <> HE.text t <> close]
children ->
-- normal indented rendering
mconcat
[ [line $ open <> renderAttributes (elementAttributes elm) <> ">"]
, fmap (addIndent ind) $ nodesLines ind children
, [line close]
]
where
open = "<" <> elm.name
close = "</" <> elm.name <> ">"
line t =
if elm.inline
then Line Inline 0 t
else Line Newline 0 t
-- Attributes ---------------------------------------------------
-- | Element's attributes do not include class, which is separated. FlatAttributes generate the class attribute and include it
newtype FlatAttributes = FlatAttributes (Map Name AttValue)
deriving newtype (Eq)
-- | The 'FlatAttributes' for an element, inclusive of class.
elementAttributes :: Element -> FlatAttributes
elementAttributes e =
FlatAttributes $
addClasses
(styleClass e)
e.attributes
where
addClasses :: AttValue -> Map Name AttValue -> Map Name AttValue
addClasses "" as = as
addClasses av as = M.insertWith (\a b -> a <> " " <> b) "class" av as
styleClass :: Element -> AttValue
styleClass elm =
classesAttValue (elementClasses elm)
renderAttributes :: FlatAttributes -> Text
renderAttributes (FlatAttributes m) =
case m of
[] -> ""
as -> " " <> T.unwords (map htmlAtt $ M.toList as)
where
htmlAtt (k, v) =
k <> "=" <> "'" <> HE.text v <> "'"
-- REnder CSS --------------------------------------------
cssRulesLines :: Map Selector Rule -> [Line]
cssRulesLines = mapMaybe cssRuleLine . M.elems
cssRuleLine :: Rule -> Maybe Line
cssRuleLine r | null r.properties = Nothing
cssRuleLine r =
let sel = (ruleSelector r).text
props = intercalate "; " (map renderProp r.properties)
med = mconcat $ fmap mediaCriteria r.media
in Just $ Line Newline 0 $ wrapMedia med $ sel <> " { " <> props <> " }"
where
renderProp :: Declaration -> Text
renderProp ((Property p) :. cv) = p <> ":" <> renderStyle cv
renderStyle :: Style -> Text
renderStyle (Style v) = pack v
wrapMedia :: MediaQuery -> Text -> Text
wrapMedia [] cnt = cnt
wrapMedia mqs cnt =
"@media " <> mediaConditionsText mqs <> " { " <> cnt <> " }"
where
mediaConditionsText :: MediaQuery -> Text
mediaConditionsText (MediaQuery cons) =
T.intercalate " and " $ fmap (\c -> "(" <> c <> ")") cons
styleLines :: [Line] -> [Line]
styleLines [] = []
styleLines rulesLines =
[Line Newline 0 "<style type='text/css'>"]
<> rulesLines
<> [Line Newline 0 "</style>"]
-- Lines ---------------------------------------
-- control inline vs newlines and indent
data Line = Line {end :: LineEnd, indent :: Int, text :: Text}
deriving (Show, Eq)
instance IsString Line where
fromString s = Line Newline 0 (pack s)
data LineEnd
= Newline
| Inline
deriving (Eq, Show)
addIndent :: Int -> Line -> Line
addIndent n (Line e ind t) = Line e (ind + n) t
-- | Render lines to text
renderLines :: [Line] -> Text
renderLines = snd . L.foldl' nextLine (False, "")
where
nextLine :: (Bool, Text) -> Line -> (Bool, Text)
nextLine (newline, t) l = (nextNewline l, t <> currentLine newline l)
currentLine :: Bool -> Line -> Text
currentLine newline l
| newline = "\n" <> spaces l.indent <> l.text
| otherwise = l.text
nextNewline l = l.end == Newline
spaces n = T.replicate n " "