elm-pages 2.1.6 → 2.1.10

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.
Files changed (75) hide show
  1. package/generator/review/elm.json +34 -0
  2. package/generator/review/src/ReviewConfig.elm +10 -0
  3. package/generator/src/basepath-middleware.js +15 -9
  4. package/generator/src/build.js +100 -6
  5. package/generator/src/cli.js +13 -9
  6. package/generator/src/compile-elm.js +43 -0
  7. package/generator/src/dev-server.js +63 -11
  8. package/generator/src/error-formatter.js +62 -9
  9. package/generator/src/generate-template-module-connector.js +17 -4
  10. package/generator/src/init.js +4 -0
  11. package/generator/src/pre-render-html.js +19 -12
  12. package/generator/src/render-worker.js +0 -1
  13. package/generator/src/render.js +1 -2
  14. package/generator/src/seo-renderer.js +21 -2
  15. package/generator/static-code/hmr.js +43 -6
  16. package/generator/template/elm-tooling.json +9 -0
  17. package/generator/template/package.json +5 -1
  18. package/package.json +16 -9
  19. package/src/ApiRoute.elm +178 -0
  20. package/src/AriaLiveAnnouncer.elm +36 -0
  21. package/src/BuildError.elm +60 -0
  22. package/src/DataSource/File.elm +288 -0
  23. package/src/DataSource/Glob.elm +1050 -0
  24. package/src/DataSource/Http.elm +467 -0
  25. package/src/DataSource/Internal/Glob.elm +74 -0
  26. package/src/DataSource/Port.elm +87 -0
  27. package/src/DataSource/ServerRequest.elm +60 -0
  28. package/src/DataSource.elm +801 -0
  29. package/src/Head/Seo.elm +516 -0
  30. package/src/Head/Twitter.elm +109 -0
  31. package/src/Head.elm +452 -0
  32. package/src/HtmlPrinter.elm +27 -0
  33. package/src/Internal/ApiRoute.elm +89 -0
  34. package/src/Internal/OptimizedDecoder.elm +18 -0
  35. package/src/KeepOrDiscard.elm +6 -0
  36. package/src/OptimizedDecoder/Pipeline.elm +335 -0
  37. package/src/OptimizedDecoder.elm +818 -0
  38. package/src/Pages/ContentCache.elm +248 -0
  39. package/src/Pages/Flags.elm +26 -0
  40. package/src/Pages/Http.elm +10 -0
  41. package/src/Pages/Internal/ApplicationType.elm +6 -0
  42. package/src/Pages/Internal/NotFoundReason.elm +256 -0
  43. package/src/Pages/Internal/Platform/Cli.elm +1015 -0
  44. package/src/Pages/Internal/Platform/Effect.elm +14 -0
  45. package/src/Pages/Internal/Platform/StaticResponses.elm +540 -0
  46. package/src/Pages/Internal/Platform/ToJsPayload.elm +138 -0
  47. package/src/Pages/Internal/Platform.elm +745 -0
  48. package/src/Pages/Internal/RoutePattern.elm +122 -0
  49. package/src/Pages/Internal/Router.elm +116 -0
  50. package/src/Pages/Internal/StaticHttpBody.elm +54 -0
  51. package/src/Pages/Internal/String.elm +39 -0
  52. package/src/Pages/Manifest/Category.elm +240 -0
  53. package/src/Pages/Manifest.elm +412 -0
  54. package/src/Pages/PageUrl.elm +38 -0
  55. package/src/Pages/ProgramConfig.elm +73 -0
  56. package/src/Pages/Review/NoContractViolations.elm +397 -0
  57. package/src/Pages/Secrets.elm +83 -0
  58. package/src/Pages/SiteConfig.elm +13 -0
  59. package/src/Pages/StaticHttp/Request.elm +42 -0
  60. package/src/Pages/StaticHttpRequest.elm +320 -0
  61. package/src/Pages/Url.elm +60 -0
  62. package/src/Path.elm +96 -0
  63. package/src/QueryParams.elm +216 -0
  64. package/src/RenderRequest.elm +163 -0
  65. package/src/RequestsAndPending.elm +20 -0
  66. package/src/Secrets.elm +111 -0
  67. package/src/SecretsDict.elm +45 -0
  68. package/src/StructuredData.elm +236 -0
  69. package/src/TerminalText.elm +242 -0
  70. package/src/Test/Html/Internal/ElmHtml/Constants.elm +53 -0
  71. package/src/Test/Html/Internal/ElmHtml/Helpers.elm +17 -0
  72. package/src/Test/Html/Internal/ElmHtml/InternalTypes.elm +529 -0
  73. package/src/Test/Html/Internal/ElmHtml/Markdown.elm +56 -0
  74. package/src/Test/Html/Internal/ElmHtml/ToString.elm +197 -0
  75. 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_