{-# LANGUAGE OverloadedStrings, TemplateHaskell, FlexibleContexts #-} module Hearth (renderPage, handleForm, markAsRead, utf8, Link) where import Text.Ginger.Parse (parseGingerFile, SourcePos) import Text.Ginger.Run (runGinger, makeContextHtml, Run) import Text.Ginger.Html (htmlSource, Html) import Text.Ginger.GVal as V (toGVal, orderedDict, (~>), GVal(..), fromFunction) import Control.Monad.Writer.Lazy (Writer) import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as LBS import qualified Data.Text as Txt import qualified Data.Text.Encoding as Txt import Data.FileEmbed import System.FilePath (normalise, ()) import qualified Data.Aeson as JS import qualified Data.Aeson.KeyMap as JS import qualified Data.Vector as V import Data.String (fromString) import Data.Maybe (fromMaybe) import Debug.Trace (traceShow) -- For error reporting! import Hearth.TopSites import Hearth.Tagging import Network.URI (uriToString, parseAbsoluteURI, isUnescapedInURIComponent, escapeURIString) import Data.Aeson (Value(..), Object, toJSON) import Data.List (sortOn) import qualified Data.Map as M import Data.Tuple (Solo(..)) renderPage :: ByteString -> [(ByteString, Maybe ByteString)] -> [Txt.Text] -> [Link] -> Value -> Maybe Txt.Text renderPage path query langs hist db = case parseGingerFile resolveSource $ utf8 path of Just (Right tpl) -> do Just $ htmlSource $ flip runGinger tpl $ makeContextHtml ctxt Just (Left err) -> traceShow err Nothing Nothing -> Nothing where ctxt :: Txt.Text -> GVal (Run SourcePos (Writer Html) Html) ctxt "Q" = orderedDict [ utf8' k~>[v | (k', v) <- query, k' == k] | (k, _) <- query ] ctxt "D" = toGVal db ctxt "tags" = list' $ reverse $ sortOn getCount [ orderedDict ["label"~>k, "count"~>v, "selected"~>(k `elem` map utf8' tags)] | (k, v) <- M.toList $ gatherTagsFromProperty "favs" db tags ] ctxt "alltags" = list' $ reverse $ sortOn getCount [ orderedDict ["label"~>k, "count"~>v] | (k, v) <- M.toList $ gatherTagsFromProperty "favs" db [] ] ctxt "_" = toGVal $ translations langs ctxt "tops" = list' [hist2gval entry | entry <- take 20 $ topsites hist] ctxt "hist" = list' $ map hist2gval $ siteHistory hist ctxt "sortByVisits" = fromFunction sortByVisits ctxt "containsAll" = fromFunction containsAll ctxt "urlquery" = fromFunction $ return . toGVal . urlquery ctxt _ = toGVal () hist2gval entry@(label, href, time) = orderedDict [ "label"~>label, "href"~>uriToString id href "", "time"~>time, "count"~>countVisits entry hist] sortByVisits [(_, self)] | Just self' <- asList self = return $ toGVal $ reverse $ sortOn inner self' where inner entry | Just get <- asLookup entry, Just page <- get "href", Just href <- parseAbsoluteURI $ Txt.unpack $ asText page = countVisitsPage href hist | otherwise = 0 sortByVisits _ = return $ toGVal () getCount self | Just get <- asLookup self, Just ret <- asNumber =<< get "count" = ret | otherwise = 0 containsAll [(_, haystack), (_, needles)] = let needles' = filter (/= "") $ map asText $ fromMaybe [] $ asList needles haystack' = map asText $ fromMaybe [haystack] $ asList haystack in return $ toGVal $ all (flip elem haystack') needles' containsAll _ = return $ toGVal () urlquery [(_, self)] | Just self' <- asDictItems self = Txt.intercalate "&" [ Txt.concat [k, "=", escapeURIText $ asText v] | (k, vs) <- self', v <- fromMaybe [vs] $ asList vs ] where escapeURIText = withString $ escapeURIString isUnescapedInURIComponent withString f = Txt.pack . f . Txt.unpack urlquery _ = "" getQuery key = [v | (k, Just v) <- query, k == key] tags = getQuery "tag" resolveSource :: FilePath -> Maybe (Maybe [Char]) resolveSource path | ret@(Just _) <- resolveSource' path = Just ret | ret@(Just _) <- resolveSource' (path "index.html") = Just ret | otherwise = Nothing resolveSource' :: FilePath -> Maybe [Char] resolveSource' = fmap utf8 . flip lookup $(makeRelativeToProject "tpl" >>= embedDir) . tail . normalise -- | Convert from UTF-8 bytestring to a string. utf8 :: ByteString -> String utf8 = Txt.unpack . Txt.decodeUtf8 utf8' :: ByteString -> Txt.Text utf8' = Txt.decodeUtf8 translations :: [Txt.Text] -> JS.Value translations (lang:langs) | Just file <- lookup (Txt.unpack lang ++ ".json") files, Just ret <- JS.decode $ LBS.fromStrict file = ret | "-" `Txt.isInfixOf` lang = let (lang', _) = Txt.breakOn "-" lang in translations (lang':langs) | otherwise = translations langs where files = $(makeRelativeToProject "i18n" >>= embedDir) translations [] = JS.Null -- | Type-constrained conversion of a list to Ginger's datamodel, -- serves to avoid type-inference issues. list' :: [GVal m] -> GVal m list' = toGVal ------ --- Interactive features ------ markAsRead :: ByteString -> Value -> Value markAsRead dest Null = markAsRead dest $ Object $ JS.empty markAsRead dest (Object db) = Object $ JS.insert "readingList" readingList' db where readingList' | Just (Array list) <- JS.lookup "readingList" db = toJSON $ V.filter isn'tDest list | otherwise = Array V.empty isn'tDest (Object record) = JS.lookup "href" record /= Just (String $ utf8' dest) isn'tDest _ = True markAsRead _ db = db handleForm :: ByteString -> [(ByteString, ByteString)] -> Value -> IO (ByteString, Value) handleForm path query Null = handleForm path query $ Object $ JS.empty handleForm "/" query db = return (fromMaybe "/" $ lookup "url" query, db) handleForm "/index.html" query db = return (fromMaybe "/" $ lookup "url" query, db) handleForm "/bookmarks/read-later" query (Object db) = return ("/", Object $ insertConcat "readingList" (query2json query) db) handleForm "/bookmarks/new.html" query (Object db) = return ("/", Object $ insertConcat "favs" (query2json query) $ removeFav (lookup "href" query) db) handleForm "/bookmarks/del" query (Object db) = return ("/", Object $ removeFav (lookup "href" query) db) handleForm path _ db = return (path, db) query2json :: [(ByteString, ByteString)] -> Value query2json query = Object $ JS.fromListWith gatherArray [(fromString $ utf8 k, toJSON $ utf8 v) | (k, v) <- query] where gatherArray (Array a) (Array b) = Array $ b V.++ a gatherArray a (Array b) = Array $ V.snoc b a gatherArray (Array a) b = Array $ V.cons b a gatherArray a b = Array $ V.fromList [b, a] insertConcat :: JS.Key -> Value -> Object -> Object insertConcat key val = JS.insertWith prependJSON key (toJSON [val]) where prependJSON (Array a) (Array b) = Array $ a V.++ b prependJSON a (Array b) = Array $ V.cons a b prependJSON (Array a) b = Array $ V.snoc a b prependJSON a b = Array $ V.fromList [a, b] removeFav :: Maybe ByteString -> Object -> Object removeFav (Just href) db | Solo ret <- JS.alterF inner "favs" db = ret where inner (Just (Array favs)) = Solo $ Just $ toJSON $ V.filter isn'tTarget favs inner (Just fav) | isn'tTarget fav = Solo $ Just fav inner _ = Solo $ Nothing isn'tTarget (Object record) = JS.lookup "href" record /= Just (String $ utf8' href) isn'tTarget _ = True removeFav _ db = db