@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 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 buttonEl = href && !disabled ? /* @__PURE__ */ jsxRuntime.jsx("a", { href, className: classes, "data-react-fancy-action": "", children: content }) : /* @__PURE__ */ jsxRuntime.jsx(
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
- if (outputFormat === "html") return value;
10393
- const format = detectFormat(value);
10394
- if (format === "html") return value;
10395
- const result = marked.marked.parse(value, { async: false });
10396
- return result.trim();
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({ html, extensions: instanceExtensions }) {
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
- if (resolvedFormat === "markdown") {
10541
- return marked.marked.parse(value, { async: false });
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;