← Corpus / cite-wide / workflow
cite-wide/workflow/2026-05-01_refactor
- Path
- workflow/2026-05-01_Refactor.md
What we’d port into our refactored code
A self-contained list:
- CiteWideSettings — add autoSaveUrlCitations: boolean (recommend default false per your framing), plus the toggle in the settings tab.
- citationFileService — add public saveAllHexCitationsFromContent(content, sourceFile?). His version uses await import(’./citationService’) to dodge a circular dep — I’d check if the cycle is real and fix it properly with a static import or a refactor, instead of dynamic-importing.
- main.ts — gate the citation-file creation in extractAndInsertCitation behind this.settings.autoSaveUrlCitations. Register the new save-all-hex-citations command.
- CitationModal.ts — header button “Save All Hex (N)” when hex count > 0; per-row “Save to Citations” button on hex groups; conditionally show “Convert All” only when numeric count > 0 (his improvement, worth keeping).
- Skip his any types — the audit work we just did would otherwise come undone. Use CitationGroup / CitationMatch types from the existing exports.
The eighth 8a9f3c6 polish commit — I’d skip; we can do our own polish later if needed.
The override path
Two clean options. Both keep our linear history.
Recommended: port his intent first, then force-push.
(1) implement the port — service, settings, modal, command (separate commit)
(2) verify build clean: pnpm build
(3) verify in Obsidian
(4) then:
git push —force-with-lease origin development
The —force-with-lease variant is safer than —force — it only succeeds if the remote ref hasn’t moved since our last fetch. So if Tanuj happens to push something between now and our push, we get a clean rejection instead of clobbering him silently.
This sequence means: at no point is the remote in a state where a real feature is missing. We replace his commits with a refactored version that does the same thing better.
Alternative: force-push first, port after. Faster, but the remote is briefly in a state where the canonical-archive feature is gone (until we port it back). For an active branch that anyone else pulls from, this is worse.
Couple of clarifying questions before I touch anything
- autoSaveUrlCitations default — false or true? Your “some, not all” framing implies false (curation-by-default). Tanuj kept true for backward compatibility. I’d flip it to false, but it’s your call.
- The —force-with-lease push — do you want me to do it, or do you handle pushes manually? I will not run any force-push without you saying so explicitly.
- Bigger “bases” question — do you mean Obsidian’s new Bases feature (the structured database-like view over markdown frontmatter, currently in catalyst) specifically, or “bases” loosely as “a foundational canonical store”? The answer changes whether we should be reshaping the citation file’s frontmatter schema for Bases-compatibility now, or just porting Tanuj’s intent and leaving schema work for later.
Tanuj’s CSS additions (full source)
Tanuj created a new readable source file at src/styles/citations.css and the minified version sits in styles.css at the project root. The minified one is byte-for-byte equivalent to the readable source — just whitespace-stripped. For reviewing the intent, only the readable source matters.
New selectors he added
These did not exist in the pre-refactor styles.css we forked from:
| Selector | Purpose |
|---|---|
.cite-wide-header-buttons | Flex column container for stacking multiple header buttons (Convert All + Save All Hex) |
.cite-wide-save-all-hex-btn (+ :hover) | Green “Save All Hex” button at the top of the modal |
.cite-wide-save-btn (+ :hover) | Green “Save to Citations” per-group button |
.cite-wide-citation-id | Monospace badge showing the hex/numeric ID at the top of each group |
.cite-wide-citation-preview | Truncated single-line preview of the citation text in the group header |
.cite-wide-badge-hex | Accent-colored small badge specifically for hex citations |
.cite-wide-citation-text-section (+ h4) | Highlighted panel inside an expanded group showing the full citation text |
.cite-wide-citation-text | The citation text inside that panel |
.cite-wide-separator | <hr> between citation text and instances list inside a group |
.cite-wide-instances-section h4 | Section header above the per-instance list |
He also restyled almost every pre-existing selector — bigger padding, larger typography, hover transforms (translateY(-1px) + box-shadow), rounded corners.
Full source — src/styles/citations.css from origin/development
/* cite-wide/src/styles/citations.css */
.cite-wide-modal {
padding: 2rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0 auto;
overflow-x: hidden; /* Prevent horizontal scrolling */
line-height: 1.6; /* Improve overall readability */
}
.cite-wide-title {
margin-top: 0;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--background-modifier-border);
font-size: 1.5rem;
font-weight: 600;
color: var(--text-normal);
}
.cite-wide-container {
margin-top: 1.5rem;
width: 100%;
box-sizing: border-box;
overflow-x: hidden; /* Prevent horizontal scrolling */
}
.cite-wide-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.5rem;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--background-modifier-border);
width: 100%;
}
.cite-wide-title {
margin: 0;
flex-shrink: 0;
font-size: 1.5rem;
font-weight: 600;
}
.cite-wide-convert-all-btn {
white-space: nowrap;
padding: 0.75rem 1.25rem;
font-size: 0.9rem;
font-weight: 500;
}
.cite-wide-header-buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: flex-end;
}
.cite-wide-save-all-hex-btn {
background-color: var(--interactive-success);
color: var(--text-on-accent);
border: 1px solid var(--interactive-success-hover);
white-space: nowrap;
padding: 0.75rem 1.25rem;
font-size: 0.9rem;
font-weight: 500;
}
.cite-wide-save-all-hex-btn:hover {
background-color: var(--interactive-success-hover);
}
.cite-wide-group {
margin-bottom: 2rem;
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s ease;
}
.cite-wide-group:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.cite-wide-group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
background-color: var(--background-secondary);
cursor: pointer;
transition: background-color 0.2s;
min-width: 0; /* Allow container to shrink */
overflow: hidden; /* Prevent content from spilling out */
}
.cite-wide-group-header:hover {
background-color: var(--background-modifier-hover);
}
.cite-wide-group-header-content {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0; /* Allow flex items to shrink */
}
.cite-wide-citation-id {
font-weight: 700;
color: var(--text-accent);
font-family: var(--font-monospace);
flex-shrink: 0;
font-size: 1rem;
padding: 0.25rem 0.5rem;
background-color: var(--background-primary);
border-radius: 4px;
border: 1px solid var(--background-modifier-border);
}
.cite-wide-citation-preview {
color: var(--text-muted);
font-size: 0.95rem;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.cite-wide-group-title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
}
.cite-wide-source-link {
font-size: 0.9rem;
opacity: 0.8;
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
transition: background-color 0.2s;
}
.cite-wide-source-link:hover {
background-color: var(--background-modifier-hover);
opacity: 1;
}
.cite-wide-group-content {
padding: 1.25rem;
background-color: var(--background-primary);
border-top: 1px solid var(--background-modifier-border);
}
/* Base instance styling */
.cite-wide-instance {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: 0.5rem;
}
.cite-wide-instance:last-child {
border-bottom: none;
margin-bottom: 0;
}
/* Reference source specific styling */
.cite-wide-instance.cite-wide-reference-source {
background-color: var(--background-secondary-alt);
margin: 0 -1.25rem 1rem -1.25rem;
padding: 1rem 1.25rem;
border-radius: 6px;
border: 1px solid var(--background-modifier-border);
border-left: 4px solid var(--interactive-accent);
}
.cite-wide-line-info {
flex: 1;
font-family: var(--font-monospace);
min-width: 0; /* Allow text to shrink */
overflow: hidden; /* Prevent text from spilling out */
font-size: 0.9rem;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.5;
}
.cite-wide-line-number {
color: var(--text-muted);
margin-right: 0.75rem;
font-weight: 500;
}
.cite-wide-line-preview {
opacity: 0.9;
color: var(--text-normal);
}
/* Badge styling */
.cite-wide-badge {
display: inline-block;
padding: 0.2em 0.6em;
font-size: 0.75rem;
font-weight: 600;
line-height: 1.3;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.cite-wide-badge-reference {
color: var(--text-normal);
background-color: var(--background-modifier-border);
}
.cite-wide-badge-hex {
color: var(--text-on-accent);
background-color: var(--interactive-accent);
font-size: 0.7em;
padding: 0.15em 0.5em;
}
/* Adjust line info spacing when badge is present */
.cite-wide-line-info > .cite-wide-badge + span {
margin-left: 0.75rem;
}
.cite-wide-instance-actions {
display: flex;
gap: 0.75rem;
margin-left: 1rem;
flex-shrink: 0; /* Prevent buttons from shrinking */
}
.cite-wide-convert-btn,
.cite-wide-save-btn,
.cite-wide-view-btn {
white-space: nowrap;
padding: 0.5rem 1rem;
font-size: 0.85rem;
line-height: 1.4;
border-radius: 6px;
transition: all 0.2s ease;
font-weight: 500;
}
.cite-wide-convert-btn {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: 1px solid var(--interactive-accent-hover);
flex-shrink: 0;
}
.cite-wide-convert-btn:hover {
background-color: var(--interactive-accent-hover);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.cite-wide-save-btn {
background-color: var(--interactive-success);
color: var(--text-on-accent);
border: 1px solid var(--interactive-success-hover);
flex-shrink: 0;
}
.cite-wide-save-btn:hover {
background-color: var(--interactive-success-hover);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.cite-wide-view-btn {
background-color: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
}
.cite-wide-view-btn:hover {
background-color: var(--background-modifier-hover);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.cite-wide-footer {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 2px solid var(--background-modifier-border);
display: flex;
justify-content: flex-end;
}
/* Ribbon Icon Styles */
.workspace-ribbon .side-dock-ribbon-action.cite-wide-ribbon-icon {
color: var(--text-muted);
transition: color 0.2s ease;
}
.workspace-ribbon .side-dock-ribbon-action.cite-wide-ribbon-icon:hover {
color: var(--text-normal);
background-color: var(--background-modifier-hover);
}
.workspace-ribbon .side-dock-ribbon-action.cite-wide-ribbon-icon.is-active {
color: var(--interactive-accent);
}
/* Citation Text Section Styles */
.cite-wide-citation-text-section {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: var(--background-secondary);
border-radius: 6px;
border-left: 4px solid var(--interactive-accent);
}
.cite-wide-citation-text-section h4 {
margin: 0 0 0.75rem 0;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.cite-wide-citation-text {
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-normal);
word-wrap: break-word;
white-space: pre-wrap;
padding: 0.5rem 0;
}
.cite-wide-separator {
margin: 1.5rem 0;
border: none;
border-top: 2px solid var(--background-modifier-border);
}
.cite-wide-instances-section h4 {
margin: 0 0 1rem 0;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
Tanuj’s CitationModal (he didn’t write a new modal — he modified the existing one)
He did not introduce a new modal class. He extended src/modals/CitationModal.ts with:
Behavioral additions:
- Top-of-modal “Save All Hex (N)” button when there are hex citations in the file (sibling to the “Convert All” button which is now itself only shown when there are numeric citations to convert).
- Per-group “Save to Citations” button on every hex group (mutually exclusive with the existing “Convert to Hex” button — that one only shows on numeric groups now).
- Group header now shows a citation-ID badge plus a truncated preview of the citation text.
- Expanded group content now shows the full citation text in a callout panel above the per-instance list.
- Per-instance “Convert” buttons no longer appear on hex citations (they shouldn’t — they’re already converted).
Wiring:
- Imports
citationFileServiceand uses it for both the per-group save (saveHexCitationToFile) and bulk save (saveAllHexCitationscalling the service’s newsaveAllHexCitationsFromContent).
Type-safety violations to scrub when porting:
contentEl.closest('.modal-container') as HTMLElement— same pattern we already had to deal with; replace withinstanceof HTMLElementnarrowing.- A
setSelectionimport is missing on hisTFileimport (TFileis imported, fine; this is more of a note that no other types beyond those already in his import line are needed). - His helpers don’t use
anydirectly in this file (thoseanys are in hiscitationFileServiceadditions). The modal itself is reasonably typed.
Full source — src/modals/CitationModal.ts from origin/development
// cite-wide/src/modals/CitationModal.ts
import { App, Modal, Notice, Editor, TFile } from 'obsidian';
import { citationService } from '../services/citationService';
import type { CitationGroup, CitationMatch } from '../services/citationService';
import { citationFileService } from '../services/citationFileService';
export class CitationModal extends Modal {
private editor: Editor;
private content: string;
private citationGroups: CitationGroup[] = [];
constructor(app: App, editor: Editor) {
super(app);
this.editor = editor;
this.content = editor.getValue();
}
async onOpen() {
const { contentEl } = this;
contentEl.empty();
// Make the modal wider
const modalContainer = contentEl.closest('.modal-container') as HTMLElement;
const modalContent = contentEl.closest('.modal-content') as HTMLElement;
if (modalContainer && modalContent) {
// Set the modal container to be very wide
modalContainer.style.width = '95vw';
modalContainer.style.maxWidth = 'none';
// Ensure the content takes full width
modalContent.style.width = '100%';
modalContent.style.maxWidth = 'none';
}
contentEl.addClass('cite-wide-modal');
// Extract all citation groups
this.citationGroups = citationService.extractCitations(this.content);
if (this.citationGroups.length === 0) {
contentEl.createEl('p', {
text: 'No citations found in the current document.'
});
return;
}
// Create a container for citation groups
const container = contentEl.createDiv('cite-wide-container');
// Create header with title and convert all button
const header = container.createDiv('cite-wide-header');
// Add title on the left
header.createEl('h2', {
text: 'Citations in Document',
cls: 'cite-wide-title'
});
// Count numeric and hex citations
const numericCitationCount = this.citationGroups.filter(group => !group.number.startsWith('hex_')).length;
const hexCitationCount = this.citationGroups.filter(group => group.number.startsWith('hex_')).length;
// Create button container for multiple buttons
const buttonContainer = header.createDiv('cite-wide-header-buttons');
// Add convert all button (only if there are numeric citations)
if (numericCitationCount > 0) {
const convertAllBtn = buttonContainer.createEl('button', {
text: `Convert All (${numericCitationCount})`,
cls: 'mod-cta cite-wide-convert-all-btn'
});
convertAllBtn.addEventListener('click', () => this.convertAllCitations());
}
// Add save all hex citations button (only if there are hex citations)
if (hexCitationCount > 0) {
const saveAllHexBtn = buttonContainer.createEl('button', {
text: `Save All Hex (${hexCitationCount})`,
cls: 'mod-cta cite-wide-save-all-hex-btn'
});
saveAllHexBtn.addEventListener('click', () => this.saveAllHexCitations());
}
// Add each citation group
for (const group of this.citationGroups) {
this.renderCitationGroup(container, group);
}
}
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');
// Add citation number/ID display
headerContent.createEl('span', {
text: group.number.startsWith('hex_') ? `[^${group.number.replace('hex_', '')}]` : `[${group.number}]`,
cls: 'cite-wide-citation-id'
});
// Add citation text preview in header
const referenceMatch = group.matches.find(match => match.isReferenceSource);
if (referenceMatch) {
const citationText = referenceMatch.lineContent.replace(/^\s*\[[^\]]+\]:\s*/, '').trim();
const previewText = citationText.length > 80
? `${citationText.substring(0, 80)}...`
: citationText;
headerContent.createEl('span', {
text: previewText,
cls: 'cite-wide-citation-preview'
});
}
if (group.url) {
headerContent.createEl('a', {
href: group.url,
text: 'Source',
cls: 'cite-wide-source-link',
attr: { target: '_blank' }
});
}
// Add convert button only for numeric citations (not hex citations)
if (!group.number.startsWith('hex_')) {
const convertBtn = header.createEl('button', {
text: 'Convert to Hex',
cls: 'mod-cta cite-wide-convert-btn'
});
convertBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.convertCitationGroup(group);
});
} else {
// Add save button for hex citations
const saveBtn = header.createEl('button', {
text: 'Save to Citations',
cls: 'mod-cta cite-wide-save-btn'
});
saveBtn.addEventListener('click', async (e) => {
e.stopPropagation();
const result = await this.saveHexCitationToFile(group);
if (result === 'saved') {
new Notice(`Citation saved to Citations folder: ${group.number.replace('hex_', '')}.md`);
} else if (result === 'updated') {
new Notice(`Citation ${group.number.replace('hex_', '')} already exists and usage updated`);
} else if (result === 'error') {
new Notice('Failed to save citation file');
}
});
}
// 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 full citation text section
if (referenceMatch) {
const citationTextSection = content.createDiv('cite-wide-citation-text-section');
citationTextSection.createEl('h4', { text: 'Citation Text' });
const citationText = referenceMatch.lineContent.replace(/^\s*\[[^\]]+\]:\s*/, '').trim();
citationTextSection.createEl('div', {
text: citationText,
cls: 'cite-wide-citation-text'
});
// Add a separator
content.createEl('hr', { cls: 'cite-wide-separator' });
}
// Add section for citation instances
const instancesSection = content.createDiv('cite-wide-instances-section');
instancesSection.createEl('h4', { text: 'Citation Instances' });
// Add each citation instance
group.matches.forEach((match: CitationMatch, matchIndex: number) => {
const isRefSource = match.isReferenceSource === true;
const instanceEl = instancesSection.createDiv(`cite-wide-instance ${isRefSource ? 'cite-wide-reference-source' : ''}`);
// Show line number and preview
const lineInfo = instanceEl.createDiv('cite-wide-line-info');
// Add a special badge for reference sources
if (isRefSource) {
lineInfo.createEl('span', {
text: 'Reference',
cls: 'cite-wide-badge cite-wide-badge-reference'
});
lineInfo.createEl('span', { text: ' • ' });
}
lineInfo.createEl('span', {
text: `Line ${match.lineNumber}: `,
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 and convert buttons
const btnContainer = instanceEl.createDiv('cite-wide-instance-actions');
// Only add view/convert buttons for non-reference entries
if (!isRefSource) {
const viewBtn = btnContainer.createEl('button', {
text: 'View',
cls: 'mod-cta-outline cite-wide-view-btn'
});
viewBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.scrollToLine(match.lineNumber);
});
}
// Only add convert button for non-reference entries and numeric citations (not hex citations)
if (!isRefSource && !group.number.startsWith('hex_')) {
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);
});
}
});
}
private async convertCitationGroup(group: CitationGroup) {
try {
const result = citationService.convertCitation(
this.content,
group.number
);
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 convertCitationInstance(group: CitationGroup, _matchIndex: number) {
try {
// Use the same approach as convertCitationGroup to ensure footnote conversion
const result = citationService.convertCitation(
this.content,
group.number
);
if (result.changed) {
await this.saveChanges(result.content);
// Update the content for future operations
this.content = result.content;
// Display the original citation format in the notice
const displayNumber = group.number.startsWith('hex_')
? `^${group.number.replace('hex_', '')}`
: group.number;
new Notice(`Converted citation [${displayNumber}] 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;
let totalConverted = 0;
// Process each group, but only numeric citations (not hex citations)
for (const group of this.citationGroups) {
// Skip hex citations that are already converted
if (group.number.startsWith('hex_')) {
continue;
}
const result = citationService.convertCitation(
updatedContent,
group.number
);
if (result.changed) {
updatedContent = result.content;
totalConverted += result.stats.citationsConverted;
}
}
if (totalConverted > 0) {
await this.saveChanges(updatedContent);
new Notice(`Converted ${totalConverted} citations to hex format`);
this.close();
} else {
new Notice('No citations were converted');
}
} catch (error) {
console.error('Error converting citations:', error);
new Notice('Error converting citations. See console for details.');
}
}
private async saveAllHexCitations() {
try {
// Get the current file path for tracking
const activeFile = this.app.workspace.getActiveFile();
const sourceFile = activeFile ? activeFile.path : '';
// Use the shared service method
const result = await citationFileService.saveAllHexCitationsFromContent(this.content, sourceFile);
if (result.saved > 0 || result.updated > 0) {
let message = '';
if (result.saved > 0) {
message += `Saved ${result.saved} new citation(s)`;
}
if (result.updated > 0) {
if (message) message += ', ';
message += `Updated ${result.updated} existing citation(s)`;
}
new Notice(message);
} else {
new Notice('No hex citations were saved');
}
} catch (error) {
console.error('Error saving hex citations:', error);
new Notice('Error saving hex citations. 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) {
try {
// Get the line content
const lineContent = this.editor.getLine(lineNumber);
// Find the citation pattern in the line (matches [1], [2], etc.)
const citationMatch = lineContent.match(/\[(\d+)\]/);
if (citationMatch) {
const citationText = citationMatch[0];
const startPos = citationMatch.index || 0;
const endPos = startPos + citationText.length;
// Create positions for the citation
const from = { line: lineNumber, ch: startPos };
const to = { line: lineNumber, ch: endPos };
// Set cursor to the start of the citation and select it
this.editor.setCursor(from);
this.editor.setSelection(from, to);
// Scroll to make the citation visible with some context
const fromLine = Math.max(0, lineNumber - 2);
const toLine = lineNumber + 2;
// Create a range for the context area
const contextRange = {
from: { line: fromLine, ch: 0 },
to: { line: toLine, ch: 0 }
};
// Scroll to show the context area
this.editor.scrollIntoView(contextRange, true);
// Then scroll to show the selection
this.editor.scrollIntoView({ from, to }, true);
// Focus the editor to show the selection
this.editor.focus();
} else {
// Fallback to just scrolling to the line if no citation pattern is found
const pos = { line: lineNumber, ch: 0 };
this.editor.setCursor(pos);
// Create a range for the context area
const contextRange = {
from: { line: Math.max(0, lineNumber - 2), ch: 0 },
to: { line: lineNumber + 2, ch: 0 }
};
this.editor.scrollIntoView(contextRange, true);
this.editor.focus();
}
// Close the modal after a short delay to ensure the selection is visible
setTimeout(() => {
this.close();
}, 100);
} catch (error) {
console.error('Error scrolling to line:', error);
this.close();
}
}
private async saveHexCitationToFile(group: CitationGroup): Promise<'saved' | 'updated' | 'error' | null> {
try {
// Extract the hex ID from the group number
const hexId = group.number.replace('hex_', '');
// Check if citation file already exists
const filename = `${hexId}.md`;
const filepath = `${citationFileService.getCitationsFolder()}/${filename}`;
const existingFile = this.app.vault.getAbstractFileByPath(filepath);
if (existingFile instanceof TFile) {
// File already exists - just update usage
const activeFile = this.app.workspace.getActiveFile();
const sourceFile = activeFile ? activeFile.path : '';
await citationFileService.updateCitationUsage(existingFile, sourceFile);
return 'updated';
}
// Find the reference source (the actual citation text)
const referenceMatch = group.matches.find(match => match.isReferenceSource);
if (!referenceMatch) {
return 'error';
}
// Extract the reference text (everything after the [^hexId]: part)
const referenceText = referenceMatch.lineContent.replace(/^\s*\[\^[a-z0-9]+\]:\s*/, '').trim();
// Extract URL from the reference text if it exists
let url: string | undefined;
const urlMatch = referenceText.match(/https?:\/\/[^\s\)]+/);
if (urlMatch) {
url = urlMatch[0];
}
// Get the current file path for tracking
const activeFile = this.app.workspace.getActiveFile();
const sourceFile = activeFile ? activeFile.path : '';
// Create the citation file
const result = await citationFileService.createCitationFile(
hexId,
referenceText,
url,
sourceFile
);
if (result) {
return 'saved';
} else {
return 'error';
}
} catch (error) {
console.error('Error saving hex citation to file:', error);
return 'error';
}
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
Tanuj’s citationFileService additions (just the new methods, our refactor would replace the rest)
These are the only two methods in citationFileService.ts that he added (everything else in his version of the file is the pre-refactor structure that our 17193a2 rewrite already replaced):
/**
* Save all hex citations from a document to citation files
* This is a shared method that can be used by both the modal and commands
*/
public async saveAllHexCitationsFromContent(
content: string,
sourceFile?: string
): Promise<{ saved: number; updated: number; errors: number }> {
try {
// Import citation service dynamically to avoid circular dependencies
const { citationService } = await import('./citationService');
// Extract all citations from the content
const citationGroups = citationService.extractCitations(content);
let saved = 0;
let updated = 0;
let errors = 0;
// Process each hex citation group
for (const group of citationGroups) {
if (!group.number.startsWith('hex_')) {
continue; // Skip non-hex citations
}
const result = await this.saveHexCitationFromGroup(group, sourceFile);
if (result === 'saved') {
saved++;
} else if (result === 'updated') {
updated++;
} else {
errors++;
}
}
return { saved, updated, errors };
} catch (error) {
console.error('Error saving all hex citations:', error);
throw error;
}
}
/**
* Save a single hex citation from a citation group
* This is a shared method that can be used by both the modal and commands
*/
private async saveHexCitationFromGroup(
group: any, // ⚠️ TYPE-SAFETY VIOLATION — should be CitationGroup
sourceFile?: string
): Promise<'saved' | 'updated' | 'error'> {
try {
// Extract the hex ID from the group number
const hexId = group.number.replace('hex_', '');
// Check if citation file already exists
const filename = `${hexId}.md`;
const filepath = `${this.citationsFolder}/${filename}`;
const existingFile = this.app.vault.getAbstractFileByPath(filepath);
if (existingFile instanceof TFile) {
// File already exists - just update usage
await this.updateCitationUsage(existingFile, sourceFile);
return 'updated';
}
// Find the reference source (the actual citation text)
const referenceMatch = group.matches.find(
(match: any) => match.isReferenceSource // ⚠️ TYPE-SAFETY VIOLATION — should be CitationMatch
);
if (!referenceMatch) {
return 'error';
}
// Extract the reference text (everything after the [^hexId]: part)
const referenceText = referenceMatch.lineContent.replace(/^\s*\[\^[a-z0-9]+\]:\s*/, '').trim();
// Extract URL from the reference text if it exists
let url: string | undefined;
const urlMatch = referenceText.match(/https?:\/\/[^\s\)]+/);
if (urlMatch) {
url = urlMatch[0];
}
// Create the citation file
const result = await this.createCitationFile(hexId, referenceText, url, sourceFile);
if (result) {
return 'saved';
} else {
return 'error';
}
} catch (error) {
console.error('Error saving hex citation from group:', error);
return 'error';
}
}
Tanuj’s settings + auto-save gating
src/settings/CiteWideSettings.ts — added one field and one toggle:
export interface CiteWideSettings {
jinaApiKey: string;
citationsFolder: string;
autoSaveUrlCitations: boolean; // NEW
}
export const DEFAULT_SETTINGS: CiteWideSettings = {
jinaApiKey: '',
citationsFolder: 'Citations',
autoSaveUrlCitations: true // NEW — Tanuj kept default ON for backward compat
};
// Inside CiteWideSettingTab.display(), added:
new Setting(containerEl)
.setName('Auto-save URL Citations')
.setDesc('Automatically save citations extracted from URLs as citation files. When disabled, citations will only be added to the document without creating separate files.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoSaveUrlCitations)
.onChange(async (value) => {
this.plugin.settings.autoSaveUrlCitations = value;
await this.plugin.saveSettings();
}));
main.ts — gated the auto-save inside extractAndInsertCitation:
// Replace the URL with the full formatted citation
editor.replaceSelection(result.citation);
// Create citation file for Dataview integration only if auto-save is enabled
if (this.settings.autoSaveUrlCitations) {
const activeFile = this.app.workspace.getActiveFile();
const sourceFile = activeFile ? activeFile.path : '';
if (result.citationData) {
await citationFileService.createCitationFileWithData(
result.hexId,
result.citationData,
sourceFile
);
} else {
await citationFileService.createCitationFile(
result.hexId,
result.citation,
url,
sourceFile
);
}
new Notice(`Citation extracted and saved: ${result.hexId}`);
} else {
new Notice(`Citation extracted successfully: ${result.hexId} (not saved to file)`);
}
main.ts — registered new command:
// Command to save all hex citations to citation files
this.addCommand({
id: 'save-all-hex-citations',
name: 'Save All Hex Citations to Citation Files',
editorCallback: async (editor: Editor) => {
try {
await this.saveAllHexCitations(editor);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
new Notice('Error saving hex citations: ' + errorMsg);
}
}
});
// And the helper method that command calls:
private async saveAllHexCitations(editor: Editor): Promise<void> {
const content = editor.getValue();
const activeFile = this.app.workspace.getActiveFile();
const sourceFile = activeFile ? activeFile.path : '';
const result = await citationFileService.saveAllHexCitationsFromContent(content, sourceFile);
if (result.saved > 0 || result.updated > 0) {
let message = '';
if (result.saved > 0) {
message += `Saved ${result.saved} new citation(s)`;
}
if (result.updated > 0) {
if (message) message += ', ';
message += `Updated ${result.updated} existing citation(s)`;
}
new Notice(message);
} else {
new Notice('No hex citations were saved');
}
}