-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathblog.hs
625 lines (552 loc) · 22.5 KB
/
blog.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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
{-# LANGUAGE OverloadedStrings #-}
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Either (fromRight)
import Data.Monoid ((<>), mconcat)
import Data.Functor ((<$>), fmap, (<&>))
import Data.Char (isSpace, toLower, toUpper)
import Data.List (intercalate, intersperse, foldl', isPrefixOf, isSuffixOf)
import Data.Text (pack)
import Data.Time.Clock (UTCTime(..))
import Control.Applicative ((<|>), Alternative(..))
import Control.Monad (msum, filterM, (<=<), liftM, filterM)
import Control.Monad.Fail (MonadFail)
import System.Environment (getArgs)
import Data.Time.Format (TimeLocale, defaultTimeLocale, parseTimeM, formatTime)
import Text.Blaze.Html (toHtml, toValue, (!))
import Text.Blaze.Html5.Attributes (href, class_)
import Text.Blaze.Html.Renderer.String (renderHtml)
import Text.HTML.TagSoup (Tag(..))
import qualified Data.Map as M
import qualified Text.Blaze.Html5 as H
import System.FilePath
import Text.Pandoc.Options
import Text.Pandoc.Templates
import Text.Pandoc.Class
import Hakyll
--------------------------------------------------------------------------------
-- SITE
--------------------------------------------------------------------------------
main :: IO ()
main = do
isWatching <- fmap (== "watch") . listToMaybe <$> getArgs
let allPattern =
case isWatching of
Just True -> blogPattern .||. draftPattern
_ -> blogPattern
hakyllWith config $ do
excludePattern <- fmap fromList $ includeTagM "icelandic" <=< getMatches $ blogPattern
let visiblePattern =
allPattern .&&. complement excludePattern
pages <- buildPages visiblePattern (fromCapture "*/index.html" . show)
categories <- buildCategories visiblePattern (fromCapture "*/index.html")
tags <- buildTags visiblePattern (fromCapture "tags/*/index.html")
-- static pages
match "*.md" $ do
route pageRoute
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/page-detail.html" defaultCtx
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= relativizeUrls
-- index
create ["index.html"] $ do
route idRoute
compile $ makeItem ""
>>= loadAndApplyTemplate "templates/blog-list.html" (blogCtx 1 pages categories tags)
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= indexCompiler
>>= relativizeUrls
-- blogs
match allPattern $ do
route blogRoute
compile $ blogCompiler
>>= saveSnapshot blogSnapshot
>>= loadAndApplyTemplate "templates/blog-detail.html" (blogDetailCtx categories tags)
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= indexCompiler
>>= relativizeUrls
-- blog pages
paginateRules pages $ \i _ -> do
route idRoute
compile $ makeItem (show i)
>>= loadAndApplyTemplate "templates/blog-list.html" (blogCtx i pages categories tags)
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= indexCompiler
>>= relativizeUrls
-- blog category index
tagsRules categories $ \category pattern -> do
catPages <- buildPages pattern (\i -> fromCaptures "*/*/index.html" [category, show i])
route idRoute
compile $ makeItem category
>>= loadAndApplyTemplate "templates/blog-list.html" (blogCtx 1 catPages categories tags)
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= indexCompiler
>>= relativizeUrls
paginateRules catPages $ \i _ -> do -- blog category pages
route idRoute
compile $ makeItem category
>>= loadAndApplyTemplate "templates/blog-list.html" (blogCtx i catPages categories tags)
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= indexCompiler
>>= relativizeUrls
-- blog tags index
tagsRules tags $ \tag pattern -> do
tagPages <- buildPages pattern (\i -> fromCaptures "tags/*/*/index.html" [tag, show i])
route idRoute
compile $ makeItem tag
>>= loadAndApplyTemplate "templates/blog-list.html" (blogCtx 1 tagPages categories tags)
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= indexCompiler
>>= relativizeUrls
paginateRules tagPages $ \i _ -> do -- blog tags pages
route idRoute
compile $
makeItem tag
>>= loadAndApplyTemplate "templates/blog-list.html" (blogCtx i tagPages categories tags)
>>= loadAndApplyTemplate "templates/default.html" defaultCtx
>>= indexCompiler
>>= relativizeUrls
-- decks
match "decks/*.md" $ do
route decksRoute
compile $ blogCompiler
>>= saveSnapshot decksSnapshot
>>= loadAndApplyTemplate "templates/decks-detail.html" decksDetailCtx
>>= indexCompiler
>>= relativizeUrls
match "decks/**" $ do
route decksAssetsRoute
compile copyFileCompiler
create ["decks/index.html"] $ do
route idRoute
compile $ makeItem "decks"
>>= loadAndApplyTemplate "templates/decks-list.html" decksCtx
>>= loadAndApplyTemplate "templates/default.html" decksCtx
>>= indexCompiler
>>= relativizeUrls
-- atom
create ["atom.xml"] $ do
route idRoute
compile $ renderBlogAtom <=< fmap (take 20) . loadBlogs $ visiblePattern
-- static content
match "static/**" $ do
route rootRoute
compile copyFileCompiler
-- static css
match "css/**.css" $ do
route idRoute
compile compressCssCompiler
match ("css/**.scss" .&&. complement "css/**_*.scss") $ do
route $ setExtension "css"
compile $ sassCompiler <&> fmap compressCss
match "css/webfonts/*" $ do
route idRoute
compile copyFileCompiler
create [".nojekyll"] $ do -- disable GitHub's Jekyll
route idRoute
compile copyFileCompiler
-- templates
match "templates/*.html" $
compile templateCompiler
--------------------------------------------------------------------------------
-- CONFIGURATION
--------------------------------------------------------------------------------
blogPattern :: Pattern
blogPattern = "blog/**"
draftPattern :: Pattern
draftPattern = "drafts/**"
blogSnapshot :: Snapshot
blogSnapshot = "blog-content"
blogPerPage :: Int
blogPerPage = 4
blogTitle :: String
blogTitle = "Crypto and Code"
blogDescription :: String
blogDescription = "My thoughts on blockchains and software"
blogAuthor :: String
blogAuthor = "Gísli Kristjánsson"
blogAuthorEmail :: String
blogAuthorEmail = "gislik@hamstur.is"
blogRoot :: String
blogRoot = "https://gisli.hamstur.is"
decksSnapshot :: Snapshot
decksSnapshot = "decks-content"
feedConfiguration :: FeedConfiguration
feedConfiguration =
FeedConfiguration
{ feedTitle = blogTitle
, feedDescription = blogDescription
, feedAuthorName = blogAuthor
, feedAuthorEmail = blogAuthorEmail
, feedRoot = blogRoot
}
blogReaderOptions :: ReaderOptions
blogReaderOptions =
defaultHakyllReaderOptions
{
readerExtensions =
readerExtensions defaultHakyllReaderOptions <> extensionsFromList
[
Ext_tex_math_single_backslash -- TeX math btw (..) [..]
, Ext_tex_math_double_backslash -- TeX math btw \(..\) \[..\]
, Ext_tex_math_dollars -- TeX math between $..$ or $$..$$
, Ext_latex_macros -- Parse LaTeX macro definitions (for math only)
, Ext_inline_code_attributes -- Ext_inline_code_attributes
, Ext_abbreviations -- PHP markdown extra abbreviation definitions
]
}
-- blogWriterOptions configures pandoc to include a table of contents
-- and uses MathJax to render math.
blogWriterOptions :: WriterOptions
blogWriterOptions =
defaultHakyllWriterOptions
{
writerHTMLMathMethod = MathJax ""
, writerTableOfContents = True
, writerNumberSections = True
, writerTOCDepth = 2
, writerTemplate =
let
toc = "$toc$" :: String
body = "$body$" :: String
html = pack . renderHtml $ do
H.div ! class_ "toc" $
toHtml toc
toHtml body
template = fromRight mempty <$> compileTemplate "" html
runPureWithDefaultPartials = runPure . runWithDefaultPartials
eitherToMaybe = either (const Nothing) Just
in
eitherToMaybe (runPureWithDefaultPartials template)
}
config :: Configuration
config =
defaultConfiguration {
ignoreFile = ignoreFile'
}
where
ignoreFile' path
| "#" `isPrefixOf` fileName = True
| "~" `isSuffixOf` fileName = True
| ".swp" `isSuffixOf` fileName = True
| otherwise = False
where
fileName = takeFileName path
--------------------------------------------------------------------------------
-- CONTEXTS
--------------------------------------------------------------------------------
pageTitleField :: String -> Context String
pageTitleField key =
aliasContext alias metadataField <> -- use page title from metadata
pathTitleField key <> -- or read from the path
constField key "Crypto and Code" -- alternatively use this
where
alias x | x == key = "title"
alias x = x
defaultCtx :: Context String
defaultCtx =
bodyField "page.body" <>
pageTitleField "page.title" <>
constField "page.description" blogDescription <>
constField "page.root" blogRoot <>
urlField' "page.url" <>
pathField "page.path" <>
polishField "polish" <>
metadataField
blogCtx :: PageNumber -> Paginate -> Tags -> Tags -> Context String
blogCtx i pages categories tags =
listField "blogs" (blogDetailCtx categories tags) (loadBlogs pattern) <>
categoryListField "categories" categories <>
tagsListField "tags" tags <>
pagesField i <>
defaultCtx
where
pattern = fromList . fromMaybe [] . M.lookup i . paginateMap $ pages
pagesField = aliasContext alias . paginateContext pages
alias "pages.first.number" = "firstPageNum"
alias "pages.first.url" = "firstPageUrl"
alias "pages.next.number" = "nextPageNum"
alias "pages.next.url" = "nextPageUrl"
alias "pages.previous.number" = "previousPageNum"
alias "pages.previous.url" = "previousPageUrl"
alias "pages.last.number" = "lastPageNum"
alias "pages.last.url" = "lastPageUrl"
alias "pages.current.number" = "currentPageNum"
alias "pages.count" = "numPages"
alias x = x
blogDetailCtx :: Tags -> Tags -> Context String
blogDetailCtx categories tags =
pageTitleField "blog.title" <>
dateField "blog.date" "%B %e, %Y" <>
urlField' "blog.url" <>
categoryField' "blog.category" categories <>
tagsField' "blog.tags" tags <>
field "blog.next.url" nextBlog <>
field "blog.previous.url" previousBlog <>
summaryField "blog.summary" <>
readingTimeField "blog.reading.time" blogSnapshot <>
defaultCtx
decksCtx :: Context String
decksCtx =
listField "decks" decksDetailCtx (loadDecks "decks/*.md") <>
defaultCtx
decksDetailCtx :: Context String
decksDetailCtx =
dateField "date" "%B %e, %Y" <>
urlField' "url" <>
defaultCtx <>
constField "theme" "black"
atomCtx :: Context String
atomCtx =
mapContext cdata (pageTitleField "title") <>
aliasContext alias metadataField <> -- description from metadata
teaserField "description" blogSnapshot <> -- teaser is description
previewField "description" blogSnapshot <> -- first paragraph is description
urlField' "url"
where
alias "description" = "summary"
alias x = x
cdata s | "<![CDATA[" `isPrefixOf` s = s
cdata s = "<![CDATA[" <> s <> "]]>"
--------------------------------------------------------------------------------
-- HELPERS
--------------------------------------------------------------------------------
-- compilers
blogCompiler :: Compiler (Item String)
blogCompiler = do
ident <- getUnderlying
toc <- getMetadataField ident "withtoc"
pandocCompilerWith blogReaderOptions (maybe defaultOptions blogOptions toc)
where
defaultOptions = defaultHakyllWriterOptions
blogOptions = const blogWriterOptions
indexCompiler :: Item String -> Compiler (Item String)
indexCompiler = withItemBody (return . withTags dropIndex)
where
dropIndex (TagOpen "a" attrs) = TagOpen "a" (dropIndex' <$> attrs)
dropIndex tag = tag
dropIndex' ("href", url) | not (isExternal url) = ("href", dropFileName url <> takeHash url)
dropIndex' z = z
takeHash = dropWhile (/= '#')
sassCompiler :: Compiler (Item String)
sassCompiler = do
ident <- getUnderlying
output <- unixFilter "sass" [toFilePath ident] ""
makeItem output
loadBlogs :: Pattern -> Compiler [Item String]
loadBlogs =
recentFirst <=< flip loadAllSnapshots blogSnapshot
nextBlog :: Item String -> Compiler String
nextBlog blog = do
blogs <- loadBlogs blogPattern
let idents = map itemIdentifier blogs
ident = itemAfter idents (itemIdentifier blog)
maybe empty (fmap (maybe empty toUrl) . getRoute) ident
where
itemAfter xs x =
lookup x $ zip xs (tail xs)
previousBlog :: Item String -> Compiler String
previousBlog blog = do
blogs <- loadBlogs blogPattern
let idents = map itemIdentifier blogs
ident = itemBefore idents (itemIdentifier blog)
maybe empty (fmap (maybe empty toUrl) . getRoute) ident
where
itemBefore xs x =
lookup x $ zip (tail xs) xs
loadDecks :: Pattern -> Compiler [Item String]
loadDecks =
recentFirst <=< flip loadAllSnapshots decksSnapshot
renderBlogAtom :: [Item String] -> Compiler (Item String)
renderBlogAtom =
renderAtom feedConfiguration atomCtx
-- routes
rootRoute :: Routes
rootRoute =
customRoute (joinPath . dropDirectory . splitPath . toFilePath)
where
dropDirectory [] = []
dropDirectory ("/":ds) = dropDirectory ds
dropDirectory ds = tail ds
pageRoute :: Routes
pageRoute =
removeExtension `composeRoutes` addIndex
where
removeExtension = setExtension mempty
addIndex = postfixRoute "index.html"
postfixRoute postfix = customRoute $ (</> postfix) . toFilePath
blogRoute :: Routes
blogRoute =
customRoute (takeFileName . toFilePath) `composeRoutes`
metadataRoute dateRoute `composeRoutes`
dropDateRoute `composeRoutes`
pageRoute
where
dateRoute metadata = customRoute $ \id' -> joinPath [dateFolder id' metadata, toFilePath id']
dateFolder id' = maybe mempty (formatTime defaultTimeLocale "%Y/%m") . tryParseDate id'
dropDateRoute = gsubRoute "[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}-" (const mempty)
decksRoute :: Routes
decksRoute =
blogRoute `composeRoutes` prefixRoute "decks"
where
prefixRoute prefix = customRoute $ (prefix </>) . toFilePath
decksAssetsRoute :: Routes
decksAssetsRoute =
yearRoute `composeRoutes`
monthRoute `composeRoutes`
dropDayRoute
where
yearRoute = gsubRoute "[[:digit:]]{4}-" (\xs -> take 4 xs <> "/")
monthRoute = gsubRoute "/[[:digit:]]{2}-" (\xs -> "/" <> (take 2 . drop 1) xs <> "/")
dropDayRoute = gsubRoute "/[[:digit:]]{2}-" (const "/")
-- contexts
pathTitleField :: String -> Context String
pathTitleField =
flip field title
where
title = maybe empty (emptyTitle . pageTitle) <=< getRoute . itemIdentifier
pageTitle = intercalate " ❯❯= " . splitDirectories . capitalize . dropFileName
emptyTitle "." = empty
emptyTitle x = return x
capitalize [] = []
capitalize (x:xs) = toUpper x : map toLower xs
urlField' :: String -> Context String
urlField' =
mapContext dropFileName . urlField
categoryField' :: String -> Tags -> Context a
categoryField' =
tagsFieldWith getCategory (renderLink "@") mconcat
categoryListField :: String -> Tags -> Context a
categoryListField key tags =
field key (const $ renderList tags)
where
renderList = renderTags makeLink unwords
makeLink tag url _ _ _ = renderHtml $ do
"@"
H.a ! href (toValue url) $ toHtml tag
tagsField' :: String -> Tags -> Context a
tagsField' =
tagsFieldWith getTags (renderLink "#") (mconcat . intersperse " ")
tagsListField :: String -> Tags -> Context a
tagsListField key tags =
field key (const $ renderList tags)
where
renderList = renderTags makeLink unwords
makeLink tag url _ _ _ = renderHtml $ do
"#"
H.a ! href (toValue url) $ toHtml tag
summaryField :: String -> Context String
summaryField key =
field key meta <> -- summary from metadata
teaserField key blogSnapshot <> -- teaser is summary
previewField key blogSnapshot -- first paragraph is summary
where
meta :: Item a -> Compiler String
meta item = do
summary <- getMetadataField' (itemIdentifier item) "summary"
return . renderHtml $
H.p (toHtml summary)
alias x | x == key = "summary"
alias x = x
previewField :: String -> Snapshot -> Context String
previewField key snapshot =
field key trim'
where
trim' item = do
body <- loadSnapshotBody (itemIdentifier item) snapshot
return $ withTagList firstParagraph body
firstParagraph = map fst . takeWhile (\(_, s) -> s > 0) . acc 0 . map cnt
acc _ [] = []
acc s ((x, s'):xs) = (x, s + s') : acc (s + s') xs
cnt tag@(TagOpen "p" _) = (tag, 1)
cnt tag@(TagClose "p") = (tag, -1)
cnt tag = (tag, 0)
readingTimeField :: String -> Snapshot -> Context String
readingTimeField key snapshot =
field key calculate
where
calculate item = do
body <- loadSnapshotBody (itemIdentifier item) snapshot
return $ withTagList acc body
acc ts = [TagText (show (time ts))]
time ts = foldl' count 0 ts `div` 265
count n (TagText s) = n + length (words s)
count n _ = n
-- aliasContext maps a new key to another key. If the other key
-- is not defined or returns empty the alias returns empty.
aliasContext :: (String -> String) -> Context a -> Context a
aliasContext f (Context c) =
Context $ \k a i -> c (f k) a i <|> c' k
where
c' k = noResult $ unwords ["Tried to alias", k, "as", f k, "which doesn't exist"]
polishField :: String -> Context String
polishField name =
functionField name $ \args _ ->
return $ withTags text' (unwords args)
where
text' (TagText s) = TagText (concatMap f (split isSpace s))
text' t = t
f "" = ""
f ":+1:" = "👍"
f ":coffee:" = "☕️"
f ":disappointed:" = "😞"
f ":frowning:" = "😦"
f ":grinning:" = "😀"
f ":heart:" = "❤"
f ":ramen:" = "🍜"
f ":rice_ball:" = "🍙"
f ":smile:" = "😄"
f ":sushi:" = "🍣"
f ":stuck_out_tongue:" = "😛"
f ":thumbsup:" = "👍"
f ":tada:" = "🎉"
f x = x
-- metadata
includeTagM :: MonadMetadata m => String -> [Identifier] -> m [Identifier]
includeTagM tag =
filterTagsM (return . elem tag)
filterTagsM :: MonadMetadata m => ([String] -> m Bool) -> [Identifier] -> m [Identifier]
filterTagsM p =
filterM (p <=< getTags)
-- pagination
buildPages :: (MonadMetadata m, MonadFail m) => Pattern -> (PageNumber -> Identifier) -> m Paginate
buildPages = buildPaginateWith
(return . paginateEvery blogPerPage <=< sortRecentFirst)
-- html
renderLink :: String -> String -> Maybe FilePath -> Maybe H.Html
renderLink _ _ Nothing = Nothing
renderLink pre text (Just url) =
Just $ do
toHtml pre
H.a ! href (toValue $ toUrl url) $ toHtml text
-- dates
tryParseDate :: Identifier -> Metadata -> Maybe UTCTime
tryParseDate =
tryParseDateWithLocale defaultTimeLocale
tryParseDateWithLocale :: TimeLocale -> Identifier -> Metadata -> Maybe UTCTime
tryParseDateWithLocale locale id' metadata = do
let tryField k fmt = lookupString k metadata >>= parseTime' fmt
fn = takeFileName $ toFilePath id'
maybe empty' return $ msum $
[tryField "published" fmt | fmt <- formats] ++
[tryField "date" fmt | fmt <- formats] ++
[parseTime' "%Y-%m-%d" $ intercalate "-" $ take 3 $ splitAll "-" fn]
where
empty' = fail $ "Hakyll.Web.Template.Context.getItemUTC: "
++ "could not parse time for " ++ show id'
parseTime' = parseTimeM True locale
formats =
[ "%a, %d %b %Y %H:%M:%S %Z"
, "%Y-%m-%dT%H:%M:%S%Z"
, "%Y-%m-%d %H:%M:%S%Z"
, "%Y-%m-%d"
, "%B %e, %Y %l:%M %p"
, "%B %e, %Y"
]
-- misc
split :: (Char -> Bool) -> String -> [String]
split p' s =
go p' ("", s)
where
go _ ("", "") = []
go p ("", y) = go (not . p) (break p y)
go p (x, y) = x : go (not . p) (break p y)