{-# LANGUAGE OverloadedStrings, PatternSynonyms, ViewPatterns #-}
module Network.URI.Fetch.XML.Table(applySort, applySortDoc, splitTable) where
import Text.XML
import Data.Text as Txt
import qualified Data.Map as M
import Data.Maybe
import qualified Data.List as L
import Text.Read (readMaybe)
-- For smarter comparisons...
import Data.Time.Format (parseTimeM, defaultTimeLocale)
import Data.Time.Clock (UTCTime)
import Data.Char (isDigit)
applySortDoc :: String -> Document -> Document
applySortDoc anchor doc@Document {documentRoot = el} = doc {documentRoot = applySort anchor el}
applySort :: String -> Element -> Element
applySort ('#':'-':'a':'r':'g':'o':'-':'%':anchor) el
| (id', ord:col) <- L.break (`elem` ['<', '>']) anchor, Just col' <- readMaybe col =
applySort' id' (ord == '<') col' el
applySort _ el = el
applySort' :: String -> Bool -> Int -> Element -> Element
applySort' ('.':id') asc col el@Element { elementNodes = childs }
| (ix, subpath) <- L.break (== '.') id', Just ix' <- readMaybe ix =
el { elementNodes = setAt ix' (rewriteNode subpath) childs }
| otherwise = el
where
rewriteNode p (NodeElement child) = NodeElement $ applySort' p asc col child
rewriteNode _ x = x
applySort' "" asc col el = applySort'' asc col el
applySort' id' asc col el@Element { elementAttributes = attrs, elementNodes = childs }
| Just actual <- "id" `M.lookup` M.mapKeys nameLocalName attrs, pack id' == actual =
applySort'' asc col el
| otherwise = el { elementNodes = L.map searchNode childs }
where
searchNode (NodeElement child) = NodeElement $ applySort' id' asc col child
searchNode x = x
applySort'' asc col el
| Just sortable <- table2sorttable el = el {
elementNodes = annotateTHead header asc col ++
(L.concatMap (L.map NodeElement . markup) $ L.sortBy compareRows sortable)
++ footer
}
| otherwise = el
where
compareRows (TableRow a _) (TableRow b _)
| asc = compareAs (a !! col) (b !! col) (comparators !! col)
| otherwise = compareAs (b !! col) (a !! col) (comparators !! col)
(header, _, footer) = splitTable $ elementNodes el
comparators = tableHeadComparators header
data TableRow = TableRow { keys :: [Text], markup :: [Element] }
table2sorttable Element {
elementName = Name "table" _ _,
elementAttributes = attrs,
elementNodes = childs
} | "-argo-unsortable" `notElem` attrs, (_, body, _) <- splitTable childs =
trs2sorttable body
table2sorttable _ = Nothing
splitTable :: [Node] -> ([Node], [Element], [Node])
splitTable (NodeElement el@Element { elementName = Name "caption" _ _}:els) =
let (header, body, footer) = splitTable els in (NodeElement el:header, body, footer)
splitTable (NodeElement el@Element { elementName = Name "colgroup" _ _}:els) =
let (header, body, footer) = splitTable els in (NodeElement el:header, body, footer)
splitTable (NodeElement el@Element { elementName = Name "thead" _ _}:els) =
let (body, footer) = splitTableBody els in ([NodeElement el], body, footer)
splitTable (NodeElement el@Element { elementName = Name "tr" _ _, elementNodes = childs}:els)
| L.all (== "th") [nameLocalName $ elementName el | NodeElement el <- childs] =
let (body, footer) = splitTableBody els in ([NodeElement el], body, footer)
splitTable els@(NodeElement _:_) =
let (body, footer) = splitTableBody els in ([], body, footer)
splitTable (_:els) = splitTable els
splitTable [] = ([], [], [])
splitTableBody :: [Node] -> ([Element], [Node])
splitTableBody (NodeElement el@Element { elementName = Name "tbody" _ _, elementNodes = childs }:els) =
([el | NodeElement el@Element { elementName = Name "tr" _ _ } <- childs], els)
splitTableBody (NodeElement el@Element { elementName = Name "tr" _ _ }:els) =
let (body, footer) = splitTableBody els in (el:body, footer)
splitTableBody els@(NodeElement _:_) = ([], els)
splitTableBody (_:els) = splitTableBody els
splitTableBody [] = ([], [])
tableHeadComparators :: [Node] -> [Text]
tableHeadComparators = Prelude.map (fromMaybe "alphanumeric") . tableHeadComparators'
tableHeadComparators' :: [Node] -> [Maybe Text]
tableHeadComparators' (NodeElement el@Element { elementName = Name name _ _, elementNodes = childs}:els)
| name == "thead" = tableHeadComparators' childs `mergeRight` tableHeadComparators' els
| name `elem` ["colgroup", "tr"] = tableRowComparators childs `mergeRight` tableHeadComparators' els
| otherwise = tableHeadComparators' els
tableHeadComparators' [] = []
tableRowComparators :: [Node] -> [Maybe Text]
tableRowComparators (NodeElement el@(Element (Name "col" _ _) attrs _):els) =
let colspan = fromMaybe 1 (M.lookup "span" attrs >>= readMaybe . unpack)
in Prelude.replicate colspan (M.lookup "-argo-sortas" attrs) ++ tableRowComparators els
tableRowComparators (NodeElement el@(Element (Name n _ _) attrs _):els) | n `elem` ["td", "th"] =
let colspan = fromMaybe 1 (M.lookup "colspan" attrs >>= readMaybe . unpack)
in Prelude.replicate colspan (M.lookup "-argo-sortas" attrs) ++ tableRowComparators els
tableRowComparators (_:els) = tableRowComparators els
tableRowComparators [] = []
mergeRight :: [Maybe a] -> [Maybe a] -> [Maybe a]
mergeRight (_:as) (Just b:bs) = Just b : mergeRight as bs
mergeRight (a:as) (_:bs) = a : mergeRight as bs
mergeRight [] bs = bs
mergeRight as [] = as
annotateTHead (NodeElement el@Element { elementName = Name "thead" _ _, elementNodes = childs }:nodes) a c =
NodeElement el { elementNodes = annotateTHead childs a c } : nodes
annotateTHead (NodeElement el@Element { elementName = Name "tr" _ _, elementNodes = childs }:nodes) a c =
NodeElement el { elementNodes = annotateTR childs a c 0 } : nodes
annotateTHead (child:childs) a c = child:annotateTHead childs a c
annotateTHead [] _ _ = []
annotateTR (NodeElement el@Element { elementName = Name n _ _, elementAttributes = attrs }:nodes) asc col count
| n `elem` ["th", "td"], count >= col =
NodeElement el { elementAttributes = M.insert "aria-sort" asc' attrs }:nodes
| n `elem` ["th", "td"] = NodeElement el:annotateTR nodes asc col (count + colspan)
where
colspan = fromMaybe 1 (readMaybe =<< unpack <$> M.lookup "colspan" attrs')
attrs' = M.mapKeys nameLocalName attrs
asc' | asc = "ascending"
| otherwise = "descending"
annotateTR (node:nodes) a c n = node:annotateTR nodes a c n
annotateTR [] _ _ _ = []
trs2sorttable els@(el@Element { elementName = Name "tr" _ _, elementNodes = childs }:_)
| Just keys' <- tds2keys [el | NodeElement el <- childs],
Just (group, rest) <- groupTrs els 1,
Just rest' <- trs2sorttable rest = Just (TableRow keys' group : rest')
trs2sorttable [] = Just []
trs2sorttable _ = Nothing
tds2keys :: [Element] -> Maybe [Text]
tds2keys (el@Element {elementName = Name n _ _, elementAttributes = attrs, elementNodes = childs }:els)
| n `elem` ["td", "th"], Just key <- "-argo-sortkey" `M.lookup` attrs, Just rest <- tds2keys els =
Just (Prelude.replicate colspan key ++ rest)
| n `elem` ["td", "th"], Just rest <- tds2keys els =
Just (Prelude.replicate colspan (nodesText childs) ++ rest)
where
colspan | Just n <- "colspan" `M.lookup` M.mapKeys nameLocalName attrs,
Just m <- readMaybe $ unpack n = m
| otherwise = 1
tds2keys [] = Just []
tds2keys _ = Nothing
groupTrs (el@Element {elementName = Name "tr" _ _}:els) n
| rowRowspan n el <= 1 = Just (el:[], els)
| Just (tail, rest) <- groupTrs els $ pred n = Just (el:tail, rest)
groupTrs (_:els) n = groupTrs els n
groupTrs _ _ = Nothing
rowRowspan n Element {elementName = Name "tr" _ _, elementNodes = childs } =
Prelude.maximum (n : [n |
NodeElement (Element (Name name _ _) attrs _) <- childs,
name `elem` ["td", "th"],
rowspan <- maybeToList ("rowspan" `M.lookup` M.mapKeys nameLocalName attrs),
n <- maybeToList $ readMaybe $ unpack rowspan])
--- Utils
(+++) = append
nodesText :: [Node] -> Text
nodesText (NodeElement (Element _ attrs children):nodes) = nodesText children +++ nodesText nodes
nodesText (NodeContent text:nodes) = text +++ nodesText nodes
nodesText (_:nodes) = nodesText nodes
nodesText [] = ""
setAt :: Int -> (a -> a) -> [a] -> [a]
setAt i a ls
| i < 0 = ls
| otherwise = go i ls
where
go 0 (x:xs) = a x : xs
go n (x:xs) = x : go (n-1) xs
go _ [] = []
pattern (:.) :: Char -> Txt.Text -> Txt.Text
pattern x :. xs <- (Txt.uncons -> Just (x, xs))
infixr 5 :.
compareAs :: Text -> Text -> Text -> Ordering
--- Hueristic that readily handles both numbers & text
compareAs (a:.as) (b:.bs) "alphanumeric"
| isDigit a && isDigit b =
let (a', as') = Txt.break (not . isDigit) as
(b', bs') = Txt.break (not . isDigit) bs
in if Txt.length a' == Txt.length b' && a == b
then compareAs as bs "alphanumeric"
else if Txt.length a' == Txt.length b' then a `compare` b
else Txt.length a' `compare` Txt.length b'
| a == b = compareAs as bs "alphanumeric"
| otherwise = a `compare` b
compareAs as bs "text" = as `compare` bs
compareAs as bs "number" = readInt as `compare` readInt bs
where
readInt :: Text -> Maybe Float
readInt = readMaybe . Prelude.filter (`elem` '-':'.':['0'..'9']) . unpack
compareAs as bs fmt = readTime as `compare` readTime bs
where
readTime :: Text -> Maybe UTCTime
readTime = parseTimeM True defaultTimeLocale (unpack fmt) . unpack