Text/XML/Expat/Internal/NodeClass.hs
{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts, TypeFamilies,
ScopedTypeVariables, Rank2Types #-}
-- | Type classes to allow for XML handling functions to be generalized to
-- work with different node types, including the ones defined in /Tree/ and
-- /Annotated/.
module Text.XML.Expat.Internal.NodeClass where
import Control.Monad (mzero, liftM)
import Data.Functor.Identity
import Data.List.Class (List(..), ListItem(..), cons, fromList, mapL, toList)
import Data.Monoid (Monoid)
import Text.XML.Expat.SAX (GenericXMLString)
-- | Type shortcut for attributes
type Attributes tag text = [(tag, text)]
-- | Type shortcut for attributes with unqualified names where tag and
-- text are the same string type.
type UAttributes text = Attributes text text
-- | Extract all text content from inside a tag into a single string, including
-- any text contained in children. This /excludes/ the contents of /comments/ or
-- /processing instructions/. To get the text for these node types, use 'getText'.
textContent :: (NodeClass n [], Monoid text) => n [] tag text -> text
textContent node = runIdentity $ textContentM node
-- | A type function to give the type of a list of nodes, using the appropriate
-- list type for the specified node type, e.g. @ListOf (UNode Text)@
type family ListOf n
class (Functor c, List c) => NodeClass (n :: (* -> *) -> * -> * -> *) c where
-- | Is the given node an element?
isElement :: n c tag text -> Bool
-- | Is the given node text?
isText :: n c tag text -> Bool
-- | Is the given node CData?
isCData :: n c tag text -> Bool
-- | Is the given node a processing instruction?
isProcessingInstruction :: n c tag text -> Bool
-- | Is the given node a comment?
isComment :: n c tag text -> Bool
-- | Extract all text content from inside a tag into a single string, including
-- any text contained in children. This /excludes/ the contents of /comments/ or
-- /processing instructions/. To get the text for these node types, use 'getText'.
textContentM :: Monoid text => n c tag text -> ItemM c text
-- | Is the given node a tag with the given name?
isNamed :: Eq tag => tag -> n c tag text -> Bool
-- | Get the name of this node if it's an element, return empty string otherwise.
getName :: Monoid tag => n c tag text -> tag
-- | Is the given node a Processing Instruction with the given target?
hasTarget :: Eq text => text -> n c tag text -> Bool
-- | Get the target of this node if it's a Processing Instruction, return empty string otherwise.
getTarget :: Monoid text => n c tag text -> text
-- | Get the attributes of a node if it's an element, return empty list otherwise.
getAttributes :: n c tag text -> [(tag,text)]
-- | Get children of a node if it's an element, return empty list otherwise.
getChildren :: n c tag text -> c (n c tag text)
-- | Get this node's text if it's a text node, comment, or processing instruction,
-- return empty text otherwise.
getText :: Monoid text => n c tag text -> text
-- | Modify name if it's an element, no-op otherwise.
modifyName :: (tag -> tag)
-> n c tag text
-> n c tag text
-- | Modify attributes if it's an element, no-op otherwise.
modifyAttributes :: ([(tag, text)] -> [(tag, text)])
-> n c tag text
-> n c tag text
-- | Modify children (non-recursively) if it's an element, no-op otherwise.
modifyChildren :: (c (n c tag text) -> c (n c tag text))
-> n c tag text
-> n c tag text
-- | Map an element non-recursively, allowing the tag type to be changed.
modifyElement :: ((tag, [(tag, text)], c (n c tag text))
-> (tag', [(tag', text)], c (n c tag' text)))
-> n c tag text
-> n c tag' text
-- | Map all tags (both tag names and attribute names) recursively.
mapAllTags :: (tag -> tag')
-> n c tag text
-> n c tag' text
-- | Change a node recursively from one container type to another, with a
-- specified function to convert the container type.
mapNodeContainer :: List c' =>
(forall a . c a -> ItemM c (c' a))
-> n c tag text
-> ItemM c (n c' tag text)
-- | Generic text node constructor.
mkText :: text -> n c tag text
-- | Change a list of nodes recursively from one container type to another, with
-- a specified function to convert the container type.
mapNodeListContainer :: (NodeClass n c, List c') =>
(forall a . c a -> ItemM c (c' a))
-> c (n c tag text)
-> ItemM c (c' (n c' tag text))
mapNodeListContainer f = f . mapL (mapNodeContainer f)
-- | Change a node recursively from one container type to another. This
-- extracts the entire tree contents to standard lists and re-constructs them
-- with the new container type. For monadic list types used in
-- @hexpat-iteratee@ this operation forces evaluation.
fromNodeContainer :: (NodeClass n c, List c') =>
n c tag text
-> ItemM c (n c' tag text)
fromNodeContainer = mapNodeContainer (\l -> fromList `liftM` toList l)
-- | Change a list of nodes recursively from one container type to another. This
-- extracts the entire tree contents to standard lists and re-constructs them
-- with the new container type. For monadic list types used in
-- @hexpat-iteratee@ this operation forces evaluation.
fromNodeListContainer :: (NodeClass n c, List c') =>
c (n c tag text)
-> ItemM c (c' (n c' tag text))
fromNodeListContainer = mapNodeListContainer (\l -> fromList `liftM` toList l)
-- | A class of node types where an Element can be constructed given a tag,
-- attributes and children.
class NodeClass n c => MkElementClass n c where
-- | Generic element constructor.
mkElement :: tag -> Attributes tag text -> c (n c tag text) -> n c tag text
-- | Get the value of the attribute having the specified name.
getAttribute :: (NodeClass n c, GenericXMLString tag) => n c tag text -> tag -> Maybe text
getAttribute n t = lookup t $ getAttributes n
-- | Set the value of the attribute with the specified name to the value, overwriting
-- the first existing attribute with that name if present.
setAttribute :: (Eq tag, NodeClass n c, GenericXMLString tag) => tag -> text -> n c tag text -> n c tag text
setAttribute t newValue = modifyAttributes set
where
set [] = [(t, newValue)]
set ((name, _):atts) | name == t = (name, newValue):atts
set (att:atts) = att:set atts
-- | Delete the first attribute matching the specified name.
deleteAttribute :: (Eq tag, NodeClass n c, GenericXMLString tag) => tag -> n c tag text -> n c tag text
deleteAttribute t = modifyAttributes del
where
del [] = []
del ((name, _):atts) | name == t = atts
del (att:atts) = att:del atts
-- | setAttribute if /Just/, deleteAttribute if /Nothing/.
alterAttribute :: (Eq tag, NodeClass n c, GenericXMLString tag) => tag -> Maybe text -> n c tag text -> n c tag text
alterAttribute t (Just newValue) = setAttribute t newValue
alterAttribute t Nothing = deleteAttribute t
-- | Generically convert an element of one node type to another. Useful for
-- adding or removing annotations.
fromElement :: (NodeClass n c, MkElementClass n' c, Monoid tag, Monoid text) =>
n c tag text
-> n' c tag text
fromElement = fromElement_ mkElement
-- | Generically convert an element of one node type to another, using
-- the specified element constructor. Useful for adding or removing annotations.
fromElement_ :: (NodeClass n c, NodeClass n' c, Monoid tag, Monoid text) =>
(tag -> Attributes tag text -> c (n' c tag text) -> n' c tag text) -- ^ Element constructor
-> n c tag text
-> n' c tag text
fromElement_ mkElement elt | isElement elt =
mkElement (getName elt) (getAttributes elt) (fromNodes_ mkElement $ getChildren elt)
fromElement_ _ _ = error "fromElement requires an Element"
-- | Generically convert a list of nodes from one node type to another. Useful for
-- adding or removing annotations.
fromNodes :: (NodeClass n c, MkElementClass n' c, Monoid tag, Monoid text) =>
c (n c tag text)
-> c (n' c tag text)
fromNodes = fromNodes_ mkElement
-- | Generically convert a list of nodes from one node type to another, using
-- the specified element constructor. Useful for adding or removing annotations.
fromNodes_ :: (NodeClass n c, NodeClass n' c, Monoid tag, Monoid text) =>
(tag -> Attributes tag text -> c (n' c tag text) -> n' c tag text) -- ^ Element constructor
-> c (n c tag text)
-> c (n' c tag text)
fromNodes_ mkElement l = joinL $ do
li <- runList l
return $ case li of
Nil -> mzero
Cons elt l' | isElement elt -> fromElement_ mkElement elt `cons` fromNodes_ mkElement l'
Cons txt l' | isText txt -> mkText (getText txt) `cons` fromNodes_ mkElement l'
-- Future node types may include other kinds of nodes, which we discard here.
Cons _ l' -> fromNodes_ mkElement l'