{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Render(retreiveStyles, renderDoc, c_renderDoc) where import qualified Data.ByteString.Lazy as B import qualified Text.XML as XML import Data.Text as Txt (pack, unpack, Text(..), intercalate) import qualified Data.Map as M import System.Directory as Dir import Data.FileEmbed import Data.Maybe (fromMaybe, maybeToList) import Text.Read (readMaybe) import Debug.Trace (trace) --- External Rhapsode subcomponents import qualified Data.CSS.Syntax.StyleSheet as CSS import qualified Data.CSS.Style as Style import Data.CSS.StyleTree import qualified Data.CSS.Syntax.Tokens as CSSTok import qualified Data.CSS.Preprocessor.Conditions as CSSCond import Data.CSS.Preprocessor.Assets import qualified Data.CSS.Preprocessor.PsuedoClasses as CSSPseudo import qualified Data.CSS.Preprocessor.Text as CSSTxt import qualified Data.HTML2CSS as H2C import Network.URI import Network.URI.Fetch import Network.URI.Charset --- For CSS assets import Data.List (nub, elem) import Control.Concurrent.Async (forConcurrently) import System.IO.Temp import Control.Exception (catch) --- For psuedoclasses import qualified Data.Trie.Text as Trie import qualified Data.CSS.Syntax.Selector as CSSSel -- Internal Rhapsode Subcomponents import SpeechStyle import SSML import Input (applyCSScharset) -- C API import Types import Foreign.StablePtr import Foreign.C.String import Data.ByteString (useAsCString) renderDoc :: Style.QueryableStyleSheet (Style.VarParser (CSSTxt.TextStyle SpeechStyle)) -> XML.Element -> B.ByteString renderDoc style html = renderElLBS $ styleToSSML $ CSSTxt.resolve $ inlinePseudos $ H2C.stylizeEl style html inlinePseudos :: Style.PropertyParser s => StyleTree [(Text, Style.VarParser s)] -> StyleTree s inlinePseudos (StyleTree self childs) = StyleTree { style = fromMaybe Style.temp $ Style.innerParser <$> lookup "" self, children = pseudo "before" ++ map inlinePseudos childs ++ pseudo "after" } where pseudo n | Just style <- Style.innerParser <$> lookup n self, Just style' <- Style.longhand style style "::" [CSSTok.Ident n] = [StyleTree style' []] | Just style <- Style.innerParser <$> lookup n self = [StyleTree style []] | otherwise = [] renderElLBS el = XML.renderLBS XML.def $ XML.Document { XML.documentPrologue = XML.Prologue [] Nothing [], XML.documentRoot = el, XML.documentEpilogue = [] } retreiveStyles :: Session -> CSSCond.ConditionalStyles (CSSTxt.TextStyle SpeechStyle) -> IO (CSSCond.ConditionalStyles (CSSTxt.TextStyle SpeechStyle)) retreiveStyles manager authorStyle = do let agentStyle = H2C.cssPriorityAgent authorStyle `CSS.parse` $(embedStringFile "useragent.css") userStyle <- loadUserStyles agentStyle CSSCond.loadImports loadURL lowerVars lowerToks userStyle [] where loadURL url = do response <- fetchURL manager ["text/css"] url let charsets' = map unpack charsets return $ case response of ("text/css", Left text) -> text ("text/css", Right bytes) -> applyCSScharset charsets' $ B.toStrict bytes (_, _) -> "" resolve' :: CSS.StyleSheet s => s -> CSSCond.ConditionalStyles (CSSTxt.TextStyle SpeechStyle) -> s resolve' = CSSCond.resolve lowerVars lowerToks lowerVars "speech" = CSSCond.B True lowerVars "-rhapsode" = CSSCond.B True lowerVars _ = CSSCond.B False lowerToks _ = CSSCond.B False loadUserStyles styles = do dir <- Dir.getXdgDirectory Dir.XdgConfig "rhapsode" exists <- Dir.doesDirectoryExist dir loadDirectory dir exists where loadDirectory _ False = return styles loadDirectory dir True = do files <- Dir.listDirectory dir loadFiles (H2C.cssPriorityUser styles) files loadFiles style (file:files) = do source <- readFile file CSS.parse style (Txt.pack source) `loadFiles` files loadFiles style [] = return style targetSel "" = [CSSTok.Ident "main"] targetSel "#" = [CSSTok.Colon, CSSTok.Ident "root"] targetSel ('#':'.':anchor) = CSSTok.Colon : CSSTok.Ident "root" : concat [ selLayer n | n <- path anchor] where path ('.':anchor) = [] : path anchor path (c:anchor) | n:path' <- path anchor, c >= '0' && c <= '9' = (c:n):path' path [] = [[]] path _ = [] selLayer n = [ CSSTok.Delim '>', CSSTok.Colon, CSSTok.Function "nth-child", CSSTok.Number (pack n) (CSSTok.NVInteger $ fromMaybe 0 $ readMaybe n), CSSTok.RightParen] targetSel ('#':id) = [CSSTok.Hash CSSTok.HUnrestricted $ Txt.pack id] targetSel _ = [] testVisited :: Trie.Trie () -> URI -> String -> Bool testVisited hist base val = uriToText url `Trie.member` hist where url = fromMaybe nullURI (parseURIReference val) `relativeTo` base uriToText uri = pack $ uriToString id uri "" rhapsodePseudoFilter url hist = -- Note: not all links must have an href tag, but it's not a bad approximation visited links must. -- Doing it this way is easier to implement in Haskell Stylist. CSSPseudo.addTest "visited" Nothing "href" (CSSSel.PropertyFunc $ testVisited hist url) $ CSSPseudo.addRewrite "link" "[src], [href], details > summary" $ CSSPseudo.addRewrite' "target" (targetSel $ uriFragment url) $ CSSPseudo.htmlPsuedoFilter Style.queryableStyleSheet -------- ---- Download assets -------- downloadAssets session mimes (StyleAssets _ assets) = do dir <- Dir.getXdgDirectory Dir.XdgCache "rhapsode" Dir.removeDirectoryRecursive dir `catch` ignoreError -- Clear cache. Dir.createDirectoryIfMissing True dir fetchURLs session mimes assets $ filterMIMEs mimes $ saveDownload nullURI dir where ignoreError :: IOError -> IO () ignoreError _ = return () filterMIMEs mimes cb download@(_, mime, _) | mime `elem` mimes = cb download | otherwise = return nullURI -------- ---- C API -------- foreign export ccall c_renderDoc :: StablePtr Session -> StablePtr Page -> Bool -> IO CString -- Hard to C bindings without IO c_renderDoc c_session c_page rewriteURLs = do session <- deRefStablePtr c_session page <- deRefStablePtr c_page css' <- retreiveStyles session $ css page let pseudoFilter = rhapsodePseudoFilter (Types.url page) $ visitedURLs page qCSS <- if rewriteURLs then do assets <- downloadAssets session [ "audio/vnd.wav" ] $ resolve' (StyleAssets ["cue-before", "cue-after", "cue"] []) css' let URIRewriter _ qCSS' = resolve' (URIRewriter assets pseudoFilter) css' return $ CSSPseudo.inner qCSS' else return $ CSSPseudo.inner $ resolve' pseudoFilter css' let ssml = renderDoc qCSS $ XML.documentRoot $ html page B.toStrict ssml `useAsCString` \cstr -> do str <- peekCString cstr newCString str