Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00cc10b90a | ||
|
|
ec75fd9d72 | ||
|
|
6ff146eaaf | ||
|
|
4cf5e9da4a | ||
|
|
d546947e61 | ||
|
|
801fdec71e |
@@ -0,0 +1,42 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Verify version
|
||||
run: |
|
||||
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
||||
MANIFEST_VERSION=$(node -p "require('./manifest.json').version")
|
||||
if [ "$TAG_VERSION" != "$MANIFEST_VERSION" ]; then
|
||||
echo "❌ Tag ($TAG_VERSION) != manifest ($MANIFEST_VERSION)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: ${{ github.ref_name }}
|
||||
files: |
|
||||
main.js
|
||||
manifest.json
|
||||
styles.css
|
||||
@@ -1,5 +1,6 @@
|
||||
import esbuild from 'esbuild';
|
||||
import process from 'process';
|
||||
import fs from 'fs';
|
||||
import builtins from 'builtin-modules';
|
||||
|
||||
const banner = `/*
|
||||
@@ -9,6 +10,11 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
|
||||
const prod = process.argv[2] === 'production';
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
const manifest = JSON.parse(fs.readFileSync('./manifest.json', 'utf8'));
|
||||
manifest.version = pkg.version;
|
||||
fs.writeFileSync('./manifest.json', JSON.stringify(manifest, null, '\t') + '\n');
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
banner: { js: banner },
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
*/
|
||||
|
||||
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: () => ZeroQoLModulesPlugin
|
||||
});
|
||||
module.exports = __toCommonJS(main_exports);
|
||||
var import_obsidian4 = require("obsidian");
|
||||
|
||||
// src/modules/attachment-clean-paste/index.ts
|
||||
var import_obsidian = require("obsidian");
|
||||
|
||||
// src/modules/base-module.ts
|
||||
var BaseModule = class {
|
||||
constructor() {
|
||||
this.cleanupFns = [];
|
||||
}
|
||||
get defaultSettings() {
|
||||
return {};
|
||||
}
|
||||
onunload(_plugin) {
|
||||
for (const fn of this.cleanupFns) {
|
||||
fn();
|
||||
}
|
||||
this.cleanupFns = [];
|
||||
}
|
||||
renderInlineSettings(_containerEl, _settings, _saveSettings) {
|
||||
}
|
||||
registerCleanup(fn) {
|
||||
this.cleanupFns.push(fn);
|
||||
}
|
||||
};
|
||||
|
||||
// src/locales/en.ts
|
||||
var en = {
|
||||
settings: {
|
||||
title: "ZeroQ QoL Modules",
|
||||
description: "Enable or disable modules as you wish."
|
||||
},
|
||||
modules: {
|
||||
"attachment-clean-paste": {
|
||||
name: "Attachment Clean Paste",
|
||||
description: "Replaces ![[embed]] with [[path/file|name]] for configured file extensions on paste/drop",
|
||||
descriptionShort: "Replaces ![[embed]] with [[path/file|name]] for specified file extensions.",
|
||||
extensionsLabel: "File extensions",
|
||||
extensionsDesc: "Comma-separated extensions (e.g.: png, jpg, pdf, mp3). Files with these extensions will be inserted as [[path/file|name]] instead of ![[path/file]].",
|
||||
extensionsPlaceholder: "png, jpg, jpeg, gif, svg, webp, pdf"
|
||||
},
|
||||
"preserve-link-aliases": {
|
||||
name: "Preserve Link Aliases",
|
||||
description: "Preserves original link aliases when attachments are renamed",
|
||||
processEmbedsLabel: "Process embed links",
|
||||
processEmbedsDesc: "Also process ![[embed]] links (disabled by default)",
|
||||
processCanvasLabel: "Process Canvas files",
|
||||
processCanvasDesc: "Also process links in Canvas (.canvas) files"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/locales/ru.ts
|
||||
var ru = {
|
||||
settings: {
|
||||
title: "ZeroQ QoL Modules",
|
||||
description: "\u0412\u043A\u043B\u044E\u0447\u0430\u0439\u0442\u0435 \u0438 \u043E\u0442\u043A\u043B\u044E\u0447\u0430\u0439\u0442\u0435 \u043C\u043E\u0434\u0443\u043B\u0438 \u043F\u043E \u0441\u0432\u043E\u0435\u043C\u0443 \u0443\u0441\u043C\u043E\u0442\u0440\u0435\u043D\u0438\u044E."
|
||||
},
|
||||
modules: {
|
||||
"attachment-clean-paste": {
|
||||
name: "Attachment Clean Paste",
|
||||
description: "\u0417\u0430\u043C\u0435\u043D\u044F\u0435\u0442 ![[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435]] \u043D\u0430 [[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435|\u0438\u043C\u044F]] \u0434\u043B\u044F \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0445 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0439 \u043F\u0440\u0438 \u0432\u0441\u0442\u0430\u0432\u043A\u0435/\u043F\u0435\u0440\u0435\u0442\u0430\u0441\u043A\u0438\u0432\u0430\u043D\u0438\u0438",
|
||||
descriptionShort: "\u0417\u0430\u043C\u0435\u043D\u044F\u0435\u0442 ![[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435]] \u043D\u0430 [[\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435|\u0438\u043C\u044F]] \u0434\u043B\u044F \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0445 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0439.",
|
||||
extensionsLabel: "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0444\u0430\u0439\u043B\u043E\u0432",
|
||||
extensionsDesc: "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u044F\u0442\u0443\u044E (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: png, jpg, pdf, mp3). \u0424\u0430\u0439\u043B\u044B \u0441 \u044D\u0442\u0438\u043C\u0438 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F\u043C\u0438 \u0431\u0443\u0434\u0443\u0442 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043A\u0430\u043A [[path/file|name]] \u0432\u043C\u0435\u0441\u0442\u043E ![[path/file]].",
|
||||
extensionsPlaceholder: "png, jpg, jpeg, gif, svg, webp, pdf"
|
||||
},
|
||||
"preserve-link-aliases": {
|
||||
name: "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u0430\u043B\u0438\u0430\u0441\u043E\u0432 \u0441\u0441\u044B\u043B\u043E\u043A",
|
||||
description: "\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u0442 \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0435 \u0430\u043B\u0438\u0430\u0441\u044B \u0441\u0441\u044B\u043B\u043E\u043A \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0438 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0439",
|
||||
processEmbedsLabel: "\u041E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C embed-\u0441\u0441\u044B\u043B\u043A\u0438",
|
||||
processEmbedsDesc: "\u0422\u0430\u043A\u0436\u0435 \u043E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C ![[embed]] \u0441\u0441\u044B\u043B\u043A\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E)",
|
||||
processCanvasLabel: "\u041E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C Canvas-\u0444\u0430\u0439\u043B\u044B",
|
||||
processCanvasDesc: "\u0422\u0430\u043A\u0436\u0435 \u043E\u0431\u0440\u0430\u0431\u0430\u0442\u044B\u0432\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0438 \u0432 Canvas (.canvas) \u0444\u0430\u0439\u043B\u0430\u0445"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/locales/index.ts
|
||||
var locales = { en, ru };
|
||||
function detectLocale() {
|
||||
const lang = (document.documentElement.lang || "en").slice(0, 2);
|
||||
return locales[lang] || en;
|
||||
}
|
||||
var locale = detectLocale();
|
||||
|
||||
// src/modules/attachment-clean-paste/index.ts
|
||||
var MODULE_ID = "attachment-clean-paste";
|
||||
var AttachmentCleanPasteModule = class extends BaseModule {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.id = MODULE_ID;
|
||||
this.name = locale.modules["attachment-clean-paste"].name;
|
||||
this.description = locale.modules["attachment-clean-paste"].description;
|
||||
}
|
||||
get defaultSettings() {
|
||||
return {
|
||||
extensions: "png, jpg, jpeg, gif, svg, webp, bmp, pdf"
|
||||
};
|
||||
}
|
||||
onload(plugin, moduleSettings) {
|
||||
this.app = plugin.app;
|
||||
const rawSettings = moduleSettings;
|
||||
const pasteRef = plugin.app.workspace.on(
|
||||
"editor-paste",
|
||||
this.createPasteHandler(rawSettings)
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.workspace.offref(pasteRef));
|
||||
const dropRef = plugin.app.workspace.on(
|
||||
"editor-drop",
|
||||
this.createDropHandler(rawSettings)
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.workspace.offref(dropRef));
|
||||
}
|
||||
renderInlineSettings(containerEl, settings, saveSettings) {
|
||||
const loc = locale.modules["attachment-clean-paste"];
|
||||
const extSettings = settings;
|
||||
new import_obsidian.Setting(containerEl).setName(loc.extensionsLabel).setDesc(loc.extensionsDesc).addTextArea((textarea) => {
|
||||
var _a;
|
||||
textarea.setPlaceholder(loc.extensionsPlaceholder).setValue((_a = extSettings == null ? void 0 : extSettings.extensions) != null ? _a : "").onChange(async (value) => {
|
||||
settings.extensions = value;
|
||||
await saveSettings();
|
||||
});
|
||||
textarea.inputEl.rows = 4;
|
||||
textarea.inputEl.style.width = "100%";
|
||||
});
|
||||
}
|
||||
async processAll(files, editor, settings) {
|
||||
const matchingFiles = Array.from(files).filter(
|
||||
(f) => this.shouldProcess(f.name, settings)
|
||||
);
|
||||
if (matchingFiles.length === 0)
|
||||
return;
|
||||
const links = [];
|
||||
for (const file of matchingFiles) {
|
||||
const link = await this.processFile(file, settings);
|
||||
if (link)
|
||||
links.push(link);
|
||||
}
|
||||
if (links.length === 0)
|
||||
return;
|
||||
editor.replaceSelection(links.join("\n\n"));
|
||||
}
|
||||
createPasteHandler(settings) {
|
||||
return async (evt, editor, _info) => {
|
||||
var _a;
|
||||
const files = (_a = evt.clipboardData) == null ? void 0 : _a.files;
|
||||
if (!files || files.length === 0)
|
||||
return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
await this.processAll(files, editor, settings);
|
||||
};
|
||||
}
|
||||
createDropHandler(settings) {
|
||||
return async (evt, editor, _info) => {
|
||||
var _a;
|
||||
const files = (_a = evt.dataTransfer) == null ? void 0 : _a.files;
|
||||
if (!files || files.length === 0)
|
||||
return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
await this.processAll(files, editor, settings);
|
||||
};
|
||||
}
|
||||
shouldProcess(filename, settings) {
|
||||
const dotIndex = filename.lastIndexOf(".");
|
||||
if (dotIndex === -1)
|
||||
return false;
|
||||
const ext = filename.slice(dotIndex + 1).toLowerCase();
|
||||
const exts = settings.extensions.split(",").map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0);
|
||||
return exts.some((e) => {
|
||||
const cleanExt = e.startsWith(".") ? e.slice(1) : e;
|
||||
return cleanExt === ext;
|
||||
});
|
||||
}
|
||||
async processFile(file, _settings) {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const filename = file.name;
|
||||
const nameWithoutExt = filename.replace(/\.[^/.]+$/, "");
|
||||
let attachmentFolder = this.app.vault.getConfig(
|
||||
"attachmentFolderPath"
|
||||
);
|
||||
if (!attachmentFolder)
|
||||
attachmentFolder = "attachments";
|
||||
const folderPath = (0, import_obsidian.normalizePath)(attachmentFolder);
|
||||
if (!await this.app.vault.adapter.exists(folderPath)) {
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
const ext = filename.slice(filename.lastIndexOf(".") + 1);
|
||||
const basePath = (0, import_obsidian.normalizePath)(`${folderPath}/${nameWithoutExt}`);
|
||||
const availablePath = await this.app.vault.getAvailablePath(
|
||||
basePath,
|
||||
ext
|
||||
);
|
||||
await this.app.vault.createBinary(availablePath, arrayBuffer);
|
||||
return `[[${availablePath}|${nameWithoutExt}]]`;
|
||||
} catch (e) {
|
||||
new import_obsidian.Notice(`Clean Paste error: ${e.message}`);
|
||||
console.error("Attachment Clean Paste:", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// src/modules/preserve-link-aliases/index.ts
|
||||
var import_obsidian2 = require("obsidian");
|
||||
var MODULE_ID2 = "preserve-link-aliases";
|
||||
var PreserveLinkAliasesModule = class extends BaseModule {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.id = MODULE_ID2;
|
||||
this.name = locale.modules["preserve-link-aliases"].name;
|
||||
this.description = locale.modules["preserve-link-aliases"].description;
|
||||
}
|
||||
get defaultSettings() {
|
||||
return {
|
||||
processEmbeds: false,
|
||||
processCanvas: false
|
||||
};
|
||||
}
|
||||
onload(plugin, moduleSettings) {
|
||||
this.vault = plugin.app.vault;
|
||||
const rawSettings = moduleSettings;
|
||||
const renameRef = plugin.app.vault.on(
|
||||
"rename",
|
||||
(file, oldPath) => this.handleRename(file, oldPath, rawSettings)
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.vault.offref(renameRef));
|
||||
}
|
||||
renderInlineSettings(containerEl, settings, saveSettings) {
|
||||
const loc = locale.modules["preserve-link-aliases"];
|
||||
const s = settings;
|
||||
new import_obsidian2.Setting(containerEl).setName(loc.processEmbedsLabel).setDesc(loc.processEmbedsDesc).addToggle(
|
||||
(toggle) => toggle.setValue(s.processEmbeds).onChange(async (value) => {
|
||||
s.processEmbeds = value;
|
||||
await saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian2.Setting(containerEl).setName(loc.processCanvasLabel).setDesc(loc.processCanvasDesc).addToggle(
|
||||
(toggle) => toggle.setValue(s.processCanvas).onChange(async (value) => {
|
||||
s.processCanvas = value;
|
||||
await saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
async handleRename(file, oldPath, settings) {
|
||||
var _a;
|
||||
const oldName = (_a = oldPath.split("/").pop()) != null ? _a : "";
|
||||
const newName = file.name;
|
||||
const oldBasename = oldName.replace(/\.[^/.]+$/, "");
|
||||
const newBasename = newName.replace(/\.[^/.]+$/, "");
|
||||
if (oldBasename === newBasename)
|
||||
return;
|
||||
const files = this.getFilesToProcess(settings);
|
||||
for (const f of files) {
|
||||
const content = await this.vault.read(f);
|
||||
const updated = this.processContent(content, newName, newBasename, oldBasename, settings);
|
||||
if (updated !== content) {
|
||||
await this.vault.modify(f, updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
getFilesToProcess(settings) {
|
||||
const files = [];
|
||||
for (const file of this.vault.getMarkdownFiles()) {
|
||||
files.push(file);
|
||||
}
|
||||
if (settings.processCanvas) {
|
||||
const abstractFiles = this.vault.getFiles();
|
||||
for (const file of abstractFiles) {
|
||||
if (file.extension === "canvas") {
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
processContent(content, newFileName, newBasename, oldBasename, settings) {
|
||||
const wikiLinkRe = /(!?)\[\[([^\]]*?)]]/g;
|
||||
return content.replace(wikiLinkRe, (fullMatch, prefix, inner) => {
|
||||
if (prefix === "!" && !settings.processEmbeds) {
|
||||
return fullMatch;
|
||||
}
|
||||
const pipeIndex = inner.indexOf("|");
|
||||
if (pipeIndex === -1)
|
||||
return fullMatch;
|
||||
const linkPath = inner.slice(0, pipeIndex);
|
||||
const alias = inner.slice(pipeIndex + 1);
|
||||
if (alias !== newBasename)
|
||||
return fullMatch;
|
||||
const linkFilename = linkPath.split("/").pop();
|
||||
if (linkFilename !== newFileName)
|
||||
return fullMatch;
|
||||
return `${prefix}[[${linkPath}|${oldBasename}]]`;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/modules/index.ts
|
||||
var MODULES = [
|
||||
new AttachmentCleanPasteModule(),
|
||||
new PreserveLinkAliasesModule()
|
||||
];
|
||||
|
||||
// src/settings.ts
|
||||
var import_obsidian3 = require("obsidian");
|
||||
var MODULE_DEFAULTS = MODULES.reduce(
|
||||
(acc, mod) => {
|
||||
acc[mod.id] = {
|
||||
enabled: true,
|
||||
settings: { ...mod.defaultSettings }
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
var DEFAULT_SETTINGS = {
|
||||
modules: MODULE_DEFAULTS
|
||||
};
|
||||
function getModuleSettings(settings, moduleId) {
|
||||
var _a, _b;
|
||||
return (_b = (_a = settings.modules[moduleId]) == null ? void 0 : _a.settings) != null ? _b : {};
|
||||
}
|
||||
var ZeroQSettingTab = class extends import_obsidian3.PluginSettingTab {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h1", { text: locale.settings.title });
|
||||
containerEl.createEl("p", {
|
||||
text: locale.settings.description,
|
||||
attr: { style: "color: var(--text-muted); margin-bottom: 20px;" }
|
||||
});
|
||||
for (const module2 of MODULES) {
|
||||
const moduleCfg = this.plugin.settings.modules[module2.id];
|
||||
new import_obsidian3.Setting(containerEl).setName(module2.name).setDesc(module2.description).addToggle(
|
||||
(toggle) => {
|
||||
var _a;
|
||||
return toggle.setValue((_a = moduleCfg == null ? void 0 : moduleCfg.enabled) != null ? _a : true).onChange(async (value) => {
|
||||
if (!this.plugin.settings.modules[module2.id]) {
|
||||
this.plugin.settings.modules[module2.id] = {
|
||||
enabled: value,
|
||||
settings: { ...module2.defaultSettings }
|
||||
};
|
||||
} else {
|
||||
this.plugin.settings.modules[module2.id].enabled = value;
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
if (value) {
|
||||
this.plugin.loadModule(module2);
|
||||
} else {
|
||||
this.plugin.unloadModule(module2);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
if (moduleCfg) {
|
||||
module2.renderInlineSettings(
|
||||
containerEl,
|
||||
moduleCfg.settings,
|
||||
() => this.plugin.saveSettings()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// src/main.ts
|
||||
var ZeroQoLModulesPlugin = class extends import_obsidian4.Plugin {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadedModules = /* @__PURE__ */ new Set();
|
||||
}
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new ZeroQSettingTab(this.app, this));
|
||||
for (const module2 of MODULES) {
|
||||
const moduleCfg = this.settings.modules[module2.id];
|
||||
if (moduleCfg == null ? void 0 : moduleCfg.enabled) {
|
||||
this.loadModule(module2);
|
||||
}
|
||||
}
|
||||
}
|
||||
onunload() {
|
||||
for (const module2 of MODULES) {
|
||||
this.unloadModule(module2);
|
||||
}
|
||||
}
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData()
|
||||
);
|
||||
for (const module2 of MODULES) {
|
||||
if (!this.settings.modules[module2.id]) {
|
||||
this.settings.modules[module2.id] = {
|
||||
enabled: true,
|
||||
settings: { ...module2.defaultSettings }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
loadModule(module2) {
|
||||
if (this.loadedModules.has(module2.id))
|
||||
return;
|
||||
const moduleSettings = getModuleSettings(this.settings, module2.id);
|
||||
module2.onload(this, moduleSettings);
|
||||
this.loadedModules.add(module2.id);
|
||||
}
|
||||
unloadModule(module2) {
|
||||
if (!this.loadedModules.has(module2.id))
|
||||
return;
|
||||
module2.onunload(this);
|
||||
this.loadedModules.delete(module2.id);
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "zeroq-qol-modules",
|
||||
"name": "ZeroQ QoL Modules",
|
||||
"version": "1.0.3",
|
||||
"version": "1.1.4",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "QoL Modules",
|
||||
"author": "oqyude",
|
||||
|
||||
+7
-3
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "zeroq-qol-modules",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.4",
|
||||
"description": "Modular Obsidian plugin with toggleable QoL modules",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "node esbuild.config.mjs production"
|
||||
"build": "node esbuild.config.mjs production",
|
||||
"release": "bash scripts/release.sh"
|
||||
},
|
||||
"keywords": ["obsidian", "plugin"],
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"plugin"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# Project Review: zeroq-qol-modules
|
||||
|
||||
## Overview
|
||||
|
||||
Obsidian plugin with a **modular architecture**. Each feature is a separate module that can be toggled on/off in settings.
|
||||
|
||||
**Plugin ID:** `zeroq-qol-modules`
|
||||
**Entry point:** `src/main.ts` → compiles to `main.js` (esbuild)
|
||||
**Version source:** `package.json` → auto-synced to `manifest.json` on build
|
||||
|
||||
## Stack
|
||||
|
||||
- **Language:** TypeScript (5.x)
|
||||
- **Bundler:** esbuild (0.17.x)
|
||||
- **Obsidian API:** `obsidian` latest (type defs only, excluded from bundle)
|
||||
- **Format:** CommonJS (`format: 'cjs'`), target ES2018
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.ts # Plugin class — loads settings, bootstraps modules
|
||||
├── settings.ts # ZeroQSettingTab — single settings pane, renders all modules
|
||||
├── types.ts # QoLModule interface, ZeroQSettings, ModuleSettings
|
||||
├── locales/
|
||||
│ ├── en.ts # Locale type definition + English dictionary
|
||||
│ ├── ru.ts # Russian dictionary (same Locale shape)
|
||||
│ └── index.ts # Auto-detects language from <html lang>, exports locale
|
||||
└── modules/
|
||||
├── index.ts # Static registry: array of all module instances
|
||||
├── base-module.ts # BaseModule abstract class
|
||||
└── attachment-clean-paste/
|
||||
└── index.ts # Concrete module implementation
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Plugin Lifecycle (`main.ts`)
|
||||
|
||||
```
|
||||
onload()
|
||||
→ loadSettings() # merge DEFAULT_SETTINGS + saved data
|
||||
→ addSettingTab() # register ZeroQSettingTab
|
||||
→ for each module:
|
||||
if enabled → loadModule(module)
|
||||
→ module.onload(this, moduleSettings)
|
||||
|
||||
onunload()
|
||||
→ for each module:
|
||||
unloadModule(module)
|
||||
→ module.onunload(this)
|
||||
```
|
||||
|
||||
### Module Lifecycle
|
||||
|
||||
Each module is a **singleton** implementing `QoLModule` (or extending `BaseModule`):
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `onload(plugin, moduleSettings)` | Initialize: register events, commands, setting tab |
|
||||
| `onunload(plugin)` | Cleanup (auto-runs `registerCleanup` callbacks) |
|
||||
| `renderInlineSettings(containerEl, settings, saveSettings)` | Render module-specific settings inline (no separate tab) |
|
||||
| `defaultSettings` | Default module settings object |
|
||||
|
||||
### Cleanup pattern
|
||||
|
||||
Modules use `this.registerCleanup(fn)` to queue cleanup callbacks. They auto-run in `onunload()`. Example:
|
||||
|
||||
```ts
|
||||
const ref = plugin.app.workspace.on('editor-paste', handler);
|
||||
this.registerCleanup(() => plugin.app.workspace.offref(ref));
|
||||
```
|
||||
|
||||
### Settings architecture
|
||||
|
||||
- **ZeroQSettingTab** (`settings.ts`) — single Obsidian settings pane
|
||||
- Iterates `MODULES`, renders each with:
|
||||
1. Toggle (enable/disable) — toggling calls `loadModule` / `unloadModule` at runtime
|
||||
2. `module.renderInlineSettings()` — module-specific controls rendered directly below the toggle
|
||||
- **No separate PluginSettingTab per module** — all in one block
|
||||
|
||||
### Settings data shape
|
||||
|
||||
```ts
|
||||
interface ZeroQSettings {
|
||||
modules: Record<string, {
|
||||
enabled: boolean;
|
||||
settings: Record<string, unknown>; // module-specific, typed per module
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
### Localization (`locales/`)
|
||||
|
||||
- **Locale type** defined in `en.ts` as the interface `Locale`
|
||||
- **Language detection:** reads `<html lang>` attribute (set by Obsidian per its own language setting)
|
||||
- Fallback to `'en'` if language is not in the dictionary
|
||||
- Usage: `import { locale } from '../../locales'` → `locale.settings.title`, `locale.modules['xxx'].name`
|
||||
|
||||
**To add a language:**
|
||||
1. Create `src/locales/de.ts` with `Locale` shape
|
||||
2. Import it in `src/locales/index.ts` and add to `locales` record
|
||||
|
||||
## Adding a New Module
|
||||
|
||||
### Step 1: Create the module class
|
||||
|
||||
Extend `BaseModule` in `src/modules/my-module/index.ts`:
|
||||
|
||||
```ts
|
||||
import { Plugin } from 'obsidian';
|
||||
import { BaseModule } from '../base-module';
|
||||
import { locale } from '../../locales';
|
||||
|
||||
export class MyModule extends BaseModule {
|
||||
id = 'my-module';
|
||||
name = locale.modules['my-module'].name;
|
||||
description = locale.modules['my-module'].description;
|
||||
|
||||
get defaultSettings(): Record<string, unknown> {
|
||||
return { myOption: 'default' };
|
||||
}
|
||||
|
||||
onload(plugin: Plugin, moduleSettings: Record<string, unknown>): void {
|
||||
// register events/commands via plugin.registerEvent / plugin.addCommand
|
||||
// use this.registerCleanup(fn) for teardown
|
||||
}
|
||||
|
||||
renderInlineSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: Record<string, unknown>,
|
||||
saveSettings: () => Promise<void>,
|
||||
): void {
|
||||
// render controls into containerEl
|
||||
// mutate settings object directly, then call saveSettings()
|
||||
new Setting(containerEl)
|
||||
.setName('My option')
|
||||
.addText(text => text
|
||||
.setValue(String(settings.myOption ?? ''))
|
||||
.onChange(async v => {
|
||||
settings.myOption = v;
|
||||
await saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Add translations
|
||||
|
||||
In `src/locales/en.ts`, add to the `Locale` interface and the `en` object:
|
||||
|
||||
```ts
|
||||
'modules': {
|
||||
'my-module': {
|
||||
name: string;
|
||||
description: string;
|
||||
// any other strings this module needs
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Do the same in `src/locales/ru.ts`.
|
||||
|
||||
### Step 3: Register in the registry
|
||||
|
||||
In `src/modules/index.ts`:
|
||||
|
||||
```ts
|
||||
import { MyModule } from './my-module';
|
||||
|
||||
export const MODULES: QoLModule[] = [
|
||||
new AttachmentCleanPasteModule(),
|
||||
new MyModule(),
|
||||
];
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build # production build (minified, no sourcemaps)
|
||||
npm run dev # dev build (inline sourcemaps)
|
||||
```
|
||||
|
||||
`esbuild.config.mjs` also syncs `version` from `package.json` to `manifest.json` before building.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **No comments** in source code
|
||||
- **No emojis** in code or UI
|
||||
- **Indentation:** tabs (Obsidian convention)
|
||||
- **Imports:** `import type` for type-only imports to avoid circular dependencies
|
||||
- **File naming:** `kebab-case` for files, `PascalCase` for classes
|
||||
- **Russian labels** for Russian locale, **English labels** for all other locales
|
||||
- Module `id` is `kebab-case` and matches the key in the locales `modules` object
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
BUMP="${1:-patch}"
|
||||
|
||||
CURRENT_VERSION=$(node -p "require('./manifest.json').version")
|
||||
|
||||
if [[ "$BUMP" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
NEW_VERSION="$BUMP"
|
||||
elif [[ "$BUMP" == "patch" ]]; then
|
||||
NEW_VERSION=$(node -p "
|
||||
const [a,b,c] = '$CURRENT_VERSION'.split('.').map(Number);
|
||||
\`\${a}.\${b}.\${c+1}\`
|
||||
")
|
||||
elif [[ "$BUMP" == "minor" ]]; then
|
||||
NEW_VERSION=$(node -p "
|
||||
const [a,b] = '$CURRENT_VERSION'.split('.').map(Number);
|
||||
\`\${a}.\${b+1}.0\`
|
||||
")
|
||||
elif [[ "$BUMP" == "major" ]]; then
|
||||
NEW_VERSION=$(node -p "
|
||||
const [a] = '$CURRENT_VERSION'.split('.').map(Number);
|
||||
\`\${a+1}.0.0\`
|
||||
")
|
||||
else
|
||||
echo "Usage: $0 [patch|minor|major|<semver>]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Current: $CURRENT_VERSION"
|
||||
echo "New: $NEW_VERSION"
|
||||
read -p "Proceed? [y/N] " CONFIRM
|
||||
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
|
||||
echo "Aborted."; exit 1
|
||||
fi
|
||||
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const m = require('./manifest.json'); m.version = '$NEW_VERSION';
|
||||
fs.writeFileSync('./manifest.json', JSON.stringify(m, null, '\t') + '\n');
|
||||
const p = require('./package.json'); p.version = '$NEW_VERSION';
|
||||
fs.writeFileSync('./package.json', JSON.stringify(p, null, '\t') + '\n');
|
||||
"
|
||||
|
||||
npm run build
|
||||
|
||||
git add -f manifest.json package.json main.js styles.css
|
||||
git commit -m "v$NEW_VERSION"
|
||||
git tag "v$NEW_VERSION"
|
||||
|
||||
echo ""
|
||||
echo "✔ Release v$NEW_VERSION ready."
|
||||
echo "Run: git push && git push --tags"
|
||||
@@ -12,6 +12,14 @@ export interface Locale {
|
||||
extensionsDesc: string;
|
||||
extensionsPlaceholder: string;
|
||||
};
|
||||
'preserve-link-aliases': {
|
||||
name: string;
|
||||
description: string;
|
||||
processEmbedsLabel: string;
|
||||
processEmbedsDesc: string;
|
||||
processCanvasLabel: string;
|
||||
processCanvasDesc: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,5 +40,14 @@ export const en: Locale = {
|
||||
'Comma-separated extensions (e.g.: png, jpg, pdf, mp3). Files with these extensions will be inserted as [[path/file|name]] instead of ![[path/file]].',
|
||||
extensionsPlaceholder: 'png, jpg, jpeg, gif, svg, webp, pdf',
|
||||
},
|
||||
'preserve-link-aliases': {
|
||||
name: 'Preserve Link Aliases',
|
||||
description:
|
||||
'Preserves original link aliases when attachments are renamed',
|
||||
processEmbedsLabel: 'Process embed links',
|
||||
processEmbedsDesc: 'Also process ![[embed]] links (disabled by default)',
|
||||
processCanvasLabel: 'Process Canvas files',
|
||||
processCanvasDesc: 'Also process links in Canvas (.canvas) files',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,5 +17,14 @@ export const ru: Locale = {
|
||||
'Расширения через запятую (например: png, jpg, pdf, mp3). Файлы с этими расширениями будут вставляться как [[path/file|name]] вместо ![[path/file]].',
|
||||
extensionsPlaceholder: 'png, jpg, jpeg, gif, svg, webp, pdf',
|
||||
},
|
||||
'preserve-link-aliases': {
|
||||
name: 'Сохранение алиасов ссылок',
|
||||
description:
|
||||
'Сохраняет оригинальные алиасы ссылок при переименовании вложений',
|
||||
processEmbedsLabel: 'Обрабатывать embed-ссылки',
|
||||
processEmbedsDesc: 'Также обрабатывать ![[embed]] ссылки (по умолчанию выключено)',
|
||||
processCanvasLabel: 'Обрабатывать Canvas-файлы',
|
||||
processCanvasDesc: 'Также обрабатывать ссылки в Canvas (.canvas) файлах',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
MarkdownFileInfo,
|
||||
MarkdownView,
|
||||
Notice,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
normalizePath,
|
||||
} from 'obsidian';
|
||||
@@ -33,25 +32,65 @@ export class AttachmentCleanPasteModule extends BaseModule {
|
||||
|
||||
onload(plugin: ZeroQoLModulesPlugin, moduleSettings: Record<string, unknown>): void {
|
||||
this.app = plugin.app;
|
||||
const settings = moduleSettings as unknown as AttachmentCleanPasteSettings;
|
||||
|
||||
plugin.addSettingTab(
|
||||
new AttachmentCleanPasteSettingTab(plugin.app, plugin, MODULE_ID),
|
||||
);
|
||||
const rawSettings = moduleSettings as AttachmentCleanPasteSettings;
|
||||
|
||||
const pasteRef = plugin.app.workspace.on(
|
||||
'editor-paste',
|
||||
this.createPasteHandler(settings),
|
||||
this.createPasteHandler(rawSettings),
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.workspace.offref(pasteRef));
|
||||
|
||||
const dropRef = plugin.app.workspace.on(
|
||||
'editor-drop',
|
||||
this.createDropHandler(settings),
|
||||
this.createDropHandler(rawSettings),
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.workspace.offref(dropRef));
|
||||
}
|
||||
|
||||
renderInlineSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: Record<string, unknown>,
|
||||
saveSettings: () => Promise<void>,
|
||||
): void {
|
||||
const loc = locale.modules['attachment-clean-paste'];
|
||||
const extSettings = settings as unknown as AttachmentCleanPasteSettings;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(loc.extensionsLabel)
|
||||
.setDesc(loc.extensionsDesc)
|
||||
.addTextArea((textarea) => {
|
||||
textarea
|
||||
.setPlaceholder(loc.extensionsPlaceholder)
|
||||
.setValue(extSettings?.extensions ?? '')
|
||||
.onChange(async (value) => {
|
||||
settings.extensions = value;
|
||||
await saveSettings();
|
||||
});
|
||||
textarea.inputEl.rows = 4;
|
||||
textarea.inputEl.style.width = '100%';
|
||||
});
|
||||
}
|
||||
|
||||
private async processAll(
|
||||
files: FileList,
|
||||
editor: Editor,
|
||||
settings: AttachmentCleanPasteSettings,
|
||||
): Promise<void> {
|
||||
const matchingFiles = Array.from(files).filter((f) =>
|
||||
this.shouldProcess(f.name, settings),
|
||||
);
|
||||
if (matchingFiles.length === 0) return;
|
||||
|
||||
const links: string[] = [];
|
||||
for (const file of matchingFiles) {
|
||||
const link = await this.processFile(file, settings);
|
||||
if (link) links.push(link);
|
||||
}
|
||||
if (links.length === 0) return;
|
||||
|
||||
editor.replaceSelection(links.join('\n\n'));
|
||||
}
|
||||
|
||||
private createPasteHandler(settings: AttachmentCleanPasteSettings) {
|
||||
return async (
|
||||
evt: ClipboardEvent,
|
||||
@@ -61,17 +100,10 @@ export class AttachmentCleanPasteModule extends BaseModule {
|
||||
const files = evt.clipboardData?.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const matchingFiles = Array.from(files).filter((f) =>
|
||||
this.shouldProcess(f.name, settings),
|
||||
);
|
||||
if (matchingFiles.length === 0) return;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
for (const file of matchingFiles) {
|
||||
await this.processFile(file, editor, settings);
|
||||
}
|
||||
await this.processAll(files, editor, settings);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,17 +116,10 @@ export class AttachmentCleanPasteModule extends BaseModule {
|
||||
const files = evt.dataTransfer?.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const matchingFiles = Array.from(files).filter((f) =>
|
||||
this.shouldProcess(f.name, settings),
|
||||
);
|
||||
if (matchingFiles.length === 0) return;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
for (const file of matchingFiles) {
|
||||
await this.processFile(file, editor, settings);
|
||||
}
|
||||
await this.processAll(files, editor, settings);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,9 +144,8 @@ export class AttachmentCleanPasteModule extends BaseModule {
|
||||
|
||||
private async processFile(
|
||||
file: File,
|
||||
editor: Editor,
|
||||
_settings: AttachmentCleanPasteSettings,
|
||||
): Promise<void> {
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const filename = file.name;
|
||||
@@ -147,58 +171,11 @@ export class AttachmentCleanPasteModule extends BaseModule {
|
||||
|
||||
await this.app.vault.createBinary(availablePath, arrayBuffer);
|
||||
|
||||
const link = `[[${availablePath}|${nameWithoutExt}]]`;
|
||||
editor.replaceSelection(link);
|
||||
return `[[${availablePath}|${nameWithoutExt}]]`;
|
||||
} catch (e) {
|
||||
new Notice(`Clean Paste error: ${e.message}`);
|
||||
console.error('Attachment Clean Paste:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentCleanPasteSettingTab extends PluginSettingTab {
|
||||
private plugin: ZeroQoLModulesPlugin;
|
||||
private moduleId: string;
|
||||
|
||||
constructor(app: App, plugin: ZeroQoLModulesPlugin, moduleId: string) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.moduleId = moduleId;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
const loc = locale.modules['attachment-clean-paste'];
|
||||
|
||||
containerEl.createEl('h2', { text: loc.name });
|
||||
containerEl.createEl('p', {
|
||||
text: loc.descriptionShort,
|
||||
attr: { style: 'color: var(--text-muted); margin-bottom: 20px;' },
|
||||
});
|
||||
|
||||
const rawSettings = this.plugin.settings.modules[this.moduleId]?.settings as
|
||||
| AttachmentCleanPasteSettings
|
||||
| undefined;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(loc.extensionsLabel)
|
||||
.setDesc(loc.extensionsDesc)
|
||||
.addTextArea((textarea) => {
|
||||
textarea
|
||||
.setPlaceholder(loc.extensionsPlaceholder)
|
||||
.setValue(rawSettings?.extensions ?? '')
|
||||
.onChange(async (value) => {
|
||||
if (this.plugin.settings.modules[this.moduleId]) {
|
||||
this.plugin.settings.modules[this.moduleId].settings = {
|
||||
extensions: value,
|
||||
};
|
||||
}
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
textarea.inputEl.rows = 4;
|
||||
textarea.inputEl.style.width = '100%';
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ export abstract class BaseModule implements QoLModule {
|
||||
this.cleanupFns = [];
|
||||
}
|
||||
|
||||
renderInlineSettings(
|
||||
_containerEl: HTMLElement,
|
||||
_settings: Record<string, unknown>,
|
||||
_saveSettings: () => Promise<void>,
|
||||
): void {}
|
||||
|
||||
protected registerCleanup(fn: () => void): void {
|
||||
this.cleanupFns.push(fn);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { AttachmentCleanPasteModule } from './attachment-clean-paste';
|
||||
import { PreserveLinkAliasesModule } from './preserve-link-aliases';
|
||||
import { QoLModule } from '../types';
|
||||
|
||||
export const MODULES: QoLModule[] = [new AttachmentCleanPasteModule()];
|
||||
export const MODULES: QoLModule[] = [
|
||||
new AttachmentCleanPasteModule(),
|
||||
new PreserveLinkAliasesModule(),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Plugin, TFile, Vault, Setting } from 'obsidian';
|
||||
import type ZeroQoLModulesPlugin from '../../main';
|
||||
import { BaseModule } from '../base-module';
|
||||
import { locale } from '../../locales';
|
||||
|
||||
const MODULE_ID = 'preserve-link-aliases';
|
||||
|
||||
interface PreserveLinkAliasesSettings {
|
||||
processEmbeds: boolean;
|
||||
processCanvas: boolean;
|
||||
}
|
||||
|
||||
export class PreserveLinkAliasesModule extends BaseModule {
|
||||
id = MODULE_ID;
|
||||
name = locale.modules['preserve-link-aliases'].name;
|
||||
description = locale.modules['preserve-link-aliases'].description;
|
||||
|
||||
private vault: Vault;
|
||||
|
||||
get defaultSettings(): Record<string, unknown> {
|
||||
return {
|
||||
processEmbeds: false,
|
||||
processCanvas: false,
|
||||
};
|
||||
}
|
||||
|
||||
onload(plugin: ZeroQoLModulesPlugin, moduleSettings: Record<string, unknown>): void {
|
||||
this.vault = plugin.app.vault;
|
||||
const rawSettings = moduleSettings as PreserveLinkAliasesSettings;
|
||||
|
||||
const renameRef = plugin.app.vault.on(
|
||||
'rename',
|
||||
(file: TFile, oldPath: string) => this.handleRename(file, oldPath, rawSettings),
|
||||
);
|
||||
this.registerCleanup(() => plugin.app.vault.offref(renameRef));
|
||||
}
|
||||
|
||||
renderInlineSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: Record<string, unknown>,
|
||||
saveSettings: () => Promise<void>,
|
||||
): void {
|
||||
const loc = locale.modules['preserve-link-aliases'];
|
||||
const s = settings as unknown as PreserveLinkAliasesSettings;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(loc.processEmbedsLabel)
|
||||
.setDesc(loc.processEmbedsDesc)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(s.processEmbeds)
|
||||
.onChange(async (value) => {
|
||||
s.processEmbeds = value;
|
||||
await saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(loc.processCanvasLabel)
|
||||
.setDesc(loc.processCanvasDesc)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(s.processCanvas)
|
||||
.onChange(async (value) => {
|
||||
s.processCanvas = value;
|
||||
await saveSettings();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async handleRename(
|
||||
file: TFile,
|
||||
oldPath: string,
|
||||
settings: PreserveLinkAliasesSettings,
|
||||
): Promise<void> {
|
||||
const oldName = oldPath.split('/').pop() ?? '';
|
||||
const newName = file.name;
|
||||
|
||||
const oldBasename = oldName.replace(/\.[^/.]+$/, '');
|
||||
const newBasename = newName.replace(/\.[^/.]+$/, '');
|
||||
|
||||
if (oldBasename === newBasename) return;
|
||||
|
||||
const files = this.getFilesToProcess(settings);
|
||||
|
||||
for (const f of files) {
|
||||
const content = await this.vault.read(f);
|
||||
const updated = this.processContent(content, newName, newBasename, oldBasename, settings);
|
||||
|
||||
if (updated !== content) {
|
||||
await this.vault.modify(f, updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getFilesToProcess(settings: PreserveLinkAliasesSettings): TFile[] {
|
||||
const files: TFile[] = [];
|
||||
|
||||
for (const file of this.vault.getMarkdownFiles()) {
|
||||
files.push(file);
|
||||
}
|
||||
|
||||
if (settings.processCanvas) {
|
||||
const abstractFiles = this.vault.getFiles();
|
||||
for (const file of abstractFiles) {
|
||||
if (file.extension === 'canvas') {
|
||||
files.push(file as TFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private processContent(
|
||||
content: string,
|
||||
newFileName: string,
|
||||
newBasename: string,
|
||||
oldBasename: string,
|
||||
settings: PreserveLinkAliasesSettings,
|
||||
): string {
|
||||
const wikiLinkRe = /(!?)\[\[([^\]]*?)]]/g;
|
||||
|
||||
return content.replace(wikiLinkRe, (fullMatch, prefix, inner) => {
|
||||
if (prefix === '!' && !settings.processEmbeds) {
|
||||
return fullMatch;
|
||||
}
|
||||
|
||||
const pipeIndex = inner.indexOf('|');
|
||||
if (pipeIndex === -1) return fullMatch;
|
||||
|
||||
const linkPath = inner.slice(0, pipeIndex);
|
||||
const alias = inner.slice(pipeIndex + 1);
|
||||
|
||||
if (alias !== newBasename) return fullMatch;
|
||||
|
||||
const linkFilename = linkPath.split('/').pop();
|
||||
if (linkFilename !== newFileName) return fullMatch;
|
||||
|
||||
return `${prefix}[[${linkPath}|${oldBasename}]]`;
|
||||
});
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -45,14 +45,14 @@ export class ZeroQSettingTab extends PluginSettingTab {
|
||||
});
|
||||
|
||||
for (const module of MODULES) {
|
||||
const moduleSettings = this.plugin.settings.modules[module.id];
|
||||
const moduleCfg = this.plugin.settings.modules[module.id];
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(module.name)
|
||||
.setDesc(module.description)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(moduleSettings?.enabled ?? true)
|
||||
.setValue(moduleCfg?.enabled ?? true)
|
||||
.onChange(async (value) => {
|
||||
if (!this.plugin.settings.modules[module.id]) {
|
||||
this.plugin.settings.modules[module.id] = {
|
||||
@@ -70,6 +70,14 @@ export class ZeroQSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (moduleCfg) {
|
||||
module.renderInlineSettings(
|
||||
containerEl,
|
||||
moduleCfg.settings,
|
||||
() => this.plugin.saveSettings(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,9 @@ export interface QoLModule {
|
||||
defaultSettings: Record<string, unknown>;
|
||||
onload(plugin: Plugin, moduleSettings: Record<string, unknown>): void;
|
||||
onunload(plugin: Plugin): void;
|
||||
renderInlineSettings(
|
||||
containerEl: HTMLElement,
|
||||
settings: Record<string, unknown>,
|
||||
saveSettings: () => Promise<void>,
|
||||
): void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user