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.

320 lines
11 KiB

6 years ago
6 years ago
6 years ago
6 years ago
  1. {-# LANGUAGE BangPatterns #-}
  2. {-# LANGUAGE MultiWayIf #-}
  3. {-# LANGUAGE OverloadedStrings #-}
  4. import qualified Data.ByteString.Lazy.Char8 as BS
  5. import Data.Functor
  6. import Control.Concurrent
  7. import Control.Exception
  8. import Control.DeepSeq (rnf)
  9. import Control.Monad
  10. import Text.Pandoc.Highlighting
  11. import Text.Pandoc.Options
  12. import Text.Pandoc.Definition
  13. import Text.Sass.Functions
  14. import Text.Pandoc.Walk (query, walkM)
  15. import Hakyll.Core.Compiler
  16. import Hakyll.Web.Sass
  17. import Hakyll
  18. import qualified Skylighting as Sky
  19. import Data.ByteString.Lazy.Char8 (pack, unpack)
  20. import qualified Network.URI.Encode as URI (encode)
  21. import System.Environment
  22. import System.Process
  23. import System.Exit
  24. import System.IO
  25. import qualified Data.Text as T
  26. import qualified Data.Text.Encoding as T
  27. import Hakyll.Core.Compiler.Internal
  28. import Data.List
  29. import Data.Char
  30. import qualified Data.Map.Strict as Map
  31. import Data.Monoid
  32. import Debug.Trace
  33. import Data.Maybe
  34. readerOpts :: ReaderOptions
  35. readerOpts = def { readerExtensions = pandocExtensions
  36. , readerIndentedCodeClasses = ["amulet"] }
  37. writerOptions :: Compiler WriterOptions
  38. writerOptions = do
  39. syntaxMap <- loadAllSnapshots "syntax/*.xml" "syntax"
  40. <&> foldr (Sky.addSyntaxDefinition . itemBody) Sky.defaultSyntaxMap
  41. pure $ defaultHakyllWriterOptions
  42. { writerExtensions = extensionsFromList
  43. [ Ext_tex_math_dollars
  44. , Ext_tex_math_double_backslash
  45. , Ext_latex_macros
  46. ] <> writerExtensions defaultHakyllWriterOptions
  47. , writerSyntaxMap = syntaxMap
  48. , writerHighlightStyle = Just kate
  49. }
  50. rssfeed :: FeedConfiguration
  51. rssfeed
  52. = FeedConfiguration { feedTitle = "Abigail's Blag: Latest articles"
  53. , feedDescription = ""
  54. , feedAuthorName = "Abigail Magalhães"
  55. , feedAuthorEmail = "[email protected]"
  56. , feedRoot = "https://abby.how"
  57. }
  58. conf :: Configuration
  59. conf = def { destinationDirectory = ".site"
  60. , storeDirectory = ".store"
  61. , tmpDirectory = ".store/tmp"
  62. , deployCommand = "./sync" }
  63. tikzFilter :: Block -> Compiler Block
  64. tikzFilter (CodeBlock (id, "tikzpicture":extraClasses, namevals) contents) =
  65. (imageBlock . T.pack . ("data:image/svg+xml;utf8," <>) . URI.encode . filter (/= '\n') . T.unpack . itemBody <$>) $
  66. makeItem contents
  67. >>= loadAndApplyTemplate (fromFilePath "templates/tikz.tex") (bodyField "body") . fmap T.unpack
  68. >>= withItemBody (pure . BS.fromStrict . T.encodeUtf8
  69. >=> unixFilterLBS "rubber-pipe" ["--pdf"]
  70. >=> unixFilterLBS "pdftocairo" ["-svg", "-", "-"]
  71. >=> pure . T.decodeUtf8 . BS.toStrict)
  72. . fmap T.pack
  73. where imageBlock fname = Para [Image (id, "tikzpicture":extraClasses, namevals) [] (fname, "")]
  74. tikzFilter x = return x
  75. katexFilter :: Pandoc -> Compiler Pandoc
  76. katexFilter (Pandoc meta doc) = do
  77. id <- compilerUnderlying <$> compilerAsk
  78. t <- getMetadata id
  79. case lookupString "fastbuild" t of
  80. Just _ -> pure (Pandoc meta doc)
  81. Nothing -> Pandoc meta <$> walkM go doc
  82. where
  83. go :: Inline -> Compiler Inline
  84. go (Math kind math) = unsafeCompiler $ do
  85. let args =
  86. case kind of
  87. DisplayMath -> ["-Std"]
  88. InlineMath -> ["-St"]
  89. (contents, _) <- readProcessBS "node_modules/.bin/katex" args . BS.fromStrict . T.encodeUtf8 $ math
  90. pure . RawInline "html" . T.init . T.init . T.decodeUtf8 . BS.toStrict $ contents
  91. go x = pure x
  92. estimateReadingTime :: Pandoc -> Pandoc
  93. estimateReadingTime (Pandoc meta doc) = Pandoc meta doc' where
  94. wordCount = T.pack (show (getSum (query inlineLen doc)))
  95. inlineLen (Str s) = Sum (length (T.words s))
  96. inlineLen _ = mempty
  97. doc' = RawBlock "html" ("<span id=reading-length>" <> wordCount <> "</span>")
  98. : doc
  99. addLanguageTag :: Block -> Block
  100. addLanguageTag block@(CodeBlock (identifier, classes@(language:classes'), kv) text) =
  101. Div
  102. ( mempty
  103. , "code-container":if haskv then "custom-tag":classes' else classes'
  104. , []
  105. )
  106. [Plain [Span (mempty, [], []) [Str tag]], block]
  107. where
  108. language' = case T.uncons language of
  109. Nothing -> mempty
  110. Just (c, cs) -> T.cons (toUpper c) cs
  111. tag = fromMaybe language' (lookup "tag" kv)
  112. haskv = fromMaybe False (True <$ lookup "tag" kv)
  113. addLanguageTag block@(CodeBlock (identifier, [], kv) text) = Div (mempty, ["code-container"], []) [block]
  114. addLanguageTag x = x
  115. sassImporter :: SassImporter
  116. sassImporter = SassImporter 0 go where
  117. go "normalize" _ = do
  118. c <- readFile "node_modules/normalize.css/normalize.css"
  119. pure [ SassImport { importPath = Nothing
  120. , importAbsolutePath = Nothing
  121. , importSource = Just c
  122. , importSourceMap = Nothing
  123. } ]
  124. go _ _ = pure []
  125. main :: IO ()
  126. main = (*>) (setEnv "AMC_LIBRARY_PATH" "/usr/lib/amuletml/lib/") $ hakyllWith conf $ do
  127. let compiler = do
  128. wops <- writerOptions
  129. pandocCompilerWithTransformM readerOpts wops $
  130. walkM tikzFilter
  131. >=> katexFilter
  132. >=> pure . estimateReadingTime
  133. >=> walkM (pure . addLanguageTag)
  134. match "static/**/*" $ do
  135. route idRoute
  136. compile copyFileCompiler
  137. match "css/**/*" $ do
  138. route idRoute
  139. compile copyFileCompiler
  140. match "css/*.css" $ do
  141. route idRoute
  142. compile copyFileCompiler
  143. match "css/*.scss" $ do
  144. route $ setExtension "css"
  145. compile $ sassCompilerWith def { sassOutputStyle = SassStyleCompressed
  146. , sassImporters = Just [ sassImporter ]
  147. }
  148. match "diagrams/**/*.tex" $ do
  149. route $ setExtension "svg"
  150. compile $ getResourceBody
  151. >>= loadAndApplyTemplate "templates/tikz.tex" (bodyField "body")
  152. >>= withItemBody (return . pack
  153. >=> unixFilterLBS "rubber-pipe" ["--pdf"]
  154. >=> unixFilterLBS "pdftocairo" ["-svg", "-", "-"]
  155. >=> return . unpack)
  156. match "pages/posts/*" $ do
  157. route $ metadataRoute pathFromTitle
  158. compile $ compiler
  159. >>= loadAndApplyTemplate "templates/post.html" postCtx
  160. >>= saveSnapshot "content"
  161. >>= loadAndApplyTemplate "templates/default.html" postCtx
  162. >>= relativizeUrls
  163. create ["archive.html"] $ do
  164. route idRoute
  165. compile $ do
  166. posts <- recentFirst =<< loadAll "pages/posts/*"
  167. let archiveCtx =
  168. listField "posts" postCtx (return posts) <>
  169. constField "title" "Archives" <>
  170. defaultContext
  171. makeItem ""
  172. >>= loadAndApplyTemplate "templates/archive.html" archiveCtx
  173. >>= loadAndApplyTemplate "templates/default.html" archiveCtx
  174. >>= relativizeUrls
  175. match "pages/*.html" $ do
  176. route $ gsubRoute "pages/" (const "")
  177. compile $ do
  178. posts <- fmap (take 5) . recentFirst =<< loadAll "pages/posts/*"
  179. let indexCtx =
  180. listField "posts" postCtx (return posts) <>
  181. constField "title" "Home" <>
  182. defaultContext
  183. getResourceBody
  184. >>= applyAsTemplate indexCtx
  185. >>= loadAndApplyTemplate "templates/default.html" indexCtx
  186. >>= relativizeUrls
  187. match "pages/*.md" $ do
  188. route $ gsubRoute "pages/" (const "") <> setExtension "html"
  189. compile $ compiler
  190. >>= loadAndApplyTemplate "templates/default.html" defaultContext
  191. >>= relativizeUrls
  192. match "syntax/*.xml" $ compile $ do
  193. path <- toFilePath <$> getUnderlying
  194. contents <- itemBody <$> getResourceBody
  195. debugCompiler ("Loaded syntax definition from " ++ show path)
  196. res <- unsafeCompiler (Sky.parseSyntaxDefinitionFromString path contents)
  197. _ <- saveSnapshot "syntax" =<< case res of
  198. Left e -> fail e
  199. Right x -> makeItem x
  200. makeItem contents
  201. match "templates/*" $ compile templateBodyCompiler
  202. create ["feed.xml"] $ do
  203. route idRoute
  204. compile $ do
  205. let feedCtx = postCtx <> bodyField "description"
  206. posts <- (take 10 <$>) . recentFirst =<< loadAllSnapshots "pages/posts/*" "content"
  207. renderRss rssfeed feedCtx posts
  208. postCtx :: Context String
  209. postCtx =
  210. dateField "date" "%B %e, %Y"
  211. <> defaultContext
  212. readProcessBS :: FilePath -> [String] -> BS.ByteString -> IO (BS.ByteString, String)
  213. readProcessBS path args input =
  214. let process = (proc path args)
  215. { std_in = CreatePipe
  216. , std_out = CreatePipe
  217. , std_err = CreatePipe
  218. }
  219. in withCreateProcess process $ \stdin stdout stderr ph ->
  220. case (stdin, stdout, stderr) of
  221. (Nothing, _, _) -> fail "Failed to get a stdin handle."
  222. (_, Nothing, _) -> fail "Failed to get a stdout handle."
  223. (_, _, Nothing) -> fail "Failed to get a stderr handle."
  224. (Just stdin, Just stdout, Just stderr) -> do
  225. out <- BS.hGetContents stdout
  226. err <- hGetContents stderr
  227. withForkWait (evaluate $ rnf out) $ \waitOut ->
  228. withForkWait (evaluate $ rnf err) $ \waitErr -> do
  229. -- Write input and close.
  230. BS.hPutStr stdin input
  231. hClose stdin
  232. -- wait on the output
  233. waitOut
  234. waitErr
  235. hClose stdout
  236. hClose stderr
  237. -- wait on the process
  238. ex <- waitForProcess ph
  239. case ex of
  240. ExitSuccess -> pure (out, err)
  241. ExitFailure ex -> fail (err ++ "Exited with " ++ show ex)
  242. where
  243. withForkWait :: IO () -> (IO () -> IO a) -> IO a
  244. withForkWait async body = do
  245. waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
  246. mask $ \restore -> do
  247. tid <- forkIO $ try (restore async) >>= putMVar waitVar
  248. let wait = takeMVar waitVar >>= either throwIO return
  249. restore (body wait) `onException` killThread tid
  250. pathFromTitle :: Metadata -> Routes
  251. pathFromTitle meta =
  252. let
  253. declaredCategory =
  254. case lookupString "category" meta of
  255. Just s -> ((s ++ "/") ++)
  256. Nothing -> ("posts/" <>)
  257. !titleString =
  258. case lookupString "title" meta of
  259. Just s -> s
  260. Nothing -> error "post has no title?"
  261. title = filter (/= "") . map (filter isAlphaNum . map toLower) . words $ titleString
  262. (category, title') =
  263. if | "or" `elem` title -> (declaredCategory, takeWhile (/= "or") title)
  264. | ["a", "quickie"] `isPrefixOf` title -> (("quick/" ++), drop 2 title)
  265. | otherwise -> (declaredCategory, title)
  266. in
  267. case lookupString "path" meta of
  268. Just p -> constRoute (category (p <> ".html"))
  269. Nothing -> constRoute (category (intercalate "-" title' <> ".html"))
  270. foldMapM :: (Monad w, Monoid m, Foldable f) => (a -> w m) -> f a -> w m
  271. foldMapM k = foldr (\x y -> do { m <- k x; (m <>) <$> y }) (pure mempty)