git-annex in flupe@gozom:~/dev/acatalepsie

This commit is contained in:
flupe 2021-03-17 21:49:22 +01:00
parent 0f4d94eb28
commit a6242afd41
15 changed files with 264 additions and 119 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ dist-newstyle
_site _site
*.local *.local
*~ *~
*.agdai

View File

@ -9,7 +9,10 @@ nix-env -if release.nix
- dark theme - dark theme
- faster thumbnail generation with openCV - 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) - better gallery (albums, webzines, media types, layouts, etc)
- tag/category/search system - tag/category/search engine
- parallelization - parallelization
- draft builds + live server - draft builds + live server

View File

@ -1,19 +0,0 @@
{ mkDerivation, achille, aeson, base, binary, bytestring
, containers, data-default, directory, feed, filepath, lucid
, optparse-applicative, pandoc, pandoc-types, process, sort, stdenv
, text, time, yaml
}:
mkDerivation {
pname = "site";
version = "0.1.0.0";
src = ./.;
isLibrary = false;
isExecutable = true;
executableHaskellDepends = [
achille aeson base binary bytestring containers data-default
directory feed filepath lucid optparse-applicative pandoc
pandoc-types process sort text time yaml
];
license = "unknown";
hydraPlatforms = stdenv.lib.platforms.none;
}

View File

@ -13,7 +13,7 @@
} }
body { body {
font: 15px Inter, sans-serif, "é"; font: 16px Inter, sans-serif, "é";
line-height: 1.54; line-height: 1.54;
margin: 0; margin: 0;
height: 100vh; height: 100vh;
@ -43,6 +43,10 @@ main > section {
counter-reset: subsection; counter-reset: subsection;
} }
main :first-child {
margin-top: 0;
}
main ::selection { main ::selection {
background: var(--yellow); background: var(--yellow);
color: var(--black); color: var(--black);
@ -124,7 +128,7 @@ strong {color: var(--darker)}
#hd nav a:hover {border-bottom:2px solid var(--yellow)} #hd nav a:hover {border-bottom:2px solid var(--yellow)}
main, #ft { main, #ft, .breadcrumb {
padding: 2em 1em; padding: 2em 1em;
max-width: var(--width); max-width: var(--width);
box-sizing: border-box; box-sizing: border-box;
@ -229,9 +233,11 @@ dt {text-align: right; font-weight: 500;}
dd {margin:0} dd {margin:0}
dd p {margin:0} dd p {margin:0}
code {font: .9em "Source Code Pro", monospace} code, pre.Agda {font: .9em "Source Code Pro", monospace}
pre.Agda a {font-weight: inherit; text-decoration: none}
pre.Agda a[href]:hover { background: var(--yellow) }
code, pre.sourceCode { code, pre.sourceCode, pre.Agda {
background: #eceff4; background: #eceff4;
border-radius: 3px; border-radius: 3px;
} }
@ -252,12 +258,18 @@ pre > code {display: block}
#ft svg:hover {opacity:1} #ft svg:hover {opacity:1}
pre.sourceCode { pre.sourceCode {
margin:1em 0; margin:2em 0;
padding: .8em 0; padding: .8em 0;
line-height: 1; line-height: 1;
overflow: auto; overflow: auto;
} }
pre.Agda {
margin: 2em 0;
padding: .8em 1em;
overflow: auto;
}
pre.sourceCode > code { pre.sourceCode > code {
display: inline-block; display: inline-block;
margin: 0 1em; margin: 0 1em;
@ -290,13 +302,24 @@ section.visual {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
} }
section.visual.tiny {
align-items: stretch;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
section.visual figure { margin: 0 } section.visual figure { margin: 0 }
section.visual.tiny figure { margin: 0; border-radius: 5px; overflow: hidden; }
figure img { figure img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
/* aspect-ratio: attr(width) / attr(height); */ /* aspect-ratio: attr(width) / attr(height); */
vertical-align: top; vertical-align: top;
} }
section.visual.tiny img {object-fit: cover}
p.right {text-align: right}
section.visual.tiny figure img { height: 100% }
.admonition { .admonition {
border-left: 3px solid var(--yellow); border-left: 3px solid var(--yellow);
@ -319,19 +342,20 @@ figure img {
.bn {color:#40a070} .bn {color:#40a070}
.cf {color:#007020; font-weight: bold; } .cf {color:#007020; font-weight: bold; }
.ch {color:#4070a0} .ch {color:#4070a0}
.cn {color:#880000} .cn, .Agda .InductiveConstructor {color:#880000}
.co {color:#60a0b0; font-style: italic; } .co, .Agda .Comment {color:#60a0b0; font-style: italic; }
.cv {color:#60a0b0; font-weight: bold; font-style: italic; } .cv {color:#60a0b0; font-weight: bold; font-style: italic; }
.do {color:#ba2121; font-style: italic; } .do {color:#ba2121; font-style: italic; }
.dt {color:#902000} .dt, .Agda .PrimitiveType
, .Agda .Datatype {color:#902000}
.dv {color:#40a070} .dv {color:#40a070}
.er {color:#f00; font-weight: bold; } .er {color:#f00; font-weight: bold; }
.fl {color:#40a070; } .fl {color:#40a070; }
.fu {color:#06287e; } .fu, .Agda .Function {color:#06287e; }
.in {color:#60a0b0; font-weight: bold; font-style: italic; } .in {color:#60a0b0; font-weight: bold; font-style: italic; }
.kw {color:#007020; } .kw, .Agda .Keyword {color:#007020; }
.op {color:#666} .op {color:#666}
.ot {color:#007020} .ot, .Agda .Symbol {color:#007020}
.pp {color:#bc7a00} .pp {color:#bc7a00}
.sc {color:#4070a0} .sc {color:#4070a0}
.ss {color:#bb6688} .ss {color:#bb6688}
@ -353,5 +377,21 @@ table.books tr td:first-child {
color: var(--blacker); color: var(--blacker);
font-weight: 500; font-weight: 500;
} }
.breadcrumb {
padding-bottom: 0;
padding-top: 1em;
}
.breadcrumb .sep {
color: #999;
padding: 0 .5em;
cursor: default;
}
main > header h1 {line-height: 1.3}
main > header p {font-style: italic; padding-left: .5em; margin: .2em 0 0 }
main > header time {font-weight: 500 }
table.books tr td:nth-child(3), table.books tr td:nth-child(3),
table.books tr td:last-child {text-align: center;color: var(--yellow)} table.books tr td:last-child {text-align: center;color: var(--yellow)}

View File

@ -1 +0,0 @@
Oh god why

View File

@ -1,3 +1,11 @@
- title: Machine Sex and Other Stories
author: Candas Jane Dorsey
- title: Art
author: Yasmina Reza
- title: Histoire de ta bêtise
author: François Bégaudeau
- title: Le Discours
author: Fabrice Caro
- title: Ayoade on Ayoade - title: Ayoade on Ayoade
author: Richard Ayoade author: Richard Ayoade
- title: Antkind - title: Antkind

View File

@ -1,16 +0,0 @@
let
compiler = "ghc884";
rev = "b78e08e981a9ad31036fc6c6fb880c1315b4ebea";
overlay = _: pkgs: {
haskellPackages = pkgs.haskellPackages.override {
overrides = self: super: rec {
achille = self.callPackage ../achille/achille.nix {};
};
};
};
nixpkgs =
import (builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
}) { overlays = [ overlay ] ; };
in nixpkgs.haskellPackages.callPackage ./acatalepsie.nix {}

View File

@ -17,6 +17,7 @@ executable site
, Visual , Visual
, Templates , Templates
, Readings , Readings
, Route
build-depends: base >= 4.12 && < 5 build-depends: base >= 4.12 && < 5
, filepath , filepath
, achille , achille
@ -37,15 +38,14 @@ executable site
, optparse-applicative , optparse-applicative
, process , process
, directory , directory
extensions: BlockArguments default-extensions: BlockArguments
, TupleSections , TupleSections
, OverloadedStrings , OverloadedStrings
, ScopedTypeVariables , ScopedTypeVariables
, DeriveGeneric , DeriveGeneric
, DeriveAnyClass , DeriveAnyClass
, RecordWildCards , RecordWildCards
, NamedFieldPuns , NamedFieldPuns
ghc-options: -threaded ghc-options: -threaded
-j8 -j8
-O2
default-language: Haskell2010 default-language: Haskell2010

View File

@ -12,11 +12,13 @@ module Common
, module Data.Binary , module Data.Binary
, module GHC.Generics , module GHC.Generics
, module Data.Aeson.Types , module Data.Aeson.Types
, toDate
) where ) where
import Achille import Achille
import Achille.Task.Pandoc import Achille.Task.Pandoc
import Data.Time (UTCTime, defaultTimeLocale, formatTime, rfc822DateFormat)
import Data.Aeson.Types (FromJSON) import Data.Aeson.Types (FromJSON)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Data.Binary (Binary) import Data.Binary (Binary)
@ -28,3 +30,6 @@ import Data.Text (Text)
import Data.Maybe (fromMaybe, mapMaybe) import Data.Maybe (fromMaybe, mapMaybe)
import System.FilePath import System.FilePath
import Lucid (Html) import Lucid (Html)
toDate :: UTCTime -> String
toDate = formatTime defaultTimeLocale rfc822DateFormat

View File

@ -4,14 +4,18 @@ import Data.Default
import Data.Text (Text) import Data.Text (Text)
import Text.Pandoc.Options as Pandoc import Text.Pandoc.Options as Pandoc
import Achille (Config(..)) import Achille (Config(..))
import Route
config :: Achille.Config config :: Achille.Config
config = def config = def
{ deployCmd = Just "rsync -avzzr _site/ --chmod=755 acatalepsie:/var/www/html" { deployCmd = Just "rsync -avzzr ~/dev/acatalepsie/_site/ --chmod=755 acatalepsie:/var/www/html"
, contentDir = root <> "content" , contentDir = root <> "content"
, outputDir = root <> "_site" , outputDir = root <> "_site"
, cacheFile = root <> ".cache" , cacheFile = root <> ".cache"
, ignore = [ "**/*.agdai"
, "**/*~"
]
} where root = "/home/flupe/dev/acatalepsie/" } where root = "/home/flupe/dev/acatalepsie/"
@ -26,6 +30,7 @@ data SiteConfig = SiteConfig
{ title :: Text { title :: Text
, description :: Text , description :: Text
, image :: Text , image :: Text
, route :: Route
} }
instance Default SiteConfig where instance Default SiteConfig where
@ -33,4 +38,5 @@ instance Default SiteConfig where
{ title = "sbbls" { title = "sbbls"
, description = "my personal web space, for your enjoyment" , description = "my personal web space, for your enjoyment"
, image = "https://acatalepsie.fr/assets/card.png" , image = "https://acatalepsie.fr/assets/card.png"
, route = IndexRoute
} }

View File

@ -17,12 +17,13 @@ import qualified Projects
import qualified Visual import qualified Visual
import qualified Readings import qualified Readings
type ShowDrafts = Bool type ShowDrafts = Bool
data Cmd data Cmd
= Build ShowDrafts -- ^ Build the site = Build ShowDrafts -- ^ Build the site
| Deploy -- ^ Deploy to the server | Deploy -- ^ Deploy to the server
| Clean -- ^ Delete all artefacts | Clean -- ^ Delete all artefacts
deriving (Eq, Show) deriving (Eq, Show)
@ -39,8 +40,7 @@ main = customExecParser p opts >>= \case
Deploy -> mapM_ Process.callCommand (deployCmd config) Deploy -> mapM_ Process.callCommand (deployCmd config)
Clean -> removePathForcibly (outputDir config) Clean -> removePathForcibly (outputDir config)
>> removePathForcibly (cacheFile config) >> removePathForcibly (cacheFile config)
Build showDrafts -> Build showDrafts -> void $ runTask [] config (build showDrafts)
void $ runTask [] config (build showDrafts)
where where
opts = info (cli <**> helper) $ fullDesc <> header desc opts = info (cli <**> helper) $ fullDesc <> header desc
p = prefs showHelpOnEmpty p = prefs showHelpOnEmpty
@ -51,6 +51,7 @@ build :: ShowDrafts -> Task IO String
build showDrafts = do build showDrafts = do
-- static assets -- static assets
match_ "assets/*" copyFile match_ "assets/*" copyFile
match_ "static/*" copyFile
-- quid page -- quid page
match_ "./quid.rst" \src -> match_ "./quid.rst" \src ->
@ -59,7 +60,7 @@ build showDrafts = do
<&> outerWith def {Config.title = "quid"} <&> outerWith def {Config.title = "quid"}
>>= write (src -<.> "html") >>= write (src -<.> "html")
Visual.build lastImages <- Visual.build
Projects.build Projects.build
Posts.build showDrafts Posts.build showDrafts lastImages
Readings.build Readings.build

View File

@ -1,21 +1,31 @@
module Posts (build) where module Posts where
import Data.Aeson.Types (FromJSON) import Data.Aeson.Types (FromJSON)
import Data.Binary (Binary, put, get) import Data.Binary (Binary, put, get)
import Data.Time (UTCTime, defaultTimeLocale) import Data.Time (UTCTime, defaultTimeLocale)
import Data.Time.Clock (getCurrentTime) import Data.Time.Clock (getCurrentTime)
import Data.Time.Format (rfc822DateFormat, formatTime) import Data.Time.Format (rfc822DateFormat, formatTime)
import Data.List (isPrefixOf)
import GHC.Generics import GHC.Generics
import Lucid import Lucid
import Text.Atom.Feed as Atom import Text.Atom.Feed as Atom
import Text.Feed.Types (Feed(..)) import Text.Feed.Types (Feed(..))
import Text.Feed.Export (textFeed) import Text.Feed.Export (textFeed)
import qualified Achille.Internal.IO as AchilleIO
import Common import Common
import Config (ropts, wopts) import Config (ropts, wopts)
import Visual (Image(..))
import qualified Config import qualified Config
import Templates import Templates
import Route
import System.FilePath
import System.Directory ( setCurrentDirectory
, getTemporaryDirectory
, renameDirectory
, createDirectoryIfMissing
)
-- metadata used for parsing YAML headers -- metadata used for parsing YAML headers
data PostMeta = PostMeta data PostMeta = PostMeta
@ -39,25 +49,57 @@ instance IsTimestamped Post where timestamp = postDate
buildPost :: FilePath -> Task IO Post buildPost :: FilePath -> Task IO Post
buildPost src = do buildPost src = do
copyFile src copyFile src
(PostMeta title draft desc, pandoc) <- readPandocMetadataWith ropts src let ext = takeExtensions src
content <- renderPandocWith wopts pandoc if ".lagda.md" `isPrefixOf` ext then processAgda src
else do
(PostMeta title draft desc, pandoc) <- readPandocMetadataWith ropts src
content <- renderPandocWith wopts pandoc
let date = timestamp src
pure (renderPost date title content)
>>= write (src -<.> "html")
<&> Post title date (fromMaybe False draft) Nothing content
pure (renderPost title src content) where
>>= write (src -<.> "html") processAgda :: FilePath -> Task IO Post
<&> Post title (timestamp src) (fromMaybe False draft) Nothing content processAgda src = do
spath <- toAbsolute src
odir <- getOutputDir <&> (</> dropExtensions src)
tmpdir <- liftIO getTemporaryDirectory <&> (</> "achille")
liftIO $ createDirectoryIfMissing False tmpdir
liftIO $ createDirectoryIfMissing False odir
liftIO $ AchilleIO.copyFile spath (tmpdir </> "index.lagda.md")
toDate :: UTCTime -> String -- agda --html needs to be invoked in the correct directory
toDate = formatTime defaultTimeLocale rfc822DateFormat liftIO $ setCurrentDirectory tmpdir
build :: Bool -> Task IO () callCommand $
build showDrafts = do "agda --html "
<> "--html-dir=. "
<> "--html-highlight=auto "
<> "index.lagda.md"
callCommand $ "cp " <> tmpdir <> "/* " <> odir
let tpath = odir </> "index.md"
(PostMeta title draft desc, pandoc) <- readAbsPandocMetadataWith ropts tpath
content <- renderPandocWith wopts pandoc
let date = timestamp src
pure (renderPost date title content)
>>= write (dropExtensions src </> "index.html")
<&> takeDirectory
<&> Post title date (fromMaybe False draft) Nothing content
build :: Bool -> [Image] -> Task IO ()
build showDrafts imgs = do
posts <- match "posts/*" buildPost posts <- match "posts/*" buildPost
<&> filter (\p -> not (postDraft p) || showDrafts) <&> filter (\p -> not (postDraft p) || showDrafts)
<&> recentFirst <&> recentFirst
watch posts $ match_ "index.rst" \src -> do watch imgs $ watch posts $ match_ "index.rst" \src -> do
compilePandoc src compilePandoc src
<&> renderIndex posts <&> renderIndex imgs posts
>>= write (src -<.> "html") >>= write (src -<.> "html")
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
@ -84,18 +126,33 @@ build showDrafts = do
} }
renderPost :: Text -> FilePath -> Text -> Html () renderPost :: UTCTime -> Text -> Text -> Html ()
renderPost title source content = renderPost date title content =
outerWith def { Config.title = title } do outerWith def { Config.title = title, Config.route = PostRoute } do
h1_ $ toHtml title header_ do
toLink source "View source" h1_ $ toHtml title
p_ do
"Posted on "
time_ $ toHtml (showDate date)
"."
toHtmlRaw content toHtmlRaw content
renderIndex :: [Post] -> Text -> Html () renderIndex :: [Image] -> [Post] -> Text -> Html ()
renderIndex posts content = renderIndex imgs posts content =
outer do outer do
toHtmlRaw content toHtmlRaw content
section_ [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" h2_ "Latest posts"
ul_ [ id_ "pidx" ] $ forM_ posts \post -> ul_ [ id_ "pidx" ] $ forM_ posts \post ->
li_ do li_ do

View File

@ -5,6 +5,7 @@ import Data.Char (digitToInt)
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Common import Common
import Route
import Types import Types
import Config import Config
import Templates import Templates
@ -48,7 +49,9 @@ buildProject src = do
(TitledPage title _, doc) <- readPandocMetadataWith ropts filepath (TitledPage title _, doc) <- readPandocMetadataWith ropts filepath
renderPandocWith wopts doc renderPandocWith wopts doc
<&> toHtmlRaw <&> toHtmlRaw
<&> outerWith (def {Config.title = title}) <&> outerWith (def { Config.title = title
, Config.route = ProjectPageRoute title (ProjectRoute $ fromString name)
})
>>= write (filepath -<.> "html") >>= write (filepath -<.> "html")
<&> (title,) <&> (title,)
@ -57,6 +60,7 @@ renderProject :: Project -> [(Text, FilePath)] -> Text -> Html ()
renderProject Project{..} children content = renderProject Project{..} children content =
outerWith def { Config.title = title outerWith def { Config.title = title
, Config.description = subtitle , Config.description = subtitle
, Config.route = ProjectRoute title
} do } do
header_ [class_ "project"] do header_ [class_ "project"] do
div_ (img_ [src_ "logo.svg"]) div_ (img_ [src_ "logo.svg"])

View File

@ -17,6 +17,7 @@ import Achille.Writable as Writable
import Lucid import Lucid
import Lucid.Base (makeAttribute) import Lucid.Base (makeAttribute)
import Route
import Types import Types
import Common import Common
import Config import Config
@ -45,7 +46,7 @@ outer :: Html () -> Html ()
outer = outerWith def outer = outerWith def
outerWith :: SiteConfig -> Html () -> Html () outerWith :: SiteConfig -> Html () -> Html ()
outerWith SiteConfig{title,..} content = doctypehtml_ do outerWith SiteConfig{title,route,..} content = doctypehtml_ do
head_ do head_ do
meta_ [ name_ "viewport" meta_ [ name_ "viewport"
, content_ "width=device-width, initial-scale=1.0, user-scalable=yes" , content_ "width=device-width, initial-scale=1.0, user-scalable=yes"
@ -77,6 +78,8 @@ outerWith SiteConfig{title,..} content = doctypehtml_ do
a_ [ href_ "/readings.html" ] "Readings" a_ [ href_ "/readings.html" ] "Readings"
a_ [ href_ "/quid.html" ] "Quid" a_ [ href_ "/quid.html" ] "Quid"
breadcrumb route
main_ content main_ content
footer_ [ id_ "ft" ] do footer_ [ id_ "ft" ] do
@ -84,6 +87,6 @@ outerWith SiteConfig{title,..} content = doctypehtml_ do
a_ [ href_ "https://creativecommons.org/licenses/by-nc/2.0/" ] a_ [ href_ "https://creativecommons.org/licenses/by-nc/2.0/" ]
"CC BY-NC 2.0" "CC BY-NC 2.0"
" · " " · "
a_ [ href_ "https://instagram.com/ba.bou.m/" ] "instagram" a_ [ href_ "https://instagram.com/ba.bou.m/", rel_ "me" ] "instagram"
" · " " · "
a_ [ href_ "/atom.xml" ] "feed" a_ [ href_ "/atom.xml" ] "feed"

View File

@ -1,54 +1,107 @@
module Visual (build) where module Visual (build, Image(..)) where
import Config import Data.Time (UTCTime)
import Data.List (isSuffixOf)
import qualified Config
import Common import Common
import Templates (outerWith, loading_) import Templates (outerWith, loading_)
import Lucid import Lucid
import Route
thumbWidth :: Int thumbWidth :: Int
thumbWidth = 710 thumbWidth = 710
build :: Task IO () data 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 = imgDate
instance IsTimestamped Entry where timestamp = entryDate
build :: Task IO [Image]
build = do build = do
pictures <- match "visual/*" \src -> do pictures <- match "visual/*" \src -> do
copyFile src path <- copyFile src
callCommandWith tpath <- callCommandWith (\a b -> "convert -resize 710x " <> a <> " " <> b)
(\a b -> "convert " <> a <> " " <> b) (-<.> "thumb.webp")
(-<.> "webp") src
src apath <- toAbsolute src
src' <- toAbsolute src size <- read <$> readCommand "identify" ["-ping", "-format", "(%w, %h)", apath]
size <- (resized . read) let nsfw = "nsfw" `isSuffixOf` dropExtension src
<$> readCommand "identify" ["-ping", "-format", "(%w, %h)", src'] pure (Image path tpath thumbWidth (thumbHeight size) (timestamp src) nsfw)
callCommandWith
(\a b -> "convert -resize 710x " <> a <> " " <> b)
(-<.> "thumb.webp")
src
<&> timestamped
<&> fmap (size,)
watch pictures $ match_ "./visual.rst" \src -> do
intro <- compilePandoc src -- webcomics/albums
write "visual.html" $ renderVisual intro (recentFirst pictures) entries <- matchDir "visual/*/" \src -> do
matchFile "index.markdown" \src -> do
(meta, doc) <- readPandocMetadata src
renderPandoc doc
<&> renderEntry meta
>>= write (src -<.> "html")
pure ()
watch pictures do
let sorted = filter (not . imgNSFW) $ recentFirst pictures
match_ "./visual.rst" \src -> do
intro <- compilePandoc src
write "visual.html" $ renderVisual intro sorted
return (take 4 sorted)
where where
resized :: (Int, Int) -> (Int, Int) thumbHeight :: (Int, Int) -> Int
resized (width, height) = thumbHeight (width, height) = round (fi height * fi thumbWidth / fi width)
(thumbWidth, round $ fi height * fi thumbWidth / fi width)
where fi :: Int -> Float
fi :: Int -> Float fi = fromIntegral
fi = fromIntegral
renderVisual :: Text -> [Timestamped ((Int, Int), FilePath)] -> Html () renderEntry :: EntryMeta -> Text -> Html ()
renderEntry meta content =
outerWith def { Config.title = title meta
, Config.route = VEntryRoute
} do
header_ do
h1_ $ toHtml (title meta)
p_ $ toHtml (date meta)
toHtmlRaw content
hr_ []
renderVisual :: Text -> [Image] -> Html ()
renderVisual txt imgs = renderVisual txt imgs =
outerWith def {title = "visual"} do outerWith def {Config.title = "visual"} do
toHtmlRaw txt toHtmlRaw txt
hr_ [] hr_ []
section_ [class_ "visual"] $ section_ [class_ "visual"] $
forM_ imgs \ (Timestamped _ ((width, height), p)) -> forM_ imgs \Image{..} ->
figure_ $ a_ [href_ (fromString (replaceExtensions p "webp"))] $ img_ figure_ $ a_ [href_ $ fromString imgPath] $ img_
[ src_ (fromString p) [ src_ (fromString imgThumbPath)
, width_ (fromString $ show width) , width_ (fromString $ show imgThumbWidth)
, height_ (fromString $ show height) , height_ (fromString $ show imgThumbHeight)
, loading_ "lazy" ] , loading_ "lazy" ]