her.esy.fun/Shakefile.hs

459 lines
15 KiB
Haskell
Raw Normal View History

2020-05-25 20:30:22 +00:00
{-# LANGUAGE OverloadedStrings #-}
2020-06-22 07:39:44 +00:00
{-# LANGUAGE NoImplicitPrelude #-}
2020-06-22 09:44:11 +00:00
import Protolude
2020-05-25 20:30:22 +00:00
2020-06-22 09:44:11 +00:00
import Development.Shake
import Development.Shake.FilePath
2020-06-14 11:19:13 +00:00
2020-08-23 12:06:08 +00:00
import Data.Time.Format.ISO8601 (iso8601Show)
2020-06-25 11:08:18 +00:00
import qualified Data.Time.Clock as Clock
2020-06-22 09:44:11 +00:00
import Control.Monad.Fail
2020-06-22 21:01:47 +00:00
import Data.Aeson
import Data.Default ( Default(def) )
import qualified Data.Text as T
import Text.Mustache
2020-06-23 17:28:41 +00:00
import Text.Pandoc.Class (PandocMonad)
2020-06-22 21:01:47 +00:00
import qualified Text.Pandoc.Class as Pandoc
import Text.Pandoc.Definition ( Pandoc(..)
, Block(..)
2020-06-24 16:36:56 +00:00
, Inline(..)
2020-06-25 11:08:18 +00:00
, MetaValue(..)
2020-06-22 21:01:47 +00:00
, nullMeta
, docTitle
, docDate
, docAuthors
2020-06-25 11:08:18 +00:00
, lookupMeta
2020-06-22 21:01:47 +00:00
)
import Text.Pandoc.Options ( ReaderOptions(..)
2020-06-23 07:15:03 +00:00
, WriterOptions(..)
2020-06-23 17:28:41 +00:00
, ObfuscationMethod(..)
2020-06-27 12:25:53 +00:00
, HTMLMathMethod(..)
2020-06-23 07:15:03 +00:00
)
2020-06-27 12:25:53 +00:00
2020-06-22 21:01:47 +00:00
import qualified Text.Pandoc.Readers as Readers
2020-08-23 12:06:08 +00:00
import Text.Pandoc.Walk (Walkable(..))
2020-06-22 21:01:47 +00:00
import qualified Text.Pandoc.Writers as Writers
2020-08-23 12:06:08 +00:00
import qualified Text.Pandoc.Templates as Templates
2020-05-25 20:28:06 +00:00
main :: IO ()
2020-06-22 10:08:07 +00:00
main = shakeArgs shOpts buildRules
where
shOpts =
shakeOptions
{ shakeVerbosity = Chatty
, shakeLintInside = ["\\"]
}
-- Configuration
-- Should probably go in a Reader Monad
2020-06-23 17:28:41 +00:00
srcDir :: FilePath
srcDir = "src"
2020-06-22 10:08:07 +00:00
siteDir :: FilePath
siteDir = "_site"
optimDir :: FilePath
optimDir = "_optim"
-- BlogPost data structure (a bit of duplication because the metas are in Pandoc)
2020-05-25 20:28:06 +00:00
2020-06-22 07:39:44 +00:00
data BlogPost =
BlogPost { postTitle :: T.Text
, postDate :: T.Text
2020-06-25 13:27:00 +00:00
, postAuthor :: T.Text
2020-06-22 07:39:44 +00:00
, postUrl :: FilePath
2020-06-25 13:27:00 +00:00
, postSrc :: FilePath
2020-06-25 11:08:18 +00:00
, postTags :: [T.Text]
, postDescr :: T.Text
2020-06-23 07:15:03 +00:00
, postToc :: Bool
2020-06-22 09:44:11 +00:00
, postBody :: Pandoc
2020-06-22 07:39:44 +00:00
}
inlineToText :: PandocMonad m => [Inline] -> m T.Text
inlineToText inline =
2020-06-22 09:44:11 +00:00
Writers.writeAsciiDoc def (Pandoc nullMeta [Plain inline])
2020-06-25 15:23:59 +00:00
reformatDate :: Text -> Text
reformatDate = T.takeWhile (/= ' ') . (T.dropAround dateEnvelope)
where
dateEnvelope ' ' = True
dateEnvelope '\n' = True
dateEnvelope '\t' = True
dateEnvelope '[' = True
dateEnvelope ']' = True
dateEnvelope _ = False
2020-06-22 09:44:11 +00:00
getBlogpostFromMetas
2020-06-23 07:15:03 +00:00
:: (MonadIO m, MonadFail m) => [Char] -> Bool -> Pandoc -> m BlogPost
getBlogpostFromMetas path toc pandoc@(Pandoc meta _) = do
2020-06-22 09:44:11 +00:00
eitherBlogpost <- liftIO $ Pandoc.runIO $ do
2020-06-24 16:36:56 +00:00
title <- fmap (T.dropEnd 1) $ inlineToText $ docTitle meta
2020-06-25 15:23:59 +00:00
date <- fmap reformatDate $ inlineToText $ docDate meta
2020-06-25 13:27:00 +00:00
author <- case head $ docAuthors meta of
2020-08-22 10:47:04 +00:00
Just m -> fmap T.strip $ inlineToText m
2020-06-25 13:27:00 +00:00
Nothing -> return ""
2020-06-25 11:08:18 +00:00
let tags = tagsToList $ lookupMeta "keywords" meta
description = descr $ lookupMeta "description" meta
2020-06-25 13:27:00 +00:00
url = "/" </> dropDirectory1 path -<.> "org"
return $ BlogPost title date author url path tags description toc pandoc
2020-06-22 09:44:11 +00:00
case eitherBlogpost of
Left _ -> fail "BAD"
Right bp -> return bp
2020-06-24 16:36:56 +00:00
where
2020-06-25 11:08:18 +00:00
tagsToList (Just (MetaList ms)) = map toStr ms
tagsToList _ = []
descr (Just (MetaString t)) = t
descr _ = ""
toStr (MetaString t) = t
toStr (MetaInlines inlines) = T.intercalate " " $ map inlineToTxt inlines
toStr _ = ""
inlineToTxt (Str t) = t
inlineToTxt _ = ""
2020-06-24 16:36:56 +00:00
2020-06-22 09:44:11 +00:00
sortByPostDate :: [BlogPost] -> [BlogPost]
sortByPostDate =
2020-06-24 16:36:56 +00:00
sortBy (\a b-> compare (postDate b) (postDate a))
2020-06-22 07:39:44 +00:00
2020-06-22 10:27:45 +00:00
2020-06-22 20:13:12 +00:00
build :: FilePath -> FilePath
2020-06-22 10:27:45 +00:00
build = (</>) siteDir
2020-06-23 17:28:41 +00:00
genAllDeps :: [FilePattern] -> Action [FilePath]
genAllDeps patterns = do
allMatchedFiles <- getDirectoryFiles srcDir patterns
allMatchedFiles &
filter ((/= "html") . takeExtension) &
filter (null . takeExtension) &
map (siteDir </>) &
return
2020-06-14 11:19:13 +00:00
buildRules :: Rules ()
2020-05-25 20:28:06 +00:00
buildRules = do
2020-06-23 17:28:41 +00:00
cleanRule
2020-08-06 08:29:34 +00:00
fastRule
2020-06-23 17:28:41 +00:00
allRule
2020-06-27 12:57:04 +00:00
fullRule
2020-06-23 17:28:41 +00:00
getPost <- mkGetPost
getPosts <- mkGetPosts getPost
getTemplate <- mkGetTemplate
build "**" %> \out -> do
2020-06-23 21:58:09 +00:00
let asset = dropDirectory1 out
case (takeExtension asset) of
2020-06-24 16:36:56 +00:00
".html" -> do
if out == siteDir </> "archive.html"
then buildArchive getPosts getTemplate out
else genHtmlAction getPost getTemplate out
2020-06-27 13:49:25 +00:00
".pdf" -> do
txtExists <- doesFileExist (srcDir </> asset)
if txtExists
then copyFileChanged (srcDir </> asset) out
else genPdfAction getPost out
2020-06-23 21:58:09 +00:00
".txt" -> do
txtExists <- doesFileExist (srcDir </> asset)
if txtExists
then copyFileChanged (srcDir </> asset) out
else genAsciiAction getPost out
".jpg" -> compressImage asset
".jpeg" -> compressImage asset
".gif" -> compressImage asset
".png" -> compressImage asset
_ -> copyFileChanged (srcDir </> asset) out
2020-06-27 12:57:04 +00:00
optimDir </> "rss.xml" %> \_ -> do
needAll
command_[] "engine/pre-deploy.sh" []
2020-06-23 17:28:41 +00:00
2020-06-24 16:36:56 +00:00
buildArchive
:: (() -> Action [BlogPost])
-> (FilePath -> Action Template) -> [Char] -> Action ()
buildArchive getPosts getTemplate out = do
css <- genAllDeps ["//*.css"]
posts <- fmap sortByPostDate $ getPosts ()
2020-06-25 13:27:00 +00:00
need $ css <> map postSrc posts
2020-06-24 16:36:56 +00:00
let
title :: Text
title = "#+title: Posts"
articleList = toS $ T.intercalate "\n" $ map postInfo posts
fileContent = title <> "\n\n" <> articleList
eitherResult <- liftIO $ Pandoc.runIO $ Readers.readOrg (def { readerStandalone = True }) (toS fileContent)
bp <- case eitherResult of
Left _ -> fail "BAD"
Right pandoc -> getBlogpostFromMetas out False pandoc
innerHtml <- genHtml bp
template <- getTemplate ("templates" </> "main.mustache")
let htmlContent =
renderMustache template
$ object [ "title" .= postTitle bp
2020-06-25 13:27:00 +00:00
, "author" .= postAuthor bp
2020-06-24 16:36:56 +00:00
, "date" .= postDate bp
2020-06-25 11:08:18 +00:00
, "tags" .= postTags bp
, "description" .= postDescr bp
2020-06-24 16:36:56 +00:00
, "body" .= innerHtml
]
writeFile' out (toS htmlContent)
postInfo :: BlogPost -> Text
postInfo bp =
"- " <> date <> ": " <> orglink
where
date = T.takeWhile (/= ' ') (postDate bp)
2020-06-25 13:27:00 +00:00
orglink = "[[file:" <> (toS (postUrl bp)) <> "][" <> (postTitle bp) <> "]]"
2020-06-24 16:36:56 +00:00
replaceLinks :: Pandoc -> Pandoc
replaceLinks = walk replaceOrgLink
where
replaceOrgLink :: Inline -> Inline
replaceOrgLink lnk@(Link attr inl (url,txt)) =
if takeExtension (toS url) == ".org"
then Link attr inl ((toS (toS url -<.> ".html")),txt)
else lnk
replaceOrgLink x = x
2020-06-23 17:28:41 +00:00
2020-06-25 13:27:00 +00:00
orgContentToText :: (MonadIO m, MonadFail m) => Text -> m Text
2020-06-25 11:08:18 +00:00
orgContentToText org = do
eitherResult <- liftIO $ Pandoc.runIO $ Readers.readOrg (def { readerStandalone = True }) org
pandoc <- case eitherResult of
Left _ -> fail "BAD"
Right p -> return p
2020-08-23 12:06:08 +00:00
eitherHtml <- liftIO $ Pandoc.runIO $
Writers.writeHtml5String (def {writerEmailObfuscation = ReferenceObfuscation}) pandoc
2020-06-25 11:08:18 +00:00
case eitherHtml of
Left _ -> fail "BAD"
Right innerHtml -> return innerHtml
2020-06-25 13:27:00 +00:00
postamble :: (MonadIO m, MonadFail m) => Text -> BlogPost -> m Text
2020-06-25 11:08:18 +00:00
postamble now bp =
orgContentToText $ unlines $
2020-06-25 12:22:58 +00:00
[ "@@html:<footer>@@"
, "@@html:<i>Any comment? Click on my email below and I'll add it.</i>@@"
2020-06-25 11:08:18 +00:00
, ""
2020-06-25 12:22:58 +00:00
, "| author | @@html:<span class=\"author\">@@ [[mailto:Yann Esposito <yann@esposito.host>?subject=yblog: " <> (postTitle bp) <> "][Yann Esposito <yann@esposito.host>]] @@html:</span>@@ |"
2020-06-25 11:08:18 +00:00
, "| tags | " <> T.intercalate " " (map ("#"<>) (postTags bp)) <> " |"
, "| date | " <> postDate bp <> " |"
, "| rss | [[file:/rss.xml][RSS]] ([[https://validator.w3.org/feed/check.cgi?url=https%3A%2F%2Fher.esy.fun%2Frss.xml][validate]]) |"
2020-08-22 17:32:12 +00:00
, "| size | @@html:<span class=\"web-file-size\">XXK (html XXK, css XXK, img XXK)</span>@@ |"
, "| gz | @@html:<span class=\"gzweb-file-size\">XXK (html XXK, css XXK, img XXK)</span>@@ |"
2020-06-25 11:08:18 +00:00
, "| generated | " <> now <> " |"
, ""
, "@@html:</footer>@@"
]
2020-10-29 18:08:17 +00:00
tpltxt :: Text
2020-08-23 12:06:08 +00:00
tpltxt = T.unlines [
"$if(toc)$"
, "<nav id=\"$idprefix$TOC\" role=\"doc-toc\">"
, "$if(toc-title)$"
, "<h2 id=\"$idprefix$toc-title\">$toc-title$</h2>"
, "$endif$"
, "$table-of-contents$"
, "</nav>"
, "$endif$"
, "$body$"
]
getPostTpl :: IO (Templates.Template Text)
getPostTpl = do
etpl <- Templates.compileTemplate "blog.template" tpltxt
case etpl of
Left e -> fail e
Right tpl -> return tpl
2020-06-23 17:28:41 +00:00
genHtml :: (MonadIO m, MonadFail m) => BlogPost -> m Text
genHtml bp = do
2020-06-24 16:36:56 +00:00
let htmlBody = replaceLinks (postBody bp)
2020-08-23 12:06:08 +00:00
eitherHtml <- liftIO $ do
tpl <- getPostTpl
Pandoc.runIO $ do
2020-06-23 17:28:41 +00:00
Writers.writeHtml5String
2020-06-25 13:27:00 +00:00
(def { writerTableOfContents = postToc bp
2020-08-23 12:06:08 +00:00
, writerTemplate = Just tpl
, writerTOCDepth = 3
2020-06-23 17:28:41 +00:00
, writerEmailObfuscation = ReferenceObfuscation
2020-06-27 12:25:53 +00:00
, writerHTMLMathMethod = MathML
2020-06-23 17:28:41 +00:00
})
2020-06-24 16:36:56 +00:00
htmlBody
2020-06-25 11:08:18 +00:00
body <- case eitherHtml of
2020-06-23 17:28:41 +00:00
Left _ -> fail "BAD"
Right innerHtml -> return innerHtml
2020-06-25 11:08:18 +00:00
now <- liftIO Clock.getCurrentTime
footer <- postamble (toS (iso8601Show now)) bp
return (body <> footer)
2020-06-23 17:28:41 +00:00
2020-06-25 13:27:00 +00:00
origin :: Text
origin = "https://her.esy.fun"
2020-08-23 12:06:08 +00:00
2020-06-23 17:28:41 +00:00
genHtmlAction
:: (FilePath -> Action BlogPost)
-> (FilePath -> Action Template) -> [Char] -> Action ()
genHtmlAction getPost getTemplate out = do
2020-06-27 13:49:25 +00:00
let tplname = case takeDirectory1 (dropDirectory1 out) of
"posts" -> "post.mustache"
2020-08-22 21:17:41 +00:00
"slides" -> "slide.mustache"
2020-06-27 13:49:25 +00:00
"drafts" -> "post.mustache"
_ -> "main.mustache"
template <- getTemplate ("templates" </> tplname)
2020-06-23 17:28:41 +00:00
let srcFile = srcDir </> (dropDirectory1 (out -<.> "org"))
liftIO $ putText $ "need: " <> (toS srcFile) <> " -> " <> (toS out)
need [srcFile]
bp <- getPost srcFile
innerHtml <- genHtml bp
let htmlContent =
2020-06-23 21:58:09 +00:00
renderMustache template
$ object [ "title" .= postTitle bp
2020-06-25 13:27:00 +00:00
, "author" .= postAuthor bp
2020-06-23 21:58:09 +00:00
, "date" .= postDate bp
2020-06-25 11:08:18 +00:00
, "tags" .= postTags bp
, "description" .= postDescr bp
2020-06-23 21:58:09 +00:00
, "body" .= innerHtml
2020-06-25 13:27:00 +00:00
, "orgsource" .= T.pack (postUrl bp -<.> "org")
, "txtsource" .= T.pack (postUrl bp -<.> "txt")
2020-06-27 13:49:25 +00:00
, "pdf" .= T.pack (postUrl bp -<.> "pdf")
2020-10-29 18:05:58 +00:00
, "permalink" .= T.pack (toS origin <> postUrl bp -<.> "html")
2020-06-23 21:58:09 +00:00
]
2020-06-23 17:28:41 +00:00
writeFile' out (toS htmlContent)
2020-06-23 21:58:09 +00:00
genAscii :: (MonadIO m, MonadFail m) => BlogPost -> m Text
genAscii bp = do
eitherAscii <- liftIO $ Pandoc.runIO $ Writers.writePlain def (postBody bp)
case eitherAscii of
Left _ -> fail "BAD"
Right innerAscii -> return innerAscii
genAsciiAction
:: (FilePath -> Action BlogPost)
-> [Char] -> Action ()
genAsciiAction getPost out = do
let srcFile = srcDir </> (dropDirectory1 (out -<.> "org"))
need [srcFile]
bp <- getPost srcFile
innerAscii <- genAscii bp
let preamble = postTitle bp <> "\n"
2020-06-25 13:27:00 +00:00
<> T.replicate (T.length (postTitle bp)) "=" <> "\n\n"
<> postAuthor bp <> "\n"
<> postDate bp <> "\n"
<> toS origin <> toS (postUrl bp) <> "\n\n"
2020-06-23 21:58:09 +00:00
writeFile' out (toS (preamble <> toS innerAscii))
2020-08-23 12:06:08 +00:00
genPdfAction :: p -> [Char] -> Action ()
genPdfAction _getPost out = do
2020-06-27 13:49:25 +00:00
let srcFile = srcDir </> (dropDirectory1 (out -<.> "org"))
need [srcFile]
command_ [] "pandoc"
["--pdf-engine=xelatex"
, "--resource-path=" <> takeDirectory srcFile
, srcFile
, "-H", "engine" </> "deeplist.tex"
, "-V", "mainfont:Hoefler Text"
, "-V", "monofont:Menlo"
, "-V", "monofontoptions:Scale=0.7"
, "-o", out ]
2020-06-23 17:28:41 +00:00
allHtmlAction :: Action ()
allHtmlAction = do
2020-06-25 11:08:18 +00:00
allOrgFiles <- getDirectoryFiles srcDir ["//*.org"]
2020-06-23 17:28:41 +00:00
let allHtmlFiles = map (-<.> "html") allOrgFiles
2020-06-23 21:58:09 +00:00
need (map build allHtmlFiles)
2020-06-27 13:49:25 +00:00
allPdfAction :: Action ()
allPdfAction = do
allOrgFiles <- getDirectoryFiles srcDir ["//*.org"]
let allHtmlFiles = map (-<.> "pdf") allOrgFiles
need (map build allHtmlFiles)
2020-06-23 21:58:09 +00:00
allAsciiAction :: Action ()
allAsciiAction = do
2020-06-25 13:27:00 +00:00
allOrgFiles <- getDirectoryFiles srcDir ["//*.org"]
2020-06-23 21:58:09 +00:00
let allAsciiFiles = map (-<.> "txt") allOrgFiles
need (map build allAsciiFiles)
2020-06-23 17:28:41 +00:00
2020-06-24 16:36:56 +00:00
compressImage :: FilePath -> Action ()
2020-06-23 17:28:41 +00:00
compressImage img = do
let src = srcDir </> img
dst = siteDir </> img
need [src]
let dir = takeDirectory dst
dirExists <- doesDirectoryExist dir
when (not dirExists) $
command [] "mkdir" ["-p", dir]
2020-06-24 16:36:56 +00:00
command_ [] "convert" [ src
, "-strip"
2020-09-20 11:20:39 +00:00
, "-resize","960x960>"
2020-06-24 16:36:56 +00:00
, "-interlace","Plane"
, "-quality","85"
, "-define","filter:blur=0.75"
, "-filter","Gaussian"
, "-ordered-dither","o4x4,4"
, dst ]
2020-06-22 10:27:45 +00:00
2020-08-06 08:29:34 +00:00
2020-08-23 12:06:08 +00:00
needFast :: Action ()
2020-08-06 08:29:34 +00:00
needFast = do
allAssets <- filter (/= ".DS_Store") <$> getDirectoryFiles srcDir ["**"]
need (map build $ allAssets <> ["archive.html"])
allHtmlAction
fastRule :: Rules ()
fastRule =
withTargetDocs "generate html" $
phony "fast" $
needFast
2020-08-23 12:06:08 +00:00
needAll :: Action ()
2020-06-27 12:57:04 +00:00
needAll = do
2020-08-06 08:29:34 +00:00
needFast
allAsciiAction
allPdfAction
2020-06-22 10:27:45 +00:00
2020-06-27 12:57:04 +00:00
allRule :: Rules ()
2020-08-06 08:29:34 +00:00
allRule =
withTargetDocs "generate all, no optim" $
phony "all" $
needAll
2020-06-27 12:57:04 +00:00
fullRule :: Rules ()
2020-08-06 08:29:34 +00:00
fullRule =
withTargetDocs "generate all and optim" $
phony "full" $
need [optimDir </> "rss.xml"]
2020-06-27 12:57:04 +00:00
2020-06-22 10:08:07 +00:00
cleanRule :: Rules ()
cleanRule =
phony "clean" $ do
putInfo "Cleaning files in _site and _optim"
forM_ [siteDir,optimDir] $ flip removeFilesAfter ["**"]
2020-06-22 10:08:07 +00:00
2020-06-22 21:01:47 +00:00
mkGetTemplate :: Rules (FilePath -> Action Template)
mkGetTemplate = newCache $ \path -> do
fileContent <- readFile' path
let res = compileMustacheText "page" (toS fileContent)
case res of
Left _ -> fail "BAD"
Right template -> return template
2020-06-23 07:15:03 +00:00
tocRequested :: Text -> Bool
tocRequested fc =
let toc = fc & T.lines
& map T.toLower
& filter (T.isPrefixOf (T.pack "#+options: "))
& head
& fmap (filter (T.isPrefixOf (T.pack "toc:")) . T.words)
in toc == Just ["toc:t"]
2020-06-23 06:44:02 +00:00
2020-06-22 20:13:12 +00:00
mkGetPost :: Rules (FilePath -> Action BlogPost)
2020-06-22 10:08:07 +00:00
mkGetPost = newCache $ \path -> do
fileContent <- readFile' path
2020-06-23 07:15:03 +00:00
let toc = tocRequested (toS fileContent)
2020-06-23 17:28:41 +00:00
eitherResult <- liftIO $ Pandoc.runIO $ Readers.readOrg (def { readerStandalone = True }) (toS fileContent)
2020-06-22 10:08:07 +00:00
case eitherResult of
Left _ -> fail "BAD"
2020-06-23 07:15:03 +00:00
Right pandoc -> getBlogpostFromMetas path toc pandoc
2020-06-14 11:19:13 +00:00
2020-06-22 20:13:12 +00:00
mkGetPosts :: (FilePath -> Action b) -> Rules (() -> Action [b])
2020-06-22 10:08:07 +00:00
mkGetPosts getPost =
2020-06-25 05:10:00 +00:00
newCache $ \() -> mapM getPost =<< getDirectoryFiles "" ["src/posts//*.org"]