@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@underverse-ui/underverse",
3
- "version": "1.0.100",
3
+ "version": "1.0.101",
4
4
  "sourceEntry": "src/index.ts",
5
5
  "totalExports": 225,
6
6
  "exports": [
package/dist/index.cjs CHANGED
@@ -23653,6 +23653,49 @@ var SlashCommand = import_core.Extension.create({
23653
23653
  // src/components/UEditor/clipboard-images.ts
23654
23654
  var import_core2 = require("@tiptap/core");
23655
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"];
23656
23699
  function getImageFiles(dataTransfer) {
23657
23700
  if (!dataTransfer) return [];
23658
23701
  const itemFiles = [];
@@ -23684,7 +23727,7 @@ async function resolveImageSrc(file, options) {
23684
23727
  if (options.insertMode === "upload" && options.upload) {
23685
23728
  try {
23686
23729
  const result = await options.upload(file);
23687
- const src = typeof result === "string" ? result : "";
23730
+ const src = typeof result === "string" ? sanitizeUEditorUrl(result, "image") : "";
23688
23731
  if (src) return src;
23689
23732
  } catch (err) {
23690
23733
  if (!options.fallbackToDataUrl) throw err;
@@ -23696,8 +23739,8 @@ var ClipboardImages = import_core2.Extension.create({
23696
23739
  name: "clipboardImages",
23697
23740
  addOptions() {
23698
23741
  return {
23699
- maxFileSize: 10 * 1024 * 1024,
23700
- 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,
23701
23744
  upload: void 0,
23702
23745
  fallbackToDataUrl: true,
23703
23746
  insertMode: "base64"
@@ -24556,6 +24599,9 @@ function buildUEditorExtensions({
24556
24599
  maxCharacters,
24557
24600
  uploadImage,
24558
24601
  imageInsertMode = "base64",
24602
+ maxImageFileSize,
24603
+ allowedImageMimeTypes,
24604
+ fallbackToDataUrl,
24559
24605
  editable = true
24560
24606
  }) {
24561
24607
  return [
@@ -24609,12 +24655,20 @@ function buildUEditorExtensions({
24609
24655
  import_extension_horizontal_rule.default,
24610
24656
  import_extension_link.default.configure({
24611
24657
  openOnClick: false,
24658
+ protocols: ["http", "https", "mailto", "tel"],
24659
+ isAllowedUri: (url) => isSafeUEditorUrl(url ?? "", "link"),
24612
24660
  HTMLAttributes: {
24613
24661
  class: "text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer"
24614
24662
  }
24615
24663
  }),
24616
24664
  resizable_image_default,
24617
- 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
+ }),
24618
24672
  import_extension_text_style.TextStyle,
24619
24673
  font_family_default,
24620
24674
  font_size_default,
@@ -24833,11 +24887,7 @@ var import_react48 = require("react");
24833
24887
  var import_lucide_react43 = require("lucide-react");
24834
24888
  var import_jsx_runtime78 = require("react/jsx-runtime");
24835
24889
  function normalizeUrl(raw) {
24836
- const url = raw.trim();
24837
- if (!url) return "";
24838
- if (url.startsWith("#") || url.startsWith("/")) return url;
24839
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) return url;
24840
- return `https://${url}`;
24890
+ return sanitizeUEditorUrl(raw, "link");
24841
24891
  }
24842
24892
  var LinkInput = ({
24843
24893
  onSubmit,
@@ -24882,8 +24932,9 @@ var ImageInput = ({ onSubmit, onCancel }) => {
24882
24932
  }, []);
24883
24933
  const handleSubmit = (e) => {
24884
24934
  e.preventDefault();
24885
- if (url) {
24886
- onSubmit(url, alt);
24935
+ const safeUrl = sanitizeUEditorUrl(url, "image");
24936
+ if (safeUrl) {
24937
+ onSubmit(safeUrl, alt);
24887
24938
  }
24888
24939
  };
24889
24940
  return /* @__PURE__ */ (0, import_jsx_runtime78.jsxs)("form", { onSubmit: handleSubmit, className: "p-3 space-y-3", children: [
@@ -25069,6 +25120,8 @@ var EditorToolbar = ({
25069
25120
  variant,
25070
25121
  uploadImage,
25071
25122
  imageInsertMode = "base64",
25123
+ maxImageFileSize = DEFAULT_UEDITOR_IMAGE_MAX_FILE_SIZE,
25124
+ allowedImageMimeTypes = DEFAULT_UEDITOR_IMAGE_MIME_TYPES,
25072
25125
  fontFamilies,
25073
25126
  fontSizes,
25074
25127
  lineHeights,
@@ -25136,10 +25189,13 @@ var EditorToolbar = ({
25136
25189
  setImageUploadError(null);
25137
25190
  for (const file of files) {
25138
25191
  if (!file.type.startsWith("image/")) continue;
25192
+ if (file.size > maxImageFileSize) continue;
25193
+ if (allowedImageMimeTypes.length > 0 && !allowedImageMimeTypes.includes(file.type)) continue;
25139
25194
  try {
25140
25195
  const src = imageInsertMode === "upload" && uploadImage ? await uploadImage(file) : await fileToDataUrl2(file);
25141
- if (!src) continue;
25142
- 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();
25143
25199
  editor.commands.createParagraphNear();
25144
25200
  } catch {
25145
25201
  setImageUploadError(t("imageInput.uploadError"));
@@ -26334,12 +26390,12 @@ var MIME_EXTENSION_MAP = {
26334
26390
  "image/x-icon": "ico",
26335
26391
  "image/avif": "avif"
26336
26392
  };
26337
- function isDataImageUrl(value) {
26393
+ function isDataImageUrl2(value) {
26338
26394
  return /^data:image\//i.test(value.trim());
26339
26395
  }
26340
26396
  function parseDataImageUrl(dataUrl) {
26341
26397
  const value = dataUrl.trim();
26342
- if (!isDataImageUrl(value)) return null;
26398
+ if (!isDataImageUrl2(value)) return null;
26343
26399
  const commaIndex = value.indexOf(",");
26344
26400
  if (commaIndex < 0) return null;
26345
26401
  const header = value.slice(5, commaIndex);
@@ -26373,19 +26429,33 @@ function createFileFromDataImageUrl(dataUrl, index) {
26373
26429
  }
26374
26430
  function normalizeUploadResult(result) {
26375
26431
  if (typeof result === "string") {
26376
- const url2 = result.trim();
26432
+ const url2 = sanitizeUEditorUrl(result, "image");
26377
26433
  if (!url2) throw new Error("Upload handler returned an empty URL.");
26378
26434
  return { url: url2 };
26379
26435
  }
26380
26436
  if (!result || typeof result !== "object") {
26381
26437
  throw new Error("Upload handler returned invalid result.");
26382
26438
  }
26383
- const url = typeof result.url === "string" ? result.url.trim() : "";
26439
+ const url = typeof result.url === "string" ? sanitizeUEditorUrl(result.url, "image") : "";
26384
26440
  if (!url) throw new Error("Upload handler object result is missing `url`.");
26385
26441
  const { url: _ignoredUrl, ...rest } = result;
26386
26442
  const meta = Object.keys(rest).length > 0 ? rest : void 0;
26387
26443
  return { url, meta };
26388
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
+ }
26389
26459
  function getErrorReason(error) {
26390
26460
  if (error instanceof Error && error.message) return error.message;
26391
26461
  if (typeof error === "string" && error.trim()) return error;
@@ -26397,7 +26467,7 @@ function decodeHtmlEntities(value) {
26397
26467
  function normalizeImageUrl(url) {
26398
26468
  const input = decodeHtmlEntities(url.trim());
26399
26469
  if (!input) return "";
26400
- if (isDataImageUrl(input)) return input;
26470
+ if (isDataImageUrl2(input)) return input;
26401
26471
  const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input);
26402
26472
  if (!isAbsolute) {
26403
26473
  return input.split("#")[0] ?? input;
@@ -26481,7 +26551,8 @@ var UEditorPrepareContentForSaveError = class extends Error {
26481
26551
  };
26482
26552
  async function prepareUEditorContentForSave({
26483
26553
  html,
26484
- uploadImageForSave
26554
+ uploadImageForSave,
26555
+ uploadConcurrency = 3
26485
26556
  }) {
26486
26557
  if (!html || !html.includes("<img")) {
26487
26558
  return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
@@ -26494,7 +26565,7 @@ async function prepareUEditorContentForSave({
26494
26565
  for (const match of imgMatches) {
26495
26566
  if (!match.srcAttr) continue;
26496
26567
  const src = match.srcAttr.value.trim();
26497
- if (!isDataImageUrl(src)) continue;
26568
+ if (!isDataImageUrl2(src)) continue;
26498
26569
  base64Candidates.push({
26499
26570
  id: `${match.start}:${match.end}`,
26500
26571
  match,
@@ -26520,8 +26591,10 @@ async function prepareUEditorContentForSave({
26520
26591
  const inlineUploaded = [];
26521
26592
  const errors = [];
26522
26593
  const replacements = /* @__PURE__ */ new Map();
26523
- const uploadResults = await Promise.all(
26524
- base64Candidates.map(async (candidate) => {
26594
+ const uploadResults = await runWithConcurrency(
26595
+ base64Candidates,
26596
+ uploadConcurrency,
26597
+ async (candidate) => {
26525
26598
  try {
26526
26599
  const file = createFileFromDataImageUrl(candidate.src, candidate.index);
26527
26600
  const uploadResult = await uploadImageForSave(file);
@@ -26530,7 +26603,7 @@ async function prepareUEditorContentForSave({
26530
26603
  } catch (error) {
26531
26604
  return { candidate, error: getErrorReason(error) };
26532
26605
  }
26533
- })
26606
+ }
26534
26607
  );
26535
26608
  for (const item of uploadResults) {
26536
26609
  if ("error" in item) {
@@ -31854,7 +31927,11 @@ var UEditor = import_react52.default.forwardRef(({
31854
31927
  onJsonChange,
31855
31928
  uploadImage,
31856
31929
  uploadImageForSave,
31930
+ uploadImageConcurrency = 3,
31857
31931
  imageInsertMode = "base64",
31932
+ maxImageFileSize,
31933
+ allowedImageMimeTypes,
31934
+ fallbackToDataUrl,
31858
31935
  placeholder,
31859
31936
  className,
31860
31937
  editable = true,
@@ -31965,8 +32042,18 @@ var UEditor = import_react52.default.forwardRef(({
31965
32042
  setEditorResizeCursor("row-resize");
31966
32043
  }, [setEditorResizeCursor]);
31967
32044
  const extensions = (0, import_react52.useMemo)(
31968
- () => buildUEditorExtensions({ placeholder: effectivePlaceholder, translate: t, maxCharacters, uploadImage, imageInsertMode, editable }),
31969
- [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]
31970
32057
  );
31971
32058
  const editor = (0, import_react53.useEditor)({
31972
32059
  immediatelyRender: false,
@@ -32163,7 +32250,8 @@ var UEditor = import_react52.default.forwardRef(({
32163
32250
  const htmlSnapshot = editor?.getHTML() ?? content ?? "";
32164
32251
  inFlightPrepareRef.current = prepareUEditorContentForSave({
32165
32252
  html: htmlSnapshot,
32166
- uploadImageForSave
32253
+ uploadImageForSave,
32254
+ uploadConcurrency: uploadImageConcurrency
32167
32255
  }).finally(() => {
32168
32256
  inFlightPrepareRef.current = null;
32169
32257
  });
@@ -32175,7 +32263,7 @@ var UEditor = import_react52.default.forwardRef(({
32175
32263
  return result;
32176
32264
  }
32177
32265
  }),
32178
- [content, editor, uploadImageForSave]
32266
+ [content, editor, uploadImageForSave, uploadImageConcurrency]
32179
32267
  );
32180
32268
  (0, import_react52.useEffect)(() => {
32181
32269
  if (!editor) return;
@@ -32305,21 +32393,6 @@ var UEditor = import_react52.default.forwardRef(({
32305
32393
  }
32306
32394
  state.previewHeight = nextHeight;
32307
32395
  applyPreviewRowHeight(state.rowElement, nextHeight);
32308
- const tr = editor.view.state.tr;
32309
- tr.setNodeMarkup(state.rowPos, void 0, {
32310
- ...state.rowNode.attrs,
32311
- rowHeight: nextHeight
32312
- });
32313
- editor.view.dispatch(tr);
32314
- state.rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32315
- const refreshedRow = state.tableElement.rows.item(state.rowElement.rowIndex);
32316
- if (refreshedRow instanceof HTMLTableRowElement) {
32317
- state.rowElement = refreshedRow;
32318
- const refreshedCell = refreshedRow.cells.item(state.cellIndex);
32319
- if (refreshedCell instanceof HTMLTableCellElement) {
32320
- state.cellElement = refreshedCell;
32321
- }
32322
- }
32323
32396
  document.body.style.cursor = "row-resize";
32324
32397
  showRowGuide(state.tableElement, state.rowElement, state.cellElement);
32325
32398
  };
@@ -32330,7 +32403,16 @@ var UEditor = import_react52.default.forwardRef(({
32330
32403
  MIN_TABLE_ROW_HEIGHT,
32331
32404
  Math.round(state.startHeight + (event.clientY - state.startY))
32332
32405
  );
32406
+ const rowNode = editor.view.state.doc.nodeAt(state.rowPos) ?? state.rowNode;
32333
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
+ }
32334
32416
  rowResizeStateRef.current = null;
32335
32417
  document.body.style.cursor = "";
32336
32418
  clearHoveredTableCell();
@@ -32418,6 +32500,8 @@ var UEditor = import_react52.default.forwardRef(({
32418
32500
  variant,
32419
32501
  uploadImage,
32420
32502
  imageInsertMode,
32503
+ maxImageFileSize,
32504
+ allowedImageMimeTypes,
32421
32505
  fontFamilies,
32422
32506
  fontSizes,
32423
32507
  lineHeights,