diff --git a/package.yaml b/package.yaml index 399a138..b272820 100644 --- a/package.yaml +++ b/package.yaml @@ -20,6 +20,7 @@ dependencies: - pandoc - pandoc-types - text +- time - twitter-conduit - xml-conduit - xml-types @@ -29,9 +30,9 @@ library: dependencies: - aeson - authenticate-oauth - - boomerang - directory - filepath + - here - http-conduit - lens - lifted-base @@ -42,14 +43,9 @@ library: - shakespeare - split - tagsoup - - time - transformers - twitter-types - unix - - web-routes - - web-routes-boomerang - - web-routes-happstack - - web-routes-th executables: multiblog: main: Main.hs @@ -71,7 +67,7 @@ tests: - multiblog - HTF - HUnit - - derive + - generic-arbitrary - QuickCheck - quickcheck-instances stability: Experimental diff --git a/src/App.hs b/src/App.hs index c94960e..66cc85f 100644 --- a/src/App.hs +++ b/src/App.hs @@ -22,10 +22,6 @@ import System.Directory import System.Environment import System.FilePath -import Web.Routes -import Web.Routes.Boomerang (boomerangSiteRouteT) -import Web.Routes.Happstack - import Cache import Import import Models @@ -39,7 +35,7 @@ import Views.Feed type App = StateT AppCache (ReaderT AppData IO) -type AppPart a = RouteT Sitemap (ServerPartT App) a +type AppPart a = ServerPartT App a loadApp :: String -- ^ directory to load from @@ -87,33 +83,19 @@ site :: ServerPartT App Response site = do address <- lift $ asks appAddress appDir <- lift $ asks appDirectory - let routedSite = boomerangSiteRouteT handler sitemap let staticDir = appDir </> "static" let staticSite = serveDirectory DisableBrowsing ["index.html"] staticDir - implSite address "" routedSite `mplus` staticSite - --- Run an action in application routing context -runRoute :: RouteT Sitemap m a -> m a -runRoute act - -- Supply a known good URL (root) to run the site, - -- producing the result of the given action - = - let (Right res) = runSite "" (boomerangSiteRouteT (const act) sitemap) [] - in res - -parseRoute :: T.Text -> Either String Sitemap -parseRoute = - fmap runIdentity . runSite "" (boomerangSiteRouteT pure sitemap) . segments - where - segments = decodePathInfo . T.encodeUtf8 + let mainSite = do + method GET + uriRest $ \uri -> case parseURL (T.pack uri) of + Nothing -> mzero + Just route -> handler route + mainSite `mplus` staticSite handler :: Sitemap -> AppPart Response handler route = case route of Index -> index - Yearly y -> yearlyIndex y - Monthly y m -> monthlyIndex y m - Daily d -> dailyIndex d ArticleView d s -> article d s MetaView s f -> meta s f Feed lang -> feedIndex lang @@ -124,15 +106,6 @@ handler route = index :: AppPart Response index = articleList $ const True -yearlyIndex :: Integer -> AppPart Response -yearlyIndex = articleList . byYear - -monthlyIndex :: Integer -> Int -> AppPart Response -monthlyIndex year month = articleList $ byYearMonth year month - -dailyIndex :: Day -> AppPart Response -dailyIndex = articleList . byDate - -- Find the most relevant language preference in a request -- Includes: explicit GET parameter, cookie and Accept-Language header languageHeaderM :: AppPart LanguagePreference @@ -169,12 +142,11 @@ articleList articleFilter = do meta :: Text -> Maybe PageFormat -> AppPart Response meta slug format' = do - let format = fromMaybe Html format' language <- languageHeaderM m <- askMeta slug - case format of - Html -> metaDisplay language m >>= okResponse - _ -> metaExport format language m >>= okResponse + case format' of + Nothing -> metaDisplay language m >>= okResponse + Just format -> metaExport format language m >>= okResponse feedIndex :: Language -> AppPart Response feedIndex language = do @@ -189,4 +161,4 @@ printStylesheet :: AppPart Response printStylesheet = renderPrintStylesheet >>= okResponse codeStylesheet :: AppPart Response -codeStylesheet = renderCodeStylesheet >>= okResponse +codeStylesheet = okResponse renderCodeStylesheet diff --git a/src/CrossPost.hs b/src/CrossPost.hs index ce83f92..b7cee35 100644 --- a/src/CrossPost.hs +++ b/src/CrossPost.hs @@ -21,7 +21,6 @@ import qualified Data.Text as T import Network.HTTP.Conduit (newManager, tlsManagerSettings) -import Web.Routes import Web.Twitter.Conduit (OAuth, TWInfo(..), call, twCredential, twOAuth) import Web.Twitter.Conduit.Api import Web.Twitter.Conduit.Parameters hiding (map) @@ -40,11 +39,11 @@ import Views crossPost :: App () crossPost = do liftIO $ putStrLn "Cross-posting new articles..." - runRoute crossPostTwitter + crossPostTwitter liftIO $ putStrLn "All new articles cross-posted." crossPostTwitter :: - (MonadRoute m, URL m ~ Sitemap, MonadIO m, MonadReader AppData m) => m () + (MonadIO m, MonadReader AppData m) => m () crossPostTwitter = do mgr <- liftIO $ newManager tlsManagerSettings address <- asks appAddress @@ -84,7 +83,7 @@ twitterArticleLinks statuses = do -- Filter out the ones for the article route let articleLinks = [ (date, slug) - | Right (ArticleView date slug) <- map parseRoute urls + | Just (ArticleView date slug) <- map parseURL urls ] -- Get the matched articles let articleFilter art = any (\(d, s) -> byDateSlug d s art) articleLinks diff --git a/src/Import.hs b/src/Import.hs index 5298740..fd633e6 100644 --- a/src/Import.hs +++ b/src/Import.hs @@ -94,7 +94,7 @@ parseContent dir = do case M.lookup (T.pack $ tail ext) readers of Nothing -> pure Nothing Just reader -> do - lang <- parseLanguage fileName + lang <- parseLanguage (T.pack fileName) case reader (T.decodeUtf8 $ sfContent file) of Left err -> throwError $ show err Right res -> pure (Just (lang, res)) diff --git a/src/Routes.hs b/src/Routes.hs index f0282fb..35f81e6 100644 --- a/src/Routes.hs +++ b/src/Routes.hs @@ -1,6 +1,6 @@ +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE QuasiQuotes #-} module Routes where @@ -8,30 +8,31 @@ import Prelude hiding ((.)) import Control.Category (Category((.))) +import Data.String.Here (i) + +import Data.Char (isDigit) import Data.Text (Text) -import qualified Data.Text as T +import qualified Data.Text as Text import Data.Time -import Text.Boomerang.TH (makeBoomerangs) +import Debug.Trace + +import GHC.Generics -import Web.Routes.Boomerang +import Text.Read import Types.Language data PageFormat - = Html - | Pdf + = Pdf | Docx - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Generic) --- TODO: use Boomerang for these formatToStr :: PageFormat -> Text -formatToStr Html = "pdf" formatToStr Pdf = "pdf" formatToStr Docx = "docx" strToFormat :: Text -> Maybe PageFormat -strToFormat "html" = Just Html strToFormat "pdf" = Just Pdf strToFormat "docx" = Just Docx strToFormat _ = Nothing @@ -40,10 +41,6 @@ type MaybeFormat = Maybe PageFormat data Sitemap = Index - | Yearly Integer - | Monthly Integer - Int - | Daily Day | ArticleView Day Text | MetaView Text @@ -52,89 +49,57 @@ data Sitemap | SiteScript | PrintStylesheet | CodeStylesheet - deriving (Eq, Ord, Show) - -makeBoomerangs ''Sitemap - -rDay :: Boomerang TextsError [Text] r (Day :- r) -rDay = xpure mkDay parseDay . (integer </> int </> int) - where - mkDay (y :- m :- d :- x) = fromGregorian y m d :- x - parseDay (day :- x) = - let (y, m, d) = toGregorian day - in Just $ y :- m :- d :- x - --- TODO: This will error on strings which are not language codes -rLanguage :: Boomerang TextsError [Text] r (Language :- r) -rLanguage = xpure mkLang parseLang . anyString - where - mkLang (str :- x) = - let Just lang = parseLanguageM str - in lang :- x - parseLang (lang :- x) = Just $ showLanguage lang :- x - -rString :: Boomerang e tok i (Text :- o) -> Boomerang e tok i (String :- o) -rString = xmaph T.unpack (Just . T.pack) - -anyString :: Boomerang TextsError [Text] o (String :- o) -anyString = rString anyText - -rExtension :: - Boomerang e tok i (Text :- o) - -> Boomerang e tok i (Text :- Maybe Text :- o) -rExtension = xmap splitExt' (Just . joinExt') - where - splitExt' :: Text :- o -> Text :- Maybe Text :- o - splitExt' (seg :- o) = - let (seg', ext) = splitExt seg - in seg' :- ext :- o - joinExt' :: Text :- Maybe Text :- o -> Text :- o - joinExt' (seg :- ext :- o) = joinExt seg ext :- o - --- Swap the top 2 components in a Boomerang -xflip :: Boomerang e tok i (a :- b :- o) -> Boomerang e tok i (b :- a :- o) -xflip = xmap pflip (Just . pflip) - where - pflip (a :- b :- o) = b :- a :- o - --- Apply a transformation to the second topmost component of a Boomerang -xmaph2 :: - (b -> c) - -> (c -> Maybe b) - -> Boomerang e tok i (a :- b :- o) - -> Boomerang e tok i (a :- c :- o) -xmaph2 f g = xflip . xmaph f g . xflip - --- Split a path component into basename and extension -splitExt :: Text -> (Text, Maybe Text) -splitExt segment = - case T.splitOn "." segment of - [] -> ("", Nothing) - [s] -> (s, Nothing) - ss -> (T.intercalate "." $ init ss, Just $ last ss) + deriving (Eq, Ord, Show, Generic) + +routeURL :: Sitemap -> Text +routeURL Index = "" +routeURL (ArticleView date text) = let (year, month, day) = toGregorian date in [i|/${pad 2 year}-${pad 2 month}-${pad 2 day}/${text}|] +routeURL (MetaView text Nothing) = [i|/${text}|] +routeURL (MetaView text (Just format)) = [i|/${text}.${formatToStr format}|] +routeURL (Feed language) = [i|/feed/${showLanguage language}|] +routeURL SiteScript = "/assets/site.js" +routeURL PrintStylesheet = "/assets/print.css" +routeURL CodeStylesheet = "/assets/code.css" + +pad :: Show a => Int -> a -> Text +pad len str = let printed = Text.pack (show str) + padding = Text.replicate (len - Text.length printed) "0" + in padding <> printed + +splitNonEmpty :: Text -> Text -> [Text] +splitNonEmpty splitter = filter (not . Text.null) . Text.splitOn splitter + +readText :: Read a => Text -> Maybe a +readText = readMaybe . Text.unpack + +parseURL :: Text -> Maybe Sitemap +parseURL = parseSegments . splitNonEmpty "/" . dropQueryParams + where + parseSegments [] = Just Index + parseSegments [metaText] = case splitNonEmpty "." metaText of + [text] -> Just $ MetaView text Nothing + [text, formatStr] -> do + format <- strToFormat formatStr + pure $ MetaView text (Just format) + _ -> Nothing + parseSegments [date, articleText] | dateLike date = parseArticle (splitNonEmpty "-" date) articleText + parseSegments [year, month, day, articleText] = parseArticle [year, month, day] articleText + parseSegments ["assets", "site.js"] = Just SiteScript + parseSegments ["assets", "print.css"] = Just PrintStylesheet + parseSegments ["assets", "code.css"] = Just CodeStylesheet + parseSegments ["feed", lang] = Feed <$> parseLanguageM lang + parseSegments _ = Nothing + dateLike = Text.all $ \c -> isDigit c || c == '-' + parseArticle [yearStr, monthStr, dayStr] text = do + year <- readText yearStr + month <- readText monthStr + day <- readText dayStr + date <- fromGregorianValid year month day + pure $ ArticleView date text + parseArticle _ _ = Nothing + dropQueryParams = Text.takeWhile (/= '?') -- Join a basename and extension together joinExt :: Text -> Maybe Text -> Text joinExt segment Nothing = segment joinExt segment (Just ext) = segment <> "." <> ext - --- Convert the second topmost component into a MaybeFormat -xFormat :: - Boomerang e tok i (Text :- Maybe Text :- o) - -> Boomerang e tok i (Text :- MaybeFormat :- o) -xFormat = xmaph2 (strToFormat =<<) (Just . fmap formatToStr) - -sitemap :: Boomerang TextsError [Text] r (Sitemap :- r) -sitemap = - mconcat - [ rIndex - , rYearly . integer - , rMonthly . integer </> int - , rDaily . rDay - , rFeed . "feed" </> rLanguage - , rSiteScript . "site.js" - , rPrintStylesheet . "print.css" - , rCodeStylesheet . "code.css" - , rArticleView . rDay </> anyText - , rMetaView . xFormat (rExtension anyText) - ] diff --git a/src/Types/Content.hs b/src/Types/Content.hs index 975ba2e..ba1dd68 100644 --- a/src/Types/Content.hs +++ b/src/Types/Content.hs @@ -1,6 +1,7 @@ {-| Types for the blog content - articles and metas. -} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} module Types.Content where @@ -15,6 +16,8 @@ import Data.Text (Text) import Data.Time import Data.Yaml +import GHC.Generics (Generic) + import Text.Blaze.Html (Html) import Text.Pandoc hiding (Meta) @@ -40,7 +43,7 @@ data Article = Article { arSlug :: Text , arContent :: LanguageContent , arAuthored :: UTCTime - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) instance Ord Article where a `compare` b = (arAuthored a, arSlug a) `compare` (arAuthored b, arSlug b) @@ -55,7 +58,7 @@ instance HasSlug Article where data Layout = BaseLayout | PresentationLayout - deriving (Eq, Show) + deriving (Eq, Show, Generic) instance FromJSON Layout where parseJSON (String v) @@ -69,7 +72,7 @@ data Meta = Meta , mtLayout :: Layout , mtExportSlugOverride :: Maybe Text , mtContent :: LanguageContent - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) mtExportSlug :: Meta -> Text mtExportSlug meta = fromMaybe (mtSlug meta) (mtExportSlugOverride meta) @@ -85,7 +88,7 @@ data Link = MetaLink { lnName :: Text } | ExternalLink { lnUrl :: Text , lnText :: LanguageString } - deriving (Eq, Show) + deriving (Eq, Show, Generic) instance FromJSON Link where parseJSON (Object v) = diff --git a/src/Types/Language.hs b/src/Types/Language.hs index 69ab13d..2f3c34f 100644 --- a/src/Types/Language.hs +++ b/src/Types/Language.hs @@ -4,6 +4,7 @@ Language-related types. {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Types.Language where @@ -16,18 +17,20 @@ import Data.Function import Data.LanguageCodes import Data.List import Data.List.Split -import qualified Data.Map as M +import Data.Map (Map) +import qualified Data.Map as Map import Data.Maybe -import qualified Data.Text as T +import Data.Text (Text) +import qualified Data.Text as Text import Utils type Language = ISO639_1 -type LanguageMap = M.Map Language +type LanguageMap = Map Language -mapKeysM :: (Monad m, Ord k2) => (k1 -> m k2) -> M.Map k1 a -> m (M.Map k2 a) -mapKeysM kfunc = fmap M.fromList . traverse kvfunc . M.toList +mapKeysM :: (Monad m, Ord k2) => (k1 -> m k2) -> Map k1 a -> m (Map k2 a) +mapKeysM kfunc = fmap Map.fromList . traverse kvfunc . Map.toList where kvfunc (k, v) = (,) <$> kfunc k <*> pure v @@ -36,7 +39,7 @@ instance A.FromJSON ISO639_1 where parseJSON _ = mzero instance A.FromJSONKey ISO639_1 where - fromJSONKey = A.FromJSONKeyTextParser $ parseLanguageM . T.unpack + fromJSONKey = A.FromJSONKeyTextParser parseLanguageM -- | Newtype for parsing either a single value or a map of values newtype LanguageChoices a = @@ -46,7 +49,7 @@ instance A.FromJSON v => A.FromJSON (LanguageChoices v) where parseJSON v@(A.Object _) = LanguageChoices <$> (A.parseJSON v >>= mapKeysM parseLanguageM) parseJSON v@(A.String _) = - LanguageChoices . M.singleton defaultLanguage <$> A.parseJSON v + LanguageChoices . Map.singleton defaultLanguage <$> A.parseJSON v parseJSON _ = mzero newtype LanguagePreference = LanguagePreference @@ -57,67 +60,70 @@ defaultLanguage :: Language defaultLanguage = EN singleLanguage :: Language -> LanguagePreference -singleLanguage lang = LanguagePreference $ M.singleton lang 1 +singleLanguage lang = LanguagePreference $ Map.singleton lang 1 bestLanguage :: LanguagePreference -> Language bestLanguage = fromMaybe defaultLanguage . listToMaybe . fmap fst . - sortBy (reverseCompare `on` snd) . M.toList . unLanguagePreference + sortBy (reverseCompare `on` snd) . Map.toList . unLanguagePreference rankLanguage :: Language -> LanguagePreference -> Float -rankLanguage lang = fromMaybe 0 . M.lookup lang . unLanguagePreference +rankLanguage lang = fromMaybe 0 . Map.lookup lang . unLanguagePreference matchLanguage :: LanguagePreference -> LanguageMap a -> Maybe a matchLanguage = matchLanguageFunc (const 1) matchLanguageFunc :: (a -> Float) -> LanguagePreference -> LanguageMap a -> Maybe a -matchLanguageFunc quality pref values = fst <$> M.maxView ranked +matchLanguageFunc quality pref values = fst <$> Map.maxView ranked where - ranked = M.fromList $ M.elems $ M.mapWithKey rank values + ranked = Map.fromList $ Map.elems $ Map.mapWithKey rank values rank lang value = (rankLanguage lang pref * quality value, value) -parseLanguage :: MonadError String m => String -> m Language +parseLanguage :: MonadError String m => Text -> m Language parseLanguage langStr = case parseLanguageM langStr :: Maybe Language of (Just lang) -> pure lang - Nothing -> throwError $ langStr ++ " is not a valid language code." - -parseLanguageM :: MonadPlus m => String -> m Language -parseLanguageM [c1, c2] = - case fromChars c1 c2 of - Just lang -> return lang - Nothing -> mzero -parseLanguageM (c1:c2:'-':_) = parseLanguageM [c1, c2] -parseLanguageM _ = mzero - -showLanguage :: Language -> String -showLanguage = (\(a, b) -> [a, b]) . toChars - -iso3166 :: Language -> String + Nothing -> throwError $ Text.unpack $ langStr <> " is not a valid language code." + +parseLanguageM :: MonadPlus m => Text -> m Language +parseLanguageM = parseLanguageStr . Text.unpack + where + parseLanguageStr [c1, c2] = + case fromChars c1 c2 of + Just lang -> return lang + Nothing -> mzero + parseLanguageStr (c1:c2:'-':_) = parseLanguageStr [c1, c2] + parseLanguageStr _ = mzero + +showLanguage :: Language -> Text +showLanguage = Text.pack . (\(a, b) -> [a, b]) . toChars + +iso3166 :: Language -> Text iso3166 EN = "gb" iso3166 ZH = "cn" iso3166 x = showLanguage x -- TODO: Parsec or library languageHeader :: Maybe String -> LanguagePreference -languageHeader Nothing = LanguagePreference $ M.singleton defaultLanguage 1 +languageHeader Nothing = LanguagePreference $ Map.singleton defaultLanguage 1 languageHeader (Just str) = - LanguagePreference $ M.fromList $ mapMaybe parsePref $ splitOn "," str + LanguagePreference $ Map.fromList $ mapMaybe parsePref $ splitOn "," str where parsePref pref = case splitOn ";q=" pref of - [lang] -> pairWith 1 <$> parseLanguageM lang - [lang, qvalue] -> pairWith (read qvalue) <$> parseLanguageM lang + [lang] -> pairWith 1 <$> parseLanguageM (Text.pack lang) + [lang, qvalue] -> pairWith (read qvalue) <$> parseLanguageM (Text.pack lang) _ -> Nothing pairWith y x = (x, y) instance Show LanguagePreference where show = - intercalate "," . - map (uncurry showPref) . M.toList . unLanguagePreference + Text.unpack . + Text.intercalate "," . + map (uncurry showPref) . Map.toList . unLanguagePreference where showPref lang 1 = showLanguage lang - showPref lang qvalue = showLanguage lang ++ ";q=" ++ show qvalue + showPref lang qvalue = showLanguage lang <> ";q=" <> Text.pack (show qvalue) diff --git a/src/Types/Services.hs b/src/Types/Services.hs index 0a4be29..68d704e 100644 --- a/src/Types/Services.hs +++ b/src/Types/Services.hs @@ -49,7 +49,7 @@ instance FromJSON AppServices where newtype AppAuth = AppAuthTwitter TwitterAuth - deriving (Eq, Show) + deriving (Eq, Show, Generic) class ToJSON a => ServiceAuth a where toAppAuth :: a -> AppAuth @@ -59,7 +59,7 @@ class ToJSON a => ServiceAuth a where data TwitterAuth = TwitterAuth { taToken :: BS.ByteString , taSecret :: BS.ByteString - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) instance ServiceAuth TwitterAuth where toAppAuth = AppAuthTwitter @@ -98,7 +98,7 @@ instance ToJSON AppAuth where data CrossPost = CrossPost { cpLanguage :: Language , cpServiceDetails :: AppAuth - } deriving (Eq, Show) + } deriving (Eq, Show, Generic) type AppCrossPost = [CrossPost] diff --git a/src/Views.hs b/src/Views.hs index 93e09ed..5960b60 100644 --- a/src/Views.hs +++ b/src/Views.hs @@ -29,8 +29,6 @@ import Text.Pandoc hiding (Meta, Reader) import Text.Pandoc.Highlighting import Text.Pandoc.Walk -import Web.Routes - import Models import Render import Routes @@ -49,8 +47,6 @@ instance Linkable Article where instance Linkable Meta where link m = MetaView (mtSlug m) Nothing -type AppRoute m = (MonadRoute m, URL m ~ Sitemap) - data PageContent = PageContent { pcTitle :: Maybe Text , pcLayout :: Layout @@ -85,13 +81,8 @@ paginate size page allItems = Paginated prev items next | null rest = Nothing | otherwise = Just (page + 1) -convRender :: (url -> [(a, Maybe b)] -> c) -> url -> [(a, b)] -> c -convRender maybeF url params = maybeF url $ map (A.second Just) params - -render :: MonadRoute m => HtmlUrl (URL m) -> m Markup -render html = do - route <- fmap convRender askRouteFn - return $ html route +render :: MonadReader AppData m => HtmlUrl Sitemap -> m Markup +render html = html <$> routeURLParams askLangStringFn :: MonadReader AppData m => LanguagePreference -> m (Text -> Text) @@ -115,15 +106,14 @@ linkTitle lang (MetaLink slug) = do pure $ langTitle lang meta linkDestination :: - (AppRoute m, MonadReader AppData m, MonadPlus m) => Link -> m Text + (MonadReader AppData m, MonadPlus m) => Link -> m Text linkDestination (ExternalLink url _) = pure url linkDestination (MetaLink slug) = do meta <- askMeta slug - route <- askRouteFn - pure $ route (link meta) [] + linkTo meta template :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> PageContent -> m Markup @@ -144,7 +134,7 @@ template lang page = do render $(hamletFile "templates/base_presentation.hamlet") articleListDisplay :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> Paginated Article -> m Markup @@ -154,7 +144,7 @@ articleListDisplay lang articles = do template lang $ def {pcContent = $(hamletFile "templates/list.hamlet")} articleDisplay :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> Article -> m Markup @@ -166,7 +156,7 @@ articleDisplay lang article = } metaDisplay :: - (AppRoute m, MonadReader AppData m, MonadPlus m) + (MonadReader AppData m, MonadPlus m) => LanguagePreference -> Meta -> m Markup @@ -185,20 +175,20 @@ metaDisplay lang meta = $(hamletFile "templates/meta_presentation.hamlet") -- Generate a link to some content -linkTo :: (Linkable a, AppRoute m) => a -> m Text +linkTo :: (MonadReader AppData m, Linkable a) => a -> m Text linkTo a = do - routeFn <- askRouteFn - pure $ routeFn (link a) [] + r <- routeURLParams + pure $ r (link a) [] -- Modify the content to have a link to itself and have no anchors linkedContent :: - (HasContent a, Linkable a, AppRoute m) + (MonadReader AppData m, HasContent a, Linkable a) => LanguagePreference -> a -> m Pandoc linkedContent lang a = do - target <- linkTo a - pure $ linkedHeader target $ langContent lang a + lnk <- linkTo a + pure $ linkedHeader lnk $ langContent lang a -- Modify the first header to be a link to a given place -- and remove all anchors from headers @@ -220,18 +210,19 @@ linkedHeader target doc = evalState (walkM linkHeader doc) True return $ Header n ("", [], []) text' linkHeader x = return x -renderSiteScript :: MonadRoute m => m JavaScript -renderSiteScript = do - route <- fmap convRender askRouteFn - return $ JavaScript $ renderJavascriptUrl route $(juliusFile "templates/site.julius") +routeURLParams :: MonadReader AppData m => m (Render Sitemap) +routeURLParams = do + address <- asks appAddress + pure $ \r _ -> address <> routeURL r + +renderSiteScript :: MonadReader AppData m => m JavaScript +renderSiteScript = routeURLParams >>= \r -> pure $ JavaScript $ renderJavascriptUrl r $(juliusFile "templates/site.julius") -renderPrintStylesheet :: MonadRoute m => m Stylesheet -renderPrintStylesheet = do - route <- fmap convRender askRouteFn - return $ Stylesheet $ renderCssUrl route $(luciusFile "templates/print.lucius") +renderPrintStylesheet :: MonadReader AppData m => m Stylesheet +renderPrintStylesheet = routeURLParams >>= \r -> pure $ Stylesheet $ renderCssUrl r $(luciusFile "templates/print.lucius") -renderCodeStylesheet :: MonadRoute m => m Stylesheet -renderCodeStylesheet = pure $ Stylesheet $ TL.pack $ styleToCss highlightingStyle +renderCodeStylesheet :: Stylesheet +renderCodeStylesheet = Stylesheet $ TL.pack $ styleToCss highlightingStyle -- | Presentation can only render the pipe tables. Disable the other kinds presentationOptions :: WriterOptions diff --git a/src/Views/Export.hs b/src/Views/Export.hs index 8e7371c..9c98ecc 100644 --- a/src/Views/Export.hs +++ b/src/Views/Export.hs @@ -31,8 +31,6 @@ import Text.StringLike import Text.Pandoc hiding (Meta) -import Web.Routes - import Cache import Models import Render @@ -43,9 +41,7 @@ import Views -- Export a meta into one of the supported formats metaExport :: - ( MonadRoute m - , URL m ~ Sitemap - , MonadReader AppData m + ( MonadReader AppData m , MonadState AppCache m , MonadIO m ) @@ -56,7 +52,6 @@ metaExport :: -- Pandoc uses TeX to render PDFs, which requires a lot of packages for Unicode -- support, etc. Use wkhtmltopdf instead metaExport Pdf lang meta = pdfExport lang meta -metaExport Html _ _ = error "HTML is not an export format" metaExport Docx lang meta = do let content = langContent lang meta pure $ @@ -68,12 +63,11 @@ metaExportFileName format meta = Text.intercalate "." [metaName, fileExtension f where metaName = mtExportSlug meta fileExtension Docx = "docx" - fileExtension Html = error "HTML is not an export format" fileExtension Pdf = "pdf" -- Export a PDF using wkhtmltopdf pdfExport :: - (MonadRoute m, URL m ~ Sitemap, MonadState AppCache m, MonadIO m) + (MonadReader AppData m, MonadState AppCache m, MonadIO m) => LanguagePreference -> Meta -> m (Export LB.ByteString) diff --git a/src/Views/Feed.hs b/src/Views/Feed.hs index 45645b1..296b0ee 100644 --- a/src/Views/Feed.hs +++ b/src/Views/Feed.hs @@ -39,8 +39,6 @@ import Text.Blaze.Renderer.Text (renderMarkup) import qualified Text.XML as C -import Web.Routes - import Models import Routes import Types.Content @@ -60,7 +58,7 @@ atomDate :: UTCTime -> Date atomDate = (<> "T00:00:00Z") . Text.pack . showGregorian . utctDay articleEntry :: - (MonadRoute m, URL m ~ Sitemap, MonadReader AppData m) + MonadReader AppData m => Language -> Article -> m Entry @@ -84,7 +82,7 @@ articleEntry lang article = do } feedDisplay :: - (MonadRoute m, URL m ~ Sitemap, MonadReader AppData m) + MonadReader AppData m => Language -> [Article] -> m Response diff --git a/stack.yaml b/stack.yaml index ff829f4..986860c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -4,14 +4,9 @@ packages: ghc-options: '$everything': -threaded extra-deps: - - boomerang-1.4.5.5 - - derive-2.6.4 - twitter-conduit-0.3.0 - twitter-types-0.7.2.2 - twitter-types-lens-0.7.2 - - web-routes-boomerang-0.28.4.2 - - web-routes-happstack-0.23.11 - - web-routes-th-0.22.6.3 flags: pandoc: embed_data_files: true diff --git a/stack.yaml.lock b/stack.yaml.lock index 4945754..f242b91 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -4,20 +4,6 @@ # https://docs.haskellstack.org/en/stable/lock_files packages: -- completed: - hackage: boomerang-1.4.5.5@sha256:a6ea7b0cdc82e7d9d8c66bd63047da13453e20acd5e299e92fa8d286f8663041,1951 - pantry-tree: - size: 791 - sha256: 71b50b11f18cc0e27d040802a8d811caee9066a4bf40dd7e8b44cc57c8133fee - original: - hackage: boomerang-1.4.5.5 -- completed: - hackage: derive-2.6.4@sha256:f697cbe62828b5d777ea148814978940bd8707cb2ad4adff9415fc6139a7c05d,3184 - pantry-tree: - size: 3967 - sha256: cb33c5025c4629ea5377dd0fc521700cac79f8a88a5d062cfbb0cd7f9f37bcb5 - original: - hackage: derive-2.6.4 - completed: hackage: twitter-conduit-0.3.0@sha256:67b68511fc19563ff4159ae37188ac3db7dbc6603d81f11c6f1c2dd8ec1f5bd8,4318 pantry-tree: @@ -39,27 +25,6 @@ packages: sha256: 2b4a738a9ca4f6c98bc82f39b92e09f18e298f524f7121e568582e8f0f4dfe2c original: hackage: twitter-types-lens-0.7.2 -- completed: - hackage: web-routes-boomerang-0.28.4.2@sha256:5885435f8527a056d820630e26f76567d4106c8e13fe67a0bc191149dc9f4242,1060 - pantry-tree: - size: 228 - sha256: bac758a18c07d100c5a259e4e9c49eea3412c0d8d3b96fa1660a8b07dd97d30f - original: - hackage: web-routes-boomerang-0.28.4.2 -- completed: - hackage: web-routes-happstack-0.23.11@sha256:c61a497a13810c15e3b3a17db4fef05357cbf31a4a56bd73cdfbb9f9ae521b10,1044 - pantry-tree: - size: 228 - sha256: 37f74334eadaf5380f76f9f42e0da6ff678ab5961f7e72b35bbe28638e069523 - original: - hackage: web-routes-happstack-0.23.11 -- completed: - hackage: web-routes-th-0.22.6.3@sha256:9a71de3770de13445bf116b16a5468d12e3dac0b91b8d3546088333a96bd235d,1533 - pantry-tree: - size: 267 - sha256: d2d7656d3cc35177ae51689dbe849d5c48d339339ba97018921d858f0a2bca0b - original: - hackage: web-routes-th-0.22.6.3 snapshots: - completed: size: 508835 diff --git a/testsuite/Data/LanguageCodes/Arbitrary.hs b/testsuite/Data/LanguageCodes/Arbitrary.hs index aa9f470..3af5935 100644 --- a/testsuite/Data/LanguageCodes/Arbitrary.hs +++ b/testsuite/Data/LanguageCodes/Arbitrary.hs @@ -1,13 +1,14 @@ -{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Data.LanguageCodes.Arbitrary where -import Prelude hiding (LT) +import Data.LanguageCodes (ISO639_1(..), fromChars) -import Data.DeriveTH -import Data.LanguageCodes (ISO639_1(..)) +import Data.Maybe import Test.QuickCheck -derive makeArbitrary ''ISO639_1 +instance Arbitrary ISO639_1 where + arbitrary = elements $ catMaybes $ fromChars <$> letter <*> letter + where + letter = ['a'..'z'] diff --git a/testsuite/Integration/Base.hs b/testsuite/Integration/Base.hs index 13ebdee..6af85ec 100644 --- a/testsuite/Integration/Base.hs +++ b/testsuite/Integration/Base.hs @@ -184,7 +184,7 @@ withLang1 = withLang . singleLanguage withLangCookie :: Language -> TestRequest -> TestRequest withLangCookie lang req = req - {trCookies = M.insert "lang" (T.pack $ showLanguage lang) (trCookies req)} + {trCookies = M.insert "lang" (showLanguage lang) (trCookies req)} -- Extract contents from a response responseContent :: Response -> IO LB.ByteString diff --git a/testsuite/Integration/TestHome.hs b/testsuite/Integration/TestHome.hs index 3fcbfa2..38e4a26 100644 --- a/testsuite/Integration/TestHome.hs +++ b/testsuite/Integration/TestHome.hs @@ -53,10 +53,10 @@ test_home_next_page = do test_css :: IO () test_css = do - css <- makeRequestText $ simpleRequest "/code.css" + css <- makeRequestText $ simpleRequest "/assets/code.css" assertTextContains "color" css test_js :: IO () test_js = do - js <- makeRequestText $ simpleRequest "/site.js" + js <- makeRequestText $ simpleRequest "/assets/site.js" assertTextContains "function" js diff --git a/testsuite/Integration/TestMeta.hs b/testsuite/Integration/TestMeta.hs index 42873f7..8a9abb2 100644 --- a/testsuite/Integration/TestMeta.hs +++ b/testsuite/Integration/TestMeta.hs @@ -17,11 +17,6 @@ test_meta = do meta <- makeRequestText $ simpleRequest "/meta" assertTextContains "<h1>Test Meta</h1>" meta -test_meta_html :: IO () -test_meta_html = do - meta <- makeRequestText $ simpleRequest "/meta.html" - assertTextContains "<h1>Test Meta</h1>" meta - test_meta_pdf :: IO () test_meta_pdf = do meta_pdf <- makeRequest $ simpleRequest "/meta.pdf" diff --git a/testsuite/TestLanguage.hs b/testsuite/TestLanguage.hs index 743236d..395bf2c 100644 --- a/testsuite/TestLanguage.hs +++ b/testsuite/TestLanguage.hs @@ -1,5 +1,6 @@ {-# OPTIONS_GHC -F -pgmF htfpp #-} {-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE OverloadedStrings #-} module TestLanguage where diff --git a/testsuite/TestModels.hs b/testsuite/TestModels.hs index 98e80dd..59e8d97 100644 --- a/testsuite/TestModels.hs +++ b/testsuite/TestModels.hs @@ -1,13 +1,9 @@ {-# OPTIONS_GHC -F -pgmF htfpp #-} {-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-missing-signatures #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} module TestModels where -import Data.DeriveTH - import Data.LanguageCodes.Arbitrary () import qualified Data.Map as M @@ -22,17 +18,23 @@ import Types.Content import Types.Services import Test.Framework +import Test.QuickCheck.Arbitrary.Generic import Test.QuickCheck.Instances () -derive makeArbitrary ''Article +instance Arbitrary Article where + arbitrary = genericArbitrary -derive makeArbitrary ''Layout +instance Arbitrary Layout where + arbitrary = genericArbitrary -derive makeArbitrary ''Meta +instance Arbitrary Meta where + arbitrary = genericArbitrary -derive makeArbitrary ''Link +instance Arbitrary Link where + arbitrary = genericArbitrary -derive makeArbitrary ''Analytics +instance Arbitrary Analytics where + arbitrary = genericArbitrary instance Arbitrary TW.OAuth where arbitrary = do @@ -44,15 +46,20 @@ instance Arbitrary TW.OAuth where , TW.oauthConsumerSecret = secret } -derive makeArbitrary ''AppServices +instance Arbitrary AppServices where + arbitrary = genericArbitrary -derive makeArbitrary ''TwitterAuth +instance Arbitrary TwitterAuth where + arbitrary = genericArbitrary -derive makeArbitrary ''AppAuth +instance Arbitrary AppAuth where + arbitrary = genericArbitrary -derive makeArbitrary ''CrossPost +instance Arbitrary CrossPost where + arbitrary = genericArbitrary -derive makeArbitrary ''AppData +instance Arbitrary AppData where + arbitrary = genericArbitrary fall :: [a] -> (a -> Bool) -> Bool fall = flip all diff --git a/testsuite/TestRoutes.hs b/testsuite/TestRoutes.hs index 1e21c7b..ddc5cd5 100644 --- a/testsuite/TestRoutes.hs +++ b/testsuite/TestRoutes.hs @@ -3,18 +3,54 @@ module TestRoutes where +import Data.LanguageCodes +import Data.LanguageCodes.Arbitrary () + +import Data.Text (Text) +import qualified Data.Text as Text + +import Data.Time.Calendar + import Routes import Test.Framework +import Test.QuickCheck +import Test.QuickCheck.Arbitrary.Generic import Test.QuickCheck.Instances () -test_splitExt = do - assertEqual ("segment", Nothing) (splitExt "segment") - assertEqual ("segment", Just "ext") (splitExt "segment.ext") - assertEqual - ("segment.more.dots", Just "ext") - (splitExt "segment.more.dots.ext") +instance Arbitrary PageFormat where + arbitrary = genericArbitrary + +arbitraryName :: Gen Text +arbitraryName = Text.pack <$> listOf1 (elements ['a'..'z']) + +instance Arbitrary Sitemap where + arbitrary = oneof [ pure Index + , ArticleView <$> arbitrary <*> arbitraryName + , MetaView <$> arbitraryName <*> arbitrary + , Feed <$> arbitrary + , pure SiteScript + , pure PrintStylesheet + , pure CodeStylesheet + ] + +test_index_URL = do + assertEqual "" (routeURL Index) + assertEqual (Just Index) $ parseURL "/" + +test_article_URL = do + let testArticle = ArticleView (fromGregorian 2020 01 01) "test" + assertEqual "/2020-01-01/test" (routeURL testArticle) + assertEqual (Just testArticle) (parseURL "/2020/01/01/test") + assertEqual (Just testArticle) (parseURL "/2020-01-01/test/") + +test_meta_URL = do + assertEqual "/meta" (routeURL $ MetaView "meta" Nothing) + assertEqual (Just $ MetaView "meta" Nothing) (parseURL "/meta/") + assertEqual "/meta.pdf" (routeURL $ MetaView "meta" (Just Pdf)) + +test_feed_URL = assertEqual "/feed/en" (routeURL $ Feed EN) -prop_splitExt_joinExt seg = - let (seg', ext) = splitExt seg - in joinExt seg' ext == seg +prop_routeURL_parseURL route = + let url = routeURL route + in parseURL url == Just route