@marktiderman/genesis-ui 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EXTENDING.md +40 -1
- package/SKILL.md +198 -9
- package/dist/{AppShell-CX0M-dfc.d.ts → AppShell-LXQykEqe.d.ts} +47 -5
- package/dist/{chunk-M62CHO3N.js → chunk-4C4ESW6O.js} +9 -4
- package/dist/chunk-4C4ESW6O.js.map +1 -0
- package/dist/chunk-DIV4BYXX.js +58 -0
- package/dist/chunk-DIV4BYXX.js.map +1 -0
- package/dist/chunk-FUVWKTPL.js +19 -0
- package/dist/chunk-FUVWKTPL.js.map +1 -0
- package/dist/{chunk-PLIYF6KI.js → chunk-MPI33I4K.js} +2 -2
- package/dist/{chunk-PLIYF6KI.js.map → chunk-MPI33I4K.js.map} +1 -1
- package/dist/{chunk-JA3ZKULS.js → chunk-OITWCIG2.js} +3 -3
- package/dist/chunk-OITWCIG2.js.map +1 -0
- package/dist/chunk-PKFDGGZV.js +65 -0
- package/dist/chunk-PKFDGGZV.js.map +1 -0
- package/dist/{chunk-IXGAEECT.js → chunk-RKPS7ERS.js} +9 -5
- package/dist/chunk-RKPS7ERS.js.map +1 -0
- package/dist/{chunk-TXOZMKNW.js → chunk-S7NXD37O.js} +27 -26
- package/dist/chunk-S7NXD37O.js.map +1 -0
- package/dist/{chunk-ZPPLUGLA.js → chunk-UYI3GLFJ.js} +162 -19
- package/dist/chunk-UYI3GLFJ.js.map +1 -0
- package/dist/{chunk-CDYZRZ6U.js → chunk-WGMEVX3Y.js} +2 -2
- package/dist/chunk-WGMEVX3Y.js.map +1 -0
- package/dist/chunk-ZDHWNQTR.js +552 -0
- package/dist/chunk-ZDHWNQTR.js.map +1 -0
- package/dist/components/data/index.d.ts +306 -19
- package/dist/components/data/index.js +1177 -284
- package/dist/components/data/index.js.map +1 -1
- package/dist/components/layout/form-page.d.ts +1 -1
- package/dist/components/layout/form-page.js +1 -1
- package/dist/components/layout/index.d.ts +2 -2
- package/dist/components/layout/index.js +8 -8
- package/dist/components/patterns/index.d.ts +1 -1
- package/dist/components/patterns/index.js +5 -5
- package/dist/components/patterns/sortable-list.js +1 -1
- package/dist/components/rich-text/rich-text-editor.d.ts +194 -0
- package/dist/components/rich-text/rich-text-editor.js +833 -0
- package/dist/components/rich-text/rich-text-editor.js.map +1 -0
- package/dist/components/rich-text/rich-text.d.ts +51 -0
- package/dist/components/rich-text/rich-text.js +4 -0
- package/dist/components/rich-text/rich-text.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +12 -1
- package/dist/components/ui/alert-dialog.js +1 -1
- package/dist/hooks/index.d.ts +4 -3
- package/dist/hooks/index.js +5 -4
- package/dist/hooks/use-data-filters.d.ts +5 -3
- package/dist/hooks/use-data-filters.js +2 -2
- package/dist/hooks/use-resource-page.d.ts +2 -1
- package/dist/hooks/use-resource-page.js +3 -2
- package/dist/hooks/use-resource-url-state.d.ts +68 -0
- package/dist/hooks/use-resource-url-state.js +3 -0
- package/dist/hooks/use-resource-url-state.js.map +1 -0
- package/dist/hooks/use-view-preference.d.ts +17 -2
- package/dist/hooks/use-view-preference.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +32 -32
- package/dist/{navigation-DBwcZ6vb.d.ts → navigation-Bha0R3Fu.d.ts} +6 -3
- package/dist/rich-text-doc-C54fME2C.d.ts +180 -0
- package/dist/router/react-router.d.ts +47 -0
- package/dist/router/react-router.js +53 -0
- package/dist/router/react-router.js.map +1 -0
- package/dist/router/tanstack-router.d.ts +47 -0
- package/dist/router/tanstack-router.js +105 -0
- package/dist/router/tanstack-router.js.map +1 -0
- package/dist/{use-resource-page-Dd2VUv30.d.ts → use-resource-page-BDnnCssE.d.ts} +83 -5
- package/package.json +61 -14
- package/dist/chunk-CDYZRZ6U.js.map +0 -1
- package/dist/chunk-FTTNGGLA.js +0 -20
- package/dist/chunk-FTTNGGLA.js.map +0 -1
- package/dist/chunk-IXGAEECT.js.map +0 -1
- package/dist/chunk-JA3ZKULS.js.map +0 -1
- package/dist/chunk-M62CHO3N.js.map +0 -1
- package/dist/chunk-TXOZMKNW.js.map +0 -1
- package/dist/chunk-ZPPLUGLA.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/navigation.tsx"],"names":["trail"],"mappings":"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","file":"chunk-WGMEVX3Y.js","sourcesContent":["\"use client\";\n\n/**\n * Router-agnostic navigation primitives.\n *\n * genesis-ui does NOT depend on any router. Components that need to render a\n * link or navigate imperatively accept an injectable abstraction defined here,\n * and default to plain browser behavior (`<a>` / `window.location`). Consumers\n * on a router inject an adapter that maps these props to their own `<Link>` /\n * router. Ready-made ones ship on their own subpaths, each with its router as\n * an optional peer: `@marktiderman/genesis-ui/router/react-router` and\n * `@marktiderman/genesis-ui/router/tanstack-router`. Next.js passes\n * `next/link` directly.\n */\nimport {\n useCallback,\n useEffect,\n useState,\n type AnchorHTMLAttributes,\n type ComponentType,\n type ReactNode,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// Link abstraction\n// ---------------------------------------------------------------------------\n\n/**\n * Props passed to an injectable link component. The contract is a plain anchor\n * (`href`), so the default works with NO router. A React Router consumer maps\n * `href` → `to`; a Next.js consumer passes `next/link` directly (it already\n * accepts `href`, `className`, `children`).\n *\n * @stability Beta\n */\nexport interface GenesisLinkProps {\n /** Destination path or URL. */\n href: string;\n className?: string;\n onClick?: () => void;\n children?: ReactNode;\n /** Set to \"page\" when the link points at the active route. */\n \"aria-current\"?: AnchorHTMLAttributes<HTMLAnchorElement>[\"aria-current\"];\n /** Accessible name — useful when the visible label is hidden (icon rail). */\n \"aria-label\"?: string;\n /** Native tooltip / title attribute. */\n title?: string;\n /**\n * Stable test/automation selector. {@link DefaultLink} renders it as\n * `data-testid`; injected router adapters (`next/link`, RR `Link`) forward it\n * to the underlying `<a>` too.\n */\n testID?: string;\n}\n\n/**\n * A component that renders a navigable link from {@link GenesisLinkProps}.\n *\n * @stability Beta\n */\nexport type LinkComponent = ComponentType<GenesisLinkProps>;\n\n/**\n * Default link — a plain `<a>`. No router required.\n *\n * @stability Beta\n */\nexport const DefaultLink: LinkComponent = ({\n href,\n children,\n testID,\n ...rest\n}) => (\n <a href={href} data-testid={testID} {...rest}>\n {children}\n </a>\n);\n\n/**\n * Split a href into its path (trailing slash dropped, except for `/`), query\n * (no `?`) and hash (no `#`).\n */\nfunction splitHref(href: string): { path: string; search: string; hash: string } {\n const hashAt = href.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : href.slice(hashAt + 1);\n const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const searchAt = beforeHash.indexOf(\"?\");\n const rawPath = searchAt === -1 ? beforeHash : beforeHash.slice(0, searchAt);\n return {\n path: rawPath.length > 1 ? rawPath.replace(/\\/+$/, \"\") || \"/\" : rawPath,\n search: searchAt === -1 ? \"\" : beforeHash.slice(searchAt + 1),\n hash,\n };\n}\n\n/**\n * Options for {@link isPathActive}.\n *\n * @stability Beta\n */\nexport interface IsPathActiveOptions {\n /** Require an exact path match instead of a prefix match (like `NavLink end`). */\n end?: boolean;\n}\n\n/**\n * Default active-path matcher, mirroring React Router's `NavLink` (prefix\n * match, with `/` treated as exact). Consumers can override via an `isActive`\n * prop where components accept one.\n *\n * `activePath` may carry a query and hash (`pathname + search + hash`). An\n * item whose `to` has neither compares against the path part only, exactly as\n * before. An item that opts in by putting a query or hash in its `to`\n * (`/admin?tab=users`, `/settings#profile`) is active only when the path\n * matches exactly AND every query param it names has the same value in\n * `activePath` (extra params such as `&page=2` are ignored) AND, if it names a\n * hash, the hash is equal.\n *\n * @stability Beta\n */\nexport function isPathActive(\n to: string,\n activePath?: string,\n options?: IsPathActiveOptions,\n): boolean {\n if (activePath == null) return false;\n if (to === activePath) return true;\n const target = splitHref(to);\n const current = splitHref(activePath);\n\n if (!target.search && !target.hash) {\n if (target.path === current.path) return true;\n if (options?.end) return false;\n return target.path !== \"/\" && current.path.startsWith(target.path + \"/\");\n }\n\n if (target.path !== current.path) return false;\n if (target.hash && target.hash !== current.hash) return false;\n if (target.search) {\n const want = new URLSearchParams(target.search);\n const have = new URLSearchParams(current.search);\n for (const key of new Set(want.keys())) {\n const wanted = want.getAll(key);\n const had = have.getAll(key);\n if (!wanted.every((value) => had.includes(value))) return false;\n }\n }\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Hierarchical navigation resolver\n// ---------------------------------------------------------------------------\n\n/**\n * One node of a logical navigation hierarchy. `path` is a route pattern whose\n * segments are either static (`clients`) or named params (`$clientId`). A\n * node's place in the tree is its LOGICAL parent, which need not share its URL\n * prefix — `/clients/$clientId` can sit under `/dashboard`.\n *\n * @stability Beta\n */\nexport interface NavigationNode<T> {\n id: string;\n path: string;\n label: string;\n /** Contextual nav items to show while this node is current. */\n items: T[];\n children?: NavigationNode<T>[];\n}\n\n/**\n * Result of {@link resolveNavigation}. `current`, `parent` and every `trail`\n * entry are shallow copies of their tree nodes with `path` interpolated from\n * `params` (a param the URL did not supply stays as `$name`).\n *\n * @stability Beta\n */\nexport interface ResolvedNavigation<T> {\n /** The best-matching node — the root when nothing else matches. */\n current: NavigationNode<T>;\n /** `current`'s logical parent, for an \"up\" / back-to-parent link. */\n parent?: NavigationNode<T>;\n /** `current.items`. */\n items: T[];\n /** Root → `current`, inclusive. */\n trail: NavigationNode<T>[];\n /** Params captured by `current.path`, URL-decoded. */\n params: Record<string, string>;\n}\n\nfunction pathSegments(path: string): string[] {\n return splitHref(path).path.split(\"/\").filter(Boolean);\n}\n\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\n/**\n * Resolve where `pathname` sits in a logical navigation tree. Pure: it never\n * reads browser history or `window`.\n *\n * A node matches when its pattern matches a leading run of WHOLE path\n * segments (`/my` matches `/my/tasks`, never `/my-other`). Among matches the\n * winner is decided by positional specificity: at the first position where\n * one pattern is static and the other a `$param`, the static one wins (so\n * `/clients/new` beats `/clients/$clientId` whatever the sibling order, and\n * `/my` beats `/$space/tasks` for `/my/tasks`). Otherwise the pattern matching\n * more segments wins, then the deeper tree node.\n * Non-matching nodes are still searched, so a match keeps its logical\n * ancestors even when their URLs differ.\n *\n * @stability Beta\n */\nexport function resolveNavigation<T>(\n tree: NavigationNode<T>,\n pathname: string,\n): ResolvedNavigation<T> {\n // Compare decoded forms on both sides, so `/caf%C3%A9` matches a `/café`\n // node (and an encoded pattern matches a decoded pathname).\n const segments = pathSegments(pathname).map(decodeSegment);\n\n type Match = { trail: NavigationNode<T>[]; pattern: string[] };\n const matches = (pattern: string[]) =>\n pattern.length <= segments.length &&\n pattern.every(\n (part, i) => part.startsWith(\"$\") || decodeSegment(part) === segments[i],\n );\n\n // > 0 when `a` should win over `b`.\n // Positional specificity, as TanStack Router and React Router rank routes:\n // walking left to right, the first position where one pattern is static and\n // the other a `$param` decides. Only then does the longer match win.\n const compare = (a: Match, b: Match): number => {\n const shared = Math.min(a.pattern.length, b.pattern.length);\n for (let i = 0; i < shared; i++) {\n const aParam = a.pattern[i]!.startsWith(\"$\");\n const bParam = b.pattern[i]!.startsWith(\"$\");\n if (aParam !== bParam) return aParam ? -1 : 1;\n }\n if (a.pattern.length !== b.pattern.length) {\n return a.pattern.length - b.pattern.length;\n }\n return a.trail.length - b.trail.length;\n };\n\n let best: Match | undefined;\n const visit = (node: NavigationNode<T>, ancestors: NavigationNode<T>[]) => {\n const trail = [...ancestors, node];\n const pattern = pathSegments(node.path);\n if (matches(pattern)) {\n const candidate = { trail, pattern };\n if (!best || compare(candidate, best) > 0) best = candidate;\n }\n for (const child of node.children ?? []) visit(child, trail);\n };\n visit(tree, []);\n\n const winner: Match = best ?? { trail: [tree], pattern: [] };\n const params: Record<string, string> = {};\n winner.pattern.forEach((part, i) => {\n if (part.startsWith(\"$\")) params[part.slice(1)] = segments[i]!;\n });\n\n const interpolate = (node: NavigationNode<T>): NavigationNode<T> => ({\n ...node,\n path: node.path\n .split(\"/\")\n .map((part) => {\n if (!part.startsWith(\"$\")) return part;\n const value = params[part.slice(1)];\n return value == null ? part : encodeURIComponent(value);\n })\n .join(\"/\"),\n });\n\n const trail = winner.trail.map(interpolate);\n const current = trail[trail.length - 1]!;\n return {\n current,\n parent: trail.length > 1 ? trail[trail.length - 2] : undefined,\n items: current.items,\n trail,\n params,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Imperative navigation abstraction\n// ---------------------------------------------------------------------------\n\n/**\n * Injectable imperative navigation callback.\n *\n * @stability Beta\n */\nexport type NavigateFn = (path: string) => void;\n\n/**\n * Default navigate — a full-page navigation. No router required.\n *\n * @stability Beta\n */\nexport const defaultNavigate: NavigateFn = (path) => {\n if (typeof window !== \"undefined\") window.location.assign(path);\n};\n\n// ---------------------------------------------------------------------------\n// Search-params abstraction (for URL-synced hooks)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal, router-agnostic replacement for React Router's `useSearchParams`\n * return shape. Consumers may inject their router's implementation for full\n * SPA integration; otherwise the browser History API is used.\n *\n * @stability Beta\n */\nexport interface SearchParamsAdapter {\n searchParams: URLSearchParams;\n setSearchParams: (\n next: URLSearchParams,\n options?: { replace?: boolean },\n ) => void;\n}\n\n/**\n * Browser-native {@link SearchParamsAdapter} built on the History API. Reads\n * the current query string and writes updates via `pushState`/`replaceState`,\n * staying reactive to `popstate` events. No router required.\n *\n * @stability Beta\n */\nexport function useBrowserSearchParams(): SearchParamsAdapter {\n const read = () =>\n new URLSearchParams(\n typeof window !== \"undefined\" ? window.location.search : \"\",\n );\n\n const [searchParams, setLocal] = useState<URLSearchParams>(read);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const onPop = () => setLocal(read());\n window.addEventListener(\"popstate\", onPop);\n return () => window.removeEventListener(\"popstate\", onPop);\n }, []);\n\n const setSearchParams = useCallback<SearchParamsAdapter[\"setSearchParams\"]>(\n (next, options) => {\n if (typeof window === \"undefined\") return;\n const qs = next.toString();\n const url =\n window.location.pathname + (qs ? `?${qs}` : \"\") + window.location.hash;\n if (options?.replace) {\n window.history.replaceState(window.history.state, \"\", url);\n } else {\n window.history.pushState(window.history.state, \"\", url);\n }\n setLocal(new URLSearchParams(next));\n },\n [],\n );\n\n return { searchParams, setSearchParams };\n}\n"]}
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { cn } from './chunk-NZ5WBA4R.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/components/rich-text/rich-text-doc.ts
|
|
6
|
+
var RichTextLimitError = class extends Error {
|
|
7
|
+
reason;
|
|
8
|
+
limit;
|
|
9
|
+
/** The measured size; for `"depth"`, the limit plus one. */
|
|
10
|
+
actual;
|
|
11
|
+
constructor(reason, limit, actual) {
|
|
12
|
+
super(
|
|
13
|
+
reason === "depth" ? `Rich text is nested more than ${limit} levels deep` : `Rich text has ${actual} ${reason}; the limit is ${limit}`
|
|
14
|
+
);
|
|
15
|
+
this.name = "RichTextLimitError";
|
|
16
|
+
this.reason = reason;
|
|
17
|
+
this.limit = limit;
|
|
18
|
+
this.actual = actual;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var MAX_DEPTH = 32;
|
|
22
|
+
var MAX_TITLE_LENGTH = 200;
|
|
23
|
+
function emptyRichTextDoc() {
|
|
24
|
+
return { type: "doc", content: [{ type: "paragraph" }] };
|
|
25
|
+
}
|
|
26
|
+
var SAFE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
27
|
+
function isSafeUrl(url) {
|
|
28
|
+
if (typeof url !== "string") return false;
|
|
29
|
+
const value = url.trim();
|
|
30
|
+
if (value === "" || value.length > 2048) return false;
|
|
31
|
+
if (/[\u0000- \u007f-\u009f]/.test(value)) return false;
|
|
32
|
+
if (value.startsWith("#")) return true;
|
|
33
|
+
if (value.startsWith("/")) return !value.startsWith("//") && !value.startsWith("/\\");
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(value);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return SAFE_PROTOCOLS.has(parsed.protocol);
|
|
41
|
+
}
|
|
42
|
+
function normalizeLinkUrl(input) {
|
|
43
|
+
const value = input.trim();
|
|
44
|
+
if (value === "") return null;
|
|
45
|
+
if (isSafeUrl(value)) return value;
|
|
46
|
+
if (/^[^\s@/:]+@[^\s@/]+\.[^\s@/]+$/.test(value)) {
|
|
47
|
+
const mail = `mailto:${value}`;
|
|
48
|
+
return isSafeUrl(mail) ? mail : null;
|
|
49
|
+
}
|
|
50
|
+
if (!/^[a-z][a-z0-9+.-]*:/i.test(value) && /^[^\s/]+\.[^\s/]+/.test(value)) {
|
|
51
|
+
const withScheme = `https://${value}`;
|
|
52
|
+
return isSafeUrl(withScheme) ? withScheme : null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
var YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
|
|
57
|
+
var VIMEO_ID = /^\d{1,12}$/;
|
|
58
|
+
var VIMEO_HASH = /^[a-f0-9]{6,32}$/;
|
|
59
|
+
var LOOM_ID = /^[A-Za-z0-9]{8,64}$/;
|
|
60
|
+
function hostOf(url) {
|
|
61
|
+
return url.hostname.toLowerCase().replace(/^www\./, "").replace(/^m\./, "");
|
|
62
|
+
}
|
|
63
|
+
function parseVideoUrl(input) {
|
|
64
|
+
let url;
|
|
65
|
+
try {
|
|
66
|
+
url = new URL(input.trim());
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
71
|
+
const host = hostOf(url);
|
|
72
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
73
|
+
if (host === "youtube.com" || host === "youtube-nocookie.com" || host === "music.youtube.com") {
|
|
74
|
+
let id = null;
|
|
75
|
+
if (parts[0] === "watch") id = url.searchParams.get("v");
|
|
76
|
+
else if (parts[0] === "embed" || parts[0] === "shorts" || parts[0] === "live" || parts[0] === "v")
|
|
77
|
+
id = parts[1];
|
|
78
|
+
return id && YOUTUBE_ID.test(id) ? { provider: "youtube", id } : null;
|
|
79
|
+
}
|
|
80
|
+
if (host === "youtu.be") {
|
|
81
|
+
const id = parts[0];
|
|
82
|
+
return id && YOUTUBE_ID.test(id) ? { provider: "youtube", id } : null;
|
|
83
|
+
}
|
|
84
|
+
if (host === "vimeo.com" || host === "player.vimeo.com") {
|
|
85
|
+
const videoAt = parts.lastIndexOf("video");
|
|
86
|
+
const idIndex = videoAt !== -1 && parts[videoAt + 1] && VIMEO_ID.test(parts[videoAt + 1]) ? videoAt + 1 : parts.findIndex((p) => VIMEO_ID.test(p));
|
|
87
|
+
if (idIndex === -1) return null;
|
|
88
|
+
const id = parts[idIndex];
|
|
89
|
+
const hash = url.searchParams.get("h") ?? (host === "vimeo.com" && idIndex === 0 ? parts[1] : void 0);
|
|
90
|
+
return hash && VIMEO_HASH.test(hash) ? { provider: "vimeo", id, hash } : { provider: "vimeo", id };
|
|
91
|
+
}
|
|
92
|
+
if (host === "loom.com") {
|
|
93
|
+
if (parts[0] !== "share" && parts[0] !== "embed") return null;
|
|
94
|
+
const id = parts[1];
|
|
95
|
+
return id && LOOM_ID.test(id) ? { provider: "loom", id } : null;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function sanitizeVideoEmbedAttrs(attrs) {
|
|
100
|
+
if (!attrs || typeof attrs !== "object") return null;
|
|
101
|
+
const a = attrs;
|
|
102
|
+
const id = typeof a.id === "string" ? a.id : "";
|
|
103
|
+
let clean = null;
|
|
104
|
+
if (a.provider === "youtube" && YOUTUBE_ID.test(id)) clean = { provider: "youtube", id };
|
|
105
|
+
else if (a.provider === "vimeo" && VIMEO_ID.test(id)) {
|
|
106
|
+
clean = { provider: "vimeo", id };
|
|
107
|
+
if (typeof a.hash === "string" && VIMEO_HASH.test(a.hash)) clean.hash = a.hash;
|
|
108
|
+
} else if (a.provider === "loom" && LOOM_ID.test(id)) clean = { provider: "loom", id };
|
|
109
|
+
if (!clean) return null;
|
|
110
|
+
if (typeof a.title === "string" && a.title.trim() !== "") {
|
|
111
|
+
clean.title = a.title.trim().slice(0, MAX_TITLE_LENGTH);
|
|
112
|
+
}
|
|
113
|
+
return clean;
|
|
114
|
+
}
|
|
115
|
+
var PROVIDER_NAMES = {
|
|
116
|
+
youtube: "YouTube",
|
|
117
|
+
vimeo: "Vimeo",
|
|
118
|
+
loom: "Loom"
|
|
119
|
+
};
|
|
120
|
+
function videoEmbedSrc(attrs) {
|
|
121
|
+
switch (attrs.provider) {
|
|
122
|
+
case "youtube":
|
|
123
|
+
return `https://www.youtube-nocookie.com/embed/${attrs.id}`;
|
|
124
|
+
case "vimeo":
|
|
125
|
+
return `https://player.vimeo.com/video/${attrs.id}${attrs.hash ? `?h=${attrs.hash}` : ""}`;
|
|
126
|
+
case "loom":
|
|
127
|
+
return `https://www.loom.com/embed/${attrs.id}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function videoWatchUrl(attrs) {
|
|
131
|
+
switch (attrs.provider) {
|
|
132
|
+
case "youtube":
|
|
133
|
+
return `https://www.youtube.com/watch?v=${attrs.id}`;
|
|
134
|
+
case "vimeo":
|
|
135
|
+
return `https://vimeo.com/${attrs.id}${attrs.hash ? `/${attrs.hash}` : ""}`;
|
|
136
|
+
case "loom":
|
|
137
|
+
return `https://www.loom.com/share/${attrs.id}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function videoEmbedTitle(attrs) {
|
|
141
|
+
return attrs.title || `${PROVIDER_NAMES[attrs.provider]} video`;
|
|
142
|
+
}
|
|
143
|
+
var VIDEO_IFRAME_ATTRS = {
|
|
144
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture",
|
|
145
|
+
loading: "lazy",
|
|
146
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
147
|
+
sandbox: "allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox"
|
|
148
|
+
};
|
|
149
|
+
var BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
150
|
+
"paragraph",
|
|
151
|
+
"heading",
|
|
152
|
+
"bulletList",
|
|
153
|
+
"orderedList",
|
|
154
|
+
"listItem",
|
|
155
|
+
"blockquote",
|
|
156
|
+
"videoEmbed"
|
|
157
|
+
]);
|
|
158
|
+
function tooDeep(depth, ctx) {
|
|
159
|
+
if (depth <= MAX_DEPTH) return false;
|
|
160
|
+
if (ctx.throwTooDeep) throw new RichTextLimitError("depth", MAX_DEPTH, depth);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
function measure(nodes, size = { nodes: 0, characters: 0 }) {
|
|
164
|
+
for (const node of nodes ?? []) {
|
|
165
|
+
size.nodes += 1;
|
|
166
|
+
if (typeof node.text === "string") size.characters += node.text.length;
|
|
167
|
+
measure(node.content, size);
|
|
168
|
+
}
|
|
169
|
+
return size;
|
|
170
|
+
}
|
|
171
|
+
function precheckRawSize(root, options) {
|
|
172
|
+
const nodeBudget = options.maxNodes !== void 0 ? options.maxNodes * 4 : Infinity;
|
|
173
|
+
const charBudget = options.maxCharacters !== void 0 ? options.maxCharacters * 2 : Infinity;
|
|
174
|
+
let nodes = 0;
|
|
175
|
+
let characters = 0;
|
|
176
|
+
const stack = [root.content];
|
|
177
|
+
while (stack.length > 0) {
|
|
178
|
+
const list = stack.pop();
|
|
179
|
+
if (!Array.isArray(list)) continue;
|
|
180
|
+
for (const node of list) {
|
|
181
|
+
if (!node || typeof node !== "object") continue;
|
|
182
|
+
nodes += 1;
|
|
183
|
+
const text = node.text;
|
|
184
|
+
if (typeof text === "string") characters += text.length;
|
|
185
|
+
if (nodes > nodeBudget) throw new RichTextLimitError("nodes", options.maxNodes, nodes);
|
|
186
|
+
if (characters > charBudget) {
|
|
187
|
+
throw new RichTextLimitError("characters", options.maxCharacters, characters);
|
|
188
|
+
}
|
|
189
|
+
stack.push(node.content);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function sanitizeMarks(marks) {
|
|
194
|
+
if (!Array.isArray(marks)) return void 0;
|
|
195
|
+
const out = [];
|
|
196
|
+
const seen = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const mark of marks) {
|
|
198
|
+
if (!mark || typeof mark !== "object") continue;
|
|
199
|
+
const type = mark.type;
|
|
200
|
+
if (seen.has(type)) continue;
|
|
201
|
+
if (type === "bold" || type === "italic" || type === "underline") {
|
|
202
|
+
out.push({ type });
|
|
203
|
+
seen.add(type);
|
|
204
|
+
} else if (type === "link") {
|
|
205
|
+
const href = mark.attrs?.href;
|
|
206
|
+
if (isSafeUrl(href)) {
|
|
207
|
+
out.push({ type: "link", attrs: { href: href.trim() } });
|
|
208
|
+
seen.add(type);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out.length > 0 ? out : void 0;
|
|
213
|
+
}
|
|
214
|
+
function sanitizeInline(nodes, depth, ctx) {
|
|
215
|
+
if (!Array.isArray(nodes) || tooDeep(depth, ctx)) return [];
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const node of nodes) {
|
|
218
|
+
if (!node || typeof node !== "object") continue;
|
|
219
|
+
const n = node;
|
|
220
|
+
if (n.type === "text") {
|
|
221
|
+
if (typeof n.text !== "string" || n.text === "") continue;
|
|
222
|
+
const marks = sanitizeMarks(n.marks);
|
|
223
|
+
out.push(marks ? { type: "text", text: n.text, marks } : { type: "text", text: n.text });
|
|
224
|
+
} else if (n.type === "hardBreak") {
|
|
225
|
+
out.push({ type: "hardBreak" });
|
|
226
|
+
} else if (Array.isArray(n.content)) {
|
|
227
|
+
for (const child of sanitizeInline(n.content, depth + 1, ctx)) out.push(child);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
function withContent(type, content, attrs) {
|
|
233
|
+
const node = { type };
|
|
234
|
+
if (attrs) node.attrs = attrs;
|
|
235
|
+
if (content.length > 0) node.content = content;
|
|
236
|
+
return node;
|
|
237
|
+
}
|
|
238
|
+
function nearestLevel(level, levels) {
|
|
239
|
+
const n = typeof level === "number" ? level : 2;
|
|
240
|
+
if (levels.includes(n)) return n;
|
|
241
|
+
const sorted = [...levels].sort((a, b) => Math.abs(a - n) - Math.abs(b - n) || a - b);
|
|
242
|
+
return sorted[0] ?? 2;
|
|
243
|
+
}
|
|
244
|
+
function sanitizeListItems(nodes, ctx, depth) {
|
|
245
|
+
if (!Array.isArray(nodes) || tooDeep(depth, ctx)) return [];
|
|
246
|
+
const items = [];
|
|
247
|
+
for (const node of nodes) {
|
|
248
|
+
if (!node || typeof node !== "object") continue;
|
|
249
|
+
const n = node;
|
|
250
|
+
const inner = n.type === "listItem" ? n.content : [n];
|
|
251
|
+
const blocks = sanitizeBlocks(inner, ctx, depth + 1);
|
|
252
|
+
if (blocks.length === 0 || blocks[0].type !== "paragraph") blocks.unshift({ type: "paragraph" });
|
|
253
|
+
items.push({ type: "listItem", content: blocks });
|
|
254
|
+
}
|
|
255
|
+
return items;
|
|
256
|
+
}
|
|
257
|
+
function sanitizeBlocks(nodes, ctx, depth) {
|
|
258
|
+
if (!Array.isArray(nodes) || tooDeep(depth, ctx)) return [];
|
|
259
|
+
const out = [];
|
|
260
|
+
let pendingInline = [];
|
|
261
|
+
const flushInline = () => {
|
|
262
|
+
if (pendingInline.length > 0) out.push(withContent("paragraph", pendingInline));
|
|
263
|
+
pendingInline = [];
|
|
264
|
+
};
|
|
265
|
+
for (const node of nodes) {
|
|
266
|
+
if (!node || typeof node !== "object") continue;
|
|
267
|
+
const n = node;
|
|
268
|
+
if (n.type === "text" || n.type === "hardBreak") {
|
|
269
|
+
for (const child of sanitizeInline([n], depth, ctx)) pendingInline.push(child);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
flushInline();
|
|
273
|
+
switch (n.type) {
|
|
274
|
+
case "paragraph":
|
|
275
|
+
out.push(withContent("paragraph", sanitizeInline(n.content, depth + 1, ctx)));
|
|
276
|
+
break;
|
|
277
|
+
case "heading":
|
|
278
|
+
out.push(
|
|
279
|
+
withContent("heading", sanitizeInline(n.content, depth + 1, ctx), {
|
|
280
|
+
level: nearestLevel(n.attrs?.level, ctx.levels)
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
break;
|
|
284
|
+
case "bulletList":
|
|
285
|
+
case "orderedList": {
|
|
286
|
+
const items = sanitizeListItems(n.content, ctx, depth);
|
|
287
|
+
if (items.length === 0) break;
|
|
288
|
+
const start = n.attrs?.start;
|
|
289
|
+
const attrs = n.type === "orderedList" && typeof start === "number" && Number.isInteger(start) && start > 1 ? { start: Math.min(start, 1e6) } : void 0;
|
|
290
|
+
out.push(withContent(n.type, items, attrs));
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "listItem":
|
|
294
|
+
for (const child of sanitizeBlocks(n.content, ctx, depth + 1)) out.push(child);
|
|
295
|
+
break;
|
|
296
|
+
case "blockquote": {
|
|
297
|
+
const inner = sanitizeBlocks(n.content, ctx, depth + 1);
|
|
298
|
+
out.push(withContent("blockquote", inner.length > 0 ? inner : [{ type: "paragraph" }]));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
case "videoEmbed": {
|
|
302
|
+
if (!ctx.embeds) break;
|
|
303
|
+
const attrs = sanitizeVideoEmbedAttrs(n.attrs);
|
|
304
|
+
if (attrs) out.push({ type: "videoEmbed", attrs: { ...attrs } });
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
default:
|
|
308
|
+
if (Array.isArray(n.content)) {
|
|
309
|
+
const nested = n.content.some(
|
|
310
|
+
(c) => !!c && typeof c === "object" && BLOCK_TYPES.has(c.type)
|
|
311
|
+
);
|
|
312
|
+
if (nested) for (const child of sanitizeBlocks(n.content, ctx, depth + 1)) out.push(child);
|
|
313
|
+
else {
|
|
314
|
+
const inline = sanitizeInline(n.content, depth + 1, ctx);
|
|
315
|
+
if (inline.length > 0) out.push(withContent("paragraph", inline));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
flushInline();
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
function sanitizeRichTextDoc(input, options = {}) {
|
|
324
|
+
const levels = options.headingLevels && options.headingLevels.length > 0 ? options.headingLevels : [2, 3, 4];
|
|
325
|
+
const ctx = { levels, embeds: options.embeds ?? true, throwTooDeep: options.onTooDeep === "throw" };
|
|
326
|
+
const root = input && typeof input === "object" ? input : null;
|
|
327
|
+
if (root && (options.maxCharacters !== void 0 || options.maxNodes !== void 0)) {
|
|
328
|
+
precheckRawSize(root, options);
|
|
329
|
+
}
|
|
330
|
+
const content = root && root.type === "doc" ? sanitizeBlocks(root.content, ctx, 0) : [];
|
|
331
|
+
while (content.length > 1) {
|
|
332
|
+
const last = content[content.length - 1];
|
|
333
|
+
if (last.type !== "paragraph" || last.content) break;
|
|
334
|
+
content.pop();
|
|
335
|
+
}
|
|
336
|
+
if (options.maxCharacters !== void 0 || options.maxNodes !== void 0) {
|
|
337
|
+
const size = measure(content);
|
|
338
|
+
if (options.maxCharacters !== void 0 && size.characters > options.maxCharacters) {
|
|
339
|
+
throw new RichTextLimitError("characters", options.maxCharacters, size.characters);
|
|
340
|
+
}
|
|
341
|
+
if (options.maxNodes !== void 0 && size.nodes > options.maxNodes) {
|
|
342
|
+
throw new RichTextLimitError("nodes", options.maxNodes, size.nodes);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { type: "doc", content: content.length > 0 ? content : [{ type: "paragraph" }] };
|
|
346
|
+
}
|
|
347
|
+
function isRichTextDoc(value) {
|
|
348
|
+
return !!value && typeof value === "object" && value.type === "doc" && Array.isArray(value.content);
|
|
349
|
+
}
|
|
350
|
+
function escapeHtml(value) {
|
|
351
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
352
|
+
}
|
|
353
|
+
function isExternalHref(href) {
|
|
354
|
+
return /^https?:\/\//i.test(href);
|
|
355
|
+
}
|
|
356
|
+
var MARK_ORDER = ["link", "bold", "italic", "underline"];
|
|
357
|
+
function orderedMarks(marks) {
|
|
358
|
+
if (!marks) return [];
|
|
359
|
+
return [...marks].sort((a, b) => MARK_ORDER.indexOf(a.type) - MARK_ORDER.indexOf(b.type));
|
|
360
|
+
}
|
|
361
|
+
function linkAttrs(href) {
|
|
362
|
+
const safe = escapeHtml(href);
|
|
363
|
+
return isExternalHref(href) ? `href="${safe}" target="_blank" rel="noopener noreferrer nofollow"` : `href="${safe}"`;
|
|
364
|
+
}
|
|
365
|
+
function inlineToHtml(nodes) {
|
|
366
|
+
if (!nodes) return "";
|
|
367
|
+
let html = "";
|
|
368
|
+
for (const node of nodes) {
|
|
369
|
+
if (node.type === "hardBreak") {
|
|
370
|
+
html += "<br>";
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
let text = escapeHtml(node.text ?? "");
|
|
374
|
+
const marks = orderedMarks(node.marks);
|
|
375
|
+
for (let i = marks.length - 1; i >= 0; i--) {
|
|
376
|
+
const mark = marks[i];
|
|
377
|
+
if (mark.type === "bold") text = `<strong>${text}</strong>`;
|
|
378
|
+
else if (mark.type === "italic") text = `<em>${text}</em>`;
|
|
379
|
+
else if (mark.type === "underline") text = `<u>${text}</u>`;
|
|
380
|
+
else if (mark.type === "link") text = `<a ${linkAttrs(String(mark.attrs?.href))}>${text}</a>`;
|
|
381
|
+
}
|
|
382
|
+
html += text;
|
|
383
|
+
}
|
|
384
|
+
return html;
|
|
385
|
+
}
|
|
386
|
+
function embedToHtml(attrs, embedAs) {
|
|
387
|
+
const title = escapeHtml(videoEmbedTitle(attrs));
|
|
388
|
+
if (embedAs === "link") {
|
|
389
|
+
return `<p><a ${linkAttrs(videoWatchUrl(attrs))}>${title}</a></p>`;
|
|
390
|
+
}
|
|
391
|
+
const a = VIDEO_IFRAME_ATTRS;
|
|
392
|
+
const data = `data-video-embed="" data-provider="${attrs.provider}" data-id="${escapeHtml(attrs.id)}"${attrs.hash ? ` data-hash="${escapeHtml(attrs.hash)}"` : ""}${attrs.title ? ` data-title="${title}"` : ""}`;
|
|
393
|
+
return `<div ${data}><iframe src="${escapeHtml(videoEmbedSrc(attrs))}" title="${title}" allow="${a.allow}" allowfullscreen loading="${a.loading}" referrerpolicy="${a.referrerPolicy}" sandbox="${a.sandbox}"></iframe></div>`;
|
|
394
|
+
}
|
|
395
|
+
function blocksToHtml(nodes, embedAs) {
|
|
396
|
+
if (!nodes) return "";
|
|
397
|
+
let html = "";
|
|
398
|
+
for (const node of nodes) {
|
|
399
|
+
switch (node.type) {
|
|
400
|
+
case "paragraph":
|
|
401
|
+
html += `<p>${inlineToHtml(node.content)}</p>`;
|
|
402
|
+
break;
|
|
403
|
+
case "heading": {
|
|
404
|
+
const level = node.attrs?.level;
|
|
405
|
+
html += `<h${level}>${inlineToHtml(node.content)}</h${level}>`;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case "bulletList":
|
|
409
|
+
html += `<ul>${blocksToHtml(node.content, embedAs)}</ul>`;
|
|
410
|
+
break;
|
|
411
|
+
case "orderedList": {
|
|
412
|
+
const start = node.attrs?.start;
|
|
413
|
+
html += `<ol${typeof start === "number" ? ` start="${start}"` : ""}>${blocksToHtml(node.content, embedAs)}</ol>`;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
case "listItem":
|
|
417
|
+
html += `<li>${blocksToHtml(node.content, embedAs)}</li>`;
|
|
418
|
+
break;
|
|
419
|
+
case "blockquote":
|
|
420
|
+
html += `<blockquote>${blocksToHtml(node.content, embedAs)}</blockquote>`;
|
|
421
|
+
break;
|
|
422
|
+
case "videoEmbed":
|
|
423
|
+
html += embedToHtml(node.attrs, embedAs);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return html;
|
|
428
|
+
}
|
|
429
|
+
function richTextToHtml(doc, options = {}) {
|
|
430
|
+
const clean = sanitizeRichTextDoc(doc, options);
|
|
431
|
+
return blocksToHtml(clean.content, options.embedAs ?? "iframe");
|
|
432
|
+
}
|
|
433
|
+
function collectText(nodes, out) {
|
|
434
|
+
if (!nodes) return;
|
|
435
|
+
for (const node of nodes) {
|
|
436
|
+
if (node.type === "text") out.push(node.text ?? "");
|
|
437
|
+
else if (node.type === "hardBreak") out.push("\n");
|
|
438
|
+
else if (node.type === "videoEmbed") {
|
|
439
|
+
const attrs = sanitizeVideoEmbedAttrs(node.attrs);
|
|
440
|
+
if (attrs) out.push(`${videoEmbedTitle(attrs)} (${videoWatchUrl(attrs)})`, "\n\n");
|
|
441
|
+
} else if (node.type === "listItem" || node.type === "bulletList" || node.type === "orderedList" || node.type === "blockquote") {
|
|
442
|
+
collectText(node.content, out);
|
|
443
|
+
} else {
|
|
444
|
+
collectText(node.content, out);
|
|
445
|
+
out.push("\n\n");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function richTextToPlainText(doc) {
|
|
450
|
+
const clean = sanitizeRichTextDoc(doc);
|
|
451
|
+
const out = [];
|
|
452
|
+
collectText(clean.content, out);
|
|
453
|
+
return out.join("").replace(/\n{3,}/g, "\n\n").trim();
|
|
454
|
+
}
|
|
455
|
+
function isRichTextEmpty(doc) {
|
|
456
|
+
const clean = sanitizeRichTextDoc(doc);
|
|
457
|
+
const walk = (nodes) => !nodes || nodes.every((n) => {
|
|
458
|
+
if (n.type === "videoEmbed") return false;
|
|
459
|
+
if (n.type === "text") return (n.text ?? "").trim() === "";
|
|
460
|
+
return walk(n.content);
|
|
461
|
+
});
|
|
462
|
+
return walk(clean.content);
|
|
463
|
+
}
|
|
464
|
+
var richTextClassName = cn(
|
|
465
|
+
"text-sm leading-relaxed text-foreground break-words",
|
|
466
|
+
"[&>*+*]:mt-3",
|
|
467
|
+
"[&_h2]:text-xl [&_h2]:font-semibold [&_h2]:tracking-tight [&_h2]:mt-6 first:[&_h2]:mt-0",
|
|
468
|
+
"[&_h3]:text-lg [&_h3]:font-semibold [&_h3]:mt-5 first:[&_h3]:mt-0",
|
|
469
|
+
"[&_h4]:text-base [&_h4]:font-semibold [&_h4]:mt-4 first:[&_h4]:mt-0",
|
|
470
|
+
"[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-6 [&_ol]:pl-6 [&_li]:my-1 [&_li>p]:m-0",
|
|
471
|
+
"[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground [&_blockquote]:italic",
|
|
472
|
+
"[&_a]:text-primary [&_a]:underline [&_a]:underline-offset-4",
|
|
473
|
+
"[&_[data-video-embed]]:relative [&_[data-video-embed]]:aspect-video [&_[data-video-embed]]:w-full [&_[data-video-embed]]:overflow-hidden [&_[data-video-embed]]:rounded-md [&_[data-video-embed]]:bg-muted",
|
|
474
|
+
"[&_[data-video-embed]_iframe]:absolute [&_[data-video-embed]_iframe]:inset-0 [&_[data-video-embed]_iframe]:h-full [&_[data-video-embed]_iframe]:w-full [&_[data-video-embed]_iframe]:border-0"
|
|
475
|
+
);
|
|
476
|
+
function renderInline(nodes) {
|
|
477
|
+
if (!nodes) return [];
|
|
478
|
+
return nodes.map((node, i) => {
|
|
479
|
+
if (node.type === "hardBreak") return /* @__PURE__ */ jsx("br", {}, i);
|
|
480
|
+
let el = node.text ?? "";
|
|
481
|
+
const marks = orderedMarks(node.marks);
|
|
482
|
+
for (let m = marks.length - 1; m >= 0; m--) {
|
|
483
|
+
const mark = marks[m];
|
|
484
|
+
if (mark.type === "bold") el = /* @__PURE__ */ jsx("strong", { children: el });
|
|
485
|
+
else if (mark.type === "italic") el = /* @__PURE__ */ jsx("em", { children: el });
|
|
486
|
+
else if (mark.type === "underline") el = /* @__PURE__ */ jsx("u", { children: el });
|
|
487
|
+
else if (mark.type === "link") {
|
|
488
|
+
const href = String(mark.attrs?.href);
|
|
489
|
+
el = isExternalHref(href) ? /* @__PURE__ */ jsx("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: el }) : /* @__PURE__ */ jsx("a", { href, children: el });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return /* @__PURE__ */ jsx(React.Fragment, { children: el }, i);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function VideoEmbedView({ attrs, asLink }) {
|
|
496
|
+
const title = videoEmbedTitle(attrs);
|
|
497
|
+
if (asLink) {
|
|
498
|
+
return /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("a", { href: videoWatchUrl(attrs), target: "_blank", rel: "noopener noreferrer nofollow", children: title }) });
|
|
499
|
+
}
|
|
500
|
+
return /* @__PURE__ */ jsx("div", { "data-video-embed": "", "data-provider": attrs.provider, children: /* @__PURE__ */ jsx(
|
|
501
|
+
"iframe",
|
|
502
|
+
{
|
|
503
|
+
src: videoEmbedSrc(attrs),
|
|
504
|
+
title,
|
|
505
|
+
allow: VIDEO_IFRAME_ATTRS.allow,
|
|
506
|
+
allowFullScreen: true,
|
|
507
|
+
loading: VIDEO_IFRAME_ATTRS.loading,
|
|
508
|
+
referrerPolicy: VIDEO_IFRAME_ATTRS.referrerPolicy,
|
|
509
|
+
sandbox: VIDEO_IFRAME_ATTRS.sandbox
|
|
510
|
+
}
|
|
511
|
+
) });
|
|
512
|
+
}
|
|
513
|
+
function renderBlocks(nodes, asLink) {
|
|
514
|
+
if (!nodes) return [];
|
|
515
|
+
return nodes.map((node, i) => {
|
|
516
|
+
switch (node.type) {
|
|
517
|
+
case "paragraph":
|
|
518
|
+
return /* @__PURE__ */ jsx("p", { children: renderInline(node.content) }, i);
|
|
519
|
+
case "heading": {
|
|
520
|
+
const Tag = `h${node.attrs?.level}`;
|
|
521
|
+
return /* @__PURE__ */ jsx(Tag, { children: renderInline(node.content) }, i);
|
|
522
|
+
}
|
|
523
|
+
case "bulletList":
|
|
524
|
+
return /* @__PURE__ */ jsx("ul", { children: renderBlocks(node.content, asLink) }, i);
|
|
525
|
+
case "orderedList":
|
|
526
|
+
return /* @__PURE__ */ jsx("ol", { start: node.attrs?.start, children: renderBlocks(node.content, asLink) }, i);
|
|
527
|
+
case "listItem":
|
|
528
|
+
return /* @__PURE__ */ jsx("li", { children: renderBlocks(node.content, asLink) }, i);
|
|
529
|
+
case "blockquote":
|
|
530
|
+
return /* @__PURE__ */ jsx("blockquote", { children: renderBlocks(node.content, asLink) }, i);
|
|
531
|
+
case "videoEmbed":
|
|
532
|
+
return /* @__PURE__ */ jsx(VideoEmbedView, { attrs: node.attrs, asLink }, i);
|
|
533
|
+
default:
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function RichText({
|
|
539
|
+
value,
|
|
540
|
+
embeds = true,
|
|
541
|
+
headingLevels,
|
|
542
|
+
className,
|
|
543
|
+
testID,
|
|
544
|
+
...props
|
|
545
|
+
}) {
|
|
546
|
+
const doc = value ? sanitizeRichTextDoc(value, headingLevels ? { headingLevels } : {}) : null;
|
|
547
|
+
return /* @__PURE__ */ jsx("div", { "data-testid": testID, className: cn(richTextClassName, className), ...props, children: doc ? renderBlocks(doc.content, !embeds) : null });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export { RichText, RichTextLimitError, VIDEO_IFRAME_ATTRS, emptyRichTextDoc, escapeHtml, isRichTextDoc, isRichTextEmpty, isSafeUrl, normalizeLinkUrl, parseVideoUrl, richTextClassName, richTextToHtml, richTextToPlainText, sanitizeRichTextDoc, sanitizeVideoEmbedAttrs, videoEmbedSrc, videoEmbedTitle, videoWatchUrl };
|
|
551
|
+
//# sourceMappingURL=chunk-ZDHWNQTR.js.map
|
|
552
|
+
//# sourceMappingURL=chunk-ZDHWNQTR.js.map
|