Files
thpeetz-notes/.obsidian/plugins/find-unlinked-files/main.js
T
2026-09-01 11:05:57 +02:00

1207 lines
45 KiB
JavaScript

/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source visit the plugins github repository (https://github.com/Vinzent03/obsidian-advanced-uri)
*/
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => FindOrphanedFilesPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian5 = require("obsidian");
// src/archiveFilesModal.ts
var import_obsidian = require("obsidian");
var ArchiveFilesModal = class extends import_obsidian.Modal {
constructor(app, filesToArchive, archiveDirectory) {
super(app);
this.filesToArchive = filesToArchive;
this.archiveDirectory = archiveDirectory;
}
onOpen() {
let { contentEl, titleEl } = this;
titleEl.setText(
"Move " + this.filesToArchive.length + " files to " + this.archiveDirectory + "?"
);
contentEl.createEl("button", { text: "Cancel" }).addEventListener("click", () => this.close());
contentEl.setAttr("margin", "auto");
contentEl.createEl("button", {
cls: "mod-cta",
text: "Confirm"
}).addEventListener("click", () => {
void this.archiveFiles();
});
}
async archiveFiles() {
let archivedFiles = 0;
let failedFiles = 0;
for (const file of this.filesToArchive) {
try {
const destinationPath = await this.getAvailableArchivePath(file);
if (destinationPath == file.path) continue;
const parentPath = destinationPath.substring(
0,
destinationPath.lastIndexOf("/")
);
await this.createFolderPath(parentPath);
await this.app.fileManager.renameFile(file, destinationPath);
archivedFiles++;
} catch (error) {
console.error("Failed to archive file", file.path, error);
failedFiles++;
}
}
const failureMessage = failedFiles > 0 ? `, ${failedFiles} failed` : "";
new import_obsidian.Notice(
`Moved ${archivedFiles} files to ${this.archiveDirectory}${failureMessage}`
);
this.close();
}
async getAvailableArchivePath(file) {
const archivePath = (0, import_obsidian.normalizePath)(
this.archiveDirectory + "/" + file.path
);
if (!await this.app.vault.adapter.exists(archivePath)) {
return archivePath;
}
const parentPath = archivePath.substring(
0,
archivePath.lastIndexOf("/")
);
const extension = file.extension ? "." + file.extension : "";
const basePath = parentPath ? parentPath + "/" + file.basename : file.basename;
let counter = 1;
let destinationPath = `${basePath} ${counter}${extension}`;
while (await this.app.vault.adapter.exists(destinationPath)) {
counter++;
destinationPath = `${basePath} ${counter}${extension}`;
}
return destinationPath;
}
async createFolderPath(folderPath) {
if (!folderPath) return;
const folders = folderPath.split("/");
let currentPath = "";
for (const folder of folders) {
currentPath = currentPath ? currentPath + "/" + folder : folder;
const existingFile = this.app.vault.getAbstractFileByPath(currentPath);
if (existingFile instanceof import_obsidian.TFolder) continue;
if (existingFile) {
throw new Error(
`Cannot create archive folder because ${currentPath} is a file`
);
}
await this.app.vault.createFolder(currentPath);
}
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
};
// src/deleteFilesModal.ts
var import_obsidian2 = require("obsidian");
var DeleteFilesModal = class extends import_obsidian2.Modal {
constructor(app, filesToDelete, itemName = "files") {
super(app);
this.filesToDelete = filesToDelete;
this.itemName = itemName;
}
onOpen() {
let { contentEl, titleEl } = this;
titleEl.setText(
"Move " + this.filesToDelete.length + " " + this.itemName + " to system trash?"
);
contentEl.createEl("button", { text: "Cancel" }).addEventListener("click", () => this.close());
contentEl.setAttr("margin", "auto");
contentEl.createEl("button", {
cls: "mod-cta",
text: "Confirm"
}).addEventListener("click", () => {
void this.deleteFiles();
});
}
async deleteFiles() {
for (const file of this.filesToDelete) {
if (file instanceof import_obsidian2.TFolder && file.children.length > 0) continue;
await this.app.fileManager.trashFile(file);
}
this.close();
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
};
// src/settingsTab.ts
var import_obsidian4 = require("obsidian");
// src/utils.ts
var import_obsidian3 = require("obsidian");
var Utils = class {
/**
* Checks for the given settings. Is used for `Find orphaned files` and `Find broken links`
* @param app
* @param filePath
* @param tagsToIgnore
* @param linksToIgnore
* @param directoriesToIgnore
* @param filesToIgnore
* @param ignoreDirectories
*/
constructor(app, filePath, tagsToIgnore, linksToIgnore, directoriesToIgnore, filesToIgnore, ignoreDirectories = true, dir) {
this.app = app;
this.filePath = filePath;
this.tagsToIgnore = tagsToIgnore;
this.linksToIgnore = linksToIgnore;
this.directoriesToIgnore = directoriesToIgnore;
this.filesToIgnore = filesToIgnore;
this.ignoreDirectories = ignoreDirectories;
this.dir = dir;
this.fileCache = app.metadataCache.getCache(filePath);
}
static splitCommaSeparatedList(value) {
return value.split(",").map((value2) => value2.trim()).filter((value2) => value2.length > 0);
}
static normalizeStringList(values) {
return values.map((value) => value.trim()).filter((value) => value.length > 0);
}
hasTagsToIgnore() {
if (!this.fileCache) {
return false;
}
const tags = (0, import_obsidian3.getAllTags)(this.fileCache);
return (tags == null ? void 0 : tags.find(
(tag) => this.tagsToIgnore.contains(tag.substring(1))
)) !== void 0;
}
hasLinksToIgnore() {
var _a, _b, _c, _d, _e;
if (!this.fileCache) {
return false;
}
if ((((_a = this.fileCache) == null ? void 0 : _a.embeds) != null || ((_b = this.fileCache) == null ? void 0 : _b.links) != null) && this.linksToIgnore[0] == "*") {
return true;
}
const allLinks = [
...(_c = this.fileCache.embeds) != null ? _c : [],
...(_d = this.fileCache.links) != null ? _d : [],
...(_e = this.fileCache.frontmatterLinks) != null ? _e : []
];
return (0, import_obsidian3.iterateRefs)(allLinks, (cb) => {
var _a2;
const link = (_a2 = this.app.metadataCache.getFirstLinkpathDest(
cb.link,
this.filePath
)) == null ? void 0 : _a2.path;
return link ? this.linksToIgnore.contains(link) : false;
});
}
checkDirectory() {
if (this.dir) {
if (!this.filePath.startsWith(this.dir)) {
return true;
}
}
const contains = this.directoriesToIgnore.find(
(value) => value.length != 0 && this.filePath.startsWith(value)
) !== void 0;
if (this.ignoreDirectories) {
return contains;
} else {
return !contains;
}
}
isFileToIgnore() {
return this.filesToIgnore.contains(this.filePath);
}
shouldIgnoreFile() {
return this.hasTagsToIgnore() || this.hasLinksToIgnore() || this.checkDirectory() || this.isFileToIgnore();
}
/**
* Writes the text to the file and opens the file in a new pane if it is not opened yet
* @param app
* @param outputFileName name of the output file
* @param text data to be written to the file
*/
static async writeAndOpenFile(app, outputFileName, text, openFile) {
await app.vault.adapter.write(outputFileName, text);
if (!openFile) return;
let fileIsAlreadyOpened = false;
app.workspace.iterateAllLeaves((leaf) => {
if (leaf.getDisplayText() != "" && outputFileName.startsWith(leaf.getDisplayText())) {
fileIsAlreadyOpened = true;
}
});
if (!fileIsAlreadyOpened) {
const newPane = app.workspace.getLeavesOfType("empty").length == 0;
if (newPane) {
await app.workspace.openLinkText(outputFileName, "/", true);
} else {
const file = app.vault.getAbstractFileByPath(outputFileName);
const emptyLeaf = app.workspace.getLeavesOfType("empty")[0];
if (file instanceof import_obsidian3.TFile && emptyLeaf) {
await emptyLeaf.openFile(file);
} else {
await app.workspace.openLinkText(outputFileName, "/", true);
}
}
}
}
};
// src/settingsTab.ts
var SettingsTab = class extends import_obsidian4.PluginSettingTab {
constructor(app, plugin, defaultSettings) {
super(app, plugin);
this.defaultSettings = defaultSettings;
this.plugin = plugin;
}
// Add trailing slash to catch files named like the directory. See https://github.com/Vinzent03/find-unlinked-files/issues/24
formatPath(path, addDirectorySlash) {
if (path.length == 0) return path;
path = (0, import_obsidian4.normalizePath)(path);
if (addDirectorySlash) return path + "/";
else return path;
}
display() {
let { containerEl } = this;
containerEl.empty();
new import_obsidian4.Setting(containerEl).setName("Find orphaned files").setHeading();
new import_obsidian4.Setting(containerEl).setName("Open output file").addToggle(
(cb) => cb.setValue(this.plugin.settings.openOutputFile).onChange((value) => {
this.plugin.settings.openOutputFile = value;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Output file name").setDesc(
"Set name of output file (without file extension). Make sure no file exists with this name because it will be overwritten! If the name is empty, the default name is set."
).addText(
(cb) => cb.onChange((value) => {
if (value.length == 0) {
this.plugin.settings.outputFileName = this.defaultSettings.outputFileName;
} else {
this.plugin.settings.outputFileName = value;
}
void this.plugin.saveSettings();
}).setValue(this.plugin.settings.outputFileName)
);
new import_obsidian4.Setting(containerEl).setName("Sort output by").setDesc("Choose the order of files in the generated output file").addDropdown(
(cb) => cb.addOption("size", "Size").addOption("alphabetical", "Alphabetical").setValue(this.plugin.settings.orphanedFilesSortOrder).onChange((value) => {
this.plugin.settings.orphanedFilesSortOrder = value;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Disable working links").setDesc(
"Indent lines to disable the link and to clean up the graph view"
).addToggle(
(cb) => cb.onChange((value) => {
this.plugin.settings.disableWorkingLinks = value;
void this.plugin.saveSettings();
}).setValue(this.plugin.settings.disableWorkingLinks)
);
new import_obsidian4.Setting(containerEl).setName("Exclude files in the given directories").setDesc(
"Enable to exclude files in the given directories. Disable to only include files in the given directories"
).addToggle(
(cb) => cb.setValue(this.plugin.settings.ignoreDirectories).onChange((value) => {
this.plugin.settings.ignoreDirectories = value;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Directories").setDesc("Add each directory path in a new line").addTextArea(
(cb) => cb.setPlaceholder("Directory/Subdirectory").setValue(
this.plugin.settings.directoriesToIgnore.join("\n")
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, true));
this.plugin.settings.directoriesToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude files").setDesc("Add each file path in a new line (with file extension!)").addTextArea(
(cb) => cb.setPlaceholder("Directory/file.md").setValue(this.plugin.settings.filesToIgnore.join("\n")).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, false));
this.plugin.settings.filesToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude links").setDesc(
"Exclude files, which contain the given file as link. Add each file path in a new line (with file extension!). Set it to `*` to exclude files with links."
).addTextArea(
(cb) => cb.setPlaceholder("Directory/file.md").setValue(this.plugin.settings.linksToIgnore.join("\n")).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, false));
this.plugin.settings.linksToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude files with the given filetypes").setDesc(
"Enable to exclude files with the given filetypes. Disable to only include files with the given filetypes"
).addToggle(
(cb) => cb.setValue(this.plugin.settings.ignoreFileTypes).onChange((value) => {
this.plugin.settings.ignoreFileTypes = value;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("File types").setDesc("Effect depends on toggle above").addTextArea(
(cb) => cb.setPlaceholder("docx,txt").setValue(this.plugin.settings.fileTypesToIgnore.join(",")).onChange((value) => {
let extensions = Utils.splitCommaSeparatedList(value);
this.plugin.settings.fileTypesToIgnore = extensions;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude tags").setDesc(
"Exclude files, which contain the given tag. Add each tag separated by comma (without `#`)"
).addTextArea(
(cb) => cb.setPlaceholder("todo,unfinished").setValue(this.plugin.settings.tagsToIgnore.join(",")).onChange((value) => {
const tags = Utils.splitCommaSeparatedList(value);
this.plugin.settings.tagsToIgnore = tags;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Filetypes to delete per command. See README.").setDesc(
"Add each filetype separated by comma. Set to `*` to delete all files."
).addTextArea(
(cb) => cb.setPlaceholder("jpg,png").setValue(this.plugin.settings.fileTypesToDelete.join(",")).onChange((value) => {
const extensions = Utils.splitCommaSeparatedList(value);
this.plugin.settings.fileTypesToDelete = extensions;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Filetypes to archive per command. See README.").setDesc(
"Add each filetype separated by comma. Set to `*` to archive all files."
).addTextArea(
(cb) => cb.setPlaceholder("jpg,png").setValue(this.plugin.settings.fileTypesToArchive.join(",")).onChange((value) => {
const extensions = Utils.splitCommaSeparatedList(value);
this.plugin.settings.fileTypesToArchive = extensions;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Archive directory").setDesc(
"Existing directory where the archive command moves orphaned files. Original paths are preserved inside this directory."
).addText(
(cb) => cb.setPlaceholder("Archive").setValue(this.plugin.settings.archiveDirectory).onChange((value) => {
this.plugin.settings.archiveDirectory = this.formatPath(
value.trim(),
false
);
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Find broken links").setHeading();
new import_obsidian4.Setting(containerEl).setName("Output file name").setDesc(
"Set name of output file (without file extension). Make sure no file exists with this name because it will be overwritten! If the name is empty, the default name is set."
).addText(
(cb) => cb.onChange((value) => {
if (value.length == 0) {
this.plugin.settings.unresolvedLinksOutputFileName = this.defaultSettings.unresolvedLinksOutputFileName;
} else {
this.plugin.settings.unresolvedLinksOutputFileName = value;
}
void this.plugin.saveSettings();
}).setValue(
this.plugin.settings.unresolvedLinksOutputFileName
)
);
new import_obsidian4.Setting(containerEl).setName("Exclude files in the given directories").setDesc(
"Enable to exclude files in the given directories. Disable to only include files in the given directories"
).addToggle(
(cb) => cb.setValue(
this.plugin.settings.unresolvedLinksIgnoreDirectories
).onChange((value) => {
this.plugin.settings.unresolvedLinksIgnoreDirectories = value;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Directories").setDesc("Add each directory path in a new line").addTextArea(
(cb) => cb.setPlaceholder("Directory/Subdirectory").setValue(
this.plugin.settings.unresolvedLinksDirectoriesToIgnore.join(
"\n"
)
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, true));
this.plugin.settings.unresolvedLinksDirectoriesToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude files").setDesc(
"Exclude links in the specified file. Add each file path in a new line (with file extension!)"
).addTextArea(
(cb) => cb.setPlaceholder("Directory/file.md").setValue(
this.plugin.settings.unresolvedLinksFilesToIgnore.join(
"\n"
)
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, false));
this.plugin.settings.unresolvedLinksFilesToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude links").setDesc(
"Exclude files, which contain the given file as link. Add each file path in a new line (with file extension!). Set it to `*` to exclude files with links."
).addTextArea(
(cb) => cb.setPlaceholder("Directory/file.md").setValue(
this.plugin.settings.unresolvedLinksLinksToIgnore.join(
"\n"
)
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, false));
this.plugin.settings.unresolvedLinksLinksToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude filetypes").setDesc(
"Exclude links with the specified filetype. Add each filetype separated by comma"
).addTextArea(
(cb) => cb.setPlaceholder("docx,txt").setValue(
this.plugin.settings.unresolvedLinksFileTypesToIgnore.join(
","
)
).onChange((value) => {
const extensions = Utils.splitCommaSeparatedList(value);
this.plugin.settings.unresolvedLinksFileTypesToIgnore = extensions;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude tags").setDesc(
"Exclude links in files, which contain the given tag. Add each tag separated by comma (without `#`)"
).addTextArea(
(cb) => cb.setPlaceholder("todo,unfinished").setValue(
this.plugin.settings.unresolvedLinksTagsToIgnore.join(
","
)
).onChange((value) => {
const tags = Utils.splitCommaSeparatedList(value);
this.plugin.settings.unresolvedLinksTagsToIgnore = tags;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Find files without tags").setHeading();
new import_obsidian4.Setting(containerEl).setName("Output file name").setDesc(
"Set name of output file (without file extension). Make sure no file exists with this name because it will be overwritten! If the name is empty, the default name is set."
).addText(
(cb) => cb.onChange((value) => {
if (value.length == 0) {
this.plugin.settings.withoutTagsOutputFileName = this.defaultSettings.withoutTagsOutputFileName;
} else {
this.plugin.settings.withoutTagsOutputFileName = value;
}
void this.plugin.saveSettings();
}).setValue(this.plugin.settings.withoutTagsOutputFileName)
);
new import_obsidian4.Setting(containerEl).setName("Exclude files").setDesc(
"Exclude the specific files. Add each file path in a new line (with file extension!)"
).addTextArea(
(cb) => cb.setPlaceholder("Directory/file.md").setValue(
this.plugin.settings.withoutTagsFilesToIgnore.join("\n")
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, false));
this.plugin.settings.withoutTagsFilesToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude directories").setDesc(
"Exclude files in the specified directories. Add each directory path in a new line"
).addTextArea(
(cb) => cb.setPlaceholder("Directory/Subdirectory").setValue(
this.plugin.settings.withoutTagsDirectoriesToIgnore.join(
"\n"
)
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, true));
this.plugin.settings.withoutTagsDirectoriesToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Find empty files").setHeading();
new import_obsidian4.Setting(containerEl).setName("Output file name").setDesc(
"Set name of output file (without file extension). Make sure no file exists with this name because it will be overwritten! If the name is empty, the default name is set."
).addText(
(cb) => cb.onChange((value) => {
if (value.length == 0) {
this.plugin.settings.emptyFilesOutputFileName = this.defaultSettings.emptyFilesOutputFileName;
} else {
this.plugin.settings.emptyFilesOutputFileName = value;
}
void this.plugin.saveSettings();
}).setValue(this.plugin.settings.emptyFilesOutputFileName)
);
new import_obsidian4.Setting(containerEl).setName("Exclude files in the given directories").setDesc(
"Enable to exclude files in the given directories. Disable to only include files in the given directories"
).addToggle(
(cb) => cb.setValue(this.plugin.settings.emptyFilesIgnoreDirectories).onChange((value) => {
this.plugin.settings.emptyFilesIgnoreDirectories = value;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Directories").setDesc("Add each directory path in a new line").addTextArea(
(cb) => cb.setPlaceholder("Directory/Subdirectory").setValue(
this.plugin.settings.emptyFilesDirectories.join("\n")
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, true));
this.plugin.settings.emptyFilesDirectories = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Exclude files").setDesc("Add each file path in a new line (with file extension!)").addTextArea(
(cb) => cb.setPlaceholder("Directory/file.md").setValue(
this.plugin.settings.emptyFilesFilesToIgnore.join("\n")
).onChange((value) => {
let paths = value.trim().split("\n").map((value2) => this.formatPath(value2, false));
this.plugin.settings.emptyFilesFilesToIgnore = paths;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Find empty folders").setHeading();
new import_obsidian4.Setting(containerEl).setName("Output file name").setDesc(
"Set name of output file (without file extension). Make sure no file exists with this name because it will be overwritten! If the name is empty, the default name is set."
).addText(
(cb) => cb.onChange((value) => {
this.plugin.settings.emptyFoldersOutputFileName = value.length === 0 ? this.defaultSettings.emptyFoldersOutputFileName : value;
void this.plugin.saveSettings();
}).setValue(this.plugin.settings.emptyFoldersOutputFileName)
);
new import_obsidian4.Setting(containerEl).setName("Exclude folders in the given directories").setDesc(
"Enable to exclude folders in the given directories. Disable to only include folders in the given directories"
).addToggle(
(cb) => cb.setValue(
this.plugin.settings.emptyFoldersIgnoreDirectories
).onChange((value) => {
this.plugin.settings.emptyFoldersIgnoreDirectories = value;
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Directories").setDesc("Add each directory path in a new line").addTextArea(
(cb) => cb.setPlaceholder("Directory/Subdirectory").setValue(
this.plugin.settings.emptyFoldersDirectories.join("\n")
).onChange((value) => {
this.plugin.settings.emptyFoldersDirectories = value.trim().split("\n").map((path) => this.formatPath(path, true));
void this.plugin.saveSettings();
})
);
new import_obsidian4.Setting(containerEl).setName("Donate").setDesc(
"If you like this Plugin, consider donating to support continued development."
).addButton((bt) => {
var _a;
const link = (_a = bt.buttonEl.parentElement) == null ? void 0 : _a.createEl("a", {
href: "https://ko-fi.com/F1F195IQ5",
attr: {
target: "_blank"
}
});
if (link) {
link.createEl("img", {
attr: {
height: "36",
style: "border:0px;height:36px;",
src: "https://cdn.ko-fi.com/cdn/kofi3.png?v=3",
border: "0",
alt: "Buy Me a Coffee at ko-fi.com"
}
});
bt.buttonEl.remove();
}
});
}
};
// src/main.ts
var DEFAULT_SETTINGS = {
outputFileName: "orphaned files output",
orphanedFilesSortOrder: "size",
disableWorkingLinks: false,
directoriesToIgnore: [],
filesToIgnore: [],
fileTypesToIgnore: [],
linksToIgnore: [],
tagsToIgnore: [],
fileTypesToDelete: [],
fileTypesToArchive: [],
archiveDirectory: "",
ignoreFileTypes: true,
ignoreDirectories: true,
unresolvedLinksIgnoreDirectories: true,
unresolvedLinksOutputFileName: "broken links output",
unresolvedLinksDirectoriesToIgnore: [],
unresolvedLinksFilesToIgnore: [],
unresolvedLinksFileTypesToIgnore: [],
unresolvedLinksLinksToIgnore: [],
unresolvedLinksTagsToIgnore: [],
withoutTagsDirectoriesToIgnore: [],
withoutTagsFilesToIgnore: [],
withoutTagsOutputFileName: "files without tags",
emptyFilesOutputFileName: "empty files",
emptyFilesDirectories: [],
emptyFilesFilesToIgnore: [],
emptyFilesIgnoreDirectories: true,
emptyFoldersOutputFileName: "empty folders",
emptyFoldersDirectories: [],
emptyFoldersIgnoreDirectories: true,
openOutputFile: true
};
var FindOrphanedFilesPlugin = class extends import_obsidian5.Plugin {
constructor() {
super(...arguments);
this.findExtensionRegex = /(\.[^.]+)$/;
}
async onload() {
await this.loadSettings();
this.addCommand({
id: "find-unlinked-files",
name: "Find orphaned files",
callback: () => this.findOrphanedFiles()
});
this.addCommand({
id: "find-unresolved-link",
name: "Find broken links",
callback: () => this.findBrokenLinks()
});
this.addCommand({
id: "delete-unlinked-files",
name: "Delete orphaned files with certain extension. See README",
callback: () => this.deleteOrphanedFiles()
});
this.addCommand({
id: "archive-unlinked-files",
name: "Move orphaned files with certain extension to archive directory. See readme",
callback: () => this.archiveOrphanedFiles()
});
this.addCommand({
id: "create-files-of-broken-links",
name: "Create files of broken links",
callback: () => this.createFilesOfBrokenLinks()
});
this.addCommand({
id: "find-files-without-tags",
name: "Find files without tags",
callback: () => this.findFilesWithoutTags()
});
this.addCommand({
id: "find-empty-files",
name: "Find empty files",
callback: () => this.findEmptyFiles()
});
this.addCommand({
id: "delete-empty-files",
name: "Delete empty files",
callback: () => this.deleteEmptyFiles()
});
this.addCommand({
id: "find-empty-folders",
name: "Find empty folders",
callback: () => this.findEmptyFolders()
});
this.addCommand({
id: "delete-empty-folders",
name: "Delete empty folders",
callback: () => this.deleteEmptyFolders()
});
this.addSettingTab(new SettingsTab(this.app, this, DEFAULT_SETTINGS));
this.app.workspace.on("file-menu", (menu, file, _, __) => {
if (file instanceof import_obsidian5.TFolder) {
menu.addItem((cb) => {
cb.setIcon("search");
cb.setTitle("Find orphaned files");
cb.onClick(() => {
void this.findOrphanedFiles(file.path + "/");
});
});
}
});
}
async createFilesOfBrokenLinks() {
var _a, _b;
if (!await this.app.vault.adapter.exists(
this.settings.unresolvedLinksOutputFileName + ".md"
)) {
new import_obsidian5.Notice(
"Can't find file - Please run the `Find broken files' command before"
);
return;
}
const links = (_a = this.app.metadataCache.getCache(
this.settings.unresolvedLinksOutputFileName + ".md"
)) == null ? void 0 : _a.links;
if (!links) {
new import_obsidian5.Notice("No broken links found");
return;
}
const filesToCreate = [];
for (const link of links) {
const file = this.app.metadataCache.getFirstLinkpathDest(
link.link,
"/"
);
if (file) continue;
const foundType = (_b = this.findExtensionRegex.exec(link.link)) == null ? void 0 : _b[0];
if ((foundType != null ? foundType : ".md") == ".md") {
if (foundType) {
filesToCreate.push(link.link);
} else {
filesToCreate.push(link.link + ".md");
}
}
}
if (filesToCreate) {
for (const file of filesToCreate) {
await this.app.vault.create(file, "");
}
}
}
async findEmptyFiles() {
const files = this.app.vault.getFiles();
const emptyFiles = [];
for (const file of files) {
if (new Utils(
this.app,
file.path,
[],
[],
this.settings.emptyFilesDirectories,
this.settings.emptyFilesFilesToIgnore,
this.settings.emptyFilesIgnoreDirectories
).shouldIgnoreFile()) {
continue;
}
const content = await this.app.vault.read(file);
const trimmedContent = content.trim();
if (!trimmedContent) {
emptyFiles.push(file);
}
const cache = this.app.metadataCache.getFileCache(file);
const position = cache == null ? void 0 : cache.frontmatterPosition;
if (position) {
const lines = content.trimEnd().split("\n").length;
if (position.end.line == lines - 1) {
emptyFiles.push(file);
}
}
}
let prefix;
if (this.settings.disableWorkingLinks) prefix = " ";
else prefix = "";
const text = emptyFiles.map((file) => `${prefix}- [[${file.path}]]`).join("\n");
await Utils.writeAndOpenFile(
this.app,
this.settings.emptyFilesOutputFileName + ".md",
text,
this.settings.openOutputFile
);
}
async findEmptyFolders() {
const emptyFolders = this.app.vault.getAllLoadedFiles().filter(
(file) => file instanceof import_obsidian5.TFolder && file.children.length === 0 && !new Utils(
this.app,
// Directory settings include a trailing slash. Add one
// here so the folder itself is matched as well.
file.path + "/",
[],
[],
this.settings.emptyFoldersDirectories,
[],
this.settings.emptyFoldersIgnoreDirectories
).shouldIgnoreFile()
);
const prefix = this.settings.disableWorkingLinks ? " " : "";
const text = emptyFolders.map((folder) => `${prefix}- [[${folder.path}]]`).join("\n");
await Utils.writeAndOpenFile(
this.app,
this.settings.emptyFoldersOutputFileName + ".md",
text,
this.settings.openOutputFile
);
}
async findOrphanedFiles(dir) {
const startTime = Date.now();
const outFileName = this.settings.outputFileName + ".md";
let outFile = null;
const allFiles = this.app.vault.getFiles();
const markdownFiles = this.app.vault.getMarkdownFiles();
const canvasFiles = allFiles.filter(
(file) => file.extension === "canvas"
);
const links = /* @__PURE__ */ new Set();
const findLinkInTextRegex = /\[\[(.*?)\]\]|\[.*?\]\((.*?)\)/g;
const canvasParsingPromises = canvasFiles.map(
async (canvasFile) => {
var _a;
const canvasFileContent = JSON.parse(
await this.app.vault.cachedRead(canvasFile) || "{}"
);
(_a = canvasFileContent.nodes) == null ? void 0 : _a.forEach((node) => {
var _a2;
let linkTexts = [];
if (node.type === "file") {
linkTexts.push(node.file);
} else if (node.type === "text") {
let match;
while ((match = findLinkInTextRegex.exec(node.text)) !== null) {
const linkText = (_a2 = match[1]) != null ? _a2 : match[2];
if (linkText) {
linkTexts.push(linkText);
}
}
} else {
return;
}
linkTexts.forEach((linkText) => {
var _a3, _b;
const targetFile = this.app.metadataCache.getFirstLinkpathDest(
(_b = (_a3 = linkText.split("|")[0]) == null ? void 0 : _a3.split("#")[0]) != null ? _b : "",
canvasFile.path
);
if (targetFile != null) links.add(targetFile.path);
});
});
}
);
markdownFiles.forEach((mdFile) => {
var _a, _b, _c;
if (outFile === null && mdFile.path == outFileName) {
outFile = mdFile;
return;
}
const cache = this.app.metadataCache.getFileCache(mdFile);
if (!cache) {
return;
}
for (const ref of [
...(_a = cache.embeds) != null ? _a : [],
...(_b = cache.links) != null ? _b : [],
...(_c = cache.frontmatterLinks) != null ? _c : []
]) {
const txt = this.app.metadataCache.getFirstLinkpathDest(
(0, import_obsidian5.getLinkpath)(ref.link),
mdFile.path
);
if (txt != null) links.add(txt.path);
}
});
await Promise.all(canvasParsingPromises);
const notLinkedFiles = allFiles.filter(
(file) => this.isFileAnOrphan(file, links, dir)
);
if (outFile) {
notLinkedFiles.remove(outFile);
}
let text = "";
let prefix;
if (this.settings.disableWorkingLinks) prefix = " ";
else prefix = "";
this.sortOrphanedFiles(notLinkedFiles);
notLinkedFiles.forEach((file) => {
text += prefix + "- [[" + this.app.metadataCache.fileToLinktext(file, "/", false) + "]]\n";
});
await Utils.writeAndOpenFile(
this.app,
outFileName,
text,
this.settings.openOutputFile
);
const endTime = Date.now();
const diff = endTime - startTime;
if (diff > 1e3) {
new import_obsidian5.Notice(
`Found ${notLinkedFiles.length} orphaned files in ${diff}ms`
);
}
}
sortOrphanedFiles(files) {
if (this.settings.orphanedFilesSortOrder == "alphabetical") {
files.sort((a, b) => {
const aLinkText = this.app.metadataCache.fileToLinktext(
a,
"/",
false
);
const bLinkText = this.app.metadataCache.fileToLinktext(
b,
"/",
false
);
return aLinkText.localeCompare(bLinkText, void 0, {
sensitivity: "base",
numeric: true
}) || a.path.localeCompare(b.path);
});
return;
}
files.sort((a, b) => b.stat.size - a.stat.size);
}
async deleteOrphanedFiles() {
const filesToDelete = await this.getOrphanedFilesFromOutput(
this.settings.fileTypesToDelete
);
if (filesToDelete.length > 0)
new DeleteFilesModal(this.app, filesToDelete).open();
}
async archiveOrphanedFiles() {
const archiveDirectory = (0, import_obsidian5.normalizePath)(this.settings.archiveDirectory);
if (!archiveDirectory) {
new import_obsidian5.Notice(
"Please set an archive directory in the plugin settings"
);
return;
}
const existingFile = this.app.vault.getAbstractFileByPath(archiveDirectory);
if (existingFile instanceof import_obsidian5.TFile) {
new import_obsidian5.Notice("Archive directory points to a file");
return;
}
if (!(existingFile instanceof import_obsidian5.TFolder)) {
new import_obsidian5.Notice("Archive directory does not exist");
return;
}
const filesToArchive = await this.getOrphanedFilesFromOutput(
this.settings.fileTypesToArchive
);
if (filesToArchive.length > 0)
new ArchiveFilesModal(
this.app,
filesToArchive,
archiveDirectory
).open();
}
async getOrphanedFilesFromOutput(fileTypes) {
var _a, _b;
if (!await this.app.vault.adapter.exists(
this.settings.outputFileName + ".md"
)) {
new import_obsidian5.Notice(
"Can't find file - Please run the `Find orphaned files' command before"
);
return [];
}
const links = (_b = (_a = this.app.metadataCache.getCache(
this.settings.outputFileName + ".md"
)) == null ? void 0 : _a.links) != null ? _b : [];
const files = [];
links.forEach((link) => {
const file = this.app.metadataCache.getFirstLinkpathDest(
link.link,
"/"
);
if (!file) return;
if (fileTypes.contains("*") || fileTypes.contains(file.extension)) {
files.push(file);
}
});
return files;
}
async deleteEmptyFiles() {
var _a, _b;
if (!await this.app.vault.adapter.exists(
this.settings.emptyFilesOutputFileName + ".md"
)) {
new import_obsidian5.Notice(
"Can't find file - Please run the `Find orphaned files' command before"
);
return;
}
const links = (_b = (_a = this.app.metadataCache.getCache(
this.settings.emptyFilesOutputFileName + ".md"
)) == null ? void 0 : _a.links) != null ? _b : [];
const filesToDelete = [];
for (const link of links) {
const file = this.app.metadataCache.getFirstLinkpathDest(
link.link,
"/"
);
if (!file) return;
filesToDelete.push(file);
}
if (filesToDelete.length > 0)
new DeleteFilesModal(this.app, filesToDelete).open();
}
async deleteEmptyFolders() {
var _a, _b;
const outputFileName = this.settings.emptyFoldersOutputFileName + ".md";
if (!await this.app.vault.adapter.exists(outputFileName)) {
new import_obsidian5.Notice(
"Can't find file - Please run the `Find empty folders' command before"
);
return;
}
const links = (_b = (_a = this.app.metadataCache.getCache(outputFileName)) == null ? void 0 : _a.links) != null ? _b : [];
const foldersToDelete = [...new Set(links.map((link) => link.link))].map((path) => this.app.vault.getAbstractFileByPath(path)).filter(
(file) => file instanceof import_obsidian5.TFolder && file.children.length === 0
);
if (foldersToDelete.length > 0) {
new DeleteFilesModal(this.app, foldersToDelete, "folders").open();
}
}
async findBrokenLinks() {
const outFileName = this.settings.unresolvedLinksOutputFileName + ".md";
const links = [];
const brokenLinks = this.app.metadataCache.unresolvedLinks;
for (const sourceFilepath in brokenLinks) {
if (sourceFilepath == this.settings.unresolvedLinksOutputFileName + ".md")
continue;
const fileType = sourceFilepath.substring(
sourceFilepath.lastIndexOf(".") + 1
);
const utils = new Utils(
this.app,
sourceFilepath,
this.settings.unresolvedLinksTagsToIgnore,
this.settings.unresolvedLinksLinksToIgnore,
this.settings.unresolvedLinksDirectoriesToIgnore,
this.settings.unresolvedLinksFilesToIgnore,
this.settings.unresolvedLinksIgnoreDirectories
);
if (utils.shouldIgnoreFile()) continue;
for (const link in brokenLinks[sourceFilepath]) {
const linkFileType = link.substring(link.lastIndexOf(".") + 1);
if (this.settings.unresolvedLinksFileTypesToIgnore.contains(
linkFileType
))
continue;
let formattedFilePath = sourceFilepath;
if (fileType == "md") {
formattedFilePath = sourceFilepath.substring(
0,
sourceFilepath.lastIndexOf(".md")
);
}
const brokenLink = {
files: [formattedFilePath],
link
};
if (links.contains(brokenLink)) continue;
const duplication = links.find((e) => e.link == link);
if (duplication) {
duplication.files.push(formattedFilePath);
} else {
links.push(brokenLink);
}
}
}
await Utils.writeAndOpenFile(
this.app,
outFileName,
[
"Don't forget that creating the file from here may create the file in the wrong directory!",
...links.map(
(e) => `- [[${e.link}]] in [[${e.files.join("]], [[")}]]`
)
].join("\n"),
this.settings.openOutputFile
);
}
async findFilesWithoutTags() {
const outFileName = this.settings.withoutTagsOutputFileName + ".md";
let outFile = null;
const files = this.app.vault.getMarkdownFiles();
let withoutFiles = files.filter((file) => {
var _a, _b;
if (outFile === null && file.path == outFileName) {
outFile = file;
return false;
}
const utils = new Utils(
this.app,
file.path,
[],
[],
this.settings.withoutTagsDirectoriesToIgnore,
this.settings.withoutTagsFilesToIgnore,
true
);
if (utils.shouldIgnoreFile()) {
return false;
}
const cache = this.app.metadataCache.getFileCache(file);
return (cache ? (_b = (_a = (0, import_obsidian5.getAllTags)(cache)) == null ? void 0 : _a.length) != null ? _b : 0 : 0) <= 0;
});
if (outFile) {
withoutFiles.remove(outFile);
}
let prefix;
if (this.settings.disableWorkingLinks) prefix = " ";
else prefix = "";
const text = withoutFiles.map((file) => `${prefix}- [[${file.path}]]`).join("\n");
await Utils.writeAndOpenFile(
this.app,
outFileName,
text,
this.settings.openOutputFile
);
}
/**
* Checks if the given file in an orphaned file
*
* @param file file to check
* @param links all links in the vault
* @param dir optional directory to check if the file is in
*/
isFileAnOrphan(file, links, dir) {
if (links.has(file.path)) return false;
if (file.extension == "css") return false;
if (this.settings.fileTypesToIgnore.length > 0) {
const containsFileType = this.settings.fileTypesToIgnore.contains(
file.extension
);
if (this.settings.ignoreFileTypes) {
if (containsFileType) return false;
} else {
if (!containsFileType) return false;
}
}
const utils = new Utils(
this.app,
file.path,
this.settings.tagsToIgnore,
this.settings.linksToIgnore,
this.settings.directoriesToIgnore,
this.settings.filesToIgnore,
this.settings.ignoreDirectories,
dir
);
if (utils.shouldIgnoreFile()) return false;
return true;
}
async loadSettings() {
const loadedData = await this.loadData();
this.settings = {
...DEFAULT_SETTINGS,
...loadedData
};
this.settings.fileTypesToIgnore = Utils.normalizeStringList(
this.settings.fileTypesToIgnore
);
this.settings.fileTypesToDelete = Utils.normalizeStringList(
this.settings.fileTypesToDelete
);
this.settings.fileTypesToArchive = Utils.normalizeStringList(
this.settings.fileTypesToArchive
);
this.settings.unresolvedLinksFileTypesToIgnore = Utils.normalizeStringList(
this.settings.unresolvedLinksFileTypesToIgnore
);
}
async saveSettings() {
await this.saveData(this.settings);
}
};
/* nosourcemap */