@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@underverse-ui/underverse",
3
- "version": "1.0.99",
3
+ "version": "1.0.101",
4
4
  "sourceEntry": "src/index.ts",
5
5
  "totalExports": 225,
6
6
  "exports": [
package/dist/index.cjs CHANGED
@@ -22844,7 +22844,7 @@ var useFormField = () => {
22844
22844
  var FormItemContext = React63.createContext({});
22845
22845
  var FormItem = React63.forwardRef(({ className, ...props }, ref) => {
22846
22846
  const id = React63.useId();
22847
- return /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(FormItemContext.Provider, { value: { id }, children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("div", { ref, className: cn("space-y-2", className), ...props }) });
22847
+ return /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(FormItemContext.Provider, { value: { id }, children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("div", { ref, className: cn("group space-y-2", className), ...props }) });
22848
22848
  });
22849
22849
  FormItem.displayName = "FormItem";
22850
22850
  var FormLabel = React63.forwardRef(
@@ -22852,10 +22852,24 @@ var FormLabel = React63.forwardRef(
22852
22852
  const { error, formItemId } = useFormField();
22853
22853
  const config = React63.useContext(FormConfigContext);
22854
22854
  const sizeClass = config.size === "sm" ? "text-xs" : config.size === "lg" ? "text-base" : "text-sm";
22855
- return /* @__PURE__ */ (0, import_jsx_runtime68.jsxs)(Label, { ref, className: cn(sizeClass, error && "text-destructive", className), htmlFor: formItemId, ...props, children: [
22856
- children,
22857
- required && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("span", { className: "text-destructive ml-1", children: "*" })
22858
- ] });
22855
+ return /* @__PURE__ */ (0, import_jsx_runtime68.jsxs)(
22856
+ Label,
22857
+ {
22858
+ ref,
22859
+ className: cn(
22860
+ sizeClass,
22861
+ "transition-colors duration-200",
22862
+ error ? "text-destructive" : "group-focus-within:text-primary",
22863
+ className
22864
+ ),
22865
+ htmlFor: formItemId,
22866
+ ...props,
22867
+ children: [
22868
+ children,
22869
+ required && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("span", { className: "text-destructive ml-1", children: "*" })
22870
+ ]
22871
+ }
22872
+ );
22859
22873
  }
22860
22874
  );
22861
22875
  FormLabel.displayName = "FormLabel";
@@ -23639,6 +23653,49 @@ var SlashCommand = import_core.Extension.create({
23639
23653
  // src/components/UEditor/clipboard-images.ts
23640
23654
  var import_core2 = require("@tiptap/core");
23641
23655
  var import_state = require("@tiptap/pm/state");
23656
+
23657
+ // src/components/UEditor/url-safety.ts
23658
+ var LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
23659
+ var IMAGE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
23660
+ function normalizeUrlInput(raw) {
23661
+ return raw.trim().replace(/[\u0000-\u001F\u007F\s]+/g, "");
23662
+ }
23663
+ function isProtocolRelativeUrl(value) {
23664
+ return value.startsWith("//");
23665
+ }
23666
+ function isRelativeUrl(value) {
23667
+ return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || value.startsWith("#");
23668
+ }
23669
+ function isDataImageUrl(value) {
23670
+ return /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml|bmp|x-icon|avif);base64,/i.test(value);
23671
+ }
23672
+ function isSafeUEditorUrl(raw, kind) {
23673
+ const value = normalizeUrlInput(raw);
23674
+ if (!value) return false;
23675
+ if (kind === "image" && isDataImageUrl(value)) return true;
23676
+ if (isRelativeUrl(value)) return true;
23677
+ if (isProtocolRelativeUrl(value)) return false;
23678
+ try {
23679
+ const parsed = new URL(value);
23680
+ return kind === "image" ? IMAGE_PROTOCOLS.has(parsed.protocol) : LINK_PROTOCOLS.has(parsed.protocol);
23681
+ } catch {
23682
+ return false;
23683
+ }
23684
+ }
23685
+ function sanitizeUEditorUrl(raw, kind) {
23686
+ const value = raw.trim();
23687
+ if (!value) return "";
23688
+ if (isSafeUEditorUrl(value, kind)) return normalizeUrlInput(value);
23689
+ if (kind === "link" && !isProtocolRelativeUrl(value) && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value)) {
23690
+ const withProtocol = `https://${value}`;
23691
+ return isSafeUEditorUrl(withProtocol, kind) ? withProtocol : "";
23692
+ }
23693
+ return "";
23694
+ }
23695
+
23696
+ // src/components/UEditor/clipboard-images.ts
23697
+ var DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE = 10 * 1024 * 1024;
23698
+ var DEFAULT_UEDITOR_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"];
23642
23699
  function getImageFiles(dataTransfer) {
23643
23700
  if (!dataTransfer) return [];
23644
23701
  const itemFiles = [];
@@ -23670,7 +23727,7 @@ async function resolveImageSrc(file, options) {
23670
23727
  if (options.insertMode === "upload" && options.upload) {
23671
23728
  try {
23672
23729
  const result = await options.upload(file);
23673
- const src = typeof result === "string" ? result : "";
23730
+ const src = typeof result === "string" ? sanitizeUEditorUrl(result, "image") : "";
23674
23731
  if (src) return src;
23675
23732
  } catch (err) {
23676
23733
  if (!options.fallbackToDataUrl) throw err;
@@ -23682,8 +23739,8 @@ var ClipboardImages = import_core2.Extension.create({
23682
23739
  name: "clipboardImages",
23683
23740
  addOptions() {
23684
23741
  return {
23685
- maxFileSize: 10 * 1024 * 1024,
23686
- allowedMimeTypes: ["image/png", "image/jpeg", "image/webp", "image/gif", "image/svg+xml"],
23742
+ maxFileSize: DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
23743
+ allowedMimeTypes: DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
23687
23744
  upload: void 0,
23688
23745
  fallbackToDataUrl: true,
23689
23746
  insertMode: "base64"
@@ -24542,6 +24599,9 @@ function buildUEditorExtensions({
24542
24599
  maxCharacters,
24543
24600
  uploadImage,
24544
24601
  imageInsertMode = "base64",
24602
+ maxImageFileSize,
24603
+ allowedImageMimeTypes,
24604
+ fallbackToDataUrl,
24545
24605
  editable = true
24546
24606
  }) {
24547
24607
  return [
@@ -24595,12 +24655,20 @@ function buildUEditorExtensions({
24595
24655
  import_extension_horizontal_rule.default,
24596
24656
  import_extension_link.default.configure({
24597
24657
  openOnClick: false,
24658
+ protocols: ["http", "https", "mailto", "tel"],
24659
+ isAllowedUri: (url) => isSafeUEditorUrl(url ?? "", "link"),
24598
24660
  HTMLAttributes: {
24599
24661
  class: "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
24600
24662
  }
24601
24663
  }),
24602
24664
  resizable_image_default,
24603
- ClipboardImages.configure({ upload: uploadImage, insertMode: imageInsertMode }),
24665
+ ClipboardImages.configure({
24666
+ upload: uploadImage,
24667
+ insertMode: imageInsertMode,
24668
+ ...maxImageFileSize !== void 0 ? { maxFileSize: maxImageFileSize } : {},
24669
+ ...allowedImageMimeTypes ? { allowedMimeTypes: allowedImageMimeTypes } : {},
24670
+ ...fallbackToDataUrl !== void 0 ? { fallbackToDataUrl } : {}
24671
+ }),
24604
24672
  import_extension_text_style.TextStyle,
24605
24673
  font_family_default,
24606
24674
  font_size_default,
@@ -24819,11 +24887,7 @@ var import_react48 = require("react");
24819
24887
  var import_lucide_react43 = require("lucide-react");
24820
24888
  var import_jsx_runtime78 = require("react/jsx-runtime");
24821
24889
  function normalizeUrl(raw) {
24822
- const url = raw.trim();
24823
- if (!url) return "";
24824
- if (url.startsWith("#") || url.startsWith("/")) return url;
24825
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
24826
- return `https://${url}`;
24890
+ return sanitizeUEditorUrl(raw, "link");
24827
24891
  }
24828
24892
  var LinkInput = ({
24829
24893
  onSubmit,
@@ -24868,8 +24932,9 @@ var ImageInput = ({ onSubmit, onCancel }) => {
24868
24932
  }, []);
24869
24933
  const handleSubmit = (e) => {
24870
24934
  e.preventDefault();
24871
- if (url) {
24872
- onSubmit(url, alt);
24935
+ const safeUrl = sanitizeUEditorUrl(url, "image");
24936
+ if (safeUrl) {
24937
+ onSubmit(safeUrl, alt);
24873
24938
  }
24874
24939
  };
24875
24940
  return /* @__PURE__ */ (0, import_jsx_runtime78.jsxs)("form", { onSubmit: handleSubmit, className: "p-3 space-y-3", children: [
@@ -25055,6 +25120,8 @@ var EditorToolbar = ({
25055
25120
  variant,
25056
25121
  uploadImage,
25057
25122
  imageInsertMode = "base64",
25123
+ maxImageFileSize = DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
25124
+ allowedImageMimeTypes = DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
25058
25125
  fontFamilies,
25059
25126
  fontSizes,
25060
25127
  lineHeights,
@@ -25122,10 +25189,13 @@ var EditorToolbar = ({
25122
25189
  setImageUploadError(null);
25123
25190
  for (const file of files) {
25124
25191
  if (!file.type.startsWith("image/")) continue;
25192
+ if (file.size > maxImageFileSize) continue;
25193
+ if (allowedImageMimeTypes.length > 0 && !allowedImageMimeTypes.includes(file.type)) continue;
25125
25194
  try {
25126
25195
  const src = imageInsertMode === "upload" && uploadImage ? await uploadImage(file) : await fileToDataUrl2(file);
25127
- if (!src) continue;
25128
- editor.chain().focus().setImage({ src, alt: file.name }).run();
25196
+ const safeSrc = sanitizeUEditorUrl(src, "image");
25197
+ if (!safeSrc) continue;
25198
+ editor.chain().focus().setImage({ src: safeSrc, alt: file.name }).run();
25129
25199
  editor.commands.createParagraphNear();
25130
25200
  } catch {
25131
25201
  setImageUploadError(t("imageInput.uploadError"));
@@ -26320,12 +26390,12 @@ var MIME_EXTENSION_MAP = {
26320
26390
  "image/x-icon": "ico",
26321
26391
  "image/avif": "avif"
26322
26392
  };
26323
- function isDataImageUrl(value) {
26393
+ function isDataImageUrl2(value) {
26324
26394
  return /^data:image\//i.test(value.trim());
26325
26395
  }
26326
26396
  function parseDataImageUrl(dataUrl) {
26327
26397
  const value = dataUrl.trim();
26328
- if (!isDataImageUrl(value)) return null;
26398
+ if (!isDataImageUrl2(value)) return null;
26329
26399
  const commaIndex = value.indexOf(",");
26330
26400
  if (commaIndex < 0) return null;
26331
26401
  const header = value.slice(5, commaIndex);
@@ -26359,19 +26429,33 @@ function createFileFromDataImageUrl(dataUrl, index) {
26359
26429
  }
26360
26430
  function normalizeUploadResult(result) {
26361
26431
  if (typeof result === "string") {
26362
- const url2 = result.trim();
26432
+ const url2 = sanitizeUEditorUrl(result, "image");
26363
26433
  if (!url2) throw new Error("Upload handler returned an empty URL.");
26364
26434
  return { url: url2 };
26365
26435
  }
26366
26436
  if (!result || typeof result !== "object") {
26367
26437
  throw new Error("Upload handler returned invalid result.");
26368
26438
  }
26369
- const url = typeof result.url === "string" ? result.url.trim() : "";
26439
+ const url = typeof result.url === "string" ? sanitizeUEditorUrl(result.url, "image") : "";
26370
26440
  if (!url) throw new Error("Upload handler object result is missing `url`.");
26371
26441
  const { url: _ignoredUrl, ...rest } = result;
26372
26442
  const meta = Object.keys(rest).length > 0 ? rest : void 0;
26373
26443
  return { url, meta };
26374
26444
  }
26445
+ async function runWithConcurrency(items, concurrency, worker) {
26446
+ const limit = Math.max(1, Math.floor(concurrency));
26447
+ const results = new Array(items.length);
26448
+ let nextIndex = 0;
26449
+ async function runNext() {
26450
+ while (nextIndex < items.length) {
26451
+ const currentIndex = nextIndex;
26452
+ nextIndex += 1;
26453
+ results[currentIndex] = await worker(items[currentIndex]);
26454
+ }
26455
+ }
26456
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runNext));
26457
+ return results;
26458
+ }
26375
26459
  function getErrorReason(error) {
26376
26460
  if (error instanceof Error && error.message) return error.message;
26377
26461
  if (typeof error === "string" && error.trim()) return error;
@@ -26383,7 +26467,7 @@ function decodeHtmlEntities(value) {
26383
26467
  function normalizeImageUrl(url) {
26384
26468
  const input = decodeHtmlEntities(url.trim());
26385
26469
  if (!input) return "";
26386
- if (isDataImageUrl(input)) return input;
26470
+ if (isDataImageUrl2(input)) return input;
26387
26471
  const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input);
26388
26472
  if (!isAbsolute) {
26389
26473
  return input.split("#")[0] ?? input;
@@ -26467,7 +26551,8 @@ var UEditorPrepareContentForSaveError = class extends Error {
26467
26551
  };
26468
26552
  async function prepareUEditorContentForSave({
26469
26553
  html,
26470
- uploadImageForSave
26554
+ uploadImageForSave,
26555
+ uploadConcurrency = 3
26471
26556
  }) {
26472
26557
  if (!html || !html.includes("<img")) {
26473
26558
  return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
@@ -26480,7 +26565,7 @@ async function prepareUEditorContentForSave({
26480
26565
  for (const match of imgMatches) {
26481
26566
  if (!match.srcAttr) continue;
26482
26567
  const src = match.srcAttr.value.trim();
26483
- if (!isDataImageUrl(src)) continue;
26568
+ if (!isDataImageUrl2(src)) continue;
26484
26569
  base64Candidates.push({
26485
26570
  id: `${match.start}:${match.end}`,
26486
26571
  match,
@@ -26506,8 +26591,10 @@ async function prepareUEditorContentForSave({
26506
26591
  const inlineUploaded = [];
26507
26592
  const errors = [];
26508
26593
  const replacements = /* @__PURE__ */ new Map();
26509
- const uploadResults = await Promise.all(
26510
- base64Candidates.map(async (candidate) => {
26594
+ const uploadResults = await runWithConcurrency(
26595
+ base64Candidates,
26596
+ uploadConcurrency,
26597
+ async (candidate) => {
26511
26598
  try {
26512
26599
  const file = createFileFromDataImageUrl(candidate.src, candidate.index);
26513
26600
  const uploadResult = await uploadImageForSave(file);
@@ -26516,7 +26603,7 @@ async function prepareUEditorContentForSave({
26516
26603
  } catch (error) {
26517
26604
  return { candidate, error: getErrorReason(error) };
26518
26605
  }
26519
- })
26606
+ }
26520
26607
  );
26521
26608
  for (const item of uploadResults) {
26522
26609
  if ("error" in item) {
@@ -31840,7 +31927,11 @@ var UEditor = import_react52.default.forwardRef(({
31840
31927
  onJsonChange,
31841
31928
  uploadImage,
31842
31929
  uploadImageForSave,
31930
+ uploadImageConcurrency = 3,
31843
31931
  imageInsertMode = "base64",
31932
+ maxImageFileSize,
31933
+ allowedImageMimeTypes,
31934
+ fallbackToDataUrl,
31844
31935
  placeholder,
31845
31936
  className,
31846
31937
  editable = true,
@@ -31951,8 +32042,18 @@ var UEditor = import_react52.default.forwardRef(({
31951
32042
  setEditorResizeCursor("row-resize");
31952
32043
  }, [setEditorResizeCursor]);
31953
32044
  const extensions = (0, import_react52.useMemo)(
31954
- () => buildUEditorExtensions({ placeholder: effectivePlaceholder, translate: t, maxCharacters, uploadImage, imageInsertMode, editable }),
31955
- [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, editable]
32045
+ () => buildUEditorExtensions({
32046
+ placeholder: effectivePlaceholder,
32047
+ translate: t,
32048
+ maxCharacters,
32049
+ uploadImage,
32050
+ imageInsertMode,
32051
+ maxImageFileSize,
32052
+ allowedImageMimeTypes,
32053
+ fallbackToDataUrl,
32054
+ editable
32055
+ }),
32056
+ [effectivePlaceholder, t, maxCharacters, uploadImage, imageInsertMode, maxImageFileSize, allowedImageMimeTypes, fallbackToDataUrl, editable]
31956
32057
  );
31957
32058
  const editor = (0, import_react53.useEditor)({
31958
32059
  immediatelyRender: false,
@@ -32149,7 +32250,8 @@ var UEditor = import_react52.default.forwardRef(({
32149
32250
  const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32150
32251
  inFlightPrepareRef.current = prepareUEditorContentForSave({
32151
32252
  html: htmlSnapshot,
32152
- uploadImageForSave
32253
+ uploadImageForSave,
32254
+ uploadConcurrency: uploadImageConcurrency
32153
32255
  }).finally(() => {
32154
32256
  inFlightPrepareRef.current = null;
32155
32257
  });
@@ -32161,7 +32263,7 @@ var UEditor = import_react52.default.forwardRef(({
32161
32263
  return result;
32162
32264
  }
32163
32265
  }),
32164
- [content, editor, uploadImageForSave]
32266
+ [content, editor, uploadImageForSave, uploadImageConcurrency]
32165
32267
  );
32166
32268
  (0, import_react52.useEffect)(() => {
32167
32269
  if (!editor) return;
@@ -32291,21 +32393,6 @@ var UEditor = import_react52.default.forwardRef(({
32291
32393
  }
32292
32394
  state.previewHeight = nextHeight;
32293
32395
  applyPreviewRowHeight(state.rowElement, nextHeight);
32294
- const tr = editor.view.state.tr;
32295
- tr.setNodeMarkup(state.rowPos, void 0, {
32296
- ...state.rowNode.attrs,
32297
- rowHeight: nextHeight
32298
- });
32299
- editor.view.dispatch(tr);
32300
- state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32301
- const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32302
- if (refreshedRow instanceof HTMLTableRowElement) {
32303
- state.rowElement = refreshedRow;
32304
- const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32305
- if (refreshedCell instanceof HTMLTableCellElement) {
32306
- state.cellElement = refreshedCell;
32307
- }
32308
- }
32309
32396
  document.body.style.cursor = "row-resize";
32310
32397
  showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32311
32398
  };
@@ -32316,7 +32403,16 @@ var UEditor = import_react52.default.forwardRef(({
32316
32403
  MIN_TABLE_ROW_HEIGHT,
32317
32404
  Math.round(state.startHeight + (event.clientY - state.startY))
32318
32405
  );
32406
+ const rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32319
32407
  clearPreviewRowHeight(state.rowElement);
32408
+ if (rowNode.attrs.rowHeight !== nextHeight) {
32409
+ const tr = editor.view.state.tr;
32410
+ tr.setNodeMarkup(state.rowPos, void 0, {
32411
+ ...rowNode.attrs,
32412
+ rowHeight: nextHeight
32413
+ });
32414
+ editor.view.dispatch(tr);
32415
+ }
32320
32416
  rowResizeStateRef.current = null;
32321
32417
  document.body.style.cursor = "";
32322
32418
  clearHoveredTableCell();
@@ -32404,6 +32500,8 @@ var UEditor = import_react52.default.forwardRef(({
32404
32500
  variant,
32405
32501
  uploadImage,
32406
32502
  imageInsertMode,
32503
+ maxImageFileSize,
32504
+ allowedImageMimeTypes,
32407
32505
  fontFamilies,
32408
32506
  fontSizes,
32409
32507
  lineHeights,