-
Notifications
You must be signed in to change notification settings - Fork 1
/
ssg.hs
233 lines (206 loc) · 7.43 KB
/
ssg.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad
import qualified Data.Char as Char
import Data.Function ((&))
import Data.List (isInfixOf, isPrefixOf)
import Hakyll hiding (fromList)
import System.FilePath
import Text.HTML.TagSoup (Tag (..))
import Text.Blaze ((!))
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import qualified Text.Pandoc as Pandoc
import Text.Pandoc.Builder
import Text.Pandoc.Options
import Text.Pandoc.Walk
import Text.Read (readMaybe)
frontpagePosts :: Int
frontpagePosts = 20
toLower :: Functor f => f Char.Char -> f Char.Char
toLower = fmap Char.toLower
-- | Compiler for each blog post page
postCompiler :: Compiler (Item String)
postCompiler = do
ident <- getUnderlying
toc <- getMetadataField ident "toc"
let writerOpts = case toc >>= readMaybe of
Nothing -> defaultHakyllWriterOptions
Just depth ->
defaultHakyllWriterOptions
{ writerTableOfContents = True,
writerTOCDepth = depth,
writerTemplate = Just tocTemplate
}
pandoc = pandocCompilerWith defaultHakyllReaderOptions
fmap (withTagList convertVideoLinks . withUrls rewriteOrgUrl . demoteHeaders) <$> pandoc writerOpts
where
tocTemplate =
either error id $
either (error . show) id $
Pandoc.runPure $
Pandoc.runWithDefaultPartials $
Pandoc.compileTemplate "" "<div class=\"toc\"><h1>Contents</h1>\n$toc$\n</div>\n$body$"
-- | Rewrite URLs to (local) .org files to .html.
rewriteOrgUrl :: String -> String
rewriteOrgUrl url
| not ("://" `isInfixOf` url) =
split "::" url & \case
[] -> url
[u] -> orgToHtml u
(u : section : _) -> orgToHtml u <> toAnchor section
| otherwise = url
where
orgToHtml u = maybe u (`addExtension` ".html") (stripExtension ".org" u)
split :: String -> String -> [String]
split delim = split' [] []
where
split' accum strings [] = reverse (reverse accum : strings)
split' accum strings i@(c : rest)
| delim `isPrefixOf` i = split' [] (reverse accum : strings) (drop (length delim) i)
| otherwise = split' (c : accum) strings rest
toAnchor :: String -> String
toAnchor = \case
('*' : rest) -> '#' : map (\case ' ' -> '-'; x -> Char.toLower x) rest
input -> input
-- | Returns true for any post which is not a preview
--
-- Useful for hiding experimental posts from the frontpage, posts & feed.xml.
-- Posts are still built and accessible, but only through direct link.
postIsNotPreview :: Item String -> Compiler Bool
postIsNotPreview item = do
preview <- getMetadataField (itemIdentifier item) "preview"
pure (preview /= Just "true")
-- | Convert links to videos to <video> HTML elements
--
-- Pandoc rewrites all org links to <a> tags. For video files this should rather
-- be an embedded video HTML element.
convertVideoLinks :: [Tag String] -> [Tag String]
convertVideoLinks (TagOpen "a" attrs : TagText txt : TagClose "a" : rest) =
case videoUrl of
Just url ->
TagOpen "video" (("src", url) : defVideoAttrs)
: TagClose "video"
: convertVideoLinks rest
_ -> TagOpen "a" attrs : TagText txt : TagClose "a" : convertVideoLinks rest
where
defVideoAttrs = [("autoplay", ""), ("controls", ""), ("loop", "")]
videoUrl = case splitExtension <$> lookup "href" attrs of
Just (path, ".webm") -> Just $ path <> ".webm"
_ -> Nothing
convertVideoLinks (tag : rest) = tag : convertVideoLinks rest
convertVideoLinks [] = []
main :: IO ()
main = hakyllWith config $ do
match "images/*" $ do
route idRoute
compile copyFileCompiler
match "js/*" $ do
route idRoute
compile copyFileCompiler
match "css/*" $ do
route idRoute
compile compressCssCompiler
tags <- buildTags "posts/*" (fromCapture "tags/*.html" . toLower)
-- Individual posts
match "posts/*" $ do
route $ setExtension "html"
compile $ do
let
renderLink _ Nothing = Nothing
renderLink tag (Just url) = Just $
H.li $ H.a ! A.href (H.toValue ("/" <> url)) $ H.toHtml tag
tagsCtx = tagsFieldWith getTags renderLink mconcat "tags" tags
postCompiler
>>= loadAndApplyTemplate "templates/post.html" (tagsCtx <> postCtx)
>>= saveSnapshot "content"
>>= loadAndApplyTemplate "templates/default.html" postCtx
>>= relativizeUrls
-- Full posts listing
create ["posts.html"] $ do
route idRoute
compile $ do
posts <-
loadAll "posts/*"
>>= filterM postIsNotPreview
>>= recentFirst
let postsCtx =
constField "title" "Posts"
<> listField "posts" postCtx (return posts)
<> defaultContext
makeItem ""
>>= loadAndApplyTemplate "templates/posts.html" postsCtx
>>= loadAndApplyTemplate "templates/default.html" postsCtx
>>= relativizeUrls
-- Individual tag pages
tagsRules tags $ \tagStr tagsPattern -> do
route idRoute
compile $ do
posts <- loadAll tagsPattern >>= filterM postIsNotPreview >>= recentFirst
let postsCtx =
constField "title" tagStr
<> listField "posts" postCtx (return posts)
<> defaultContext
makeItem ""
>>= loadAndApplyTemplate "templates/posts.html" postsCtx
>>= loadAndApplyTemplate "templates/default.html" postsCtx
>>= relativizeUrls
match "projects.org" $ do
route $ setExtension "html"
let saveProjectTitles p = do
projects <- writePandoc <$> makeItem (extractHeaders 10 p)
void $ saveSnapshot "headers" projects
pure p
compile $
pandocCompilerWithTransformM def def saveProjectTitles
>>= loadAndApplyTemplate "templates/default.html" postCtx
create ["feed.xml"] $ do
route idRoute
compile $ do
publicPosts <-
loadAllSnapshots "posts/*" "content"
>>= filterM postIsNotPreview
>>= recentFirst
let feedCtx = postCtx <> bodyField "description"
renderRss feedConfig feedCtx publicPosts
match "index.html" $ do
route idRoute
compile $ do
publicPosts <-
loadAllSnapshots "posts/*" "content"
>>= filterM postIsNotPreview
>>= recentFirst
projects <- loadSnapshotBody "projects.org" "headers"
let indexCtx =
constField "title" "Home"
<> constField "projects" projects
<> listField "posts" postCtx (pure $ take frontpagePosts publicPosts)
<> defaultContext
getResourceBody
>>= applyAsTemplate indexCtx
>>= loadAndApplyTemplate "templates/default.html" indexCtx
>>= relativizeUrls
match "templates/*" (compile templateBodyCompiler)
config :: Configuration
config =
defaultConfiguration
{ destinationDirectory = "public"
}
postCtx :: Context String
postCtx = dateField "date" "%F" <> defaultContext
feedConfig :: FeedConfiguration
feedConfig =
FeedConfiguration
{ feedTitle = "myme.no tech blog",
feedDescription = "Blog posts with technincal and programming related content.",
feedAuthorName = "Martin Myrseth",
feedAuthorEmail = "myrseth@gmail.com",
feedRoot = "https://myme.no"
}
-- | Extract headers from a Pandoc document
extractHeaders :: Int -> Pandoc -> Pandoc
extractHeaders n p = doc (bulletList links)
where
links = plain . fromList . take n <$> query headers p
headers (Header lvl _ content) | lvl > 1 = [content]
headers _ = []