@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/api-reference.json +1 -1
- package/dist/index.cjs +126 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +126 -42
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/api-reference.json
CHANGED
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:
|
|
23700
|
-
allowedMimeTypes:
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
24886
|
-
|
|
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
|
-
|
|
25142
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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 (
|
|
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 (!
|
|
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
|
|
26524
|
-
base64Candidates
|
|
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({
|
|
31969
|
-
|
|
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,
|