elm-pages 2.1.7 → 2.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/generator/review/elm.json +34 -0
- package/generator/review/src/ReviewConfig.elm +10 -0
- package/generator/src/basepath-middleware.js +15 -9
- package/generator/src/build.js +77 -4
- package/generator/src/cli.js +13 -9
- package/generator/src/compile-elm.js +43 -0
- package/generator/src/dev-server.js +63 -11
- package/generator/src/error-formatter.js +62 -9
- package/generator/src/generate-template-module-connector.js +17 -4
- package/generator/src/init.js +4 -0
- package/generator/src/pre-render-html.js +19 -12
- package/generator/src/render-worker.js +0 -1
- package/generator/src/render.js +1 -2
- package/generator/src/seo-renderer.js +21 -2
- package/generator/static-code/hmr.js +43 -6
- package/generator/template/elm.json +13 -5
- package/generator/template/package.json +3 -2
- package/package.json +14 -8
- package/src/ApiRoute.elm +178 -0
- package/src/AriaLiveAnnouncer.elm +36 -0
- package/src/BuildError.elm +60 -0
- package/src/DataSource/File.elm +288 -0
- package/src/DataSource/Glob.elm +1050 -0
- package/src/DataSource/Http.elm +467 -0
- package/src/DataSource/Internal/Glob.elm +74 -0
- package/src/DataSource/Port.elm +87 -0
- package/src/DataSource/ServerRequest.elm +60 -0
- package/src/DataSource.elm +801 -0
- package/src/Head/Seo.elm +516 -0
- package/src/Head/Twitter.elm +109 -0
- package/src/Head.elm +452 -0
- package/src/HtmlPrinter.elm +27 -0
- package/src/Internal/ApiRoute.elm +89 -0
- package/src/Internal/OptimizedDecoder.elm +18 -0
- package/src/KeepOrDiscard.elm +6 -0
- package/src/OptimizedDecoder/Pipeline.elm +335 -0
- package/src/OptimizedDecoder.elm +818 -0
- package/src/Pages/ContentCache.elm +248 -0
- package/src/Pages/Flags.elm +26 -0
- package/src/Pages/Http.elm +10 -0
- package/src/Pages/Internal/ApplicationType.elm +6 -0
- package/src/Pages/Internal/NotFoundReason.elm +256 -0
- package/src/Pages/Internal/Platform/Cli.elm +1015 -0
- package/src/Pages/Internal/Platform/Effect.elm +14 -0
- package/src/Pages/Internal/Platform/StaticResponses.elm +540 -0
- package/src/Pages/Internal/Platform/ToJsPayload.elm +138 -0
- package/src/Pages/Internal/Platform.elm +745 -0
- package/src/Pages/Internal/RoutePattern.elm +122 -0
- package/src/Pages/Internal/Router.elm +116 -0
- package/src/Pages/Internal/StaticHttpBody.elm +54 -0
- package/src/Pages/Internal/String.elm +39 -0
- package/src/Pages/Manifest/Category.elm +240 -0
- package/src/Pages/Manifest.elm +412 -0
- package/src/Pages/PageUrl.elm +38 -0
- package/src/Pages/ProgramConfig.elm +73 -0
- package/src/Pages/Review/NoContractViolations.elm +397 -0
- package/src/Pages/Secrets.elm +83 -0
- package/src/Pages/SiteConfig.elm +13 -0
- package/src/Pages/StaticHttp/Request.elm +42 -0
- package/src/Pages/StaticHttpRequest.elm +320 -0
- package/src/Pages/Url.elm +60 -0
- package/src/Path.elm +96 -0
- package/src/QueryParams.elm +216 -0
- package/src/RenderRequest.elm +163 -0
- package/src/RequestsAndPending.elm +20 -0
- package/src/Secrets.elm +111 -0
- package/src/SecretsDict.elm +45 -0
- package/src/StructuredData.elm +236 -0
- package/src/TerminalText.elm +242 -0
- package/src/Test/Html/Internal/ElmHtml/Constants.elm +53 -0
- package/src/Test/Html/Internal/ElmHtml/Helpers.elm +17 -0
- package/src/Test/Html/Internal/ElmHtml/InternalTypes.elm +529 -0
- package/src/Test/Html/Internal/ElmHtml/Markdown.elm +56 -0
- package/src/Test/Html/Internal/ElmHtml/ToString.elm +197 -0
- package/src/Test/Internal/KernelConstants.elm +34 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
module DataSource.Glob exposing
|
|
2
|
+
( Glob
|
|
3
|
+
, capture, match
|
|
4
|
+
, captureFilePath
|
|
5
|
+
, wildcard, recursiveWildcard
|
|
6
|
+
, int, digits
|
|
7
|
+
, expectUniqueMatch, expectUniqueMatchFromList
|
|
8
|
+
, literal
|
|
9
|
+
, map, succeed, toDataSource
|
|
10
|
+
, oneOf
|
|
11
|
+
, zeroOrMore, atLeastOne
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
{-|
|
|
15
|
+
|
|
16
|
+
@docs Glob
|
|
17
|
+
|
|
18
|
+
This module helps you get a List of matching file paths from your local file system as a [`DataSource`](DataSource#DataSource). See the [`DataSource`](DataSource) module documentation
|
|
19
|
+
for ways you can combine and map `DataSource`s.
|
|
20
|
+
|
|
21
|
+
A common example would be to find all the markdown files of your blog posts. If you have all your blog posts in `content/blog/*.md`
|
|
22
|
+
, then you could use that glob pattern in most shells to refer to each of those files.
|
|
23
|
+
|
|
24
|
+
With the `DataSource.Glob` API, you could get all of those files like so:
|
|
25
|
+
|
|
26
|
+
import DataSource exposing (DataSource)
|
|
27
|
+
|
|
28
|
+
blogPostsGlob : DataSource (List String)
|
|
29
|
+
blogPostsGlob =
|
|
30
|
+
Glob.succeed (\slug -> slug)
|
|
31
|
+
|> Glob.match (Glob.literal "content/blog/")
|
|
32
|
+
|> Glob.capture Glob.wildcard
|
|
33
|
+
|> Glob.match (Glob.literal ".md")
|
|
34
|
+
|> Glob.toDataSource
|
|
35
|
+
|
|
36
|
+
Let's say you have these files locally:
|
|
37
|
+
|
|
38
|
+
```shell
|
|
39
|
+
- elm.json
|
|
40
|
+
- src/
|
|
41
|
+
- content/
|
|
42
|
+
- blog/
|
|
43
|
+
- first-post.md
|
|
44
|
+
- second-post.md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
We would end up with a `DataSource` like this:
|
|
48
|
+
|
|
49
|
+
DataSource.succeed [ "first-post", "second-post" ]
|
|
50
|
+
|
|
51
|
+
Of course, if you add or remove matching files, the DataSource will get those new files (unlike `DataSource.succeed`). That's why we have Glob!
|
|
52
|
+
|
|
53
|
+
You can even see the `elm-pages dev` server will automatically flow through any added/removed matching files with its hot module reloading.
|
|
54
|
+
|
|
55
|
+
But why did we get `"first-post"` instead of a full file path, like `"content/blog/first-post.md"`? That's the difference between
|
|
56
|
+
`capture` and `match`.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Capture and Match
|
|
60
|
+
|
|
61
|
+
There are two functions for building up a Glob pattern: `capture` and `match`.
|
|
62
|
+
|
|
63
|
+
`capture` and `match` both build up a `Glob` pattern that will match 0 or more files on your local file system.
|
|
64
|
+
There will be one argument for every `capture` in your pipeline, whereas `match` does not apply any arguments.
|
|
65
|
+
|
|
66
|
+
import DataSource exposing (DataSource)
|
|
67
|
+
import DataSource.Glob as Glob
|
|
68
|
+
|
|
69
|
+
blogPostsGlob : DataSource (List String)
|
|
70
|
+
blogPostsGlob =
|
|
71
|
+
Glob.succeed (\slug -> slug)
|
|
72
|
+
-- no argument from this, but we will only
|
|
73
|
+
-- match files that begin with `content/blog/`
|
|
74
|
+
|> Glob.match (Glob.literal "content/blog/")
|
|
75
|
+
-- we get the value of the `wildcard`
|
|
76
|
+
-- as the slug argument
|
|
77
|
+
|> Glob.capture Glob.wildcard
|
|
78
|
+
-- no argument from this, but we will only
|
|
79
|
+
-- match files that end with `.md`
|
|
80
|
+
|> Glob.match (Glob.literal ".md")
|
|
81
|
+
|> Glob.toDataSource
|
|
82
|
+
|
|
83
|
+
So to understand _which_ files will match, you can ignore whether you are using `capture` or `match` and just read
|
|
84
|
+
the patterns you're using in order to understand what will match. To understand what Elm data type you will get
|
|
85
|
+
_for each matching file_, you need to see which parts are being captured and how each of those captured values are being
|
|
86
|
+
used in the function you use in `Glob.succeed`.
|
|
87
|
+
|
|
88
|
+
@docs capture, match
|
|
89
|
+
|
|
90
|
+
`capture` is a lot like building up a JSON decoder with a pipeline.
|
|
91
|
+
|
|
92
|
+
Let's try our blogPostsGlob from before, but change every `match` to `capture`.
|
|
93
|
+
|
|
94
|
+
import DataSource exposing (DataSource)
|
|
95
|
+
|
|
96
|
+
blogPostsGlob :
|
|
97
|
+
DataSource
|
|
98
|
+
(List
|
|
99
|
+
{ filePath : String
|
|
100
|
+
, slug : String
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
blogPostsGlob =
|
|
104
|
+
Glob.succeed
|
|
105
|
+
(\capture1 capture2 capture3 ->
|
|
106
|
+
{ filePath = capture1 ++ capture2 ++ capture3
|
|
107
|
+
, slug = capture2
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|> Glob.capture (Glob.literal "content/blog/")
|
|
111
|
+
|> Glob.capture Glob.wildcard
|
|
112
|
+
|> Glob.capture (Glob.literal ".md")
|
|
113
|
+
|> Glob.toDataSource
|
|
114
|
+
|
|
115
|
+
Notice that we now need 3 arguments at the start of our pipeline instead of 1. That's because
|
|
116
|
+
we apply 1 more argument every time we do a `Glob.capture`, much like `Json.Decode.Pipeline.required`, or other pipeline APIs.
|
|
117
|
+
|
|
118
|
+
Now we actually have the full file path of our files. But having that slug (like `first-post`) is also very helpful sometimes, so
|
|
119
|
+
we kept that in our record as well. So we'll now have the equivalent of this `DataSource` with the current `.md` files in our `blog` folder:
|
|
120
|
+
|
|
121
|
+
DataSource.succeed
|
|
122
|
+
[ { filePath = "content/blog/first-post.md"
|
|
123
|
+
, slug = "first-post"
|
|
124
|
+
}
|
|
125
|
+
, { filePath = "content/blog/second-post.md"
|
|
126
|
+
, slug = "second-post"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
Having the full file path lets us read in files. But concatenating it manually is tedious
|
|
131
|
+
and error prone. That's what the [`captureFilePath`](#captureFilePath) helper is for.
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
## Reading matching files
|
|
135
|
+
|
|
136
|
+
@docs captureFilePath
|
|
137
|
+
|
|
138
|
+
In many cases you will want to take the matching files from a `Glob` and then read the body or frontmatter from matching files.
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## Reading Metadata for each Glob Match
|
|
142
|
+
|
|
143
|
+
For example, if we had files like this:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
---
|
|
147
|
+
title: My First Post
|
|
148
|
+
---
|
|
149
|
+
This is my first post!
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then we could read that title for our blog post list page using our `blogPosts` `DataSource` that we defined above.
|
|
153
|
+
|
|
154
|
+
import DataSource.File
|
|
155
|
+
import OptimizedDecoder as Decode exposing (Decoder)
|
|
156
|
+
|
|
157
|
+
titles : DataSource (List BlogPost)
|
|
158
|
+
titles =
|
|
159
|
+
blogPosts
|
|
160
|
+
|> DataSource.map
|
|
161
|
+
(List.map
|
|
162
|
+
(\blogPost ->
|
|
163
|
+
DataSource.File.request
|
|
164
|
+
blogPost.filePath
|
|
165
|
+
(DataSource.File.frontmatter blogFrontmatterDecoder)
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|> DataSource.resolve
|
|
169
|
+
|
|
170
|
+
type alias BlogPost =
|
|
171
|
+
{ title : String }
|
|
172
|
+
|
|
173
|
+
blogFrontmatterDecoder : Decoder BlogPost
|
|
174
|
+
blogFrontmatterDecoder =
|
|
175
|
+
Decode.map BlogPost
|
|
176
|
+
(Decode.field "title" Decode.string)
|
|
177
|
+
|
|
178
|
+
That will give us
|
|
179
|
+
|
|
180
|
+
DataSource.succeed
|
|
181
|
+
[ { title = "My First Post" }
|
|
182
|
+
, { title = "My Second Post" }
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## Capturing Patterns
|
|
187
|
+
|
|
188
|
+
@docs wildcard, recursiveWildcard
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
## Capturing Specific Characters
|
|
192
|
+
|
|
193
|
+
@docs int, digits
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
## Matching a Specific Number of Files
|
|
197
|
+
|
|
198
|
+
@docs expectUniqueMatch, expectUniqueMatchFromList
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## Glob Patterns
|
|
202
|
+
|
|
203
|
+
@docs literal
|
|
204
|
+
|
|
205
|
+
@docs map, succeed, toDataSource
|
|
206
|
+
|
|
207
|
+
@docs oneOf
|
|
208
|
+
|
|
209
|
+
@docs zeroOrMore, atLeastOne
|
|
210
|
+
|
|
211
|
+
-}
|
|
212
|
+
|
|
213
|
+
import DataSource exposing (DataSource)
|
|
214
|
+
import DataSource.Http
|
|
215
|
+
import DataSource.Internal.Glob exposing (Glob(..))
|
|
216
|
+
import List.Extra
|
|
217
|
+
import OptimizedDecoder
|
|
218
|
+
import Regex
|
|
219
|
+
import Secrets
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
{-| A pattern to match local files and capture parts of the path into a nice Elm data type.
|
|
223
|
+
-}
|
|
224
|
+
type alias Glob a =
|
|
225
|
+
DataSource.Internal.Glob.Glob a
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
{-| A `Glob` can be mapped. This can be useful for transforming a sub-match in-place.
|
|
229
|
+
|
|
230
|
+
For example, if you wanted to take the slugs for a blog post and make sure they are normalized to be all lowercase, you
|
|
231
|
+
could use
|
|
232
|
+
|
|
233
|
+
import DataSource exposing (DataSource)
|
|
234
|
+
import DataSource.Glob as Glob
|
|
235
|
+
|
|
236
|
+
blogPostsGlob : DataSource (List String)
|
|
237
|
+
blogPostsGlob =
|
|
238
|
+
Glob.succeed (\slug -> slug)
|
|
239
|
+
|> Glob.match (Glob.literal "content/blog/")
|
|
240
|
+
|> Glob.capture (Glob.wildcard |> Glob.map String.toLower)
|
|
241
|
+
|> Glob.match (Glob.literal ".md")
|
|
242
|
+
|> Glob.toDataSource
|
|
243
|
+
|
|
244
|
+
If you want to validate file formats, you can combine that with some `DataSource` helpers to turn a `Glob (Result String value)` into
|
|
245
|
+
a `DataSource (List value)`.
|
|
246
|
+
|
|
247
|
+
For example, you could take a date and parse it.
|
|
248
|
+
|
|
249
|
+
import DataSource exposing (DataSource)
|
|
250
|
+
import DataSource.Glob as Glob
|
|
251
|
+
|
|
252
|
+
example : DataSource (List ( String, String ))
|
|
253
|
+
example =
|
|
254
|
+
Glob.succeed
|
|
255
|
+
(\dateResult slug ->
|
|
256
|
+
dateResult
|
|
257
|
+
|> Result.map (\okDate -> ( okDate, slug ))
|
|
258
|
+
)
|
|
259
|
+
|> Glob.match (Glob.literal "blog/")
|
|
260
|
+
|> Glob.capture (Glob.recursiveWildcard |> Glob.map expectDateFormat)
|
|
261
|
+
|> Glob.match (Glob.literal "/")
|
|
262
|
+
|> Glob.capture Glob.wildcard
|
|
263
|
+
|> Glob.match (Glob.literal ".md")
|
|
264
|
+
|> Glob.toDataSource
|
|
265
|
+
|> DataSource.map (List.map DataSource.fromResult)
|
|
266
|
+
|> DataSource.resolve
|
|
267
|
+
|
|
268
|
+
expectDateFormat : List String -> Result String String
|
|
269
|
+
expectDateFormat dateParts =
|
|
270
|
+
case dateParts of
|
|
271
|
+
[ year, month, date ] ->
|
|
272
|
+
Ok (String.join "-" [ year, month, date ])
|
|
273
|
+
|
|
274
|
+
_ ->
|
|
275
|
+
Err "Unexpected date format, expected yyyy/mm/dd folder structure."
|
|
276
|
+
|
|
277
|
+
-}
|
|
278
|
+
map : (a -> b) -> Glob a -> Glob b
|
|
279
|
+
map mapFn (Glob pattern regex applyCapture) =
|
|
280
|
+
Glob pattern
|
|
281
|
+
regex
|
|
282
|
+
(\fullPath captures ->
|
|
283
|
+
captures
|
|
284
|
+
|> applyCapture fullPath
|
|
285
|
+
|> Tuple.mapFirst mapFn
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
{-| `succeed` is how you start a pipeline for a `Glob`. You will need one argument for each `capture` in your `Glob`.
|
|
290
|
+
-}
|
|
291
|
+
succeed : constructor -> Glob constructor
|
|
292
|
+
succeed constructor =
|
|
293
|
+
Glob "" "" (\_ captures -> ( constructor, captures ))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
fullFilePath : Glob String
|
|
297
|
+
fullFilePath =
|
|
298
|
+
Glob ""
|
|
299
|
+
""
|
|
300
|
+
(\fullPath captures ->
|
|
301
|
+
( fullPath, captures )
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
{-|
|
|
306
|
+
|
|
307
|
+
import DataSource exposing (DataSource)
|
|
308
|
+
import DataSource.Glob as Glob
|
|
309
|
+
|
|
310
|
+
blogPosts :
|
|
311
|
+
DataSource
|
|
312
|
+
(List
|
|
313
|
+
{ filePath : String
|
|
314
|
+
, slug : String
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
blogPosts =
|
|
318
|
+
Glob.succeed
|
|
319
|
+
(\filePath slug ->
|
|
320
|
+
{ filePath = filePath
|
|
321
|
+
, slug = slug
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
|> Glob.captureFilePath
|
|
325
|
+
|> Glob.match (Glob.literal "content/blog/")
|
|
326
|
+
|> Glob.capture Glob.wildcard
|
|
327
|
+
|> Glob.match (Glob.literal ".md")
|
|
328
|
+
|> Glob.toDataSource
|
|
329
|
+
|
|
330
|
+
This function does not change which files will or will not match. It just gives you the full matching
|
|
331
|
+
file path in your `Glob` pipeline.
|
|
332
|
+
|
|
333
|
+
Whenever possible, it's a good idea to use function to make sure you have an accurate file path when you need to read a file.
|
|
334
|
+
|
|
335
|
+
-}
|
|
336
|
+
captureFilePath : Glob (String -> value) -> Glob value
|
|
337
|
+
captureFilePath =
|
|
338
|
+
capture fullFilePath
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
{-| Matches anything except for a `/` in a file path. You may be familiar with this syntax from shells like bash
|
|
342
|
+
where you can run commands like `rm client/*.js` to remove all `.js` files in the `client` directory.
|
|
343
|
+
|
|
344
|
+
Just like a `*` glob pattern in bash, this `Glob.wildcard` function will only match within a path part. If you need to
|
|
345
|
+
match 0 or more path parts like, see `recursiveWildcard`.
|
|
346
|
+
|
|
347
|
+
import DataSource exposing (DataSource)
|
|
348
|
+
import DataSource.Glob as Glob
|
|
349
|
+
|
|
350
|
+
type alias BlogPost =
|
|
351
|
+
{ year : String
|
|
352
|
+
, month : String
|
|
353
|
+
, day : String
|
|
354
|
+
, slug : String
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
example : DataSource (List BlogPost)
|
|
358
|
+
example =
|
|
359
|
+
Glob.succeed BlogPost
|
|
360
|
+
|> Glob.match (Glob.literal "blog/")
|
|
361
|
+
|> Glob.match Glob.wildcard
|
|
362
|
+
|> Glob.match (Glob.literal "-")
|
|
363
|
+
|> Glob.capture Glob.wildcard
|
|
364
|
+
|> Glob.match (Glob.literal "-")
|
|
365
|
+
|> Glob.capture Glob.wildcard
|
|
366
|
+
|> Glob.match (Glob.literal "/")
|
|
367
|
+
|> Glob.capture Glob.wildcard
|
|
368
|
+
|> Glob.match (Glob.literal ".md")
|
|
369
|
+
|> Glob.toDataSource
|
|
370
|
+
|
|
371
|
+
```shell
|
|
372
|
+
|
|
373
|
+
- blog/
|
|
374
|
+
- 2021-05-27/
|
|
375
|
+
- first-post.md
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
That will match to:
|
|
379
|
+
|
|
380
|
+
results : DataSource (List BlogPost)
|
|
381
|
+
results =
|
|
382
|
+
DataSource.succeed
|
|
383
|
+
[ { year = "2021"
|
|
384
|
+
, month = "05"
|
|
385
|
+
, day = "27"
|
|
386
|
+
, slug = "first-post"
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
Note that we can "destructure" the date part of this file path in the format `yyyy-mm-dd`. The `wildcard` matches
|
|
391
|
+
will match _within_ a path part (think between the slashes of a file path). `recursiveWildcard` can match across path parts.
|
|
392
|
+
|
|
393
|
+
-}
|
|
394
|
+
wildcard : Glob String
|
|
395
|
+
wildcard =
|
|
396
|
+
Glob "*"
|
|
397
|
+
wildcardRegex
|
|
398
|
+
(\_ captures ->
|
|
399
|
+
case captures of
|
|
400
|
+
first :: rest ->
|
|
401
|
+
( first, rest )
|
|
402
|
+
|
|
403
|
+
[] ->
|
|
404
|
+
( "ERROR", [] )
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
wildcardRegex : String
|
|
409
|
+
wildcardRegex =
|
|
410
|
+
"([^/]*?)"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
{-| This is similar to [`wildcard`](#wildcard), but it will only match 1 or more digits (i.e. `[0-9]+`).
|
|
414
|
+
|
|
415
|
+
See [`int`](#int) for a convenience function to get an Int value instead of a String of digits.
|
|
416
|
+
|
|
417
|
+
-}
|
|
418
|
+
digits : Glob String
|
|
419
|
+
digits =
|
|
420
|
+
Glob "[0-9]+"
|
|
421
|
+
"([0-9]+?)"
|
|
422
|
+
(\_ captures ->
|
|
423
|
+
case captures of
|
|
424
|
+
first :: rest ->
|
|
425
|
+
( first, rest )
|
|
426
|
+
|
|
427
|
+
[] ->
|
|
428
|
+
( "ERROR", [] )
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
{-| Same as [`digits`](#digits), but it safely turns the digits String into an `Int`.
|
|
433
|
+
|
|
434
|
+
Leading 0's are ignored.
|
|
435
|
+
|
|
436
|
+
import DataSource exposing (DataSource)
|
|
437
|
+
import DataSource.Glob as Glob
|
|
438
|
+
|
|
439
|
+
slides : DataSource (List Int)
|
|
440
|
+
slides =
|
|
441
|
+
Glob.succeed identity
|
|
442
|
+
|> Glob.match (Glob.literal "slide-")
|
|
443
|
+
|> Glob.capture Glob.int
|
|
444
|
+
|> Glob.match (Glob.literal ".md")
|
|
445
|
+
|> Glob.toDataSource
|
|
446
|
+
|
|
447
|
+
With files
|
|
448
|
+
|
|
449
|
+
```shell
|
|
450
|
+
- slide-no-match.md
|
|
451
|
+
- slide-.md
|
|
452
|
+
- slide-1.md
|
|
453
|
+
- slide-01.md
|
|
454
|
+
- slide-2.md
|
|
455
|
+
- slide-03.md
|
|
456
|
+
- slide-4.md
|
|
457
|
+
- slide-05.md
|
|
458
|
+
- slide-06.md
|
|
459
|
+
- slide-007.md
|
|
460
|
+
- slide-08.md
|
|
461
|
+
- slide-09.md
|
|
462
|
+
- slide-10.md
|
|
463
|
+
- slide-11.md
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Yields
|
|
467
|
+
|
|
468
|
+
matches : DataSource (List Int)
|
|
469
|
+
matches =
|
|
470
|
+
DataSource.succeed
|
|
471
|
+
[ 1
|
|
472
|
+
, 1
|
|
473
|
+
, 2
|
|
474
|
+
, 3
|
|
475
|
+
, 4
|
|
476
|
+
, 5
|
|
477
|
+
, 6
|
|
478
|
+
, 7
|
|
479
|
+
, 8
|
|
480
|
+
, 9
|
|
481
|
+
, 10
|
|
482
|
+
, 11
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
Note that neither `slide-no-match.md` nor `slide-.md` match.
|
|
486
|
+
And both `slide-1.md` and `slide-01.md` match and turn into `1`.
|
|
487
|
+
|
|
488
|
+
-}
|
|
489
|
+
int : Glob Int
|
|
490
|
+
int =
|
|
491
|
+
digits
|
|
492
|
+
|> map
|
|
493
|
+
(\matchedDigits ->
|
|
494
|
+
matchedDigits
|
|
495
|
+
|> String.toInt
|
|
496
|
+
|> Maybe.withDefault -1
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
{-| Matches any number of characters, including `/`, as long as it's the only thing in a path part.
|
|
501
|
+
|
|
502
|
+
In contrast, `wildcard` will never match `/`, so it only matches within a single path part.
|
|
503
|
+
|
|
504
|
+
This is the elm-pages equivalent of `**/*.txt` in standard shell syntax:
|
|
505
|
+
|
|
506
|
+
import DataSource exposing (DataSource)
|
|
507
|
+
import DataSource.Glob as Glob
|
|
508
|
+
|
|
509
|
+
example : DataSource (List ( List String, String ))
|
|
510
|
+
example =
|
|
511
|
+
Glob.succeed Tuple.pair
|
|
512
|
+
|> Glob.match (Glob.literal "articles/")
|
|
513
|
+
|> Glob.capture Glob.recursiveWildcard
|
|
514
|
+
|> Glob.match (Glob.literal "/")
|
|
515
|
+
|> Glob.capture Glob.wildcard
|
|
516
|
+
|> Glob.match (Glob.literal ".txt")
|
|
517
|
+
|> Glob.toDataSource
|
|
518
|
+
|
|
519
|
+
With these files:
|
|
520
|
+
|
|
521
|
+
```shell
|
|
522
|
+
- articles/
|
|
523
|
+
- google-io-2021-recap.txt
|
|
524
|
+
- archive/
|
|
525
|
+
- 1977/
|
|
526
|
+
- 06/
|
|
527
|
+
- 10/
|
|
528
|
+
- apple-2-announced.txt
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
We would get the following matches:
|
|
532
|
+
|
|
533
|
+
matches : DataSource (List ( List String, String ))
|
|
534
|
+
matches =
|
|
535
|
+
DataSource.succeed
|
|
536
|
+
[ ( [ "archive", "1977", "06", "10" ], "apple-2-announced" )
|
|
537
|
+
, ( [], "google-io-2021-recap" )
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
Note that the recursive wildcard conveniently gives us a `List String`, where
|
|
541
|
+
each String is a path part with no slashes (like `archive`).
|
|
542
|
+
|
|
543
|
+
And also note that it matches 0 path parts into an empty list.
|
|
544
|
+
|
|
545
|
+
If we didn't include the `wildcard` after the `recursiveWildcard`, then we would only get
|
|
546
|
+
a single level of matches because it is followed by a file extension.
|
|
547
|
+
|
|
548
|
+
example : DataSource (List String)
|
|
549
|
+
example =
|
|
550
|
+
Glob.succeed identity
|
|
551
|
+
|> Glob.match (Glob.literal "articles/")
|
|
552
|
+
|> Glob.capture Glob.recursiveWildcard
|
|
553
|
+
|> Glob.match (Glob.literal ".txt")
|
|
554
|
+
|
|
555
|
+
matches : DataSource (List String)
|
|
556
|
+
matches =
|
|
557
|
+
DataSource.succeed
|
|
558
|
+
[ "google-io-2021-recap"
|
|
559
|
+
]
|
|
560
|
+
|
|
561
|
+
This is usually not what is intended. Using `recursiveWildcard` is usually followed by a `wildcard` for this reason.
|
|
562
|
+
|
|
563
|
+
-}
|
|
564
|
+
recursiveWildcard : Glob (List String)
|
|
565
|
+
recursiveWildcard =
|
|
566
|
+
Glob "**"
|
|
567
|
+
recursiveWildcardRegex
|
|
568
|
+
(\_ captures ->
|
|
569
|
+
case captures of
|
|
570
|
+
first :: rest ->
|
|
571
|
+
( first, rest )
|
|
572
|
+
|
|
573
|
+
[] ->
|
|
574
|
+
( "ERROR", [] )
|
|
575
|
+
)
|
|
576
|
+
|> map (String.split "/")
|
|
577
|
+
|> map (List.filter (not << String.isEmpty))
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
recursiveWildcardRegex : String
|
|
581
|
+
recursiveWildcardRegex =
|
|
582
|
+
"(.*?)"
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
{-| -}
|
|
586
|
+
zeroOrMore : List String -> Glob (Maybe String)
|
|
587
|
+
zeroOrMore matchers =
|
|
588
|
+
Glob
|
|
589
|
+
("*("
|
|
590
|
+
++ (matchers |> String.join "|")
|
|
591
|
+
++ ")"
|
|
592
|
+
)
|
|
593
|
+
("((?:"
|
|
594
|
+
++ (matchers |> List.map regexEscaped |> String.join "|")
|
|
595
|
+
++ ")*)"
|
|
596
|
+
)
|
|
597
|
+
(\_ captures ->
|
|
598
|
+
case captures of
|
|
599
|
+
first :: rest ->
|
|
600
|
+
( if first == "" then
|
|
601
|
+
Nothing
|
|
602
|
+
|
|
603
|
+
else
|
|
604
|
+
Just first
|
|
605
|
+
, rest
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
[] ->
|
|
609
|
+
( Just "ERROR", [] )
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
{-| Match a literal part of a path. Can include `/`s.
|
|
614
|
+
|
|
615
|
+
Some common uses include
|
|
616
|
+
|
|
617
|
+
- The leading part of a pattern, to say "starts with `content/blog/`"
|
|
618
|
+
- The ending part of a pattern, to say "ends with `.md`"
|
|
619
|
+
- In-between wildcards, to say "these dynamic parts are separated by `/`"
|
|
620
|
+
|
|
621
|
+
```elm
|
|
622
|
+
import DataSource exposing (DataSource)
|
|
623
|
+
import DataSource.Glob as Glob
|
|
624
|
+
|
|
625
|
+
blogPosts =
|
|
626
|
+
Glob.succeed
|
|
627
|
+
(\section slug ->
|
|
628
|
+
{ section = section, slug = slug }
|
|
629
|
+
)
|
|
630
|
+
|> Glob.match (Glob.literal "content/blog/")
|
|
631
|
+
|> Glob.capture Glob.wildcard
|
|
632
|
+
|> Glob.match (Glob.literal "/")
|
|
633
|
+
|> Glob.capture Glob.wildcard
|
|
634
|
+
|> Glob.match (Glob.literal ".md")
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
-}
|
|
638
|
+
literal : String -> Glob String
|
|
639
|
+
literal string =
|
|
640
|
+
Glob string (regexEscaped string) (\_ captures -> ( string, captures ))
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
regexEscaped : String -> String
|
|
644
|
+
regexEscaped stringLiteral =
|
|
645
|
+
--https://stackoverflow.com/a/6969486
|
|
646
|
+
stringLiteral
|
|
647
|
+
|> Regex.replace regexEscapePattern (\match_ -> "\\" ++ match_.match)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
regexEscapePattern : Regex.Regex
|
|
651
|
+
regexEscapePattern =
|
|
652
|
+
"[.*+?^${}()|[\\]\\\\]"
|
|
653
|
+
|> Regex.fromString
|
|
654
|
+
|> Maybe.withDefault Regex.never
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
{-| Adds on to the glob pattern, but does not capture it in the resulting Elm match value. That means this changes which
|
|
658
|
+
files will match, but does not change the Elm data type you get for each matching file.
|
|
659
|
+
|
|
660
|
+
Exactly the same as `capture` except it doesn't capture the matched sub-pattern.
|
|
661
|
+
|
|
662
|
+
-}
|
|
663
|
+
match : Glob a -> Glob value -> Glob value
|
|
664
|
+
match (Glob matcherPattern regex1 apply1) (Glob pattern regex2 apply2) =
|
|
665
|
+
Glob
|
|
666
|
+
(pattern ++ matcherPattern)
|
|
667
|
+
(combineRegexes regex1 regex2)
|
|
668
|
+
(\fullPath captures ->
|
|
669
|
+
let
|
|
670
|
+
( _, captured1 ) =
|
|
671
|
+
-- apply to make sure we drop from the captures list for all capturing patterns
|
|
672
|
+
-- but don't change the return value
|
|
673
|
+
captures
|
|
674
|
+
|> apply1 fullPath
|
|
675
|
+
|
|
676
|
+
( applied2, captured2 ) =
|
|
677
|
+
captured1
|
|
678
|
+
|> apply2 fullPath
|
|
679
|
+
in
|
|
680
|
+
( applied2
|
|
681
|
+
, captured2
|
|
682
|
+
)
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
{-| Adds on to the glob pattern, and captures it in the resulting Elm match value. That means this both changes which
|
|
687
|
+
files will match, and gives you the sub-match as Elm data for each matching file.
|
|
688
|
+
|
|
689
|
+
Exactly the same as `match` except it also captures the matched sub-pattern.
|
|
690
|
+
|
|
691
|
+
type alias ArchivesArticle =
|
|
692
|
+
{ year : String
|
|
693
|
+
, month : String
|
|
694
|
+
, day : String
|
|
695
|
+
, slug : String
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
archives : DataSource ArchivesArticle
|
|
699
|
+
archives =
|
|
700
|
+
Glob.succeed ArchivesArticle
|
|
701
|
+
|> Glob.match (Glob.literal "archive/")
|
|
702
|
+
|> Glob.capture Glob.int
|
|
703
|
+
|> Glob.match (Glob.literal "/")
|
|
704
|
+
|> Glob.capture Glob.int
|
|
705
|
+
|> Glob.match (Glob.literal "/")
|
|
706
|
+
|> Glob.capture Glob.int
|
|
707
|
+
|> Glob.match (Glob.literal "/")
|
|
708
|
+
|> Glob.capture Glob.wildcard
|
|
709
|
+
|> Glob.match (Glob.literal ".md")
|
|
710
|
+
|> Glob.toDataSource
|
|
711
|
+
|
|
712
|
+
The file `archive/1977/06/10/apple-2-released.md` will give us this match:
|
|
713
|
+
|
|
714
|
+
matches : List ArchivesArticle
|
|
715
|
+
matches =
|
|
716
|
+
DataSource.succeed
|
|
717
|
+
[ { year = 1977
|
|
718
|
+
, month = 6
|
|
719
|
+
, day = 10
|
|
720
|
+
, slug = "apple-2-released"
|
|
721
|
+
}
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
When possible, it's best to grab data and turn it into structured Elm data when you have it. That way,
|
|
725
|
+
you don't end up with duplicate validation logic and data normalization, and your code will be more robust.
|
|
726
|
+
|
|
727
|
+
If you only care about getting the full matched file paths, you can use `match`. `capture` is very useful because
|
|
728
|
+
you can pick apart structured data as you build up your glob pattern. This follows the principle of
|
|
729
|
+
[Parse, Don't Validate](https://elm-radio.com/episode/parse-dont-validate/).
|
|
730
|
+
|
|
731
|
+
-}
|
|
732
|
+
capture : Glob a -> Glob (a -> value) -> Glob value
|
|
733
|
+
capture (Glob matcherPattern regex1 apply1) (Glob pattern regex2 apply2) =
|
|
734
|
+
Glob
|
|
735
|
+
(pattern ++ matcherPattern)
|
|
736
|
+
(combineRegexes regex1 regex2)
|
|
737
|
+
(\fullPath captures ->
|
|
738
|
+
let
|
|
739
|
+
( applied1, captured1 ) =
|
|
740
|
+
captures
|
|
741
|
+
|> apply1 fullPath
|
|
742
|
+
|
|
743
|
+
( applied2, captured2 ) =
|
|
744
|
+
captured1
|
|
745
|
+
|> apply2 fullPath
|
|
746
|
+
in
|
|
747
|
+
( applied1 |> applied2
|
|
748
|
+
, captured2
|
|
749
|
+
)
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
combineRegexes : String -> String -> String
|
|
754
|
+
combineRegexes regex1 regex2 =
|
|
755
|
+
if isRecursiveWildcardSlashWildcard regex1 regex2 then
|
|
756
|
+
(regex2 |> String.dropRight 1) ++ regex1
|
|
757
|
+
|
|
758
|
+
else
|
|
759
|
+
regex2 ++ regex1
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
isRecursiveWildcardSlashWildcard : String -> String -> Bool
|
|
763
|
+
isRecursiveWildcardSlashWildcard regex1 regex2 =
|
|
764
|
+
(regex2 |> String.endsWith (recursiveWildcardRegex ++ "/"))
|
|
765
|
+
&& (regex1 |> String.startsWith wildcardRegex)
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
{-|
|
|
769
|
+
|
|
770
|
+
import DataSource.Glob as Glob
|
|
771
|
+
|
|
772
|
+
type Extension
|
|
773
|
+
= Json
|
|
774
|
+
| Yml
|
|
775
|
+
|
|
776
|
+
type alias DataFile =
|
|
777
|
+
{ name : String
|
|
778
|
+
, extension : String
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
dataFiles : DataSource (List DataFile)
|
|
782
|
+
dataFiles =
|
|
783
|
+
Glob.succeed DataFile
|
|
784
|
+
|> Glob.match (Glob.literal "my-data/")
|
|
785
|
+
|> Glob.capture Glob.wildcard
|
|
786
|
+
|> Glob.match (Glob.literal ".")
|
|
787
|
+
|> Glob.capture
|
|
788
|
+
(Glob.oneOf
|
|
789
|
+
( ( "yml", Yml )
|
|
790
|
+
, [ ( "json", Json )
|
|
791
|
+
]
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
If we have the following files
|
|
796
|
+
|
|
797
|
+
```shell
|
|
798
|
+
- my-data/
|
|
799
|
+
- authors.yml
|
|
800
|
+
- events.json
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
That gives us
|
|
804
|
+
|
|
805
|
+
results : DataSource (List DataFile)
|
|
806
|
+
results =
|
|
807
|
+
DataSource.succeed
|
|
808
|
+
[ { name = "authors"
|
|
809
|
+
, extension = Yml
|
|
810
|
+
}
|
|
811
|
+
, { name = "events"
|
|
812
|
+
, extension = Json
|
|
813
|
+
}
|
|
814
|
+
]
|
|
815
|
+
|
|
816
|
+
You could also match an optional file path segment using `oneOf`.
|
|
817
|
+
|
|
818
|
+
rootFilesMd : DataSource (List String)
|
|
819
|
+
rootFilesMd =
|
|
820
|
+
Glob.succeed (\slug -> slug)
|
|
821
|
+
|> Glob.match (Glob.literal "blog/")
|
|
822
|
+
|> Glob.capture Glob.wildcard
|
|
823
|
+
|> Glob.match
|
|
824
|
+
(Glob.oneOf
|
|
825
|
+
( ( "", () )
|
|
826
|
+
, [ ( "/index", () ) ]
|
|
827
|
+
)
|
|
828
|
+
)
|
|
829
|
+
|> Glob.match (Glob.literal ".md")
|
|
830
|
+
|> Glob.toDataSource
|
|
831
|
+
|
|
832
|
+
With these files:
|
|
833
|
+
|
|
834
|
+
```markdown
|
|
835
|
+
- blog/
|
|
836
|
+
- first-post.md
|
|
837
|
+
- second-post/
|
|
838
|
+
- index.md
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
This would give us:
|
|
842
|
+
|
|
843
|
+
results : DataSource (List String)
|
|
844
|
+
results =
|
|
845
|
+
DataSource.succeed
|
|
846
|
+
[ "first-post"
|
|
847
|
+
, "second-post"
|
|
848
|
+
]
|
|
849
|
+
|
|
850
|
+
-}
|
|
851
|
+
oneOf : ( ( String, a ), List ( String, a ) ) -> Glob a
|
|
852
|
+
oneOf ( defaultMatch, otherMatchers ) =
|
|
853
|
+
let
|
|
854
|
+
allMatchers : List ( String, a )
|
|
855
|
+
allMatchers =
|
|
856
|
+
defaultMatch :: otherMatchers
|
|
857
|
+
in
|
|
858
|
+
Glob
|
|
859
|
+
("{"
|
|
860
|
+
++ (allMatchers |> List.map Tuple.first |> String.join ",")
|
|
861
|
+
++ "}"
|
|
862
|
+
)
|
|
863
|
+
("("
|
|
864
|
+
++ String.join "|"
|
|
865
|
+
((allMatchers |> List.map Tuple.first |> List.map regexEscaped)
|
|
866
|
+
|> List.map regexEscaped
|
|
867
|
+
)
|
|
868
|
+
++ ")"
|
|
869
|
+
)
|
|
870
|
+
(\_ captures ->
|
|
871
|
+
case captures of
|
|
872
|
+
match_ :: rest ->
|
|
873
|
+
( allMatchers
|
|
874
|
+
|> List.Extra.findMap
|
|
875
|
+
(\( literalString, result ) ->
|
|
876
|
+
if literalString == match_ then
|
|
877
|
+
Just result
|
|
878
|
+
|
|
879
|
+
else
|
|
880
|
+
Nothing
|
|
881
|
+
)
|
|
882
|
+
|> Maybe.withDefault (defaultMatch |> Tuple.second)
|
|
883
|
+
, rest
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
[] ->
|
|
887
|
+
( Tuple.second defaultMatch, [] )
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
{-| -}
|
|
892
|
+
atLeastOne : ( ( String, a ), List ( String, a ) ) -> Glob ( a, List a )
|
|
893
|
+
atLeastOne ( defaultMatch, otherMatchers ) =
|
|
894
|
+
let
|
|
895
|
+
allMatchers : List ( String, a )
|
|
896
|
+
allMatchers =
|
|
897
|
+
defaultMatch :: otherMatchers
|
|
898
|
+
in
|
|
899
|
+
Glob
|
|
900
|
+
("+("
|
|
901
|
+
++ (allMatchers |> List.map Tuple.first |> String.join "|")
|
|
902
|
+
++ ")"
|
|
903
|
+
)
|
|
904
|
+
("((?:"
|
|
905
|
+
++ (allMatchers |> List.map Tuple.first |> List.map regexEscaped |> String.join "|")
|
|
906
|
+
++ ")+)"
|
|
907
|
+
)
|
|
908
|
+
(\_ captures ->
|
|
909
|
+
case captures of
|
|
910
|
+
match_ :: rest ->
|
|
911
|
+
( --( allMatchers
|
|
912
|
+
-- |> List.Extra.findMap
|
|
913
|
+
-- (\( literalString, result ) ->
|
|
914
|
+
-- if literalString == match_ then
|
|
915
|
+
-- Just result
|
|
916
|
+
--
|
|
917
|
+
-- else
|
|
918
|
+
-- Nothing
|
|
919
|
+
-- )
|
|
920
|
+
-- |> Maybe.withDefault (defaultMatch |> Tuple.second)
|
|
921
|
+
-- , []
|
|
922
|
+
-- )
|
|
923
|
+
DataSource.Internal.Glob.extractMatches (defaultMatch |> Tuple.second) allMatchers match_
|
|
924
|
+
|> toNonEmptyWithDefault (defaultMatch |> Tuple.second)
|
|
925
|
+
, rest
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
[] ->
|
|
929
|
+
( ( Tuple.second defaultMatch, [] ), [] )
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
toNonEmptyWithDefault : a -> List a -> ( a, List a )
|
|
934
|
+
toNonEmptyWithDefault default list =
|
|
935
|
+
case list of
|
|
936
|
+
first :: rest ->
|
|
937
|
+
( first, rest )
|
|
938
|
+
|
|
939
|
+
_ ->
|
|
940
|
+
( default, [] )
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
{-| In order to get match data from your glob, turn it into a `DataSource` with this function.
|
|
944
|
+
-}
|
|
945
|
+
toDataSource : Glob a -> DataSource (List a)
|
|
946
|
+
toDataSource glob =
|
|
947
|
+
DataSource.Http.get (Secrets.succeed <| "glob://" ++ DataSource.Internal.Glob.toPattern glob)
|
|
948
|
+
(OptimizedDecoder.string
|
|
949
|
+
|> OptimizedDecoder.list
|
|
950
|
+
|> OptimizedDecoder.map
|
|
951
|
+
(\rawGlob -> rawGlob |> List.map (\matchedPath -> DataSource.Internal.Glob.run matchedPath glob |> .match))
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
{-| Sometimes you want to make sure there is a unique file matching a particular pattern.
|
|
956
|
+
This is a simple helper that will give you a `DataSource` error if there isn't exactly 1 matching file.
|
|
957
|
+
If there is exactly 1, then you successfully get back that single match.
|
|
958
|
+
|
|
959
|
+
For example, maybe you can have
|
|
960
|
+
|
|
961
|
+
import DataSource exposing (DataSource)
|
|
962
|
+
import DataSource.Glob as Glob
|
|
963
|
+
|
|
964
|
+
findBlogBySlug : String -> DataSource String
|
|
965
|
+
findBlogBySlug slug =
|
|
966
|
+
Glob.succeed identity
|
|
967
|
+
|> Glob.captureFilePath
|
|
968
|
+
|> Glob.match (Glob.literal "blog/")
|
|
969
|
+
|> Glob.capture (Glob.literal slug)
|
|
970
|
+
|> Glob.match
|
|
971
|
+
(Glob.oneOf
|
|
972
|
+
( ( "", () )
|
|
973
|
+
, [ ( "/index", () ) ]
|
|
974
|
+
)
|
|
975
|
+
)
|
|
976
|
+
|> Glob.match (Glob.literal ".md")
|
|
977
|
+
|> Glob.expectUniqueMatch
|
|
978
|
+
|
|
979
|
+
If we used `findBlogBySlug "first-post"` with these files:
|
|
980
|
+
|
|
981
|
+
```markdown
|
|
982
|
+
- blog/
|
|
983
|
+
- first-post/
|
|
984
|
+
- index.md
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
This would give us:
|
|
988
|
+
|
|
989
|
+
results : DataSource String
|
|
990
|
+
results =
|
|
991
|
+
DataSource.succeed "blog/first-post/index.md"
|
|
992
|
+
|
|
993
|
+
If we used `findBlogBySlug "first-post"` with these files:
|
|
994
|
+
|
|
995
|
+
```markdown
|
|
996
|
+
- blog/
|
|
997
|
+
- first-post.md
|
|
998
|
+
- first-post/
|
|
999
|
+
- index.md
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
Then we will get a `DataSource` error saying `More than one file matched.` Keep in mind that `DataSource` failures
|
|
1003
|
+
in build-time routes will cause a build failure, giving you the opportunity to fix the problem before users see the issue,
|
|
1004
|
+
so it's ideal to make this kind of assertion rather than having fallback behavior that could silently cover up
|
|
1005
|
+
issues (like if we had instead ignored the case where there are two or more matching blog post files).
|
|
1006
|
+
|
|
1007
|
+
-}
|
|
1008
|
+
expectUniqueMatch : Glob a -> DataSource a
|
|
1009
|
+
expectUniqueMatch glob =
|
|
1010
|
+
glob
|
|
1011
|
+
|> toDataSource
|
|
1012
|
+
|> DataSource.andThen
|
|
1013
|
+
(\matchingFiles ->
|
|
1014
|
+
case matchingFiles of
|
|
1015
|
+
[ file ] ->
|
|
1016
|
+
DataSource.succeed file
|
|
1017
|
+
|
|
1018
|
+
[] ->
|
|
1019
|
+
DataSource.fail <| "No files matched the pattern: " ++ toPatternString glob
|
|
1020
|
+
|
|
1021
|
+
_ ->
|
|
1022
|
+
DataSource.fail "More than one file matched."
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
{-| -}
|
|
1027
|
+
expectUniqueMatchFromList : List (Glob a) -> DataSource a
|
|
1028
|
+
expectUniqueMatchFromList globs =
|
|
1029
|
+
globs
|
|
1030
|
+
|> List.map toDataSource
|
|
1031
|
+
|> DataSource.combine
|
|
1032
|
+
|> DataSource.andThen
|
|
1033
|
+
(\matchingFiles ->
|
|
1034
|
+
case List.concat matchingFiles of
|
|
1035
|
+
[ file ] ->
|
|
1036
|
+
DataSource.succeed file
|
|
1037
|
+
|
|
1038
|
+
[] ->
|
|
1039
|
+
DataSource.fail <| "No files matched the patterns: " ++ (globs |> List.map toPatternString |> String.join ", ")
|
|
1040
|
+
|
|
1041
|
+
_ ->
|
|
1042
|
+
DataSource.fail "More than one file matched."
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
toPatternString : Glob a -> String
|
|
1047
|
+
toPatternString glob =
|
|
1048
|
+
case glob of
|
|
1049
|
+
Glob pattern_ _ _ ->
|
|
1050
|
+
pattern_
|