UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

207 lines (176 loc) • 6.11 kB
import {omit} from 'lodash' import {decodeJsonParams, encodeJsonParams, route} from 'sanity/router' import {type RouterPaneGroup, type RouterPanes, type RouterPaneSibling} from './types' const EMPTY_PARAMS = {} /** * @internal */ export function legacyEditParamsToState(params: string): Record<string, unknown> { try { return JSON.parse(decodeURIComponent(params)) } catch (err) { // eslint-disable-next-line no-console console.warn('Failed to parse JSON parameters') return {} } } export function encodePanesSegment(panes: RouterPanes): string { return (panes || []) .map((group) => group.map(encodeChunks).join('|')) .map(encodeURIComponent) .join(';') } /** * @internal */ export function legacyEditParamsToPath(params: Record<string, unknown>): string { return JSON.stringify(params) } // http://localhost:3333/intent/create/template=book-by-author;type=book/eyJhdXRob3JJZCI6Imdycm0ifQ== /** * @internal */ export function toState(pathSegment: string): RouterPaneGroup[] { return parsePanesSegment(decodeURIComponent(pathSegment)) } /** * @internal */ export function toPath(panes: RouterPaneGroup[]): string { return encodePanesSegment(panes) } export const router = route.create('/', [ // "Asynchronous intent resolving" route route.intents('/intent'), // Legacy fallback route, will be redirected to new format route.create('/edit/:type/:editDocumentId', [ route.create({ path: '/:params', transform: {params: {toState: legacyEditParamsToState, toPath: legacyEditParamsToPath}}, }), ]), // The regular path - when the intent can be resolved to a specific pane route.create({ path: '/:panes', // Legacy URLs, used to handle redirects children: [route.create('/:action', route.create('/:legacyEditDocumentId'))], transform: { panes: {toState, toPath}, }, }), ]) // old: authors;knut,{"template":"diaryEntry"} // new: authors;knut,view=diff,eyJyZXYxIjoiYWJjMTIzIiwicmV2MiI6ImRlZjQ1NiJ9|latest-posts const panePattern = /^([.a-z0-9_-]+),?({.*?})?(?:(;|$))/i const isParam = (str: string) => /^[a-z0-9]+=[^=]+/i.test(str) const isPayloadLike = (str: string) => /^[A-Za-z0-9\-_]+(?:={0,2})$/.test(str) const exclusiveParams = ['view', 'since', 'rev', 'inspect', 'comment'] type Truthy<T> = T extends false ? never : T extends '' ? never : T extends 0 ? never : T extends 0n ? never : T extends null | undefined ? NonNullable<T> : T const isTruthy = Boolean as (t: unknown) => boolean as <T>(t: T) => t is Truthy<T> function parseChunks(chunks: string[], initial: RouterPaneSibling): RouterPaneSibling { const sibling: RouterPaneSibling = {...initial, params: EMPTY_PARAMS, payload: undefined} return chunks.reduce((pane, chunk) => { if (isParam(chunk)) { const key = chunk.slice(0, chunk.indexOf('=')) const value = chunk.slice(key.length + 1) pane.params = {...pane.params, [decodeURIComponent(key)]: decodeURIComponent(value)} } else if (isPayloadLike(chunk)) { pane.payload = tryParseBase64Payload(chunk) } else { // eslint-disable-next-line no-console console.warn('Unknown pane segment: %s - skipping', chunk) } return pane }, sibling) } function encodeChunks(pane: RouterPaneSibling, index: number, group: RouterPaneGroup): string { const {payload, params = {}, id} = pane const [firstSibling] = group const paneIsFirstSibling = pane === firstSibling const sameAsFirst = index !== 0 && id === firstSibling.id const encodedPayload = typeof payload === 'undefined' ? undefined : encodeJsonParams(payload as any) const encodedParams = Object.entries(params) .filter((entry): entry is [string, string] => { const [key, value] = entry if (!value) return false if (paneIsFirstSibling) return true // omit the value if it's the same as the value from the first sibling const valueFromFirstSibling = firstSibling.params?.[key] if (value === valueFromFirstSibling && !exclusiveParams.includes(key)) return false return true }) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) return ( [sameAsFirst ? '' : id] .concat([encodedParams.length > 0 && encodedParams, encodedPayload].filter(isTruthy).flat()) .join(',') || ',' ) } export function parsePanesSegment(str: string): RouterPanes { if (str.indexOf(',{') !== -1) { return parseOldPanesSegment(str) } return str .split(';') .map((group) => { const [firstSibling, ...restOfSiblings] = group.split('|').map((segment) => { const [id, ...chunks] = segment.split(',') return parseChunks(chunks, {id}) }) return [ firstSibling, ...restOfSiblings.map((sibling) => ({ ...firstSibling, ...sibling, id: sibling.id || firstSibling.id, params: {...omit(firstSibling.params, exclusiveParams), ...sibling.params}, payload: sibling.payload || firstSibling.payload, })), ] }) .filter((group) => group.length > 0) } function parseOldPanesSegment(str: string): RouterPanes { const chunks: RouterPaneGroup = [] let buffer = str while (buffer.length) { const [match, id, payloadChunk] = buffer.match(panePattern) || [] if (!match) { buffer = buffer.slice(1) continue } const payload = payloadChunk && tryParsePayload(payloadChunk) chunks.push({id, payload}) buffer = buffer.slice(match.length) } return [chunks] } function tryParsePayload(json: string) { try { return JSON.parse(json) } catch (err) { // eslint-disable-next-line no-console console.warn(`Failed to parse parameters: ${err.message}`) return undefined } } function tryParseBase64Payload(data: string): unknown { try { return data ? decodeJsonParams(data) : undefined } catch (err) { // eslint-disable-next-line no-console console.warn(`Failed to parse parameters: ${err.message}`) return undefined } }