progress(modal): progress on citations modal, now isolates single

citations

 On branch development
 Changes to be committed:
	modified:   src/modals/CitationModal.ts
	modified:   src/services/citationService.ts
	modified:   src/styles/citations.css
This commit is contained in:
mpstaton 2025-07-08 02:12:40 +03:00
parent ad3303c0e8
commit 9f6cbfd4c0
3 changed files with 179 additions and 100 deletions

View file

@ -93,7 +93,7 @@ export class CitationModal extends Modal {
});
// Add each citation instance
group.matches.forEach((match) => {
group.matches.forEach((match, matchIndex) => {
const instanceEl = content.createDiv('cite-wide-instance');
// Show line number and preview
@ -114,15 +114,28 @@ export class CitationModal extends Modal {
cls: 'cite-wide-line-preview'
});
// Add view button
const viewBtn = instanceEl.createEl('button', {
// Add view and convert buttons
const btnContainer = instanceEl.createDiv('cite-wide-instance-actions');
const viewBtn = btnContainer.createEl('button', {
text: 'View',
cls: 'mod-cta-outline cite-wide-view-btn'
});
viewBtn.addEventListener('click', () => {
viewBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.scrollToLine(match.lineNumber);
});
const convertBtn = btnContainer.createEl('button', {
text: 'Convert',
cls: 'mod-cta cite-wide-convert-btn'
});
convertBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await this.convertCitationInstance(group, matchIndex);
});
});
}
@ -146,6 +159,48 @@ export class CitationModal extends Modal {
}
}
private async convertCitationInstance(group: CitationGroup, matchIndex: number) {
try {
const match = group.matches[matchIndex];
if (!match) return;
// Get current cursor position
const cursor = this.editor.getCursor();
// Convert just this specific instance
const result = citationService.convertCitation(
this.content,
group.number,
undefined, // Let it generate a new hex ID
matchIndex // Convert only this specific instance
);
if (result.changed) {
// Update the content
await this.saveChanges(result.content);
// Calculate new cursor position
const offsetChange = result.content.length - this.content.length;
const newCursor = {
line: cursor.line,
ch: cursor.ch + (match.index <= cursor.ch ? offsetChange : 0)
};
// Update content and restore cursor
this.content = result.content;
this.editor.setCursor(newCursor);
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 instance:', error);
new Notice('Error converting citation. See console for details.');
}
}
private async convertAllCitations() {
try {
let updatedContent = this.content;

View file

@ -26,82 +26,68 @@ export interface ConversionResult {
}
export class CitationService {
private hexCache: Map<string, string> = new Map(); // Cache URL to hex ID mapping
/**
* Find all citations in the content
*/
public findCitations(content: string): CitationGroup[] {
const lines = content.split('\n');
const matches: CitationMatch[] = [];
const urlMap = new Map<string, string>(); // 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 = {
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,
original: fullMatch,
index: citationMatch.index,
number: perplexityMatch[1],
original: perplexityMatch[0],
url: undefined, // URL will be set in the group
index: currentPosition + line.indexOf(perplexityMatch[0]),
lineContent: line,
lineNumber: lineIndex,
url: url || undefined
};
matches.push(match);
lineNumber: 0 // Will be set in the group
});
}
// 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;
// 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],
url: undefined,
index: currentPosition + (footnoteMatch.index || 0),
lineContent: line,
lineNumber: 0 // Will be set in the group
});
}
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);
}
});
// 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],
url: undefined,
index: currentPosition + (citationMatch.index || 0),
lineContent: line,
lineNumber: 0 // Will be set in the group
});
}
}
// Update position for the next line
currentPosition += line.length + 1; // +1 for the newline character
}
// Group matches by number
const groups = new Map<string, CitationGroup>();
@ -127,7 +113,8 @@ export class CitationService {
public convertCitation(
content: string,
citationNumber: string,
hexId?: string
hexId?: string,
matchIndex: number = -1
): ConversionResult {
const groups = this.findCitations(content);
const group = groups.find(g => g.number === citationNumber);
@ -138,44 +125,54 @@ export class CitationService {
// 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;
const url = group.matches[0]?.url;
// Process in reverse to avoid position shifting
const sortedMatches = [...group.matches].sort((a, b) => b.index - a.index);
// If a specific match index is provided, only convert that one
const matchesToProcess = matchIndex >= 0 && matchIndex < group.matches.length
? [group.matches[matchIndex]]
: [...group.matches].sort((a, b) => b.index - a.index);
for (const match of sortedMatches) {
// Process matches in reverse order to avoid position shifting
for (const match of matchesToProcess) {
if (!match) continue;
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;
// Replace with hex reference
updatedContent = `${before}[^${targetHexId}]${after}`;
citationsConverted++;
// If this was a Perplexity-style citation, clean up the URL part
if (match.type === 'perplexity' && match.url) {
// Match the URL pattern that might follow the citation
const urlPattern = new RegExp(
`\\[${citationNumber}\\]\\s*\\[([^\]]+)\\]\([^)]+\)`
);
const urlMatch = updatedContent.match(urlPattern);
if (urlMatch) {
const urlStart = updatedContent.indexOf(urlMatch[0]);
const urlEnd = urlStart + urlMatch[0].length;
updatedContent = updatedContent.substring(0, urlStart) +
updatedContent.substring(urlEnd);
}
}
}
// 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}`
);
// Add footnote definition if we have a URL and converted citations
if (citationsConverted > 0 && url) {
const footnoteDef = `[^${targetHexId}]: ${url}`;
const footnoteSection = this.ensureFootnoteSection(updatedContent);
if (!footnoteSection.content.includes(`[^${targetHexId}]:`)) {
updatedContent = updatedContent.replace(
footnoteSection.marker,
`${footnoteSection.marker}\n${footnoteDef}`
);
}
}
return {

View file

@ -91,12 +91,39 @@
opacity: 0.9;
}
.cite-wide-instance-actions {
display: flex;
gap: 0.5rem;
margin-left: 0.75rem;
}
.cite-wide-convert-btn,
.cite-wide-view-btn {
white-space: nowrap;
padding: 0.25rem 0.75rem;
font-size: 0.85em;
line-height: 1.4;
border-radius: 4px;
transition: all 0.1s ease;
}
.cite-wide-convert-btn {
margin-left: 0.5rem;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: 1px solid var(--interactive-accent-hover);
}
.cite-wide-convert-btn:hover {
background-color: var(--interactive-accent-hover);
}
.cite-wide-view-btn {
margin-left: 0.5rem;
background-color: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
}
.cite-wide-view-btn:hover {
background-color: var(--background-modifier-hover);
}
.cite-wide-footer {