mirror of
https://github.com/puhhh/clear-unused-images-obsidian.git
synced 2026-07-22 06:51:54 +00:00
Harden cleanup safety and review all deletions
This commit is contained in:
parent
ace0fcb177
commit
623ff6b4f6
14 changed files with 340 additions and 93 deletions
10
.prettierrc
10
.prettierrc
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 155,
|
||||
"jsxBracketSameLine": true,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
4
main.js
4
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "clear-unused-images",
|
||||
"name": "Clear Unused Images",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"minAppVersion": "0.11.13",
|
||||
"description": "Fork of oz-clear-unused-images for clearing unused images from Obsidian vaults.",
|
||||
"author": "Aleksei Blinov",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "clear-unused-images",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.2",
|
||||
"description": "Fork of oz-clear-unused-images for clearing unused images from Obsidian vaults.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
22
src/frontmatterWalker.ts
Normal file
22
src/frontmatterWalker.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export const walkFrontmatterValues = (
|
||||
frontmatterValue: unknown,
|
||||
visitString: (value: string) => void
|
||||
): void => {
|
||||
if (typeof frontmatterValue === 'string') {
|
||||
visitString(frontmatterValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(frontmatterValue)) {
|
||||
for (const value of frontmatterValue) {
|
||||
walkFrontmatterValues(value, visitString);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (frontmatterValue && typeof frontmatterValue === 'object') {
|
||||
for (const value of Object.values(frontmatterValue as Record<string, unknown>)) {
|
||||
walkFrontmatterValues(value, visitString);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { TFile, App } from 'obsidian';
|
||||
import { extractMarkdownLinkMatches } from './referenceUtils';
|
||||
import { extractMarkdownLinkMatches, parseMarkdownLinkDestination } from './referenceUtils';
|
||||
|
||||
/* -------------------- LINK DETECTOR -------------------- */
|
||||
|
||||
|
|
@ -67,11 +67,15 @@ export const getAllLinkMatchesInFile = async (mdFile: TFile, app: App, fileText?
|
|||
// --> Get All Markdown Links
|
||||
let markdownMatches = extractMarkdownLinkMatches(fileText.toString());
|
||||
if (markdownMatches) {
|
||||
let fileRegex = /(?<=\().*(?=\))/;
|
||||
for (let markdownMatch of markdownMatches) {
|
||||
const destination = parseMarkdownLinkDestination(markdownMatch);
|
||||
if (!destination) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// --> Check if it is Transclusion
|
||||
if (matchIsMdTransclusion(markdownMatch)) {
|
||||
let fileName = getTransclusionFileName(markdownMatch);
|
||||
if (destination.includes('#')) {
|
||||
let fileName = destination.split('#', 1)[0];
|
||||
let file = app.metadataCache.getFirstLinkpathDest(fileName, mdFile.path);
|
||||
if (fileName !== '') {
|
||||
let linkMatch: LinkMatch = {
|
||||
|
|
@ -85,19 +89,16 @@ export const getAllLinkMatchesInFile = async (mdFile: TFile, app: App, fileText?
|
|||
}
|
||||
}
|
||||
// --> Normal Internal Link
|
||||
let fileMatch = markdownMatch.match(fileRegex);
|
||||
if (fileMatch) {
|
||||
// Web links are to be skipped
|
||||
if (fileMatch[0].startsWith('http')) continue;
|
||||
let file = app.metadataCache.getFirstLinkpathDest(fileMatch[0], mdFile.path);
|
||||
let linkMatch: LinkMatch = {
|
||||
type: 'markdown',
|
||||
match: markdownMatch,
|
||||
linkText: file ? file.path : fileMatch[0],
|
||||
sourceFilePath: mdFile.path,
|
||||
};
|
||||
linkMatches.push(linkMatch);
|
||||
}
|
||||
// Web links are to be skipped
|
||||
if (destination.startsWith('http')) continue;
|
||||
let file = app.metadataCache.getFirstLinkpathDest(destination, mdFile.path);
|
||||
let linkMatch: LinkMatch = {
|
||||
type: 'markdown',
|
||||
match: markdownMatch,
|
||||
linkText: file ? file.path : destination,
|
||||
sourceFilePath: mdFile.path,
|
||||
};
|
||||
linkMatches.push(linkMatch);
|
||||
}
|
||||
}
|
||||
return linkMatches;
|
||||
|
|
@ -108,27 +109,19 @@ export const getAllLinkMatchesInFile = async (mdFile: TFile, app: App, fileText?
|
|||
const wikiTransclusionRegex = /\[\[(.*?)#.*?\]\]/;
|
||||
const wikiTransclusionFileNameRegex = /(?<=\[\[)(.*)(?=#)/;
|
||||
|
||||
const mdTransclusionRegex = /\[.*?]\((.*?)#.*?\)/;
|
||||
const mdTransclusionFileNameRegex = /(?<=\]\()(.*)(?=#)/;
|
||||
|
||||
const matchIsWikiTransclusion = (match: string): boolean => {
|
||||
return wikiTransclusionRegex.test(match);
|
||||
};
|
||||
|
||||
const matchIsMdTransclusion = (match: string): boolean => {
|
||||
return mdTransclusionRegex.test(match);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param match
|
||||
* @returns file name if there is a match or empty string if no match
|
||||
*/
|
||||
const getTransclusionFileName = (match: string): string => {
|
||||
let isWiki = wikiTransclusionRegex.test(match);
|
||||
let isMd = mdTransclusionRegex.test(match);
|
||||
if (isWiki || isMd) {
|
||||
let fileNameMatch = match.match(isWiki ? wikiTransclusionFileNameRegex : mdTransclusionFileNameRegex);
|
||||
if (matchIsWikiTransclusion(match)) {
|
||||
let fileNameMatch = match.match(wikiTransclusionFileNameRegex);
|
||||
if (fileNameMatch) return fileNameMatch[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
|
|
|||
74
src/main.ts
74
src/main.ts
|
|
@ -2,6 +2,7 @@ import { Plugin, TFile, Notice } from 'obsidian';
|
|||
import { OzanClearImagesSettingsTab } from './settings';
|
||||
import { OzanClearImagesSettings, DEFAULT_SETTINGS } from './settings';
|
||||
import { LogsModal } from './modals';
|
||||
import { CleanupReviewModal } from './reviewModal';
|
||||
import * as Util from './util';
|
||||
import { createPeriodicCleanupScheduler, createVaultLoadCleanupScheduler } from './startupCleanup';
|
||||
|
||||
|
|
@ -12,6 +13,10 @@ export default class OzanClearImages extends Plugin {
|
|||
periodicCleanupTimerId: number | undefined = undefined;
|
||||
periodicCleanupScheduler: ReturnType<typeof createPeriodicCleanupScheduler<number>> | undefined = undefined;
|
||||
cleanupInProgress = false;
|
||||
private vaultLayoutReady = false;
|
||||
private vaultMetadataResolved = false;
|
||||
private vaultReadyCallbacks: Array<() => void | Promise<void>> = [];
|
||||
private vaultReadyListenersRegistered = false;
|
||||
|
||||
async onload() {
|
||||
console.log('Clear Unused Images plugin loaded...');
|
||||
|
|
@ -61,11 +66,7 @@ export default class OzanClearImages extends Plugin {
|
|||
|
||||
this.startupCleanupScheduled = true;
|
||||
const scheduleCleanup = createVaultLoadCleanupScheduler(
|
||||
(callback) => {
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
void callback();
|
||||
});
|
||||
},
|
||||
(callback) => this.onVaultReady(callback),
|
||||
async (type) => {
|
||||
await this.clearUnusedAttachments(type);
|
||||
}
|
||||
|
|
@ -77,9 +78,7 @@ export default class OzanClearImages extends Plugin {
|
|||
refreshPeriodicCleanup(): void {
|
||||
if (!this.periodicCleanupScheduler) {
|
||||
this.periodicCleanupScheduler = createPeriodicCleanupScheduler<number>(
|
||||
(callback) => {
|
||||
this.app.workspace.onLayoutReady(callback);
|
||||
},
|
||||
(callback) => this.onVaultReady(callback),
|
||||
(callback, intervalMs) => {
|
||||
this.clearPeriodicCleanupTimer();
|
||||
const timerId = window.setInterval(callback, intervalMs);
|
||||
|
|
@ -93,7 +92,7 @@ export default class OzanClearImages extends Plugin {
|
|||
}
|
||||
},
|
||||
async (type) => {
|
||||
await this.clearUnusedAttachments(type, { silentIfBusy: true });
|
||||
await this.clearUnusedAttachments(type);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -115,6 +114,52 @@ export default class OzanClearImages extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
onVaultReady(callback: () => void | Promise<void>): void {
|
||||
this.ensureVaultReadyListeners();
|
||||
|
||||
if (this.vaultLayoutReady && this.vaultMetadataResolved) {
|
||||
void callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.vaultReadyCallbacks.push(callback);
|
||||
}
|
||||
|
||||
private ensureVaultReadyListeners(): void {
|
||||
if (this.vaultReadyListenersRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.vaultReadyListenersRegistered = true;
|
||||
this.vaultLayoutReady = this.app.workspace.layoutReady;
|
||||
this.vaultMetadataResolved = Object.keys(this.app.metadataCache.resolvedLinks).length > 0;
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.vaultLayoutReady = true;
|
||||
this.flushVaultReadyCallbacks();
|
||||
});
|
||||
|
||||
this.registerEvent(
|
||||
this.app.metadataCache.on('resolved', () => {
|
||||
this.vaultMetadataResolved = true;
|
||||
this.flushVaultReadyCallbacks();
|
||||
})
|
||||
);
|
||||
|
||||
this.flushVaultReadyCallbacks();
|
||||
}
|
||||
|
||||
private flushVaultReadyCallbacks(): void {
|
||||
if (!this.vaultLayoutReady || !this.vaultMetadataResolved || this.vaultReadyCallbacks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callbacks = this.vaultReadyCallbacks.splice(0, this.vaultReadyCallbacks.length);
|
||||
for (const callback of callbacks) {
|
||||
void callback();
|
||||
}
|
||||
}
|
||||
|
||||
clearPeriodicCleanupTimer(): void {
|
||||
if (this.periodicCleanupTimerId !== undefined) {
|
||||
window.clearInterval(this.periodicCleanupTimerId);
|
||||
|
|
@ -139,6 +184,17 @@ export default class OzanClearImages extends Plugin {
|
|||
var unusedAttachments: TFile[] = await Util.getUnusedAttachments(this.app, type);
|
||||
var len = unusedAttachments.length;
|
||||
if (len > 0) {
|
||||
if (type === 'all') {
|
||||
const reviewAccepted = await new CleanupReviewModal(
|
||||
this.app,
|
||||
unusedAttachments.map((file) => file.path)
|
||||
).prompt();
|
||||
if (!reviewAccepted) {
|
||||
new Notice('Cleanup cancelled.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.settings.deleteOption === 'permanent' && !this.confirmPermanentDelete(len, type)) {
|
||||
new Notice('Cleanup cancelled.');
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -21,10 +21,15 @@ export const resolveVaultAttachmentReference = (
|
|||
reference: string,
|
||||
sourcePath: string,
|
||||
resolveLinkpathDest: (referencePath: string, sourceFilePath: string) => string | null,
|
||||
hasExactPath: (path: string) => boolean
|
||||
hasExactPath: (path: string) => boolean,
|
||||
scope: 'image' | 'all' = 'image'
|
||||
): string | null => {
|
||||
const trimmedReference = reference.trim();
|
||||
if (!trimmedReference || EXTERNAL_REFERENCE_REGEX.test(trimmedReference) || !hasImageExtension(trimmedReference)) {
|
||||
if (!trimmedReference || EXTERNAL_REFERENCE_REGEX.test(trimmedReference)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (scope === 'image' && !hasImageExtension(trimmedReference)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +41,30 @@ export const resolveVaultAttachmentReference = (
|
|||
return hasExactPath(trimmedReference) ? trimmedReference : null;
|
||||
};
|
||||
|
||||
export const parseMarkdownLinkDestination = (markdownMatch: string): string => {
|
||||
const openParenIndex = markdownMatch.indexOf('](');
|
||||
if (openParenIndex === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const rawDestination = markdownMatch.slice(openParenIndex + 2, -1).trim();
|
||||
if (!rawDestination) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (rawDestination.startsWith('<')) {
|
||||
const closingAngleIndex = rawDestination.indexOf('>');
|
||||
if (closingAngleIndex === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return unescapeMarkdownDestination(rawDestination.slice(1, closingAngleIndex).trim());
|
||||
}
|
||||
|
||||
const destinationEnd = findMarkdownDestinationEnd(rawDestination);
|
||||
return unescapeMarkdownDestination(rawDestination.slice(0, destinationEnd).trim());
|
||||
};
|
||||
|
||||
export const splitExcludedFolders = (input: string): string[] => {
|
||||
return input
|
||||
.split(',')
|
||||
|
|
@ -123,3 +152,30 @@ const findClosingDelimiter = (text: string, openingIndex: number, openingChar: s
|
|||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const findMarkdownDestinationEnd = (destination: string): number => {
|
||||
let escaped = false;
|
||||
|
||||
for (let index = 0; index < destination.length; index++) {
|
||||
const character = destination[index];
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/\s/.test(character)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return destination.length;
|
||||
};
|
||||
|
||||
const unescapeMarkdownDestination = (destination: string): string => {
|
||||
return destination.replace(/\\([\\[\]()<>\s!#%&'*,.:;=?@^`~])/g, '$1');
|
||||
};
|
||||
|
|
|
|||
71
src/reviewModal.ts
Normal file
71
src/reviewModal.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { App, Modal } from 'obsidian';
|
||||
|
||||
export class CleanupReviewModal extends Modal {
|
||||
private readonly filePaths: string[];
|
||||
private resolveDecision: ((decision: boolean) => void) | undefined;
|
||||
private decisionResolved = false;
|
||||
|
||||
constructor(app: App, filePaths: string[]) {
|
||||
super(app);
|
||||
this.filePaths = filePaths;
|
||||
}
|
||||
|
||||
prompt(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
this.resolveDecision = resolve;
|
||||
this.open();
|
||||
});
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
const headerWrapper = contentEl.createEl('div');
|
||||
headerWrapper.addClass('unused-images-center-wrapper');
|
||||
headerWrapper.createEl('h1', { text: 'Review Unused Files' }).addClass('modal-title');
|
||||
|
||||
contentEl.createEl('p', {
|
||||
text: 'These files are about to be processed by Clear Unused Attachments. Review the exact paths before continuing.',
|
||||
});
|
||||
|
||||
const listWrapper = contentEl.createEl('div');
|
||||
listWrapper.addClass('unused-images-logs');
|
||||
for (const filePath of this.filePaths) {
|
||||
listWrapper.createDiv({ text: filePath });
|
||||
}
|
||||
|
||||
const buttonWrapper = contentEl.createEl('div');
|
||||
buttonWrapper.addClass('unused-images-center-wrapper');
|
||||
|
||||
const cancelButton = buttonWrapper.createEl('button', { text: 'Cancel' });
|
||||
cancelButton.addClass('unused-images-button');
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.closeWithDecision(false);
|
||||
});
|
||||
|
||||
const continueButton = buttonWrapper.createEl('button', { text: 'Continue' });
|
||||
continueButton.addClass('unused-images-button');
|
||||
continueButton.addEventListener('click', () => {
|
||||
this.closeWithDecision(true);
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
if (!this.decisionResolved) {
|
||||
this.decisionResolved = true;
|
||||
this.resolveDecision?.(false);
|
||||
}
|
||||
}
|
||||
|
||||
private closeWithDecision(decision: boolean): void {
|
||||
if (this.decisionResolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.decisionResolved = true;
|
||||
this.resolveDecision?.(decision);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
68
src/util.ts
68
src/util.ts
|
|
@ -8,6 +8,7 @@ import {
|
|||
resolveVaultAttachmentReference,
|
||||
splitExcludedFolders,
|
||||
} from './referenceUtils';
|
||||
import { walkFrontmatterValues } from './frontmatterWalker';
|
||||
|
||||
/* ------------------ Image Handlers ------------------ */
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ export const getUnusedAttachments = async (app: App, type: 'image' | 'all') => {
|
|||
var usedAttachmentsSet: Set<string>;
|
||||
|
||||
// Get Used Attachments in All Markdown Files
|
||||
usedAttachmentsSet = await getAttachmentPathSetForVault(app);
|
||||
usedAttachmentsSet = await getAttachmentPathSetForVault(app, type);
|
||||
|
||||
// Compare All Attachments vs Used Attachments
|
||||
allAttachmentsInVault.forEach((attachment) => {
|
||||
|
|
@ -50,7 +51,7 @@ const getAttachmentsInVault = (app: App, type: 'image' | 'all'): TFile[] => {
|
|||
};
|
||||
|
||||
// New Method for Getting All Used Attachments
|
||||
const getAttachmentPathSetForVault = async (app: App): Promise<Set<string>> => {
|
||||
const getAttachmentPathSetForVault = async (app: App, type: 'image' | 'all'): Promise<Set<string>> => {
|
||||
var attachmentsSet: Set<string> = new Set();
|
||||
var resolvedLinks = app.metadataCache.resolvedLinks;
|
||||
if (resolvedLinks) {
|
||||
|
|
@ -71,23 +72,7 @@ const getAttachmentPathSetForVault = async (app: App): Promise<Set<string>> => {
|
|||
// Frontmatter
|
||||
let fileCache = app.metadataCache.getFileCache(obsFile);
|
||||
if (fileCache.frontmatter) {
|
||||
let frontmatter = fileCache.frontmatter;
|
||||
for (let k of Object.keys(frontmatter)) {
|
||||
if (typeof frontmatter[k] === 'string') {
|
||||
if (frontmatter[k].match(bannerRegex)) {
|
||||
let fileName = frontmatter[k].match(bannerRegex)[1];
|
||||
let file = app.metadataCache.getFirstLinkpathDest(fileName, obsFile.path);
|
||||
if (file) {
|
||||
addToSet(attachmentsSet, file.path);
|
||||
}
|
||||
} else {
|
||||
const resolvedPath = resolveAttachmentReference(app, frontmatter[k], obsFile.path);
|
||||
if (resolvedPath) {
|
||||
addToSet(attachmentsSet, resolvedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
collectFrontmatterAttachmentReferences(fileCache.frontmatter, app, obsFile.path, attachmentsSet, type);
|
||||
}
|
||||
// Any Additional Link
|
||||
let linkMatches: LinkMatch[] = await getAllLinkMatchesInFile(obsFile, app);
|
||||
|
|
@ -220,7 +205,12 @@ const addToSet = (setObj: Set<string>, value: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const resolveAttachmentReference = (app: App, reference: string, sourcePath: string): string | null => {
|
||||
const resolveAttachmentReference = (
|
||||
app: App,
|
||||
reference: string,
|
||||
sourcePath: string,
|
||||
type: 'image' | 'all'
|
||||
): string | null => {
|
||||
return resolveVaultAttachmentReference(
|
||||
reference,
|
||||
sourcePath,
|
||||
|
|
@ -230,11 +220,45 @@ const resolveAttachmentReference = (app: App, reference: string, sourcePath: str
|
|||
},
|
||||
(referencePath) => {
|
||||
const file = app.vault.getAbstractFileByPath(referencePath);
|
||||
return file instanceof TFile && (hasImageExtension(file.path) || file.extension !== 'md');
|
||||
}
|
||||
if (!(file instanceof TFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
return hasImageExtension(file.path);
|
||||
}
|
||||
|
||||
return file.extension !== 'md' && file.extension !== 'canvas';
|
||||
},
|
||||
type
|
||||
);
|
||||
};
|
||||
|
||||
const collectFrontmatterAttachmentReferences = (
|
||||
frontmatterValue: unknown,
|
||||
app: App,
|
||||
sourcePath: string,
|
||||
attachmentsSet: Set<string>,
|
||||
type: 'image' | 'all'
|
||||
) => {
|
||||
walkFrontmatterValues(frontmatterValue, (stringValue) => {
|
||||
const bannerMatch = stringValue.match(bannerRegex);
|
||||
if (bannerMatch) {
|
||||
const fileName = bannerMatch[1];
|
||||
const file = app.metadataCache.getFirstLinkpathDest(fileName, sourcePath);
|
||||
if (file && (type === 'all' || hasImageExtension(file.path))) {
|
||||
addToSet(attachmentsSet, file.path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = resolveAttachmentReference(app, stringValue, sourcePath, type);
|
||||
if (resolvedPath) {
|
||||
addToSet(attachmentsSet, resolvedPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: unknown): string => {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
.unused-images-logs {
|
||||
margin-bottom: 13px;
|
||||
margin-top: 5px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.unused-images-center-wrapper {
|
||||
|
|
|
|||
19
tests/frontmatterWalker.test.ts
Normal file
19
tests/frontmatterWalker.test.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { walkFrontmatterValues } from '../src/frontmatterWalker.ts';
|
||||
|
||||
test('walkFrontmatterValues visits strings inside arrays and nested objects', () => {
|
||||
const visited: string[] = [];
|
||||
|
||||
walkFrontmatterValues(
|
||||
{
|
||||
cover: 'assets/photo.png',
|
||||
attachments: ['docs/report.pdf', { nested: 'audio/song.mp3' }],
|
||||
ignored: 42,
|
||||
},
|
||||
(value) => visited.push(value)
|
||||
);
|
||||
|
||||
assert.deepEqual(visited, ['assets/photo.png', 'docs/report.pdf', 'audio/song.mp3']);
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
extractMarkdownLinkMatches,
|
||||
hasImageExtension,
|
||||
isPathCoveredByExcludedFolder,
|
||||
parseMarkdownLinkDestination,
|
||||
resolveVaultAttachmentReference,
|
||||
} from '../src/referenceUtils.ts';
|
||||
|
||||
|
|
@ -29,6 +30,30 @@ test('resolveVaultAttachmentReference prefers resolved vault path and ignores ex
|
|||
);
|
||||
});
|
||||
|
||||
test('resolveVaultAttachmentReference respects cleanup scope', () => {
|
||||
assert.equal(
|
||||
resolveVaultAttachmentReference(
|
||||
'report.pdf',
|
||||
'notes/daily.md',
|
||||
(referencePath) => (referencePath === 'report.pdf' ? 'docs/report.pdf' : null),
|
||||
() => false,
|
||||
'all'
|
||||
),
|
||||
'docs/report.pdf'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveVaultAttachmentReference(
|
||||
'report.pdf',
|
||||
'notes/daily.md',
|
||||
(referencePath) => (referencePath === 'report.pdf' ? 'docs/report.pdf' : null),
|
||||
() => false,
|
||||
'image'
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveVaultAttachmentReference falls back to exact vault path lookup', () => {
|
||||
const resolvedPath = resolveVaultAttachmentReference(
|
||||
'assets/cover.webp',
|
||||
|
|
@ -54,3 +79,9 @@ test('extractMarkdownLinkMatches keeps paths with parentheses intact', () => {
|
|||
|
||||
assert.deepEqual(matches, ['[photo](assets/photo (1).png)', '[doc](files/report.pdf)']);
|
||||
});
|
||||
|
||||
test('parseMarkdownLinkDestination strips titles and angle brackets', () => {
|
||||
assert.equal(parseMarkdownLinkDestination('[img](assets/photo.png "caption")'), 'assets/photo.png');
|
||||
assert.equal(parseMarkdownLinkDestination('[img](<assets/photo (1).png>)'), 'assets/photo (1).png');
|
||||
assert.equal(parseMarkdownLinkDestination('[doc](<files/report.pdf> "caption")'), 'files/report.pdf');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"1.1.1": "0.11.13",
|
||||
"1.1.0": "0.11.13",
|
||||
"1.0.9": "0.11.13",
|
||||
"1.0.8": "0.11.13",
|
||||
"1.0.7": "0.11.13",
|
||||
"1.0.6": "0.11.13",
|
||||
"1.0.5": "0.11.13",
|
||||
"1.0.4": "0.11.13",
|
||||
"1.0.3": "0.11.13",
|
||||
"1.0.2": "0.11.13",
|
||||
"1.0.1": "0.11.13",
|
||||
"1.0.0": "0.11.13",
|
||||
"0.0.3": "0.11.13",
|
||||
"0.0.2": "0.11.13",
|
||||
"0.0.1": "0.11.13",
|
||||
"0.0.0": "0.11.13"
|
||||
}
|
||||
Loading…
Reference in a new issue