@deepcitation/deepcitation-js 1.1.24 → 1.1.26
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/lib/index.d.ts +1 -0
- package/lib/index.js +2 -0
- package/lib/react/CitationComponent.js +48 -22
- package/lib/react/icons.d.ts +10 -0
- package/lib/react/icons.js +8 -1
- package/lib/react/utils.d.ts +1 -0
- package/lib/react/utils.js +6 -0
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { parseCitation, getCitationStatus, getAllCitationsFromLlmOutput, groupCi
|
|
|
8
8
|
export { normalizeCitations, getCitationPageNumber, } from "./parsing/normalizeCitation.js";
|
|
9
9
|
export { isGeminiGarbage, cleanRepeatingLastSentence, } from "./parsing/parseWorkAround.js";
|
|
10
10
|
export type { Citation, CitationStatus, VerifyCitationRequest, VerifyCitationResponse, OutputImageFormat, } from "./types/citation.js";
|
|
11
|
+
export { DeepCitationIcon } from "./react/icons.js";
|
|
11
12
|
export { DEFAULT_OUTPUT_IMAGE_FORMAT } from "./types/citation.js";
|
|
12
13
|
export type { Verification } from "./types/verification.js";
|
|
13
14
|
export { NOT_FOUND_VERIFICATION_INDEX, PENDING_VERIFICATION_INDEX, BLANK_VERIFICATION, } from "./types/verification.js";
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export { DeepCitation } from "./client/index.js";
|
|
|
8
8
|
export { parseCitation, getCitationStatus, getAllCitationsFromLlmOutput, groupCitationsByAttachmentId, groupCitationsByAttachmentIdObject, } from "./parsing/parseCitation.js";
|
|
9
9
|
export { normalizeCitations, getCitationPageNumber, } from "./parsing/normalizeCitation.js";
|
|
10
10
|
export { isGeminiGarbage, cleanRepeatingLastSentence, } from "./parsing/parseWorkAround.js";
|
|
11
|
+
// Icons
|
|
12
|
+
export { DeepCitationIcon } from "./react/icons.js";
|
|
11
13
|
export { DEFAULT_OUTPUT_IMAGE_FORMAT } from "./types/citation.js";
|
|
12
14
|
export { NOT_FOUND_VERIFICATION_INDEX, PENDING_VERIFICATION_INDEX, BLANK_VERIFICATION, } from "./types/verification.js";
|
|
13
15
|
// Utilities
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, memo, useCallback, useEffect, useMemo, useState, } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
-
import { CheckIcon, WarningIcon } from "./icons.js";
|
|
4
|
+
import { CheckIcon, SpinnerIcon, WarningIcon } from "./icons.js";
|
|
5
5
|
import { Popover, PopoverContent, PopoverTrigger } from "./Popover.js";
|
|
6
|
-
import { generateCitationInstanceId, generateCitationKey, getCitationDisplayText, } from "./utils.js";
|
|
6
|
+
import { cn, generateCitationInstanceId, generateCitationKey, getCitationDisplayText, } from "./utils.js";
|
|
7
7
|
import { useSmartDiff } from "./useSmartDiff.js";
|
|
8
|
-
// =============================================================================
|
|
9
|
-
// UTILITY FUNCTIONS
|
|
10
|
-
// =============================================================================
|
|
11
|
-
function cn(...classes) {
|
|
12
|
-
return classes.filter(Boolean).join(" ");
|
|
13
|
-
}
|
|
14
8
|
function getStatusLabel(status) {
|
|
15
9
|
if (status.isVerified && !status.isPartialMatch)
|
|
16
10
|
return "Verified";
|
|
@@ -30,7 +24,12 @@ function getStatusFromVerification(verification) {
|
|
|
30
24
|
const status = verification?.status;
|
|
31
25
|
// No verification or no status = pending
|
|
32
26
|
if (!verification || !status) {
|
|
33
|
-
return {
|
|
27
|
+
return {
|
|
28
|
+
isVerified: false,
|
|
29
|
+
isMiss: false,
|
|
30
|
+
isPartialMatch: false,
|
|
31
|
+
isPending: true,
|
|
32
|
+
};
|
|
34
33
|
}
|
|
35
34
|
const isMiss = status === "not_found";
|
|
36
35
|
const isPending = status === "pending" || status === "loading";
|
|
@@ -74,14 +73,12 @@ function ImageOverlay({ src, alt, onClose }) {
|
|
|
74
73
|
//
|
|
75
74
|
// Use `renderIndicator` prop to customize. Use `variant="indicator"` to show only the icon.
|
|
76
75
|
// =============================================================================
|
|
77
|
-
/** Spinner component for loading/pending state */
|
|
78
|
-
const Spinner = ({ className }) => (_jsxs("svg", { className: cn("animate-spin", className), xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", width: "12", height: "12", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }));
|
|
79
76
|
/** Verified indicator - green checkmark for exact matches */
|
|
80
77
|
const VerifiedIndicator = () => (_jsx("span", { className: "inline-flex relative ml-0.5 text-green-600 dark:text-green-500", "aria-hidden": "true", children: _jsx(CheckIcon, {}) }));
|
|
81
78
|
/** Partial match indicator - amber checkmark for partial/relocated matches */
|
|
82
79
|
const PartialIndicator = () => (_jsx("span", { className: "inline-flex relative ml-0.5 text-amber-600 dark:text-amber-500", "aria-hidden": "true", children: _jsx(CheckIcon, {}) }));
|
|
83
80
|
/** Pending indicator - spinner for loading state */
|
|
84
|
-
const PendingIndicator = () => (_jsx("span", { className: "inline-flex ml-1 text-gray-400 dark:text-gray-500", "aria-hidden": "true", children: _jsx(
|
|
81
|
+
const PendingIndicator = () => (_jsx("span", { className: "inline-flex ml-1 text-gray-400 dark:text-gray-500", "aria-hidden": "true", children: _jsx(SpinnerIcon, {}) }));
|
|
85
82
|
/** Miss indicator - red warning triangle for not found */
|
|
86
83
|
const MissIndicator = () => (_jsx("span", { className: "inline-flex relative ml-0.5 text-red-500 dark:text-red-400", "aria-hidden": "true", children: _jsx(WarningIcon, {}) }));
|
|
87
84
|
function DefaultPopoverContent({ citation, verification, status, onImageClick, }) {
|
|
@@ -101,7 +98,9 @@ function DefaultPopoverContent({ citation, verification, status, onImageClick, }
|
|
|
101
98
|
const pageNumber = verification?.verifiedPageNumber;
|
|
102
99
|
if (!hasSnippet && !statusLabel)
|
|
103
100
|
return null;
|
|
104
|
-
return (_jsxs("div", { className: "p-3 flex flex-col gap-2 min-w-[200px] max-w-[400px]", children: [statusLabel && (_jsx("span", { className: cn("text-xs font-medium", status.isVerified &&
|
|
101
|
+
return (_jsxs("div", { className: "p-3 flex flex-col gap-2 min-w-[200px] max-w-[400px]", children: [statusLabel && (_jsx("span", { className: cn("text-xs font-medium", status.isVerified &&
|
|
102
|
+
!status.isPartialMatch &&
|
|
103
|
+
"text-green-600 dark:text-green-500", status.isPartialMatch && "text-amber-600 dark:text-amber-500", status.isMiss && "text-red-600 dark:text-red-500", status.isPending && "text-gray-500 dark:text-gray-400"), children: statusLabel })), hasSnippet && (_jsxs("span", { className: "text-sm text-gray-700 dark:text-gray-300 italic", children: ["\"", verification.verifiedMatchSnippet, "\""] })), pageNumber && pageNumber > 0 && (_jsxs("span", { className: "text-xs text-gray-500 dark:text-gray-400", children: ["Page ", pageNumber] })), (isMiss || isPartialMatch) && (_jsx(DiffDetails, { citation: citation, verification: verification, status: status }))] }));
|
|
105
104
|
}
|
|
106
105
|
// =============================================================================
|
|
107
106
|
// DIFF DETAILS COMPONENT
|
|
@@ -128,10 +127,16 @@ function DiffDetails({ citation, verification, status, }) {
|
|
|
128
127
|
const pageDiffers = expectedPage != null && actualPage != null && expectedPage !== actualPage;
|
|
129
128
|
// For "not_found" status, show expected text and "Not found" message
|
|
130
129
|
if (isMiss) {
|
|
131
|
-
return (_jsxs("div", { className: "mt-2 pt-2 border-t border-gray-200 dark:border-gray-700 text-xs space-y-2", children: [expectedText && (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Expected" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-red-600 dark:text-red-400 line-through opacity-70", children: expectedText.length > 100
|
|
130
|
+
return (_jsxs("div", { className: "mt-2 pt-2 border-t border-gray-200 dark:border-gray-700 text-xs space-y-2", children: [expectedText && (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Expected" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-red-600 dark:text-red-400 line-through opacity-70", children: expectedText.length > 100
|
|
131
|
+
? expectedText.slice(0, 100) + "…"
|
|
132
|
+
: expectedText })] })), _jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Found" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] text-amber-600 dark:text-amber-500 italic", children: "Not found in source" })] })] }));
|
|
132
133
|
}
|
|
133
134
|
// For partial matches, show word-level diff
|
|
134
|
-
return (_jsxs("div", { className: "mt-2 pt-2 border-t border-gray-200 dark:border-gray-700 text-xs space-y-2", children: [expectedText && actualText && hasDiff ? (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Diff" }), _jsx("div", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-gray-700 dark:text-gray-300", children: isHighVariance ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { children: [
|
|
135
|
+
return (_jsxs("div", { className: "mt-2 pt-2 border-t border-gray-200 dark:border-gray-700 text-xs space-y-2", children: [expectedText && actualText && hasDiff ? (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Diff" }), _jsx("div", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-gray-700 dark:text-gray-300", children: isHighVariance ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { children: [_jsxs("span", { className: "text-gray-500 dark:text-gray-400 text-[10px]", children: ["Expected:", " "] }), _jsx("span", { className: "text-red-600 dark:text-red-400 line-through opacity-70", children: expectedText.length > 100
|
|
136
|
+
? expectedText.slice(0, 100) + "…"
|
|
137
|
+
: expectedText })] }), _jsxs("div", { children: [_jsxs("span", { className: "text-gray-500 dark:text-gray-400 text-[10px]", children: ["Found:", " "] }), _jsx("span", { className: "text-green-600 dark:text-green-400", children: actualText.length > 100
|
|
138
|
+
? actualText.slice(0, 100) + "…"
|
|
139
|
+
: actualText })] })] })) : (
|
|
135
140
|
// Inline word-level diff
|
|
136
141
|
diffResult.map((block, blockIndex) => (_jsx("span", { children: block.parts.map((part, partIndex) => {
|
|
137
142
|
const key = `p-${blockIndex}-${partIndex}`;
|
|
@@ -145,7 +150,9 @@ function DiffDetails({ citation, verification, status, }) {
|
|
|
145
150
|
return _jsx("span", { children: part.value }, key);
|
|
146
151
|
}) }, `block-${blockIndex}`)))) })] })) : expectedText && !hasDiff ? (
|
|
147
152
|
// Text matches exactly (partial match is due to location difference)
|
|
148
|
-
_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Text" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-gray-700 dark:text-gray-300", children: expectedText.length > 100
|
|
153
|
+
_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Text" }), _jsx("p", { className: "mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded font-mono text-[11px] break-words text-gray-700 dark:text-gray-300", children: expectedText.length > 100
|
|
154
|
+
? expectedText.slice(0, 100) + "…"
|
|
155
|
+
: expectedText })] })) : null, pageDiffers && (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Page" }), _jsxs("p", { className: "mt-1 font-mono text-[11px] text-gray-700 dark:text-gray-300", children: [_jsx("span", { className: "text-red-600 dark:text-red-400 line-through opacity-70", children: expectedPage }), " → ", actualPage] })] })), lineIdDiffers && (_jsxs("div", { children: [_jsx("span", { className: "text-gray-500 dark:text-gray-400 font-medium uppercase text-[10px]", children: "Line" }), _jsxs("p", { className: "mt-1 font-mono text-[11px] text-gray-700 dark:text-gray-300", children: [_jsx("span", { className: "text-red-600 dark:text-red-400 line-through opacity-70", children: expectedLineIds?.join(", ") }), " → ", actualLineIds?.join(", ")] })] }))] }));
|
|
149
156
|
}
|
|
150
157
|
// =============================================================================
|
|
151
158
|
// MAIN COMPONENT
|
|
@@ -193,7 +200,8 @@ export const CitationComponent = forwardRef(({ citation, children, className, hi
|
|
|
193
200
|
if (typeof actions.setImageExpanded === "string") {
|
|
194
201
|
setExpandedImageSrc(actions.setImageExpanded);
|
|
195
202
|
}
|
|
196
|
-
else if (actions.setImageExpanded === true &&
|
|
203
|
+
else if (actions.setImageExpanded === true &&
|
|
204
|
+
verification?.verificationImageBase64) {
|
|
197
205
|
setExpandedImageSrc(verification.verificationImageBase64);
|
|
198
206
|
}
|
|
199
207
|
else if (actions.setImageExpanded === false) {
|
|
@@ -240,14 +248,26 @@ export const CitationComponent = forwardRef(({ citation, children, className, hi
|
|
|
240
248
|
behaviorConfig.onHover.onEnter(getBehaviorContext());
|
|
241
249
|
}
|
|
242
250
|
eventHandlers?.onMouseEnter?.(citation, citationKey);
|
|
243
|
-
}, [
|
|
251
|
+
}, [
|
|
252
|
+
eventHandlers,
|
|
253
|
+
behaviorConfig,
|
|
254
|
+
citation,
|
|
255
|
+
citationKey,
|
|
256
|
+
getBehaviorContext,
|
|
257
|
+
]);
|
|
244
258
|
const handleMouseLeave = useCallback(() => {
|
|
245
259
|
setIsHovering(false);
|
|
246
260
|
if (behaviorConfig?.onHover?.onLeave) {
|
|
247
261
|
behaviorConfig.onHover.onLeave(getBehaviorContext());
|
|
248
262
|
}
|
|
249
263
|
eventHandlers?.onMouseLeave?.(citation, citationKey);
|
|
250
|
-
}, [
|
|
264
|
+
}, [
|
|
265
|
+
eventHandlers,
|
|
266
|
+
behaviorConfig,
|
|
267
|
+
citation,
|
|
268
|
+
citationKey,
|
|
269
|
+
getBehaviorContext,
|
|
270
|
+
]);
|
|
251
271
|
// Touch handler for mobile
|
|
252
272
|
const handleTouchEnd = useCallback((e) => {
|
|
253
273
|
if (isMobile) {
|
|
@@ -257,13 +277,18 @@ export const CitationComponent = forwardRef(({ citation, children, className, hi
|
|
|
257
277
|
}
|
|
258
278
|
}, [eventHandlers, citation, citationKey, isMobile]);
|
|
259
279
|
// Early return for miss with fallback display
|
|
260
|
-
if (fallbackDisplay !== null &&
|
|
280
|
+
if (fallbackDisplay !== null &&
|
|
281
|
+
fallbackDisplay !== undefined &&
|
|
282
|
+
!hideKeySpan &&
|
|
283
|
+
isMiss) {
|
|
261
284
|
return (_jsx("span", { className: cn("text-gray-400 dark:text-gray-500", className), children: fallbackDisplay }));
|
|
262
285
|
}
|
|
263
286
|
// Status classes for text styling
|
|
264
287
|
const statusClasses = cn(
|
|
265
288
|
// Found status (text color) - verified or partial match
|
|
266
|
-
(isVerified || isPartialMatch) &&
|
|
289
|
+
(isVerified || isPartialMatch) &&
|
|
290
|
+
variant === "brackets" &&
|
|
291
|
+
"text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline", isMiss && "opacity-70 line-through text-gray-400 dark:text-gray-500", isPending && "text-gray-500 dark:text-gray-400");
|
|
267
292
|
// Render indicator based on status priority:
|
|
268
293
|
// 1. Custom renderIndicator (if provided)
|
|
269
294
|
// 2. Pending → Spinner
|
|
@@ -310,7 +335,8 @@ export const CitationComponent = forwardRef(({ citation, children, className, hi
|
|
|
310
335
|
const isPopoverHidden = popoverPosition === "hidden";
|
|
311
336
|
const shouldShowPopover = !isPopoverHidden &&
|
|
312
337
|
verification &&
|
|
313
|
-
(verification.verificationImageBase64 ||
|
|
338
|
+
(verification.verificationImageBase64 ||
|
|
339
|
+
verification.verifiedMatchSnippet);
|
|
314
340
|
const hasImage = !!verification?.verificationImageBase64;
|
|
315
341
|
// Image overlay
|
|
316
342
|
const imageOverlay = expandedImageSrc ? (_jsx(ImageOverlay, { src: expandedImageSrc, alt: "Citation verification - full size", onClose: () => setExpandedImageSrc(null) })) : null;
|
package/lib/react/icons.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepCitation icon SVG (no dependencies)
|
|
3
|
+
*/
|
|
4
|
+
export declare const DeepCitationIcon: ({ className }: {
|
|
5
|
+
className?: string;
|
|
6
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
1
7
|
/**
|
|
2
8
|
* Check icon SVG (no dependencies)
|
|
3
9
|
*/
|
|
@@ -6,3 +12,7 @@ export declare const CheckIcon: () => import("react/jsx-runtime").JSX.Element;
|
|
|
6
12
|
* Warning icon SVG (no dependencies)
|
|
7
13
|
*/
|
|
8
14
|
export declare const WarningIcon: () => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
/** Spinner component for loading/pending state */
|
|
16
|
+
export declare const SpinnerIcon: ({ className }: {
|
|
17
|
+
className?: string;
|
|
18
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
package/lib/react/icons.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* DeepCitation icon SVG (no dependencies)
|
|
5
|
+
*/
|
|
6
|
+
export const DeepCitationIcon = ({ className }) => (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "square", shapeRendering: "crispEdges", className: className, children: [_jsx("path", { d: "M7 3 L3 3 L3 21 L7 21" }), _jsx("path", { d: "M17 3 L21 3 L21 21 L17 21" })] }));
|
|
2
7
|
/**
|
|
3
8
|
* Check icon SVG (no dependencies)
|
|
4
9
|
*/
|
|
@@ -7,3 +12,5 @@ export const CheckIcon = () => (_jsx("svg", { className: "dc-check-icon", viewBo
|
|
|
7
12
|
* Warning icon SVG (no dependencies)
|
|
8
13
|
*/
|
|
9
14
|
export const WarningIcon = () => (_jsx("svg", { className: "dc-status-icon", viewBox: "0 0 256 256", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM120,104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm8,88a12,12,0,1,1,12-12A12,12,0,0,1,128,192Z" }) }));
|
|
15
|
+
/** Spinner component for loading/pending state */
|
|
16
|
+
export const SpinnerIcon = ({ className }) => (_jsxs("svg", { className: cn("animate-spin", className), xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", width: "12", height: "12", children: [_jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), _jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }));
|
package/lib/react/utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Citation } from "../types/citation.js";
|
|
2
2
|
import type { Verification } from "../types/verification.js";
|
|
3
|
+
export declare function cn(...classes: (string | undefined | null | false)[]): string;
|
|
3
4
|
/**
|
|
4
5
|
* Generates a unique, deterministic key for a citation based on its content.
|
|
5
6
|
* @param citation - The citation to generate a key for
|
package/lib/react/utils.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { sha1Hash } from "../utils/sha.js";
|
|
2
2
|
import { getCitationPageNumber } from "../parsing/normalizeCitation.js";
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// UTILITY FUNCTIONS
|
|
5
|
+
// =============================================================================
|
|
6
|
+
export function cn(...classes) {
|
|
7
|
+
return classes.filter(Boolean).join(" ");
|
|
8
|
+
}
|
|
3
9
|
/**
|
|
4
10
|
* Generates a unique, deterministic key for a citation based on its content.
|
|
5
11
|
* @param citation - The citation to generate a key for
|