# Bibliography System - Technical Specification (Revised for citation.js) ## Overview This document provides detailed technical specifications for implementing the bibliography/reference management system in Constellation Analyzer, **deeply integrated with the citation.js library** to avoid reinventing functionality. Based on the UX design in `UX_BIBLIOGRAPHY_DESIGN.md`. **Key Change**: This revision leverages citation.js for all parsing, formatting, and conversion tasks, eliminating custom implementations where the library already provides the functionality. --- ## 1. Citation.js Architecture ### 1.1 What citation.js Provides Citation.js will handle: - ✅ **Input parsing**: DOI, BibTeX, RIS, URLs, ISBNs, PubMed IDs, etc. - ✅ **Format conversion**: Between BibTeX, RIS, CSL-JSON, and more - ✅ **Citation formatting**: APA, MLA, Chicago, Vancouver, and 10,000+ CSL styles - ✅ **Data normalization**: All inputs → CSL-JSON internally - ✅ **Bibliography generation**: Formatted HTML, text, RTF output - ✅ **Async fetching**: DOI/ISBN/PubMed metadata retrieval ### 1.2 What We Build on Top We still need to build: - ❌ UI components (modals, forms, selectors) - ❌ State management (Zustand stores) - ❌ Document persistence (storing bibliography with graph) - ❌ App-specific metadata (tags, favorites, usage tracking) - ❌ Citation linking (connecting references to nodes/edges) - ❌ History tracking (undo/redo integration) ### 1.3 Integration Strategy **Core Pattern**: Use a `Cite` instance per document as the single source of truth for references, synced with our Zustand store for UI state. ```typescript // citation.js manages the CSL-JSON data const citeInstance = new Cite(references); // Our store manages app-specific data and UI state const bibliographyStore = { citeInstance, // The citation.js instance appMetadata: {}, // App-specific fields (_app) // ... other state }; ``` --- ## 2. Data Models ### 2.1 CSL-JSON Types (from citation.js) Citation.js uses standard CSL-JSON. We import types from `@citation-js/core` or define compatible interfaces: **Location**: `/src/types/bibliography.ts` ```typescript /** * CSL-JSON types are provided by citation.js * We extend with app-specific metadata */ // Standard CSL-JSON types (compatible with citation.js) export type ReferenceType = | 'article-journal' | 'article-magazine' | 'article-newspaper' | 'book' | 'chapter' | 'paper-conference' | 'report' | 'thesis' | 'webpage' | 'interview' | 'manuscript' | 'personal_communication' | 'entry-encyclopedia' | 'entry-dictionary'; /** * Standard CSL-JSON reference structure * This is what citation.js expects */ export interface CSLReference { id: string; type: ReferenceType; title?: string; author?: Array<{ family?: string; given?: string; literal?: string }>; issued?: { 'date-parts': [[number, number?, number?]] }; 'container-title'?: string; publisher?: string; volume?: string | number; issue?: string | number; page?: string; DOI?: string; ISBN?: string; URL?: string; // ... many more CSL-JSON fields [key: string]: any; // CSL-JSON is extensible } /** * App-specific metadata (NOT part of CSL-JSON) * Stored separately from the Cite instance */ export interface ReferenceAppMetadata { id: string; // Matches CSL reference ID tags?: string[]; favorite?: boolean; color?: string; createdAt: string; updatedAt: string; } /** * Combined reference for UI display * Merges CSL data + app metadata */ export interface BibliographyReference extends CSLReference { _app?: ReferenceAppMetadata; } /** * Bibliography settings per document */ export interface BibliographySettings { defaultStyle: string; // CSL style ID (e.g., "apa", "chicago") sortOrder: 'author' | 'year' | 'title'; } /** * Complete bibliography data structure for persistence */ export interface Bibliography { // CSL-JSON array (can be loaded directly into Cite) references: CSLReference[]; // App-specific metadata (stored separately) metadata: Record; // Settings settings: BibliographySettings; } ``` ### 2.2 Extend Existing Types **Location**: `/src/types/index.ts` ```typescript // Add to ActorData interface export interface ActorData { // ... existing fields citations?: string[]; // Array of reference IDs } // Add to RelationData interface export interface RelationData { // ... existing fields citations?: string[]; // Array of reference IDs } ``` **Location**: `/src/stores/persistence/types.ts` ```typescript // Add to ConstellationDocument interface export interface ConstellationDocument { metadata: {...}; nodeTypes: NodeTypeConfig[]; edgeTypes: EdgeTypeConfig[]; labels?: LabelConfig[]; bibliography?: Bibliography; // NEW FIELD timeline: {...}; } ``` --- ## 3. State Management with citation.js ### 3.1 Bibliography Store (Revised) **Location**: `/src/stores/bibliographyStore.ts` ```typescript import { create } from 'zustand'; import { Cite } from '@citation-js/core'; // Load plugins import '@citation-js/plugin-csl'; import '@citation-js/plugin-doi'; import '@citation-js/plugin-bibtex'; import '@citation-js/plugin-ris'; import type { Bibliography, BibliographyReference, CSLReference, ReferenceAppMetadata, BibliographySettings } from '@/types/bibliography'; interface BibliographyStore { // State citeInstance: Cite; // The citation.js instance appMetadata: Record; // App-specific data settings: BibliographySettings; // Getters getReferences: () => BibliographyReference[]; // Merges CSL + app data getReferenceById: (id: string) => BibliographyReference | undefined; getCSLData: () => CSLReference[]; // Raw CSL-JSON from Cite // CRUD operations (use citation.js methods) addReference: (ref: Partial) => string; updateReference: (id: string, updates: Partial) => void; deleteReference: (id: string) => void; duplicateReference: (id: string) => string; // Bulk operations setReferences: (refs: CSLReference[]) => void; importReferences: (refs: CSLReference[]) => void; clearAll: () => void; // Metadata operations updateMetadata: (id: string, metadata: Partial) => void; // Settings setSettings: (settings: BibliographySettings) => void; // Citation.js powered operations formatReference: (id: string, style?: string, format?: 'html' | 'text') => string; formatBibliography: (style?: string, format?: 'html' | 'text') => string; parseInput: (input: string) => Promise[]>; exportAs: (format: 'bibtex' | 'ris' | 'json') => string; } export const useBibliographyStore = create((set, get) => ({ citeInstance: new Cite([]), appMetadata: {}, settings: { defaultStyle: 'apa', sortOrder: 'author', }, getReferences: () => { const csl = get().citeInstance.data as CSLReference[]; const metadata = get().appMetadata; // Merge CSL data with app metadata return csl.map(ref => ({ ...ref, _app: metadata[ref.id], })); }, getReferenceById: (id) => { const refs = get().getReferences(); return refs.find(ref => ref.id === id); }, getCSLData: () => { return get().citeInstance.data as CSLReference[]; }, addReference: (ref) => { const { citeInstance } = get(); // Generate ID if not provided const id = ref.id || `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const fullRef = { ...ref, id }; // Use citation.js add() method citeInstance.add(fullRef); // Add app metadata const now = new Date().toISOString(); set(state => ({ appMetadata: { ...state.appMetadata, [id]: { id, tags: [], createdAt: now, updatedAt: now, }, }, })); return id; }, updateReference: (id, updates) => { const { citeInstance } = get(); const data = citeInstance.data as CSLReference[]; // Find and update the reference const updatedData = data.map(ref => ref.id === id ? { ...ref, ...updates } : ref ); // Use citation.js set() method to replace all data citeInstance.set(updatedData); // Update metadata timestamp set(state => ({ appMetadata: { ...state.appMetadata, [id]: { ...state.appMetadata[id], updatedAt: new Date().toISOString(), }, }, })); }, deleteReference: (id) => { const { citeInstance } = get(); const data = citeInstance.data as CSLReference[]; // Remove from citation.js const filteredData = data.filter(ref => ref.id !== id); citeInstance.set(filteredData); // Remove metadata set(state => { const { [id]: removed, ...rest } = state.appMetadata; return { appMetadata: rest }; }); }, duplicateReference: (id) => { const original = get().getReferenceById(id); if (!original) return ''; const newId = `ref-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; const duplicate = { ...original, id: newId, title: `${original.title} (Copy)`, }; // Remove _app before adding to Cite const { _app, ...cslData } = duplicate; get().addReference(cslData); return newId; }, setReferences: (refs) => { const { citeInstance } = get(); // Use citation.js set() to replace all references citeInstance.set(refs); // Initialize metadata for new references const metadata: Record = {}; const now = new Date().toISOString(); refs.forEach(ref => { metadata[ref.id] = { id: ref.id, tags: [], createdAt: now, updatedAt: now, }; }); set({ appMetadata: metadata }); }, importReferences: (refs) => { const { citeInstance } = get(); // Use citation.js add() to append references citeInstance.add(refs); // Add metadata for new references const now = new Date().toISOString(); set(state => { const newMetadata = { ...state.appMetadata }; refs.forEach(ref => { if (!newMetadata[ref.id]) { newMetadata[ref.id] = { id: ref.id, tags: [], createdAt: now, updatedAt: now, }; } }); return { appMetadata: newMetadata }; }); }, clearAll: () => { get().citeInstance.reset(); // citation.js method to clear all data set({ appMetadata: {} }); }, updateMetadata: (id, updates) => { set(state => ({ appMetadata: { ...state.appMetadata, [id]: { ...state.appMetadata[id], ...updates, updatedAt: new Date().toISOString(), }, }, })); }, setSettings: (settings) => set({ settings }), formatReference: (id, style, format = 'html') => { const { citeInstance, settings } = get(); const styleToUse = style || settings.defaultStyle; // Find the reference const ref = (citeInstance.data as CSLReference[]).find(r => r.id === id); if (!ref) return ''; // Create temporary Cite instance for single reference const cite = new Cite(ref); // Use citation.js format() method return cite.format('bibliography', { format: format, template: styleToUse, lang: 'en-US', }); }, formatBibliography: (style, format = 'html') => { const { citeInstance, settings } = get(); const styleToUse = style || settings.defaultStyle; // Use citation.js format() method on entire instance return citeInstance.format('bibliography', { format: format, template: styleToUse, lang: 'en-US', }); }, parseInput: async (input) => { try { // Use citation.js async parsing // This handles DOIs, URLs, BibTeX, RIS, etc. automatically const cite = await Cite.async(input); return cite.data as CSLReference[]; } catch (error) { console.error('Failed to parse input:', error); throw new Error('Could not parse citation data'); } }, exportAs: (format) => { const { citeInstance } = get(); // Use citation.js format() method switch (format) { case 'bibtex': return citeInstance.format('bibtex'); case 'ris': return citeInstance.format('ris'); case 'json': return JSON.stringify(citeInstance.data, null, 2); default: return ''; } }, })); ``` ### 3.2 Integration with GraphStore (Unchanged) **Location**: `/src/hooks/useBibliographyWithHistory.ts` ```typescript import { useGraphStore } from '@/stores/graphStore'; import { useBibliographyStore } from '@/stores/bibliographyStore'; import { useHistoryStore } from '@/stores/historyStore'; import { useWorkspaceStore } from '@/stores/workspaceStore'; import type { CSLReference } from '@/types/bibliography'; /** * Hook that wraps bibliography operations with history tracking * Similar to useGraphWithHistory pattern */ export const useBibliographyWithHistory = () => { const { addReference, updateReference, deleteReference } = useBibliographyStore(); const { pushHistory } = useHistoryStore(); const { activeDocumentId } = useWorkspaceStore(); const { nodes, edges, updateNode, updateEdge } = useGraphStore(); const addReferenceWithHistory = (ref: Partial) => { const id = addReference(ref); pushHistory(activeDocumentId!, `Added reference: ${ref.title}`); return id; }; const updateReferenceWithHistory = (id: string, updates: Partial) => { const ref = useBibliographyStore.getState().getReferenceById(id); updateReference(id, updates); pushHistory(activeDocumentId!, `Updated reference: ${ref?.title}`); }; const deleteReferenceWithHistory = (id: string) => { const ref = useBibliographyStore.getState().getReferenceById(id); // Remove citations from nodes and edges nodes.forEach(node => { if (node.data.citations?.includes(id)) { updateNode(node.id, { citations: node.data.citations.filter(cid => cid !== id), }); } }); edges.forEach(edge => { if (edge.data?.citations?.includes(id)) { updateEdge(edge.id, { citations: edge.data.citations.filter(cid => cid !== id), }); } }); deleteReference(id); pushHistory(activeDocumentId!, `Deleted reference: ${ref?.title}`); }; const getCitationCount = (referenceId: string) => { const nodeCount = nodes.filter(n => n.data.citations?.includes(referenceId)).length; const edgeCount = edges.filter(e => e.data?.citations?.includes(referenceId)).length; return { nodes: nodeCount, edges: edgeCount }; }; return { addReference: addReferenceWithHistory, updateReference: updateReferenceWithHistory, deleteReference: deleteReferenceWithHistory, getCitationCount, }; }; ``` --- ## 4. Utility Functions (Simplified with citation.js) ### 4.1 Smart Input Parser (Uses citation.js) **Location**: `/src/utils/bibliography/smart-parser.ts` ```typescript import { Cite } from '@citation-js/core'; import type { CSLReference } from '@/types/bibliography'; /** * Parse any citation input using citation.js * Handles: DOI, URL, BibTeX, RIS, ISBN, PubMed ID, etc. */ export const parseSmartInput = async (input: string): Promise => { try { const cite = await Cite.async(input); return cite.data as CSLReference[]; } catch (error) { throw new Error(`Could not parse input: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Check if input looks like it can be parsed */ export const isValidCitationInput = (input: string): boolean => { const trimmed = input.trim(); // DOI patterns if (/^(https?:\/\/)?(dx\.)?doi\.org\/10\.\d{4,}/i.test(trimmed)) return true; if (/^10\.\d{4,}\/.+/.test(trimmed)) return true; // URL patterns if (/^https?:\/\/.+/.test(trimmed)) return true; // BibTeX patterns if (/^@\w+\{/.test(trimmed)) return true; // PubMed ID if (/^PMID:\s*\d+/i.test(trimmed)) return true; // ISBN if (/^ISBN[:\s]*[\d-]+/i.test(trimmed)) return true; return false; }; /** * Get input type hint for user */ export const getInputTypeHint = (input: string): string => { const trimmed = input.trim(); if (/^10\.\d{4,}\//.test(trimmed)) return 'DOI'; if (/^https?:\/\//.test(trimmed)) return 'URL'; if (/^@\w+\{/.test(trimmed)) return 'BibTeX'; if (/^PMID:/i.test(trimmed)) return 'PubMed ID'; if (/^ISBN/i.test(trimmed)) return 'ISBN'; return 'Unknown'; }; ``` ### 4.2 Citation Formatting (Uses citation.js) **Location**: `/src/utils/bibliography/formatting.ts` ```typescript import { Cite } from '@citation-js/core'; import type { CSLReference } from '@/types/bibliography'; /** * Format a single reference using citation.js */ export const formatReference = ( ref: CSLReference, style: string = 'apa', format: 'html' | 'text' = 'text' ): string => { try { const cite = new Cite(ref); return cite.format('bibliography', { format, template: style, lang: 'en-US', }); } catch (error) { console.error('Formatting error:', error); return `[Error formatting reference: ${ref.title}]`; } }; /** * Format short citation for lists (Author, Year) * Uses citation.js citation format */ export const formatShortCitation = ( ref: CSLReference, style: string = 'apa' ): string => { try { const cite = new Cite(ref); // Use citation format (in-text) instead of bibliography return cite.format('citation', { format: 'text', template: style, lang: 'en-US', }); } catch (error) { // Fallback to simple format const author = ref.author?.[0]; const authorStr = author?.family || author?.literal || 'Unknown'; const year = ref.issued?.['date-parts']?.[0]?.[0] || 'n.d.'; return `${authorStr} (${year})`; } }; /** * Format full bibliography for multiple references */ export const formatBibliography = ( refs: CSLReference[], style: string = 'apa', format: 'html' | 'text' = 'html' ): string => { try { const cite = new Cite(refs); return cite.format('bibliography', { format, template: style, lang: 'en-US', }); } catch (error) { console.error('Bibliography formatting error:', error); return '[Error formatting bibliography]'; } }; /** * Get list of available citation styles * Citation.js supports 10,000+ styles via CSL */ export const getAvailableStyles = (): Array<{ id: string; label: string }> => { // Common styles for social sciences return [ { id: 'apa', label: 'APA 7th Edition' }, { id: 'chicago-author-date', label: 'Chicago Author-Date' }, { id: 'chicago-note-bibliography', label: 'Chicago Notes' }, { id: 'mla', label: 'MLA 9th Edition' }, { id: 'harvard1', label: 'Harvard' }, { id: 'vancouver', label: 'Vancouver' }, { id: 'american-sociological-association', label: 'ASA' }, { id: 'american-political-science-association', label: 'APSA' }, ]; }; ``` ### 4.3 Import/Export Utilities (Uses citation.js) **Location**: `/src/utils/bibliography/import-export.ts` ```typescript import { Cite } from '@citation-js/core'; import type { CSLReference } from '@/types/bibliography'; /** * Import from various formats using citation.js */ export const importFromFile = async ( content: string, format: 'bibtex' | 'ris' | 'json' ): Promise => { try { const cite = await Cite.async(content); return cite.data as CSLReference[]; } catch (error) { throw new Error(`Failed to import ${format}: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Export to various formats using citation.js */ export const exportToFormat = ( refs: CSLReference[], format: 'bibtex' | 'ris' | 'json' ): string => { try { const cite = new Cite(refs); switch (format) { case 'bibtex': return cite.format('bibtex'); case 'ris': return cite.format('ris'); case 'json': return JSON.stringify(cite.data, null, 2); default: throw new Error(`Unsupported format: ${format}`); } } catch (error) { throw new Error(`Failed to export: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Export formatted bibliography as HTML */ export const exportFormattedBibliography = ( refs: CSLReference[], style: string = 'apa' ): string => { try { const cite = new Cite(refs); const bibliography = cite.format('bibliography', { format: 'html', template: style, lang: 'en-US', }); return ` Bibliography

Bibliography

${bibliography} `; } catch (error) { throw new Error(`Failed to generate HTML: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Detect format of input string */ export const detectFormat = (content: string): 'bibtex' | 'ris' | 'json' | 'unknown' => { const trimmed = content.trim(); if (trimmed.startsWith('@')) return 'bibtex'; if (trimmed.startsWith('TY -')) return 'ris'; if (trimmed.startsWith('[') || trimmed.startsWith('{')) { try { JSON.parse(trimmed); return 'json'; } catch { return 'unknown'; } } return 'unknown'; }; ``` --- ## 5. Component Integration (Key Changes) ### 5.1 Smart Input Component **Location**: `/src/components/Bibliography/SmartInputField.tsx` ```typescript import React, { useState } from 'react'; import { parseSmartInput, isValidCitationInput, getInputTypeHint } from '@/utils/bibliography/smart-parser'; import { useBibliographyWithHistory } from '@/hooks/useBibliographyWithHistory'; import { useToastStore } from '@/stores/toastStore'; interface Props { onSuccess?: () => void; } export const SmartInputField: React.FC = ({ onSuccess }) => { const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const { addReference } = useBibliographyWithHistory(); const { showToast } = useToastStore(); const handleParse = async () => { if (!input.trim()) return; setLoading(true); try { // Use citation.js to parse the input const references = await parseSmartInput(input); if (references.length === 0) { showToast('No references found in input', 'warning'); return; } // Add all parsed references references.forEach(ref => { addReference(ref); }); showToast( `Added ${references.length} reference${references.length > 1 ? 's' : ''}`, 'success' ); setInput(''); onSuccess?.(); } catch (error) { showToast( error instanceof Error ? error.message : 'Failed to parse input', 'error' ); } finally { setLoading(false); } }; const isValid = isValidCitationInput(input); const hint = input.trim() ? getInputTypeHint(input) : ''; return (