my blog lives here now
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

321 lines
11 KiB

{-# 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)