-
Notifications
You must be signed in to change notification settings - Fork 0
/
site.hs
executable file
·169 lines (141 loc) · 5.78 KB
/
site.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
#!/usr/bin/env stack
-- stack --resolver lts-9.21 --install-ghc runghc --package hakyll --package yaml --package markdown
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
module Main where
import qualified Data.ByteString.Lazy as LBS
import Data.Text.Lazy (toStrict, fromStrict)
import qualified Data.Text as T
import Data.Text (Text, unpack)
import System.FilePath (splitExtension, (<.>), (</>))
import qualified Data.List as List
import Data.Monoid (mempty, (<>))
import Data.Traversable
import Data.Aeson.Types (typeMismatch)
import qualified Text.Markdown as MD
import Text.Blaze.Html.Renderer.Text (renderHtml)
import qualified Data.Yaml as Yaml
import Data.Yaml ((.:), (.:?), FromJSON)
import Hakyll
main :: IO ()
main = hakyll $ do
-- Copy CNAME and favicon.ico
match ("CNAME" .||. "favicon.ico") $ do
route idRoute
compile copyFileCompiler
-- Compress CSS
match "css/*" $ do
route idRoute
compile compressCssCompiler
-- Copy static files
match "static/**" $ do
route idRoute
compile copyFileCompiler
-- Compile all markdown files
match "*.markdown" $ do
route $ composeRoutes (setExtension ".html") appendIndex
compile $ pandocCompiler
>>= loadAndApplyTemplate "templates/base.html" defaultContext
>>= relativizeUrls
-- Build publications page from YAML data
match "publications.yaml" $ do
route $ composeRoutes (setExtension ".html") appendIndex
let defCtxt = constField "is-publications" "true"
<> constField "title" "SILC: Publications"
<> defaultContext
compile $ yamlCompiler
>>= loadAndApplyTemplate "templates/publications.html" (listOfCtxt "publications" pubCtxt)
>>= loadAndApplyTemplate "templates/base.html" defCtxt
>>= relativizeUrls
-- Build people page from YAML data
match "people.yaml" $ do
route $ composeRoutes (setExtension ".html") appendIndex
let defCtxt = constField "is-people" "true"
<> constField "title" "SILC: People"
<> defaultContext
compile $ yamlCompiler
>>= loadAndApplyTemplate "templates/people.html" peopleCtxt
>>= loadAndApplyTemplate "templates/base.html" defCtxt
>>= relativizeUrls
-- Build templates (used by the above)
match "templates/*" $ compile templateBodyCompiler
personCtxt :: Context Person
personCtxt = tfield "name" rName
<> tfield "role" rTitle
<> tfield "homepage" rHomepage
<> tfield "photo" rPhoto
groupCtxt :: Context (Text, [Person])
groupCtxt = tfield "group" fst
<> (contramap snd $ listOfCtxt "people" personCtxt)
contramap :: (a -> b) -> Context b -> Context a
contramap f (Context c) = Context $ \s ss i -> c s ss (f <$> i)
peopleCtxt :: Context [Person]
peopleCtxt = listFieldWith "groups" groupCtxt (return . traverse separate)
where separate :: [Person] -> [(Text, [Person])]
separate ppl = let (currentPeople, formerPeople) = List.partition rCurrent ppl
in [("Current Members", currentPeople), ("Former Members", formerPeople)]
pubCtxt :: Context Publication
pubCtxt = tfield "title" pTitle
<> mfield "url" pUrl
<> tfield "authors" pAuthors
<> tfield "venue" pVenue
<> mfield "extra" pExtra
listOfCtxt :: String -> Context a -> Context [a]
listOfCtxt s ctx = listFieldWith s ctx (return . sequenceA)
-- NOTE(dbp 2017-07-14): The blaze html library makes it difficult (possibly
-- impossible?) to easily manipulate tags, so since we are really just trying to get rid of an extraneous wrapping <p>, we render and then do it in text.
stripP :: Text -> Text
stripP t = if "<p>" `T.isPrefixOf` t && "</p>" `T.isSuffixOf` t then T.dropEnd 4 $ T.drop 3 t else t
md :: Text -> Text
md s = stripP . toStrict . renderHtml . MD.markdown MD.def . fromStrict $ s
-- | Build field from string / getter function
tfield :: String -> (a -> Text) -> Context a
tfield s f = field s (return . unpack . md . f . itemBody)
-- | Build (potentially missing) optional field from string / getter function
mfield :: String -> (a -> Maybe Text) -> Context a
mfield s f = field s (maybe (fail "") (return . unpack . md) . f . itemBody)
yamlCompiler :: (FromJSON a) => Compiler (Item a)
yamlCompiler = do
path <- getResourceFilePath
rawItem <- getResourceLBS
for rawItem $ \raw ->
case Yaml.decodeEither . LBS.toStrict $ raw of
Left err -> error $ "Failed to parse " <> path <> " : " <> show err
Right parsed -> return parsed
data Publication = Publication { pTitle :: Text
, pUrl :: Maybe Text
, pAuthors :: Text
, pVenue :: Text
, pExtra :: Maybe Text
}
instance FromJSON Publication where
parseJSON (Yaml.Object v) =
Publication <$>
v .: "title" <*>
v .:? "url" <*>
v .: "authors" <*>
v .: "venue" <*>
v .:? "extra"
parseJSON invalid = typeMismatch "Publication" invalid
data Person = Person { rName :: Text
, rTitle :: Text
, rHomepage :: Text
, rPhoto :: Text
, rCurrent :: Bool -- ^ True if current member, False if former member
}
instance FromJSON Person where
parseJSON (Yaml.Object v) =
Person <$>
v .: "name" <*>
v .: "title" <*>
v .: "homepage" <*>
v .: "photo" <*>
(trueIfMissing <$> (v .:? "current"))
where
trueIfMissing :: Maybe Bool -> Bool
trueIfMissing (Just b) = b
trueIfMissing Nothing = True
parseJSON invalid = typeMismatch "Person" invalid
appendIndex :: Routes
appendIndex = customRoute $
(\(p, e) -> if p /= "index" then p </> "index" <.> e else p <.> e) . splitExtension . toFilePath