From 148827eafd1778746397f7c5f277c375178b2bb6 Mon Sep 17 00:00:00 2001
From: Ad-closeNN <1709301095@qq.com>
Date: Mon, 20 Apr 2026 12:42:03 +0800
Subject: [PATCH] =?UTF-8?q?chore:=20=E5=85=AC=E5=BC=80=E9=BB=91=E6=9B=9C?=
=?UTF-8?q?=E7=9F=B3=20obsidian=20=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?=
=?UTF-8?q?=E5=A4=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 4 +-
.obsidian/app.json | 8 +
.obsidian/appearance.json | 1 +
.obsidian/community-plugins.json | 5 +
.obsidian/core-plugins.json | 33 +
.../obsidian-auto-link-title/data.json | 15 +
.../plugins/obsidian-auto-link-title/main.js | 771 ++++++++++++++
.../obsidian-auto-link-title/manifest.json | 10 +
.../obsidian-auto-link-title/styles.css | 1 +
.../obsidian-paste-image-rename/data.json | 11 +
.../obsidian-paste-image-rename/main.js | 965 ++++++++++++++++++
.../obsidian-paste-image-rename/manifest.json | 10 +
.../obsidian-paste-image-rename/styles.css | 79 ++
.obsidian/plugins/slash/main.js | 65 ++
.obsidian/plugins/slash/manifest.json | 10 +
.obsidian/workspace.json | 214 ++++
16 files changed, 2200 insertions(+), 2 deletions(-)
create mode 100644 .obsidian/app.json
create mode 100644 .obsidian/appearance.json
create mode 100644 .obsidian/community-plugins.json
create mode 100644 .obsidian/core-plugins.json
create mode 100644 .obsidian/plugins/obsidian-auto-link-title/data.json
create mode 100644 .obsidian/plugins/obsidian-auto-link-title/main.js
create mode 100644 .obsidian/plugins/obsidian-auto-link-title/manifest.json
create mode 100644 .obsidian/plugins/obsidian-auto-link-title/styles.css
create mode 100644 .obsidian/plugins/obsidian-paste-image-rename/data.json
create mode 100644 .obsidian/plugins/obsidian-paste-image-rename/main.js
create mode 100644 .obsidian/plugins/obsidian-paste-image-rename/manifest.json
create mode 100644 .obsidian/plugins/obsidian-paste-image-rename/styles.css
create mode 100644 .obsidian/plugins/slash/main.js
create mode 100644 .obsidian/plugins/slash/manifest.json
create mode 100644 .obsidian/workspace.json
diff --git a/.gitignore b/.gitignore
index b7a989f..4ade5e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,13 +26,13 @@ package-lock.json
bun.lockb
yarn.lock
-src/content/.obsidian
+# src/content/.obsidian
.playwright-mcp
.serena
.claude
-.obsidian
+# .obsidian
.cache
build.log
\ No newline at end of file
diff --git a/.obsidian/app.json b/.obsidian/app.json
new file mode 100644
index 0000000..6848d12
--- /dev/null
+++ b/.obsidian/app.json
@@ -0,0 +1,8 @@
+{
+ "newLinkFormat": "absolute",
+ "newFileLocation": "folder",
+ "newFileFolderPath": "src/content/posts",
+ "alwaysUpdateLinks": true,
+ "attachmentFolderPath": "/public/pic",
+ "useMarkdownLinks": true
+}
\ No newline at end of file
diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/.obsidian/appearance.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/.obsidian/community-plugins.json b/.obsidian/community-plugins.json
new file mode 100644
index 0000000..2a4769a
--- /dev/null
+++ b/.obsidian/community-plugins.json
@@ -0,0 +1,5 @@
+[
+ "fix-public-links",
+ "obsidian-auto-link-title",
+ "obsidian-paste-image-rename"
+]
\ No newline at end of file
diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json
new file mode 100644
index 0000000..639b90d
--- /dev/null
+++ b/.obsidian/core-plugins.json
@@ -0,0 +1,33 @@
+{
+ "file-explorer": true,
+ "global-search": true,
+ "switcher": true,
+ "graph": true,
+ "backlink": true,
+ "canvas": true,
+ "outgoing-link": true,
+ "tag-pane": true,
+ "footnotes": false,
+ "properties": true,
+ "page-preview": true,
+ "daily-notes": true,
+ "templates": true,
+ "note-composer": true,
+ "command-palette": true,
+ "slash-command": false,
+ "editor-status": true,
+ "bookmarks": true,
+ "markdown-importer": false,
+ "zk-prefixer": false,
+ "random-note": false,
+ "outline": true,
+ "word-count": true,
+ "slides": false,
+ "audio-recorder": false,
+ "workspaces": false,
+ "file-recovery": true,
+ "publish": false,
+ "sync": true,
+ "bases": true,
+ "webviewer": false
+}
\ No newline at end of file
diff --git a/.obsidian/plugins/obsidian-auto-link-title/data.json b/.obsidian/plugins/obsidian-auto-link-title/data.json
new file mode 100644
index 0000000..5fe250c
--- /dev/null
+++ b/.obsidian/plugins/obsidian-auto-link-title/data.json
@@ -0,0 +1,15 @@
+{
+ "regex": {},
+ "lineRegex": {},
+ "linkRegex": {},
+ "linkLineRegex": {},
+ "imageRegex": {},
+ "enhanceDefaultPaste": true,
+ "shouldPreserveSelectionAsTitle": false,
+ "enhanceDropEvents": true,
+ "websiteBlacklist": "",
+ "maximumTitleLength": 0,
+ "useNewScraper": false,
+ "linkPreviewApiKey": "",
+ "useBetterPasteId": true
+}
\ No newline at end of file
diff --git a/.obsidian/plugins/obsidian-auto-link-title/main.js b/.obsidian/plugins/obsidian-auto-link-title/main.js
new file mode 100644
index 0000000..72ffd51
--- /dev/null
+++ b/.obsidian/plugins/obsidian-auto-link-title/main.js
@@ -0,0 +1,771 @@
+/*
+THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
+if you want to view the source visit the plugins github repository
+*/
+
+'use strict';
+
+var obsidian = require('obsidian');
+
+/******************************************************************************
+Copyright (c) Microsoft Corporation.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+***************************************************************************** */
+
+function __awaiter(thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+}
+
+typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
+ var e = new Error(message);
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
+};
+
+const DEFAULT_SETTINGS = {
+ regex: /^(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})$/i,
+ lineRegex: /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi,
+ linkRegex: /^\[([^\[\]]*)\]\((https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})\)$/i,
+ linkLineRegex: /\[([^\[\]]*)\]\((https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})\)/gi,
+ imageRegex: /\.(gif|jpe?g|tiff?|png|webp|bmp|tga|psd|ai)$/i,
+ enhanceDefaultPaste: true,
+ shouldPreserveSelectionAsTitle: false,
+ enhanceDropEvents: true,
+ websiteBlacklist: "",
+ maximumTitleLength: 0,
+ useNewScraper: false,
+ linkPreviewApiKey: "",
+ useBetterPasteId: false,
+};
+class AutoLinkTitleSettingTab extends obsidian.PluginSettingTab {
+ constructor(app, plugin) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
+ display() {
+ let { containerEl } = this;
+ containerEl.empty();
+ new obsidian.Setting(containerEl)
+ .setName("Enhance Default Paste")
+ .setDesc("Fetch the link title when pasting a link in the editor with the default paste command")
+ .addToggle((val) => val
+ .setValue(this.plugin.settings.enhanceDefaultPaste)
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ console.log(value);
+ this.plugin.settings.enhanceDefaultPaste = value;
+ yield this.plugin.saveSettings();
+ })));
+ new obsidian.Setting(containerEl)
+ .setName("Enhance Drop Events")
+ .setDesc("Fetch the link title when drag and dropping a link from another program")
+ .addToggle((val) => val
+ .setValue(this.plugin.settings.enhanceDropEvents)
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ console.log(value);
+ this.plugin.settings.enhanceDropEvents = value;
+ yield this.plugin.saveSettings();
+ })));
+ new obsidian.Setting(containerEl)
+ .setName("Maximum title length")
+ .setDesc("Set the maximum length of the title. Set to 0 to disable.")
+ .addText((val) => val
+ .setValue(this.plugin.settings.maximumTitleLength.toString(10))
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ const titleLength = Number(value);
+ this.plugin.settings.maximumTitleLength =
+ isNaN(titleLength) || titleLength < 0 ? 0 : titleLength;
+ yield this.plugin.saveSettings();
+ })));
+ new obsidian.Setting(containerEl)
+ .setName("Preserve selection as title")
+ .setDesc("Whether to prefer selected text as title over fetched title when pasting")
+ .addToggle((val) => val
+ .setValue(this.plugin.settings.shouldPreserveSelectionAsTitle)
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ console.log(value);
+ this.plugin.settings.shouldPreserveSelectionAsTitle = value;
+ yield this.plugin.saveSettings();
+ })));
+ new obsidian.Setting(containerEl)
+ .setName("Website Blacklist")
+ .setDesc("List of strings (comma separated) that disable autocompleting website titles. Can be URLs or arbitrary text.")
+ .addTextArea((val) => val
+ .setValue(this.plugin.settings.websiteBlacklist)
+ .setPlaceholder("localhost, tiktok.com")
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ this.plugin.settings.websiteBlacklist = value;
+ yield this.plugin.saveSettings();
+ })));
+ new obsidian.Setting(containerEl)
+ .setName("Use New Scraper")
+ .setDesc("Use experimental new scraper, seems to work well on desktop but not mobile.")
+ .addToggle((val) => val
+ .setValue(this.plugin.settings.useNewScraper)
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ console.log(value);
+ this.plugin.settings.useNewScraper = value;
+ yield this.plugin.saveSettings();
+ })));
+ new obsidian.Setting(containerEl)
+ .setName("Use Better Fetching Placeholder")
+ .setDesc("Use a more readable placeholder when fetching the title of a link.")
+ .addToggle((val) => val
+ .setValue(this.plugin.settings.useBetterPasteId)
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ console.log(value);
+ this.plugin.settings.useBetterPasteId = value;
+ yield this.plugin.saveSettings();
+ })));
+ new obsidian.Setting(containerEl)
+ .setName("LinkPreview API Key")
+ .setDesc("API key for the LinkPreview.net service. Get one at https://my.linkpreview.net/access_keys")
+ .addText((text) => text
+ .setValue(this.plugin.settings.linkPreviewApiKey || "")
+ .onChange((value) => __awaiter(this, void 0, void 0, function* () {
+ const trimmedValue = value.trim();
+ if (trimmedValue.length > 0 && trimmedValue.length !== 32) {
+ new obsidian.Notice("LinkPreview API key must be 32 characters long");
+ this.plugin.settings.linkPreviewApiKey = "";
+ }
+ else {
+ this.plugin.settings.linkPreviewApiKey = trimmedValue;
+ }
+ yield this.plugin.saveSettings();
+ })));
+ }
+}
+
+class CheckIf {
+ static isMarkdownLinkAlready(editor) {
+ let cursor = editor.getCursor();
+ // Check if the characters before the url are ]( to indicate a markdown link
+ var titleEnd = editor.getRange({ ch: cursor.ch - 2, line: cursor.line }, { ch: cursor.ch, line: cursor.line });
+ return titleEnd == "](";
+ }
+ static isAfterQuote(editor) {
+ let cursor = editor.getCursor();
+ // Check if the characters before the url are " or ' to indicate we want the url directly
+ // This is common in elements like
+ var beforeChar = editor.getRange({ ch: cursor.ch - 1, line: cursor.line }, { ch: cursor.ch, line: cursor.line });
+ return beforeChar == "\"" || beforeChar == "'";
+ }
+ static isUrl(text) {
+ let urlRegex = new RegExp(DEFAULT_SETTINGS.regex);
+ return urlRegex.test(text);
+ }
+ static isImage(text) {
+ let imageRegex = new RegExp(DEFAULT_SETTINGS.imageRegex);
+ return imageRegex.test(text);
+ }
+ static isLinkedUrl(text) {
+ let urlRegex = new RegExp(DEFAULT_SETTINGS.linkRegex);
+ return urlRegex.test(text);
+ }
+}
+
+class EditorExtensions {
+ static getSelectedText(editor) {
+ if (!editor.somethingSelected()) {
+ let wordBoundaries = this.getWordBoundaries(editor);
+ editor.setSelection(wordBoundaries.start, wordBoundaries.end);
+ }
+ return editor.getSelection();
+ }
+ static cursorWithinBoundaries(cursor, match) {
+ let startIndex = match.index;
+ let endIndex = match.index + match[0].length;
+ return startIndex <= cursor.ch && cursor.ch <= endIndex;
+ }
+ static getWordBoundaries(editor) {
+ let cursor = editor.getCursor();
+ // If its a normal URL token this is not a markdown link
+ // In this case we can simply overwrite the link boundaries as-is
+ let lineText = editor.getLine(cursor.line);
+ // First check if we're in a link
+ let linksInLine = lineText.matchAll(DEFAULT_SETTINGS.linkLineRegex);
+ for (let match of linksInLine) {
+ if (this.cursorWithinBoundaries(cursor, match)) {
+ return {
+ start: { line: cursor.line, ch: match.index },
+ end: { line: cursor.line, ch: match.index + match[0].length },
+ };
+ }
+ }
+ // If not, check if we're in just a standard ol' URL.
+ let urlsInLine = lineText.matchAll(DEFAULT_SETTINGS.lineRegex);
+ for (let match of urlsInLine) {
+ if (this.cursorWithinBoundaries(cursor, match)) {
+ return {
+ start: { line: cursor.line, ch: match.index },
+ end: { line: cursor.line, ch: match.index + match[0].length },
+ };
+ }
+ }
+ return {
+ start: cursor,
+ end: cursor,
+ };
+ }
+ static getEditorPositionFromIndex(content, index) {
+ let substr = content.substr(0, index);
+ let l = 0;
+ let offset = -1;
+ let r = -1;
+ for (; (r = substr.indexOf("\n", r + 1)) !== -1; l++, offset = r)
+ ;
+ offset += 1;
+ let ch = content.substr(offset, index - offset).length;
+ return { line: l, ch: ch };
+ }
+}
+
+function blank$1(text) {
+ return text === undefined || text === null || text === '';
+}
+function notBlank$1(text) {
+ return !blank$1(text);
+}
+function scrape(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ const response = yield obsidian.requestUrl(url);
+ if (!response.headers['content-type'].includes('text/html'))
+ return getUrlFinalSegment$1(url);
+ const html = response.text;
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ const title = doc.querySelector('title');
+ if (blank$1(title === null || title === void 0 ? void 0 : title.innerText)) {
+ // If site is javascript based and has a no-title attribute when unloaded, use it.
+ var noTitle = title === null || title === void 0 ? void 0 : title.getAttr('no-title');
+ if (notBlank$1(noTitle)) {
+ return noTitle;
+ }
+ // Otherwise if the site has no title/requires javascript simply return Title Unknown
+ return url;
+ }
+ return title.innerText;
+ }
+ catch (ex) {
+ console.error(ex);
+ return '';
+ }
+ });
+}
+function getUrlFinalSegment$1(url) {
+ try {
+ const segments = new URL(url).pathname.split('/');
+ const last = segments.pop() || segments.pop(); // Handle potential trailing slash
+ return last;
+ }
+ catch (_) {
+ return 'File';
+ }
+}
+function getPageTitle$1(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (!(url.startsWith('http') || url.startsWith('https'))) {
+ url = 'https://' + url;
+ }
+ return scrape(url);
+ });
+}
+
+const electronPkg = require("electron");
+function blank(text) {
+ return text === undefined || text === null || text === "";
+}
+function notBlank(text) {
+ return !blank(text);
+}
+// async wrapper to load a url and settle on load finish or fail
+function load(window, url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return new Promise((resolve, reject) => {
+ window.webContents.on("did-finish-load", (event) => resolve(event));
+ window.webContents.on("did-fail-load", (event) => reject(event));
+ window.loadURL(url);
+ });
+ });
+}
+function electronGetPageTitle(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const { remote } = electronPkg;
+ const { BrowserWindow } = remote;
+ try {
+ const window = new BrowserWindow({
+ width: 1000,
+ height: 600,
+ webPreferences: {
+ webSecurity: false,
+ nodeIntegration: true,
+ images: false,
+ },
+ show: false,
+ });
+ window.webContents.setAudioMuted(true);
+ window.webContents.on("will-navigate", (event, newUrl) => {
+ event.preventDefault();
+ window.loadURL(newUrl);
+ });
+ yield load(window, url);
+ try {
+ const title = window.webContents.getTitle();
+ window.destroy();
+ if (notBlank(title)) {
+ return title;
+ }
+ else {
+ return url;
+ }
+ }
+ catch (ex) {
+ window.destroy();
+ return url;
+ }
+ }
+ catch (ex) {
+ console.error(ex);
+ return "";
+ }
+ });
+}
+function nonElectronGetPageTitle(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ const html = yield obsidian.request({ url });
+ const doc = new DOMParser().parseFromString(html, "text/html");
+ const title = doc.querySelectorAll("title")[0];
+ if (title == null || blank(title === null || title === void 0 ? void 0 : title.innerText)) {
+ // If site is javascript based and has a no-title attribute when unloaded, use it.
+ var noTitle = title === null || title === void 0 ? void 0 : title.getAttr("no-title");
+ if (notBlank(noTitle)) {
+ return noTitle;
+ }
+ // Otherwise if the site has no title/requires javascript simply return Title Unknown
+ return url;
+ }
+ return title.innerText;
+ }
+ catch (ex) {
+ console.error(ex);
+ return "";
+ }
+ });
+}
+function getUrlFinalSegment(url) {
+ try {
+ const segments = new URL(url).pathname.split('/');
+ const last = segments.pop() || segments.pop(); // Handle potential trailing slash
+ return last;
+ }
+ catch (_) {
+ return "File";
+ }
+}
+function tryGetFileType(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ const response = yield fetch(url, { method: "HEAD" });
+ // Ensure site returns an ok status code before scraping
+ if (!response.ok) {
+ return "Site Unreachable";
+ }
+ // Ensure site is an actual HTML page and not a pdf or 3 gigabyte video file.
+ let contentType = response.headers.get("content-type");
+ if (!contentType.includes("text/html")) {
+ return getUrlFinalSegment(url);
+ }
+ return null;
+ }
+ catch (err) {
+ return null;
+ }
+ });
+}
+function getPageTitle(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // If we're on Desktop use the Electron scraper
+ if (!(url.startsWith("http") || url.startsWith("https"))) {
+ url = "https://" + url;
+ }
+ // Try to do a HEAD request to see if the site is reachable and if it's an HTML page
+ // If we error out due to CORS, we'll just try to scrape the page anyway.
+ let fileType = yield tryGetFileType(url);
+ if (fileType) {
+ return fileType;
+ }
+ if (electronPkg != null) {
+ return electronGetPageTitle(url);
+ }
+ else {
+ return nonElectronGetPageTitle(url);
+ }
+ });
+}
+
+class AutoLinkTitle extends obsidian.Plugin {
+ constructor() {
+ super(...arguments);
+ this.shortTitle = (title) => {
+ if (this.settings.maximumTitleLength === 0) {
+ return title;
+ }
+ if (title.length < this.settings.maximumTitleLength + 3) {
+ return title;
+ }
+ const shortenedTitle = `${title.slice(0, this.settings.maximumTitleLength)}...`;
+ return shortenedTitle;
+ };
+ }
+ onload() {
+ return __awaiter(this, void 0, void 0, function* () {
+ console.log("loading obsidian-auto-link-title");
+ yield this.loadSettings();
+ this.blacklist = this.settings.websiteBlacklist
+ .split(",")
+ .map((s) => s.trim())
+ .filter((s) => s.length > 0);
+ // Listen to paste event
+ this.pasteFunction = this.pasteUrlWithTitle.bind(this);
+ // Listen to drop event
+ this.dropFunction = this.dropUrlWithTitle.bind(this);
+ this.addCommand({
+ id: "auto-link-title-paste",
+ name: "Paste URL and auto fetch title",
+ editorCallback: (editor) => this.manualPasteUrlWithTitle(editor),
+ hotkeys: [],
+ });
+ this.addCommand({
+ id: "auto-link-title-normal-paste",
+ name: "Normal paste (no fetching behavior)",
+ editorCallback: (editor) => this.normalPaste(editor),
+ hotkeys: [
+ {
+ modifiers: ["Mod", "Shift"],
+ key: "v",
+ },
+ ],
+ });
+ this.registerEvent(this.app.workspace.on("editor-paste", this.pasteFunction));
+ this.registerEvent(this.app.workspace.on("editor-drop", this.dropFunction));
+ this.addCommand({
+ id: "enhance-url-with-title",
+ name: "Enhance existing URL with link and title",
+ editorCallback: (editor) => this.addTitleToLink(editor),
+ hotkeys: [
+ {
+ modifiers: ["Mod", "Shift"],
+ key: "e",
+ },
+ ],
+ });
+ this.addSettingTab(new AutoLinkTitleSettingTab(this.app, this));
+ });
+ }
+ addTitleToLink(editor) {
+ // Only attempt fetch if online
+ if (!navigator.onLine)
+ return;
+ let selectedText = (EditorExtensions.getSelectedText(editor) || "").trim();
+ // If the cursor is on a raw html link, convert to a markdown link and fetch title
+ if (CheckIf.isUrl(selectedText)) {
+ this.convertUrlToTitledLink(editor, selectedText);
+ }
+ // If the cursor is on the URL part of a markdown link, fetch title and replace existing link title
+ else if (CheckIf.isLinkedUrl(selectedText)) {
+ const link = this.getUrlFromLink(selectedText);
+ this.convertUrlToTitledLink(editor, link);
+ }
+ }
+ normalPaste(editor) {
+ return __awaiter(this, void 0, void 0, function* () {
+ let clipboardText = yield navigator.clipboard.readText();
+ if (clipboardText === null || clipboardText === "")
+ return;
+ editor.replaceSelection(clipboardText);
+ });
+ }
+ // Simulate standard paste but using editor.replaceSelection with clipboard text since we can't seem to dispatch a paste event.
+ manualPasteUrlWithTitle(editor) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const clipboardText = yield navigator.clipboard.readText();
+ // Only attempt fetch if online
+ if (!navigator.onLine) {
+ editor.replaceSelection(clipboardText);
+ return;
+ }
+ if (clipboardText == null || clipboardText == "")
+ return;
+ // If its not a URL, we return false to allow the default paste handler to take care of it.
+ // Similarly, image urls don't have a meaningful
attribute so downloading it
+ // to fetch the title is a waste of bandwidth.
+ if (!CheckIf.isUrl(clipboardText) || CheckIf.isImage(clipboardText)) {
+ editor.replaceSelection(clipboardText);
+ return;
+ }
+ // If it looks like we're pasting the url into a markdown link already, don't fetch title
+ // as the user has already probably put a meaningful title, also it would lead to the title
+ // being inside the link.
+ if (CheckIf.isMarkdownLinkAlready(editor) || CheckIf.isAfterQuote(editor)) {
+ editor.replaceSelection(clipboardText);
+ return;
+ }
+ // If url is pasted over selected text and setting is enabled, no need to fetch title,
+ // just insert a link
+ let selectedText = (EditorExtensions.getSelectedText(editor) || "").trim();
+ if (selectedText && this.settings.shouldPreserveSelectionAsTitle) {
+ editor.replaceSelection(`[${selectedText}](${clipboardText})`);
+ return;
+ }
+ // At this point we're just pasting a link in a normal fashion, fetch its title.
+ this.convertUrlToTitledLink(editor, clipboardText);
+ return;
+ });
+ }
+ pasteUrlWithTitle(clipboard, editor) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (!this.settings.enhanceDefaultPaste) {
+ return;
+ }
+ if (clipboard.defaultPrevented)
+ return;
+ // Only attempt fetch if online
+ if (!navigator.onLine)
+ return;
+ let clipboardText = clipboard.clipboardData.getData("text/plain");
+ if (clipboardText === null || clipboardText === "")
+ return;
+ // If its not a URL, we return false to allow the default paste handler to take care of it.
+ // Similarly, image urls don't have a meaningful attribute so downloading it
+ // to fetch the title is a waste of bandwidth.
+ if (!CheckIf.isUrl(clipboardText) || CheckIf.isImage(clipboardText)) {
+ return;
+ }
+ // We've decided to handle the paste, stop propagation to the default handler.
+ clipboard.stopPropagation();
+ clipboard.preventDefault();
+ // If it looks like we're pasting the url into a markdown link already, don't fetch title
+ // as the user has already probably put a meaningful title, also it would lead to the title
+ // being inside the link.
+ if (CheckIf.isMarkdownLinkAlready(editor) || CheckIf.isAfterQuote(editor)) {
+ editor.replaceSelection(clipboardText);
+ return;
+ }
+ // If url is pasted over selected text and setting is enabled, no need to fetch title,
+ // just insert a link
+ let selectedText = (EditorExtensions.getSelectedText(editor) || "").trim();
+ if (selectedText && this.settings.shouldPreserveSelectionAsTitle) {
+ editor.replaceSelection(`[${selectedText}](${clipboardText})`);
+ return;
+ }
+ // At this point we're just pasting a link in a normal fashion, fetch its title.
+ this.convertUrlToTitledLink(editor, clipboardText);
+ return;
+ });
+ }
+ dropUrlWithTitle(dropEvent, editor) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (!this.settings.enhanceDropEvents) {
+ return;
+ }
+ if (dropEvent.defaultPrevented)
+ return;
+ // Only attempt fetch if online
+ if (!navigator.onLine)
+ return;
+ let dropText = dropEvent.dataTransfer.getData("text/plain");
+ if (dropText === null || dropText === "")
+ return;
+ // If its not a URL, we return false to allow the default paste handler to take care of it.
+ // Similarly, image urls don't have a meaningful attribute so downloading it
+ // to fetch the title is a waste of bandwidth.
+ if (!CheckIf.isUrl(dropText) || CheckIf.isImage(dropText)) {
+ return;
+ }
+ // We've decided to handle the paste, stop propagation to the default handler.
+ dropEvent.stopPropagation();
+ dropEvent.preventDefault();
+ // If it looks like we're pasting the url into a markdown link already, don't fetch title
+ // as the user has already probably put a meaningful title, also it would lead to the title
+ // being inside the link.
+ if (CheckIf.isMarkdownLinkAlready(editor) || CheckIf.isAfterQuote(editor)) {
+ editor.replaceSelection(dropText);
+ return;
+ }
+ // If url is pasted over selected text and setting is enabled, no need to fetch title,
+ // just insert a link
+ let selectedText = (EditorExtensions.getSelectedText(editor) || "").trim();
+ if (selectedText && this.settings.shouldPreserveSelectionAsTitle) {
+ editor.replaceSelection(`[${selectedText}](${dropText})`);
+ return;
+ }
+ // At this point we're just pasting a link in a normal fashion, fetch its title.
+ this.convertUrlToTitledLink(editor, dropText);
+ return;
+ });
+ }
+ isBlacklisted(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield this.loadSettings();
+ this.blacklist = this.settings.websiteBlacklist
+ .split(/,|\n/)
+ .map((s) => s.trim())
+ .filter((s) => s.length > 0);
+ return this.blacklist.some((site) => url.includes(site));
+ });
+ }
+ convertUrlToTitledLink(editor, url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (yield this.isBlacklisted(url)) {
+ let domain = new URL(url).hostname;
+ editor.replaceSelection(`[${domain}](${url})`);
+ return;
+ }
+ // Generate a unique id for find/replace operations for the title.
+ const pasteId = this.getPasteId();
+ // Instantly paste so you don't wonder if paste is broken
+ editor.replaceSelection(`[${pasteId}](${url})`);
+ // Fetch title from site, replace Fetching Title with actual title
+ const title = yield this.fetchUrlTitle(url);
+ const escapedTitle = this.escapeMarkdown(title);
+ const shortenedTitle = this.shortTitle(escapedTitle);
+ const text = editor.getValue();
+ const start = text.indexOf(pasteId);
+ if (start < 0) {
+ console.log(`Unable to find text "${pasteId}" in current editor, bailing out; link ${url}`);
+ }
+ else {
+ const end = start + pasteId.length;
+ const startPos = EditorExtensions.getEditorPositionFromIndex(text, start);
+ const endPos = EditorExtensions.getEditorPositionFromIndex(text, end);
+ editor.replaceRange(shortenedTitle, startPos, endPos);
+ }
+ });
+ }
+ escapeMarkdown(text) {
+ var unescaped = text.replace(/\\(\*|_|`|~|\\|\[|\])/g, "$1"); // unescape any "backslashed" character
+ var escaped = unescaped.replace(/(\*|_|`|<|>|~|\\|\[|\])/g, "\\$1"); // escape *, _, `, ~, \, [, ], <, and >
+ var escaped = unescaped.replace(/(\*|_|`|\||<|>|~|\\|\[|\])/g, "\\$1"); // escape *, _, `, ~, \, |, [, ], <, and >
+ return escaped;
+ }
+ fetchUrlTitleViaLinkPreview(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (this.settings.linkPreviewApiKey.length !== 32) {
+ console.error("LinkPreview API key is not 32 characters long, please check your settings");
+ return "";
+ }
+ try {
+ const apiEndpoint = `https://api.linkpreview.net/?q=${encodeURIComponent(url)}`;
+ const response = yield fetch(apiEndpoint, {
+ headers: {
+ "X-Linkpreview-Api-Key": this.settings.linkPreviewApiKey,
+ },
+ });
+ const data = yield response.json();
+ return data.title;
+ }
+ catch (error) {
+ console.error(error);
+ return "";
+ }
+ });
+ }
+ fetchUrlTitle(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ let title = "";
+ title = yield this.fetchUrlTitleViaLinkPreview(url);
+ console.log(`Title via Link Preview: ${title}`);
+ if (title === "") {
+ console.log("Title via Link Preview failed, falling back to scraper");
+ if (this.settings.useNewScraper) {
+ console.log("Using new scraper");
+ title = yield getPageTitle$1(url);
+ }
+ else {
+ console.log("Using old scraper");
+ title = yield getPageTitle(url);
+ }
+ }
+ console.log(`Title: ${title}`);
+ title =
+ title.replace(/(\r\n|\n|\r)/gm, "").trim() ||
+ "Title Unavailable | Site Unreachable";
+ return title;
+ }
+ catch (error) {
+ console.error(error);
+ return "Error fetching title";
+ }
+ });
+ }
+ getUrlFromLink(link) {
+ let urlRegex = new RegExp(DEFAULT_SETTINGS.linkRegex);
+ return urlRegex.exec(link)[2];
+ }
+ getPasteId() {
+ var base = "Fetching Title";
+ if (this.settings.useBetterPasteId) {
+ return this.getBetterPasteId(base);
+ }
+ else {
+ return `${base}#${this.createBlockHash()}`;
+ }
+ }
+ getBetterPasteId(base) {
+ // After every character, add 0, 1 or 2 invisible characters
+ // so that to the user it looks just like the base string.
+ // The number of combinations is 3^14 = 4782969
+ let result = "";
+ var invisibleCharacter = "\u200B";
+ var maxInvisibleCharacters = 2;
+ for (var i = 0; i < base.length; i++) {
+ var count = Math.floor(Math.random() * (maxInvisibleCharacters + 1));
+ result += base.charAt(i) + invisibleCharacter.repeat(count);
+ }
+ return result;
+ }
+ // Custom hashid by @shabegom
+ createBlockHash() {
+ let result = "";
+ var characters = "abcdefghijklmnopqrstuvwxyz0123456789";
+ var charactersLength = characters.length;
+ for (var i = 0; i < 4; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+ return result;
+ }
+ onunload() {
+ console.log("unloading obsidian-auto-link-title");
+ }
+ loadSettings() {
+ return __awaiter(this, void 0, void 0, function* () {
+ this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
+ });
+ }
+ saveSettings() {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield this.saveData(this.settings);
+ });
+ }
+}
+
+module.exports = AutoLinkTitle;
+
+
+/* nosourcemap */
\ No newline at end of file
diff --git a/.obsidian/plugins/obsidian-auto-link-title/manifest.json b/.obsidian/plugins/obsidian-auto-link-title/manifest.json
new file mode 100644
index 0000000..e3f4db2
--- /dev/null
+++ b/.obsidian/plugins/obsidian-auto-link-title/manifest.json
@@ -0,0 +1,10 @@
+{
+ "id": "obsidian-auto-link-title",
+ "name": "Auto Link Title",
+ "version": "1.5.5",
+ "minAppVersion": "0.12.17",
+ "description": "This plugin automatically fetches the titles of links from the web",
+ "author": "Matt Furden",
+ "authorUrl": "https://github.com/zolrath",
+ "isDesktopOnly": false
+}
diff --git a/.obsidian/plugins/obsidian-auto-link-title/styles.css b/.obsidian/plugins/obsidian-auto-link-title/styles.css
new file mode 100644
index 0000000..ad3bb8f
--- /dev/null
+++ b/.obsidian/plugins/obsidian-auto-link-title/styles.css
@@ -0,0 +1 @@
+/* no styles */
\ No newline at end of file
diff --git a/.obsidian/plugins/obsidian-paste-image-rename/data.json b/.obsidian/plugins/obsidian-paste-image-rename/data.json
new file mode 100644
index 0000000..2ea8a7a
--- /dev/null
+++ b/.obsidian/plugins/obsidian-paste-image-rename/data.json
@@ -0,0 +1,11 @@
+{
+ "imageNamePattern": "{{fileHash}}",
+ "dupNumberAtStart": false,
+ "dupNumberDelimiter": "-",
+ "dupNumberAlways": false,
+ "autoRename": true,
+ "handleAllAttachments": true,
+ "excludeExtensionPattern": "",
+ "disableRenameNotice": false,
+ "useFileHashName": true
+}
\ No newline at end of file
diff --git a/.obsidian/plugins/obsidian-paste-image-rename/main.js b/.obsidian/plugins/obsidian-paste-image-rename/main.js
new file mode 100644
index 0000000..2d4ffb9
--- /dev/null
+++ b/.obsidian/plugins/obsidian-paste-image-rename/main.js
@@ -0,0 +1,965 @@
+/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD */
+var __defProp = Object.defineProperty;
+var __defProps = Object.defineProperties;
+var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
+var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
+var __getOwnPropNames = Object.getOwnPropertyNames;
+var __getOwnPropSymbols = Object.getOwnPropertySymbols;
+var __hasOwnProp = Object.prototype.hasOwnProperty;
+var __propIsEnum = Object.prototype.propertyIsEnumerable;
+var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
+var __spreadValues = (a, b) => {
+ for (var prop in b || (b = {}))
+ if (__hasOwnProp.call(b, prop))
+ __defNormalProp(a, prop, b[prop]);
+ if (__getOwnPropSymbols)
+ for (var prop of __getOwnPropSym )
+ __defNormalProp(a, prop, b[prop]);
+ return a;
+};
+var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
+var __commonJS = (cb, mod) => function __require() {
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+};
+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);
+var __async = (__this, __arguments, generator) => {
+ return new Promise((resolve, reject) => {
+ var fulfilled = (value) => {
+ try {
+ step(generator.next(value));
+ } catch (e) {
+ reject(e);
+ }
+ };
+ var rejected = (value) => {
+ try {
+ step(generator.throw(value));
+ } catch (e) {
+ reject(e);
+ }
+ };
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
+ step((generator = generator.apply(__this, __arguments)).next());
+ });
+};
+
+// package.json
+var require_package = __commonJS({
+ "package.json"(exports, module2) {
+ module2.exports = {
+ name: "obsidian-paste-image-rename",
+ version: "1.6.1",
+ main: "main.js",
+ scripts: {
+ start: "node esbuild.config.mjs",
+ build: "tsc -noEmit -skipLibCheck && BUILD_ENV=production node esbuild.config.mjs && cp manifest.json build",
+ version: "node version-bump.mjs && git add manifest.json versions.json",
+ release: "npm run build && gh release create ${npm_package_version} build/*"
+ },
+ keywords: [],
+ author: "Reorx",
+ license: "MIT",
+ devDependencies: {
+ "@types/node": "^18.11.18",
+ "@typescript-eslint/eslint-plugin": "^5.49.0",
+ "@typescript-eslint/parser": "^5.49.0",
+ "builtin-modules": "^3.3.0",
+ esbuild: "0.16.17",
+ obsidian: "^1.1.1",
+ tslib: "2.5.0",
+ typescript: "4.9.4"
+ },
+ dependencies: {
+ "cash-dom": "^8.1.2"
+ }
+ };
+ }
+});
+
+// src/main.ts
+var main_exports = {};
+__export(main_exports, {
+ default: () => PasteImageRenamePlugin
+});
+module.exports = __toCommonJS(main_exports);
+var import_obsidian2 = require("obsidian");
+
+// src/batch.ts
+var import_obsidian = require("obsidian");
+
+// src/utils.ts
+var DEBUG = false;
+if (DEBUG)
+ console.log("DEBUG is enabled");
+function debugLog(...args) {
+ if (DEBUG) {
+ console.log(new Date().toISOString().slice(11, 23), ...args);
+ }
+}
+function createElementTree(rootEl, opts) {
+ const result = {
+ el: rootEl.createEl(opts.tag, opts),
+ children: []
+ };
+ const children = opts.children || [];
+ for (const child of children) {
+ result.children.push(createElementTree(result.el, child));
+ }
+ return result;
+}
+var path = {
+ // Credit: @creationix/path.js
+ join(...partSegments) {
+ let parts = [];
+ for (let i = 0, l = partSegments.length; i < l; i++) {
+ parts = parts.concat(partSegments[i].split("/"));
+ }
+ const newParts = [];
+ for (let i = 0, l = parts.length; i < l; i++) {
+ const part = parts[i];
+ if (!part || part === ".")
+ continue;
+ else
+ newParts.push(part);
+ }
+ if (parts[0] === "")
+ newParts.unshift("");
+ return newParts.join("/");
+ },
+ // returns the last part of a path, e.g. 'foo.jpg'
+ basename(fullpath) {
+ const sp = fullpath.split("/");
+ return sp[sp.length - 1];
+ },
+ // return extension without dot, e.g. 'jpg'
+ extension(fullpath) {
+ const positions = [...fullpath.matchAll(new RegExp("\\.", "gi"))].map((a) => a.index);
+ return fullpath.slice(positions[positions.length - 1] + 1);
+ }
+};
+var filenameNotAllowedChars = /[^\p{L}0-9~`!@$&*()\-_=+{};'",<.>? ]/ug;
+var sanitizer = {
+ filename(s) {
+ return s.replace(filenameNotAllowedChars, "").trim();
+ },
+ delimiter(s) {
+ s = this.filename(s);
+ if (!s)
+ s = "-";
+ return s;
+ }
+};
+function escapeRegExp(s) {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+function lockInputMethodComposition(el) {
+ const state = {
+ lock: false
+ };
+ el.addEventListener("compositionstart", () => {
+ state.lock = true;
+ });
+ el.addEventListener("compositionend", () => {
+ state.lock = false;
+ });
+ return state;
+}
+function toHex(buffer) {
+ return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
+}
+function sha256(buffer) {
+ return __async(this, null, function* () {
+ const digest = yield crypto.subtle.digest("SHA-256", buffer);
+ return toHex(digest);
+ });
+}
+
+// src/batch.ts
+var ImageBatchRenameModal = class extends import_obsidian.Modal {
+ constructor(app, activeFile, renameFunc, onClose) {
+ super(app);
+ this.activeFile = activeFile;
+ this.renameFunc = renameFunc;
+ this.onCloseExtra = onClose;
+ this.state = {
+ namePattern: "",
+ extPattern: "",
+ nameReplace: "",
+ renameTasks: []
+ };
+ }
+ onOpen() {
+ this.containerEl.addClass("image-rename-modal");
+ const { contentEl, titleEl } = this;
+ titleEl.setText("Batch rename embeded files");
+ const namePatternSetting = new import_obsidian.Setting(contentEl).setName("Name pattern").setDesc("Please input the name pattern to match files (regex)").addText((text) => text.setValue(this.state.namePattern).onChange(
+ (value) => __async(this, null, function* () {
+ this.state.namePattern = value;
+ })
+ ));
+ const npInputEl = namePatternSetting.controlEl.children[0];
+ npInputEl.focus();
+ const npInputState = lockInputMethodComposition(npInputEl);
+ npInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
+ if (e.key === "Enter" && !npInputState.lock) {
+ e.preventDefault();
+ if (!this.state.namePattern) {
+ errorEl.innerText = 'Error: "Name pattern" could not be empty';
+ errorEl.style.display = "block";
+ return;
+ }
+ this.matchImageNames(tbodyEl);
+ }
+ }));
+ const extPatternSetting = new import_obsidian.Setting(contentEl).setName("Extension pattern").setDesc("Please input the extension pattern to match files (regex)").addText((text) => text.setValue(this.state.extPattern).onChange(
+ (value) => __async(this, null, function* () {
+ this.state.extPattern = value;
+ })
+ ));
+ const extInputEl = extPatternSetting.controlEl.children[0];
+ extInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ this.matchImageNames(tbodyEl);
+ }
+ }));
+ const nameReplaceSetting = new import_obsidian.Setting(contentEl).setName("Name replace").setDesc("Please input the string to replace the matched name (use $1, $2 for regex groups)").addText((text) => text.setValue(this.state.nameReplace).onChange(
+ (value) => __async(this, null, function* () {
+ this.state.nameReplace = value;
+ })
+ ));
+ const nrInputEl = nameReplaceSetting.controlEl.children[0];
+ const nrInputState = lockInputMethodComposition(nrInputEl);
+ nrInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
+ if (e.key === "Enter" && !nrInputState.lock) {
+ e.preventDefault();
+ this.matchImageNames(tbodyEl);
+ }
+ }));
+ const matchedContainer = contentEl.createDiv({
+ cls: "matched-container"
+ });
+ const tableET = createElementTree(matchedContainer, {
+ tag: "table",
+ children: [
+ {
+ tag: "thead",
+ children: [
+ {
+ tag: "tr",
+ children: [
+ {
+ tag: "td",
+ text: "Original path"
+ },
+ {
+ tag: "td",
+ text: "Renamed Name"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ tag: "tbody"
+ }
+ ]
+ });
+ const tbodyEl = tableET.children[1].el;
+ const errorEl = contentEl.createDiv({
+ cls: "error",
+ attr: {
+ style: "display: none;"
+ }
+ });
+ new import_obsidian.Setting(contentEl).addButton((button) => {
+ button.setButtonText("Rename all").setClass("mod-cta").onClick(() => {
+ new ConfirmModal(
+ this.app,
+ "Confirm rename all",
+ `Are you sure? This will rename all the ${this.state.renameTasks.length} images matched the pattern.`,
+ () => {
+ this.renameAll();
+ this.close();
+ }
+ ).open();
+ });
+ }).addButton((button) => {
+ button.setButtonText("Cancel").onClick(() => {
+ this.close();
+ });
+ });
+ }
+ onClose() {
+ const { contentEl } = this;
+ contentEl.empty();
+ this.onCloseExtra();
+ }
+ renameAll() {
+ return __async(this, null, function* () {
+ debugLog("renameAll", this.state);
+ for (const task of this.state.renameTasks) {
+ yield this.renameFunc(task.file, task.name);
+ }
+ });
+ }
+ matchImageNames(tbodyEl) {
+ const { state } = this;
+ const renameTasks = [];
+ tbodyEl.empty();
+ const fileCache = this.app.metadataCache.getFileCache(this.activeFile);
+ if (!fileCache || !fileCache.embeds)
+ return;
+ const namePatternRegex = new RegExp(state.namePattern, "g");
+ const extPatternRegex = new RegExp(state.extPattern);
+ fileCache.embeds.forEach((embed) => {
+ const file = this.app.metadataCache.getFirstLinkpathDest(embed.link, this.activeFile.path);
+ if (!file) {
+ console.warn("file not found", embed.link);
+ return;
+ }
+ if (state.extPattern) {
+ const m0 = extPatternRegex.exec(file.extension);
+ if (!m0)
+ return;
+ }
+ const stem = file.basename;
+ namePatternRegex.lastIndex = 0;
+ const m1 = namePatternRegex.exec(stem);
+ if (!m1)
+ return;
+ let renamedName = file.name;
+ if (state.nameReplace) {
+ namePatternRegex.lastIndex = 0;
+ renamedName = stem.replace(namePatternRegex, state.nameReplace);
+ renamedName = `${renamedName}.${file.extension}`;
+ }
+ renameTasks.push({
+ file,
+ name: renamedName
+ });
+ createElementTree(tbodyEl, {
+ tag: "tr",
+ children: [
+ {
+ tag: "td",
+ children: [
+ {
+ tag: "span",
+ text: file.name
+ },
+ {
+ tag: "div",
+ text: file.path,
+ attr: {
+ class: "file-path"
+ }
+ }
+ ]
+ },
+ {
+ tag: "td",
+ children: [
+ {
+ tag: "span",
+ text: renamedName
+ },
+ {
+ tag: "div",
+ text: path.join(file.parent.path, renamedName),
+ attr: {
+ class: "file-path"
+ }
+ }
+ ]
+ }
+ ]
+ });
+ });
+ debugLog("new renameTasks", renameTasks);
+ state.renameTasks = renameTasks;
+ }
+};
+var ConfirmModal = class extends import_obsidian.Modal {
+ constructor(app, title, message, onConfirm) {
+ super(app);
+ this.title = title;
+ this.message = message;
+ this.onConfirm = onConfirm;
+ }
+ onOpen() {
+ const { contentEl, titleEl } = this;
+ titleEl.setText(this.title);
+ contentEl.createEl("p", {
+ text: this.message
+ });
+ new import_obsidian.Setting(contentEl).addButton((button) => {
+ button.setButtonText("Yes").setClass("mod-warning").onClick(() => {
+ this.onConfirm();
+ this.close();
+ });
+ }).addButton((button) => {
+ button.setButtonText("No").onClick(() => {
+ this.close();
+ });
+ });
+ }
+};
+
+// src/template.ts
+var dateTmplRegex = /{{DATE:([^}]+)}}/gm;
+var frontmatterTmplRegex = /{{frontmatter:([^}]+)}}/gm;
+var replaceDateVar = (s, date) => {
+ const m = dateTmplRegex.exec(s);
+ if (!m)
+ return s;
+ return s.replace(m[0], date.format(m[1]));
+};
+var replaceFrontmatterVar = (s, frontmatter) => {
+ if (!frontmatter)
+ return s;
+ const m = frontmatterTmplRegex.exec(s);
+ if (!m)
+ return s;
+ return s.replace(m[0], frontmatter[m[1]] || "");
+};
+var renderTemplate = (tmpl, data, frontmatter) => {
+ const now = window.moment();
+ let text = tmpl;
+ let newtext;
+ while ((newtext = replaceDateVar(text, now)) != text) {
+ text = newtext;
+ }
+ while ((newtext = replaceFrontmatterVar(text, frontmatter)) != text) {
+ text = newtext;
+ }
+ text = text.replace(/{{imageNameKey}}/gm, data.imageNameKey).replace(/{{fileName}}/gm, data.fileName).replace(/{{dirName}}/gm, data.dirName).replace(/{{firstHeading}}/gm, data.firstHeading).replace(/{{fileHash}}/gm, data.fileHash);
+ return text;
+};
+
+// src/main.ts
+var DEFAULT_SETTINGS = {
+ imageNamePattern: "{{fileName}}",
+ dupNumberAtStart: false,
+ dupNumberDelimiter: "-",
+ dupNumberAlways: false,
+ autoRename: false,
+ handleAllAttachments: false,
+ excludeExtensionPattern: "",
+ disableRenameNotice: false
+};
+var PASTED_IMAGE_PREFIX = "Pasted image ";
+var PasteImageRenamePlugin = class extends import_obsidian2.Plugin {
+ constructor() {
+ super(...arguments);
+ this.modals = [];
+ }
+ onload() {
+ return __async(this, null, function* () {
+ const pkg = require_package();
+ console.log(`Plugin loading: ${pkg.name} ${pkg.version} BUILD_ENV=${"production"}`);
+ yield this.loadSettings();
+ this.registerEvent(
+ this.app.vault.on("create", (file) => {
+ if (!(file instanceof import_obsidian2.TFile))
+ return;
+ const timeGapMs = new Date().getTime() - file.stat.ctime;
+ if (timeGapMs > 1e3)
+ return;
+ if (isMarkdownFile(file))
+ return;
+ if (isPastedImage(file)) {
+ debugLog("pasted image created", file);
+ this.startRenameProcess(file, this.settings.autoRename);
+ } else {
+ if (this.settings.handleAllAttachments) {
+ debugLog("handleAllAttachments for file", file);
+ if (this.testExcludeExtension(file)) {
+ debugLog("excluded file by ext", file);
+ return;
+ }
+ this.startRenameProcess(file, this.settings.autoRename);
+ }
+ }
+ })
+ );
+ const startBatchRenameProcess = () => {
+ this.openBatchRenameModal();
+ };
+ this.addCommand({
+ id: "batch-rename-embeded-files",
+ name: "Batch rename embeded files (in the current file)",
+ callback: startBatchRenameProcess
+ });
+ if (DEBUG) {
+ this.addRibbonIcon("wand-glyph", "Batch rename embeded files", startBatchRenameProcess);
+ }
+ const batchRenameAllImages = () => {
+ this.batchRenameAllImages();
+ };
+ this.addCommand({
+ id: "batch-rename-all-images",
+ name: "Batch rename all images instantly (in the current file)",
+ callback: batchRenameAllImages
+ });
+ if (DEBUG) {
+ this.addRibbonIcon("wand-glyph", "Batch rename all images instantly (in the current file)", batchRenameAllImages);
+ }
+ this.addSettingTab(new SettingTab(this.app, this));
+ });
+ }
+ startRenameProcess(file, autoRename = false) {
+ return __async(this, null, function* () {
+ const activeFile = this.getActiveFile();
+ if (!activeFile) {
+ new import_obsidian2.Notice("Error: No active file found.");
+ return;
+ }
+ const { stem, newName, isMeaningful } = yield this.generateNewName(file, activeFile);
+ debugLog("generated newName:", newName, isMeaningful);
+ if (!isMeaningful || !autoRename) {
+ this.openRenameModal(file, isMeaningful ? stem : "", activeFile.path);
+ return;
+ }
+ this.renameFile(file, newName, activeFile.path, true);
+ });
+ }
+ renameFile(file, inputNewName, sourcePath, replaceCurrentLine) {
+ return __async(this, null, function* () {
+ const { name: newName } = yield this.deduplicateNewName(inputNewName, file);
+ debugLog("deduplicated newName:", newName);
+ const originName = file.name;
+ const linkText = this.app.fileManager.generateMarkdownLink(file, sourcePath);
+ const newPath = path.join(file.parent.path, newName);
+ try {
+ yield this.app.fileManager.renameFile(file, newPath);
+ } catch (err) {
+ new import_obsidian2.Notice(`Failed to rename ${newName}: ${err}`);
+ throw err;
+ }
+ if (!replaceCurrentLine) {
+ return;
+ }
+ const newLinkText = this.app.fileManager.generateMarkdownLink(file, sourcePath);
+ debugLog("replace text", linkText, newLinkText);
+ const editor = this.getActiveEditor();
+ if (!editor) {
+ new import_obsidian2.Notice(`Failed to rename ${newName}: no active editor`);
+ return;
+ }
+ const changes = [];
+ for (let lineNumber = 0; lineNumber < editor.lineCount(); lineNumber++) {
+ const line = editor.getLine(lineNumber);
+ const replacedLine = line.split(linkText).join(newLinkText);
+ if (line === replacedLine) {
+ continue;
+ }
+ debugLog("replace line", lineNumber, line, replacedLine);
+ changes.push({
+ from: { line: lineNumber, ch: 0 },
+ to: { line: lineNumber, ch: line.length },
+ text: replacedLine
+ });
+ }
+ if (changes.length > 0) {
+ editor.transaction({ changes });
+ }
+ if (!this.settings.disableRenameNotice) {
+ new import_obsidian2.Notice(`Renamed ${originName} to ${newName}`);
+ }
+ });
+ }
+ openRenameModal(file, newName, sourcePath) {
+ const modal = new ImageRenameModal(
+ this.app,
+ file,
+ newName,
+ (confirmedName) => {
+ debugLog("confirmedName:", confirmedName);
+ this.renameFile(file, confirmedName, sourcePath, true);
+ },
+ () => {
+ this.modals.splice(this.modals.indexOf(modal), 1);
+ }
+ );
+ this.modals.push(modal);
+ modal.open();
+ debugLog("modals count", this.modals.length);
+ }
+ openBatchRenameModal() {
+ const activeFile = this.getActiveFile();
+ const modal = new ImageBatchRenameModal(
+ this.app,
+ activeFile,
+ (file, name) => __async(this, null, function* () {
+ yield this.renameFile(file, name, activeFile.path);
+ }),
+ () => {
+ this.modals.splice(this.modals.indexOf(modal), 1);
+ }
+ );
+ this.modals.push(modal);
+ modal.open();
+ }
+ batchRenameAllImages() {
+ return __async(this, null, function* () {
+ const activeFile = this.getActiveFile();
+ const fileCache = this.app.metadataCache.getFileCache(activeFile);
+ if (!fileCache || !fileCache.embeds)
+ return;
+ const extPatternRegex = /jpe?g|png|gif|tiff|webp/i;
+ for (const embed of fileCache.embeds) {
+ const file = this.app.metadataCache.getFirstLinkpathDest(embed.link, activeFile.path);
+ if (!file) {
+ console.warn("file not found", embed.link);
+ return;
+ }
+ const m0 = extPatternRegex.exec(file.extension);
+ if (!m0)
+ return;
+ const { newName, isMeaningful } = yield this.generateNewName(file, activeFile);
+ debugLog("generated newName:", newName, isMeaningful);
+ if (!isMeaningful) {
+ new import_obsidian2.Notice("Failed to batch rename images: the generated name is not meaningful");
+ break;
+ }
+ yield this.renameFile(file, newName, activeFile.path, false);
+ }
+ });
+ }
+ // returns a new name for the input file, with extension
+ generateNewName(file, activeFile) {
+ return __async(this, null, function* () {
+ let imageNameKey = "";
+ let firstHeading = "";
+ let frontmatter;
+ const fileCache = this.app.metadataCache.getFileCache(activeFile);
+ if (fileCache) {
+ debugLog("frontmatter", fileCache.frontmatter);
+ frontmatter = fileCache.frontmatter;
+ imageNameKey = (frontmatter == null ? void 0 : frontmatter.imageNameKey) || "";
+ firstHeading = getFirstHeading(fileCache.headings);
+ } else {
+ console.warn("could not get file cache from active file", activeFile.name);
+ }
+ let fileHash = "";
+ if (this.settings.imageNamePattern.includes("{{fileHash}}")) {
+ const buffer = yield this.app.vault.readBinary(file);
+ fileHash = yield sha256(buffer);
+ }
+ const stem = renderTemplate(
+ this.settings.imageNamePattern,
+ {
+ imageNameKey,
+ fileName: activeFile.basename,
+ dirName: activeFile.parent.name,
+ firstHeading,
+ fileHash
+ },
+ frontmatter
+ );
+ const meaninglessRegex = new RegExp(`[${this.settings.dupNumberDelimiter}\\s]`, "gm");
+ return {
+ stem,
+ newName: stem + "." + file.extension,
+ isMeaningful: stem.replace(meaninglessRegex, "") !== ""
+ };
+ });
+ }
+ // newName: foo.ext
+ deduplicateNewName(newName, file) {
+ return __async(this, null, function* () {
+ const dir = file.parent.path;
+ const listed = yield this.app.vault.adapter.list(dir);
+ debugLog("sibling files", listed);
+ const newNameExt = path.extension(newName), newNameStem = newName.slice(0, newName.length - newNameExt.length - 1), newNameStemEscaped = escapeRegExp(newNameStem), delimiter = this.settings.dupNumberDelimiter, delimiterEscaped = escapeRegExp(delimiter);
+ let dupNameRegex;
+ if (this.settings.dupNumberAtStart) {
+ dupNameRegex = new RegExp(
+ `^(?\\d+)${delimiterEscaped}(?${newNameStemEscaped})\\.${newNameExt}$`
+ );
+ } else {
+ dupNameRegex = new RegExp(
+ `^(?${newNameStemEscaped})${delimiterEscaped}(?\\d+)\\.${newNameExt}$`
+ );
+ }
+ debugLog("dupNameRegex", dupNameRegex);
+ const dupNameNumbers = [];
+ let isNewNameExist = false;
+ for (let sibling of listed.files) {
+ sibling = path.basename(sibling);
+ if (sibling == newName) {
+ isNewNameExist = true;
+ continue;
+ }
+ const m = dupNameRegex.exec(sibling);
+ if (!m)
+ continue;
+ dupNameNumbers.push(parseInt(m.groups.number));
+ }
+ if (isNewNameExist || this.settings.dupNumberAlways) {
+ const newNumber = dupNameNumbers.length > 0 ? Math.max(...dupNameNumbers) + 1 : 1;
+ if (this.settings.dupNumberAtStart) {
+ newName = `${newNumber}${delimiter}${newNameStem}.${newNameExt}`;
+ } else {
+ newName = `${newNameStem}${delimiter}${newNumber}.${newNameExt}`;
+ }
+ }
+ return {
+ name: newName,
+ stem: newName.slice(0, newName.length - newNameExt.length - 1),
+ extension: newNameExt
+ };
+ });
+ }
+ getActiveFile() {
+ const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView);
+ const file = view == null ? void 0 : view.file;
+ debugLog("active file", file == null ? void 0 : file.path);
+ return file;
+ }
+ getActiveEditor() {
+ const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView);
+ return view == null ? void 0 : view.editor;
+ }
+ onunload() {
+ this.modals.map((modal) => modal.close());
+ }
+ testExcludeExtension(file) {
+ const pattern = this.settings.excludeExtensionPattern;
+ if (!pattern)
+ return false;
+ return new RegExp(pattern).test(file.extension);
+ }
+ loadSettings() {
+ return __async(this, null, function* () {
+ this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
+ });
+ }
+ saveSettings() {
+ return __async(this, null, function* () {
+ yield this.saveData(this.settings);
+ });
+ }
+};
+function getFirstHeading(headings) {
+ if (headings && headings.length > 0) {
+ for (const heading of headings) {
+ if (heading.level === 1) {
+ return heading.heading;
+ }
+ }
+ }
+ return "";
+}
+function isPastedImage(file) {
+ if (file instanceof import_obsidian2.TFile) {
+ if (file.name.startsWith(PASTED_IMAGE_PREFIX)) {
+ return true;
+ }
+ }
+ return false;
+}
+function isMarkdownFile(file) {
+ if (file instanceof import_obsidian2.TFile) {
+ if (file.extension === "md") {
+ return true;
+ }
+ }
+ return false;
+}
+var ImageRenameModal = class extends import_obsidian2.Modal {
+ constructor(app, src, stem, renameFunc, onClose) {
+ super(app);
+ this.src = src;
+ this.stem = stem;
+ this.renameFunc = renameFunc;
+ this.onCloseExtra = onClose;
+ }
+ onOpen() {
+ this.containerEl.addClass("image-rename-modal");
+ const { contentEl, titleEl } = this;
+ titleEl.setText("Rename image");
+ const imageContainer = contentEl.createDiv({
+ cls: "image-container"
+ });
+ imageContainer.createEl("img", {
+ attr: {
+ src: this.app.vault.getResourcePath(this.src)
+ }
+ });
+ let stem = this.stem;
+ const ext = this.src.extension;
+ const getNewName = (stem2) => stem2 + "." + ext;
+ const getNewPath = (stem2) => path.join(this.src.parent.path, getNewName(stem2));
+ const infoET = createElementTree(contentEl, {
+ tag: "ul",
+ cls: "info",
+ children: [
+ {
+ tag: "li",
+ children: [
+ {
+ tag: "span",
+ text: "Origin path"
+ },
+ {
+ tag: "span",
+ text: this.src.path
+ }
+ ]
+ },
+ {
+ tag: "li",
+ children: [
+ {
+ tag: "span",
+ text: "New path"
+ },
+ {
+ tag: "span",
+ text: getNewPath(stem)
+ }
+ ]
+ }
+ ]
+ });
+ const doRename = () => __async(this, null, function* () {
+ debugLog("doRename", `stem=${stem}`);
+ this.renameFunc(getNewName(stem));
+ });
+ const nameSetting = new import_obsidian2.Setting(contentEl).setName("New name").setDesc("Please input the new name for the image (without extension)").addText((text) => text.setValue(stem).onChange(
+ (value) => __async(this, null, function* () {
+ stem = sanitizer.filename(value);
+ infoET.children[1].children[1].el.innerText = getNewPath(stem);
+ })
+ ));
+ const nameInputEl = nameSetting.controlEl.children[0];
+ nameInputEl.focus();
+ const nameInputState = lockInputMethodComposition(nameInputEl);
+ nameInputEl.addEventListener("keydown", (e) => __async(this, null, function* () {
+ if (e.key === "Enter" && !nameInputState.lock) {
+ e.preventDefault();
+ if (!stem) {
+ errorEl.innerText = 'Error: "New name" could not be empty';
+ errorEl.style.display = "block";
+ return;
+ }
+ doRename();
+ this.close();
+ }
+ }));
+ const errorEl = contentEl.createDiv({
+ cls: "error",
+ attr: {
+ style: "display: none;"
+ }
+ });
+ new import_obsidian2.Setting(contentEl).addButton((button) => {
+ button.setButtonText("Rename").onClick(() => {
+ doRename();
+ this.close();
+ });
+ }).addButton((button) => {
+ button.setButtonText("Cancel").onClick(() => {
+ this.close();
+ });
+ });
+ }
+ onClose() {
+ const { contentEl } = this;
+ contentEl.empty();
+ this.onCloseExtra();
+ }
+};
+var imageNamePatternDesc = `
+The pattern indicates how the new name should be generated.
+
+Available variables:
+- {{fileName}}: name of the active file, without ".md" extension.
+- {{imageNameKey}}: this variable is read from the markdown file's frontmatter, from the same key "imageNameKey".
+- {{fileHash}}: SHA-256 hash of the image file.
+- {{DATE:$FORMAT}}: use "$FORMAT" to format the current date, "$FORMAT" must be a Moment.js format string, e.g. {{DATE:YYYY-MM-DD}}.
+
+Here are some examples from pattern to image names (repeat in sequence), variables: fileName = "My note", imageNameKey = "foo":
+- {{fileName}}: My note, My note-1, My note-2
+- {{imageNameKey}}: foo, foo-1, foo-2
+- {{fileHash}}: , -1, -2
+- {{imageNameKey}}-{{DATE:YYYYMMDD}}: foo-20220408, foo-20220408-1, foo-20220408-2
+`;
+var SettingTab = class extends import_obsidian2.PluginSettingTab {
+ constructor(app, plugin) {
+ super(app, plugin);
+ this.plugin = plugin;
+ }
+ display() {
+ const { containerEl } = this;
+ containerEl.empty();
+ new import_obsidian2.Setting(containerEl).setName("Image name pattern").setDesc(imageNamePatternDesc).setClass("long-description-setting-item").addText((text) => text.setPlaceholder("{{imageNameKey}}").setValue(this.plugin.settings.imageNamePattern).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.imageNamePattern = value;
+ yield this.plugin.saveSettings();
+ })
+ ));
+ new import_obsidian2.Setting(containerEl).setName("Duplicate number at start (or end)").setDesc(`If enabled, duplicate number will be added at the start as prefix for the image name, otherwise it will be added at the end as suffix for the image name.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.dupNumberAtStart).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.dupNumberAtStart = value;
+ yield this.plugin.saveSettings();
+ })
+ ));
+ new import_obsidian2.Setting(containerEl).setName("Duplicate number delimiter").setDesc(`The delimiter to generate the number prefix/suffix for duplicated names. For example, if the value is "-", the suffix will be like "-1", "-2", "-3", and the prefix will be like "1-", "2-", "3-". Only characters that are valid in file names are allowed.`).addText((text) => text.setValue(this.plugin.settings.dupNumberDelimiter).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.dupNumberDelimiter = sanitizer.delimiter(value);
+ yield this.plugin.saveSettings();
+ })
+ ));
+ new import_obsidian2.Setting(containerEl).setName("Always add duplicate number").setDesc(`If enabled, duplicate number will always be added to the image name. Otherwise, it will only be added when the name is duplicated.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.dupNumberAlways).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.dupNumberAlways = value;
+ yield this.plugin.saveSettings();
+ })
+ ));
+ new import_obsidian2.Setting(containerEl).setName("Auto rename").setDesc(`By default, the rename modal will always be shown to confirm before renaming, if this option is set, the image will be auto renamed after pasting.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.autoRename).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.autoRename = value;
+ yield this.plugin.saveSettings();
+ })
+ ));
+ new import_obsidian2.Setting(containerEl).setName("Handle all attachments").setDesc(`By default, the plugin only handles images that starts with "Pasted image " in name,
+ which is the prefix Obsidian uses to create images from pasted content.
+ If this option is set, the plugin will handle all attachments that are created in the vault.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.handleAllAttachments).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.handleAllAttachments = value;
+ yield this.plugin.saveSettings();
+ })
+ ));
+ new import_obsidian2.Setting(containerEl).setName("Exclude extension pattern").setDesc(`This option is only useful when "Handle all attachments" is enabled.
+ Write a Regex pattern to exclude certain extensions from being handled. Only the first line will be used.`).setClass("single-line-textarea").addTextArea((text) => text.setPlaceholder("docx?|xlsx?|pptx?|zip|rar").setValue(this.plugin.settings.excludeExtensionPattern).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.excludeExtensionPattern = value;
+ yield this.plugin.saveSettings();
+ })
+ ));
+ new import_obsidian2.Setting(containerEl).setName("Disable rename notice").setDesc(`Turn off this option if you don't want to see the notice when renaming images.
+ Note that Obsidian may display a notice when a link has changed, this option cannot disable that.`).addToggle((toggle) => toggle.setValue(this.plugin.settings.disableRenameNotice).onChange(
+ (value) => __async(this, null, function* () {
+ this.plugin.settings.disableRenameNotice = value;
+ yield this.plugin.saveSettings();
+ })
+ ));
+ }
+};
+
+/* nosourcemap */
\ No newline at end of file
diff --git a/.obsidian/plugins/obsidian-paste-image-rename/manifest.json b/.obsidian/plugins/obsidian-paste-image-rename/manifest.json
new file mode 100644
index 0000000..0415e39
--- /dev/null
+++ b/.obsidian/plugins/obsidian-paste-image-rename/manifest.json
@@ -0,0 +1,10 @@
+{
+ "id": "obsidian-paste-image-rename",
+ "name": "Paste image rename (Edited)",
+ "version": "1.6.1",
+ "minAppVersion": "0.12.0",
+ "description": "Rename pasted images and all the other attchments added to the vault",
+ "author": "Reorx",
+ "authorUrl": "https://github.com/reorx",
+ "isDesktopOnly": false
+}
\ No newline at end of file
diff --git a/.obsidian/plugins/obsidian-paste-image-rename/styles.css b/.obsidian/plugins/obsidian-paste-image-rename/styles.css
new file mode 100644
index 0000000..d542d56
--- /dev/null
+++ b/.obsidian/plugins/obsidian-paste-image-rename/styles.css
@@ -0,0 +1,79 @@
+/* src/styles.css */
+:root {
+ --shadow-color: 0deg 0% 0%;
+ --shadow-elevation-medium:
+ 0.5px 0.5px 0.7px hsl(var(--shadow-color) / 0.14),
+ 1.1px 1.1px 1.5px -0.9px hsl(var(--shadow-color) / 0.12),
+ 2.4px 2.5px 3.3px -1.8px hsl(var(--shadow-color) / 0.1),
+ 5.3px 5.6px 7.3px -2.7px hsl(var(--shadow-color) / 0.09),
+ 11px 11.4px 15.1px -3.6px hsl(var(--shadow-color) / 0.07);
+}
+.image-rename-modal .modal {
+ width: 65%;
+ min-width: 600px;
+}
+.image-rename-modal .modal-content {
+ padding: 10px 5px;
+}
+.image-rename-modal .image-container {
+ display: flex;
+ justify-content: center;
+}
+.image-rename-modal .info {
+ padding: 10px 0;
+ color: var(--text-muted);
+ user-select: text;
+}
+.image-rename-modal .info li > span:nth-of-type(1) {
+ display: inline-block;
+ width: 6em;
+ margin-right: .5em;
+}
+.image-rename-modal .info li > span:nth-of-type(1):after {
+ content: ":";
+ float: right;
+}
+.image-rename-modal .image-container img {
+ display: block;
+ max-height: 300px;
+ box-shadow: var(--shadow-elevation-medium);
+}
+.image-rename-modal .setting-item-control input {
+ min-width: 300px;
+}
+.image-rename-modal .error {
+ border: 1px solid rgb(201, 90, 90);
+ color: rgb(134, 22, 22);
+ padding: 10px;
+}
+.image-rename-modal table {
+ font-size: .9em;
+ line-height: 1.8;
+ margin-bottom: 1.5em;
+ user-select: text;
+}
+.image-rename-modal table td {
+ padding-right: 1em;
+}
+.image-rename-modal table thead td {
+ font-weight: 700;
+}
+.image-rename-modal table tbody td .file-path {
+ font-size: .8em;
+ color: var(--text-faint);
+ line-height: 1;
+}
+.long-description-setting-item {
+ flex-wrap: wrap;
+}
+.long-description-setting-item .setting-item-description {
+ white-space: pre-wrap;
+ line-height: 1.3em;
+}
+.long-description-setting-item .setting-item-control {
+ padding-top: 10px;
+}
+.long-description-setting-item .setting-item-control input {
+ min-width: 300px;
+ width: 50%;
+}
diff --git a/.obsidian/plugins/slash/main.js b/.obsidian/plugins/slash/main.js
new file mode 100644
index 0000000..cd8128c
--- /dev/null
+++ b/.obsidian/plugins/slash/main.js
@@ -0,0 +1,65 @@
+const { Plugin } = require('obsidian');
+
+module.exports = class FixPublicLinksPlugin extends Plugin {
+ async onload() {
+ console.log('Loading Fix Public Links plugin');
+
+ // 监听文件创建事件(粘贴图片时触发)
+ this.registerEvent(
+ this.app.vault.on('create', (file) => {
+ // 延迟执行,确保 Obsidian 已经插入了链接
+ setTimeout(() => {
+ this.fixPublicLinksInActiveFile();
+ }, 100);
+ })
+ );
+
+ // 添加命令:手动修复当前文件的所有链接
+ this.addCommand({
+ id: 'fix-public-links',
+ name: 'Fix public/ links in current file',
+ editorCallback: (editor) => {
+ this.fixPublicLinksInEditor(editor);
+ }
+ });
+ }
+
+ fixPublicLinksInActiveFile() {
+ const activeView = this.app.workspace.getActiveViewOfType(require('obsidian').MarkdownView);
+ if (!activeView) return;
+
+ const editor = activeView.editor;
+ this.fixPublicLinksInEditor(editor);
+ }
+
+ fixPublicLinksInEditor(editor) {
+ const cursor = editor.getCursor();
+ const lineCount = editor.lineCount();
+ let fixed = false;
+
+ // 遍历所有行
+ for (let i = 0; i < lineCount; i++) {
+ const line = editor.getLine(i);
+
+ // 匹配 Markdown 图片语法:
+ const fixedLine = line.replace(/\]\(public\//g, '](/public/');
+
+ if (fixedLine !== line) {
+ editor.replaceRange(
+ fixedLine,
+ { line: i, ch: 0 },
+ { line: i, ch: line.length }
+ );
+ fixed = true;
+ }
+ }
+
+ if (fixed) {
+ console.log('Fixed public/ links in current file');
+ }
+ }
+
+ onunload() {
+ console.log('Unloading Fix Public Links plugin');
+ }
+};
diff --git a/.obsidian/plugins/slash/manifest.json b/.obsidian/plugins/slash/manifest.json
new file mode 100644
index 0000000..58822af
--- /dev/null
+++ b/.obsidian/plugins/slash/manifest.json
@@ -0,0 +1,10 @@
+{
+ "id": "fix-public-links",
+ "name": "Fix Public Links",
+ "version": "1.0.0",
+ "minAppVersion": "0.15.0",
+ "description": "Automatically fix image links that start with public/ to /public/",
+ "author": "AcoFork",
+ "authorUrl": "https://blog.acofork.com",
+ "isDesktopOnly": false
+}
\ No newline at end of file
diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json
new file mode 100644
index 0000000..9b87221
--- /dev/null
+++ b/.obsidian/workspace.json
@@ -0,0 +1,214 @@
+{
+ "main": {
+ "id": "16a573e2ddb25847",
+ "type": "split",
+ "children": [
+ {
+ "id": "ead2c6543d1842b7",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "e32607a73f296aa4",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "src/content/posts/codex-warp.md",
+ "mode": "source",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "codex-warp"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "vertical"
+ },
+ "left": {
+ "id": "b3b818b3595a168f",
+ "type": "split",
+ "children": [
+ {
+ "id": "f871c6ee02fda945",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "6719a883e9f34850",
+ "type": "leaf",
+ "state": {
+ "type": "file-explorer",
+ "state": {
+ "sortOrder": "alphabetical",
+ "autoReveal": false
+ },
+ "icon": "lucide-folder-closed",
+ "title": "文件列表"
+ }
+ },
+ {
+ "id": "8f2d88709f3efdd0",
+ "type": "leaf",
+ "state": {
+ "type": "search",
+ "state": {
+ "query": "",
+ "matchingCase": false,
+ "explainSearch": false,
+ "collapseAll": false,
+ "extraContext": false,
+ "sortOrder": "alphabetical"
+ },
+ "icon": "lucide-search",
+ "title": "搜索"
+ }
+ },
+ {
+ "id": "e4e31602ca6ef73f",
+ "type": "leaf",
+ "state": {
+ "type": "bookmarks",
+ "state": {},
+ "icon": "lucide-bookmark",
+ "title": "书签"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "width": 300
+ },
+ "right": {
+ "id": "0e5ea46bbde4f663",
+ "type": "split",
+ "children": [
+ {
+ "id": "9a2e329e85417358",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "cf180cd264a5895e",
+ "type": "leaf",
+ "state": {
+ "type": "backlink",
+ "state": {
+ "file": "src/content/posts/codex-warp.md",
+ "collapseAll": false,
+ "extraContext": false,
+ "sortOrder": "alphabetical",
+ "showSearch": false,
+ "searchQuery": "",
+ "backlinkCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-coming-in",
+ "title": "codex-warp 的反向链接列表"
+ }
+ },
+ {
+ "id": "20df57f8cd76fbac",
+ "type": "leaf",
+ "state": {
+ "type": "outgoing-link",
+ "state": {
+ "file": "src/content/posts/codex-warp.md",
+ "linksCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-going-out",
+ "title": "codex-warp 的出链列表"
+ }
+ },
+ {
+ "id": "fc6075104c1b1a76",
+ "type": "leaf",
+ "state": {
+ "type": "tag",
+ "state": {
+ "sortOrder": "frequency",
+ "useHierarchy": true,
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-tags",
+ "title": "标签"
+ }
+ },
+ {
+ "id": "f9fd6fcf70c8f5c5",
+ "type": "leaf",
+ "state": {
+ "type": "all-properties",
+ "state": {
+ "sortOrder": "frequency",
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-archive",
+ "title": "添加笔记属性"
+ }
+ },
+ {
+ "id": "41cb4f8448efc28f",
+ "type": "leaf",
+ "state": {
+ "type": "outline",
+ "state": {
+ "file": "src/content/posts/codex-warp.md",
+ "followCursor": false,
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-list",
+ "title": "codex-warp 的大纲"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "width": 300,
+ "collapsed": true
+ },
+ "left-ribbon": {
+ "hiddenItems": {
+ "switcher:打开快速切换": false,
+ "graph:查看关系图谱": false,
+ "canvas:新建白板": false,
+ "daily-notes:打开/创建今天的日记": false,
+ "templates:插入模板": false,
+ "command-palette:打开命令面板": false,
+ "bases:新建数据库": false
+ }
+ },
+ "active": "e32607a73f296aa4",
+ "lastOpenFiles": [
+ "public/pic/f1d08603a003c00e8664b4cb80b20358e9b9638bcedce5a307c91bb853b10eb7.png",
+ "public/pic/18489ae97bb1f883adb8a3b531fd820aa7e57140553a19291c8bf8d14be97afc-1.png",
+ "public/pic/18489ae97bb1f883adb8a3b531fd820aa7e57140553a19291c8bf8d14be97afc.png",
+ "public/pic/18489ae97bb1f883adb8a3b531fd820aa7e57140553a19291c8bf8d14be97afc-4.png",
+ "public/pic/18489ae97bb1f883adb8a3b531fd820aa7e57140553a19291c8bf8d14be97afc-3.png",
+ "public/pic/18489ae97bb1f883adb8a3b531fd820aa7e57140553a19291c8bf8d14be97afc-2.png",
+ "public/pic/122.png",
+ "public/pic/codex-warp-2.png",
+ "public/pic/codex-warp.png",
+ "public/pic/18489ae97bb1f883adb8a3b531fd820aa7e57140553a19291c8bf8d14be97afc-1.png",
+ "public/pic/18489ae97bb1f883adb8a3b531fd820aa7e57140553a19291c8bf8d14be97afc.png",
+ "public/pic",
+ "src/content/assets/images",
+ "src/content/assets",
+ "dist/pagefind/index/zh-cn_6473869.pf_index",
+ "dist/pagefind/index/zh-cn_52f3f92.pf_index",
+ "dist/pagefind/fragment/zh-cn_f061267.pf_fragment",
+ "dist/pagefind/fragment/zh-cn_de70424.pf_fragment",
+ "dist/pagefind/fragment/zh-cn_ddd5448.pf_fragment",
+ "dist/pagefind/fragment/zh-cn_dd6dfac.pf_fragment",
+ "dist/pagefind/fragment/zh-cn_c5d7eb4.pf_fragment",
+ "src/content/posts/codex-warp.md",
+ "src/content/posts/new-domain.md",
+ "dist/pic/Pasted image 20260420023402.png.md",
+ "public/pic/Pasted image 20260420023402.png.md"
+ ]
+}
\ No newline at end of file