@underverse-ui/underverse 1.0.99 → 1.0.101

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.d.cts CHANGED
@@ -2955,7 +2955,11 @@ interface UEditorProps {
2955
2955
  onJsonChange?: (json: object) => void;
2956
2956
  uploadImage?: (file: File) => Promise<string> | string;
2957
2957
  uploadImageForSave?: UEditorUploadImageForSave;
2958
+ uploadImageConcurrency?: number;
2958
2959
  imageInsertMode?: "base64" | "upload";
2960
+ maxImageFileSize?: number;
2961
+ allowedImageMimeTypes?: string[];
2962
+ fallbackToDataUrl?: boolean;
2959
2963
  placeholder?: string;
2960
2964
  className?: string;
2961
2965
  editable?: boolean;
@@ -2983,9 +2987,10 @@ declare class UEditorPrepareContentForSaveError extends Error {
2983
2987
  readonly result: UEditorPrepareContentForSaveResult;
2984
2988
  constructor(result: UEditorPrepareContentForSaveResult);
2985
2989
  }
2986
- declare function prepareUEditorContentForSave({ html, uploadImageForSave, }: {
2990
+ declare function prepareUEditorContentForSave({ html, uploadImageForSave, uploadConcurrency, }: {
2987
2991
  html: string;
2988
2992
  uploadImageForSave?: UEditorUploadImageForSave;
2993
+ uploadConcurrency?: number;
2989
2994
  }): Promise<UEditorPrepareContentForSaveResult>;
2990
2995
 
2991
2996
  /** Button component for actions, icon buttons, loading states, and submit flows. */
package/dist/index.d.ts CHANGED
@@ -2955,7 +2955,11 @@ interface UEditorProps {
2955
2955
  onJsonChange?: (json: object) => void;
2956
2956
  uploadImage?: (file: File) => Promise<string> | string;
2957
2957
  uploadImageForSave?: UEditorUploadImageForSave;
2958
+ uploadImageConcurrency?: number;
2958
2959
  imageInsertMode?: "base64" | "upload";
2960
+ maxImageFileSize?: number;
2961
+ allowedImageMimeTypes?: string[];
2962
+ fallbackToDataUrl?: boolean;
2959
2963
  placeholder?: string;
2960
2964
  className?: string;
2961
2965
  editable?: boolean;
@@ -2983,9 +2987,10 @@ declare class UEditorPrepareContentForSaveError extends Error {
2983
2987
  readonly result: UEditorPrepareContentForSaveResult;
2984
2988
  constructor(result: UEditorPrepareContentForSaveResult);
2985
2989
  }
2986
- declare function prepareUEditorContentForSave({ html, uploadImageForSave, }: {
2990
+ declare function prepareUEditorContentForSave({ html, uploadImageForSave, uploadConcurrency, }: {
2987
2991
  html: string;
2988
2992
  uploadImageForSave?: UEditorUploadImageForSave;
2993
+ uploadConcurrency?: number;
2989
2994
  }): Promise<UEditorPrepareContentForSaveResult>;
2990
2995
 
2991
2996
  /** Button component for actions, icon buttons, loading states, and submit flows. */
package/dist/index.js CHANGED
@@ -22675,7 +22675,7 @@ var useFormField = () => {
22675
22675
  var FormItemContext = React63.createContext({});
22676
22676
  var FormItem = React63.forwardRef(({ className, ...props }, ref) => {
22677
22677
  const id = React63.useId();
22678
- return /* @__PURE__ */ jsx68(FormItemContext.Provider, { value: { id }, children: /* @__PURE__ */ jsx68("div", { ref, className: cn("space-y-2", className), ...props }) });
22678
+ return /* @__PURE__ */ jsx68(FormItemContext.Provider, { value: { id }, children: /* @__PURE__ */ jsx68("div", { ref, className: cn("group space-y-2", className), ...props }) });
22679
22679
  });
22680
22680
  FormItem.displayName = "FormItem";
22681
22681
  var FormLabel = React63.forwardRef(
@@ -22683,10 +22683,24 @@ var FormLabel = React63.forwardRef(
22683
22683
  const { error, formItemId } = useFormField();
22684
22684
  const config = React63.useContext(FormConfigContext);
22685
22685
  const sizeClass = config.size === "sm" ? "text-xs" : config.size === "lg" ? "text-base" : "text-sm";
22686
- return /* @__PURE__ */ jsxs57(Label, { ref, className: cn(sizeClass, error && "text-destructive", className), htmlFor: formItemId, ...props, children: [
22687
- children,
22688
- required && /* @__PURE__ */ jsx68("span", { className: "text-destructive ml-1", children: "*" })
22689
- ] });
22686
+ return /* @__PURE__ */ jsxs57(
22687
+ Label,
22688
+ {
22689
+ ref,
22690
+ className: cn(
22691
+ sizeClass,
22692
+ "transition-colors duration-200",
22693
+ error ? "text-destructive" : "group-focus-within:text-primary",
22694
+ className
22695
+ ),
22696
+ htmlFor: formItemId,
22697
+ ...props,
22698
+ children: [
22699
+ children,
22700
+ required && /* @__PURE__ */ jsx68("span", { className: "text-destructive ml-1", children: "*" })
22701
+ ]
22702
+ }
22703
+ );
22690
22704
  }
22691
22705
  );
22692
22706
  FormLabel.displayName = "FormLabel";
@@ -23482,6 +23496,49 @@ var SlashCommand = Extension.create({
23482
23496
  // src/components/UEditor/clipboard-images.ts
23483
23497
  import { Extension as Extension2 } from "@tiptap/core";
23484
23498
  import { Plugin } from "@tiptap/pm/state";
23499
+
23500
+ // src/components/UEditor/url-safety.ts
23501
+ var LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
23502
+ var IMAGE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
23503
+ function normalizeUrlInput(raw) {
23504
+ return raw.trim().replace(/[\u0000-\u001F\u007F\s]+/g, "");
23505
+ }
23506
+ function isProtocolRelativeUrl(value) {
23507
+ return value.startsWith("//");
23508
+ }
23509
+ function isRelativeUrl(value) {
23510
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("#");
23511
+ }
23512
+ function isDataImageUrl(value) {
23513
+ return /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml|bmp|x-icon|avif);base64,/i.test(value);
23514
+ }
23515
+ function isSafeUEditorUrl(raw, kind) {
23516
+ const value = normalizeUrlInput(raw);
23517
+ if (!value) return false;
23518
+ if (kind === "image" && isDataImageUrl(value)) return true;
23519
+ if (isRelativeUrl(value)) return true;
23520
+ if (isProtocolRelativeUrl(value)) return false;
23521
+ try {
23522
+ const parsed = new URL(value);
23523
+ return kind === "image" ? IMAGE_PROTOCOLS.has(parsed.protocol) : LINK_PROTOCOLS.has(parsed.protocol);
23524
+ } catch {
23525
+ return false;
23526
+ }
23527
+ }
23528
+ function sanitizeUEditorUrl(raw, kind) {
23529
+ const value = raw.trim();
23530
+ if (!value) return "";
23531
+ if (isSafeUEditorUrl(value, kind)) return normalizeUrlInput(value);
23532
+ if (kind === "link" && !isProtocolRelativeUrl(value) && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) {
23533
+ const withProtocol = `https://${value}`;
23534
+ return isSafeUEditorUrl(withProtocol, kind) ? withProtocol : "";
23535
+ }
23536
+ return "";
23537
+ }
23538
+
23539
+ // src/components/UEditor/clipboard-images.ts
23540
+ var DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE = 10 * 1024 * 1024;
23541
+ var DEFAULT_UEDITOR_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"];
23485
23542
  function getImageFiles(dataTransfer) {
23486
23543
  if (!dataTransfer) return [];
23487
23544
  const itemFiles = [];
@@ -23513,7 +23570,7 @@ async function resolveImageSrc(file, options) {
23513
23570
  if (options.insertMode === "upload" && options.upload) {
23514
23571
  try {
23515
23572
  const result = await options.upload(file);
23516
- const src = typeof result === "string" ? result : "";
23573
+ const src = typeof result === "string" ? sanitizeUEditorUrl(result, "image") : "";
23517
23574
  if (src) return src;
23518
23575
  } catch (err) {
23519
23576
  if (!options.fallbackToDataUrl) throw err;
@@ -23525,8 +23582,8 @@ var ClipboardImages = Extension2.create({
23525
23582
  name: "clipboardImages",
23526
23583
  addOptions() {
23527
23584
  return {
23528
- maxFileSize: 10 * 1024 * 1024,
23529
- allowedMimeTypes: ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"],
23585
+ maxFileSize: DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
23586
+ allowedMimeTypes: DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
23530
23587
  upload: void 0,
23531
23588
  fallbackToDataUrl: true,
23532
23589
  insertMode: "base64"
@@ -24385,6 +24442,9 @@ function buildUEditorExtensions({
24385
24442
  maxCharacters,
24386
24443
  uploadImage,
24387
24444
  imageInsertMode = "base64",
24445
+ maxImageFileSize,
24446
+ allowedImageMimeTypes,
24447
+ fallbackToDataUrl,
24388
24448
  editable = true
24389
24449
  }) {
24390
24450
  return [
@@ -24438,12 +24498,20 @@ function buildUEditorExtensions({
24438
24498
  HorizontalRule,
24439
24499
  Link.configure({
24440
24500
  openOnClick: false,
24501
+ protocols: ["http", "https", "mailto", "tel"],
24502
+ isAllowedUri: (url) => isSafeUEditorUrl(url ?? "", "link"),
24441
24503
  HTMLAttributes: {
24442
24504
  class: "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
24443
24505
  }
24444
24506
  }),
24445
24507
  resizable_image_default,
24446
- ClipboardImages.configure({ upload: uploadImage, insertMode: imageInsertMode }),
24508
+ ClipboardImages.configure({
24509
+ upload: uploadImage,
24510
+ insertMode: imageInsertMode,
24511
+ ...maxImageFileSize !== void 0 ? { maxFileSize: maxImageFileSize } : {},
24512
+ ...allowedImageMimeTypes ? { allowedMimeTypes: allowedImageMimeTypes } : {},
24513
+ ...fallbackToDataUrl !== void 0 ? { fallbackToDataUrl } : {}
24514
+ }),
24447
24515
  TextStyle,
24448
24516
  font_family_default,
24449
24517
  font_size_default,
@@ -24699,11 +24767,7 @@ import { useEffect as useEffect33, useRef as useRef28, useState as useState43 }
24699
24767
  import { Check as Check10, X as X19 } from "lucide-react";
24700
24768
  import { jsx as jsx78, jsxs as jsxs66 } from "react/jsx-runtime";
24701
24769
  function normalizeUrl(raw) {
24702
- const url = raw.trim();
24703
- if (!url) return "";
24704
- if (url.startsWith("#") || url.startsWith("/")) return url;
24705
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
24706
- return `https://${url}`;
24770
+ return sanitizeUEditorUrl(raw, "link");
24707
24771
  }
24708
24772
  var LinkInput = ({
24709
24773
  onSubmit,
@@ -24748,8 +24812,9 @@ var ImageInput = ({ onSubmit, onCancel }) => {
24748
24812
  }, []);
24749
24813
  const handleSubmit = (e) => {
24750
24814
  e.preventDefault();
24751
- if (url) {
24752
- onSubmit(url, alt);
24815
+ const safeUrl = sanitizeUEditorUrl(url, "image");
24816
+ if (safeUrl) {
24817
+ onSubmit(safeUrl, alt);
24753
24818
  }
24754
24819
  };
24755
24820
  return /* @__PURE__ */ jsxs66("form", { onSubmit: handleSubmit, className: "p-3 space-y-3", children: [
@@ -24935,6 +25000,8 @@ var EditorToolbar = ({
24935
25000
  variant,
24936
25001
  uploadImage,
24937
25002
  imageInsertMode = "base64",
25003
+ maxImageFileSize = DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
25004
+ allowedImageMimeTypes = DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
24938
25005
  fontFamilies,
24939
25006
  fontSizes,
24940
25007
  lineHeights,
@@ -25002,10 +25069,13 @@ var EditorToolbar = ({
25002
25069
  setImageUploadError(null);
25003
25070
  for (const file of files) {
25004
25071
  if (!file.type.startsWith("image/")) continue;
25072
+ if (file.size > maxImageFileSize) continue;
25073
+ if (allowedImageMimeTypes.length > 0 && !allowedImageMimeTypes.includes(file.type)) continue;
25005
25074
  try {
25006
25075
  const src = imageInsertMode === "upload" && uploadImage ? await uploadImage(file) : await fileToDataUrl2(file);
25007
- if (!src) continue;
25008
- editor.chain().focus().setImage({ src, alt: file.name }).run();
25076
+ const safeSrc = sanitizeUEditorUrl(src, "image");
25077
+ if (!safeSrc) continue;
25078
+ editor.chain().focus().setImage({ src: safeSrc, alt: file.name }).run();
25009
25079
  editor.commands.createParagraphNear();
25010
25080
  } catch {
25011
25081
  setImageUploadError(t("imageInput.uploadError"));
@@ -26216,12 +26286,12 @@ var MIME_EXTENSION_MAP = {
26216
26286
  "image/x-icon": "ico",
26217
26287
  "image/avif": "avif"
26218
26288
  };
26219
- function isDataImageUrl(value) {
26289
+ function isDataImageUrl2(value) {
26220
26290
  return /^data:image\//i.test(value.trim());
26221
26291
  }
26222
26292
  function parseDataImageUrl(dataUrl) {
26223
26293
  const value = dataUrl.trim();
26224
- if (!isDataImageUrl(value)) return null;
26294
+ if (!isDataImageUrl2(value)) return null;
26225
26295
  const commaIndex = value.indexOf(",");
26226
26296
  if (commaIndex < 0) return null;
26227
26297
  const header = value.slice(5, commaIndex);
@@ -26255,19 +26325,33 @@ function createFileFromDataImageUrl(dataUrl, index) {
26255
26325
  }
26256
26326
  function normalizeUploadResult(result) {
26257
26327
  if (typeof result === "string") {
26258
- const url2 = result.trim();
26328
+ const url2 = sanitizeUEditorUrl(result, "image");
26259
26329
  if (!url2) throw new Error("Upload handler returned an empty URL.");
26260
26330
  return { url: url2 };
26261
26331
  }
26262
26332
  if (!result || typeof result !== "object") {
26263
26333
  throw new Error("Upload handler returned invalid result.");
26264
26334
  }
26265
- const url = typeof result.url === "string" ? result.url.trim() : "";
26335
+ const url = typeof result.url === "string" ? sanitizeUEditorUrl(result.url, "image") : "";
26266
26336
  if (!url) throw new Error("Upload handler object result is missing `url`.");
26267
26337
  const { url: _ignoredUrl, ...rest } = result;
26268
26338
  const meta = Object.keys(rest).length > 0 ? rest : void 0;
26269
26339
  return { url, meta };
26270
26340
  }
26341
+ async function runWithConcurrency(items, concurrency, worker) {
26342
+ const limit = Math.max(1, Math.floor(concurrency));
26343
+ const results = new Array(items.length);
26344
+ let nextIndex = 0;
26345
+ async function runNext() {
26346
+ while (nextIndex < items.length) {
26347
+ const currentIndex = nextIndex;
26348
+ nextIndex += 1;
26349
+ results[currentIndex] = await worker(items[currentIndex]);
26350
+ }
26351
+ }
26352
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runNext));
26353
+ return results;
26354
+ }
26271
26355
  function getErrorReason(error) {
26272
26356
  if (error instanceof Error && error.message) return error.message;
26273
26357
  if (typeof error === "string" && error.trim()) return error;
@@ -26279,7 +26363,7 @@ function decodeHtmlEntities(value) {
26279
26363
  function normalizeImageUrl(url) {
26280
26364
  const input = decodeHtmlEntities(url.trim());
26281
26365
  if (!input) return "";
26282
- if (isDataImageUrl(input)) return input;
26366
+ if (isDataImageUrl2(input)) return input;
26283
26367
  const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input);
26284
26368
  if (!isAbsolute) {
26285
26369
  return input.split("#")[0] ?? input;
@@ -26363,7 +26447,8 @@ var UEditorPrepareContentForSaveError = class extends Error {
26363
26447
  };
26364
26448
  async function prepareUEditorContentForSave({
26365
26449
  html,
26366
- uploadImageForSave
26450
+ uploadImageForSave,
26451
+ uploadConcurrency = 3
26367
26452
  }) {
26368
26453
  if (!html || !html.includes("<img")) {
26369
26454
  return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
@@ -26376,7 +26461,7 @@ async function prepareUEditorContentForSave({
26376
26461
  for (const match of imgMatches) {
26377
26462
  if (!match.srcAttr) continue;
26378
26463
  const src = match.srcAttr.value.trim();
26379
- if (!isDataImageUrl(src)) continue;
26464
+ if (!isDataImageUrl2(src)) continue;
26380
26465
  base64Candidates.push({
26381
26466
  id: `${match.start}:${match.end}`,
26382
26467
  match,
@@ -26402,8 +26487,10 @@ async function prepareUEditorContentForSave({
26402
26487
  const inlineUploaded = [];
26403
26488
  const errors = [];
26404
26489
  const replacements = /* @__PURE__ */ new Map();
26405
- const uploadResults = await Promise.all(
26406
- base64Candidates.map(async (candidate) => {
26490
+ const uploadResults = await runWithConcurrency(
26491
+ base64Candidates,
26492
+ uploadConcurrency,
26493
+ async (candidate) => {
26407
26494
  try {
26408
26495
  const file = createFileFromDataImageUrl(candidate.src, candidate.index);
26409
26496
  const uploadResult = await uploadImageForSave(file);
@@ -26412,7 +26499,7 @@ async function prepareUEditorContentForSave({
26412
26499
  } catch (error) {
26413
26500
  return { candidate, error: getErrorReason(error) };
26414
26501
  }
26415
- })
26502
+ }
26416
26503
  );
26417
26504
  for (const item of uploadResults) {
26418
26505
  if ("error" in item) {
@@ -31750,7 +31837,11 @@ var UEditor = React74.forwardRef(({
31750
31837
  onJsonChange,
31751
31838
  uploadImage,
31752
31839
  uploadImageForSave,
31840
+ uploadImageConcurrency = 3,
31753
31841
  imageInsertMode = "base64",
31842
+ maxImageFileSize,
31843
+ allowedImageMimeTypes,
31844
+ fallbackToDataUrl,
31754
31845
  placeholder,
31755
31846
  className,
31756
31847
  editable = true,
@@ -31861,8 +31952,18 @@ var UEditor = React74.forwardRef(({
31861
31952
  setEditorResizeCursor("row-resize");
31862
31953
  }, [setEditorResizeCursor]);
31863
31954
  const extensions = useMemo24(
31864
- () => buildUEditorExtensions({ placeholder: effectivePlaceholder, translate: t, maxCharacters, uploadImage, imageInsertMode, editable }),
31865
- [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, editable]
31955
+ () => buildUEditorExtensions({
31956
+ placeholder: effectivePlaceholder,
31957
+ translate: t,
31958
+ maxCharacters,
31959
+ uploadImage,
31960
+ imageInsertMode,
31961
+ maxImageFileSize,
31962
+ allowedImageMimeTypes,
31963
+ fallbackToDataUrl,
31964
+ editable
31965
+ }),
31966
+ [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, maxImageFileSize, allowedImageMimeTypes, fallbackToDataUrl, editable]
31866
31967
  );
31867
31968
  const editor = useEditor({
31868
31969
  immediatelyRender: false,
@@ -32059,7 +32160,8 @@ var UEditor = React74.forwardRef(({
32059
32160
  const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32060
32161
  inFlightPrepareRef.current = prepareUEditorContentForSave({
32061
32162
  html: htmlSnapshot,
32062
- uploadImageForSave
32163
+ uploadImageForSave,
32164
+ uploadConcurrency: uploadImageConcurrency
32063
32165
  }).finally(() => {
32064
32166
  inFlightPrepareRef.current = null;
32065
32167
  });
@@ -32071,7 +32173,7 @@ var UEditor = React74.forwardRef(({
32071
32173
  return result;
32072
32174
  }
32073
32175
  }),
32074
- [content, editor, uploadImageForSave]
32176
+ [content, editor, uploadImageForSave, uploadImageConcurrency]
32075
32177
  );
32076
32178
  useEffect35(() => {
32077
32179
  if (!editor) return;
@@ -32201,21 +32303,6 @@ var UEditor = React74.forwardRef(({
32201
32303
  }
32202
32304
  state.previewHeight = nextHeight;
32203
32305
  applyPreviewRowHeight(state.rowElement, nextHeight);
32204
- const tr = editor.view.state.tr;
32205
- tr.setNodeMarkup(state.rowPos, void 0, {
32206
- ...state.rowNode.attrs,
32207
- rowHeight: nextHeight
32208
- });
32209
- editor.view.dispatch(tr);
32210
- state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32211
- const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32212
- if (refreshedRow instanceof HTMLTableRowElement) {
32213
- state.rowElement = refreshedRow;
32214
- const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32215
- if (refreshedCell instanceof HTMLTableCellElement) {
32216
- state.cellElement = refreshedCell;
32217
- }
32218
- }
32219
32306
  document.body.style.cursor = "row-resize";
32220
32307
  showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32221
32308
  };
@@ -32226,7 +32313,16 @@ var UEditor = React74.forwardRef(({
32226
32313
  MIN_TABLE_ROW_HEIGHT,
32227
32314
  Math.round(state.startHeight + (event.clientY - state.startY))
32228
32315
  );
32316
+ const rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32229
32317
  clearPreviewRowHeight(state.rowElement);
32318
+ if (rowNode.attrs.rowHeight !== nextHeight) {
32319
+ const tr = editor.view.state.tr;
32320
+ tr.setNodeMarkup(state.rowPos, void 0, {
32321
+ ...rowNode.attrs,
32322
+ rowHeight: nextHeight
32323
+ });
32324
+ editor.view.dispatch(tr);
32325
+ }
32230
32326
  rowResizeStateRef.current = null;
32231
32327
  document.body.style.cursor = "";
32232
32328
  clearHoveredTableCell();
@@ -32314,6 +32410,8 @@ var UEditor = React74.forwardRef(({
32314
32410
  variant,
32315
32411
  uploadImage,
32316
32412
  imageInsertMode,
32413
+ maxImageFileSize,
32414
+ allowedImageMimeTypes,
32317
32415
  fontFamilies,
32318
32416
  fontSizes,
32319
32417
  lineHeights,