Skip to content

Commit

Permalink
Merge branch 'export-meta'
Browse files Browse the repository at this point in the history
  • Loading branch information
koterpillar committed Dec 19, 2015
2 parents 4ded320 + dec1ddb commit ed84476
Show file tree
Hide file tree
Showing 26 changed files with 609 additions and 82 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ language: haskell
ghc:
- 7.8 # Only used for Cabal

addons:
apt:
packages:
- wkhtmltopdf
- xvfb

before_install:
- wget https://www.stackage.org/stack/linux-x86_64 -O stack.tar.gz
- tar xf stack.tar.gz
Expand Down
4 changes: 4 additions & 0 deletions multiblog.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,16 @@ Executable multiblog
, network
, pandoc
, pandoc-types
, process
, process-extras
, shakespeare
, split
, system-argv0
, system-filepath
, tagsoup
, text
, time
, transformers
, xml
, web-routes
, web-routes-boomerang
Expand Down
54 changes: 39 additions & 15 deletions src/App.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
module App where

import Control.Applicative (optional)
import Control.Monad.Reader
import Control.Monad.State

import qualified Data.ByteString.Char8 as B
Expand All @@ -16,35 +17,42 @@ import Web.Routes
import Web.Routes.Boomerang
import Web.Routes.Happstack

import Cache
import Import
import Language
import Models
import Routes
import Utils
import Views
import Views.Export
import Views.Feed

-- TODO: This should be a ReaderT
type App = StateT AppState IO
type App = StateT AppCache (ReaderT AppData IO)

type AppPart a = RouteT Sitemap (ServerPartT App) a

loadApp :: String -- directory to load from
-> String -- site address
-> IO AppState
-> IO AppData
loadApp dataDirectory siteAddress = do
app <- loadFromDirectory dataDirectory
case app of
Left err -> error err
Right appState -> return appState { appAddress = siteAddress }

runApp :: AppState -> App a -> IO a
runApp app a = evalStateT a app
initAppCache :: IO AppCache
initAppCache = do
pdfCache <- initCache
return $ AppCache pdfCache

runApp :: AppCache -> AppData -> App a -> IO a
runApp cache app a = do
runReaderT (evalStateT a cache) app

site :: ServerPartT App Response
site = do
address <- lift $ gets appAddress
appDir <- lift $ gets appDirectory
address <- lift $ asks appAddress
appDir <- lift $ asks appDirectory
let routedSite = boomerangSiteRouteT handler sitemap
let staticSite = serveDirectory DisableBrowsing [] $ appDir ++ "/static"
implSite (T.pack address) "" routedSite `mplus` staticSite
Expand All @@ -56,9 +64,10 @@ handler route = case route of
Monthly y m -> monthlyIndex y m
Daily d -> dailyIndex d
ArticleView d s -> article d s
MetaView s -> meta s
MetaView s f -> meta s f
Feed lang -> feedIndex lang
SiteScript -> siteScript
PrintStylesheet -> printStylesheet

index :: AppPart Response
index = articleList $ const True
Expand Down Expand Up @@ -89,27 +98,42 @@ html = ok . toResponse
article :: Day -> String -> AppPart Response
article date slug = do
language <- languageHeaderM
a <- onlyOne $ lift $ getFiltered $ byDateSlug date slug
a <- onlyOne $ lift $ askFiltered $ byDateSlug date slug
articleDisplay language a >>= html

articleList :: (Article -> Bool) -> AppPart Response
articleList articleFilter = do
articles <- lift $ getFiltered articleFilter
articles <- lift $ askFiltered articleFilter
let sorted = sortBy reverseCompare articles
language <- languageHeaderM
articleListDisplay language sorted >>= html

meta :: String -> AppPart Response
meta slug = do
meta :: String -> Maybe PageFormat -> AppPart Response
meta slug format' = do
let format = fromMaybe Html format'
language <- languageHeaderM
m <- getMeta slug
metaDisplay language m >>= html
m <- askMeta slug
case format of
Html -> metaDisplay language m >>= html
_ -> metaExport format language m >>= html

feedIndex :: Language -> AppPart Response
feedIndex language = do
articles <- lift $ getFiltered (const True)
articles <- lift $ askFiltered (const True)
let sorted = sortBy reverseCompare articles
feedDisplay language sorted >>= html

-- Override content type on any response
data WithContentType r = WithContentType String r

instance ToMessage r => ToMessage (WithContentType r) where
toResponse (WithContentType ct r) = setHeaderBS (B.pack "Content-Type") (B.pack ct) $ toResponse r

asCss :: ToMessage r => r -> WithContentType r
asCss = WithContentType "text/css"

siteScript :: AppPart Response
siteScript = renderSiteScript >>= html

printStylesheet :: AppPart Response
printStylesheet = renderPrintStylesheet >>= html . asCss
63 changes: 63 additions & 0 deletions src/Cache.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
module Cache (
Cache,
CacheMonad,
HasCache,
getCache,
getCacheM,
initCache,
withCache,
withCacheM,
) where

import Control.Concurrent.MVar
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.State

import Data.Map (Map)
import qualified Data.Map as M

-- Opaque cache type storing values of type v against keys of type k
data Cache k v = Cache (MVar (Map k v))

instance Show (Cache k v) where
show _ = "(cache)"

-- Initialize an empty cache
initCache :: MonadIO m => m (Cache k v)
initCache = liftM Cache $ liftIO $ newMVar M.empty

-- Cache the result of monadic action under a certain key
withCache :: (MonadIO m, Ord k) => Cache k v -> k -> m v -> m v
withCache (Cache cache) key action = do
values <- liftIO $ readMVar cache
case M.lookup key values of
Just val -> return val
Nothing -> do
val <- action
liftIO $ modifyMVar_ cache $ return . insertIfMissing key val
return val

-- Class for data structures with a cache field
class HasCache k v a where
getCache :: a -> Cache k v

-- Monads that have caches
class CacheMonad k v m where
getCacheM :: m (Cache k v)

-- If the state has cache, it can be used
instance (MonadState s m, HasCache k v s) => CacheMonad k v m where
getCacheM = gets getCache

withCacheM :: (MonadIO m, CacheMonad k v m, Ord k) => k -> m v -> m v
withCacheM key action = do
cache <- getCacheM
withCache cache key action

-- Insert the new value only if there's not an existing one
insertIfMissing :: Ord k => k -> v -> Map k v -> Map k v
insertIfMissing = M.insertWith (flip const)
2 changes: 1 addition & 1 deletion src/Import.hs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ extractIf test = do
Nothing -> (result, m:ms') where (result, ms') = tryExtract ms

-- Load the application state from a directory
loadFromDirectory :: FilePath -> IO (Either String AppState)
loadFromDirectory :: FilePath -> IO (Either String AppData)
loadFromDirectory path = do
sources <- sourcesFromDirectory path
stringsFile <- readFile $ path </> "strings.yaml"
Expand Down
3 changes: 2 additions & 1 deletion src/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ main = reloadHup $ do
address <- siteAddress
-- TODO: directory name as parameter?
app <- loadApp "content" address
cache <- initAppCache
lport <- listenPort
let conf = nullConf { port = lport }
-- Manually bind the socket to close it on exception
bracket
(bindPort conf)
close
(\sock -> simpleHTTPWithSocket' (runApp app) sock conf site)
(\sock -> simpleHTTPWithSocket' (runApp cache app) sock conf site)

siteAddress :: IO String
siteAddress = do
Expand Down
54 changes: 32 additions & 22 deletions src/Models.hs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Models where

import Control.Monad
import Control.Monad.Reader
import Control.Monad.State

import qualified Data.ByteString.Lazy as LB
import qualified Data.Map as M
import Data.Maybe
import qualified Data.Set as S
Expand All @@ -13,6 +16,7 @@ import Data.Time
import Text.Pandoc hiding (Meta, readers)
import Text.Pandoc.Walk

import Cache
import Language
import Utils

Expand Down Expand Up @@ -55,21 +59,21 @@ instance HasSlug Article where
instance HasSlug Meta where
getSlug = mtSlug

data AppState = AppState { appDirectory :: String
, appAddress :: String
, appArticles :: [Article]
, appMeta :: [Meta]
, appStrings :: M.Map String (LanguageMap String)
}
data AppData = AppData { appDirectory :: String
, appAddress :: String
, appArticles :: [Article]
, appMeta :: [Meta]
, appStrings :: M.Map String (LanguageMap String)
}
deriving (Eq, Show)

emptyState :: AppState
emptyState = AppState { appDirectory = ""
, appAddress = ""
, appArticles = []
, appMeta = []
, appStrings = M.empty
}
emptyState :: AppData
emptyState = AppData { appDirectory = ""
, appAddress = ""
, appArticles = []
, appMeta = []
, appStrings = M.empty
}

mkDate :: Integer -> Int -> Int -> UTCTime
mkDate y m d = atMidnight $ fromGregorian y m d
Expand All @@ -94,21 +98,21 @@ bySlug slug = (== slug) . getSlug
byDateSlug :: Day -> String -> Article -> Bool
byDateSlug d s a = byDate d a && bySlug s a

getApp :: MonadState AppState m => m AppState
getApp = get
askApp :: MonadReader AppData m => m AppData
askApp = ask

getFiltered :: MonadState AppState m => (Article -> Bool) -> m [Article]
getFiltered articleFilter = gets $ filter articleFilter . appArticles
askFiltered :: MonadReader AppData m => (Article -> Bool) -> m [Article]
askFiltered articleFilter = asks $ filter articleFilter . appArticles

getOne :: (MonadState AppState m, MonadPlus m) =>
askOne :: (MonadReader AppData m, MonadPlus m) =>
(Article -> Bool) -> m Article
getOne articleFilter = onlyOne $ getFiltered articleFilter
askOne articleFilter = onlyOne $ askFiltered articleFilter

getMeta :: (MonadState AppState m, MonadPlus m) => String -> m Meta
getMeta slug = onlyOne $ gets $ filter (bySlug slug) . appMeta
askMeta :: (MonadReader AppData m, MonadPlus m) => String -> m Meta
askMeta slug = onlyOne $ asks $ filter (bySlug slug) . appMeta

-- Find all languages used on the site
allLanguages :: AppState -> S.Set Language
allLanguages :: AppData -> S.Set Language
allLanguages app = S.union articleLangs metaLangs
where articleLangs = allContentLangs $ appArticles app
metaLangs = allContentLangs $ appMeta app
Expand Down Expand Up @@ -142,3 +146,9 @@ stripTitle (Pandoc meta blocks) = Pandoc meta blocks'

langContent :: HasContent a => LanguagePreference -> a -> Pandoc
langContent lang = fromJust . matchLanguage lang . getContent

data AppCache = AppCache { appcachePdf :: Cache (Language, String) LB.ByteString
}

instance HasCache (Language, String) LB.ByteString AppCache where
getCache = appcachePdf
Loading

0 comments on commit ed84476

Please sign in to comment.