|
{-# LANGUAGE BangPatterns #-}
|
|
{-# LANGUAGE MultiWayIf #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
import qualified Data.ByteString.Lazy.Char8 as BS
|
|
import Data.Functor
|
|
|
|
import Control.Concurrent
|
|
import Control.Exception
|
|
import Control.DeepSeq (rnf)
|
|
import Control.Monad
|
|
|
|
import Text.Pandoc.Highlighting
|
|
import Text.Pandoc.Options
|
|
import Text.Pandoc.Definition
|
|
import Text.Sass.Functions
|
|
import Text.Pandoc.Walk (query, walkM)
|
|
|
|
import Hakyll.Core.Compiler
|
|
import Hakyll.Web.Sass
|
|
import Hakyll
|
|
|
|
import qualified Skylighting as Sky
|
|
|
|
import Data.ByteString.Lazy.Char8 (pack, unpack)
|
|
import qualified Network.URI.Encode as URI (encode)
|
|
|
|
import System.Environment
|
|
import System.Process
|
|
import System.Exit
|
|
import System.IO
|
|
|
|
import qualified Data.Text as T
|
|
import qualified Data.Text.Encoding as T
|
|
import Hakyll.Core.Compiler.Internal
|
|
import Data.List
|
|
import Data.Char
|
|
import qualified Data.Map.Strict as Map
|
|
import Data.Monoid
|
|
|
|
import Debug.Trace
|
|
import Data.Maybe
|
|
|
|
readerOpts :: ReaderOptions
|
|
readerOpts = def { readerExtensions = pandocExtensions
|
|
, readerIndentedCodeClasses = ["amulet"] }
|
|
|
|
writerOptions :: Compiler WriterOptions
|
|
writerOptions = do
|
|
syntaxMap <- loadAllSnapshots "syntax/*.xml" "syntax"
|
|
<&> foldr (Sky.addSyntaxDefinition . itemBody) Sky.defaultSyntaxMap
|
|
|
|
pure $ defaultHakyllWriterOptions
|
|
{ writerExtensions = extensionsFromList
|
|
[ Ext_tex_math_dollars
|
|
, Ext_tex_math_double_backslash
|
|
, Ext_latex_macros
|
|
] <> writerExtensions defaultHakyllWriterOptions
|
|
, writerSyntaxMap = syntaxMap
|
|
, writerHighlightStyle = Just kate
|
|
}
|
|
|
|
rssfeed :: FeedConfiguration
|
|
rssfeed
|
|
= FeedConfiguration { feedTitle = "Abigail's Blag: Latest articles"
|
|
, feedDescription = ""
|
|
, feedAuthorName = "Abigail Magalhães"
|
|
, feedAuthorEmail = "[email protected]"
|
|
, feedRoot = "https://abby.how"
|
|
}
|
|
|
|
conf :: Configuration
|
|
conf = def { destinationDirectory = ".site"
|
|
, storeDirectory = ".store"
|
|
, tmpDirectory = ".store/tmp"
|
|
, deployCommand = "./sync" }
|
|
|
|
tikzFilter :: Block -> Compiler Block
|
|
tikzFilter (CodeBlock (id, "tikzpicture":extraClasses, namevals) contents) =
|
|
(imageBlock . T.pack . ("data:image/svg+xml;utf8," <>) . URI.encode . filter (/= '\n') . T.unpack . itemBody <$>) $
|
|
makeItem contents
|
|
>>= loadAndApplyTemplate (fromFilePath "templates/tikz.tex") (bodyField "body") . fmap T.unpack
|
|
>>= withItemBody (pure . BS.fromStrict . T.encodeUtf8
|
|
>=> unixFilterLBS "rubber-pipe" ["--pdf"]
|
|
>=> unixFilterLBS "pdftocairo" ["-svg", "-", "-"]
|
|
>=> pure . T.decodeUtf8 . BS.toStrict)
|
|
. fmap T.pack
|
|
where imageBlock fname = Para [Image (id, "tikzpicture":extraClasses, namevals) [] (fname, "")]
|
|
tikzFilter x = return x
|
|
|
|
katexFilter :: Pandoc -> Compiler Pandoc
|
|
katexFilter (Pandoc meta doc) = do
|
|
id <- compilerUnderlying <$> compilerAsk
|
|
t <- getMetadata id
|
|
case lookupString "fastbuild" t of
|
|
Just _ -> pure (Pandoc meta doc)
|
|
Nothing -> Pandoc meta <$> walkM go doc
|
|
where
|
|
go :: Inline -> Compiler Inline
|
|
go (Math kind math) = unsafeCompiler $ do
|
|
let args =
|
|
case kind of
|
|
DisplayMath -> ["-Std"]
|
|
InlineMath -> ["-St"]
|
|
(contents, _) <- readProcessBS "node_modules/.bin/katex" args . BS.fromStrict . T.encodeUtf8 $ math
|
|
pure . RawInline "html" . T.init . T.init . T.decodeUtf8 . BS.toStrict $ contents
|
|
go x = pure x
|
|
|
|
estimateReadingTime :: Pandoc -> Pandoc
|
|
estimateReadingTime (Pandoc meta doc) = Pandoc meta doc' where
|
|
wordCount = T.pack (show (getSum (query inlineLen doc)))
|
|
|
|
inlineLen (Str s) = Sum (length (T.words s))
|
|
inlineLen _ = mempty
|
|
|
|
doc' = RawBlock "html" ("<span id=reading-length>" <> wordCount <> "</span>")
|
|
: doc
|
|
|
|
addLanguageTag :: Block -> Block
|
|
addLanguageTag block@(CodeBlock (identifier, classes@(language:classes'), kv) text) =
|
|
Div
|
|
( mempty
|
|
, "code-container":if haskv then "custom-tag":classes' else classes'
|
|
, []
|
|
)
|
|
[Plain [Span (mempty, [], []) [Str tag]], block]
|
|
where
|
|
language' = case T.uncons language of
|
|
Nothing -> mempty
|
|
Just (c, cs) -> T.cons (toUpper c) cs
|
|
tag = fromMaybe language' (lookup "tag" kv)
|
|
haskv = fromMaybe False (True <$ lookup "tag" kv)
|
|
|
|
addLanguageTag block@(CodeBlock (identifier, [], kv) text) = Div (mempty, ["code-container"], []) [block]
|
|
|
|
addLanguageTag x = x
|
|
|
|
sassImporter :: SassImporter
|
|
sassImporter = SassImporter 0 go where
|
|
go "normalize" _ = do
|
|
c <- readFile "node_modules/normalize.css/normalize.css"
|
|
pure [ SassImport { importPath = Nothing
|
|
, importAbsolutePath = Nothing
|
|
, importSource = Just c
|
|
, importSourceMap = Nothing
|
|
} ]
|
|
go _ _ = pure []
|
|
|
|
main :: IO ()
|
|
main = (*>) (setEnv "AMC_LIBRARY_PATH" "/usr/lib/amuletml/lib/") $ hakyllWith conf $ do
|
|
let compiler = do
|
|
wops <- writerOptions
|
|
pandocCompilerWithTransformM readerOpts wops $
|
|
walkM tikzFilter
|
|
>=> katexFilter
|
|
>=> pure . estimateReadingTime
|
|
>=> walkM (pure . addLanguageTag)
|
|
|
|
match "static/**/*" $ do
|
|
route idRoute
|
|
compile copyFileCompiler
|
|
|
|
match "css/**/*" $ do
|
|
route idRoute
|
|
compile copyFileCompiler
|
|
|
|
match "css/*.css" $ do
|
|
route idRoute
|
|
compile copyFileCompiler
|
|
|
|
match "css/*.scss" $ do
|
|
route $ setExtension "css"
|
|
compile $ sassCompilerWith def { sassOutputStyle = SassStyleCompressed
|
|
, sassImporters = Just [ sassImporter ]
|
|
}
|
|
|
|
match "diagrams/**/*.tex" $ do
|
|
route $ setExtension "svg"
|
|
compile $ getResourceBody
|
|
>>= loadAndApplyTemplate "templates/tikz.tex" (bodyField "body")
|
|
>>= withItemBody (return . pack
|
|
>=> unixFilterLBS "rubber-pipe" ["--pdf"]
|
|
>=> unixFilterLBS "pdftocairo" ["-svg", "-", "-"]
|
|
>=> return . unpack)
|
|
|
|
|
|
match "pages/posts/*" $ do
|
|
route $ metadataRoute pathFromTitle
|
|
|
|
compile $ compiler
|
|
>>= loadAndApplyTemplate "templates/post.html" postCtx
|
|
>>= saveSnapshot "content"
|
|
>>= loadAndApplyTemplate "templates/default.html" postCtx
|
|
>>= relativizeUrls
|
|
|
|
create ["archive.html"] $ do
|
|
route idRoute
|
|
compile $ do
|
|
posts <- recentFirst =<< loadAll "pages/posts/*"
|
|
let archiveCtx =
|
|
listField "posts" postCtx (return posts) <>
|
|
constField "title" "Archives" <>
|
|
defaultContext
|
|
makeItem ""
|
|
>>= loadAndApplyTemplate "templates/archive.html" archiveCtx
|
|
>>= loadAndApplyTemplate "templates/default.html" archiveCtx
|
|
>>= relativizeUrls
|
|
|
|
match "pages/*.html" $ do
|
|
route $ gsubRoute "pages/" (const "")
|
|
compile $ do
|
|
posts <- fmap (take 5) . recentFirst =<< loadAll "pages/posts/*"
|
|
let indexCtx =
|
|
listField "posts" postCtx (return posts) <>
|
|
constField "title" "Home" <>
|
|
defaultContext
|
|
getResourceBody
|
|
>>= applyAsTemplate indexCtx
|
|
>>= loadAndApplyTemplate "templates/default.html" indexCtx
|
|
>>= relativizeUrls
|
|
|
|
match "pages/*.md" $ do
|
|
route $ gsubRoute "pages/" (const "") <> setExtension "html"
|
|
compile $ compiler
|
|
>>= loadAndApplyTemplate "templates/default.html" defaultContext
|
|
>>= relativizeUrls
|
|
|
|
match "syntax/*.xml" $ compile $ do
|
|
path <- toFilePath <$> getUnderlying
|
|
contents <- itemBody <$> getResourceBody
|
|
debugCompiler ("Loaded syntax definition from " ++ show path)
|
|
res <- unsafeCompiler (Sky.parseSyntaxDefinitionFromString path contents)
|
|
_ <- saveSnapshot "syntax" =<< case res of
|
|
Left e -> fail e
|
|
Right x -> makeItem x
|
|
makeItem contents
|
|
|
|
match "templates/*" $ compile templateBodyCompiler
|
|
|
|
create ["feed.xml"] $ do
|
|
route idRoute
|
|
compile $ do
|
|
let feedCtx = postCtx <> bodyField "description"
|
|
posts <- (take 10 <$>) . recentFirst =<< loadAllSnapshots "pages/posts/*" "content"
|
|
renderRss rssfeed feedCtx posts
|
|
|
|
postCtx :: Context String
|
|
postCtx =
|
|
dateField "date" "%B %e, %Y"
|
|
<> defaultContext
|
|
|
|
readProcessBS :: FilePath -> [String] -> BS.ByteString -> IO (BS.ByteString, String)
|
|
readProcessBS path args input =
|
|
let process = (proc path args)
|
|
{ std_in = CreatePipe
|
|
, std_out = CreatePipe
|
|
, std_err = CreatePipe
|
|
}
|
|
in withCreateProcess process $ \stdin stdout stderr ph ->
|
|
case (stdin, stdout, stderr) of
|
|
(Nothing, _, _) -> fail "Failed to get a stdin handle."
|
|
(_, Nothing, _) -> fail "Failed to get a stdout handle."
|
|
(_, _, Nothing) -> fail "Failed to get a stderr handle."
|
|
(Just stdin, Just stdout, Just stderr) -> do
|
|
out <- BS.hGetContents stdout
|
|
err <- hGetContents stderr
|
|
|
|
withForkWait (evaluate $ rnf out) $ \waitOut ->
|
|
withForkWait (evaluate $ rnf err) $ \waitErr -> do
|
|
-- Write input and close.
|
|
BS.hPutStr stdin input
|
|
hClose stdin
|
|
|
|
-- wait on the output
|
|
waitOut
|
|
waitErr
|
|
|
|
hClose stdout
|
|
hClose stderr
|
|
|
|
-- wait on the process
|
|
ex <- waitForProcess ph
|
|
case ex of
|
|
ExitSuccess -> pure (out, err)
|
|
ExitFailure ex -> fail (err ++ "Exited with " ++ show ex)
|
|
|
|
where
|
|
withForkWait :: IO () -> (IO () -> IO a) -> IO a
|
|
withForkWait async body = do
|
|
waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
|
|
mask $ \restore -> do
|
|
tid <- forkIO $ try (restore async) >>= putMVar waitVar
|
|
let wait = takeMVar waitVar >>= either throwIO return
|
|
restore (body wait) `onException` killThread tid
|
|
|
|
pathFromTitle :: Metadata -> Routes
|
|
pathFromTitle meta =
|
|
let
|
|
declaredCategory =
|
|
case lookupString "category" meta of
|
|
Just s -> ((s ++ "/") ++)
|
|
Nothing -> ("posts/" <>)
|
|
|
|
!titleString =
|
|
case lookupString "title" meta of
|
|
Just s -> s
|
|
Nothing -> error "post has no title?"
|
|
|
|
title = filter (/= "") . map (filter isAlphaNum . map toLower) . words $ titleString
|
|
|
|
(category, title') =
|
|
if | "or" `elem` title -> (declaredCategory, takeWhile (/= "or") title)
|
|
| ["a", "quickie"] `isPrefixOf` title -> (("quick/" ++), drop 2 title)
|
|
| otherwise -> (declaredCategory, title)
|
|
in
|
|
case lookupString "path" meta of
|
|
Just p -> constRoute (category (p <> ".html"))
|
|
Nothing -> constRoute (category (intercalate "-" title' <> ".html"))
|
|
|
|
foldMapM :: (Monad w, Monoid m, Foldable f) => (a -> w m) -> f a -> w m
|
|
foldMapM k = foldr (\x y -> do { m <- k x; (m <>) <$> y }) (pure mempty)
|