UNPKG

@paroicms/server

Version:
445 lines 15.9 kB
import { getRegularDocumentTypeByName, getRoutingDocumentTypeByName, } from "@paroicms/internal-anywhere-lib"; import { shouldPrefixWithLanguage } from "../../helpers/url-helpers.js"; export function parsePageRoute(siteContext, urlPath) { const { siteSchema } = siteContext; const { path } = parseUrlParameters(urlPath); const tokens = path .split("/") .slice(1) .filter((token) => token.length > 0); const languageResult = parseLanguageFromUrl(siteSchema, tokens); if (!languageResult) return; const { language, urlSegments } = languageResult; const urlCacheKey = []; if (shouldPrefixWithLanguage(siteSchema, language)) urlCacheKey.push(language); const hierarchyResult = parseDocumentHierarchy(siteSchema, urlSegments, language, urlCacheKey); if (!hierarchyResult) return; return { language, redirectTo: hierarchyResult.redirectTo, frontendAppPath: hierarchyResult.frontendAppPath, urlCacheKey: urlCacheKey.join("/"), documentHierarchy: hierarchyResult.documentHierarchy, }; } function parseUrlParameters(urlPath) { return { path: urlPath, }; } function parseLanguageFromUrl(siteSchema, tokens) { let language; let urlSegments; if (siteSchema.languageRoutingMode === "prefixAll") { language = tokens[0]; urlSegments = tokens.slice(1); } else if (siteSchema.languageRoutingMode === "prefixSecondary") { if (tokens.length > 0 && siteSchema.languages.includes(tokens[0]) && tokens[0] !== siteSchema.defaultLanguage) { language = tokens[0]; urlSegments = tokens.slice(1); } else { language = siteSchema.defaultLanguage; urlSegments = tokens; } } else throw new Error(`invalid routing mode "${siteSchema.languageRoutingMode}"`); if (!language || !siteSchema.languages.includes(language)) return; return { language, urlSegments }; } function parseDocumentHierarchy(siteSchema, urlSegments, language, urlCacheKey) { const documentHierarchy = []; const homeDocument = siteSchema.nodeTypes.home; documentHierarchy.push({ entryKind: "routing", documentType: homeDocument, }); const homeFrontendAppResult = checkFrontendAppWithRoutingPrecedence(homeDocument, urlSegments, 0, siteSchema, language); if (homeFrontendAppResult.shouldUseFrontendApp) { return { documentHierarchy, redirectTo: homeDocument.redirectTo, frontendAppPath: `/${urlSegments.join("/")}`, }; } return parseDocumentHierarchyRecursive({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, currentParent: homeDocument, index: 0, }); } function parseDocumentHierarchyRecursive({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, currentParent, index, }) { const routingResult = parseRoutingDocuments({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, currentParent, index, }); if (routingResult.frontendAppPath || routingResult.index >= urlSegments.length) { return { documentHierarchy, redirectTo: routingResult.redirectTo, frontendAppPath: routingResult.frontendAppPath, }; } return parseRegularDocuments({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, parentCandidates: [routingResult.currentParent], index: routingResult.index, redirectTo: routingResult.redirectTo, }); } function parseRoutingDocuments({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, currentParent, index, }) { let currentIndex = index; let parent = currentParent; while (currentIndex < urlSegments.length) { const urlSegment = urlSegments[currentIndex]; const routingDocument = findRoutingDocumentByUrlPath({ siteSchema, parent, urlSegment, language, }); if (!routingDocument) break; documentHierarchy.push({ entryKind: "routing", documentType: routingDocument, }); urlCacheKey.push(urlSegment); parent = routingDocument; currentIndex++; const frontendAppResult = checkFrontendAppWithRoutingPrecedence(routingDocument, urlSegments, currentIndex, siteSchema, language); if (frontendAppResult.shouldUseFrontendApp) { return { currentParent: parent, index: currentIndex, redirectTo: routingDocument.redirectTo, frontendAppPath: `/${urlSegments.slice(currentIndex).join("/")}`, }; } } return { currentParent: parent, index: currentIndex, redirectTo: parent.redirectTo, frontendAppPath: undefined, }; } function parseRegularDocuments({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, parentCandidates, index, redirectTo, }) { let currentIndex = index; let candidates = parentCandidates; let lastRedirectTo = redirectTo; if (currentIndex >= urlSegments.length) { return { documentHierarchy, redirectTo: lastRedirectTo, frontendAppPath: undefined, }; } while (currentIndex < urlSegments.length) { const found = findRegularDocumentByUrlPath({ siteSchema, parentCandidates: candidates, urlSegments, index: currentIndex, }); if (!found) return; const isClusterRoot = found.candidateTypes.some((type) => (type.routingChildren?.length ?? 0) > 0); const lookupFields = found.lookupKind === "relativeId" ? { lookupKind: found.lookupKind, relativeId: found.relativeId } : { lookupKind: found.lookupKind, slug: found.slug }; documentHierarchy.push({ entryKind: "regular", ...lookupFields, candidateTypes: found.candidateTypes, isClusterRoot, }); urlCacheKey.push(found.lookupKind === "relativeId" ? `regular:${found.relativeId}` : `slug:${found.slug}`); lastRedirectTo = undefined; currentIndex += found.skipUrlSegments; candidates = found.candidateTypes; const frontendAppResult = checkRegularDocumentFrontendApp({ candidateTypes: found.candidateTypes, urlSegments, index: currentIndex, siteSchema, language, }); if (frontendAppResult.shouldUseFrontendApp) { return { documentHierarchy, redirectTo: lastRedirectTo, frontendAppPath: `/${urlSegments.slice(currentIndex).join("/")}`, }; } if (isClusterRoot && currentIndex < urlSegments.length) { const clusterResult = parseClusterDocuments({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, regularCandidates: found.candidateTypes, index: currentIndex, }); return clusterResult; } } return { documentHierarchy, redirectTo: lastRedirectTo, frontendAppPath: undefined, }; } function parseClusterDocuments({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, regularCandidates, index, }) { let currentIndex = index; let lastRedirectTo; while (currentIndex < urlSegments.length) { const urlSegment = urlSegments[currentIndex]; let routingDocument; for (const candidateType of regularCandidates) { if (!candidateType.routingChildren) continue; routingDocument = findRoutingDocumentByUrlPath({ siteSchema, parent: candidateType, urlSegment, language, }); if (routingDocument) break; } if (!routingDocument) { const hasRegularChildren = regularCandidates.some((type) => type.regularChildren?.length); if (hasRegularChildren) { return parseRegularDocuments({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, parentCandidates: regularCandidates, index: currentIndex, redirectTo: lastRedirectTo, }); } return { documentHierarchy, redirectTo: lastRedirectTo, frontendAppPath: undefined, }; } documentHierarchy.push({ entryKind: "routing", documentType: routingDocument, }); urlCacheKey.push(urlSegment); currentIndex++; lastRedirectTo = routingDocument.redirectTo; const frontendAppResult = checkFrontendAppWithRoutingPrecedence(routingDocument, urlSegments, currentIndex, siteSchema, language); if (frontendAppResult.shouldUseFrontendApp) { return { documentHierarchy, redirectTo: routingDocument.redirectTo, frontendAppPath: `/${urlSegments.slice(currentIndex).join("/")}`, }; } if (routingDocument.regularChildren && currentIndex < urlSegments.length) { return parseDocumentHierarchyRecursive({ siteSchema, urlSegments, language, urlCacheKey, documentHierarchy, currentParent: routingDocument, index: currentIndex, }); } } return { documentHierarchy, redirectTo: lastRedirectTo, frontendAppPath: undefined, }; } function checkRegularDocumentFrontendApp({ candidateTypes, urlSegments, index, siteSchema, language, }) { const hasFrontendApp = candidateTypes.some((type) => type.hasFrontendApp); if (!hasFrontendApp) { return { shouldUseFrontendApp: false }; } const hasRoutingPrecedence = index < urlSegments.length && candidateTypes.some((type) => { if (!type.routingChildren?.length) return false; const nextUrlSegment = urlSegments[index]; return type.routingChildren.some((childTypeName) => { const childType = getRoutingDocumentTypeByName(siteSchema, childTypeName); return childType.route && nextUrlSegment === childType.route[language]; }); }); return { shouldUseFrontendApp: !hasRoutingPrecedence }; } function checkFrontendAppWithRoutingPrecedence(document, urlSegments, currentIndex, siteSchema, language) { if (!document.hasFrontendApp) { return { shouldUseFrontendApp: false }; } if (currentIndex >= urlSegments.length) { return { shouldUseFrontendApp: true }; } const nextUrlSegment = urlSegments[currentIndex]; const hasRoutingMatch = (document.routingChildren ?? []).some((childTypeName) => { const childType = getRoutingDocumentTypeByName(siteSchema, childTypeName); return childType.route && nextUrlSegment === childType.route[language]; }); if (hasRoutingMatch) { return { shouldUseFrontendApp: false }; } return { shouldUseFrontendApp: true }; } function findRoutingDocumentByUrlPath({ siteSchema, parent, urlSegment, language, }) { for (const typeName of parent.routingChildren ?? []) { const documentType = getRoutingDocumentTypeByName(siteSchema, typeName); if (!documentType.route) continue; if (urlSegment === documentType.route[language]) return documentType; } } function findRegularDocumentByUrlPath({ siteSchema, parentCandidates, urlSegments, index: partsIndex, }) { const candidateTypes = parentCandidates .reduce((childTypeNames, parent) => { if (parent.regularChildren) childTypeNames.push(...parent.regularChildren); return childTypeNames; }, []) ?.map((typeName) => getRegularDocumentTypeByName(siteSchema, typeName)); if (!candidateTypes || candidateTypes.length === 0) return; const route = candidateTypes[0].route; if (route === ":relativeId-:slug") { const urlPath = urlSegments[partsIndex]; const relativeId = parseRegularRelativeIdSlug(urlPath); return { lookupKind: "relativeId", relativeId, candidateTypes, skipUrlSegments: 1, }; } if (route === ":yyyy/:mm/:dd/:relativeId-:slug") { if (urlSegments.length < partsIndex + 3 || !isYear(urlSegments[partsIndex]) || !isMonth(urlSegments[partsIndex + 1]) || !isDay(urlSegments[partsIndex + 2])) { return; } const urlPath = urlSegments[partsIndex + 3]; const relativeId = parseRegularRelativeIdSlug(urlPath); return { lookupKind: "relativeId", relativeId, candidateTypes, skipUrlSegments: 4, }; } if (route === ":slug") { const slug = urlSegments[partsIndex]; return { lookupKind: "slug", slug, candidateTypes, skipUrlSegments: 1, }; } if (route === ":relativeId") { const relativeId = urlSegments[partsIndex]; return { lookupKind: "relativeId", relativeId, candidateTypes, skipUrlSegments: 1, }; } } function parseRegularRelativeIdSlug(urlPath) { const index = urlPath.indexOf("-"); return index === -1 ? urlPath : urlPath.substring(0, index); } function isYear(s) { return /[0-9]{4}/.test(s); } function isMonth(s) { if (!/[0-9]{2}/.test(s)) return false; const val = Number(s); return val >= 1 && val <= 12; } function isDay(s) { if (!/[0-9]{2}/.test(s)) return false; const val = Number(s); return val >= 1 && val <= 31; } export function findParentParsedRouteWithDocument(parsedRoute) { let curRoute = parsedRoute; while (true) { curRoute = findParentParsedRoute(curRoute); if (!curRoute) break; if (!curRoute.redirectTo) return curRoute; } } function findParentParsedRoute(parsedRoute) { const documentHierarchy = [...parsedRoute.documentHierarchy]; const urlCacheKey = parsedRoute.urlCacheKey.split("/"); if (documentHierarchy.length <= 1) return; const lastEntry = documentHierarchy.pop(); if (!lastEntry) return; if (lastEntry.entryKind === "regular") { const expectedKey = lastEntry.lookupKind === "relativeId" ? `regular:${lastEntry.relativeId}` : `slug:${lastEntry.slug}`; if (urlCacheKey[urlCacheKey.length - 1] === expectedKey) { urlCacheKey.pop(); } } else { urlCacheKey.pop(); } const lastRemainingEntry = documentHierarchy[documentHierarchy.length - 1]; const redirectTo = lastRemainingEntry?.entryKind === "routing" ? lastRemainingEntry.documentType.redirectTo : undefined; return { language: parsedRoute.language, documentHierarchy, redirectTo, urlCacheKey: urlCacheKey.join("/"), frontendAppPath: undefined, }; } //# sourceMappingURL=page-route-parser.js.map