{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module Zenacy.HTML.Internal.DOM
( DOM(..)
, DOMNode(..)
, DOMAttr(..)
, DOMType(..)
, DOMQuirks(..)
, DOMPos(..)
, DOMID
, DOMMap
, domAttrMake
, domDefaultDocument
, domDefaultDoctype
, domDefaultFragment
, domDefaultElement
, domDefaultTemplate
, domDefaultText
, domDefaultComment
, domDefaultType
, domMakeTypeHTML
, domMakeTypeMathML
, domMakeTypeSVG
, domPos
, domNull
, domRoot
, domRootPos
, domDocument
, domQuirksSet
, domQuirksGet
, domNewID
, domGetNode
, domPutNode
, domInsert
, domInsertNew
, domAppend
, domAppendNew
, domElementHasAttr
, domElementFindAttr
, domElementAttrValue
, domAttrMerge
, domMatch
, domLastChild
, domMapID
, domFindParent
, domSetParent
, domMapChild
, domRemoveChild
, domRemoveChildren
, domMove
, domMoveChildren
, domChildren
, domHasChild
, domNodeID
, domNodeParent
, domNodeIsHTML
, domNodeIsSVG
, domNodeIsMathML
, domNodeIsDocument
, domNodeIsFragment
, domNodeIsElement
, domNodeIsTemplate
, domNodeIsHtmlElement
, domNodeIsText
, domNodeElementName
, domNodeElementNamespace
, domNodeType
, domTypesHTML
, domTypesMathML
, domTypesSVG
, domRender
) where
import Zenacy.HTML.Internal.BS
import Zenacy.HTML.Internal.Core
import Zenacy.HTML.Internal.Types
import Data.Foldable
( toList
)
import Data.List
( find
)
import Data.Maybe
( fromJust
, listToMaybe
, isJust
, mapMaybe
)
import Data.Monoid
( (<>)
)
import Data.Word
( Word8
)
import Data.Default
( Default(..)
)
import Data.IntMap
( IntMap
)
import qualified Data.IntMap as IntMap
( singleton
, lookup
, insert
, keys
)
import Data.Sequence
( Seq(..)
, ViewL(..)
, ViewR(..)
, (<|)
, (|>)
, (><)
)
import qualified Data.Sequence as Seq
( breakl
, empty
, filter
, viewl
, viewr
)
data DOM = DOM
{ domNodes :: !DOMMap
, domNextID :: !DOMID
} deriving (Eq, Ord, Show)
type DOMID = Int
type DOMMap = IntMap DOMNode
data DOMNode
= DOMDocument
{ domDocumentID :: DOMID
, domDocumentParent :: DOMID
, domDocumentName :: BS
, domDocumentChildren :: Seq DOMID
, domDocumentQuirksMode :: DOMQuirks
}
| DOMDoctype
{ domDoctypeID :: DOMID
, domDoctypeParent :: DOMID
, domDoctypeName :: BS
, domDoctypePublicID :: Maybe BS
, domDoctypeSystemID :: Maybe BS
}
| DOMFragment
{ domFragmentID :: DOMID
, domFragmentParent :: DOMID
, domFragmentName :: BS
, domFragmentChildren :: Seq DOMID
}
| DOMElement
{ domElementID :: DOMID
, domElementParent :: DOMID
, domElementName :: BS
, domElementNamespace :: HTMLNamespace
, domElementAttributes :: Seq DOMAttr
, domElementChildren :: Seq DOMID
}
| DOMTemplate
{ domTemplateID :: DOMID
, domTemplateParent :: DOMID
, domTemplateNamespace :: HTMLNamespace
, domTemplateAttributes :: Seq DOMAttr
, domTemplateContents :: DOMID
}
| DOMText
{ domTextID :: DOMID
, domTextParent :: DOMID
, domTextData :: BS
}
| DOMComment
{ domCommentID :: DOMID
, domCommentParent :: DOMID
, domCommentData :: BS
}
deriving (Eq, Ord, Show)
data DOMAttr = DOMAttr
{ domAttrName :: BS
, domAttrVal :: BS
, domAttrNamespace :: HTMLAttrNamespace
} deriving (Eq, Ord, Show)
data DOMType = DOMType
{ domTypeName :: BS
, domTypeNamespace :: HTMLNamespace
} deriving (Eq, Ord, Show)
data DOMQuirks
= DOMQuirksNone
| DOMQuirksMode
| DOMQuirksLimited
deriving (Eq, Ord, Show)
data DOMPos = DOMPos
{ domPosParent :: DOMID
, domPosChild :: Maybe DOMID
} deriving (Eq, Ord, Show)
instance Default DOM where
def = DOM
{ domNodes = IntMap.singleton domRoot domDefaultDocument
, domNextID = domRoot + 1
}
instance Default DOMAttr where
def = DOMAttr
{ domAttrName = bsEmpty
, domAttrVal = bsEmpty
, domAttrNamespace = def
}
domAttrMake :: BS -> BS -> DOMAttr
domAttrMake n v = DOMAttr n v def
domDefaultDocument :: DOMNode
domDefaultDocument =
DOMDocument
{ domDocumentID = domNull
, domDocumentName = bsEmpty
, domDocumentChildren = Seq.empty
, domDocumentQuirksMode = DOMQuirksNone
, domDocumentParent = domNull
}
domDefaultDoctype :: DOMNode
domDefaultDoctype =
DOMDoctype
{ domDoctypeID = domNull
, domDoctypeName = bsEmpty
, domDoctypePublicID = Nothing
, domDoctypeSystemID = Nothing
, domDoctypeParent = domNull
}
domDefaultFragment :: DOMNode
domDefaultFragment =
DOMFragment
{ domFragmentID = domNull
, domFragmentName = bsEmpty
, domFragmentChildren = Seq.empty
, domFragmentParent = domNull
}
domDefaultElement :: DOMNode
domDefaultElement =
DOMElement
{ domElementID = domNull
, domElementName = bsEmpty
, domElementNamespace = HTMLNamespaceHTML
, domElementAttributes = Seq.empty
, domElementChildren = Seq.empty
, domElementParent = domNull
}
domDefaultTemplate :: DOMNode
domDefaultTemplate =
DOMTemplate
{ domTemplateID = domNull
, domTemplateNamespace = HTMLNamespaceHTML
, domTemplateAttributes = Seq.empty
, domTemplateContents = domNull
, domTemplateParent = domNull
}
domDefaultText :: DOMNode
domDefaultText =
DOMText
{ domTextID = domNull
, domTextData = bsEmpty
, domTextParent = domNull
}
domDefaultComment :: DOMNode
domDefaultComment =
DOMComment
{ domCommentID = domNull
, domCommentData = bsEmpty
, domCommentParent = domNull
}
domDefaultType :: DOMType
domDefaultType = domMakeTypeHTML bsEmpty
domMakeTypeHTML :: BS -> DOMType
domMakeTypeHTML = flip DOMType HTMLNamespaceHTML
domMakeTypeMathML :: BS -> DOMType
domMakeTypeMathML = flip DOMType HTMLNamespaceMathML
domMakeTypeSVG :: BS -> DOMType
domMakeTypeSVG = flip DOMType HTMLNamespaceSVG
domPos :: DOMID -> DOMPos
domPos x = DOMPos x Nothing
domNull :: DOMID
domNull = 0
domRoot :: DOMID
domRoot = 1
domRootPos :: DOMPos
domRootPos = domPos domRoot
domDocument :: DOM -> DOMNode
domDocument d = fromJust $ IntMap.lookup domRoot $ domNodes d
domQuirksSet :: DOMQuirks -> DOM -> DOM
domQuirksSet x d = domPutNode domRoot y' d
where
y = domDocument d
y' = y { domDocumentQuirksMode = x }
domQuirksGet :: DOM -> DOMQuirks
domQuirksGet = domDocumentQuirksMode . domDocument
domNewID :: DOM -> DOMNode -> (DOM, DOMID)
domNewID d n = (d', i)
where
i = domNextID d
n' = domSetID n i
d' = d { domNodes = IntMap.insert i n' $ domNodes d
, domNextID = i + 1
}
domSetID :: DOMNode -> DOMID -> DOMNode
domSetID x y =
case x of
DOMDocument{} -> x { domDocumentID = y }
DOMDoctype{} -> x { domDoctypeID = y }
DOMFragment{} -> x { domFragmentID = y }
DOMElement{} -> x { domElementID = y }
DOMTemplate{} -> x { domTemplateID = y }
DOMText{} -> x { domTextID = y }
DOMComment{} -> x { domCommentID = y }
domGetNode :: DOM -> DOMID -> Maybe DOMNode
domGetNode d x = IntMap.lookup x $ domNodes d
domPutNode :: DOMID -> DOMNode -> DOM -> DOM
domPutNode x n d = d { domNodes = IntMap.insert x n $ domNodes d }
domInsert :: DOMPos -> DOMID -> DOM -> DOM
domInsert p@(DOMPos r c) x d =
case domGetNode d r of
Just n@(DOMDocument { domDocumentChildren = a }) ->
f $ n { domDocumentChildren = g a }
Just n@(DOMElement { domElementChildren = a }) ->
f $ n { domElementChildren = g a }
Just n@(DOMFragment { domFragmentChildren = a }) ->
f $ n { domFragmentChildren = g a }
Just n@(DOMTemplate { domTemplateContents = a }) ->
domInsert (DOMPos a c) x d
_otherwise -> d
where
f a = domSetParent x r (domPutNode r a d)
g = domInsertChild p x
domInsertNew :: DOMPos -> DOMNode -> DOM -> (DOM, DOMID)
domInsertNew p x d =
(domInsert p i d', i)
where
(d', i) = domNewID d x
domInsertChild :: DOMPos -> DOMID -> Seq DOMID -> Seq DOMID
domInsertChild (DOMPos _ Nothing) x = (|> x)
domInsertChild (DOMPos _ (Just a)) x = seqInsertBefore (==a) x
domAppend :: DOMID -> DOMID -> DOM -> DOM
domAppend x y d =
case domGetNode d x of
Just (DOMDocument i p n c q) ->
f $ DOMDocument i p n (c |> y) q
Just (DOMElement i p n s a c) ->
f $ DOMElement i p n s a (c |> y)
Just (DOMFragment i p n c) ->
f $ DOMFragment i p n (c |> y)
Just (DOMTemplate _ _ _ _ c) ->
domAppend c y d
_otherwise -> d
where
f a = domSetParent y x (domPutNode x a d)
domAppendNew :: DOMID -> DOMNode -> DOM -> DOM
domAppendNew x y d = domAppend x i d'
where (d', i) = domNewID d y
domElementFindAttr :: DOMNode -> BS -> Maybe DOMAttr
domElementFindAttr node name = case node of
DOMElement{..} -> f domElementAttributes
DOMTemplate{..} -> f domTemplateAttributes
_otherwise -> Nothing
where
f = seqFind (\DOMAttr{..} -> domAttrName == name)
seqLast :: Seq a -> Maybe a
seqLast (Seq.viewr -> EmptyR) = Nothing
seqLast (Seq.viewr -> _ :> a) = Just a
seqLast _ = Nothing
seqFind :: (a -> Bool) -> Seq a -> Maybe a
seqFind f x = go x
where
go (Seq.viewl -> EmptyL) = Nothing
go (Seq.viewl -> a :< b) = if f a then Just a else go b
go _ = Nothing
seqInsertBefore :: (a -> Bool) -> a -> Seq a -> Seq a
seqInsertBefore f x y =
(a |> x) <> b
where
(a, b) = Seq.breakl f y
domElementAttrValue :: DOMNode -> BS -> Maybe BS
domElementAttrValue x n = domAttrVal <$> domElementFindAttr x n
domElementHasAttr :: DOMNode -> BS -> Bool
domElementHasAttr x = isJust . domElementFindAttr x
domAttrMerge :: DOMID -> Seq DOMAttr -> DOM -> DOM
domAttrMerge x y d =
case domGetNode d x of
Just n@(DOMElement { domElementAttributes = a }) ->
domPutNode x (n { domElementAttributes = a <> f n y }) d
Just n@(DOMTemplate { domTemplateAttributes = a }) ->
domPutNode x (n { domTemplateAttributes = a <> f n y }) d
_otherwise -> d
where
f n = Seq.filter (not . domElementHasAttr n . domAttrName)
domMatch :: DOM -> DOMID -> DOMID -> Bool
domMatch d i j =
case (domGetNode d i, domGetNode d j) of
(Just (DOMElement _ _ n1 s1 a1 _), Just (DOMElement _ _ n2 s2 a2 _)) ->
n1 == n2 && s1 == s2 && a1 == a1
(Just (DOMTemplate _ _ s1 a1 _ ), Just (DOMTemplate _ _ s2 a2 _)) ->
s1 == s2 && a1 == a1
_otherwise ->
False
domLastChild :: DOM -> DOMID -> Maybe DOMID
domLastChild d x =
domGetNode d x >>= \case
DOMDocument{..} -> seqLast domDocumentChildren
DOMFragment{..} -> seqLast domFragmentChildren
DOMElement{..} -> seqLast domElementChildren
DOMTemplate{..} -> domLastChild d domTemplateContents
_otherwise -> Nothing
domMapID :: DOM -> [DOMID] -> [DOMNode]
domMapID d = mapMaybe $ domGetNode d
domFindParent :: DOM -> DOMID -> Maybe DOMID
domFindParent d x = find (domHasChild d x) $ IntMap.keys $ domNodes d
domSetParent :: DOMID -> DOMID -> DOM -> DOM
domSetParent x y d =
case domGetNode d x of
Just a -> case a of
DOMDocument{} -> f a { domDocumentParent = y }
DOMDoctype{} -> f a { domDoctypeParent = y }
DOMFragment{} -> f a { domFragmentParent = y }
DOMElement{} -> f a { domElementParent = y }
DOMTemplate{} -> f a { domTemplateParent = y }
DOMText{} -> f a { domTextParent = y }
DOMComment{} -> f a { domCommentParent = y }
Nothing -> d
where
f z = domPutNode x z d
domMapChild :: DOMID -> (Seq DOMID -> Seq DOMID)-> DOM -> DOM
domMapChild x f d =
case domGetNode d x of
Just a -> case a of
DOMDocument { domDocumentChildren = c } ->
domPutNode x a { domDocumentChildren = f c } d
DOMFragment { domFragmentChildren = c } ->
domPutNode x a { domFragmentChildren = f c } d
DOMElement { domElementChildren = c } ->
domPutNode x a { domElementChildren = f c } d
DOMTemplate { domTemplateContents = c } ->
domMapChild c f d
_otherwise -> d
Nothing -> d
domRemoveChild :: DOMID -> DOMID -> DOM -> DOM
domRemoveChild parent child = domMapChild parent $ Seq.filter (/=child)
domRemoveChildren :: DOMID -> DOM -> DOM
domRemoveChildren x = domMapChild x $ const Seq.empty
domMove :: DOMID -> DOMID -> DOM -> DOM
domMove x newParent d =
case domGetNode d x of
Just a ->
let d' = domRemoveChild (domNodeParent a) x d
in domAppend newParent x d'
Nothing -> d
domMoveChildren :: DOMID -> DOMID -> DOM -> DOM
domMoveChildren x y d =
foldl (\d' c -> domAppend y c d') (domRemoveChildren x d) $ domChildren d x
domChildren :: DOM -> DOMID -> Seq DOMID
domChildren d x =
case domGetNode d x of
Just (DOMDocument{..}) -> domDocumentChildren
Just (DOMFragment{..}) -> domFragmentChildren
Just (DOMElement{..}) -> domElementChildren
Just (DOMTemplate{..}) -> domChildren d domTemplateContents
_otherwise -> Seq.empty
domHasChild :: DOM -> DOMID -> DOMID -> Bool
domHasChild d x z = z `elem` domChildren d x
domNodeID :: DOMNode -> DOMID
domNodeID = \case
DOMDocument{..} -> domDocumentID
DOMDoctype{..} -> domDoctypeID
DOMFragment{..} -> domFragmentID
DOMElement{..} -> domElementID
DOMTemplate{..} -> domTemplateID
DOMText{..} -> domTextID
DOMComment{..} -> domCommentID
domNodeParent :: DOMNode -> DOMID
domNodeParent = \case
DOMDocument{..} -> domDocumentParent
DOMDoctype{..} -> domDoctypeParent
DOMFragment{..} -> domFragmentParent
DOMElement{..} -> domElementParent
DOMTemplate{..} -> domTemplateParent
DOMText{..} -> domTextParent
DOMComment{..} -> domCommentParent
domNodeIsHTML :: DOMNode -> Bool
domNodeIsHTML = \case
DOMElement{..} -> domElementNamespace == HTMLNamespaceHTML
DOMTemplate{..} -> domTemplateNamespace == HTMLNamespaceHTML
_otherwise -> False
domNodeIsSVG :: DOMNode -> Bool
domNodeIsSVG = \case
DOMElement{..} -> domElementNamespace == HTMLNamespaceSVG
DOMTemplate{..} -> domTemplateNamespace == HTMLNamespaceSVG
_otherwise -> False
domNodeIsMathML :: DOMNode -> Bool
domNodeIsMathML = \case
DOMElement{..} -> domElementNamespace == HTMLNamespaceMathML
DOMTemplate{..} -> domTemplateNamespace == HTMLNamespaceMathML
_otherwise -> False
domNodeIsDocument :: DOMNode -> Bool
domNodeIsDocument DOMDocument{} = True
domNodeIsDocument _ = False
domNodeIsFragment :: DOMNode -> Bool
domNodeIsFragment DOMFragment{} = True
domNodeIsFragment _ = False
domNodeIsElement :: DOMNode -> Bool
domNodeIsElement DOMElement{} = True
domNodeIsElement _ = False
domNodeIsTemplate :: DOMNode -> Bool
domNodeIsTemplate DOMTemplate{} = True
domNodeIsTemplate _ = False
domNodeIsHtmlElement :: DOMNode -> Bool
domNodeIsHtmlElement x = domNodeIsElement x && domNodeIsHTML x
domNodeIsText :: DOMNode -> Bool
domNodeIsText DOMText{} = True
domNodeIsText _ = False
domNodeElementName :: DOMNode -> BS
domNodeElementName DOMElement{..} = domElementName
domNodeElementName DOMTemplate{} = "template"
domNodeElementName _ = ""
domNodeElementNamespace :: DOMNode -> HTMLNamespace
domNodeElementNamespace DOMElement{..} = domElementNamespace
domNodeElementNamespace DOMTemplate{..} = domTemplateNamespace
domNodeElementNamespace _ = HTMLNamespaceHTML
domNodeType :: DOMNode -> DOMType
domNodeType x = DOMType (domNodeElementName x) (domNodeElementNamespace x)
domTypesHTML :: [BS] -> [DOMType]
domTypesHTML = map domMakeTypeHTML
domTypesMathML :: [BS] -> [DOMType]
domTypesMathML = map domMakeTypeMathML
domTypesSVG :: [BS] -> [DOMType]
domTypesSVG = map domMakeTypeSVG
domRender :: DOM -> BS
domRender d = domRenderIndent d 0 domRoot
domRenderIndent :: DOM -> Int -> DOMID -> BS
domRenderIndent d x y =
case fromJust (domGetNode d y) of
DOMDocument{..} ->
bsConcat $ map (domRenderIndent d x) $ toList domDocumentChildren
DOMDoctype{} ->
bsEmpty
DOMFragment{..} ->
bsConcat $ map (domRenderIndent d x) $ toList domFragmentChildren
DOMElement{..} ->
bsConcat
[ indent
, domElementName
, "\n"
, bsConcat $ map (domRenderIndent d $ x + 1) $ toList domElementChildren
]
DOMTemplate{..} ->
bsConcat
[ indent
, "template"
, "\n"
, domRenderIndent d (x + 1) domTemplateContents
]
DOMText{..} ->
bsConcat
[ indent
, domTextData
, "\n"
]
DOMComment{} ->
bsEmpty
where
indent = bsPack $ take x $ repeat 0x20