@underverse-ui/underverse 1.0.23 → 1.0.25

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,
@@ -173,9 +174,12 @@ __export(index_exports, {
173
174
  Watermark: () => Watermark_default,
174
175
  cn: () => cn,
175
176
  cnLocal: () => cn2,
177
+ extractImageSrcsFromHtml: () => extractImageSrcsFromHtml,
176
178
  getAnimationStyles: () => getAnimationStyles,
177
179
  getUnderverseMessages: () => getUnderverseMessages,
178
180
  injectAnimationStyles: () => injectAnimationStyles,
181
+ normalizeImageUrl: () => normalizeImageUrl,
182
+ prepareUEditorContentForSave: () => prepareUEditorContentForSave,
179
183
  shadcnAnimationStyles: () => shadcnAnimationStyles2,
180
184
  underverseMessages: () => underverseMessages,
181
185
  useFormField: () => useFormField,
@@ -21461,7 +21465,7 @@ function useSmartLocale() {
21461
21465
  }
21462
21466
 
21463
21467
  // ../../components/ui/UEditor/UEditor.tsx
21464
- var import_react51 = require("react");
21468
+ var import_react51 = __toESM(require("react"), 1);
21465
21469
  var import_next_intl6 = require("next-intl");
21466
21470
  var import_react52 = require("@tiptap/react");
21467
21471
 
@@ -24355,14 +24359,261 @@ var CharacterCountDisplay = ({ editor, maxCharacters }) => {
24355
24359
  ] });
24356
24360
  };
24357
24361
 
24362
+ // ../../components/ui/UEditor/prepare-content-for-save.ts
24363
+ var MIME_EXTENSION_MAP = {
24364
+ "image/png": "png",
24365
+ "image/jpeg": "jpg",
24366
+ "image/webp": "webp",
24367
+ "image/gif": "gif",
24368
+ "image/svg+xml": "svg",
24369
+ "image/bmp": "bmp",
24370
+ "image/x-icon": "ico",
24371
+ "image/avif": "avif"
24372
+ };
24373
+ function isDataImageUrl(value) {
24374
+ return /^data:image\//i.test(value.trim());
24375
+ }
24376
+ function parseDataImageUrl(dataUrl) {
24377
+ const value = dataUrl.trim();
24378
+ if (!isDataImageUrl(value)) return null;
24379
+ const commaIndex = value.indexOf(",");
24380
+ if (commaIndex < 0) return null;
24381
+ const header = value.slice(5, commaIndex);
24382
+ const base64Data = value.slice(commaIndex + 1).trim();
24383
+ if (!/;base64/i.test(header)) return null;
24384
+ const mime = header.split(";")[0]?.trim().toLowerCase();
24385
+ if (!mime || !base64Data) return null;
24386
+ return { mime, base64Data };
24387
+ }
24388
+ function decodeBase64ToBytes(base64Data) {
24389
+ const normalized = base64Data.replace(/\s+/g, "");
24390
+ const binary = atob(normalized);
24391
+ const bytes = new Uint8Array(binary.length);
24392
+ for (let i = 0; i < binary.length; i += 1) {
24393
+ bytes[i] = binary.charCodeAt(i);
24394
+ }
24395
+ return bytes;
24396
+ }
24397
+ function inferFileExtension(mime) {
24398
+ return MIME_EXTENSION_MAP[mime] ?? "bin";
24399
+ }
24400
+ function createFileFromDataImageUrl(dataUrl, index) {
24401
+ const parsed = parseDataImageUrl(dataUrl);
24402
+ if (!parsed) {
24403
+ throw new Error("Invalid data image URL format.");
24404
+ }
24405
+ const bytes = decodeBase64ToBytes(parsed.base64Data);
24406
+ const extension = inferFileExtension(parsed.mime);
24407
+ const name = `ueditor-image-${index + 1}.${extension}`;
24408
+ return new File([bytes], name, { type: parsed.mime });
24409
+ }
24410
+ function normalizeUploadResult(result) {
24411
+ if (typeof result === "string") {
24412
+ const url2 = result.trim();
24413
+ if (!url2) throw new Error("Upload handler returned an empty URL.");
24414
+ return { url: url2 };
24415
+ }
24416
+ if (!result || typeof result !== "object") {
24417
+ throw new Error("Upload handler returned invalid result.");
24418
+ }
24419
+ const url = typeof result.url === "string" ? result.url.trim() : "";
24420
+ if (!url) throw new Error("Upload handler object result is missing `url`.");
24421
+ const { url: _ignoredUrl, ...rest } = result;
24422
+ const meta = Object.keys(rest).length > 0 ? rest : void 0;
24423
+ return { url, meta };
24424
+ }
24425
+ function getErrorReason(error) {
24426
+ if (error instanceof Error && error.message) return error.message;
24427
+ if (typeof error === "string" && error.trim()) return error;
24428
+ return "Unknown upload error.";
24429
+ }
24430
+ function decodeHtmlEntities(value) {
24431
+ return value.replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&nbsp;/gi, " ");
24432
+ }
24433
+ function normalizeImageUrl(url) {
24434
+ const input = decodeHtmlEntities(url.trim());
24435
+ if (!input) return "";
24436
+ if (isDataImageUrl(input)) return input;
24437
+ const isAbsolute = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input);
24438
+ if (!isAbsolute) {
24439
+ return input.split("#")[0] ?? input;
24440
+ }
24441
+ try {
24442
+ const parsed = new URL(input);
24443
+ parsed.hash = "";
24444
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") {
24445
+ parsed.hostname = parsed.hostname.toLowerCase();
24446
+ if (parsed.protocol === "http:" && parsed.port === "80" || parsed.protocol === "https:" && parsed.port === "443") {
24447
+ parsed.port = "";
24448
+ }
24449
+ if (parsed.pathname.length > 1 && parsed.pathname.endsWith("/")) {
24450
+ parsed.pathname = parsed.pathname.slice(0, -1);
24451
+ }
24452
+ }
24453
+ return parsed.toString();
24454
+ } catch {
24455
+ return input.split("#")[0] ?? input;
24456
+ }
24457
+ }
24458
+ function replaceSrcInTag(match, nextSrc) {
24459
+ if (!match.srcAttr) return match.tag;
24460
+ const { start, end, quote } = match.srcAttr;
24461
+ const escaped = quote === '"' ? nextSrc.replace(/"/g, "&quot;") : quote === "'" ? nextSrc.replace(/'/g, "&#39;") : nextSrc;
24462
+ const srcAttr = quote ? `src=${quote}${escaped}${quote}` : `src=${escaped}`;
24463
+ return `${match.tag.slice(0, start)}${srcAttr}${match.tag.slice(end)}`;
24464
+ }
24465
+ function collectImgTagMatches(html) {
24466
+ const matches = [];
24467
+ const imgRegex = /<img\b[^>]*>/gi;
24468
+ const srcAttrRegex = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/i;
24469
+ let tagMatch = imgRegex.exec(html);
24470
+ while (tagMatch) {
24471
+ const tag = tagMatch[0];
24472
+ const start = tagMatch.index;
24473
+ const end = start + tag.length;
24474
+ const srcMatch = srcAttrRegex.exec(tag);
24475
+ let srcAttr = null;
24476
+ if (srcMatch) {
24477
+ const value = srcMatch[1] ?? srcMatch[2] ?? srcMatch[3] ?? "";
24478
+ const quote = srcMatch[1] !== void 0 ? '"' : srcMatch[2] !== void 0 ? "'" : "";
24479
+ srcAttr = {
24480
+ start: srcMatch.index,
24481
+ end: srcMatch.index + srcMatch[0].length,
24482
+ value,
24483
+ quote
24484
+ };
24485
+ }
24486
+ matches.push({ start, end, tag, srcAttr });
24487
+ tagMatch = imgRegex.exec(html);
24488
+ }
24489
+ return matches;
24490
+ }
24491
+ function extractImageSrcsFromHtml(html) {
24492
+ if (!html || !html.includes("<img")) return [];
24493
+ return collectImgTagMatches(html).map((match) => decodeHtmlEntities(match.srcAttr?.value.trim() ?? "")).filter(Boolean);
24494
+ }
24495
+ function createResult({
24496
+ html,
24497
+ uploaded,
24498
+ inlineUploaded,
24499
+ errors
24500
+ }) {
24501
+ return {
24502
+ html,
24503
+ uploaded,
24504
+ inlineImageUrls: extractImageSrcsFromHtml(html),
24505
+ inlineUploaded,
24506
+ errors
24507
+ };
24508
+ }
24509
+ var UEditorPrepareContentForSaveError = class extends Error {
24510
+ constructor(result) {
24511
+ super(
24512
+ `Failed to upload ${result.errors.length} image(s): ${result.errors.map((item) => `#${item.index} ${item.reason}`).join("; ")}`
24513
+ );
24514
+ this.name = "UEditorPrepareContentForSaveError";
24515
+ this.result = result;
24516
+ }
24517
+ };
24518
+ async function prepareUEditorContentForSave({
24519
+ html,
24520
+ uploadImageForSave
24521
+ }) {
24522
+ if (!html || !html.includes("<img")) {
24523
+ return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
24524
+ }
24525
+ const imgMatches = collectImgTagMatches(html);
24526
+ if (imgMatches.length === 0) {
24527
+ return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
24528
+ }
24529
+ const base64Candidates = [];
24530
+ for (const match of imgMatches) {
24531
+ if (!match.srcAttr) continue;
24532
+ const src = match.srcAttr.value.trim();
24533
+ if (!isDataImageUrl(src)) continue;
24534
+ base64Candidates.push({
24535
+ id: `${match.start}:${match.end}`,
24536
+ match,
24537
+ index: base64Candidates.length,
24538
+ src
24539
+ });
24540
+ }
24541
+ if (base64Candidates.length === 0) {
24542
+ return createResult({ html, uploaded: [], inlineUploaded: [], errors: [] });
24543
+ }
24544
+ if (!uploadImageForSave) {
24545
+ return createResult({
24546
+ html,
24547
+ uploaded: [],
24548
+ inlineUploaded: [],
24549
+ errors: base64Candidates.map((item) => ({
24550
+ index: item.index,
24551
+ reason: "`uploadImageForSave` is required to transform base64 images before save."
24552
+ }))
24553
+ });
24554
+ }
24555
+ const uploaded = [];
24556
+ const inlineUploaded = [];
24557
+ const errors = [];
24558
+ const replacements = /* @__PURE__ */ new Map();
24559
+ const uploadResults = await Promise.all(
24560
+ base64Candidates.map(async (candidate) => {
24561
+ try {
24562
+ const file = createFileFromDataImageUrl(candidate.src, candidate.index);
24563
+ const uploadResult = await uploadImageForSave(file);
24564
+ const normalized = normalizeUploadResult(uploadResult);
24565
+ return { candidate, file, ...normalized };
24566
+ } catch (error) {
24567
+ return { candidate, error: getErrorReason(error) };
24568
+ }
24569
+ })
24570
+ );
24571
+ for (const item of uploadResults) {
24572
+ if ("error" in item) {
24573
+ errors.push({
24574
+ index: item.candidate.index,
24575
+ reason: item.error ?? "Unknown upload error."
24576
+ });
24577
+ continue;
24578
+ }
24579
+ replacements.set(item.candidate.id, item.url);
24580
+ uploaded.push({
24581
+ url: item.url,
24582
+ file: item.file,
24583
+ meta: item.meta
24584
+ });
24585
+ inlineUploaded.push({
24586
+ index: item.candidate.index,
24587
+ url: item.url,
24588
+ file: item.file,
24589
+ meta: item.meta
24590
+ });
24591
+ }
24592
+ if (replacements.size === 0) {
24593
+ return createResult({ html, uploaded, inlineUploaded, errors });
24594
+ }
24595
+ let transformed = "";
24596
+ let cursor = 0;
24597
+ for (const match of imgMatches) {
24598
+ transformed += html.slice(cursor, match.start);
24599
+ const replacementKey = `${match.start}:${match.end}`;
24600
+ const replacementUrl = replacements.get(replacementKey);
24601
+ transformed += replacementUrl ? replaceSrcInTag(match, replacementUrl) : match.tag;
24602
+ cursor = match.end;
24603
+ }
24604
+ transformed += html.slice(cursor);
24605
+ return createResult({ html: transformed, uploaded, inlineUploaded, errors });
24606
+ }
24607
+
24358
24608
  // ../../components/ui/UEditor/UEditor.tsx
24359
24609
  var import_jsx_runtime86 = require("react/jsx-runtime");
24360
- var UEditor = ({
24610
+ var UEditor = import_react51.default.forwardRef(({
24361
24611
  content = "",
24362
24612
  onChange,
24363
24613
  onHtmlChange,
24364
24614
  onJsonChange,
24365
24615
  uploadImage,
24616
+ uploadImageForSave,
24366
24617
  imageInsertMode = "base64",
24367
24618
  placeholder,
24368
24619
  className,
@@ -24376,9 +24627,10 @@ var UEditor = ({
24376
24627
  minHeight = "200px",
24377
24628
  maxHeight = "auto",
24378
24629
  variant = "default"
24379
- }) => {
24630
+ }, ref) => {
24380
24631
  const t = (0, import_next_intl6.useTranslations)("UEditor");
24381
24632
  const effectivePlaceholder = placeholder ?? t("placeholder");
24633
+ const inFlightPrepareRef = (0, import_react51.useRef)(null);
24382
24634
  const extensions = (0, import_react51.useMemo)(
24383
24635
  () => buildUEditorExtensions({ placeholder: effectivePlaceholder, maxCharacters, uploadImage, imageInsertMode, editable }),
24384
24636
  [effectivePlaceholder, maxCharacters, uploadImage, imageInsertMode, editable]
@@ -24488,6 +24740,28 @@ var UEditor = ({
24488
24740
  onJsonChange?.(editor2.getJSON());
24489
24741
  }
24490
24742
  });
24743
+ (0, import_react51.useImperativeHandle)(
24744
+ ref,
24745
+ () => ({
24746
+ prepareContentForSave: async ({ throwOnError = false } = {}) => {
24747
+ if (!inFlightPrepareRef.current) {
24748
+ const htmlSnapshot = editor?.getHTML() ?? content ?? "";
24749
+ inFlightPrepareRef.current = prepareUEditorContentForSave({
24750
+ html: htmlSnapshot,
24751
+ uploadImageForSave
24752
+ }).finally(() => {
24753
+ inFlightPrepareRef.current = null;
24754
+ });
24755
+ }
24756
+ const result = await inFlightPrepareRef.current;
24757
+ if (throwOnError && result.errors.length > 0) {
24758
+ throw new UEditorPrepareContentForSaveError(result);
24759
+ }
24760
+ return result;
24761
+ }
24762
+ }),
24763
+ [content, editor, uploadImageForSave]
24764
+ );
24491
24765
  (0, import_react51.useEffect)(() => {
24492
24766
  if (editor && content !== editor.getHTML()) {
24493
24767
  if (editor.isEmpty && content) {
@@ -24535,7 +24809,8 @@ var UEditor = ({
24535
24809
  ]
24536
24810
  }
24537
24811
  );
24538
- };
24812
+ });
24813
+ UEditor.displayName = "UEditor";
24539
24814
  var UEditor_default = UEditor;
24540
24815
 
24541
24816
  // src/index.ts
@@ -24681,6 +24956,7 @@ function getUnderverseMessages(locale = "en") {
24681
24956
  Tooltip,
24682
24957
  TranslationProvider,
24683
24958
  UEditor,
24959
+ UEditorPrepareContentForSaveError,
24684
24960
  UnderverseProvider,
24685
24961
  VARIANT_STYLES_ALERT,
24686
24962
  VARIANT_STYLES_BTN,
@@ -24688,9 +24964,12 @@ function getUnderverseMessages(locale = "en") {
24688
24964
  Watermark,
24689
24965
  cn,
24690
24966
  cnLocal,
24967
+ extractImageSrcsFromHtml,
24691
24968
  getAnimationStyles,
24692
24969
  getUnderverseMessages,
24693
24970
  injectAnimationStyles,
24971
+ normalizeImageUrl,
24972
+ prepareUEditorContentForSave,
24694
24973
  shadcnAnimationStyles,
24695
24974
  underverseMessages,
24696
24975
  useFormField,