QXUUTPJOCPVNEEUOVRPBXDYBZ2W4RZM5NKK6C5PU6ZCYTYTSY5KQC module Visual (build, Image(..)) whereimport Data.Time (UTCTime)import Data.List (isSuffixOf)import qualified Configimport Commonimport Templates (outerWith, loading_)import Lucidimport RoutethumbWidth :: IntthumbWidth = 710data Image = Image{ imgPath :: FilePath, imgThumbPath :: FilePath, imgThumbWidth :: Int, imgThumbHeight :: Int, imgDate :: UTCTime, imgNSFW :: Bool -- is the image nsfw?} deriving (Generic, Eq, Binary)data EntryMeta = EntryMeta{ title :: Text, date :: Text, updated :: Maybe Text} deriving (Generic, Eq, FromJSON)data Entry = Entry{ entryTitle :: Text, entryContent :: Text, entryItems :: [Image], entryDate :: UTCTime, entryType :: Text} deriving (Generic, Eq, Binary)instance IsTimestamped Image where timestamp = imgDateinstance IsTimestamped Entry where timestamp = entryDatebuild :: Task IO [Image]build = dopictures <- match "visual/*" \src -> dopath <- copyFile srctpath <- callCommandWith (\a b -> "convert -resize 710x " <> a <> " " <> b)(-<.> "thumb.webp")srcapath <- toAbsolute srcsize <- read <$> readCommand "identify" ["-ping", "-format", "(%w, %h)", apath]let nsfw = "nsfw" `isSuffixOf` dropExtension srcpure (Image path tpath thumbWidth (thumbHeight size) (timestamp src) nsfw)-- webcomics/albumsentries <- matchDir "visual/*/" \src -> domatchFile "index.markdown" \src -> do(meta, doc) <- readPandocMetadata srcrenderPandoc doc<&> renderEntry meta>>= write (src -<.> "html")pure ()watch pictures dolet sorted = filter (not . imgNSFW) $ recentFirst picturesmatch_ "./visual.rst" \src -> dointro <- compilePandoc srcwrite "visual.html" $ renderVisual intro sortedreturn (take 4 sorted)wherethumbHeight :: (Int, Int) -> IntthumbHeight (width, height) = round (fi height * fi thumbWidth / fi width)fi :: Int -> Floatfi = fromIntegralrenderEntry :: EntryMeta -> Text -> Html ()renderEntry meta content =outerWith def { Config.title = title meta, Config.route = VEntryRoute} doheader_ doh1_ $ toHtml (title meta)p_ $ toHtml (date meta)toHtmlRaw contenthr_ []renderVisual :: Text -> [Image] -> Html ()renderVisual txt imgs =outerWith def {Config.title = "visual"} dotoHtmlRaw txthr_ []section_ [class_ "visual"] $forM_ imgs \Image{..} ->figure_ $ a_ [href_ $ fromString imgPath] $ img_[ src_ (fromString imgThumbPath), width_ (fromString $ show imgThumbWidth), height_ (fromString $ show imgThumbHeight), loading_ "lazy" ]
module Types whereimport Commondata TitledPage = TitledPage{ title :: Text, description :: Maybe Text} deriving (Generic, Eq, FromJSON, Binary)
{-# LANGUAGE DuplicateRecordFields #-}{-# LANGUAGE DisambiguateRecordFields #-}{-# LANGUAGE TypeSynonymInstances #-}{-# LANGUAGE FlexibleInstances #-}{-# LANGUAGE MultiParamTypeClasses #-}module Templates whereimport Data.Time (UTCTime)import Data.Time.Format (formatTime, defaultTimeLocale)import Data.Time.LocalTime (zonedTimeToUTC)import qualified Data.Map.Strict as Mapimport Achille.Internal.IO (AchilleIO)import Achille.Writable as Writableimport Lucidimport Lucid.Base (makeAttribute)import Routeimport Typesimport Commonimport Configinstance AchilleIO m => Writable m (Html a) wherewrite to = Writable.write to . renderBSshowDate :: UTCTime -> StringshowDate = formatTime defaultTimeLocale "%b %d, %_Y"loading_ :: Text -> Attributeloading_ = makeAttribute "loading"property_ :: Text -> Attributeproperty_ = makeAttribute "property"toLink :: FilePath -> Html () -> Html ()toLink url = a_ [ href_ (fromString $ "/" <> url) ]logo :: Html ()logo = toHtmlRaw ("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" height=\"19px\" width=\"29px\"><path d=\"M 2,2 A 5,5 0 0 1 7,7 L 7, 12 A 5, 5 0 0 1 2,17 M 7,7 A 5,5 0 0 1 12,2 L 22,2 A 5,5 0 0 1 27,7 L 27,12 A 5, 5 0 0 1 22,17 L 12,17\" style=\"stroke-width: 2; stroke-linecap: butt; stroke-linejoin: bevel; stroke: #fff\" fill=\"none\"/></svg>" :: Text)outer :: Html () -> Html ()outer = outerWith defouterWith :: SiteConfig -> Html () -> Html ()outerWith SiteConfig{title,route,..} content = doctypehtml_ dohead_ dometa_ [ name_ "viewport", content_ "width=device-width, initial-scale=1.0, user-scalable=yes"]meta_ [ name_ "theme-color", content_ "#000000" ]meta_ [ name_ "robots", content_ "index, follow" ]meta_ [ charset_ "utf-8" ]link_ [ rel_ "stylesheet", href_ "/assets/theme.css" ]link_ [ rel_ "shortcut icon", type_ "image/svg", href_ "/assets/favicon.svg"]link_ [ rel_ "alternate", type_ "application/atom+xml", href_ "/atom.xml"]meta_ [ property_ "og:title", content_ title ]meta_ [ property_ "og:type", content_ "website" ]meta_ [ property_ "og:image", content_ image ]meta_ [ property_ "og:description", content_ description ]title_ $ toHtml titlebody_ doheader_ [ id_ "hd" ] $ section_ doa_ [ href_ "/" ] $ logosection_ $ nav_ doa_ [ href_ "/projects.html" ] "Projects"a_ [ href_ "/visual.html" ] "Visual"a_ [ href_ "/readings.html" ] "Readings"a_ [ href_ "/quid.html" ] "Quid"breadcrumb routemain_ contentfooter_ [ id_ "ft" ] do"flupe 2020 · "a_ [ href_ "https://creativecommons.org/licenses/by-nc/2.0/" ]"CC BY-NC 2.0"" · "a_ [ href_ "https://instagram.com/ba.bou.m/", rel_ "me" ] "instagram"" · "a_ [ href_ "/atom.xml" ] "feed"
module Route (Route(..), link, breadcrumb) whereimport Commonimport Lucidimport Data.List (foldl')data Route= IndexRoute| VisualRoute| PostRoute| ProjectsRoute| ProjectRoute Text| ProjectPageRoute Text Route| VEntryRoutelink :: Route -> Html ()link route = a_ [href_ (path route)] (toHtml $ name route)wherepath IndexRoute = "/"path ProjectsRoute = "/projects.html"path VisualRoute = "/visual.html"path PostRoute = "/"path (ProjectRoute _) = "/projects/"path (ProjectPageRoute _ _) = "/"path VEntryRoute = "/"name IndexRoute = "index"name ProjectsRoute = "projects"name VisualRoute = "visual"name PostRoute = "post"name (ProjectRoute n) = nname (ProjectPageRoute n r) = nwalk :: Route -> [Route]walk IndexRoute = []walk VisualRoute = []walk PostRoute = [IndexRoute]walk ProjectsRoute = []walk (ProjectRoute _) = [ProjectsRoute]walk (ProjectPageRoute _ r) = walk r ++ [r]walk (VEntryRoute) = [VisualRoute]breadcrumb :: Route -> Html ()breadcrumb route =case walk route of[] -> memptyxs -> p_ [class_ "breadcrumb"] $foldl' (\b r -> b <> sep <> link r) "∅" xswhere sep = span_ [class_ "sep"] "←"
module Readings (build) whereimport qualified Data.Yaml as Yamlimport Lucidimport Commonimport Configimport Templatesdata Book = Book{ title :: Text, author :: Text, rating :: Maybe Int} deriving (Generic, Show, FromJSON)build :: Task IO FilePathbuild = matchFile "readings.yaml" \p ->readBS p>>= (liftIO . Yaml.decodeThrow)<&> renderReadings>>= write (p -<.> "html")renderReadings :: [Book] -> Html ()renderReadings books =outerWith def { Config.title = "readings", Config.description = "books I've read"} dotable_ [ class_ "books" ] $forM_ books \Book {title, author, rating} ->tr_ dotd_ (toHtml title)td_ (toHtml author)td_ (toHtml $ fromMaybe "." $ flip replicate '★' <$> rating)
module Projects (build) whereimport Lucidimport Data.Char (digitToInt)import qualified Data.Map.Strict as Mapimport Commonimport Routeimport Typesimport Configimport Templatesdata Project = Project{ title :: Text, subtitle :: Text, year :: Text, labels :: Map.Map Text Text} deriving (Generic, Eq, FromJSON, Binary)build :: Task IO ()build = doprojects <- matchDir "projects/*/" buildProjectwatch projects $ match_ "./projects.rst" \src -> dointro <- compilePandocWith def wopts srcwrite "projects.html" (renderIndex intro projects)buildProject :: FilePath -> Task IO (Project, FilePath)buildProject src = domatch "*" copyFilename <- takeBaseName <$> getCurrentDirchildren <- buildChildren namewatch children $ matchFile "index.*" \src -> do(meta, doc) <- readPandocMetadataWith ropts srcrenderPandocWith wopts doc<&> renderProject meta children>>= write (src -<.> "html")(meta,) <$> getCurrentDirwherebuildChildren :: String -> Task IO [(Text, FilePath)]buildChildren name = match "pages/*" \filepath -> dolet (key, file) = getKey $ takeFileName filepath(TitledPage title _, doc) <- readPandocMetadataWith ropts filepathrenderPandocWith wopts doc<&> toHtmlRaw<&> outerWith (def { Config.title = title, Config.route = ProjectPageRoute title (ProjectRoute $ fromString name)})>>= write (filepath -<.> "html")<&> (title,)renderProject :: Project -> [(Text, FilePath)] -> Text -> Html ()renderProject Project{..} children content =outerWith def { Config.title = title, Config.description = subtitle, Config.route = ProjectRoute title} doheader_ [class_ "project"] dodiv_ (img_ [src_ "logo.svg"])div_ doh1_ (toHtml title)p_ (toHtml subtitle)ul_ $ forM_ (Map.toList labels) \(k, v) -> li_ dotoHtml k <> ": "if k == "repo" thena_ [href_ $ "https://github.com/" <> v]$ toHtml velse toHtml vwhen (length children > 0) $ol_ [class_ "pages"] $ forM_ children \(title, path) ->li_ $ a_ [href_ (fromString path)] (toHtml title)toHtmlRaw contentrenderIndex :: Text -> [(Project, FilePath)] -> Html ()renderIndex intro projects =outerWith def { Config.title = "projects", Config.description = intro} dotoHtmlRaw introul_ [class_ "projects"] $ forM_ projects projectLinkwhereprojectLink :: (Project, FilePath) -> Html ()projectLink (Project{..}, path) =li_ $ a_ [href_ (fromString path)] dodiv_ $ img_ [src_ (fromString $ path </> "logo.svg")]div_ $ h2_ (toHtml title) >> p_ (toHtml subtitle)getKey :: String -> (Int, String)getKey xs = getKey' 0 xswheregetKey' :: Int -> String -> (Int, String)getKey' k (x : xs) | x >= '0' && x <= '9' =getKey' (k * 10 + digitToInt x) xsgetKey' k ('-' : xs) = (k, xs)getKey' k xs = (k, xs)
module Posts whereimport Data.Aeson.Types (FromJSON)import Data.Binary (Binary, put, get)import Data.Time (UTCTime, defaultTimeLocale)import Data.Time.Clock (getCurrentTime)import Data.Time.Format (rfc822DateFormat, formatTime)import Data.List (isPrefixOf)import GHC.Genericsimport Lucidimport Text.Atom.Feed as Atomimport Text.Feed.Types (Feed(..))import Text.Feed.Export (textFeed)import qualified Achille.Internal.IO as AchilleIOimport Commonimport Config (ropts, wopts)import Visual (Image(..))import qualified Configimport Templatesimport Routeimport System.FilePathimport System.Directory ( setCurrentDirectory, getTemporaryDirectory, renameDirectory, createDirectoryIfMissing)-- metadata used for parsing YAML headersdata PostMeta = PostMeta{ title :: Text, draft :: Maybe Bool, description :: Maybe Text} deriving (Generic, Eq, Show, FromJSON)data Post = Post{ postTitle :: Text, postDate :: UTCTime, postDraft :: Bool, postDescription :: Maybe Text, postContent :: Text, postPath :: FilePath} deriving (Generic, Eq, Show, Binary)instance IsTimestamped Post where timestamp = postDatebuildPost :: FilePath -> Task IO PostbuildPost src = docopyFile srclet ext = takeExtensions srcif ".lagda.md" `isPrefixOf` ext then processAgda srcelse do(PostMeta title draft desc, pandoc) <- readPandocMetadataWith ropts srccontent <- renderPandocWith wopts pandoclet date = timestamp srcpure (renderPost date title content)>>= write (src -<.> "html")<&> Post title date (fromMaybe False draft) Nothing contentwhereprocessAgda :: FilePath -> Task IO PostprocessAgda src = dospath <- toAbsolute srcodir <- getOutputDir <&> (</> dropExtensions src)tmpdir <- liftIO getTemporaryDirectory <&> (</> "achille")liftIO $ createDirectoryIfMissing False tmpdirliftIO $ createDirectoryIfMissing False odirliftIO $ AchilleIO.copyFile spath (tmpdir </> "index.lagda.md")-- agda --html needs to be invoked in the correct directoryliftIO $ setCurrentDirectory tmpdircallCommand $"agda --html "<> "--html-dir=. "<> "--html-highlight=auto "<> "index.lagda.md"callCommand $ "cp " <> tmpdir <> "/* " <> odirlet tpath = odir </> "index.md"(PostMeta title draft desc, pandoc) <- readAbsPandocMetadataWith ropts tpathcontent <- renderPandocWith wopts pandoclet date = timestamp srcpure (renderPost date title content)>>= write (dropExtensions src </> "index.html")<&> takeDirectory<&> Post title date (fromMaybe False draft) Nothing contentbuild :: Bool -> [Image] -> Task IO ()build showDrafts imgs = doposts <- match "posts/*" buildPost<&> filter (\p -> not (postDraft p) || showDrafts)<&> recentFirstwatch imgs $ watch posts $ match_ "index.rst" \src -> docompilePandoc src<&> renderIndex imgs posts>>= write (src -<.> "html")now <- liftIO getCurrentTimelet (Just feed) = textFeed (AtomFeed $ postsToFeed now posts)write "atom.xml" feedwherepostsToFeed now posts =( Atom.nullFeed"https://acatalepsie.fr/atom.xml"(Atom.TextString "acatalepsie")"2017-08-01"){ Atom.feedEntries = postToEntry <$> posts, Atom.feedUpdated = fromString $ toDate now}postToEntry :: Post -> Atom.EntrypostToEntry post =( Atom.nullEntry (fromString $ postPath post)(Atom.TextString $ postTitle post)(fromString $ toDate $ postDate post)){ Atom.entryContent = Just $ Atom.HTMLContent $ postContent post, Atom.entrySummary = Atom.HTMLString <$> postDescription post}renderPost :: UTCTime -> Text -> Text -> Html ()renderPost date title content =outerWith def { Config.title = title, Config.route = PostRoute } doheader_ doh1_ $ toHtml titlep_ do"Posted on "time_ $ toHtml (showDate date)"."toHtmlRaw contentrenderIndex :: [Image] -> [Post] -> Text -> Html ()renderIndex imgs posts content =outer dotoHtmlRaw contentsection_ [class_ "visual tiny"] $forM_ imgs \Image{..} ->figure_ $ a_ [href_ $ fromString imgPath] $ img_[ src_ (fromString imgThumbPath), width_ (fromString $ show imgThumbWidth), height_ (fromString $ show imgThumbHeight)]p_ [class_ "right"] $ a_ [href_ "/visual.html"] "→ View more visual work"h2_ "Latest posts"ul_ [ id_ "pidx" ] $ forM_ posts \post ->li_ dospan_ $ fromString $ showDate (postDate post)toLink (postPath post) (toHtml $ postTitle post)
{-# LANGUAGE LambdaCase #-}module Main whereimport qualified System.Process as Processimport System.Directory (removePathForcibly)import Control.Monad (void, mapM_)import Options.Applicativeimport Lucidimport Commonimport Templatesimport Config (config, ropts, wopts, SiteConfig(title))import qualified Postsimport qualified Projectsimport qualified Visualimport qualified Readingstype ShowDrafts = Booldata Cmd= Build ShowDrafts -- ^ Build the site| Deploy -- ^ Deploy to the server| Clean -- ^ Delete all artefactsderiving (Eq, Show)cli :: Parser Cmdcli = hsubparser $command "build" (info (Build <$> switch (long "draft" <> short 'D' <> help "Display drafts"))(progDesc "Build the site once" ))<> command "deploy" (info (pure Deploy) (progDesc "Server go brrr" ))<> command "clean" (info (pure Clean) (progDesc "Delete all artefacts"))main :: IO ()main = customExecParser p opts >>= \caseDeploy -> mapM_ Process.callCommand (deployCmd config)Clean -> removePathForcibly (outputDir config)>> removePathForcibly (cacheFile config)Build showDrafts -> void $ runTask [] config (build showDrafts)whereopts = info (cli <**> helper) $ fullDesc <> header descp = prefs showHelpOnEmptydesc = "acatalepsie & co"build :: ShowDrafts -> Task IO Stringbuild showDrafts = do-- static assetsmatch_ "assets/*" copyFilematch_ "static/*" copyFile-- quid pagematch_ "./quid.rst" \src ->compilePandoc src<&> toHtmlRaw<&> outerWith def {Config.title = "quid"}>>= write (src -<.> "html")lastImages <- Visual.buildProjects.buildPosts.build showDrafts lastImagesReadings.build
module Config (config, ropts, wopts, SiteConfig(..), def) whereimport Data.Defaultimport Data.Text (Text)import Text.Pandoc.Options as Pandocimport Achille (Config(..))import Routeconfig :: Achille.Configconfig = def{ deployCmd = Just "rsync -avzzr ~/dev/acatalepsie/_site/ --chmod=755 acatalepsie:/var/www/html", contentDir = root <> "content", outputDir = root <> "_site", cacheFile = root <> ".cache", ignore = [ "**/*.agdai", "**/*~"]} where root = "/home/flupe/dev/acatalepsie/"ropts :: Pandoc.ReaderOptionsropts = def { readerExtensions = pandocExtensions }wopts :: Pandoc.WriterOptionswopts = def { writerHTMLMathMethod = KaTeX "" }data SiteConfig = SiteConfig{ title :: Text, description :: Text, image :: Text, route :: Route}instance Default SiteConfig wheredef = SiteConfig{ title = "sbbls", description = "my personal web space, for your enjoyment", image = "https://acatalepsie.fr/assets/card.png", route = IndexRoute}
module Common( module Data.Functor, module Data.Sort, module Data.String, module System.FilePath, module Achille, module Achille.Task.Pandoc, module Data.Text, module Control.Monad, module Data.Maybe, module Lucid, module Data.Binary, module GHC.Generics, module Data.Aeson.Types, toDate) whereimport Achilleimport Achille.Task.Pandocimport Data.Time (UTCTime, defaultTimeLocale, formatTime, rfc822DateFormat)import Data.Aeson.Types (FromJSON)import GHC.Generics (Generic)import Data.Binary (Binary)import Data.Functor ((<&>))import Control.Monad (forM_, when)import Data.Sort (sort)import Data.String (fromString)import Data.Text (Text)import Data.Maybe (fromMaybe, mapMaybe)import System.FilePathimport Lucid (Html)toDate :: UTCTime -> StringtoDate = formatTime defaultTimeLocale rfc822DateFormat
cabal-version: >=1.10name: siteversion: 0.1.0.0author: flupemaintainer: lucas@escot.mebuild-type: Simpleexecutable sitemain-is: Main.hshs-source-dirs: srcother-modules: Templates, Types, Posts, Projects, Common, Config, Visual, Templates, Readings, Routebuild-depends: base >= 4.12 && < 5, filepath, achille, data-default, pandoc, pandoc-types, text, bytestring, filepath, aeson, yaml, binary, containers, sort, feed, time, lucid, optparse-applicative, process, directorydefault-extensions: BlockArguments, TupleSections, OverloadedStrings, ScopedTypeVariables, DeriveGeneric, DeriveAnyClass, RecordWildCards, NamedFieldPunsghc-options: -threaded-j8default-language: Haskell2010
This site is under construction, please be kind.-----All content on this website is licensed under `CC BY-NC 2.0`_ unless statedotherwise. In other words, you are free to copy, redistribute and edit thiscontent, provided you: give appropriate credit; indicate where changes were madeand do not do so for commercial purposes.This website is hosted on a 2014 RPi Model B+ somewhere in France.The domain name `acatalepsie.fr <https://acatalepsie.fr>`_ hasbeen registered at `gandi.net <https://gandi.net>`_... _CC BY-NC 2.0: https://creativecommons.org/licenses/by-nc/2.0/
packages: ../achille/achille.cabal site.cabaljobs: 8
my personal website, made with [achille](https://acatalepsie.fr/projects/achille).```nix-shell --attr env release.nixnix-env -if release.nix```## todo- dark theme- faster thumbnail generation with openCV- generic feed generation- indieweb interactions (webmentions, etc)- bin packing / grid system for galery- better gallery (albums, webzines, media types, layouts, etc)- tag/category/search engine- parallelization- draft builds + live server