@haklex/rich-ext-embed 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Innei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
24
+ Additional Terms and Conditions
25
+
26
+ ----------------
27
+
28
+ Use of this software is governed by the terms of MIT and, in addition, by the terms and conditions described in the additional file (ADDITIONAL_TERMS.md). By using this software, you agree to abide by these additional terms and conditions.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @haklex/rich-ext-embed
2
+
3
+ 嵌入内容扩展(Twitter、YouTube、Bilibili 等)。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @haklex/rich-ext-embed @haklex/rich-editor
9
+ ```
10
+
11
+ ## 导出
12
+
13
+ ```ts
14
+ // 节点
15
+ export { EmbedNode } from './nodes/EmbedNode'
16
+ export { $createEmbedNode, $isEmbedNode } from './nodes/EmbedNode'
17
+ export type { SerializedEmbedNode } from './nodes/EmbedNode'
18
+
19
+ export { EmbedEditNode } from './nodes/EmbedEditNode'
20
+ export { $createEmbedEditNode, $isEmbedEditNode } from './nodes/EmbedEditNode'
21
+
22
+ export { embedNodes, embedEditNodes } from './nodes'
23
+
24
+ // 插件
25
+ export { EmbedPlugin, INSERT_EMBED_COMMAND } from './EmbedPlugin'
26
+ export type { EmbedPluginProps } from './EmbedPlugin'
27
+
28
+ // 渲染器
29
+ export { EmbedStaticRenderer } from './renderers/EmbedStaticRenderer'
30
+ export type { EmbedStaticRendererProps } from './renderers/EmbedStaticRenderer'
31
+ export { EmbedLinkRenderer } from './renderers/EmbedLinkRenderer'
32
+ export type { EmbedLinkRendererProps } from './renderers/EmbedLinkRenderer'
33
+
34
+ // 上下文
35
+ export { EmbedRendererProvider, useEmbedRenderers } from './context/EmbedRendererContext'
36
+ export type { EmbedRendererComponent, EmbedRendererMap } from './context/EmbedRendererContext'
37
+
38
+ // URL 匹配器
39
+ export type { EmbedType } from './url-matchers'
40
+ export {
41
+ matchEmbedUrl, isTweetUrl, isYoutubeUrl,
42
+ isBilibiliVideoUrl, isGistUrl, isCodesandboxUrl,
43
+ isGithubFilePreviewUrl, createSelfThinkingMatcher
44
+ } from './url-matchers'
45
+ ```
46
+
47
+ ## 使用
48
+
49
+ ```tsx
50
+ import { EmbedPlugin, embedEditNodes } from '@haklex/rich-ext-embed'
51
+ import { RichEditor } from '@haklex/rich-editor'
52
+
53
+ <RichEditor extraNodes={[...embedEditNodes]}>
54
+ <EmbedPlugin />
55
+ </RichEditor>
56
+ ```
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,6 @@
1
+ export declare const INSERT_EMBED_COMMAND: import('lexical').LexicalCommand<string>;
2
+ export interface EmbedPluginProps {
3
+ selfHostnames?: string[];
4
+ }
5
+ export declare function EmbedPlugin({ selfHostnames }: EmbedPluginProps): null;
6
+ //# sourceMappingURL=EmbedPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmbedPlugin.d.ts","sourceRoot":"","sources":["../src/EmbedPlugin.tsx"],"names":[],"mappings":"AAYA,eAAO,MAAM,oBAAoB,0CAAwC,CAAA;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;CACzB;AAED,wBAAgB,WAAW,CAAC,EAAE,aAAa,EAAE,EAAE,gBAAgB,QA6D9D"}
@@ -0,0 +1,9 @@
1
+ import { ComponentType } from 'react';
2
+ export type EmbedRendererComponent = ComponentType<{
3
+ url: string;
4
+ type: string | null;
5
+ }>;
6
+ export type EmbedRendererMap = Partial<Record<string, EmbedRendererComponent>>;
7
+ export declare const EmbedRendererProvider: import('react').Provider<Partial<Record<string, EmbedRendererComponent>>>;
8
+ export declare function useEmbedRenderers(): EmbedRendererMap;
9
+ //# sourceMappingURL=EmbedRendererContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmbedRendererContext.d.ts","sourceRoot":"","sources":["../../src/context/EmbedRendererContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAG1C,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB,CAAC,CAAA;AAEF,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC,CAAA;AAI9E,eAAO,MAAM,qBAAqB,2EAA4B,CAAA;AAE9D,wBAAgB,iBAAiB,IAAI,gBAAgB,CAEpD"}
@@ -0,0 +1,15 @@
1
+ export type { SerializedEmbedNode } from './nodes/EmbedNode';
2
+ export { $createEmbedNode, $isEmbedNode, EmbedNode } from './nodes/EmbedNode';
3
+ export { $createEmbedEditNode, $isEmbedEditNode, EmbedEditNode, } from './nodes/EmbedEditNode';
4
+ export { embedEditNodes, embedNodes } from './nodes';
5
+ export type { EmbedPluginProps } from './EmbedPlugin';
6
+ export { EmbedPlugin, INSERT_EMBED_COMMAND } from './EmbedPlugin';
7
+ export type { EmbedLinkRendererProps } from './renderers/EmbedLinkRenderer';
8
+ export { EmbedLinkRenderer } from './renderers/EmbedLinkRenderer';
9
+ export type { EmbedStaticRendererProps } from './renderers/EmbedStaticRenderer';
10
+ export { EmbedStaticRenderer } from './renderers/EmbedStaticRenderer';
11
+ export type { EmbedRendererComponent, EmbedRendererMap, } from './context/EmbedRendererContext';
12
+ export { EmbedRendererProvider, useEmbedRenderers, } from './context/EmbedRendererContext';
13
+ export type { EmbedType } from './url-matchers';
14
+ export { createSelfThinkingMatcher, isBilibiliVideoUrl, isCodesandboxUrl, isGistUrl, isGithubFilePreviewUrl, isTweetUrl, isYoutubeUrl, matchEmbedUrl, } from './url-matchers';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAG7E,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,GACd,MAAM,uBAAuB,CAAA;AAG9B,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGpD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AAGjE,YAAY,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,YAAY,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAA;AAC/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAGrE,YAAY,EACV,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,gCAAgC,CAAA;AAGvC,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EACL,yBAAyB,EACzB,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EACT,sBAAsB,EACtB,UAAU,EACV,YAAY,EACZ,aAAa,GACd,MAAM,gBAAgB,CAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,663 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { DecoratorNode, $getNodeByKey, $insertNodes, createCommand, PASTE_COMMAND, COMMAND_PRIORITY_LOW } from "lexical";
5
+ import { createContext, use, lazy, useState, useMemo, useEffect, Suspense, Component, createElement, useRef, useCallback } from "react";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { useColorScheme } from "@haklex/rich-editor";
8
+ import { Github, ExternalLink, Trash2, Code } from "lucide-react";
9
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
10
+ const EmbedRendererCtx = createContext({});
11
+ const EmbedRendererProvider = EmbedRendererCtx.Provider;
12
+ function useEmbedRenderers() {
13
+ return use(EmbedRendererCtx);
14
+ }
15
+ const extToLang = {
16
+ ".js": "javascript",
17
+ ".ts": "typescript",
18
+ ".mjs": "javascript",
19
+ ".cjs": "javascript",
20
+ ".mts": "typescript",
21
+ ".cts": "typescript",
22
+ ".jsx": "jsx",
23
+ ".tsx": "tsx",
24
+ ".md": "markdown",
25
+ ".css": "css",
26
+ ".scss": "scss",
27
+ ".html": "html",
28
+ ".json": "json",
29
+ ".yml": "yaml",
30
+ ".yaml": "yaml",
31
+ ".toml": "toml",
32
+ ".xml": "xml",
33
+ ".sh": "bash",
34
+ ".bash": "bash",
35
+ ".zsh": "bash",
36
+ ".go": "go",
37
+ ".py": "python",
38
+ ".rb": "ruby",
39
+ ".java": "java",
40
+ ".c": "c",
41
+ ".cpp": "cpp",
42
+ ".cs": "csharp",
43
+ ".rs": "rust",
44
+ ".swift": "swift",
45
+ ".kt": "kotlin",
46
+ ".lua": "lua",
47
+ ".sql": "sql",
48
+ ".graphql": "graphql",
49
+ ".scala": "scala",
50
+ ".dart": "dart",
51
+ ".vue": "vue",
52
+ ".h": "c",
53
+ ".hpp": "cpp",
54
+ ".m": "objectivec",
55
+ ".ex": "elixir",
56
+ ".r": "r"
57
+ };
58
+ function getLangFromPath(path) {
59
+ const dot = path.lastIndexOf(".");
60
+ if (dot === -1) return "text";
61
+ return extToLang[path.slice(dot).toLowerCase()] || "text";
62
+ }
63
+ const shikiPromise = import("shiki/bundle/web").catch(() => null);
64
+ const typeLabels$1 = {
65
+ tweet: "X / Twitter",
66
+ youtube: "YouTube",
67
+ codesandbox: "CodeSandbox",
68
+ bilibili: "Bilibili",
69
+ "github-file": "GitHub File",
70
+ "github-gist": "GitHub Gist",
71
+ thinking: "Thinking"
72
+ };
73
+ const LazyTweet = lazy(
74
+ () => import("react-tweet").then((mod) => ({ default: mod.Tweet }))
75
+ );
76
+ class EmbedErrorBoundary extends Component {
77
+ constructor() {
78
+ super(...arguments);
79
+ __publicField(this, "state", { hasError: false });
80
+ }
81
+ static getDerivedStateFromError() {
82
+ return { hasError: true };
83
+ }
84
+ render() {
85
+ return this.state.hasError ? this.props.fallback : this.props.children;
86
+ }
87
+ }
88
+ function GitHubSvg() {
89
+ return /* @__PURE__ */ jsx(Github, { size: 16 });
90
+ }
91
+ function FixedRatioContainer({
92
+ children,
93
+ ratio = 58
94
+ }) {
95
+ return /* @__PURE__ */ jsx("div", { className: "embed-static-ratio-container", children: /* @__PURE__ */ jsx(
96
+ "div",
97
+ {
98
+ className: "embed-static-ratio-inner",
99
+ style: { paddingBottom: `${ratio}%` },
100
+ children
101
+ }
102
+ ) });
103
+ }
104
+ function FallbackLink({ url, type }) {
105
+ const label = type ? typeLabels$1[type] : "Embed";
106
+ const cssModifier = type || "generic";
107
+ return /* @__PURE__ */ jsxs(
108
+ "div",
109
+ {
110
+ className: `embed-static-fallback embed-static-fallback--${cssModifier}`,
111
+ children: [
112
+ /* @__PURE__ */ jsxs("span", { className: "embed-static-fallback__badge", children: [
113
+ /* @__PURE__ */ jsx("span", { className: "embed-static-fallback__dot" }),
114
+ label
115
+ ] }),
116
+ /* @__PURE__ */ jsx(
117
+ "a",
118
+ {
119
+ className: "embed-static-fallback__link",
120
+ href: url,
121
+ target: "_blank",
122
+ rel: "noreferrer",
123
+ children: url
124
+ }
125
+ )
126
+ ]
127
+ }
128
+ );
129
+ }
130
+ function TweetRenderer({ url }) {
131
+ let parsedUrl = null;
132
+ try {
133
+ parsedUrl = new URL(url);
134
+ } catch {
135
+ return /* @__PURE__ */ jsx(FallbackLink, { url, type: "tweet" });
136
+ }
137
+ const id = parsedUrl.pathname.split("/").pop();
138
+ if (!id) return /* @__PURE__ */ jsx(FallbackLink, { url, type: "tweet" });
139
+ const fallback = /* @__PURE__ */ jsx(FallbackLink, { url, type: "tweet" });
140
+ return /* @__PURE__ */ jsx(EmbedErrorBoundary, { fallback, children: /* @__PURE__ */ jsx("div", { className: "embed-static-tweet", children: /* @__PURE__ */ jsx(
141
+ Suspense,
142
+ {
143
+ fallback: /* @__PURE__ */ jsx("div", { className: "embed-static-loading", children: "Loading tweet..." }),
144
+ children: /* @__PURE__ */ jsx(LazyTweet, { id })
145
+ }
146
+ ) }) });
147
+ }
148
+ function GithubFileEmbed({ url, href }) {
149
+ const colorScheme = useColorScheme();
150
+ const shikiTheme = colorScheme === "dark" ? "github-dark" : "github-light";
151
+ const split = url.pathname.split("/");
152
+ const [, owner, repo, , ...rest] = split;
153
+ const ref = rest[0];
154
+ const path = ref ? rest.slice(1).join("/") : rest.join("/");
155
+ const matchResult = url.hash.match(/L\d+/g);
156
+ let startLine = 0;
157
+ let endLine;
158
+ if (matchResult) {
159
+ if (matchResult.length === 1) {
160
+ startLine = Number.parseInt(matchResult[0].slice(1)) - 1;
161
+ endLine = startLine + 1;
162
+ } else {
163
+ startLine = Number.parseInt(matchResult[0].slice(1)) - 1;
164
+ endLine = Number.parseInt(matchResult[1].slice(1));
165
+ }
166
+ }
167
+ const [content, setContent] = useState(null);
168
+ const [highlightedHtml, setHighlightedHtml] = useState(null);
169
+ const [loading, setLoading] = useState(true);
170
+ const [error, setError] = useState(false);
171
+ const lang = useMemo(() => getLangFromPath(path), [path]);
172
+ useEffect(() => {
173
+ let cancelled = false;
174
+ setContent(null);
175
+ setHighlightedHtml(null);
176
+ setLoading(true);
177
+ setError(false);
178
+ const fetchPromise = fetch(
179
+ `https://cdn.jsdelivr.net/gh/${owner}/${repo}${ref ? `@${ref}` : ""}/${path}`
180
+ ).then((res) => {
181
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
182
+ return res.text();
183
+ });
184
+ Promise.all([fetchPromise, shikiPromise]).then(([text, shikiMod]) => {
185
+ if (cancelled) return;
186
+ setContent(text);
187
+ const lines2 = text.split("\n");
188
+ const end2 = endLine ?? lines2.length;
189
+ const displayCode = lines2.slice(startLine, end2).join("\n");
190
+ if (!shikiMod) {
191
+ setLoading(false);
192
+ return;
193
+ }
194
+ shikiMod.codeToHtml(displayCode, {
195
+ lang,
196
+ theme: shikiTheme
197
+ }).then((html) => {
198
+ if (!cancelled) {
199
+ setHighlightedHtml(html);
200
+ setLoading(false);
201
+ }
202
+ }).catch(() => {
203
+ if (!cancelled) setLoading(false);
204
+ });
205
+ }).catch(() => {
206
+ if (!cancelled) {
207
+ setError(true);
208
+ setLoading(false);
209
+ }
210
+ });
211
+ return () => {
212
+ cancelled = true;
213
+ };
214
+ }, [owner, repo, ref, path, lang, startLine, endLine, shikiTheme]);
215
+ if (loading) {
216
+ return /* @__PURE__ */ jsx("div", { className: "embed-static-loading", children: "Loading GitHub File Preview..." });
217
+ }
218
+ if (error || content === null) {
219
+ return /* @__PURE__ */ jsx(FallbackLink, { url: href, type: "github-file" });
220
+ }
221
+ const lines = content.split("\n");
222
+ const end = endLine ?? lines.length;
223
+ const isLong = end - startLine > 20;
224
+ return /* @__PURE__ */ jsxs("div", { className: "embed-static-github-file", children: [
225
+ /* @__PURE__ */ jsx(
226
+ "div",
227
+ {
228
+ className: `embed-static-github-file__code${isLong ? " embed-static-github-file__code--long" : ""}`,
229
+ children: highlightedHtml ? /* @__PURE__ */ jsx(
230
+ "div",
231
+ {
232
+ className: "embed-static-shiki",
233
+ style: { "--start-line": startLine },
234
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
235
+ }
236
+ ) : /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: lines.slice(startLine, end).map((line, i) => /* @__PURE__ */ jsxs("span", { className: "embed-static-github-file__line", children: [
237
+ /* @__PURE__ */ jsx("span", { className: "embed-static-github-file__line-num", children: startLine + i + 1 }),
238
+ line,
239
+ "\n"
240
+ ] }, i)) }) })
241
+ }
242
+ ),
243
+ /* @__PURE__ */ jsxs(
244
+ "a",
245
+ {
246
+ className: "embed-static-source-link",
247
+ href,
248
+ target: "_blank",
249
+ rel: "noreferrer",
250
+ children: [
251
+ /* @__PURE__ */ jsx(GitHubSvg, {}),
252
+ /* @__PURE__ */ jsx("span", { children: href })
253
+ ]
254
+ }
255
+ )
256
+ ] });
257
+ }
258
+ function EmbedStaticRenderer({ type, url }) {
259
+ const customRenderers = useEmbedRenderers();
260
+ if (!url) return null;
261
+ if (type && customRenderers[type]) {
262
+ const Custom = customRenderers[type];
263
+ return /* @__PURE__ */ jsx(Custom, { url, type });
264
+ }
265
+ let parsedUrl = null;
266
+ try {
267
+ parsedUrl = new URL(url);
268
+ } catch {
269
+ return /* @__PURE__ */ jsx(FallbackLink, { url, type });
270
+ }
271
+ switch (type) {
272
+ case "tweet": {
273
+ return /* @__PURE__ */ jsx(TweetRenderer, { url });
274
+ }
275
+ case "youtube": {
276
+ const id = parsedUrl.searchParams.get("v");
277
+ if (!id) return /* @__PURE__ */ jsx(FallbackLink, { url, type });
278
+ return /* @__PURE__ */ jsx(FixedRatioContainer, { children: /* @__PURE__ */ jsx(
279
+ "iframe",
280
+ {
281
+ src: `https://www.youtube.com/embed/${id}`,
282
+ className: "embed-static-iframe",
283
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
284
+ allowFullScreen: true,
285
+ title: "YouTube video player"
286
+ }
287
+ ) });
288
+ }
289
+ case "bilibili": {
290
+ const id = parsedUrl.pathname.split("/")[2];
291
+ if (!id) return /* @__PURE__ */ jsx(FallbackLink, { url, type });
292
+ return /* @__PURE__ */ jsx(FixedRatioContainer, { children: /* @__PURE__ */ jsx(
293
+ "iframe",
294
+ {
295
+ src: `//player.bilibili.com/player.html?bvid=${id}&autoplay=0`,
296
+ scrolling: "no",
297
+ className: "embed-static-iframe",
298
+ allowFullScreen: true
299
+ }
300
+ ) });
301
+ }
302
+ case "codesandbox": {
303
+ const path = parsedUrl.pathname.slice(2);
304
+ return /* @__PURE__ */ jsx(FixedRatioContainer, { children: /* @__PURE__ */ jsx(
305
+ "iframe",
306
+ {
307
+ className: "embed-static-iframe",
308
+ src: `https://codesandbox.io/embed/${path}?fontsize=14&hidenavigation=1&theme=dark${parsedUrl.search}`
309
+ }
310
+ ) });
311
+ }
312
+ case "github-gist": {
313
+ const [, owner, id] = parsedUrl.pathname.split("/");
314
+ if (!owner || !id) return /* @__PURE__ */ jsx(FallbackLink, { url, type });
315
+ return /* @__PURE__ */ jsxs("div", { className: "embed-static-gist", children: [
316
+ /* @__PURE__ */ jsx(
317
+ "iframe",
318
+ {
319
+ src: `https://gist.github.com/${owner}/${id}.pibb`,
320
+ className: "embed-static-gist__iframe"
321
+ }
322
+ ),
323
+ /* @__PURE__ */ jsxs(
324
+ "a",
325
+ {
326
+ className: "embed-static-source-link",
327
+ href: url,
328
+ target: "_blank",
329
+ rel: "noreferrer",
330
+ children: [
331
+ /* @__PURE__ */ jsx(GitHubSvg, {}),
332
+ /* @__PURE__ */ jsx("span", { children: url })
333
+ ]
334
+ }
335
+ )
336
+ ] });
337
+ }
338
+ case "github-file": {
339
+ return /* @__PURE__ */ jsx(GithubFileEmbed, { url: parsedUrl, href: url });
340
+ }
341
+ default:
342
+ return /* @__PURE__ */ jsx(FallbackLink, { url, type });
343
+ }
344
+ }
345
+ class EmbedNode extends DecoratorNode {
346
+ constructor(url, source, key) {
347
+ super(key);
348
+ __publicField(this, "__url");
349
+ __publicField(this, "__source");
350
+ this.__url = url;
351
+ this.__source = source;
352
+ }
353
+ static getType() {
354
+ return "embed";
355
+ }
356
+ static clone(node) {
357
+ return new EmbedNode(node.__url, node.__source, node.__key);
358
+ }
359
+ createDOM(_config) {
360
+ const div = document.createElement("div");
361
+ div.className = "rich-embed-link-wrapper";
362
+ return div;
363
+ }
364
+ updateDOM() {
365
+ return false;
366
+ }
367
+ isInline() {
368
+ return false;
369
+ }
370
+ static importJSON(serializedNode) {
371
+ return $createEmbedNode(serializedNode.url, serializedNode.source);
372
+ }
373
+ exportJSON() {
374
+ return {
375
+ ...super.exportJSON(),
376
+ type: "embed",
377
+ url: this.__url,
378
+ source: this.__source,
379
+ version: 1
380
+ };
381
+ }
382
+ getUrl() {
383
+ return this.getLatest().__url;
384
+ }
385
+ setUrl(url) {
386
+ const writable = this.getWritable();
387
+ writable.__url = url;
388
+ }
389
+ getSource() {
390
+ return this.getLatest().__source;
391
+ }
392
+ setSource(source) {
393
+ const writable = this.getWritable();
394
+ writable.__source = source;
395
+ }
396
+ decorate(_editor, _config) {
397
+ return createElement(EmbedStaticRenderer, {
398
+ type: this.__source,
399
+ url: this.__url
400
+ });
401
+ }
402
+ }
403
+ function $createEmbedNode(url, source) {
404
+ return new EmbedNode(url, source);
405
+ }
406
+ function $isEmbedNode(node) {
407
+ return node instanceof EmbedNode;
408
+ }
409
+ const isTweetUrl = (url) => (url.hostname === "twitter.com" || url.hostname === "x.com") && url.pathname.startsWith("/");
410
+ const isYoutubeUrl = (url) => url.hostname === "www.youtube.com" && url.pathname.startsWith("/watch");
411
+ const isCodesandboxUrl = (url) => url.hostname === "codesandbox.io" && url.pathname.split("/").length === 3;
412
+ const isBilibiliVideoUrl = (url) => url.hostname.includes("bilibili.com") && url.pathname.startsWith("/video/BV");
413
+ const isGithubFilePreviewUrl = (url) => {
414
+ const [, , , type] = url.pathname.split("/");
415
+ return url.hostname === "github.com" && type === "blob";
416
+ };
417
+ const isGistUrl = (url) => url.hostname === "gist.github.com";
418
+ function createSelfThinkingMatcher(hostnames) {
419
+ return (url) => hostnames.includes(url.hostname) && url.pathname.startsWith("/thinking/");
420
+ }
421
+ function matchEmbedUrl(url, selfThinkingMatcher) {
422
+ if (isTweetUrl(url)) return "tweet";
423
+ if (isYoutubeUrl(url)) return "youtube";
424
+ if (isCodesandboxUrl(url)) return "codesandbox";
425
+ if (isBilibiliVideoUrl(url)) return "bilibili";
426
+ if (isGithubFilePreviewUrl(url)) return "github-file";
427
+ if (isGistUrl(url)) return "github-gist";
428
+ if (selfThinkingMatcher?.(url)) return "thinking";
429
+ return null;
430
+ }
431
+ const typeLabels = {
432
+ tweet: "X / Twitter",
433
+ youtube: "YouTube",
434
+ codesandbox: "CodeSandbox",
435
+ bilibili: "Bilibili",
436
+ "github-file": "GitHub File",
437
+ "github-gist": "GitHub Gist",
438
+ thinking: "Thinking"
439
+ };
440
+ function EmbedLinkRenderer({
441
+ type,
442
+ url,
443
+ nodeKey
444
+ }) {
445
+ const [editor] = useLexicalComposerContext();
446
+ const [editUrl, setEditUrl] = useState(url);
447
+ const inputRef = useRef(null);
448
+ useEffect(() => {
449
+ setEditUrl(url);
450
+ }, [url]);
451
+ useEffect(() => {
452
+ if (!url) {
453
+ inputRef.current?.focus();
454
+ }
455
+ }, [url]);
456
+ const commitUrl = useCallback(() => {
457
+ const trimmed = editUrl.trim();
458
+ if (!trimmed) return;
459
+ editor.update(() => {
460
+ const node = $getNodeByKey(nodeKey);
461
+ if (!node || !("setUrl" in node)) return;
462
+ const n = node;
463
+ const currentUrl = n.getUrl?.();
464
+ if (currentUrl !== trimmed) {
465
+ n.setUrl(trimmed);
466
+ }
467
+ if (n.setSource) {
468
+ try {
469
+ n.setSource(matchEmbedUrl(new URL(trimmed)));
470
+ } catch {
471
+ }
472
+ }
473
+ });
474
+ }, [editor, nodeKey, editUrl]);
475
+ const handleKeyDown = useCallback(
476
+ (e) => {
477
+ if (e.key === "Enter") {
478
+ e.preventDefault();
479
+ commitUrl();
480
+ inputRef.current?.blur();
481
+ } else if (e.key === "Escape") {
482
+ e.preventDefault();
483
+ setEditUrl(url);
484
+ inputRef.current?.blur();
485
+ }
486
+ },
487
+ [commitUrl, url]
488
+ );
489
+ const handleDelete = useCallback(() => {
490
+ editor.update(() => {
491
+ const node = $getNodeByKey(nodeKey);
492
+ if (node) node.remove();
493
+ });
494
+ }, [editor, nodeKey]);
495
+ const handleOpen = useCallback(() => {
496
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
497
+ }, [url]);
498
+ const label = type ? typeLabels[type] : "Embed";
499
+ const cssModifier = type || "generic";
500
+ return /* @__PURE__ */ jsxs("div", { className: `rich-embed rich-embed--${cssModifier}`, children: [
501
+ /* @__PURE__ */ jsxs("span", { className: "rich-embed__badge", children: [
502
+ /* @__PURE__ */ jsx("span", { className: `rich-embed__dot rich-embed__dot--${cssModifier}` }),
503
+ label
504
+ ] }),
505
+ /* @__PURE__ */ jsx("span", { className: "rich-embed__divider" }),
506
+ /* @__PURE__ */ jsx(
507
+ "input",
508
+ {
509
+ ref: inputRef,
510
+ className: "rich-embed__input",
511
+ type: "url",
512
+ value: editUrl,
513
+ onChange: (e) => setEditUrl(e.target.value),
514
+ onBlur: commitUrl,
515
+ onKeyDown: handleKeyDown,
516
+ placeholder: "https://..."
517
+ }
518
+ ),
519
+ /* @__PURE__ */ jsxs("span", { className: "rich-embed__actions", children: [
520
+ /* @__PURE__ */ jsx(
521
+ "button",
522
+ {
523
+ type: "button",
524
+ className: "rich-embed__action-btn",
525
+ onClick: handleOpen,
526
+ title: "Open in new tab",
527
+ disabled: !url,
528
+ children: /* @__PURE__ */ jsx(ExternalLink, {})
529
+ }
530
+ ),
531
+ /* @__PURE__ */ jsx(
532
+ "button",
533
+ {
534
+ type: "button",
535
+ className: "rich-embed__action-btn rich-embed__action-btn--danger",
536
+ onClick: handleDelete,
537
+ title: "Delete",
538
+ children: /* @__PURE__ */ jsx(Trash2, {})
539
+ }
540
+ )
541
+ ] })
542
+ ] });
543
+ }
544
+ const _EmbedEditNode = class _EmbedEditNode extends EmbedNode {
545
+ static clone(node) {
546
+ return new _EmbedEditNode(node.__url, node.__source, node.__key);
547
+ }
548
+ static importJSON(serializedNode) {
549
+ return new _EmbedEditNode(serializedNode.url, serializedNode.source);
550
+ }
551
+ decorate(_editor, _config) {
552
+ return createElement(EmbedLinkRenderer, {
553
+ type: this.__source,
554
+ url: this.__url,
555
+ nodeKey: this.__key
556
+ });
557
+ }
558
+ };
559
+ __publicField(_EmbedEditNode, "slashMenuItems", [
560
+ {
561
+ title: "Embed",
562
+ icon: createElement(Code, { size: 20 }),
563
+ description: "Embed external content (Tweet, YouTube, Bilibili, etc.)",
564
+ keywords: [
565
+ "embed",
566
+ "tweet",
567
+ "youtube",
568
+ "bilibili",
569
+ "codesandbox",
570
+ "thinking"
571
+ ],
572
+ section: "EMBED",
573
+ onSelect: (editor) => {
574
+ editor.update(() => {
575
+ $insertNodes([$createEmbedEditNode("", null)]);
576
+ });
577
+ }
578
+ }
579
+ ]);
580
+ let EmbedEditNode = _EmbedEditNode;
581
+ function $createEmbedEditNode(url, source) {
582
+ return new EmbedEditNode(url, source);
583
+ }
584
+ function $isEmbedEditNode(node) {
585
+ return node instanceof EmbedEditNode;
586
+ }
587
+ const embedNodes = [EmbedNode];
588
+ const embedEditNodes = [EmbedEditNode];
589
+ const INSERT_EMBED_COMMAND = createCommand("INSERT_EMBED");
590
+ function EmbedPlugin({ selfHostnames }) {
591
+ const [editor] = useLexicalComposerContext();
592
+ const thinkingMatcher = useMemo(
593
+ () => selfHostnames?.length ? createSelfThinkingMatcher(selfHostnames) : void 0,
594
+ [selfHostnames]
595
+ );
596
+ useEffect(() => {
597
+ const removePaste = editor.registerCommand(
598
+ PASTE_COMMAND,
599
+ (event) => {
600
+ const text = event.clipboardData?.getData("text/plain")?.trim();
601
+ if (!text) return false;
602
+ let url;
603
+ try {
604
+ url = new URL(text);
605
+ } catch {
606
+ return false;
607
+ }
608
+ const source = matchEmbedUrl(url, thinkingMatcher);
609
+ if (!source) return false;
610
+ event.preventDefault();
611
+ editor.update(() => {
612
+ $insertNodes([$createEmbedEditNode(text, source)]);
613
+ });
614
+ return true;
615
+ },
616
+ COMMAND_PRIORITY_LOW
617
+ );
618
+ const removeInsert = editor.registerCommand(
619
+ INSERT_EMBED_COMMAND,
620
+ (url) => {
621
+ let source = null;
622
+ try {
623
+ source = matchEmbedUrl(new URL(url));
624
+ } catch {
625
+ }
626
+ editor.update(() => {
627
+ $insertNodes([$createEmbedEditNode(url, source)]);
628
+ });
629
+ return true;
630
+ },
631
+ COMMAND_PRIORITY_LOW
632
+ );
633
+ return () => {
634
+ removePaste();
635
+ removeInsert();
636
+ };
637
+ }, [editor, thinkingMatcher]);
638
+ return null;
639
+ }
640
+ export {
641
+ $createEmbedEditNode,
642
+ $createEmbedNode,
643
+ $isEmbedEditNode,
644
+ $isEmbedNode,
645
+ EmbedEditNode,
646
+ EmbedLinkRenderer,
647
+ EmbedNode,
648
+ EmbedPlugin,
649
+ EmbedRendererProvider,
650
+ EmbedStaticRenderer,
651
+ INSERT_EMBED_COMMAND,
652
+ createSelfThinkingMatcher,
653
+ embedEditNodes,
654
+ embedNodes,
655
+ isBilibiliVideoUrl,
656
+ isCodesandboxUrl,
657
+ isGistUrl,
658
+ isGithubFilePreviewUrl,
659
+ isTweetUrl,
660
+ isYoutubeUrl,
661
+ matchEmbedUrl,
662
+ useEmbedRenderers
663
+ };
@@ -0,0 +1,14 @@
1
+ import { SlashMenuItemConfig } from '@haklex/rich-editor';
2
+ import { EditorConfig, LexicalEditor, LexicalNode } from 'lexical';
3
+ import { ReactElement } from 'react';
4
+ import { EmbedType } from '../url-matchers';
5
+ import { EmbedNode, SerializedEmbedNode } from './EmbedNode';
6
+ export declare class EmbedEditNode extends EmbedNode {
7
+ static slashMenuItems: SlashMenuItemConfig[];
8
+ static clone(node: EmbedEditNode): EmbedEditNode;
9
+ static importJSON(serializedNode: SerializedEmbedNode): EmbedEditNode;
10
+ decorate(_editor: LexicalEditor, _config: EditorConfig): ReactElement;
11
+ }
12
+ export declare function $createEmbedEditNode(url: string, source: EmbedType | null): EmbedEditNode;
13
+ export declare function $isEmbedEditNode(node: LexicalNode | null | undefined): node is EmbedEditNode;
14
+ //# sourceMappingURL=EmbedEditNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmbedEditNode.d.ts","sourceRoot":"","sources":["../../src/nodes/EmbedEditNode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAGvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAIzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEjE,qBAAa,aAAc,SAAQ,SAAS;IAC1C,MAAM,CAAC,cAAc,EAAE,mBAAmB,EAAE,CAoB3C;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa;IAIhD,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,mBAAmB,GAAG,aAAa;IAIrE,QAAQ,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY;CAOtE;AAED,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAAG,IAAI,GACvB,aAAa,CAEf;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GACnC,IAAI,IAAI,aAAa,CAEvB"}
@@ -0,0 +1,27 @@
1
+ import { EditorConfig, LexicalEditor, LexicalNode, NodeKey, SerializedLexicalNode, Spread, DecoratorNode } from 'lexical';
2
+ import { ReactElement } from 'react';
3
+ import { EmbedType } from '../url-matchers';
4
+ export type SerializedEmbedNode = Spread<{
5
+ url: string;
6
+ source: EmbedType | null;
7
+ }, SerializedLexicalNode>;
8
+ export declare class EmbedNode extends DecoratorNode<ReactElement> {
9
+ __url: string;
10
+ __source: EmbedType | null;
11
+ static getType(): string;
12
+ static clone(node: EmbedNode): EmbedNode;
13
+ constructor(url: string, source: EmbedType | null, key?: NodeKey);
14
+ createDOM(_config: EditorConfig): HTMLElement;
15
+ updateDOM(): boolean;
16
+ isInline(): boolean;
17
+ static importJSON(serializedNode: SerializedEmbedNode): EmbedNode;
18
+ exportJSON(): SerializedEmbedNode;
19
+ getUrl(): string;
20
+ setUrl(url: string): void;
21
+ getSource(): EmbedType | null;
22
+ setSource(source: EmbedType | null): void;
23
+ decorate(_editor: LexicalEditor, _config: EditorConfig): ReactElement;
24
+ }
25
+ export declare function $createEmbedNode(url: string, source: EmbedType | null): EmbedNode;
26
+ export declare function $isEmbedNode(node: LexicalNode | null | undefined): node is EmbedNode;
27
+ //# sourceMappingURL=EmbedNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmbedNode.d.ts","sourceRoot":"","sources":["../../src/nodes/EmbedNode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,WAAW,EACX,OAAO,EACP,qBAAqB,EACrB,MAAM,EACP,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAA;AAIzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,MAAM,mBAAmB,GAAG,MAAM,CACtC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE,EACzC,qBAAqB,CACtB,CAAA;AAED,qBAAa,SAAU,SAAQ,aAAa,CAAC,YAAY,CAAC;IACxD,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAA;IAE1B,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS;gBAI5B,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,OAAO;IAMhE,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG,WAAW;IAM7C,SAAS,IAAI,OAAO;IAIpB,QAAQ,IAAI,OAAO;IAInB,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,mBAAmB,GAAG,SAAS;IAIjE,UAAU,IAAI,mBAAmB;IAUjC,MAAM,IAAI,MAAM;IAIhB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAKzB,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI;IAKzC,QAAQ,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,GAAG,YAAY;CAMtE;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,GAAG,IAAI,GACvB,SAAS,CAEX;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GACnC,IAAI,IAAI,SAAS,CAEnB"}
@@ -0,0 +1,4 @@
1
+ import { Klass, LexicalNode } from 'lexical';
2
+ export declare const embedNodes: Array<Klass<LexicalNode>>;
3
+ export declare const embedEditNodes: Array<Klass<LexicalNode>>;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/nodes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAKjD,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAe,CAAA;AAChE,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAmB,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { EmbedType } from '../url-matchers';
2
+ export interface EmbedLinkRendererProps {
3
+ type: EmbedType | null;
4
+ url: string;
5
+ nodeKey: string;
6
+ }
7
+ export declare function EmbedLinkRenderer({ type, url, nodeKey, }: EmbedLinkRendererProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=EmbedLinkRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmbedLinkRenderer.d.ts","sourceRoot":"","sources":["../../src/renderers/EmbedLinkRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAA;AAOzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAShD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;CAChB;AAYD,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,GAAG,EACH,OAAO,GACR,EAAE,sBAAsB,2CAuGxB"}
@@ -0,0 +1,7 @@
1
+ import { EmbedType } from '../url-matchers';
2
+ export interface EmbedStaticRendererProps {
3
+ type: EmbedType | null;
4
+ url: string;
5
+ }
6
+ export declare function EmbedStaticRenderer({ type, url }: EmbedStaticRendererProps): import("react/jsx-runtime").JSX.Element | null;
7
+ //# sourceMappingURL=EmbedStaticRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmbedStaticRenderer.d.ts","sourceRoot":"","sources":["../../src/renderers/EmbedStaticRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,yBAAyB,CAAA;AAQhC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAuDhD,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;CACZ;AA0PD,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,wBAAwB,kDA+F1E"}
@@ -0,0 +1,316 @@
1
+ .rich-embed-link-wrapper {
2
+ margin: 8px 0;
3
+ }
4
+ .embed-static-ratio-container {
5
+ margin: 8px 0;
6
+ display: flex;
7
+ justify-content: center;
8
+ }
9
+ .embed-static-ratio-inner {
10
+ position: relative;
11
+ height: 0;
12
+ width: 100%;
13
+ }
14
+ .embed-static-iframe {
15
+ position: absolute;
16
+ inset: 0;
17
+ width: 100%;
18
+ height: 100%;
19
+ border: none;
20
+ border-radius: 8px;
21
+ }
22
+ .embed-static-gist {
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ }
27
+ .embed-static-gist__iframe {
28
+ width: 100%;
29
+ height: 300px;
30
+ border: none;
31
+ overflow: auto;
32
+ }
33
+ .embed-static-source-link {
34
+ display: inline-flex;
35
+ align-items: flex-start;
36
+ gap: 8px;
37
+ margin-top: 8px;
38
+ color: var(--rc-text-secondary);
39
+ font-size: 14px;
40
+ line-height: 1.4;
41
+ text-decoration: none;
42
+ word-break: break-all;
43
+ }
44
+ .embed-static-source-link svg {
45
+ flex-shrink: 0;
46
+ margin-top: 2px;
47
+ }
48
+ .embed-static-source-link:hover {
49
+ color: var(--rc-text);
50
+ }
51
+ .embed-static-github-file {
52
+ display: flex;
53
+ flex-direction: column;
54
+ align-items: center;
55
+ width: 100%;
56
+ }
57
+ .embed-static-github-file__code {
58
+ width: 100%;
59
+ overflow: auto;
60
+ }
61
+ .embed-static-github-file__code--long {
62
+ max-height: 50vh;
63
+ }
64
+ .embed-static-github-file__code pre {
65
+ margin: 0;
66
+ padding: 16px;
67
+ background: var(--rc-code-bg);
68
+ border-radius: 8px;
69
+ font-size: 13px;
70
+ line-height: 1.5;
71
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
72
+ color: var(--rc-text);
73
+ }
74
+ .embed-static-github-file__line {
75
+ display: block;
76
+ }
77
+ .embed-static-github-file__line-num {
78
+ display: inline-block;
79
+ width: 4ch;
80
+ text-align: right;
81
+ margin-right: 16px;
82
+ color: color-mix(in srgb, var(--rc-text-secondary) 60%, transparent);
83
+ user-select: none;
84
+ }
85
+ .embed-static-shiki {
86
+ counter-reset: shiki-line var(--start-line, 0);
87
+ }
88
+ .embed-static-shiki pre {
89
+ margin: 0;
90
+ padding: 16px;
91
+ border-radius: 8px;
92
+ font-size: 13px;
93
+ line-height: 1.5;
94
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
95
+ overflow-x: auto;
96
+ }
97
+ .embed-static-shiki code {
98
+ font-family: inherit;
99
+ display: flex;
100
+ flex-direction: column;
101
+ }
102
+ .embed-static-shiki .line {
103
+ display: flex;
104
+ line-height: 1.5;
105
+ }
106
+ .embed-static-shiki .line::before {
107
+ content: counter(shiki-line);
108
+ counter-increment: shiki-line;
109
+ flex-shrink: 0;
110
+ width: 4ch;
111
+ text-align: right;
112
+ margin-right: 16px;
113
+ color: color-mix(in srgb, var(--rc-text-secondary) 60%, transparent);
114
+ user-select: none;
115
+ }
116
+ .embed-static-fallback {
117
+ --embed-accent: 115, 115, 115;
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 0;
121
+ padding: 0 14px;
122
+ height: 48px;
123
+ border: 1px solid rgba(var(--embed-accent), 0.25);
124
+ border-radius: 12px;
125
+ background-color: rgba(var(--embed-accent), 0.08);
126
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
127
+ font-size: 14px;
128
+ margin: 8px 0;
129
+ }
130
+ .embed-static-fallback--tweet {
131
+ --embed-accent: 29, 155, 240;
132
+ }
133
+ .embed-static-fallback--youtube {
134
+ --embed-accent: 255, 0, 0;
135
+ }
136
+ .embed-static-fallback--codesandbox {
137
+ --embed-accent: 163, 163, 163;
138
+ }
139
+ .embed-static-fallback--bilibili {
140
+ --embed-accent: 0, 161, 214;
141
+ }
142
+ .embed-static-fallback--github-file {
143
+ --embed-accent: 110, 84, 148;
144
+ }
145
+ .embed-static-fallback--github-gist {
146
+ --embed-accent: 110, 84, 148;
147
+ }
148
+ .embed-static-fallback--thinking {
149
+ --embed-accent: 139, 92, 246;
150
+ }
151
+ .embed-static-fallback__badge {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ gap: 8px;
155
+ flex-shrink: 0;
156
+ font-weight: 500;
157
+ font-size: 14px;
158
+ color: var(--rc-text-secondary);
159
+ white-space: nowrap;
160
+ user-select: none;
161
+ padding-right: 14px;
162
+ }
163
+ .embed-static-fallback__dot {
164
+ width: 8px;
165
+ height: 8px;
166
+ border-radius: 50%;
167
+ flex-shrink: 0;
168
+ background-color: rgb(var(--embed-accent));
169
+ }
170
+ .embed-static-fallback__link {
171
+ flex: 1;
172
+ min-width: 0;
173
+ font-size: 14px;
174
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
175
+ color: var(--rc-text-secondary);
176
+ text-decoration: none;
177
+ overflow: hidden;
178
+ text-overflow: ellipsis;
179
+ white-space: nowrap;
180
+ }
181
+ .embed-static-fallback__link:hover {
182
+ color: var(--rc-text);
183
+ text-decoration: underline;
184
+ }
185
+ .embed-static-tweet {
186
+ display: flex;
187
+ justify-content: center;
188
+ margin: 8px 0;
189
+ }
190
+ .embed-static-loading {
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ height: 200px;
195
+ color: var(--rc-text-secondary);
196
+ font-size: 14px;
197
+ }.rich-embed-link-wrapper {
198
+ margin: 8px 0;
199
+ }
200
+ .rich-embed {
201
+ --embed-accent: 115, 115, 115;
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 0;
205
+ padding: 0 14px;
206
+ height: 48px;
207
+ border: 1px solid rgba(var(--embed-accent), 0.25);
208
+ border-radius: 12px;
209
+ background-color: rgba(var(--embed-accent), 0.12);
210
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
211
+ font-size: 14px;
212
+ transition: border-color 0.15s ease, background-color 0.15s ease;
213
+ }
214
+ .rich-embed:focus-within {
215
+ border-color: rgba(var(--embed-accent), 0.5);
216
+ }
217
+ .rich-embed--tweet {
218
+ --embed-accent: 29, 155, 240;
219
+ }
220
+ .rich-embed--youtube {
221
+ --embed-accent: 255, 0, 0;
222
+ }
223
+ .rich-embed--codesandbox {
224
+ --embed-accent: 163, 163, 163;
225
+ }
226
+ .rich-embed--bilibili {
227
+ --embed-accent: 0, 161, 214;
228
+ }
229
+ .rich-embed--github-file {
230
+ --embed-accent: 110, 84, 148;
231
+ }
232
+ .rich-embed--github-gist {
233
+ --embed-accent: 110, 84, 148;
234
+ }
235
+ .rich-embed--thinking {
236
+ --embed-accent: 139, 92, 246;
237
+ }
238
+ .rich-embed__badge {
239
+ display: inline-flex;
240
+ align-items: center;
241
+ gap: 8px;
242
+ flex-shrink: 0;
243
+ font-weight: 500;
244
+ font-size: 14px;
245
+ color: var(--rc-text-secondary);
246
+ white-space: nowrap;
247
+ user-select: none;
248
+ padding-right: 14px;
249
+ }
250
+ .rich-embed__dot {
251
+ width: 8px;
252
+ height: 8px;
253
+ border-radius: 50%;
254
+ flex-shrink: 0;
255
+ background-color: rgb(var(--embed-accent));
256
+ }
257
+ .rich-embed__divider {
258
+ width: 1px;
259
+ height: 20px;
260
+ background-color: var(--rc-border);
261
+ flex-shrink: 0;
262
+ }
263
+ .rich-embed__input {
264
+ flex: 1;
265
+ min-width: 0;
266
+ height: 100%;
267
+ font-size: 14px;
268
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
269
+ color: var(--rc-text);
270
+ background: transparent;
271
+ border: none;
272
+ padding: 0 14px;
273
+ outline: none;
274
+ }
275
+ .rich-embed__input::placeholder {
276
+ color: color-mix(in srgb, var(--rc-text-secondary) 60%, transparent);
277
+ }
278
+ .rich-embed__actions {
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 2px;
282
+ flex-shrink: 0;
283
+ }
284
+ .rich-embed__action-btn {
285
+ display: inline-flex;
286
+ align-items: center;
287
+ justify-content: center;
288
+ appearance: none;
289
+ border: none;
290
+ background: none;
291
+ color: var(--rc-text-secondary);
292
+ cursor: pointer;
293
+ padding: 6px;
294
+ border-radius: 6px;
295
+ transition: color 0.15s ease, background-color 0.15s ease;
296
+ }
297
+ .rich-embed__action-btn:hover {
298
+ color: var(--rc-text);
299
+ background-color: color-mix(in srgb, var(--rc-text) 8%, transparent);
300
+ }
301
+ .rich-embed__action-btn:disabled {
302
+ opacity: 0.3;
303
+ cursor: default;
304
+ }
305
+ .rich-embed__action-btn:disabled:hover {
306
+ background: none;
307
+ color: var(--rc-text-secondary);
308
+ }
309
+ .rich-embed__action-btn--danger:hover {
310
+ color: var(--rc-alert-caution);
311
+ background-color: color-mix(in srgb, var(--rc-alert-caution) 10%, transparent);
312
+ }
313
+ .rich-embed__action-btn svg {
314
+ width: 16px;
315
+ height: 16px;
316
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=styles-static.css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles-static.css.d.ts","sourceRoot":"","sources":["../src/styles-static.css.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=styles.css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.css.d.ts","sourceRoot":"","sources":["../src/styles.css.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ export type EmbedType = 'tweet' | 'youtube' | 'codesandbox' | 'bilibili' | 'github-file' | 'github-gist' | 'thinking';
2
+ export declare const isTweetUrl: (url: URL) => boolean;
3
+ export declare const isYoutubeUrl: (url: URL) => boolean;
4
+ export declare const isCodesandboxUrl: (url: URL) => boolean;
5
+ export declare const isBilibiliVideoUrl: (url: URL) => boolean;
6
+ export declare const isGithubFilePreviewUrl: (url: URL) => boolean;
7
+ export declare const isGistUrl: (url: URL) => boolean;
8
+ export declare function createSelfThinkingMatcher(hostnames: string[]): (url: URL) => boolean;
9
+ export declare function matchEmbedUrl(url: URL, selfThinkingMatcher?: (url: URL) => boolean): EmbedType | null;
10
+ //# sourceMappingURL=url-matchers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-matchers.d.ts","sourceRoot":"","sources":["../src/url-matchers.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,SAAS,GACT,aAAa,GACb,UAAU,GACV,aAAa,GACb,aAAa,GACb,UAAU,CAAA;AAEd,eAAO,MAAM,UAAU,GAAI,KAAK,GAAG,YAEL,CAAA;AAE9B,eAAO,MAAM,YAAY,GAAI,KAAK,GAAG,YACoC,CAAA;AAEzE,eAAO,MAAM,gBAAgB,GAAI,KAAK,GAAG,YACkC,CAAA;AAE3E,eAAO,MAAM,kBAAkB,GAAI,KAAK,GAAG,YACoC,CAAA;AAE/E,eAAO,MAAM,sBAAsB,GAAI,KAAK,GAAG,YAG9C,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,KAAK,GAAG,YAAuC,CAAA;AAEzE,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,IACnD,KAAK,GAAG,aAEjB;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,GAAG,EACR,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,GAC1C,SAAS,GAAG,IAAI,CASlB"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@haklex/rich-ext-embed",
3
+ "version": "0.0.1",
4
+ "description": "Embed extension for Twitter, YouTube, etc.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./style.css": "./dist/rich-ext-embed.css"
13
+ },
14
+ "main": "./dist/index.mjs",
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "lucide-react": "^0.574.0",
20
+ "@haklex/rich-style-token": "0.0.1"
21
+ },
22
+ "devDependencies": {
23
+ "@lexical/react": "^0.39.0",
24
+ "@types/react": "^19.0.0",
25
+ "@types/react-dom": "^19.0.0",
26
+ "@vanilla-extract/css": "^1.17.1",
27
+ "@vanilla-extract/vite-plugin": "^4.0.20",
28
+ "lexical": "^0.39.0",
29
+ "react": "19.2.4",
30
+ "react-dom": "19.2.4",
31
+ "react-tweet": "npm:@innei/react-tweet@3.4.2",
32
+ "shiki": "^3.21.0",
33
+ "typescript": "^5.9.0",
34
+ "vite": "^7.3.1",
35
+ "vite-plugin-dts": "^4.5.0",
36
+ "@haklex/rich-editor": "0.0.1"
37
+ },
38
+ "peerDependencies": {
39
+ "@lexical/react": "^0.39.0",
40
+ "lexical": "^0.39.0",
41
+ "react": ">=19",
42
+ "react-dom": ">=19",
43
+ "shiki": ">=3",
44
+ "@haklex/rich-editor": "0.0.1"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "shiki": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "vite build",
56
+ "dev:build": "vite build --watch"
57
+ },
58
+ "types": "./dist/index.d.ts"
59
+ }