@haklex/rich-renderer-linkcard 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.
Files changed (61) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +67 -0
  3. package/dist/LinkCardEditDecorator.d.ts +10 -0
  4. package/dist/LinkCardEditDecorator.d.ts.map +1 -0
  5. package/dist/LinkCardEditNode.d.ts +10 -0
  6. package/dist/LinkCardEditNode.d.ts.map +1 -0
  7. package/dist/LinkCardRenderer.d.ts +12 -0
  8. package/dist/LinkCardRenderer.d.ts.map +1 -0
  9. package/dist/LinkCardSkeleton.d.ts +5 -0
  10. package/dist/LinkCardSkeleton.d.ts.map +1 -0
  11. package/dist/hooks/useCardFetcher.d.ts +19 -0
  12. package/dist/hooks/useCardFetcher.d.ts.map +1 -0
  13. package/dist/hooks/useUrlMatcher.d.ts +7 -0
  14. package/dist/hooks/useUrlMatcher.d.ts.map +1 -0
  15. package/dist/index.d.ts +14 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.mjs +1432 -0
  18. package/dist/plugins/academic/arxiv.d.ts +3 -0
  19. package/dist/plugins/academic/arxiv.d.ts.map +1 -0
  20. package/dist/plugins/academic/index.d.ts +2 -0
  21. package/dist/plugins/academic/index.d.ts.map +1 -0
  22. package/dist/plugins/code/index.d.ts +2 -0
  23. package/dist/plugins/code/index.d.ts.map +1 -0
  24. package/dist/plugins/code/leetcode.d.ts +3 -0
  25. package/dist/plugins/code/leetcode.d.ts.map +1 -0
  26. package/dist/plugins/github/commit.d.ts +3 -0
  27. package/dist/plugins/github/commit.d.ts.map +1 -0
  28. package/dist/plugins/github/discussion.d.ts +3 -0
  29. package/dist/plugins/github/discussion.d.ts.map +1 -0
  30. package/dist/plugins/github/index.d.ts +6 -0
  31. package/dist/plugins/github/index.d.ts.map +1 -0
  32. package/dist/plugins/github/issue.d.ts +3 -0
  33. package/dist/plugins/github/issue.d.ts.map +1 -0
  34. package/dist/plugins/github/pr.d.ts +3 -0
  35. package/dist/plugins/github/pr.d.ts.map +1 -0
  36. package/dist/plugins/github/repo.d.ts +3 -0
  37. package/dist/plugins/github/repo.d.ts.map +1 -0
  38. package/dist/plugins/index.d.ts +13 -0
  39. package/dist/plugins/index.d.ts.map +1 -0
  40. package/dist/plugins/media/bangumi.d.ts +3 -0
  41. package/dist/plugins/media/bangumi.d.ts.map +1 -0
  42. package/dist/plugins/media/index.d.ts +5 -0
  43. package/dist/plugins/media/index.d.ts.map +1 -0
  44. package/dist/plugins/media/netease-music.d.ts +3 -0
  45. package/dist/plugins/media/netease-music.d.ts.map +1 -0
  46. package/dist/plugins/media/qq-music.d.ts +3 -0
  47. package/dist/plugins/media/qq-music.d.ts.map +1 -0
  48. package/dist/plugins/media/tmdb.d.ts +3 -0
  49. package/dist/plugins/media/tmdb.d.ts.map +1 -0
  50. package/dist/plugins/self/index.d.ts +3 -0
  51. package/dist/plugins/self/index.d.ts.map +1 -0
  52. package/dist/plugins/self/mx-space.d.ts +8 -0
  53. package/dist/plugins/self/mx-space.d.ts.map +1 -0
  54. package/dist/rich-renderer-linkcard.css +331 -0
  55. package/dist/styles.css.d.ts +2 -0
  56. package/dist/styles.css.d.ts.map +1 -0
  57. package/dist/types.d.ts +88 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/utils.d.ts +10 -0
  60. package/dist/utils.d.ts.map +1 -0
  61. package/package.json +54 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1432 @@
1
+ import { useMemo, useState, useRef, useEffect, useCallback, createElement, useLayoutEffect } from "react";
2
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
3
+ import { Star, Link, ExternalLink, Unlink, Globe } from "lucide-react";
4
+ import { $isLinkCardNode, LinkCardNode, createRendererDecoration, LinkCardRenderer as LinkCardRenderer$1 } from "@haklex/rich-editor";
5
+ import { Popover, PopoverTrigger, PopoverPanel } from "@haklex/rich-editor-ui";
6
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
7
+ import { $getNodeByKey } from "lexical";
8
+ import { useInView } from "react-intersection-observer";
9
+ const arxivPlugin = {
10
+ name: "arxiv",
11
+ displayName: "arXiv Paper",
12
+ priority: 80,
13
+ typeClass: "academic",
14
+ matchUrl(url) {
15
+ if (url.hostname !== "arxiv.org") return null;
16
+ const match = url.pathname.match(/\/(abs|pdf)\/(\d{4}\.\d+(?:v\d+)?)/i);
17
+ if (!match) return null;
18
+ return { id: match[2].toLowerCase(), fullUrl: url.toString() };
19
+ },
20
+ isValidId(id) {
21
+ return /^\d{4}\.\d+(?:v\d+)?$/.test(id);
22
+ },
23
+ async fetch(id) {
24
+ const response = await fetch(
25
+ `https://export.arxiv.org/api/query?id_list=${id}`
26
+ );
27
+ const text = await response.text();
28
+ const parser = new DOMParser();
29
+ const xmlDoc = parser.parseFromString(text, "application/xml");
30
+ const entry = xmlDoc.getElementsByTagName("entry")[0];
31
+ const title = entry.getElementsByTagName("title")[0].textContent;
32
+ const authors = entry.getElementsByTagName("author");
33
+ const authorNames = Array.from(authors).map(
34
+ (author) => author.getElementsByTagName("name")[0].textContent
35
+ );
36
+ return {
37
+ title: /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
38
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: title }),
39
+ /* @__PURE__ */ jsx("span", { style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx(
40
+ "span",
41
+ {
42
+ style: {
43
+ display: "inline-flex",
44
+ alignItems: "center",
45
+ gap: "4px",
46
+ fontSize: "0.875rem",
47
+ color: "#fb923c"
48
+ },
49
+ children: /* @__PURE__ */ jsx("span", { style: { fontFamily: "sans-serif", fontWeight: 500 }, children: id })
50
+ }
51
+ ) })
52
+ ] }),
53
+ desc: authorNames.length > 1 ? `${authorNames[0]} et al.` : authorNames[0]
54
+ };
55
+ }
56
+ };
57
+ function toCamelCase(str) {
58
+ return str.replaceAll(/_([a-z])/g, (_, c) => c.toUpperCase());
59
+ }
60
+ function camelcaseKeys(obj) {
61
+ if (Array.isArray(obj)) return obj.map(camelcaseKeys);
62
+ if (obj !== null && typeof obj === "object") {
63
+ return Object.fromEntries(
64
+ Object.entries(obj).map(([k, v]) => [toCamelCase(k), camelcaseKeys(v)])
65
+ );
66
+ }
67
+ return obj;
68
+ }
69
+ async function fetchJsonWithContext(url, context, provider, init) {
70
+ const providerAdapter = provider && context?.adapters ? context.adapters[provider] : void 0;
71
+ if (providerAdapter) {
72
+ return providerAdapter.request(url, init);
73
+ }
74
+ if (context?.fetchJson) {
75
+ return context.fetchJson(url, init);
76
+ }
77
+ const response = await fetch(url, init);
78
+ if (!response.ok) {
79
+ throw new Error(`Request failed: ${response.status}`);
80
+ }
81
+ return response.json();
82
+ }
83
+ async function fetchGitHubApi(url, context) {
84
+ const path = url.replace("https://api.github.com", "");
85
+ return fetchJsonWithContext(
86
+ `https://api.github.com${path}`,
87
+ context,
88
+ "github"
89
+ );
90
+ }
91
+ const LanguageToColorMap = {
92
+ typescript: "#2b7489",
93
+ javascript: "#f1e05a",
94
+ html: "#e34c26",
95
+ java: "#b07219",
96
+ go: "#00add8",
97
+ vue: "#2c3e50",
98
+ css: "#563d7c",
99
+ yaml: "#cb171e",
100
+ json: "#292929",
101
+ markdown: "#083fa1",
102
+ csharp: "#178600",
103
+ "c#": "#178600",
104
+ c: "#555555",
105
+ cpp: "#f34b7d",
106
+ "c++": "#f34b7d",
107
+ python: "#3572a5",
108
+ lua: "#000080",
109
+ vimscript: "#199f4b",
110
+ shell: "#89e051",
111
+ dockerfile: "#384d54",
112
+ ruby: "#701516",
113
+ php: "#4f5d95",
114
+ lisp: "#3fb68b",
115
+ kotlin: "#F18E33",
116
+ rust: "#dea584",
117
+ dart: "#00B4AB",
118
+ swift: "#ffac45",
119
+ "objective-c": "#438eff",
120
+ "objective-c++": "#6866fb",
121
+ r: "#198ce7",
122
+ matlab: "#e16737",
123
+ scala: "#c22d40",
124
+ sql: "#e38c00",
125
+ perl: "#0298c3"
126
+ };
127
+ const bangumiTypeMap = {
128
+ subject: "subjects",
129
+ character: "characters",
130
+ person: "persons"
131
+ };
132
+ const allowedBangumiTypes = Object.keys(bangumiTypeMap);
133
+ function hslToHex(h, s, l) {
134
+ s /= 100;
135
+ l /= 100;
136
+ const a = s * Math.min(l, 1 - l);
137
+ const f = (n) => {
138
+ const k = (n + h / 30) % 12;
139
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
140
+ return Math.round(255 * color).toString(16).padStart(2, "0");
141
+ };
142
+ return `#${f(0)}${f(8)}${f(4)}`;
143
+ }
144
+ function generateColor(str, saturation = [30, 35], lightness = [60, 70]) {
145
+ let hash = 0;
146
+ for (let i = 0; i < str.length; i++) {
147
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
148
+ }
149
+ const sRange = saturation[1] - saturation[0] + 1;
150
+ const lRange = lightness[1] - lightness[0] + 1;
151
+ const h = Math.abs(hash) % 360;
152
+ const s = saturation[0] + Math.abs(hash >> 8) % sRange;
153
+ const l = lightness[0] + Math.abs(hash >> 16) % lRange;
154
+ return hslToHex(h, s, l);
155
+ }
156
+ function stripMarkdown(text) {
157
+ return text.replaceAll(/!\[.*?\]\(.*?\)/g, "").replaceAll(/\[([^\]]*)\]\(.*?\)/g, "$1").replaceAll(/[*_~`#>]/g, "").replaceAll(/\n+/g, " ").trim();
158
+ }
159
+ function getDifficultyColor(difficulty) {
160
+ switch (difficulty) {
161
+ case "Easy":
162
+ return "#00BFA5";
163
+ case "Medium":
164
+ return "#FFA726";
165
+ case "Hard":
166
+ return "#F44336";
167
+ default:
168
+ return "#757575";
169
+ }
170
+ }
171
+ const leetcodePlugin = {
172
+ name: "leetcode",
173
+ displayName: "LeetCode",
174
+ priority: 65,
175
+ typeClass: "wide",
176
+ matchUrl(url) {
177
+ if (url.hostname !== "leetcode.cn" && url.hostname !== "leetcode.com")
178
+ return null;
179
+ const parts = url.pathname.split("/").filter(Boolean);
180
+ if (parts[0] !== "problems" || !parts[1]) return null;
181
+ return { id: parts[1], fullUrl: url.toString() };
182
+ },
183
+ isValidId(id) {
184
+ return typeof id === "string" && id.length > 0;
185
+ },
186
+ async fetch(id) {
187
+ const body = {
188
+ query: `query questionData($titleSlug: String!) {
189
+ question(titleSlug: $titleSlug) {translatedTitle
190
+ difficulty
191
+ likes
192
+ topicTags { translatedName
193
+ }
194
+ stats
195
+ }
196
+ }
197
+ `,
198
+ variables: { titleSlug: id }
199
+ };
200
+ const questionData = await fetch("/api/leetcode", {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json" },
203
+ body: JSON.stringify(body)
204
+ }).then(async (res) => {
205
+ if (!res.ok) throw new Error("Failed to fetch LeetCode question");
206
+ return res.json();
207
+ });
208
+ const questionTitleData = camelcaseKeys(questionData.data.question);
209
+ const stats = JSON.parse(questionTitleData.stats);
210
+ return {
211
+ title: /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
212
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: questionTitleData.translatedTitle }),
213
+ /* @__PURE__ */ jsx("span", { style: { flexShrink: 0 }, children: questionTitleData.likes > 0 && /* @__PURE__ */ jsxs(
214
+ "span",
215
+ {
216
+ style: {
217
+ display: "inline-flex",
218
+ alignItems: "center",
219
+ gap: "4px",
220
+ fontSize: "0.875rem",
221
+ color: "#fb923c"
222
+ },
223
+ children: [
224
+ "👍",
225
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: "sans-serif", fontWeight: 500 }, children: questionTitleData.likes })
226
+ ]
227
+ }
228
+ ) })
229
+ ] }),
230
+ desc: /* @__PURE__ */ jsxs(Fragment, { children: [
231
+ /* @__PURE__ */ jsx(
232
+ "span",
233
+ {
234
+ style: {
235
+ marginRight: "16px",
236
+ fontWeight: "bold",
237
+ color: getDifficultyColor(questionTitleData.difficulty)
238
+ },
239
+ children: questionTitleData.difficulty
240
+ }
241
+ ),
242
+ /* @__PURE__ */ jsx("span", { style: { overflow: "hidden" }, children: questionTitleData.topicTags.map((tag) => tag.translatedName).join(" / ") }),
243
+ /* @__PURE__ */ jsxs("span", { style: { float: "right", overflow: "hidden" }, children: [
244
+ "AR: ",
245
+ stats.acRate
246
+ ] })
247
+ ] }),
248
+ image: "https://upload.wikimedia.org/wikipedia/commons/1/19/LeetCode_logo_black.png",
249
+ color: getDifficultyColor(questionTitleData.difficulty)
250
+ };
251
+ }
252
+ };
253
+ const githubCommitPlugin = {
254
+ name: "gh-commit",
255
+ displayName: "GitHub Commit",
256
+ priority: 95,
257
+ typeClass: "github",
258
+ provider: "github",
259
+ matchUrl(url) {
260
+ if (url.hostname !== "github.com") return null;
261
+ const parts = url.pathname.split("/").filter(Boolean);
262
+ if (parts.length < 4 || parts[2] !== "commit") return null;
263
+ const [owner, repo, , commitId] = parts;
264
+ return {
265
+ id: `${owner}/${repo}/commit/${commitId}`,
266
+ fullUrl: url.toString()
267
+ };
268
+ },
269
+ isValidId(id) {
270
+ const parts = id.split("/");
271
+ return parts.length === 4 && parts.every((p) => p.length > 0) && parts[2] === "commit";
272
+ },
273
+ async fetch(id, _meta, context) {
274
+ const [owner, repo, , commitId] = id.split("/");
275
+ const response = await fetchGitHubApi(
276
+ `https://api.github.com/repos/${owner}/${repo}/commits/${commitId}`,
277
+ context
278
+ );
279
+ const data = camelcaseKeys(response);
280
+ return {
281
+ title: /* @__PURE__ */ jsx("span", { style: { fontWeight: "normal" }, children: data.commit.message.replace(/Signed-off-by:.+/s, "").trim() }),
282
+ desc: /* @__PURE__ */ jsxs(
283
+ "span",
284
+ {
285
+ style: {
286
+ display: "flex",
287
+ flexWrap: "wrap",
288
+ alignItems: "center",
289
+ gap: "4px 12px",
290
+ fontFamily: "ui-monospace, monospace"
291
+ },
292
+ children: [
293
+ /* @__PURE__ */ jsxs(
294
+ "span",
295
+ {
296
+ style: {
297
+ display: "inline-flex",
298
+ alignItems: "center",
299
+ gap: "8px"
300
+ },
301
+ children: [
302
+ /* @__PURE__ */ jsxs("span", { style: { color: "#238636" }, children: [
303
+ "+",
304
+ data.stats.additions
305
+ ] }),
306
+ /* @__PURE__ */ jsxs("span", { style: { color: "#f85149" }, children: [
307
+ "-",
308
+ data.stats.deletions
309
+ ] })
310
+ ]
311
+ }
312
+ ),
313
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.875rem" }, children: data.sha.slice(0, 7) }),
314
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.8 }, children: [
315
+ owner,
316
+ "/",
317
+ repo
318
+ ] })
319
+ ]
320
+ }
321
+ ),
322
+ image: data.author?.avatarUrl
323
+ };
324
+ }
325
+ };
326
+ const githubDiscussionPlugin = {
327
+ name: "gh-discussion",
328
+ displayName: "GitHub Discussion",
329
+ priority: 95,
330
+ typeClass: "github",
331
+ provider: "github",
332
+ matchUrl(url) {
333
+ if (url.hostname !== "github.com") return null;
334
+ if (!url.pathname.includes("/discussions/")) return null;
335
+ const parts = url.pathname.split("/").filter(Boolean);
336
+ if (parts.length < 4 || parts[2] !== "discussions") return null;
337
+ const discussionNumber = parts[3];
338
+ if (!/^\d+$/.test(discussionNumber)) return null;
339
+ const [owner, repo] = parts;
340
+ return {
341
+ id: `${owner}/${repo}/${discussionNumber}`,
342
+ fullUrl: url.toString()
343
+ };
344
+ },
345
+ isValidId(id) {
346
+ const parts = id.split("/");
347
+ return parts.length === 3 && parts.every((p) => p.length > 0) && /^\d+$/.test(parts[2]);
348
+ },
349
+ async fetch(id, _meta, context) {
350
+ const [owner, repo, discussionNumber] = id.split("/");
351
+ const response = await fetchGitHubApi(
352
+ `https://api.github.com/repos/${owner}/${repo}/discussions/${discussionNumber}`,
353
+ context
354
+ );
355
+ const data = camelcaseKeys(response);
356
+ const categoryName = data.category?.name || "Discussion";
357
+ return {
358
+ title: `Discussion: ${data.title}`,
359
+ desc: /* @__PURE__ */ jsxs(
360
+ "span",
361
+ {
362
+ style: {
363
+ display: "flex",
364
+ flexWrap: "wrap",
365
+ alignItems: "center",
366
+ gap: "4px 12px",
367
+ fontFamily: "ui-monospace, monospace"
368
+ },
369
+ children: [
370
+ /* @__PURE__ */ jsx(
371
+ "span",
372
+ {
373
+ style: {
374
+ borderRadius: "4px",
375
+ backgroundColor: "rgba(128,128,128,0.15)",
376
+ padding: "2px 6px",
377
+ fontSize: "0.75rem"
378
+ },
379
+ children: categoryName
380
+ }
381
+ ),
382
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.8 }, children: [
383
+ "#",
384
+ discussionNumber,
385
+ " · ",
386
+ owner,
387
+ "/",
388
+ repo
389
+ ] })
390
+ ]
391
+ }
392
+ ),
393
+ image: data.user?.avatarUrl
394
+ };
395
+ }
396
+ };
397
+ const stateColors = {
398
+ open: "#238636",
399
+ closed: "#f85149"
400
+ };
401
+ const stateTextMap$1 = {
402
+ open: "Open",
403
+ closed: "Closed"
404
+ };
405
+ const githubIssuePlugin = {
406
+ name: "gh-issue",
407
+ displayName: "GitHub Issue",
408
+ priority: 95,
409
+ typeClass: "github",
410
+ provider: "github",
411
+ matchUrl(url) {
412
+ if (url.hostname !== "github.com") return null;
413
+ if (!url.pathname.includes("/issues/")) return null;
414
+ const parts = url.pathname.split("/").filter(Boolean);
415
+ if (parts.length < 4 || parts[2] !== "issues") return null;
416
+ const issueNumber = parts[3];
417
+ if (!/^\d+$/.test(issueNumber)) return null;
418
+ const [owner, repo] = parts;
419
+ return { id: `${owner}/${repo}/${issueNumber}`, fullUrl: url.toString() };
420
+ },
421
+ isValidId(id) {
422
+ const parts = id.split("/");
423
+ return parts.length === 3 && parts.every((p) => p.length > 0) && /^\d+$/.test(parts[2]);
424
+ },
425
+ async fetch(id, _meta, context) {
426
+ const [owner, repo, issueNumber] = id.split("/");
427
+ const response = await fetchGitHubApi(
428
+ `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
429
+ context
430
+ );
431
+ const data = camelcaseKeys(response);
432
+ const color = stateColors[data.state] || "#6b7280";
433
+ const stateText = stateTextMap$1[data.state] || data.state;
434
+ return {
435
+ title: `Issue: ${data.title}`,
436
+ color,
437
+ desc: /* @__PURE__ */ jsxs(
438
+ "span",
439
+ {
440
+ style: {
441
+ display: "flex",
442
+ flexWrap: "wrap",
443
+ alignItems: "center",
444
+ gap: "4px 12px",
445
+ fontFamily: "ui-monospace, monospace"
446
+ },
447
+ children: [
448
+ /* @__PURE__ */ jsx("span", { style: { color }, children: stateText }),
449
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.8 }, children: [
450
+ "#",
451
+ issueNumber,
452
+ " · ",
453
+ owner,
454
+ "/",
455
+ repo
456
+ ] })
457
+ ]
458
+ }
459
+ ),
460
+ image: data.user?.avatarUrl
461
+ };
462
+ }
463
+ };
464
+ const getPrState = (data) => {
465
+ if (data.merged) return "merged";
466
+ return data.state;
467
+ };
468
+ const stateColorMap = {
469
+ open: "#238636",
470
+ merged: "#8957e5",
471
+ closed: "#f85149"
472
+ };
473
+ const stateTextMap = {
474
+ open: "Open",
475
+ merged: "Merged",
476
+ closed: "Closed"
477
+ };
478
+ const githubPrPlugin = {
479
+ name: "gh-pr",
480
+ displayName: "GitHub Pull Request",
481
+ priority: 95,
482
+ typeClass: "github",
483
+ provider: "github",
484
+ matchUrl(url) {
485
+ if (url.hostname !== "github.com") return null;
486
+ if (!url.pathname.includes("/pull/")) return null;
487
+ const parts = url.pathname.split("/").filter(Boolean);
488
+ if (parts.length < 4 || parts[2] !== "pull") return null;
489
+ const [owner, repo, , prNumber] = parts;
490
+ return { id: `${owner}/${repo}/${prNumber}`, fullUrl: url.toString() };
491
+ },
492
+ isValidId(id) {
493
+ const parts = id.split("/");
494
+ return parts.length === 3 && parts.every((p) => p.length > 0);
495
+ },
496
+ async fetch(id, _meta, context) {
497
+ const [owner, repo, prNumber] = id.split("/");
498
+ const response = await fetchGitHubApi(
499
+ `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
500
+ context
501
+ );
502
+ const data = camelcaseKeys(response);
503
+ const state = getPrState(data);
504
+ const color = stateColorMap[state];
505
+ const stateText = stateTextMap[state];
506
+ return {
507
+ title: `PR: ${data.title}`,
508
+ color,
509
+ desc: /* @__PURE__ */ jsxs(
510
+ "span",
511
+ {
512
+ style: {
513
+ display: "flex",
514
+ flexWrap: "wrap",
515
+ alignItems: "center",
516
+ gap: "4px 12px",
517
+ fontFamily: "ui-monospace, monospace"
518
+ },
519
+ children: [
520
+ /* @__PURE__ */ jsx("span", { style: { color }, children: stateText }),
521
+ /* @__PURE__ */ jsxs(
522
+ "span",
523
+ {
524
+ style: {
525
+ display: "inline-flex",
526
+ alignItems: "center",
527
+ gap: "8px",
528
+ whiteSpace: "nowrap"
529
+ },
530
+ children: [
531
+ /* @__PURE__ */ jsxs("span", { style: { color: "#238636" }, children: [
532
+ "+",
533
+ data.additions
534
+ ] }),
535
+ /* @__PURE__ */ jsxs("span", { style: { color: "#f85149" }, children: [
536
+ "-",
537
+ data.deletions
538
+ ] })
539
+ ]
540
+ }
541
+ ),
542
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.8 }, children: [
543
+ owner,
544
+ "/",
545
+ repo
546
+ ] })
547
+ ]
548
+ }
549
+ ),
550
+ image: data.user.avatarUrl
551
+ };
552
+ }
553
+ };
554
+ function formatStargazers(n) {
555
+ return n.toLocaleString("en-US");
556
+ }
557
+ const githubRepoPlugin = {
558
+ name: "gh-repo",
559
+ displayName: "GitHub Repository",
560
+ priority: 100,
561
+ typeClass: "github",
562
+ provider: "github",
563
+ matchUrl(url) {
564
+ if (url.hostname !== "github.com") return null;
565
+ const parts = url.pathname.split("/").filter(Boolean);
566
+ if (parts.length !== 2) return null;
567
+ const [owner, repo] = parts;
568
+ if (!owner || !repo) return null;
569
+ return { id: `${owner}/${repo}`, fullUrl: url.toString() };
570
+ },
571
+ isValidId(id) {
572
+ const parts = id.split("/");
573
+ return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
574
+ },
575
+ async fetch(id, _meta, context) {
576
+ const [owner, repo] = id.split("/");
577
+ const response = await fetchGitHubApi(
578
+ `https://api.github.com/repos/${owner}/${repo}`,
579
+ context
580
+ );
581
+ const data = camelcaseKeys(response);
582
+ return {
583
+ title: /* @__PURE__ */ jsxs("span", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
584
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: data.name }),
585
+ /* @__PURE__ */ jsx("span", { style: { flexShrink: 0 }, children: data.stargazersCount > 0 && /* @__PURE__ */ jsxs(
586
+ "span",
587
+ {
588
+ style: {
589
+ display: "inline-flex",
590
+ alignItems: "center",
591
+ gap: "4px",
592
+ fontSize: "0.875rem",
593
+ color: "#fb923c"
594
+ },
595
+ children: [
596
+ /* @__PURE__ */ jsx(Star, { size: 14, strokeWidth: 2, "aria-hidden": true }),
597
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: "sans-serif", fontWeight: 500 }, children: formatStargazers(data.stargazersCount) })
598
+ ]
599
+ }
600
+ ) })
601
+ ] }),
602
+ desc: data.description,
603
+ image: data.owner.avatarUrl,
604
+ color: LanguageToColorMap[data.language?.toLowerCase()]
605
+ };
606
+ }
607
+ };
608
+ const bangumiPlugin = {
609
+ name: "bangumi",
610
+ displayName: "Bangumi",
611
+ priority: 70,
612
+ typeClass: "media",
613
+ matchUrl(url) {
614
+ if (url.hostname !== "bgm.tv" && url.hostname !== "bangumi.tv") return null;
615
+ const parts = url.pathname.split("/").filter(Boolean);
616
+ if (parts.length < 2) return null;
617
+ const [type, realId] = parts;
618
+ if (!allowedBangumiTypes.includes(type)) return null;
619
+ return { id: `${type}/${realId}`, fullUrl: url.toString(), meta: { type } };
620
+ },
621
+ isValidId(id) {
622
+ const [type, realId] = id.split("/");
623
+ return allowedBangumiTypes.includes(type) && realId?.length > 0;
624
+ },
625
+ async fetch(id) {
626
+ const [type, realId] = id.split("/");
627
+ const json = await fetch(`/api/bangumi/${type}/${realId}`).then(
628
+ (r) => r.json()
629
+ );
630
+ let title = "";
631
+ let originalTitle = "";
632
+ if (type === "subject") {
633
+ if (json.name_cn && json.name_cn !== json.name && json.name_cn !== "") {
634
+ title = json.name_cn;
635
+ originalTitle = json.name;
636
+ } else {
637
+ title = json.name;
638
+ originalTitle = json.name;
639
+ }
640
+ } else if (type === "character" || type === "person") {
641
+ const { infobox } = json;
642
+ infobox.forEach(
643
+ (item) => {
644
+ if (item.key === "简体中文名") {
645
+ title = typeof item.value === "string" ? item.value : item.value[0].v;
646
+ } else if (item.key === "别名") {
647
+ const aliases = item.value;
648
+ aliases.forEach((alias) => {
649
+ originalTitle += `${alias.v} / `;
650
+ });
651
+ originalTitle = originalTitle.slice(0, -3);
652
+ }
653
+ }
654
+ );
655
+ }
656
+ const starStyle = {
657
+ display: "inline-flex",
658
+ flexShrink: 0,
659
+ alignItems: "center",
660
+ gap: "4px",
661
+ alignSelf: "center",
662
+ fontSize: "0.75rem",
663
+ color: "#fb923c"
664
+ };
665
+ return {
666
+ title: /* @__PURE__ */ jsxs(
667
+ "span",
668
+ {
669
+ style: {
670
+ display: "flex",
671
+ flexWrap: "wrap",
672
+ alignItems: "flex-end",
673
+ gap: "8px"
674
+ },
675
+ children: [
676
+ /* @__PURE__ */ jsx("span", { children: title }),
677
+ title !== originalTitle && /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.7 }, children: [
678
+ "(",
679
+ originalTitle,
680
+ ")"
681
+ ] }),
682
+ type === "subject" && /* @__PURE__ */ jsxs(
683
+ "span",
684
+ {
685
+ style: {
686
+ display: "inline-flex",
687
+ flexShrink: 0,
688
+ alignItems: "center",
689
+ gap: "12px",
690
+ alignSelf: "center"
691
+ },
692
+ children: [
693
+ /* @__PURE__ */ jsxs("span", { style: starStyle, children: [
694
+ "★",
695
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: "sans-serif", fontWeight: 500 }, children: json.rating.score > 0 && json.rating.score.toFixed(1) })
696
+ ] }),
697
+ /* @__PURE__ */ jsxs("span", { style: starStyle, children: [
698
+ "☆",
699
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: "sans-serif", fontWeight: 500 }, children: json.collection && json.collection.on_hold + json.collection.dropped + json.collection.wish + json.collection.collect + json.collection.doing })
700
+ ] })
701
+ ]
702
+ }
703
+ ),
704
+ (type === "character" || type === "person") && /* @__PURE__ */ jsxs("span", { style: starStyle, children: [
705
+ "☆",
706
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: "sans-serif", fontWeight: 500 }, children: json.stat.collects > 0 && json.stat.collects })
707
+ ] })
708
+ ]
709
+ }
710
+ ),
711
+ desc: /* @__PURE__ */ jsx("span", { style: { overflow: "visible", whiteSpace: "pre-wrap" }, children: json.summary }),
712
+ image: json.images.grid,
713
+ color: generateColor(title),
714
+ classNames: {
715
+ image: type === "subject" ? "link-card__image--poster-md" : "link-card__image--poster-sm",
716
+ cardRoot: "link-card--reversed"
717
+ }
718
+ };
719
+ }
720
+ };
721
+ const neteaseMusicPlugin = {
722
+ name: "netease-music-song",
723
+ displayName: "Netease Music Song",
724
+ priority: 60,
725
+ typeClass: "wide",
726
+ matchUrl(url) {
727
+ if (url.hostname !== "music.163.com") return null;
728
+ if (!url.pathname.includes("/song") && !url.hash.includes("/song"))
729
+ return null;
730
+ const urlString = url.toString().replaceAll("/#/", "/");
731
+ const _url = new URL(urlString);
732
+ const id = _url.searchParams.get("id");
733
+ if (!id) return null;
734
+ return { id, fullUrl: url.toString() };
735
+ },
736
+ isValidId(id) {
737
+ return id.length > 0;
738
+ },
739
+ async fetch(id) {
740
+ const songData = await fetch("/api/music/netease", {
741
+ method: "POST",
742
+ headers: { "Content-Type": "application/json" },
743
+ body: JSON.stringify({ songId: id })
744
+ }).then(async (res) => {
745
+ if (!res.ok) throw new Error("Failed to fetch NeteaseMusic song");
746
+ return res.json();
747
+ });
748
+ const songInfo = songData.songs[0];
749
+ const albumInfo = songInfo.al;
750
+ const singerInfo = songInfo.ar;
751
+ return {
752
+ title: /* @__PURE__ */ jsxs(Fragment, { children: [
753
+ /* @__PURE__ */ jsx("span", { children: songInfo.name }),
754
+ songInfo.tns && /* @__PURE__ */ jsx(
755
+ "span",
756
+ {
757
+ style: {
758
+ marginLeft: "8px",
759
+ fontSize: "0.875rem",
760
+ color: "#a1a1aa"
761
+ },
762
+ children: songInfo.tns[0]
763
+ }
764
+ )
765
+ ] }),
766
+ desc: /* @__PURE__ */ jsxs(Fragment, { children: [
767
+ /* @__PURE__ */ jsxs("span", { style: { display: "block" }, children: [
768
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: "bold" }, children: "歌手:" }),
769
+ /* @__PURE__ */ jsx("span", { children: singerInfo.map((p) => p.name).join(" / ") })
770
+ ] }),
771
+ /* @__PURE__ */ jsxs("span", { style: { display: "block" }, children: [
772
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: "bold" }, children: "专辑:" }),
773
+ /* @__PURE__ */ jsx("span", { children: albumInfo.name })
774
+ ] })
775
+ ] }),
776
+ image: albumInfo.picUrl,
777
+ color: "#e72d2c"
778
+ };
779
+ }
780
+ };
781
+ const qqMusicPlugin = {
782
+ name: "qq-music-song",
783
+ displayName: "QQ Music Song",
784
+ priority: 60,
785
+ typeClass: "wide",
786
+ matchUrl(url) {
787
+ if (url.hostname !== "y.qq.com") return null;
788
+ if (!url.pathname.includes("/songDetail/")) return null;
789
+ const parts = url.pathname.split("/");
790
+ const songDetailIndex = parts.indexOf("songDetail");
791
+ if (songDetailIndex === -1 || !parts[songDetailIndex + 1]) return null;
792
+ return { id: parts[songDetailIndex + 1], fullUrl: url.toString() };
793
+ },
794
+ isValidId(id) {
795
+ return typeof id === "string" && id.length > 0;
796
+ },
797
+ async fetch(id) {
798
+ const songData = await fetch("/api/music/tencent", {
799
+ method: "POST",
800
+ headers: { "Content-Type": "application/json" },
801
+ body: JSON.stringify({ songId: id })
802
+ }).then(async (res) => {
803
+ if (!res.ok) throw new Error("Failed to fetch QQMusic song");
804
+ return res.json();
805
+ });
806
+ const songInfo = songData.data[0];
807
+ const albumId = songInfo.album.mid;
808
+ return {
809
+ title: /* @__PURE__ */ jsxs(Fragment, { children: [
810
+ /* @__PURE__ */ jsx("span", { children: songInfo.title }),
811
+ songInfo.subtitle && /* @__PURE__ */ jsx(
812
+ "span",
813
+ {
814
+ style: {
815
+ marginLeft: "8px",
816
+ fontSize: "0.875rem",
817
+ color: "#a1a1aa"
818
+ },
819
+ children: songInfo.subtitle
820
+ }
821
+ )
822
+ ] }),
823
+ desc: /* @__PURE__ */ jsxs(Fragment, { children: [
824
+ /* @__PURE__ */ jsxs("span", { style: { display: "block" }, children: [
825
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: "bold" }, children: "歌手:" }),
826
+ /* @__PURE__ */ jsx("span", { children: songInfo.singer.map((p) => p.name).join(" / ") })
827
+ ] }),
828
+ /* @__PURE__ */ jsxs("span", { style: { display: "block" }, children: [
829
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: "bold" }, children: "专辑:" }),
830
+ /* @__PURE__ */ jsx("span", { children: songInfo.album.name })
831
+ ] })
832
+ ] }),
833
+ image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumId}.jpg?max_age=2592000`,
834
+ color: "#31c27c"
835
+ };
836
+ }
837
+ };
838
+ const TMDB_LANGUAGE_BY_LOCALE = {
839
+ zh: "zh-CN",
840
+ en: "en-US",
841
+ ja: "ja-JP"
842
+ };
843
+ const getCurrentLocale = () => {
844
+ if (typeof document !== "undefined") {
845
+ const lang = document.documentElement?.lang?.trim();
846
+ if (lang) return lang;
847
+ }
848
+ if (typeof window !== "undefined") {
849
+ const firstSegment = window.location.pathname.split("/").find((s) => s.length > 0);
850
+ if (firstSegment) return firstSegment;
851
+ }
852
+ return "en";
853
+ };
854
+ const tmdbPlugin = {
855
+ name: "tmdb",
856
+ displayName: "The Movie Database",
857
+ priority: 70,
858
+ typeClass: "media",
859
+ matchUrl(url) {
860
+ if (!url.hostname.includes("themoviedb.org")) return null;
861
+ const parts = url.pathname.split("/").filter(Boolean);
862
+ if (parts.length < 2) return null;
863
+ const [type, realId] = parts;
864
+ const canParsedTypes = ["tv", "movie"];
865
+ if (!canParsedTypes.includes(type) || !realId) return null;
866
+ return { id: `${type}/${realId}`, fullUrl: url.toString(), meta: { type } };
867
+ },
868
+ isValidId(id) {
869
+ const [type, realId] = id.split("/");
870
+ const canParsedTypes = ["tv", "movie"];
871
+ return canParsedTypes.includes(type) && realId?.length > 0;
872
+ },
873
+ async fetch(id) {
874
+ const [type, realId] = id.split("/");
875
+ const locale = getCurrentLocale();
876
+ const userLanguage = TMDB_LANGUAGE_BY_LOCALE[locale] || (typeof navigator !== "undefined" ? navigator.language : "en-US") || "en-US";
877
+ const json = await fetch(
878
+ `/api/tmdb/${type}/${realId}?language=${userLanguage}`
879
+ ).then((r) => r.json());
880
+ const title = type === "tv" ? json.name : json.title;
881
+ const originalTitle = type === "tv" ? json.original_name : json.original_title;
882
+ return {
883
+ title: /* @__PURE__ */ jsxs(
884
+ "span",
885
+ {
886
+ style: {
887
+ display: "flex",
888
+ flexWrap: "wrap",
889
+ alignItems: "flex-end",
890
+ gap: "8px"
891
+ },
892
+ children: [
893
+ /* @__PURE__ */ jsx("span", { children: title }),
894
+ title !== originalTitle && /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.875rem", opacity: 0.7 }, children: [
895
+ "(",
896
+ originalTitle,
897
+ ")"
898
+ ] }),
899
+ /* @__PURE__ */ jsxs(
900
+ "span",
901
+ {
902
+ style: {
903
+ display: "inline-flex",
904
+ flexShrink: 0,
905
+ alignItems: "center",
906
+ gap: "4px",
907
+ alignSelf: "center",
908
+ fontSize: "0.75rem",
909
+ color: "#fb923c"
910
+ },
911
+ children: [
912
+ "★",
913
+ /* @__PURE__ */ jsx("span", { style: { fontFamily: "sans-serif", fontWeight: 500 }, children: json.vote_average > 0 && json.vote_average.toFixed(1) })
914
+ ]
915
+ }
916
+ )
917
+ ]
918
+ }
919
+ ),
920
+ desc: /* @__PURE__ */ jsx("span", { style: { overflow: "visible", whiteSpace: "pre-wrap" }, children: json.overview }),
921
+ image: `https://image.tmdb.org/t/p/w500${json.poster_path}`,
922
+ color: generateColor(title || originalTitle || id),
923
+ classNames: {
924
+ image: "link-card__image--poster",
925
+ cardRoot: "link-card--reversed"
926
+ }
927
+ };
928
+ }
929
+ };
930
+ function createMxSpacePlugin(config) {
931
+ const { webUrl, apiBaseUrl } = config;
932
+ const webHost = new URL(webUrl).hostname;
933
+ return {
934
+ name: "self",
935
+ displayName: "MxSpace Article",
936
+ priority: 10,
937
+ matchUrl(url) {
938
+ if (webHost !== url.hostname) return null;
939
+ if (!url.pathname.startsWith("/posts/") && !url.pathname.startsWith("/notes/")) {
940
+ return null;
941
+ }
942
+ return { id: url.pathname.slice(1), fullUrl: url.toString() };
943
+ },
944
+ isValidId(id) {
945
+ const [type, ...rest] = id.split("/");
946
+ if (type !== "posts" && type !== "notes") return false;
947
+ if (type === "posts") return rest.length === 2;
948
+ return rest.length === 1;
949
+ },
950
+ async fetch(id) {
951
+ const [type, ...rest] = id.split("/");
952
+ const base = apiBaseUrl || "";
953
+ let data = { title: "", text: "" };
954
+ if (type === "posts") {
955
+ const [cate, slug] = rest;
956
+ data = await fetch(`${base}/api/v2/posts/${cate}/${slug}`).then(
957
+ (r) => r.json()
958
+ );
959
+ } else if (type === "notes") {
960
+ const [nid] = rest;
961
+ const response = await fetch(`${base}/api/v2/notes/${nid}`).then(
962
+ (r) => r.json()
963
+ );
964
+ data = response.data || response;
965
+ }
966
+ const coverImage = data.cover || data.meta?.cover;
967
+ const spotlightColor = generateColor(data.title);
968
+ return {
969
+ title: data.title,
970
+ desc: data.summary || `${stripMarkdown(data.text).slice(0, 50)}...`,
971
+ image: coverImage || data.images?.[0]?.src,
972
+ color: spotlightColor
973
+ };
974
+ }
975
+ };
976
+ }
977
+ const mxSpacePlugin = {
978
+ name: "self",
979
+ displayName: "MxSpace Article",
980
+ priority: 10,
981
+ matchUrl(_url) {
982
+ return null;
983
+ },
984
+ isValidId(id) {
985
+ const [type, ...rest] = id.split("/");
986
+ if (type !== "posts" && type !== "notes") return false;
987
+ if (type === "posts") return rest.length === 2;
988
+ return rest.length === 1;
989
+ },
990
+ async fetch(_id) {
991
+ throw new Error(
992
+ "MxSpace plugin requires configuration. Use createMxSpacePlugin()."
993
+ );
994
+ }
995
+ };
996
+ const plugins = [
997
+ githubRepoPlugin,
998
+ githubCommitPlugin,
999
+ githubPrPlugin,
1000
+ githubIssuePlugin,
1001
+ githubDiscussionPlugin,
1002
+ arxivPlugin,
1003
+ tmdbPlugin,
1004
+ bangumiPlugin,
1005
+ qqMusicPlugin,
1006
+ neteaseMusicPlugin,
1007
+ leetcodePlugin,
1008
+ mxSpacePlugin
1009
+ ].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
1010
+ function getPluginByName(name) {
1011
+ return plugins.find((p) => p.name === name);
1012
+ }
1013
+ const pluginMap = new Map(
1014
+ plugins.map((p) => [p.name, p])
1015
+ );
1016
+ function useUrlMatcher(url, pluginRegistry = plugins) {
1017
+ return useMemo(() => {
1018
+ if (!url) return null;
1019
+ try {
1020
+ const parsed = new URL(url);
1021
+ for (const plugin of pluginRegistry) {
1022
+ const match = plugin.matchUrl(parsed);
1023
+ if (match) return { plugin, match };
1024
+ }
1025
+ } catch {
1026
+ }
1027
+ return null;
1028
+ }, [url, pluginRegistry]);
1029
+ }
1030
+ function LinkCardEditDecorator({
1031
+ nodeKey,
1032
+ payload,
1033
+ children
1034
+ }) {
1035
+ const [editor] = useLexicalComposerContext();
1036
+ const editable = editor.isEditable();
1037
+ const [open, setOpen] = useState(false);
1038
+ const [url, setUrl] = useState(payload.url);
1039
+ const inputRef = useRef(null);
1040
+ useEffect(() => {
1041
+ setUrl(payload.url);
1042
+ }, [payload.url]);
1043
+ useEffect(() => {
1044
+ inputRef.current?.focus();
1045
+ inputRef.current?.select();
1046
+ }, []);
1047
+ const commitUrl = useCallback(() => {
1048
+ const trimmed = url.trim();
1049
+ if (!trimmed) return;
1050
+ editor.update(() => {
1051
+ const node = $getNodeByKey(nodeKey);
1052
+ if ($isLinkCardNode(node) && node.getUrl() !== trimmed) {
1053
+ node.setUrl(trimmed);
1054
+ }
1055
+ });
1056
+ }, [editor, nodeKey, url]);
1057
+ const handleDelete = useCallback(() => {
1058
+ editor.update(() => {
1059
+ const node = $getNodeByKey(nodeKey);
1060
+ if (node) node.remove();
1061
+ });
1062
+ setOpen(false);
1063
+ }, [editor, nodeKey]);
1064
+ const handleKeyDown = useCallback(
1065
+ (e) => {
1066
+ if (e.key === "Enter") {
1067
+ e.preventDefault();
1068
+ commitUrl();
1069
+ } else if (e.key === "Escape") {
1070
+ e.preventDefault();
1071
+ setUrl(payload.url);
1072
+ }
1073
+ },
1074
+ [commitUrl, payload.url]
1075
+ );
1076
+ const handleOpen = useCallback(() => {
1077
+ window.open(payload.url, "_blank", "noopener,noreferrer");
1078
+ }, [payload.url]);
1079
+ if (!editable) {
1080
+ return children;
1081
+ }
1082
+ return /* @__PURE__ */ jsxs(
1083
+ Popover,
1084
+ {
1085
+ open,
1086
+ onOpenChange: (nextOpen) => {
1087
+ setOpen(nextOpen);
1088
+ if (!nextOpen) {
1089
+ setUrl(payload.url);
1090
+ }
1091
+ },
1092
+ children: [
1093
+ /* @__PURE__ */ jsx(
1094
+ PopoverTrigger,
1095
+ {
1096
+ delay: 200,
1097
+ closeDelay: 300,
1098
+ openOnHover: true,
1099
+ render: /* @__PURE__ */ jsx("span", { className: "rich-link-card-edit-wrapper" }),
1100
+ children
1101
+ }
1102
+ ),
1103
+ /* @__PURE__ */ jsxs(
1104
+ PopoverPanel,
1105
+ {
1106
+ side: "bottom",
1107
+ sideOffset: 8,
1108
+ className: "rich-link-card-edit-panel",
1109
+ children: [
1110
+ /* @__PURE__ */ jsxs("div", { className: "rich-link-card-edit-url-row", children: [
1111
+ /* @__PURE__ */ jsx(Link, { className: "rich-link-card-edit-link-icon", size: 16 }),
1112
+ /* @__PURE__ */ jsx(
1113
+ "input",
1114
+ {
1115
+ ref: inputRef,
1116
+ className: "rich-link-card-edit-input",
1117
+ type: "url",
1118
+ value: url,
1119
+ onChange: (e) => setUrl(e.target.value),
1120
+ onBlur: commitUrl,
1121
+ onKeyDown: handleKeyDown,
1122
+ placeholder: "https://..."
1123
+ }
1124
+ )
1125
+ ] }),
1126
+ /* @__PURE__ */ jsxs("div", { className: "rich-link-card-edit-actions", children: [
1127
+ /* @__PURE__ */ jsxs(
1128
+ "button",
1129
+ {
1130
+ className: "rich-link-card-edit-action-btn",
1131
+ type: "button",
1132
+ onClick: handleOpen,
1133
+ children: [
1134
+ /* @__PURE__ */ jsx(ExternalLink, { size: 14 }),
1135
+ "Open"
1136
+ ]
1137
+ }
1138
+ ),
1139
+ /* @__PURE__ */ jsxs(
1140
+ "button",
1141
+ {
1142
+ className: "rich-link-card-edit-action-btn rich-link-card-edit-action-btn--end",
1143
+ type: "button",
1144
+ onClick: handleDelete,
1145
+ children: [
1146
+ /* @__PURE__ */ jsx(Unlink, { size: 14 }),
1147
+ "Remove"
1148
+ ]
1149
+ }
1150
+ )
1151
+ ] })
1152
+ ]
1153
+ }
1154
+ )
1155
+ ]
1156
+ }
1157
+ );
1158
+ }
1159
+ class LinkCardEditNode extends LinkCardNode {
1160
+ static clone(node) {
1161
+ return new LinkCardEditNode(
1162
+ {
1163
+ url: node.__url,
1164
+ title: node.__title,
1165
+ description: node.__description,
1166
+ favicon: node.__favicon,
1167
+ image: node.__image
1168
+ },
1169
+ node.__key
1170
+ );
1171
+ }
1172
+ static importJSON(serializedNode) {
1173
+ return new LinkCardEditNode({
1174
+ url: serializedNode.url,
1175
+ title: serializedNode.title,
1176
+ description: serializedNode.description,
1177
+ favicon: serializedNode.favicon,
1178
+ image: serializedNode.image
1179
+ });
1180
+ }
1181
+ decorate(_editor, _config) {
1182
+ const payload = {
1183
+ url: this.__url,
1184
+ title: this.__title,
1185
+ description: this.__description,
1186
+ favicon: this.__favicon,
1187
+ image: this.__image
1188
+ };
1189
+ const rendererEl = createRendererDecoration(
1190
+ "LinkCard",
1191
+ LinkCardRenderer$1,
1192
+ payload
1193
+ );
1194
+ return createElement(LinkCardEditDecorator, {
1195
+ nodeKey: this.__key,
1196
+ payload,
1197
+ children: rendererEl
1198
+ });
1199
+ }
1200
+ }
1201
+ const linkCardEditNodes = [LinkCardEditNode];
1202
+ function useCardFetcher(options) {
1203
+ const { source, plugin, id, fallbackUrl, enabled = true, context } = options;
1204
+ const [loading, setLoading] = useState(true);
1205
+ const [isError, setIsError] = useState(false);
1206
+ const [fullUrl] = useState(fallbackUrl || "javascript:;");
1207
+ const [cardInfo, setCardInfo] = useState();
1208
+ const isValid = useMemo(() => {
1209
+ if (!enabled || !plugin) return false;
1210
+ return plugin.isValidId(id);
1211
+ }, [plugin, enabled, id]);
1212
+ const fetchInfo = useCallback(async () => {
1213
+ if (!plugin || !isValid) return;
1214
+ setLoading(true);
1215
+ setIsError(false);
1216
+ try {
1217
+ const data = await plugin.fetch(id, void 0, context);
1218
+ setCardInfo(data);
1219
+ } catch (err) {
1220
+ console.error(
1221
+ `[LinkCard] Error fetching ${source || plugin.name} data:`,
1222
+ err
1223
+ );
1224
+ setIsError(true);
1225
+ } finally {
1226
+ setLoading(false);
1227
+ }
1228
+ }, [context, plugin, isValid, id, source]);
1229
+ const { ref } = useInView({
1230
+ triggerOnce: true,
1231
+ onChange(inView) {
1232
+ if (!inView || !enabled) return;
1233
+ fetchInfo();
1234
+ }
1235
+ });
1236
+ return {
1237
+ loading,
1238
+ isError,
1239
+ cardInfo,
1240
+ fullUrl,
1241
+ isValid,
1242
+ ref
1243
+ };
1244
+ }
1245
+ function LinkCardSkeleton({
1246
+ source,
1247
+ className
1248
+ }) {
1249
+ const plugin = source ? pluginMap.get(source) : void 0;
1250
+ const typeClass = plugin?.typeClass ? `link-card--${plugin.typeClass}` : "";
1251
+ return /* @__PURE__ */ jsxs(
1252
+ "span",
1253
+ {
1254
+ "data-hide-print": true,
1255
+ "data-source": source || void 0,
1256
+ className: ["link-card", "link-card--skeleton", typeClass, className].filter(Boolean).join(" "),
1257
+ children: [
1258
+ /* @__PURE__ */ jsx("span", { className: "link-card__image" }),
1259
+ /* @__PURE__ */ jsxs("span", { className: "link-card__content", children: [
1260
+ /* @__PURE__ */ jsx("span", { className: "link-card__title", children: /* @__PURE__ */ jsx("span", { className: "link-card__title-text" }) }),
1261
+ /* @__PURE__ */ jsx("span", { className: "link-card__desc" }),
1262
+ /* @__PURE__ */ jsx("span", { className: "link-card__desc link-card__desc-2" })
1263
+ ] })
1264
+ ]
1265
+ }
1266
+ );
1267
+ }
1268
+ function FallbackIcon({ favicon }) {
1269
+ const [faviconFailed, setFaviconFailed] = useState(false);
1270
+ return /* @__PURE__ */ jsx("span", { className: "link-card__icon", children: favicon && !faviconFailed ? /* @__PURE__ */ jsx("img", { src: favicon, alt: "", onError: () => setFaviconFailed(true) }) : /* @__PURE__ */ jsx(Globe, { "aria-hidden": "true" }) });
1271
+ }
1272
+ const LinkCardRenderer = (props) => {
1273
+ const {
1274
+ url,
1275
+ title,
1276
+ description,
1277
+ favicon,
1278
+ image,
1279
+ source: explicitSource,
1280
+ id: explicitId,
1281
+ className,
1282
+ plugins: plugins$1 = plugins,
1283
+ fetchContext
1284
+ } = props;
1285
+ const pluginMap2 = useMemo(
1286
+ () => new Map(plugins$1.map((plugin) => [plugin.name, plugin])),
1287
+ [plugins$1]
1288
+ );
1289
+ const urlMatch = useUrlMatcher(
1290
+ !explicitSource || !explicitId ? url : void 0,
1291
+ plugins$1
1292
+ );
1293
+ const source = explicitSource || urlMatch?.plugin.name;
1294
+ const id = explicitId || urlMatch?.match.id;
1295
+ const matchedFullUrl = urlMatch?.match.fullUrl;
1296
+ const useDynamicFetch = !!source && !!id;
1297
+ const selectedPlugin = source ? pluginMap2.get(source) : void 0;
1298
+ const { loading, isError, cardInfo, fullUrl, isValid, ref } = useCardFetcher({
1299
+ source,
1300
+ plugin: selectedPlugin,
1301
+ id: id || "",
1302
+ fallbackUrl: matchedFullUrl || url,
1303
+ enabled: useDynamicFetch,
1304
+ context: fetchContext
1305
+ });
1306
+ const typeClass = selectedPlugin?.typeClass ? `link-card--${selectedPlugin.typeClass}` : "";
1307
+ const finalTitle = cardInfo?.title || title || url;
1308
+ const finalDesc = cardInfo?.desc || description;
1309
+ const finalImage = cardInfo?.image || image;
1310
+ const finalColor = cardInfo?.color;
1311
+ const classNames = cardInfo?.classNames || {};
1312
+ const [shortDesc, setShortDesc] = useState(false);
1313
+ const descRef = useRef(null);
1314
+ useLayoutEffect(() => {
1315
+ const el = descRef.current;
1316
+ if (!el || !finalDesc) {
1317
+ setShortDesc(false);
1318
+ return;
1319
+ }
1320
+ const style = getComputedStyle(el);
1321
+ const lineHeight = Number.parseFloat(style.lineHeight) || 1.5 * 14;
1322
+ const maxTwoLines = lineHeight * 2;
1323
+ setShortDesc(el.scrollHeight <= maxTwoLines + 1);
1324
+ }, [finalDesc, finalTitle]);
1325
+ if (useDynamicFetch && !isValid) {
1326
+ return null;
1327
+ }
1328
+ if (useDynamicFetch && loading) {
1329
+ return /* @__PURE__ */ jsx(
1330
+ "a",
1331
+ {
1332
+ "data-hide-print": true,
1333
+ ref,
1334
+ href: fullUrl,
1335
+ target: "_blank",
1336
+ rel: "noopener noreferrer",
1337
+ children: /* @__PURE__ */ jsx(LinkCardSkeleton, { source })
1338
+ }
1339
+ );
1340
+ }
1341
+ const hasImage = !!finalImage;
1342
+ const showImagePlaceholder = useDynamicFetch && isError && !hasImage;
1343
+ return /* @__PURE__ */ jsxs(
1344
+ "a",
1345
+ {
1346
+ "data-hide-print": true,
1347
+ ref: useDynamicFetch ? ref : void 0,
1348
+ className: [
1349
+ "link-card",
1350
+ typeClass,
1351
+ shortDesc && "link-card--short-desc",
1352
+ useDynamicFetch && (loading || isError) && "link-card--skeleton",
1353
+ useDynamicFetch && isError && "link-card--error",
1354
+ "not-prose",
1355
+ className,
1356
+ classNames.cardRoot
1357
+ ].filter(Boolean).join(" "),
1358
+ "data-source": source || void 0,
1359
+ href: useDynamicFetch ? fullUrl : url,
1360
+ target: "_blank",
1361
+ rel: "noopener noreferrer",
1362
+ style: {
1363
+ borderColor: finalColor ? `${finalColor}30` : void 0
1364
+ },
1365
+ children: [
1366
+ finalColor && /* @__PURE__ */ jsx(
1367
+ "div",
1368
+ {
1369
+ className: "link-card__bg",
1370
+ style: {
1371
+ backgroundColor: finalColor,
1372
+ opacity: 0.04
1373
+ }
1374
+ }
1375
+ ),
1376
+ hasImage || showImagePlaceholder ? /* @__PURE__ */ jsx(
1377
+ "span",
1378
+ {
1379
+ className: ["link-card__image", classNames.image].filter(Boolean).join(" "),
1380
+ "data-image": finalImage || "",
1381
+ style: {
1382
+ backgroundImage: finalImage ? `url(${finalImage})` : void 0
1383
+ }
1384
+ }
1385
+ ) : /* @__PURE__ */ jsx(FallbackIcon, { favicon }),
1386
+ /* @__PURE__ */ jsxs("span", { className: "link-card__content", children: [
1387
+ /* @__PURE__ */ jsxs("span", { className: "link-card__title", children: [
1388
+ /* @__PURE__ */ jsx("span", { className: "link-card__title-text", children: finalTitle }),
1389
+ /* @__PURE__ */ jsx(
1390
+ ExternalLink,
1391
+ {
1392
+ className: "link-card__external",
1393
+ "aria-hidden": "true",
1394
+ focusable: false
1395
+ }
1396
+ )
1397
+ ] }),
1398
+ finalDesc && /* @__PURE__ */ jsx("span", { ref: descRef, className: "link-card__desc", children: finalDesc })
1399
+ ] })
1400
+ ]
1401
+ }
1402
+ );
1403
+ };
1404
+ export {
1405
+ LanguageToColorMap,
1406
+ LinkCardEditDecorator,
1407
+ LinkCardEditNode,
1408
+ LinkCardRenderer,
1409
+ LinkCardSkeleton,
1410
+ arxivPlugin,
1411
+ bangumiPlugin,
1412
+ camelcaseKeys,
1413
+ createMxSpacePlugin,
1414
+ fetchGitHubApi,
1415
+ fetchJsonWithContext,
1416
+ generateColor,
1417
+ getPluginByName,
1418
+ githubCommitPlugin,
1419
+ githubDiscussionPlugin,
1420
+ githubIssuePlugin,
1421
+ githubPrPlugin,
1422
+ githubRepoPlugin,
1423
+ leetcodePlugin,
1424
+ linkCardEditNodes,
1425
+ mxSpacePlugin,
1426
+ neteaseMusicPlugin,
1427
+ pluginMap,
1428
+ plugins,
1429
+ qqMusicPlugin,
1430
+ tmdbPlugin,
1431
+ useUrlMatcher
1432
+ };