@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
+ '"': "&quot;",
12
+ "&": "&amp;",
13
+ "<": "&lt;",
14
+ ">": "&gt;"
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 ? normalizeHtmlText(html) : html ?? "";
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 normalizeHtmlText(html) {
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;AAG5C,UAAU,gBAAiB,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC/D,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAS,EACT,QAAQ,EACR,SAAc,EACd,GAAG,KAAK,EACT,EAAE,gBAAgB,2CAalB"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yimingliao/cms",
3
- "version": "0.0.251",
3
+ "version": "0.0.252",
4
4
  "author": "Yiming Liao",
5
5
  "license": "MIT",
6
6
  "type": "module",