Use new custom router.

This commit is contained in:
Dillon Kearns 2021-04-27 16:53:20 -07:00
parent ab87bc049a
commit 80ffd029eb
4 changed files with 375 additions and 28 deletions

View File

@ -0,0 +1,83 @@
module Page.Cats.Name__ exposing (Data, Model, Msg, page)
import DataSource
import Document exposing (Document)
import Element exposing (Element)
import Head
import Head.Seo as Seo
import Html.Styled exposing (text)
import Page exposing (Page, PageWithState, StaticPayload)
import Pages.ImagePath as ImagePath
import Shared
type alias Model =
()
type alias Msg =
Never
type alias RouteParams =
{ name : Maybe String }
page : Page RouteParams Data
page =
Page.prerenderedRoute
{ head = head
, routes = routes
, data = data
}
|> Page.buildNoState { view = view }
routes : DataSource.DataSource (List RouteParams)
routes =
DataSource.succeed
[ { name = Just "larry"
}
, { name = Nothing
}
]
data : RouteParams -> DataSource.DataSource Data
data routeParams =
DataSource.succeed ()
head :
StaticPayload Data RouteParams
-> List Head.Tag
head static =
Seo.summary
{ canonicalUrlOverride = Nothing
, siteName = "elm-pages"
, image =
{ url = ImagePath.build [ "TODO" ]
, alt = "elm-pages logo"
, dimensions = Nothing
, mimeType = Nothing
}
, description = "TODO"
, locale = Nothing
, title = "TODO title" -- metadata.title -- TODO
}
|> Seo.website
type alias Data =
()
view :
StaticPayload Data RouteParams
-> Document Msg
view static =
{ body =
[ text (static.routeParams.name |> Maybe.withDefault "NOTHING")
]
, title = ""
}

View File

@ -500,22 +500,27 @@ mapBoth fnA fnB ( a, b, c ) =
`,
routesModule: `module Route exposing (..)
import Url
import Url.Parser as Parser exposing ((</>), Parser)
import Router
type Route
= ${templates.map(routeHelpers.routeVariantDefinition).join("\n | ")}
urlToRoute : Url.Url -> Maybe Route
urlToRoute : { url | path : String } -> Maybe Route
urlToRoute url =
Parser.parse (Parser.oneOf routes) url
Router.firstMatch matchers url.path
routes : List (Parser (Route -> a) a)
routes =
[ ${templates.map((name) => `${routeParser(name)}\n`).join(" , ")}
matchers : List (Router.Matcher Route)
matchers =
[ ${templates
.map(
(name) => `{ pattern = "^${routeRegex(name).pattern}$"
, toRoute = ${routeRegex(name).toRoute}
}\n`
)
.join(" , ")}
]
@ -536,32 +541,178 @@ routeToPath maybeRoute =
};
}
/**
* @param {string} segment
* @returns {'static' | 'dynamic' | 'optional' | 'index'}
*/
function segmentKind(segment) {
if (segment === "Index") {
return "index";
}
const routeParamMatch = segment.match(/([A-Z][A-Za-z0-9]*)(_?_?)$/);
const segmentKind = (routeParamMatch && routeParamMatch[2]) || "";
if (segmentKind === "") {
return "static";
} else if (segmentKind === "_") {
return "dynamic";
} else if (segmentKind === "__") {
return "optional";
} else {
throw "Unhandled segmentKind";
}
}
/**
* @param {string[]} name
*/
function routeParser(name) {
const parsedParams = routeHelpers.parseRouteParams(name);
const includesOptional = parsedParams.some(
(param) => param.kind === "optional"
);
const params = routeHelpers.routeParams(name);
if (includesOptional) {
const parserCode = name
.map((section) => {
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)(_?_?)$/);
const maybeParam = routeParamMatch && routeParamMatch[1];
switch (segmentKind(section)) {
case "static": {
return `Parser.s "${camelToKebab(section)}"`;
}
case "index": {
return `Parser.top`;
}
case "dynamic": {
return `Parser.string`;
}
case "optional": {
return `Parser.string`;
}
}
})
.join(" </> ");
const parserCodeWithoutOptional = name
.flatMap((section) => {
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)(_?_?)$/);
const maybeParam = routeParamMatch && routeParamMatch[1];
switch (segmentKind(section)) {
case "static": {
return [`Parser.s "${camelToKebab(section)}"`];
}
case "index": {
return [`Parser.top`];
}
case "dynamic": {
return [`Parser.string`];
}
case "optional": {
return [];
}
}
})
.join(" </> ");
return `Parser.oneOf
[ Parser.map (\\${params.join(" ")} -> ${pathNormalizedName(
name
)} { ${params.map((param) => `${param} = Just ${param}`)} }) (${parserCode})
, Parser.map (${pathNormalizedName(name)} { ${params.map(
(param) => `${param} = Nothing`
)} }) (${parserCodeWithoutOptional})
]`;
} else {
const parserCode = name
.map((section) => {
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)(_?_?)$/);
const maybeParam = routeParamMatch && routeParamMatch[1];
switch (segmentKind(section)) {
case "static": {
return `Parser.s "${camelToKebab(section)}"`;
}
case "index": {
return `Parser.top`;
}
case "dynamic": {
return `Parser.string`;
}
case "optional": {
return `(Debug.todo "optional")`;
}
}
// if (maybeParam) {
// return `Parser.string`;
// } else if (section === "Index") {
// // TODO give an error if it isn't the final element
// return "Parser.top";
// } else {
// return `Parser.s "${camelToKebab(section)}"`;
// }
})
.join(" </> ");
if (params.length > 0) {
return `Parser.map (\\${params.join(" ")} -> ${pathNormalizedName(
name
)} { ${params.map((param) => `${param} = ${param}`)} }) (${parserCode})`;
} else {
return `Parser.map (${pathNormalizedName(name)} {}) (${parserCode})`;
}
}
}
/**
* @param {string[]} name
*/
function routeRegex(name) {
const parsedParams = routeHelpers.parseRouteParams(name);
const includesOptional = parsedParams.some(
(param) => param.kind === "optional"
);
const params = routeHelpers.routeParams(name);
const parserCode = name
.map((section) => {
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)_$/);
.flatMap((section) => {
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)(_?_?)$/);
const maybeParam = routeParamMatch && routeParamMatch[1];
if (maybeParam) {
return `Parser.string`;
} else if (section === "Index") {
// TODO give an error if it isn't the final element
return "Parser.top";
} else {
return `Parser.s "${camelToKebab(section)}"`;
switch (segmentKind(section)) {
case "static": {
return [camelToKebab(section)];
}
case "index": {
return [];
}
case "dynamic": {
return [`(?:([^/]+))`];
}
case "optional": {
return [`(([^/]+))?`];
}
}
})
.join(" </> ");
if (params.length > 0) {
return `Parser.map (\\${params.join(" ")} -> ${pathNormalizedName(
name
)} { ${params.map((param) => `${param} = ${param}`)} }) (${parserCode})`;
} else {
return `Parser.map (${pathNormalizedName(name)} {}) (${parserCode})`;
}
.join("\\\\/");
const toRoute = `\\matches ->
case matches of
[ ${parsedParams
.flatMap((parsedParam) => {
switch (parsedParam.kind) {
case "optional": {
return parsedParam.name;
}
case "dynamic": {
return `Just ${parsedParam.name}`;
}
}
})
.join(", ")} ] ->
Just (${pathNormalizedName(name)} { ${params.map(
(param) => `${param} = ${param}`
)} })
_ ->
Nothing
`;
return { pattern: parserCode, toRoute };
}
/**

View File

@ -4,20 +4,62 @@
function routeParams(name) {
return name
.map((section) => {
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)_$/);
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)__?$/);
const maybeParam = routeParamMatch && routeParamMatch[1];
return maybeParam && toFieldName(maybeParam);
})
.filter((maybeParam) => maybeParam !== null);
}
/** @typedef { { kind: ('dynamic' | 'optional'); name: string } } Segment */
/**
* @param {string[]} name
* @returns {Segment[]}
*/
function parseRouteParams(name) {
return name.flatMap((section) => {
const routeParamMatch = section.match(/([A-Z][A-Za-z0-9]*)(_?_?)$/);
const maybeParam = (routeParamMatch && routeParamMatch[1]) || "TODO";
// return maybeParam && toFieldName(maybeParam);
if (routeParamMatch[2] === "") {
return [];
} else if (routeParamMatch[2] === "_") {
return [
{
kind: "dynamic",
name: toFieldName(maybeParam),
},
];
} else if (routeParamMatch[2] === "__") {
return [
{
kind: "optional",
name: toFieldName(maybeParam),
},
];
} else {
throw "Unhandled";
}
});
}
/**
* @param {string[]} name
* @returns {string}
*/
function routeVariantDefinition(name) {
return `${routeVariant(name)} { ${routeParams(name).map(
(param) => `${param} : String`
)} }`;
return `${routeVariant(name)} { ${parseRouteParams(name).map((param) => {
switch (param.kind) {
case "dynamic": {
return `${param.name} : String`;
}
case "optional": {
return `${param.name} : Maybe String`;
}
}
})} }`;
}
/**
@ -47,4 +89,5 @@ module.exports = {
routeVariant,
toFieldName,
paramsRecord,
parseRouteParams,
};

70
src/Router.elm Normal file
View File

@ -0,0 +1,70 @@
module Router exposing (Matcher, firstMatch)
import List.Extra
import Regex
firstMatch : List (Matcher route) -> String -> Maybe route
firstMatch matchers path =
List.Extra.findMap
(\matcher ->
if Regex.contains (matcher.pattern |> toRegex) (normalizePath path) then
tryMatch matcher path
else
Nothing
)
matchers
toRegex : String -> Regex.Regex
toRegex pattern =
Regex.fromString pattern
|> Maybe.withDefault Regex.never
type alias Matcher route =
{ pattern : String, toRoute : List (Maybe String) -> Maybe route }
tryMatch : { pattern : String, toRoute : List (Maybe String) -> Maybe route } -> String -> Maybe route
tryMatch { pattern, toRoute } path =
path
|> normalizePath
|> submatches pattern
|> toRoute
submatches : String -> String -> List (Maybe String)
submatches pattern path =
Regex.find
(Regex.fromString pattern
|> Maybe.withDefault Regex.never
)
path
|> List.concatMap .submatches
normalizePath : String -> String
normalizePath path =
path
|> stripLeadingSlash
|> stripTrailingSlash
stripLeadingSlash : String -> String
stripLeadingSlash path =
if path |> String.startsWith "/" then
String.dropLeft 1 path
else
path
stripTrailingSlash : String -> String
stripTrailingSlash path =
if path |> String.endsWith "/" then
String.dropRight 1 path
else
path