@adminforth/markdown 1.5.0 → 1.6.0
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/build.log +2 -2
- package/custom/MarkdownEditor.vue +134 -14
- package/dist/custom/MarkdownEditor.vue +134 -14
- package/dist/index.js +65 -11
- package/index.ts +74 -11
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -10,5 +10,5 @@ custom/package-lock.json
|
|
|
10
10
|
custom/package.json
|
|
11
11
|
custom/tsconfig.json
|
|
12
12
|
|
|
13
|
-
sent
|
|
14
|
-
total size is
|
|
13
|
+
sent 29,778 bytes received 115 bytes 59,786.00 bytes/sec
|
|
14
|
+
total size is 29,346 speedup is 0.98
|
|
@@ -240,6 +240,8 @@ let removePasteListener: (() => void) | null = null;
|
|
|
240
240
|
let removePasteListenerSecondary: (() => void) | null = null;
|
|
241
241
|
let removeGlobalPasteListener: (() => void) | null = null;
|
|
242
242
|
let removeGlobalKeydownListener: (() => void) | null = null;
|
|
243
|
+
let removeDragOverListener: (() => void) | null = null;
|
|
244
|
+
let removeDropListener: (() => void) | null = null;
|
|
243
245
|
|
|
244
246
|
type MarkdownImageRef = {
|
|
245
247
|
lineNumber: number;
|
|
@@ -387,6 +389,62 @@ function fileFromClipboardImage(blob: Blob): File {
|
|
|
387
389
|
return new File([blob], filename, { type });
|
|
388
390
|
}
|
|
389
391
|
|
|
392
|
+
function escapeMarkdownLinkText(text: string): string {
|
|
393
|
+
return text.replace(/[\[\]\\]/g, '\\$&');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function showAdminforthError(message: string) {
|
|
397
|
+
const api = (window as any).adminforth;
|
|
398
|
+
if (api && typeof api.alert === 'function') {
|
|
399
|
+
api.alert({
|
|
400
|
+
message,
|
|
401
|
+
variant: 'danger',
|
|
402
|
+
timeout: 30,
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
console.error('[adminforth-markdown]', message);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function extractErrorMessage(error: any): string {
|
|
410
|
+
if (!error) return 'Upload failed';
|
|
411
|
+
if (typeof error === 'string') return error;
|
|
412
|
+
if (typeof error?.error === 'string') return error.error;
|
|
413
|
+
if (typeof error?.message === 'string') return error.message;
|
|
414
|
+
try {
|
|
415
|
+
return JSON.stringify(error);
|
|
416
|
+
} catch {
|
|
417
|
+
return 'Upload failed';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function markdownForUploadedFile(file: File, url: string): string {
|
|
422
|
+
if (file.type?.startsWith('image/')) {
|
|
423
|
+
return ``;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (file.type?.startsWith('video/')) {
|
|
427
|
+
const mediaType = file.type || 'video/mp4';
|
|
428
|
+
return `<video width="400">\n<!-- For gif-like videos use: <video width="400" autoplay loop muted playsinline> -->\n <source src="${url}" type="${mediaType}">\n</video>`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// alert that file cant be uploaded
|
|
432
|
+
showAdminforthError(`Uploaded file "${file.name}" is not an image or video and cannot be embedded. It has been uploaded and can be accessed at: ${url}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function uploadFileAndGetMarkdownTag(file: File): Promise<string | undefined> {
|
|
436
|
+
try {
|
|
437
|
+
const url = await uploadFileToS3(file);
|
|
438
|
+
if (!url) return;
|
|
439
|
+
return markdownForUploadedFile(file, url);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const message = extractErrorMessage(error);
|
|
442
|
+
showAdminforthError(message);
|
|
443
|
+
console.error('[adminforth-markdown] upload failed', error);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
390
448
|
onMounted(async () => {
|
|
391
449
|
if (!editorContainer.value) return;
|
|
392
450
|
try {
|
|
@@ -433,6 +491,59 @@ onMounted(async () => {
|
|
|
433
491
|
const noopPaste = () => {};
|
|
434
492
|
domNode.addEventListener('paste', noopPaste, true);
|
|
435
493
|
removePasteListener = () => domNode.removeEventListener('paste', noopPaste, true);
|
|
494
|
+
|
|
495
|
+
const onDragOver = (e: DragEvent) => {
|
|
496
|
+
if (!e.dataTransfer) return;
|
|
497
|
+
if (!Array.from(e.dataTransfer.types).includes('Files')) return;
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const onDrop = async (e: DragEvent) => {
|
|
503
|
+
const dt = e.dataTransfer;
|
|
504
|
+
if (!dt) return;
|
|
505
|
+
if (!dt.files || !dt.files.length) return;
|
|
506
|
+
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
e.stopPropagation();
|
|
509
|
+
|
|
510
|
+
if (!editor) return;
|
|
511
|
+
editor.focus();
|
|
512
|
+
|
|
513
|
+
const target = editor.getTargetAtClientPoint(e.clientX, e.clientY);
|
|
514
|
+
const dropPosition = target?.position || target?.range?.getStartPosition?.();
|
|
515
|
+
if (dropPosition) {
|
|
516
|
+
editor.setPosition(dropPosition);
|
|
517
|
+
editor.setSelection(new monaco.Selection(
|
|
518
|
+
dropPosition.lineNumber,
|
|
519
|
+
dropPosition.column,
|
|
520
|
+
dropPosition.lineNumber,
|
|
521
|
+
dropPosition.column,
|
|
522
|
+
));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!props.meta?.uploadPluginInstanceId) {
|
|
526
|
+
const msg = 'uploadPluginInstanceId is missing; cannot upload dropped file.';
|
|
527
|
+
showAdminforthError(msg);
|
|
528
|
+
console.error('[adminforth-markdown]', msg);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const markdownTags: string[] = [];
|
|
533
|
+
for (const file of Array.from(dt.files)) {
|
|
534
|
+
const tag = await uploadFileAndGetMarkdownTag(file);
|
|
535
|
+
if (tag) markdownTags.push(tag);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (markdownTags.length) {
|
|
539
|
+
insertAtCursor(`${markdownTags.join('\n\n')}\n`);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
domNode.addEventListener('dragover', onDragOver, true);
|
|
544
|
+
domNode.addEventListener('drop', onDrop, true);
|
|
545
|
+
removeDragOverListener = () => domNode.removeEventListener('dragover', onDragOver, true);
|
|
546
|
+
removeDropListener = () => domNode.removeEventListener('drop', onDrop, true);
|
|
436
547
|
}
|
|
437
548
|
if (editorContainer.value) {
|
|
438
549
|
const noopPaste = () => {};
|
|
@@ -496,15 +607,9 @@ onMounted(async () => {
|
|
|
496
607
|
const markdownTags: string[] = [];
|
|
497
608
|
for (const blob of imageBlobs) {
|
|
498
609
|
const file = blob instanceof File ? blob : fileFromClipboardImage(blob);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (typeof url === 'string' && url.length) {
|
|
503
|
-
markdownTags.push(``);
|
|
504
|
-
}
|
|
505
|
-
} catch (err) {
|
|
506
|
-
console.error('[adminforth-markdown] upload failed', err);
|
|
507
|
-
}
|
|
610
|
+
const tag = await uploadFileAndGetMarkdownTag(file);
|
|
611
|
+
if (!tag) continue;
|
|
612
|
+
markdownTags.push(tag);
|
|
508
613
|
}
|
|
509
614
|
|
|
510
615
|
if (markdownTags.length) {
|
|
@@ -570,7 +675,7 @@ async function uploadFileToS3(file: File): Promise<string | undefined> {
|
|
|
570
675
|
const originalFilename = file.name.split('.').slice(0, -1).join('.');
|
|
571
676
|
const originalExtension = file.name.split('.').pop();
|
|
572
677
|
|
|
573
|
-
const { uploadUrl, tagline, previewUrl,
|
|
678
|
+
const { uploadUrl, tagline, previewUrl, error } = await callAdminForthApi({
|
|
574
679
|
path: `/plugin/${props.meta.uploadPluginInstanceId}/get_file_upload_url`,
|
|
575
680
|
method: 'POST',
|
|
576
681
|
body: {
|
|
@@ -582,8 +687,13 @@ async function uploadFileToS3(file: File): Promise<string | undefined> {
|
|
|
582
687
|
});
|
|
583
688
|
|
|
584
689
|
if (error) {
|
|
585
|
-
|
|
586
|
-
|
|
690
|
+
const message = extractErrorMessage(error);
|
|
691
|
+
if (/too\s*large|max\s*file\s*size|size\s*limit|limit\s*reached|exceed/i.test(message)) {
|
|
692
|
+
showAdminforthError(message);
|
|
693
|
+
} else {
|
|
694
|
+
showAdminforthError(message);
|
|
695
|
+
}
|
|
696
|
+
throw new Error(message);
|
|
587
697
|
}
|
|
588
698
|
|
|
589
699
|
const xhr = new XMLHttpRequest();
|
|
@@ -597,12 +707,16 @@ async function uploadFileToS3(file: File): Promise<string | undefined> {
|
|
|
597
707
|
if (xhr.status === 200) {
|
|
598
708
|
resolve(previewUrl as string);
|
|
599
709
|
} else {
|
|
600
|
-
|
|
710
|
+
const message = `Error uploading to S3 (status ${xhr.status})`;
|
|
711
|
+
showAdminforthError(message);
|
|
712
|
+
reject(message);
|
|
601
713
|
}
|
|
602
714
|
};
|
|
603
715
|
|
|
604
716
|
xhr.onerror = () => {
|
|
605
|
-
|
|
717
|
+
const message = 'Error uploading to S3';
|
|
718
|
+
showAdminforthError(message);
|
|
719
|
+
reject(message);
|
|
606
720
|
};
|
|
607
721
|
});
|
|
608
722
|
}
|
|
@@ -627,6 +741,12 @@ onBeforeUnmount(() => {
|
|
|
627
741
|
removeGlobalKeydownListener?.();
|
|
628
742
|
removeGlobalKeydownListener = null;
|
|
629
743
|
|
|
744
|
+
removeDragOverListener?.();
|
|
745
|
+
removeDragOverListener = null;
|
|
746
|
+
|
|
747
|
+
removeDropListener?.();
|
|
748
|
+
removeDropListener = null;
|
|
749
|
+
|
|
630
750
|
for (const d of disposables) d.dispose();
|
|
631
751
|
disposables.length = 0;
|
|
632
752
|
|
|
@@ -240,6 +240,8 @@ let removePasteListener: (() => void) | null = null;
|
|
|
240
240
|
let removePasteListenerSecondary: (() => void) | null = null;
|
|
241
241
|
let removeGlobalPasteListener: (() => void) | null = null;
|
|
242
242
|
let removeGlobalKeydownListener: (() => void) | null = null;
|
|
243
|
+
let removeDragOverListener: (() => void) | null = null;
|
|
244
|
+
let removeDropListener: (() => void) | null = null;
|
|
243
245
|
|
|
244
246
|
type MarkdownImageRef = {
|
|
245
247
|
lineNumber: number;
|
|
@@ -387,6 +389,62 @@ function fileFromClipboardImage(blob: Blob): File {
|
|
|
387
389
|
return new File([blob], filename, { type });
|
|
388
390
|
}
|
|
389
391
|
|
|
392
|
+
function escapeMarkdownLinkText(text: string): string {
|
|
393
|
+
return text.replace(/[\[\]\\]/g, '\\$&');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function showAdminforthError(message: string) {
|
|
397
|
+
const api = (window as any).adminforth;
|
|
398
|
+
if (api && typeof api.alert === 'function') {
|
|
399
|
+
api.alert({
|
|
400
|
+
message,
|
|
401
|
+
variant: 'danger',
|
|
402
|
+
timeout: 30,
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
console.error('[adminforth-markdown]', message);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function extractErrorMessage(error: any): string {
|
|
410
|
+
if (!error) return 'Upload failed';
|
|
411
|
+
if (typeof error === 'string') return error;
|
|
412
|
+
if (typeof error?.error === 'string') return error.error;
|
|
413
|
+
if (typeof error?.message === 'string') return error.message;
|
|
414
|
+
try {
|
|
415
|
+
return JSON.stringify(error);
|
|
416
|
+
} catch {
|
|
417
|
+
return 'Upload failed';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function markdownForUploadedFile(file: File, url: string): string {
|
|
422
|
+
if (file.type?.startsWith('image/')) {
|
|
423
|
+
return ``;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (file.type?.startsWith('video/')) {
|
|
427
|
+
const mediaType = file.type || 'video/mp4';
|
|
428
|
+
return `<video width="400">\n<!-- For gif-like videos use: <video width="400" autoplay loop muted playsinline> -->\n <source src="${url}" type="${mediaType}">\n</video>`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// alert that file cant be uploaded
|
|
432
|
+
showAdminforthError(`Uploaded file "${file.name}" is not an image or video and cannot be embedded. It has been uploaded and can be accessed at: ${url}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function uploadFileAndGetMarkdownTag(file: File): Promise<string | undefined> {
|
|
436
|
+
try {
|
|
437
|
+
const url = await uploadFileToS3(file);
|
|
438
|
+
if (!url) return;
|
|
439
|
+
return markdownForUploadedFile(file, url);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const message = extractErrorMessage(error);
|
|
442
|
+
showAdminforthError(message);
|
|
443
|
+
console.error('[adminforth-markdown] upload failed', error);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
390
448
|
onMounted(async () => {
|
|
391
449
|
if (!editorContainer.value) return;
|
|
392
450
|
try {
|
|
@@ -433,6 +491,59 @@ onMounted(async () => {
|
|
|
433
491
|
const noopPaste = () => {};
|
|
434
492
|
domNode.addEventListener('paste', noopPaste, true);
|
|
435
493
|
removePasteListener = () => domNode.removeEventListener('paste', noopPaste, true);
|
|
494
|
+
|
|
495
|
+
const onDragOver = (e: DragEvent) => {
|
|
496
|
+
if (!e.dataTransfer) return;
|
|
497
|
+
if (!Array.from(e.dataTransfer.types).includes('Files')) return;
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const onDrop = async (e: DragEvent) => {
|
|
503
|
+
const dt = e.dataTransfer;
|
|
504
|
+
if (!dt) return;
|
|
505
|
+
if (!dt.files || !dt.files.length) return;
|
|
506
|
+
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
e.stopPropagation();
|
|
509
|
+
|
|
510
|
+
if (!editor) return;
|
|
511
|
+
editor.focus();
|
|
512
|
+
|
|
513
|
+
const target = editor.getTargetAtClientPoint(e.clientX, e.clientY);
|
|
514
|
+
const dropPosition = target?.position || target?.range?.getStartPosition?.();
|
|
515
|
+
if (dropPosition) {
|
|
516
|
+
editor.setPosition(dropPosition);
|
|
517
|
+
editor.setSelection(new monaco.Selection(
|
|
518
|
+
dropPosition.lineNumber,
|
|
519
|
+
dropPosition.column,
|
|
520
|
+
dropPosition.lineNumber,
|
|
521
|
+
dropPosition.column,
|
|
522
|
+
));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!props.meta?.uploadPluginInstanceId) {
|
|
526
|
+
const msg = 'uploadPluginInstanceId is missing; cannot upload dropped file.';
|
|
527
|
+
showAdminforthError(msg);
|
|
528
|
+
console.error('[adminforth-markdown]', msg);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const markdownTags: string[] = [];
|
|
533
|
+
for (const file of Array.from(dt.files)) {
|
|
534
|
+
const tag = await uploadFileAndGetMarkdownTag(file);
|
|
535
|
+
if (tag) markdownTags.push(tag);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (markdownTags.length) {
|
|
539
|
+
insertAtCursor(`${markdownTags.join('\n\n')}\n`);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
domNode.addEventListener('dragover', onDragOver, true);
|
|
544
|
+
domNode.addEventListener('drop', onDrop, true);
|
|
545
|
+
removeDragOverListener = () => domNode.removeEventListener('dragover', onDragOver, true);
|
|
546
|
+
removeDropListener = () => domNode.removeEventListener('drop', onDrop, true);
|
|
436
547
|
}
|
|
437
548
|
if (editorContainer.value) {
|
|
438
549
|
const noopPaste = () => {};
|
|
@@ -496,15 +607,9 @@ onMounted(async () => {
|
|
|
496
607
|
const markdownTags: string[] = [];
|
|
497
608
|
for (const blob of imageBlobs) {
|
|
498
609
|
const file = blob instanceof File ? blob : fileFromClipboardImage(blob);
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (typeof url === 'string' && url.length) {
|
|
503
|
-
markdownTags.push(``);
|
|
504
|
-
}
|
|
505
|
-
} catch (err) {
|
|
506
|
-
console.error('[adminforth-markdown] upload failed', err);
|
|
507
|
-
}
|
|
610
|
+
const tag = await uploadFileAndGetMarkdownTag(file);
|
|
611
|
+
if (!tag) continue;
|
|
612
|
+
markdownTags.push(tag);
|
|
508
613
|
}
|
|
509
614
|
|
|
510
615
|
if (markdownTags.length) {
|
|
@@ -570,7 +675,7 @@ async function uploadFileToS3(file: File): Promise<string | undefined> {
|
|
|
570
675
|
const originalFilename = file.name.split('.').slice(0, -1).join('.');
|
|
571
676
|
const originalExtension = file.name.split('.').pop();
|
|
572
677
|
|
|
573
|
-
const { uploadUrl, tagline, previewUrl,
|
|
678
|
+
const { uploadUrl, tagline, previewUrl, error } = await callAdminForthApi({
|
|
574
679
|
path: `/plugin/${props.meta.uploadPluginInstanceId}/get_file_upload_url`,
|
|
575
680
|
method: 'POST',
|
|
576
681
|
body: {
|
|
@@ -582,8 +687,13 @@ async function uploadFileToS3(file: File): Promise<string | undefined> {
|
|
|
582
687
|
});
|
|
583
688
|
|
|
584
689
|
if (error) {
|
|
585
|
-
|
|
586
|
-
|
|
690
|
+
const message = extractErrorMessage(error);
|
|
691
|
+
if (/too\s*large|max\s*file\s*size|size\s*limit|limit\s*reached|exceed/i.test(message)) {
|
|
692
|
+
showAdminforthError(message);
|
|
693
|
+
} else {
|
|
694
|
+
showAdminforthError(message);
|
|
695
|
+
}
|
|
696
|
+
throw new Error(message);
|
|
587
697
|
}
|
|
588
698
|
|
|
589
699
|
const xhr = new XMLHttpRequest();
|
|
@@ -597,12 +707,16 @@ async function uploadFileToS3(file: File): Promise<string | undefined> {
|
|
|
597
707
|
if (xhr.status === 200) {
|
|
598
708
|
resolve(previewUrl as string);
|
|
599
709
|
} else {
|
|
600
|
-
|
|
710
|
+
const message = `Error uploading to S3 (status ${xhr.status})`;
|
|
711
|
+
showAdminforthError(message);
|
|
712
|
+
reject(message);
|
|
601
713
|
}
|
|
602
714
|
};
|
|
603
715
|
|
|
604
716
|
xhr.onerror = () => {
|
|
605
|
-
|
|
717
|
+
const message = 'Error uploading to S3';
|
|
718
|
+
showAdminforthError(message);
|
|
719
|
+
reject(message);
|
|
606
720
|
};
|
|
607
721
|
});
|
|
608
722
|
}
|
|
@@ -627,6 +741,12 @@ onBeforeUnmount(() => {
|
|
|
627
741
|
removeGlobalKeydownListener?.();
|
|
628
742
|
removeGlobalKeydownListener = null;
|
|
629
743
|
|
|
744
|
+
removeDragOverListener?.();
|
|
745
|
+
removeDragOverListener = null;
|
|
746
|
+
|
|
747
|
+
removeDropListener?.();
|
|
748
|
+
removeDropListener = null;
|
|
749
|
+
|
|
630
750
|
for (const d of disposables) d.dispose();
|
|
631
751
|
disposables.length = 0;
|
|
632
752
|
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,14 @@ export default class MarkdownPlugin extends AdminForthPlugin {
|
|
|
17
17
|
instanceUniqueRepresentation(pluginOptions) {
|
|
18
18
|
return pluginOptions.fieldName;
|
|
19
19
|
}
|
|
20
|
+
// Placeholder for future Upload Plugin API integration.
|
|
21
|
+
// For now, treat all extracted URLs as plugin-owned public URLs.
|
|
22
|
+
isPluginPublicUrl(_url) {
|
|
23
|
+
// todo: here we need to check that host name is same as upload plugin, probably create upload plugin endpoint
|
|
24
|
+
// should handle cases that user might define custom preview url
|
|
25
|
+
// and that local storage has no host name, here, the fact of luck of hostname might be used as
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
20
28
|
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
21
29
|
this.adminforth = adminforth;
|
|
22
30
|
const column = resourceConfig.columns.find(c => c.name === this.options.fieldName);
|
|
@@ -125,34 +133,80 @@ export default class MarkdownPlugin extends AdminForthPlugin {
|
|
|
125
133
|
return stripQueryAndHash(url).replace(/^https?:\/\/[^\/]+\/+/, '').replace(/^\/+/, '');
|
|
126
134
|
}
|
|
127
135
|
};
|
|
136
|
+
const shouldTrackUrl = (url) => {
|
|
137
|
+
try {
|
|
138
|
+
return this.isPluginPublicUrl(url);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error('Error checking URL ownership', url, err);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const getKeyFromTrackedUrl = (rawUrl) => {
|
|
146
|
+
const srcTrimmed = rawUrl.trim().replace(/^<|>$/g, '');
|
|
147
|
+
if (!srcTrimmed || srcTrimmed.startsWith('data:') || srcTrimmed.startsWith('javascript:')) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (!shouldTrackUrl(srcTrimmed)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const srcNoQuery = stripQueryAndHash(srcTrimmed);
|
|
154
|
+
const key = extractKeyFromUrl(srcNoQuery);
|
|
155
|
+
if (!key) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return key;
|
|
159
|
+
};
|
|
160
|
+
const upsertMeta = (byKey, key, next) => {
|
|
161
|
+
var _a, _b;
|
|
162
|
+
const existing = byKey.get(key);
|
|
163
|
+
if (!existing) {
|
|
164
|
+
byKey.set(key, {
|
|
165
|
+
key,
|
|
166
|
+
alt: (_a = next.alt) !== null && _a !== void 0 ? _a : null,
|
|
167
|
+
title: (_b = next.title) !== null && _b !== void 0 ? _b : null,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if ((existing.alt === null || existing.alt === '') && next.alt !== undefined && next.alt !== null) {
|
|
172
|
+
existing.alt = next.alt;
|
|
173
|
+
}
|
|
174
|
+
if ((existing.title === null || existing.title === '') && next.title !== undefined && next.title !== null) {
|
|
175
|
+
existing.title = next.title;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
128
178
|
function getAttachmentMetas(markdown) {
|
|
129
|
-
var _a, _b, _c;
|
|
179
|
+
var _a, _b, _c, _d, _e, _f;
|
|
130
180
|
if (!markdown) {
|
|
131
181
|
return [];
|
|
132
182
|
}
|
|
133
|
-
//
|
|
134
|
-
// We track external (http/https) and relative sources, but skip data: URLs.
|
|
183
|
+
// Markdown image syntax:  or  or 
|
|
135
184
|
const imageRegex = /!\[([^\]]*)\]\(\s*([^\s)]+)\s*(?:\s+(?:\"([^\"]*)\"|'([^']*)'))?\s*\)/g;
|
|
185
|
+
// HTML embedded media links.
|
|
186
|
+
const htmlSrcRegex = /<(?:source|video)\b[^>]*\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s"'=<>`]+))[^>]*>/gi;
|
|
136
187
|
const byKey = new Map();
|
|
137
188
|
for (const match of markdown.matchAll(imageRegex)) {
|
|
138
189
|
const altRaw = (_a = match[1]) !== null && _a !== void 0 ? _a : '';
|
|
139
190
|
const srcRaw = match[2];
|
|
140
191
|
const titleRaw = (_c = ((_b = match[3]) !== null && _b !== void 0 ? _b : match[4])) !== null && _c !== void 0 ? _c : null;
|
|
141
|
-
const
|
|
142
|
-
if (!srcTrimmed || srcTrimmed.startsWith('data:')) {
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
const srcNoQuery = stripQueryAndHash(srcTrimmed);
|
|
146
|
-
const key = extractKeyFromUrl(srcNoQuery);
|
|
192
|
+
const key = getKeyFromTrackedUrl(srcRaw);
|
|
147
193
|
if (!key) {
|
|
148
194
|
continue;
|
|
149
195
|
}
|
|
150
|
-
byKey
|
|
151
|
-
key,
|
|
196
|
+
upsertMeta(byKey, key, {
|
|
152
197
|
alt: altRaw,
|
|
153
198
|
title: titleRaw,
|
|
154
199
|
});
|
|
155
200
|
}
|
|
201
|
+
let srcMatch;
|
|
202
|
+
while ((srcMatch = htmlSrcRegex.exec(markdown)) !== null) {
|
|
203
|
+
const srcRaw = (_f = (_e = (_d = srcMatch[1]) !== null && _d !== void 0 ? _d : srcMatch[2]) !== null && _e !== void 0 ? _e : srcMatch[3]) !== null && _f !== void 0 ? _f : '';
|
|
204
|
+
const key = getKeyFromTrackedUrl(srcRaw);
|
|
205
|
+
if (!key) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
upsertMeta(byKey, key, {});
|
|
209
|
+
}
|
|
156
210
|
return [...byKey.values()];
|
|
157
211
|
}
|
|
158
212
|
const createAttachmentRecords = (adminforth, options, recordId, metas, adminUser) => __awaiter(this, void 0, void 0, function* () {
|
package/index.ts
CHANGED
|
@@ -17,6 +17,15 @@ export default class MarkdownPlugin extends AdminForthPlugin {
|
|
|
17
17
|
return pluginOptions.fieldName;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Placeholder for future Upload Plugin API integration.
|
|
21
|
+
// For now, treat all extracted URLs as plugin-owned public URLs.
|
|
22
|
+
isPluginPublicUrl(_url: string): boolean {
|
|
23
|
+
// todo: here we need to check that host name is same as upload plugin, probably create upload plugin endpoint
|
|
24
|
+
// should handle cases that user might define custom preview url
|
|
25
|
+
// and that local storage has no host name, here, the fact of luck of hostname might be used as
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
21
30
|
this.adminforth = adminforth;
|
|
22
31
|
const column = resourceConfig.columns.find(c => c.name === this.options.fieldName);
|
|
@@ -137,37 +146,91 @@ export default class MarkdownPlugin extends AdminForthPlugin {
|
|
|
137
146
|
}
|
|
138
147
|
};
|
|
139
148
|
|
|
149
|
+
const shouldTrackUrl = (url: string) => {
|
|
150
|
+
try {
|
|
151
|
+
return this.isPluginPublicUrl(url);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error('Error checking URL ownership', url, err);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const getKeyFromTrackedUrl = (rawUrl: string): string | null => {
|
|
159
|
+
const srcTrimmed = rawUrl.trim().replace(/^<|>$/g, '');
|
|
160
|
+
if (!srcTrimmed || srcTrimmed.startsWith('data:') || srcTrimmed.startsWith('javascript:')) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (!shouldTrackUrl(srcTrimmed)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const srcNoQuery = stripQueryAndHash(srcTrimmed);
|
|
167
|
+
const key = extractKeyFromUrl(srcNoQuery);
|
|
168
|
+
if (!key) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return key;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const upsertMeta = (
|
|
175
|
+
byKey: Map<string, AttachmentMeta>,
|
|
176
|
+
key: string,
|
|
177
|
+
next: { alt?: string | null; title?: string | null }
|
|
178
|
+
) => {
|
|
179
|
+
const existing = byKey.get(key);
|
|
180
|
+
if (!existing) {
|
|
181
|
+
byKey.set(key, {
|
|
182
|
+
key,
|
|
183
|
+
alt: next.alt ?? null,
|
|
184
|
+
title: next.title ?? null,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if ((existing.alt === null || existing.alt === '') && next.alt !== undefined && next.alt !== null) {
|
|
190
|
+
existing.alt = next.alt;
|
|
191
|
+
}
|
|
192
|
+
if ((existing.title === null || existing.title === '') && next.title !== undefined && next.title !== null) {
|
|
193
|
+
existing.title = next.title;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
140
197
|
function getAttachmentMetas(markdown: string): AttachmentMeta[] {
|
|
141
198
|
if (!markdown) {
|
|
142
199
|
return [];
|
|
143
200
|
}
|
|
144
201
|
|
|
145
|
-
//
|
|
146
|
-
// We track external (http/https) and relative sources, but skip data: URLs.
|
|
202
|
+
// Markdown image syntax:  or  or 
|
|
147
203
|
const imageRegex = /!\[([^\]]*)\]\(\s*([^\s)]+)\s*(?:\s+(?:\"([^\"]*)\"|'([^']*)'))?\s*\)/g;
|
|
148
204
|
|
|
205
|
+
// HTML embedded media links.
|
|
206
|
+
const htmlSrcRegex = /<(?:source|video)\b[^>]*\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s"'=<>`]+))[^>]*>/gi;
|
|
207
|
+
|
|
149
208
|
const byKey = new Map<string, AttachmentMeta>();
|
|
150
209
|
for (const match of markdown.matchAll(imageRegex)) {
|
|
151
210
|
const altRaw = match[1] ?? '';
|
|
152
211
|
const srcRaw = match[2];
|
|
153
212
|
const titleRaw = (match[3] ?? match[4]) ?? null;
|
|
154
213
|
|
|
155
|
-
const
|
|
156
|
-
if (!srcTrimmed || srcTrimmed.startsWith('data:')) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const srcNoQuery = stripQueryAndHash(srcTrimmed);
|
|
161
|
-
const key = extractKeyFromUrl(srcNoQuery);
|
|
214
|
+
const key = getKeyFromTrackedUrl(srcRaw);
|
|
162
215
|
if (!key) {
|
|
163
216
|
continue;
|
|
164
217
|
}
|
|
165
|
-
byKey
|
|
166
|
-
key,
|
|
218
|
+
upsertMeta(byKey, key, {
|
|
167
219
|
alt: altRaw,
|
|
168
220
|
title: titleRaw,
|
|
169
221
|
});
|
|
170
222
|
}
|
|
223
|
+
|
|
224
|
+
let srcMatch: RegExpExecArray | null;
|
|
225
|
+
while ((srcMatch = htmlSrcRegex.exec(markdown)) !== null) {
|
|
226
|
+
const srcRaw = srcMatch[1] ?? srcMatch[2] ?? srcMatch[3] ?? '';
|
|
227
|
+
const key = getKeyFromTrackedUrl(srcRaw);
|
|
228
|
+
if (!key) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
upsertMeta(byKey, key, {});
|
|
232
|
+
}
|
|
233
|
+
|
|
171
234
|
return [...byKey.values()];
|
|
172
235
|
}
|
|
173
236
|
|