/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/main.js var main_exports = {}; __export(main_exports, { default: () => main_default }); module.exports = __toCommonJS(main_exports); var import_obsidian4 = require("obsidian"); // src/modals.js var import_obsidian = require("obsidian"); var ReleaseNotesModal = class extends import_obsidian.Modal { constructor(app, plugin, version, releaseNotes2) { super(app); this.plugin = plugin; this.version = version; this.releaseNotes = releaseNotes2; } async onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h2", { text: `Welcome to \u{1F9B6} Rich Foot v${this.version}` }); contentEl.createEl("p", { text: "After each update you'll be prompted with the release notes. You can disable this in the plugin settings.", cls: "release-notes-instructions" }); const promotionalLinks = contentEl.createEl("div", { cls: "release-notes-promotional-links" }); const equilllabsLink = promotionalLinks.createEl("a", { href: "https://www.equilllabs.com", target: "_blank" }); equilllabsLink.createEl("img", { attr: { height: "36", style: "border:0px;height:36px;", src: "https://raw.githubusercontent.com/jparkerweb/pixel-banner/refs/heads/main/img/equilllabs.png?raw=true", border: "0", alt: "eQuill-Labs" } }); const discordLink = promotionalLinks.createEl("a", { href: "https://discord.gg/sp8AQQhMJ7", target: "_blank" }); discordLink.createEl("img", { attr: { height: "36", style: "border:0px;height:36px;", src: "https://raw.githubusercontent.com/jparkerweb/pixel-banner/refs/heads/main/img/discord.png?raw=true", border: "0", alt: "Discord" } }); const kofiLink = promotionalLinks.createEl("a", { href: "https://ko-fi.com/Z8Z212UMBI", target: "_blank" }); kofiLink.createEl("img", { attr: { height: "36", style: "border:0px;height:36px;", src: "https://raw.githubusercontent.com/jparkerweb/pixel-banner/refs/heads/main/img/support.png?raw=true", border: "0", alt: "Buy Me a Coffee at ko-fi.com" } }); const notesContainer = contentEl.createDiv("release-notes-container"); await import_obsidian.MarkdownRenderer.render( this.app, this.releaseNotes, notesContainer, "", this ); contentEl.createEl("div", { cls: "release-notes-spacer" }); new import_obsidian.Setting(contentEl).addButton((btn) => btn.setButtonText("Close").onClick(() => this.close())); } onClose() { const { contentEl } = this; contentEl.empty(); } }; // virtual-module:virtual:release-notes var releaseNotes = "

\u{1F680} New Feature

\n

v1.13.0

\n

\u2728 Adjustable Footer Width

\n\n

v1.12.0

\n

\u2728 Limit Links Shown

\n\n

\u{1F680} Code Refactoring

\n

v1.11.1

\n

\u{1F4E6} Update

\n\n

v1.11.0

\n

This release represents a complete architectural overhaul of the Rich Foot plugin, implementing modern best practices and significant performance improvements.

\n

\u2728 Performance Enhancements

\n\n

\u{1F3D7}\uFE0F Architecture Improvements

\n\n

\u{1F9F9} Cleanup & Stability

\n\n

\u{1F527} Compatibility

\n\n

\u{1F4CA} Code Quality

\n\n

\u{1F3A8} CSS Optimizations

\n\n

This update maintains 100% backwards compatibility with all existing settings and configurations while providing a more robust, performant foundation for future enhancements.

\n"; // src/settings.js var import_obsidian2 = require("obsidian"); // src/utils.js function rgbToHex(color) { if (color.startsWith("hsl")) { const temp = document.createElement("div"); temp.style.color = color; document.body.appendChild(temp); color = getComputedStyle(temp).color; document.body.removeChild(temp); } const rgb = color.match(/\d+/g); if (!rgb || rgb.length < 3) return "#000000"; const [r, g, b] = rgb.slice(0, 3).map((x) => { const val = Math.min(255, Math.max(0, Math.round(parseFloat(x)))); return val.toString(16).padStart(2, "0"); }); return `#${r}${g}${b}`; } function resolveCssVar(cssValue, property = "color") { const temp = document.createElement("div"); temp.style[property] = cssValue; document.body.appendChild(temp); const computed = getComputedStyle(temp)[property]; document.body.removeChild(temp); return rgbToHex(computed); } // src/settings.js var DEFAULT_SETTINGS = { borderWidth: 1, borderStyle: "dashed", borderOpacity: 1, borderRadius: 15, datesOpacity: 1, linksOpacity: 1, showReleaseNotes: true, excludedFolders: [], dateColor: "var(--text-accent)", borderColor: "var(--text-accent)", linkColor: "var(--link-color)", linkBackgroundColor: "var(--tag-background)", linkBorderColor: "rgba(255, 255, 255, 0.204)", customCreatedDateProp: "", customModifiedDateProp: "", dateDisplayFormat: "mmmm dd, yyyy", showBacklinks: true, showOutlinks: true, showDates: true, combineLinks: false, limitLinks: false, linksLimit: 10, footerWidth: "default", footerMaxWidth: 700, updateDelay: 3e3, excludedParentSelectors: [], frontmatterExclusionField: "" }; var RichFootSettingTab = class extends import_obsidian2.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; this.excludedParentSelectors = []; this.frontmatterExclusionField = ""; } display() { var _a, _b; const { containerEl } = this; containerEl.empty(); containerEl.addClass("rich-foot-settings"); containerEl.createEl("div", { cls: "rich-foot-info", text: "\u{1F9B6} Rich Foot adds a footer to your notes with useful information such as backlinks, creation date, and last modified date. Use the settings below to customize the appearance." }); containerEl.createEl("h3", { text: "Visibility Settings" }); new import_obsidian2.Setting(containerEl).setName("Show Backlinks").setDesc("Show backlinks in the footer").addToggle((toggle) => toggle.setValue(this.plugin.settings.showBacklinks).onChange(async (value) => { this.plugin.settings.showBacklinks = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); })); new import_obsidian2.Setting(containerEl).setName("Show Outlinks").setDesc("Show outgoing links in the footer").addToggle((toggle) => toggle.setValue(this.plugin.settings.showOutlinks).onChange(async (value) => { this.plugin.settings.showOutlinks = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); })); new import_obsidian2.Setting(containerEl).setName("Show Combine Links").setDesc("Show backlinks and outlinks in a single combined section (overrides show backlinks and show outlinks settings)").addToggle((toggle) => toggle.setValue(this.plugin.settings.combineLinks).onChange(async (value) => { this.plugin.settings.combineLinks = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); })); new import_obsidian2.Setting(containerEl).setName("Limit Links Shown").setDesc('Limit the number of backlinks/outlinks shown in the footer. Surplus links are hidden behind a "Show More" button that expands them on click.').addToggle((toggle) => toggle.setValue(this.plugin.settings.limitLinks).onChange(async (value) => { this.plugin.settings.limitLinks = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); })); let linksLimitInput; new import_obsidian2.Setting(containerEl).setName("Links Limit").setDesc('Maximum number of links to show before the "Show More" button (only applies when "Limit Links Shown" is enabled)').addText((text) => { linksLimitInput = text; text.setPlaceholder("10").setValue(String(this.plugin.settings.linksLimit)).onChange(async (value) => { const numValue = Math.floor(Number(value)); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.linksLimit = numValue; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); } }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.linksLimit = DEFAULT_SETTINGS.linksLimit; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); if (linksLimitInput) { linksLimitInput.setValue(String(DEFAULT_SETTINGS.linksLimit)); } })); let updateDelayInput; new import_obsidian2.Setting(containerEl).setName("Rich-foot update delay").setDesc("Delay in milliseconds before updating the rich-foot in edit mode (lower values may impact performance)").addText((text) => { updateDelayInput = text; text.setPlaceholder("3000").setValue(String(this.plugin.settings.updateDelay)).onChange(async (value) => { const numValue = Math.floor(Number(value)); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.updateDelay = numValue; await this.plugin.saveSettings(); } }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.updateDelay = DEFAULT_SETTINGS.updateDelay; await this.plugin.saveSettings(); if (updateDelayInput) { updateDelayInput.setValue(String(DEFAULT_SETTINGS.updateDelay)); } })); containerEl.createEl("hr"); containerEl.createEl("h3", { text: "Date Settings" }); new import_obsidian2.Setting(containerEl).setName("Show Dates").setDesc("Show creation and modification dates in the footer").addToggle((toggle) => toggle.setValue(this.plugin.settings.showDates).onChange(async (value) => { this.plugin.settings.showDates = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); })); new import_obsidian2.Setting(containerEl).setName("Date Display Format").setDesc("Choose how dates should be displayed in the footer").addDropdown((dropdown) => { const today = /* @__PURE__ */ new Date(); const formats = [ "mm/dd/yyyy", "dd/mm/yyyy", "yyyy-mm-dd", "mmm dd, yyyy", "dd mmm yyyy", "mmmm dd, yyyy", "ddd, mmm dd, yyyy", "dddd, mmmm dd, yyyy", "mm/dd/yy", "dd/mm/yy", "yy-mm-dd", "m/d/yy" ]; formats.forEach((format) => { const example = this.plugin.dataManager.formatDate(today, format); dropdown.addOption(format, `${format} (${example})`); }); dropdown.setValue(this.plugin.settings.dateDisplayFormat).onChange(async (value) => { this.plugin.settings.dateDisplayFormat = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }); new import_obsidian2.Setting(containerEl).setName("Custom Created Date Property").setDesc("Specify a frontmatter property to use for creation date (leave empty to use file creation date)").addText((text) => { text.setValue(this.plugin.settings.customCreatedDateProp).onChange(async (value) => { this.plugin.settings.customCreatedDateProp = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); this.createdDateInput = text; return text; }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.customCreatedDateProp = ""; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); this.createdDateInput.setValue(""); })); new import_obsidian2.Setting(containerEl).setName("Custom Modified Date Property").setDesc("Specify a frontmatter property to use for modification date (leave empty to use file modification date)").addText((text) => { text.setValue(this.plugin.settings.customModifiedDateProp).onChange(async (value) => { this.plugin.settings.customModifiedDateProp = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); this.modifiedDateInput = text; return text; }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.customModifiedDateProp = ""; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); this.modifiedDateInput.setValue(""); })); containerEl.createEl("hr"); containerEl.createEl("h3", { text: "Style Settings" }); let customWidthSetting; let footerMaxWidthSlider; const toggleCustomWidthVisibility = (value) => { if (customWidthSetting) { customWidthSetting.settingEl.style.display = value === "custom" ? "" : "none"; } }; new import_obsidian2.Setting(containerEl).setName("Footer Width").setDesc(`Control how wide the footer is. "Readable line length" locks the footer to Obsidian's readable line width (recommended when your notes use Readable Line Length), and "Custom" lets you set a maximum width in pixels. The footer never grows wider than the note, so it won't cause horizontal scrolling on narrow screens.`).addDropdown((dropdown) => { dropdown.addOptions({ "default": "Default (match content width)", "readable": "Readable line length", "custom": "Custom width (px)" }).setValue(this.plugin.settings.footerWidth).onChange(async (value) => { this.plugin.settings.footerWidth = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); toggleCustomWidthVisibility(value); }); }); let footerMaxWidthDebounce; customWidthSetting = new import_obsidian2.Setting(containerEl).setName("Custom Footer Width").setDesc("Maximum width of the footer in pixels (200-1200px)").addSlider((slider) => { footerMaxWidthSlider = slider; slider.setLimits(200, 1200, 10).setValue(this.plugin.settings.footerMaxWidth).setDynamicTooltip().onChange((value) => { this.plugin.settings.footerMaxWidth = value; document.documentElement.style.setProperty("--rich-foot-content-max-width", value + "px"); if (footerMaxWidthDebounce) clearTimeout(footerMaxWidthDebounce); footerMaxWidthDebounce = setTimeout(async () => { await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }, 300); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.footerMaxWidth = DEFAULT_SETTINGS.footerMaxWidth; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); footerMaxWidthSlider.setValue(DEFAULT_SETTINGS.footerMaxWidth); })); toggleCustomWidthVisibility(this.plugin.settings.footerWidth); let borderWidthSlider; new import_obsidian2.Setting(containerEl).setName("Border Width").setDesc("Adjust the width of the footer border (1-10px)").addSlider((slider) => { borderWidthSlider = slider; slider.setLimits(1, 10, 1).setValue(this.plugin.settings.borderWidth).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.borderWidth = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.borderWidth = DEFAULT_SETTINGS.borderWidth; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); borderWidthSlider.setValue(DEFAULT_SETTINGS.borderWidth); })); let borderStyleDropdown; new import_obsidian2.Setting(containerEl).setName("Border Style").setDesc("Choose the style of the footer border").addDropdown((dropdown) => { borderStyleDropdown = dropdown; dropdown.addOptions({ "solid": "Solid", "dashed": "Dashed", "dotted": "Dotted", "double": "Double", "groove": "Groove", "ridge": "Ridge", "inset": "Inset", "outset": "Outset" }).setValue(this.plugin.settings.borderStyle).onChange(async (value) => { this.plugin.settings.borderStyle = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.borderStyle = DEFAULT_SETTINGS.borderStyle; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); borderStyleDropdown.setValue(DEFAULT_SETTINGS.borderStyle); })); let borderOpacitySlider; new import_obsidian2.Setting(containerEl).setName("Border Opacity").setDesc("Adjust the opacity of the footer border (0-1)").addSlider((slider) => { borderOpacitySlider = slider; slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.borderOpacity).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.borderOpacity = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.borderOpacity = DEFAULT_SETTINGS.borderOpacity; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); borderOpacitySlider.setValue(DEFAULT_SETTINGS.borderOpacity); })); let borderColorPicker; new import_obsidian2.Setting(containerEl).setName("Border Color").setDesc("Choose the color for the footer border").addColorPicker((color) => { borderColorPicker = color; color.setValue(this.plugin.settings.borderColor.startsWith("var(--") ? resolveCssVar("var(--text-accent)", "borderColor") : this.plugin.settings.borderColor).onChange(async (value) => { this.plugin.settings.borderColor = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.borderColor = DEFAULT_SETTINGS.borderColor; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); borderColorPicker.setValue(resolveCssVar("var(--text-accent)", "borderColor")); })); let borderRadiusSlider; new import_obsidian2.Setting(containerEl).setName("Link Border Radius").setDesc("Adjust the border radius of Backlinks and Outlinks (0-15px)").addSlider((slider) => { borderRadiusSlider = slider; slider.setLimits(0, 15, 1).setValue(this.plugin.settings.borderRadius).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.borderRadius = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.borderRadius = DEFAULT_SETTINGS.borderRadius; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); borderRadiusSlider.setValue(DEFAULT_SETTINGS.borderRadius); })); let linksOpacitySlider; new import_obsidian2.Setting(containerEl).setName("Links Opacity").setDesc("Adjust the opacity of Backlinks and Outlinks (0-1)").addSlider((slider) => { linksOpacitySlider = slider; slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.linksOpacity).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.linksOpacity = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.linksOpacity = DEFAULT_SETTINGS.linksOpacity; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); linksOpacitySlider.setValue(DEFAULT_SETTINGS.linksOpacity); })); let linkColorPicker; new import_obsidian2.Setting(containerEl).setName("Link Text Color").setDesc("Choose the color for link text").addColorPicker((color) => { linkColorPicker = color; color.setValue(this.plugin.settings.linkColor.startsWith("var(--") ? resolveCssVar("var(--link-color)", "color") : this.plugin.settings.linkColor).onChange(async (value) => { this.plugin.settings.linkColor = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.linkColor = DEFAULT_SETTINGS.linkColor; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); linkColorPicker.setValue(resolveCssVar("var(--link-color)", "color")); })); let linkBackgroundColorPicker; new import_obsidian2.Setting(containerEl).setName("Link Background Color").setDesc("Choose the background color for links").addColorPicker((color) => { linkBackgroundColorPicker = color; color.setValue(this.plugin.settings.linkBackgroundColor.startsWith("var(--") ? resolveCssVar("var(--tag-background)", "backgroundColor") : this.plugin.settings.linkBackgroundColor).onChange(async (value) => { this.plugin.settings.linkBackgroundColor = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.linkBackgroundColor = DEFAULT_SETTINGS.linkBackgroundColor; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); linkBackgroundColorPicker.setValue(resolveCssVar("var(--tag-background)", "backgroundColor")); })); let linkBorderColorPicker; new import_obsidian2.Setting(containerEl).setName("Link Border Color").setDesc("Choose the border color for links").addColorPicker((color) => { linkBorderColorPicker = color; color.setValue(this.plugin.settings.linkBorderColor.startsWith("rgba(") ? resolveCssVar(this.plugin.settings.linkBorderColor, "borderColor") : this.plugin.settings.linkBorderColor).onChange(async (value) => { this.plugin.settings.linkBorderColor = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.linkBorderColor = DEFAULT_SETTINGS.linkBorderColor; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); linkBorderColorPicker.setValue(resolveCssVar(DEFAULT_SETTINGS.linkBorderColor, "borderColor")); })); let datesOpacitySlider; new import_obsidian2.Setting(containerEl).setName("Dates Opacity").setDesc("Adjust the opacity of the Created / Modified Dates (0-1)").addSlider((slider) => { datesOpacitySlider = slider; slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.datesOpacity).setDynamicTooltip().onChange(async (value) => { this.plugin.settings.datesOpacity = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.datesOpacity = DEFAULT_SETTINGS.datesOpacity; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); datesOpacitySlider.setValue(DEFAULT_SETTINGS.datesOpacity); })); let dateColorPicker; new import_obsidian2.Setting(containerEl).setName("Date Color").setDesc("Choose the color for Created / Modified Dates").addColorPicker((color) => { dateColorPicker = color; color.setValue(this.plugin.settings.dateColor.startsWith("var(--") ? resolveCssVar("var(--text-accent)", "color") : this.plugin.settings.dateColor).onChange(async (value) => { this.plugin.settings.dateColor = value; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.dateColor = DEFAULT_SETTINGS.dateColor; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); dateColorPicker.setValue(resolveCssVar("var(--text-accent)", "color")); })); containerEl.createEl("hr"); containerEl.createEl("h3", { text: "Exclusion Rules" }); containerEl.createEl("h3", { text: "Excluded Folders" }); containerEl.createEl("p", { text: "Notes in excluded folders (and their subfolders) will not display the Rich Foot footer. This is useful for system folders or areas where you don't want footer information to appear.", cls: "setting-item-description" }); const excludedFoldersContainer = containerEl.createDiv("excluded-folders-container"); if ((_a = this.plugin.settings) == null ? void 0 : _a.excludedFolders) { this.plugin.settings.excludedFolders.forEach((folder, index) => { const folderDiv = excludedFoldersContainer.createDiv("excluded-folder-item"); folderDiv.createSpan({ text: folder }); const deleteButton = folderDiv.createEl("button", { text: "Delete", cls: "excluded-folder-delete" }); deleteButton.addEventListener("click", async () => { this.plugin.settings.excludedFolders.splice(index, 1); await this.plugin.saveSettings(); this.display(); }); }); } const newFolderSetting = new import_obsidian2.Setting(containerEl).setName("Add excluded folder").setDesc("Enter a folder path or browse to select").addText((text) => text.setPlaceholder("folder/subfolder")).addButton((button) => button.setButtonText("Browse").onClick(async () => { const folder = await this.browseForFolder(); if (folder) { const textComponent = newFolderSetting.components[0]; textComponent.setValue(folder); } })).addButton((button) => button.setButtonText("Add").onClick(async () => { const textComponent = newFolderSetting.components[0]; const newFolder = textComponent.getValue().trim(); if (newFolder && !this.plugin.settings.excludedFolders.includes(newFolder)) { this.plugin.settings.excludedFolders.push(newFolder); await this.plugin.saveSettings(); textComponent.setValue(""); this.display(); } })); containerEl.createEl("h4", { text: "Exclude Rich Foot via Frontmatter" }); let frontmatterExclusionInput; new import_obsidian2.Setting(containerEl).setName("Frontmatter Exclusion Field").setDesc("If this frontmatter field exists and has a truthy value (true, yes, 1, on), Rich Foot will not be shown on that note").addText((text) => { frontmatterExclusionInput = text; text.setPlaceholder("e.g., exclude-rich-foot").setValue(this.plugin.settings.frontmatterExclusionField).onChange(async (value) => { this.plugin.settings.frontmatterExclusionField = value.trim(); await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); }); }).addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.frontmatterExclusionField = ""; await this.plugin.saveSettings(); await this.plugin.updateRichFoot(); if (frontmatterExclusionInput) { frontmatterExclusionInput.setValue(""); } })); containerEl.createEl("h4", { text: "Excluded Parent Selectors" }); containerEl.createEl("p", { text: "Rich Foot will not be added to notes that have any of these parent selectors in their DOM hierarchy. Useful for compatibility with other plugins.", cls: "setting-item-description" }); const excludedSelectorsContainer = containerEl.createDiv("excluded-selectors-container"); if ((_b = this.plugin.settings) == null ? void 0 : _b.excludedParentSelectors) { this.plugin.settings.excludedParentSelectors.forEach((selector, index) => { const selectorDiv = excludedSelectorsContainer.createDiv("excluded-selector-item"); selectorDiv.createSpan({ text: selector }); const deleteButton = selectorDiv.createEl("button", { text: "Delete", cls: "excluded-selector-delete" }); deleteButton.addEventListener("click", async () => { this.plugin.settings.excludedParentSelectors.splice(index, 1); await this.plugin.saveSettings(); this.display(); }); }); } const newSelectorSetting = new import_obsidian2.Setting(containerEl).setName("Add excluded parent selector").setDesc('Enter a CSS selector (e.g., .some-class, #some-id, [data-type="special"])').addText((text) => text.setPlaceholder("Enter selector").onChange(() => { try { document.querySelector(text.getValue()); text.inputEl.removeClass("rich-foot-input-error"); } catch (e) { text.inputEl.addClass("rich-foot-input-error"); } })).addButton((button) => button.setButtonText("Add").onClick(async () => { const textComponent = newSelectorSetting.components[0]; const newSelector = textComponent.getValue().trim(); if (!newSelector) return; try { document.querySelector(newSelector); } catch (e) { new import_obsidian2.Notice("Invalid CSS selector"); return; } if (!this.plugin.settings.excludedParentSelectors.includes(newSelector)) { this.plugin.settings.excludedParentSelectors.push(newSelector); await this.plugin.saveSettings(); textComponent.setValue(""); this.display(); } })); containerEl.createEl("h3", { text: "Example Screenshot", cls: "rich-foot-example-title" }); const exampleDiv = containerEl.createDiv({ cls: "rich-foot-example" }); exampleDiv.createEl("img", { attr: { src: "https://raw.githubusercontent.com/jparkerweb/rich-foot/refs/heads/main/rich-foot.jpg", alt: "Rich Foot Example" } }); new import_obsidian2.Setting(containerEl).setName("Show Release Notes").setDesc("Show release notes after plugin updates").addToggle((toggle) => toggle.setValue(this.plugin.settings.showReleaseNotes).onChange(async (value) => { this.plugin.settings.showReleaseNotes = value; await this.plugin.saveSettings(); })); new import_obsidian2.Setting(containerEl).setName("View Release Notes").setDesc("View release notes for the current version").addButton((button) => button.setButtonText("View Release Notes").onClick(async () => { const notes = await this.plugin.getReleaseNotes(this.plugin.manifest.version); new ReleaseNotesModal(this.app, this.plugin, this.plugin.manifest.version, notes).open(); })); } async browseForFolder() { const folders = this.app.vault.getAllLoadedFiles().filter((file) => file.children).map((folder) => folder.path); return new Promise((resolve) => { const modal = new FolderSuggestModal(this.app, folders, (result) => { resolve(result); }); modal.open(); }); } }; var FolderSuggestModal = class extends import_obsidian2.FuzzySuggestModal { constructor(app, folders, onChoose) { super(app); this.folders = folders; this.onChoose = onChoose; } getItems() { return this.folders; } getItemText(item) { return item; } onChooseItem(item, evt) { this.onChoose(item); } }; // src/data-manager.js var RichFootDataManager = class { constructor(app) { this.app = app; } /** * Get backlinks for a file * @param {TFile} file - The file to get backlinks for * @returns {Map} Map of backlink paths to their data */ getBacklinks(file) { const resolvedLinks = this.app.metadataCache.resolvedLinks; const backlinks = /* @__PURE__ */ new Map(); for (const sourcePath in resolvedLinks) { const targets = resolvedLinks[sourcePath]; if (targets[file.path]) { backlinks.set(sourcePath, targets[file.path]); } } return backlinks; } /** * Get all outlinks from a file (links, embeds, frontmatter, footnotes) * @param {TFile} file - The file to get outlinks from * @returns {Promise} Set of outlink paths */ async getOutlinks(file) { const cache = this.app.metadataCache.getFileCache(file); const links = /* @__PURE__ */ new Set(); if (cache == null ? void 0 : cache.links) { for (const link of cache.links) { this.addResolvedLink(link.link, file, links); } } if (cache == null ? void 0 : cache.embeds) { for (const embed of cache.embeds) { this.addResolvedLink(embed.link, file, links); } } if (cache == null ? void 0 : cache.frontmatterLinks) { for (const link of cache.frontmatterLinks) { this.addResolvedLink(link.link, file, links); } } if (cache == null ? void 0 : cache.blocks) { for (const block of Object.values(cache.blocks)) { if (block.type === "footnote") { this.extractWikiLinks(block.text, file, links); } } } const fileContent = await this.app.vault.cachedRead(file); this.processFootnotes(fileContent, file, links); return links; } /** * Add a resolved link to the set * @private */ addResolvedLink(linkText, sourceFile, linksSet) { const linkPath = linkText.split("#")[0]; const targetFile = this.app.metadataCache.getFirstLinkpathDest(linkPath, sourceFile.path); if (targetFile && targetFile.extension === "md") { linksSet.add(targetFile.path); } } /** * Extract wiki links from text * @private */ extractWikiLinks(text, sourceFile, linksSet) { const wikiLinkRegex = /\[\[(.*?)\]\]/g; let match; while ((match = wikiLinkRegex.exec(text)) !== null) { const linkText = match[1].trim(); this.addResolvedLink(linkText, sourceFile, linksSet); } } /** * Process footnotes from file content * @private */ processFootnotes(content, file, links) { const inlineFootnoteRegex = /\^\[((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]/g; const refFootnoteRegex = /\[\^[^\]]+\]:\s*((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)/g; let match; while ((match = inlineFootnoteRegex.exec(content)) !== null) { const footnoteContent = match[1]; this.extractWikiLinks(footnoteContent, file, links); } while ((match = refFootnoteRegex.exec(content)) !== null) { const footnoteContent = match[1]; this.extractWikiLinks(footnoteContent, file, links); } } /** * Get dates for a file (created and modified) * @param {TFile} file - The file to get dates for * @param {Object} settings - Plugin settings * @returns {Object} { created, modified } formatted dates */ getDates(file, settings) { const cache = this.app.metadataCache.getFileCache(file); const frontmatter = cache == null ? void 0 : cache.frontmatter; return { created: this.getFormattedDate( file, frontmatter, settings.customCreatedDateProp, "ctime", settings.dateDisplayFormat ), modified: this.getFormattedDate( file, frontmatter, settings.customModifiedDateProp, "mtime", settings.dateDisplayFormat ) }; } /** * Get a formatted date from frontmatter or file stats * @private */ getFormattedDate(file, frontmatter, customProp, statProp, format) { let dateValue; if (customProp && frontmatter && frontmatter[customProp]) { const parsed = this.parseDate(frontmatter[customProp]); if (parsed) { dateValue = parsed; } } if (!dateValue) { dateValue = new Date(file.stat[statProp]); } return this.formatDate(dateValue, format); } /** * Parse a date string with multiple format attempts * @param {string} value - Date string to parse * @returns {Date|null} Parsed date or null if invalid */ parseDate(value) { if (!value) return null; let tempDate = String(value); if (!isNaN(Date.parse(tempDate))) { return this.createDateWithTime(tempDate); } tempDate = this.replaceNTimes(String(value), /\./g, "-", 2); if (!isNaN(Date.parse(tempDate))) { return this.createDateWithTime(tempDate); } tempDate = this.replaceNTimes(String(value), /\//g, "-", 2); if (!isNaN(Date.parse(tempDate))) { return this.createDateWithTime(tempDate); } return null; } /** * Replace pattern N times in a string * @private */ replaceNTimes(str, pattern, replacement, times) { let count = 0; return str.replace(pattern, (match) => { count++; return count <= times ? replacement : match; }); } /** * Create date object and add midnight time if no time component exists * @private */ createDateWithTime(dateStr) { let tempDate = dateStr; if (!tempDate.includes("T") && !tempDate.includes(" ")) { tempDate = `${tempDate}T00:00:00`; } return new Date(tempDate); } /** * Format a date according to the specified format * @param {Date} date - Date to format * @param {string} format - Format string * @returns {string} Formatted date string */ formatDate(date, format) { const d = new Date(date); const year = d.getFullYear(); const month = d.getMonth(); const day = d.getDate(); const weekday = d.getDay(); const months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; const monthsShort = months.map((m) => m.slice(0, 3)); const weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const weekdaysShort = weekdays.map((w) => w.slice(0, 3)); const pad = (num) => num.toString().padStart(2, "0"); const tokens = { "dddd": weekdays[weekday], "ddd": weekdaysShort[weekday], "dd": pad(day), "d": day.toString(), "mmmm": months[month], "mmm": monthsShort[month], "mm": pad(month + 1), "m": (month + 1).toString(), "yyyy": year.toString(), "yy": year.toString().slice(-2) }; const sortedTokens = Object.keys(tokens).sort((a, b) => b.length - a.length); let result = format.toLowerCase(); const replacements = /* @__PURE__ */ new Map(); sortedTokens.forEach((token, index) => { const placeholder = `__${index}__`; replacements.set(placeholder, tokens[token]); result = result.replace(new RegExp(token, "gi"), placeholder); }); replacements.forEach((value, placeholder) => { result = result.replace(new RegExp(placeholder, "g"), value); }); return result; } }; // src/renderer.js var import_obsidian3 = require("obsidian"); var showMoreIdCounter = 0; var RichFootRenderer = class { constructor(plugin) { this.plugin = plugin; } /** * Create the complete footer element for a file * @param {TFile} file - The file to create footer for * @param {Object} data - Pre-fetched data { backlinks, outlinks, dates } * @returns {HTMLElement} The footer element */ createFooter(file, data) { const { backlinks, outlinks, dates } = data; const { settings } = this.plugin; const richFoot = createDiv({ cls: "rich-foot rich-foot--hidden" }); richFoot.setAttribute("data-rich-foot", "true"); richFoot.setAttribute("data-file-path", file.path); richFoot.createDiv({ cls: "rich-foot--dashed-line" }); if (settings.combineLinks) { this.createCombinedLinksSection(richFoot, file, backlinks, outlinks); } else { if (settings.showBacklinks) { this.createLinksSection(richFoot, file, backlinks, "backlinks"); } if (settings.showOutlinks) { this.createLinksSection(richFoot, file, outlinks, "outlinks"); } } if (settings.showDates && dates) { this.createDatesSection(richFoot, dates); } return richFoot; } /** * Create combined links section (backlinks + outlinks) * @private */ createCombinedLinksSection(container, file, backlinks, outlinks) { if (backlinks.size === 0 && outlinks.size === 0) return; const linksDiv = container.createDiv({ cls: "rich-foot--links" }); const linksUl = linksDiv.createEl("ul"); const processedLinks = /* @__PURE__ */ new Set(); for (const [linkPath] of backlinks) { if (!linkPath.endsWith(".md")) continue; processedLinks.add(linkPath); const metadata = { isBacklink: true, isOutlink: outlinks.has(linkPath) }; const li = linksUl.createEl("li"); this.createLinkElement(li, file, linkPath, metadata); } for (const linkPath of outlinks) { if (processedLinks.has(linkPath)) continue; const metadata = { isBacklink: false, isOutlink: true }; const li = linksUl.createEl("li"); this.createLinkElement(li, file, linkPath, metadata); } if (linksUl.childElementCount === 0) { linksDiv.remove(); return; } this.applyLinkLimit(linksUl); } /** * Create links section (backlinks or outlinks) * @private */ createLinksSection(container, file, links, type) { const linksArray = type === "backlinks" ? Array.from(links.keys()).filter((path) => path.endsWith(".md")) : Array.from(links); if (linksArray.length === 0) return; const className = `rich-foot--${type}`; const linksDiv = container.createDiv({ cls: className }); const linksUl = linksDiv.createEl("ul"); for (const linkPath of linksArray) { const li = linksUl.createEl("li"); const metadata = { isBacklink: type === "backlinks", isOutlink: type === "outlinks" }; this.createLinkElement(li, file, linkPath, metadata); } if (linksUl.childElementCount === 0) { linksDiv.remove(); return; } this.applyLinkLimit(linksUl); } /** * Limit the number of visible links, hiding the surplus behind a * toggleable "Show More (X)" button. * @private * @param {HTMLElement} linksUl - The