@underverse-ui/underverse 1.0.23 → 1.0.24

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
@@ -166,6 +166,7 @@ __export(index_exports, {
166
166
  Tooltip: () => Tooltip,
167
167
  TranslationProvider: () => TranslationProvider,
168
168
  UEditor: () => UEditor_default,
169
+ UEditorPrepareContentForSaveError: () => UEditorPrepareContentForSaveError,
169
170
  UnderverseProvider: () => UnderverseProvider,
170
171
  VARIANT_STYLES_ALERT: () => VARIANT_STYLES_ALERT,
171
172
  VARIANT_STYLES_BTN: () => VARIANT_STYLES_BTN,
@@ -176,6 +177,7 @@ __export(index_exports, {
176
177
  getAnimationStyles: () => getAnimationStyles,
177
178
  getUnderverseMessages: () => getUnderverseMessages,
178
179
  injectAnimationStyles: () => injectAnimationStyles,
180
+ prepareUEditorContentForSave: () => prepareUEditorContentForSave,
179
181
  shadcnAnimationStyles: () => shadcnAnimationStyles2,
180
182
  underverseMessages: () => underverseMessages,
181
183
  useFormField: () => useFormField,
@@ -21461,7 +21463,7 @@ function useSmartLocale() {
21461
21463
  }
21462
21464
 
21463
21465
  // ../../components/ui/UEditor/UEditor.tsx
21464
- var import_react51 = require("react");
21466
+ var import_react51 = __toESM(require("react"), 1);
21465
21467
  var import_next_intl6 = require("next-intl");
21466
21468
  var import_react52 = require("@tiptap/react");
21467
21469
 
@@ -24355,14 +24357,207 @@ var CharacterCountDisplay = ({ editor, maxCharacters }) => {
24355
24357
  ] });
24356
24358
  };
24357
24359
 
24360
+ // ../../components/ui/UEditor/prepare-content-for-save.ts
24361
+ var MIME_EXTENSION_MAP = {
24362
+ "image/png": "png",
24363
+ "image/jpeg": "jpg",
24364
+ "image/webp": "webp",
24365
+ "image/gif": "gif",
24366
+ "image/svg+xml": "svg",
24367
+ "image/bmp": "bmp",
24368
+ "image/x-icon": "ico",
24369
+ "image/avif": "avif"
24370
+ };
24371
+ function isDataImageUrl(value) {
24372
+ return /^data:image\//i.test(value.trim());
24373
+ }
24374
+ function parseDataImageUrl(dataUrl) {
24375
+ const value = dataUrl.trim();
24376
+ if (!isDataImageUrl(value)) return null;
24377
+ const commaIndex = value.indexOf(",");
24378
+ if (commaIndex < 0) return null;
24379
+ const header = value.slice(5, commaIndex);
24380
+ const base64Data = value.slice(commaIndex + 1).trim();
24381
+ if (!/;base64/i.test(header)) return null;
24382
+ const mime = header.split(";")[0]?.trim().toLowerCase();
24383
+ if (!mime || !base64Data) return null;
24384
+ return { mime, base64Data };
24385
+ }
24386
+ function decodeBase64ToBytes(base64Data) {
24387
+ const normalized = base64Data.replace(/\s+/g, "");
24388
+ const binary = atob(normalized);
24389
+ const bytes = new Uint8Array(binary.length);
24390
+ for (let i = 0; i < binary.length; i += 1) {
24391
+ bytes[i] = binary.charCodeAt(i);
24392
+ }
24393
+ return bytes;
24394
+ }
24395
+ function inferFileExtension(mime) {
24396
+ return MIME_EXTENSION_MAP[mime] ?? "bin";
24397
+ }
24398
+ function createFileFromDataImageUrl(dataUrl, index) {
24399
+ const parsed = parseDataImageUrl(dataUrl);
24400
+ if (!parsed) {
24401
+ throw new Error("Invalid data image URL format.");
24402
+ }
24403
+ const bytes = decodeBase64ToBytes(parsed.base64Data);
24404
+ const extension = inferFileExtension(parsed.mime);
24405
+ const name = `ueditor-image-${index + 1}.${extension}`;
24406
+ return new File([bytes], name, { type: parsed.mime });
24407
+ }
24408
+ function normalizeUploadResult(result) {
24409
+ if (typeof result === "string") {
24410
+ const url2 = result.trim();
24411
+ if (!url2) throw new Error("Upload handler returned an empty URL.");
24412
+ return { url: url2 };
24413
+ }
24414
+ if (!result || typeof result !== "object") {
24415
+ throw new Error("Upload handler returned invalid result.");
24416
+ }
24417
+ const url = typeof result.url === "string" ? result.url.trim() : "";
24418
+ if (!url) throw new Error("Upload handler object result is missing `url`.");
24419
+ const { url: _ignoredUrl, ...rest } = result;
24420
+ const meta = Object.keys(rest).length > 0 ? rest : void 0;
24421
+ return { url, meta };
24422
+ }
24423
+ function getErrorReason(error) {
24424
+ if (error instanceof Error && error.message) return error.message;
24425
+ if (typeof error === "string" && error.trim()) return error;
24426
+ return "Unknown upload error.";
24427
+ }
24428
+ function replaceSrcInTag(match, nextSrc) {
24429
+ if (!match.srcAttr) return match.tag;
24430
+ const { start, end, quote } = match.srcAttr;
24431
+ const escaped = quote === '"' ? nextSrc.replace(/"/g, "&quot;") : quote === "'" ? nextSrc.replace(/'/g, "&#39;") : nextSrc;
24432
+ const srcAttr = quote ? `src=${quote}${escaped}${quote}` : `src=${escaped}`;
24433
+ return `${match.tag.slice(0, start)}${srcAttr}${match.tag.slice(end)}`;
24434
+ }
24435
+ function collectImgTagMatches(html) {
24436
+ const matches = [];
24437
+ const imgRegex = /<img\b[^>]*>/gi;
24438
+ const srcAttrRegex = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/i;
24439
+ let tagMatch = imgRegex.exec(html);
24440
+ while (tagMatch) {
24441
+ const tag = tagMatch[0];
24442
+ const start = tagMatch.index;
24443
+ const end = start + tag.length;
24444
+ const srcMatch = srcAttrRegex.exec(tag);
24445
+ let srcAttr = null;
24446
+ if (srcMatch) {
24447
+ const value = srcMatch[1] ?? srcMatch[2] ?? srcMatch[3] ?? "";
24448
+ const quote = srcMatch[1] !== void 0 ? '"' : srcMatch[2] !== void 0 ? "'" : "";
24449
+ srcAttr = {
24450
+ start: srcMatch.index,
24451
+ end: srcMatch.index + srcMatch[0].length,
24452
+ value,
24453
+ quote
24454
+ };
24455
+ }
24456
+ matches.push({ start, end, tag, srcAttr });
24457
+ tagMatch = imgRegex.exec(html);
24458
+ }
24459
+ return matches;
24460
+ }
24461
+ var UEditorPrepareContentForSaveError = class extends Error {
24462
+ constructor(result) {
24463
+ super(
24464
+ `Failed to upload ${result.errors.length} image(s): ${result.errors.map((item) => `#${item.index} ${item.reason}`).join("; ")}`
24465
+ );
24466
+ this.name = "UEditorPrepareContentForSaveError";
24467
+ this.result = result;
24468
+ }
24469
+ };
24470
+ async function prepareUEditorContentForSave({
24471
+ html,
24472
+ uploadImageForSave
24473
+ }) {
24474
+ if (!html || !html.includes("<img")) {
24475
+ return { html, uploaded: [], errors: [] };
24476
+ }
24477
+ const imgMatches = collectImgTagMatches(html);
24478
+ if (imgMatches.length === 0) {
24479
+ return { html, uploaded: [], errors: [] };
24480
+ }
24481
+ const base64Candidates = [];
24482
+ for (const match of imgMatches) {
24483
+ if (!match.srcAttr) continue;
24484
+ const src = match.srcAttr.value.trim();
24485
+ if (!isDataImageUrl(src)) continue;
24486
+ base64Candidates.push({
24487
+ id: `${match.start}:${match.end}`,
24488
+ match,
24489
+ index: base64Candidates.length,
24490
+ src
24491
+ });
24492
+ }
24493
+ if (base64Candidates.length === 0) {
24494
+ return { html, uploaded: [], errors: [] };
24495
+ }
24496
+ if (!uploadImageForSave) {
24497
+ return {
24498
+ html,
24499
+ uploaded: [],
24500
+ errors: base64Candidates.map((item) => ({
24501
+ index: item.index,
24502
+ reason: "`uploadImageForSave` is required to transform base64 images before save."
24503
+ }))
24504
+ };
24505
+ }
24506
+ const uploaded = [];
24507
+ const errors = [];
24508
+ const replacements = /* @__PURE__ */ new Map();
24509
+ const uploadResults = await Promise.all(
24510
+ base64Candidates.map(async (candidate) => {
24511
+ try {
24512
+ const file = createFileFromDataImageUrl(candidate.src, candidate.index);
24513
+ const uploadResult = await uploadImageForSave(file);
24514
+ const normalized = normalizeUploadResult(uploadResult);
24515
+ return { candidate, file, ...normalized };
24516
+ } catch (error) {
24517
+ return { candidate, error: getErrorReason(error) };
24518
+ }
24519
+ })
24520
+ );
24521
+ for (const item of uploadResults) {
24522
+ if ("error" in item) {
24523
+ errors.push({
24524
+ index: item.candidate.index,
24525
+ reason: item.error ?? "Unknown upload error."
24526
+ });
24527
+ continue;
24528
+ }
24529
+ replacements.set(item.candidate.id, item.url);
24530
+ uploaded.push({
24531
+ url: item.url,
24532
+ file: item.file,
24533
+ meta: item.meta
24534
+ });
24535
+ }
24536
+ if (replacements.size === 0) {
24537
+ return { html, uploaded, errors };
24538
+ }
24539
+ let transformed = "";
24540
+ let cursor = 0;
24541
+ for (const match of imgMatches) {
24542
+ transformed += html.slice(cursor, match.start);
24543
+ const replacementKey = `${match.start}:${match.end}`;
24544
+ const replacementUrl = replacements.get(replacementKey);
24545
+ transformed += replacementUrl ? replaceSrcInTag(match, replacementUrl) : match.tag;
24546
+ cursor = match.end;
24547
+ }
24548
+ transformed += html.slice(cursor);
24549
+ return { html: transformed, uploaded, errors };
24550
+ }
24551
+
24358
24552
  // ../../components/ui/UEditor/UEditor.tsx
24359
24553
  var import_jsx_runtime86 = require("react/jsx-runtime");
24360
- var UEditor = ({
24554
+ var UEditor = import_react51.default.forwardRef(({
24361
24555
  content = "",
24362
24556
  onChange,
24363
24557
  onHtmlChange,
24364
24558
  onJsonChange,
24365
24559
  uploadImage,
24560
+ uploadImageForSave,
24366
24561
  imageInsertMode = "base64",
24367
24562
  placeholder,
24368
24563
  className,
@@ -24376,9 +24571,10 @@ var UEditor = ({
24376
24571
  minHeight = "200px",
24377
24572
  maxHeight = "auto",
24378
24573
  variant = "default"
24379
- }) => {
24574
+ }, ref) => {
24380
24575
  const t = (0, import_next_intl6.useTranslations)("UEditor");
24381
24576
  const effectivePlaceholder = placeholder ?? t("placeholder");
24577
+ const inFlightPrepareRef = (0, import_react51.useRef)(null);
24382
24578
  const extensions = (0, import_react51.useMemo)(
24383
24579
  () => buildUEditorExtensions({ placeholder: effectivePlaceholder, maxCharacters, uploadImage, imageInsertMode, editable }),
24384
24580
  [effectivePlaceholder, maxCharacters, uploadImage, imageInsertMode, editable]
@@ -24488,6 +24684,28 @@ var UEditor = ({
24488
24684
  onJsonChange?.(editor2.getJSON());
24489
24685
  }
24490
24686
  });
24687
+ (0, import_react51.useImperativeHandle)(
24688
+ ref,
24689
+ () => ({
24690
+ prepareContentForSave: async ({ throwOnError = false } = {}) => {
24691
+ if (!inFlightPrepareRef.current) {
24692
+ const htmlSnapshot = editor?.getHTML() ?? content ?? "";
24693
+ inFlightPrepareRef.current = prepareUEditorContentForSave({
24694
+ html: htmlSnapshot,
24695
+ uploadImageForSave
24696
+ }).finally(() => {
24697
+ inFlightPrepareRef.current = null;
24698
+ });
24699
+ }
24700
+ const result = await inFlightPrepareRef.current;
24701
+ if (throwOnError && result.errors.length > 0) {
24702
+ throw new UEditorPrepareContentForSaveError(result);
24703
+ }
24704
+ return result;
24705
+ }
24706
+ }),
24707
+ [content, editor, uploadImageForSave]
24708
+ );
24491
24709
  (0, import_react51.useEffect)(() => {
24492
24710
  if (editor && content !== editor.getHTML()) {
24493
24711
  if (editor.isEmpty && content) {
@@ -24535,7 +24753,8 @@ var UEditor = ({
24535
24753
  ]
24536
24754
  }
24537
24755
  );
24538
- };
24756
+ });
24757
+ UEditor.displayName = "UEditor";
24539
24758
  var UEditor_default = UEditor;
24540
24759
 
24541
24760
  // src/index.ts
@@ -24681,6 +24900,7 @@ function getUnderverseMessages(locale = "en") {
24681
24900
  Tooltip,
24682
24901
  TranslationProvider,
24683
24902
  UEditor,
24903
+ UEditorPrepareContentForSaveError,
24684
24904
  UnderverseProvider,
24685
24905
  VARIANT_STYLES_ALERT,
24686
24906
  VARIANT_STYLES_BTN,
@@ -24691,6 +24911,7 @@ function getUnderverseMessages(locale = "en") {
24691
24911
  getAnimationStyles,
24692
24912
  getUnderverseMessages,
24693
24913
  injectAnimationStyles,
24914
+ prepareUEditorContentForSave,
24694
24915
  shadcnAnimationStyles,
24695
24916
  underverseMessages,
24696
24917
  useFormField,