dom-selector-0.2.0.0: Text/XML/Selector.hs
{-# LANGUAGE OverloadedStrings, DoAndIfThenElse, TemplateHaskell, QuasiQuotes #-}
-- CSS selector for Text.XML (xml-conduit) library.
--
--
-- Examples
-- doc <- H.readFile "input.html"
-- let c = fromDocument c
-- c $| query
-- c $| query "meta[name='dc.Creator']" >=> attribute "content"
-- |This module has query functions for traversing DOM. 'queryT', a quasiquote version, is also available in "Text.XML.Selector.TH" module.
module Text.XML.Selector (
query,
query1,
searchTree,
showJQ,
byId,
byClass,
selectorMatch,
next,
maybeText,
headm,
queryMatchNode
)
where
-- import Import
-- import qualified Data.Tree as Tr
import Data.List
import qualified Data.Text as T
import Text.XML.Cursor
import Text.XML as X -- hiding (Name)
-- import Data.Either.Utils
import qualified Data.Map as M
import Data.Maybe
-- import Language.Haskell.TH (runQ)
import Text.XML.Selector.Parser
import Text.XML.Selector.Types
import Debug.Trace
-- | Show a parsed selector.
-- (parseJQ . showJQ) == id
showJQ :: [JQSelector] -> String
showJQ ss = foldl f "" ss
where
f str sel = str ++ g (relPrev sel) ++
maybe "" id (jqTagName sel) ++
maybe "" (("#"++)) (jqTagId sel) ++
concat (map (("."++)) (jqTagClass sel)) ++
if null (jqTagAttr sel) then "" else (concatMap h (jqTagAttr sel))
g Descendant = " "
g Child = " > "
g Next = " + "
g Sibling = " ~ "
h (TagAttr k Nothing _) = "["++k++"]"
h (TagAttr k (Just v) r) = "["++k ++ relToStr r ++ "\"" ++ v ++ "\"]"
-- h _ = error "Invalid TagAttr"
-- Some elementary search
-- | Axis for choosing elements by an id
byId :: String -> Axis
byId s = checkElement (elemHasId (Just s))
-- byIdT :: String -> Axis
byIdT s = [e| checkElement (elemHasId (Just s)) |]
-- | Axis for choosing elements by a class
byClass :: String -> Axis
byClass s = checkElement (elemHasClass [s])
-- Some traversing
-- |Gets the next sibling. Note that this is not a Axis.
next :: Cursor -> Maybe Cursor
next c = headm (c $| followingSibling)
-- Auxiliary functions
headm :: [a] -> Maybe a
headm [] = Nothing
headm (x:_) = Just x
take1' :: [a] -> [a]
take1' xs = if null xs then [] else take 1 xs
maybeText :: T.Text -> Maybe T.Text
maybeText "" = Nothing
maybeText t = Just t
--
-- Traversing by jQuery string
--
-- | Get 'Axis' from jQuery selector string.
query :: String -> Axis
query keystr = case parseJQ keystr of
[] -> error "query: Invalid selector"
sels -> searchTree sels
-- | Return Just (the first element of query results). If no element matches, it returns Nothing.
query1 :: String -> Cursor -> Maybe Cursor
query1 s n | null res = Nothing
| otherwise = Just (head res)
where res = query s n
{-
-- | Old version: search direction should be child -> parent to avoid duplicates for nested elements with same tag.
searchTreeOld :: [JQSelector] -> Axis
searchTreeOld [] c = [c]
searchTreeOld (x@(JQSelector Descendant _ _ _ _):xs) c = c $// checkElement (selectorMatch x) >=> searchTree xs
searchTreeOld (x@(JQSelector Child _ _ _ _):xs) c = c $/ checkElement (selectorMatch x) >=> searchTree xs
searchTreeOld (x@(JQSelector Next _ _ _ _):xs) c =
case c $| followingSibling >=> anyElement of
[] -> []
cs -> (head cs) $| checkElement (selectorMatch x) >=> searchTree xs
searchTreeOld ((JQSelector Sibling _ _ _ _):xs) c = c $| followingSibling >=> searchTree xs
-}
searchTree :: [JQSelector] -> Axis
searchTree xs = search (reverse xs)
where
search [] c = [c]
search (x@(JQSelector rel _ _ _ _):xs) c = c $// checkElement (selectorMatch x) >=> check (traceAncestors rel xs)
traceAncestors :: RelPrev -> [JQSelector] -> Axis
traceAncestors _ [] c = [c]
traceAncestors Child (x:xs) c
| isJust p = if matchCursor x (fromJust p) then traceAncestors (relPrev x) xs (fromJust p) else []
| otherwise = []
where
p :: Maybe Cursor
p = headm (parent c)
traceAncestors Descendant (x:xs) c
= case filter (matchCursor x) (ancestor c) of
[] -> []
as -> concatMap (traceAncestors (relPrev x) xs) as
traceAncestors Next (x:xs) c
| isJust p = if matchCursor x (fromJust p) then traceAncestors (relPrev x) xs (fromJust p) else []
| otherwise = []
where
p :: Maybe Cursor
p = headm (precedingSibling c)
traceAncestors Sibling (x:xs) c
= case filter (matchCursor x) (precedingSibling c) of
[] -> []
as -> concatMap (traceAncestors (relPrev x) xs) as
-- |Return if an element matches a selector
selectorMatch :: JQSelector -> Element -> Bool
selectorMatch (JQSelector _ name id klass attr) e
= elemIsTag name e && elemHasId id e && elemHasClass klass e && all (flip elemHasAttr e) attr
matchNode :: JQSelector -> Node -> Bool
matchNode sel (NodeElement elem) = selectorMatch sel elem
matchNode _ _ = False
matchCursor :: JQSelector -> Cursor -> Bool
matchCursor sel cursor = matchNode sel (node cursor)
-- |Return if a node matches a selector given by string
-- |Only first token is used (i.e. no hierarchy is enabled.)
queryMatchNode :: String -> Node -> Bool
queryMatchNode s (NodeElement e)
= case qq of
Just q -> selectorMatch q e
Nothing -> False
where qq = headm $ parseJQ s
queryMatchNode _ _ = False
-- return False when tag is specified in selector (1st arg) but not the one in the element (2nd arg)
elemIsTag :: Maybe String -> Element -> Bool
elemIsTag Nothing _ = True
elemIsTag (Just tag) (Element n _ _) = nameLocalName n == T.pack tag
elemHasId :: Maybe String -> Element -> Bool
elemHasId Nothing _ = True
elemHasId (Just id) (Element _ as _) = M.lookup "id" as == Just (T.pack id)
elemHasClass :: [String] -> Element -> Bool
elemHasClass [] _ = True
elemHasClass ks (Element _ as _) = all (`elem` kl) $ map T.pack ks
where
kl = maybe [] T.words (M.lookup "class" as)
-- This is different from the above three funcs: by default return False
elemHasAttr :: TagAttr -> Element -> Bool
elemHasAttr attr (Element _ as _) = relFunc (attrRel attr) (g as) (Just (fromMaybe "" (attrVal attr)))
where
relFunc :: AttrRel -> Maybe String -> Maybe String -> Bool
relFunc Equal (Just a) (Just b) = a == b
relFunc Exists a _ = isJust a -- '[checked]'
relFunc Contains (Just a) (Just b) = b `isInfixOf` a
relFunc Begin (Just a) (Just b) = b `isPrefixOf` a
relFunc End (Just a) (Just b) = b `isSuffixOf` a
relFunc NotEqual (Just a) (Just b) = a /= b
relFunc ContainsWord (Just a) (Just b) = all (`isInfixOf` a) (words b)
relFunc _ _ _ = False
-- relFunc r a b = error ("Attribute query invalid pattern: " ++ show attr ++ " : " ++ show r ++ show a ++ show b)
-- nameToStr :: M.Map Name T.Text -> M.Map String T.Text
-- nameToStr m = trace (show m) m
toName s = Name (T.pack s) Nothing Nothing
g :: M.Map Name T.Text -> Maybe String
g as = fmap T.unpack (M.lookup (toName $ attrName attr) as)
-- elemClass :: Element -> [T.Text]
-- elemClass (Element _ a _) = concat $ maybeToList $ fmap T.words $ (M.lookup "class" a)