feat: add PubMed and software citation format support

Extends bibliography smart import capabilities with additional citation.js plugins:
- @citation-js/plugin-pubmed: Support for PubMed IDs (PMID/PMCID)
- @citation-js/plugin-software-formats: Support for software citations (CFF, GitHub, npm, Zenodo)

Updates smart-parser.ts to recognize and validate Zenodo DOI format (10.5281/zenodo.xxxxx).
Improves input type detection for better user feedback.

Includes code formatting improvements to bibliographyStore.ts for consistency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-10-17 15:18:09 +02:00
parent 14ccb2da5b
commit ef16b9d060
4 changed files with 169 additions and 86 deletions

106
package-lock.json generated
View file

@ -12,7 +12,9 @@
"@citation-js/plugin-bibtex": "^0.7.18", "@citation-js/plugin-bibtex": "^0.7.18",
"@citation-js/plugin-csl": "^0.7.18", "@citation-js/plugin-csl": "^0.7.18",
"@citation-js/plugin-doi": "^0.7.18", "@citation-js/plugin-doi": "^0.7.18",
"@citation-js/plugin-pubmed": "^0.3.0",
"@citation-js/plugin-ris": "^0.7.18", "@citation-js/plugin-ris": "^0.7.18",
"@citation-js/plugin-software-formats": "^0.6.1",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.10", "@mui/icons-material": "^5.15.10",
@ -377,6 +379,18 @@
"@citation-js/core": "^0.7.0" "@citation-js/core": "^0.7.0"
} }
}, },
"node_modules/@citation-js/plugin-cff": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-cff/-/plugin-cff-0.6.1.tgz",
"integrity": "sha512-tLjTgsfzNOdQWGn5mNc2NAaydHnlRucSERoyAXLN7u0BQBfp7j5zwdxCmxcQD/N7hH3fpDKMG+qDzbqpJuKyNA==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/plugin-yaml": "^0.6.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@citation-js/plugin-csl": { "node_modules/@citation-js/plugin-csl": {
"version": "0.7.18", "version": "0.7.18",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.18.tgz", "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.18.tgz",
@ -406,6 +420,41 @@
"@citation-js/core": "^0.7.0" "@citation-js/core": "^0.7.0"
} }
}, },
"node_modules/@citation-js/plugin-github": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-github/-/plugin-github-0.6.1.tgz",
"integrity": "sha512-1ZeSgQ5AoYsa8n2acVooUeRk76oA8rLszYNBjzj5z6MPa11BZlQJ9O+Gy4tHjlImvsENLbLPx5f8/V1VHXaCfQ==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@citation-js/plugin-npm": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-npm/-/plugin-npm-0.6.1.tgz",
"integrity": "sha512-rojJA+l/p2KBpDoY+8n0YfNyQO1Aw03fQR5BN+gXD1LNAP1V+8wqvdPsaHnzPsrhrd4ZXDR7ch/Nk0yynPkJ3Q==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@citation-js/plugin-pubmed": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-pubmed/-/plugin-pubmed-0.3.0.tgz",
"integrity": "sha512-E3l83VP5UnTh6lLJaTaNBIkNgIx3U6IjDNf7j5l4geBOaOwDIhkl9X+Ss33/MMNEIMNDsBoodoMJTZ8Cq+C/ug==",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@citation-js/core": ">=0.5.1 <=0.6"
}
},
"node_modules/@citation-js/plugin-ris": { "node_modules/@citation-js/plugin-ris": {
"version": "0.7.18", "version": "0.7.18",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-ris/-/plugin-ris-0.7.18.tgz", "resolved": "https://registry.npmjs.org/@citation-js/plugin-ris/-/plugin-ris-0.7.18.tgz",
@ -421,6 +470,44 @@
"@citation-js/core": "^0.7.0" "@citation-js/core": "^0.7.0"
} }
}, },
"node_modules/@citation-js/plugin-software-formats": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-software-formats/-/plugin-software-formats-0.6.1.tgz",
"integrity": "sha512-BDF9rqi56K0hoTgYTVANCFVRSbWKC9V06Uap7oa8SjqCTgnHJAy8t/F3NxsyYPPG+zmRsLW9VNbcIsJOl0eu/w==",
"dependencies": {
"@citation-js/plugin-cff": "^0.6.1",
"@citation-js/plugin-github": "^0.6.1",
"@citation-js/plugin-npm": "^0.6.1",
"@citation-js/plugin-yaml": "^0.6.1",
"@citation-js/plugin-zenodo": "^0.6.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@citation-js/plugin-yaml": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-yaml/-/plugin-yaml-0.6.1.tgz",
"integrity": "sha512-XEVVks1cJTqRbjy+nmthfw/puR6NwRB3fyJWi1tX13UYXlkhP/h45nsv4zjgLLGekdcMHQvhad9MAYunOftGKA==",
"dependencies": {
"js-yaml": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@citation-js/plugin-zenodo": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@citation-js/plugin-zenodo/-/plugin-zenodo-0.6.1.tgz",
"integrity": "sha512-bUybENHoZqJ6gheUqgkumjI+mu+fA2bg6VoniDmZTb7Qng9iEpi+IWEAR26/vBE0gK0EWrJjczyDW3HCwrhvVw==",
"dependencies": {
"@citation-js/date": "^0.5.0",
"@citation-js/name": "^0.4.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@emotion/babel-plugin": { "node_modules/@emotion/babel-plugin": {
"version": "11.13.5", "version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@ -2103,6 +2190,7 @@
"version": "18.3.26", "version": "18.3.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"dev": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -2436,8 +2524,7 @@
"node_modules/argparse": { "node_modules/argparse": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"dev": true
}, },
"node_modules/array-union": { "node_modules/array-union": {
"version": "2.1.0", "version": "2.1.0",
@ -3784,7 +3871,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
@ -5402,20 +5488,6 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true "dev": true
}, },
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -14,7 +14,9 @@
"@citation-js/plugin-bibtex": "^0.7.18", "@citation-js/plugin-bibtex": "^0.7.18",
"@citation-js/plugin-csl": "^0.7.18", "@citation-js/plugin-csl": "^0.7.18",
"@citation-js/plugin-doi": "^0.7.18", "@citation-js/plugin-doi": "^0.7.18",
"@citation-js/plugin-pubmed": "^0.3.0",
"@citation-js/plugin-ris": "^0.7.18", "@citation-js/plugin-ris": "^0.7.18",
"@citation-js/plugin-software-formats": "^0.6.1",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.10", "@mui/icons-material": "^5.15.10",

View file

@ -1,18 +1,20 @@
import { create } from 'zustand'; import { create } from "zustand";
// @ts-expect-error - citation.js doesn't have TypeScript definitions // @ts-expect-error - citation.js doesn't have TypeScript definitions
import { Cite } from '@citation-js/core'; import { Cite } from "@citation-js/core";
// Load plugins // Load plugins
import '@citation-js/plugin-csl'; import "@citation-js/plugin-csl";
import '@citation-js/plugin-doi'; import "@citation-js/plugin-doi";
import '@citation-js/plugin-bibtex'; import "@citation-js/plugin-bibtex";
import '@citation-js/plugin-ris'; import "@citation-js/plugin-ris";
import "@citation-js/plugin-pubmed";
import "@citation-js/plugin-software-formats";
import type { import type {
BibliographyReference, BibliographyReference,
CSLReference, CSLReference,
ReferenceAppMetadata, ReferenceAppMetadata,
BibliographySettings BibliographySettings,
} from '../types/bibliography'; } from "../types/bibliography";
interface BibliographyStore { interface BibliographyStore {
// State // State
@ -43,18 +45,22 @@ interface BibliographyStore {
setSettings: (settings: BibliographySettings) => void; setSettings: (settings: BibliographySettings) => void;
// Citation.js powered operations // Citation.js powered operations
formatReference: (id: string, style?: string, format?: 'html' | 'text') => string; formatReference: (
formatBibliography: (style?: string, format?: 'html' | 'text') => string; id: string,
style?: string,
format?: "html" | "text",
) => string;
formatBibliography: (style?: string, format?: "html" | "text") => string;
parseInput: (input: string) => Promise<CSLReference[]>; parseInput: (input: string) => Promise<CSLReference[]>;
exportAs: (format: 'bibtex' | 'ris' | 'json') => string; exportAs: (format: "bibtex" | "ris" | "json") => string;
} }
export const useBibliographyStore = create<BibliographyStore>((set, get) => ({ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
citeInstance: new Cite([]), citeInstance: new Cite([]),
appMetadata: {}, appMetadata: {},
settings: { settings: {
defaultStyle: 'apa', defaultStyle: "apa",
sortOrder: 'author', sortOrder: "author",
}, },
getReferences: () => { getReferences: () => {
@ -62,7 +68,7 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
const metadata = get().appMetadata; const metadata = get().appMetadata;
// Merge CSL data with app metadata // Merge CSL data with app metadata
return csl.map(ref => ({ return csl.map((ref) => ({
...ref, ...ref,
_app: metadata[ref.id], _app: metadata[ref.id],
})); }));
@ -70,7 +76,7 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
getReferenceById: (id) => { getReferenceById: (id) => {
const refs = get().getReferences(); const refs = get().getReferences();
return refs.find(ref => ref.id === id); return refs.find((ref) => ref.id === id);
}, },
getCSLData: () => { getCSLData: () => {
@ -81,7 +87,8 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
const { citeInstance } = get(); const { citeInstance } = get();
// Generate ID if not provided // Generate ID if not provided
const id = ref.id || `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const id =
ref.id || `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const fullRef = { ...ref, id }; const fullRef = { ...ref, id };
// Use citation.js add() method // Use citation.js add() method
@ -89,7 +96,7 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
// Add app metadata // Add app metadata
const now = new Date().toISOString(); const now = new Date().toISOString();
set(state => ({ set((state) => ({
appMetadata: { appMetadata: {
...state.appMetadata, ...state.appMetadata,
[id]: { [id]: {
@ -109,15 +116,15 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
const data = citeInstance.data as CSLReference[]; const data = citeInstance.data as CSLReference[];
// Find and update the reference // Find and update the reference
const updatedData = data.map(ref => const updatedData = data.map((ref) =>
ref.id === id ? { ...ref, ...updates } : ref ref.id === id ? { ...ref, ...updates } : ref,
); );
// Use citation.js set() method to replace all data // Use citation.js set() method to replace all data
citeInstance.set(updatedData); citeInstance.set(updatedData);
// Update metadata timestamp // Update metadata timestamp
set(state => ({ set((state) => ({
appMetadata: { appMetadata: {
...state.appMetadata, ...state.appMetadata,
[id]: { [id]: {
@ -133,11 +140,11 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
const data = citeInstance.data as CSLReference[]; const data = citeInstance.data as CSLReference[];
// Remove from citation.js // Remove from citation.js
const filteredData = data.filter(ref => ref.id !== id); const filteredData = data.filter((ref) => ref.id !== id);
citeInstance.set(filteredData); citeInstance.set(filteredData);
// Remove metadata // Remove metadata
set(state => { set((state) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [id]: _removed, ...rest } = state.appMetadata; const { [id]: _removed, ...rest } = state.appMetadata;
return { appMetadata: rest }; return { appMetadata: rest };
@ -146,7 +153,7 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
duplicateReference: (id) => { duplicateReference: (id) => {
const original = get().getReferenceById(id); const original = get().getReferenceById(id);
if (!original) return ''; if (!original) return "";
const newId = `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const newId = `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
const duplicate = { const duplicate = {
@ -173,7 +180,7 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
const metadata: Record<string, ReferenceAppMetadata> = {}; const metadata: Record<string, ReferenceAppMetadata> = {};
const now = new Date().toISOString(); const now = new Date().toISOString();
refs.forEach(ref => { refs.forEach((ref) => {
metadata[ref.id] = { metadata[ref.id] = {
id: ref.id, id: ref.id,
tags: [], tags: [],
@ -193,9 +200,9 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
// Add metadata for new references // Add metadata for new references
const now = new Date().toISOString(); const now = new Date().toISOString();
set(state => { set((state) => {
const newMetadata = { ...state.appMetadata }; const newMetadata = { ...state.appMetadata };
refs.forEach(ref => { refs.forEach((ref) => {
if (!newMetadata[ref.id]) { if (!newMetadata[ref.id]) {
newMetadata[ref.id] = { newMetadata[ref.id] = {
id: ref.id, id: ref.id,
@ -215,7 +222,7 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
}, },
updateMetadata: (id, updates) => { updateMetadata: (id, updates) => {
set(state => ({ set((state) => ({
appMetadata: { appMetadata: {
...state.appMetadata, ...state.appMetadata,
[id]: { [id]: {
@ -229,34 +236,34 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
setSettings: (settings) => set({ settings }), setSettings: (settings) => set({ settings }),
formatReference: (id, style, format = 'html') => { formatReference: (id, style, format = "html") => {
const { citeInstance, settings } = get(); const { citeInstance, settings } = get();
const styleToUse = style || settings.defaultStyle; const styleToUse = style || settings.defaultStyle;
// Find the reference // Find the reference
const ref = (citeInstance.data as CSLReference[]).find(r => r.id === id); const ref = (citeInstance.data as CSLReference[]).find((r) => r.id === id);
if (!ref) return ''; if (!ref) return "";
// Create temporary Cite instance for single reference // Create temporary Cite instance for single reference
const cite = new Cite(ref); const cite = new Cite(ref);
// Use citation.js format() method // Use citation.js format() method
return cite.format('bibliography', { return cite.format("bibliography", {
format: format, format: format,
template: styleToUse, template: styleToUse,
lang: 'en-US', lang: "en-US",
}); });
}, },
formatBibliography: (style, format = 'html') => { formatBibliography: (style, format = "html") => {
const { citeInstance, settings } = get(); const { citeInstance, settings } = get();
const styleToUse = style || settings.defaultStyle; const styleToUse = style || settings.defaultStyle;
// Use citation.js format() method on entire instance // Use citation.js format() method on entire instance
return citeInstance.format('bibliography', { return citeInstance.format("bibliography", {
format: format, format: format,
template: styleToUse, template: styleToUse,
lang: 'en-US', lang: "en-US",
}); });
}, },
@ -267,8 +274,8 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
const cite = await Cite.async(input); const cite = await Cite.async(input);
return cite.data as CSLReference[]; return cite.data as CSLReference[];
} catch (error) { } catch (error) {
console.error('Failed to parse input:', error); console.error("Failed to parse input:", error);
throw new Error('Could not parse citation data'); throw new Error("Could not parse citation data");
} }
}, },
@ -277,14 +284,14 @@ export const useBibliographyStore = create<BibliographyStore>((set, get) => ({
// Use citation.js format() method // Use citation.js format() method
switch (format) { switch (format) {
case 'bibtex': case "bibtex":
return citeInstance.format('bibtex'); return citeInstance.format("bibtex");
case 'ris': case "ris":
return citeInstance.format('ris'); return citeInstance.format("ris");
case 'json': case "json":
return JSON.stringify(citeInstance.data, null, 2); return JSON.stringify(citeInstance.data, null, 2);
default: default:
return ''; return "";
} }
}, },
})); }));
@ -295,8 +302,8 @@ export const clearBibliographyForDocumentSwitch = () => {
citeInstance: new Cite([]), citeInstance: new Cite([]),
appMetadata: {}, appMetadata: {},
settings: { settings: {
defaultStyle: 'apa', defaultStyle: "apa",
sortOrder: 'author', sortOrder: "author",
}, },
}); });
}; };

View file

@ -1,17 +1,21 @@
// @ts-expect-error - citation.js doesn't have TypeScript definitions // @ts-expect-error - citation.js doesn't have TypeScript definitions
import { Cite } from '@citation-js/core'; import { Cite } from "@citation-js/core";
import type { CSLReference } from '../../types/bibliography'; import type { CSLReference } from "../../types/bibliography";
/** /**
* Parse any citation input using citation.js * Parse any citation input using citation.js
* Handles: DOI, URL, BibTeX, RIS, ISBN, PubMed ID, etc. * Handles: DOI, URL, BibTeX, RIS, ISBN, PubMed ID, etc.
*/ */
export const parseSmartInput = async (input: string): Promise<CSLReference[]> => { export const parseSmartInput = async (
input: string,
): Promise<CSLReference[]> => {
try { try {
const cite = await Cite.async(input); const cite = await Cite.async(input);
return cite.data as CSLReference[]; return cite.data as CSLReference[];
} catch (error) { } catch (error) {
throw new Error(`Could not parse input: ${error instanceof Error ? error.message : 'Unknown error'}`); throw new Error(
`Could not parse input: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} }
}; };
@ -46,9 +50,8 @@ export const isValidCitationInput = (input: string): boolean => {
// Wikidata ID (Q followed by numbers) // Wikidata ID (Q followed by numbers)
if (/^(wikidata:)?Q\d+$/i.test(trimmed)) return true; if (/^(wikidata:)?Q\d+$/i.test(trimmed)) return true;
// Zenodo ID or URL // Zenodo DOI (10.5281/zenodo.xxxxx)
if (/^zenodo\.\d+$/i.test(trimmed)) return true; if (/^10\.5281\/zenodo\.\d+/i.test(trimmed)) return true;
if (/zenodo\.org\/record\/\d+/i.test(trimmed)) return true;
// CFF (Citation File Format) - YAML-based // CFF (Citation File Format) - YAML-based
if (/^cff-version:/m.test(trimmed)) return true; if (/^cff-version:/m.test(trimmed)) return true;
@ -62,17 +65,16 @@ export const isValidCitationInput = (input: string): boolean => {
export const getInputTypeHint = (input: string): string => { export const getInputTypeHint = (input: string): string => {
const trimmed = input.trim(); const trimmed = input.trim();
if (/^10\.\d{4,}\//.test(trimmed)) return 'DOI'; if (/^10\.5281\/zenodo\.\d+/i.test(trimmed)) return "Zenodo DOI";
if (/zenodo\.org\/record\/\d+/i.test(trimmed)) return 'Zenodo URL'; if (/^10\.\d{4,}\//.test(trimmed)) return "DOI";
if (/^zenodo\.\d+$/i.test(trimmed)) return 'Zenodo ID'; if (/^https?:\/\//.test(trimmed)) return "URL";
if (/^https?:\/\//.test(trimmed)) return 'URL'; if (/^@\w+\{/.test(trimmed)) return "BibTeX";
if (/^@\w+\{/.test(trimmed)) return 'BibTeX'; if (/^TY\s+-\s+/m.test(trimmed)) return "RIS Format";
if (/^TY\s+-\s+/m.test(trimmed)) return 'RIS Format'; if (/^PMID:/i.test(trimmed)) return "PubMed ID (PMID)";
if (/^PMID:/i.test(trimmed)) return 'PubMed ID (PMID)'; if (/^PMC\d+/i.test(trimmed)) return "PubMed Central ID (PMCID)";
if (/^PMC\d+/i.test(trimmed)) return 'PubMed Central ID (PMCID)'; if (/^ISBN/i.test(trimmed)) return "ISBN";
if (/^ISBN/i.test(trimmed)) return 'ISBN'; if (/^(wikidata:)?Q\d+$/i.test(trimmed)) return "Wikidata ID";
if (/^(wikidata:)?Q\d+$/i.test(trimmed)) return 'Wikidata ID'; if (/^cff-version:/m.test(trimmed)) return "Citation File Format (CFF)";
if (/^cff-version:/m.test(trimmed)) return 'Citation File Format (CFF)';
return 'Unknown'; return "Unknown";
}; };