@underverse-ui/underverse 1.0.100 → 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
@@ -23496,6 +23496,49 @@ var SlashCommand = Extension.create({
23496
23496
  // src/components/UEditor/clipboard-images.ts
23497
23497
  import { Extension as Extension2 } from "@tiptap/core";
23498
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"];
23499
23542
  function getImageFiles(dataTransfer) {
23500
23543
  if (!dataTransfer) return [];
23501
23544
  const itemFiles = [];
@@ -23527,7 +23570,7 @@ async function resolveImageSrc(file, options) {
23527
23570
  if (options.insertMode === "upload" && options.upload) {
23528
23571
  try {
23529
23572
  const result = await options.upload(file);
23530
- const src = typeof result === "string" ? result : "";
23573
+ const src = typeof result === "string" ? sanitizeUEditorUrl(result, "image") : "";
23531
23574
  if (src) return src;
23532
23575
  } catch (err) {
23533
23576
  if (!options.fallbackToDataUrl) throw err;
@@ -23539,8 +23582,8 @@ var ClipboardImages = Extension2.create({
23539
23582
  name: "clipboardImages",
23540
23583
  addOptions() {
23541
23584
  return {
23542
- maxFileSize: 10 * 1024 * 1024,
23543
- 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,
23544
23587
  upload: void 0,
23545
23588
  fallbackToDataUrl: true,
23546
23589
  insertMode: "base64"
@@ -24399,6 +24442,9 @@ function buildUEditorExtensions({
24399
24442
  maxCharacters,
24400
24443
  uploadImage,
24401
24444
  imageInsertMode = "base64",
24445
+ maxImageFileSize,
24446
+ allowedImageMimeTypes,
24447
+ fallbackToDataUrl,
24402
24448
  editable = true
24403
24449
  }) {
24404
24450
  return [
@@ -24452,12 +24498,20 @@ function buildUEditorExtensions({
24452
24498
  HorizontalRule,
24453
24499
  Link.configure({
24454
24500
  openOnClick: false,
24501
+ protocols: ["http", "https", "mailto", "tel"],
24502
+ isAllowedUri: (url) => isSafeUEditorUrl(url ?? "", "link"),
24455
24503
  HTMLAttributes: {
24456
24504
  class: "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
24457
24505
  }
24458
24506
  }),
24459
24507
  resizable_image_default,
24460
- 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
+ }),
24461
24515
  TextStyle,
24462
24516
  font_family_default,
24463
24517
  font_size_default,
@@ -24713,11 +24767,7 @@ import { useEffect as useEffect33, useRef as useRef28, useState as useState43 }
24713
24767
  import { Check as Check10, X as X19 } from "lucide-react";
24714
24768
  import { jsx as jsx78, jsxs as jsxs66 } from "react/jsx-runtime";
24715
24769
  function normalizeUrl(raw) {
24716
- const url = raw.trim();
24717
- if (!url) return "";
24718
- if (url.startsWith("#") || url.startsWith("/")) return url;
24719
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
24720
- return `https://${url}`;
24770
+ return sanitizeUEditorUrl(raw, "link");
24721
24771
  }
24722
24772
  var LinkInput = ({
24723
24773
  onSubmit,
@@ -24762,8 +24812,9 @@ var ImageInput = ({ onSubmit, onCancel }) => {
24762
24812
  }, []);
24763
24813
  const handleSubmit = (e) => {
24764
24814
  e.preventDefault();
24765
- if (url) {
24766
- onSubmit(url, alt);
24815
+ const safeUrl = sanitizeUEditorUrl(url, "image");
24816
+ if (safeUrl) {
24817
+ onSubmit(safeUrl, alt);
24767
24818
  }
24768
24819
  };
24769
24820
  return /* @__PURE__ */ jsxs66("form", { onSubmit: handleSubmit, className: "p-3 space-y-3", children: [
@@ -24949,6 +25000,8 @@ var EditorToolbar = ({
24949
25000
  variant,
24950
25001
  uploadImage,
24951
25002
  imageInsertMode = "base64",
25003
+ maxImageFileSize = DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
25004
+ allowedImageMimeTypes = DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
24952
25005
  fontFamilies,
24953
25006
  fontSizes,
24954
25007
  lineHeights,
@@ -25016,10 +25069,13 @@ var EditorToolbar = ({
25016
25069
  setImageUploadError(null);
25017
25070
  for (const file of files) {
25018
25071
  if (!file.type.startsWith("image/")) continue;
25072
+ if (file.size > maxImageFileSize) continue;
25073
+ if (allowedImageMimeTypes.length > 0 && !allowedImageMimeTypes.includes(file.type)) continue;
25019
25074
  try {
25020
25075
  const src = imageInsertMode === "upload" && uploadImage ? await uploadImage(file) : await fileToDataUrl2(file);
25021
- if (!src) continue;
25022
- 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();
25023
25079
  editor.commands.createParagraphNear();
25024
25080
  } catch {
25025
25081
  setImageUploadError(t("imageInput.uploadError"));
@@ -26230,12 +26286,12 @@ var MIME_EXTENSION_MAP = {
26230
26286
  "image/x-icon": "ico",
26231
26287
  "image/avif": "avif"
26232
26288
  };
26233
- function isDataImageUrl(value) {
26289
+ function isDataImageUrl2(value) {
26234
26290
  return /^data:image\//i.test(value.trim());
26235
26291
  }
26236
26292
  function parseDataImageUrl(dataUrl) {
26237
26293
  const value = dataUrl.trim();
26238
- if (!isDataImageUrl(value)) return null;
26294
+ if (!isDataImageUrl2(value)) return null;
26239
26295
  const commaIndex = value.indexOf(",");
26240
26296
  if (commaIndex < 0) return null;
26241
26297
  const header = value.slice(5, commaIndex);
@@ -26269,19 +26325,33 @@ function createFileFromDataImageUrl(dataUrl, index) {
26269
26325
  }
26270
26326
  function normalizeUploadResult(result) {
26271
26327
  if (typeof result === "string") {
26272
- const url2 = result.trim();
26328
+ const url2 = sanitizeUEditorUrl(result, "image");
26273
26329
  if (!url2) throw new Error("Upload handler returned an empty URL.");
26274
26330
  return { url: url2 };
26275
26331
  }
26276
26332
  if (!result || typeof result !== "object") {
26277
26333
  throw new Error("Upload handler returned invalid result.");
26278
26334
  }
26279
- const url = typeof result.url === "string" ? result.url.trim() : "";
26335
+ const url = typeof result.url === "string" ? sanitizeUEditorUrl(result.url, "image") : "";
26280
26336
  if (!url) throw new Error("Upload handler object result is missing `url`.");
26281
26337
  const { url: _ignoredUrl, ...rest } = result;
26282
26338
  const meta = Object.keys(rest).length > 0 ? rest : void 0;
26283
26339
  return { url, meta };
26284
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
+ }
26285
26355
  function getErrorReason(error) {
26286
26356
  if (error instanceof Error && error.message) return error.message;
26287
26357
  if (typeof error === "string" && error.trim()) return error;
@@ -26293,7 +26363,7 @@ function decodeHtmlEntities(value) {
26293
26363
  function normalizeImageUrl(url) {
26294
26364
  const input = decodeHtmlEntities(url.trim());
26295
26365
  if (!input) return "";
26296
- if (isDataImageUrl(input)) return input;
26366
+ if (isDataImageUrl2(input)) return input;
26297
26367
  const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input);
26298
26368
  if (!isAbsolute) {
26299
26369
  return input.split("#")[0] ?? input;
@@ -26377,7 +26447,8 @@ var UEditorPrepareContentForSaveError = class extends Error {
26377
26447
  };
26378
26448
  async function prepareUEditorContentForSave({
26379
26449
  html,
26380
- uploadImageForSave
26450
+ uploadImageForSave,
26451
+ uploadConcurrency = 3
26381
26452
  }) {
26382
26453
  if (!html || !html.includes("<img")) {
26383
26454
  return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
@@ -26390,7 +26461,7 @@ async function prepareUEditorContentForSave({
26390
26461
  for (const match of imgMatches) {
26391
26462
  if (!match.srcAttr) continue;
26392
26463
  const src = match.srcAttr.value.trim();
26393
- if (!isDataImageUrl(src)) continue;
26464
+ if (!isDataImageUrl2(src)) continue;
26394
26465
  base64Candidates.push({
26395
26466
  id: `${match.start}:${match.end}`,
26396
26467
  match,
@@ -26416,8 +26487,10 @@ async function prepareUEditorContentForSave({
26416
26487
  const inlineUploaded = [];
26417
26488
  const errors = [];
26418
26489
  const replacements = /* @__PURE__ */ new Map();
26419
- const uploadResults = await Promise.all(
26420
- base64Candidates.map(async (candidate) => {
26490
+ const uploadResults = await runWithConcurrency(
26491
+ base64Candidates,
26492
+ uploadConcurrency,
26493
+ async (candidate) => {
26421
26494
  try {
26422
26495
  const file = createFileFromDataImageUrl(candidate.src, candidate.index);
26423
26496
  const uploadResult = await uploadImageForSave(file);
@@ -26426,7 +26499,7 @@ async function prepareUEditorContentForSave({
26426
26499
  } catch (error) {
26427
26500
  return { candidate, error: getErrorReason(error) };
26428
26501
  }
26429
- })
26502
+ }
26430
26503
  );
26431
26504
  for (const item of uploadResults) {
26432
26505
  if ("error" in item) {
@@ -31764,7 +31837,11 @@ var UEditor = React74.forwardRef(({
31764
31837
  onJsonChange,
31765
31838
  uploadImage,
31766
31839
  uploadImageForSave,
31840
+ uploadImageConcurrency = 3,
31767
31841
  imageInsertMode = "base64",
31842
+ maxImageFileSize,
31843
+ allowedImageMimeTypes,
31844
+ fallbackToDataUrl,
31768
31845
  placeholder,
31769
31846
  className,
31770
31847
  editable = true,
@@ -31875,8 +31952,18 @@ var UEditor = React74.forwardRef(({
31875
31952
  setEditorResizeCursor("row-resize");
31876
31953
  }, [setEditorResizeCursor]);
31877
31954
  const extensions = useMemo24(
31878
- () => buildUEditorExtensions({ placeholder: effectivePlaceholder, translate: t, maxCharacters, uploadImage, imageInsertMode, editable }),
31879
- [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]
31880
31967
  );
31881
31968
  const editor = useEditor({
31882
31969
  immediatelyRender: false,
@@ -32073,7 +32160,8 @@ var UEditor = React74.forwardRef(({
32073
32160
  const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32074
32161
  inFlightPrepareRef.current = prepareUEditorContentForSave({
32075
32162
  html: htmlSnapshot,
32076
- uploadImageForSave
32163
+ uploadImageForSave,
32164
+ uploadConcurrency: uploadImageConcurrency
32077
32165
  }).finally(() => {
32078
32166
  inFlightPrepareRef.current = null;
32079
32167
  });
@@ -32085,7 +32173,7 @@ var UEditor = React74.forwardRef(({
32085
32173
  return result;
32086
32174
  }
32087
32175
  }),
32088
- [content, editor, uploadImageForSave]
32176
+ [content, editor, uploadImageForSave, uploadImageConcurrency]
32089
32177
  );
32090
32178
  useEffect35(() => {
32091
32179
  if (!editor) return;
@@ -32215,21 +32303,6 @@ var UEditor = React74.forwardRef(({
32215
32303
  }
32216
32304
  state.previewHeight = nextHeight;
32217
32305
  applyPreviewRowHeight(state.rowElement, nextHeight);
32218
- const tr = editor.view.state.tr;
32219
- tr.setNodeMarkup(state.rowPos, void 0, {
32220
- ...state.rowNode.attrs,
32221
- rowHeight: nextHeight
32222
- });
32223
- editor.view.dispatch(tr);
32224
- state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32225
- const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32226
- if (refreshedRow instanceof HTMLTableRowElement) {
32227
- state.rowElement = refreshedRow;
32228
- const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32229
- if (refreshedCell instanceof HTMLTableCellElement) {
32230
- state.cellElement = refreshedCell;
32231
- }
32232
- }
32233
32306
  document.body.style.cursor = "row-resize";
32234
32307
  showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32235
32308
  };
@@ -32240,7 +32313,16 @@ var UEditor = React74.forwardRef(({
32240
32313
  MIN_TABLE_ROW_HEIGHT,
32241
32314
  Math.round(state.startHeight + (event.clientY - state.startY))
32242
32315
  );
32316
+ const rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32243
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
+ }
32244
32326
  rowResizeStateRef.current = null;
32245
32327
  document.body.style.cursor = "";
32246
32328
  clearHoveredTableCell();
@@ -32328,6 +32410,8 @@ var UEditor = React74.forwardRef(({
32328
32410
  variant,
32329
32411
  uploadImage,
32330
32412
  imageInsertMode,
32413
+ maxImageFileSize,
32414
+ allowedImageMimeTypes,
32331
32415
  fontFamilies,
32332
32416
  fontSizes,
32333
32417
  lineHeights,