From ba6f0903cef9e9cef9147007500514f415a0c73c Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 10:16:00 -0700 Subject: [PATCH 01/69] Add boilerplate for elm-codegen. --- .gitignore | 1 + codegen/.gitignore | 2 + codegen/Generate.elm | 24 ++++++++++ codegen/elm.codegen.json | 13 ++++++ codegen/elm.json | 34 ++++++++++++++ generator/src/codegen.js | 21 ++++++--- .../src/generate-template-module-connector.js | 42 ++++++++++++++++- package-lock.json | 45 +++++++++++++++++++ package.json | 1 + 9 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 codegen/.gitignore create mode 100644 codegen/Generate.elm create mode 100644 codegen/elm.codegen.json create mode 100644 codegen/elm.json diff --git a/.gitignore b/.gitignore index b35492d3..e5dc7c6a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ tests/VerifyExamples/ cypress/videos cypress/screenshots .idea +generated/ diff --git a/codegen/.gitignore b/codegen/.gitignore new file mode 100644 index 00000000..3dfc6adc --- /dev/null +++ b/codegen/.gitignore @@ -0,0 +1,2 @@ +Gen/ + diff --git a/codegen/Generate.elm b/codegen/Generate.elm new file mode 100644 index 00000000..61dc7dc3 --- /dev/null +++ b/codegen/Generate.elm @@ -0,0 +1,24 @@ +module Generate exposing (main) + +{-| -} + +import Elm +import Elm.Annotation as Type +import Gen.CodeGen.Generate as Generate +import Gen.Helper + + +main : Program {} () () +main = + Generate.run + [ file + ] + + +file : Elm.File +file = + Elm.file [ "Route" ] + [ Elm.customType "Route" + [ Elm.variant "Index" + ] + ] diff --git a/codegen/elm.codegen.json b/codegen/elm.codegen.json new file mode 100644 index 00000000..19364cd6 --- /dev/null +++ b/codegen/elm.codegen.json @@ -0,0 +1,13 @@ +{ + "elm-codegen-version": "0.2.0", + "codegen-helpers": { + "packages": { + "elm/core": "1.0.5", + "elm/html": "1.0.0" + }, + "local": [ + "codegen/helpers/", + "src/" + ] + } +} \ No newline at end of file diff --git a/codegen/elm.json b/codegen/elm.json new file mode 100644 index 00000000..9f5b2dbe --- /dev/null +++ b/codegen/elm.json @@ -0,0 +1,34 @@ + +{ + "type": "application", + "source-directories": [ + "." + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.0", + "elm/json": "1.1.3", + "mdgriffith/elm-codegen": "2.0.0" + }, + "indirect": { + "elm/parser": "1.1.0", + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.2", + "elm-community/basics-extra": "4.1.0", + "elm-community/list-extra": "8.6.0", + "miniBill/elm-unicode": "1.0.2", + "rtfeldman/elm-hex": "1.0.0", + "stil4m/elm-syntax": "7.2.9", + "stil4m/structured-writer": "1.0.3", + "the-sett/elm-pretty-printer": "3.0.0" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/generator/src/codegen.js b/generator/src/codegen.js index 12251b6f..5f474a56 100644 --- a/generator/src/codegen.js +++ b/generator/src/codegen.js @@ -16,8 +16,11 @@ global.builtAt = new Date(); * @param {string} basePath */ async function generate(basePath) { - const cliCode = generateTemplateModuleConnector(basePath, "cli"); - const browserCode = generateTemplateModuleConnector(basePath, "browser"); + const cliCode = await generateTemplateModuleConnector(basePath, "cli"); + const browserCode = await generateTemplateModuleConnector( + basePath, + "browser" + ); ensureDirSync("./elm-stuff"); ensureDirSync("./.elm-pages"); ensureDirSync("./gen"); @@ -46,10 +49,13 @@ async function generate(basePath) { ), fs.promises.writeFile( "./elm-stuff/elm-pages/.elm-pages/Route.elm", - cliCode.routesModule + cliCode.routesModuleNew ), fs.promises.writeFile("./.elm-pages/Main.elm", browserCode.mainModule), - fs.promises.writeFile("./.elm-pages/Route.elm", browserCode.routesModule), + fs.promises.writeFile( + "./.elm-pages/Route.elm", + browserCode.routesModuleNew + ), writeFetcherModules("./.elm-pages", browserCode.fetcherModules), writeFetcherModules( "./elm-stuff/elm-pages/client/.elm-pages", @@ -83,7 +89,10 @@ async function newCopyBoth(modulePath) { } async function generateClientFolder(basePath) { - const browserCode = generateTemplateModuleConnector(basePath, "browser"); + const browserCode = await generateTemplateModuleConnector( + basePath, + "browser" + ); const uiFileContent = elmPagesUiFile(); ensureDirSync("./elm-stuff/elm-pages/client/app"); ensureDirSync("./elm-stuff/elm-pages/client/.elm-pages"); @@ -102,7 +111,7 @@ async function generateClientFolder(basePath) { ); await fs.promises.writeFile( "./elm-stuff/elm-pages/client/.elm-pages/Route.elm", - browserCode.routesModule + browserCode.routesModuleNew ); await fs.promises.writeFile( "./elm-stuff/elm-pages/client/.elm-pages/Pages.elm", diff --git a/generator/src/generate-template-module-connector.js b/generator/src/generate-template-module-connector.js index bfbc87b3..ae2beac4 100644 --- a/generator/src/generate-template-module-connector.js +++ b/generator/src/generate-template-module-connector.js @@ -2,12 +2,14 @@ const globby = require("globby"); const path = require("path"); const mm = require("micromatch"); const routeHelpers = require("./route-codegen-helpers"); +const { runElmCodegenInstall } = require("./elm-codegen"); +const { compileCliApp } = require("./compile-elm"); /** * @param {string} basePath * @param {'browser' | 'cli'} phase */ -function generateTemplateModuleConnector(basePath, phase) { +async function generateTemplateModuleConnector(basePath, phase) { const templates = globby.sync(["app/Route/**/*.elm"], {}).map((file) => { const captures = mm.capture("app/Route/**/*.elm", file); if (captures) { @@ -36,6 +38,7 @@ function generateTemplateModuleConnector(basePath, phase) { ], }; } + const routesModuleNew = await runElmCodegenCli(templates); return { mainModule: `port module Main exposing (..) @@ -997,6 +1000,7 @@ decodeBytes bytesDecoder items = -- Lamdera.Wire3.bytesDecodeStrict bytesDecoder items |> Result.fromMaybe "Decoding error" `, + routesModuleNew, routesModule: `module Route exposing (baseUrlAsPath, Route(..), link, matchers, routeToPath, toLink, urlToRoute, toPath, redirectTo, toString) {-| @@ -1142,6 +1146,42 @@ redirectTo route = }; } +async function runElmCodegenCli(templates) { + // await runElmCodegenInstall(); + await compileCliApp( + // { debug: true }, + {}, + `Generate.elm`, + path.join(process.cwd(), "elm-stuff/elm-pages-codegen.js"), + path.join(__dirname, "../../codegen"), + + path.join(process.cwd(), "elm-stuff/elm-pages-codegen.js") + ); + + // TODO use uncached require here to prevent stale code from running + + const promise = new Promise((resolve, reject) => { + const elmPagesCodegen = require(path.join( + process.cwd(), + "./elm-stuff/elm-pages-codegen.js" + )).Elm.Generate; + + const app = elmPagesCodegen.init({ flags: {} }); + if (app.ports.onSuccessSend) { + app.ports.onSuccessSend.subscribe(resolve); + } + if (app.ports.onInfoSend) { + app.ports.onInfoSend.subscribe((info) => console.log(info)); + } + if (app.ports.onFailureSend) { + app.ports.onFailureSend.subscribe(reject); + } + }); + const filesToGenerate = await promise; + + return filesToGenerate[0].contents; +} + function emptyRouteParams(name) { return routeHelpers.parseRouteParams(name).length === 0; } diff --git a/package-lock.json b/package-lock.json index 975c1ea8..4be43445 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "@types/node": "12.20.12", "@types/serve-static": "^1.15.0", "cypress": "^10.6.0", + "elm-codegen": "^0.2.0", "elm-optimize-level-2": "^0.1.5", "elm-review": "^2.7.4", "elm-test": "^0.19.1-revision9", @@ -1680,6 +1681,30 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "node_modules/elm-codegen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/elm-codegen/-/elm-codegen-0.2.0.tgz", + "integrity": "sha512-JXEbEl8wctVf47uH8M9gE5YF59e7YcSsBjofsPihepRSpPya+IYcva0qANlmNp1/N/p4T0HXXPbSiI3ake47VA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.1", + "chokidar": "^3.5.1", + "commander": "^8.3.0", + "node-elm-compiler": "^5.0.6" + }, + "bin": { + "elm-codegen": "bin/elm-codegen" + } + }, + "node_modules/elm-codegen/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/elm-doc-preview": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/elm-doc-preview/-/elm-doc-preview-5.0.5.tgz", @@ -8111,6 +8136,26 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "elm-codegen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/elm-codegen/-/elm-codegen-0.2.0.tgz", + "integrity": "sha512-JXEbEl8wctVf47uH8M9gE5YF59e7YcSsBjofsPihepRSpPya+IYcva0qANlmNp1/N/p4T0HXXPbSiI3ake47VA==", + "dev": true, + "requires": { + "chalk": "^4.1.1", + "chokidar": "^3.5.1", + "commander": "^8.3.0", + "node-elm-compiler": "^5.0.6" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + } + } + }, "elm-doc-preview": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/elm-doc-preview/-/elm-doc-preview-5.0.5.tgz", diff --git a/package.json b/package.json index e864a548..a1e4cd8a 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/node": "12.20.12", "@types/serve-static": "^1.15.0", "cypress": "^10.6.0", + "elm-codegen": "^0.2.0", "elm-optimize-level-2": "^0.1.5", "elm-review": "^2.7.4", "elm-test": "^0.19.1-revision9", From aafbfd22d23effcf8f3ec3b44bae1059e541617f Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 11:47:51 -0700 Subject: [PATCH 02/69] Move decapitlize logic out of parsing for RoutePattern and into building up record fields. --- src/Pages/Internal/RoutePattern.elm | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 9a9f030f..6e6399a0 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -56,7 +56,7 @@ toRouteParamsRecord pattern = [] DynamicSegment name -> - [ ( name, Elm.Annotation.string ) ] + [ ( name |> decapitalize, Elm.Annotation.string ) ] ) ) ++ (case pattern.ending of @@ -78,7 +78,7 @@ toRouteParamsRecord pattern = ] Just (Optional name) -> - [ ( name + [ ( name |> decapitalize , Elm.Annotation.maybe Elm.Annotation.string ) ] @@ -95,7 +95,7 @@ toRouteParamTypes pattern = [] DynamicSegment name -> - [ ( name, RequiredParam ) ] + [ ( name |> decapitalize, RequiredParam ) ] ) ) ++ (case pattern.ending of @@ -115,7 +115,7 @@ toRouteParamTypes pattern = ] Just (Optional name) -> - [ ( name + [ ( name |> decapitalize , OptionalParam ) ] @@ -199,7 +199,6 @@ tryAsEnding segment = else if segment |> String.endsWith "__" then (segment |> String.dropRight 2 - |> decapitalize |> Optional ) |> Just @@ -213,13 +212,10 @@ segmentToParam segment = if segment |> String.endsWith "_" then segment |> String.dropRight 1 - |> decapitalize |> DynamicSegment else segment - |> String.dropRight 1 - |> decapitalize |> StaticSegment From d8639e617b61fce3ee47876efbec9736aaa64318 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 11:49:02 -0700 Subject: [PATCH 03/69] Pass in list of templates to new code generation for Route module. --- codegen/Generate.elm | 88 ++++++++++++++++--- codegen/elm.json | 3 +- .../src/generate-template-module-connector.js | 2 +- 3 files changed, 80 insertions(+), 13 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 61dc7dc3..b88b8cc6 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -1,24 +1,90 @@ -module Generate exposing (main) +port module Generate exposing (main) {-| -} -import Elm +import Elm exposing (File) import Elm.Annotation as Type -import Gen.CodeGen.Generate as Generate +import Gen.CodeGen.Generate as Generate exposing (Error) import Gen.Helper +import Pages.Internal.RoutePattern as RoutePattern -main : Program {} () () +type alias Flags = + { templates : List (List String) + } + + +main : Program Flags () () main = - Generate.run - [ file - ] + Platform.worker + { init = + \{ templates } -> + ( () + , onSuccessSend [ file templates ] + ) + , update = + \_ model -> + ( model, Cmd.none ) + , subscriptions = \_ -> Sub.none + } -file : Elm.File -file = +file : List (List String) -> Elm.File +file templates = + let + routes : List RoutePattern.RoutePattern + routes = + templates + |> List.filterMap RoutePattern.fromModuleName + in Elm.file [ "Route" ] [ Elm.customType "Route" - [ Elm.variant "Index" - ] + (routes + |> List.map + (\route -> + route.segments + |> List.map + (\segment -> + case segment of + RoutePattern.DynamicSegment name -> + name ++ "_" + + RoutePattern.StaticSegment name -> + name + ) + |> String.join "__" + |> addEnding route.ending + |> Elm.variant + ) + ) ] + + +addEnding : Maybe RoutePattern.Ending -> String -> String +addEnding maybeEnding string = + case maybeEnding of + Nothing -> + string + + Just ending -> + string + ++ "__" + ++ (case ending of + RoutePattern.Optional name -> + name ++ "__" + + RoutePattern.RequiredSplat -> + "SPLAT_" + + RoutePattern.OptionalSplat -> + "SPLAT__" + ) + + +port onSuccessSend : List File -> Cmd msg + + +port onFailureSend : List Error -> Cmd msg + + +port onInfoSend : String -> Cmd msg diff --git a/codegen/elm.json b/codegen/elm.json index 9f5b2dbe..376f2c3e 100644 --- a/codegen/elm.json +++ b/codegen/elm.json @@ -2,7 +2,8 @@ { "type": "application", "source-directories": [ - "." + ".", + "../src/" ], "elm-version": "0.19.1", "dependencies": { diff --git a/generator/src/generate-template-module-connector.js b/generator/src/generate-template-module-connector.js index ae2beac4..40c9e35b 100644 --- a/generator/src/generate-template-module-connector.js +++ b/generator/src/generate-template-module-connector.js @@ -1166,7 +1166,7 @@ async function runElmCodegenCli(templates) { "./elm-stuff/elm-pages-codegen.js" )).Elm.Generate; - const app = elmPagesCodegen.init({ flags: {} }); + const app = elmPagesCodegen.init({ flags: { templates: templates } }); if (app.ports.onSuccessSend) { app.ports.onSuccessSend.subscribe(resolve); } From c27459fa8d0f53d0597934c1383b2c637d28002e Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 11:59:36 -0700 Subject: [PATCH 04/69] Add RoutePattern.toVariant helper. --- src/Pages/Internal/RoutePattern.elm | 45 ++++++++++++++++++++++++++--- tests/Pages/RouteParamsTest.elm | 15 ++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 6e6399a0..49e8f0bf 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -1,14 +1,15 @@ module Pages.Internal.RoutePattern exposing - ( Ending(..), RoutePattern, Segment(..), view + ( Ending(..), RoutePattern, Segment(..), view, toVariant , Param(..), fromModuleName, toRouteParamTypes, toRouteParamsRecord ) {-| Exposed for internal use only (used in generated code). -@docs Ending, RoutePattern, Segment, view +@docs Ending, RoutePattern, Segment, view, toVariant -} +import Elm import Elm.Annotation exposing (Annotation) import Html exposing (Html) @@ -43,7 +44,7 @@ fromModuleName moduleNameSegments = |> Just [] -> - Nothing + Just { segments = [], ending = Nothing } toRouteParamsRecord : RoutePattern -> List ( String, Annotation ) @@ -122,8 +123,44 @@ toRouteParamTypes pattern = ) +{-| -} +toVariant : RoutePattern -> Elm.Variant +toVariant pattern = + if List.isEmpty pattern.segments && pattern.ending == Nothing then + Elm.variant "Index" ---[] + else + ((pattern.segments + |> List.map + (\segment -> + case segment of + DynamicSegment name -> + name ++ "_" + + StaticSegment name -> + name + ) + ) + ++ ([ Maybe.map endingToVariantName pattern.ending + ] + |> List.filterMap identity + ) + ) + |> String.join "__" + |> Elm.variant + + +endingToVariantName : Ending -> String +endingToVariantName ending = + case ending of + Optional name -> + name ++ "__" + + RequiredSplat -> + "SPLAT_" + + OptionalSplat -> + "SPLAT__" {-| -} diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 39f71f87..eebce835 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -1,5 +1,6 @@ module Pages.RouteParamsTest exposing (..) +import Elm import Elm.Annotation import Expect import Pages.Internal.RoutePattern as RoutePattern @@ -67,4 +68,18 @@ suite = [ ( "section", Elm.Annotation.maybe Elm.Annotation.string ) ] ) + , describe "toRouteVariant" + [ test "root route" <| + \() -> + RoutePattern.fromModuleName [] + |> Maybe.map RoutePattern.toVariant + |> Expect.equal + (Just (Elm.variant "Index")) + , test "static-only route" <| + \() -> + RoutePattern.fromModuleName [ "About" ] + |> Maybe.map RoutePattern.toVariant + |> Expect.equal + (Just (Elm.variant "About")) + ] ] From 3f12c6ccb50b2d73bddd0d738aa3daaa03e8f57f Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 12:16:18 -0700 Subject: [PATCH 05/69] Add test for dynamic route param. --- codegen/Generate.elm | 44 +-------------------- src/Pages/Internal/RoutePattern.elm | 59 +++++++++++++++++++---------- tests/Pages/RouteParamsTest.elm | 13 +++++++ 3 files changed, 54 insertions(+), 62 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index b88b8cc6..6f383445 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -3,9 +3,7 @@ port module Generate exposing (main) {-| -} import Elm exposing (File) -import Elm.Annotation as Type -import Gen.CodeGen.Generate as Generate exposing (Error) -import Gen.Helper +import Gen.CodeGen.Generate exposing (Error) import Pages.Internal.RoutePattern as RoutePattern @@ -39,48 +37,10 @@ file templates = in Elm.file [ "Route" ] [ Elm.customType "Route" - (routes - |> List.map - (\route -> - route.segments - |> List.map - (\segment -> - case segment of - RoutePattern.DynamicSegment name -> - name ++ "_" - - RoutePattern.StaticSegment name -> - name - ) - |> String.join "__" - |> addEnding route.ending - |> Elm.variant - ) - ) + (routes |> List.map RoutePattern.toVariant) ] -addEnding : Maybe RoutePattern.Ending -> String -> String -addEnding maybeEnding string = - case maybeEnding of - Nothing -> - string - - Just ending -> - string - ++ "__" - ++ (case ending of - RoutePattern.Optional name -> - name ++ "__" - - RoutePattern.RequiredSplat -> - "SPLAT_" - - RoutePattern.OptionalSplat -> - "SPLAT__" - ) - - port onSuccessSend : List File -> Cmd msg diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 49e8f0bf..9c3e5593 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -130,37 +130,56 @@ toVariant pattern = Elm.variant "Index" else - ((pattern.segments - |> List.map - (\segment -> - case segment of - DynamicSegment name -> - name ++ "_" + let + something = + (pattern.segments + |> List.map + (\segment -> + case segment of + DynamicSegment name -> + ( name ++ "_", Just ( decapitalize name, Elm.Annotation.string ) ) - StaticSegment name -> - name + StaticSegment name -> + ( name, Nothing ) + ) ) - ) - ++ ([ Maybe.map endingToVariantName pattern.ending - ] - |> List.filterMap identity - ) - ) - |> String.join "__" - |> Elm.variant + ++ ([ Maybe.map endingToVariantName pattern.ending + ] + |> List.filterMap identity + ) + + fieldThings : List ( String, Annotation ) + fieldThings = + something + |> List.filterMap Tuple.second + + innerType = + case fieldThings of + [] -> + [] + + nonEmpty -> + nonEmpty |> Elm.Annotation.record |> List.singleton + in + Elm.variantWith + (something + |> List.map Tuple.first + |> String.join "__" + ) + innerType -endingToVariantName : Ending -> String +endingToVariantName : Ending -> ( String, Maybe ( String, Annotation ) ) endingToVariantName ending = case ending of Optional name -> - name ++ "__" + ( name ++ "__", Just ( decapitalize name, Elm.Annotation.maybe Elm.Annotation.string ) ) RequiredSplat -> - "SPLAT_" + ( "SPLAT_", Just ( "splat", Elm.Annotation.string ) ) OptionalSplat -> - "SPLAT__" + ( "SPLAT__", Just ( "splat", Elm.Annotation.string ) ) {-| -} diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index eebce835..55b0772a 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -81,5 +81,18 @@ suite = |> Maybe.map RoutePattern.toVariant |> Expect.equal (Just (Elm.variant "About")) + , test "dynamic param" <| + \() -> + RoutePattern.fromModuleName [ "User", "Id_" ] + |> Maybe.map RoutePattern.toVariant + |> Expect.equal + (Just + (Elm.variantWith "User__Id_" + [ Elm.Annotation.record + [ ( "id", Elm.Annotation.string ) + ] + ] + ) + ) ] ] From 7df5986d80a94909831c83959307e654d785918f Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 12:16:35 -0700 Subject: [PATCH 06/69] Sort routes for elm-codegen generator flag. --- generator/src/generate-template-module-connector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generator/src/generate-template-module-connector.js b/generator/src/generate-template-module-connector.js index 40c9e35b..08ab8176 100644 --- a/generator/src/generate-template-module-connector.js +++ b/generator/src/generate-template-module-connector.js @@ -38,7 +38,7 @@ async function generateTemplateModuleConnector(basePath, phase) { ], }; } - const routesModuleNew = await runElmCodegenCli(templates); + const routesModuleNew = await runElmCodegenCli(sortTemplates(templates)); return { mainModule: `port module Main exposing (..) From fdefee7036dcd265207db9150b2bd863143d46b5 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 12:17:32 -0700 Subject: [PATCH 07/69] Update send-grid repo branch. --- examples/todos/send-grid | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/todos/send-grid b/examples/todos/send-grid index 7cf8516d..c05ffc2c 160000 --- a/examples/todos/send-grid +++ b/examples/todos/send-grid @@ -1 +1 @@ -Subproject commit 7cf8516df2f9e4c6995a544217bf87ec57701c9f +Subproject commit c05ffc2c4e2a872e8e862082104c81d87b83d553 From 2115b9be9d257ab1343a66500bdf347cc622dfbb Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 13:19:25 -0700 Subject: [PATCH 08/69] Handle splat route for generating variants. --- src/Pages/Internal/RoutePattern.elm | 9 ++++- tests/Pages/RouteParamsTest.elm | 56 +++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 9c3e5593..e12b5b72 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -176,7 +176,14 @@ endingToVariantName ending = ( name ++ "__", Just ( decapitalize name, Elm.Annotation.maybe Elm.Annotation.string ) ) RequiredSplat -> - ( "SPLAT_", Just ( "splat", Elm.Annotation.string ) ) + ( "SPLAT_" + , Just + ( "splat" + , Elm.Annotation.tuple + Elm.Annotation.string + (Elm.Annotation.list Elm.Annotation.string) + ) + ) OptionalSplat -> ( "SPLAT__", Just ( "splat", Elm.Annotation.string ) ) diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 55b0772a..9d08e8b9 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -2,7 +2,8 @@ module Pages.RouteParamsTest exposing (..) import Elm import Elm.Annotation -import Expect +import Elm.ToString +import Expect exposing (Expectation) import Pages.Internal.RoutePattern as RoutePattern import Test exposing (Test, describe, test) @@ -71,10 +72,9 @@ suite = , describe "toRouteVariant" [ test "root route" <| \() -> - RoutePattern.fromModuleName [] - |> Maybe.map RoutePattern.toVariant - |> Expect.equal - (Just (Elm.variant "Index")) + [] + |> expectRouteDefinition + (Elm.variant "Index") , test "static-only route" <| \() -> RoutePattern.fromModuleName [ "About" ] @@ -83,16 +83,44 @@ suite = (Just (Elm.variant "About")) , test "dynamic param" <| \() -> - RoutePattern.fromModuleName [ "User", "Id_" ] - |> Maybe.map RoutePattern.toVariant - |> Expect.equal - (Just - (Elm.variantWith "User__Id_" - [ Elm.Annotation.record - [ ( "id", Elm.Annotation.string ) - ] + [ "User", "Id_" ] + |> expectRouteDefinition + (Elm.variantWith "User__Id_" + [ Elm.Annotation.record + [ ( "id", Elm.Annotation.string ) ] - ) + ] + ) + , test "required splat" <| + \() -> + [ "Username_", "Repo_", "Blob", "SPLAT_" ] + |> expectRouteDefinition + (Elm.variantWith "Username___Repo___Blob__SPLAT_" + [ Elm.Annotation.record + [ ( "username", Elm.Annotation.string ) + , ( "repo", Elm.Annotation.string ) + , ( "splat" + , Elm.Annotation.tuple + Elm.Annotation.string + (Elm.Annotation.list Elm.Annotation.string) + ) + ] + ] ) ] ] + + +expectRouteDefinition : Elm.Variant -> List String -> Expectation +expectRouteDefinition expected moduleName = + RoutePattern.fromModuleName moduleName + |> Maybe.map (RoutePattern.toVariant >> toString) + |> Maybe.withDefault "" + |> Expect.equal (expected |> toString) + + +toString : Elm.Variant -> String +toString variants = + Elm.customType "Route" [ variants ] + |> Elm.ToString.declaration + |> .body From 261cd64ed5bf0d45fd24401689b54b82c3f2e17b Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 13:20:44 -0700 Subject: [PATCH 09/69] Handle optional splat. --- src/Pages/Internal/RoutePattern.elm | 7 ++++++- tests/Pages/RouteParamsTest.elm | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index e12b5b72..00031aec 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -186,7 +186,12 @@ endingToVariantName ending = ) OptionalSplat -> - ( "SPLAT__", Just ( "splat", Elm.Annotation.string ) ) + ( "SPLAT__" + , Just + ( "splat" + , Elm.Annotation.list Elm.Annotation.string + ) + ) {-| -} diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 9d08e8b9..21915af8 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -107,6 +107,18 @@ suite = ] ] ) + , test "optional splat" <| + \() -> + [ "SPLAT__" ] + |> expectRouteDefinition + (Elm.variantWith "SPLAT__" + [ Elm.Annotation.record + [ ( "splat" + , Elm.Annotation.list Elm.Annotation.string + ) + ] + ] + ) ] ] From a9ac1ca707c03abfc3f089fc5656ff5a69ba3827 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 13:21:41 -0700 Subject: [PATCH 10/69] Add test case for optional segment. --- tests/Pages/RouteParamsTest.elm | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 21915af8..a7046c25 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -119,6 +119,18 @@ suite = ] ] ) + , test "optional param" <| + \() -> + [ "Docs", "Section__" ] + |> expectRouteDefinition + (Elm.variantWith "Docs__Section__" + [ Elm.Annotation.record + [ ( "section" + , Elm.Annotation.maybe Elm.Annotation.string + ) + ] + ] + ) ] ] From d38d3984600201db9a6f963e711a3d7afa160cc7 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 13:43:02 -0700 Subject: [PATCH 11/69] Include hardcoded baseUrl in generated Route file. --- codegen/Generate.elm | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 6f383445..523666c5 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -38,9 +38,21 @@ file templates = Elm.file [ "Route" ] [ Elm.customType "Route" (routes |> List.map RoutePattern.toVariant) + |> expose + , Elm.declaration "baseUrl" (Elm.string "/") + |> expose ] +expose : Elm.Declaration -> Elm.Declaration +expose declaration = + declaration + |> Elm.exposeWith + { exposeConstructor = True + , group = Nothing + } + + port onSuccessSend : List File -> Cmd msg From 2e881e0de5eb45eb04684afca31ca7035d68fd18 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 14:36:54 -0700 Subject: [PATCH 12/69] Add more functions to generated Route.elm. --- codegen/Generate.elm | 55 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 523666c5..857c841a 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -3,7 +3,14 @@ port module Generate exposing (main) {-| -} import Elm exposing (File) +import Elm.Annotation +import Elm.Op +import Gen.Basics import Gen.CodeGen.Generate exposing (Error) +import Gen.List +import Gen.Path +import Gen.Server.Response +import Gen.String import Pages.Internal.RoutePattern as RoutePattern @@ -35,12 +42,58 @@ file templates = templates |> List.filterMap RoutePattern.fromModuleName in - Elm.file [ "Route" ] + Elm.file + [ "Route" ] [ Elm.customType "Route" (routes |> List.map RoutePattern.toVariant) |> expose , Elm.declaration "baseUrl" (Elm.string "/") |> expose + , Elm.declaration "baseUrlAsPath" + (Gen.List.call_.filter + (Elm.fn ( "item", Nothing ) + (\item -> + Gen.Basics.call_.not + (Gen.String.call_.isEmpty item) + ) + ) + (Gen.String.call_.split (Elm.string "/") + (Elm.val "baseUrl") + ) + ) + |> expose + , Elm.declaration "toPath" + (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Path.call_.fromString + (Gen.String.call_.join + (Elm.string "/") + (Elm.Op.append + (Elm.val "baseUrlAsPath") + (Elm.apply (Elm.val "routeToPath") + [ route ] + ) + ) + ) + ) + ) + |> expose + , Elm.declaration "toString" + (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Path.toAbsolute + (Elm.apply (Elm.val "toPath") [ route ]) + ) + ) + |> expose + , Elm.declaration "redirectTo" + (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Server.Response.call_.temporaryRedirect + route + ) + ) + |> expose ] From 012aac229a9710ec0ef7db75863aea39b6f31163 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 15:01:59 -0700 Subject: [PATCH 13/69] Add initial function generation for segmentsToRoute. --- codegen/Generate.elm | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 857c841a..6ddcd8f6 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -4,6 +4,7 @@ port module Generate exposing (main) import Elm exposing (File) import Elm.Annotation +import Elm.Case import Elm.Op import Gen.Basics import Gen.CodeGen.Generate exposing (Error) @@ -11,7 +12,7 @@ import Gen.List import Gen.Path import Gen.Server.Response import Gen.String -import Pages.Internal.RoutePattern as RoutePattern +import Pages.Internal.RoutePattern as RoutePattern exposing (RoutePattern) type alias Flags = @@ -47,6 +48,36 @@ file templates = [ Elm.customType "Route" (routes |> List.map RoutePattern.toVariant) |> expose + , Elm.declaration "segmentsToRoute" + (Elm.fn + ( "segments" + , Elm.Annotation.list Elm.Annotation.string |> Just + ) + (\segments -> + Elm.Case.custom segments + (Elm.Annotation.list Elm.Annotation.string) + ((routes |> List.map routeToBranch) + ++ [ Elm.Case.otherwise (\_ -> Elm.nothing) ] + ) + |> Elm.withType + (Elm.Annotation.named [] "Route" + |> Elm.Annotation.maybe + ) + ) + ) + |> expose + , Elm.declaration "urlToRoute" + (Elm.fn + ( "url" + , Elm.Annotation.extensible "url" [ ( "path", Elm.Annotation.string ) ] + |> Just + ) + (\url -> + url + |> Elm.get "path" + ) + ) + |> expose , Elm.declaration "baseUrl" (Elm.string "/") |> expose , Elm.declaration "baseUrlAsPath" @@ -97,6 +128,15 @@ file templates = ] +routeToBranch : RoutePattern -> Elm.Case.Branch +routeToBranch route = + Elm.Case.branchList 0 + (\_ -> + Elm.val "Index" + |> Elm.just + ) + + expose : Elm.Declaration -> Elm.Declaration expose declaration = declaration From fe3c79f30a7654e559da20df4365e9ba1425bb3b Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 15:08:11 -0700 Subject: [PATCH 14/69] Add test setup for branch generator. --- src/Pages/Internal/RoutePattern.elm | 14 ++++++++++++-- tests/Pages/RouteParamsTest.elm | 30 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 00031aec..b07496bc 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -1,16 +1,17 @@ module Pages.Internal.RoutePattern exposing - ( Ending(..), RoutePattern, Segment(..), view, toVariant + ( Ending(..), RoutePattern, Segment(..), view, toVariant, routeToBranch , Param(..), fromModuleName, toRouteParamTypes, toRouteParamsRecord ) {-| Exposed for internal use only (used in generated code). -@docs Ending, RoutePattern, Segment, view, toVariant +@docs Ending, RoutePattern, Segment, view, toVariant, routeToBranch -} import Elm import Elm.Annotation exposing (Annotation) +import Elm.Case import Html exposing (Html) @@ -123,6 +124,15 @@ toRouteParamTypes pattern = ) +routeToBranch : RoutePattern -> Elm.Case.Branch +routeToBranch route = + Elm.Case.branchList 0 + (\_ -> + Elm.val "Index" + |> Elm.just + ) + + {-| -} toVariant : RoutePattern -> Elm.Variant toVariant pattern = diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index a7046c25..ef20e15c 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -2,6 +2,7 @@ module Pages.RouteParamsTest exposing (..) import Elm import Elm.Annotation +import Elm.Case import Elm.ToString import Expect exposing (Expectation) import Pages.Internal.RoutePattern as RoutePattern @@ -132,9 +133,38 @@ suite = ] ) ] + , describe "toCase" + [ test "root route" <| + \() -> + [] + |> testCaseGenerator + (Elm.Case.branchList 0 + (\_ -> + Elm.val "Index" + |> Elm.just + ) + ) + ] ] +testCaseGenerator : Elm.Case.Branch -> List String -> Expectation +testCaseGenerator expected moduleName = + RoutePattern.fromModuleName moduleName + |> Maybe.map (RoutePattern.routeToBranch >> toStringCase) + |> Maybe.withDefault "" + |> Expect.equal (expected |> toStringCase) + + +toStringCase : Elm.Case.Branch -> String +toStringCase branch = + Elm.Case.custom (Elm.val "segments") + (Elm.Annotation.list Elm.Annotation.string) + [ branch ] + |> Elm.ToString.expression + |> .body + + expectRouteDefinition : Elm.Variant -> List String -> Expectation expectRouteDefinition expected moduleName = RoutePattern.fromModuleName moduleName From 9e9cf969e99b4cda81bbcb0a78b2cd6479a3c75e Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 16:50:08 -0700 Subject: [PATCH 15/69] Use elm-syntax-dsl for case expression. --- codegen/Generate.elm | 15 +++++++---- codegen/elm.codegen.json | 1 - codegen/elm.json | 10 +++++--- elm.json | 2 ++ src/Pages/Internal/RoutePattern.elm | 19 +++++++++----- tests/Pages/RouteParamsTest.elm | 40 +++++++++++++++++++---------- 6 files changed, 58 insertions(+), 29 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 6ddcd8f6..b7c02d32 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -5,7 +5,9 @@ port module Generate exposing (main) import Elm exposing (File) import Elm.Annotation import Elm.Case +import Elm.CodeGen import Elm.Op +import Elm.Pretty import Gen.Basics import Gen.CodeGen.Generate exposing (Error) import Gen.List @@ -13,6 +15,7 @@ import Gen.Path import Gen.Server.Response import Gen.String import Pages.Internal.RoutePattern as RoutePattern exposing (RoutePattern) +import Pretty type alias Flags = @@ -54,11 +57,13 @@ file templates = , Elm.Annotation.list Elm.Annotation.string |> Just ) (\segments -> - Elm.Case.custom segments - (Elm.Annotation.list Elm.Annotation.string) - ((routes |> List.map routeToBranch) - ++ [ Elm.Case.otherwise (\_ -> Elm.nothing) ] - ) + (routes + |> List.map RoutePattern.routeToBranch + |> Elm.CodeGen.caseExpr (Elm.CodeGen.val "segments") + ) + |> Elm.Pretty.prettyExpression + |> Pretty.pretty 120 + |> Elm.val |> Elm.withType (Elm.Annotation.named [] "Route" |> Elm.Annotation.maybe diff --git a/codegen/elm.codegen.json b/codegen/elm.codegen.json index 19364cd6..97564ff8 100644 --- a/codegen/elm.codegen.json +++ b/codegen/elm.codegen.json @@ -6,7 +6,6 @@ "elm/html": "1.0.0" }, "local": [ - "codegen/helpers/", "src/" ] } diff --git a/codegen/elm.json b/codegen/elm.json index 376f2c3e..9126cbde 100644 --- a/codegen/elm.json +++ b/codegen/elm.json @@ -1,4 +1,3 @@ - { "type": "application", "source-directories": [ @@ -12,20 +11,23 @@ "elm/core": "1.0.5", "elm/html": "1.0.0", "elm/json": "1.1.3", - "mdgriffith/elm-codegen": "2.0.0" + "mdgriffith/elm-codegen": "2.0.0", + "the-sett/elm-pretty-printer": "3.0.0", + "the-sett/elm-syntax-dsl": "6.0.2" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "elm/parser": "1.1.0", "elm/time": "1.0.0", "elm/url": "1.0.0", "elm/virtual-dom": "1.0.2", "elm-community/basics-extra": "4.1.0", "elm-community/list-extra": "8.6.0", + "elm-community/maybe-extra": "5.3.0", "miniBill/elm-unicode": "1.0.2", "rtfeldman/elm-hex": "1.0.0", "stil4m/elm-syntax": "7.2.9", - "stil4m/structured-writer": "1.0.3", - "the-sett/elm-pretty-printer": "3.0.0" + "stil4m/structured-writer": "1.0.3" } }, "test-dependencies": { diff --git a/elm.json b/elm.json index 9766e056..45569e9f 100644 --- a/elm.json +++ b/elm.json @@ -65,6 +65,8 @@ "robinheghan/fnv1a": "1.0.0 <= v < 2.0.0", "rtfeldman/elm-css": "17.1.1 <= v < 18.0.0", "stil4m/elm-syntax": "7.2.7 <= v < 8.0.0", + "the-sett/elm-pretty-printer": "3.0.0 <= v < 4.0.0", + "the-sett/elm-syntax-dsl": "6.0.2 <= v < 7.0.0", "turboMaCk/non-empty-list-alias": "1.2.0 <= v < 2.0.0", "vito/elm-ansi": "10.0.1 <= v < 11.0.0" }, diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index b07496bc..ae17e9fc 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -12,6 +12,7 @@ module Pages.Internal.RoutePattern exposing import Elm import Elm.Annotation exposing (Annotation) import Elm.Case +import Elm.CodeGen import Html exposing (Html) @@ -124,13 +125,19 @@ toRouteParamTypes pattern = ) -routeToBranch : RoutePattern -> Elm.Case.Branch +routeToBranch : RoutePattern -> ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) routeToBranch route = - Elm.Case.branchList 0 - (\_ -> - Elm.val "Index" - |> Elm.just - ) + case route.segments of + [] -> + ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "TODO" ) + + _ -> + ( Elm.CodeGen.listPattern + [ Elm.CodeGen.stringPattern "user" + , Elm.CodeGen.varPattern "id" + ] + , Elm.CodeGen.val "TODO" + ) {-| -} diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index ef20e15c..9ea775d8 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -3,9 +3,13 @@ module Pages.RouteParamsTest exposing (..) import Elm import Elm.Annotation import Elm.Case +import Elm.CodeGen +import Elm.Pretty import Elm.ToString import Expect exposing (Expectation) import Pages.Internal.RoutePattern as RoutePattern +import Pretty +import Pretty.Renderer import Test exposing (Test, describe, test) @@ -138,31 +142,41 @@ suite = \() -> [] |> testCaseGenerator - (Elm.Case.branchList 0 - (\_ -> - Elm.val "Index" - |> Elm.just - ) + ( Elm.CodeGen.listPattern [] + , Elm.CodeGen.val "TODO" + ) + , test "dynamic segment" <| + \() -> + [ "User", "Id_" ] + |> testCaseGenerator + ( Elm.CodeGen.listPattern + [ Elm.CodeGen.stringPattern "user" + , Elm.CodeGen.varPattern "id" + ] + , Elm.CodeGen.val "TODO" ) ] ] -testCaseGenerator : Elm.Case.Branch -> List String -> Expectation +testCaseGenerator : ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) -> List String -> Expectation testCaseGenerator expected moduleName = RoutePattern.fromModuleName moduleName |> Maybe.map (RoutePattern.routeToBranch >> toStringCase) - |> Maybe.withDefault "" + |> Maybe.withDefault ( "", "" ) |> Expect.equal (expected |> toStringCase) -toStringCase : Elm.Case.Branch -> String +toStringCase : ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) -> ( String, String ) toStringCase branch = - Elm.Case.custom (Elm.val "segments") - (Elm.Annotation.list Elm.Annotation.string) - [ branch ] - |> Elm.ToString.expression - |> .body + branch + |> Tuple.mapBoth + (Elm.Pretty.prettyPattern + >> Pretty.pretty 120 + ) + (Elm.Pretty.prettyExpression + >> Pretty.pretty 120 + ) expectRouteDefinition : Elm.Variant -> List String -> Expectation From b93c98ba231212993330458fb677e40c707a8e18 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 17:00:58 -0700 Subject: [PATCH 16/69] Remove hardcoded name. --- src/Pages/Internal/RoutePattern.elm | 2 +- tests/Pages/RouteParamsTest.elm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index ae17e9fc..ced88a6d 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -129,7 +129,7 @@ routeToBranch : RoutePattern -> ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) routeToBranch route = case route.segments of [] -> - ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "TODO" ) + ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) _ -> ( Elm.CodeGen.listPattern diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 9ea775d8..8a022b1d 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -143,7 +143,7 @@ suite = [] |> testCaseGenerator ( Elm.CodeGen.listPattern [] - , Elm.CodeGen.val "TODO" + , Elm.CodeGen.val "Index" ) , test "dynamic segment" <| \() -> From 378a8d7b41da9d2638f4496a554ed8e111fccf7e Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 17:02:19 -0700 Subject: [PATCH 17/69] Remove some hardcoding. --- src/Pages/Internal/RoutePattern.elm | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index ced88a6d..941e16dd 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -133,9 +133,17 @@ routeToBranch route = _ -> ( Elm.CodeGen.listPattern - [ Elm.CodeGen.stringPattern "user" - , Elm.CodeGen.varPattern "id" - ] + (route.segments + |> List.map + (\segment -> + case segment of + StaticSegment name -> + Elm.CodeGen.stringPattern "user" + + DynamicSegment string -> + Elm.CodeGen.varPattern "id" + ) + ) , Elm.CodeGen.val "TODO" ) From b75e285247144864221108d658ce8cad142c5518 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 17:02:54 -0700 Subject: [PATCH 18/69] Remove some hardcoding. --- src/Pages/Internal/RoutePattern.elm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 941e16dd..67b4475b 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -138,10 +138,10 @@ routeToBranch route = (\segment -> case segment of StaticSegment name -> - Elm.CodeGen.stringPattern "user" + Elm.CodeGen.stringPattern (decapitalize name) - DynamicSegment string -> - Elm.CodeGen.varPattern "id" + DynamicSegment name -> + Elm.CodeGen.varPattern (decapitalize name) ) ) , Elm.CodeGen.val "TODO" From 693094e6bfbd3b853103f67c7729ca89ce99eda7 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 21:05:01 -0700 Subject: [PATCH 19/69] Generate records and case expression with no ending. --- src/Pages/Internal/RoutePattern.elm | 81 ++++++++++++++++++++++++----- tests/Pages/RouteParamsTest.elm | 2 +- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 67b4475b..9a925047 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -131,21 +131,76 @@ routeToBranch route = [] -> ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) - _ -> - ( Elm.CodeGen.listPattern - (route.segments - |> List.map - (\segment -> - case segment of - StaticSegment name -> - Elm.CodeGen.stringPattern (decapitalize name) + segments -> + case route.ending of + Just ending -> + ( Elm.CodeGen.listPattern [] + , Elm.CodeGen.val + "TODO unhandled" + ) - DynamicSegment name -> - Elm.CodeGen.varPattern (decapitalize name) + Nothing -> + let + something : List ( String, Maybe ( String, Elm.CodeGen.Expression ) ) + something = + (segments + |> List.map + (\segment -> + case segment of + DynamicSegment name -> + ( name ++ "_" + , ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) + |> Just + ) + + StaticSegment name -> + ( name, Nothing ) + ) + ) + |> List.map identity + + fieldThings : List ( String, Elm.CodeGen.Expression ) + fieldThings = + something + |> List.filterMap Tuple.second + + innerType : Maybe Elm.CodeGen.Expression + innerType = + case fieldThings of + [] -> + Nothing + + nonEmpty -> + nonEmpty |> Elm.CodeGen.record |> Just + in + ( Elm.CodeGen.listPattern + (route.segments + |> List.map + (\segment -> + case segment of + StaticSegment name -> + Elm.CodeGen.stringPattern (decapitalize name) + + DynamicSegment name -> + Elm.CodeGen.varPattern (decapitalize name) + ) ) - ) - , Elm.CodeGen.val "TODO" - ) + , case innerType of + Just innerRecord -> + Elm.CodeGen.apply + [ something + |> List.map Tuple.first + |> String.join "__" + |> Elm.CodeGen.val + , innerRecord + ] + + Nothing -> + something + |> List.map Tuple.first + |> String.join "__" + |> Elm.CodeGen.val + ) {-| -} diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 8a022b1d..6962e193 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -153,7 +153,7 @@ suite = [ Elm.CodeGen.stringPattern "user" , Elm.CodeGen.varPattern "id" ] - , Elm.CodeGen.val "TODO" + , Elm.CodeGen.val "User__Id_ { id = id }" ) ] ] From aba66fef3bd9e775f4f184c6881a7c688b52cc6a Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 21:24:14 -0700 Subject: [PATCH 20/69] Handle optional ending. --- src/Pages/Internal/RoutePattern.elm | 100 +++++++++++++++++++++++++++- tests/Pages/RouteParamsTest.elm | 10 +++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 9a925047..45dceb17 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -132,11 +132,83 @@ routeToBranch route = ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) segments -> + let + somethingNew = + (route.segments + |> List.map + (\segment -> + case segment of + DynamicSegment name -> + ( name ++ "_" + , ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) + |> Just + ) + + StaticSegment name -> + ( name, Nothing ) + ) + ) + ++ ([ Maybe.map endingToVariantNameFields route.ending + ] + |> List.filterMap identity + ) + in case route.ending of Just ending -> - ( Elm.CodeGen.listPattern [] - , Elm.CodeGen.val - "TODO unhandled" + let + fieldThings : List ( String, Elm.CodeGen.Expression ) + fieldThings = + somethingNew + |> List.filterMap Tuple.second + + innerType : Maybe Elm.CodeGen.Expression + innerType = + case fieldThings of + [] -> + Nothing + + nonEmpty -> + nonEmpty |> Elm.CodeGen.record |> Just + in + ( Elm.CodeGen.listPattern + ((route.segments + |> List.map + (\segment -> + case segment of + StaticSegment name -> + Elm.CodeGen.stringPattern (decapitalize name) + + DynamicSegment name -> + Elm.CodeGen.varPattern (decapitalize name) + ) + ) + ++ [ case ending of + Optional name -> + Elm.CodeGen.varPattern (decapitalize name) + + RequiredSplat -> + -- TODO splatRest + Elm.CodeGen.varPattern "splatFirst" + + OptionalSplat -> + Elm.CodeGen.varPattern "splat" + ] + ) + , case innerType of + Just innerRecord -> + Elm.CodeGen.apply + [ somethingNew + |> List.map Tuple.first + |> String.join "__" + |> Elm.CodeGen.val + , innerRecord + ] + + Nothing -> + somethingNew + |> List.map Tuple.first + |> String.join "__" + |> Elm.CodeGen.val ) Nothing -> @@ -249,6 +321,28 @@ toVariant pattern = innerType +endingToVariantNameFields : Ending -> ( String, Maybe ( String, Elm.CodeGen.Expression ) ) +endingToVariantNameFields ending = + case ending of + Optional name -> + ( name ++ "__" + , Just ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) + ) + + RequiredSplat -> + ( "SPLAT_" + , Just + ( "splat" + , Elm.CodeGen.val "( requiredSplat, splat )" + ) + ) + + OptionalSplat -> + ( "SPLAT__" + , Just ( "splat", Elm.CodeGen.val "splat" ) + ) + + endingToVariantName : Ending -> ( String, Maybe ( String, Annotation ) ) endingToVariantName ending = case ending of diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 6962e193..3a4d87d9 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -155,6 +155,16 @@ suite = ] , Elm.CodeGen.val "User__Id_ { id = id }" ) + , test "optional ending" <| + \() -> + [ "Docs", "Section__" ] + |> testCaseGenerator + ( Elm.CodeGen.listPattern + [ Elm.CodeGen.stringPattern "docs" + , Elm.CodeGen.varPattern "section" + ] + , Elm.CodeGen.val "Docs__Section__ { section = section }" + ) ] ] From 00a59d67566697ad3ac34126430ad7f7884af054 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 21:37:01 -0700 Subject: [PATCH 21/69] Extract function. --- src/Pages/Internal/RoutePattern.elm | 51 ++++++++++++----------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 45dceb17..9f69e30f 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -194,21 +194,7 @@ routeToBranch route = Elm.CodeGen.varPattern "splat" ] ) - , case innerType of - Just innerRecord -> - Elm.CodeGen.apply - [ somethingNew - |> List.map Tuple.first - |> String.join "__" - |> Elm.CodeGen.val - , innerRecord - ] - - Nothing -> - somethingNew - |> List.map Tuple.first - |> String.join "__" - |> Elm.CodeGen.val + , toRecordVariant innerType somethingNew ) Nothing -> @@ -257,24 +243,29 @@ routeToBranch route = Elm.CodeGen.varPattern (decapitalize name) ) ) - , case innerType of - Just innerRecord -> - Elm.CodeGen.apply - [ something - |> List.map Tuple.first - |> String.join "__" - |> Elm.CodeGen.val - , innerRecord - ] - - Nothing -> - something - |> List.map Tuple.first - |> String.join "__" - |> Elm.CodeGen.val + , toRecordVariant innerType something ) +toRecordVariant : Maybe Elm.CodeGen.Expression -> List ( String, b ) -> Elm.CodeGen.Expression +toRecordVariant innerType something = + case innerType of + Just innerRecord -> + Elm.CodeGen.apply + [ something + |> List.map Tuple.first + |> String.join "__" + |> Elm.CodeGen.val + , innerRecord + ] + + Nothing -> + something + |> List.map Tuple.first + |> String.join "__" + |> Elm.CodeGen.val + + {-| -} toVariant : RoutePattern -> Elm.Variant toVariant pattern = From 0e942f0a5d7f067443f7d4916a1a3ebcf3b09ed3 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 21:38:24 -0700 Subject: [PATCH 22/69] Add annotation. --- src/Pages/Internal/RoutePattern.elm | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 9f69e30f..85750ef5 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -133,6 +133,7 @@ routeToBranch route = segments -> let + somethingNew : List ( String, Maybe ( String, Elm.CodeGen.Expression ) ) somethingNew = (route.segments |> List.map From 27d13c5a1544e565670705fe112eb67e92cee9f4 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 12 Sep 2022 21:45:34 -0700 Subject: [PATCH 23/69] Prepare for generating 2 branches for optional params. --- codegen/Generate.elm | 2 +- src/Pages/Internal/RoutePattern.elm | 76 +++++++++++++++-------------- tests/Pages/RouteParamsTest.elm | 52 ++++++++++++-------- 3 files changed, 73 insertions(+), 57 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index b7c02d32..72b8eb99 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -58,7 +58,7 @@ file templates = ) (\segments -> (routes - |> List.map RoutePattern.routeToBranch + |> List.concatMap RoutePattern.routeToBranch |> Elm.CodeGen.caseExpr (Elm.CodeGen.val "segments") ) |> Elm.Pretty.prettyExpression diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 85750ef5..e47a1ea1 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -125,11 +125,11 @@ toRouteParamTypes pattern = ) -routeToBranch : RoutePattern -> ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) +routeToBranch : RoutePattern -> List ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) routeToBranch route = case route.segments of [] -> - ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) + [ ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) ] segments -> let @@ -171,32 +171,33 @@ routeToBranch route = nonEmpty -> nonEmpty |> Elm.CodeGen.record |> Just in - ( Elm.CodeGen.listPattern - ((route.segments - |> List.map - (\segment -> - case segment of - StaticSegment name -> - Elm.CodeGen.stringPattern (decapitalize name) + [ ( Elm.CodeGen.listPattern + ((route.segments + |> List.map + (\segment -> + case segment of + StaticSegment name -> + Elm.CodeGen.stringPattern (decapitalize name) - DynamicSegment name -> + DynamicSegment name -> + Elm.CodeGen.varPattern (decapitalize name) + ) + ) + ++ [ case ending of + Optional name -> Elm.CodeGen.varPattern (decapitalize name) - ) - ) - ++ [ case ending of - Optional name -> - Elm.CodeGen.varPattern (decapitalize name) - RequiredSplat -> - -- TODO splatRest - Elm.CodeGen.varPattern "splatFirst" + RequiredSplat -> + -- TODO splatRest + Elm.CodeGen.varPattern "splatFirst" - OptionalSplat -> - Elm.CodeGen.varPattern "splat" - ] - ) - , toRecordVariant innerType somethingNew - ) + OptionalSplat -> + Elm.CodeGen.varPattern "splat" + ] + ) + , toRecordVariant innerType somethingNew + ) + ] Nothing -> let @@ -232,20 +233,21 @@ routeToBranch route = nonEmpty -> nonEmpty |> Elm.CodeGen.record |> Just in - ( Elm.CodeGen.listPattern - (route.segments - |> List.map - (\segment -> - case segment of - StaticSegment name -> - Elm.CodeGen.stringPattern (decapitalize name) + [ ( Elm.CodeGen.listPattern + (route.segments + |> List.map + (\segment -> + case segment of + StaticSegment name -> + Elm.CodeGen.stringPattern (decapitalize name) - DynamicSegment name -> - Elm.CodeGen.varPattern (decapitalize name) - ) - ) - , toRecordVariant innerType something - ) + DynamicSegment name -> + Elm.CodeGen.varPattern (decapitalize name) + ) + ) + , toRecordVariant innerType something + ) + ] toRecordVariant : Maybe Elm.CodeGen.Expression -> List ( String, b ) -> Elm.CodeGen.Expression diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 3a4d87d9..2c8635bd 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -142,39 +142,53 @@ suite = \() -> [] |> testCaseGenerator - ( Elm.CodeGen.listPattern [] - , Elm.CodeGen.val "Index" - ) + [ ( Elm.CodeGen.listPattern [] + , Elm.CodeGen.val "Index" + ) + ] , test "dynamic segment" <| \() -> [ "User", "Id_" ] |> testCaseGenerator - ( Elm.CodeGen.listPattern - [ Elm.CodeGen.stringPattern "user" - , Elm.CodeGen.varPattern "id" - ] - , Elm.CodeGen.val "User__Id_ { id = id }" - ) + [ ( Elm.CodeGen.listPattern + [ Elm.CodeGen.stringPattern "user" + , Elm.CodeGen.varPattern "id" + ] + , Elm.CodeGen.val "User__Id_ { id = id }" + ) + ] , test "optional ending" <| \() -> [ "Docs", "Section__" ] |> testCaseGenerator - ( Elm.CodeGen.listPattern - [ Elm.CodeGen.stringPattern "docs" - , Elm.CodeGen.varPattern "section" - ] - , Elm.CodeGen.val "Docs__Section__ { section = section }" - ) + [ ( Elm.CodeGen.listPattern + [ Elm.CodeGen.stringPattern "docs" + , Elm.CodeGen.varPattern "section" + ] + , Elm.CodeGen.val "Docs__Section__ { section = section }" + ) + ] + + --, test "splat" <| + -- \() -> + -- [ "Docs", "Section__" ] + -- |> testCaseGenerator + -- ( Elm.CodeGen.listPattern + -- [ Elm.CodeGen.stringPattern "docs" + -- , Elm.CodeGen.varPattern "section" + -- ] + -- , Elm.CodeGen.val "Docs__Section__ { section = section }" + -- ) ] ] -testCaseGenerator : ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) -> List String -> Expectation +testCaseGenerator : List ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) -> List String -> Expectation testCaseGenerator expected moduleName = RoutePattern.fromModuleName moduleName - |> Maybe.map (RoutePattern.routeToBranch >> toStringCase) - |> Maybe.withDefault ( "", "" ) - |> Expect.equal (expected |> toStringCase) + |> Maybe.map (RoutePattern.routeToBranch >> List.map toStringCase) + |> Maybe.withDefault [ ( "", "" ) ] + |> Expect.equal (expected |> List.map toStringCase) toStringCase : ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) -> ( String, String ) From 392389ebbc079e902440cd75c18c2ad32e043b67 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 06:35:42 -0700 Subject: [PATCH 24/69] Include Just in front of route helper. --- codegen/Generate.elm | 1 + 1 file changed, 1 insertion(+) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 72b8eb99..19c26c7f 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -59,6 +59,7 @@ file templates = (\segments -> (routes |> List.concatMap RoutePattern.routeToBranch + |> List.map (Tuple.mapSecond (\constructRoute -> Elm.CodeGen.apply [ Elm.CodeGen.val "Just", constructRoute ])) |> Elm.CodeGen.caseExpr (Elm.CodeGen.val "segments") ) |> Elm.Pretty.prettyExpression From 27e58f10277760e0b09a0805accca58ec8ac7db3 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 06:37:51 -0700 Subject: [PATCH 25/69] Handle optional case with correctly generating record argument. --- src/Pages/Internal/RoutePattern.elm | 80 ++++++++++++++++++++++------- tests/Pages/RouteParamsTest.elm | 7 ++- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index e47a1ea1..9b62d4df 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -127,6 +127,23 @@ toRouteParamTypes pattern = routeToBranch : RoutePattern -> List ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) routeToBranch route = + let + something : List ( String, Maybe ( String, Elm.CodeGen.Expression ) ) + something = + route.segments + |> List.map + (\segment -> + case segment of + DynamicSegment name -> + ( name ++ "_" + , ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) + |> Just + ) + + StaticSegment name -> + ( name, Nothing ) + ) + in case route.segments of [] -> [ ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) ] @@ -198,27 +215,52 @@ routeToBranch route = , toRecordVariant innerType somethingNew ) ] + ++ (case ending of + Optional optionalName -> + [ ( Elm.CodeGen.listPattern + (route.segments + |> List.map + (\segment -> + case segment of + StaticSegment name -> + Elm.CodeGen.stringPattern (decapitalize name) + + DynamicSegment name -> + Elm.CodeGen.varPattern (decapitalize name) + ) + ) + , toRecordVariant + ((something + ++ [ ( optionalName ++ "__" + , ( "section" + , Elm.CodeGen.val "Nothing" + ) + |> Just + ) + ] + ) + |> List.filterMap Tuple.second + |> Elm.CodeGen.record + |> Just + ) + (something + ++ [ ( optionalName ++ "__" + , ( "section" + , Elm.CodeGen.val "Nothing" + ) + |> Just + ) + ] + ) + ) + ] + + _ -> + [] + ) Nothing -> let - something : List ( String, Maybe ( String, Elm.CodeGen.Expression ) ) - something = - (segments - |> List.map - (\segment -> - case segment of - DynamicSegment name -> - ( name ++ "_" - , ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) - |> Just - ) - - StaticSegment name -> - ( name, Nothing ) - ) - ) - |> List.map identity - fieldThings : List ( String, Elm.CodeGen.Expression ) fieldThings = something @@ -320,7 +362,7 @@ endingToVariantNameFields ending = case ending of Optional name -> ( name ++ "__" - , Just ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) + , Just ( decapitalize name, [ Elm.CodeGen.val "Just", Elm.CodeGen.val (decapitalize name) ] |> Elm.CodeGen.apply ) ) RequiredSplat -> diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 2c8635bd..868340f1 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -165,7 +165,12 @@ suite = [ Elm.CodeGen.stringPattern "docs" , Elm.CodeGen.varPattern "section" ] - , Elm.CodeGen.val "Docs__Section__ { section = section }" + , Elm.CodeGen.val "Docs__Section__ { section = Just section }" + ) + , ( Elm.CodeGen.listPattern + [ Elm.CodeGen.stringPattern "docs" + ] + , Elm.CodeGen.val "Docs__Section__ { section = Nothing }" ) ] From c81649d93b435388e163976fa791a35fcfbd6538 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 06:51:21 -0700 Subject: [PATCH 26/69] Handle required splats in generated route module. --- src/Pages/Internal/RoutePattern.elm | 42 +++++++++++++++++++++------ tests/Pages/RouteParamsTest.elm | 44 +++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 9b62d4df..ba08cf6c 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -188,7 +188,13 @@ routeToBranch route = nonEmpty -> nonEmpty |> Elm.CodeGen.record |> Just in - [ ( Elm.CodeGen.listPattern + [ ( (case ending of + Optional _ -> + Elm.CodeGen.listPattern + + _ -> + unconsPattern + ) ((route.segments |> List.map (\segment -> @@ -200,17 +206,18 @@ routeToBranch route = Elm.CodeGen.varPattern (decapitalize name) ) ) - ++ [ case ending of + ++ (case ending of Optional name -> - Elm.CodeGen.varPattern (decapitalize name) + [ Elm.CodeGen.varPattern (decapitalize name) ] RequiredSplat -> - -- TODO splatRest - Elm.CodeGen.varPattern "splatFirst" + [ Elm.CodeGen.varPattern "splatFirst" + , Elm.CodeGen.varPattern "splatRest" + ] OptionalSplat -> - Elm.CodeGen.varPattern "splat" - ] + [ Elm.CodeGen.varPattern "splat" ] + ) ) , toRecordVariant innerType somethingNew ) @@ -369,7 +376,10 @@ endingToVariantNameFields ending = ( "SPLAT_" , Just ( "splat" - , Elm.CodeGen.val "( requiredSplat, splat )" + , Elm.CodeGen.tuple + [ Elm.CodeGen.val "splatFirst" + , Elm.CodeGen.val "splatRest" + ] ) ) @@ -524,3 +534,19 @@ type Param | OptionalParam | RequiredSplatParam | OptionalSplatParam + + +unconsPattern : List Elm.CodeGen.Pattern -> Elm.CodeGen.Pattern +unconsPattern list = + case list of + [] -> + Debug.todo "" + + listFirst :: listRest -> + List.foldl + (\soFar item -> + soFar + |> Elm.CodeGen.unConsPattern item + ) + listFirst + listRest diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 868340f1..af2fb66f 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -173,21 +173,43 @@ suite = , Elm.CodeGen.val "Docs__Section__ { section = Nothing }" ) ] - - --, test "splat" <| - -- \() -> - -- [ "Docs", "Section__" ] - -- |> testCaseGenerator - -- ( Elm.CodeGen.listPattern - -- [ Elm.CodeGen.stringPattern "docs" - -- , Elm.CodeGen.varPattern "section" - -- ] - -- , Elm.CodeGen.val "Docs__Section__ { section = section }" - -- ) + , test "required splat" <| + \() -> + [ "Username_", "Repo_", "Blob", "SPLAT_" ] + |> testCaseGenerator + [ ( --Elm. """username :: repo :: "blob" :: splatFirst :: splatRest""" + --( Elm.CodeGen.unConsPattern + unconsPattern + [ Elm.CodeGen.varPattern "username" + , Elm.CodeGen.varPattern "repo" + , Elm.CodeGen.stringPattern "blob" + , Elm.CodeGen.varPattern "splatFirst" + , Elm.CodeGen.varPattern "splatRest" + ] + , Elm.CodeGen.val + "Username___Repo___Blob__SPLAT_ { username = username, repo = repo, splat = ( splatFirst, splatRest ) }" + ) + ] ] ] +unconsPattern : List Elm.CodeGen.Pattern -> Elm.CodeGen.Pattern +unconsPattern list = + case list of + [] -> + Debug.todo "" + + listFirst :: listRest -> + List.foldl + (\soFar item -> + soFar + |> Elm.CodeGen.unConsPattern item + ) + listFirst + listRest + + testCaseGenerator : List ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) -> List String -> Expectation testCaseGenerator expected moduleName = RoutePattern.fromModuleName moduleName From 56558ca126c6c6e789916ddf8d98bf2ad997b597 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 07:11:41 -0700 Subject: [PATCH 27/69] Use segmentsToRoute in generated module. --- codegen/Generate.elm | 82 ++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 19c26c7f..e29461c8 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -4,8 +4,8 @@ port module Generate exposing (main) import Elm exposing (File) import Elm.Annotation -import Elm.Case import Elm.CodeGen +import Elm.Declare import Elm.Op import Elm.Pretty import Gen.Basics @@ -38,6 +38,43 @@ main = } +splitPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } +splitPath = + Elm.Declare.fn "splitPath" + ( "path", Just Gen.Path.annotation_.path ) + (\path -> + Gen.List.call_.filter + (Elm.fn ( "item", Just Elm.Annotation.string ) + (\item -> Elm.Op.notEqual item (Elm.string "")) + ) + (Gen.String.call_.split (Elm.string "/") path) + ) + + +segmentsToRoute : + List RoutePattern + -> { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } +segmentsToRoute routes = + Elm.Declare.fn "segmentsToRoute" + ( "segments" + , Elm.Annotation.list Elm.Annotation.string |> Just + ) + (\segments -> + (routes + |> List.concatMap RoutePattern.routeToBranch + |> List.map (Tuple.mapSecond (\constructRoute -> Elm.CodeGen.apply [ Elm.CodeGen.val "Just", constructRoute ])) + |> Elm.CodeGen.caseExpr (Elm.CodeGen.val "segments") + ) + |> Elm.Pretty.prettyExpression + |> Pretty.pretty 120 + |> Elm.val + |> Elm.withType + (Elm.Annotation.named [] "Route" + |> Elm.Annotation.maybe + ) + ) + + file : List (List String) -> Elm.File file templates = let @@ -45,33 +82,18 @@ file templates = routes = templates |> List.filterMap RoutePattern.fromModuleName + + segmentsToRouteFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } + segmentsToRouteFn = + segmentsToRoute routes in Elm.file [ "Route" ] [ Elm.customType "Route" (routes |> List.map RoutePattern.toVariant) |> expose - , Elm.declaration "segmentsToRoute" - (Elm.fn - ( "segments" - , Elm.Annotation.list Elm.Annotation.string |> Just - ) - (\segments -> - (routes - |> List.concatMap RoutePattern.routeToBranch - |> List.map (Tuple.mapSecond (\constructRoute -> Elm.CodeGen.apply [ Elm.CodeGen.val "Just", constructRoute ])) - |> Elm.CodeGen.caseExpr (Elm.CodeGen.val "segments") - ) - |> Elm.Pretty.prettyExpression - |> Pretty.pretty 120 - |> Elm.val - |> Elm.withType - (Elm.Annotation.named [] "Route" - |> Elm.Annotation.maybe - ) - ) - ) - |> expose + , segmentsToRouteFn.declaration |> expose + , splitPath.declaration , Elm.declaration "urlToRoute" (Elm.fn ( "url" @@ -79,8 +101,11 @@ file templates = |> Just ) (\url -> - url - |> Elm.get "path" + segmentsToRouteFn.call + (splitPath.call + (url |> Elm.get "path") + ) + |> Elm.withType (Elm.Annotation.maybe (Elm.Annotation.named [] "Route")) ) ) |> expose @@ -134,15 +159,6 @@ file templates = ] -routeToBranch : RoutePattern -> Elm.Case.Branch -routeToBranch route = - Elm.Case.branchList 0 - (\_ -> - Elm.val "Index" - |> Elm.just - ) - - expose : Elm.Declaration -> Elm.Declaration expose declaration = declaration From 24622df871af8af47052794320a8d9249d132fa4 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 08:17:23 -0700 Subject: [PATCH 28/69] Generate routeToPath. --- codegen/Generate.elm | 88 +++++++++++++++++++++++++++++ src/Pages/Internal/RoutePattern.elm | 70 ++++++++++++++++++++++- 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index e29461c8..c178c73f 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -4,6 +4,7 @@ port module Generate exposing (main) import Elm exposing (File) import Elm.Annotation +import Elm.Case import Elm.CodeGen import Elm.Declare import Elm.Op @@ -51,6 +52,19 @@ splitPath = ) +maybeToList : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } +maybeToList = + Elm.Declare.fn "maybeToList" + ( "maybeString", Just (Elm.Annotation.maybe Elm.Annotation.string) ) + (\maybeString -> + Elm.Case.maybe maybeString + { nothing = Elm.list [] + , just = ( "string", \string -> Elm.list [ string ] ) + } + |> Elm.withType (Elm.Annotation.list Elm.Annotation.string) + ) + + segmentsToRoute : List RoutePattern -> { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } @@ -75,6 +89,78 @@ segmentsToRoute routes = ) +routeToPath : List RoutePattern -> { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } +routeToPath routes = + Elm.Declare.fn "routeToPath" + ( "route", Just (Elm.Annotation.named [] "Route") ) + (\route_ -> + Elm.Case.custom route_ + (Elm.Annotation.list Elm.Annotation.string) + (routes + |> List.map + (\route -> + case + RoutePattern.toVariantName route + |> .params + |> List.filter + (\param -> + case param of + RoutePattern.StaticParam _ -> + False + + _ -> + True + ) + of + [] -> + Elm.Case.branch0 (RoutePattern.toVariantName route |> .variantName) + (RoutePattern.toVariantName route + |> .params + |> List.map + (\param -> + case param of + RoutePattern.StaticParam name -> + [ Elm.string name ] + |> Elm.list + + RoutePattern.DynamicParam name -> + Elm.list [] + + RoutePattern.OptionalParam2 name -> + Elm.list [] + ) + |> Elm.list + ) + + nonEmptyDynamicParams -> + Elm.Case.branch1 (RoutePattern.toVariantName route |> .variantName) + ( "params", Elm.Annotation.record [] ) + (\params -> + RoutePattern.toVariantName route + |> .params + |> List.map + (\param -> + case param of + RoutePattern.StaticParam name -> + [ Elm.string name ] + |> Elm.list + + RoutePattern.DynamicParam name -> + [ Elm.get name params ] + |> Elm.list + + RoutePattern.OptionalParam2 name -> + maybeToList.call (Elm.get name params) + ) + |> Elm.list + ) + ) + ) + |> Gen.List.call_.concat + |> Elm.withType (Elm.Annotation.list Elm.Annotation.string) + ) + + file : List (List String) -> Elm.File file templates = let @@ -111,6 +197,8 @@ file templates = |> expose , Elm.declaration "baseUrl" (Elm.string "/") |> expose + , maybeToList.declaration + , routeToPath routes |> .declaration |> expose , Elm.declaration "baseUrlAsPath" (Gen.List.call_.filter (Elm.fn ( "item", Nothing ) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index ba08cf6c..0698e73d 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -1,6 +1,6 @@ module Pages.Internal.RoutePattern exposing ( Ending(..), RoutePattern, Segment(..), view, toVariant, routeToBranch - , Param(..), fromModuleName, toRouteParamTypes, toRouteParamsRecord + , Param(..), RouteParam(..), fromModuleName, toRouteParamTypes, toRouteParamsRecord, toVariantName ) {-| Exposed for internal use only (used in generated code). @@ -299,6 +299,74 @@ routeToBranch route = ] +type RouteParam + = StaticParam String + | DynamicParam String + | OptionalParam2 String + + +toVariantName : RoutePattern -> { variantName : String, params : List RouteParam } +toVariantName route = + let + something : List ( String, Maybe RouteParam ) + something = + route.segments + |> List.map + (\segment -> + case segment of + DynamicSegment name -> + ( name ++ "_" + , DynamicParam (decapitalize name) + |> Just + ) + + StaticSegment name -> + ( name + , if name == "Index" then + Nothing + + else + Just (StaticParam (decapitalize name)) + ) + ) + + something2 = + something + ++ ([ Maybe.map + (\ending -> + case ending of + Optional name -> + ( name ++ "__" + , Just (OptionalParam2 (decapitalize name)) + ) + + RequiredSplat -> + ( "SPLAT_" + , DynamicParam "TODO" + |> Just + ) + + OptionalSplat -> + ( "SPLAT__" + , DynamicParam "TODO" + |> Just + ) + ) + route.ending + ] + |> List.filterMap identity + ) + in + something2 + |> List.map Tuple.first + |> String.join "__" + |> (\name -> + { variantName = name + , params = something2 |> List.filterMap Tuple.second + } + ) + + toRecordVariant : Maybe Elm.CodeGen.Expression -> List ( String, b ) -> Elm.CodeGen.Expression toRecordVariant innerType something = case innerType of From ef9b664cb398cd89badf9d45b7f33d788fd15c80 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 08:20:29 -0700 Subject: [PATCH 29/69] Fix redirectTo generated definition. --- codegen/Generate.elm | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index c178c73f..a5ae3d17 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -228,25 +228,29 @@ file templates = ) ) |> expose - , Elm.declaration "toString" - (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) - (\route -> - Gen.Path.toAbsolute - (Elm.apply (Elm.val "toPath") [ route ]) - ) - ) + , toString.declaration |> expose , Elm.declaration "redirectTo" (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) (\route -> Gen.Server.Response.call_.temporaryRedirect - route + (toString.call route) ) ) |> expose ] +toString : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } +toString = + Elm.Declare.fn "toString" + ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Path.toAbsolute + (Elm.apply (Elm.val "toPath") [ route ]) + ) + + expose : Elm.Declaration -> Elm.Declaration expose declaration = declaration From 90ae03c27130acc9c2b737fa242a21037e407a3e Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 08:42:12 -0700 Subject: [PATCH 30/69] Improve some type annotations that are getting referenced incorrectly without the help of Elm.withType (see https://github.com/mdgriffith/elm-codegen/issues/48). --- codegen/Generate.elm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index a5ae3d17..cc293c9e 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -225,6 +225,7 @@ file templates = ) ) ) + |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") ) ) |> expose @@ -235,6 +236,13 @@ file templates = (\route -> Gen.Server.Response.call_.temporaryRedirect (toString.call route) + |> Elm.withType + (Elm.Annotation.namedWith [ "Server", "Response" ] + "Response" + [ Elm.Annotation.var "data" + , Elm.Annotation.var "error" + ] + ) ) ) |> expose From f07f5feac99fe9e570dc7d432b88c900601c0fbc Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 08:50:24 -0700 Subject: [PATCH 31/69] Remove debug.todo. --- src/Pages/Internal/RoutePattern.elm | 2 +- tests/Pages/RouteParamsTest.elm | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 0698e73d..81634a2e 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -608,7 +608,7 @@ unconsPattern : List Elm.CodeGen.Pattern -> Elm.CodeGen.Pattern unconsPattern list = case list of [] -> - Debug.todo "" + Elm.CodeGen.listPattern [] listFirst :: listRest -> List.foldl diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index af2fb66f..666f5704 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -2,14 +2,12 @@ module Pages.RouteParamsTest exposing (..) import Elm import Elm.Annotation -import Elm.Case import Elm.CodeGen import Elm.Pretty import Elm.ToString import Expect exposing (Expectation) import Pages.Internal.RoutePattern as RoutePattern import Pretty -import Pretty.Renderer import Test exposing (Test, describe, test) @@ -198,7 +196,7 @@ unconsPattern : List Elm.CodeGen.Pattern -> Elm.CodeGen.Pattern unconsPattern list = case list of [] -> - Debug.todo "" + Elm.CodeGen.listPattern [] listFirst :: listRest -> List.foldl From c85463933f3db9ac13c02723ad19a8510d2d71bb Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 08:50:31 -0700 Subject: [PATCH 32/69] Add missing dependency to example project. --- examples/docs/elm.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/docs/elm.json b/examples/docs/elm.json index cfe8f244..6947dbcd 100644 --- a/examples/docs/elm.json +++ b/examples/docs/elm.json @@ -44,14 +44,18 @@ "robinheghan/fnv1a": "1.0.0", "robinheghan/murmur3": "1.0.0", "rtfeldman/elm-css": "16.1.1", + "the-sett/elm-pretty-printer": "3.0.0", + "the-sett/elm-syntax-dsl": "6.0.2", "turboMaCk/non-empty-list-alias": "1.2.0", "vito/elm-ansi": "10.0.1" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "billstclair/elm-xml-eeue56": "2.0.0", "dmy/elm-imf-date-time": "1.0.1", "elm/file": "1.0.5", "elm-community/basics-extra": "4.1.0", + "elm-community/maybe-extra": "5.3.0", "fredcy/elm-parseint": "2.0.1", "justinmimbs/time-extra": "1.1.0", "lazamar/dict-parser": "1.0.2", @@ -60,8 +64,7 @@ "rtfeldman/elm-iso8601-date-strings": "1.1.4", "ryannhg/date-format": "2.3.0", "stil4m/elm-syntax": "7.2.9", - "stil4m/structured-writer": "1.0.3", - "the-sett/elm-pretty-printer": "3.0.0" + "stil4m/structured-writer": "1.0.3" } }, "test-dependencies": { From b4aceaeb645a417ac4f7d7102987a29e06f05e9c Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 08:50:40 -0700 Subject: [PATCH 33/69] Add missing catchall branch. --- codegen/Generate.elm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index cc293c9e..30a87b0e 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -74,9 +74,13 @@ segmentsToRoute routes = , Elm.Annotation.list Elm.Annotation.string |> Just ) (\segments -> - (routes + (((routes |> List.concatMap RoutePattern.routeToBranch |> List.map (Tuple.mapSecond (\constructRoute -> Elm.CodeGen.apply [ Elm.CodeGen.val "Just", constructRoute ])) + ) + ++ [ ( Elm.CodeGen.allPattern, Elm.CodeGen.val "Nothing" ) + ] + ) |> Elm.CodeGen.caseExpr (Elm.CodeGen.val "segments") ) |> Elm.Pretty.prettyExpression From c93e3d8f85bcbca6c694d60dd42d8836751bc3b4 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 08:55:41 -0700 Subject: [PATCH 34/69] Fix case for index route codegen. --- src/Pages/Internal/RoutePattern.elm | 2 +- tests/Pages/RouteParamsTest.elm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 81634a2e..0f1aac82 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -145,7 +145,7 @@ routeToBranch route = ) in case route.segments of - [] -> + [ StaticSegment "Index" ] -> [ ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) ] segments -> diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 666f5704..4c475665 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -138,7 +138,7 @@ suite = , describe "toCase" [ test "root route" <| \() -> - [] + [ "Index" ] |> testCaseGenerator [ ( Elm.CodeGen.listPattern [] , Elm.CodeGen.val "Index" From 621b7a76c14a53f1e11f5a2d46530787d7d4d648 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 10:31:01 -0700 Subject: [PATCH 35/69] Generate toLink function. --- codegen/Generate.elm | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 30a87b0e..2d306cf6 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -11,6 +11,7 @@ import Elm.Op import Elm.Pretty import Gen.Basics import Gen.CodeGen.Generate exposing (Error) +import Gen.Html.Attributes import Gen.List import Gen.Path import Gen.Server.Response @@ -250,6 +251,21 @@ file templates = ) ) |> expose + , Elm.declaration "toLink" + (Elm.fn2 + ( "toAnchorTag", Nothing ) + ( "route", Just (Elm.Annotation.named [] "Route") ) + (\toAnchorTag route -> + Elm.apply + toAnchorTag + [ Elm.list + [ route |> toString.call |> Gen.Html.Attributes.call_.href + , Gen.Html.Attributes.attribute "elm-pages:prefetch" "" + ] + ] + ) + ) + |> expose ] From 47751f3a2eac5ca4f92c9a46d6f960bf79b3d6f5 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 10:38:16 -0700 Subject: [PATCH 36/69] Generate Route.link helper. --- codegen/Generate.elm | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 2d306cf6..b86444d9 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -11,6 +11,7 @@ import Elm.Op import Elm.Pretty import Gen.Basics import Gen.CodeGen.Generate exposing (Error) +import Gen.Html import Gen.Html.Attributes import Gen.List import Gen.Path @@ -266,9 +267,44 @@ file templates = ) ) |> expose + , Elm.declaration "link" + (Elm.fn3 + ( "attributes", Nothing ) + ( "children", Nothing ) + ( "route", Just (Elm.Annotation.named [] "Route") ) + (\attributes children route -> + toLink.call + (Elm.fn + ( "anchorAttrs", Nothing ) + (\anchorAttrs -> + Gen.Html.call_.a + (Elm.Op.append anchorAttrs attributes) + children + ) + ) + route + ) + ) + |> expose ] +toLink : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression } +toLink = + Elm.Declare.fn2 "toLink" + ( "toAnchorTag", Nothing ) + ( "route", Just (Elm.Annotation.named [] "Route") ) + (\toAnchorTag route -> + Elm.apply + toAnchorTag + [ Elm.list + [ route |> toString.call |> Gen.Html.Attributes.call_.href + , Gen.Html.Attributes.attribute "elm-pages:prefetch" "" + ] + ] + ) + + toString : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } toString = Elm.Declare.fn "toString" From 92365d95b1dcfcff2e71087bedb5ec1806c68921 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 10:47:03 -0700 Subject: [PATCH 37/69] Generate Route.withoutBaseUrl. --- codegen/Generate.elm | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index b86444d9..6463e50b 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -286,6 +286,19 @@ file templates = ) ) |> expose + , Elm.declaration "withoutBaseUrl" + (Elm.fn ( "path", Just Elm.Annotation.string ) + (\path -> + Elm.ifThen + (path |> Gen.String.call_.startsWith (Elm.val "baseUrl")) + (Gen.String.call_.dropLeft + (Gen.String.call_.length (Elm.val "baseUrl")) + path + ) + path + ) + ) + |> expose ] From 08015f49b9cf93f7edbd2cba7b9c266d5467e6ad Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 10:49:51 -0700 Subject: [PATCH 38/69] Wire up baseUrl for Route module generation. --- codegen/Generate.elm | 11 ++++++----- generator/src/generate-template-module-connector.js | 11 ++++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 6463e50b..f55eb7d7 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -23,6 +23,7 @@ import Pretty type alias Flags = { templates : List (List String) + , basePath : String } @@ -30,9 +31,9 @@ main : Program Flags () () main = Platform.worker { init = - \{ templates } -> + \{ templates, basePath } -> ( () - , onSuccessSend [ file templates ] + , onSuccessSend [ file templates basePath ] ) , update = \_ model -> @@ -167,8 +168,8 @@ routeToPath routes = ) -file : List (List String) -> Elm.File -file templates = +file : List (List String) -> String -> Elm.File +file templates basePath = let routes : List RoutePattern.RoutePattern routes = @@ -201,7 +202,7 @@ file templates = ) ) |> expose - , Elm.declaration "baseUrl" (Elm.string "/") + , Elm.declaration "baseUrl" (Elm.string basePath) |> expose , maybeToList.declaration , routeToPath routes |> .declaration |> expose diff --git a/generator/src/generate-template-module-connector.js b/generator/src/generate-template-module-connector.js index 08ab8176..ee32e29d 100644 --- a/generator/src/generate-template-module-connector.js +++ b/generator/src/generate-template-module-connector.js @@ -38,7 +38,10 @@ async function generateTemplateModuleConnector(basePath, phase) { ], }; } - const routesModuleNew = await runElmCodegenCli(sortTemplates(templates)); + const routesModuleNew = await runElmCodegenCli( + sortTemplates(templates), + basePath + ); return { mainModule: `port module Main exposing (..) @@ -1146,7 +1149,7 @@ redirectTo route = }; } -async function runElmCodegenCli(templates) { +async function runElmCodegenCli(templates, basePath) { // await runElmCodegenInstall(); await compileCliApp( // { debug: true }, @@ -1166,7 +1169,9 @@ async function runElmCodegenCli(templates) { "./elm-stuff/elm-pages-codegen.js" )).Elm.Generate; - const app = elmPagesCodegen.init({ flags: { templates: templates } }); + const app = elmPagesCodegen.init({ + flags: { templates: templates, basePath }, + }); if (app.ports.onSuccessSend) { app.ports.onSuccessSend.subscribe(resolve); } From ba517563f75bc923316b43e9ad7990d1af97d492 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 10:54:19 -0700 Subject: [PATCH 39/69] Remove hardcoded value. --- src/Pages/Internal/RoutePattern.elm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 0f1aac82..9de459b7 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -239,7 +239,7 @@ routeToBranch route = , toRecordVariant ((something ++ [ ( optionalName ++ "__" - , ( "section" + , ( decapitalize optionalName , Elm.CodeGen.val "Nothing" ) |> Just @@ -252,7 +252,7 @@ routeToBranch route = ) (something ++ [ ( optionalName ++ "__" - , ( "section" + , ( decapitalize optionalName , Elm.CodeGen.val "Nothing" ) |> Just From 59a7165fa3cd596804d184839d60897546edeccf Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 10:54:49 -0700 Subject: [PATCH 40/69] Add missing dependencies. --- examples/todos/elm.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/todos/elm.json b/examples/todos/elm.json index 60f64737..ba8079a9 100644 --- a/examples/todos/elm.json +++ b/examples/todos/elm.json @@ -44,21 +44,24 @@ "robinheghan/fnv1a": "1.0.0", "rtfeldman/elm-css": "16.1.1", "rtfeldman/elm-iso8601-date-strings": "1.1.4", + "the-sett/elm-pretty-printer": "3.0.0", + "the-sett/elm-syntax-dsl": "6.0.2", "turboMaCk/non-empty-list-alias": "1.2.0", "vito/elm-ansi": "10.0.1" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "elm/file": "1.0.5", "elm/random": "1.0.0", "elm-community/basics-extra": "4.1.0", + "elm-community/maybe-extra": "5.3.0", "fredcy/elm-parseint": "2.0.1", "j-maas/elm-ordered-containers": "1.0.0", "lukewestby/elm-string-interpolate": "1.0.4", "miniBill/elm-unicode": "1.0.2", "rtfeldman/elm-hex": "1.0.0", "stil4m/elm-syntax": "7.2.9", - "stil4m/structured-writer": "1.0.3", - "the-sett/elm-pretty-printer": "3.0.0" + "stil4m/structured-writer": "1.0.3" } }, "test-dependencies": { From d9f0a3ea08e550bd0c5262e11036ab1f51080d80 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 10:58:03 -0700 Subject: [PATCH 41/69] Remove unused code. --- .../src/generate-template-module-connector.js | 139 ------------------ 1 file changed, 139 deletions(-) diff --git a/generator/src/generate-template-module-connector.js b/generator/src/generate-template-module-connector.js index ee32e29d..3656fe35 100644 --- a/generator/src/generate-template-module-connector.js +++ b/generator/src/generate-template-module-connector.js @@ -1004,145 +1004,6 @@ decodeBytes bytesDecoder items = |> Result.fromMaybe "Decoding error" `, routesModuleNew, - routesModule: `module Route exposing (baseUrlAsPath, Route(..), link, matchers, routeToPath, toLink, urlToRoute, toPath, redirectTo, toString) - -{-| - -@docs Route, link, matchers, routeToPath, toLink, urlToRoute, toPath, redirectTo, toString, baseUrlAsPath - --} - - -import Server.Response -import Html exposing (Attribute, Html) -import Html.Attributes as Attr -import Path exposing (Path) -import Pages.Internal.Router -import Pattern - - -{-| -} -type Route - = ${templates.map(routeHelpers.routeVariantDefinition).join("\n | ")} - - -{-| -} -urlToRoute : { url | path : String } -> Maybe Route -urlToRoute url = - url.path - |> withoutBaseUrl - |> Pages.Internal.Router.firstMatch matchers - - -baseUrl : String -baseUrl = - "${basePath}" - - -{-| -} -baseUrlAsPath : List String -baseUrlAsPath = - baseUrl - |> String.split "/" - |> List.filter (not << String.isEmpty) - - -withoutBaseUrl path = - if (path |> String.startsWith baseUrl) then - String.dropLeft (String.length baseUrl) path - else - path - -{-| -} -matchers : List (Pages.Internal.Router.Matcher Route) -matchers = - [ ${sortTemplates(templates) - .map( - (name) => `{ pattern = "^${routeRegex(name).pattern}$" - , toRoute = ${routeRegex(name).toRoute} - }\n` - ) - .join(" , ")} - ] - - -{-| -} -routeToPath : Route -> List String -routeToPath route = - case route of - ${templates - .map( - (name) => - `${routeHelpers.routeVariant(name)}${ - routeHelpers.parseRouteParams(name).length === 0 - ? "" - : ` params` - } ->\n List.concat [ ${routeHelpers - .parseRouteParamsWithStatic(name) - .map((param) => { - switch (param.kind) { - case "static": { - return param.name === "Index" - ? `[]` - : `[ "${camelToKebab(param.name)}" ]`; - } - case "optional": { - return `Pages.Internal.Router.maybeToList params.${param.name}`; - } - case "required-splat": { - return `Pages.Internal.Router.nonEmptyToList params.${param.name}`; - } - case "dynamic": { - return `[ params.${param.name} ]`; - } - case "optional-splat": { - return `params.${param.name}`; - } - } - })} ]` - ) - .join("\n ")} - -{-| -} -toPath : Route -> Path -toPath route = - (baseUrlAsPath ++ (route |> routeToPath)) |> String.join "/" |> Path.fromString - - -{-| -} -toString : Route -> String -toString route = - route |> toPath |> Path.toAbsolute - - -{-| -} -toLink : (List (Attribute msg) -> tag) -> Route -> tag -toLink toAnchorTag route = - toAnchorTag - [ route |> toString |> Attr.href - , Attr.attribute "elm-pages:prefetch" "" - ] - - -{-| -} -link : List (Attribute msg) -> List (Html msg) -> Route -> Html msg -link attributes children route = - toLink - (\\anchorAttrs -> - Html.a - (anchorAttrs ++ attributes) - children - ) - route - - -{-| -} -redirectTo : Route -> Server.Response.Response data error -redirectTo route = - route - |> toString - |> Server.Response.temporaryRedirect -`, fetcherModules: templates.map((name) => { return [name, fetcherModule(name)]; }), From 4c2e9d264a3730e90f18f48e0017934f7e8a0928 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 14:25:51 -0700 Subject: [PATCH 42/69] Add additional param types. --- codegen/Generate.elm | 12 ++++++++++++ src/Pages/Internal/RoutePattern.elm | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index f55eb7d7..25d5b30f 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -135,6 +135,12 @@ routeToPath routes = RoutePattern.OptionalParam2 name -> Elm.list [] + + RoutePattern.RequiredSplatParam2 -> + Elm.val "TODO" + + RoutePattern.OptionalSplatParam2 -> + Elm.val "TODO" ) |> Elm.list ) @@ -158,6 +164,12 @@ routeToPath routes = RoutePattern.OptionalParam2 name -> maybeToList.call (Elm.get name params) + + RoutePattern.RequiredSplatParam2 -> + Elm.val "Debug.todo \"\"" + + RoutePattern.OptionalSplatParam2 -> + Elm.val "Debug.todo \"\"" ) |> Elm.list ) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 9de459b7..64c000a8 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -303,6 +303,8 @@ type RouteParam = StaticParam String | DynamicParam String | OptionalParam2 String + | RequiredSplatParam2 + | OptionalSplatParam2 toVariantName : RoutePattern -> { variantName : String, params : List RouteParam } @@ -342,13 +344,13 @@ toVariantName route = RequiredSplat -> ( "SPLAT_" - , DynamicParam "TODO" + , RequiredSplatParam2 |> Just ) OptionalSplat -> ( "SPLAT__" - , DynamicParam "TODO" + , OptionalSplatParam2 |> Just ) ) From a39c381264747ae82dcc6cf9dc7b428cd21a9dd8 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 14:31:50 -0700 Subject: [PATCH 43/69] Inline code. --- src/Pages/Internal/RoutePattern.elm | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 64c000a8..1c281477 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -11,7 +11,6 @@ module Pages.Internal.RoutePattern exposing import Elm import Elm.Annotation exposing (Annotation) -import Elm.Case import Elm.CodeGen import Html exposing (Html) @@ -219,7 +218,11 @@ routeToBranch route = [ Elm.CodeGen.varPattern "splat" ] ) ) - , toRecordVariant innerType somethingNew + , toRecordVariant innerType + (somethingNew + |> List.map Tuple.first + |> String.join "__" + ) ) ] ++ (case ending of @@ -258,6 +261,8 @@ routeToBranch route = |> Just ) ] + |> List.map Tuple.first + |> String.join "__" ) ) ] @@ -294,7 +299,11 @@ routeToBranch route = Elm.CodeGen.varPattern (decapitalize name) ) ) - , toRecordVariant innerType something + , toRecordVariant innerType + (something + |> List.map Tuple.first + |> String.join "__" + ) ) ] @@ -369,23 +378,17 @@ toVariantName route = ) -toRecordVariant : Maybe Elm.CodeGen.Expression -> List ( String, b ) -> Elm.CodeGen.Expression -toRecordVariant innerType something = +toRecordVariant : Maybe Elm.CodeGen.Expression -> String -> Elm.CodeGen.Expression +toRecordVariant innerType constructorName = case innerType of Just innerRecord -> Elm.CodeGen.apply - [ something - |> List.map Tuple.first - |> String.join "__" - |> Elm.CodeGen.val + [ constructorName |> Elm.CodeGen.val , innerRecord ] Nothing -> - something - |> List.map Tuple.first - |> String.join "__" - |> Elm.CodeGen.val + constructorName |> Elm.CodeGen.val {-| -} From c24f8fe4c15955eae417d77cd39668e1cd7b5631 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 14:33:55 -0700 Subject: [PATCH 44/69] Remove duplication. --- src/Pages/Internal/RoutePattern.elm | 32 +++++++++-------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 1c281477..6211d70c 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -218,11 +218,7 @@ routeToBranch route = [ Elm.CodeGen.varPattern "splat" ] ) ) - , toRecordVariant innerType - (somethingNew - |> List.map Tuple.first - |> String.join "__" - ) + , toRecordVariant innerType route ) ] ++ (case ending of @@ -253,17 +249,7 @@ routeToBranch route = |> Elm.CodeGen.record |> Just ) - (something - ++ [ ( optionalName ++ "__" - , ( decapitalize optionalName - , Elm.CodeGen.val "Nothing" - ) - |> Just - ) - ] - |> List.map Tuple.first - |> String.join "__" - ) + route ) ] @@ -300,10 +286,7 @@ routeToBranch route = ) ) , toRecordVariant innerType - (something - |> List.map Tuple.first - |> String.join "__" - ) + route ) ] @@ -378,8 +361,13 @@ toVariantName route = ) -toRecordVariant : Maybe Elm.CodeGen.Expression -> String -> Elm.CodeGen.Expression -toRecordVariant innerType constructorName = +toRecordVariant : Maybe Elm.CodeGen.Expression -> RoutePattern -> Elm.CodeGen.Expression +toRecordVariant innerType route = + let + constructorName : String + constructorName = + route |> toVariantName |> .variantName + in case innerType of Just innerRecord -> Elm.CodeGen.apply From 4c14042d6c592bca48f43a818fa2a4cd443c8e3b Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Tue, 13 Sep 2022 14:53:47 -0700 Subject: [PATCH 45/69] Extract some common code. --- src/Pages/Internal/RoutePattern.elm | 141 ++++++++++------------------ 1 file changed, 52 insertions(+), 89 deletions(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 6211d70c..604795fb 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -126,67 +126,13 @@ toRouteParamTypes pattern = routeToBranch : RoutePattern -> List ( Elm.CodeGen.Pattern, Elm.CodeGen.Expression ) routeToBranch route = - let - something : List ( String, Maybe ( String, Elm.CodeGen.Expression ) ) - something = - route.segments - |> List.map - (\segment -> - case segment of - DynamicSegment name -> - ( name ++ "_" - , ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) - |> Just - ) - - StaticSegment name -> - ( name, Nothing ) - ) - in case route.segments of [ StaticSegment "Index" ] -> [ ( Elm.CodeGen.listPattern [], Elm.CodeGen.val "Index" ) ] segments -> - let - somethingNew : List ( String, Maybe ( String, Elm.CodeGen.Expression ) ) - somethingNew = - (route.segments - |> List.map - (\segment -> - case segment of - DynamicSegment name -> - ( name ++ "_" - , ( decapitalize name, Elm.CodeGen.val (decapitalize name) ) - |> Just - ) - - StaticSegment name -> - ( name, Nothing ) - ) - ) - ++ ([ Maybe.map endingToVariantNameFields route.ending - ] - |> List.filterMap identity - ) - in case route.ending of Just ending -> - let - fieldThings : List ( String, Elm.CodeGen.Expression ) - fieldThings = - somethingNew - |> List.filterMap Tuple.second - - innerType : Maybe Elm.CodeGen.Expression - innerType = - case fieldThings of - [] -> - Nothing - - nonEmpty -> - nonEmpty |> Elm.CodeGen.record |> Just - in [ ( (case ending of Optional _ -> Elm.CodeGen.listPattern @@ -218,7 +164,7 @@ routeToBranch route = [ Elm.CodeGen.varPattern "splat" ] ) ) - , toRecordVariant innerType route + , toRecordVariant False route ) ] ++ (case ending of @@ -235,21 +181,7 @@ routeToBranch route = Elm.CodeGen.varPattern (decapitalize name) ) ) - , toRecordVariant - ((something - ++ [ ( optionalName ++ "__" - , ( decapitalize optionalName - , Elm.CodeGen.val "Nothing" - ) - |> Just - ) - ] - ) - |> List.filterMap Tuple.second - |> Elm.CodeGen.record - |> Just - ) - route + , toRecordVariant True route ) ] @@ -258,21 +190,6 @@ routeToBranch route = ) Nothing -> - let - fieldThings : List ( String, Elm.CodeGen.Expression ) - fieldThings = - something - |> List.filterMap Tuple.second - - innerType : Maybe Elm.CodeGen.Expression - innerType = - case fieldThings of - [] -> - Nothing - - nonEmpty -> - nonEmpty |> Elm.CodeGen.record |> Just - in [ ( Elm.CodeGen.listPattern (route.segments |> List.map @@ -285,8 +202,7 @@ routeToBranch route = Elm.CodeGen.varPattern (decapitalize name) ) ) - , toRecordVariant innerType - route + , toRecordVariant False route ) ] @@ -361,12 +277,59 @@ toVariantName route = ) -toRecordVariant : Maybe Elm.CodeGen.Expression -> RoutePattern -> Elm.CodeGen.Expression -toRecordVariant innerType route = +toRecordVariant : Bool -> RoutePattern -> Elm.CodeGen.Expression +toRecordVariant nothingCase route = let constructorName : String constructorName = route |> toVariantName |> .variantName + + innerType : Maybe Elm.CodeGen.Expression + innerType = + case fieldThings of + [] -> + Nothing + + nonEmpty -> + nonEmpty |> Elm.CodeGen.record |> Just + + fieldThings : List ( String, Elm.CodeGen.Expression ) + fieldThings = + route + |> toVariantName + |> .params + |> List.filterMap + (\param -> + case param of + OptionalParam2 name -> + Just + ( decapitalize name + --, Elm.CodeGen.apply [ Elm.CodeGen.val "Just", Elm.CodeGen.val (decapitalize name) ] + , if nothingCase then + Elm.CodeGen.val "Nothing" + + else + [ Elm.CodeGen.val "Just", Elm.CodeGen.val (decapitalize name) ] |> Elm.CodeGen.apply + ) + + StaticParam name -> + Nothing + + DynamicParam name -> + Just + ( decapitalize name + , Elm.CodeGen.val (decapitalize name) + ) + + RequiredSplatParam2 -> + Just + ( "splat" + , Elm.CodeGen.tuple [ Elm.CodeGen.val "splatFirst", Elm.CodeGen.val "splatRest" ] + ) + + OptionalSplatParam2 -> + Just ( "splat", Elm.CodeGen.val "splat" ) + ) in case innerType of Just innerRecord -> From 2dcc1d08187168f134eb861f114fad20b89bfe0b Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 10:28:16 -0700 Subject: [PATCH 46/69] Remove duplicate definition. --- codegen/Generate.elm | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 25d5b30f..cc1ab962 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -265,20 +265,7 @@ file templates basePath = ) ) |> expose - , Elm.declaration "toLink" - (Elm.fn2 - ( "toAnchorTag", Nothing ) - ( "route", Just (Elm.Annotation.named [] "Route") ) - (\toAnchorTag route -> - Elm.apply - toAnchorTag - [ Elm.list - [ route |> toString.call |> Gen.Html.Attributes.call_.href - , Gen.Html.Attributes.attribute "elm-pages:prefetch" "" - ] - ] - ) - ) + , toLink.declaration |> expose , Elm.declaration "link" (Elm.fn3 From b97dec34d1085b0843540d8b131662d6a4a3892b Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 10:30:37 -0700 Subject: [PATCH 47/69] Extract definition. --- codegen/Generate.elm | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index cc1ab962..c6e3b3f6 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -231,22 +231,7 @@ file templates basePath = ) ) |> expose - , Elm.declaration "toPath" - (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) - (\route -> - Gen.Path.call_.fromString - (Gen.String.call_.join - (Elm.string "/") - (Elm.Op.append - (Elm.val "baseUrlAsPath") - (Elm.apply (Elm.val "routeToPath") - [ route ] - ) - ) - ) - |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") - ) - ) + , toPath.declaration |> expose , toString.declaration |> expose @@ -302,6 +287,25 @@ file templates basePath = ] +toPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } +toPath = + Elm.Declare.fn "toPath" + ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Path.call_.fromString + (Gen.String.call_.join + (Elm.string "/") + (Elm.Op.append + (Elm.val "baseUrlAsPath") + (Elm.apply (Elm.val "routeToPath") + [ route ] + ) + ) + ) + |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") + ) + + toLink : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression } toLink = Elm.Declare.fn2 "toLink" @@ -322,10 +326,7 @@ toString : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Express toString = Elm.Declare.fn "toString" ( "route", Elm.Annotation.named [] "Route" |> Just ) - (\route -> - Gen.Path.toAbsolute - (Elm.apply (Elm.val "toPath") [ route ]) - ) + (\route -> Gen.Path.toAbsolute (toPath.call route)) expose : Elm.Declaration -> Elm.Declaration From dda46333e72f4b7018601d87c3892f0f8d847571 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 10:32:26 -0700 Subject: [PATCH 48/69] Reuse declaration. --- codegen/Generate.elm | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index c6e3b3f6..5803ed62 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -191,6 +191,26 @@ file templates basePath = segmentsToRouteFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } segmentsToRouteFn = segmentsToRoute routes + + routeToPathFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } + routeToPathFn = + routeToPath routes + + toPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } + toPath = + Elm.Declare.fn "toPath" + ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Path.call_.fromString + (Gen.String.call_.join + (Elm.string "/") + (Elm.Op.append + (Elm.val "baseUrlAsPath") + (routeToPathFn.call route) + ) + ) + |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") + ) in Elm.file [ "Route" ] @@ -217,7 +237,7 @@ file templates basePath = , Elm.declaration "baseUrl" (Elm.string basePath) |> expose , maybeToList.declaration - , routeToPath routes |> .declaration |> expose + , routeToPathFn.declaration |> expose , Elm.declaration "baseUrlAsPath" (Gen.List.call_.filter (Elm.fn ( "item", Nothing ) @@ -287,25 +307,6 @@ file templates basePath = ] -toPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } -toPath = - Elm.Declare.fn "toPath" - ( "route", Elm.Annotation.named [] "Route" |> Just ) - (\route -> - Gen.Path.call_.fromString - (Gen.String.call_.join - (Elm.string "/") - (Elm.Op.append - (Elm.val "baseUrlAsPath") - (Elm.apply (Elm.val "routeToPath") - [ route ] - ) - ) - ) - |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") - ) - - toLink : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression } toLink = Elm.Declare.fn2 "toLink" From 79ab6858bfc86885b89c32025979d3986aaf4b8f Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 10:44:24 -0700 Subject: [PATCH 49/69] Re-use top-level values. --- codegen/Generate.elm | 67 +++++++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 5803ed62..f91bb9b8 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -205,12 +205,32 @@ file templates basePath = (Gen.String.call_.join (Elm.string "/") (Elm.Op.append - (Elm.val "baseUrlAsPath") + baseUrlAsPath.reference (routeToPathFn.call route) ) ) |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") ) + + baseUrlAsPath : { declaration : Elm.Declaration, reference : Elm.Expression, referenceFrom : List String -> Elm.Expression } + baseUrlAsPath = + topLevelValue + "baseUrlAsPath" + (Gen.List.call_.filter + (Elm.fn ( "item", Nothing ) + (\item -> + Gen.Basics.call_.not + (Gen.String.call_.isEmpty item) + ) + ) + (Gen.String.call_.split (Elm.string "/") + baseUrl.reference + ) + ) + + baseUrl : { declaration : Elm.Declaration, reference : Elm.Expression, referenceFrom : List String -> Elm.Expression } + baseUrl = + topLevelValue "baseUrl" (Elm.string basePath) in Elm.file [ "Route" ] @@ -234,22 +254,11 @@ file templates basePath = ) ) |> expose - , Elm.declaration "baseUrl" (Elm.string basePath) + , baseUrl.declaration |> expose , maybeToList.declaration , routeToPathFn.declaration |> expose - , Elm.declaration "baseUrlAsPath" - (Gen.List.call_.filter - (Elm.fn ( "item", Nothing ) - (\item -> - Gen.Basics.call_.not - (Gen.String.call_.isEmpty item) - ) - ) - (Gen.String.call_.split (Elm.string "/") - (Elm.val "baseUrl") - ) - ) + , baseUrlAsPath.declaration |> expose , toPath.declaration |> expose @@ -295,9 +304,9 @@ file templates basePath = (Elm.fn ( "path", Just Elm.Annotation.string ) (\path -> Elm.ifThen - (path |> Gen.String.call_.startsWith (Elm.val "baseUrl")) + (path |> Gen.String.call_.startsWith baseUrl.reference) (Gen.String.call_.dropLeft - (Gen.String.call_.length (Elm.val "baseUrl")) + (Gen.String.call_.length baseUrl.reference) path ) path @@ -307,6 +316,32 @@ file templates basePath = ] +topLevelValue : + String + -> Elm.Expression + -> + { declaration : Elm.Declaration + , reference : Elm.Expression + , referenceFrom : List String -> Elm.Expression + } +topLevelValue name expression = + let + declaration_ : + { declaration : Elm.Declaration + , call : List Elm.Expression -> Elm.Expression + , callFrom : List String -> List Elm.Expression -> Elm.Expression + } + declaration_ = + Elm.Declare.function name + [] + (\_ -> expression) + in + { declaration = declaration_.declaration + , reference = declaration_.call [] + , referenceFrom = \from -> declaration_.callFrom from [] + } + + toLink : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression } toLink = Elm.Declare.fn2 "toLink" From cd896a3a454a89b4a0af7d790db1a87bbf683784 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:06:21 -0700 Subject: [PATCH 50/69] Rearrange code. --- codegen/Generate.elm | 334 +++++++++++++++++++++++-------------------- 1 file changed, 175 insertions(+), 159 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index f91bb9b8..8c6821e9 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -42,6 +42,181 @@ main = } +file : List (List String) -> String -> Elm.File +file templates basePath = + let + routes : List RoutePattern.RoutePattern + routes = + templates + |> List.filterMap RoutePattern.fromModuleName + + segmentsToRouteFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } + segmentsToRouteFn = + segmentsToRoute routes + + routeToPathFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } + routeToPathFn = + routeToPath routes + + toPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } + toPath = + Elm.Declare.fn "toPath" + ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Path.call_.fromString + (Gen.String.call_.join + (Elm.string "/") + (Elm.Op.append + baseUrlAsPath.reference + (routeToPathFn.call route) + ) + ) + |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") + ) + + baseUrlAsPath : { declaration : Elm.Declaration, reference : Elm.Expression, referenceFrom : List String -> Elm.Expression } + baseUrlAsPath = + topLevelValue + "baseUrlAsPath" + (Gen.List.call_.filter + (Elm.fn ( "item", Nothing ) + (\item -> + Gen.Basics.call_.not + (Gen.String.call_.isEmpty item) + ) + ) + (Gen.String.call_.split (Elm.string "/") + baseUrl.reference + ) + ) + + urlToRoute : Elm.Declaration + urlToRoute = + Elm.declaration "urlToRoute" + (Elm.fn + ( "url" + , Elm.Annotation.extensible "url" [ ( "path", Elm.Annotation.string ) ] + |> Just + ) + (\url -> + segmentsToRouteFn.call + (splitPath.call + (url |> Elm.get "path") + ) + |> Elm.withType (Elm.Annotation.maybe (Elm.Annotation.named [] "Route")) + ) + ) + + withoutBaseUrl : Elm.Declaration + withoutBaseUrl = + Elm.declaration "withoutBaseUrl" + (Elm.fn ( "path", Just Elm.Annotation.string ) + (\path -> + Elm.ifThen + (path |> Gen.String.call_.startsWith baseUrl.reference) + (Gen.String.call_.dropLeft + (Gen.String.call_.length baseUrl.reference) + path + ) + path + ) + ) + + toString : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } + toString = + Elm.Declare.fn "toString" + ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> Gen.Path.toAbsolute (toPath.call route)) + + redirectTo : Elm.Declaration + redirectTo = + Elm.declaration "redirectTo" + (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) + (\route -> + Gen.Server.Response.call_.temporaryRedirect + (toString.call route) + |> Elm.withType + (Elm.Annotation.namedWith [ "Server", "Response" ] + "Response" + [ Elm.Annotation.var "data" + , Elm.Annotation.var "error" + ] + ) + ) + ) + + toLink : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression } + toLink = + Elm.Declare.fn2 "toLink" + ( "toAnchorTag", Nothing ) + ( "route", Just (Elm.Annotation.named [] "Route") ) + (\toAnchorTag route -> + Elm.apply + toAnchorTag + [ Elm.list + [ route |> toString.call |> Gen.Html.Attributes.call_.href + , Gen.Html.Attributes.attribute "elm-pages:prefetch" "" + ] + ] + ) + + link : Elm.Declaration + link = + Elm.declaration "link" + (Elm.fn3 + ( "attributes", Nothing ) + ( "children", Nothing ) + ( "route", Just (Elm.Annotation.named [] "Route") ) + (\attributes children route -> + toLink.call + (Elm.fn + ( "anchorAttrs", Nothing ) + (\anchorAttrs -> + Gen.Html.call_.a + (Elm.Op.append anchorAttrs attributes) + children + ) + ) + route + ) + ) + + baseUrl : { declaration : Elm.Declaration, reference : Elm.Expression, referenceFrom : List String -> Elm.Expression } + baseUrl = + topLevelValue "baseUrl" (Elm.string basePath) + in + Elm.file + [ "Route" ] + [ Elm.customType "Route" + (routes |> List.map RoutePattern.toVariant) + |> expose + , segmentsToRouteFn.declaration + |> expose + , urlToRoute + |> expose + , baseUrl.declaration + |> expose + , routeToPathFn.declaration + |> expose + , baseUrlAsPath.declaration + |> expose + , toPath.declaration + |> expose + , toString.declaration + |> expose + , redirectTo + |> expose + , toLink.declaration + |> expose + , link + |> expose + , withoutBaseUrl + |> expose + , splitPath.declaration + , maybeToList.declaration + ] + + splitPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } splitPath = Elm.Declare.fn "splitPath" @@ -180,142 +355,6 @@ routeToPath routes = ) -file : List (List String) -> String -> Elm.File -file templates basePath = - let - routes : List RoutePattern.RoutePattern - routes = - templates - |> List.filterMap RoutePattern.fromModuleName - - segmentsToRouteFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } - segmentsToRouteFn = - segmentsToRoute routes - - routeToPathFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } - routeToPathFn = - routeToPath routes - - toPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } - toPath = - Elm.Declare.fn "toPath" - ( "route", Elm.Annotation.named [] "Route" |> Just ) - (\route -> - Gen.Path.call_.fromString - (Gen.String.call_.join - (Elm.string "/") - (Elm.Op.append - baseUrlAsPath.reference - (routeToPathFn.call route) - ) - ) - |> Elm.withType (Elm.Annotation.named [ "Path" ] "Path") - ) - - baseUrlAsPath : { declaration : Elm.Declaration, reference : Elm.Expression, referenceFrom : List String -> Elm.Expression } - baseUrlAsPath = - topLevelValue - "baseUrlAsPath" - (Gen.List.call_.filter - (Elm.fn ( "item", Nothing ) - (\item -> - Gen.Basics.call_.not - (Gen.String.call_.isEmpty item) - ) - ) - (Gen.String.call_.split (Elm.string "/") - baseUrl.reference - ) - ) - - baseUrl : { declaration : Elm.Declaration, reference : Elm.Expression, referenceFrom : List String -> Elm.Expression } - baseUrl = - topLevelValue "baseUrl" (Elm.string basePath) - in - Elm.file - [ "Route" ] - [ Elm.customType "Route" - (routes |> List.map RoutePattern.toVariant) - |> expose - , segmentsToRouteFn.declaration |> expose - , splitPath.declaration - , Elm.declaration "urlToRoute" - (Elm.fn - ( "url" - , Elm.Annotation.extensible "url" [ ( "path", Elm.Annotation.string ) ] - |> Just - ) - (\url -> - segmentsToRouteFn.call - (splitPath.call - (url |> Elm.get "path") - ) - |> Elm.withType (Elm.Annotation.maybe (Elm.Annotation.named [] "Route")) - ) - ) - |> expose - , baseUrl.declaration - |> expose - , maybeToList.declaration - , routeToPathFn.declaration |> expose - , baseUrlAsPath.declaration - |> expose - , toPath.declaration - |> expose - , toString.declaration - |> expose - , Elm.declaration "redirectTo" - (Elm.fn ( "route", Elm.Annotation.named [] "Route" |> Just ) - (\route -> - Gen.Server.Response.call_.temporaryRedirect - (toString.call route) - |> Elm.withType - (Elm.Annotation.namedWith [ "Server", "Response" ] - "Response" - [ Elm.Annotation.var "data" - , Elm.Annotation.var "error" - ] - ) - ) - ) - |> expose - , toLink.declaration - |> expose - , Elm.declaration "link" - (Elm.fn3 - ( "attributes", Nothing ) - ( "children", Nothing ) - ( "route", Just (Elm.Annotation.named [] "Route") ) - (\attributes children route -> - toLink.call - (Elm.fn - ( "anchorAttrs", Nothing ) - (\anchorAttrs -> - Gen.Html.call_.a - (Elm.Op.append anchorAttrs attributes) - children - ) - ) - route - ) - ) - |> expose - , Elm.declaration "withoutBaseUrl" - (Elm.fn ( "path", Just Elm.Annotation.string ) - (\path -> - Elm.ifThen - (path |> Gen.String.call_.startsWith baseUrl.reference) - (Gen.String.call_.dropLeft - (Gen.String.call_.length baseUrl.reference) - path - ) - path - ) - ) - |> expose - ] - - topLevelValue : String -> Elm.Expression @@ -342,29 +381,6 @@ topLevelValue name expression = } -toLink : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression } -toLink = - Elm.Declare.fn2 "toLink" - ( "toAnchorTag", Nothing ) - ( "route", Just (Elm.Annotation.named [] "Route") ) - (\toAnchorTag route -> - Elm.apply - toAnchorTag - [ Elm.list - [ route |> toString.call |> Gen.Html.Attributes.call_.href - , Gen.Html.Attributes.attribute "elm-pages:prefetch" "" - ] - ] - ) - - -toString : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } -toString = - Elm.Declare.fn "toString" - ( "route", Elm.Annotation.named [] "Route" |> Just ) - (\route -> Gen.Path.toAbsolute (toPath.call route)) - - expose : Elm.Declaration -> Elm.Declaration expose declaration = declaration From 60329582d16e687989fa5fd181bf7607442409c2 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:08:17 -0700 Subject: [PATCH 51/69] Extract out exposed generator functions to one place. --- codegen/Generate.elm | 48 ++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 8c6821e9..d6838821 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -187,34 +187,26 @@ file templates basePath = in Elm.file [ "Route" ] - [ Elm.customType "Route" - (routes |> List.map RoutePattern.toVariant) - |> expose - , segmentsToRouteFn.declaration - |> expose - , urlToRoute - |> expose - , baseUrl.declaration - |> expose - , routeToPathFn.declaration - |> expose - , baseUrlAsPath.declaration - |> expose - , toPath.declaration - |> expose - , toString.declaration - |> expose - , redirectTo - |> expose - , toLink.declaration - |> expose - , link - |> expose - , withoutBaseUrl - |> expose - , splitPath.declaration - , maybeToList.declaration - ] + ([ [ Elm.customType "Route" (routes |> List.map RoutePattern.toVariant) + , segmentsToRouteFn.declaration + , urlToRoute + , baseUrl.declaration + , routeToPathFn.declaration + , baseUrlAsPath.declaration + , toPath.declaration + , toString.declaration + , redirectTo + , toLink.declaration + , link + , withoutBaseUrl + ] + |> List.map expose + , [ splitPath.declaration + , maybeToList.declaration + ] + ] + |> List.concat + ) splitPath : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } From b93f6c6918c2d1053289181f2bc257f744371f60 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:28:12 -0700 Subject: [PATCH 52/69] Add missing dependency. --- examples/routing/elm.json | 7 +++++-- examples/routing/package-lock.json | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/routing/elm.json b/examples/routing/elm.json index 9cfb196b..a84afeff 100644 --- a/examples/routing/elm.json +++ b/examples/routing/elm.json @@ -37,18 +37,21 @@ "noahzgordon/elm-color-extra": "1.0.2", "robinheghan/fnv1a": "1.0.0", "rtfeldman/elm-css": "16.1.1", + "the-sett/elm-pretty-printer": "3.0.0", + "the-sett/elm-syntax-dsl": "6.0.2", "turboMaCk/non-empty-list-alias": "1.2.0", "vito/elm-ansi": "10.0.1" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "elm/file": "1.0.5", "elm-community/basics-extra": "4.1.0", + "elm-community/maybe-extra": "5.3.0", "fredcy/elm-parseint": "2.0.1", "miniBill/elm-unicode": "1.0.2", "rtfeldman/elm-hex": "1.0.0", "stil4m/elm-syntax": "7.2.9", - "stil4m/structured-writer": "1.0.3", - "the-sett/elm-pretty-printer": "3.0.0" + "stil4m/structured-writer": "1.0.3" } }, "test-dependencies": { diff --git a/examples/routing/package-lock.json b/examples/routing/package-lock.json index a78c6062..f8841755 100644 --- a/examples/routing/package-lock.json +++ b/examples/routing/package-lock.json @@ -54,6 +54,7 @@ "@types/node": "12.20.12", "@types/serve-static": "^1.15.0", "cypress": "^10.6.0", + "elm-codegen": "^0.2.0", "elm-optimize-level-2": "^0.1.5", "elm-review": "^2.7.4", "elm-test": "^0.19.1-revision9", @@ -1506,6 +1507,7 @@ "cross-spawn": "7.0.3", "cypress": "^10.6.0", "devcert": "^1.2.2", + "elm-codegen": "^0.2.0", "elm-doc-preview": "^5.0.5", "elm-hot": "^1.1.6", "elm-optimize-level-2": "^0.1.5", From 88dbfb5a78072601daea5d1248bb2f0977e44fb2 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:28:37 -0700 Subject: [PATCH 53/69] Remove generated Debug.todo. --- codegen/Generate.elm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index d6838821..6008eab7 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -17,6 +17,7 @@ import Gen.List import Gen.Path import Gen.Server.Response import Gen.String +import Gen.Tuple import Pages.Internal.RoutePattern as RoutePattern exposing (RoutePattern) import Pretty @@ -333,10 +334,10 @@ routeToPath routes = maybeToList.call (Elm.get name params) RoutePattern.RequiredSplatParam2 -> - Elm.val "Debug.todo \"\"" + Elm.Op.cons (Gen.Tuple.first (Elm.get "splat" params)) (Gen.Tuple.second (Elm.get "splat" params)) RoutePattern.OptionalSplatParam2 -> - Elm.val "Debug.todo \"\"" + Elm.get "splat" params ) |> Elm.list ) From a29d3a8b85f49dcb631e16648cc9e2c9375ba96f Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:33:50 -0700 Subject: [PATCH 54/69] Don't generate catchall in case expression when it's alreaady covered by a top-level optional splat route. --- codegen/Generate.elm | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index 6008eab7..ea198348 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -245,12 +245,31 @@ segmentsToRoute routes = , Elm.Annotation.list Elm.Annotation.string |> Just ) (\segments -> + let + alreadyHasCatchallBranch : Bool + alreadyHasCatchallBranch = + routes + |> List.map RoutePattern.toVariantName + |> List.any + (\{ params } -> + case params of + [ RoutePattern.OptionalSplatParam2 ] -> + True + + _ -> + False + ) + in (((routes |> List.concatMap RoutePattern.routeToBranch |> List.map (Tuple.mapSecond (\constructRoute -> Elm.CodeGen.apply [ Elm.CodeGen.val "Just", constructRoute ])) ) - ++ [ ( Elm.CodeGen.allPattern, Elm.CodeGen.val "Nothing" ) - ] + ++ (if alreadyHasCatchallBranch then + [] + + else + [ ( Elm.CodeGen.allPattern, Elm.CodeGen.val "Nothing" ) ] + ) ) |> Elm.CodeGen.caseExpr (Elm.CodeGen.val "segments") ) From ea5ad24b0b2a04850e5960cac971afbf1ffdbf69 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:34:25 -0700 Subject: [PATCH 55/69] Delete commented code. --- src/Pages/Internal/RoutePattern.elm | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 604795fb..0df84010 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -304,7 +304,6 @@ toRecordVariant nothingCase route = OptionalParam2 name -> Just ( decapitalize name - --, Elm.CodeGen.apply [ Elm.CodeGen.val "Just", Elm.CodeGen.val (decapitalize name) ] , if nothingCase then Elm.CodeGen.val "Nothing" From 0b8e7f9e8b867dea9de650e93e0820ae47bda8dc Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:35:42 -0700 Subject: [PATCH 56/69] Install missing dependencies. --- examples/base-path/elm.json | 7 +++++-- examples/base-path/package-lock.json | 2 ++ examples/escaping/elm.json | 7 +++++-- examples/escaping/package-lock.json | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/base-path/elm.json b/examples/base-path/elm.json index 5a83ff07..3797c474 100644 --- a/examples/base-path/elm.json +++ b/examples/base-path/elm.json @@ -41,23 +41,26 @@ "robinheghan/fnv1a": "1.0.0", "robinheghan/murmur3": "1.0.0", "rtfeldman/elm-css": "16.1.1", + "the-sett/elm-pretty-printer": "3.0.0", + "the-sett/elm-syntax-dsl": "6.0.2", "tripokey/elm-fuzzy": "5.2.1", "turboMaCk/non-empty-list-alias": "1.2.0", "vito/elm-ansi": "10.0.1", "zwilias/json-decode-exploration": "6.0.0" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "bburdette/toop": "1.0.1", "elm/file": "1.0.5", "elm/random": "1.0.0", "elm-community/basics-extra": "4.1.0", + "elm-community/maybe-extra": "5.3.0", "fredcy/elm-parseint": "2.0.1", "mgold/elm-nonempty-list": "4.2.0", "miniBill/elm-unicode": "1.0.2", "rtfeldman/elm-hex": "1.0.0", "stil4m/elm-syntax": "7.2.9", - "stil4m/structured-writer": "1.0.3", - "the-sett/elm-pretty-printer": "3.0.0" + "stil4m/structured-writer": "1.0.3" } }, "test-dependencies": { diff --git a/examples/base-path/package-lock.json b/examples/base-path/package-lock.json index cb16587f..b60b0c2d 100644 --- a/examples/base-path/package-lock.json +++ b/examples/base-path/package-lock.json @@ -55,6 +55,7 @@ "@types/node": "12.20.12", "@types/serve-static": "^1.15.0", "cypress": "^10.6.0", + "elm-codegen": "^0.2.0", "elm-optimize-level-2": "^0.1.5", "elm-review": "^2.7.4", "elm-test": "^0.19.1-revision9", @@ -1565,6 +1566,7 @@ "cross-spawn": "7.0.3", "cypress": "^10.6.0", "devcert": "^1.2.2", + "elm-codegen": "^0.2.0", "elm-doc-preview": "^5.0.5", "elm-hot": "^1.1.6", "elm-optimize-level-2": "^0.1.5", diff --git a/examples/escaping/elm.json b/examples/escaping/elm.json index 5a83ff07..3797c474 100644 --- a/examples/escaping/elm.json +++ b/examples/escaping/elm.json @@ -41,23 +41,26 @@ "robinheghan/fnv1a": "1.0.0", "robinheghan/murmur3": "1.0.0", "rtfeldman/elm-css": "16.1.1", + "the-sett/elm-pretty-printer": "3.0.0", + "the-sett/elm-syntax-dsl": "6.0.2", "tripokey/elm-fuzzy": "5.2.1", "turboMaCk/non-empty-list-alias": "1.2.0", "vito/elm-ansi": "10.0.1", "zwilias/json-decode-exploration": "6.0.0" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "bburdette/toop": "1.0.1", "elm/file": "1.0.5", "elm/random": "1.0.0", "elm-community/basics-extra": "4.1.0", + "elm-community/maybe-extra": "5.3.0", "fredcy/elm-parseint": "2.0.1", "mgold/elm-nonempty-list": "4.2.0", "miniBill/elm-unicode": "1.0.2", "rtfeldman/elm-hex": "1.0.0", "stil4m/elm-syntax": "7.2.9", - "stil4m/structured-writer": "1.0.3", - "the-sett/elm-pretty-printer": "3.0.0" + "stil4m/structured-writer": "1.0.3" } }, "test-dependencies": { diff --git a/examples/escaping/package-lock.json b/examples/escaping/package-lock.json index 27abd7a6..c4f447ae 100644 --- a/examples/escaping/package-lock.json +++ b/examples/escaping/package-lock.json @@ -52,6 +52,7 @@ "@types/node": "12.20.12", "@types/serve-static": "^1.15.0", "cypress": "^10.6.0", + "elm-codegen": "^0.2.0", "elm-optimize-level-2": "^0.1.5", "elm-review": "^2.7.4", "elm-test": "^0.19.1-revision9", @@ -5932,6 +5933,7 @@ "cross-spawn": "7.0.3", "cypress": "^10.6.0", "devcert": "^1.2.2", + "elm-codegen": "^0.2.0", "elm-doc-preview": "^5.0.5", "elm-hot": "^1.1.6", "elm-optimize-level-2": "^0.1.5", From eebe71505132e1289f6a4a639e9bca55e82c964b Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:38:51 -0700 Subject: [PATCH 57/69] Run workflow on PRs to v3 alpha branch. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba40f3ea..87c511a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - master + - serverless-latest env: SESSION_SECRET: hello From dffdfcd3ed7ca4a3bdf7757e191419ea63a3ed89 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:44:51 -0700 Subject: [PATCH 58/69] Run elm-codegen install in prepare script. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a1e4cd8a..42678cdc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test": "./test.sh", "test:snapshot": "(cd examples/escaping && npm install && npm test) && (cd examples/base-path && npm install && npm test)", "cypress": "npm start & cypress run", + "prepare": "elm-codegen install", "review": "elm-review" }, "repository": "https://github.com/dillonkearns/elm-pages", @@ -74,4 +75,4 @@ "bin": { "elm-pages": "generator/src/cli.js" } -} +} \ No newline at end of file From e48f640019e81553678933c60e9b0ab173d5fb91 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:47:49 -0700 Subject: [PATCH 59/69] Install missing dependencies. --- examples/end-to-end/elm.json | 6 ++++-- examples/end-to-end/package-lock.json | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/end-to-end/elm.json b/examples/end-to-end/elm.json index 7cc1db93..3cac9bdb 100644 --- a/examples/end-to-end/elm.json +++ b/examples/end-to-end/elm.json @@ -44,11 +44,14 @@ "noahzgordon/elm-color-extra": "1.0.2", "robinheghan/fnv1a": "1.0.0", "rtfeldman/elm-css": "16.1.1", + "the-sett/elm-pretty-printer": "3.0.0", + "the-sett/elm-syntax-dsl": "6.0.2", "turboMaCk/non-empty-list-alias": "1.2.0", "vito/elm-ansi": "10.0.1", "ymtszw/elm-xml-decode": "3.2.1" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "elm/file": "1.0.5", "elm-community/basics-extra": "4.1.0", "elm-community/maybe-extra": "5.3.0", @@ -57,8 +60,7 @@ "miniBill/elm-unicode": "1.0.2", "rtfeldman/elm-hex": "1.0.0", "stil4m/elm-syntax": "7.2.9", - "stil4m/structured-writer": "1.0.3", - "the-sett/elm-pretty-printer": "3.0.0" + "stil4m/structured-writer": "1.0.3" } }, "test-dependencies": { diff --git a/examples/end-to-end/package-lock.json b/examples/end-to-end/package-lock.json index 6f4dcc59..faee90e9 100644 --- a/examples/end-to-end/package-lock.json +++ b/examples/end-to-end/package-lock.json @@ -53,6 +53,7 @@ "@types/node": "12.20.12", "@types/serve-static": "^1.15.0", "cypress": "^10.6.0", + "elm-codegen": "^0.2.0", "elm-optimize-level-2": "^0.1.5", "elm-review": "^2.7.4", "elm-test": "^0.19.1-revision9", @@ -1493,6 +1494,7 @@ "cross-spawn": "7.0.3", "cypress": "^10.6.0", "devcert": "^1.2.2", + "elm-codegen": "^0.2.0", "elm-doc-preview": "^5.0.5", "elm-hot": "^1.1.6", "elm-optimize-level-2": "^0.1.5", From 3b9da8772338c8f50c6cb62d8ed5a0d9f973c02d Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 14:53:10 -0700 Subject: [PATCH 60/69] Fix command. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87c511a8..231b92f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: run: lamdera make --docs docs.json - name: Setup for cypress - run: (cd examples/end-to-end && npm install && npx elm-tooling install && rm -rf elm-stuff && npx elm-pages codegen && lamdera make app/Route/Index.elm) + run: (cd examples/end-to-end && npm install && npx elm-tooling install && rm -rf elm-stuff && npx elm-pages gen && lamdera make app/Route/Index.elm) - name: Cypress tests uses: cypress-io/github-action@v4 with: From e9ecdf582e6babb484ab247cd18f30dde0e13692 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 14 Sep 2022 15:53:27 -0700 Subject: [PATCH 61/69] Fix casing in route matching. --- codegen/Generate.elm | 42 +++++++++++++++++++++++++++-- codegen/elm.json | 1 + src/Pages/Internal/RoutePattern.elm | 22 ++++++++++++--- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/codegen/Generate.elm b/codegen/Generate.elm index ea198348..7f34977a 100644 --- a/codegen/Generate.elm +++ b/codegen/Generate.elm @@ -20,6 +20,7 @@ import Gen.String import Gen.Tuple import Pages.Internal.RoutePattern as RoutePattern exposing (RoutePattern) import Pretty +import Regex exposing (Regex) type alias Flags = @@ -314,7 +315,7 @@ routeToPath routes = (\param -> case param of RoutePattern.StaticParam name -> - [ Elm.string name ] + [ Elm.string (toKebab name) ] |> Elm.list RoutePattern.DynamicParam name -> @@ -342,7 +343,7 @@ routeToPath routes = (\param -> case param of RoutePattern.StaticParam name -> - [ Elm.string name ] + [ Elm.string (toKebab name) ] |> Elm.list RoutePattern.DynamicParam name -> @@ -402,6 +403,43 @@ expose declaration = } +{-| Decapitalize the first letter of a string. +decapitalize "This is a phrase" == "this is a phrase" +decapitalize "Hello, World" == "hello, World" +-} +decapitalize : String -> String +decapitalize word = + -- Source: https://github.com/elm-community/string-extra/blob/4.0.1/src/String/Extra.elm + changeCase Char.toLower word + + +{-| Change the case of the first letter of a string to either uppercase or +lowercase, depending of the value of `wantedCase`. This is an internal +function for use in `toSentenceCase` and `decapitalize`. +-} +changeCase : (Char -> Char) -> String -> String +changeCase mutator word = + -- Source: https://github.com/elm-community/string-extra/blob/4.0.1/src/String/Extra.elm + String.uncons word + |> Maybe.map (\( head, tail ) -> String.cons (mutator head) tail) + |> Maybe.withDefault "" + + +toKebab : String -> String +toKebab string = + string + |> decapitalize + |> String.trim + |> Regex.replace (regexFromString "([A-Z])") (.match >> String.append "-") + |> Regex.replace (regexFromString "[_-\\s]+") (always "-") + |> String.toLower + + +regexFromString : String -> Regex +regexFromString = + Regex.fromString >> Maybe.withDefault Regex.never + + port onSuccessSend : List File -> Cmd msg diff --git a/codegen/elm.json b/codegen/elm.json index 9126cbde..e560557c 100644 --- a/codegen/elm.json +++ b/codegen/elm.json @@ -11,6 +11,7 @@ "elm/core": "1.0.5", "elm/html": "1.0.0", "elm/json": "1.1.3", + "elm/regex": "1.0.0", "mdgriffith/elm-codegen": "2.0.0", "the-sett/elm-pretty-printer": "3.0.0", "the-sett/elm-syntax-dsl": "6.0.2" diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index 0df84010..e591739e 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -13,6 +13,7 @@ import Elm import Elm.Annotation exposing (Annotation) import Elm.CodeGen import Html exposing (Html) +import Regex exposing (Regex) {-| -} @@ -145,7 +146,7 @@ routeToBranch route = (\segment -> case segment of StaticSegment name -> - Elm.CodeGen.stringPattern (decapitalize name) + Elm.CodeGen.stringPattern (toKebab name) DynamicSegment name -> Elm.CodeGen.varPattern (decapitalize name) @@ -175,7 +176,7 @@ routeToBranch route = (\segment -> case segment of StaticSegment name -> - Elm.CodeGen.stringPattern (decapitalize name) + Elm.CodeGen.stringPattern (toKebab name) DynamicSegment name -> Elm.CodeGen.varPattern (decapitalize name) @@ -196,7 +197,7 @@ routeToBranch route = (\segment -> case segment of StaticSegment name -> - Elm.CodeGen.stringPattern (decapitalize name) + Elm.CodeGen.stringPattern (toKebab name) DynamicSegment name -> Elm.CodeGen.varPattern (decapitalize name) @@ -573,3 +574,18 @@ unconsPattern list = ) listFirst listRest + + +toKebab : String -> String +toKebab string = + string + |> decapitalize + |> String.trim + |> Regex.replace (regexFromString "([A-Z])") (.match >> String.append "-") + |> Regex.replace (regexFromString "[_-\\s]+") (always "-") + |> String.toLower + + +regexFromString : String -> Regex +regexFromString = + Regex.fromString >> Maybe.withDefault Regex.never From 1df22b779e4ad513ab65b9ee09a90a1959a216db Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 07:43:46 -0700 Subject: [PATCH 62/69] Address review error. --- elm.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elm.json b/elm.json index 45569e9f..e3ae4279 100644 --- a/elm.json +++ b/elm.json @@ -65,13 +65,13 @@ "robinheghan/fnv1a": "1.0.0 <= v < 2.0.0", "rtfeldman/elm-css": "17.1.1 <= v < 18.0.0", "stil4m/elm-syntax": "7.2.7 <= v < 8.0.0", - "the-sett/elm-pretty-printer": "3.0.0 <= v < 4.0.0", "the-sett/elm-syntax-dsl": "6.0.2 <= v < 7.0.0", "turboMaCk/non-empty-list-alias": "1.2.0 <= v < 2.0.0", "vito/elm-ansi": "10.0.1 <= v < 11.0.0" }, "test-dependencies": { "avh4/elm-program-test": "3.1.0 <= v < 4.0.0", - "elm-explorations/test": "1.2.2 <= v < 2.0.0" + "elm-explorations/test": "1.2.2 <= v < 2.0.0", + "the-sett/elm-pretty-printer": "3.0.0 <= v < 4.0.0" } } From 59a0bc1306e4ae36dff1f1ac7fee3fdfa8356c01 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 07:45:10 -0700 Subject: [PATCH 63/69] Review fixes. --- src/Pages/Generate.elm | 7 +++++-- src/Pages/Internal/Platform.elm | 8 ++++---- src/Pages/Review/NoContractViolations.elm | 10 +++++----- tests/Pages/RouteParamsTest.elm | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Pages/Generate.elm b/src/Pages/Generate.elm index 641649f9..113976f6 100644 --- a/src/Pages/Generate.elm +++ b/src/Pages/Generate.elm @@ -1,4 +1,7 @@ -module Pages.Generate exposing (Type(..), serverRender, buildWithLocalState, buildNoState) +module Pages.Generate exposing + ( Type(..), serverRender, buildWithLocalState, buildNoState + , Builder + ) {-| @@ -157,7 +160,7 @@ userFunction moduleName definitions = in { declaration = thing.declaration , call = \_ -> thing.call - , callFrom = \a b c d -> thing.callFrom a c d + , callFrom = \a _ c d -> thing.callFrom a c d } localDefinitions = diff --git a/src/Pages/Internal/Platform.elm b/src/Pages/Internal/Platform.elm index 17c3cf40..8e5fe5e7 100644 --- a/src/Pages/Internal/Platform.elm +++ b/src/Pages/Internal/Platform.elm @@ -558,10 +558,6 @@ update config appMsg model = redirectPending : Bool redirectPending = newUrl /= urlWithoutRedirectResolution - - stayingOnSamePath : Bool - stayingOnSamePath = - newUrl.path == model.url.path in if redirectPending then ( { model @@ -582,6 +578,10 @@ update config appMsg model = else let + stayingOnSamePath : Bool + stayingOnSamePath = + newUrl.path == model.url.path + ( newPageData, newSharedData, newActionData ) = case newData of ResponseSketch.RenderPage pageData actionData -> diff --git a/src/Pages/Review/NoContractViolations.elm b/src/Pages/Review/NoContractViolations.elm index de07e390..586df7af 100644 --- a/src/Pages/Review/NoContractViolations.elm +++ b/src/Pages/Review/NoContractViolations.elm @@ -209,7 +209,7 @@ routeParamsMatchesNameOrError annotation moduleName = Ok actualStringFields -> let - expectedFields : Dict String RoutePattern.Param + expectedFields : Dict String Param expectedFields = expectedRouteParamsFromModuleName moduleName in @@ -241,7 +241,7 @@ expectedFieldsToRecordString moduleName = ) -expectedRouteParamsFromModuleName : List String -> Dict String RoutePattern.Param +expectedRouteParamsFromModuleName : List String -> Dict String Param expectedRouteParamsFromModuleName moduleSegments = case moduleSegments of "Route" :: segments -> @@ -258,12 +258,12 @@ expectedRouteParamsFromModuleName moduleSegments = stringFields : Node TypeAnnotation - -> Result (Error {}) (Dict String (Result (Node TypeAnnotation) RoutePattern.Param)) + -> Result (Error {}) (Dict String (Result (Node TypeAnnotation) Param)) stringFields typeAnnotation = case Node.value typeAnnotation of TypeAnnotation.Record recordDefinition -> let - fields : Dict String (Result (Node TypeAnnotation) RoutePattern.Param) + fields : Dict String (Result (Node TypeAnnotation) Param) fields = recordDefinition |> List.map Node.value @@ -287,7 +287,7 @@ stringFields typeAnnotation = ) -paramType : Node TypeAnnotation -> Result (Node TypeAnnotation) RoutePattern.Param +paramType : Node TypeAnnotation -> Result (Node TypeAnnotation) Param paramType typeAnnotation = case Node.value typeAnnotation of TypeAnnotation.Tupled [ first, second ] -> diff --git a/tests/Pages/RouteParamsTest.elm b/tests/Pages/RouteParamsTest.elm index 4c475665..a0b1e96c 100644 --- a/tests/Pages/RouteParamsTest.elm +++ b/tests/Pages/RouteParamsTest.elm @@ -1,4 +1,4 @@ -module Pages.RouteParamsTest exposing (..) +module Pages.RouteParamsTest exposing (suite) import Elm import Elm.Annotation From 6b6e2c851a485175f2cec3b2ee0152448ed4f2aa Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 07:51:14 -0700 Subject: [PATCH 64/69] Review fixes. --- src/Pages/Generate.elm | 33 +++++++++++++++++++++++++---- src/Pages/Internal/RoutePattern.elm | 13 +++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/Pages/Generate.elm b/src/Pages/Generate.elm index 113976f6..fd54ce85 100644 --- a/src/Pages/Generate.elm +++ b/src/Pages/Generate.elm @@ -125,6 +125,11 @@ userFunction : -> Elm.File userFunction moduleName definitions = let + viewFn : + { declaration : Elm.Declaration + , call : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + , callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + } viewFn = case definitions.localState of Just _ -> @@ -144,7 +149,12 @@ userFunction moduleName definitions = Nothing -> let - thing = + viewDeclaration : + { declaration : Elm.Declaration + , call : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + , callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + } + viewDeclaration = Elm.Declare.fn3 "view" ( "maybeUrl" , "PageUrl" @@ -158,11 +168,21 @@ userFunction moduleName definitions = ( "app", Just appType ) (definitions.view Elm.unit) in - { declaration = thing.declaration - , call = \_ -> thing.call - , callFrom = \a _ c d -> thing.callFrom a c d + { declaration = viewDeclaration.declaration + , call = \_ -> viewDeclaration.call + , callFrom = \a _ c d -> viewDeclaration.callFrom a c d } + localDefinitions : + Maybe + { updateFn : + { declaration : Elm.Declaration + , call : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + , callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression + } + , initFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression } + , subscriptionsFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression } + } localDefinitions = definitions.localState |> Maybe.map @@ -207,6 +227,7 @@ userFunction moduleName definitions = } ) + dataFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } dataFn = Elm.Declare.fn "data" ( "routeParams" @@ -216,6 +237,7 @@ userFunction moduleName definitions = ) (definitions.data >> Elm.withType (myType "Data")) + actionFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } actionFn = Elm.Declare.fn "action" ( "routeParams" @@ -225,6 +247,7 @@ userFunction moduleName definitions = ) (definitions.action >> Elm.withType (myType "ActionData")) + headFn : { declaration : Elm.Declaration, call : Elm.Expression -> Elm.Expression, callFrom : List String -> Elm.Expression -> Elm.Expression } headFn = Elm.Declare.fn "head" ( "app", Just appType ) @@ -312,10 +335,12 @@ userFunction moduleName definitions = ) +localType : String -> Elm.Annotation.Annotation localType = Elm.Annotation.named [] +myType : String -> Elm.Annotation.Annotation myType dataType = Elm.Annotation.namedWith [ "Server", "Request" ] "Parser" diff --git a/src/Pages/Internal/RoutePattern.elm b/src/Pages/Internal/RoutePattern.elm index e591739e..0ffe3845 100644 --- a/src/Pages/Internal/RoutePattern.elm +++ b/src/Pages/Internal/RoutePattern.elm @@ -241,6 +241,7 @@ toVariantName route = ) ) + something2 : List ( String, Maybe RouteParam ) something2 = something ++ ([ Maybe.map @@ -350,7 +351,8 @@ toVariant pattern = else let - something = + allSegments : List ( String, Maybe ( String, Annotation ) ) + allSegments = (pattern.segments |> List.map (\segment -> @@ -369,10 +371,11 @@ toVariant pattern = fieldThings : List ( String, Annotation ) fieldThings = - something + allSegments |> List.filterMap Tuple.second - innerType = + noArgsOrNonEmptyRecordArg : List Annotation + noArgsOrNonEmptyRecordArg = case fieldThings of [] -> [] @@ -381,11 +384,11 @@ toVariant pattern = nonEmpty |> Elm.Annotation.record |> List.singleton in Elm.variantWith - (something + (allSegments |> List.map Tuple.first |> String.join "__" ) - innerType + noArgsOrNonEmptyRecordArg endingToVariantNameFields : Ending -> ( String, Maybe ( String, Elm.CodeGen.Expression ) ) From 48dec4bbd00ad81bbacc81000d5b694ec9cdde88 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 07:56:31 -0700 Subject: [PATCH 65/69] Add missing dependency. --- examples/todos/codegen/elm.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/todos/codegen/elm.json b/examples/todos/codegen/elm.json index 7680169e..27f10b28 100644 --- a/examples/todos/codegen/elm.json +++ b/examples/todos/codegen/elm.json @@ -13,15 +13,18 @@ "elm/html": "1.0.0", "elm/json": "1.1.3", "elm/regex": "1.0.0", - "mdgriffith/elm-codegen": "2.0.0" + "mdgriffith/elm-codegen": "2.0.0", + "the-sett/elm-syntax-dsl": "6.0.2" }, "indirect": { + "Chadtech/elm-bool-extra": "2.4.2", "elm/parser": "1.1.0", "elm/time": "1.0.0", "elm/url": "1.0.0", "elm/virtual-dom": "1.0.2", "elm-community/basics-extra": "4.1.0", "elm-community/list-extra": "8.6.0", + "elm-community/maybe-extra": "5.3.0", "miniBill/elm-unicode": "1.0.2", "rtfeldman/elm-hex": "1.0.0", "stil4m/elm-syntax": "7.2.9", From 810eb62aacef5df2d19e008a7410294399fcdf48 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 07:56:59 -0700 Subject: [PATCH 66/69] Add withType helper for temporary incorrect type in codegen. --- examples/todos/codegen/Cli.elm | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/todos/codegen/Cli.elm b/examples/todos/codegen/Cli.elm index 7ecf12b7..3bda24dc 100644 --- a/examples/todos/codegen/Cli.elm +++ b/examples/todos/codegen/Cli.elm @@ -114,11 +114,18 @@ createFile moduleName = Elm.Case.custom msg (Elm.Annotation.named [] "Msg") [ Elm.Case.branch0 "NoOp" - (Elm.tuple model Gen.Effect.none) + (Elm.tuple model + (Gen.Effect.none + |> Elm.withType effectType + ) + ) ] , init = \pageUrl sharedModel app -> - Elm.tuple (Elm.record []) Gen.Effect.none + Elm.tuple (Elm.record []) + (Gen.Effect.none + |> Elm.withType effectType + ) , subscriptions = \maybePageUrl routeParams path sharedModel model -> Gen.Platform.Sub.none @@ -129,6 +136,11 @@ createFile moduleName = } +effectType : Elm.Annotation.Annotation +effectType = + Elm.Annotation.namedWith [ "Effect" ] "Effect" [ Elm.Annotation.var "msg" ] + + port print : String -> Cmd msg From c3d3909eb54f8ec073102fe78be24b93a8efd254 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 08:07:50 -0700 Subject: [PATCH 67/69] Improve error output for elm-pages codegen command. --- generator/src/cli.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/generator/src/cli.js b/generator/src/cli.js index 387e5836..7920c8a9 100755 --- a/generator/src/cli.js +++ b/generator/src/cli.js @@ -8,6 +8,7 @@ const init = require("./init.js"); const codegen = require("./codegen.js"); const fs = require("fs"); const path = require("path"); +const { restoreColorSafe } = require("./error-formatter"); const commander = require("commander"); const { compileCliApp } = require("./compile-elm.js"); @@ -124,19 +125,22 @@ async function main() { if (!fs.existsSync(expectedFilePath)) { throw `I couldn't find a module named ${expectedFilePath}`; } - await codegen.generate(""); - await runElmCodegenInstall(); - await compileCliApp( - // { debug: true }, - {}, - `${splitModuleName.join("/")}.elm`, - path.join(process.cwd(), "codegen/elm-stuff/scaffold.js"), - // "elm-stuff/scaffold.js", - "codegen", + try { + await codegen.generate(""); + await runElmCodegenInstall(); + await compileCliApp( + // { debug: true }, + {}, + `${splitModuleName.join("/")}.elm`, + path.join(process.cwd(), "codegen/elm-stuff/scaffold.js"), + "codegen", - path.join(process.cwd(), "codegen/elm-stuff/scaffold.js") - // "elm-stuff/scaffold.js" - ); + path.join(process.cwd(), "codegen/elm-stuff/scaffold.js") + ); + } catch (error) { + console.log(restoreColorSafe(error)); + process.exit(1); + } const elmScaffoldProgram = getAt( splitModuleName, From cddf0ca412cefac1588a30cc54bdc46a804078e2 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 08:10:01 -0700 Subject: [PATCH 68/69] Update docs. --- docs.json | 2 +- src/Pages/Generate.elm | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs.json b/docs.json index 48ba8703..446baf15 100644 --- a/docs.json +++ b/docs.json @@ -1 +1 @@ -[{"name":"ApiRoute","comment":" ApiRoute's are defined in `src/Api.elm` and are a way to generate files, like RSS feeds, sitemaps, or any text-based file that you output with an Elm function! You get access\nto a DataSource so you can pull in HTTP data, etc. Because ApiRoutes don't hydrate into Elm apps (like pages in elm-pages do), you can pull in as much data as you want in\nthe DataSource for your ApiRoutes, and it won't effect the payload size. Instead, the size of an ApiRoute is just the content you output for that route.\n\nIn a future release, ApiRoutes may be able to run at request-time in a serverless function, allowing you to use pure Elm code to create dynamic APIs, and even pulling in data from\nDataSources dynamically.\n\n@docs ApiRoute, ApiRouteBuilder, Response\n\n@docs capture, literal, slash, succeed\n\n\n## Pre-Rendering\n\n@docs single, preRender\n\n\n## Server Rendering\n\n@docs preRenderWithFallback, serverRender\n\n\n## Including Head Tags\n\n@docs withGlobalHeadTags\n\n\n## Internals\n\n@docs toJson, getBuildTimeRoutes, getGlobalHeadTagsDataSource\n\n","unions":[],"aliases":[{"name":"ApiRoute","comment":" ","args":["response"],"type":"Internal.ApiRoute.ApiRoute response"},{"name":"ApiRouteBuilder","comment":" ","args":["a","constructor"],"type":"Internal.ApiRoute.ApiRouteBuilder a constructor"},{"name":"Response","comment":" ","args":[],"type":"Json.Encode.Value"}],"values":[{"name":"capture","comment":" ","type":"ApiRoute.ApiRouteBuilder (String.String -> a) constructor -> ApiRoute.ApiRouteBuilder a (String.String -> constructor)"},{"name":"getBuildTimeRoutes","comment":" For internal use by generated code. Not so useful in user-land.\n","type":"ApiRoute.ApiRoute response -> DataSource.DataSource (List.List String.String)"},{"name":"getGlobalHeadTagsDataSource","comment":" ","type":"ApiRoute.ApiRoute response -> Maybe.Maybe (DataSource.DataSource (List.List Head.Tag))"},{"name":"literal","comment":" ","type":"String.String -> ApiRoute.ApiRouteBuilder a constructor -> ApiRoute.ApiRouteBuilder a constructor"},{"name":"preRender","comment":" ","type":"(constructor -> DataSource.DataSource (List.List (List.List String.String))) -> ApiRoute.ApiRouteBuilder (DataSource.DataSource String.String) constructor -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"preRenderWithFallback","comment":" ","type":"(constructor -> DataSource.DataSource (List.List (List.List String.String))) -> ApiRoute.ApiRouteBuilder (DataSource.DataSource (Server.Response.Response Basics.Never Basics.Never)) constructor -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"serverRender","comment":" ","type":"ApiRoute.ApiRouteBuilder (Server.Request.Parser (DataSource.DataSource (Server.Response.Response Basics.Never Basics.Never))) constructor -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"single","comment":" ","type":"ApiRoute.ApiRouteBuilder (DataSource.DataSource String.String) (List.List String.String) -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"slash","comment":" ","type":"ApiRoute.ApiRouteBuilder a constructor -> ApiRoute.ApiRouteBuilder a constructor"},{"name":"succeed","comment":" ","type":"a -> ApiRoute.ApiRouteBuilder a (List.List String.String)"},{"name":"toJson","comment":" Turn the route into a pattern in JSON format. For internal uses.\n","type":"ApiRoute.ApiRoute response -> Json.Encode.Value"},{"name":"withGlobalHeadTags","comment":" Include head tags on every page's HTML.\n","type":"DataSource.DataSource (List.List Head.Tag) -> ApiRoute.ApiRoute response -> ApiRoute.ApiRoute response"}],"binops":[]},{"name":"DataSource","comment":" In an `elm-pages` app, each page can define a value `data` which is a `DataSource` that will be resolved **before** `init` is called. That means it is also available\nwhen the page's HTML is pre-rendered during the build step. You can also access the resolved data in `head` to use it for the page's SEO meta tags.\n\nA `DataSource` lets you pull in data from:\n\n - Local files ([`DataSource.File`](DataSource-File))\n - HTTP requests ([`DataSource.Http`](DataSource-Http))\n - Globs, i.e. listing out local files based on a pattern like `content/*.txt` ([`DataSource.Glob`](DataSource-Glob))\n - Ports, i.e. getting JSON data from running custom NodeJS, similar to a port in a vanilla Elm app except run at build-time in NodeJS, rather than at run-time in the browser ([`DataSource.Port`](DataSource-Port))\n - Hardcoded data (`DataSource.succeed \"Hello!\"`)\n - Or any combination of the above, using `DataSource.map2`, `DataSource.andThen`, or other combining/continuing helpers from this module\n\n\n## Where Does DataSource Data Come From?\n\nData from a `DataSource` is resolved when you load a page in the `elm-pages` dev server, or when you run `elm-pages build`.\n\nBecause `elm-pages` hydrates into a full Elm single-page app, it does need the data in order to initialize the Elm app.\nSo why not just get the data the old-fashioned way, with `elm/http`, for example?\n\nA few reasons:\n\n1. DataSource's allow you to pull in data that you wouldn't normally be able to access from an Elm app, like local files, or listings of files in a folder. Not only that, but the dev server knows to automatically hot reload the data when the files it depends on change, so you can edit the files you used in your DataSource and see the page hot reload as you save!\n2. Because `elm-pages` has a build step, you know that your `DataSource.Http` requests succeeded, your decoders succeeded, your custom DataSource validations succeeded, and everything went smoothly. If something went wrong, you get a build failure and can deal with the issues before the site goes live. That means your users won't see those errors, and as a developer you don't need to handle those error cases in your code! Think of it as \"parse, don't validate\", but for your entire build.\n3. You don't have to worry about an API being down, or hitting it repeatedly. You can build in data and it will end up as JSON files served up with all the other assets of your site. If your CDN (static site host) is down, then the rest of your site is probably down anyway. If your site host is up, then so is all of your `DataSource` data. Also, it will be served up extremely quickly without needing to wait for any database queries to be performed, `andThen` requests to be resolved, etc., because all of that work and waiting was done at build-time!\n4. You can pre-render pages, including the SEO meta tags, with all that rich, well-typed Elm data available! That's something you can't accomplish with a vanilla Elm app, and it's one of the main use cases for elm-pages.\n\n\n## Mental Model\n\nYou can think of a DataSource as a declarative (not imperative) definition of data. It represents where to get the data from, and how to transform it (map, combine with other DataSources, etc.).\n\nEven though an HTTP request is non-deterministic, you should think of it that way as much as possible with a DataSource because elm-pages will only perform a given DataSource.Http request once, and\nit will share the result between any other DataSource.Http requests that have the exact same URL, Method, Body, and Headers.\n\nSo calling a function to increment a counter on a server through an HTTP request would not be a good fit for a `DataSource`. Let's imagine we have an HTTP endpoint that gives these stateful results when called repeatedly:\n\n\n-> Returns 1\n\n-> Returns 2\n\n-> Returns 3\n\nIf we define a `DataSource` that hits that endpoint:\n\n data =\n DataSource.Http.get\n \"https://my-api.example.com/increment-counter\"\n Decode.int\n\nNo matter how many places we use that `DataSource`, its response will be \"locked in\" (let's say the response was `3`, then every page would have the same value of `3` for that request).\n\nSo even though HTTP requests, JavaScript code, etc. can be non-deterministic, a `DataSource` always represents a single snapshot of a resource, and those values will be re-used as if they were a deterministic, declarative resource.\nSo it's best to use that mental model to avoid confusion.\n\n\n## Basics\n\n@docs DataSource\n\n@docs map, succeed, fail\n\n@docs fromResult\n\n\n## Chaining Requests\n\n@docs andThen, resolve, combine\n\n@docs andMap\n\n@docs map2, map3, map4, map5, map6, map7, map8, map9\n\n","unions":[],"aliases":[{"name":"DataSource","comment":" A DataSource represents data that will be gathered at build time. Multiple `DataSource`s can be combined together using the `mapN` functions,\nvery similar to how you can manipulate values with Json Decoders in Elm.\n","args":["value"],"type":"Pages.StaticHttpRequest.RawRequest value"}],"values":[{"name":"andMap","comment":" A helper for combining `DataSource`s in pipelines.\n","type":"DataSource.DataSource a -> DataSource.DataSource (a -> b) -> DataSource.DataSource b"},{"name":"andThen","comment":" Build off of the response from a previous `DataSource` request to build a follow-up request. You can use the data\nfrom the previous response to build up the URL, headers, etc. that you send to the subsequent request.\n\n import DataSource\n import Json.Decode as Decode exposing (Decoder)\n\n licenseData : DataSource String\n licenseData =\n DataSource.Http.get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-pages\")\n (Decode.at [ \"license\", \"url\" ] Decode.string)\n |> DataSource.andThen\n (\\licenseUrl ->\n DataSource.Http.get (Secrets.succeed licenseUrl) (Decode.field \"description\" Decode.string)\n )\n\n","type":"(a -> DataSource.DataSource b) -> DataSource.DataSource a -> DataSource.DataSource b"},{"name":"combine","comment":" Turn a list of `StaticHttp.Request`s into a single one.\n\n import DataSource\n import Json.Decode as Decode exposing (Decoder)\n\n type alias Pokemon =\n { name : String\n , sprite : String\n }\n\n pokemonDetailRequest : StaticHttp.Request (List Pokemon)\n pokemonDetailRequest =\n StaticHttp.get\n (Secrets.succeed \"https://pokeapi.co/api/v2/pokemon/?limit=3\")\n (Decode.field \"results\"\n (Decode.list\n (Decode.map2 Tuple.pair\n (Decode.field \"name\" Decode.string)\n (Decode.field \"url\" Decode.string)\n |> Decode.map\n (\\( name, url ) ->\n StaticHttp.get (Secrets.succeed url)\n (Decode.at\n [ \"sprites\", \"front_default\" ]\n Decode.string\n |> Decode.map (Pokemon name)\n )\n )\n )\n )\n )\n |> StaticHttp.andThen StaticHttp.combine\n\n","type":"List.List (DataSource.DataSource value) -> DataSource.DataSource (List.List value)"},{"name":"fail","comment":" Stop the StaticHttp chain with the given error message. If you reach a `fail` in your request,\nyou will get a build error. Or in the dev server, you will see the error message in an overlay in your browser (and in\nthe terminal).\n","type":"String.String -> DataSource.DataSource a"},{"name":"fromResult","comment":" Turn an Err into a DataSource failure.\n","type":"Result.Result String.String value -> DataSource.DataSource value"},{"name":"map","comment":" Transform a request into an arbitrary value. The same underlying HTTP requests will be performed during the build\nstep, but mapping allows you to change the resulting values by applying functions to the results.\n\nA common use for this is to map your data into your elm-pages view:\n\n import DataSource\n import Json.Decode as Decode exposing (Decoder)\n\n view =\n DataSource.Http.get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-pages\")\n (Decode.field \"stargazers_count\" Decode.int)\n |> DataSource.map\n (\\stars ->\n { view =\n \\model viewForPage ->\n { title = \"Current stars: \" ++ String.fromInt stars\n , body = Html.text <| \"⭐️ \" ++ String.fromInt stars\n , head = []\n }\n }\n )\n\n","type":"(a -> b) -> DataSource.DataSource a -> DataSource.DataSource b"},{"name":"map2","comment":" Like map, but it takes in two `Request`s.\n\n view siteMetadata page =\n StaticHttp.map2\n (\\elmPagesStars elmMarkdownStars ->\n { view =\n \\model viewForPage ->\n { title = \"Repo Stargazers\"\n , body = starsView elmPagesStars elmMarkdownStars\n }\n , head = head elmPagesStars elmMarkdownStars\n }\n )\n (get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-pages\")\n (Decode.field \"stargazers_count\" Decode.int)\n )\n (get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-markdown\")\n (Decode.field \"stargazers_count\" Decode.int)\n )\n\n","type":"(a -> b -> c) -> DataSource.DataSource a -> DataSource.DataSource b -> DataSource.DataSource c"},{"name":"map3","comment":" ","type":"(value1 -> value2 -> value3 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource valueCombined"},{"name":"map4","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource valueCombined"},{"name":"map5","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource valueCombined"},{"name":"map6","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource valueCombined"},{"name":"map7","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource value7 -> DataSource.DataSource valueCombined"},{"name":"map8","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource value7 -> DataSource.DataSource value8 -> DataSource.DataSource valueCombined"},{"name":"map9","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> value9 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource value7 -> DataSource.DataSource value8 -> DataSource.DataSource value9 -> DataSource.DataSource valueCombined"},{"name":"resolve","comment":" Helper to remove an inner layer of Request wrapping.\n","type":"DataSource.DataSource (List.List (DataSource.DataSource value)) -> DataSource.DataSource (List.List value)"},{"name":"succeed","comment":" This is useful for prototyping with some hardcoded data, or for having a view that doesn't have any StaticHttp data.\n\n import DataSource\n\n view :\n List ( PagePath, Metadata )\n ->\n { path : PagePath\n , frontmatter : Metadata\n }\n ->\n StaticHttp.Request\n { view : Model -> View -> { title : String, body : Html Msg }\n , head : List (Head.Tag Pages.PathKey)\n }\n view siteMetadata page =\n StaticHttp.succeed\n { view =\n \\model viewForPage ->\n mainView model viewForPage\n , head = head page.frontmatter\n }\n\n","type":"a -> DataSource.DataSource a"}],"binops":[]},{"name":"DataSource.Env","comment":"\n\n@docs get, expect\n\n","unions":[],"aliases":[],"values":[{"name":"expect","comment":" ","type":"String.String -> DataSource.DataSource String.String"},{"name":"get","comment":" ","type":"String.String -> DataSource.DataSource (Maybe.Maybe String.String)"}],"binops":[]},{"name":"DataSource.File","comment":" This module lets you read files from the local filesystem as a [`DataSource`](DataSource#DataSource).\nFile paths are relative to the root of your `elm-pages` project (next to the `elm.json` file and `src/` directory).\n\n\n## Files With Frontmatter\n\nFrontmatter is a convention used to keep metadata at the top of a file between `---`'s.\n\nFor example, you might have a file called `blog/hello-world.md` with this content:\n\n```markdown\n---\ntitle: Hello, World!\ntags: elm\n---\nHey there! This is my first post :)\n```\n\nThe frontmatter is in the [YAML format](https://en.wikipedia.org/wiki/YAML) here. You can also use JSON in your elm-pages frontmatter.\n\n```markdown\n---\n{\"title\": \"Hello, World!\", \"tags\": \"elm\"}\n---\nHey there! This is my first post :)\n```\n\nWhether it's YAML or JSON, you use an `Decode` to decode your frontmatter, so it feels just like using\nplain old JSON in Elm.\n\n@docs bodyWithFrontmatter, bodyWithoutFrontmatter, onlyFrontmatter\n\n\n## Reading Files Without Frontmatter\n\n@docs jsonFile, rawFile\n\n","unions":[],"aliases":[],"values":[{"name":"bodyWithFrontmatter","comment":"\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n\n blogPost : DataSource BlogPostMetadata\n blogPost =\n File.bodyWithFrontmatter blogPostDecoder\n \"blog/hello-world.md\"\n\n type alias BlogPostMetadata =\n { body : String\n , title : String\n , tags : List String\n }\n\n blogPostDecoder : String -> Decoder BlogPostMetadata\n blogPostDecoder body =\n Decode.map2 (BlogPostMetadata body)\n (Decode.field \"title\" Decode.string)\n (Decode.field \"tags\" tagsDecoder)\n\n tagsDecoder : Decoder (List String)\n tagsDecoder =\n Decode.map (String.split \" \")\n Decode.string\n\nThis will give us a DataSource that results in the following value:\n\n value =\n { body = \"Hey there! This is my first post :)\"\n , title = \"Hello, World!\"\n , tags = [ \"elm\" ]\n }\n\nIt's common to parse the body with a markdown parser or other format.\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n import Html exposing (Html)\n\n example :\n DataSource\n { title : String\n , body : List (Html msg)\n }\n example =\n File.bodyWithFrontmatter\n (\\markdownString ->\n Decode.map2\n (\\title renderedMarkdown ->\n { title = title\n , body = renderedMarkdown\n }\n )\n (Decode.field \"title\" Decode.string)\n (markdownString\n |> markdownToView\n |> Decode.fromResult\n )\n )\n \"foo.md\"\n\n markdownToView :\n String\n -> Result String (List (Html msg))\n markdownToView markdownString =\n markdownString\n |> Markdown.Parser.parse\n |> Result.mapError (\\_ -> \"Markdown error.\")\n |> Result.andThen\n (\\blocks ->\n Markdown.Renderer.render\n Markdown.Renderer.defaultHtmlRenderer\n blocks\n )\n\n","type":"(String.String -> Json.Decode.Decoder frontmatter) -> String.String -> DataSource.DataSource frontmatter"},{"name":"bodyWithoutFrontmatter","comment":" Same as `bodyWithFrontmatter` except it doesn't include the frontmatter.\n\nFor example, if you have a file called `blog/hello-world.md` with\n\n```markdown\n---\ntitle: Hello, World!\ntags: elm\n---\nHey there! This is my first post :)\n```\n\n import DataSource exposing (DataSource)\n\n data : DataSource String\n data =\n bodyWithoutFrontmatter \"blog/hello-world.md\"\n\nThen data will yield the value `\"Hey there! This is my first post :)\"`.\n\n","type":"String.String -> DataSource.DataSource String.String"},{"name":"jsonFile","comment":" Read a file as JSON.\n\nThe Decode will strip off any unused JSON data.\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n\n sourceDirectories : DataSource (List String)\n sourceDirectories =\n File.jsonFile\n (Decode.field\n \"source-directories\"\n (Decode.list Decode.string)\n )\n \"elm.json\"\n\n","type":"Json.Decode.Decoder a -> String.String -> DataSource.DataSource a"},{"name":"onlyFrontmatter","comment":" Same as `bodyWithFrontmatter` except it doesn't include the body.\n\nThis is often useful when you're aggregating data, for example getting a listing of blog posts and need to extract\njust the metadata.\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n\n blogPost : DataSource BlogPostMetadata\n blogPost =\n File.onlyFrontmatter\n blogPostDecoder\n \"blog/hello-world.md\"\n\n type alias BlogPostMetadata =\n { title : String\n , tags : List String\n }\n\n blogPostDecoder : Decoder BlogPostMetadata\n blogPostDecoder =\n Decode.map2 BlogPostMetadata\n (Decode.field \"title\" Decode.string)\n (Decode.field \"tags\" (Decode.list Decode.string))\n\nIf you wanted to use this to get this metadata for all blog posts in a folder, you could use\nthe [`DataSource`](DataSource) API along with [`DataSource.Glob`](DataSource.Glob).\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n\n blogPostFiles : DataSource (List String)\n blogPostFiles =\n Glob.succeed identity\n |> Glob.captureFilePath\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.match Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\n allMetadata : DataSource (List BlogPostMetadata)\n allMetadata =\n blogPostFiles\n |> DataSource.map\n (List.map\n (File.onlyFrontmatter\n blogPostDecoder\n )\n )\n |> DataSource.resolve\n\n","type":"Json.Decode.Decoder frontmatter -> String.String -> DataSource.DataSource frontmatter"},{"name":"rawFile","comment":" Get the raw file content. Unlike the frontmatter helpers in this module, this function will not strip off frontmatter if there is any.\n\nThis is the function you want if you are reading in a file directly. For example, if you read in a CSV file, a raw text file, or any other file that doesn't\nhave frontmatter.\n\nThere's a special function for reading in JSON files, [`jsonFile`](#jsonFile). If you're reading a JSON file then be sure to\nuse `jsonFile` to get the benefits of the `Decode` here.\n\nYou could read a file called `hello.txt` in your root project directory like this:\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n\n elmJsonFile : DataSource String\n elmJsonFile =\n File.rawFile \"hello.txt\"\n\n","type":"String.String -> DataSource.DataSource String.String"}],"binops":[]},{"name":"DataSource.Glob","comment":"\n\n@docs Glob\n\nThis 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\nfor ways you can combine and map `DataSource`s.\n\nA 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`\n, then you could use that glob pattern in most shells to refer to each of those files.\n\nWith the `DataSource.Glob` API, you could get all of those files like so:\n\n import DataSource exposing (DataSource)\n\n blogPostsGlob : DataSource (List String)\n blogPostsGlob =\n Glob.succeed (\\slug -> slug)\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nLet's say you have these files locally:\n\n```shell\n- elm.json\n- src/\n- content/\n - blog/\n - first-post.md\n - second-post.md\n```\n\nWe would end up with a `DataSource` like this:\n\n DataSource.succeed [ \"first-post\", \"second-post\" ]\n\nOf course, if you add or remove matching files, the DataSource will get those new files (unlike `DataSource.succeed`). That's why we have Glob!\n\nYou can even see the `elm-pages dev` server will automatically flow through any added/removed matching files with its hot module reloading.\n\nBut why did we get `\"first-post\"` instead of a full file path, like `\"content/blog/first-post.md\"`? That's the difference between\n`capture` and `match`.\n\n\n## Capture and Match\n\nThere are two functions for building up a Glob pattern: `capture` and `match`.\n\n`capture` and `match` both build up a `Glob` pattern that will match 0 or more files on your local file system.\nThere will be one argument for every `capture` in your pipeline, whereas `match` does not apply any arguments.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n blogPostsGlob : DataSource (List String)\n blogPostsGlob =\n Glob.succeed (\\slug -> slug)\n -- no argument from this, but we will only\n -- match files that begin with `content/blog/`\n |> Glob.match (Glob.literal \"content/blog/\")\n -- we get the value of the `wildcard`\n -- as the slug argument\n |> Glob.capture Glob.wildcard\n -- no argument from this, but we will only\n -- match files that end with `.md`\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nSo to understand _which_ files will match, you can ignore whether you are using `capture` or `match` and just read\nthe patterns you're using in order to understand what will match. To understand what Elm data type you will get\n_for each matching file_, you need to see which parts are being captured and how each of those captured values are being\nused in the function you use in `Glob.succeed`.\n\n@docs capture, match\n\n`capture` is a lot like building up a JSON decoder with a pipeline.\n\nLet's try our blogPostsGlob from before, but change every `match` to `capture`.\n\n import DataSource exposing (DataSource)\n\n blogPostsGlob :\n DataSource\n (List\n { filePath : String\n , slug : String\n }\n )\n blogPostsGlob =\n Glob.succeed\n (\\capture1 capture2 capture3 ->\n { filePath = capture1 ++ capture2 ++ capture3\n , slug = capture2\n }\n )\n |> Glob.capture (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.capture (Glob.literal \".md\")\n |> Glob.toDataSource\n\nNotice that we now need 3 arguments at the start of our pipeline instead of 1. That's because\nwe apply 1 more argument every time we do a `Glob.capture`, much like `Json.Decode.Pipeline.required`, or other pipeline APIs.\n\nNow we actually have the full file path of our files. But having that slug (like `first-post`) is also very helpful sometimes, so\nwe 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:\n\n DataSource.succeed\n [ { filePath = \"content/blog/first-post.md\"\n , slug = \"first-post\"\n }\n , { filePath = \"content/blog/second-post.md\"\n , slug = \"second-post\"\n }\n ]\n\nHaving the full file path lets us read in files. But concatenating it manually is tedious\nand error prone. That's what the [`captureFilePath`](#captureFilePath) helper is for.\n\n\n## Reading matching files\n\n@docs captureFilePath\n\nIn many cases you will want to take the matching files from a `Glob` and then read the body or frontmatter from matching files.\n\n\n## Reading Metadata for each Glob Match\n\nFor example, if we had files like this:\n\n```markdown\n---\ntitle: My First Post\n---\nThis is my first post!\n```\n\nThen we could read that title for our blog post list page using our `blogPosts` `DataSource` that we defined above.\n\n import DataSource.File\n import Json.Decode as Decode exposing (Decoder)\n\n titles : DataSource (List BlogPost)\n titles =\n blogPosts\n |> DataSource.map\n (List.map\n (\\blogPost ->\n DataSource.File.request\n blogPost.filePath\n (DataSource.File.frontmatter blogFrontmatterDecoder)\n )\n )\n |> DataSource.resolve\n\n type alias BlogPost =\n { title : String }\n\n blogFrontmatterDecoder : Decoder BlogPost\n blogFrontmatterDecoder =\n Decode.map BlogPost\n (Decode.field \"title\" Decode.string)\n\nThat will give us\n\n DataSource.succeed\n [ { title = \"My First Post\" }\n , { title = \"My Second Post\" }\n ]\n\n\n## Capturing Patterns\n\n@docs wildcard, recursiveWildcard\n\n\n## Capturing Specific Characters\n\n@docs int, digits\n\n\n## Matching a Specific Number of Files\n\n@docs expectUniqueMatch, expectUniqueMatchFromList\n\n\n## Glob Patterns\n\n@docs literal\n\n@docs map, succeed\n\n@docs oneOf\n\n@docs zeroOrMore, atLeastOne\n\n\n## Getting Glob Data from a DataSource\n\n@docs toDataSource\n\n\n### With Custom Options\n\n@docs toDataSourceWithOptions\n\n@docs defaultOptions, Options, Include\n\n","unions":[{"name":"Include","comment":" \n\n\n\n","args":[],"cases":[["OnlyFiles",[]],["OnlyFolders",[]],["FilesAndFolders",[]]]}],"aliases":[{"name":"Glob","comment":" A pattern to match local files and capture parts of the path into a nice Elm data type.\n","args":["a"],"type":"DataSource.Internal.Glob.Glob a"},{"name":"Options","comment":" Custom options you can pass in to run the glob with [`toDataSourceWithOptions`](#toDataSourceWithOptions).\n\n { includeDotFiles = Bool -- https://github.com/mrmlnc/fast-glob#dot\n , include = Include -- return results that are `OnlyFiles`, `OnlyFolders`, or both `FilesAndFolders` (default is `OnlyFiles`)\n , followSymbolicLinks = Bool -- https://github.com/mrmlnc/fast-glob#followsymboliclinks\n , caseSensitiveMatch = Bool -- https://github.com/mrmlnc/fast-glob#casesensitivematch\n , gitignore = Bool -- https://www.npmjs.com/package/globby#gitignore\n , maxDepth = Maybe Int -- https://github.com/mrmlnc/fast-glob#deep\n }\n\n","args":[],"type":"{ includeDotFiles : Basics.Bool, include : DataSource.Glob.Include, followSymbolicLinks : Basics.Bool, caseSensitiveMatch : Basics.Bool, gitignore : Basics.Bool, maxDepth : Maybe.Maybe Basics.Int }"}],"values":[{"name":"atLeastOne","comment":" ","type":"( ( String.String, a ), List.List ( String.String, a ) ) -> DataSource.Glob.Glob ( a, List.List a )"},{"name":"capture","comment":" Adds on to the glob pattern, and captures it in the resulting Elm match value. That means this both changes which\nfiles will match, and gives you the sub-match as Elm data for each matching file.\n\nExactly the same as `match` except it also captures the matched sub-pattern.\n\n type alias ArchivesArticle =\n { year : String\n , month : String\n , day : String\n , slug : String\n }\n\n archives : DataSource ArchivesArticle\n archives =\n Glob.succeed ArchivesArticle\n |> Glob.match (Glob.literal \"archive/\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nThe file `archive/1977/06/10/apple-2-released.md` will give us this match:\n\n matches : List ArchivesArticle\n matches =\n DataSource.succeed\n [ { year = 1977\n , month = 6\n , day = 10\n , slug = \"apple-2-released\"\n }\n ]\n\nWhen possible, it's best to grab data and turn it into structured Elm data when you have it. That way,\nyou don't end up with duplicate validation logic and data normalization, and your code will be more robust.\n\nIf you only care about getting the full matched file paths, you can use `match`. `capture` is very useful because\nyou can pick apart structured data as you build up your glob pattern. This follows the principle of\n[Parse, Don't Validate](https://elm-radio.com/episode/parse-dont-validate/).\n\n","type":"DataSource.Glob.Glob a -> DataSource.Glob.Glob (a -> value) -> DataSource.Glob.Glob value"},{"name":"captureFilePath","comment":"\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n blogPosts :\n DataSource\n (List\n { filePath : String\n , slug : String\n }\n )\n blogPosts =\n Glob.succeed\n (\\filePath slug ->\n { filePath = filePath\n , slug = slug\n }\n )\n |> Glob.captureFilePath\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nThis function does not change which files will or will not match. It just gives you the full matching\nfile path in your `Glob` pipeline.\n\nWhenever 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.\n\n","type":"DataSource.Glob.Glob (String.String -> value) -> DataSource.Glob.Glob value"},{"name":"defaultOptions","comment":" The default options used in [`toDataSource`](#toDataSource). To use a custom set of options, use [`toDataSourceWithOptions`](#toDataSourceWithOptions).\n","type":"DataSource.Glob.Options"},{"name":"digits","comment":" This is similar to [`wildcard`](#wildcard), but it will only match 1 or more digits (i.e. `[0-9]+`).\n\nSee [`int`](#int) for a convenience function to get an Int value instead of a String of digits.\n\n","type":"DataSource.Glob.Glob String.String"},{"name":"expectUniqueMatch","comment":" Sometimes you want to make sure there is a unique file matching a particular pattern.\nThis is a simple helper that will give you a `DataSource` error if there isn't exactly 1 matching file.\nIf there is exactly 1, then you successfully get back that single match.\n\nFor example, maybe you can have\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n findBlogBySlug : String -> DataSource String\n findBlogBySlug slug =\n Glob.succeed identity\n |> Glob.captureFilePath\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.capture (Glob.literal slug)\n |> Glob.match\n (Glob.oneOf\n ( ( \"\", () )\n , [ ( \"/index\", () ) ]\n )\n )\n |> Glob.match (Glob.literal \".md\")\n |> Glob.expectUniqueMatch\n\nIf we used `findBlogBySlug \"first-post\"` with these files:\n\n```markdown\n- blog/\n - first-post/\n - index.md\n```\n\nThis would give us:\n\n results : DataSource String\n results =\n DataSource.succeed \"blog/first-post/index.md\"\n\nIf we used `findBlogBySlug \"first-post\"` with these files:\n\n```markdown\n- blog/\n - first-post.md\n - first-post/\n - index.md\n```\n\nThen we will get a `DataSource` error saying `More than one file matched.` Keep in mind that `DataSource` failures\nin build-time routes will cause a build failure, giving you the opportunity to fix the problem before users see the issue,\nso it's ideal to make this kind of assertion rather than having fallback behavior that could silently cover up\nissues (like if we had instead ignored the case where there are two or more matching blog post files).\n\n","type":"DataSource.Glob.Glob a -> DataSource.DataSource a"},{"name":"expectUniqueMatchFromList","comment":" ","type":"List.List (DataSource.Glob.Glob a) -> DataSource.DataSource a"},{"name":"int","comment":" Same as [`digits`](#digits), but it safely turns the digits String into an `Int`.\n\nLeading 0's are ignored.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n slides : DataSource (List Int)\n slides =\n Glob.succeed identity\n |> Glob.match (Glob.literal \"slide-\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nWith files\n\n```shell\n- slide-no-match.md\n- slide-.md\n- slide-1.md\n- slide-01.md\n- slide-2.md\n- slide-03.md\n- slide-4.md\n- slide-05.md\n- slide-06.md\n- slide-007.md\n- slide-08.md\n- slide-09.md\n- slide-10.md\n- slide-11.md\n```\n\nYields\n\n matches : DataSource (List Int)\n matches =\n DataSource.succeed\n [ 1\n , 1\n , 2\n , 3\n , 4\n , 5\n , 6\n , 7\n , 8\n , 9\n , 10\n , 11\n ]\n\nNote that neither `slide-no-match.md` nor `slide-.md` match.\nAnd both `slide-1.md` and `slide-01.md` match and turn into `1`.\n\n","type":"DataSource.Glob.Glob Basics.Int"},{"name":"literal","comment":" Match a literal part of a path. Can include `/`s.\n\nSome common uses include\n\n - The leading part of a pattern, to say \"starts with `content/blog/`\"\n - The ending part of a pattern, to say \"ends with `.md`\"\n - In-between wildcards, to say \"these dynamic parts are separated by `/`\"\n\n```elm\nimport DataSource exposing (DataSource)\nimport DataSource.Glob as Glob\n\nblogPosts =\n Glob.succeed\n (\\section slug ->\n { section = section, slug = slug }\n )\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n```\n\n","type":"String.String -> DataSource.Glob.Glob String.String"},{"name":"map","comment":" A `Glob` can be mapped. This can be useful for transforming a sub-match in-place.\n\nFor example, if you wanted to take the slugs for a blog post and make sure they are normalized to be all lowercase, you\ncould use\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n blogPostsGlob : DataSource (List String)\n blogPostsGlob =\n Glob.succeed (\\slug -> slug)\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture (Glob.wildcard |> Glob.map String.toLower)\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nIf you want to validate file formats, you can combine that with some `DataSource` helpers to turn a `Glob (Result String value)` into\na `DataSource (List value)`.\n\nFor example, you could take a date and parse it.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n example : DataSource (List ( String, String ))\n example =\n Glob.succeed\n (\\dateResult slug ->\n dateResult\n |> Result.map (\\okDate -> ( okDate, slug ))\n )\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.capture (Glob.recursiveWildcard |> Glob.map expectDateFormat)\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n |> DataSource.map (List.map DataSource.fromResult)\n |> DataSource.resolve\n\n expectDateFormat : List String -> Result String String\n expectDateFormat dateParts =\n case dateParts of\n [ year, month, date ] ->\n Ok (String.join \"-\" [ year, month, date ])\n\n _ ->\n Err \"Unexpected date format, expected yyyy/mm/dd folder structure.\"\n\n","type":"(a -> b) -> DataSource.Glob.Glob a -> DataSource.Glob.Glob b"},{"name":"match","comment":" Adds on to the glob pattern, but does not capture it in the resulting Elm match value. That means this changes which\nfiles will match, but does not change the Elm data type you get for each matching file.\n\nExactly the same as `capture` except it doesn't capture the matched sub-pattern.\n\n","type":"DataSource.Glob.Glob a -> DataSource.Glob.Glob value -> DataSource.Glob.Glob value"},{"name":"oneOf","comment":"\n\n import DataSource.Glob as Glob\n\n type Extension\n = Json\n | Yml\n\n type alias DataFile =\n { name : String\n , extension : String\n }\n\n dataFiles : DataSource (List DataFile)\n dataFiles =\n Glob.succeed DataFile\n |> Glob.match (Glob.literal \"my-data/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".\")\n |> Glob.capture\n (Glob.oneOf\n ( ( \"yml\", Yml )\n , [ ( \"json\", Json )\n ]\n )\n )\n\nIf we have the following files\n\n```shell\n- my-data/\n - authors.yml\n - events.json\n```\n\nThat gives us\n\n results : DataSource (List DataFile)\n results =\n DataSource.succeed\n [ { name = \"authors\"\n , extension = Yml\n }\n , { name = \"events\"\n , extension = Json\n }\n ]\n\nYou could also match an optional file path segment using `oneOf`.\n\n rootFilesMd : DataSource (List String)\n rootFilesMd =\n Glob.succeed (\\slug -> slug)\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match\n (Glob.oneOf\n ( ( \"\", () )\n , [ ( \"/index\", () ) ]\n )\n )\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nWith these files:\n\n```markdown\n- blog/\n - first-post.md\n - second-post/\n - index.md\n```\n\nThis would give us:\n\n results : DataSource (List String)\n results =\n DataSource.succeed\n [ \"first-post\"\n , \"second-post\"\n ]\n\n","type":"( ( String.String, a ), List.List ( String.String, a ) ) -> DataSource.Glob.Glob a"},{"name":"recursiveWildcard","comment":" Matches any number of characters, including `/`, as long as it's the only thing in a path part.\n\nIn contrast, `wildcard` will never match `/`, so it only matches within a single path part.\n\nThis is the elm-pages equivalent of `**/*.txt` in standard shell syntax:\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n example : DataSource (List ( List String, String ))\n example =\n Glob.succeed Tuple.pair\n |> Glob.match (Glob.literal \"articles/\")\n |> Glob.capture Glob.recursiveWildcard\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".txt\")\n |> Glob.toDataSource\n\nWith these files:\n\n```shell\n- articles/\n - google-io-2021-recap.txt\n - archive/\n - 1977/\n - 06/\n - 10/\n - apple-2-announced.txt\n```\n\nWe would get the following matches:\n\n matches : DataSource (List ( List String, String ))\n matches =\n DataSource.succeed\n [ ( [ \"archive\", \"1977\", \"06\", \"10\" ], \"apple-2-announced\" )\n , ( [], \"google-io-2021-recap\" )\n ]\n\nNote that the recursive wildcard conveniently gives us a `List String`, where\neach String is a path part with no slashes (like `archive`).\n\nAnd also note that it matches 0 path parts into an empty list.\n\nIf we didn't include the `wildcard` after the `recursiveWildcard`, then we would only get\na single level of matches because it is followed by a file extension.\n\n example : DataSource (List String)\n example =\n Glob.succeed identity\n |> Glob.match (Glob.literal \"articles/\")\n |> Glob.capture Glob.recursiveWildcard\n |> Glob.match (Glob.literal \".txt\")\n\n matches : DataSource (List String)\n matches =\n DataSource.succeed\n [ \"google-io-2021-recap\"\n ]\n\nThis is usually not what is intended. Using `recursiveWildcard` is usually followed by a `wildcard` for this reason.\n\n","type":"DataSource.Glob.Glob (List.List String.String)"},{"name":"succeed","comment":" `succeed` is how you start a pipeline for a `Glob`. You will need one argument for each `capture` in your `Glob`.\n","type":"constructor -> DataSource.Glob.Glob constructor"},{"name":"toDataSource","comment":" In order to get match data from your glob, turn it into a `DataSource` with this function.\n","type":"DataSource.Glob.Glob a -> DataSource.DataSource (List.List a)"},{"name":"toDataSourceWithOptions","comment":" Same as toDataSource, but lets you set custom glob options. For example, to list folders instead of files,\n\n import DataSource.Glob as Glob exposing (OnlyFolders, defaultOptions)\n\n matchingFiles : Glob a -> DataSource (List a)\n matchingFiles glob =\n glob\n |> Glob.toDataSourceWithOptions { defaultOptions | include = OnlyFolders }\n\n","type":"DataSource.Glob.Options -> DataSource.Glob.Glob a -> DataSource.DataSource (List.List a)"},{"name":"wildcard","comment":" Matches anything except for a `/` in a file path. You may be familiar with this syntax from shells like bash\nwhere you can run commands like `rm client/*.js` to remove all `.js` files in the `client` directory.\n\nJust like a `*` glob pattern in bash, this `Glob.wildcard` function will only match within a path part. If you need to\nmatch 0 or more path parts like, see `recursiveWildcard`.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n type alias BlogPost =\n { year : String\n , month : String\n , day : String\n , slug : String\n }\n\n example : DataSource (List BlogPost)\n example =\n Glob.succeed BlogPost\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.match Glob.wildcard\n |> Glob.match (Glob.literal \"-\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \"-\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\n```shell\n\n- blog/\n - 2021-05-27/\n - first-post.md\n```\n\nThat will match to:\n\n results : DataSource (List BlogPost)\n results =\n DataSource.succeed\n [ { year = \"2021\"\n , month = \"05\"\n , day = \"27\"\n , slug = \"first-post\"\n }\n ]\n\nNote that we can \"destructure\" the date part of this file path in the format `yyyy-mm-dd`. The `wildcard` matches\nwill match _within_ a path part (think between the slashes of a file path). `recursiveWildcard` can match across path parts.\n\n","type":"DataSource.Glob.Glob String.String"},{"name":"zeroOrMore","comment":" ","type":"List.List String.String -> DataSource.Glob.Glob (Maybe.Maybe String.String)"}],"binops":[]},{"name":"DataSource.Http","comment":" `DataSource.Http` requests are an alternative to doing Elm HTTP requests the traditional way using the `elm/http` package.\n\nThe key differences are:\n\n - `DataSource.Http.Request`s are performed once at build time (`Http.Request`s are performed at runtime, at whenever point you perform them)\n - `DataSource.Http.Request`s have a built-in `DataSource.andThen` that allows you to perform follow-up requests without using tasks\n\n\n## Scenarios where DataSource.Http is a good fit\n\nIf you need data that is refreshed often you may want to do a traditional HTTP request with the `elm/http` package.\nThe kinds of situations that are served well by static HTTP are with data that updates moderately frequently or infrequently (or never).\nA common pattern is to trigger a new build when data changes. Many JAMstack services\nallow you to send a WebHook to your host (for example, Netlify is a good static file host that supports triggering builds with webhooks). So\nyou may want to have your site rebuild everytime your calendar feed has an event added, or whenever a page or article is added\nor updated on a CMS service like Contentful.\n\nIn scenarios like this, you can serve data that is just as up-to-date as it would be using `elm/http`, but you get the performance\ngains of using `DataSource.Http.Request`s as well as the simplicity and robustness that comes with it. Read more about these benefits\nin [this article introducing DataSource.Http requests and some concepts around it](https://elm-pages.com/blog/static-http).\n\n\n## Scenarios where DataSource.Http is not a good fit\n\n - Data that is specific to the logged-in user\n - Data that needs to be the very latest and changes often (for example, sports scores)\n\n@docs RequestDetails\n@docs get, request\n\n\n## Decoding Request Body\n\n@docs Expect, expectString, expectJson, expectBytes, expectWhatever\n\n\n## Expecting Responses\n\n@docs Response, Metadata, Error\n\n@docs expectStringResponse, expectBytesResponse\n\n\n## Building a DataSource.Http Request Body\n\nThe way you build a body is analogous to the `elm/http` package. Currently, only `emptyBody` and\n`stringBody` are supported. If you have a use case that calls for a different body type, please open a Github issue\nand describe your use case!\n\n@docs Body, emptyBody, stringBody, jsonBody\n\n\n## Uncached Requests\n\n@docs uncachedRequest\n\n","unions":[{"name":"Error","comment":" ","args":[],"cases":[["BadUrl",["String.String"]],["Timeout",[]],["NetworkError",[]],["BadStatus",["DataSource.Http.Metadata","String.String"]],["BadBody",["String.String"]]]},{"name":"Expect","comment":" Analogous to the `Expect` type in the `elm/http` package. This represents how you will process the data that comes\nback in your DataSource.Http request.\n\nYou can derive `ExpectJson` from `ExpectString`. Or you could build your own helper to process the String\nas XML, for example, or give an `elm-pages` build error if the response can't be parsed as XML.\n\n","args":["value"],"cases":[]},{"name":"Response","comment":" ","args":["body"],"cases":[["BadUrl_",["String.String"]],["Timeout_",[]],["NetworkError_",[]],["BadStatus_",["DataSource.Http.Metadata","body"]],["GoodStatus_",["DataSource.Http.Metadata","body"]]]}],"aliases":[{"name":"Body","comment":" A body for a DataSource.Http request.\n","args":[],"type":"Pages.Internal.StaticHttpBody.Body"},{"name":"Metadata","comment":" ","args":[],"type":"{ url : String.String, statusCode : Basics.Int, statusText : String.String, headers : Dict.Dict String.String String.String }"},{"name":"RequestDetails","comment":" The full details to perform a DataSource.Http request.\n","args":[],"type":"{ url : String.String, method : String.String, headers : List.List ( String.String, String.String ), body : DataSource.Http.Body }"}],"values":[{"name":"emptyBody","comment":" Build an empty body for a DataSource.Http request. See [elm/http's `Http.emptyBody`](https://package.elm-lang.org/packages/elm/http/latest/Http#emptyBody).\n","type":"DataSource.Http.Body"},{"name":"expectBytes","comment":" ","type":"Bytes.Decode.Decoder value -> DataSource.Http.Expect value"},{"name":"expectBytesResponse","comment":" ","type":"DataSource.Http.Expect (DataSource.Http.Response Bytes.Bytes)"},{"name":"expectJson","comment":" Handle the incoming response as JSON and don't optimize the asset and strip out unused values.\nBe sure to use the `DataSource.Http.request` function if you want an optimized request that\nstrips out unused JSON to optimize your asset size. This function makes sense to use for things like a GraphQL request\nwhere the JSON payload is already trimmed down to the data you explicitly requested.\n\nIf the function you pass to `expectString` yields an `Err`, then you will get a build error that will\nfail your `elm-pages` build and print out the String from the `Err`.\n\n","type":"Json.Decode.Decoder value -> DataSource.Http.Expect value"},{"name":"expectString","comment":" Gives the HTTP response body as a raw String.\n\n import DataSource exposing (DataSource)\n import DataSource.Http\n\n request : DataSource String\n request =\n DataSource.Http.request\n { url = \"https://example.com/file.txt\"\n , method = \"GET\"\n , headers = []\n , body = DataSource.Http.emptyBody\n }\n DataSource.Http.expectString\n\n","type":"DataSource.Http.Expect String.String"},{"name":"expectStringResponse","comment":" ","type":"DataSource.Http.Expect (DataSource.Http.Response String.String)"},{"name":"expectWhatever","comment":" ","type":"value -> DataSource.Http.Expect value"},{"name":"get","comment":" A simplified helper around [`DataSource.Http.request`](#request), which builds up a DataSource.Http GET request.\n\n import DataSource\n import DataSource.Http\n import Json.Decode as Decode exposing (Decoder)\n\n getRequest : DataSource Int\n getRequest =\n DataSource.Http.get\n \"https://api.github.com/repos/dillonkearns/elm-pages\"\n (Decode.field \"stargazers_count\" Decode.int)\n\n","type":"String.String -> Json.Decode.Decoder a -> DataSource.DataSource a"},{"name":"jsonBody","comment":" Builds a JSON body for a DataSource.Http request. See [elm/http's `Http.jsonBody`](https://package.elm-lang.org/packages/elm/http/latest/Http#jsonBody).\n","type":"Json.Encode.Value -> DataSource.Http.Body"},{"name":"request","comment":" ","type":"DataSource.Http.RequestDetails -> DataSource.Http.Expect a -> DataSource.DataSource a"},{"name":"stringBody","comment":" Builds a string body for a DataSource.Http request. See [elm/http's `Http.stringBody`](https://package.elm-lang.org/packages/elm/http/latest/Http#stringBody).\n\nNote from the `elm/http` docs:\n\n> The first argument is a [MIME type](https://en.wikipedia.org/wiki/Media_type) of the body. Some servers are strict about this!\n\n","type":"String.String -> String.String -> DataSource.Http.Body"},{"name":"uncachedRequest","comment":" ","type":"DataSource.Http.RequestDetails -> DataSource.Http.Expect a -> DataSource.DataSource a"}],"binops":[]},{"name":"DataSource.Port","comment":"\n\n@docs get\n\n","unions":[],"aliases":[],"values":[{"name":"get","comment":" In a vanilla Elm application, ports let you either send or receive JSON data between your Elm application and the JavaScript context in the user's browser at runtime.\n\nWith `DataSource.Port`, you send and receive JSON to JavaScript running in NodeJS during build-time. This means that you can call shell scripts, or run NPM packages that are installed, or anything else you could do with NodeJS.\n\nA `DataSource.Port` will call an async JavaScript function with the given name. The function receives the input JSON value, and the Decoder is used to decode the return value of the async function.\n\nHere is the Elm code and corresponding JavaScript definition for getting an environment variable (or a build error if it isn't found).\n\n import DataSource exposing (DataSource)\n import DataSource.Port\n import Json.Encode\n import OptimizedDecoder as Decode\n\n data : DataSource String\n data =\n DataSource.Port.get \"environmentVariable\"\n (Json.Encode.string \"EDITOR\")\n Decode.string\n\n -- will resolve to \"VIM\" if you run `EDITOR=vim elm-pages dev`\n\n```javascript\nconst kleur = require(\"kleur\");\n\n\nmodule.exports =\n /**\n * @param { unknown } fromElm\n * @returns { Promise }\n */\n {\n environmentVariable: async function (name) {\n const result = process.env[name];\n if (result) {\n return result;\n } else {\n throw `No environment variable called ${kleur\n .yellow()\n .underline(name)}\\n\\nAvailable:\\n\\n${Object.keys(process.env).join(\n \"\\n\"\n )}`;\n }\n },\n }\n```\n\n\n## Error Handling\n\n`port-data-source.js`\n\nAny time you throw an exception from a DataSource.Port definition, it will result in a build error in your `elm-pages build` or dev server. In the example above, if the environment variable\nis not found it will result in a build failure. Notice that the NPM package `kleur` is being used in this example to add color to the output for that build error. You can use any tool you\nprefer to add ANSI color codes within the error string in an exception and it will show up with color output in the build output and dev server.\n\n\n## Performance\n\nAs with any JavaScript or NodeJS code, avoid doing blocking IO operations. For example, avoid using `fs.readFileSync`, because blocking IO can slow down your elm-pages builds and dev server.\n\n","type":"String.String -> Json.Encode.Value -> Json.Decode.Decoder b -> DataSource.DataSource b"}],"binops":[]},{"name":"Form","comment":" One of the core features of elm-pages is helping you manage form data end-to-end, including\n\n - Presenting the HTML form with its fields\n - Maintaining client-side form state\n - Showing validation errors on the client-side\n - Receiving a form submission on the server-side\n - Using the exact same client-side validations on the server-side\n - Letting you run server-only Validations with DataSource's (things like checking for a unique username)\n\nBecause elm-pages is a framework, it has its own internal Model and Msg's. That means you, the user,\ncan offload some of the responsibility to elm-pages and build an interactive form with real-time\nclient-side state and validation errors without wiring up your own Model and Msg's to manage that\nstate. You define the source of truth for your form (how to parse it into data or errors), and\nelm-pages manages the state.\n\nLet's look at a sign-up form example.\n\n\n### Step 1 - Define the Form\n\nWhat to look for:\n\n**The field declarations**\n\nBelow the `Form.init` call you will find all of the form's fields declared with\n\n |> Form.field ...\n\nThese are the form's field declarations.\n\nThese fields each have individual validations. For example, `|> Field.required ...` means we'll get a validation\nerror if that field is empty (similar for checking the minimum password length).\n\nThere will be a corresponding parameter in the function we pass in to `Form.init` for every\nfield declaration (in this example, `\\email password passwordConfirmation -> ...`).\n\n**The `combine` validation**\n\nIn addition to the validation errors that individual fields can have independently (like\nrequired fields or minimum password length), we can also do _dependent validations_.\n\nWe use the [`Form.Validation`](Form-Validation) module to take each individual field and combine\nthem into a type and/or errors.\n\n**The `view`**\n\nTotally customizable. Uses [`Form.FieldView`](Form-FieldView) to render all of the fields declared.\n\n import DataSource exposing (DataSource)\n import ErrorPage exposing (ErrorPage)\n import Form\n import Form.Field as Field\n import Form.FieldView as FieldView\n import Form.Validation as Validation\n import Html exposing (Html)\n import Html.Attributes as Attr\n import Route\n import Server.Request as Request\n import Server.Response exposing (Response)\n\n type alias NewUser =\n { email : String\n , password : String\n }\n\n signupForm : Form.HtmlForm String NewUser () Msg\n signupForm =\n Form.init\n (\\email password passwordConfirmation ->\n { combine =\n Validation.succeed Login\n |> Validation.andMap email\n |> Validation.andMap\n (Validation.map2\n (\\pass confirmation ->\n if pass == confirmation then\n Validation.succeed pass\n\n else\n passwordConfirmation\n |> Validation.fail\n \"Must match password\"\n )\n password\n passwordConfirmation\n |> Validation.andThen identity\n )\n , view =\n \\info ->\n [ Html.label []\n [ fieldView info \"Email\" email\n , fieldView info \"Password\" password\n , fieldView info \"Confirm Password\" passwordConfirmation\n ]\n , Html.button []\n [ if info.isTransitioning then\n Html.text \"Signing Up...\"\n\n else\n Html.text \"Sign Up\"\n ]\n ]\n }\n )\n |> Form.field \"email\"\n (Field.text\n |> Field.required \"Required\"\n )\n |> Form.field \"password\"\n passwordField\n |> Form.field \"passwordConfirmation\"\n passwordField\n\n passwordField =\n Field.text\n |> Field.password\n |> Field.required \"Required\"\n |> Field.withClientValidation\n (\\password ->\n ( Just password\n , if String.length password < 4 then\n [ \"Must be at least 4 characters\" ]\n\n else\n []\n )\n )\n\n fieldView :\n Form.Context String data\n -> String\n -> Validation.Field String parsed FieldView.Input\n -> Html msg\n fieldView formState label field =\n Html.div []\n [ Html.label []\n [ Html.text (label ++ \" \")\n , field |> Form.FieldView.input []\n ]\n , (if formState.submitAttempted then\n formState.errors\n |> Form.errorsForField field\n |> List.map\n (\\error ->\n Html.li [] [ Html.text error ]\n )\n\n else\n []\n )\n |> Html.ul [ Attr.style \"color\" \"red\" ]\n ]\n\n\n### Step 2 - Render the Form's View\n\n view maybeUrl sharedModel app =\n { title = \"Sign Up\"\n , body =\n [ form\n |> Form.toDynamicTransition \"login\"\n |> Form.renderHtml [] Nothing app ()\n ]\n }\n\n\n### Step 3 - Handle Server-Side Form Submissions\n\n action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))\n action routeParams =\n Request.formData [ signupForm ]\n |> Request.map\n (\\signupResult ->\n case signupResult of\n Ok newUser ->\n newUser\n |> myCreateUserDataSource\n |> DataSource.map\n (\\() ->\n -- redirect to the home page\n -- after successful sign-up\n Route.redirectTo Route.Index\n )\n\n Err _ ->\n Route.redirectTo Route.Login\n |> DataSource.succeed\n )\n\n myCreateUserDataSource : DataSource ()\n myCreateUserDataSource =\n DataSource.fail\n \"TODO - make a database call to create a new user\"\n\n\n## Building a Form Parser\n\n@docs Form, HtmlForm, StyledHtmlForm, DoneForm\n\n@docs Response\n\n@docs init\n\n\n## Adding Fields\n\n@docs field, hiddenField, hiddenKind\n\n\n## View Functions\n\n@docs Context\n\n\n## Rendering Forms\n\n@docs renderHtml, renderStyledHtml\n\n@docs FinalForm, withGetMethod, toDynamicTransition, toDynamicFetcher\n\n\n## Showing Errors\n\n@docs Errors, errorsForField\n\n\n## Running Parsers\n\n@docs parse, runServerSide, runOneOfServerSide\n\n\n## Combining Forms to Run on Server\n\n@docs ServerForms\n\n@docs initCombined, combine, initCombinedServer, combineServer\n\n\n## Dynamic Fields\n\n@docs dynamic\n\n@docs AppContext\n\n\n## Submission\n\n@docs toServerForm, withOnSubmit\n\n","unions":[{"name":"Errors","comment":" ","args":["error"],"cases":[]},{"name":"FinalForm","comment":" ","args":["error","parsed","data","view","userMsg"],"cases":[]},{"name":"Form","comment":" ","args":["error","combineAndView","input"],"cases":[]},{"name":"Response","comment":" ","args":["error"],"cases":[["Response",["{ fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) }"]]]},{"name":"ServerForms","comment":" ","args":["error","parsed"],"cases":[["ServerForms",["List.List (Form.Form error (Form.Validation.Combined error parsed) Basics.Never)"]]]}],"aliases":[{"name":"AppContext","comment":" ","args":["app","actionData"],"type":"{ app | path : Path.Path, transition : Maybe.Maybe Pages.Transition.Transition, fetchers : Dict.Dict String.String (Pages.Transition.FetcherState actionData), pageFormState : Dict.Dict String.String { fields : Dict.Dict String.String { value : String.String, status : Form.FieldStatus.FieldStatus }, submitAttempted : Basics.Bool } }"},{"name":"Context","comment":" ","args":["error","data"],"type":"{ errors : Form.Errors error, isTransitioning : Basics.Bool, submitAttempted : Basics.Bool, data : data }"},{"name":"DoneForm","comment":" ","args":["error","parsed","data","view"],"type":"Form.Form error { combine : Form.Validation.Combined error parsed, view : Form.Context error data -> view } data"},{"name":"HtmlForm","comment":" ","args":["error","parsed","input","msg"],"type":"Form.Form error { combine : Form.Validation.Combined error parsed, view : Form.Context error input -> List.List (Html.Html (Pages.Msg.Msg msg)) } input"},{"name":"StyledHtmlForm","comment":" ","args":["error","parsed","data","msg"],"type":"Form.Form error { combine : Form.Validation.Combined error parsed, view : Form.Context error data -> List.List (Html.Styled.Html (Pages.Msg.Msg msg)) } data"}],"values":[{"name":"combine","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Pages.Internal.Form.Validation error parsed kind constraints } input -> Form.ServerForms error combined -> Form.ServerForms error combined"},{"name":"combineServer","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Form.Validation.Combined error (DataSource.DataSource (Pages.Internal.Form.Validation error parsed kind constraints)) } input -> Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints)) -> Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints))"},{"name":"dynamic","comment":" ","type":"(decider -> Form.Form error { combine : Pages.Internal.Form.Validation error parsed named constraints1, view : subView } data) -> Form.Form error ({ combine : decider -> Pages.Internal.Form.Validation error parsed named constraints1, view : decider -> subView } -> combineAndView) data -> Form.Form error combineAndView data"},{"name":"errorsForField","comment":" ","type":"Form.Validation.Field error parsed kind -> Form.Errors error -> List.List error"},{"name":"field","comment":" Declare a visible field for the form.\n\nUse [`Form.Field`](Form-Field) to define the field and its validations.\n\n form =\n Form.init\n (\\email ->\n { combine =\n Validation.succeed NewUser\n |> Validation.andMap email\n , view = \\info -> [{- render fields -}]\n }\n )\n |> Form.field \"email\"\n (Field.text |> Field.required \"Required\")\n\n","type":"String.String -> Form.Field.Field error parsed data kind constraints -> Form.Form error (Form.Validation.Field error parsed kind -> combineAndView) data -> Form.Form error combineAndView data"},{"name":"hiddenField","comment":" Declare a hidden field for the form.\n\nUnlike [`field`](#field) declarations which are rendered using [`Form.ViewField`](Form-ViewField)\nfunctions, `hiddenField` inputs are automatically inserted into the form when you render it.\n\nYou define the field's validations the same way as for `field`, with the\n[`Form.Field`](Form-Field) API.\n\n form =\n Form.init\n (\\quantity productId ->\n { combine = {- combine fields -}\n , view = \\info -> [{- render visible fields -}]\n }\n )\n |> Form.field \"quantity\"\n (Field.int |> Field.required \"Required\")\n |> Form.field \"productId\"\n (Field.text\n |> Field.required \"Required\"\n |> Field.withInitialValue (\\product -> Form.Value.string product.id)\n )\n\n","type":"String.String -> Form.Field.Field error parsed data kind constraints -> Form.Form error (Form.Validation.Field error parsed Form.FieldView.Hidden -> combineAndView) data -> Form.Form error combineAndView data"},{"name":"hiddenKind","comment":" ","type":"( String.String, String.String ) -> error -> Form.Form error combineAndView data -> Form.Form error combineAndView data"},{"name":"init","comment":" ","type":"combineAndView -> Form.Form String.String combineAndView data"},{"name":"initCombined","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Pages.Internal.Form.Validation error parsed kind constraints } input -> Form.ServerForms error combined"},{"name":"initCombinedServer","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Form.Validation.Combined error (DataSource.DataSource (Pages.Internal.Form.Validation error parsed kind constraints)) } input -> Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints))"},{"name":"parse","comment":" ","type":"String.String -> Form.AppContext app actionData -> data -> Form.Form error { info | combine : Pages.Internal.Form.Validation error parsed named constraints } data -> ( Maybe.Maybe parsed, Dict.Dict String.String (List.List error) )"},{"name":"renderHtml","comment":" ","type":"List.List (Html.Attribute (Pages.Msg.Msg msg)) -> Maybe.Maybe { fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) } -> Form.AppContext app actionData -> data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed named constraints) data (Form.Context error data -> List.List (Html.Html (Pages.Msg.Msg msg))) msg -> Html.Html (Pages.Msg.Msg msg)"},{"name":"renderStyledHtml","comment":" ","type":"List.List (Html.Styled.Attribute (Pages.Msg.Msg msg)) -> Maybe.Maybe { fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) } -> Form.AppContext app actionData -> data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed named constraints) data (Form.Context error data -> List.List (Html.Styled.Html (Pages.Msg.Msg msg))) msg -> Html.Styled.Html (Pages.Msg.Msg msg)"},{"name":"runOneOfServerSide","comment":" ","type":"List.List ( String.String, String.String ) -> Form.ServerForms error parsed -> ( Maybe.Maybe parsed, Dict.Dict String.String (List.List error) )"},{"name":"runServerSide","comment":" ","type":"List.List ( String.String, String.String ) -> Form.Form error (Pages.Internal.Form.Validation error parsed kind constraints) data -> ( Basics.Bool, ( Maybe.Maybe parsed, Dict.Dict String.String (List.List error) ) )"},{"name":"toDynamicFetcher","comment":" ","type":"String.String -> Form.Form error { combine : Pages.Internal.Form.Validation error parsed field constraints, view : Form.Context error data -> view } data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed field constraints) data (Form.Context error data -> view) userMsg"},{"name":"toDynamicTransition","comment":" ","type":"String.String -> Form.Form error { combine : Pages.Internal.Form.Validation error parsed field constraints, view : Form.Context error data -> view } data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed field constraints) data (Form.Context error data -> view) userMsg"},{"name":"toServerForm","comment":" ","type":"Form.Form error { combine : Pages.Internal.Form.Validation error combined kind constraints, view : viewFn } data -> Form.Form error { combine : Pages.Internal.Form.Validation error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints)) kind constraints, view : viewFn } data"},{"name":"withGetMethod","comment":" ","type":"Form.FinalForm error parsed data view userMsg -> Form.FinalForm error parsed data view userMsg"},{"name":"withOnSubmit","comment":" ","type":"({ fields : List.List ( String.String, String.String ) } -> userMsg) -> Form.FinalForm error parsed data view userMsg -> Form.FinalForm error parsed data view userMsg"}],"binops":[]},{"name":"Form.Field","comment":"\n\n\n## Base Fields\n\n@docs text, checkbox, int, float\n\n\n## Multiple Choice Fields\n\n@docs select, range, OutsideRange\n\n\n## Date/Time Fields\n\n@docs date, time, TimeOfDay\n\n\n## Other\n\n@docs Field, FieldInfo, exactValue\n\n\n## Field Configuration\n\n@docs required, withClientValidation, withInitialValue, map\n\n\n## Text Field Display Options\n\n@docs email, password, search, telephone, url, textarea\n\n\n## Numeric Field Options\n\n@docs withMax, withMin, withStep, withMinLength, withMaxLength\n\n\n## Phantom Options\n\n@docs No, Yes\n\n","unions":[{"name":"Field","comment":" ","args":["error","parsed","data","kind","constraints"],"cases":[["Field",["Form.Field.FieldInfo error parsed data","kind"]]]},{"name":"No","comment":" ","args":[],"cases":[["No",["Basics.Never"]]]},{"name":"OutsideRange","comment":" ","args":[],"cases":[["AboveRange",[]],["BelowRange",[]]]},{"name":"Yes","comment":" ","args":[],"cases":[["Yes",["Basics.Never"]]]}],"aliases":[{"name":"FieldInfo","comment":" ","args":["error","parsed","data"],"type":"{ initialValue : Maybe.Maybe (data -> String.String), decode : Maybe.Maybe String.String -> ( Maybe.Maybe parsed, List.List error ), properties : List.List ( String.String, Json.Encode.Value ) }"},{"name":"TimeOfDay","comment":" ","args":[],"type":"{ hours : Basics.Int, minutes : Basics.Int }"}],"values":[{"name":"checkbox","comment":" ","type":"Form.Field.Field error Basics.Bool data Form.FieldView.Input { required : (), initial : Basics.Bool }"},{"name":"date","comment":" ","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Date.Date) data Form.FieldView.Input { min : Date.Date, max : Date.Date, required : (), wasMapped : Form.Field.No, initial : Date.Date }"},{"name":"email","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"exactValue","comment":" ","type":"String.String -> error -> Form.Field.Field error String.String data Form.FieldView.Input { required : (), plainText : (), wasMapped : Form.Field.No, initial : String.String }"},{"name":"float","comment":" ","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Basics.Float) data Form.FieldView.Input { min : Basics.Float, max : Basics.Float, required : (), wasMapped : Form.Field.No, initial : Basics.Float }"},{"name":"int","comment":" ","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Basics.Int) data Form.FieldView.Input { min : Basics.Int, max : Basics.Int, required : (), wasMapped : Form.Field.No, step : Basics.Int, initial : Basics.Int }"},{"name":"map","comment":" ","type":"(parsed -> mapped) -> Form.Field.Field error parsed data kind constraints -> Form.Field.Field error mapped data kind { constraints | wasMapped : Form.Field.Yes }"},{"name":"password","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"range","comment":" ","type":"{ min : Form.Value.Value valueType, max : Form.Value.Value valueType, initial : data -> Form.Value.Value valueType, missing : error, invalid : Form.Field.OutsideRange -> error } -> Form.Field.Field error (Maybe.Maybe valueType) data kind { constraints | required : (), initial : valueType, min : valueType, max : valueType, wasMapped : Form.Field.No } -> Form.Field.Field error valueType data Form.FieldView.Input { constraints | wasMapped : Form.Field.No }"},{"name":"required","comment":" ","type":"error -> Form.Field.Field error (Maybe.Maybe parsed) data kind { constraints | required : (), wasMapped : Form.Field.No } -> Form.Field.Field error parsed data kind { constraints | wasMapped : Form.Field.No }"},{"name":"search","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"select","comment":" ","type":"List.List ( String.String, option ) -> (String.String -> error) -> Form.Field.Field error (Maybe.Maybe option) data (Form.FieldView.Options option) { required : (), wasMapped : Form.Field.No }"},{"name":"telephone","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"text","comment":" ","type":"Form.Field.Field error (Maybe.Maybe String.String) data Form.FieldView.Input { required : (), plainText : (), wasMapped : Form.Field.No, initial : String.String, minlength : (), maxlength : () }"},{"name":"textarea","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"time","comment":" \n","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Form.Field.TimeOfDay) data Form.FieldView.Input { required : (), wasMapped : Form.Field.No }"},{"name":"url","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"withClientValidation","comment":" ","type":"(parsed -> ( Maybe.Maybe mapped, List.List error )) -> Form.Field.Field error parsed data kind constraints -> Form.Field.Field error mapped data kind { constraints | wasMapped : Form.Field.Yes }"},{"name":"withInitialValue","comment":" ","type":"(data -> Form.Value.Value valueType) -> Form.Field.Field error value data kind { constraints | initial : valueType } -> Form.Field.Field error value data kind constraints"},{"name":"withMax","comment":" ","type":"Form.Value.Value valueType -> error -> Form.Field.Field error parsed data kind { constraints | max : valueType } -> Form.Field.Field error parsed data kind constraints"},{"name":"withMaxLength","comment":" ","type":"Basics.Int -> error -> Form.Field.Field error parsed data kind { constraints | maxlength : () } -> Form.Field.Field error parsed data kind constraints"},{"name":"withMin","comment":" ","type":"Form.Value.Value valueType -> error -> Form.Field.Field error parsed data kind { constraints | min : valueType } -> Form.Field.Field error parsed data kind constraints"},{"name":"withMinLength","comment":" ","type":"Basics.Int -> error -> Form.Field.Field error parsed data kind { constraints | minlength : () } -> Form.Field.Field error parsed data kind constraints"},{"name":"withStep","comment":" ","type":"Form.Value.Value valueType -> Form.Field.Field msg error value view { constraints | step : valueType } -> Form.Field.Field msg error value view constraints"}],"binops":[]},{"name":"Form.FieldStatus","comment":" elm-pages manages the client-side state of fields, including Status which you can use to determine when\nin the user's workflow to show validation errors.\n\n\n## Field Status\n\n@docs FieldStatus, fieldStatusToString\n\n","unions":[{"name":"FieldStatus","comment":" ","args":[],"cases":[["NotVisited",[]],["Focused",[]],["Changed",[]],["Blurred",[]]]}],"aliases":[],"values":[{"name":"fieldStatusToString","comment":" ","type":"Form.FieldStatus.FieldStatus -> String.String"}],"binops":[]},{"name":"Form.FieldView","comment":"\n\n@docs Input, InputType, Options, input, inputTypeToString, radio, toHtmlProperties, Hidden, select\n\n\n## Html.Styled Helpers\n\n@docs radioStyled, inputStyled\n\n","unions":[{"name":"Hidden","comment":" There are no render helpers for hidden fields because the `Form.renderHtml` helper functions automatically render hidden fields for you.\n","args":[],"cases":[["Hidden",[]]]},{"name":"Input","comment":" ","args":[],"cases":[["Input",["Form.FieldView.InputType"]]]},{"name":"InputType","comment":" ","args":[],"cases":[["Text",[]],["Number",[]],["Range",[]],["Radio",[]],["Date",[]],["Time",[]],["Checkbox",[]],["Tel",[]],["Search",[]],["Password",[]],["Email",[]],["Url",[]],["Textarea",[]]]},{"name":"Options","comment":" ","args":["a"],"cases":[["Options",["String.String -> Maybe.Maybe a","List.List String.String"]]]}],"aliases":[],"values":[{"name":"input","comment":" ","type":"List.List (Html.Attribute msg) -> Form.Validation.Field error parsed Form.FieldView.Input -> Html.Html msg"},{"name":"inputStyled","comment":" ","type":"List.List (Html.Styled.Attribute msg) -> Form.Validation.Field error parsed Form.FieldView.Input -> Html.Styled.Html msg"},{"name":"inputTypeToString","comment":" ","type":"Form.FieldView.InputType -> String.String"},{"name":"radio","comment":" ","type":"List.List (Html.Attribute msg) -> (parsed -> (List.List (Html.Attribute msg) -> Html.Html msg) -> Html.Html msg) -> Form.Validation.Field error parsed2 (Form.FieldView.Options parsed) -> Html.Html msg"},{"name":"radioStyled","comment":" ","type":"List.List (Html.Styled.Attribute msg) -> (parsed -> (List.List (Html.Styled.Attribute msg) -> Html.Styled.Html msg) -> Html.Styled.Html msg) -> Form.Validation.Field error parsed2 (Form.FieldView.Options parsed) -> Html.Styled.Html msg"},{"name":"select","comment":" ","type":"List.List (Html.Attribute msg) -> (parsed -> ( List.List (Html.Attribute msg), String.String )) -> Form.Validation.Field error parsed2 (Form.FieldView.Options parsed) -> Html.Html msg"},{"name":"toHtmlProperties","comment":" ","type":"List.List ( String.String, Json.Encode.Value ) -> List.List (Html.Attribute msg)"}],"binops":[]},{"name":"Form.FormData","comment":"\n\n@docs FormData, Method\n\n","unions":[{"name":"Method","comment":" ","args":[],"cases":[["Get",[]],["Post",[]]]}],"aliases":[{"name":"FormData","comment":" ","args":[],"type":"{ fields : List.List ( String.String, String.String ), method : Form.FormData.Method, action : String.String, id : Maybe.Maybe String.String }"}],"values":[],"binops":[]},{"name":"Form.Validation","comment":"\n\n\n## Validations\n\n@docs Combined, Field, Validation\n\n@docs andMap, andThen, fail, fromMaybe, fromResult, map, map2, parseWithError, succeed, succeed2, withError, withErrorIf, withFallback\n\n\n## Field Metadata\n\n@docs value, fieldName, fieldStatus\n\n\n## Mapping\n\n@docs map3, map4, map5, map6, map7, map8, map9\n\n\n## Global Validation\n\n@docs global\n\n\n## Temporary?\n\n@docs mapWithNever\n\n","unions":[],"aliases":[{"name":"Combined","comment":" ","args":["error","parsed"],"type":"Form.Validation.Validation error parsed Basics.Never Basics.Never"},{"name":"Field","comment":" ","args":["error","parsed","kind"],"type":"Form.Validation.Validation error parsed kind { field : kind }"},{"name":"Validation","comment":" ","args":["error","parsed","kind","constraints"],"type":"Pages.Internal.Form.Validation error parsed kind constraints"}],"values":[{"name":"andMap","comment":" ","type":"Form.Validation.Validation error a named1 constraints1 -> Form.Validation.Validation error (a -> b) named2 constraints2 -> Form.Validation.Combined error b"},{"name":"andThen","comment":" ","type":"(parsed -> Form.Validation.Validation error mapped named1 constraints1) -> Form.Validation.Validation error parsed named2 constraints2 -> Form.Validation.Combined error mapped"},{"name":"fail","comment":" ","type":"error -> Form.Validation.Field error parsed1 field -> Form.Validation.Combined error parsed"},{"name":"fieldName","comment":" ","type":"Form.Validation.Field error parsed kind -> String.String"},{"name":"fieldStatus","comment":" ","type":"Form.Validation.Field error parsed kind -> Form.FieldStatus.FieldStatus"},{"name":"fromMaybe","comment":" ","type":"Maybe.Maybe parsed -> Form.Validation.Combined error parsed"},{"name":"fromResult","comment":" ","type":"Form.Validation.Field error (Result.Result error parsed) kind -> Form.Validation.Combined error parsed"},{"name":"global","comment":" ","type":"Form.Validation.Field error () Basics.Never"},{"name":"map","comment":" ","type":"(parsed -> mapped) -> Form.Validation.Validation error parsed named constraint -> Form.Validation.Validation error mapped named constraint"},{"name":"map2","comment":" ","type":"(a -> b -> c) -> Form.Validation.Validation error a named1 constraints1 -> Form.Validation.Validation error b named2 constraints2 -> Form.Validation.Combined error c"},{"name":"map3","comment":" ","type":"(a1 -> a2 -> a3 -> a4) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Combined error a4"},{"name":"map4","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Combined error a5"},{"name":"map5","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Combined error a6"},{"name":"map6","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Combined error a7"},{"name":"map7","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> a8) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Validation error a7 named7 constraints7 -> Form.Validation.Combined error a8"},{"name":"map8","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> a8 -> a9) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Validation error a7 named7 constraints7 -> Form.Validation.Validation error a8 named8 constraints8 -> Form.Validation.Combined error a9"},{"name":"map9","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> a8 -> a9 -> a10) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Validation error a7 named7 constraints7 -> Form.Validation.Validation error a8 named8 constraints8 -> Form.Validation.Validation error a9 named9 constraints9 -> Form.Validation.Combined error a10"},{"name":"mapWithNever","comment":" ","type":"(parsed -> mapped) -> Form.Validation.Validation error parsed named constraint -> Form.Validation.Validation error mapped Basics.Never Basics.Never"},{"name":"parseWithError","comment":" ","type":"parsed -> ( String.String, error ) -> Form.Validation.Combined error parsed"},{"name":"succeed","comment":" ","type":"parsed -> Form.Validation.Combined error parsed"},{"name":"succeed2","comment":" ","type":"parsed -> Form.Validation.Validation error parsed kind constraints"},{"name":"value","comment":" ","type":"Form.Validation.Validation error parsed named constraint -> Maybe.Maybe parsed"},{"name":"withError","comment":" ","type":"Form.Validation.Field error parsed1 field -> error -> Form.Validation.Validation error parsed2 named constraints -> Form.Validation.Validation error parsed2 named constraints"},{"name":"withErrorIf","comment":" ","type":"Basics.Bool -> Form.Validation.Field error ignored field -> error -> Form.Validation.Validation error parsed named constraints -> Form.Validation.Validation error parsed named constraints"},{"name":"withFallback","comment":" ","type":"parsed -> Form.Validation.Validation error parsed named constraints -> Form.Validation.Validation error parsed named constraints"}],"binops":[]},{"name":"Form.Value","comment":"\n\n@docs Value, date, float, int, string, bool, toString\n\n\n## Comparison\n\n@docs compare\n\n","unions":[{"name":"Value","comment":" ","args":["dataType"],"cases":[]}],"aliases":[],"values":[{"name":"bool","comment":" ","type":"Basics.Bool -> Form.Value.Value Basics.Bool"},{"name":"compare","comment":" You probably don't need this helper as it's mostly useful for internal implementation.\n","type":"String.String -> Form.Value.Value value -> Basics.Order"},{"name":"date","comment":" ","type":"Date.Date -> Form.Value.Value Date.Date"},{"name":"float","comment":" ","type":"Basics.Float -> Form.Value.Value Basics.Float"},{"name":"int","comment":" ","type":"Basics.Int -> Form.Value.Value Basics.Int"},{"name":"string","comment":" ","type":"String.String -> Form.Value.Value String.String"},{"name":"toString","comment":" ","type":"Form.Value.Value dataType -> String.String"}],"binops":[]},{"name":"Head","comment":" This module contains low-level functions for building up\nvalues that will be rendered into the page's `` tag\nwhen you run `elm-pages build`. Most likely the `Head.Seo` module\nwill do everything you need out of the box, and you will just need to import `Head`\nso you can use the `Tag` type in your type annotations.\n\nBut this module might be useful if you have a special use case, or if you are\nwriting a plugin package to extend `elm-pages`.\n\n@docs Tag, metaName, metaProperty, metaRedirect\n@docs rssLink, sitemapLink, rootLanguage, manifestLink\n\n\n## Structured Data\n\n@docs structuredData\n\n\n## `AttributeValue`s\n\n@docs AttributeValue\n@docs currentPageFullUrl, urlAttribute, raw\n\n\n## Icons\n\n@docs appleTouchIcon, icon\n\n\n## Functions for use by generated code\n\n@docs toJson, canonicalLink\n\n","unions":[{"name":"AttributeValue","comment":" Values, such as between the `<>`'s here:\n\n```html\n\" content=\"\" />\n```\n\n","args":[],"cases":[]},{"name":"Tag","comment":" Values that can be passed to the generated `Pages.application` config\nthrough the `head` function.\n","args":[],"cases":[]}],"aliases":[],"values":[{"name":"appleTouchIcon","comment":" Note: the type must be png.\nSee .\n\nIf a size is provided, it will be turned into square dimensions as per the recommendations here: \n\nImages must be png's, and non-transparent images are recommended. Current recommended dimensions are 180px and 192px.\n\n","type":"Maybe.Maybe Basics.Int -> Pages.Url.Url -> Head.Tag"},{"name":"canonicalLink","comment":" It's recommended that you use the `Seo` module helpers, which will provide this\nfor you, rather than directly using this.\n\nExample:\n\n Head.canonicalLink \"https://elm-pages.com\"\n\n","type":"Maybe.Maybe String.String -> Head.Tag"},{"name":"currentPageFullUrl","comment":" Create an `AttributeValue` representing the current page's full url.\n","type":"Head.AttributeValue"},{"name":"icon","comment":" ","type":"List.List ( Basics.Int, Basics.Int ) -> MimeType.MimeImage -> Pages.Url.Url -> Head.Tag"},{"name":"manifestLink","comment":" Let's you link to your manifest.json file, see .\n","type":"String.String -> Head.Tag"},{"name":"metaName","comment":" Example:\n\n Head.metaName \"twitter:card\" (Head.raw \"summary_large_image\")\n\nResults in ``\n\n","type":"String.String -> Head.AttributeValue -> Head.Tag"},{"name":"metaProperty","comment":" Example:\n\n Head.metaProperty \"fb:app_id\" (Head.raw \"123456789\")\n\nResults in ``\n\n","type":"String.String -> Head.AttributeValue -> Head.Tag"},{"name":"metaRedirect","comment":" Example:\n\n metaRedirect (Raw \"0; url=https://google.com\")\n\nResults in ``\n\n","type":"Head.AttributeValue -> Head.Tag"},{"name":"raw","comment":" Create a raw `AttributeValue` (as opposed to some kind of absolute URL).\n","type":"String.String -> Head.AttributeValue"},{"name":"rootLanguage","comment":" Set the language for a page.\n\n\n\n import Head\n import LanguageTag\n import LanguageTag.Language\n\n LanguageTag.Language.de -- sets the page's language to German\n |> LanguageTag.build LanguageTag.emptySubtags\n |> Head.rootLanguage\n\nThis results pre-rendered HTML with a global lang tag set.\n\n```html\n\n...\n\n```\n\n","type":"LanguageTag.LanguageTag -> Head.Tag"},{"name":"rssLink","comment":" Add a link to the site's RSS feed.\n\nExample:\n\n rssLink \"/feed.xml\"\n\n```html\n\n```\n\n","type":"String.String -> Head.Tag"},{"name":"sitemapLink","comment":" Add a link to the site's RSS feed.\n\nExample:\n\n sitemapLink \"/feed.xml\"\n\n```html\n\n```\n\n","type":"String.String -> Head.Tag"},{"name":"structuredData","comment":" You can learn more about structured data in [Google's intro to structured data](https://developers.google.com/search/docs/guides/intro-structured-data).\n\nWhen you add a `structuredData` item to one of your pages in `elm-pages`, it will add `json-ld` data to your document that looks like this:\n\n```html\n\n```\n\nTo get that data, you would write this in your `elm-pages` head tags:\n\n import Json.Encode as Encode\n\n {-| \n -}\n encodeArticle :\n { title : String\n , description : String\n , author : StructuredDataHelper { authorMemberOf | personOrOrganization : () } authorPossibleFields\n , publisher : StructuredDataHelper { publisherMemberOf | personOrOrganization : () } publisherPossibleFields\n , url : String\n , imageUrl : String\n , datePublished : String\n , mainEntityOfPage : Encode.Value\n }\n -> Head.Tag\n encodeArticle info =\n Encode.object\n [ ( \"@context\", Encode.string \"http://schema.org/\" )\n , ( \"@type\", Encode.string \"Article\" )\n , ( \"headline\", Encode.string info.title )\n , ( \"description\", Encode.string info.description )\n , ( \"image\", Encode.string info.imageUrl )\n , ( \"author\", encode info.author )\n , ( \"publisher\", encode info.publisher )\n , ( \"url\", Encode.string info.url )\n , ( \"datePublished\", Encode.string info.datePublished )\n , ( \"mainEntityOfPage\", info.mainEntityOfPage )\n ]\n |> Head.structuredData\n\nTake a look at this [Google Search Gallery](https://developers.google.com/search/docs/guides/search-gallery)\nto see some examples of how structured data can be used by search engines to give rich search results. It can help boost\nyour rankings, get better engagement for your content, and also make your content more accessible. For example,\nvoice assistant devices can make use of structured data. If you're hosting a conference and want to make the event\ndate and location easy for attendees to find, this can make that information more accessible.\n\nFor the current version of API, you'll need to make sure that the format is correct and contains the required and recommended\nstructure.\n\nCheck out for a comprehensive listing of possible data types and fields. And take a look at\nGoogle's [Structured Data Testing Tool](https://search.google.com/structured-data/testing-tool)\ntoo make sure that your structured data is valid and includes the recommended values.\n\nIn the future, `elm-pages` will likely support a typed API, but schema.org is a massive spec, and changes frequently.\nAnd there are multiple sources of information on the possible and recommended structure. So it will take some time\nfor the right API design to evolve. In the meantime, this allows you to make use of this for SEO purposes.\n\n","type":"Json.Encode.Value -> Head.Tag"},{"name":"toJson","comment":" Feel free to use this, but in 99% of cases you won't need it. The generated\ncode will run this for you to generate your `manifest.json` file automatically!\n","type":"String.String -> String.String -> Head.Tag -> Json.Encode.Value"},{"name":"urlAttribute","comment":" Create an `AttributeValue` from an `ImagePath`.\n","type":"Pages.Url.Url -> Head.AttributeValue"}],"binops":[]},{"name":"Head.Seo","comment":" \n\n\nThis module encapsulates some of the best practices for SEO for your site.\n\n`elm-pages` will pre-render each of the static pages (in your `content` directory) so that\nweb crawlers can efficiently and accurately process it. The functions in this module are for use\nwith the `head` function that you pass to your Pages config (`Pages.application`).\n\n import Date\n import Head\n import Head.Seo as Seo\n\n\n -- justinmimbs/date package\n type alias ArticleMetadata =\n { title : String\n , description : String\n , published : Date\n , author : Data.Author.Author\n }\n\n head : ArticleMetadata -> List Head.Tag\n head articleMetadata =\n Seo.summaryLarge\n { canonicalUrlOverride = Nothing\n , siteName = \"elm-pages\"\n , image =\n { url = Pages.images.icon\n , alt = articleMetadata.description\n , dimensions = Nothing\n , mimeType = Nothing\n }\n , description = articleMetadata.description\n , locale = Nothing\n , title = articleMetadata.title\n }\n |> Seo.article\n { tags = []\n , section = Nothing\n , publishedTime = Just (Date.toIsoString articleMetadata.published)\n , modifiedTime = Nothing\n , expirationTime = Nothing\n }\n\n@docs Common, Image, article, audioPlayer, book, profile, song, summary, summaryLarge, videoPlayer, website\n\n","unions":[],"aliases":[{"name":"Common","comment":" These fields apply to any type in the og object types\nSee and \n\nSkipping this for now, if there's a use case I can add it in:\n\n - og:determiner - The word that appears before this object's title in a sentence. An enum of (a, an, the, \"\", auto). If auto is chosen, the consumer of your data should chose between \"a\" or \"an\". Default is \"\" (blank).\n\n","args":[],"type":"{ title : String.String, image : Head.Seo.Image, canonicalUrlOverride : Maybe.Maybe String.String, description : String.String, siteName : String.String, audio : Maybe.Maybe Head.Seo.Audio, video : Maybe.Maybe Head.Seo.Video, locale : Maybe.Maybe Head.Seo.Locale, alternateLocales : List.List Head.Seo.Locale, twitterCard : Head.Twitter.TwitterCard }"},{"name":"Image","comment":" See \n","args":[],"type":"{ url : Pages.Url.Url, alt : String.String, dimensions : Maybe.Maybe { width : Basics.Int, height : Basics.Int }, mimeType : Maybe.Maybe Head.Seo.MimeType }"}],"values":[{"name":"article","comment":" See \n","type":"{ tags : List.List String.String, section : Maybe.Maybe String.String, publishedTime : Maybe.Maybe Head.Seo.Iso8601DateTime, modifiedTime : Maybe.Maybe Head.Seo.Iso8601DateTime, expirationTime : Maybe.Maybe Head.Seo.Iso8601DateTime } -> Head.Seo.Common -> List.List Head.Tag"},{"name":"audioPlayer","comment":" Will be displayed as a Player card in twitter\nSee: \n\nOpenGraph audio will also be included.\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, audio : Head.Seo.Audio, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"book","comment":" See \n","type":"Head.Seo.Common -> { tags : List.List String.String, isbn : Maybe.Maybe String.String, releaseDate : Maybe.Maybe Head.Seo.Iso8601DateTime } -> List.List Head.Tag"},{"name":"profile","comment":" See \n","type":"{ firstName : String.String, lastName : String.String, username : Maybe.Maybe String.String } -> Head.Seo.Common -> List.List Head.Tag"},{"name":"song","comment":" See \n","type":"Head.Seo.Common -> { duration : Maybe.Maybe Basics.Int, album : Maybe.Maybe Basics.Int, disc : Maybe.Maybe Basics.Int, track : Maybe.Maybe Basics.Int } -> List.List Head.Tag"},{"name":"summary","comment":" Will be displayed as a large card in twitter\nSee: \n\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\nNote: You cannot include audio or video tags with summaries.\nIf you want one of those, use `audioPlayer` or `videoPlayer`\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"summaryLarge","comment":" Will be displayed as a large card in twitter\nSee: \n\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\nNote: You cannot include audio or video tags with summaries.\nIf you want one of those, use `audioPlayer` or `videoPlayer`\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"videoPlayer","comment":" Will be displayed as a Player card in twitter\nSee: \n\nOpenGraph video will also be included.\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, video : Head.Seo.Video, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"website","comment":" \n","type":"Head.Seo.Common -> List.List Head.Tag"}],"binops":[]},{"name":"Pages.Fetcher","comment":"\n\n@docs Fetcher, FetcherInfo, submit, map\n\n","unions":[{"name":"Fetcher","comment":" ","args":["decoded"],"cases":[["Fetcher",["Pages.Fetcher.FetcherInfo decoded"]]]}],"aliases":[{"name":"FetcherInfo","comment":" ","args":["decoded"],"type":"{ decoder : Result.Result Http.Error Bytes.Bytes -> decoded, fields : List.List ( String.String, String.String ), headers : List.List ( String.String, String.String ), url : Maybe.Maybe String.String }"}],"values":[{"name":"map","comment":" ","type":"(a -> b) -> Pages.Fetcher.Fetcher a -> Pages.Fetcher.Fetcher b"},{"name":"submit","comment":" ","type":"Bytes.Decode.Decoder decoded -> { fields : List.List ( String.String, String.String ), headers : List.List ( String.String, String.String ) } -> Pages.Fetcher.Fetcher (Result.Result Http.Error decoded)"}],"binops":[]},{"name":"Pages.Flags","comment":"\n\n@docs Flags\n\n","unions":[{"name":"Flags","comment":" elm-pages apps run in two different contexts\n\n1. In the browser (like a regular Elm app)\n2. In pre-render mode. For example when you run `elm-pages build`, there is no browser involved, it just runs Elm directly.\n\nYou can pass in Flags and use them in your `Shared.init` function. You can store data in your `Shared.Model` from these flags and then access it across any page.\n\nYou will need to handle the `PreRender` case with no flags value because there is no browser to get flags from. For example, say you wanted to get the\ncurrent user's Browser window size and pass it in as a flag. When that page is pre-rendered, you need to decide on a value to use for the window size\nsince there is no window (the user hasn't requested the page yet, and the page isn't even loaded in a Browser window yet).\n\n","args":[],"cases":[["BrowserFlags",["Json.Decode.Value"]],["PreRenderFlags",[]]]}],"aliases":[],"values":[],"binops":[]},{"name":"Pages.Manifest","comment":" Represents the configuration of a\n[web manifest file](https://developer.mozilla.org/en-US/docs/Web/Manifest).\n\nYou pass your `Pages.Manifest.Config` record into the `Pages.application` function\n(from your generated `Pages.elm` file).\n\n import Pages.Manifest as Manifest\n import Pages.Manifest.Category\n\n manifest : Manifest.Config\n manifest =\n Manifest.init\n { name = static.siteName\n , description = \"elm-pages - \" ++ tagline\n , startUrl = Route.Index {} |> Route.toPath\n , icons =\n [ icon webp 192\n , icon webp 512\n , icon MimeType.Png 192\n , icon MimeType.Png 512\n ]\n }\n |> Manifest.withShortName \"elm-pages\"\n\n@docs Config, Icon\n\n\n## Builder options\n\n@docs init\n\n@docs withBackgroundColor, withCategories, withDisplayMode, withIarcRatingId, withLang, withOrientation, withShortName, withThemeColor\n\n\n## Config options\n\n@docs DisplayMode, Orientation, IconPurpose\n\n\n## Generating a Manifest.json\n\n@docs generator\n\n\n## Functions for use by the generated code (`Pages.elm`)\n\n@docs toJson\n\n","unions":[{"name":"DisplayMode","comment":" See \n","args":[],"cases":[["Fullscreen",[]],["Standalone",[]],["MinimalUi",[]],["Browser",[]]]},{"name":"IconPurpose","comment":" \n","args":[],"cases":[["IconPurposeMonochrome",[]],["IconPurposeMaskable",[]],["IconPurposeAny",[]]]},{"name":"Orientation","comment":" \n","args":[],"cases":[["Any",[]],["Natural",[]],["Landscape",[]],["LandscapePrimary",[]],["LandscapeSecondary",[]],["Portrait",[]],["PortraitPrimary",[]],["PortraitSecondary",[]]]}],"aliases":[{"name":"Config","comment":" Represents a [web app manifest file](https://developer.mozilla.org/en-US/docs/Web/Manifest)\n(see above for how to use it).\n","args":[],"type":"{ backgroundColor : Maybe.Maybe Color.Color, categories : List.List Pages.Manifest.Category.Category, displayMode : Pages.Manifest.DisplayMode, orientation : Pages.Manifest.Orientation, description : String.String, iarcRatingId : Maybe.Maybe String.String, name : String.String, themeColor : Maybe.Maybe Color.Color, startUrl : Path.Path, shortName : Maybe.Maybe String.String, icons : List.List Pages.Manifest.Icon, lang : LanguageTag.LanguageTag }"},{"name":"Icon","comment":" \n","args":[],"type":"{ src : Pages.Url.Url, sizes : List.List ( Basics.Int, Basics.Int ), mimeType : Maybe.Maybe MimeType.MimeImage, purposes : List.List Pages.Manifest.IconPurpose }"}],"values":[{"name":"generator","comment":" A generator for Api.elm to include a manifest.json.\n","type":"String.String -> DataSource.DataSource Pages.Manifest.Config -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"init","comment":" Setup a minimal Manifest.Config. You can then use the `with...` builder functions to set additional options.\n","type":"{ description : String.String, name : String.String, startUrl : Path.Path, icons : List.List Pages.Manifest.Icon } -> Pages.Manifest.Config"},{"name":"toJson","comment":" Feel free to use this, but in 99% of cases you won't need it. The generated\ncode will run this for you to generate your `manifest.json` file automatically!\n","type":"String.String -> Pages.Manifest.Config -> Json.Encode.Value"},{"name":"withBackgroundColor","comment":" Set .\n","type":"Color.Color -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withCategories","comment":" Set .\n","type":"List.List Pages.Manifest.Category.Category -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withDisplayMode","comment":" Set .\n","type":"Pages.Manifest.DisplayMode -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withIarcRatingId","comment":" Set .\n","type":"String.String -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withLang","comment":" Set .\n","type":"LanguageTag.LanguageTag -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withOrientation","comment":" Set .\n","type":"Pages.Manifest.Orientation -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withShortName","comment":" Set .\n","type":"String.String -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withThemeColor","comment":" Set .\n","type":"Color.Color -> Pages.Manifest.Config -> Pages.Manifest.Config"}],"binops":[]},{"name":"Pages.Manifest.Category","comment":" See and\n\n\n@docs toString, Category\n\n@docs books, business, education, entertainment, finance, fitness, food, games, government, health, kids, lifestyle, magazines, medical, music, navigation, news, personalization, photo, politics, productivity, security, shopping, social, sports, travel, utilities, weather\n\n\n## Custom categories\n\n@docs custom\n\n","unions":[{"name":"Category","comment":" Represents a known, valid category, as specified by\n. If this document is updated\nand I don't add it, please open an issue or pull request to let me know!\n","args":[],"cases":[]}],"aliases":[],"values":[{"name":"books","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"business","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"custom","comment":" It's best to use the pre-defined categories to ensure that clients (Android, iOS,\nChrome, Windows app store, etc.) are aware of it and can handle it appropriately.\nBut, if you're confident about using a custom one, you can do so with `Pages.Manifest.custom`.\n","type":"String.String -> Pages.Manifest.Category.Category"},{"name":"education","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"entertainment","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"finance","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"fitness","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"food","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"games","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"government","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"health","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"kids","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"lifestyle","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"magazines","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"medical","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"music","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"navigation","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"news","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"personalization","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"photo","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"politics","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"productivity","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"security","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"shopping","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"social","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"sports","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"toString","comment":" Turn a category into its official String representation, as seen\nhere: .\n","type":"Pages.Manifest.Category.Category -> String.String"},{"name":"travel","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"utilities","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"weather","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"}],"binops":[]},{"name":"Pages.Msg","comment":"\n\n@docs Msg\n\n@docs map, onSubmit, fetcherOnSubmit, submitIfValid\n\n","unions":[{"name":"Msg","comment":" ","args":["userMsg"],"cases":[["UserMsg",["userMsg"]],["Submit",["Form.FormData.FormData"]],["SubmitIfValid",["String.String","Form.FormData.FormData","Basics.Bool"]],["SubmitFetcher",["String.String","Form.FormData.FormData","Basics.Bool","Maybe.Maybe userMsg"]],["FormFieldEvent",["Json.Decode.Value"]]]}],"aliases":[],"values":[{"name":"fetcherOnSubmit","comment":" ","type":"Maybe.Maybe ({ fields : List.List ( String.String, String.String ) } -> userMsg) -> String.String -> (List.List ( String.String, String.String ) -> Basics.Bool) -> Html.Attribute (Pages.Msg.Msg userMsg)"},{"name":"map","comment":" ","type":"(a -> b) -> Pages.Msg.Msg a -> Pages.Msg.Msg b"},{"name":"onSubmit","comment":" ","type":"Html.Attribute (Pages.Msg.Msg userMsg)"},{"name":"submitIfValid","comment":" ","type":"String.String -> (List.List ( String.String, String.String ) -> Basics.Bool) -> Html.Attribute (Pages.Msg.Msg userMsg)"}],"binops":[]},{"name":"Pages.PageUrl","comment":" Same as a Url in `elm/url`, but slightly more structured. The path portion of the URL is parsed into a [`Path`](Path) type, and\nthe query params use the [`QueryParams`](QueryParams) type which allows you to parse just the query params or access them into a Dict.\n\nBecause `elm-pages` takes care of the main routing for pages in your app, the standard Elm URL parser API isn't suited\nto parsing query params individually, which is why the structure of these types is different.\n\n@docs PageUrl, toUrl\n\n","unions":[],"aliases":[{"name":"PageUrl","comment":" ","args":[],"type":"{ protocol : Url.Protocol, host : String.String, port_ : Maybe.Maybe Basics.Int, path : Path.Path, query : Maybe.Maybe QueryParams.QueryParams, fragment : Maybe.Maybe String.String }"}],"values":[{"name":"toUrl","comment":" ","type":"Pages.PageUrl.PageUrl -> Url.Url"}],"binops":[]},{"name":"Pages.Transition","comment":"\n\n@docs Transition, LoadingState, map\n\n\n## Fetchers\n\n@docs FetcherState, FetcherSubmitStatus\n\n","unions":[{"name":"FetcherSubmitStatus","comment":" ","args":["actionData"],"cases":[["FetcherSubmitting",[]],["FetcherReloading",["actionData"]],["FetcherComplete",["actionData"]]]},{"name":"LoadingState","comment":" ","args":[],"cases":[["Redirecting",[]],["Load",[]],["ActionRedirect",[]]]},{"name":"Transition","comment":" ","args":[],"cases":[["Submitting",["Form.FormData.FormData"]],["LoadAfterSubmit",["Form.FormData.FormData","Path.Path","Pages.Transition.LoadingState"]],["Loading",["Path.Path","Pages.Transition.LoadingState"]]]}],"aliases":[{"name":"FetcherState","comment":" ","args":["actionData"],"type":"{ status : Pages.Transition.FetcherSubmitStatus actionData, payload : Form.FormData.FormData, initiatedAt : Time.Posix }"}],"values":[{"name":"map","comment":" ","type":"(a -> b) -> Pages.Transition.FetcherState a -> Pages.Transition.FetcherState b"}],"binops":[]},{"name":"Pages.Url","comment":" Some of the `elm-pages` APIs will take internal URLs and ensure that they have the `canonicalSiteUrl` prepended.\n\nThat's the purpose for this type. If you have an external URL, like `Pages.Url.external \"https://google.com\"`,\nthen the canonicalUrl will not be prepended when it is used in a head tag.\n\nIf you refer to a local page, like `Route.Index |> Route.toPath |> Pages.Url.fromPath`, or `Pages.Url.fromPath`\n\n@docs Url, external, fromPath, toAbsoluteUrl, toString\n\n","unions":[{"name":"Url","comment":" ","args":[],"cases":[]}],"aliases":[],"values":[{"name":"external","comment":" ","type":"String.String -> Pages.Url.Url"},{"name":"fromPath","comment":" ","type":"Path.Path -> Pages.Url.Url"},{"name":"toAbsoluteUrl","comment":" ","type":"String.String -> Pages.Url.Url -> String.String"},{"name":"toString","comment":" ","type":"Pages.Url.Url -> String.String"}],"binops":[]},{"name":"Path","comment":" Represents the path portion of a URL (not query parameters, fragment, protocol, port, etc.).\n\nThis helper lets you combine together path parts without worrying about having too many or too few slashes.\nThese two examples will result in the same URL, even though the first example has trailing and leading slashes, and the\nsecond does not.\n\n Path.join [ \"/blog/\", \"/post-1/\" ]\n |> Path.toAbsolute\n --> \"/blog/post-1\"\n\n Path.join [ \"blog\", \"post-1\" ]\n |> Path.toAbsolute\n --> \"/blog/post-1\"\n\nWe can also safely join Strings that include multiple path parts, a single path part per string, or a mix of the two:\n\n Path.join [ \"/articles/archive/\", \"1977\", \"06\", \"10\", \"post-1\" ]\n |> Path.toAbsolute\n --> \"/articles/archive/1977/06/10/post-1\"\n\n\n## Creating Paths\n\n@docs Path, join, fromString\n\n\n## Turning Paths to String\n\n@docs toAbsolute, toRelative, toSegments\n\n","unions":[{"name":"Path","comment":" The path portion of the URL, normalized to ensure that path segments are joined with `/`s in the right places (no doubled up or missing slashes).\n","args":[],"cases":[]}],"aliases":[],"values":[{"name":"fromString","comment":" Create a Path from a path String.\n\n Path.fromString \"blog/post-1/\"\n |> Path.toAbsolute\n |> Expect.equal \"/blog/post-1\"\n\n","type":"String.String -> Path.Path"},{"name":"join","comment":" Create a Path from multiple path parts. Each part can either be a single path segment, like `blog`, or a\nmulti-part path part, like `blog/post-1`.\n","type":"List.List String.String -> Path.Path"},{"name":"toAbsolute","comment":" Turn a Path to an absolute URL (with no trailing slash).\n","type":"Path.Path -> String.String"},{"name":"toRelative","comment":" Turn a Path to a relative URL.\n","type":"Path.Path -> String.String"},{"name":"toSegments","comment":" ","type":"Path.Path -> List.List String.String"}],"binops":[]},{"name":"QueryParams","comment":" Represents the query portion of a URL. You can use `toDict` or `toString` to turn it into basic types, or you can\nparse it into a custom type using the other functions in this module.\n\n@docs QueryParams\n\n\n## Parsing\n\n@docs Parser\n\n@docs andThen, fail, fromResult, fromString, optionalString, parse, string, strings, succeed\n\n\n## Combining\n\n@docs map2, oneOf\n\n\n## Accessing as Built-In Types\n\n@docs toDict, toString\n\n","unions":[{"name":"Parser","comment":" ","args":["a"],"cases":[]},{"name":"QueryParams","comment":" ","args":[],"cases":[]}],"aliases":[],"values":[{"name":"andThen","comment":" ","type":"(a -> QueryParams.Parser b) -> QueryParams.Parser a -> QueryParams.Parser b"},{"name":"fail","comment":" ","type":"String.String -> QueryParams.Parser a"},{"name":"fromResult","comment":" ","type":"Result.Result String.String a -> QueryParams.Parser a"},{"name":"fromString","comment":" ","type":"String.String -> QueryParams.QueryParams"},{"name":"map2","comment":" ","type":"(a -> b -> combined) -> QueryParams.Parser a -> QueryParams.Parser b -> QueryParams.Parser combined"},{"name":"oneOf","comment":" ","type":"List.List (QueryParams.Parser a) -> QueryParams.Parser a"},{"name":"optionalString","comment":" ","type":"String.String -> QueryParams.Parser (Maybe.Maybe String.String)"},{"name":"parse","comment":" ","type":"QueryParams.Parser a -> QueryParams.QueryParams -> Result.Result String.String a"},{"name":"string","comment":" ","type":"String.String -> QueryParams.Parser String.String"},{"name":"strings","comment":" ","type":"String.String -> QueryParams.Parser (List.List String.String)"},{"name":"succeed","comment":" ","type":"a -> QueryParams.Parser a"},{"name":"toDict","comment":" ","type":"QueryParams.QueryParams -> Dict.Dict String.String (List.List String.String)"},{"name":"toString","comment":" ","type":"QueryParams.QueryParams -> String.String"}],"binops":[]},{"name":"Server.Request","comment":"\n\n@docs Parser\n\n@docs succeed, fromResult, skip\n\n\n## Forms\n\n@docs formData, formDataWithServerValidation\n\n@docs rawFormData\n\n\n## Direct Values\n\n@docs method, rawBody, allCookies, rawHeaders, queryParams\n\n@docs requestTime, optionalHeader, expectContentType, expectJsonBody\n\n@docs acceptMethod, acceptContentTypes\n\n\n## Transforming\n\n@docs map, map2, oneOf, andMap, andThen\n\n\n## Query Parameters\n\n@docs queryParam, expectQueryParam\n\n\n## Cookies\n\n@docs cookie, expectCookie\n\n\n## Headers\n\n@docs expectHeader\n\n\n## Multi-part forms and file uploads\n\n@docs File, expectMultiPartFormPost\n\n\n## Request Parsers That Can Fail\n\n@docs expectBody\n\n\n## Map Functions\n\n@docs map3, map4, map5, map6, map7, map8, map9\n\n\n## Method Type\n\n@docs Method, methodToString\n\n\n## Internals\n\n@docs errorsToString, errorToString, getDecoder, ValidationError\n\n","unions":[{"name":"Method","comment":" ","args":[],"cases":[["Connect",[]],["Delete",[]],["Get",[]],["Head",[]],["Options",[]],["Patch",[]],["Post",[]],["Put",[]],["Trace",[]],["NonStandard",["String.String"]]]},{"name":"ValidationError","comment":" ","args":[],"cases":[]}],"aliases":[{"name":"File","comment":" ","args":[],"type":"{ name : String.String, mimeType : String.String, body : String.String }"},{"name":"Parser","comment":" A `Server.Request.Parser` lets you send a `Server.Response.Response` based on an incoming HTTP request. For example,\nusing a `Server.Request.Parser`, you could check a session cookie to decide whether to respond by rendering a page\nfor the logged-in user, or else respond with an HTTP redirect response (see the [`Server.Response` docs](Server-Response)).\n\nYou can access the incoming HTTP request's:\n\n - Headers\n - Cookies\n - [`method`](#method)\n - URL query parameters\n - [`requestTime`](#requestTime) (as a `Time.Posix`)\n\nNote that this data is not available for pre-rendered pages or pre-rendered API Routes, only for server-rendered pages.\nThis is because when a page is pre-rendered, there _is_ no incoming HTTP request to respond to, it is rendered before a user\nrequests the page and then the pre-rendered page is served as a plain file (without running your Route Module).\n\nThat's why `RouteBuilder.preRender` has `data : RouteParams -> DataSource Data`:\n\n import DataSource exposing (DataSource)\n import RouteBuilder exposing (StatelessRoute)\n\n type alias Data =\n {}\n\n data : RouteParams -> DataSource Data\n data routeParams =\n DataSource.succeed Data\n\n route : StatelessRoute RouteParams Data ActionData\n route =\n RouteBuilder.preRender\n { data = data\n , head = head\n , pages = pages\n }\n |> RouteBuilder.buildNoState { view = view }\n\nA server-rendered Route Module _does_ have access to a user's incoming HTTP request because it runs every time the page\nis loaded. That's why `data` is a `Request.Parser` in server-rendered Route Modules. Since you have an incoming HTTP request for server-rendered routes,\n`RouteBuilder.serverRender` has `data : RouteParams -> Request.Parser (DataSource (Response Data))`. That means that you\ncan use the incoming HTTP request data to choose how to respond. For example, you could check for a dark-mode preference\ncookie and render a light- or dark-themed page and render a different page.\n\nThat's a mouthful, so let's unpack what it means.\n\n`Request.Parser` means you can pull out\n\ndata from the request payload using a Server Request Parser.\n\n import DataSource exposing (DataSource)\n import RouteBuilder exposing (StatelessRoute)\n import Server.Request as Request exposing (Request)\n import Server.Response as Response exposing (Response)\n\n type alias Data =\n {}\n\n data :\n RouteParams\n -> Request.Parser (DataSource (Response Data))\n data routeParams =\n {}\n |> Server.Response.render\n |> DataSource.succeed\n |> Request.succeed\n\n route : StatelessRoute RouteParams Data ActionData\n route =\n RouteBuilder.serverRender\n { head = head\n , data = data\n }\n |> RouteBuilder.buildNoState { view = view }\n\n","args":["decodesTo"],"type":"Internal.Request.Parser decodesTo Server.Request.ValidationError"}],"values":[{"name":"acceptContentTypes","comment":" ","type":"( String.String, List.List String.String ) -> Server.Request.Parser value -> Server.Request.Parser value"},{"name":"acceptMethod","comment":" ","type":"( Server.Request.Method, List.List Server.Request.Method ) -> Server.Request.Parser value -> Server.Request.Parser value"},{"name":"allCookies","comment":" ","type":"Server.Request.Parser (Dict.Dict String.String String.String)"},{"name":"andMap","comment":" Decode an argument and provide it to a function in a decoder.\n\n decoder : Decoder String\n decoder =\n succeed (String.repeat)\n |> andMap (field \"count\" int)\n |> andMap (field \"val\" string)\n\n\n \"\"\" { \"val\": \"hi\", \"count\": 3 } \"\"\"\n |> decodeString decoder\n --> Success \"hihihi\"\n\n","type":"Server.Request.Parser a -> Server.Request.Parser (a -> b) -> Server.Request.Parser b"},{"name":"andThen","comment":" ","type":"(a -> Server.Request.Parser b) -> Server.Request.Parser a -> Server.Request.Parser b"},{"name":"cookie","comment":" ","type":"String.String -> Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"errorToString","comment":" TODO internal only\n","type":"Server.Request.ValidationError -> String.String"},{"name":"errorsToString","comment":" ","type":"( Server.Request.ValidationError, List.List Server.Request.ValidationError ) -> String.String"},{"name":"expectBody","comment":" Same as [`rawBody`](#rawBody), but will only match when a body is present in the HTTP request.\n","type":"Server.Request.Parser String.String"},{"name":"expectContentType","comment":" ","type":"String.String -> Server.Request.Parser ()"},{"name":"expectCookie","comment":" ","type":"String.String -> Server.Request.Parser String.String"},{"name":"expectHeader","comment":" ","type":"String.String -> Server.Request.Parser String.String"},{"name":"expectJsonBody","comment":" ","type":"Json.Decode.Decoder value -> Server.Request.Parser value"},{"name":"expectMultiPartFormPost","comment":" ","type":"({ field : String.String -> Server.Request.Parser String.String, optionalField : String.String -> Server.Request.Parser (Maybe.Maybe String.String), fileField : String.String -> Server.Request.Parser Server.Request.File } -> Server.Request.Parser decodedForm) -> Server.Request.Parser decodedForm"},{"name":"expectQueryParam","comment":" ","type":"String.String -> Server.Request.Parser String.String"},{"name":"formData","comment":" ","type":"Form.ServerForms error combined -> Server.Request.Parser (Result.Result { fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) } combined)"},{"name":"formDataWithServerValidation","comment":" ","type":"Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints)) -> Server.Request.Parser (DataSource.DataSource (Result.Result (Form.Response error) ( Form.Response error, combined )))"},{"name":"fromResult","comment":" Turn a Result into a Request. Useful with `andThen`. Turns `Err` into a skipped request handler (non-matching request),\nand `Ok` values into a `succeed` (matching request).\n","type":"Result.Result String.String value -> Server.Request.Parser value"},{"name":"getDecoder","comment":" TODO internal only\n","type":"Server.Request.Parser (DataSource.DataSource response) -> Json.Decode.Decoder (Result.Result ( Server.Request.ValidationError, List.List Server.Request.ValidationError ) (DataSource.DataSource response))"},{"name":"map","comment":" ","type":"(a -> b) -> Server.Request.Parser a -> Server.Request.Parser b"},{"name":"map2","comment":" ","type":"(a -> b -> c) -> Server.Request.Parser a -> Server.Request.Parser b -> Server.Request.Parser c"},{"name":"map3","comment":" ","type":"(value1 -> value2 -> value3 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser valueCombined"},{"name":"map4","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser valueCombined"},{"name":"map5","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser valueCombined"},{"name":"map6","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser valueCombined"},{"name":"map7","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser value7 -> Server.Request.Parser valueCombined"},{"name":"map8","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser value7 -> Server.Request.Parser value8 -> Server.Request.Parser valueCombined"},{"name":"map9","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> value9 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser value7 -> Server.Request.Parser value8 -> Server.Request.Parser value9 -> Server.Request.Parser valueCombined"},{"name":"method","comment":" ","type":"Server.Request.Parser Server.Request.Method"},{"name":"methodToString","comment":" Gets the HTTP Method as a String, like 'GET', 'PUT', etc.\n","type":"Server.Request.Method -> String.String"},{"name":"oneOf","comment":" ","type":"List.List (Server.Request.Parser a) -> Server.Request.Parser a"},{"name":"optionalHeader","comment":" ","type":"String.String -> Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"queryParam","comment":" ","type":"String.String -> Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"queryParams","comment":" ","type":"Server.Request.Parser (Dict.Dict String.String (List.List String.String))"},{"name":"rawBody","comment":" ","type":"Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"rawFormData","comment":" ","type":"Server.Request.Parser (List.List ( String.String, String.String ))"},{"name":"rawHeaders","comment":" ","type":"Server.Request.Parser (Dict.Dict String.String String.String)"},{"name":"requestTime","comment":" ","type":"Server.Request.Parser Time.Posix"},{"name":"skip","comment":" This is a Request.Parser that will never match an HTTP request. Similar to `Json.Decode.fail`.\n\nWhy would you want it to always fail? It's helpful for building custom `Server.Request.Parser`. For example, let's say\nyou wanted to define a custom `Server.Request.Parser` to use an XML Decoding package on the request body.\nYou could define a custom function like this\n\n import Server.Request as Request\n\n expectXmlBody : XmlDecoder value -> Request.Parser value\n expectXmlBody xmlDecoder =\n Request.expectBody\n |> Request.andThen\n (\\bodyAsString ->\n case runXmlDecoder xmlDecoder bodyAsString of\n Ok decodedXml ->\n Request.succeed decodedXml\n\n Err error ->\n Request.skip (\"XML could not be decoded \" ++ xmlErrorToString error)\n )\n\nNote that when we said `Request.skip`, remaining Request Parsers will run (for example if you use [`Server.Request.oneOf`](#oneOf)).\nYou could build this with different semantics if you wanted to handle _any_ valid XML body. This Request Parser will _not_\nhandle any valid XML body. It will only handle requests that can match the XmlDecoder that is passed in.\n\nSo when you define your `Server.Request.Parser`s, think carefully about whether you want to handle invalid cases and give an\nerror, or fall through to other Parsers. There's no universal right answer, it's just something to decide for your use case.\n\n expectXmlBody : Request.Parser value\n expectXmlBody =\n Request.map2\n acceptContentTypes\n Request.expectBody\n |> Request.andThen\n (\\bodyAsString ->\n case runXmlDecoder xmlDecoder bodyAsString of\n Ok decodedXml ->\n Request.succeed decodedXml\n\n Err error ->\n Request.skip (\"XML could not be decoded \" ++ xmlErrorToString error)\n )\n\n","type":"String.String -> Server.Request.Parser value"},{"name":"succeed","comment":" ","type":"value -> Server.Request.Parser value"}],"binops":[]},{"name":"Server.Response","comment":"\n\n\n## Responses\n\n@docs Response\n\nThere are two top-level response types:\n\n1. Server Responses\n2. Render Responses\n\nA Server Response is a way to directly send a low-level server response, with no additional magic. You can set a String body,\na list of headers, the status code, etc. The Server Response helpers like `json` and `temporaryRedirect` are just helpers for\nbuilding up those low-level Server Responses.\n\nRender Responses are a little more special in the way they are connected to your elm-pages app. They allow you to render\nthe current Route Module. To do that, you'll need to pass along the `data` for your Route Module.\n\nYou can use `withHeader` and `withStatusCode` to customize either type of Response (Server Responses or Render Responses).\n\n\n## Server Responses\n\n@docs json, plainText, temporaryRedirect, permanentRedirect\n\n\n## Custom Responses\n\n@docs emptyBody, body, bytesBody, base64Body\n\n\n## Render Responses\n\n@docs render\n\n\n## Rendering Error Pages\n\n@docs errorPage, mapError\n\n@docs map\n\n\n## Amending Responses\n\n@docs withHeader, withHeaders, withStatusCode, withSetCookieHeader\n\n\n## Internals\n\n@docs toJson\n\n","unions":[],"aliases":[{"name":"Response","comment":" ","args":["data","error"],"type":"PageServerResponse.PageServerResponse data error"}],"values":[{"name":"base64Body","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"body","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"bytesBody","comment":" ","type":"Bytes.Bytes -> Server.Response.Response data error"},{"name":"emptyBody","comment":" ","type":"Server.Response.Response data error"},{"name":"errorPage","comment":" ","type":"errorPage -> Server.Response.Response data errorPage"},{"name":"json","comment":" ","type":"Json.Encode.Value -> Server.Response.Response data error"},{"name":"map","comment":" ","type":"(data -> mappedData) -> Server.Response.Response data error -> Server.Response.Response mappedData error"},{"name":"mapError","comment":" ","type":"(errorPage -> mappedErrorPage) -> Server.Response.Response data errorPage -> Server.Response.Response data mappedErrorPage"},{"name":"permanentRedirect","comment":" Build a 308 permanent redirect response.\n\nPermanent redirects tell the browser that a resource has permanently moved. If you redirect because a user is not logged in,\nthen you **do not** want to use a permanent redirect because the page they are looking for hasn't changed, you are just\ntemporarily pointing them to a new page since they need to authenticate.\n\nPermanent redirects are aggressively cached so be careful not to use them when you mean to use temporary redirects instead.\n\nIf you need to specifically rely on a 301 permanent redirect (see on the difference between 301 and 308),\nuse `customResponse` instead.\n\n","type":"String.String -> Server.Response.Response data error"},{"name":"plainText","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"render","comment":" ","type":"data -> Server.Response.Response data error"},{"name":"temporaryRedirect","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"toJson","comment":" ","type":"Server.Response.Response Basics.Never Basics.Never -> Json.Encode.Value"},{"name":"withHeader","comment":" ","type":"String.String -> String.String -> Server.Response.Response data error -> Server.Response.Response data error"},{"name":"withHeaders","comment":" ","type":"List.List ( String.String, String.String ) -> Server.Response.Response data error -> Server.Response.Response data error"},{"name":"withSetCookieHeader","comment":" ","type":"Server.SetCookie.SetCookie -> Server.Response.Response data error -> Server.Response.Response data error"},{"name":"withStatusCode","comment":" ","type":"Basics.Int -> Server.Response.Response data Basics.Never -> Server.Response.Response data Basics.Never"}],"binops":[]},{"name":"Server.Session","comment":"\n\n@docs Decoder, NotLoadedReason, Session, Value, clearFlashCookies, empty, expectSession, flashPrefix, get, insert, remove, setValues, succeed, unwrap, update, withFlash, withSession\n\n","unions":[{"name":"NotLoadedReason","comment":" ","args":[],"cases":[["NoCookies",[]],["MissingHeaders",[]]]},{"name":"Session","comment":" ","args":[],"cases":[["Session",["Dict.Dict String.String Server.Session.Value"]]]},{"name":"Value","comment":" ","args":[],"cases":[["Persistent",["String.String"]],["ExpiringFlash",["String.String"]],["NewFlash",["String.String"]]]}],"aliases":[{"name":"Decoder","comment":" ","args":["decoded"],"type":"Json.Decode.Decoder decoded"}],"values":[{"name":"clearFlashCookies","comment":" ","type":"Dict.Dict String.String String.String -> Dict.Dict String.String String.String"},{"name":"empty","comment":" ","type":"Server.Session.Session"},{"name":"expectSession","comment":" ","type":"{ name : String.String, secrets : DataSource.DataSource (List.List String.String), sameSite : String.String } -> Server.Request.Parser request -> (request -> Result.Result () Server.Session.Session -> DataSource.DataSource ( Server.Session.Session, Server.Response.Response data errorPage )) -> Server.Request.Parser (DataSource.DataSource (Server.Response.Response data errorPage))"},{"name":"flashPrefix","comment":" ","type":"String.String"},{"name":"get","comment":" ","type":"String.String -> Server.Session.Session -> Maybe.Maybe String.String"},{"name":"insert","comment":" ","type":"String.String -> String.String -> Server.Session.Session -> Server.Session.Session"},{"name":"remove","comment":" ","type":"String.String -> Server.Session.Session -> Server.Session.Session"},{"name":"setValues","comment":" ","type":"Server.Session.Session -> Json.Encode.Value"},{"name":"succeed","comment":" ","type":"constructor -> Server.Session.Decoder constructor"},{"name":"unwrap","comment":" ","type":"Server.Session.Value -> String.String"},{"name":"update","comment":" ","type":"String.String -> (Maybe.Maybe String.String -> Maybe.Maybe String.String) -> Server.Session.Session -> Server.Session.Session"},{"name":"withFlash","comment":" ","type":"String.String -> String.String -> Server.Session.Session -> Server.Session.Session"},{"name":"withSession","comment":" ","type":"{ name : String.String, secrets : DataSource.DataSource (List.List String.String), sameSite : String.String } -> Server.Request.Parser request -> (request -> Result.Result () (Maybe.Maybe Server.Session.Session) -> DataSource.DataSource ( Server.Session.Session, Server.Response.Response data errorPage )) -> Server.Request.Parser (DataSource.DataSource (Server.Response.Response data errorPage))"}],"binops":[]},{"name":"Server.SetCookie","comment":" \n\n\n\n@docs SetCookie, SameSite\n@docs withImmediateExpiration, httpOnly, nonSecure, setCookie, withDomain, withExpiration, withMaxAge, withPath, withSameSite\n\n@docs toString\n\n","unions":[{"name":"SameSite","comment":" ","args":[],"cases":[["Strict",[]],["Lax",[]],["None",[]]]}],"aliases":[{"name":"SetCookie","comment":" ","args":[],"type":"{ name : String.String, value : String.String, expiration : Maybe.Maybe Time.Posix, httpOnly : Basics.Bool, maxAge : Maybe.Maybe Basics.Int, path : Maybe.Maybe String.String, domain : Maybe.Maybe String.String, secure : Basics.Bool, sameSite : Maybe.Maybe Server.SetCookie.SameSite }"}],"values":[{"name":"httpOnly","comment":" ","type":"Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"nonSecure","comment":" Secure (only sent over https, or localhost on http) is the default. This overrides that and\nremoves the `Secure` attribute from the cookie.\n","type":"Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"setCookie","comment":" ","type":"String.String -> String.String -> Server.SetCookie.SetCookie"},{"name":"toString","comment":" ","type":"Server.SetCookie.SetCookie -> String.String"},{"name":"withDomain","comment":" ","type":"String.String -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withExpiration","comment":" ","type":"Time.Posix -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withImmediateExpiration","comment":" ","type":"Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withMaxAge","comment":" ","type":"Basics.Int -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withPath","comment":" ","type":"String.String -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withSameSite","comment":" The default SameSite policy is Lax if one is not explicitly set. See the SameSite section in .\n","type":"Server.SetCookie.SameSite -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"}],"binops":[]}] \ No newline at end of file +[{"name":"ApiRoute","comment":" ApiRoute's are defined in `src/Api.elm` and are a way to generate files, like RSS feeds, sitemaps, or any text-based file that you output with an Elm function! You get access\nto a DataSource so you can pull in HTTP data, etc. Because ApiRoutes don't hydrate into Elm apps (like pages in elm-pages do), you can pull in as much data as you want in\nthe DataSource for your ApiRoutes, and it won't effect the payload size. Instead, the size of an ApiRoute is just the content you output for that route.\n\nIn a future release, ApiRoutes may be able to run at request-time in a serverless function, allowing you to use pure Elm code to create dynamic APIs, and even pulling in data from\nDataSources dynamically.\n\n@docs ApiRoute, ApiRouteBuilder, Response\n\n@docs capture, literal, slash, succeed\n\n\n## Pre-Rendering\n\n@docs single, preRender\n\n\n## Server Rendering\n\n@docs preRenderWithFallback, serverRender\n\n\n## Including Head Tags\n\n@docs withGlobalHeadTags\n\n\n## Internals\n\n@docs toJson, getBuildTimeRoutes, getGlobalHeadTagsDataSource\n\n","unions":[],"aliases":[{"name":"ApiRoute","comment":" ","args":["response"],"type":"Internal.ApiRoute.ApiRoute response"},{"name":"ApiRouteBuilder","comment":" ","args":["a","constructor"],"type":"Internal.ApiRoute.ApiRouteBuilder a constructor"},{"name":"Response","comment":" ","args":[],"type":"Json.Encode.Value"}],"values":[{"name":"capture","comment":" ","type":"ApiRoute.ApiRouteBuilder (String.String -> a) constructor -> ApiRoute.ApiRouteBuilder a (String.String -> constructor)"},{"name":"getBuildTimeRoutes","comment":" For internal use by generated code. Not so useful in user-land.\n","type":"ApiRoute.ApiRoute response -> DataSource.DataSource (List.List String.String)"},{"name":"getGlobalHeadTagsDataSource","comment":" ","type":"ApiRoute.ApiRoute response -> Maybe.Maybe (DataSource.DataSource (List.List Head.Tag))"},{"name":"literal","comment":" ","type":"String.String -> ApiRoute.ApiRouteBuilder a constructor -> ApiRoute.ApiRouteBuilder a constructor"},{"name":"preRender","comment":" ","type":"(constructor -> DataSource.DataSource (List.List (List.List String.String))) -> ApiRoute.ApiRouteBuilder (DataSource.DataSource String.String) constructor -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"preRenderWithFallback","comment":" ","type":"(constructor -> DataSource.DataSource (List.List (List.List String.String))) -> ApiRoute.ApiRouteBuilder (DataSource.DataSource (Server.Response.Response Basics.Never Basics.Never)) constructor -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"serverRender","comment":" ","type":"ApiRoute.ApiRouteBuilder (Server.Request.Parser (DataSource.DataSource (Server.Response.Response Basics.Never Basics.Never))) constructor -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"single","comment":" ","type":"ApiRoute.ApiRouteBuilder (DataSource.DataSource String.String) (List.List String.String) -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"slash","comment":" ","type":"ApiRoute.ApiRouteBuilder a constructor -> ApiRoute.ApiRouteBuilder a constructor"},{"name":"succeed","comment":" ","type":"a -> ApiRoute.ApiRouteBuilder a (List.List String.String)"},{"name":"toJson","comment":" Turn the route into a pattern in JSON format. For internal uses.\n","type":"ApiRoute.ApiRoute response -> Json.Encode.Value"},{"name":"withGlobalHeadTags","comment":" Include head tags on every page's HTML.\n","type":"DataSource.DataSource (List.List Head.Tag) -> ApiRoute.ApiRoute response -> ApiRoute.ApiRoute response"}],"binops":[]},{"name":"DataSource","comment":" In an `elm-pages` app, each page can define a value `data` which is a `DataSource` that will be resolved **before** `init` is called. That means it is also available\nwhen the page's HTML is pre-rendered during the build step. You can also access the resolved data in `head` to use it for the page's SEO meta tags.\n\nA `DataSource` lets you pull in data from:\n\n - Local files ([`DataSource.File`](DataSource-File))\n - HTTP requests ([`DataSource.Http`](DataSource-Http))\n - Globs, i.e. listing out local files based on a pattern like `content/*.txt` ([`DataSource.Glob`](DataSource-Glob))\n - Ports, i.e. getting JSON data from running custom NodeJS, similar to a port in a vanilla Elm app except run at build-time in NodeJS, rather than at run-time in the browser ([`DataSource.Port`](DataSource-Port))\n - Hardcoded data (`DataSource.succeed \"Hello!\"`)\n - Or any combination of the above, using `DataSource.map2`, `DataSource.andThen`, or other combining/continuing helpers from this module\n\n\n## Where Does DataSource Data Come From?\n\nData from a `DataSource` is resolved when you load a page in the `elm-pages` dev server, or when you run `elm-pages build`.\n\nBecause `elm-pages` hydrates into a full Elm single-page app, it does need the data in order to initialize the Elm app.\nSo why not just get the data the old-fashioned way, with `elm/http`, for example?\n\nA few reasons:\n\n1. DataSource's allow you to pull in data that you wouldn't normally be able to access from an Elm app, like local files, or listings of files in a folder. Not only that, but the dev server knows to automatically hot reload the data when the files it depends on change, so you can edit the files you used in your DataSource and see the page hot reload as you save!\n2. Because `elm-pages` has a build step, you know that your `DataSource.Http` requests succeeded, your decoders succeeded, your custom DataSource validations succeeded, and everything went smoothly. If something went wrong, you get a build failure and can deal with the issues before the site goes live. That means your users won't see those errors, and as a developer you don't need to handle those error cases in your code! Think of it as \"parse, don't validate\", but for your entire build.\n3. You don't have to worry about an API being down, or hitting it repeatedly. You can build in data and it will end up as JSON files served up with all the other assets of your site. If your CDN (static site host) is down, then the rest of your site is probably down anyway. If your site host is up, then so is all of your `DataSource` data. Also, it will be served up extremely quickly without needing to wait for any database queries to be performed, `andThen` requests to be resolved, etc., because all of that work and waiting was done at build-time!\n4. You can pre-render pages, including the SEO meta tags, with all that rich, well-typed Elm data available! That's something you can't accomplish with a vanilla Elm app, and it's one of the main use cases for elm-pages.\n\n\n## Mental Model\n\nYou can think of a DataSource as a declarative (not imperative) definition of data. It represents where to get the data from, and how to transform it (map, combine with other DataSources, etc.).\n\nEven though an HTTP request is non-deterministic, you should think of it that way as much as possible with a DataSource because elm-pages will only perform a given DataSource.Http request once, and\nit will share the result between any other DataSource.Http requests that have the exact same URL, Method, Body, and Headers.\n\nSo calling a function to increment a counter on a server through an HTTP request would not be a good fit for a `DataSource`. Let's imagine we have an HTTP endpoint that gives these stateful results when called repeatedly:\n\n\n-> Returns 1\n\n-> Returns 2\n\n-> Returns 3\n\nIf we define a `DataSource` that hits that endpoint:\n\n data =\n DataSource.Http.get\n \"https://my-api.example.com/increment-counter\"\n Decode.int\n\nNo matter how many places we use that `DataSource`, its response will be \"locked in\" (let's say the response was `3`, then every page would have the same value of `3` for that request).\n\nSo even though HTTP requests, JavaScript code, etc. can be non-deterministic, a `DataSource` always represents a single snapshot of a resource, and those values will be re-used as if they were a deterministic, declarative resource.\nSo it's best to use that mental model to avoid confusion.\n\n\n## Basics\n\n@docs DataSource\n\n@docs map, succeed, fail\n\n@docs fromResult\n\n\n## Chaining Requests\n\n@docs andThen, resolve, combine\n\n@docs andMap\n\n@docs map2, map3, map4, map5, map6, map7, map8, map9\n\n","unions":[],"aliases":[{"name":"DataSource","comment":" A DataSource represents data that will be gathered at build time. Multiple `DataSource`s can be combined together using the `mapN` functions,\nvery similar to how you can manipulate values with Json Decoders in Elm.\n","args":["value"],"type":"Pages.StaticHttpRequest.RawRequest value"}],"values":[{"name":"andMap","comment":" A helper for combining `DataSource`s in pipelines.\n","type":"DataSource.DataSource a -> DataSource.DataSource (a -> b) -> DataSource.DataSource b"},{"name":"andThen","comment":" Build off of the response from a previous `DataSource` request to build a follow-up request. You can use the data\nfrom the previous response to build up the URL, headers, etc. that you send to the subsequent request.\n\n import DataSource\n import Json.Decode as Decode exposing (Decoder)\n\n licenseData : DataSource String\n licenseData =\n DataSource.Http.get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-pages\")\n (Decode.at [ \"license\", \"url\" ] Decode.string)\n |> DataSource.andThen\n (\\licenseUrl ->\n DataSource.Http.get (Secrets.succeed licenseUrl) (Decode.field \"description\" Decode.string)\n )\n\n","type":"(a -> DataSource.DataSource b) -> DataSource.DataSource a -> DataSource.DataSource b"},{"name":"combine","comment":" Turn a list of `StaticHttp.Request`s into a single one.\n\n import DataSource\n import Json.Decode as Decode exposing (Decoder)\n\n type alias Pokemon =\n { name : String\n , sprite : String\n }\n\n pokemonDetailRequest : StaticHttp.Request (List Pokemon)\n pokemonDetailRequest =\n StaticHttp.get\n (Secrets.succeed \"https://pokeapi.co/api/v2/pokemon/?limit=3\")\n (Decode.field \"results\"\n (Decode.list\n (Decode.map2 Tuple.pair\n (Decode.field \"name\" Decode.string)\n (Decode.field \"url\" Decode.string)\n |> Decode.map\n (\\( name, url ) ->\n StaticHttp.get (Secrets.succeed url)\n (Decode.at\n [ \"sprites\", \"front_default\" ]\n Decode.string\n |> Decode.map (Pokemon name)\n )\n )\n )\n )\n )\n |> StaticHttp.andThen StaticHttp.combine\n\n","type":"List.List (DataSource.DataSource value) -> DataSource.DataSource (List.List value)"},{"name":"fail","comment":" Stop the StaticHttp chain with the given error message. If you reach a `fail` in your request,\nyou will get a build error. Or in the dev server, you will see the error message in an overlay in your browser (and in\nthe terminal).\n","type":"String.String -> DataSource.DataSource a"},{"name":"fromResult","comment":" Turn an Err into a DataSource failure.\n","type":"Result.Result String.String value -> DataSource.DataSource value"},{"name":"map","comment":" Transform a request into an arbitrary value. The same underlying HTTP requests will be performed during the build\nstep, but mapping allows you to change the resulting values by applying functions to the results.\n\nA common use for this is to map your data into your elm-pages view:\n\n import DataSource\n import Json.Decode as Decode exposing (Decoder)\n\n view =\n DataSource.Http.get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-pages\")\n (Decode.field \"stargazers_count\" Decode.int)\n |> DataSource.map\n (\\stars ->\n { view =\n \\model viewForPage ->\n { title = \"Current stars: \" ++ String.fromInt stars\n , body = Html.text <| \"⭐️ \" ++ String.fromInt stars\n , head = []\n }\n }\n )\n\n","type":"(a -> b) -> DataSource.DataSource a -> DataSource.DataSource b"},{"name":"map2","comment":" Like map, but it takes in two `Request`s.\n\n view siteMetadata page =\n StaticHttp.map2\n (\\elmPagesStars elmMarkdownStars ->\n { view =\n \\model viewForPage ->\n { title = \"Repo Stargazers\"\n , body = starsView elmPagesStars elmMarkdownStars\n }\n , head = head elmPagesStars elmMarkdownStars\n }\n )\n (get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-pages\")\n (Decode.field \"stargazers_count\" Decode.int)\n )\n (get\n (Secrets.succeed \"https://api.github.com/repos/dillonkearns/elm-markdown\")\n (Decode.field \"stargazers_count\" Decode.int)\n )\n\n","type":"(a -> b -> c) -> DataSource.DataSource a -> DataSource.DataSource b -> DataSource.DataSource c"},{"name":"map3","comment":" ","type":"(value1 -> value2 -> value3 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource valueCombined"},{"name":"map4","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource valueCombined"},{"name":"map5","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource valueCombined"},{"name":"map6","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource valueCombined"},{"name":"map7","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource value7 -> DataSource.DataSource valueCombined"},{"name":"map8","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource value7 -> DataSource.DataSource value8 -> DataSource.DataSource valueCombined"},{"name":"map9","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> value9 -> valueCombined) -> DataSource.DataSource value1 -> DataSource.DataSource value2 -> DataSource.DataSource value3 -> DataSource.DataSource value4 -> DataSource.DataSource value5 -> DataSource.DataSource value6 -> DataSource.DataSource value7 -> DataSource.DataSource value8 -> DataSource.DataSource value9 -> DataSource.DataSource valueCombined"},{"name":"resolve","comment":" Helper to remove an inner layer of Request wrapping.\n","type":"DataSource.DataSource (List.List (DataSource.DataSource value)) -> DataSource.DataSource (List.List value)"},{"name":"succeed","comment":" This is useful for prototyping with some hardcoded data, or for having a view that doesn't have any StaticHttp data.\n\n import DataSource\n\n view :\n List ( PagePath, Metadata )\n ->\n { path : PagePath\n , frontmatter : Metadata\n }\n ->\n StaticHttp.Request\n { view : Model -> View -> { title : String, body : Html Msg }\n , head : List (Head.Tag Pages.PathKey)\n }\n view siteMetadata page =\n StaticHttp.succeed\n { view =\n \\model viewForPage ->\n mainView model viewForPage\n , head = head page.frontmatter\n }\n\n","type":"a -> DataSource.DataSource a"}],"binops":[]},{"name":"DataSource.Env","comment":"\n\n@docs get, expect\n\n","unions":[],"aliases":[],"values":[{"name":"expect","comment":" ","type":"String.String -> DataSource.DataSource String.String"},{"name":"get","comment":" ","type":"String.String -> DataSource.DataSource (Maybe.Maybe String.String)"}],"binops":[]},{"name":"DataSource.File","comment":" This module lets you read files from the local filesystem as a [`DataSource`](DataSource#DataSource).\nFile paths are relative to the root of your `elm-pages` project (next to the `elm.json` file and `src/` directory).\n\n\n## Files With Frontmatter\n\nFrontmatter is a convention used to keep metadata at the top of a file between `---`'s.\n\nFor example, you might have a file called `blog/hello-world.md` with this content:\n\n```markdown\n---\ntitle: Hello, World!\ntags: elm\n---\nHey there! This is my first post :)\n```\n\nThe frontmatter is in the [YAML format](https://en.wikipedia.org/wiki/YAML) here. You can also use JSON in your elm-pages frontmatter.\n\n```markdown\n---\n{\"title\": \"Hello, World!\", \"tags\": \"elm\"}\n---\nHey there! This is my first post :)\n```\n\nWhether it's YAML or JSON, you use an `Decode` to decode your frontmatter, so it feels just like using\nplain old JSON in Elm.\n\n@docs bodyWithFrontmatter, bodyWithoutFrontmatter, onlyFrontmatter\n\n\n## Reading Files Without Frontmatter\n\n@docs jsonFile, rawFile\n\n","unions":[],"aliases":[],"values":[{"name":"bodyWithFrontmatter","comment":"\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n\n blogPost : DataSource BlogPostMetadata\n blogPost =\n File.bodyWithFrontmatter blogPostDecoder\n \"blog/hello-world.md\"\n\n type alias BlogPostMetadata =\n { body : String\n , title : String\n , tags : List String\n }\n\n blogPostDecoder : String -> Decoder BlogPostMetadata\n blogPostDecoder body =\n Decode.map2 (BlogPostMetadata body)\n (Decode.field \"title\" Decode.string)\n (Decode.field \"tags\" tagsDecoder)\n\n tagsDecoder : Decoder (List String)\n tagsDecoder =\n Decode.map (String.split \" \")\n Decode.string\n\nThis will give us a DataSource that results in the following value:\n\n value =\n { body = \"Hey there! This is my first post :)\"\n , title = \"Hello, World!\"\n , tags = [ \"elm\" ]\n }\n\nIt's common to parse the body with a markdown parser or other format.\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n import Html exposing (Html)\n\n example :\n DataSource\n { title : String\n , body : List (Html msg)\n }\n example =\n File.bodyWithFrontmatter\n (\\markdownString ->\n Decode.map2\n (\\title renderedMarkdown ->\n { title = title\n , body = renderedMarkdown\n }\n )\n (Decode.field \"title\" Decode.string)\n (markdownString\n |> markdownToView\n |> Decode.fromResult\n )\n )\n \"foo.md\"\n\n markdownToView :\n String\n -> Result String (List (Html msg))\n markdownToView markdownString =\n markdownString\n |> Markdown.Parser.parse\n |> Result.mapError (\\_ -> \"Markdown error.\")\n |> Result.andThen\n (\\blocks ->\n Markdown.Renderer.render\n Markdown.Renderer.defaultHtmlRenderer\n blocks\n )\n\n","type":"(String.String -> Json.Decode.Decoder frontmatter) -> String.String -> DataSource.DataSource frontmatter"},{"name":"bodyWithoutFrontmatter","comment":" Same as `bodyWithFrontmatter` except it doesn't include the frontmatter.\n\nFor example, if you have a file called `blog/hello-world.md` with\n\n```markdown\n---\ntitle: Hello, World!\ntags: elm\n---\nHey there! This is my first post :)\n```\n\n import DataSource exposing (DataSource)\n\n data : DataSource String\n data =\n bodyWithoutFrontmatter \"blog/hello-world.md\"\n\nThen data will yield the value `\"Hey there! This is my first post :)\"`.\n\n","type":"String.String -> DataSource.DataSource String.String"},{"name":"jsonFile","comment":" Read a file as JSON.\n\nThe Decode will strip off any unused JSON data.\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n\n sourceDirectories : DataSource (List String)\n sourceDirectories =\n File.jsonFile\n (Decode.field\n \"source-directories\"\n (Decode.list Decode.string)\n )\n \"elm.json\"\n\n","type":"Json.Decode.Decoder a -> String.String -> DataSource.DataSource a"},{"name":"onlyFrontmatter","comment":" Same as `bodyWithFrontmatter` except it doesn't include the body.\n\nThis is often useful when you're aggregating data, for example getting a listing of blog posts and need to extract\njust the metadata.\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n\n blogPost : DataSource BlogPostMetadata\n blogPost =\n File.onlyFrontmatter\n blogPostDecoder\n \"blog/hello-world.md\"\n\n type alias BlogPostMetadata =\n { title : String\n , tags : List String\n }\n\n blogPostDecoder : Decoder BlogPostMetadata\n blogPostDecoder =\n Decode.map2 BlogPostMetadata\n (Decode.field \"title\" Decode.string)\n (Decode.field \"tags\" (Decode.list Decode.string))\n\nIf you wanted to use this to get this metadata for all blog posts in a folder, you could use\nthe [`DataSource`](DataSource) API along with [`DataSource.Glob`](DataSource.Glob).\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n import Decode as Decode exposing (Decoder)\n\n blogPostFiles : DataSource (List String)\n blogPostFiles =\n Glob.succeed identity\n |> Glob.captureFilePath\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.match Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\n allMetadata : DataSource (List BlogPostMetadata)\n allMetadata =\n blogPostFiles\n |> DataSource.map\n (List.map\n (File.onlyFrontmatter\n blogPostDecoder\n )\n )\n |> DataSource.resolve\n\n","type":"Json.Decode.Decoder frontmatter -> String.String -> DataSource.DataSource frontmatter"},{"name":"rawFile","comment":" Get the raw file content. Unlike the frontmatter helpers in this module, this function will not strip off frontmatter if there is any.\n\nThis is the function you want if you are reading in a file directly. For example, if you read in a CSV file, a raw text file, or any other file that doesn't\nhave frontmatter.\n\nThere's a special function for reading in JSON files, [`jsonFile`](#jsonFile). If you're reading a JSON file then be sure to\nuse `jsonFile` to get the benefits of the `Decode` here.\n\nYou could read a file called `hello.txt` in your root project directory like this:\n\n import DataSource exposing (DataSource)\n import DataSource.File as File\n\n elmJsonFile : DataSource String\n elmJsonFile =\n File.rawFile \"hello.txt\"\n\n","type":"String.String -> DataSource.DataSource String.String"}],"binops":[]},{"name":"DataSource.Glob","comment":"\n\n@docs Glob\n\nThis 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\nfor ways you can combine and map `DataSource`s.\n\nA 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`\n, then you could use that glob pattern in most shells to refer to each of those files.\n\nWith the `DataSource.Glob` API, you could get all of those files like so:\n\n import DataSource exposing (DataSource)\n\n blogPostsGlob : DataSource (List String)\n blogPostsGlob =\n Glob.succeed (\\slug -> slug)\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nLet's say you have these files locally:\n\n```shell\n- elm.json\n- src/\n- content/\n - blog/\n - first-post.md\n - second-post.md\n```\n\nWe would end up with a `DataSource` like this:\n\n DataSource.succeed [ \"first-post\", \"second-post\" ]\n\nOf course, if you add or remove matching files, the DataSource will get those new files (unlike `DataSource.succeed`). That's why we have Glob!\n\nYou can even see the `elm-pages dev` server will automatically flow through any added/removed matching files with its hot module reloading.\n\nBut why did we get `\"first-post\"` instead of a full file path, like `\"content/blog/first-post.md\"`? That's the difference between\n`capture` and `match`.\n\n\n## Capture and Match\n\nThere are two functions for building up a Glob pattern: `capture` and `match`.\n\n`capture` and `match` both build up a `Glob` pattern that will match 0 or more files on your local file system.\nThere will be one argument for every `capture` in your pipeline, whereas `match` does not apply any arguments.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n blogPostsGlob : DataSource (List String)\n blogPostsGlob =\n Glob.succeed (\\slug -> slug)\n -- no argument from this, but we will only\n -- match files that begin with `content/blog/`\n |> Glob.match (Glob.literal \"content/blog/\")\n -- we get the value of the `wildcard`\n -- as the slug argument\n |> Glob.capture Glob.wildcard\n -- no argument from this, but we will only\n -- match files that end with `.md`\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nSo to understand _which_ files will match, you can ignore whether you are using `capture` or `match` and just read\nthe patterns you're using in order to understand what will match. To understand what Elm data type you will get\n_for each matching file_, you need to see which parts are being captured and how each of those captured values are being\nused in the function you use in `Glob.succeed`.\n\n@docs capture, match\n\n`capture` is a lot like building up a JSON decoder with a pipeline.\n\nLet's try our blogPostsGlob from before, but change every `match` to `capture`.\n\n import DataSource exposing (DataSource)\n\n blogPostsGlob :\n DataSource\n (List\n { filePath : String\n , slug : String\n }\n )\n blogPostsGlob =\n Glob.succeed\n (\\capture1 capture2 capture3 ->\n { filePath = capture1 ++ capture2 ++ capture3\n , slug = capture2\n }\n )\n |> Glob.capture (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.capture (Glob.literal \".md\")\n |> Glob.toDataSource\n\nNotice that we now need 3 arguments at the start of our pipeline instead of 1. That's because\nwe apply 1 more argument every time we do a `Glob.capture`, much like `Json.Decode.Pipeline.required`, or other pipeline APIs.\n\nNow we actually have the full file path of our files. But having that slug (like `first-post`) is also very helpful sometimes, so\nwe 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:\n\n DataSource.succeed\n [ { filePath = \"content/blog/first-post.md\"\n , slug = \"first-post\"\n }\n , { filePath = \"content/blog/second-post.md\"\n , slug = \"second-post\"\n }\n ]\n\nHaving the full file path lets us read in files. But concatenating it manually is tedious\nand error prone. That's what the [`captureFilePath`](#captureFilePath) helper is for.\n\n\n## Reading matching files\n\n@docs captureFilePath\n\nIn many cases you will want to take the matching files from a `Glob` and then read the body or frontmatter from matching files.\n\n\n## Reading Metadata for each Glob Match\n\nFor example, if we had files like this:\n\n```markdown\n---\ntitle: My First Post\n---\nThis is my first post!\n```\n\nThen we could read that title for our blog post list page using our `blogPosts` `DataSource` that we defined above.\n\n import DataSource.File\n import Json.Decode as Decode exposing (Decoder)\n\n titles : DataSource (List BlogPost)\n titles =\n blogPosts\n |> DataSource.map\n (List.map\n (\\blogPost ->\n DataSource.File.request\n blogPost.filePath\n (DataSource.File.frontmatter blogFrontmatterDecoder)\n )\n )\n |> DataSource.resolve\n\n type alias BlogPost =\n { title : String }\n\n blogFrontmatterDecoder : Decoder BlogPost\n blogFrontmatterDecoder =\n Decode.map BlogPost\n (Decode.field \"title\" Decode.string)\n\nThat will give us\n\n DataSource.succeed\n [ { title = \"My First Post\" }\n , { title = \"My Second Post\" }\n ]\n\n\n## Capturing Patterns\n\n@docs wildcard, recursiveWildcard\n\n\n## Capturing Specific Characters\n\n@docs int, digits\n\n\n## Matching a Specific Number of Files\n\n@docs expectUniqueMatch, expectUniqueMatchFromList\n\n\n## Glob Patterns\n\n@docs literal\n\n@docs map, succeed\n\n@docs oneOf\n\n@docs zeroOrMore, atLeastOne\n\n\n## Getting Glob Data from a DataSource\n\n@docs toDataSource\n\n\n### With Custom Options\n\n@docs toDataSourceWithOptions\n\n@docs defaultOptions, Options, Include\n\n","unions":[{"name":"Include","comment":" \n\n\n\n","args":[],"cases":[["OnlyFiles",[]],["OnlyFolders",[]],["FilesAndFolders",[]]]}],"aliases":[{"name":"Glob","comment":" A pattern to match local files and capture parts of the path into a nice Elm data type.\n","args":["a"],"type":"DataSource.Internal.Glob.Glob a"},{"name":"Options","comment":" Custom options you can pass in to run the glob with [`toDataSourceWithOptions`](#toDataSourceWithOptions).\n\n { includeDotFiles = Bool -- https://github.com/mrmlnc/fast-glob#dot\n , include = Include -- return results that are `OnlyFiles`, `OnlyFolders`, or both `FilesAndFolders` (default is `OnlyFiles`)\n , followSymbolicLinks = Bool -- https://github.com/mrmlnc/fast-glob#followsymboliclinks\n , caseSensitiveMatch = Bool -- https://github.com/mrmlnc/fast-glob#casesensitivematch\n , gitignore = Bool -- https://www.npmjs.com/package/globby#gitignore\n , maxDepth = Maybe Int -- https://github.com/mrmlnc/fast-glob#deep\n }\n\n","args":[],"type":"{ includeDotFiles : Basics.Bool, include : DataSource.Glob.Include, followSymbolicLinks : Basics.Bool, caseSensitiveMatch : Basics.Bool, gitignore : Basics.Bool, maxDepth : Maybe.Maybe Basics.Int }"}],"values":[{"name":"atLeastOne","comment":" ","type":"( ( String.String, a ), List.List ( String.String, a ) ) -> DataSource.Glob.Glob ( a, List.List a )"},{"name":"capture","comment":" Adds on to the glob pattern, and captures it in the resulting Elm match value. That means this both changes which\nfiles will match, and gives you the sub-match as Elm data for each matching file.\n\nExactly the same as `match` except it also captures the matched sub-pattern.\n\n type alias ArchivesArticle =\n { year : String\n , month : String\n , day : String\n , slug : String\n }\n\n archives : DataSource ArchivesArticle\n archives =\n Glob.succeed ArchivesArticle\n |> Glob.match (Glob.literal \"archive/\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nThe file `archive/1977/06/10/apple-2-released.md` will give us this match:\n\n matches : List ArchivesArticle\n matches =\n DataSource.succeed\n [ { year = 1977\n , month = 6\n , day = 10\n , slug = \"apple-2-released\"\n }\n ]\n\nWhen possible, it's best to grab data and turn it into structured Elm data when you have it. That way,\nyou don't end up with duplicate validation logic and data normalization, and your code will be more robust.\n\nIf you only care about getting the full matched file paths, you can use `match`. `capture` is very useful because\nyou can pick apart structured data as you build up your glob pattern. This follows the principle of\n[Parse, Don't Validate](https://elm-radio.com/episode/parse-dont-validate/).\n\n","type":"DataSource.Glob.Glob a -> DataSource.Glob.Glob (a -> value) -> DataSource.Glob.Glob value"},{"name":"captureFilePath","comment":"\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n blogPosts :\n DataSource\n (List\n { filePath : String\n , slug : String\n }\n )\n blogPosts =\n Glob.succeed\n (\\filePath slug ->\n { filePath = filePath\n , slug = slug\n }\n )\n |> Glob.captureFilePath\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nThis function does not change which files will or will not match. It just gives you the full matching\nfile path in your `Glob` pipeline.\n\nWhenever 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.\n\n","type":"DataSource.Glob.Glob (String.String -> value) -> DataSource.Glob.Glob value"},{"name":"defaultOptions","comment":" The default options used in [`toDataSource`](#toDataSource). To use a custom set of options, use [`toDataSourceWithOptions`](#toDataSourceWithOptions).\n","type":"DataSource.Glob.Options"},{"name":"digits","comment":" This is similar to [`wildcard`](#wildcard), but it will only match 1 or more digits (i.e. `[0-9]+`).\n\nSee [`int`](#int) for a convenience function to get an Int value instead of a String of digits.\n\n","type":"DataSource.Glob.Glob String.String"},{"name":"expectUniqueMatch","comment":" Sometimes you want to make sure there is a unique file matching a particular pattern.\nThis is a simple helper that will give you a `DataSource` error if there isn't exactly 1 matching file.\nIf there is exactly 1, then you successfully get back that single match.\n\nFor example, maybe you can have\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n findBlogBySlug : String -> DataSource String\n findBlogBySlug slug =\n Glob.succeed identity\n |> Glob.captureFilePath\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.capture (Glob.literal slug)\n |> Glob.match\n (Glob.oneOf\n ( ( \"\", () )\n , [ ( \"/index\", () ) ]\n )\n )\n |> Glob.match (Glob.literal \".md\")\n |> Glob.expectUniqueMatch\n\nIf we used `findBlogBySlug \"first-post\"` with these files:\n\n```markdown\n- blog/\n - first-post/\n - index.md\n```\n\nThis would give us:\n\n results : DataSource String\n results =\n DataSource.succeed \"blog/first-post/index.md\"\n\nIf we used `findBlogBySlug \"first-post\"` with these files:\n\n```markdown\n- blog/\n - first-post.md\n - first-post/\n - index.md\n```\n\nThen we will get a `DataSource` error saying `More than one file matched.` Keep in mind that `DataSource` failures\nin build-time routes will cause a build failure, giving you the opportunity to fix the problem before users see the issue,\nso it's ideal to make this kind of assertion rather than having fallback behavior that could silently cover up\nissues (like if we had instead ignored the case where there are two or more matching blog post files).\n\n","type":"DataSource.Glob.Glob a -> DataSource.DataSource a"},{"name":"expectUniqueMatchFromList","comment":" ","type":"List.List (DataSource.Glob.Glob a) -> DataSource.DataSource a"},{"name":"int","comment":" Same as [`digits`](#digits), but it safely turns the digits String into an `Int`.\n\nLeading 0's are ignored.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n slides : DataSource (List Int)\n slides =\n Glob.succeed identity\n |> Glob.match (Glob.literal \"slide-\")\n |> Glob.capture Glob.int\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nWith files\n\n```shell\n- slide-no-match.md\n- slide-.md\n- slide-1.md\n- slide-01.md\n- slide-2.md\n- slide-03.md\n- slide-4.md\n- slide-05.md\n- slide-06.md\n- slide-007.md\n- slide-08.md\n- slide-09.md\n- slide-10.md\n- slide-11.md\n```\n\nYields\n\n matches : DataSource (List Int)\n matches =\n DataSource.succeed\n [ 1\n , 1\n , 2\n , 3\n , 4\n , 5\n , 6\n , 7\n , 8\n , 9\n , 10\n , 11\n ]\n\nNote that neither `slide-no-match.md` nor `slide-.md` match.\nAnd both `slide-1.md` and `slide-01.md` match and turn into `1`.\n\n","type":"DataSource.Glob.Glob Basics.Int"},{"name":"literal","comment":" Match a literal part of a path. Can include `/`s.\n\nSome common uses include\n\n - The leading part of a pattern, to say \"starts with `content/blog/`\"\n - The ending part of a pattern, to say \"ends with `.md`\"\n - In-between wildcards, to say \"these dynamic parts are separated by `/`\"\n\n```elm\nimport DataSource exposing (DataSource)\nimport DataSource.Glob as Glob\n\nblogPosts =\n Glob.succeed\n (\\section slug ->\n { section = section, slug = slug }\n )\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n```\n\n","type":"String.String -> DataSource.Glob.Glob String.String"},{"name":"map","comment":" A `Glob` can be mapped. This can be useful for transforming a sub-match in-place.\n\nFor example, if you wanted to take the slugs for a blog post and make sure they are normalized to be all lowercase, you\ncould use\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n blogPostsGlob : DataSource (List String)\n blogPostsGlob =\n Glob.succeed (\\slug -> slug)\n |> Glob.match (Glob.literal \"content/blog/\")\n |> Glob.capture (Glob.wildcard |> Glob.map String.toLower)\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nIf you want to validate file formats, you can combine that with some `DataSource` helpers to turn a `Glob (Result String value)` into\na `DataSource (List value)`.\n\nFor example, you could take a date and parse it.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n example : DataSource (List ( String, String ))\n example =\n Glob.succeed\n (\\dateResult slug ->\n dateResult\n |> Result.map (\\okDate -> ( okDate, slug ))\n )\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.capture (Glob.recursiveWildcard |> Glob.map expectDateFormat)\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n |> DataSource.map (List.map DataSource.fromResult)\n |> DataSource.resolve\n\n expectDateFormat : List String -> Result String String\n expectDateFormat dateParts =\n case dateParts of\n [ year, month, date ] ->\n Ok (String.join \"-\" [ year, month, date ])\n\n _ ->\n Err \"Unexpected date format, expected yyyy/mm/dd folder structure.\"\n\n","type":"(a -> b) -> DataSource.Glob.Glob a -> DataSource.Glob.Glob b"},{"name":"match","comment":" Adds on to the glob pattern, but does not capture it in the resulting Elm match value. That means this changes which\nfiles will match, but does not change the Elm data type you get for each matching file.\n\nExactly the same as `capture` except it doesn't capture the matched sub-pattern.\n\n","type":"DataSource.Glob.Glob a -> DataSource.Glob.Glob value -> DataSource.Glob.Glob value"},{"name":"oneOf","comment":"\n\n import DataSource.Glob as Glob\n\n type Extension\n = Json\n | Yml\n\n type alias DataFile =\n { name : String\n , extension : String\n }\n\n dataFiles : DataSource (List DataFile)\n dataFiles =\n Glob.succeed DataFile\n |> Glob.match (Glob.literal \"my-data/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".\")\n |> Glob.capture\n (Glob.oneOf\n ( ( \"yml\", Yml )\n , [ ( \"json\", Json )\n ]\n )\n )\n\nIf we have the following files\n\n```shell\n- my-data/\n - authors.yml\n - events.json\n```\n\nThat gives us\n\n results : DataSource (List DataFile)\n results =\n DataSource.succeed\n [ { name = \"authors\"\n , extension = Yml\n }\n , { name = \"events\"\n , extension = Json\n }\n ]\n\nYou could also match an optional file path segment using `oneOf`.\n\n rootFilesMd : DataSource (List String)\n rootFilesMd =\n Glob.succeed (\\slug -> slug)\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match\n (Glob.oneOf\n ( ( \"\", () )\n , [ ( \"/index\", () ) ]\n )\n )\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\nWith these files:\n\n```markdown\n- blog/\n - first-post.md\n - second-post/\n - index.md\n```\n\nThis would give us:\n\n results : DataSource (List String)\n results =\n DataSource.succeed\n [ \"first-post\"\n , \"second-post\"\n ]\n\n","type":"( ( String.String, a ), List.List ( String.String, a ) ) -> DataSource.Glob.Glob a"},{"name":"recursiveWildcard","comment":" Matches any number of characters, including `/`, as long as it's the only thing in a path part.\n\nIn contrast, `wildcard` will never match `/`, so it only matches within a single path part.\n\nThis is the elm-pages equivalent of `**/*.txt` in standard shell syntax:\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n example : DataSource (List ( List String, String ))\n example =\n Glob.succeed Tuple.pair\n |> Glob.match (Glob.literal \"articles/\")\n |> Glob.capture Glob.recursiveWildcard\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".txt\")\n |> Glob.toDataSource\n\nWith these files:\n\n```shell\n- articles/\n - google-io-2021-recap.txt\n - archive/\n - 1977/\n - 06/\n - 10/\n - apple-2-announced.txt\n```\n\nWe would get the following matches:\n\n matches : DataSource (List ( List String, String ))\n matches =\n DataSource.succeed\n [ ( [ \"archive\", \"1977\", \"06\", \"10\" ], \"apple-2-announced\" )\n , ( [], \"google-io-2021-recap\" )\n ]\n\nNote that the recursive wildcard conveniently gives us a `List String`, where\neach String is a path part with no slashes (like `archive`).\n\nAnd also note that it matches 0 path parts into an empty list.\n\nIf we didn't include the `wildcard` after the `recursiveWildcard`, then we would only get\na single level of matches because it is followed by a file extension.\n\n example : DataSource (List String)\n example =\n Glob.succeed identity\n |> Glob.match (Glob.literal \"articles/\")\n |> Glob.capture Glob.recursiveWildcard\n |> Glob.match (Glob.literal \".txt\")\n\n matches : DataSource (List String)\n matches =\n DataSource.succeed\n [ \"google-io-2021-recap\"\n ]\n\nThis is usually not what is intended. Using `recursiveWildcard` is usually followed by a `wildcard` for this reason.\n\n","type":"DataSource.Glob.Glob (List.List String.String)"},{"name":"succeed","comment":" `succeed` is how you start a pipeline for a `Glob`. You will need one argument for each `capture` in your `Glob`.\n","type":"constructor -> DataSource.Glob.Glob constructor"},{"name":"toDataSource","comment":" In order to get match data from your glob, turn it into a `DataSource` with this function.\n","type":"DataSource.Glob.Glob a -> DataSource.DataSource (List.List a)"},{"name":"toDataSourceWithOptions","comment":" Same as toDataSource, but lets you set custom glob options. For example, to list folders instead of files,\n\n import DataSource.Glob as Glob exposing (OnlyFolders, defaultOptions)\n\n matchingFiles : Glob a -> DataSource (List a)\n matchingFiles glob =\n glob\n |> Glob.toDataSourceWithOptions { defaultOptions | include = OnlyFolders }\n\n","type":"DataSource.Glob.Options -> DataSource.Glob.Glob a -> DataSource.DataSource (List.List a)"},{"name":"wildcard","comment":" Matches anything except for a `/` in a file path. You may be familiar with this syntax from shells like bash\nwhere you can run commands like `rm client/*.js` to remove all `.js` files in the `client` directory.\n\nJust like a `*` glob pattern in bash, this `Glob.wildcard` function will only match within a path part. If you need to\nmatch 0 or more path parts like, see `recursiveWildcard`.\n\n import DataSource exposing (DataSource)\n import DataSource.Glob as Glob\n\n type alias BlogPost =\n { year : String\n , month : String\n , day : String\n , slug : String\n }\n\n example : DataSource (List BlogPost)\n example =\n Glob.succeed BlogPost\n |> Glob.match (Glob.literal \"blog/\")\n |> Glob.match Glob.wildcard\n |> Glob.match (Glob.literal \"-\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \"-\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \"/\")\n |> Glob.capture Glob.wildcard\n |> Glob.match (Glob.literal \".md\")\n |> Glob.toDataSource\n\n```shell\n\n- blog/\n - 2021-05-27/\n - first-post.md\n```\n\nThat will match to:\n\n results : DataSource (List BlogPost)\n results =\n DataSource.succeed\n [ { year = \"2021\"\n , month = \"05\"\n , day = \"27\"\n , slug = \"first-post\"\n }\n ]\n\nNote that we can \"destructure\" the date part of this file path in the format `yyyy-mm-dd`. The `wildcard` matches\nwill match _within_ a path part (think between the slashes of a file path). `recursiveWildcard` can match across path parts.\n\n","type":"DataSource.Glob.Glob String.String"},{"name":"zeroOrMore","comment":" ","type":"List.List String.String -> DataSource.Glob.Glob (Maybe.Maybe String.String)"}],"binops":[]},{"name":"DataSource.Http","comment":" `DataSource.Http` requests are an alternative to doing Elm HTTP requests the traditional way using the `elm/http` package.\n\nThe key differences are:\n\n - `DataSource.Http.Request`s are performed once at build time (`Http.Request`s are performed at runtime, at whenever point you perform them)\n - `DataSource.Http.Request`s have a built-in `DataSource.andThen` that allows you to perform follow-up requests without using tasks\n\n\n## Scenarios where DataSource.Http is a good fit\n\nIf you need data that is refreshed often you may want to do a traditional HTTP request with the `elm/http` package.\nThe kinds of situations that are served well by static HTTP are with data that updates moderately frequently or infrequently (or never).\nA common pattern is to trigger a new build when data changes. Many JAMstack services\nallow you to send a WebHook to your host (for example, Netlify is a good static file host that supports triggering builds with webhooks). So\nyou may want to have your site rebuild everytime your calendar feed has an event added, or whenever a page or article is added\nor updated on a CMS service like Contentful.\n\nIn scenarios like this, you can serve data that is just as up-to-date as it would be using `elm/http`, but you get the performance\ngains of using `DataSource.Http.Request`s as well as the simplicity and robustness that comes with it. Read more about these benefits\nin [this article introducing DataSource.Http requests and some concepts around it](https://elm-pages.com/blog/static-http).\n\n\n## Scenarios where DataSource.Http is not a good fit\n\n - Data that is specific to the logged-in user\n - Data that needs to be the very latest and changes often (for example, sports scores)\n\n@docs RequestDetails\n@docs get, request\n\n\n## Decoding Request Body\n\n@docs Expect, expectString, expectJson, expectBytes, expectWhatever\n\n\n## Expecting Responses\n\n@docs Response, Metadata, Error\n\n@docs expectStringResponse, expectBytesResponse\n\n\n## Building a DataSource.Http Request Body\n\nThe way you build a body is analogous to the `elm/http` package. Currently, only `emptyBody` and\n`stringBody` are supported. If you have a use case that calls for a different body type, please open a Github issue\nand describe your use case!\n\n@docs Body, emptyBody, stringBody, jsonBody\n\n\n## Uncached Requests\n\n@docs uncachedRequest\n\n","unions":[{"name":"Error","comment":" ","args":[],"cases":[["BadUrl",["String.String"]],["Timeout",[]],["NetworkError",[]],["BadStatus",["DataSource.Http.Metadata","String.String"]],["BadBody",["String.String"]]]},{"name":"Expect","comment":" Analogous to the `Expect` type in the `elm/http` package. This represents how you will process the data that comes\nback in your DataSource.Http request.\n\nYou can derive `ExpectJson` from `ExpectString`. Or you could build your own helper to process the String\nas XML, for example, or give an `elm-pages` build error if the response can't be parsed as XML.\n\n","args":["value"],"cases":[]},{"name":"Response","comment":" ","args":["body"],"cases":[["BadUrl_",["String.String"]],["Timeout_",[]],["NetworkError_",[]],["BadStatus_",["DataSource.Http.Metadata","body"]],["GoodStatus_",["DataSource.Http.Metadata","body"]]]}],"aliases":[{"name":"Body","comment":" A body for a DataSource.Http request.\n","args":[],"type":"Pages.Internal.StaticHttpBody.Body"},{"name":"Metadata","comment":" ","args":[],"type":"{ url : String.String, statusCode : Basics.Int, statusText : String.String, headers : Dict.Dict String.String String.String }"},{"name":"RequestDetails","comment":" The full details to perform a DataSource.Http request.\n","args":[],"type":"{ url : String.String, method : String.String, headers : List.List ( String.String, String.String ), body : DataSource.Http.Body }"}],"values":[{"name":"emptyBody","comment":" Build an empty body for a DataSource.Http request. See [elm/http's `Http.emptyBody`](https://package.elm-lang.org/packages/elm/http/latest/Http#emptyBody).\n","type":"DataSource.Http.Body"},{"name":"expectBytes","comment":" ","type":"Bytes.Decode.Decoder value -> DataSource.Http.Expect value"},{"name":"expectBytesResponse","comment":" ","type":"DataSource.Http.Expect (DataSource.Http.Response Bytes.Bytes)"},{"name":"expectJson","comment":" Handle the incoming response as JSON and don't optimize the asset and strip out unused values.\nBe sure to use the `DataSource.Http.request` function if you want an optimized request that\nstrips out unused JSON to optimize your asset size. This function makes sense to use for things like a GraphQL request\nwhere the JSON payload is already trimmed down to the data you explicitly requested.\n\nIf the function you pass to `expectString` yields an `Err`, then you will get a build error that will\nfail your `elm-pages` build and print out the String from the `Err`.\n\n","type":"Json.Decode.Decoder value -> DataSource.Http.Expect value"},{"name":"expectString","comment":" Gives the HTTP response body as a raw String.\n\n import DataSource exposing (DataSource)\n import DataSource.Http\n\n request : DataSource String\n request =\n DataSource.Http.request\n { url = \"https://example.com/file.txt\"\n , method = \"GET\"\n , headers = []\n , body = DataSource.Http.emptyBody\n }\n DataSource.Http.expectString\n\n","type":"DataSource.Http.Expect String.String"},{"name":"expectStringResponse","comment":" ","type":"DataSource.Http.Expect (DataSource.Http.Response String.String)"},{"name":"expectWhatever","comment":" ","type":"value -> DataSource.Http.Expect value"},{"name":"get","comment":" A simplified helper around [`DataSource.Http.request`](#request), which builds up a DataSource.Http GET request.\n\n import DataSource\n import DataSource.Http\n import Json.Decode as Decode exposing (Decoder)\n\n getRequest : DataSource Int\n getRequest =\n DataSource.Http.get\n \"https://api.github.com/repos/dillonkearns/elm-pages\"\n (Decode.field \"stargazers_count\" Decode.int)\n\n","type":"String.String -> Json.Decode.Decoder a -> DataSource.DataSource a"},{"name":"jsonBody","comment":" Builds a JSON body for a DataSource.Http request. See [elm/http's `Http.jsonBody`](https://package.elm-lang.org/packages/elm/http/latest/Http#jsonBody).\n","type":"Json.Encode.Value -> DataSource.Http.Body"},{"name":"request","comment":" ","type":"DataSource.Http.RequestDetails -> DataSource.Http.Expect a -> DataSource.DataSource a"},{"name":"stringBody","comment":" Builds a string body for a DataSource.Http request. See [elm/http's `Http.stringBody`](https://package.elm-lang.org/packages/elm/http/latest/Http#stringBody).\n\nNote from the `elm/http` docs:\n\n> The first argument is a [MIME type](https://en.wikipedia.org/wiki/Media_type) of the body. Some servers are strict about this!\n\n","type":"String.String -> String.String -> DataSource.Http.Body"},{"name":"uncachedRequest","comment":" ","type":"DataSource.Http.RequestDetails -> DataSource.Http.Expect a -> DataSource.DataSource a"}],"binops":[]},{"name":"DataSource.Port","comment":"\n\n@docs get\n\n","unions":[],"aliases":[],"values":[{"name":"get","comment":" In a vanilla Elm application, ports let you either send or receive JSON data between your Elm application and the JavaScript context in the user's browser at runtime.\n\nWith `DataSource.Port`, you send and receive JSON to JavaScript running in NodeJS during build-time. This means that you can call shell scripts, or run NPM packages that are installed, or anything else you could do with NodeJS.\n\nA `DataSource.Port` will call an async JavaScript function with the given name. The function receives the input JSON value, and the Decoder is used to decode the return value of the async function.\n\nHere is the Elm code and corresponding JavaScript definition for getting an environment variable (or a build error if it isn't found).\n\n import DataSource exposing (DataSource)\n import DataSource.Port\n import Json.Encode\n import OptimizedDecoder as Decode\n\n data : DataSource String\n data =\n DataSource.Port.get \"environmentVariable\"\n (Json.Encode.string \"EDITOR\")\n Decode.string\n\n -- will resolve to \"VIM\" if you run `EDITOR=vim elm-pages dev`\n\n```javascript\nconst kleur = require(\"kleur\");\n\n\nmodule.exports =\n /**\n * @param { unknown } fromElm\n * @returns { Promise }\n */\n {\n environmentVariable: async function (name) {\n const result = process.env[name];\n if (result) {\n return result;\n } else {\n throw `No environment variable called ${kleur\n .yellow()\n .underline(name)}\\n\\nAvailable:\\n\\n${Object.keys(process.env).join(\n \"\\n\"\n )}`;\n }\n },\n }\n```\n\n\n## Error Handling\n\n`port-data-source.js`\n\nAny time you throw an exception from a DataSource.Port definition, it will result in a build error in your `elm-pages build` or dev server. In the example above, if the environment variable\nis not found it will result in a build failure. Notice that the NPM package `kleur` is being used in this example to add color to the output for that build error. You can use any tool you\nprefer to add ANSI color codes within the error string in an exception and it will show up with color output in the build output and dev server.\n\n\n## Performance\n\nAs with any JavaScript or NodeJS code, avoid doing blocking IO operations. For example, avoid using `fs.readFileSync`, because blocking IO can slow down your elm-pages builds and dev server.\n\n","type":"String.String -> Json.Encode.Value -> Json.Decode.Decoder b -> DataSource.DataSource b"}],"binops":[]},{"name":"Form","comment":" One of the core features of elm-pages is helping you manage form data end-to-end, including\n\n - Presenting the HTML form with its fields\n - Maintaining client-side form state\n - Showing validation errors on the client-side\n - Receiving a form submission on the server-side\n - Using the exact same client-side validations on the server-side\n - Letting you run server-only Validations with DataSource's (things like checking for a unique username)\n\nBecause elm-pages is a framework, it has its own internal Model and Msg's. That means you, the user,\ncan offload some of the responsibility to elm-pages and build an interactive form with real-time\nclient-side state and validation errors without wiring up your own Model and Msg's to manage that\nstate. You define the source of truth for your form (how to parse it into data or errors), and\nelm-pages manages the state.\n\nLet's look at a sign-up form example.\n\n\n### Step 1 - Define the Form\n\nWhat to look for:\n\n**The field declarations**\n\nBelow the `Form.init` call you will find all of the form's fields declared with\n\n |> Form.field ...\n\nThese are the form's field declarations.\n\nThese fields each have individual validations. For example, `|> Field.required ...` means we'll get a validation\nerror if that field is empty (similar for checking the minimum password length).\n\nThere will be a corresponding parameter in the function we pass in to `Form.init` for every\nfield declaration (in this example, `\\email password passwordConfirmation -> ...`).\n\n**The `combine` validation**\n\nIn addition to the validation errors that individual fields can have independently (like\nrequired fields or minimum password length), we can also do _dependent validations_.\n\nWe use the [`Form.Validation`](Form-Validation) module to take each individual field and combine\nthem into a type and/or errors.\n\n**The `view`**\n\nTotally customizable. Uses [`Form.FieldView`](Form-FieldView) to render all of the fields declared.\n\n import DataSource exposing (DataSource)\n import ErrorPage exposing (ErrorPage)\n import Form\n import Form.Field as Field\n import Form.FieldView as FieldView\n import Form.Validation as Validation\n import Html exposing (Html)\n import Html.Attributes as Attr\n import Route\n import Server.Request as Request\n import Server.Response exposing (Response)\n\n type alias NewUser =\n { email : String\n , password : String\n }\n\n signupForm : Form.HtmlForm String NewUser () Msg\n signupForm =\n Form.init\n (\\email password passwordConfirmation ->\n { combine =\n Validation.succeed Login\n |> Validation.andMap email\n |> Validation.andMap\n (Validation.map2\n (\\pass confirmation ->\n if pass == confirmation then\n Validation.succeed pass\n\n else\n passwordConfirmation\n |> Validation.fail\n \"Must match password\"\n )\n password\n passwordConfirmation\n |> Validation.andThen identity\n )\n , view =\n \\info ->\n [ Html.label []\n [ fieldView info \"Email\" email\n , fieldView info \"Password\" password\n , fieldView info \"Confirm Password\" passwordConfirmation\n ]\n , Html.button []\n [ if info.isTransitioning then\n Html.text \"Signing Up...\"\n\n else\n Html.text \"Sign Up\"\n ]\n ]\n }\n )\n |> Form.field \"email\"\n (Field.text\n |> Field.required \"Required\"\n )\n |> Form.field \"password\"\n passwordField\n |> Form.field \"passwordConfirmation\"\n passwordField\n\n passwordField =\n Field.text\n |> Field.password\n |> Field.required \"Required\"\n |> Field.withClientValidation\n (\\password ->\n ( Just password\n , if String.length password < 4 then\n [ \"Must be at least 4 characters\" ]\n\n else\n []\n )\n )\n\n fieldView :\n Form.Context String data\n -> String\n -> Validation.Field String parsed FieldView.Input\n -> Html msg\n fieldView formState label field =\n Html.div []\n [ Html.label []\n [ Html.text (label ++ \" \")\n , field |> Form.FieldView.input []\n ]\n , (if formState.submitAttempted then\n formState.errors\n |> Form.errorsForField field\n |> List.map\n (\\error ->\n Html.li [] [ Html.text error ]\n )\n\n else\n []\n )\n |> Html.ul [ Attr.style \"color\" \"red\" ]\n ]\n\n\n### Step 2 - Render the Form's View\n\n view maybeUrl sharedModel app =\n { title = \"Sign Up\"\n , body =\n [ form\n |> Form.toDynamicTransition \"login\"\n |> Form.renderHtml [] Nothing app ()\n ]\n }\n\n\n### Step 3 - Handle Server-Side Form Submissions\n\n action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))\n action routeParams =\n Request.formData [ signupForm ]\n |> Request.map\n (\\signupResult ->\n case signupResult of\n Ok newUser ->\n newUser\n |> myCreateUserDataSource\n |> DataSource.map\n (\\() ->\n -- redirect to the home page\n -- after successful sign-up\n Route.redirectTo Route.Index\n )\n\n Err _ ->\n Route.redirectTo Route.Login\n |> DataSource.succeed\n )\n\n myCreateUserDataSource : DataSource ()\n myCreateUserDataSource =\n DataSource.fail\n \"TODO - make a database call to create a new user\"\n\n\n## Building a Form Parser\n\n@docs Form, HtmlForm, StyledHtmlForm, DoneForm\n\n@docs Response\n\n@docs init\n\n\n## Adding Fields\n\n@docs field, hiddenField, hiddenKind\n\n\n## View Functions\n\n@docs Context\n\n\n## Rendering Forms\n\n@docs renderHtml, renderStyledHtml\n\n@docs FinalForm, withGetMethod, toDynamicTransition, toDynamicFetcher\n\n\n## Showing Errors\n\n@docs Errors, errorsForField\n\n\n## Running Parsers\n\n@docs parse, runServerSide, runOneOfServerSide\n\n\n## Combining Forms to Run on Server\n\n@docs ServerForms\n\n@docs initCombined, combine, initCombinedServer, combineServer\n\n\n## Dynamic Fields\n\n@docs dynamic\n\n@docs AppContext\n\n\n## Submission\n\n@docs toServerForm, withOnSubmit\n\n","unions":[{"name":"Errors","comment":" ","args":["error"],"cases":[]},{"name":"FinalForm","comment":" ","args":["error","parsed","data","view","userMsg"],"cases":[]},{"name":"Form","comment":" ","args":["error","combineAndView","input"],"cases":[]},{"name":"Response","comment":" ","args":["error"],"cases":[["Response",["{ fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) }"]]]},{"name":"ServerForms","comment":" ","args":["error","parsed"],"cases":[["ServerForms",["List.List (Form.Form error (Form.Validation.Combined error parsed) Basics.Never)"]]]}],"aliases":[{"name":"AppContext","comment":" ","args":["app","actionData"],"type":"{ app | path : Path.Path, transition : Maybe.Maybe Pages.Transition.Transition, fetchers : Dict.Dict String.String (Pages.Transition.FetcherState actionData), pageFormState : Dict.Dict String.String { fields : Dict.Dict String.String { value : String.String, status : Form.FieldStatus.FieldStatus }, submitAttempted : Basics.Bool } }"},{"name":"Context","comment":" ","args":["error","data"],"type":"{ errors : Form.Errors error, isTransitioning : Basics.Bool, submitAttempted : Basics.Bool, data : data }"},{"name":"DoneForm","comment":" ","args":["error","parsed","data","view"],"type":"Form.Form error { combine : Form.Validation.Combined error parsed, view : Form.Context error data -> view } data"},{"name":"HtmlForm","comment":" ","args":["error","parsed","input","msg"],"type":"Form.Form error { combine : Form.Validation.Combined error parsed, view : Form.Context error input -> List.List (Html.Html (Pages.Msg.Msg msg)) } input"},{"name":"StyledHtmlForm","comment":" ","args":["error","parsed","data","msg"],"type":"Form.Form error { combine : Form.Validation.Combined error parsed, view : Form.Context error data -> List.List (Html.Styled.Html (Pages.Msg.Msg msg)) } data"}],"values":[{"name":"combine","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Pages.Internal.Form.Validation error parsed kind constraints } input -> Form.ServerForms error combined -> Form.ServerForms error combined"},{"name":"combineServer","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Form.Validation.Combined error (DataSource.DataSource (Pages.Internal.Form.Validation error parsed kind constraints)) } input -> Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints)) -> Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints))"},{"name":"dynamic","comment":" ","type":"(decider -> Form.Form error { combine : Pages.Internal.Form.Validation error parsed named constraints1, view : subView } data) -> Form.Form error ({ combine : decider -> Pages.Internal.Form.Validation error parsed named constraints1, view : decider -> subView } -> combineAndView) data -> Form.Form error combineAndView data"},{"name":"errorsForField","comment":" ","type":"Form.Validation.Field error parsed kind -> Form.Errors error -> List.List error"},{"name":"field","comment":" Declare a visible field for the form.\n\nUse [`Form.Field`](Form-Field) to define the field and its validations.\n\n form =\n Form.init\n (\\email ->\n { combine =\n Validation.succeed NewUser\n |> Validation.andMap email\n , view = \\info -> [{- render fields -}]\n }\n )\n |> Form.field \"email\"\n (Field.text |> Field.required \"Required\")\n\n","type":"String.String -> Form.Field.Field error parsed data kind constraints -> Form.Form error (Form.Validation.Field error parsed kind -> combineAndView) data -> Form.Form error combineAndView data"},{"name":"hiddenField","comment":" Declare a hidden field for the form.\n\nUnlike [`field`](#field) declarations which are rendered using [`Form.ViewField`](Form-ViewField)\nfunctions, `hiddenField` inputs are automatically inserted into the form when you render it.\n\nYou define the field's validations the same way as for `field`, with the\n[`Form.Field`](Form-Field) API.\n\n form =\n Form.init\n (\\quantity productId ->\n { combine = {- combine fields -}\n , view = \\info -> [{- render visible fields -}]\n }\n )\n |> Form.field \"quantity\"\n (Field.int |> Field.required \"Required\")\n |> Form.field \"productId\"\n (Field.text\n |> Field.required \"Required\"\n |> Field.withInitialValue (\\product -> Form.Value.string product.id)\n )\n\n","type":"String.String -> Form.Field.Field error parsed data kind constraints -> Form.Form error (Form.Validation.Field error parsed Form.FieldView.Hidden -> combineAndView) data -> Form.Form error combineAndView data"},{"name":"hiddenKind","comment":" ","type":"( String.String, String.String ) -> error -> Form.Form error combineAndView data -> Form.Form error combineAndView data"},{"name":"init","comment":" ","type":"combineAndView -> Form.Form String.String combineAndView data"},{"name":"initCombined","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Pages.Internal.Form.Validation error parsed kind constraints } input -> Form.ServerForms error combined"},{"name":"initCombinedServer","comment":" ","type":"(parsed -> combined) -> Form.Form error { combineAndView | combine : Form.Validation.Combined error (DataSource.DataSource (Pages.Internal.Form.Validation error parsed kind constraints)) } input -> Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints))"},{"name":"parse","comment":" ","type":"String.String -> Form.AppContext app actionData -> data -> Form.Form error { info | combine : Pages.Internal.Form.Validation error parsed named constraints } data -> ( Maybe.Maybe parsed, Dict.Dict String.String (List.List error) )"},{"name":"renderHtml","comment":" ","type":"List.List (Html.Attribute (Pages.Msg.Msg msg)) -> Maybe.Maybe { fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) } -> Form.AppContext app actionData -> data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed named constraints) data (Form.Context error data -> List.List (Html.Html (Pages.Msg.Msg msg))) msg -> Html.Html (Pages.Msg.Msg msg)"},{"name":"renderStyledHtml","comment":" ","type":"List.List (Html.Styled.Attribute (Pages.Msg.Msg msg)) -> Maybe.Maybe { fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) } -> Form.AppContext app actionData -> data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed named constraints) data (Form.Context error data -> List.List (Html.Styled.Html (Pages.Msg.Msg msg))) msg -> Html.Styled.Html (Pages.Msg.Msg msg)"},{"name":"runOneOfServerSide","comment":" ","type":"List.List ( String.String, String.String ) -> Form.ServerForms error parsed -> ( Maybe.Maybe parsed, Dict.Dict String.String (List.List error) )"},{"name":"runServerSide","comment":" ","type":"List.List ( String.String, String.String ) -> Form.Form error (Pages.Internal.Form.Validation error parsed kind constraints) data -> ( Basics.Bool, ( Maybe.Maybe parsed, Dict.Dict String.String (List.List error) ) )"},{"name":"toDynamicFetcher","comment":" ","type":"String.String -> Form.Form error { combine : Pages.Internal.Form.Validation error parsed field constraints, view : Form.Context error data -> view } data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed field constraints) data (Form.Context error data -> view) userMsg"},{"name":"toDynamicTransition","comment":" ","type":"String.String -> Form.Form error { combine : Pages.Internal.Form.Validation error parsed field constraints, view : Form.Context error data -> view } data -> Form.FinalForm error (Pages.Internal.Form.Validation error parsed field constraints) data (Form.Context error data -> view) userMsg"},{"name":"toServerForm","comment":" ","type":"Form.Form error { combine : Pages.Internal.Form.Validation error combined kind constraints, view : viewFn } data -> Form.Form error { combine : Pages.Internal.Form.Validation error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints)) kind constraints, view : viewFn } data"},{"name":"withGetMethod","comment":" ","type":"Form.FinalForm error parsed data view userMsg -> Form.FinalForm error parsed data view userMsg"},{"name":"withOnSubmit","comment":" ","type":"({ fields : List.List ( String.String, String.String ) } -> userMsg) -> Form.FinalForm error parsed data view userMsg -> Form.FinalForm error parsed data view userMsg"}],"binops":[]},{"name":"Form.Field","comment":"\n\n\n## Base Fields\n\n@docs text, checkbox, int, float\n\n\n## Multiple Choice Fields\n\n@docs select, range, OutsideRange\n\n\n## Date/Time Fields\n\n@docs date, time, TimeOfDay\n\n\n## Other\n\n@docs Field, FieldInfo, exactValue\n\n\n## Field Configuration\n\n@docs required, withClientValidation, withInitialValue, map\n\n\n## Text Field Display Options\n\n@docs email, password, search, telephone, url, textarea\n\n\n## Numeric Field Options\n\n@docs withMax, withMin, withStep, withMinLength, withMaxLength\n\n\n## Phantom Options\n\n@docs No, Yes\n\n","unions":[{"name":"Field","comment":" ","args":["error","parsed","data","kind","constraints"],"cases":[["Field",["Form.Field.FieldInfo error parsed data","kind"]]]},{"name":"No","comment":" ","args":[],"cases":[["No",["Basics.Never"]]]},{"name":"OutsideRange","comment":" ","args":[],"cases":[["AboveRange",[]],["BelowRange",[]]]},{"name":"Yes","comment":" ","args":[],"cases":[["Yes",["Basics.Never"]]]}],"aliases":[{"name":"FieldInfo","comment":" ","args":["error","parsed","data"],"type":"{ initialValue : Maybe.Maybe (data -> String.String), decode : Maybe.Maybe String.String -> ( Maybe.Maybe parsed, List.List error ), properties : List.List ( String.String, Json.Encode.Value ) }"},{"name":"TimeOfDay","comment":" ","args":[],"type":"{ hours : Basics.Int, minutes : Basics.Int }"}],"values":[{"name":"checkbox","comment":" ","type":"Form.Field.Field error Basics.Bool data Form.FieldView.Input { required : (), initial : Basics.Bool }"},{"name":"date","comment":" ","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Date.Date) data Form.FieldView.Input { min : Date.Date, max : Date.Date, required : (), wasMapped : Form.Field.No, initial : Date.Date }"},{"name":"email","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"exactValue","comment":" ","type":"String.String -> error -> Form.Field.Field error String.String data Form.FieldView.Input { required : (), plainText : (), wasMapped : Form.Field.No, initial : String.String }"},{"name":"float","comment":" ","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Basics.Float) data Form.FieldView.Input { min : Basics.Float, max : Basics.Float, required : (), wasMapped : Form.Field.No, initial : Basics.Float }"},{"name":"int","comment":" ","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Basics.Int) data Form.FieldView.Input { min : Basics.Int, max : Basics.Int, required : (), wasMapped : Form.Field.No, step : Basics.Int, initial : Basics.Int }"},{"name":"map","comment":" ","type":"(parsed -> mapped) -> Form.Field.Field error parsed data kind constraints -> Form.Field.Field error mapped data kind { constraints | wasMapped : Form.Field.Yes }"},{"name":"password","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"range","comment":" ","type":"{ min : Form.Value.Value valueType, max : Form.Value.Value valueType, initial : data -> Form.Value.Value valueType, missing : error, invalid : Form.Field.OutsideRange -> error } -> Form.Field.Field error (Maybe.Maybe valueType) data kind { constraints | required : (), initial : valueType, min : valueType, max : valueType, wasMapped : Form.Field.No } -> Form.Field.Field error valueType data Form.FieldView.Input { constraints | wasMapped : Form.Field.No }"},{"name":"required","comment":" ","type":"error -> Form.Field.Field error (Maybe.Maybe parsed) data kind { constraints | required : (), wasMapped : Form.Field.No } -> Form.Field.Field error parsed data kind { constraints | wasMapped : Form.Field.No }"},{"name":"search","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"select","comment":" ","type":"List.List ( String.String, option ) -> (String.String -> error) -> Form.Field.Field error (Maybe.Maybe option) data (Form.FieldView.Options option) { required : (), wasMapped : Form.Field.No }"},{"name":"telephone","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"text","comment":" ","type":"Form.Field.Field error (Maybe.Maybe String.String) data Form.FieldView.Input { required : (), plainText : (), wasMapped : Form.Field.No, initial : String.String, minlength : (), maxlength : () }"},{"name":"textarea","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"time","comment":" \n","type":"{ invalid : String.String -> error } -> Form.Field.Field error (Maybe.Maybe Form.Field.TimeOfDay) data Form.FieldView.Input { required : (), wasMapped : Form.Field.No }"},{"name":"url","comment":" ","type":"Form.Field.Field error parsed data Form.FieldView.Input { constraints | plainText : () } -> Form.Field.Field error parsed data Form.FieldView.Input constraints"},{"name":"withClientValidation","comment":" ","type":"(parsed -> ( Maybe.Maybe mapped, List.List error )) -> Form.Field.Field error parsed data kind constraints -> Form.Field.Field error mapped data kind { constraints | wasMapped : Form.Field.Yes }"},{"name":"withInitialValue","comment":" ","type":"(data -> Form.Value.Value valueType) -> Form.Field.Field error value data kind { constraints | initial : valueType } -> Form.Field.Field error value data kind constraints"},{"name":"withMax","comment":" ","type":"Form.Value.Value valueType -> error -> Form.Field.Field error parsed data kind { constraints | max : valueType } -> Form.Field.Field error parsed data kind constraints"},{"name":"withMaxLength","comment":" ","type":"Basics.Int -> error -> Form.Field.Field error parsed data kind { constraints | maxlength : () } -> Form.Field.Field error parsed data kind constraints"},{"name":"withMin","comment":" ","type":"Form.Value.Value valueType -> error -> Form.Field.Field error parsed data kind { constraints | min : valueType } -> Form.Field.Field error parsed data kind constraints"},{"name":"withMinLength","comment":" ","type":"Basics.Int -> error -> Form.Field.Field error parsed data kind { constraints | minlength : () } -> Form.Field.Field error parsed data kind constraints"},{"name":"withStep","comment":" ","type":"Form.Value.Value valueType -> Form.Field.Field msg error value view { constraints | step : valueType } -> Form.Field.Field msg error value view constraints"}],"binops":[]},{"name":"Form.FieldStatus","comment":" elm-pages manages the client-side state of fields, including Status which you can use to determine when\nin the user's workflow to show validation errors.\n\n\n## Field Status\n\n@docs FieldStatus, fieldStatusToString\n\n","unions":[{"name":"FieldStatus","comment":" ","args":[],"cases":[["NotVisited",[]],["Focused",[]],["Changed",[]],["Blurred",[]]]}],"aliases":[],"values":[{"name":"fieldStatusToString","comment":" ","type":"Form.FieldStatus.FieldStatus -> String.String"}],"binops":[]},{"name":"Form.FieldView","comment":"\n\n@docs Input, InputType, Options, input, inputTypeToString, radio, toHtmlProperties, Hidden, select\n\n\n## Html.Styled Helpers\n\n@docs radioStyled, inputStyled\n\n","unions":[{"name":"Hidden","comment":" There are no render helpers for hidden fields because the `Form.renderHtml` helper functions automatically render hidden fields for you.\n","args":[],"cases":[["Hidden",[]]]},{"name":"Input","comment":" ","args":[],"cases":[["Input",["Form.FieldView.InputType"]]]},{"name":"InputType","comment":" ","args":[],"cases":[["Text",[]],["Number",[]],["Range",[]],["Radio",[]],["Date",[]],["Time",[]],["Checkbox",[]],["Tel",[]],["Search",[]],["Password",[]],["Email",[]],["Url",[]],["Textarea",[]]]},{"name":"Options","comment":" ","args":["a"],"cases":[["Options",["String.String -> Maybe.Maybe a","List.List String.String"]]]}],"aliases":[],"values":[{"name":"input","comment":" ","type":"List.List (Html.Attribute msg) -> Form.Validation.Field error parsed Form.FieldView.Input -> Html.Html msg"},{"name":"inputStyled","comment":" ","type":"List.List (Html.Styled.Attribute msg) -> Form.Validation.Field error parsed Form.FieldView.Input -> Html.Styled.Html msg"},{"name":"inputTypeToString","comment":" ","type":"Form.FieldView.InputType -> String.String"},{"name":"radio","comment":" ","type":"List.List (Html.Attribute msg) -> (parsed -> (List.List (Html.Attribute msg) -> Html.Html msg) -> Html.Html msg) -> Form.Validation.Field error parsed2 (Form.FieldView.Options parsed) -> Html.Html msg"},{"name":"radioStyled","comment":" ","type":"List.List (Html.Styled.Attribute msg) -> (parsed -> (List.List (Html.Styled.Attribute msg) -> Html.Styled.Html msg) -> Html.Styled.Html msg) -> Form.Validation.Field error parsed2 (Form.FieldView.Options parsed) -> Html.Styled.Html msg"},{"name":"select","comment":" ","type":"List.List (Html.Attribute msg) -> (parsed -> ( List.List (Html.Attribute msg), String.String )) -> Form.Validation.Field error parsed2 (Form.FieldView.Options parsed) -> Html.Html msg"},{"name":"toHtmlProperties","comment":" ","type":"List.List ( String.String, Json.Encode.Value ) -> List.List (Html.Attribute msg)"}],"binops":[]},{"name":"Form.FormData","comment":"\n\n@docs FormData, Method\n\n","unions":[{"name":"Method","comment":" ","args":[],"cases":[["Get",[]],["Post",[]]]}],"aliases":[{"name":"FormData","comment":" ","args":[],"type":"{ fields : List.List ( String.String, String.String ), method : Form.FormData.Method, action : String.String, id : Maybe.Maybe String.String }"}],"values":[],"binops":[]},{"name":"Form.Validation","comment":"\n\n\n## Validations\n\n@docs Combined, Field, Validation\n\n@docs andMap, andThen, fail, fromMaybe, fromResult, map, map2, parseWithError, succeed, succeed2, withError, withErrorIf, withFallback\n\n\n## Field Metadata\n\n@docs value, fieldName, fieldStatus\n\n\n## Mapping\n\n@docs map3, map4, map5, map6, map7, map8, map9\n\n\n## Global Validation\n\n@docs global\n\n\n## Temporary?\n\n@docs mapWithNever\n\n","unions":[],"aliases":[{"name":"Combined","comment":" ","args":["error","parsed"],"type":"Form.Validation.Validation error parsed Basics.Never Basics.Never"},{"name":"Field","comment":" ","args":["error","parsed","kind"],"type":"Form.Validation.Validation error parsed kind { field : kind }"},{"name":"Validation","comment":" ","args":["error","parsed","kind","constraints"],"type":"Pages.Internal.Form.Validation error parsed kind constraints"}],"values":[{"name":"andMap","comment":" ","type":"Form.Validation.Validation error a named1 constraints1 -> Form.Validation.Validation error (a -> b) named2 constraints2 -> Form.Validation.Combined error b"},{"name":"andThen","comment":" ","type":"(parsed -> Form.Validation.Validation error mapped named1 constraints1) -> Form.Validation.Validation error parsed named2 constraints2 -> Form.Validation.Combined error mapped"},{"name":"fail","comment":" ","type":"error -> Form.Validation.Field error parsed1 field -> Form.Validation.Combined error parsed"},{"name":"fieldName","comment":" ","type":"Form.Validation.Field error parsed kind -> String.String"},{"name":"fieldStatus","comment":" ","type":"Form.Validation.Field error parsed kind -> Form.FieldStatus.FieldStatus"},{"name":"fromMaybe","comment":" ","type":"Maybe.Maybe parsed -> Form.Validation.Combined error parsed"},{"name":"fromResult","comment":" ","type":"Form.Validation.Field error (Result.Result error parsed) kind -> Form.Validation.Combined error parsed"},{"name":"global","comment":" ","type":"Form.Validation.Field error () Basics.Never"},{"name":"map","comment":" ","type":"(parsed -> mapped) -> Form.Validation.Validation error parsed named constraint -> Form.Validation.Validation error mapped named constraint"},{"name":"map2","comment":" ","type":"(a -> b -> c) -> Form.Validation.Validation error a named1 constraints1 -> Form.Validation.Validation error b named2 constraints2 -> Form.Validation.Combined error c"},{"name":"map3","comment":" ","type":"(a1 -> a2 -> a3 -> a4) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Combined error a4"},{"name":"map4","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Combined error a5"},{"name":"map5","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Combined error a6"},{"name":"map6","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Combined error a7"},{"name":"map7","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> a8) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Validation error a7 named7 constraints7 -> Form.Validation.Combined error a8"},{"name":"map8","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> a8 -> a9) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Validation error a7 named7 constraints7 -> Form.Validation.Validation error a8 named8 constraints8 -> Form.Validation.Combined error a9"},{"name":"map9","comment":" ","type":"(a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a7 -> a8 -> a9 -> a10) -> Form.Validation.Validation error a1 named1 constraints1 -> Form.Validation.Validation error a2 named2 constraints2 -> Form.Validation.Validation error a3 named3 constraints3 -> Form.Validation.Validation error a4 named4 constraints4 -> Form.Validation.Validation error a5 named5 constraints5 -> Form.Validation.Validation error a6 named6 constraints6 -> Form.Validation.Validation error a7 named7 constraints7 -> Form.Validation.Validation error a8 named8 constraints8 -> Form.Validation.Validation error a9 named9 constraints9 -> Form.Validation.Combined error a10"},{"name":"mapWithNever","comment":" ","type":"(parsed -> mapped) -> Form.Validation.Validation error parsed named constraint -> Form.Validation.Validation error mapped Basics.Never Basics.Never"},{"name":"parseWithError","comment":" ","type":"parsed -> ( String.String, error ) -> Form.Validation.Combined error parsed"},{"name":"succeed","comment":" ","type":"parsed -> Form.Validation.Combined error parsed"},{"name":"succeed2","comment":" ","type":"parsed -> Form.Validation.Validation error parsed kind constraints"},{"name":"value","comment":" ","type":"Form.Validation.Validation error parsed named constraint -> Maybe.Maybe parsed"},{"name":"withError","comment":" ","type":"Form.Validation.Field error parsed1 field -> error -> Form.Validation.Validation error parsed2 named constraints -> Form.Validation.Validation error parsed2 named constraints"},{"name":"withErrorIf","comment":" ","type":"Basics.Bool -> Form.Validation.Field error ignored field -> error -> Form.Validation.Validation error parsed named constraints -> Form.Validation.Validation error parsed named constraints"},{"name":"withFallback","comment":" ","type":"parsed -> Form.Validation.Validation error parsed named constraints -> Form.Validation.Validation error parsed named constraints"}],"binops":[]},{"name":"Form.Value","comment":"\n\n@docs Value, date, float, int, string, bool, toString\n\n\n## Comparison\n\n@docs compare\n\n","unions":[{"name":"Value","comment":" ","args":["dataType"],"cases":[]}],"aliases":[],"values":[{"name":"bool","comment":" ","type":"Basics.Bool -> Form.Value.Value Basics.Bool"},{"name":"compare","comment":" You probably don't need this helper as it's mostly useful for internal implementation.\n","type":"String.String -> Form.Value.Value value -> Basics.Order"},{"name":"date","comment":" ","type":"Date.Date -> Form.Value.Value Date.Date"},{"name":"float","comment":" ","type":"Basics.Float -> Form.Value.Value Basics.Float"},{"name":"int","comment":" ","type":"Basics.Int -> Form.Value.Value Basics.Int"},{"name":"string","comment":" ","type":"String.String -> Form.Value.Value String.String"},{"name":"toString","comment":" ","type":"Form.Value.Value dataType -> String.String"}],"binops":[]},{"name":"Head","comment":" This module contains low-level functions for building up\nvalues that will be rendered into the page's `` tag\nwhen you run `elm-pages build`. Most likely the `Head.Seo` module\nwill do everything you need out of the box, and you will just need to import `Head`\nso you can use the `Tag` type in your type annotations.\n\nBut this module might be useful if you have a special use case, or if you are\nwriting a plugin package to extend `elm-pages`.\n\n@docs Tag, metaName, metaProperty, metaRedirect\n@docs rssLink, sitemapLink, rootLanguage, manifestLink\n\n\n## Structured Data\n\n@docs structuredData\n\n\n## `AttributeValue`s\n\n@docs AttributeValue\n@docs currentPageFullUrl, urlAttribute, raw\n\n\n## Icons\n\n@docs appleTouchIcon, icon\n\n\n## Functions for use by generated code\n\n@docs toJson, canonicalLink\n\n","unions":[{"name":"AttributeValue","comment":" Values, such as between the `<>`'s here:\n\n```html\n\" content=\"\" />\n```\n\n","args":[],"cases":[]},{"name":"Tag","comment":" Values that can be passed to the generated `Pages.application` config\nthrough the `head` function.\n","args":[],"cases":[]}],"aliases":[],"values":[{"name":"appleTouchIcon","comment":" Note: the type must be png.\nSee .\n\nIf a size is provided, it will be turned into square dimensions as per the recommendations here: \n\nImages must be png's, and non-transparent images are recommended. Current recommended dimensions are 180px and 192px.\n\n","type":"Maybe.Maybe Basics.Int -> Pages.Url.Url -> Head.Tag"},{"name":"canonicalLink","comment":" It's recommended that you use the `Seo` module helpers, which will provide this\nfor you, rather than directly using this.\n\nExample:\n\n Head.canonicalLink \"https://elm-pages.com\"\n\n","type":"Maybe.Maybe String.String -> Head.Tag"},{"name":"currentPageFullUrl","comment":" Create an `AttributeValue` representing the current page's full url.\n","type":"Head.AttributeValue"},{"name":"icon","comment":" ","type":"List.List ( Basics.Int, Basics.Int ) -> MimeType.MimeImage -> Pages.Url.Url -> Head.Tag"},{"name":"manifestLink","comment":" Let's you link to your manifest.json file, see .\n","type":"String.String -> Head.Tag"},{"name":"metaName","comment":" Example:\n\n Head.metaName \"twitter:card\" (Head.raw \"summary_large_image\")\n\nResults in ``\n\n","type":"String.String -> Head.AttributeValue -> Head.Tag"},{"name":"metaProperty","comment":" Example:\n\n Head.metaProperty \"fb:app_id\" (Head.raw \"123456789\")\n\nResults in ``\n\n","type":"String.String -> Head.AttributeValue -> Head.Tag"},{"name":"metaRedirect","comment":" Example:\n\n metaRedirect (Raw \"0; url=https://google.com\")\n\nResults in ``\n\n","type":"Head.AttributeValue -> Head.Tag"},{"name":"raw","comment":" Create a raw `AttributeValue` (as opposed to some kind of absolute URL).\n","type":"String.String -> Head.AttributeValue"},{"name":"rootLanguage","comment":" Set the language for a page.\n\n\n\n import Head\n import LanguageTag\n import LanguageTag.Language\n\n LanguageTag.Language.de -- sets the page's language to German\n |> LanguageTag.build LanguageTag.emptySubtags\n |> Head.rootLanguage\n\nThis results pre-rendered HTML with a global lang tag set.\n\n```html\n\n...\n\n```\n\n","type":"LanguageTag.LanguageTag -> Head.Tag"},{"name":"rssLink","comment":" Add a link to the site's RSS feed.\n\nExample:\n\n rssLink \"/feed.xml\"\n\n```html\n\n```\n\n","type":"String.String -> Head.Tag"},{"name":"sitemapLink","comment":" Add a link to the site's RSS feed.\n\nExample:\n\n sitemapLink \"/feed.xml\"\n\n```html\n\n```\n\n","type":"String.String -> Head.Tag"},{"name":"structuredData","comment":" You can learn more about structured data in [Google's intro to structured data](https://developers.google.com/search/docs/guides/intro-structured-data).\n\nWhen you add a `structuredData` item to one of your pages in `elm-pages`, it will add `json-ld` data to your document that looks like this:\n\n```html\n\n```\n\nTo get that data, you would write this in your `elm-pages` head tags:\n\n import Json.Encode as Encode\n\n {-| \n -}\n encodeArticle :\n { title : String\n , description : String\n , author : StructuredDataHelper { authorMemberOf | personOrOrganization : () } authorPossibleFields\n , publisher : StructuredDataHelper { publisherMemberOf | personOrOrganization : () } publisherPossibleFields\n , url : String\n , imageUrl : String\n , datePublished : String\n , mainEntityOfPage : Encode.Value\n }\n -> Head.Tag\n encodeArticle info =\n Encode.object\n [ ( \"@context\", Encode.string \"http://schema.org/\" )\n , ( \"@type\", Encode.string \"Article\" )\n , ( \"headline\", Encode.string info.title )\n , ( \"description\", Encode.string info.description )\n , ( \"image\", Encode.string info.imageUrl )\n , ( \"author\", encode info.author )\n , ( \"publisher\", encode info.publisher )\n , ( \"url\", Encode.string info.url )\n , ( \"datePublished\", Encode.string info.datePublished )\n , ( \"mainEntityOfPage\", info.mainEntityOfPage )\n ]\n |> Head.structuredData\n\nTake a look at this [Google Search Gallery](https://developers.google.com/search/docs/guides/search-gallery)\nto see some examples of how structured data can be used by search engines to give rich search results. It can help boost\nyour rankings, get better engagement for your content, and also make your content more accessible. For example,\nvoice assistant devices can make use of structured data. If you're hosting a conference and want to make the event\ndate and location easy for attendees to find, this can make that information more accessible.\n\nFor the current version of API, you'll need to make sure that the format is correct and contains the required and recommended\nstructure.\n\nCheck out for a comprehensive listing of possible data types and fields. And take a look at\nGoogle's [Structured Data Testing Tool](https://search.google.com/structured-data/testing-tool)\ntoo make sure that your structured data is valid and includes the recommended values.\n\nIn the future, `elm-pages` will likely support a typed API, but schema.org is a massive spec, and changes frequently.\nAnd there are multiple sources of information on the possible and recommended structure. So it will take some time\nfor the right API design to evolve. In the meantime, this allows you to make use of this for SEO purposes.\n\n","type":"Json.Encode.Value -> Head.Tag"},{"name":"toJson","comment":" Feel free to use this, but in 99% of cases you won't need it. The generated\ncode will run this for you to generate your `manifest.json` file automatically!\n","type":"String.String -> String.String -> Head.Tag -> Json.Encode.Value"},{"name":"urlAttribute","comment":" Create an `AttributeValue` from an `ImagePath`.\n","type":"Pages.Url.Url -> Head.AttributeValue"}],"binops":[]},{"name":"Head.Seo","comment":" \n\n\nThis module encapsulates some of the best practices for SEO for your site.\n\n`elm-pages` will pre-render each of the static pages (in your `content` directory) so that\nweb crawlers can efficiently and accurately process it. The functions in this module are for use\nwith the `head` function that you pass to your Pages config (`Pages.application`).\n\n import Date\n import Head\n import Head.Seo as Seo\n\n\n -- justinmimbs/date package\n type alias ArticleMetadata =\n { title : String\n , description : String\n , published : Date\n , author : Data.Author.Author\n }\n\n head : ArticleMetadata -> List Head.Tag\n head articleMetadata =\n Seo.summaryLarge\n { canonicalUrlOverride = Nothing\n , siteName = \"elm-pages\"\n , image =\n { url = Pages.images.icon\n , alt = articleMetadata.description\n , dimensions = Nothing\n , mimeType = Nothing\n }\n , description = articleMetadata.description\n , locale = Nothing\n , title = articleMetadata.title\n }\n |> Seo.article\n { tags = []\n , section = Nothing\n , publishedTime = Just (Date.toIsoString articleMetadata.published)\n , modifiedTime = Nothing\n , expirationTime = Nothing\n }\n\n@docs Common, Image, article, audioPlayer, book, profile, song, summary, summaryLarge, videoPlayer, website\n\n","unions":[],"aliases":[{"name":"Common","comment":" These fields apply to any type in the og object types\nSee and \n\nSkipping this for now, if there's a use case I can add it in:\n\n - og:determiner - The word that appears before this object's title in a sentence. An enum of (a, an, the, \"\", auto). If auto is chosen, the consumer of your data should chose between \"a\" or \"an\". Default is \"\" (blank).\n\n","args":[],"type":"{ title : String.String, image : Head.Seo.Image, canonicalUrlOverride : Maybe.Maybe String.String, description : String.String, siteName : String.String, audio : Maybe.Maybe Head.Seo.Audio, video : Maybe.Maybe Head.Seo.Video, locale : Maybe.Maybe Head.Seo.Locale, alternateLocales : List.List Head.Seo.Locale, twitterCard : Head.Twitter.TwitterCard }"},{"name":"Image","comment":" See \n","args":[],"type":"{ url : Pages.Url.Url, alt : String.String, dimensions : Maybe.Maybe { width : Basics.Int, height : Basics.Int }, mimeType : Maybe.Maybe Head.Seo.MimeType }"}],"values":[{"name":"article","comment":" See \n","type":"{ tags : List.List String.String, section : Maybe.Maybe String.String, publishedTime : Maybe.Maybe Head.Seo.Iso8601DateTime, modifiedTime : Maybe.Maybe Head.Seo.Iso8601DateTime, expirationTime : Maybe.Maybe Head.Seo.Iso8601DateTime } -> Head.Seo.Common -> List.List Head.Tag"},{"name":"audioPlayer","comment":" Will be displayed as a Player card in twitter\nSee: \n\nOpenGraph audio will also be included.\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, audio : Head.Seo.Audio, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"book","comment":" See \n","type":"Head.Seo.Common -> { tags : List.List String.String, isbn : Maybe.Maybe String.String, releaseDate : Maybe.Maybe Head.Seo.Iso8601DateTime } -> List.List Head.Tag"},{"name":"profile","comment":" See \n","type":"{ firstName : String.String, lastName : String.String, username : Maybe.Maybe String.String } -> Head.Seo.Common -> List.List Head.Tag"},{"name":"song","comment":" See \n","type":"Head.Seo.Common -> { duration : Maybe.Maybe Basics.Int, album : Maybe.Maybe Basics.Int, disc : Maybe.Maybe Basics.Int, track : Maybe.Maybe Basics.Int } -> List.List Head.Tag"},{"name":"summary","comment":" Will be displayed as a large card in twitter\nSee: \n\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\nNote: You cannot include audio or video tags with summaries.\nIf you want one of those, use `audioPlayer` or `videoPlayer`\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"summaryLarge","comment":" Will be displayed as a large card in twitter\nSee: \n\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\nNote: You cannot include audio or video tags with summaries.\nIf you want one of those, use `audioPlayer` or `videoPlayer`\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"videoPlayer","comment":" Will be displayed as a Player card in twitter\nSee: \n\nOpenGraph video will also be included.\nThe options will also be used to build up the appropriate OpenGraph `` tags.\n\n","type":"{ canonicalUrlOverride : Maybe.Maybe String.String, siteName : String.String, image : Head.Seo.Image, description : String.String, title : String.String, video : Head.Seo.Video, locale : Maybe.Maybe Head.Seo.Locale } -> Head.Seo.Common"},{"name":"website","comment":" \n","type":"Head.Seo.Common -> List.List Head.Tag"}],"binops":[]},{"name":"Pages.Fetcher","comment":"\n\n@docs Fetcher, FetcherInfo, submit, map\n\n","unions":[{"name":"Fetcher","comment":" ","args":["decoded"],"cases":[["Fetcher",["Pages.Fetcher.FetcherInfo decoded"]]]}],"aliases":[{"name":"FetcherInfo","comment":" ","args":["decoded"],"type":"{ decoder : Result.Result Http.Error Bytes.Bytes -> decoded, fields : List.List ( String.String, String.String ), headers : List.List ( String.String, String.String ), url : Maybe.Maybe String.String }"}],"values":[{"name":"map","comment":" ","type":"(a -> b) -> Pages.Fetcher.Fetcher a -> Pages.Fetcher.Fetcher b"},{"name":"submit","comment":" ","type":"Bytes.Decode.Decoder decoded -> { fields : List.List ( String.String, String.String ), headers : List.List ( String.String, String.String ) } -> Pages.Fetcher.Fetcher (Result.Result Http.Error decoded)"}],"binops":[]},{"name":"Pages.Flags","comment":"\n\n@docs Flags\n\n","unions":[{"name":"Flags","comment":" elm-pages apps run in two different contexts\n\n1. In the browser (like a regular Elm app)\n2. In pre-render mode. For example when you run `elm-pages build`, there is no browser involved, it just runs Elm directly.\n\nYou can pass in Flags and use them in your `Shared.init` function. You can store data in your `Shared.Model` from these flags and then access it across any page.\n\nYou will need to handle the `PreRender` case with no flags value because there is no browser to get flags from. For example, say you wanted to get the\ncurrent user's Browser window size and pass it in as a flag. When that page is pre-rendered, you need to decide on a value to use for the window size\nsince there is no window (the user hasn't requested the page yet, and the page isn't even loaded in a Browser window yet).\n\n","args":[],"cases":[["BrowserFlags",["Json.Decode.Value"]],["PreRenderFlags",[]]]}],"aliases":[],"values":[],"binops":[]},{"name":"Pages.Generate","comment":"\n\n@docs Type, serverRender, buildWithLocalState, buildNoState, Builder\n\n","unions":[{"name":"Builder","comment":" ","args":[],"cases":[]},{"name":"Type","comment":" ","args":[],"cases":[["Alias",["Elm.Annotation.Annotation"]],["Custom",["List.List Elm.Variant"]]]}],"aliases":[],"values":[{"name":"buildNoState","comment":" ","type":"{ view : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression } -> Pages.Generate.Builder -> Elm.File"},{"name":"buildWithLocalState","comment":" ","type":"{ view : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression, update : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression, init : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression, subscriptions : Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression -> Elm.Expression, msg : Pages.Generate.Type, model : Pages.Generate.Type } -> Pages.Generate.Builder -> Elm.File"},{"name":"serverRender","comment":" ","type":"{ data : ( Pages.Generate.Type, Elm.Expression -> Elm.Expression ), action : ( Pages.Generate.Type, Elm.Expression -> Elm.Expression ), head : Elm.Expression -> Elm.Expression, moduleName : List.List String.String } -> Pages.Generate.Builder"}],"binops":[]},{"name":"Pages.Manifest","comment":" Represents the configuration of a\n[web manifest file](https://developer.mozilla.org/en-US/docs/Web/Manifest).\n\nYou pass your `Pages.Manifest.Config` record into the `Pages.application` function\n(from your generated `Pages.elm` file).\n\n import Pages.Manifest as Manifest\n import Pages.Manifest.Category\n\n manifest : Manifest.Config\n manifest =\n Manifest.init\n { name = static.siteName\n , description = \"elm-pages - \" ++ tagline\n , startUrl = Route.Index {} |> Route.toPath\n , icons =\n [ icon webp 192\n , icon webp 512\n , icon MimeType.Png 192\n , icon MimeType.Png 512\n ]\n }\n |> Manifest.withShortName \"elm-pages\"\n\n@docs Config, Icon\n\n\n## Builder options\n\n@docs init\n\n@docs withBackgroundColor, withCategories, withDisplayMode, withIarcRatingId, withLang, withOrientation, withShortName, withThemeColor\n\n\n## Config options\n\n@docs DisplayMode, Orientation, IconPurpose\n\n\n## Generating a Manifest.json\n\n@docs generator\n\n\n## Functions for use by the generated code (`Pages.elm`)\n\n@docs toJson\n\n","unions":[{"name":"DisplayMode","comment":" See \n","args":[],"cases":[["Fullscreen",[]],["Standalone",[]],["MinimalUi",[]],["Browser",[]]]},{"name":"IconPurpose","comment":" \n","args":[],"cases":[["IconPurposeMonochrome",[]],["IconPurposeMaskable",[]],["IconPurposeAny",[]]]},{"name":"Orientation","comment":" \n","args":[],"cases":[["Any",[]],["Natural",[]],["Landscape",[]],["LandscapePrimary",[]],["LandscapeSecondary",[]],["Portrait",[]],["PortraitPrimary",[]],["PortraitSecondary",[]]]}],"aliases":[{"name":"Config","comment":" Represents a [web app manifest file](https://developer.mozilla.org/en-US/docs/Web/Manifest)\n(see above for how to use it).\n","args":[],"type":"{ backgroundColor : Maybe.Maybe Color.Color, categories : List.List Pages.Manifest.Category.Category, displayMode : Pages.Manifest.DisplayMode, orientation : Pages.Manifest.Orientation, description : String.String, iarcRatingId : Maybe.Maybe String.String, name : String.String, themeColor : Maybe.Maybe Color.Color, startUrl : Path.Path, shortName : Maybe.Maybe String.String, icons : List.List Pages.Manifest.Icon, lang : LanguageTag.LanguageTag }"},{"name":"Icon","comment":" \n","args":[],"type":"{ src : Pages.Url.Url, sizes : List.List ( Basics.Int, Basics.Int ), mimeType : Maybe.Maybe MimeType.MimeImage, purposes : List.List Pages.Manifest.IconPurpose }"}],"values":[{"name":"generator","comment":" A generator for Api.elm to include a manifest.json.\n","type":"String.String -> DataSource.DataSource Pages.Manifest.Config -> ApiRoute.ApiRoute ApiRoute.Response"},{"name":"init","comment":" Setup a minimal Manifest.Config. You can then use the `with...` builder functions to set additional options.\n","type":"{ description : String.String, name : String.String, startUrl : Path.Path, icons : List.List Pages.Manifest.Icon } -> Pages.Manifest.Config"},{"name":"toJson","comment":" Feel free to use this, but in 99% of cases you won't need it. The generated\ncode will run this for you to generate your `manifest.json` file automatically!\n","type":"String.String -> Pages.Manifest.Config -> Json.Encode.Value"},{"name":"withBackgroundColor","comment":" Set .\n","type":"Color.Color -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withCategories","comment":" Set .\n","type":"List.List Pages.Manifest.Category.Category -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withDisplayMode","comment":" Set .\n","type":"Pages.Manifest.DisplayMode -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withIarcRatingId","comment":" Set .\n","type":"String.String -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withLang","comment":" Set .\n","type":"LanguageTag.LanguageTag -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withOrientation","comment":" Set .\n","type":"Pages.Manifest.Orientation -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withShortName","comment":" Set .\n","type":"String.String -> Pages.Manifest.Config -> Pages.Manifest.Config"},{"name":"withThemeColor","comment":" Set .\n","type":"Color.Color -> Pages.Manifest.Config -> Pages.Manifest.Config"}],"binops":[]},{"name":"Pages.Manifest.Category","comment":" See and\n\n\n@docs toString, Category\n\n@docs books, business, education, entertainment, finance, fitness, food, games, government, health, kids, lifestyle, magazines, medical, music, navigation, news, personalization, photo, politics, productivity, security, shopping, social, sports, travel, utilities, weather\n\n\n## Custom categories\n\n@docs custom\n\n","unions":[{"name":"Category","comment":" Represents a known, valid category, as specified by\n. If this document is updated\nand I don't add it, please open an issue or pull request to let me know!\n","args":[],"cases":[]}],"aliases":[],"values":[{"name":"books","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"business","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"custom","comment":" It's best to use the pre-defined categories to ensure that clients (Android, iOS,\nChrome, Windows app store, etc.) are aware of it and can handle it appropriately.\nBut, if you're confident about using a custom one, you can do so with `Pages.Manifest.custom`.\n","type":"String.String -> Pages.Manifest.Category.Category"},{"name":"education","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"entertainment","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"finance","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"fitness","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"food","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"games","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"government","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"health","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"kids","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"lifestyle","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"magazines","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"medical","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"music","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"navigation","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"news","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"personalization","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"photo","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"politics","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"productivity","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"security","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"shopping","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"social","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"sports","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"toString","comment":" Turn a category into its official String representation, as seen\nhere: .\n","type":"Pages.Manifest.Category.Category -> String.String"},{"name":"travel","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"utilities","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"},{"name":"weather","comment":" Creates the described category.\n","type":"Pages.Manifest.Category.Category"}],"binops":[]},{"name":"Pages.Msg","comment":"\n\n@docs Msg\n\n@docs map, onSubmit, fetcherOnSubmit, submitIfValid\n\n","unions":[{"name":"Msg","comment":" ","args":["userMsg"],"cases":[["UserMsg",["userMsg"]],["Submit",["Form.FormData.FormData"]],["SubmitIfValid",["String.String","Form.FormData.FormData","Basics.Bool"]],["SubmitFetcher",["String.String","Form.FormData.FormData","Basics.Bool","Maybe.Maybe userMsg"]],["FormFieldEvent",["Json.Decode.Value"]]]}],"aliases":[],"values":[{"name":"fetcherOnSubmit","comment":" ","type":"Maybe.Maybe ({ fields : List.List ( String.String, String.String ) } -> userMsg) -> String.String -> (List.List ( String.String, String.String ) -> Basics.Bool) -> Html.Attribute (Pages.Msg.Msg userMsg)"},{"name":"map","comment":" ","type":"(a -> b) -> Pages.Msg.Msg a -> Pages.Msg.Msg b"},{"name":"onSubmit","comment":" ","type":"Html.Attribute (Pages.Msg.Msg userMsg)"},{"name":"submitIfValid","comment":" ","type":"String.String -> (List.List ( String.String, String.String ) -> Basics.Bool) -> Html.Attribute (Pages.Msg.Msg userMsg)"}],"binops":[]},{"name":"Pages.PageUrl","comment":" Same as a Url in `elm/url`, but slightly more structured. The path portion of the URL is parsed into a [`Path`](Path) type, and\nthe query params use the [`QueryParams`](QueryParams) type which allows you to parse just the query params or access them into a Dict.\n\nBecause `elm-pages` takes care of the main routing for pages in your app, the standard Elm URL parser API isn't suited\nto parsing query params individually, which is why the structure of these types is different.\n\n@docs PageUrl, toUrl\n\n","unions":[],"aliases":[{"name":"PageUrl","comment":" ","args":[],"type":"{ protocol : Url.Protocol, host : String.String, port_ : Maybe.Maybe Basics.Int, path : Path.Path, query : Maybe.Maybe QueryParams.QueryParams, fragment : Maybe.Maybe String.String }"}],"values":[{"name":"toUrl","comment":" ","type":"Pages.PageUrl.PageUrl -> Url.Url"}],"binops":[]},{"name":"Pages.Transition","comment":"\n\n@docs Transition, LoadingState, map\n\n\n## Fetchers\n\n@docs FetcherState, FetcherSubmitStatus\n\n","unions":[{"name":"FetcherSubmitStatus","comment":" ","args":["actionData"],"cases":[["FetcherSubmitting",[]],["FetcherReloading",["actionData"]],["FetcherComplete",["actionData"]]]},{"name":"LoadingState","comment":" ","args":[],"cases":[["Redirecting",[]],["Load",[]],["ActionRedirect",[]]]},{"name":"Transition","comment":" ","args":[],"cases":[["Submitting",["Form.FormData.FormData"]],["LoadAfterSubmit",["Form.FormData.FormData","Path.Path","Pages.Transition.LoadingState"]],["Loading",["Path.Path","Pages.Transition.LoadingState"]]]}],"aliases":[{"name":"FetcherState","comment":" ","args":["actionData"],"type":"{ status : Pages.Transition.FetcherSubmitStatus actionData, payload : Form.FormData.FormData, initiatedAt : Time.Posix }"}],"values":[{"name":"map","comment":" ","type":"(a -> b) -> Pages.Transition.FetcherState a -> Pages.Transition.FetcherState b"}],"binops":[]},{"name":"Pages.Url","comment":" Some of the `elm-pages` APIs will take internal URLs and ensure that they have the `canonicalSiteUrl` prepended.\n\nThat's the purpose for this type. If you have an external URL, like `Pages.Url.external \"https://google.com\"`,\nthen the canonicalUrl will not be prepended when it is used in a head tag.\n\nIf you refer to a local page, like `Route.Index |> Route.toPath |> Pages.Url.fromPath`, or `Pages.Url.fromPath`\n\n@docs Url, external, fromPath, toAbsoluteUrl, toString\n\n","unions":[{"name":"Url","comment":" ","args":[],"cases":[]}],"aliases":[],"values":[{"name":"external","comment":" ","type":"String.String -> Pages.Url.Url"},{"name":"fromPath","comment":" ","type":"Path.Path -> Pages.Url.Url"},{"name":"toAbsoluteUrl","comment":" ","type":"String.String -> Pages.Url.Url -> String.String"},{"name":"toString","comment":" ","type":"Pages.Url.Url -> String.String"}],"binops":[]},{"name":"Path","comment":" Represents the path portion of a URL (not query parameters, fragment, protocol, port, etc.).\n\nThis helper lets you combine together path parts without worrying about having too many or too few slashes.\nThese two examples will result in the same URL, even though the first example has trailing and leading slashes, and the\nsecond does not.\n\n Path.join [ \"/blog/\", \"/post-1/\" ]\n |> Path.toAbsolute\n --> \"/blog/post-1\"\n\n Path.join [ \"blog\", \"post-1\" ]\n |> Path.toAbsolute\n --> \"/blog/post-1\"\n\nWe can also safely join Strings that include multiple path parts, a single path part per string, or a mix of the two:\n\n Path.join [ \"/articles/archive/\", \"1977\", \"06\", \"10\", \"post-1\" ]\n |> Path.toAbsolute\n --> \"/articles/archive/1977/06/10/post-1\"\n\n\n## Creating Paths\n\n@docs Path, join, fromString\n\n\n## Turning Paths to String\n\n@docs toAbsolute, toRelative, toSegments\n\n","unions":[{"name":"Path","comment":" The path portion of the URL, normalized to ensure that path segments are joined with `/`s in the right places (no doubled up or missing slashes).\n","args":[],"cases":[]}],"aliases":[],"values":[{"name":"fromString","comment":" Create a Path from a path String.\n\n Path.fromString \"blog/post-1/\"\n |> Path.toAbsolute\n |> Expect.equal \"/blog/post-1\"\n\n","type":"String.String -> Path.Path"},{"name":"join","comment":" Create a Path from multiple path parts. Each part can either be a single path segment, like `blog`, or a\nmulti-part path part, like `blog/post-1`.\n","type":"List.List String.String -> Path.Path"},{"name":"toAbsolute","comment":" Turn a Path to an absolute URL (with no trailing slash).\n","type":"Path.Path -> String.String"},{"name":"toRelative","comment":" Turn a Path to a relative URL.\n","type":"Path.Path -> String.String"},{"name":"toSegments","comment":" ","type":"Path.Path -> List.List String.String"}],"binops":[]},{"name":"QueryParams","comment":" Represents the query portion of a URL. You can use `toDict` or `toString` to turn it into basic types, or you can\nparse it into a custom type using the other functions in this module.\n\n@docs QueryParams\n\n\n## Parsing\n\n@docs Parser\n\n@docs andThen, fail, fromResult, fromString, optionalString, parse, string, strings, succeed\n\n\n## Combining\n\n@docs map2, oneOf\n\n\n## Accessing as Built-In Types\n\n@docs toDict, toString\n\n","unions":[{"name":"Parser","comment":" ","args":["a"],"cases":[]},{"name":"QueryParams","comment":" ","args":[],"cases":[]}],"aliases":[],"values":[{"name":"andThen","comment":" ","type":"(a -> QueryParams.Parser b) -> QueryParams.Parser a -> QueryParams.Parser b"},{"name":"fail","comment":" ","type":"String.String -> QueryParams.Parser a"},{"name":"fromResult","comment":" ","type":"Result.Result String.String a -> QueryParams.Parser a"},{"name":"fromString","comment":" ","type":"String.String -> QueryParams.QueryParams"},{"name":"map2","comment":" ","type":"(a -> b -> combined) -> QueryParams.Parser a -> QueryParams.Parser b -> QueryParams.Parser combined"},{"name":"oneOf","comment":" ","type":"List.List (QueryParams.Parser a) -> QueryParams.Parser a"},{"name":"optionalString","comment":" ","type":"String.String -> QueryParams.Parser (Maybe.Maybe String.String)"},{"name":"parse","comment":" ","type":"QueryParams.Parser a -> QueryParams.QueryParams -> Result.Result String.String a"},{"name":"string","comment":" ","type":"String.String -> QueryParams.Parser String.String"},{"name":"strings","comment":" ","type":"String.String -> QueryParams.Parser (List.List String.String)"},{"name":"succeed","comment":" ","type":"a -> QueryParams.Parser a"},{"name":"toDict","comment":" ","type":"QueryParams.QueryParams -> Dict.Dict String.String (List.List String.String)"},{"name":"toString","comment":" ","type":"QueryParams.QueryParams -> String.String"}],"binops":[]},{"name":"Server.Request","comment":"\n\n@docs Parser\n\n@docs succeed, fromResult, skip\n\n\n## Forms\n\n@docs formData, formDataWithServerValidation\n\n@docs rawFormData\n\n\n## Direct Values\n\n@docs method, rawBody, allCookies, rawHeaders, queryParams\n\n@docs requestTime, optionalHeader, expectContentType, expectJsonBody\n\n@docs acceptMethod, acceptContentTypes\n\n\n## Transforming\n\n@docs map, map2, oneOf, andMap, andThen\n\n\n## Query Parameters\n\n@docs queryParam, expectQueryParam\n\n\n## Cookies\n\n@docs cookie, expectCookie\n\n\n## Headers\n\n@docs expectHeader\n\n\n## Multi-part forms and file uploads\n\n@docs File, expectMultiPartFormPost\n\n\n## Request Parsers That Can Fail\n\n@docs expectBody\n\n\n## Map Functions\n\n@docs map3, map4, map5, map6, map7, map8, map9\n\n\n## Method Type\n\n@docs Method, methodToString\n\n\n## Internals\n\n@docs errorsToString, errorToString, getDecoder, ValidationError\n\n","unions":[{"name":"Method","comment":" ","args":[],"cases":[["Connect",[]],["Delete",[]],["Get",[]],["Head",[]],["Options",[]],["Patch",[]],["Post",[]],["Put",[]],["Trace",[]],["NonStandard",["String.String"]]]},{"name":"ValidationError","comment":" ","args":[],"cases":[]}],"aliases":[{"name":"File","comment":" ","args":[],"type":"{ name : String.String, mimeType : String.String, body : String.String }"},{"name":"Parser","comment":" A `Server.Request.Parser` lets you send a `Server.Response.Response` based on an incoming HTTP request. For example,\nusing a `Server.Request.Parser`, you could check a session cookie to decide whether to respond by rendering a page\nfor the logged-in user, or else respond with an HTTP redirect response (see the [`Server.Response` docs](Server-Response)).\n\nYou can access the incoming HTTP request's:\n\n - Headers\n - Cookies\n - [`method`](#method)\n - URL query parameters\n - [`requestTime`](#requestTime) (as a `Time.Posix`)\n\nNote that this data is not available for pre-rendered pages or pre-rendered API Routes, only for server-rendered pages.\nThis is because when a page is pre-rendered, there _is_ no incoming HTTP request to respond to, it is rendered before a user\nrequests the page and then the pre-rendered page is served as a plain file (without running your Route Module).\n\nThat's why `RouteBuilder.preRender` has `data : RouteParams -> DataSource Data`:\n\n import DataSource exposing (DataSource)\n import RouteBuilder exposing (StatelessRoute)\n\n type alias Data =\n {}\n\n data : RouteParams -> DataSource Data\n data routeParams =\n DataSource.succeed Data\n\n route : StatelessRoute RouteParams Data ActionData\n route =\n RouteBuilder.preRender\n { data = data\n , head = head\n , pages = pages\n }\n |> RouteBuilder.buildNoState { view = view }\n\nA server-rendered Route Module _does_ have access to a user's incoming HTTP request because it runs every time the page\nis loaded. That's why `data` is a `Request.Parser` in server-rendered Route Modules. Since you have an incoming HTTP request for server-rendered routes,\n`RouteBuilder.serverRender` has `data : RouteParams -> Request.Parser (DataSource (Response Data))`. That means that you\ncan use the incoming HTTP request data to choose how to respond. For example, you could check for a dark-mode preference\ncookie and render a light- or dark-themed page and render a different page.\n\nThat's a mouthful, so let's unpack what it means.\n\n`Request.Parser` means you can pull out\n\ndata from the request payload using a Server Request Parser.\n\n import DataSource exposing (DataSource)\n import RouteBuilder exposing (StatelessRoute)\n import Server.Request as Request exposing (Request)\n import Server.Response as Response exposing (Response)\n\n type alias Data =\n {}\n\n data :\n RouteParams\n -> Request.Parser (DataSource (Response Data))\n data routeParams =\n {}\n |> Server.Response.render\n |> DataSource.succeed\n |> Request.succeed\n\n route : StatelessRoute RouteParams Data ActionData\n route =\n RouteBuilder.serverRender\n { head = head\n , data = data\n }\n |> RouteBuilder.buildNoState { view = view }\n\n","args":["decodesTo"],"type":"Internal.Request.Parser decodesTo Server.Request.ValidationError"}],"values":[{"name":"acceptContentTypes","comment":" ","type":"( String.String, List.List String.String ) -> Server.Request.Parser value -> Server.Request.Parser value"},{"name":"acceptMethod","comment":" ","type":"( Server.Request.Method, List.List Server.Request.Method ) -> Server.Request.Parser value -> Server.Request.Parser value"},{"name":"allCookies","comment":" ","type":"Server.Request.Parser (Dict.Dict String.String String.String)"},{"name":"andMap","comment":" Decode an argument and provide it to a function in a decoder.\n\n decoder : Decoder String\n decoder =\n succeed (String.repeat)\n |> andMap (field \"count\" int)\n |> andMap (field \"val\" string)\n\n\n \"\"\" { \"val\": \"hi\", \"count\": 3 } \"\"\"\n |> decodeString decoder\n --> Success \"hihihi\"\n\n","type":"Server.Request.Parser a -> Server.Request.Parser (a -> b) -> Server.Request.Parser b"},{"name":"andThen","comment":" ","type":"(a -> Server.Request.Parser b) -> Server.Request.Parser a -> Server.Request.Parser b"},{"name":"cookie","comment":" ","type":"String.String -> Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"errorToString","comment":" TODO internal only\n","type":"Server.Request.ValidationError -> String.String"},{"name":"errorsToString","comment":" ","type":"( Server.Request.ValidationError, List.List Server.Request.ValidationError ) -> String.String"},{"name":"expectBody","comment":" Same as [`rawBody`](#rawBody), but will only match when a body is present in the HTTP request.\n","type":"Server.Request.Parser String.String"},{"name":"expectContentType","comment":" ","type":"String.String -> Server.Request.Parser ()"},{"name":"expectCookie","comment":" ","type":"String.String -> Server.Request.Parser String.String"},{"name":"expectHeader","comment":" ","type":"String.String -> Server.Request.Parser String.String"},{"name":"expectJsonBody","comment":" ","type":"Json.Decode.Decoder value -> Server.Request.Parser value"},{"name":"expectMultiPartFormPost","comment":" ","type":"({ field : String.String -> Server.Request.Parser String.String, optionalField : String.String -> Server.Request.Parser (Maybe.Maybe String.String), fileField : String.String -> Server.Request.Parser Server.Request.File } -> Server.Request.Parser decodedForm) -> Server.Request.Parser decodedForm"},{"name":"expectQueryParam","comment":" ","type":"String.String -> Server.Request.Parser String.String"},{"name":"formData","comment":" ","type":"Form.ServerForms error combined -> Server.Request.Parser (Result.Result { fields : List.List ( String.String, String.String ), errors : Dict.Dict String.String (List.List error) } combined)"},{"name":"formDataWithServerValidation","comment":" ","type":"Form.ServerForms error (DataSource.DataSource (Pages.Internal.Form.Validation error combined kind constraints)) -> Server.Request.Parser (DataSource.DataSource (Result.Result (Form.Response error) ( Form.Response error, combined )))"},{"name":"fromResult","comment":" Turn a Result into a Request. Useful with `andThen`. Turns `Err` into a skipped request handler (non-matching request),\nand `Ok` values into a `succeed` (matching request).\n","type":"Result.Result String.String value -> Server.Request.Parser value"},{"name":"getDecoder","comment":" TODO internal only\n","type":"Server.Request.Parser (DataSource.DataSource response) -> Json.Decode.Decoder (Result.Result ( Server.Request.ValidationError, List.List Server.Request.ValidationError ) (DataSource.DataSource response))"},{"name":"map","comment":" ","type":"(a -> b) -> Server.Request.Parser a -> Server.Request.Parser b"},{"name":"map2","comment":" ","type":"(a -> b -> c) -> Server.Request.Parser a -> Server.Request.Parser b -> Server.Request.Parser c"},{"name":"map3","comment":" ","type":"(value1 -> value2 -> value3 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser valueCombined"},{"name":"map4","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser valueCombined"},{"name":"map5","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser valueCombined"},{"name":"map6","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser valueCombined"},{"name":"map7","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser value7 -> Server.Request.Parser valueCombined"},{"name":"map8","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser value7 -> Server.Request.Parser value8 -> Server.Request.Parser valueCombined"},{"name":"map9","comment":" ","type":"(value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> value9 -> valueCombined) -> Server.Request.Parser value1 -> Server.Request.Parser value2 -> Server.Request.Parser value3 -> Server.Request.Parser value4 -> Server.Request.Parser value5 -> Server.Request.Parser value6 -> Server.Request.Parser value7 -> Server.Request.Parser value8 -> Server.Request.Parser value9 -> Server.Request.Parser valueCombined"},{"name":"method","comment":" ","type":"Server.Request.Parser Server.Request.Method"},{"name":"methodToString","comment":" Gets the HTTP Method as a String, like 'GET', 'PUT', etc.\n","type":"Server.Request.Method -> String.String"},{"name":"oneOf","comment":" ","type":"List.List (Server.Request.Parser a) -> Server.Request.Parser a"},{"name":"optionalHeader","comment":" ","type":"String.String -> Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"queryParam","comment":" ","type":"String.String -> Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"queryParams","comment":" ","type":"Server.Request.Parser (Dict.Dict String.String (List.List String.String))"},{"name":"rawBody","comment":" ","type":"Server.Request.Parser (Maybe.Maybe String.String)"},{"name":"rawFormData","comment":" ","type":"Server.Request.Parser (List.List ( String.String, String.String ))"},{"name":"rawHeaders","comment":" ","type":"Server.Request.Parser (Dict.Dict String.String String.String)"},{"name":"requestTime","comment":" ","type":"Server.Request.Parser Time.Posix"},{"name":"skip","comment":" This is a Request.Parser that will never match an HTTP request. Similar to `Json.Decode.fail`.\n\nWhy would you want it to always fail? It's helpful for building custom `Server.Request.Parser`. For example, let's say\nyou wanted to define a custom `Server.Request.Parser` to use an XML Decoding package on the request body.\nYou could define a custom function like this\n\n import Server.Request as Request\n\n expectXmlBody : XmlDecoder value -> Request.Parser value\n expectXmlBody xmlDecoder =\n Request.expectBody\n |> Request.andThen\n (\\bodyAsString ->\n case runXmlDecoder xmlDecoder bodyAsString of\n Ok decodedXml ->\n Request.succeed decodedXml\n\n Err error ->\n Request.skip (\"XML could not be decoded \" ++ xmlErrorToString error)\n )\n\nNote that when we said `Request.skip`, remaining Request Parsers will run (for example if you use [`Server.Request.oneOf`](#oneOf)).\nYou could build this with different semantics if you wanted to handle _any_ valid XML body. This Request Parser will _not_\nhandle any valid XML body. It will only handle requests that can match the XmlDecoder that is passed in.\n\nSo when you define your `Server.Request.Parser`s, think carefully about whether you want to handle invalid cases and give an\nerror, or fall through to other Parsers. There's no universal right answer, it's just something to decide for your use case.\n\n expectXmlBody : Request.Parser value\n expectXmlBody =\n Request.map2\n acceptContentTypes\n Request.expectBody\n |> Request.andThen\n (\\bodyAsString ->\n case runXmlDecoder xmlDecoder bodyAsString of\n Ok decodedXml ->\n Request.succeed decodedXml\n\n Err error ->\n Request.skip (\"XML could not be decoded \" ++ xmlErrorToString error)\n )\n\n","type":"String.String -> Server.Request.Parser value"},{"name":"succeed","comment":" ","type":"value -> Server.Request.Parser value"}],"binops":[]},{"name":"Server.Response","comment":"\n\n\n## Responses\n\n@docs Response\n\nThere are two top-level response types:\n\n1. Server Responses\n2. Render Responses\n\nA Server Response is a way to directly send a low-level server response, with no additional magic. You can set a String body,\na list of headers, the status code, etc. The Server Response helpers like `json` and `temporaryRedirect` are just helpers for\nbuilding up those low-level Server Responses.\n\nRender Responses are a little more special in the way they are connected to your elm-pages app. They allow you to render\nthe current Route Module. To do that, you'll need to pass along the `data` for your Route Module.\n\nYou can use `withHeader` and `withStatusCode` to customize either type of Response (Server Responses or Render Responses).\n\n\n## Server Responses\n\n@docs json, plainText, temporaryRedirect, permanentRedirect\n\n\n## Custom Responses\n\n@docs emptyBody, body, bytesBody, base64Body\n\n\n## Render Responses\n\n@docs render\n\n\n## Rendering Error Pages\n\n@docs errorPage, mapError\n\n@docs map\n\n\n## Amending Responses\n\n@docs withHeader, withHeaders, withStatusCode, withSetCookieHeader\n\n\n## Internals\n\n@docs toJson\n\n","unions":[],"aliases":[{"name":"Response","comment":" ","args":["data","error"],"type":"PageServerResponse.PageServerResponse data error"}],"values":[{"name":"base64Body","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"body","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"bytesBody","comment":" ","type":"Bytes.Bytes -> Server.Response.Response data error"},{"name":"emptyBody","comment":" ","type":"Server.Response.Response data error"},{"name":"errorPage","comment":" ","type":"errorPage -> Server.Response.Response data errorPage"},{"name":"json","comment":" ","type":"Json.Encode.Value -> Server.Response.Response data error"},{"name":"map","comment":" ","type":"(data -> mappedData) -> Server.Response.Response data error -> Server.Response.Response mappedData error"},{"name":"mapError","comment":" ","type":"(errorPage -> mappedErrorPage) -> Server.Response.Response data errorPage -> Server.Response.Response data mappedErrorPage"},{"name":"permanentRedirect","comment":" Build a 308 permanent redirect response.\n\nPermanent redirects tell the browser that a resource has permanently moved. If you redirect because a user is not logged in,\nthen you **do not** want to use a permanent redirect because the page they are looking for hasn't changed, you are just\ntemporarily pointing them to a new page since they need to authenticate.\n\nPermanent redirects are aggressively cached so be careful not to use them when you mean to use temporary redirects instead.\n\nIf you need to specifically rely on a 301 permanent redirect (see on the difference between 301 and 308),\nuse `customResponse` instead.\n\n","type":"String.String -> Server.Response.Response data error"},{"name":"plainText","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"render","comment":" ","type":"data -> Server.Response.Response data error"},{"name":"temporaryRedirect","comment":" ","type":"String.String -> Server.Response.Response data error"},{"name":"toJson","comment":" ","type":"Server.Response.Response Basics.Never Basics.Never -> Json.Encode.Value"},{"name":"withHeader","comment":" ","type":"String.String -> String.String -> Server.Response.Response data error -> Server.Response.Response data error"},{"name":"withHeaders","comment":" ","type":"List.List ( String.String, String.String ) -> Server.Response.Response data error -> Server.Response.Response data error"},{"name":"withSetCookieHeader","comment":" ","type":"Server.SetCookie.SetCookie -> Server.Response.Response data error -> Server.Response.Response data error"},{"name":"withStatusCode","comment":" ","type":"Basics.Int -> Server.Response.Response data Basics.Never -> Server.Response.Response data Basics.Never"}],"binops":[]},{"name":"Server.Session","comment":"\n\n@docs Decoder, NotLoadedReason, Session, Value, clearFlashCookies, empty, expectSession, flashPrefix, get, insert, remove, setValues, succeed, unwrap, update, withFlash, withSession\n\n","unions":[{"name":"NotLoadedReason","comment":" ","args":[],"cases":[["NoCookies",[]],["MissingHeaders",[]]]},{"name":"Session","comment":" ","args":[],"cases":[["Session",["Dict.Dict String.String Server.Session.Value"]]]},{"name":"Value","comment":" ","args":[],"cases":[["Persistent",["String.String"]],["ExpiringFlash",["String.String"]],["NewFlash",["String.String"]]]}],"aliases":[{"name":"Decoder","comment":" ","args":["decoded"],"type":"Json.Decode.Decoder decoded"}],"values":[{"name":"clearFlashCookies","comment":" ","type":"Dict.Dict String.String String.String -> Dict.Dict String.String String.String"},{"name":"empty","comment":" ","type":"Server.Session.Session"},{"name":"expectSession","comment":" ","type":"{ name : String.String, secrets : DataSource.DataSource (List.List String.String), sameSite : String.String } -> Server.Request.Parser request -> (request -> Result.Result () Server.Session.Session -> DataSource.DataSource ( Server.Session.Session, Server.Response.Response data errorPage )) -> Server.Request.Parser (DataSource.DataSource (Server.Response.Response data errorPage))"},{"name":"flashPrefix","comment":" ","type":"String.String"},{"name":"get","comment":" ","type":"String.String -> Server.Session.Session -> Maybe.Maybe String.String"},{"name":"insert","comment":" ","type":"String.String -> String.String -> Server.Session.Session -> Server.Session.Session"},{"name":"remove","comment":" ","type":"String.String -> Server.Session.Session -> Server.Session.Session"},{"name":"setValues","comment":" ","type":"Server.Session.Session -> Json.Encode.Value"},{"name":"succeed","comment":" ","type":"constructor -> Server.Session.Decoder constructor"},{"name":"unwrap","comment":" ","type":"Server.Session.Value -> String.String"},{"name":"update","comment":" ","type":"String.String -> (Maybe.Maybe String.String -> Maybe.Maybe String.String) -> Server.Session.Session -> Server.Session.Session"},{"name":"withFlash","comment":" ","type":"String.String -> String.String -> Server.Session.Session -> Server.Session.Session"},{"name":"withSession","comment":" ","type":"{ name : String.String, secrets : DataSource.DataSource (List.List String.String), sameSite : String.String } -> Server.Request.Parser request -> (request -> Result.Result () (Maybe.Maybe Server.Session.Session) -> DataSource.DataSource ( Server.Session.Session, Server.Response.Response data errorPage )) -> Server.Request.Parser (DataSource.DataSource (Server.Response.Response data errorPage))"}],"binops":[]},{"name":"Server.SetCookie","comment":" \n\n\n\n@docs SetCookie, SameSite\n@docs withImmediateExpiration, httpOnly, nonSecure, setCookie, withDomain, withExpiration, withMaxAge, withPath, withSameSite\n\n@docs toString\n\n","unions":[{"name":"SameSite","comment":" ","args":[],"cases":[["Strict",[]],["Lax",[]],["None",[]]]}],"aliases":[{"name":"SetCookie","comment":" ","args":[],"type":"{ name : String.String, value : String.String, expiration : Maybe.Maybe Time.Posix, httpOnly : Basics.Bool, maxAge : Maybe.Maybe Basics.Int, path : Maybe.Maybe String.String, domain : Maybe.Maybe String.String, secure : Basics.Bool, sameSite : Maybe.Maybe Server.SetCookie.SameSite }"}],"values":[{"name":"httpOnly","comment":" ","type":"Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"nonSecure","comment":" Secure (only sent over https, or localhost on http) is the default. This overrides that and\nremoves the `Secure` attribute from the cookie.\n","type":"Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"setCookie","comment":" ","type":"String.String -> String.String -> Server.SetCookie.SetCookie"},{"name":"toString","comment":" ","type":"Server.SetCookie.SetCookie -> String.String"},{"name":"withDomain","comment":" ","type":"String.String -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withExpiration","comment":" ","type":"Time.Posix -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withImmediateExpiration","comment":" ","type":"Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withMaxAge","comment":" ","type":"Basics.Int -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withPath","comment":" ","type":"String.String -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"},{"name":"withSameSite","comment":" The default SameSite policy is Lax if one is not explicitly set. See the SameSite section in .\n","type":"Server.SetCookie.SameSite -> Server.SetCookie.SetCookie -> Server.SetCookie.SetCookie"}],"binops":[]}] \ No newline at end of file diff --git a/src/Pages/Generate.elm b/src/Pages/Generate.elm index fd54ce85..6c34da44 100644 --- a/src/Pages/Generate.elm +++ b/src/Pages/Generate.elm @@ -1,11 +1,8 @@ -module Pages.Generate exposing - ( Type(..), serverRender, buildWithLocalState, buildNoState - , Builder - ) +module Pages.Generate exposing (Type(..), serverRender, buildWithLocalState, buildNoState, Builder) {-| -@docs Type, serverRender, buildWithLocalState, buildNoState +@docs Type, serverRender, buildWithLocalState, buildNoState, Builder -} @@ -31,6 +28,7 @@ typeToDeclaration name type_ = Elm.customType name variants +{-| -} type Builder = Builder { data : ( Type, Elm.Expression -> Elm.Expression ) From 83f9adcf94c72679aa5f5b91515693cacf9d67bb Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 15 Sep 2022 09:22:10 -0700 Subject: [PATCH 69/69] Rename property. --- generator/src/codegen.js | 9 +++------ generator/src/generate-template-module-connector.js | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/generator/src/codegen.js b/generator/src/codegen.js index 5f474a56..9f58de4a 100644 --- a/generator/src/codegen.js +++ b/generator/src/codegen.js @@ -49,13 +49,10 @@ async function generate(basePath) { ), fs.promises.writeFile( "./elm-stuff/elm-pages/.elm-pages/Route.elm", - cliCode.routesModuleNew + cliCode.routesModule ), fs.promises.writeFile("./.elm-pages/Main.elm", browserCode.mainModule), - fs.promises.writeFile( - "./.elm-pages/Route.elm", - browserCode.routesModuleNew - ), + fs.promises.writeFile("./.elm-pages/Route.elm", browserCode.routesModule), writeFetcherModules("./.elm-pages", browserCode.fetcherModules), writeFetcherModules( "./elm-stuff/elm-pages/client/.elm-pages", @@ -111,7 +108,7 @@ async function generateClientFolder(basePath) { ); await fs.promises.writeFile( "./elm-stuff/elm-pages/client/.elm-pages/Route.elm", - browserCode.routesModuleNew + browserCode.routesModule ); await fs.promises.writeFile( "./elm-stuff/elm-pages/client/.elm-pages/Pages.elm", diff --git a/generator/src/generate-template-module-connector.js b/generator/src/generate-template-module-connector.js index 3656fe35..29fae10b 100644 --- a/generator/src/generate-template-module-connector.js +++ b/generator/src/generate-template-module-connector.js @@ -38,7 +38,7 @@ async function generateTemplateModuleConnector(basePath, phase) { ], }; } - const routesModuleNew = await runElmCodegenCli( + const routesModule = await runElmCodegenCli( sortTemplates(templates), basePath ); @@ -1003,7 +1003,7 @@ decodeBytes bytesDecoder items = -- Lamdera.Wire3.bytesDecodeStrict bytesDecoder items |> Result.fromMaybe "Decoding error" `, - routesModuleNew, + routesModule, fetcherModules: templates.map((name) => { return [name, fetcherModule(name)]; }),