@particle-academy/react-fancy 2.4.0 → 2.5.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/dist/index.cjs +103 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -3
- package/dist/index.d.ts +48 -3
- package/dist/index.js +102 -20
- package/dist/index.js.map +1 -1
- package/docs/Action.md +1 -1
- package/docs/ContentRenderer.md +26 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -322,6 +322,80 @@ function cn(...inputs) {
|
|
|
322
322
|
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
// src/utils/sanitize.ts
|
|
326
|
+
var DANGEROUS_TAGS = /* @__PURE__ */ new Set([
|
|
327
|
+
"script",
|
|
328
|
+
"style",
|
|
329
|
+
"iframe",
|
|
330
|
+
"object",
|
|
331
|
+
"embed",
|
|
332
|
+
"link",
|
|
333
|
+
"meta",
|
|
334
|
+
"base",
|
|
335
|
+
"form"
|
|
336
|
+
]);
|
|
337
|
+
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href"]);
|
|
338
|
+
var SAFE_PROTOCOL = /^(?:https?:|mailto:|tel:|sms:|ftp:|#|\/|\.\/|\.\.\/|[^:]*$)/i;
|
|
339
|
+
function sanitizeHref(href) {
|
|
340
|
+
if (href == null) return void 0;
|
|
341
|
+
const trimmed = href.trim();
|
|
342
|
+
if (!trimmed) return void 0;
|
|
343
|
+
return SAFE_PROTOCOL.test(trimmed) ? trimmed : void 0;
|
|
344
|
+
}
|
|
345
|
+
function stripDangerousAttrs(el) {
|
|
346
|
+
const names = [];
|
|
347
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
348
|
+
names.push(el.attributes[i].name);
|
|
349
|
+
}
|
|
350
|
+
for (const name of names) {
|
|
351
|
+
const lower = name.toLowerCase();
|
|
352
|
+
if (lower.startsWith("on")) {
|
|
353
|
+
el.removeAttribute(name);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (URL_ATTRS.has(lower)) {
|
|
357
|
+
const sanitized = sanitizeHref(el.getAttribute(name));
|
|
358
|
+
if (sanitized === void 0) {
|
|
359
|
+
el.removeAttribute(name);
|
|
360
|
+
} else {
|
|
361
|
+
el.setAttribute(name, sanitized);
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (lower === "srcdoc") {
|
|
366
|
+
el.removeAttribute(name);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function walk(el, removeQueue) {
|
|
371
|
+
const tag = el.tagName.toLowerCase();
|
|
372
|
+
if (DANGEROUS_TAGS.has(tag)) {
|
|
373
|
+
removeQueue.push(el);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
stripDangerousAttrs(el);
|
|
377
|
+
const children = Array.from(el.children);
|
|
378
|
+
for (const child of children) {
|
|
379
|
+
walk(child, removeQueue);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function sanitizeHtml(html) {
|
|
383
|
+
if (typeof window === "undefined" || typeof DOMParser === "undefined") {
|
|
384
|
+
return html;
|
|
385
|
+
}
|
|
386
|
+
const doc = new DOMParser().parseFromString(`<body>${html}</body>`, "text/html");
|
|
387
|
+
const body = doc.body;
|
|
388
|
+
if (!body) return html;
|
|
389
|
+
const removeQueue = [];
|
|
390
|
+
for (const child of Array.from(body.children)) {
|
|
391
|
+
walk(child, removeQueue);
|
|
392
|
+
}
|
|
393
|
+
for (const el of removeQueue) {
|
|
394
|
+
el.parentNode?.removeChild(el);
|
|
395
|
+
}
|
|
396
|
+
return body.innerHTML;
|
|
397
|
+
}
|
|
398
|
+
|
|
325
399
|
// src/data/emoji-data.ts
|
|
326
400
|
var EMOJI_CATEGORY_ORDER = [
|
|
327
401
|
"smileys",
|
|
@@ -2721,7 +2795,8 @@ var Action = react.forwardRef(
|
|
|
2721
2795
|
children != null && /* @__PURE__ */ jsxRuntime.jsx("span", { children }),
|
|
2722
2796
|
trailingElements
|
|
2723
2797
|
] });
|
|
2724
|
-
const
|
|
2798
|
+
const safeHref = sanitizeHref(href);
|
|
2799
|
+
const buttonEl = safeHref && !disabled ? /* @__PURE__ */ jsxRuntime.jsx("a", { href: safeHref, className: classes, "data-react-fancy-action": "", children: content }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2725
2800
|
"button",
|
|
2726
2801
|
{
|
|
2727
2802
|
ref,
|
|
@@ -10387,13 +10462,15 @@ function mergeExtensions(instanceExtensions) {
|
|
|
10387
10462
|
}
|
|
10388
10463
|
return merged;
|
|
10389
10464
|
}
|
|
10390
|
-
function toHtml(value, outputFormat) {
|
|
10465
|
+
function toHtml(value, outputFormat, unsafe) {
|
|
10391
10466
|
if (!value) return "";
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10467
|
+
const raw = (() => {
|
|
10468
|
+
if (outputFormat === "html") return value;
|
|
10469
|
+
const format = detectFormat(value);
|
|
10470
|
+
if (format === "html") return value;
|
|
10471
|
+
return marked.marked.parse(value, { async: false }).trim();
|
|
10472
|
+
})();
|
|
10473
|
+
return unsafe ? raw : sanitizeHtml(raw);
|
|
10397
10474
|
}
|
|
10398
10475
|
function EditorRoot({
|
|
10399
10476
|
children,
|
|
@@ -10404,12 +10481,13 @@ function EditorRoot({
|
|
|
10404
10481
|
outputFormat = "html",
|
|
10405
10482
|
lineSpacing = 1.6,
|
|
10406
10483
|
placeholder,
|
|
10407
|
-
extensions: instanceExtensions
|
|
10484
|
+
extensions: instanceExtensions,
|
|
10485
|
+
unsafe = false
|
|
10408
10486
|
}) {
|
|
10409
10487
|
const contentRef = react.useRef(null);
|
|
10410
10488
|
const [, setValue] = useControllableState(controlledValue, defaultValue, onChange);
|
|
10411
10489
|
const initialHtml = react.useMemo(
|
|
10412
|
-
() => toHtml(controlledValue ?? defaultValue, outputFormat),
|
|
10490
|
+
() => toHtml(controlledValue ?? defaultValue, outputFormat, unsafe),
|
|
10413
10491
|
// Only compute once on mount — don't re-run when value changes from user input
|
|
10414
10492
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
10415
10493
|
[]
|
|
@@ -10494,7 +10572,11 @@ var Editor = Object.assign(EditorRoot, {
|
|
|
10494
10572
|
Toolbar: ToolbarWithSeparator,
|
|
10495
10573
|
Content: EditorContent
|
|
10496
10574
|
});
|
|
10497
|
-
function RenderedContent({
|
|
10575
|
+
function RenderedContent({
|
|
10576
|
+
html,
|
|
10577
|
+
extensions: instanceExtensions,
|
|
10578
|
+
unsafe = false
|
|
10579
|
+
}) {
|
|
10498
10580
|
const extensions = react.useMemo(
|
|
10499
10581
|
() => mergeExtensions(instanceExtensions),
|
|
10500
10582
|
[instanceExtensions]
|
|
@@ -10503,15 +10585,16 @@ function RenderedContent({ html, extensions: instanceExtensions }) {
|
|
|
10503
10585
|
() => parseSegments(html, extensions),
|
|
10504
10586
|
[html, extensions]
|
|
10505
10587
|
);
|
|
10588
|
+
const renderHtml = (content) => unsafe ? content : sanitizeHtml(content);
|
|
10506
10589
|
if (segments.length === 1 && segments[0].type === "html") {
|
|
10507
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: segments[0].content } });
|
|
10590
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segments[0].content) } });
|
|
10508
10591
|
}
|
|
10509
10592
|
if (segments.length === 0) {
|
|
10510
10593
|
return null;
|
|
10511
10594
|
}
|
|
10512
10595
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: segments.map((segment, i) => {
|
|
10513
10596
|
if (segment.type === "html") {
|
|
10514
|
-
return segment.content ? /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: segment.content } }, i) : null;
|
|
10597
|
+
return segment.content ? /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segment.content) } }, i) : null;
|
|
10515
10598
|
}
|
|
10516
10599
|
const ext = extensions.find(
|
|
10517
10600
|
(e) => e.tag.toLowerCase() === segment.tag
|
|
@@ -10529,7 +10612,8 @@ function ContentRenderer({
|
|
|
10529
10612
|
format = "auto",
|
|
10530
10613
|
lineSpacing = 1.6,
|
|
10531
10614
|
className,
|
|
10532
|
-
extensions: instanceExtensions
|
|
10615
|
+
extensions: instanceExtensions,
|
|
10616
|
+
unsafe = false
|
|
10533
10617
|
}) {
|
|
10534
10618
|
const extensions = react.useMemo(
|
|
10535
10619
|
() => mergeExtensions(instanceExtensions),
|
|
@@ -10537,11 +10621,9 @@ function ContentRenderer({
|
|
|
10537
10621
|
);
|
|
10538
10622
|
const html = react.useMemo(() => {
|
|
10539
10623
|
const resolvedFormat = format === "auto" ? detectFormat(value) : format;
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
return value;
|
|
10544
|
-
}, [value, format]);
|
|
10624
|
+
const raw = resolvedFormat === "markdown" ? marked.marked.parse(value, { async: false }) : value;
|
|
10625
|
+
return unsafe ? raw : sanitizeHtml(raw);
|
|
10626
|
+
}, [value, format, unsafe]);
|
|
10545
10627
|
const hasExtensions = extensions.length > 0;
|
|
10546
10628
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
10547
10629
|
"div",
|
|
@@ -10549,7 +10631,7 @@ function ContentRenderer({
|
|
|
10549
10631
|
"data-react-fancy-content-renderer": "",
|
|
10550
10632
|
style: { lineHeight: lineSpacing },
|
|
10551
10633
|
className: cn("text-sm", proseClasses, className),
|
|
10552
|
-
children: hasExtensions ? /* @__PURE__ */ jsxRuntime.jsx(RenderedContent, { html, extensions }) : /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: html } })
|
|
10634
|
+
children: hasExtensions ? /* @__PURE__ */ jsxRuntime.jsx(RenderedContent, { html, extensions, unsafe }) : /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: html } })
|
|
10553
10635
|
}
|
|
10554
10636
|
);
|
|
10555
10637
|
}
|
|
@@ -12565,6 +12647,8 @@ exports.registerExtensions = registerExtensions;
|
|
|
12565
12647
|
exports.registerIconSet = registerIconSet;
|
|
12566
12648
|
exports.registerIcons = registerIcons;
|
|
12567
12649
|
exports.resolve = resolve;
|
|
12650
|
+
exports.sanitizeHref = sanitizeHref;
|
|
12651
|
+
exports.sanitizeHtml = sanitizeHtml;
|
|
12568
12652
|
exports.search = search;
|
|
12569
12653
|
exports.skinTones = skinTones;
|
|
12570
12654
|
exports.useAccordion = useAccordion;
|