@curvenote/renderers 1.0.1 → 2.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/dist/articles.d.ts.map +1 -1
- package/dist/articles.js +8 -2
- package/dist/components/admonition.d.ts.map +1 -1
- package/dist/components/admonition.js +24 -4
- package/dist/components/anywidget-host.d.ts +7 -0
- package/dist/components/anywidget-host.d.ts.map +1 -0
- package/dist/components/anywidget-host.js +11 -0
- package/dist/components/bluesky.d.ts +6 -0
- package/dist/components/bluesky.d.ts.map +1 -0
- package/dist/components/bluesky.js +113 -0
- package/dist/components/cite-figurebar.d.ts +2 -1
- package/dist/components/cite-figurebar.d.ts.map +1 -1
- package/dist/components/cite-figurebar.js +73 -27
- package/dist/components/cite.d.ts +11 -0
- package/dist/components/cite.d.ts.map +1 -1
- package/dist/components/cite.js +130 -8
- package/dist/components/copy-icon.d.ts +11 -0
- package/dist/components/copy-icon.d.ts.map +1 -0
- package/dist/components/copy-icon.js +32 -0
- package/dist/components/definition-list.d.ts.map +1 -1
- package/dist/components/definition-list.js +3 -2
- package/dist/components/faq.js +1 -1
- package/dist/components/hash-link.d.ts +3 -0
- package/dist/components/hash-link.d.ts.map +1 -0
- package/dist/components/hash-link.js +3 -0
- package/dist/components/hero.d.ts +1 -1
- package/dist/components/hero.d.ts.map +1 -1
- package/dist/components/hero.js +14 -2
- package/dist/components/hover-popover.d.ts +26 -0
- package/dist/components/hover-popover.d.ts.map +1 -0
- package/dist/components/hover-popover.js +26 -0
- package/dist/components/images.d.ts.map +1 -1
- package/dist/components/images.js +7 -2
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -0
- package/dist/components/pdb.d.ts.map +1 -1
- package/dist/components/pdb.js +42 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/myst/abbreviation.d.ts +6 -0
- package/dist/myst/abbreviation.d.ts.map +1 -0
- package/dist/myst/abbreviation.js +13 -0
- package/dist/myst/anywidget.d.ts +5 -0
- package/dist/myst/anywidget.d.ts.map +1 -0
- package/dist/myst/anywidget.js +12 -0
- package/dist/myst/code.d.ts +6 -0
- package/dist/myst/code.d.ts.map +1 -0
- package/dist/myst/code.js +103 -0
- package/dist/myst/container.d.ts +7 -0
- package/dist/myst/container.d.ts.map +1 -0
- package/dist/myst/container.js +57 -0
- package/dist/myst/cross-reference.d.ts +23 -0
- package/dist/myst/cross-reference.d.ts.map +1 -0
- package/dist/myst/cross-reference.js +140 -0
- package/dist/myst/footnotes.d.ts +7 -0
- package/dist/myst/footnotes.d.ts.map +1 -0
- package/dist/myst/footnotes.js +32 -0
- package/dist/myst/index.d.ts +14 -0
- package/dist/myst/index.d.ts.map +1 -0
- package/dist/myst/index.js +17 -0
- package/dist/myst/inline-expression.d.ts +6 -0
- package/dist/myst/inline-expression.d.ts.map +1 -0
- package/dist/myst/inline-expression.js +20 -0
- package/dist/myst/links/figshare.d.ts +8 -0
- package/dist/myst/links/figshare.d.ts.map +1 -0
- package/dist/myst/links/figshare.js +45 -0
- package/dist/myst/links/geo.d.ts +8 -0
- package/dist/myst/links/geo.d.ts.map +1 -0
- package/dist/myst/links/geo.js +116 -0
- package/dist/myst/links/github.d.ts +15 -0
- package/dist/myst/links/github.d.ts.map +1 -0
- package/dist/myst/links/github.js +156 -0
- package/dist/myst/links/huggingface.d.ts +10 -0
- package/dist/myst/links/huggingface.d.ts.map +1 -0
- package/dist/myst/links/huggingface.js +73 -0
- package/dist/myst/links/index.d.ts +12 -0
- package/dist/myst/links/index.d.ts.map +1 -0
- package/dist/myst/links/index.js +95 -0
- package/dist/myst/links/ror.d.ts +7 -0
- package/dist/myst/links/ror.d.ts.map +1 -0
- package/dist/myst/links/ror.js +34 -0
- package/dist/myst/links/rrid.d.ts +5 -0
- package/dist/myst/links/rrid.d.ts.map +1 -0
- package/dist/myst/links/rrid.js +31 -0
- package/dist/myst/links/wiki.d.ts +9 -0
- package/dist/myst/links/wiki.d.ts.map +1 -0
- package/dist/myst/links/wiki.js +41 -0
- package/dist/myst/math.d.ts +15 -0
- package/dist/myst/math.d.ts.map +1 -0
- package/dist/myst/math.js +53 -0
- package/dist/transforms/articles.d.ts +2 -1
- package/dist/transforms/articles.d.ts.map +1 -1
- package/dist/transforms/articles.js +2 -2
- package/dist/transforms/index.d.ts +5 -2
- package/dist/transforms/index.d.ts.map +1 -1
- package/dist/transforms/index.js +1 -0
- package/dist/utils/anywidget-analytics.d.ts +13 -0
- package/dist/utils/anywidget-analytics.d.ts.map +1 -0
- package/dist/utils/anywidget-analytics.js +15 -0
- package/dist/utils/content-analytics.d.ts +44 -0
- package/dist/utils/content-analytics.d.ts.map +1 -0
- package/dist/utils/content-analytics.js +59 -0
- package/package.json +22 -13
package/dist/articles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["../src/articles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["../src/articles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAgD1D,eAAO,MAAM,kBAAkB;;CAA0C,CAAC"}
|
package/dist/articles.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArticleCard } from '@curvenote/theme-ui';
|
|
2
|
+
import { ArticleCard, useAnalytics } from '@curvenote/theme-ui';
|
|
3
3
|
const ArticlesRenderer = ({ node }) => {
|
|
4
|
-
|
|
4
|
+
const capture = useAnalytics();
|
|
5
|
+
return (_jsxs("div", { children: [!node.error && (_jsx("div", { className: "space-y-6", children: node.items.map((article) => (_jsx("div", { onClick: () => capture('content_article_card_clicked', {
|
|
6
|
+
surface: 'content',
|
|
7
|
+
articleId: article.id,
|
|
8
|
+
articleSlug: article.slug,
|
|
9
|
+
articleTitle: article.title,
|
|
10
|
+
}), children: _jsx(ArticleCard, { to: `/articles/${article.slug ?? article.id}`, article: article, showThumbnails: node['show-thumbnails'] ?? false, showAuthors: node['show-authors'] ?? false, showDoi: node['show-doi'] ?? false, showDate: node['show-date'] ?? false }) }, article.id))) })), !node.error && node.items.length === 0 && (_jsx("div", { className: "px-2 py-4 text-gray-600 border-gray-500 border-gray-[1px] rounded-sm bg-gray-50", children: "There are no articles in this listing yet." })), node.error && (_jsx("div", { className: "text-red-500 p-4 my-2 bg-red-50 border-[1px] border-red-500 rounded-sm", children: node.error }))] }));
|
|
5
11
|
};
|
|
6
12
|
export const ARTICLES_RENDERERS = { curvenoteArticles: ArticlesRenderer };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admonition.d.ts","sourceRoot":"","sources":["../../src/components/admonition.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"admonition.d.ts","sourceRoot":"","sources":["../../src/components/admonition.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA+B1D,oBAAY,cAAc;IACxB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,GAAG,QAAQ;IACX,OAAO,YAAY;CACpB;AAoRD,eAAO,MAAM,oBAAoB;;CAAqC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Collapsible, CollapsibleContent, CollapsibleTrigger, Button, SimpleTooltip, } from '@curvenote/theme-ui';
|
|
2
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger, Button, SimpleTooltip, useAnalytics, } from '@curvenote/theme-ui';
|
|
3
3
|
import { ChevronRight, Link as LinkIcon, Info, AlertTriangle, AlertCircle, XCircle, Megaphone, Lightbulb, Zap, ArrowRight, SquarePen, } from 'lucide-react';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { MyST } from 'myst-to-react';
|
|
@@ -54,7 +54,7 @@ const admonitionVariants = cva('border border-muted border-l-2 bg-muted/30', {
|
|
|
54
54
|
variant: 'default',
|
|
55
55
|
},
|
|
56
56
|
});
|
|
57
|
-
const iconVariants = cva('w-5 h-5 mt-0.5
|
|
57
|
+
const iconVariants = cva('w-5 h-5 mt-0.5 shrink-0 self-center', {
|
|
58
58
|
variants: {
|
|
59
59
|
color: {
|
|
60
60
|
blue: 'text-blue-600 dark:text-blue-400',
|
|
@@ -108,16 +108,28 @@ function getFirstKind({ kind, classes = [], }) {
|
|
|
108
108
|
return ADMONITION_CONFIGS[AdmonitionKind.note];
|
|
109
109
|
}
|
|
110
110
|
const AdmonitionComponent = ({ id, title, content, defaultOpen, config, isDropdown, hideIcon, className, }) => {
|
|
111
|
+
const capture = useAnalytics();
|
|
111
112
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
112
113
|
const IconComponent = config.icon;
|
|
113
114
|
const handleCopyLink = async () => {
|
|
114
115
|
const url = `${window.location.origin}${window.location.pathname}#${id}`;
|
|
115
116
|
try {
|
|
116
117
|
await navigator.clipboard.writeText(url);
|
|
117
|
-
toast.success('Link copied to clipboard');
|
|
118
118
|
}
|
|
119
119
|
catch (err) {
|
|
120
120
|
console.error('Failed to copy link:', err);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
toast.success('Link copied to clipboard');
|
|
124
|
+
try {
|
|
125
|
+
capture('content_admonition_link_copied', {
|
|
126
|
+
surface: 'content',
|
|
127
|
+
admonitionKind: config.kind,
|
|
128
|
+
targetId: id,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// best-effort analytics — errors must not surface to the user here
|
|
121
133
|
}
|
|
122
134
|
};
|
|
123
135
|
if (!isDropdown) {
|
|
@@ -126,7 +138,15 @@ const AdmonitionComponent = ({ id, title, content, defaultOpen, config, isDropdo
|
|
|
126
138
|
return (_jsx("div", { className: "overflow-hidden my-4 rounded-xs", children: _jsx("div", { className: cn(admonitionVariants({
|
|
127
139
|
color: config.color,
|
|
128
140
|
variant: 'collapsible',
|
|
129
|
-
}), className), children: _jsxs(Collapsible, { open: isOpen, onOpenChange:
|
|
141
|
+
}), className), children: _jsxs(Collapsible, { open: isOpen, onOpenChange: (open) => {
|
|
142
|
+
setIsOpen(open);
|
|
143
|
+
capture('content_admonition_toggled', {
|
|
144
|
+
surface: 'content',
|
|
145
|
+
admonitionKind: config.kind,
|
|
146
|
+
targetId: id,
|
|
147
|
+
open,
|
|
148
|
+
});
|
|
149
|
+
}, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { className: "flex justify-between items-start p-4 w-full text-left cursor-pointer", children: [_jsxs("div", { className: "flex gap-3 items-start", children: [_jsx("div", { className: "shrink-0 mt-1.5", children: _jsx(ChevronRight, { className: cn('w-4 h-4 transition-transform duration-200', {
|
|
130
150
|
'rotate-90': isOpen,
|
|
131
151
|
}) }) }), _jsxs("div", { className: "flex gap-3 items-start", children: [!hideIcon && (_jsx(IconComponent, { className: iconVariants({ color: config.color }) })), _jsx("span", { className: "font-medium text-foreground", children: title })] })] }), _jsx(SimpleTooltip, { title: "Copy link", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => {
|
|
132
152
|
e.stopPropagation();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type AnywidgetIdentity } from '../utils/anywidget-analytics';
|
|
3
|
+
export declare function AnywidgetClickHost({ children, identity, }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
identity: AnywidgetIdentity;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=anywidget-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anywidget-host.d.ts","sourceRoot":"","sources":["../../src/components/anywidget-host.tsx"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAE/F,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,iBAAiB,CAAC;CAC7B,2CAYA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useAnalytics } from '@curvenote/theme-ui';
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { captureAnywidgetClicked } from '../utils/anywidget-analytics';
|
|
5
|
+
export function AnywidgetClickHost({ children, identity, }) {
|
|
6
|
+
const capture = useAnalytics();
|
|
7
|
+
const handleClick = useCallback(() => {
|
|
8
|
+
captureAnywidgetClicked(capture, identity);
|
|
9
|
+
}, [capture, identity]);
|
|
10
|
+
return (_jsx("div", { className: "relative w-full", style: { position: 'relative' }, onClick: handleClick, children: children }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { GenericNode } from 'myst-common';
|
|
2
|
+
import type { NodeRenderer, NodeRenderers } from '@myst-theme/providers';
|
|
3
|
+
export declare const BlueskyBlockRenderer: NodeRenderer<GenericNode>;
|
|
4
|
+
export declare const BlueskyLinkRenderer: NodeRenderer<GenericNode>;
|
|
5
|
+
export declare const BLUESKY_LINK_RENDERERS: NodeRenderers;
|
|
6
|
+
//# sourceMappingURL=bluesky.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bluesky.d.ts","sourceRoot":"","sources":["../../src/components/bluesky.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAsNzE,eAAO,MAAM,oBAAoB,EAAE,YAAY,CAAC,WAAW,CAI1D,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,YAAY,CAAC,WAAW,CAsCzD,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,aAOpC,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { default as useSWR } from 'swr';
|
|
3
|
+
import { MyST } from 'myst-to-react';
|
|
4
|
+
import { HoverPopover } from './hover-popover';
|
|
5
|
+
import { useAnalytics } from '@curvenote/theme-ui';
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { captureContentLinkClicked, useContentEnrichmentDisplay, } from '../utils/content-analytics';
|
|
8
|
+
// ─── Bluesky logo SVG ──────────────────────────────────────────────────────────
|
|
9
|
+
// Official logomark from github.com/bluesky-social/social-app
|
|
10
|
+
function BskyIcon() {
|
|
11
|
+
return (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "currentColor", xmlns: "http://www.w3.org/2000/svg", className: "flex-shrink-0", children: _jsx("path", { d: "M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z" }) }));
|
|
12
|
+
}
|
|
13
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
+
function fmt(n) {
|
|
15
|
+
if (n == null)
|
|
16
|
+
return '0';
|
|
17
|
+
if (n >= 1_000_000)
|
|
18
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
19
|
+
if (n >= 1_000)
|
|
20
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
21
|
+
return String(n);
|
|
22
|
+
}
|
|
23
|
+
const profileFetcher = (url) => fetch(url).then(async (res) => {
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const body = await res.json().catch(() => ({}));
|
|
26
|
+
throw new Error(body.message || res.statusText);
|
|
27
|
+
}
|
|
28
|
+
return res.json();
|
|
29
|
+
});
|
|
30
|
+
// ─── Profile card ─────────────────────────────────────────────────────────────
|
|
31
|
+
function BlueskyProfileCard({ handle, engagementSurface = 'hover_card', }) {
|
|
32
|
+
const capture = useAnalytics();
|
|
33
|
+
const apiUrl = `https://api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`;
|
|
34
|
+
const { data, error } = useSWR(apiUrl, profileFetcher, {
|
|
35
|
+
revalidateOnFocus: false,
|
|
36
|
+
revalidateOnReconnect: false,
|
|
37
|
+
dedupingInterval: 60_000,
|
|
38
|
+
errorRetryCount: 2,
|
|
39
|
+
errorRetryInterval: 1_000,
|
|
40
|
+
});
|
|
41
|
+
const profileUrl = `https://bsky.app/profile/${handle}`;
|
|
42
|
+
const captureProfileLinkClick = useCallback(() => {
|
|
43
|
+
captureContentLinkClicked(capture, {
|
|
44
|
+
linkKind: 'bluesky_profile',
|
|
45
|
+
destinationUrl: profileUrl,
|
|
46
|
+
engagementSurface,
|
|
47
|
+
cardKind: 'bluesky_profile',
|
|
48
|
+
properties: { handle },
|
|
49
|
+
});
|
|
50
|
+
}, [capture, engagementSurface, handle, profileUrl]);
|
|
51
|
+
const enrichmentOutcome = !data && !error ? 'loading' : error || !data ? 'error' : 'success';
|
|
52
|
+
useContentEnrichmentDisplay({
|
|
53
|
+
capture,
|
|
54
|
+
enrichmentSource: 'bluesky_profile',
|
|
55
|
+
outcome: enrichmentOutcome,
|
|
56
|
+
cardKind: 'bluesky_profile',
|
|
57
|
+
engagementSurface,
|
|
58
|
+
properties: { handle },
|
|
59
|
+
});
|
|
60
|
+
// Skeleton
|
|
61
|
+
if (!data && !error) {
|
|
62
|
+
return (_jsxs("div", { className: "w-[420px] max-w-[420px] rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-[#16202a] shadow-md", children: [_jsx("div", { className: "w-full h-[120px] bg-gradient-to-br from-[#0085ff] via-[#0060c4] to-[#6a00f4]" }), _jsxs("div", { className: "px-4 pb-4", children: [_jsx("div", { className: "mt-[-36px] mb-2.5 inline-block", children: _jsx("div", { className: "w-[72px] h-[72px] rounded-full border-[3px] border-white dark:border-[#16202a] bg-gray-200 dark:bg-gray-700 animate-pulse" }) }), _jsx("div", { className: "h-5 w-[55%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2" }), _jsx("div", { className: "h-3.5 w-[35%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-3.5" }), _jsx("div", { className: "h-3 w-[90%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-1.5" }), _jsx("div", { className: "h-3 w-[55%] bg-gray-200 dark:bg-gray-700 rounded animate-pulse" })] })] }));
|
|
63
|
+
}
|
|
64
|
+
// Error
|
|
65
|
+
if (error || !data) {
|
|
66
|
+
return (_jsx("div", { className: "w-[420px] max-w-[420px] rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-[#16202a] shadow-md", children: _jsxs("div", { className: "flex items-start gap-2.5 p-5 text-sm text-red-500 dark:text-red-400", children: [_jsx("span", { children: "\u26A0" }), _jsxs("span", { children: ["Failed to load @", handle, ": ", _jsx("em", { children: error?.message })] })] }) }));
|
|
67
|
+
}
|
|
68
|
+
const initials = (data.displayName || data.handle || '?')[0].toUpperCase();
|
|
69
|
+
return (_jsxs("div", { className: "w-[420px] max-w-[420px] rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-700 bg-white dark:bg-[#16202a] text-[#0f1419] dark:text-[#e7e9ea] shadow-md", children: [_jsx("div", { className: "w-full h-[120px] overflow-hidden", children: data.banner ? (_jsx("img", { src: data.banner, alt: "", className: "block object-cover my-0 w-full h-full" })) : (_jsx("div", { className: "w-full h-full bg-gradient-to-br from-[#0085ff] via-[#0060c4] to-[#6a00f4]" })) }), _jsxs("div", { className: "px-4 pb-3.5", children: [_jsx("div", { className: "mt-[-36px] mb-2.5 inline-block", children: data.avatar ? (_jsx("img", { src: data.avatar, alt: data.displayName || data.handle, className: "w-[72px] h-[72px] rounded-full object-cover border-[3px] border-white dark:border-[#16202a] shadow my-0" })) : (_jsx("div", { className: "w-[72px] h-[72px] rounded-full bg-gradient-to-br from-[#0085ff] to-[#6a00f4] flex items-center justify-center text-white text-3xl font-bold border-[3px] border-white dark:border-[#16202a] shadow", children: initials })) }), _jsxs("div", { className: "mb-2", children: [_jsx("div", { className: "text-lg font-extrabold tracking-tight leading-tight", children: data.displayName || data.handle }), _jsxs("div", { className: "text-sm text-[#536471] dark:text-[#8b98a5] mt-0.5", children: ["@", data.handle] })] }), data.description && (_jsx("p", { className: "mb-3 text-sm leading-snug whitespace-pre-wrap break-words", children: data.description })), _jsx("div", { className: "flex flex-wrap gap-4 mb-3", children: [
|
|
70
|
+
[fmt(data.followersCount), 'Followers'],
|
|
71
|
+
[fmt(data.followsCount), 'Following'],
|
|
72
|
+
[fmt(data.postsCount), 'Posts'],
|
|
73
|
+
].map(([num, label]) => (_jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { className: "text-sm font-bold", children: num }), _jsx("span", { className: "text-xs text-[#536471] dark:text-[#8b98a5]", children: label })] }, label))) }), _jsx("div", { className: "flex justify-end mt-1", children: _jsxs("a", { href: profileUrl, target: "_blank", rel: "noopener noreferrer", onClick: (e) => {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
captureProfileLinkClick();
|
|
76
|
+
}, className: "text-xs text-[#0085ff] no-underline inline-flex items-center gap-1 opacity-70 hover:opacity-100 hover:underline transition-opacity", children: [_jsx(BskyIcon, {}), "View on Bluesky"] }) })] })] }));
|
|
77
|
+
}
|
|
78
|
+
// ─── Renderers ────────────────────────────────────────────────────────────────
|
|
79
|
+
export const BlueskyBlockRenderer = ({ node }) => {
|
|
80
|
+
const { handle } = (node.data ?? {});
|
|
81
|
+
if (!handle)
|
|
82
|
+
return null;
|
|
83
|
+
return _jsx(BlueskyProfileCard, { handle: handle, engagementSurface: "inline_block" });
|
|
84
|
+
};
|
|
85
|
+
export const BlueskyLinkRenderer = ({ node }) => {
|
|
86
|
+
const capture = useAnalytics();
|
|
87
|
+
const { handle, kind } = (node.data ?? {});
|
|
88
|
+
const captureTriggerClick = useCallback(() => {
|
|
89
|
+
if (!node.url)
|
|
90
|
+
return;
|
|
91
|
+
captureContentLinkClicked(capture, {
|
|
92
|
+
linkKind: kind === 'profile' ? 'bluesky_profile' : 'bluesky_post',
|
|
93
|
+
destinationUrl: node.url,
|
|
94
|
+
engagementSurface: 'trigger',
|
|
95
|
+
cardKind: kind === 'profile' ? 'bluesky_profile' : undefined,
|
|
96
|
+
properties: { handle, blueskyKind: kind },
|
|
97
|
+
});
|
|
98
|
+
}, [capture, handle, kind, node.url]);
|
|
99
|
+
// Only profile cards are implemented; fall back to a plain link for posts
|
|
100
|
+
// or any URL we couldn't fully parse.
|
|
101
|
+
if (!handle || kind !== 'profile') {
|
|
102
|
+
return (_jsx("a", { href: node.url, target: "_blank", rel: "noopener noreferrer", onClick: captureTriggerClick, children: _jsx(MyST, { ast: node.children }) }));
|
|
103
|
+
}
|
|
104
|
+
return (_jsx(HoverPopover, { card: _jsx(BlueskyProfileCard, { handle: handle, engagementSurface: "hover_card" }), analytics: { cardKind: 'bluesky_profile', properties: { handle } }, children: _jsx("a", { href: node.url, target: "_blank", rel: "noopener noreferrer", onClick: captureTriggerClick, children: _jsx(MyST, { ast: node.children }) }) }));
|
|
105
|
+
};
|
|
106
|
+
export const BLUESKY_LINK_RENDERERS = {
|
|
107
|
+
link: {
|
|
108
|
+
'link[protocol=bluesky]': BlueskyLinkRenderer,
|
|
109
|
+
},
|
|
110
|
+
block: {
|
|
111
|
+
'block[kind=bluesky]': BlueskyBlockRenderer,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export declare function CiteFigureBar({ doi: doiString, className }: {
|
|
1
|
+
export declare function CiteFigureBar({ doi: doiString, citeLabel, className, }: {
|
|
2
2
|
doi: string;
|
|
3
|
+
citeLabel: string;
|
|
3
4
|
className?: string;
|
|
4
5
|
}): import("react/jsx-runtime").JSX.Element | null;
|
|
5
6
|
//# sourceMappingURL=cite-figurebar.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cite-figurebar.d.ts","sourceRoot":"","sources":["../../src/components/cite-figurebar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cite-figurebar.d.ts","sourceRoot":"","sources":["../../src/components/cite-figurebar.tsx"],"names":[],"mappings":"AAkJA,wBAAgB,aAAa,CAAC,EAC5B,GAAG,EAAE,SAAS,EACd,SAAS,EACT,SAAS,GACV,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,kDA4EA"}
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import useSWR from 'swr';
|
|
3
4
|
import { selectAll } from 'unist-util-select';
|
|
4
5
|
import { Image, LightboxContainer } from '@curvenote/theme-ui';
|
|
5
6
|
import { cn } from '@curvenote/react-utils';
|
|
6
7
|
import { MyST } from 'myst-to-react';
|
|
7
8
|
import { doi } from 'doi-utils';
|
|
9
|
+
import { useAnalytics } from '@curvenote/theme-ui';
|
|
10
|
+
import { useContentEnrichmentDisplay } from '../utils/content-analytics';
|
|
11
|
+
const OPENRXIV_READER = 'https://reader.openrxivlabs.org';
|
|
12
|
+
// DOI prefixes served by the openRxiv reader: bioRxiv/medRxiv (10.1101) and openRxiv (10.64898).
|
|
13
|
+
// These fetch full content from the reader; everything else falls back to the curvenote overlay.
|
|
14
|
+
const OPENRXIV_PREFIXES = new Set(['10.1101', '10.64898']);
|
|
15
|
+
// Picks the content endpoint for a cited DOI. Reader responses use relative `/img/...` urls, so
|
|
16
|
+
// we also return the base they resolve against; overlay responses already have absolute image urls.
|
|
17
|
+
function contentSource(doiString) {
|
|
18
|
+
const normalized = doi.normalize(doiString);
|
|
19
|
+
const prefix = normalized?.split('/')[0];
|
|
20
|
+
if (normalized && prefix && OPENRXIV_PREFIXES.has(prefix)) {
|
|
21
|
+
return { url: `${OPENRXIV_READER}/content/${normalized}.json`, imageBase: OPENRXIV_READER };
|
|
22
|
+
}
|
|
23
|
+
return { url: `https://overlay.curvenote.dev/doi/${normalized}.json` };
|
|
24
|
+
}
|
|
25
|
+
function resolveImageUrl(url, base) {
|
|
26
|
+
if (!url || !base || /^https?:\/\//i.test(url))
|
|
27
|
+
return url;
|
|
28
|
+
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
29
|
+
}
|
|
8
30
|
// Fetcher function for the OpenRxiv API
|
|
9
31
|
async function openRxivFetcher(url) {
|
|
10
32
|
const response = await fetch(url);
|
|
@@ -29,45 +51,69 @@ function useOpenRxivData(url) {
|
|
|
29
51
|
};
|
|
30
52
|
}
|
|
31
53
|
// Specialized image component for figures
|
|
32
|
-
function FigureImageComponent({
|
|
33
|
-
|
|
54
|
+
function FigureImageComponent({ figure, citeLabel, citeDoi, onError, }) {
|
|
55
|
+
const capture = useAnalytics();
|
|
56
|
+
return (_jsx("div", { className: cn('overflow-hidden relative bg-gray-50 rounded-lg h-40',
|
|
57
|
+
// Force a constant height and contain the figure regardless of whether the
|
|
58
|
+
// Image wraps the <img> in a <picture> (when an optimized source exists).
|
|
59
|
+
'[&_picture]:block [&_picture]:w-full [&_picture]:h-full', '[&_img]:w-full [&_img]:h-full [&_img]:object-contain'), children: _jsx(Image, { src: figure.src, srcOptimized: figure.srcOptimized, urlSource: figure.urlSource, alt: figure.alt || 'Figure', caption: figure.caption, onError: onError, onLightboxOpen: () => capture('content_image_lightbox_opened', {
|
|
60
|
+
surface: 'content',
|
|
61
|
+
imageSource: 'cite_figure_bar',
|
|
62
|
+
citeLabel,
|
|
63
|
+
citeDoi,
|
|
64
|
+
imageUrl: figure.src,
|
|
65
|
+
imageAlt: figure.alt,
|
|
66
|
+
}) }) }));
|
|
34
67
|
}
|
|
35
68
|
// Main component that renders up to 3 figures
|
|
36
|
-
export function CiteFigureBar({ doi: doiString, className }) {
|
|
37
|
-
const url =
|
|
69
|
+
export function CiteFigureBar({ doi: doiString, citeLabel, className, }) {
|
|
70
|
+
const { url, imageBase } = contentSource(doiString);
|
|
38
71
|
const { data, error, isLoading } = useOpenRxivData(url);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const figures = selectAll('container[kind=figure]', data.mdast);
|
|
44
|
-
// Take only the first 3 figures
|
|
45
|
-
const displayFigures = figures.slice(0, 3);
|
|
46
|
-
if (displayFigures.length === 0) {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
const imageList = figures
|
|
72
|
+
const capture = useAnalytics();
|
|
73
|
+
// `src` of images that failed to load — these figures are dropped from the bar.
|
|
74
|
+
const [failed, setFailed] = useState(new Set());
|
|
75
|
+
const allFigures = (data?.mdast ? selectAll('container[kind=figure]', data.mdast) : [])
|
|
50
76
|
.map((figure) => {
|
|
51
77
|
const image = figure.children.find((child) => child.type === 'image');
|
|
52
78
|
const caption = figure.children.find((child) => child.type === 'caption');
|
|
53
79
|
if (!image)
|
|
54
80
|
return null;
|
|
55
81
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
key: figure.key,
|
|
83
|
+
src: resolveImageUrl(image.url, imageBase) ?? image.url,
|
|
84
|
+
srcOptimized: resolveImageUrl(image.urlOptimized, imageBase),
|
|
58
85
|
urlSource: image.urlSource,
|
|
59
86
|
alt: image.alt,
|
|
60
87
|
caption: caption,
|
|
61
88
|
};
|
|
62
89
|
})
|
|
63
|
-
.filter((
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
.filter((figure) => figure !== null);
|
|
91
|
+
// Remove figures whose image failed to load, then show up to 3.
|
|
92
|
+
const visibleFigures = allFigures.filter((figure) => !failed.has(figure.src));
|
|
93
|
+
const displayFigures = visibleFigures.slice(0, 3);
|
|
94
|
+
const enrichmentOutcome = error ? 'error' : displayFigures.length > 0 ? 'success' : 'empty';
|
|
95
|
+
useContentEnrichmentDisplay({
|
|
96
|
+
capture,
|
|
97
|
+
enrichmentSource: 'openrxiv',
|
|
98
|
+
outcome: enrichmentOutcome,
|
|
99
|
+
cardKind: 'citation',
|
|
100
|
+
engagementSurface: 'hover_card',
|
|
101
|
+
enabled: !isLoading,
|
|
102
|
+
properties: { citeLabel, citeDoi: doiString, figureCount: displayFigures.length },
|
|
103
|
+
});
|
|
104
|
+
if (isLoading || error || !data?.mdast) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
if (displayFigures.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const markFailed = (src) => setFailed((prev) => (prev.has(src) ? prev : new Set(prev).add(src)));
|
|
111
|
+
const imageList = visibleFigures.map((figure) => ({
|
|
112
|
+
src: figure.src,
|
|
113
|
+
srcOptimized: figure.srcOptimized,
|
|
114
|
+
urlSource: figure.urlSource,
|
|
115
|
+
alt: figure.alt,
|
|
116
|
+
caption: figure.caption,
|
|
117
|
+
}));
|
|
118
|
+
return (_jsx(LightboxContainer, { imageList: imageList, children: _jsx("div", { className: cn('grid grid-cols-1 gap-4 md:grid-cols-3', className), children: displayFigures.map((figure, index) => (_jsxs("div", { className: "flex flex-col", children: [_jsx(FigureImageComponent, { figure: figure, citeLabel: citeLabel, citeDoi: doiString, onError: () => markFailed(figure.src) }), _jsx(MyST, { ast: figure.caption, className: "text-xs line-clamp-5" })] }, figure.key || index))) }) }));
|
|
73
119
|
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { NodeRenderer } from '@myst-theme/providers';
|
|
2
2
|
import type { GenericParent } from 'myst-common';
|
|
3
3
|
export declare const CiteGroup: NodeRenderer<GenericParent>;
|
|
4
|
+
/**
|
|
5
|
+
* Conservatively recover a `https://doi.org/...` URL from a known publisher URL.
|
|
6
|
+
*
|
|
7
|
+
* Only URLs that map unambiguously to a DOI are handled. Anything we are not
|
|
8
|
+
* confident about returns `undefined` rather than guessing.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* doiUrlFromPublisherUrl('https://www.nature.com/articles/s41586-024-07441-w')
|
|
12
|
+
* // => 'https://doi.org/10.1038/s41586-024-07441-w'
|
|
13
|
+
*/
|
|
14
|
+
export declare function doiUrlFromPublisherUrl(refUrl?: string): string | undefined;
|
|
4
15
|
export declare const Cite: ({ label, error, children, className, }: {
|
|
5
16
|
label?: string;
|
|
6
17
|
error?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cite.d.ts","sourceRoot":"","sources":["../../src/components/cite.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"cite.d.ts","sourceRoot":"","sources":["../../src/components/cite.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAoLjD,eAAO,MAAM,SAAS,EAAE,YAAY,CAAC,aAAa,CAiBjD,CAAC;AAWF;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAW1E;AAoBD,eAAO,MAAM,IAAI,GAAI,wCAKlB;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,4CAkEA,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,YAO1B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAGvD,CAAC"}
|
package/dist/components/cite.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useReferences, useSiteManifest } from '@myst-theme/providers';
|
|
3
3
|
import { doi } from 'doi-utils';
|
|
4
|
-
import {
|
|
4
|
+
import { MyST } from 'myst-to-react';
|
|
5
|
+
import { HoverPopover } from './hover-popover';
|
|
5
6
|
import { createId } from 'myst-common';
|
|
6
7
|
import { visit } from 'unist-util-visit';
|
|
7
8
|
import { cn } from '@curvenote/react-utils';
|
|
8
9
|
import { InlineError } from './inlineError';
|
|
9
10
|
import { useOpenAlexWork } from '../hooks/useOpenAlex';
|
|
10
11
|
import { reverseInvertedAbstract, formatAuthors } from '../utils/abstract';
|
|
11
|
-
import { LinkCard } from '@curvenote/theme-ui';
|
|
12
|
+
import { LinkCard, useAnalytics } from '@curvenote/theme-ui';
|
|
12
13
|
import { CiteFigureBar } from './cite-figurebar';
|
|
13
14
|
import { fromHtml } from 'hast-util-from-html';
|
|
14
15
|
import { toMdast } from 'hast-util-to-mdast';
|
|
15
16
|
import { CiteYouTube, isYouTubeUrl } from './cite-youtube';
|
|
17
|
+
import { useCallback } from 'react';
|
|
18
|
+
import { captureContentCiteClicked, captureContentLinkClicked, useContentEnrichmentDisplay, } from '../utils/content-analytics';
|
|
16
19
|
// TODO:
|
|
17
20
|
// this should be turned into an upgrade step when we do citations.
|
|
18
21
|
// We are only expecting very basic HTML in the citations, so this should be fine.
|
|
19
22
|
function convertHtmlToMdast(html) {
|
|
20
23
|
const hast = fromHtml(html, { fragment: true });
|
|
24
|
+
// The types play better hast-util-to-mdast@10, but we downgraded to 8 to align with myst.
|
|
21
25
|
const mdast = toMdast(hast);
|
|
22
26
|
// Ensure all nodes have a keys for react rendering errors
|
|
23
27
|
visit(mdast, (node) => {
|
|
@@ -30,16 +34,56 @@ function useNumberedReferences() {
|
|
|
30
34
|
const numbered_references = !!config?.options?.numbered_references;
|
|
31
35
|
return numbered_references;
|
|
32
36
|
}
|
|
33
|
-
function CiteChild({ html, doi: doiString, url }) {
|
|
37
|
+
function CiteChild({ html, doi: doiString, url, citeLabel, }) {
|
|
38
|
+
const capture = useAnalytics();
|
|
34
39
|
const { data: openAlexData, error, isLoading } = useOpenAlexWork(doiString);
|
|
35
|
-
|
|
40
|
+
const citeProperties = { citeLabel, doi: doiString, url };
|
|
41
|
+
const showOpenAlexCard = Boolean(doiString && doi.validate(doiString) && !error);
|
|
42
|
+
const openAlexOutcome = isLoading
|
|
43
|
+
? 'loading'
|
|
44
|
+
: error
|
|
45
|
+
? 'error'
|
|
46
|
+
: openAlexData
|
|
47
|
+
? 'success'
|
|
48
|
+
: 'empty';
|
|
49
|
+
useContentEnrichmentDisplay({
|
|
50
|
+
capture,
|
|
51
|
+
enrichmentSource: 'openalex',
|
|
52
|
+
outcome: openAlexOutcome,
|
|
53
|
+
cardKind: 'citation',
|
|
54
|
+
engagementSurface: 'hover_card',
|
|
55
|
+
enabled: Boolean(doiString),
|
|
56
|
+
properties: {
|
|
57
|
+
citeLabel,
|
|
58
|
+
doi: doiString,
|
|
59
|
+
url,
|
|
60
|
+
hasAbstract: Boolean(openAlexData?.abstract_inverted_index),
|
|
61
|
+
hasTitle: Boolean(openAlexData?.title),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
useContentEnrichmentDisplay({
|
|
65
|
+
capture,
|
|
66
|
+
enrichmentSource: 'cite_html',
|
|
67
|
+
outcome: 'fallback',
|
|
68
|
+
cardKind: 'citation',
|
|
69
|
+
engagementSurface: 'hover_card',
|
|
70
|
+
enabled: !showOpenAlexCard,
|
|
71
|
+
properties: { citeLabel, doi: doiString, url, hasYouTube: Boolean(url && isYouTubeUrl(url)) },
|
|
72
|
+
});
|
|
73
|
+
if (showOpenAlexCard && ['success', 'loading'].includes(openAlexOutcome)) {
|
|
36
74
|
const abstract = openAlexData?.abstract_inverted_index
|
|
37
75
|
? reverseInvertedAbstract(openAlexData.abstract_inverted_index)
|
|
38
76
|
: '';
|
|
39
77
|
const authors = formatAuthors(openAlexData?.authorships);
|
|
40
78
|
const journal = openAlexData?.primary_location?.source?.display_name;
|
|
41
79
|
const year = openAlexData?.publication_year;
|
|
42
|
-
return (_jsx(LinkCard, { url: doi.buildUrl(doiString), title: journal && year ? `${journal} (${year})` : journal || year, size: "lg",
|
|
80
|
+
return (_jsx(LinkCard, { url: doi.buildUrl(doiString), title: journal && year ? `${journal} (${year})` : journal || year, size: "lg", onLinkClick: () => captureContentLinkClicked(capture, {
|
|
81
|
+
linkKind: 'citation',
|
|
82
|
+
destinationUrl: doi.buildUrl(doiString),
|
|
83
|
+
engagementSurface: 'hover_card',
|
|
84
|
+
cardKind: 'citation',
|
|
85
|
+
properties: citeProperties,
|
|
86
|
+
}), description: _jsxs("div", { className: "flex flex-col gap-3 max-w-none prose", children: [openAlexData?.title && (_jsx("div", { className: "flex flex-col gap-1 font-bold", children: _jsx("div", { className: "text-sm leading-relaxed", children: openAlexData.title.replace(/<[^>]+>/g, '') }) })), authors && (_jsx("div", { className: "flex flex-col gap-1", children: _jsx("div", { className: "text-sm", children: authors }) })), abstract && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-xs font-bold", children: "Abstract" }), _jsx("div", { className: "overflow-y-auto max-h-48 text-xs leading-relaxed", children: abstract })] })), _jsx(CiteFigureBar, { doi: doiString, citeLabel: citeLabel }), isLoading && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString }), _jsx("div", { className: "text-xs text-gray-500", children: "Loading additional information..." })] })), error && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString }), _jsx("div", { className: "text-xs text-gray-500", children: "Unable to load additional information" })] })), !isLoading && !error && !openAlexData && (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("div", { className: "text-sm font-medium text-gray-900", children: "DOI" }), _jsx("div", { className: "text-sm", children: doiString })] }))] }) }));
|
|
43
87
|
}
|
|
44
88
|
const mdast = convertHtmlToMdast(html);
|
|
45
89
|
return (_jsxs("div", { className: "hover-document article w-[500px] sm:max-w-[500px] p-3", children: [url && isYouTubeUrl(url) && _jsx(CiteYouTube, { url: url }), _jsx(MyST, { ast: mdast })] }));
|
|
@@ -53,18 +97,96 @@ export const CiteGroup = ({ node, className }) => {
|
|
|
53
97
|
parenthetical: node.kind === 'parenthetical',
|
|
54
98
|
}, className), children: _jsx(MyST, { ast: node.children }) }));
|
|
55
99
|
};
|
|
100
|
+
function isUrl(url) {
|
|
101
|
+
try {
|
|
102
|
+
new URL(url);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Conservatively recover a `https://doi.org/...` URL from a known publisher URL.
|
|
111
|
+
*
|
|
112
|
+
* Only URLs that map unambiguously to a DOI are handled. Anything we are not
|
|
113
|
+
* confident about returns `undefined` rather than guessing.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* doiUrlFromPublisherUrl('https://www.nature.com/articles/s41586-024-07441-w')
|
|
117
|
+
* // => 'https://doi.org/10.1038/s41586-024-07441-w'
|
|
118
|
+
*/
|
|
119
|
+
export function doiUrlFromPublisherUrl(refUrl) {
|
|
120
|
+
if (!refUrl || !isUrl(refUrl))
|
|
121
|
+
return undefined;
|
|
122
|
+
const parsed = new URL(refUrl);
|
|
123
|
+
const host = parsed.hostname.replace(/^www\./, '');
|
|
124
|
+
// Nature (Springer Nature) articles use the DOI prefix 10.1038 and embed the
|
|
125
|
+
// suffix directly in the path, e.g. /articles/s41586-024-07441-w
|
|
126
|
+
if (host === 'nature.com') {
|
|
127
|
+
const match = parsed.pathname.match(/^\/articles\/([a-z0-9.-]+)$/i);
|
|
128
|
+
if (match)
|
|
129
|
+
return `https://doi.org/10.1038/${match[1]}`;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
function getDoi(doiString, refUrl) {
|
|
134
|
+
const cleanDoi = doi.validate(doiString);
|
|
135
|
+
if (cleanDoi)
|
|
136
|
+
return doi.buildUrl(doiString);
|
|
137
|
+
const url = getUrl(doiString, refUrl);
|
|
138
|
+
if (!url || !isUrl(url))
|
|
139
|
+
return undefined;
|
|
140
|
+
const publisherDoi = doiUrlFromPublisherUrl(url);
|
|
141
|
+
if (publisherDoi)
|
|
142
|
+
return publisherDoi;
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
function getUrl(doiString, refUrl) {
|
|
146
|
+
const cleanDoi = doi.validate(doiString);
|
|
147
|
+
if (cleanDoi)
|
|
148
|
+
return doi.buildUrl(doiString);
|
|
149
|
+
if (refUrl && isUrl(refUrl))
|
|
150
|
+
return refUrl;
|
|
151
|
+
if (doiString && isUrl(doiString))
|
|
152
|
+
return doiString;
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
56
155
|
export const Cite = ({ label, error, children, className, }) => {
|
|
57
156
|
const references = useReferences();
|
|
157
|
+
const capture = useAnalytics();
|
|
58
158
|
if (!label) {
|
|
59
159
|
return (_jsx(InlineError, { value: "cite (no label)", message: 'Citation Has No Label', className: className }));
|
|
60
160
|
}
|
|
61
|
-
const { html, doi:
|
|
161
|
+
const { html, doi: doiStringData, url: refUrlData } = references?.cite?.data[label] ?? {};
|
|
162
|
+
const doiString = getDoi(doiStringData, refUrlData);
|
|
163
|
+
const url = getUrl(doiStringData, refUrlData);
|
|
62
164
|
if (error) {
|
|
63
165
|
return _jsx(InlineError, { value: label, message: 'Citation Not Found', className: className });
|
|
64
166
|
}
|
|
65
|
-
const url = doiString ? doi.buildUrl(doiString) : refUrl;
|
|
66
167
|
const isButtonLike = (className ?? '').split(' ').includes('button');
|
|
67
|
-
|
|
168
|
+
const captureTriggerClick = useCallback(() => {
|
|
169
|
+
if (url) {
|
|
170
|
+
captureContentLinkClicked(capture, {
|
|
171
|
+
linkKind: 'citation',
|
|
172
|
+
destinationUrl: url,
|
|
173
|
+
engagementSurface: 'trigger',
|
|
174
|
+
cardKind: 'citation',
|
|
175
|
+
properties: { citeLabel: label, doi: doiString, url },
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
captureContentCiteClicked(capture, {
|
|
180
|
+
citeLabel: label,
|
|
181
|
+
doi: doiString,
|
|
182
|
+
hasUrl: false,
|
|
183
|
+
engagementSurface: 'trigger',
|
|
184
|
+
});
|
|
185
|
+
}, [capture, url, label, doiString]);
|
|
186
|
+
return (_jsx(HoverPopover, { openDelay: 300, card: _jsx(CiteChild, { html: html, doi: doiString, url: url, citeLabel: label }), analytics: {
|
|
187
|
+
cardKind: 'citation',
|
|
188
|
+
properties: { citeLabel: label, doi: doiString, url },
|
|
189
|
+
}, children: _jsxs("cite", { className: className, children: [url && (_jsx("a", { href: url, target: "_blank", rel: "noreferrer", className: cn({ 'hover-link': !isButtonLike }), onClick: captureTriggerClick, children: children })), !url && (_jsx("span", { className: "hover-link", onClick: captureTriggerClick, children: children }))] }) }));
|
|
68
190
|
};
|
|
69
191
|
export const CiteRenderer = ({ node, className }) => {
|
|
70
192
|
const numbered = useNumberedReferences();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type CopyIconAnalytics = {
|
|
2
|
+
contentKind?: string;
|
|
3
|
+
properties?: Record<string, unknown>;
|
|
4
|
+
};
|
|
5
|
+
export type CopyIconProps = {
|
|
6
|
+
text: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
analytics?: CopyIconAnalytics;
|
|
9
|
+
};
|
|
10
|
+
export declare function CopyIcon({ text, className, analytics }: CopyIconProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=copy-icon.d.ts.map
|