File: Routes.hs

package info (click to toggle)
haskell-hakyll 4.16.2.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 928 kB
  • sloc: haskell: 6,504; xml: 44; makefile: 9
file content (356 lines) | stat: -rw-r--r-- 13,352 bytes parent folder | download
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
--------------------------------------------------------------------------------
{- | 'Routes' is part of the 'Hakyll.Core.Rules.Rules' processing pipeline.
It determines if and where the compilation result of the underlying
'Hakyll.Core.Item.Item' being processed is written out to
(relative to the destination directory as configured in
'Hakyll.Core.Configuration.destinationDirectory').

* __If there is no route for an item, the compiled item won't be written__
__out to a file__ and so won't appear in the destination (site) directory.

* If an item matches multiple routes, the first route will be chosen.

__Examples__

Suppose we have a markdown file @posts\/hakyll.md@. We can route its
compilation result to @posts\/hakyll.html@ using 'setExtension':

> -- file on disk: '<project-directory>/posts/hakyll.md'
> match "posts/*" $ do
>     -- compilation result is written to '<destination-directory>/posts/hakyll.html'
>     route (setExtension "html")
>     compile pandocCompiler
Hint: You can configure the destination directory with
'Hakyll.Core.Configuration.destinationDirectory'.

If we do not want to change the extension, we can replace 'setExtension' with
'idRoute' (the simplest route available):

>     -- compilation result is written to '<destination-directory>/posts/hakyll.md'
>     route idRoute

That will route the file @posts\/hakyll.md@ from the project directory to
@posts\/hakyll.md@ in the destination directory.

Note:
__The extension of the destination filepath says nothing about the content!__
If you set the extension to @.html@, you have to ensure that the
compilation result is indeed HTML (for example with the
'Hakyll.Web.Pandoc.pandocCompiler' to transform Markdown to HTML).

Take a look at the built-in routes here for detailed usage examples.
-}
{-# LANGUAGE Rank2Types #-}
module Hakyll.Core.Routes
    ( Routes
    , UsedMetadata
    , runRoutes
    , idRoute
    , setExtension
    , matchRoute
    , customRoute
    , constRoute
    , gsubRoute
    , metadataRoute
    , composeRoutes
    ) where


--------------------------------------------------------------------------------
import           System.FilePath                (replaceExtension, normalise)


--------------------------------------------------------------------------------
import           Hakyll.Core.Identifier
import           Hakyll.Core.Identifier.Pattern
import           Hakyll.Core.Metadata
import           Hakyll.Core.Provider
import           Hakyll.Core.Util.String


--------------------------------------------------------------------------------
-- | When you ran a route, it's useful to know whether or not this used
-- metadata. This allows us to do more granular dependency analysis.
type UsedMetadata = Bool


--------------------------------------------------------------------------------
data RoutesRead = RoutesRead
    { routesProvider   :: Provider
    , routesUnderlying :: Identifier
    }


--------------------------------------------------------------------------------
-- | Type used for a route
newtype Routes = Routes
    { unRoutes :: RoutesRead -> Identifier -> IO (Maybe FilePath, UsedMetadata)
    }


--------------------------------------------------------------------------------
instance Semigroup Routes where
    (<>) (Routes f) (Routes g) = Routes $ \p id' -> do
        (mfp, um) <- f p id'
        case mfp of
            Nothing -> g p id'
            Just _  -> return (mfp, um)

instance Monoid Routes where
    mempty  = Routes $ \_ _ -> return (Nothing, False)
    mappend = (<>)


--------------------------------------------------------------------------------
-- | Apply a route to an identifier
runRoutes :: Routes -> Provider -> Identifier
          -> IO (Maybe FilePath, UsedMetadata)
runRoutes routes provider identifier =
    unRoutes routes (RoutesRead provider identifier) identifier


--------------------------------------------------------------------------------
{- | An "identity" route that interprets the identifier (of the item being
processed) as the destination filepath. This identifier is normally the
filepath of the source file being processed.
See 'Hakyll.Core.Identifier.Identifier' for details.

=== __Examples__
__Route when using match__

> -- e.g. file on disk: '<project-directory>/posts/hakyll.md'
>
> -- 'hakyll.md' source file implicitly gets filepath as identifier:
> -- 'posts/hakyll.md'
> match "posts/*" $ do
>
>     -- compilation result is written to '<destination-directory>/posts/hakyll.md'
>     route idRoute
>
>     compile getResourceBody
-}
idRoute :: Routes
idRoute = customRoute toFilePath


--------------------------------------------------------------------------------
{- | Create a route like 'idRoute' that interprets the identifier (of the item
being processed) as the destination filepath but also sets (or replaces) the
extension suffix of that path. This identifier is normally the filepath of the
source file being processed.
See 'Hakyll.Core.Identifier.Identifier' for details.

=== __Examples__
__Route with an existing extension__

> -- e.g. file on disk: '<project-directory>/posts/hakyll.md'
>
> -- 'hakyll.md' source file implicitly gets filepath as identifier:
> -- 'posts/hakyll.md'
> match "posts/*" $ do
>
>     -- compilation result is written to '<destination-directory>/posts/hakyll.html'
>     route (setExtension "html")
>
>     compile pandocCompiler

__Route without an existing extension__

> -- implicitly gets identifier: 'about'
> create ["about"] $ do
>
>     -- compilation result is written to '<destination-directory>/about.html'
>     route (setExtension "html")
>
>     compile $ makeItem ("Hello world" :: String)
-}
setExtension :: String -> Routes
setExtension extension = customRoute $
    (`replaceExtension` extension) . toFilePath


--------------------------------------------------------------------------------
-- | Apply the route if the identifier matches the given pattern, fail
-- otherwise
matchRoute :: Pattern -> Routes -> Routes
matchRoute pattern (Routes route) = Routes $ \p id' ->
    if matches pattern id' then route p id' else return (Nothing, False)


--------------------------------------------------------------------------------
{- | Create a route where the destination filepath is built with the given
construction function. The provided identifier for that function is normally the
filepath of the source file being processed.
See 'Hakyll.Core.Identifier.Identifier' for details.

=== __Examples__
__Route that appends a custom extension__

> -- e.g. file on disk: '<project-directory>/posts/hakyll.md'
>
> -- 'hakyll.md' source file implicitly gets filepath as identifier:
> -- 'posts/hakyll.md'
> match "posts/*" $ do
>
>     -- compilation result is written to '<destination-directory>/posts/hakyll.md.html'
>     route $ customRoute ((<> ".html") . toFilePath)
>
>     compile pandocCompiler
Note that the last part of the destination filepath becomes @.md.html@
-}
customRoute :: (Identifier -> FilePath) -- ^ Destination filepath construction function
            -> Routes                   -- ^ Resulting route
customRoute f = Routes $ const $ \id' -> return (Just (f id'), False)


--------------------------------------------------------------------------------
{- | Create a route that writes the compiled item to the given destination
filepath (ignoring any identifier or other data about the item being processed).
Warning: you should __use a specific destination path only for a single file in__
__a single compilation rule__. Otherwise it's unclear which of the contents
should be written to that route.

=== __Examples__
__Route to a specific filepath__

> -- implicitly gets identifier: 'main' (ignored on next line)
> create ["main"] $ do
>
>     -- compilation result is written to '<destination-directory>/index.html'
>     route $ constRoute "index.html"
>
>     compile $ makeItem ("<h1>Hello World</h1>" :: String)
-}
constRoute :: FilePath -> Routes
constRoute = customRoute . const


--------------------------------------------------------------------------------
{- | Create a "substituting" route that searches for substrings (in the
underlying identifier) that match the given pattern and transforms them
according to the given replacement function.
The identifier here is that of the underlying item being processed and is
interpreted as an destination filepath. It's normally the filepath of the
source file being processed.
See 'Hakyll.Core.Identifier.Identifier' for details.

Hint: The name "gsub" comes from a similar function in
[R](https://www.r-project.org) and can be read as "globally substituting"
(globally in the Unix sense of repeated, not just once).

=== __Examples__
__Route that replaces part of the filepath__

> -- e.g. file on disk: '<project-directory>/posts/hakyll.md'
>
> -- 'hakyll.md' source file implicitly gets filepath as identifier:
> -- 'posts/hakyll.md'
> match "posts/*" $ do
>
>     -- compilation result is written to '<destination-directory>/haskell/hakyll.md'
>     route $ gsubRoute "posts/" (const "haskell/")
>
>     compile getResourceBody
Note that "posts\/" is replaced with "haskell\/" in the destination filepath.

__Route that removes part of the filepath__

> -- implicitly gets identifier: 'tags/rss/bar.xml'
> create ["tags/rss/bar.xml"] $ do
>
>     -- compilation result is written to '<destination-directory>/tags/bar.xml'
>     route $ gsubRoute "rss/" (const "")
>
>     compile ...
Note that "rss\/" is removed from the destination filepath.
-}
gsubRoute :: String              -- ^ Pattern to repeatedly match against in the underlying identifier
          -> (String -> String)  -- ^ Replacement function to apply to the matched substrings
          -> Routes              -- ^ Resulting route
gsubRoute pattern replacement = customRoute $
    normalise . replaceAll pattern (replacement . removeWinPathSeparator) . removeWinPathSeparator . toFilePath
    where
        -- Filepaths on Windows containing `\\' will trip Regex matching, which
        -- is used in replaceAll. We normalise filepaths to have '/' as a path separator
        -- using removeWinPathSeparator


--------------------------------------------------------------------------------
{- | Wrapper function around other route construction functions to get
access to the metadata (of the underlying item being processed) and use that for
the destination filepath construction.
Warning: you have to
__ensure that the accessed metadata fields actually exist__.

=== __Examples__
__Route that uses a custom slug markdown metadata field__

To create a search engine optimized yet human-readable url, we can introduce
a [slug](https://en.wikipedia.org/wiki/Clean_URL#Slug) metadata field to our
files, e.g. like in the following Markdown file: 'posts\/hakyll.md'

> ---
> title: Hakyll Post
> slug: awesome-post
> ...
> ---
> In this blog post we learn about Hakyll ...

Then we can construct a route whose destination filepath is based on that field:

> match "posts/*" $ do
>
>     -- compilation result is written to '<destination-directory>/awesome-post.html'
>     route $ metadataRoute $ \meta ->
>         constRoute $ fromJust (lookupString "slug" meta) <> ".html"
>
>     compile pandocCompiler
Note how we wrap 'metadataRoute' around the 'constRoute' function and how the
slug is looked up from the markdown field to construct the destination filepath.
You can use helper functions like 'Hakyll.Core.Metadata.lookupString' to access
a specific metadata field.
-}
metadataRoute :: (Metadata -> Routes) -- ^ Wrapped route construction function
              -> Routes               -- ^ Resulting route
metadataRoute f = Routes $ \r i -> do
    metadata <- resourceMetadata (routesProvider r) (routesUnderlying r)
    unRoutes (f metadata) r i


--------------------------------------------------------------------------------
{- | Compose two routes where __the first route is applied before the second__.
So @f \`composeRoutes\` g@ is more or less equivalent with @g . f@.

Warning: If the first route fails (e.g. when using 'matchRoute'), Hakyll will
not apply the second route (if you need Hakyll to try the second route,
use '<>' on 'Routes' instead).

=== __Examples__
__Route that applies two transformations__

> -- e.g. file on disk: '<project-directory>/posts/hakyll.md'
>
> -- 'hakyll.md' source file implicitly gets filepath as identifier:
> -- 'posts/hakyll.md'
> match "posts/*" $ do
>
>     -- compilation result is written to '<destination-directory>/hakyll.html'
>     route $ gsubRoute "posts/" (const "") `composeRoutes` setExtension "html"
>
>     compile pandocCompiler
The identifier here is that of the underlying item being processed and is
interpreted as an destination filepath.
See 'Hakyll.Core.Identifier.Identifier' for details.
Note how we first remove the "posts\/" substring from that destination filepath
 with 'gsubRoute' and then replace the extension with 'setExtension'.
-}
composeRoutes :: Routes  -- ^ First route to apply
              -> Routes  -- ^ Second route to apply
              -> Routes  -- ^ Resulting route
composeRoutes (Routes f) (Routes g) = Routes $ \p i -> do
    (mfp, um) <- f p i
    case mfp of
        Nothing -> return (Nothing, um)
        Just fp -> do
            (mfp', um') <- g p (fromFilePath fp)
            return (mfp', um || um')