@yimingliao/cms 0.0.251 → 0.0.252
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.
|
@@ -5,18 +5,27 @@ import 'mime-types';
|
|
|
5
5
|
import { cn } from '../../../../applications/cms/shadcn/utils.js';
|
|
6
6
|
import 'next/navigation';
|
|
7
7
|
|
|
8
|
+
const FIGURE_OEMBED_PATTERN = /<figure\b[^>]*\bclass=(["'])[^"']*\bmedia\b[^"']*\1[^>]*>\s*<oembed\b[^>]*\burl=(["'])(.*?)\2[^>]*>\s*<\/oembed>\s*<\/figure>/gi;
|
|
9
|
+
const YOUTUBE_VIDEO_ID_PATTERN = /^[\w-]{11}$/;
|
|
10
|
+
const HTML_ESCAPE_MAP = {
|
|
11
|
+
'"': """,
|
|
12
|
+
"&": "&",
|
|
13
|
+
"<": "<",
|
|
14
|
+
">": ">"
|
|
15
|
+
};
|
|
8
16
|
function HtmlDisplay({
|
|
9
17
|
html = "",
|
|
10
|
-
textOnly,
|
|
18
|
+
textOnly = false,
|
|
11
19
|
className = "",
|
|
12
20
|
...props
|
|
13
21
|
}) {
|
|
14
|
-
const content = textOnly ?
|
|
22
|
+
const content = textOnly ? toPlainText(html) : normalizeHtml(html ?? "");
|
|
15
23
|
return /* @__PURE__ */ jsx(
|
|
16
24
|
"div",
|
|
17
25
|
{
|
|
18
26
|
className: cn(
|
|
19
27
|
className,
|
|
28
|
+
"w-full",
|
|
20
29
|
"ck ck-content"
|
|
21
30
|
// CKEditor styles prefix: .ck (ckeditor.css)
|
|
22
31
|
),
|
|
@@ -25,9 +34,94 @@ function HtmlDisplay({
|
|
|
25
34
|
}
|
|
26
35
|
);
|
|
27
36
|
}
|
|
28
|
-
function
|
|
37
|
+
function toPlainText(html) {
|
|
29
38
|
if (!html) return "";
|
|
30
|
-
return html.replace(/<\/(p|div|li|h[1-6]|tr|td)>/gi, " ").replace(/<br\s*\/?>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\r?\n|\r/g, " ").replace(/\s{2,}/g, " ").trim();
|
|
39
|
+
return removeMediaContent(html).replace(/<\/(p|div|li|h[1-6]|tr|td)>/gi, " ").replace(/<br\s*\/?>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\r?\n|\r/g, " ").replace(/\s{2,}/g, " ").replace(/\s+([,。!?;:、])/g, "$1").trim();
|
|
40
|
+
}
|
|
41
|
+
function normalizeHtml(html) {
|
|
42
|
+
if (!html) return "";
|
|
43
|
+
return html.replace(
|
|
44
|
+
FIGURE_OEMBED_PATTERN,
|
|
45
|
+
(figureHtml, _classQuote, _urlQuote, rawUrl) => {
|
|
46
|
+
const embedHtml = createEmbedHtml(rawUrl);
|
|
47
|
+
return embedHtml ?? createEmbedFallbackHtml(rawUrl, figureHtml);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
function removeMediaContent(html) {
|
|
52
|
+
return html.replace(FIGURE_OEMBED_PATTERN, " ").replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, " ").replace(/<video\b[^>]*>[\s\S]*?<\/video>/gi, " ").replace(/<picture\b[^>]*>[\s\S]*?<\/picture>/gi, " ").replace(/<img\b[^>]*>/gi, " ").replace(/<source\b[^>]*>/gi, " ");
|
|
53
|
+
}
|
|
54
|
+
function createEmbedHtml(rawUrl) {
|
|
55
|
+
const embedUrl = getYouTubeEmbedUrl(rawUrl);
|
|
56
|
+
if (!embedUrl) return null;
|
|
57
|
+
return `<iframe
|
|
58
|
+
src="${escapeHtmlAttribute(embedUrl)}"
|
|
59
|
+
title="Embedded media"
|
|
60
|
+
frameborder="0"
|
|
61
|
+
loading="lazy"
|
|
62
|
+
referrerpolicy="strict-origin-when-cross-origin"
|
|
63
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
64
|
+
allowfullscreen
|
|
65
|
+
class="media"
|
|
66
|
+
style="display: block; width: 100%; aspect-ratio: 16 / 9; border: 0;"
|
|
67
|
+
></iframe>`.trim();
|
|
68
|
+
}
|
|
69
|
+
function createEmbedFallbackHtml(rawUrl, fallbackHtml) {
|
|
70
|
+
const safeUrl = getSafeExternalUrl(rawUrl);
|
|
71
|
+
if (!safeUrl) return fallbackHtml;
|
|
72
|
+
return `<figure class="media">
|
|
73
|
+
<a
|
|
74
|
+
href="${escapeHtmlAttribute(safeUrl)}"
|
|
75
|
+
target="_blank"
|
|
76
|
+
rel="noreferrer noopener"
|
|
77
|
+
>
|
|
78
|
+
${escapeHtmlText(safeUrl)}
|
|
79
|
+
</a>
|
|
80
|
+
</figure>`.trim();
|
|
81
|
+
}
|
|
82
|
+
function getYouTubeEmbedUrl(rawUrl) {
|
|
83
|
+
const url = safeParseUrl(rawUrl);
|
|
84
|
+
if (!url) return null;
|
|
85
|
+
const hostname = url.hostname.toLowerCase().replace(/^www\./, "");
|
|
86
|
+
let videoId = "";
|
|
87
|
+
if (hostname === "youtu.be") {
|
|
88
|
+
videoId = url.pathname.split("/").filter(Boolean)[0] ?? "";
|
|
89
|
+
} else if (hostname === "youtube.com" || hostname === "m.youtube.com") {
|
|
90
|
+
if (url.pathname === "/watch") {
|
|
91
|
+
videoId = url.searchParams.get("v") ?? "";
|
|
92
|
+
} else if (url.pathname.startsWith("/embed/")) {
|
|
93
|
+
videoId = url.pathname.split("/")[2] ?? "";
|
|
94
|
+
} else if (url.pathname.startsWith("/shorts/")) {
|
|
95
|
+
videoId = url.pathname.split("/")[2] ?? "";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!YOUTUBE_VIDEO_ID_PATTERN.test(videoId)) return null;
|
|
99
|
+
return `https://www.youtube-nocookie.com/embed/${videoId}`;
|
|
100
|
+
}
|
|
101
|
+
function getSafeExternalUrl(rawUrl) {
|
|
102
|
+
const url = safeParseUrl(rawUrl);
|
|
103
|
+
if (!url) return null;
|
|
104
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
105
|
+
return url.toString();
|
|
106
|
+
}
|
|
107
|
+
function safeParseUrl(rawUrl) {
|
|
108
|
+
try {
|
|
109
|
+
return new URL(rawUrl);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function escapeHtmlAttribute(value) {
|
|
115
|
+
return value.replaceAll(
|
|
116
|
+
/["&<>]/g,
|
|
117
|
+
(character) => HTML_ESCAPE_MAP[character] ?? character
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
function escapeHtmlText(value) {
|
|
121
|
+
return value.replaceAll(
|
|
122
|
+
/[&<>]/g,
|
|
123
|
+
(character) => HTML_ESCAPE_MAP[character] ?? character
|
|
124
|
+
);
|
|
31
125
|
}
|
|
32
126
|
|
|
33
127
|
export { HtmlDisplay };
|
|
@@ -3,6 +3,9 @@ interface HtmlDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
3
3
|
html?: string | null;
|
|
4
4
|
textOnly?: boolean;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Render CMS HTML content with CKEditor-compatible media transforms.
|
|
8
|
+
*/
|
|
6
9
|
export declare function HtmlDisplay({ html, textOnly, className, ...props }: HtmlDisplayProps): import("react/jsx-runtime").JSX.Element;
|
|
7
10
|
export {};
|
|
8
11
|
//# sourceMappingURL=html-display.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-display.d.ts","sourceRoot":"","sources":["../../../../../../../../src/client/interfaces/components/ui/display/html-display.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"html-display.d.ts","sourceRoot":"","sources":["../../../../../../../../src/client/interfaces/components/ui/display/html-display.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAa5C,UAAU,gBAAiB,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC/D,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAC1B,IAAS,EACT,QAAgB,EAChB,SAAc,EACd,GAAG,KAAK,EACT,EAAE,gBAAgB,2CAclB"}
|