@marktiderman/genesis-ui 2.7.0 → 2.8.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 +144 -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-IXGAEECT.js → chunk-DWCAC2E7.js} +6 -3
- package/dist/chunk-DWCAC2E7.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-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 +239 -15
- package/dist/components/data/index.js +522 -107
- 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 +3 -3
- package/dist/hooks/index.js +4 -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-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 +30 -30
- 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-D4byuwAs.d.ts} +20 -3
- package/package.json +61 -10
- 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,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
|