packages feed

miso-from-html-0.3.0.0: src/Miso/From/Html.hs

-----------------------------------------------------------------------------
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE ViewPatterns               #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-----------------------------------------------------------------------------
module Miso.From.Html where
-----------------------------------------------------------------------------
import           Data.Char
import           Control.Monad (guard)
import           Control.Monad.State
import           Control.Applicative
import           Data.List hiding (takeWhile)
import           Data.Map (Map)
import qualified Data.Map as M
import           Data.Maybe
import           Data.Text (Text)
import qualified Data.Text.Lazy as LT
import qualified Data.Text as T
import           Prelude hiding (takeWhile)
import           Text.HTML.Parser
import           Text.Pretty.Simple
import           Text.HTML.Tree
-----------------------------------------------------------------------------
type IsOpen = Bool
-----------------------------------------------------------------------------
data HTML
  = Node IsOpen HTMLTagName [ HTMLAttr ] [ HTML ]
  | TextNode Text
  deriving stock (Eq)
-----------------------------------------------------------------------------
newtype CSS = CSS (Map Text Text)
  deriving stock (Eq)
  deriving newtype (Monoid, Semigroup)
-----------------------------------------------------------------------------
instance Show CSS where
  show (CSS hmap) =
    mconcat
    [ "CSS.style [ "
    , intercalate "," (go <$> M.assocs hmap)
    , " ]"
    ]
    where
      go (k,v) = "\"" <>
        T.unpack (T.strip k) <> "\" =: " <> "\"" <>
          T.unpack (T.strip v) <> "\""
-----------------------------------------------------------------------------
data HTMLAttr = HTMLAttr Text (Maybe Text)
  deriving (Eq)
-----------------------------------------------------------------------------
instance Show HTML where
  show (TextNode x) = "\"" <> T.unpack x <> "\""
  show (Node isOpen t as cs) =
    mconcat $
    [ T.unpack t
    , "_ "
    , show as
    ] ++
    [ show cs
    | isOpen
    ]
-----------------------------------------------------------------------------
upper :: Text -> Text
upper (T.uncons -> Just (h,xs)) = T.cons (toUpper h) xs
upper x = x
-----------------------------------------------------------------------------
instance Show HTMLAttr where
  show (HTMLAttr "style" (Just v)) =
    mconcat
    [ T.unpack v
    ]
  show (HTMLAttr k (Just v))
    | "stroke-" `T.isPrefixOf` k
    , Just rest <- T.stripPrefix "stroke-" k =
      mconcat
      [ " stroke" <> T.unpack (upper rest) <> "_"
      , " \""
      , T.unpack v
      , "\" "
      ]
    | "data-" `T.isPrefixOf` k
    , Just rest <- T.stripPrefix "data-" k =
      mconcat
      [ " data_ \""
      , T.unpack rest
      , "\""
      , " \""
      , T.unpack v
      , "\" "
      ]
    | "aria-" `T.isPrefixOf` k
    , Just rest <- T.stripPrefix "aria-" k =
      mconcat
      [ " aria_ \""
      , T.unpack rest
      , "\""
      , " \""
      , T.unpack v
      , "\" "
      ]
    | T.any (=='-') k =
      mconcat
      [ " textProp \""
      , T.unpack k
      , "\""
      , " \""
      , T.unpack v
      , "\" "
      ]
    | otherwise =
      mconcat
      [ " "
      , T.unpack k
      , "_ "
      , "\""
      , T.unpack v
      , "\" "
      ]
  show (HTMLAttr x@(T.isPrefixOf "data-" -> True) Nothing) =
    case T.stripPrefix "data-" x of
      Just rest -> "data_ " <> "\"" <> T.unpack rest <> "\"" <> " \"\""
      Nothing -> T.unpack x
  show (HTMLAttr "checked" Nothing) =
    "checked_ True"
  show (HTMLAttr k Nothing) =
    mconcat
    [ "textProp \""
    , T.unpack k
    , "\" \"\""
    ]
-----------------------------------------------------------------------------
type HTMLTagName = Text
-----------------------------------------------------------------------------
html :: Parser HTML
html = withoutKids <|> withKids
  where
    withoutKids =
      textNode <|> tagSelfClose
    withKids = do
      (openName, attrs) <- tagOpen
      kids <- many html
      closeName <- tagClose
      guard (openName == closeName)
      pure (Node True openName attrs kids)
-----------------------------------------------------------------------------
tagOpen :: Parser (TagName, [HTMLAttr])
tagOpen = do
  TagOpen openName attrs <-
    satisfy $ \case
      TagOpen{} -> True
      _ -> False
  let htmlAttrs =
        [ processStyle (HTMLAttr attrName value)
        | Attr attrName attrValue <- attrs
        , let value
                | T.null attrValue = Nothing
                | otherwise = Just attrValue
        ]
  pure (openName, htmlAttrs)
-----------------------------------------------------------------------------
tagClose :: Parser TagName
tagClose = do
  TagClose closeName <-
    satisfy $ \case
      TagClose{} -> True
      _ -> False
  pure closeName
-----------------------------------------------------------------------------
tagSelfClose :: Parser HTML
tagSelfClose = do
  TagSelfClose name attrs <-
    satisfy $ \case
      TagSelfClose {} -> True
      _ -> False
  let htmlAttrs =
        [ processStyle (HTMLAttr attrName value)
        | Attr attrName attrValue <- attrs
        , let value
                | T.null attrValue = Nothing
                | otherwise = Just attrValue
        ]
  pure (Node False name htmlAttrs [])
-----------------------------------------------------------------------------
textNode :: Parser HTML
textNode = leaf <|> leafChar
  where
    leaf :: Parser HTML
    leaf = do
      ContentText txt <-
        satisfy $ \case
          ContentText {} -> True
          _ -> False
      pure (TextNode txt)

    leafChar :: Parser HTML
    leafChar = do
      ContentChar t <-
        satisfy $ \case
          ContentChar {} -> True
          _ -> False
      pure (TextNode (T.singleton t))
------------------------------------------------------------------------------
processStyle :: HTMLAttr -> HTMLAttr
processStyle (HTMLAttr "style" (Just cssText)) =
  HTMLAttr "style" $ Just (T.pack (show parsedCss))
    where
      parsedCss :: CSS
      parsedCss = CSS cssMap
        where
          cssMap
            = M.fromList
            [ (k,v)
            | [k,v] <- T.splitOn ":" <$> T.splitOn ";" cssText
            ]
processStyle attr = attr
------------------------------------------------------------------------------
isComment :: Token -> Bool
isComment Comment {} = True
isComment _ = False
-----------------------------------------------------------------------------
isDoctype :: Token -> Bool
isDoctype Doctype {} = True
isDoctype _ = False
-----------------------------------------------------------------------------
isEmptyTextNode :: Token -> Bool
isEmptyTextNode (ContentText txt)
  = T.null
  $ T.filter (`notElem` ['\n', '\t', ' '])
  $ txt
isEmptyTextNode _ = False
-----------------------------------------------------------------------------
getTokens :: Text -> [Token]
getTokens input = preprocess $
  let
    tokens = parseTokens input
  in
    [ case t of
        ContentText txt ->
          ContentText (T.strip txt)
        _ -> t
    | t <- tokens
    , not (isComment t)
      && not (isDoctype t)
      && not (isEmptyTextNode t)
    ]
-----------------------------------------------------------------------------
process :: Text -> Text
process input =
  case parse html (getTokens input) of
    Right r ->
      T.pack (show r)
    Left e ->
      T.pack (show e)
-----------------------------------------------------------------------------
preprocess :: [Token] -> [Token]
preprocess = fmap go
  where
    go (TagOpen name attrs)
      | name `elem` nonClosing = TagSelfClose name attrs
      | otherwise = TagOpen name attrs
    go x = x
-----------------------------------------------------------------------------
processPretty :: Text -> Text
processPretty input =
  case parse html (getTokens input) of
    Right r ->
      LT.toStrict (pShow r)
    Left e ->
      LT.toStrict (pShow e)
-----------------------------------------------------------------------------
data ParseError a
  = UnexpectedParse [Token]
  | Ambiguous [(a, [Token])]
  | NoParses Token
  | EmptyStream
  deriving (Show, Eq)
-----------------------------------------------------------------------------
parse :: Parser a -> [Token] -> Either (ParseError a) a
parse _ []          = Left EmptyStream
parse parser tokens =
  case runStateT parser tokens of
    []        -> Left (NoParses (last tokens))
    [(x, [])] -> Right x
    [(_, xs)] -> Left (UnexpectedParse xs)
    xs        -> Left (Ambiguous xs)
-----------------------------------------------------------------------------
type Parser a = StateT [Token] [] a
-----------------------------------------------------------------------------
satisfy :: (Token -> Bool) -> Parser Token
satisfy f = StateT $ \tokens ->
  case tokens of
    t : ts | f t -> [(t, ts)]
    _ -> []
-----------------------------------------------------------------------------