From ad3303c0e883b032174283e8167c2f9d8d6a19bf Mon Sep 17 00:00:00 2001 From: mpstaton Date: Tue, 8 Jul 2025 01:51:47 +0300 Subject: [PATCH] iterate(modal): rebuild of modal code - Fixed method name mismatch between convertCitation and convertCitations - Made generateHexId private and added public getNewHexId method - Improved type safety for CitationMatch interface with explicit URL handling - Cleaned up unused imports and variables in CitationModal - Fixed file handling to use correct Obsidian API types - Updated citation conversion logic to handle multiple groups - Added proper error handling for file operations - Improved code documentation and type annotations - Formatted styles.css for better readability --- main.ts | 22 +- src/modals/CitationModal.ts | 363 ++++++++++-------------- src/services/citationService.ts | 485 ++++++++++++++------------------ src/styles/citations.css | 172 +++++------ styles.css | 146 +++++----- 5 files changed, 543 insertions(+), 645 deletions(-) diff --git a/main.ts b/main.ts index 8848936..b88929c 100644 --- a/main.ts +++ b/main.ts @@ -44,11 +44,23 @@ export default class CiteWidePlugin extends Plugin { editorCallback: async (editor: Editor) => { try { const content = editor.getValue(); - const result = citationService.convertCitations(content); + // Get all citation groups + const groups = citationService.findCitations(content); + let totalConverted = 0; + let updatedContent = content; - if (result.changed) { - editor.setValue(result.updatedContent); - new Notice(`Updated ${result.stats.citationsConverted} citations`); + // Convert each citation group + for (const group of groups) { + const result = citationService.convertCitation(updatedContent, group.number); + if (result.changed) { + updatedContent = result.content; + totalConverted += result.stats.citationsConverted; + } + } + + if (totalConverted > 0) { + editor.setValue(updatedContent); + new Notice(`Updated ${totalConverted} citations`); } else { new Notice('No citations needed conversion'); } @@ -66,7 +78,7 @@ export default class CiteWidePlugin extends Plugin { editorCallback: (editor: Editor) => { try { const cursor = editor.getCursor(); - const hexId = citationService.generateHexId(); + const hexId = citationService.getNewHexId(); // Insert the citation reference at cursor editor.replaceRange(`[^${hexId}]`, cursor); diff --git a/src/modals/CitationModal.ts b/src/modals/CitationModal.ts index 2838f6c..a9ff9fa 100644 --- a/src/modals/CitationModal.ts +++ b/src/modals/CitationModal.ts @@ -1,38 +1,11 @@ -import { App, Modal, Notice, Editor, TFile } from 'obsidian'; -import { citationService, type CitationConversionResult } from '../services/citationService'; +// cite-wide/src/modals/CitationModal.ts +import { App, Modal, Notice, Editor } from 'obsidian'; +import { citationService, type CitationGroup } from '../services/citationService'; -// Extend the Obsidian Modal interface to include contentEl -declare module 'obsidian' { - interface Modal { - contentEl: HTMLElement; - } -} - -/** - * Represents a single citation match in the document - */ -interface CitationMatch { - type: 'footnote' | 'reference' | 'perplexity'; - original: string; - number: string; - position: number; - line: number; - lineContent: string; -} - -interface CitationGroup { - type: string; - number: string; - matches: CitationMatch[]; -} - -/** - * Modal for displaying and managing citations in the current document - */ export class CitationModal extends Modal { - private matches: CitationMatch[] = []; private editor: Editor; private content: string; + private citationGroups: CitationGroup[] = []; constructor(app: App, editor: Editor) { super(app); @@ -40,185 +13,160 @@ export class CitationModal extends Modal { this.content = editor.getValue(); } - async onOpen(): Promise { + async onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.addClass('cite-wide-modal'); - // Find all citations in the document - this.findCitations(); + // Find all citation groups + this.citationGroups = citationService.findCitations(this.content); - // If no citations found, show a message - if (this.matches.length === 0) { - contentEl.createEl('p', { text: 'No citations found in the current document.' }); + if (this.citationGroups.length === 0) { + contentEl.createEl('p', { + text: 'No citations found in the current document.' + }); return; } - // Group citations by type and number - const groupedCitations = this.groupCitations(); - - // Create a container for the citation groups - const container = contentEl.createDiv({ cls: 'citation-groups' }); - - // Render each citation group - Object.entries(groupedCitations).forEach(([key, group]) => { - const [type, number] = key.split(':'); - const groupEl = container.createDiv({ cls: 'citation-group' }); - - // Create header with toggle functionality and actions - const header = groupEl.createDiv({ cls: 'citation-group-header' }); - - // Header content with integrated To Hex button - const headerContent = header.createDiv({ cls: 'citation-group-header-content' }); - const headerTitle = headerContent.createEl('h3'); - - // Add the title text - headerTitle.createSpan({ - text: `${type === 'footnote' ? 'Footnote' : 'Citation'} [${number}] (${group.matches.length} occurrences)` - }); - - // Add Convert to Hex button next to the title - const convertButton = headerTitle.createEl('button', { - text: 'To Hex', - cls: 'mod-cta', - attr: { 'data-action': 'convert-all-group' } - }); - convertButton.addEventListener('click', (e) => { - e.stopPropagation(); - const firstMatch = group.matches[0]; - if (firstMatch) { - this.convertCitationToHex(firstMatch); - } - }); - - // Create content container (initially hidden) - const content = groupEl.createDiv({ cls: 'citation-group-content' }); - - // Add toggle functionality to header - header.onclick = () => { - content.style.display = content.style.display === 'none' ? 'block' : 'none'; - }; - - // Add each citation context - group.matches.forEach((match: CitationMatch) => { - const contextEl = content.createDiv({ cls: 'citation-context' }); - contextEl.createEl('div', { - text: `Line ${match.line + 1}: ${match.lineContent}`, - cls: 'citation-line' - }); - - // Add action buttons - const actions = contextEl.createDiv({ cls: 'citation-actions' }); - - // View button - actions.createEl('button', { - text: 'View', - cls: 'mod-cta', - attr: { 'data-action': 'view' } - }).addEventListener('click', () => this.scrollToCitation(match)); - }); + // Create a container for citation groups + const container = contentEl.createDiv('cite-wide-container'); + + // Add a title + container.createEl('h2', { + text: 'Citations in Document', + cls: 'cite-wide-title' }); - // Add a button to convert all citations - const footer = contentEl.createDiv({ cls: 'modal-button-container' }); - footer.createEl('button', { + // Add each citation group + for (const group of this.citationGroups) { + this.renderCitationGroup(container, group); + } + + // Add convert all button + const footer = contentEl.createDiv('cite-wide-footer'); + const convertAllBtn = footer.createEl('button', { text: 'Convert All to Hex', - cls: 'mod-cta', - attr: { 'data-action': 'convert-all' } - }).addEventListener('click', () => this.convertAllCitations()); - } - - /** - * Groups citations by their type and number - */ - private groupCitations(): Record { - const groups: Record = {}; - - this.matches.forEach((match: CitationMatch) => { - const key = `${match.type}:${match.number}`; - if (!groups[key]) { - groups[key] = { - type: match.type, - number: match.number, - matches: [] - }; - } - groups[key]?.matches.push(match); + cls: 'mod-cta' }); - return groups; + convertAllBtn.addEventListener('click', () => this.convertAllCitations()); } - /** - * Scrolls the editor to a specific citation - */ - private scrollToCitation(match: CitationMatch): void { - const line = Math.max(0, match.line - 2); // Show a couple lines before the match - this.editor.setCursor({ line, ch: 0 }); - this.editor.scrollIntoView({ from: { line, ch: 0 }, to: { line: line + 5, ch: 0 } }); - this.close(); - } + private renderCitationGroup(container: HTMLElement, group: CitationGroup) { + const groupEl = container.createDiv('cite-wide-group'); + const header = groupEl.createDiv('cite-wide-group-header'); + + // Create a collapsible header + const headerContent = header.createDiv('cite-wide-group-header-content'); + headerContent.createEl('h3', { + text: `Citation [${group.number}] (${group.matches.length} instances)`, + cls: 'cite-wide-group-title' + }); - /** - * Finds all citations in the current document - */ - private findCitations(): void { - const lines = this.content.split('\n'); + if (group.url) { + headerContent.createEl('a', { + href: group.url, + text: 'Source', + cls: 'cite-wide-source-link', + attr: { target: '_blank' } + }); + } - lines.forEach((line: string, lineIndex: number) => { - if (!line) return; + // Add convert button + const convertBtn = header.createEl('button', { + text: 'Convert to Hex', + cls: 'mod-cta cite-wide-convert-btn' + }); - // Find footnote references [^1] - const footnoteRegex = /\[\^(\d+)\]/g; - let footnoteMatch; - while ((footnoteMatch = footnoteRegex.exec(line)) !== null) { - this.matches.push({ - type: 'footnote', - number: footnoteMatch[1] || '', - original: footnoteMatch[0] || '', - position: footnoteMatch.index || 0, - line: lineIndex, - lineContent: line - }); - } + convertBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.convertCitationGroup(group); + }); - // Find standard citations [1] (but not links [text](url)) - const citationRegex = /\[(\d+)\]/g; - let citationMatch; - while ((citationMatch = citationRegex.exec(line)) !== null) { - // Skip if it's part of a markdown link - if (!/\]\([^)]*$/.test(line.substring(0, citationMatch.index || 0))) { - this.matches.push({ - type: 'reference', - number: citationMatch[1] || '', - original: citationMatch[0] || '', - position: citationMatch.index || 0, - line: lineIndex, - lineContent: line - }); - } - } + // Create collapsible content + const content = groupEl.createDiv('cite-wide-group-content'); + content.style.display = 'none'; // Start collapsed + + // Toggle content on header click + header.addEventListener('click', () => { + content.style.display = content.style.display === 'none' ? 'block' : 'none'; + }); + + // Add each citation instance + group.matches.forEach((match) => { + const instanceEl = content.createDiv('cite-wide-instance'); + + // Show line number and preview + const lineInfo = instanceEl.createDiv('cite-wide-line-info'); + lineInfo.createEl('span', { + text: `Line ${match.lineNumber + 1}: `, + cls: 'cite-wide-line-number' + }); + + // Create a preview of the line content + const preview = match.lineContent.trim(); + const previewText = preview.length > 100 + ? `${preview.substring(0, 100)}...` + : preview; + + lineInfo.createEl('span', { + text: previewText, + cls: 'cite-wide-line-preview' + }); + + // Add view button + const viewBtn = instanceEl.createEl('button', { + text: 'View', + cls: 'mod-cta-outline cite-wide-view-btn' + }); + + viewBtn.addEventListener('click', () => { + this.scrollToLine(match.lineNumber); + }); }); } - /** - * Converts all citations in the document to hex format - */ - private async convertAllCitations(): Promise { - new Notice('Converting all citations to hex format...'); + private async convertCitationGroup(group: CitationGroup) { try { - const content = this.editor.getValue(); - const result = citationService.convertCitations(content); + const result = citationService.convertCitation( + this.content, + group.number + ); - if (result?.changed) { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - new Notice('No active file found'); - return; + if (result.changed) { + await this.saveChanges(result.content); + new Notice(`Converted citation [${group.number}] to hex format`); + this.close(); + } else { + new Notice('No changes were made to the document'); + } + } catch (error) { + console.error('Error converting citation:', error); + new Notice('Error converting citation. See console for details.'); + } + } + + private async convertAllCitations() { + try { + let updatedContent = this.content; + let totalConverted = 0; + + // Process each group + for (const group of this.citationGroups) { + const result = citationService.convertCitation( + updatedContent, + group.number + ); + + if (result.changed) { + updatedContent = result.content; + totalConverted += result.stats.citationsConverted; } + } - await this.app.vault.modify(activeFile as TFile, result.updatedContent); - new Notice('Successfully converted all citations to hex format'); + if (totalConverted > 0) { + await this.saveChanges(updatedContent); + new Notice(`Converted ${totalConverted} citations to hex format`); this.close(); } else { new Notice('No citations were converted'); @@ -229,41 +177,26 @@ export class CitationModal extends Modal { } } - /** - * Converts a specific citation to hex format - */ - private async convertCitationToHex(match: CitationMatch): Promise { - try { - const content = this.editor.getValue(); - - // Use the citation service to handle the conversion - const result = citationService.convertCitations( - content, - match.type === 'footnote' ? `[^${match.number}]` : `[${match.number}]` - ); - - if (result?.changed) { - // Get the current file - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - new Notice('No active file found'); - return; - } - - await this.app.vault.modify(activeFile as TFile, result.updatedContent); - new Notice(`Successfully converted ${match.type} [${match.number}] to hex format`); - this.close(); - } else { - new Notice(`No changes were made to ${match.type} [${match.number}]`); - } - } catch (error) { - console.error(`Error converting citation ${match.number}:`, error); - new Notice(`Error converting citation ${match.number}. See console for details.`); + private async saveChanges(newContent: string) { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + throw new Error('No active file'); } + + await this.app.vault.modify(activeFile, newContent); + } + + private scrollToLine(lineNumber: number) { + this.editor.setCursor({ line: lineNumber, ch: 0 }); + this.editor.scrollIntoView({ + from: { line: Math.max(0, lineNumber - 2), ch: 0 }, + to: { line: lineNumber + 2, ch: 0 } + }, true); + this.close(); } onClose() { const { contentEl } = this; contentEl.empty(); } -} +} \ No newline at end of file diff --git a/src/services/citationService.ts b/src/services/citationService.ts index 69dd705..10571b2 100644 --- a/src/services/citationService.ts +++ b/src/services/citationService.ts @@ -1,7 +1,24 @@ -import * as crypto from 'crypto'; +// cite-wide/src/services/citationService.ts +import crypto from 'crypto'; -export interface CitationConversionResult { - updatedContent: string; +export interface CitationMatch { + type: 'perplexity' | 'reference' | 'footnote'; + number: string; + original: string; + url: string | undefined; // Explicitly allow undefined + index: number; + lineContent: string; + lineNumber: number; +} + +export interface CitationGroup { + number: string; + matches: CitationMatch[]; + url: string | undefined; // Explicitly allow undefined +} + +export interface ConversionResult { + content: string; changed: boolean; stats: { citationsConverted: number; @@ -9,287 +26,205 @@ export interface CitationConversionResult { } export class CitationService { + private hexCache: Map = new Map(); // Cache URL to hex ID mapping + /** - * Generate a random hex ID of specified length - * @param length - Length of the hex ID to generate (default: 6) - * @returns Random hex string + * Find all citations in the content */ - public generateHexId(length: number = 6): string { - return crypto.randomBytes(Math.ceil(length / 2)) + public findCitations(content: string): CitationGroup[] { + const lines = content.split('\n'); + const matches: CitationMatch[] = []; + const urlMap = new Map(); // Map of number to URL + + // First pass: Find all footnote definitions to map numbers to URLs + lines.forEach((line, _lineIndex) => { + // Match Perplexity-style footnotes: "1. [url](url)" + const footnoteMatch = line.match(/^(\d+)\.\s+(\[([^\]]+)\]\(([^)]+)\)|(https?:\/\/\S+))/); + if (footnoteMatch) { + const number = footnoteMatch[1]; + const url = footnoteMatch[4] || footnoteMatch[2]; + if (number && url) { + urlMap.set(number, url); + } + } + }); + + // Second pass: Find all citations in the content + lines.forEach((line, lineIndex) => { + // Match Perplexity-style citations: [1](url) + const citationRegex = /\[(\d+)\]\(([^)]+)\)/g; + let citationMatch; + + while ((citationMatch = citationRegex.exec(line)) !== null) { + const [fullMatch, number, url] = citationMatch; + + // Skip if number is undefined + if (!number) continue; + + const match: CitationMatch = { + type: 'perplexity', + number, + original: fullMatch, + index: citationMatch.index, + lineContent: line, + lineNumber: lineIndex, + url: url || undefined + }; + + matches.push(match); + } + + // Match standard markdown links that might be citations + const linkRegex = /\[(\d+)\]/g; + let linkMatch; + + while ((linkMatch = linkRegex.exec(line)) !== null) { + const [fullMatch, number] = linkMatch; + + // Skip if number is undefined or if it's part of a markdown link + if (!number || line.substring(0, linkMatch.index).endsWith('](')) { + continue; + } + + const url = urlMap.get(number); + + // Create the match object with all required properties + const match: CitationMatch = { + type: 'reference', + number, + original: fullMatch, + index: linkMatch.index, + lineContent: line, + lineNumber: lineIndex, + url: url || undefined + }; + + matches.push(match); + } + }); + + // Group matches by number + const groups = new Map(); + + matches.forEach(match => { + if (!groups.has(match.number)) { + const newGroup: CitationGroup = { + number: match.number, + matches: [], + url: match.url + }; + groups.set(match.number, newGroup); + } + groups.get(match.number)?.matches.push(match); + }); + + return Array.from(groups.values()); + } + + /** + * Convert a specific citation to hex format + */ + public convertCitation( + content: string, + citationNumber: string, + hexId?: string + ): ConversionResult { + const groups = this.findCitations(content); + const group = groups.find(g => g.number === citationNumber); + + if (!group) { + return { content, changed: false, stats: { citationsConverted: 0 } }; + } + + // Generate or use provided hex ID + const targetHexId = hexId || this.generateHexId(); + const url = group.url || group.matches[0]?.url; + + if (!url) { + return { content, changed: false, stats: { citationsConverted: 0 } }; + } + + // Cache the URL to hex ID mapping + this.hexCache.set(url, targetHexId); + + let updatedContent = content; + let citationsConverted = 0; + + // Process in reverse to avoid position shifting + const sortedMatches = [...group.matches].sort((a, b) => b.index - a.index); + + for (const match of sortedMatches) { + const before = updatedContent.substring(0, match.index); + const after = updatedContent.substring(match.index + match.original.length); + + // Ensure proper spacing + const needsLeadingSpace = !before.endsWith(' ') && !before.endsWith('\n') && before.length > 0; + const needsTrailingSpace = !after.startsWith(' ') && !after.startsWith('\n') && after.length > 0; + + const replacement = `${needsLeadingSpace ? ' ' : ''}[^${targetHexId}]${needsTrailingSpace ? ' ' : ''}`; + + updatedContent = before + replacement + after; + citationsConverted++; + } + + // Add or update the footnote definition + const footnoteDef = `[^${targetHexId}]: ${url}`; + const footnoteSection = this.ensureFootnoteSection(updatedContent); + + if (!footnoteSection.content.includes(`[^${targetHexId}]:`)) { + updatedContent = updatedContent.replace( + footnoteSection.marker, + `${footnoteSection.marker}\n${footnoteDef}` + ); + } + + return { + content: updatedContent, + changed: citationsConverted > 0, + stats: { citationsConverted } + }; + } + + /** + * Ensure the document has a footnotes section + */ + private ensureFootnoteSection(content: string): { content: string; marker: string } { + const footnoteMarker = '\n\n# Footnotes\n'; + + if (content.includes(footnoteMarker)) { + return { content, marker: footnoteMarker }; + } + + const altMarker = '\n## Footnotes\n'; + if (content.includes(altMarker)) { + return { content, marker: altMarker }; + } + + // Add a new footnotes section at the end + return { + content: content + footnoteMarker, + marker: footnoteMarker + }; + } + + /** + * Generate a consistent hex ID for a given URL + */ + private generateHexId(length: number = 6): string { + return crypto + .randomBytes(Math.ceil(length / 2)) .toString('hex') .slice(0, length); } /** - * Convert citations to random hex format - * @param content - The markdown content to process - * @param targetCitation - Optional specific citation to convert (e.g., '[1]' or '[^1]') - * @returns Object with updated content and statistics + * Generate a new hex ID for citations + * @returns A new unique hex ID */ - /** - * Enhanced citation matching function that handles multiple citation formats - */ - private basicCitationMatch(content: string): Array<{ - type: 'footnote' | 'reference' | 'perplexity'; - number: string; - index: number; - original: string; - lineContent: string; - }> { - const matches: Array<{ - type: 'footnote' | 'reference' | 'perplexity'; - number: string; - index: number; - original: string; - lineContent: string; - }> = []; - - // Split content into lines to handle Perplexity-style footnotes - const lines = content.split('\n'); - let currentPosition = 0; - - for (const line of lines) { - // 1. Check for Perplexity-style footnotes (e.g., "1. [https://...]") - const perplexityMatch = line.match(/^(\d+)\.\s+\[(https?:\/\/[^\]]+)\]/); - if (perplexityMatch && perplexityMatch[1] && perplexityMatch[0]) { - matches.push({ - type: 'perplexity', - number: perplexityMatch[1], - original: perplexityMatch[0], - index: currentPosition + line.indexOf(perplexityMatch[0]), - lineContent: line - }); - } - - // 2. Find standard footnote references [^1] - const footnoteRegex = /\[\^(\d+)\]/g; - let footnoteMatch; - while ((footnoteMatch = footnoteRegex.exec(line)) !== null) { - if (footnoteMatch[1] && footnoteMatch[0]) { - matches.push({ - type: 'footnote', - number: footnoteMatch[1], - original: footnoteMatch[0], - index: currentPosition + (footnoteMatch.index || 0), - lineContent: line - }); - } - } - - // 3. Find standard citations [1] (but not links [text](url)) - const citationRegex = /\[(\d+)\]/g; - let citationMatch; - while ((citationMatch = citationRegex.exec(line)) !== null) { - // Skip if it's part of a markdown link - if (citationMatch[1] && citationMatch[0] && - !/\]\([^)]*$/.test(line.substring(0, citationMatch.index || 0))) { - matches.push({ - type: 'reference', - number: citationMatch[1], - original: citationMatch[0], - index: currentPosition + (citationMatch.index || 0), - lineContent: line - }); - } - } - - // Update position for the next line - currentPosition += line.length + 1; // +1 for the newline character - } - - return matches; - } - - /** - * Enhanced citation replacement function - */ - private basicMatchAndReplace( - content: string, - targetNumber: string, - hexId: string, - matchType: 'footnote' | 'reference' | 'perplexity' = 'reference', - lineContent: string = '' - ): string { - if (matchType === 'perplexity' && lineContent) { - // For Perplexity-style footnotes, replace the entire line with a footnote reference - const url = lineContent.match(/^(\d+)\.\s+\[(https?:\/\/[^\]]+)\]/)?.[2] || ''; - const footnoteDef = `[^${hexId}]: ${url}`; - - // Replace the line with just the footnote reference - let updatedContent = content.replace( - lineContent, - `[^${hexId}]` - ); - - // Add footnotes section if it doesn't exist - if (!updatedContent.includes('# Footnotes')) { - updatedContent += '\n\n# Footnotes\n'; - } - - // Add the footnote definition if it doesn't exist - if (!updatedContent.includes(`[^${hexId}]:`)) { - updatedContent += `\n${footnoteDef}`; - } - - return updatedContent; - } else { - // For standard footnotes and references, replace only the specific instance - const matches = this.basicCitationMatch(content); - const targetMatch = matches.find(m => - m.type === matchType && - m.number === targetNumber && - (matchType === 'perplexity' ? m.lineContent === lineContent : true) - ); - - if (!targetMatch) { - return content; - } - - // For reference type with URL, convert to footnote format and add URL to footnotes - const citationWithUrlPattern = new RegExp(`\\[${targetNumber}\\]\\(([^)]+)\\)`); - if (matchType === 'reference' && citationWithUrlPattern.test(targetMatch.original)) { - // Extract the URL from the original citation - const urlMatch = targetMatch.original.match(citationWithUrlPattern); - const url = urlMatch ? urlMatch[1] : ''; - - // Replace the entire [number](url) with [^hex] - const before = content.substring(0, targetMatch.index); - const after = content.substring(targetMatch.index + targetMatch.original.length); - - // Add the URL to the footnotes section if it doesn't exist - let updatedContent = `${before}[^${hexId}]${after}`; - - // Ensure footnotes section exists - if (!updatedContent.includes('# Footnotes')) { - updatedContent += '\n\n# Footnotes\n'; - } - - // Add the footnote definition if it doesn't exist - if (!updatedContent.includes(`[^${hexId}]:`)) { - updatedContent += `\n[^${hexId}]: ${url}`; - } - - return updatedContent; - } else { - // For other types, just replace the number inside the brackets - const before = content.substring(0, targetMatch.index + (matchType === 'footnote' ? 2 : 1)); - const after = content.substring(targetMatch.index + targetMatch.original.length - 1); - return `${before}${hexId}${after}`; - } - } - } - - public convertCitations(content: string, targetCitation?: string): CitationConversionResult { - if (!content) { - return { updatedContent: content || '', changed: false, stats: { citationsConverted: 0 } }; - } - - const matches = this.basicCitationMatch(content); - if (matches.length === 0) { - return { updatedContent: content, changed: false, stats: { citationsConverted: 0 } }; - } - - let updatedContent = content; - const processedOriginals = new Set(); - let citationsConverted = 0; - - // If we have a target citation, only process that one - const matchesToProcess = targetCitation - ? matches.filter(m => m.original === targetCitation) - : matches; - - // Process each match in reverse order to avoid position shifting issues - for (let i = matchesToProcess.length - 1; i >= 0; i--) { - const match = matchesToProcess[i]; - if (!match || processedOriginals.has(match.original)) { - continue; - } - - try { - // Generate a hex ID for this citation - const hexId = this.generateHexId(); - - // Replace the citation - updatedContent = this.basicMatchAndReplace( - updatedContent, - match.number, - hexId, - match.type, - match.lineContent - ); - - citationsConverted++; - processedOriginals.add(match.original); - } catch (error) { - console.error(`Error processing citation ${match.number}:`, error); - } - } - - try { - // Map to store URL to hex ID mappings - const urlToHexMap = new Map(); - const hexToUrlMap = new Map(); - - // First pass: Find all [number](url) patterns and replace with [^hex] - // This handles citations that are adjacent to text or punctuation - updatedContent = updatedContent.replace(/([^\s\[]|^)\[(\d+)\]\(([^)]+)\)/g, (_match, prefix, _number, url) => { - let hexId: string; - - // If we've seen this URL before, use the existing hex ID - if (urlToHexMap.has(url)) { - hexId = urlToHexMap.get(url)!; - } else { - // Generate a new hex ID for this URL - hexId = this.generateHexId(); - urlToHexMap.set(url, hexId); - hexToUrlMap.set(hexId, url); - } - - citationsConverted++; - // Add a space before the citation if it's not at the start of the line - const space = prefix === '' ? '' : ' '; - return `${prefix}${space}[^${hexId}]`; - }); - - // If we found any citations, add the footnotes section - if (citationsConverted > 0) { - // Check if footnotes section already exists - const hasFootnotesSection = /\n#+\s*Footnotes\s*\n/.test(updatedContent); - - // If no footnotes section exists, add one - if (!hasFootnotesSection) { - updatedContent += '\n\n# Footnotes\n'; - } else { - updatedContent += '\n'; - } - - // Add all footnote definitions - hexToUrlMap.forEach((url, hexId) => { - // Check if this footnote is already defined - const footnoteRegex = new RegExp(`\\[\\^${hexId}\\]:.*`, 'g'); - if (!footnoteRegex.test(updatedContent)) { - updatedContent += `\n[^${hexId}]: ${url}`; - } - }); - } - - return { - updatedContent, - changed: citationsConverted > 0, - stats: { - citationsConverted - } - }; - } catch (error) { - console.error('Error processing citations:', error); - return { - updatedContent: content, // Return original content on error - changed: false, - stats: { citationsConverted: 0 } - }; - } + public getNewHexId(): string { + return this.generateHexId(); } } // Export a singleton instance -export const citationService = new CitationService(); +export const citationService = new CitationService(); \ No newline at end of file diff --git a/src/styles/citations.css b/src/styles/citations.css index 2534b7b..ccc93ad 100644 --- a/src/styles/citations.css +++ b/src/styles/citations.css @@ -1,98 +1,108 @@ -/* Citations Modal Styles */ +/* cite-wide/src/styles/citations.css */ + .cite-wide-modal { - padding: 20px; + padding: 1.5rem; + max-width: 800px; + margin: 0 auto; } -.cite-wide-modal h3 { +.cite-wide-title { margin-top: 0; - color: var(--text-normal); - border-bottom: 1px solid var(--background-modifier-border); - padding-bottom: 10px; -} - -.cite-wide-table { - width: 100%; - border-collapse: collapse; - margin: 15px 0; - font-size: 0.9em; -} - -.cite-wide-table th, -.cite-wide-table td { - padding: 8px 12px; - text-align: left; + padding-bottom: 0.75rem; border-bottom: 1px solid var(--background-modifier-border); } -.cite-wide-table th { - font-weight: 600; - color: var(--text-muted); - background-color: var(--background-primary-alt); +.cite-wide-container { + margin-top: 1rem; } -.cite-type-cell { - font-weight: 600; - color: var(--text-accent); - width: 100px; -} - -.cite-number-cell { - width: 80px; - text-align: center !important; -} - -.cite-context-cell { - max-width: 200px; +.cite-wide-group { + margin-bottom: 1.5rem; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -.cite-actions-cell { - width: 150px; - text-align: right !important; - white-space: nowrap; -} - -.cite-actions-cell button { - margin-left: 4px; - padding: 4px 8px; - font-size: 0.8em; -} - -.cite-actions-cell button.mod-cta { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - border: none; - border-radius: 4px; - cursor: pointer; -} - -.cite-actions-cell button.mod-cta:hover { - background-color: var(--interactive-accent-hover); -} - -.button-spacer { - display: inline-block; - width: 6px; -} - -.modal-button-container { +.cite-wide-group-header { display: flex; - justify-content: flex-end; - margin-top: 20px; - padding-top: 15px; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background-color: var(--background-secondary); + cursor: pointer; + transition: background-color 0.2s; +} + +.cite-wide-group-header:hover { + background-color: var(--background-modifier-hover); +} + +.cite-wide-group-header-content { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.cite-wide-group-title { + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +.cite-wide-source-link { + font-size: 0.85rem; + opacity: 0.8; + text-decoration: none; +} + +.cite-wide-group-content { + padding: 0.75rem 1rem; + background-color: var(--background-primary); border-top: 1px solid var(--background-modifier-border); } -/* Make the modal responsive */ -@media (max-width: 600px) { - .cite-wide-table { - display: block; - overflow-x: auto; - } - - .cite-context-cell { - max-width: 150px; - } +.cite-wide-instance { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid var(--background-modifier-border); } + +.cite-wide-instance:last-child { + border-bottom: none; +} + +.cite-wide-line-info { + flex: 1; + font-family: var(--font-monospace); + font-size: 0.9em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cite-wide-line-number { + color: var(--text-muted); + margin-right: 0.5rem; +} + +.cite-wide-line-preview { + opacity: 0.9; +} + +.cite-wide-convert-btn { + margin-left: 0.5rem; +} + +.cite-wide-view-btn { + margin-left: 0.5rem; +} + +.cite-wide-footer { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--background-modifier-border); + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/styles.css b/styles.css index 0c114b7..0cfbc59 100644 --- a/styles.css +++ b/styles.css @@ -1,82 +1,90 @@ /* src/styles/citations.css */ .cite-wide-modal { - padding: 20px; + padding: 1.5rem; + max-width: 800px; + margin: 0 auto; } -.cite-wide-modal h3 { +.cite-wide-title { margin-top: 0; - color: var(--text-normal); - border-bottom: 1px solid var(--background-modifier-border); - padding-bottom: 10px; -} -.cite-wide-table { - width: 100%; - border-collapse: collapse; - margin: 15px 0; - font-size: 0.9em; -} -.cite-wide-table th, -.cite-wide-table td { - padding: 8px 12px; - text-align: left; + padding-bottom: 0.75rem; border-bottom: 1px solid var(--background-modifier-border); } -.cite-wide-table th { - font-weight: 600; - color: var(--text-muted); - background-color: var(--background-primary-alt); +.cite-wide-container { + margin-top: 1rem; } -.cite-type-cell { - font-weight: 600; - color: var(--text-accent); - width: 100px; -} -.cite-number-cell { - width: 80px; - text-align: center !important; -} -.cite-context-cell { - max-width: 200px; +.cite-wide-group { + margin-bottom: 1.5rem; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -.cite-actions-cell { - width: 150px; - text-align: right !important; - white-space: nowrap; -} -.cite-actions-cell button { - margin-left: 4px; - padding: 4px 8px; - font-size: 0.8em; -} -.cite-actions-cell button.mod-cta { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - border: none; - border-radius: 4px; - cursor: pointer; -} -.cite-actions-cell button.mod-cta:hover { - background-color: var(--interactive-accent-hover); -} -.button-spacer { - display: inline-block; - width: 6px; -} -.modal-button-container { +.cite-wide-group-header { display: flex; - justify-content: flex-end; - margin-top: 20px; - padding-top: 15px; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background-color: var(--background-secondary); + cursor: pointer; + transition: background-color 0.2s; +} +.cite-wide-group-header:hover { + background-color: var(--background-modifier-hover); +} +.cite-wide-group-header-content { + display: flex; + align-items: center; + gap: 0.5rem; +} +.cite-wide-group-title { + margin: 0; + font-size: 1rem; + font-weight: 600; +} +.cite-wide-source-link { + font-size: 0.85rem; + opacity: 0.8; + text-decoration: none; +} +.cite-wide-group-content { + padding: 0.75rem 1rem; + background-color: var(--background-primary); border-top: 1px solid var(--background-modifier-border); } -@media (max-width: 600px) { - .cite-wide-table { - display: block; - overflow-x: auto; - } - .cite-context-cell { - max-width: 150px; - } +.cite-wide-instance { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + border-bottom: 1px solid var(--background-modifier-border); +} +.cite-wide-instance:last-child { + border-bottom: none; +} +.cite-wide-line-info { + flex: 1; + font-family: var(--font-monospace); + font-size: 0.9em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.cite-wide-line-number { + color: var(--text-muted); + margin-right: 0.5rem; +} +.cite-wide-line-preview { + opacity: 0.9; +} +.cite-wide-convert-btn { + margin-left: 0.5rem; +} +.cite-wide-view-btn { + margin-left: 0.5rem; +} +.cite-wide-footer { + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--background-modifier-border); + display: flex; + justify-content: flex-end; }