From 4ced15da02651ec011465e266a4a069c4ffcc863 Mon Sep 17 00:00:00 2001 From: Nathan Price Date: Thu, 16 Jan 2025 21:54:13 -0500 Subject: [PATCH] vault backup: 2025-01-16 21:54:13 --- .obsidian/community-plugins.json | 3 +- .../plugins/obsidian-auto-link-title/main.js | 771 ++++++++++++++++++ .../obsidian-auto-link-title/manifest.json | 10 + .../obsidian-auto-link-title/styles.css | 1 + .../quartz-docker/environment variables.md | 70 +- 5 files changed, 817 insertions(+), 38 deletions(-) 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 diff --git a/.obsidian/community-plugins.json b/.obsidian/community-plugins.json index 33a07c6..7d4b3f4 100644 --- a/.obsidian/community-plugins.json +++ b/.obsidian/community-plugins.json @@ -3,5 +3,6 @@ "obsidian-git", "obsidian-linter", "editing-toolbar", - "copilot" + "copilot", + "obsidian-auto-link-title" ] \ 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..a6f8910 --- /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 <title> 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 <title> 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..66ad205 --- /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/Applications/quartz-docker/environment variables.md b/Applications/quartz-docker/environment variables.md index e97a14c..b5b2854 100644 --- a/Applications/quartz-docker/environment variables.md +++ b/Applications/quartz-docker/environment variables.md @@ -3,41 +3,37 @@ title: "CHANGEME" draft: false date: 2025-01-16 --- - -| Variable | Default | Purpose | -| ------------- | ------- | ------------------------------------- | -| `NGINX_PORT` | 8080 | Port on which the NGINX server is run | -| `ENABLE_CRON` | false | | - - BUILD_SCHEDULE: "*/10 * * * *" - PAGE_TITLE: "Quartz Docker" - ENABLE_SPA: "true" - ENABLE_POPOVERS: "true" - ANALYTICS_PROVIDER: "plausible" - BASE_URL: "localhost" - IGNORE_PATTERNS: "private,templates" - TYPOGRAPHY_HEADER: "Schibsted Grotesk" - TYPOGRAPHY_BODY: "Source Sans Pro" - TYPOGRAPHY_CODE: "IBM-Plex Mono" - - # Light Mode Colors - LIGHTMODE_LIGHT: "#faf8f8" - LIGHTMODE_LIGHTGRAY: "#e5e5e5" - LIGHTMODE_GRAY: "#bbbbbb" - LIGHTMODE_DARKGRAY: "#4e4e4e" - LIGHTMODE_DARK: "#2b2b2b" - LIGHTMODE_SECONDARY: "#284b63" - LIGHTMODE_TERTIARY: "#84a59d" - LIGHTMODE_HIGHLIGHT: "rgba(143,159,169,0.15)" - - # Dark Mode Colors - DARKMODE_LIGHT: "#161618" - DARKMODE_LIGHTGRAY: "#393639" - DARKMODE_GRAY: "#646464" - DARKMODE_DARKGRAY: "#4d4d4d" - DARKMODE_DARK: "#ebebec" - DARKMODE_SECONDARY: "#7b97aa" - DARKMODE_TERTIARY: "#84a59d" - DARKMODE_HIGHLIGHT: "rgba(143,159,169,0.15)" - +[Configuration](https://quartz.jzhao.xyz/configuration) options that are configurable via environment variable. +|**Variable**|**Default**|**Purpose**| +|---|---|---| +|`NGINX_PORT`|`8080`|Port on which the NGINX server runs| +|`ENABLE_CRON`|`false`|Enable or disable the cron job| +|`BUILD_SCHEDULE`|`*/10 * * * *`|Cron schedule for running `git pull` and Quartz build| +|`PAGE_TITLE`|`"Quartz Docker"`|Title of the Quartz-generated website| +|`ENABLE_SPA`|`"true"`|Enable Single Page Application (SPA) mode| +|`ENABLE_POPOVERS`|`"true"`|Enable popovers for additional UI interactions| +|`ANALYTICS_PROVIDER`|`"plausible"`|Analytics provider for tracking usage| +|`BASE_URL`|`"localhost"`|Base URL for the Quartz site| +|`IGNORE_PATTERNS`|`"private,templates"`|Files or directories to ignore in the Quartz build| +|`TYPOGRAPHY_HEADER`|`"Schibsted Grotesk"`|Font for headers| +|`TYPOGRAPHY_BODY`|`"Source Sans Pro"`|Font for body text| +|`TYPOGRAPHY_CODE`|`"IBM-Plex Mono"`|Font for code blocks| +|**Light Mode Colors**||| +|`LIGHTMODE_LIGHT`|`"#faf8f8"`|Background color for light mode| +|`LIGHTMODE_LIGHTGRAY`|`"#e5e5e5"`|Light gray color for light mode| +|`LIGHTMODE_GRAY`|`"#bbbbbb"`|Gray color for light mode| +|`LIGHTMODE_DARKGRAY`|`"#4e4e4e"`|Dark gray color for light mode| +|`LIGHTMODE_DARK`|`"#2b2b2b"`|Darkest color for light mode| +|`LIGHTMODE_SECONDARY`|`"#284b63"`|Secondary accent color for light mode| +|`LIGHTMODE_TERTIARY`|`"#84a59d"`|Tertiary accent color for light mode| +|`LIGHTMODE_HIGHLIGHT`|`"rgba(143,159,169,0.15)"`|Highlight color for light mode| +|**Dark Mode Colors**||| +|`DARKMODE_LIGHT`|`"#161618"`|Background color for dark mode| +|`DARKMODE_LIGHTGRAY`|`"#393639"`|Light gray color for dark mode| +|`DARKMODE_GRAY`|`"#646464"`|Gray color for dark mode| +|`DARKMODE_DARKGRAY`|`"#4d4d4d"`|Dark gray color for dark mode| +|`DARKMODE_DARK`|`"#ebebec"`|Lightest color for dark mode| +|`DARKMODE_SECONDARY`|`"#7b97aa"`|Secondary accent color for dark mode| +|`DARKMODE_TERTIARY`|`"#84a59d"`|Tertiary accent color for dark mode| +|`DARKMODE_HIGHLIGHT`|`"rgba(143,159,169,0.15)"`|Highlight color for dark mode|