{-# 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 = "magalhaes.alcantara@pucpr.edu.br" , 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" ("" <> wordCount <> "") : 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)