Compare commits

..

12 commits

27 changed files with 459 additions and 236 deletions

View file

@ -1,27 +0,0 @@
(use-modules (gnu packages haskell-xyz)
(gnu packages haskell-web)
(guix build-system haskell)
(guix download)
(guix gexp)
(guix git-download)
(guix licenses)
(guix packages))
(package
(name "ghc-template")
(version "0.2.0.10")
(source (origin
(method url-fetch)
(uri (hackage-uri "template" version))
(sha256
(base32
"10mcnhi2rdflmv79z0359nn5sylifvk9ih38xnjqqby6n4hs7mcg"))))
(build-system haskell-build-system)
(properties '((upstream-name . "template")))
(home-page "http://hackage.haskell.org/package/template")
(synopsis "Simple string substitution")
(description
"Simple string substitution library that supports \\\"$\\\"-based substitution.
Meant to be used when Text.Printf or string concatenation would lead to code
that is hard to read but when a full blown templating system is overkill.")
(license bsd-3))

View file

@ -1,44 +0,0 @@
(use-modules (gnu packages haskell-xyz)
(gnu packages haskell-web)
(guix build-system haskell)
(guix download)
(guix gexp)
(guix git-download)
(guix licenses)
(guix packages)
(loom packages sjw))
(let
((%source-dir (dirname (current-filename)))
(ghc-template (load "ghc-template.scm")))
(package
(name "hablo")
(version "devel")
(source
(local-file %source-dir
#:recursive? #t
#:select? (git-predicate %source-dir)))
(build-system haskell-build-system)
(inputs
(list ghc-aeson
ghc-attoparsec
ghc-lucid
ghc-optparse-applicative
ghc-parsec
ghc-random
ghc-sjw
ghc-template
ghc-xdg-basedir))
(native-search-paths
(list
(search-path-specification (variable "SJW_PATH")
(files '("lib/SJW")))))
(home-page "https://git.marvid.fr/Tissevert/SJW")
(synopsis "The Simple Javascript Wrench")
(description
"SJW is a very simple tool to pack several JS modules into a single
script. It doesn't really do proper compilation work (yet) except
resolving the modules dependencies and detecting import loops but it
provides each module with an independent execution context in the
resulting script.")
(license gpl3+)))

View file

@ -50,21 +50,23 @@ library
, Pretty , Pretty
, RSS , RSS
-- other-extensions: -- other-extensions:
build-depends: aeson >= 1.2.0 && < 2.1 build-depends: aeson >= 1.2.0 && < 1.6
, base >= 4.9.1 && < 4.17 , base >= 4.9.1 && < 4.16
, bytestring >= 0.10.8 && < 0.12 , bytestring >= 0.10.8 && < 0.12
, containers >= 0.5.11 && < 0.7 , containers >= 0.5.11 && < 0.7
, directory >= 1.3.1 && < 1.4 , directory >= 1.3.1 && < 1.4
, filepath >= 1.4.2 && < 1.5 , filepath >= 1.4.2 && < 1.5
, lucid >= 2.8.0 && < 2.12 , blaze-html
, blaze-markup
, mtl >= 2.2.2 && < 2.3 , mtl >= 2.2.2 && < 2.3
, optparse-applicative >= 0.14.0 && < 0.18 , optparse-applicative >= 0.14.0 && < 0.17
, parsec >= 3.1.13 && < 3.2 , parsec >= 3.1.13 && < 3.2
, template >= 0.2.0 && < 0.3 , template >= 0.2.0 && < 0.3
, text >= 1.2.3 && < 1.3 , text >= 1.2.3 && < 1.3
, time >= 1.8.0 && < 1.12 , time >= 1.8.0 && < 1.12
, SJW >= 0.1.2 && < 0.2 , SJW >= 0.1.2 && < 0.2
, unix >= 2.7.2 && < 2.8 , unix >= 2.7.2 && < 2.8
, url >= 2.1.3 && < 2.2
ghc-options: -Wall ghc-options: -Wall
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010
@ -75,7 +77,7 @@ executable hablo
-- other-extensions: -- other-extensions:
build-depends: base build-depends: base
, hablo , hablo
, mtl >= 2.2.2 && < 2.3 , mtl
ghc-options: -Wall ghc-options: -Wall
default-language: Haskell2010 default-language: Haskell2010
@ -93,11 +95,14 @@ test-suite tests
, Mock.Blog.Wording , Mock.Blog.Wording
, Mock.Collection , Mock.Collection
, Mock.Markdown , Mock.Markdown
, Mock.URL
, Structure , Structure
, URLs
, Utils , Utils
, XML.Card , XML.Card
, XML.Card.Component , XML.Card.Component
, XML.Card.Output , XML.Card.Output
, XML.Favicon
build-depends: base build-depends: base
, Cabal , Cabal
, containers , containers
@ -107,6 +112,7 @@ test-suite tests
, lucid , lucid
, mtl , mtl
, text , text
, url
hs-source-dirs: test hs-source-dirs: test
ghc-options: -Wall ghc-options: -Wall
default-language: Haskell2010 default-language: Haskell2010

View file

@ -6,6 +6,7 @@ module ArticlesList (
, description , description
, getArticles , getArticles
, otherURL , otherURL
, ArticlesList.path
, rssLinkTexts , rssLinkTexts
) where ) where
@ -14,9 +15,7 @@ import Blog (Blog(..), Renderer, Skin(..), template)
import Collection (Collection(..)) import Collection (Collection(..))
import Control.Monad.Reader (MonadReader, asks) import Control.Monad.Reader (MonadReader, asks)
import Data.Text (Text, pack) import Data.Text (Text, pack)
import Files (absoluteLink)
import Pretty ((.$)) import Pretty ((.$))
import System.FilePath.Posix ((</>))
data ArticlesList = ArticlesList { data ArticlesList = ArticlesList {
full :: Bool full :: Bool
@ -28,9 +27,8 @@ getArticles (ArticlesList {full, collection = Collection {featured}}) = do
limit <- take <$> (asks $skin.$previewArticlesCount) limit <- take <$> (asks $skin.$previewArticlesCount)
return $ if full then featured else limit featured return $ if full then featured else limit featured
otherURL :: ArticlesList -> String otherURL :: ArticlesList -> FilePath
otherURL (ArticlesList {full, collection}) = absoluteLink $ otherURL (ArticlesList {full}) = if full then "." else "all.html"
(if full then id else (</> "all.html")) . maybe "" id $ tag collection
description :: Renderer m => ArticlesList -> m Text description :: Renderer m => ArticlesList -> m Text
description (ArticlesList {full, collection}) = description (ArticlesList {full, collection}) =
@ -46,3 +44,6 @@ rssLinkTexts (ArticlesList {collection}) = do
return (text, title) return (text, title)
where where
environment = maybe [] (\v -> [("tag", pack v)]) $ tag collection environment = maybe [] (\v -> [("tag", pack v)]) $ tag collection
path :: ArticlesList -> FilePath
path = maybe "" id . tag . collection

View file

@ -6,7 +6,7 @@ module Blog (
, Path(..) , Path(..)
, Renderer , Renderer
, Skin(..) , Skin(..)
, URL(..) , URLs(..)
, Wording , Wording
, build , build
, template , template
@ -22,7 +22,7 @@ import Blog.Template (Environment, Templates, render)
import qualified Blog.Template as Template (build) import qualified Blog.Template as Template (build)
import Blog.Skin (Skin(..)) import Blog.Skin (Skin(..))
import qualified Blog.Skin as Skin (build) import qualified Blog.Skin as Skin (build)
import Blog.URL (URL(..)) import Blog.URL (URLs(..))
import qualified Blog.URL as URL (build) import qualified Blog.URL as URL (build)
import Blog.Wording (Wording) import Blog.Wording (Wording)
import qualified Blog.Wording as Wording (build) import qualified Blog.Wording as Wording (build)
@ -57,7 +57,7 @@ data Blog = Blog {
, skin :: Skin , skin :: Skin
, tags :: Collection (Set String) , tags :: Collection (Set String)
, templates :: Templates , templates :: Templates
, urls :: URL , urls :: URLs
, wording :: Wording , wording :: Wording
} }

View file

@ -1,45 +1,49 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
module Blog.Skin ( module Blog.Skin (
Skin(..) Skin(..)
, build , build
, findImage
) where ) where
import Arguments (Arguments) import Arguments (Arguments)
import qualified Arguments (bannerPath, favicon, cardImage, headPath, previewArticlesCount, previewLinesCount) import qualified Arguments (bannerPath, favicon, cardImage, headPath, previewArticlesCount, previewLinesCount)
import Blog.URL (checkURL, pathRelative)
import Control.Monad (filterM) import Control.Monad (filterM)
import Data.Aeson (ToJSON(..), (.=), pairs) import Data.Aeson (ToJSON(..), (.=), object, pairs)
import Data.Maybe (listToMaybe) import Data.Maybe (listToMaybe)
#if !MIN_VERSION_base(4,11,0) #if !MIN_VERSION_base(4,11,0)
import Data.Monoid ((<>)) import Data.Monoid ((<>))
#endif #endif
import Files (absoluteLink) import Network.URL (URL)
import GHC.Generics (Generic)
import Prelude hiding (head) import Prelude hiding (head)
import System.Directory (doesFileExist) import System.Directory (doesFileExist)
import System.FilePath ((</>), (<.>)) import System.FilePath ((</>), (<.>))
data Skin = Skin { data Skin = Skin {
banner :: Maybe String banner :: Maybe String
, cardImage :: Maybe FilePath , cardImage :: Maybe URL
, favicon :: Maybe FilePath , favicon :: Maybe URL
, head :: Maybe String , head :: Maybe String
, previewArticlesCount :: Int , previewArticlesCount :: Int
, previewLinesCount :: Int , previewLinesCount :: Int
} deriving Generic }
instance ToJSON Skin where instance ToJSON Skin where
toJSON (Skin {previewArticlesCount, previewLinesCount}) = object [
("previewArticlesCount", toJSON previewArticlesCount)
, ("previewLinesCount", toJSON previewLinesCount)
]
toEncoding (Skin {previewArticlesCount, previewLinesCount}) = pairs ( toEncoding (Skin {previewArticlesCount, previewLinesCount}) = pairs (
"previewArticlesCount" .= previewArticlesCount "previewArticlesCount" .= previewArticlesCount
<> "previewLinesCount" .= previewLinesCount <> "previewLinesCount" .= previewLinesCount
) )
findImage :: String -> Maybe FilePath -> IO (Maybe FilePath) findImage :: String -> Maybe FilePath -> IO (Maybe URL)
findImage _ (Just path) = return . Just $ absoluteLink path findImage _ (Just path) = Just <$> checkURL path
findImage name Nothing = findImage name Nothing =
listToMaybe <$> filterM doesFileExist pathsToCheck fmap pathRelative . listToMaybe <$> filterM doesFileExist pathsToCheck
where where
directories = [".", "image", "images", "pictures", "skin", "static"] directories = [".", "image", "images", "pictures", "skin", "static"]
extensions = ["ico", "gif", "jpeg", "jpg", "png", "svg"] extensions = ["ico", "gif", "jpeg", "jpg", "png", "svg"]

View file

@ -1,45 +1,113 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Blog.URL ( module Blog.URL (
URL(..) AbsoluteURL(..)
, URLs(..)
, build , build
, checkURL
, defaultOn
, localPrefix
, pathOn
, pathRelative
, toText
) where ) where
import Arguments (Arguments) import Arguments (Arguments)
import qualified Arguments as Arguments (Arguments(..)) import qualified Arguments as Arguments (Arguments(..))
import Data.Aeson (ToJSON(..), (.=), pairs) import Control.Monad.IO.Class (MonadIO(..))
import GHC.Generics (Generic) import Data.Aeson (ToJSON(..), (.=), object, pairs)
import Data.Function (on)
import Data.List (unionBy)
import Data.Text (Text, pack)
import Network.URL (Host, URL(..), URLType(..), exportURL, importURL)
import System.Exit (die) import System.Exit (die)
import System.IO (hPutStrLn, stderr) import System.IO (hPutStrLn, stderr)
import System.FilePath ((</>))
import Text.Printf (printf) import Text.Printf (printf)
data URL = URL { data AbsoluteURL = AbsoluteURL {
cards :: Maybe String host :: Host
, comments :: Maybe String , urlPath :: FilePath
, rss :: Maybe String , urlParams :: [(String, String)]
} deriving Generic }
instance ToJSON URL where toText :: AbsoluteURL -> Text
toEncoding (URL {comments}) = pairs ( toText (AbsoluteURL {host, urlPath, urlParams}) =
"comments" .= comments pack . exportURL $ URL (Absolute host) urlPath urlParams
data URLs = URLs {
cards :: Maybe AbsoluteURL
, comments :: Maybe AbsoluteURL
, rss :: Maybe AbsoluteURL
}
instance ToJSON URLs where
toJSON (URLs {comments}) = object [
("comments", toJSON (toText <$> comments))
]
toEncoding (URLs {comments}) = pairs (
"comments" .= (toText <$> comments)
) )
build :: Arguments -> IO URL checkURL :: MonadIO m => String -> m URL
build arguments = do checkURL url =
cards <- getSiteURL argOGCards "Open Graph cards" maybe (liftIO . die $ printf "Invalid URL %s" url) pure $ importURL url
rss <- getSiteURL argRSS "RSS feeds"
checksUsed (argOGCards || argRSS) siteURL checkAbsolute :: MonadIO m => URL -> m AbsoluteURL
return $ URL {cards, comments, rss} checkAbsolute (URL {url_type = Absolute host, url_path, url_params}) =
where pure $ AbsoluteURL host url_path url_params
comments = Arguments.commentsURL arguments checkAbsolute url =
siteURL = Arguments.siteURL arguments liftIO . die . printf "%s is not an absolute URL" $ exportURL url
argOGCards = Arguments.openGraphCards arguments
argRSS = Arguments.rss arguments checksUsed :: Bool -> Maybe a -> IO ()
errorMsg :: String -> String
errorMsg = printf "Enabling %s requires setting the site url with --site-url"
getSiteURL False _ = return Nothing
getSiteURL True name = maybe (die $ errorMsg name) (return . Just) siteURL
checksUsed False (Just _) = checksUsed False (Just _) =
hPutStrLn stderr "Site url is set but not used, did you forget --open-graph-cards or --rss ?" hPutStrLn stderr "Site url is set but not used, did you forget --open-graph-cards or --rss ?"
checksUsed _ _ = return () checksUsed _ _ = return ()
getURL :: Maybe Bool -> Maybe String -> IO (Maybe AbsoluteURL) -> IO (Maybe AbsoluteURL)
getURL Nothing (Just url) _ = Just <$> (checkAbsolute =<< checkURL url)
getURL (Just True) Nothing failure = failure
getURL (Just True) (Just url) _ = Just <$> (checkAbsolute =<< checkURL url)
getURL _ _ _ = pure Nothing
build :: Arguments -> IO URLs
build arguments = do
cards <- getURL (Just argOGCards) siteURL (failBecauseOf "Open Graph cards")
rss <- getURL (Just argRSS) siteURL (failBecauseOf "RSS feeds")
comments <- getURL Nothing commentsURL (pure Nothing)
checksUsed (argOGCards || argRSS) siteURL
return $ URLs {cards, comments, rss}
where
commentsURL = Arguments.commentsURL arguments
siteURL = Arguments.siteURL arguments
argOGCards = Arguments.openGraphCards arguments
argRSS = Arguments.rss arguments
failBecauseOf :: String -> IO a
failBecauseOf =
die . printf "Enabling %s requires setting the site url with --site-url"
pathRelative :: String -> URL
pathRelative url_path = URL {url_type = PathRelative, url_path, url_params = []}
addParams :: AbsoluteURL -> [(String, String)] -> AbsoluteURL
addParams url newParams = url {
urlParams = unionBy ((==) `on` fst) newParams (urlParams url)
}
pathOn :: AbsoluteURL -> FilePath -> Text
pathOn url = toText . setPath
where setPath ('/':urlPath) = url {urlPath}
setPath subPath = url {urlPath = urlPath url </> subPath}
defaultOn :: AbsoluteURL -> URL -> Text
defaultOn _ (URL {url_type = Absolute host, url_path, url_params}) =
toText $ AbsoluteURL host url_path url_params
defaultOn url (URL {url_type = HostRelative, url_path, url_params}) =
toText $ addParams (url {urlPath = url_path}) url_params
defaultOn url (URL {url_path, url_params}) =
pathOn (addParams url url_params) url_path
localPrefix :: FilePath -> URL -> Text
localPrefix base url = pack . prefix (url_type url) $ exportURL url
where prefix PathRelative = (base </>)
prefix _ = id

View file

@ -1,7 +1,7 @@
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module DOM ( module DOM (
HasContent(..) PageType(..)
, htmlDocument , htmlDocument
) where ) where
@ -10,115 +10,129 @@ import qualified Article (preview)
import ArticlesList ( import ArticlesList (
ArticlesList(..), description, getArticles, otherURL, rssLinkTexts ArticlesList(..), description, getArticles, otherURL, rssLinkTexts
) )
import Blog (Blog(..), Skin(..), URL(..), template) import Blog (Blog(..), Skin(..), URLs(..), template)
import Blog.URL (localPrefix)
import qualified Collection (tag)
import Control.Monad.Reader (ReaderT, asks) import Control.Monad.Reader (ReaderT, asks)
import Data.Map as Map (Map, toList) import Data.Map as Map (Map, toList)
import Data.Text (Text, pack, empty) import Data.Text (Text, pack, empty)
import DOM.Card (HasCard) import DOM.Card (HasCard)
import qualified DOM.Card as Card (make) import qualified DOM.Card as Card (make)
import Files (absoluteLink) --import Lucid (
import Lucid ( -- Attribute, HtmlT, a_, article_, body_, charset_, class_, div_, doctypehtml_
HtmlT, a_, article_, body_, charset_, class_, div_, doctypehtml_, h1_, h2_ -- , h1_, h2_, head_, header_, href_, li_, link_, id_, meta_, pre_, rel_
, head_, header_, href_, li_, link_, id_, meta_, pre_, rel_, script_, src_ -- , script_, src_, title_, toHtml, toHtmlRaw, type_, ul_
, title_, toHtml, toHtmlRaw, type_, ul_ -- )
)
import Markdown (Markdown(..), MarkdownContent(..)) import Markdown (Markdown(..), MarkdownContent(..))
import Network.URL (URL)
import Page (Page) import Page (Page)
import Prelude hiding (head, lookup) import Prelude hiding (div, head, id, lookup)
import Pretty ((.$)) import Pretty ((.$))
import System.FilePath.Posix ((<.>)) import System.FilePath.Posix ((</>), (<.>))
import Text.Blaze.Html5 as H (
(!), Html, ToValue(..), body, div, docTypeHtml, h2, head, li, meta, preEscapedText, script, text, title, ul
)
import Text.Blaze.Html5.Attributes (
charset, class_, id, src
)
type HtmlGenerator = HtmlT (ReaderT Blog IO) type HtmlGenerator = ReaderT Blog IO Html
class HasCard a => HasContent a where class HasCard a => PageType a where
content :: a -> HtmlGenerator () content :: a -> HtmlGenerator
pathToRoot :: a -> FilePath
instance HasContent Article where instance PageType Article where
content = mDContent True . getMarkdown content = mDContent True ".." . getMarkdown
pathToRoot _ = ".."
instance HasContent Page where instance PageType Page where
content = mDContent True . getMarkdown content = mDContent True ".." . getMarkdown
pathToRoot _ = ".."
instance HasContent ArticlesList where instance PageType ArticlesList where
content al@(ArticlesList {full}) = do content al@(ArticlesList {full}) = do
preview <- Article.preview <$> (asks $skin.$previewLinesCount) preview <- Article.preview <$> (asks $skin.$previewLinesCount)
h2_ . toHtml =<< description al h2_ . text =<< description al
ul_ $ do ul_ $ do
asks hasRSS >>= rssLink asks hasRSS >>= rssLink
li_ . a_ [href_ . pack $ otherURL al, class_ "other"] =<< otherLink li_ . a_ [href_ . pack $ otherURL al, class_ "other"] =<< otherLink
div_ [class_ "articles"] ( div [class_ "articles"] (
mapM_ (mDContent False . preview) =<< getArticles al mapM_ (mDContent False (pathToRoot al) . preview) =<< getArticles al
) )
where where
otherLink = otherLink =
toHtml <$> template (if full then "latestLink" else "allLink") [] toHtml <$> template (if full then "latestLink" else "allLink") []
rssLink :: Bool -> HtmlGenerator () rssLink :: Bool -> HtmlGenerator
rssLink True = do rssLink True = do
(text, title) <- rssLinkTexts al (text, title) <- rssLinkTexts al
li_ . a_ [href_ "rss.xml", class_ "RSS", title_ title] $ toHtml text li_ . a_ [href_ "rss.xml", class_ "RSS", title_ title] $ toHtml text
rssLink False = return () rssLink False = return ()
mDContent :: Bool -> Markdown -> HtmlGenerator () pathToRoot = maybe "." (\_ -> "..") . Collection.tag . collection
mDContent raw markdown@(Markdown {key, body}) =
article_ [id_ $ pack key] (do mDContent :: Bool -> FilePath -> Markdown -> HtmlGenerator
header_ . h1_ $ mDLink raw markdown mDContent raw base markdown@(Markdown {key, Markdown.body}) =
article_ ! id (toValue key) (do
header_ . h1_ $ mDLink raw base markdown
pre_ . toHtml $ unlines body pre_ . toHtml $ unlines body
) )
mDLink :: Bool -> Markdown -> HtmlGenerator () mDLink :: Bool -> FilePath -> Markdown -> HtmlGenerator
mDLink raw (Markdown {Markdown.path, title}) = mDLink raw base (Markdown {Markdown.path, Markdown.title}) = link $ toHtml title
a_ [href_ $ pack url] $ toHtml title
where where
url = absoluteLink $ path <.> (if raw then "md" else "html") link = a_ [href_ . prefix base $ path <.> (if raw then "md" else "html")]
tag :: String -> HtmlGenerator () prefix :: FilePath -> FilePath -> Text
tag name = prefix base = pack . (base </>)
a_ [href_ . pack $ absoluteLink name ++ "/", class_ "tag"] $ toHtml name
defaultBanner :: HtmlGenerator () tag :: FilePath -> String -> HtmlGenerator
defaultBanner = tag base name =
div_ [id_ "header"] ( a_ [href_ . prefix base $ name ++ "/", class_ "tag"] $ toHtml name
a_ [href_ "/"] (
defaultBanner :: FilePath -> HtmlGenerator
defaultBanner base =
div ! id "header" $
a_ [href_ $ pack base] (
h1_ . toHtml =<< asks name h1_ . toHtml =<< asks name
) )
)
faviconLink :: FilePath -> HtmlGenerator () faviconLink :: FilePath -> URL -> HtmlGenerator
faviconLink url = link_ [ faviconLink base url = link_ [
rel_ "shortcut icon", href_ . pack $ absoluteLink url, type_ "image/x-icon" rel_ "shortcut icon", href_ $ localPrefix base url, type_ "image/x-icon"
] ]
optional :: (a -> HtmlGenerator ()) -> Maybe a -> HtmlGenerator () optional :: (a -> HtmlGenerator) -> Maybe a -> HtmlGenerator
optional = maybe (return ()) optional = maybe (return mempty)
navigationSection :: navigationSection ::
Text -> String -> ((String, a) -> HtmlGenerator ()) -> Map String a -> HtmlGenerator () Text -> String -> ((String, a) -> HtmlGenerator) -> Map String a -> HtmlGenerator
navigationSection sectionId templateKey generator collection navigationSection sectionId templateKey generator collection
| null collection = return () | null collection = return ()
| otherwise = | otherwise =
div_ [id_ sectionId, class_ "navigator"] (do div ! id sectionId ! class_ "navigator" $ do
h2_ . toHtml =<< template templateKey [] h2 . toHtml =<< template templateKey []
ul_ . mapM_ (li_ . generator) $ Map.toList collection ul $ (li . generator) <$> Map.toList collection
)
htmlDocument :: HasContent a => a -> HtmlGenerator () htmlDocument :: PageType a => a -> HtmlGenerator
htmlDocument someContent = htmlDocument someContent =
doctypehtml_ (do let base = pathToRoot someContent in
head_ (do docTypeHtml (do
meta_ [charset_ "utf-8"] H.head (do
title_ . toHtml =<< asks name meta ! charset "utf-8"
script_ [src_ "/js/remarkable.min.js"] empty H.title . text =<< asks name
script_ [src_ "/js/hablo.js"] empty script ! src (prefix base "js/remarkable.min.js") mempty
optional faviconLink =<< (asks $skin.$favicon) script ! src (prefix base "js/hablo.js") mempty
optional (faviconLink base) =<< (asks $skin.$favicon)
optional (Card.make someContent) =<< (asks $urls.$cards) optional (Card.make someContent) =<< (asks $urls.$cards)
optional toHtmlRaw =<< (asks $skin.$head) optional preEscapedText =<< (asks $skin.$Blog.head)
) )
body_ (do H.body (do
maybe defaultBanner toHtmlRaw =<< (asks $skin.$banner) maybe (defaultBanner base) preEscapedText =<< (asks $skin.$banner)
asks tags >>= navigationSection "tags" "tagsList" asks tags >>= navigationSection "tags" "tagsList"
(\(key, _) -> tag key) (\(key, _) -> tag base key)
asks pages >>= navigationSection "pages" "pagesList" asks pages >>= navigationSection "pages" "pagesList"
(\(_, page) -> mDLink False $ getMarkdown page) (\(_, page) -> mDLink False base $ getMarkdown page)
div_ [id_ "contents"] $ content someContent div ! id "contents" $ content someContent
) )
) )

View file

@ -10,48 +10,50 @@ import Article (Article(..))
import ArticlesList (ArticlesList(..)) import ArticlesList (ArticlesList(..))
import qualified ArticlesList (description) import qualified ArticlesList (description)
import Blog (Blog(..), Renderer, Skin(..), template) import Blog (Blog(..), Renderer, Skin(..), template)
import Blog.URL (AbsoluteURL, checkURL, defaultOn, pathOn)
import Collection (Collection(..)) import Collection (Collection(..))
import qualified Collection (title) import qualified Collection (title)
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Monad.Reader (asks) import Control.Monad.Reader (asks)
import qualified Data.Map as Map (lookup) import qualified Data.Map as Map (lookup)
import Data.Text (Text, pack) import Data.Text (Text, pack)
import Lucid (HtmlT, content_, meta_) --import Lucid (HtmlT, content_, meta_)
import Lucid.Base (makeAttribute) --import Lucid.Base (makeAttribute)
import Markdown (MarkdownContent(..), metadata) import Markdown (MarkdownContent(..), metadata)
import Network.URL (URL)
import qualified Markdown (Markdown(..)) import qualified Markdown (Markdown(..))
import Page (Page(..)) import Page (Page(..))
import Pretty ((.$)) import Pretty ((.$))
import System.FilePath.Posix ((</>), (<.>)) import System.FilePath.Posix ((</>), (<.>))
import Text.Blaze.Html5 ((!), AttributeValue, Html, ToValue(..), meta, customAttribute)
import Text.Blaze.Html5.Attributes as A (content)
class HasCard a where class HasCard a where
cardType :: Renderer m => a -> m Text cardType :: Renderer m => a -> m Text
description :: Renderer m => a -> m Text description :: Renderer m => a -> m Text
image :: Renderer m => a -> m (Maybe String) image :: Renderer m => a -> m (Maybe URL)
title :: Renderer m => a -> m String title :: Renderer m => a -> m String
urlPath :: Renderer m => a -> m String urlPath :: Renderer m => a -> m String
og :: Applicative m => Text -> Text -> HtmlT m () og :: Applicative m => AttributeValue -> Text -> m Html
og attribute value = og attribute t =
meta_ [ pure $ meta
makeAttribute "property" $ "og:" <> attribute ! (customAttribute "property" $ "og:" <> attribute)
, content_ value ! content (toValue t)
]
make :: (HasCard a, Renderer m) => a -> String -> HtmlT m () make :: (HasCard a, Renderer m) => a -> AbsoluteURL -> m Html
make element siteURL = do make element siteURL = do
og "url" . sitePrefix =<< urlPath element og "url" . (pathOn siteURL) =<< urlPath element
og "type" =<< cardType element og "type" =<< cardType element
og "title" . pack =<< title element og "title" . pack =<< title element
og "description" =<< description element og "description" =<< description element
maybeImage =<< ((<|>) <$> image element <*> (asks $skin.$cardImage)) maybeImage =<< ((<|>) <$> image element <*> (asks $skin.$cardImage))
og "site_name" =<< (asks $name.$pack) og "site_name" =<< (asks $name.$pack)
where where
maybeImage = maybe (return ()) (og "image" . sitePrefix) maybeImage = maybe (return mempty) (og "image" . defaultOn siteURL)
sitePrefix = pack . (siteURL </>)
mDImage :: (Renderer m, MarkdownContent a ) => a -> m (Maybe String) mDImage :: (Renderer m, MarkdownContent a) => a -> m (Maybe URL)
mDImage = return . Map.lookup "featuredImage" . metadata . getMarkdown mDImage = mapM checkURL . Map.lookup "featuredImage" . metadata . getMarkdown
mDTitle :: (Renderer m, MarkdownContent a) => a -> m String mDTitle :: (Renderer m, MarkdownContent a) => a -> m String
mDTitle = return . Markdown.title . getMarkdown mDTitle = return . Markdown.title . getMarkdown

View file

@ -1,6 +1,5 @@
module Files ( module Files (
File(..) File(..)
, absoluteLink
, filePath , filePath
, find , find
) where ) where
@ -10,10 +9,6 @@ import System.FilePath ((</>))
data File = File FilePath | Dir FilePath data File = File FilePath | Dir FilePath
absoluteLink :: FilePath -> FilePath
absoluteLink ('.':path) = path
absoluteLink path = "/" </> path
filePath :: File -> IO (Either String FilePath) filePath :: File -> IO (Either String FilePath)
filePath = filePathAux filePath = filePathAux
where where

View file

@ -12,7 +12,7 @@ import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Reader (ReaderT, asks) import Control.Monad.Reader (ReaderT, asks)
import qualified Data.Map as Map (elems) import qualified Data.Map as Map (elems)
import qualified Data.Text.Lazy.IO as TextIO (writeFile) import qualified Data.Text.Lazy.IO as TextIO (writeFile)
import DOM (HasContent, htmlDocument) import DOM (PageType, htmlDocument)
import Lucid (renderTextT) import Lucid (renderTextT)
import Markdown (Markdown(..), MarkdownContent(..)) import Markdown (Markdown(..), MarkdownContent(..))
import Pretty ((.$)) import Pretty ((.$))
@ -26,7 +26,7 @@ articlesLists collection@(Collection {basePath}) = [
file bool = if bool then "all" else "index" file bool = if bool then "all" else "index"
path bool = basePath </> file bool <.> "html" path bool = basePath </> file bool <.> "html"
generateMarkdown :: (HasContent a, MarkdownContent a) => [a] -> ReaderT Blog IO () generateMarkdown :: (PageType a, MarkdownContent a) => [a] -> ReaderT Blog IO ()
generateMarkdown = mapM_ $ \content -> do generateMarkdown = mapM_ $ \content -> do
let relativePath = Markdown.path (getMarkdown content) <.> "html" let relativePath = Markdown.path (getMarkdown content) <.> "html"
filePath <- (</> relativePath) <$> (asks $Blog.path.$root) filePath <- (</> relativePath) <$> (asks $Blog.path.$root)

View file

@ -16,7 +16,7 @@ import Data.Text.Encoding (encodeUtf8)
import JSON (exportBlog) import JSON (exportBlog)
import Paths_hablo (getDataDir) import Paths_hablo (getDataDir)
import Pretty ((.$)) import Pretty ((.$))
import SJW (compile, source) import SJW (compile, source, sourceCode)
import System.Directory (createDirectoryIfMissing) import System.Directory (createDirectoryIfMissing)
import System.Exit (die) import System.Exit (die)
import System.FilePath ((</>)) import System.FilePath ((</>))
@ -45,11 +45,10 @@ generateConfig destinationDir = do
generateMain :: FilePath -> IO () generateMain :: FilePath -> IO ()
generateMain destinationDir = do generateMain destinationDir = do
habloSources <- (</> "js") <$> getDataDir habloSources <- (</> "js") <$> getDataDir
compile (source [destinationDir, "unitJS", habloSources]) result <- compile $ source [destinationDir, "unitJS", habloSources]
>>= either abort (output . fst) maybe (die "JS compilation failed\n") output =<< sourceCode result
where where
output = writeFile (destinationDir </> "hablo.js") . fromStrict . encodeUtf8 output = writeFile (destinationDir </> "hablo.js") . fromStrict . encodeUtf8
abort = die . (<> "JS compilation failed\n")
generate :: ReaderT Blog IO () generate :: ReaderT Blog IO ()
generate = do generate = do

View file

@ -4,7 +4,7 @@ module JSON (
exportBlog exportBlog
) where ) where
import Blog (Blog, Path, Skin, URL, Wording) import Blog (Blog, Path, Skin, URLs, Wording)
import qualified Blog (Blog(..)) import qualified Blog (Blog(..))
import Data.Aeson (Options(..), ToJSON(..), genericToEncoding, defaultOptions) import Data.Aeson (Options(..), ToJSON(..), genericToEncoding, defaultOptions)
import Data.Map (Map, mapWithKey) import Data.Map (Map, mapWithKey)
@ -39,7 +39,7 @@ data BlogExport = BlogExport {
, pages :: Map String MarkdownExport , pages :: Map String MarkdownExport
, skin :: Skin , skin :: Skin
, tags :: Map String [String] , tags :: Map String [String]
, urls :: URL , urls :: URLs
, wording :: Wording , wording :: Wording
} deriving (Generic) } deriving (Generic)

View file

@ -7,24 +7,26 @@ module RSS (
import Article (Article(..)) import Article (Article(..))
import ArticlesList (ArticlesList(..), getArticles) import ArticlesList (ArticlesList(..), getArticles)
import qualified ArticlesList (description) import qualified ArticlesList (description, path)
import Blog (Blog(urls), Renderer, URL(..)) import Blog (Blog(urls), Renderer, URLs(..))
import Blog.URL (AbsoluteURL, pathOn)
import Collection (Collection(..), getAll) import Collection (Collection(..), getAll)
import qualified Collection (title) import qualified Collection (title)
import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Reader (MonadReader, ReaderT, asks) import Control.Monad.Reader (ReaderT, asks)
import Data.Text (Text) import Data.Text (Text)
import Data.Map ((!)) import Data.Map ((!))
import qualified Data.Text.Lazy.IO as TextIO (writeFile) import qualified Data.Text.Lazy.IO as TextIO (writeFile)
import Data.Time (defaultTimeLocale, formatTime, rfc822DateFormat) import Data.Time (defaultTimeLocale, formatTime, rfc822DateFormat)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Lucid (Attribute, HtmlT, Term, ToHtml(..), term, renderTextT) --import Lucid (Attribute, HtmlT, Term, ToHtml(..), term, renderTextT)
import Lucid.Base (makeAttribute) import Lucid.Base (makeAttribute)
import Markdown (Markdown(..)) import Markdown (Markdown(..))
import Pretty ((.$)) import Pretty ((.$))
import System.FilePath.Posix ((</>), (<.>)) import System.FilePath.Posix ((</>), (<.>))
import Text.Blaze.Html (Html)
prolog :: Monad m => HtmlT m () prolog :: Monad m => m Html
prolog = toHtmlRaw ("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" :: String) prolog = toHtmlRaw ("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" :: String)
version_ :: Text -> Attribute version_ :: Text -> Attribute
@ -57,24 +59,24 @@ item_ = term "item"
pubDate_ :: Term arg result => arg -> result pubDate_ :: Term arg result => arg -> result
pubDate_ = term "pubDate" pubDate_ = term "pubDate"
articleItem :: MonadReader Blog m => String -> Article -> HtmlT m () articleItem :: Monad m => AbsoluteURL -> Article -> m Html
articleItem siteURL (Article (Markdown {path, metadata, title})) = articleItem siteURL (Article (Markdown {path, metadata, title})) =
item_ $ do item_ $ do
title_ $ toHtml title title_ $ toHtml title
link_ $ toHtml (siteURL </> path <.> "html") link_ . toHtml $ pathOn siteURL (path <.> "html")
pubDate_ . toHtml . rfc822Date $ metadata ! "date" pubDate_ . toHtml . rfc822Date $ metadata ! "date"
where where
rfc822Date = rfc822Date =
formatTime defaultTimeLocale rfc822DateFormat formatTime defaultTimeLocale rfc822DateFormat
. posixSecondsToUTCTime . fromIntegral . (read :: String -> Int) . posixSecondsToUTCTime . fromIntegral . (read :: String -> Int)
feed :: Renderer m => String -> ArticlesList -> HtmlT m () feed :: Renderer m => AbsoluteURL -> ArticlesList -> m Html
feed siteURL al@(ArticlesList {collection}) = do feed siteURL al@(ArticlesList {collection}) = do
prolog prolog
rss_ [version, content, atom] $ do rss_ [version, content, atom] $ do
channel_ $ do channel_ $ do
title_ . toHtml =<< Collection.title collection title_ . toHtml =<< Collection.title collection
link_ . toHtml $ siteURL </> maybe "" (++ "/") (tag collection) link_ . toHtml . pathOn siteURL $ ArticlesList.path al
description_ . toHtml =<< ArticlesList.description al description_ . toHtml =<< ArticlesList.description al
mapM_ (articleItem siteURL) =<< getArticles al mapM_ (articleItem siteURL) =<< getArticles al
where where
@ -82,7 +84,7 @@ feed siteURL al@(ArticlesList {collection}) = do
content = xmlns_content_ "http://purl.org/rss/1.0/modules/content/" content = xmlns_content_ "http://purl.org/rss/1.0/modules/content/"
atom = xmlns_atom_ "http://www.w3.org/2005/Atom" atom = xmlns_atom_ "http://www.w3.org/2005/Atom"
generateCollection :: String -> Collection -> ReaderT Blog IO () generateCollection :: AbsoluteURL -> Collection -> ReaderT Blog IO ()
generateCollection siteURL collection = generateCollection siteURL collection =
renderTextT (feed siteURL $ ArticlesList {full = False, collection}) renderTextT (feed siteURL $ ArticlesList {full = False, collection})
>>= liftIO . TextIO.writeFile (basePath collection </> "rss" <.> "xml") >>= liftIO . TextIO.writeFile (basePath collection </> "rss" <.> "xml")

View file

@ -1,12 +1,14 @@
module Mock.Article ( module Mock.Article (
noDescription hostRelativeImage
, noDescription
, noImage , noImage
, noMeta , noMeta
, remoteImage
, simple , simple
) where ) where
import Article (Article(..)) import Article (Article(..))
import qualified Data.Map as Map (fromList) import qualified Data.Map as Map (fromList, insert)
import Markdown (Markdown(..)) import Markdown (Markdown(..))
import Mock.Markdown (article) import Mock.Markdown (article)
@ -16,6 +18,16 @@ simple = Article article
noImage :: Article noImage :: Article
noImage = Article $ article {metadata = Map.fromList [("summary", "It's a test")]} noImage = Article $ article {metadata = Map.fromList [("summary", "It's a test")]}
remoteImage :: Article
remoteImage = Article $ article {metadata = setImage $ metadata article}
where
setImage = Map.insert "featuredImage" "https://somewhere.el.se/test.png"
hostRelativeImage :: Article
hostRelativeImage = Article $ article {metadata = setImage $ metadata article}
where
setImage = Map.insert "featuredImage" "/media/test.png"
noDescription :: Article noDescription :: Article
noDescription = Article $ article {metadata = Map.fromList [("featuredImage", "test.png")]} noDescription = Article $ article {metadata = Map.fromList [("featuredImage", "test.png")]}

View file

@ -3,6 +3,7 @@ module Mock.Blog (
noCards noCards
, noRSS , noRSS
, simple , simple
, subPath
) where ) where
import Blog (Blog(..)) import Blog (Blog(..))
@ -12,7 +13,7 @@ import qualified Mock.Article (simple)
import qualified Mock.Blog.Path (defaultArticles) import qualified Mock.Blog.Path (defaultArticles)
import qualified Mock.Blog.Skin (simple) import qualified Mock.Blog.Skin (simple)
import qualified Mock.Blog.Template (simple) import qualified Mock.Blog.Template (simple)
import qualified Mock.Blog.URL (simple, noCards) import qualified Mock.Blog.URL (simple, subPath, noCards)
import qualified Mock.Blog.Wording (defaultWording) import qualified Mock.Blog.Wording (defaultWording)
simple :: IO Blog simple :: IO Blog
@ -32,6 +33,9 @@ simple =
, wording , wording
} }
subPath :: IO Blog
subPath = (\b -> b {urls = Mock.Blog.URL.subPath}) <$> simple
noCards :: IO Blog noCards :: IO Blog
noCards = (\b -> b {urls = Mock.Blog.URL.noCards}) <$> simple noCards = (\b -> b {urls = Mock.Blog.URL.noCards}) <$> simple

View file

@ -1,16 +1,25 @@
module Mock.Blog.URL ( module Mock.Blog.URL (
noCards noCards
, simple , simple
, subPath
) where ) where
import Blog.URL (URL(..)) import Blog.URL (URLs(..))
import Mock.URL (prefixedTestSite, testSite)
simple :: URL simple :: URLs
simple = URL { simple = URLs {
cards = Just "https://test.net" cards = Just testSite
, comments = Nothing , comments = Nothing
, rss = Nothing , rss = Nothing
} }
noCards :: URL subPath :: URLs
subPath = URLs {
cards = Just prefixedTestSite
, comments = Nothing
, rss = Nothing
}
noCards :: URLs
noCards = simple {cards = Nothing} noCards = simple {cards = Nothing}

60
test/Mock/URL.hs Normal file
View file

@ -0,0 +1,60 @@
module Mock.URL (
cdnFavicon
, hostFavicon
, localDiscovered
, localFavicon
, prefixedTestSite
, testHost
, testSite
) where
import Blog.URL (AbsoluteURL(..))
import Network.URL (Host(..), Protocol(..), URL(..), URLType(..))
testHost :: Host
testHost = Host (HTTP True) "test.net" Nothing
testSite :: AbsoluteURL
testSite = AbsoluteURL {
Blog.URL.host = testHost
, urlPath = ""
, urlParams = []
}
prefixedTestSite :: AbsoluteURL
prefixedTestSite = AbsoluteURL {
Blog.URL.host = testHost
, urlPath = "subPath"
, urlParams = []
}
cdn :: Host
cdn = Host (HTTP True) "cdn.net" Nothing
cdnFavicon :: URL
cdnFavicon = URL {
url_type = Absolute cdn
, url_path = "favicon.png"
, url_params = []
}
hostFavicon :: URL
hostFavicon = URL {
url_type = HostRelative
, url_path = "favicon.png"
, url_params = []
}
localFavicon :: URL
localFavicon = URL {
url_type = PathRelative
, url_path = "favicon.png"
, url_params = []
}
localDiscovered :: URL
localDiscovered = URL {
url_type = PathRelative
, url_path = "skin/favicon.png"
, url_params = []
}

View file

@ -4,11 +4,15 @@ module Tests (
import Distribution.TestSuite import Distribution.TestSuite
import qualified Structure (test) import qualified Structure (test)
import qualified URLs (test)
import Utils (tag) import Utils (tag)
import qualified XML.Card (test) import qualified XML.Card (test)
import qualified XML.Favicon (test)
tests :: IO [Test] tests :: IO [Test]
tests = return $ tag "xml" <$> [ tests = return $ tag "xml" <$> [
XML.Card.test XML.Card.test
, XML.Favicon.test
, Structure.test , Structure.test
, URLs.test
] ]

73
test/URLs.hs Normal file
View file

@ -0,0 +1,73 @@
{-# LANGUAGE OverloadedStrings #-}
module URLs (
test
) where
import Blog.URL (defaultOn, pathOn, localPrefix)
import Data.Text (Text)
import Distribution.TestSuite
import Mock.URL (
cdnFavicon, hostFavicon, localDiscovered, localFavicon, prefixedTestSite
, testSite
)
import Utils (simpleTest, tag)
check :: Text -> Text -> IO Progress
check actual expected =
return . Finished $
if actual == expected
then Pass
else Fail $ "Expected " ++ show expected ++ " but got " ++ show actual
testPathOn :: Test
testPathOn = tag "pathOn" . testGroup "Concat path" $ simpleTest <$> [
("no prefix / relative path",
check (pathOn testSite "tag/all.html") "https://test.net/tag/all.html")
, ("no prefix / absolute path",
check
(pathOn testSite "/media/logo.png")
"https://test.net/media/logo.png")
, ("prefixed / relative path",
check
(pathOn prefixedTestSite "tag/all.html")
"https://test.net/subPath/tag/all.html")
, ("prefixed / absolute path ",
check
(pathOn prefixedTestSite "/media/logo.png")
"https://test.net/media/logo.png")
]
testDefaultOn :: Test
testDefaultOn = tag "defaultOn" . testGroup "Concat path or pick URL" $ simpleTest <$> [
("external resource", -- shouldn't even touch the first argument when the second is an absolute URL
check (defaultOn undefined cdnFavicon) "https://cdn.net/favicon.png")
, ("host relative",
check (defaultOn testSite hostFavicon) "https://test.net/favicon.png")
, ("host relative / prefix",
check
(defaultOn prefixedTestSite hostFavicon)
"https://test.net/favicon.png")
, ("relative",
check
(defaultOn testSite localDiscovered)
"https://test.net/skin/favicon.png")
, ("relative / prefix",
check
(defaultOn prefixedTestSite localDiscovered)
"https://test.net/subPath/skin/favicon.png")
]
testLocalPrefix :: Test
testLocalPrefix = tag "localPrefix" . testGroup "Prefixed URLs" $ simpleTest <$> [
("absolute URL", check (localPrefix ".." cdnFavicon) "https://cdn.net/favicon.png")
, ("host relative", check (localPrefix ".." hostFavicon) "/favicon.png")
, ("relative path", check (localPrefix ".." localDiscovered) "../skin/favicon.png")
, ("relative path no prefix", check (localPrefix "" localFavicon) "favicon.png")
]
test :: Test
test = tag "URLs" $ testGroup "URLs handling" [
testPathOn
, testDefaultOn
, testLocalPrefix
]

View file

@ -4,6 +4,7 @@ module XML.Card.Component (
) where ) where
import Blog (Blog) import Blog (Blog)
import Blog.URL (pathRelative)
import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Reader (runReaderT) import Control.Monad.Reader (runReaderT)
import Data.Text (Text) import Data.Text (Text)
@ -14,9 +15,10 @@ import Mock.Article as Article (noDescription, noImage, simple)
import Mock.ArticlesList as ArticlesList ( import Mock.ArticlesList as ArticlesList (
longMain, longTesting, shortMain, shortTesting longMain, longTesting, shortMain, shortTesting
) )
import Network.URL (URL)
import Utils (assertAll, assertEqual, simpleTest, tag) import Utils (assertAll, assertEqual, simpleTest, tag)
check :: HasCard a => IO Blog -> a -> (Text, Text, Maybe String, String, String) -> IO Progress check :: HasCard a => IO Blog -> a -> (Text, Text, Maybe URL, String, String) -> IO Progress
check getBlog input (expectedCT, expectedD, expectedI, expectedT, expectedU) = check getBlog input (expectedCT, expectedD, expectedI, expectedT, expectedU) =
getBlog >>= runReaderT ( getBlog >>= runReaderT (
sequence [ sequence [
@ -33,14 +35,14 @@ articleCard = tag "article" . testGroup "Article cards" $ simpleTest <$> [
("simple article components", check Blog.simple Article.simple ( ("simple article components", check Blog.simple Article.simple (
"article" "article"
, "It's a test" , "It's a test"
, Just "test.png" , Just (pathRelative "test.png")
, "Some test" , "Some test"
, "articles/test.html" , "articles/test.html"
)) ))
, ("article components without description", check Blog.simple Article.noDescription ( , ("article components without description", check Blog.simple Article.noDescription (
"article" "article"
, "A new article on The Test Blog" , "A new article on The Test Blog"
, Just "test.png" , Just (pathRelative "test.png")
, "Some test" , "Some test"
, "articles/test.html" , "articles/test.html"
)) ))

View file

@ -2,15 +2,17 @@ module XML.Card.Output (
test test
) where ) where
import Blog (Blog(..), URL(..)) import Blog (Blog(..), URLs(..))
import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.Reader (asks, runReaderT) import Control.Monad.Reader (asks, runReaderT)
import qualified Data.Text.Lazy.IO as Lazy (readFile) import qualified Data.Text.Lazy.IO as Lazy (readFile)
import Distribution.TestSuite import Distribution.TestSuite
import DOM.Card (HasCard(..), make) import DOM.Card (HasCard(..), make)
import Lucid (renderTextT) import Lucid (renderTextT)
import Mock.Blog as Blog (noCards, simple) import Mock.Blog as Blog (noCards, simple, subPath)
import Mock.Article as Article (noDescription, noImage, simple) import Mock.Article as Article (
hostRelativeImage, noDescription, noImage, remoteImage, simple
)
import Mock.ArticlesList as ArticlesList ( import Mock.ArticlesList as ArticlesList (
longMain, longTesting, shortMain, shortTesting longMain, longTesting, shortMain, shortTesting
) )
@ -31,8 +33,15 @@ check getBlog input expectedFile =
articleCard :: Test articleCard :: Test
articleCard = tag "article" . testGroup "Article cards" $ simpleTest <$> [ articleCard = tag "article" . testGroup "Article cards" $ simpleTest <$> [
("simple article output", check Blog.simple Article.simple "simple.html") ("simple article output", check Blog.simple Article.simple "simple.html")
, ("article output without description", check Blog.simple Article.noDescription "noDescription.html") , ("simple article output with subPath", check Blog.subPath Article.simple "subPath.html")
, ("article output without image", check Blog.simple Article.noImage "noImage.html") , ("article output without description"
, check Blog.simple Article.noDescription "noDescription.html")
, ("article output without image"
, check Blog.simple Article.noImage "noImage.html")
, ("article output with an image on a remote server"
, check Blog.simple Article.remoteImage "remoteImage.html")
, ("article output with an image in a host-relative folder"
, check Blog.simple Article.hostRelativeImage "hostRelativeImage.html")
, ("no card article output", check Blog.noCards Article.simple "/dev/null") , ("no card article output", check Blog.noCards Article.simple "/dev/null")
] ]

View file

@ -0,0 +1 @@
<meta property="og:url" content="https://test.net/articles/test.html"><meta property="og:type" content="article"><meta property="og:title" content="Some test"><meta property="og:description" content="It&#39;s a test"><meta property="og:image" content="https://test.net/media/test.png"><meta property="og:site_name" content="The Test Blog">

View file

@ -0,0 +1 @@
<meta property="og:url" content="https://test.net/articles/test.html"><meta property="og:type" content="article"><meta property="og:title" content="Some test"><meta property="og:description" content="It&#39;s a test"><meta property="og:image" content="https://somewhere.el.se/test.png"><meta property="og:site_name" content="The Test Blog">

View file

@ -0,0 +1 @@
<meta property="og:url" content="https://test.net/subPath/articles/test.html"><meta property="og:type" content="article"><meta property="og:title" content="Some test"><meta property="og:description" content="It&#39;s a test"><meta property="og:image" content="https://test.net/subPath/test.png"><meta property="og:site_name" content="The Test Blog">

27
test/XML/Favicon.hs Normal file
View file

@ -0,0 +1,27 @@
module XML.Favicon (
test
) where
import Blog.Skin (findImage)
import Distribution.TestSuite
import Mock.URL (cdnFavicon, hostFavicon, localDiscovered, localFavicon)
import Network.URL (URL)
import System.Directory (withCurrentDirectory)
import Utils (assertAll, assertEqual, simpleTest, tag, testDataPath)
check :: IO (Maybe URL) -> Maybe URL -> IO Progress
check getter expected =
withCurrentDirectory (testDataPath "XML/Favicon/Input") $ do
actual <- getter
assertAll $ [
assertEqual "URLs" actual expected
]
test :: Test
test = tag "favicon" . testGroup "Favicons" $ simpleTest <$> [
("auto-discover", check (findImage "favicon" Nothing) (Just localDiscovered))
, ("none", check (findImage "blerp" Nothing) Nothing)
, ("manual absolute", check (findImage "" (Just "https://cdn.net/favicon.png")) (Just cdnFavicon))
, ("manual host-relative", check (findImage "" (Just "/favicon.png")) (Just hostFavicon))
, ("manual relative", check (findImage "" (Just "favicon.png")) (Just localFavicon))
]

View file