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
486 lines (485 loc) • 20.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: !0 });
var jsxRuntime = require("react/jsx-runtime"), React = require("react"), difference = require("lodash/difference.js"), intersection = require("lodash/intersection.js"), isPlainObject = require("lodash/isPlainObject.js"), pick = require("lodash/pick.js"), debug$1 = require("debug"), identity = require("lodash/identity.js");
function _interopDefaultCompat(e) {
return e && typeof e == "object" && "default" in e ? e : { default: e };
}
var difference__default = /* @__PURE__ */ _interopDefaultCompat(difference), intersection__default = /* @__PURE__ */ _interopDefaultCompat(intersection), isPlainObject__default = /* @__PURE__ */ _interopDefaultCompat(isPlainObject), pick__default = /* @__PURE__ */ _interopDefaultCompat(pick), debug__default = /* @__PURE__ */ _interopDefaultCompat(debug$1), identity__default = /* @__PURE__ */ _interopDefaultCompat(identity);
const RouterContext = React.createContext(null);
function useRouter() {
const router = React.useContext(RouterContext);
if (!router)
throw new Error("Router: missing context value");
return router;
}
function isLeftClickEvent(event) {
return event.button === 0;
}
function isModifiedEvent(event) {
return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
}
function useLink(options) {
const { onClick: onClickProp, href, target, replace = !1 } = options, { navigateUrl } = useRouter();
return { onClick: React.useCallback(
(event) => {
event.isDefaultPrevented() || href && (onClickProp && onClickProp(event), !(isModifiedEvent(event) || !isLeftClickEvent(event)) && (target || (event.preventDefault(), navigateUrl({ path: href, replace }))));
},
[href, navigateUrl, onClickProp, replace, target]
) };
}
function useIntentLink(options) {
const { intent, onClick: onClickProp, params, replace, target } = options, { resolveIntentLink } = useRouter(), href = React.useMemo(() => resolveIntentLink(intent, params), [intent, params, resolveIntentLink]), { onClick } = useLink({ href, onClick: onClickProp, replace, target });
return { onClick, href };
}
const IntentLink = React.forwardRef(function(props, ref) {
const { intent, params, target, ...restProps } = props, { onClick, href } = useIntentLink({
intent,
params,
target,
onClick: props.onClick
});
return /* @__PURE__ */ jsxRuntime.jsx("a", { ...restProps, href, onClick, ref, target });
}), Link = React.forwardRef(function(props, ref) {
const { onClick: onClickProp, href, target, replace, ...restProps } = props, { onClick } = useLink({ onClick: onClickProp, href, target, replace });
return /* @__PURE__ */ jsxRuntime.jsx("a", { ...restProps, onClick, href, target, ref });
}), VALID_PARAM_SEGMENT = /^[a-zA-Z0-9_-]+$/;
function createSegment(segment) {
if (!segment)
return null;
if (segment.startsWith(":")) {
const paramName = segment.substring(1);
if (!VALID_PARAM_SEGMENT.test(paramName)) {
const addendum = segment.includes("*") ? " Splats are not supported. Consider using child routes instead" : "";
console.error(
new Error(`Warning: Param segments "${segment}" includes invalid characters.${addendum}`)
);
}
return { type: "param", name: paramName };
}
return { type: "dir", name: segment };
}
function _parseRoute(route2) {
const [pathname] = route2.split("?"), segments = pathname.split("/").map(createSegment).filter(Boolean);
return {
raw: route2,
segments
};
}
function arrayify(val) {
return Array.isArray(val) ? val : val ? [val] : [];
}
function isRecord(value) {
return isPlainObject__default.default(value);
}
function createMatchError(node, missingKeys, unmappableStateKeys) {
return { type: "error", node, missingKeys, unmappableStateKeys };
}
function createMatchOk(node, matchedState, searchParams, child) {
return { type: "ok", node, matchedState, searchParams, child };
}
function _findMatchingRoutes(node, _state) {
if (!_state)
return createMatchOk(node, {}, []);
const scopedState = node.scope ? _state[node.scope] : _state, { _searchParams: searchParams = [], ...state } = scopedState || {}, requiredParams = node.route.segments.filter((seg) => seg.type === "param").map((seg) => seg.name), stateKeys = isRecord(state) ? Object.keys(state) : [], consumedParams = intersection__default.default(stateKeys, requiredParams), missingParams = difference__default.default(requiredParams, consumedParams), remainingParams = difference__default.default(stateKeys, consumedParams);
if (missingParams.length > 0)
return createMatchError(node, missingParams, []);
const scopedParams = searchParams.map(([key, value]) => [[key], value]), consumedState = pick__default.default(state, consumedParams);
if (remainingParams.length === 0)
return createMatchOk(node, consumedState, scopedParams);
const children = arrayify(
(typeof node.children == "function" ? node.children(isRecord(state) ? state : {}) : node.children) || []
);
if (remainingParams.length > 0 && children.length === 0)
return createMatchError(node, [], remainingParams);
const remainingState = pick__default.default(state, remainingParams), found = children.map((childNode) => _findMatchingRoutes(childNode, remainingState)).find((res) => res.type === "ok");
return found ? createMatchOk(node, consumedState, scopedParams, found) : createMatchError(node, [], remainingParams);
}
function encodeURIComponentExcept(uriComponent, unescaped) {
const chars = [...String(uriComponent)];
let res = "";
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
unescaped.includes(char) ? res += char : res += encodeURIComponent(char);
}
return res;
}
const debug = debug__default.default("state-router");
function _resolvePathFromState(node, _state) {
debug("Resolving path from state %o", _state);
const match = _findMatchingRoutes(node, _state);
if (match.type === "error") {
const unmappable = match.unmappableStateKeys;
if (unmappable.length > 0)
throw new Error(
`Unable to find matching route for state. Could not map the following state key${unmappable.length == 1 ? "" : "s"} to a valid url: ${unmappable.map(quote).join(", ")}`
);
const missingKeys = match.missingKeys;
throw new Error(
`Unable to find matching route for state. State object is missing the following key${missingKeys.length == 1 ? "" : "s"} defined in route: ${missingKeys.map(quote).join(", ")}`
);
}
const { path, searchParams } = pathFromMatchResult(match), search = searchParams.length > 0 ? encodeParams$1(searchParams) : "";
return `/${path.join("/")}${search ? `?${search}` : ""}`;
}
function bracketify(value) {
return `[${value}]`;
}
function encodeParams$1(params) {
return params.flatMap(([key, value]) => value === void 0 ? [] : [encodeSearchParamKey(serializeScopedPath(key)), encodeSearchParamValue(value)].join(
"="
)).join("&");
}
function serializeScopedPath(scopedPath) {
const [head, ...tail] = scopedPath;
return tail.length > 0 ? [head, ...tail.map(bracketify)].join("") : head;
}
function encodeSearchParamValue(value) {
return encodeURIComponentExcept(value, "/");
}
function encodeSearchParamKey(value) {
return encodeURIComponentExcept(value, "[]");
}
function pathFromMatchResult(match) {
const matchedState = match.matchedState, base = match.node.route.segments.map((segment) => {
if (segment.type === "dir")
return segment.name;
const transform = match.node.transform && match.node.transform[segment.name];
return transform ? transform.toPath(matchedState[segment.name]) : matchedState[segment.name];
}), childMatch = match.child ? pathFromMatchResult(match.child) : void 0, searchParams = childMatch != null && childMatch.searchParams ? [...match.searchParams, ...childMatch.searchParams] : match.searchParams;
return {
searchParams: addNodeScope(match.node, searchParams),
path: [...base || [], ...(childMatch == null ? void 0 : childMatch.path) || []]
};
}
function addNodeScope(node, searchParams) {
const scope = node.scope;
return scope && !node.__unsafe_disableScopedSearchParams ? searchParams.map(([namespaces, value]) => [[scope, ...namespaces], value]) : searchParams;
}
function quote(value) {
return `"${value}"`;
}
function parseScopedParams(params) {
return params.map(([key, value]) => [parse(key), value]);
}
const OPEN = 1, CLOSED = 0;
function parse(str) {
const result = [];
let i = 0, state = CLOSED;
for (; i < str.length; ) {
const nextBracketIdx = str.indexOf("[", i);
if (nextBracketIdx === -1) {
result.push(str.slice(i, str.length));
break;
}
if (state === OPEN)
throw new Error("Nested brackets not supported");
state = OPEN, nextBracketIdx > i && (result.push(str.slice(i, nextBracketIdx)), i = nextBracketIdx);
const nextClosing = str.indexOf("]", nextBracketIdx);
if (nextClosing === -1) {
if (state === OPEN)
throw new Error("Unclosed bracket");
break;
}
state = CLOSED, result.push(str.slice(i + 1, nextClosing)), i = nextClosing + 1;
}
return result;
}
function matchPath(node, path, searchParams) {
const parts = path.split("/").filter(Boolean), segmentsLength = node.route.segments.length;
if (parts.length < segmentsLength)
return null;
const state = {};
if (!node.route.segments.every((segment, i) => {
if (segment.type === "dir")
return segment.name === parts[i];
const transform = node.transform && node.transform[segment.name];
return state[segment.name] = transform ? transform.toState(parts[i]) : parts[i], !0;
}))
return null;
const rest = parts.slice(segmentsLength);
let childState = null;
const children = typeof node.children == "function" ? arrayify(node.children(state)) : node.children, unscopedParams = removeScope(node.scope, searchParams);
if (children.some((childNode) => {
if (childNode) {
const childParams = childNode.scope ? unscopedParams.filter(([namespaces]) => childNode.scope === namespaces[0]) : unscopedParams;
return childState = matchPath(childNode, rest.join("/"), childParams), childState;
}
}), rest.length > 0 && !childState)
return null;
const selfParams = unscopedParams.flatMap(([namespace, value]) => namespace.length === 1 ? [[namespace[0], value]] : []), mergedState = {
...state,
...childState || {},
...selfParams.length > 0 ? { _searchParams: selfParams } : {}
};
return node.scope ? { [node.scope]: mergedState } : mergedState;
}
function _resolveStateFromPath(node, path) {
debug("resolving state from path %s", path);
const [pathname, search] = path.split("?"), urlSearchParams = Array.from(new URLSearchParams(search).entries()), pathMatch = matchPath(node, pathname, parseScopedParams(urlSearchParams));
return debug("resolved: %o", pathMatch || null), pathMatch || null;
}
function removeScope(scope, searchParams) {
return scope ? searchParams.map(([namespaces, value]) => [
namespaces[0] === scope ? namespaces.slice(1) : namespaces,
value
]) : searchParams;
}
function encodeBase64Url(str) {
return encodeBase64(str).replace(/\//g, "_").replace(/\+/g, "-").replace(/[=]+$/, "");
}
function decodeBase64Url(str) {
return decodeBase64(str.replace(/-/g, "+").replace(/_/g, "/"));
}
function percentToByte(p) {
return String.fromCharCode(parseInt(p.slice(1), 16));
}
function encodeBase64(str) {
return btoa(encodeURIComponent(str).replace(/%[0-9A-F]{2}/g, percentToByte));
}
function byteToPercent(b) {
return `%${`00${b.charCodeAt(0).toString(16)}`.slice(-2)}`;
}
function decodeBase64(str) {
return decodeURIComponent(Array.from(atob(str), byteToPercent).join(""));
}
function decodeJsonParams(pathSegment = "") {
const segment = decodeURIComponent(pathSegment);
if (!segment)
return {};
try {
return JSON.parse(decodeBase64Url(segment));
} catch {
}
try {
return JSON.parse(atob(segment));
} catch {
}
try {
return JSON.parse(segment);
} catch {
console.warn("Failed to parse JSON parameters");
}
return {};
}
function encodeJsonParams(params) {
return params ? encodeBase64Url(JSON.stringify(params)) : "";
}
function decodeParams(pathSegment) {
return pathSegment.split(";").reduce((params, pair) => {
const [key, value] = pair.split("=");
return params[decodeURIComponent(key)] = decodeURIComponent(value), params;
}, {});
}
function encodeParams(params) {
return Object.entries(params).filter(([, value]) => value != null).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join(";");
}
const route = {
create: (routeOrOpts, childrenOrOpts, children) => _createNode(normalizeArgs(routeOrOpts, childrenOrOpts, children)),
intents: (base) => {
const basePath = normalize(base).join("/");
return route.create(`${basePath}/:intent`, [
route.create(
":params",
{
transform: {
params: {
toState: decodeParams,
toPath: encodeParams
}
}
},
[
route.create(":payload", {
transform: {
payload: {
toState: decodeJsonParams,
toPath: encodeJsonParams
}
}
})
]
)
]);
},
scope(scopeName, routeOrOpts, childrenOrOpts, children) {
const options = normalizeArgs(routeOrOpts, childrenOrOpts, children);
return _createNode({
...options,
scope: scopeName
});
}
};
function normalizeChildren(children) {
return Array.isArray(children) || typeof children == "function" ? children : children ? [children] : [];
}
function isRoute(val) {
return val && "_isRoute" in val;
}
function normalizeArgs(path, childrenOrOpts, children) {
return typeof path == "object" ? path : Array.isArray(childrenOrOpts) || typeof childrenOrOpts == "function" || isRoute(childrenOrOpts) ? { path, children: normalizeChildren(childrenOrOpts) } : children ? { path, ...childrenOrOpts, children: normalizeChildren(children) } : { path, ...childrenOrOpts };
}
function normalize(...paths) {
return paths.reduce((acc, path) => acc.concat(path.split("/")), []).filter(Boolean);
}
const EMPTY_STATE$1 = {};
function isRoot(pathname) {
return pathname.split("/").every((segment) => !segment);
}
function _createNode(options) {
const { path, scope, transform, children, __unsafe_disableScopedSearchParams } = options;
if (!path)
throw new TypeError("Missing path");
const parsedRoute = _parseRoute(path);
return {
_isRoute: !0,
// todo: make a Router class instead
scope,
// eslint-disable-next-line camelcase
__unsafe_disableScopedSearchParams,
route: parsedRoute,
children: children || [],
transform,
encode(state) {
return _resolvePathFromState(this, state);
},
decode(_path) {
return _resolveStateFromPath(this, _path);
},
isRoot,
isNotFound(pathname) {
return this.decode(pathname) === null;
},
getBasePath() {
return this.encode(EMPTY_STATE$1);
},
getRedirectBase(pathname) {
if (isRoot(pathname)) {
const basePath = this.getBasePath();
if (pathname !== basePath)
return basePath;
}
return null;
}
};
}
function RouterProvider(props) {
const { onNavigate, router: routerProp, state } = props, resolveIntentLink = React.useCallback(
(intentName, parameters) => {
const [params, payload] = Array.isArray(parameters) ? parameters : [parameters];
return routerProp.encode({ intent: intentName, params, payload });
},
[routerProp]
), resolvePathFromState = React.useCallback(
(nextState) => routerProp.encode(nextState),
[routerProp]
), navigate = React.useCallback(
(nextState, options = {}) => {
onNavigate({ path: resolvePathFromState(nextState), replace: options.replace });
},
[onNavigate, resolvePathFromState]
), navigateIntent = React.useCallback(
(intentName, params, options = {}) => {
onNavigate({ path: resolveIntentLink(intentName, params), replace: options.replace });
},
[onNavigate, resolveIntentLink]
), router = React.useMemo(
() => ({
navigate,
navigateIntent,
navigateUrl: onNavigate,
resolveIntentLink,
resolvePathFromState,
state
}),
[navigate, navigateIntent, onNavigate, resolveIntentLink, resolvePathFromState, state]
);
return /* @__PURE__ */ jsxRuntime.jsx(RouterContext.Provider, { value: router, children: props.children });
}
function addScope(routerState, scope, scopedState) {
return scopedState && {
...routerState,
[scope]: scopedState
};
}
function RouteScope(props) {
const { children, scope, __unsafe_disableScopedSearchParams } = props, parentRouter = useRouter(), { resolvePathFromState: parent_resolvePathFromState, navigate: parent_navigate } = parentRouter, parentStateRef = React.useRef(parentRouter.state);
parentStateRef.current = parentRouter.state;
const resolveNextParentState = React.useCallback(
(_nextState) => {
const { _searchParams, ...nextState } = _nextState, nextParentState = addScope(parentStateRef.current, scope, nextState);
return __unsafe_disableScopedSearchParams ? nextParentState._searchParams = _searchParams : nextParentState[scope]._searchParams = _searchParams, nextParentState;
},
[scope, __unsafe_disableScopedSearchParams]
), resolvePathFromState = React.useCallback(
(nextState) => parent_resolvePathFromState(resolveNextParentState(nextState)),
[parent_resolvePathFromState, resolveNextParentState]
), navigate = React.useCallback(
(nextState) => parent_navigate(resolveNextParentState(nextState)),
[parent_navigate, resolveNextParentState]
), childRouter = React.useMemo(() => {
const parentState = parentRouter.state, childState = { ...parentState[scope] || {} };
return __unsafe_disableScopedSearchParams && (childState._searchParams = parentState._searchParams), {
...parentRouter,
navigate,
resolvePathFromState,
state: childState
};
}, [scope, parentRouter, navigate, resolvePathFromState, __unsafe_disableScopedSearchParams]);
return /* @__PURE__ */ jsxRuntime.jsx(RouterContext.Provider, { value: childRouter, children });
}
const EMPTY_STATE = {};
function useStateLink(options) {
const { onClick: onClickProp, replace, state, target, toIndex = !1 } = options;
if (state && toIndex)
throw new Error("Passing both `state` and `toIndex={true}` as props to StateLink is invalid");
!state && !toIndex && console.error(
new Error(
"No state passed to StateLink. If you want to link to an empty state, its better to use the the `toIndex` property"
)
);
const { resolvePathFromState } = useRouter(), href = React.useMemo(
() => resolvePathFromState(toIndex ? EMPTY_STATE : state || EMPTY_STATE),
[resolvePathFromState, state, toIndex]
), { onClick } = useLink({ href, onClick: onClickProp, replace, target });
return { onClick, href };
}
const StateLink = React.forwardRef(function(props, ref) {
const { onClick: onClickProp, replace, state, target, toIndex = !1, ...restProps } = props, { onClick, href } = useStateLink({
onClick: onClickProp,
replace,
state,
target,
toIndex
});
return /* @__PURE__ */ jsxRuntime.jsx("a", { ...restProps, href, onClick, ref });
});
function useRouterState(selector = identity__default.default) {
const { state } = useRouter();
return React.useMemo(() => selector(state), [selector, state]);
}
function withRouter(Component) {
function WithRouter2(props) {
const router = useRouter();
return /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props, router });
}
return WithRouter2.displayName = `withRouter(${Component.displayName || Component.name})`, WithRouter2;
}
const WithRouter = withRouter((props) => props.children(props.router));
exports.IntentLink = IntentLink;
exports.Link = Link;
exports.RouteScope = RouteScope;
exports.RouterContext = RouterContext;
exports.RouterProvider = RouterProvider;
exports.StateLink = StateLink;
exports.WithRouter = WithRouter;
exports._createNode = _createNode;
exports.decodeJsonParams = decodeJsonParams;
exports.encodeJsonParams = encodeJsonParams;
exports.route = route;
exports.useIntentLink = useIntentLink;
exports.useLink = useLink;
exports.useRouter = useRouter;
exports.useRouterState = useRouterState;
exports.useStateLink = useStateLink;
exports.withRouter = withRouter;
//# sourceMappingURL=router.js.map