Files
cours/.obsidian/plugins/supercharged-links-obsidian/main.js

1437 lines
61 KiB
JavaScript

/*
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');
var state = require('@codemirror/state');
var view = require('@codemirror/view');
var language = require('@codemirror/language');
/******************************************************************************
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 matchTypes = {
'exact': "Exact match",
'contains': "Contains value",
'whiteSpace': "Value within whitespace separated words",
'startswith': "Starts with this value",
'endswith': "Ends with this value"
};
const matchSign = {
'exact': "",
'contains': "*",
'startswith': "^",
'endswith': "$",
'whiteSpace': "~"
};
const matchPreview = {
'exact': "with value",
'contains': "containing",
'whiteSpace': "containing",
'startswith': "starting with",
'endswith': "ending with"
};
const matchPreviewPath = {
'exact': "is",
'contains': "contains",
'whiteSpace': "contains",
'startswith': "starts with",
'endswith': "ends with"
};
const selectorType = {
'attribute': 'Attribute value',
'tag': 'Tag',
'path': 'Note path'
};
class CSSLink {
constructor() {
this.type = 'attribute';
this.name = "";
this.value = "";
this.matchCaseSensitive = false;
this.match = "exact";
let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
//return id of format 'aaaaaaaa'-'aaaa'-'aaaa'-'aaaa'-'aaaaaaaaaaaa'
this.uid = s4() + "-" + s4();
this.selectText = true;
this.selectAppend = true;
this.selectPrepend = true;
this.selectBackground = true;
}
}
function displayText(link, settings) {
if (link.type === 'tag') {
if (!link.value) {
return "<b>Please choose a tag</b>";
}
return `<span class="data-link-icon data-link-text data-link-icon-after" data-link-tags="${link.value}">Note</span> has tag <a class="tag">#${link.value}</a>`;
}
else if (link.type === 'attribute') {
if (settings.targetAttributes.length === 0) {
return `<b>No attributes added to "Target attributes". Go to plugin settings to add them.</b>`;
}
if (!link.name) {
return "<b>Please choose an attribute name.</b>";
}
if (!link.value) {
return "<b>Please choose an attribute value.</b>";
}
return `<span class="data-link-icon data-link-text data-link-icon-after" data-link-${link.name}="${link.value}">Note</span> has attribute <b>${link.name}</b> ${matchPreview[link.match]} <b>${link.value}</b>.`;
}
if (!link.value) {
return "<b>Please choose a path.</b>";
}
return `The path of the <span class="data-link-icon data-link-text data-link-icon-after" data-link-path="${link.value}">note</span> ${matchPreviewPath[link.match]} <b>${link.value}</b>`;
}
function updateDisplay(textArea, link, settings) {
let toDisplay = displayText(link, settings);
let disabled = false;
if (link.type === 'tag') {
if (!link.value) {
disabled = true;
}
}
else if (link.type === 'attribute') {
if (settings.targetAttributes.length === 0) {
disabled = true;
}
else if (!link.name) {
disabled = true;
}
else if (!link.value) {
disabled = true;
}
}
else {
if (!link.value) {
disabled = true;
}
}
textArea.innerHTML = toDisplay;
return disabled;
}
class CSSBuilderModal extends obsidian.Modal {
constructor(plugin, saveCallback, cssLink = null) {
super(plugin.app);
this.cssLink = cssLink;
if (!cssLink) {
this.cssLink = new CSSLink();
}
this.plugin = plugin;
this.saveCallback = saveCallback;
}
onOpen() {
this.titleEl.setText(`Select what links to style!`);
// is tag
const matchAttrPlaceholder = "Attribute value to match.";
const matchTagPlaceholder = "Note tag to match (without #).";
const matchPathPlaceholder = "File path to match.";
const matchAttrTxt = "Attribute value";
const matchTagTxt = "Tag";
const matchPathTxt = "Path";
const cssLink = this.cssLink;
const plugin = this.plugin;
this.contentEl.addClass("supercharged-modal");
// Type
new obsidian.Setting(this.contentEl)
.setName("Type of selector")
.setDesc("Attributes selects YAML and DataView attributes" +
", tags chooses the tags of a note, and path considers the name of the note including in what folder it is.")
.addDropdown(dc => {
Object.keys(selectorType).forEach((type) => {
dc.addOption(type, selectorType[type]);
if (type === this.cssLink.type) {
dc.setValue(type);
}
});
dc.onChange((type) => {
cssLink.type = type;
updateContainer(cssLink.type);
saveButton.setDisabled(updateDisplay(preview, this.cssLink, this.plugin.settings));
});
});
// attribute name
const attrName = new obsidian.Setting(this.contentEl)
.setName("Attribute name")
.setDesc("What attribute to target? Make sure to first add target attributes to the settings at the top!")
.addDropdown(dc => {
plugin.settings.targetAttributes.forEach((attribute) => {
dc.addOption(attribute, attribute);
if (attribute === cssLink.name) {
dc.setValue(attribute);
}
});
dc.onChange(name => {
cssLink.name = name;
saveButton.setDisabled(updateDisplay(preview, cssLink, plugin.settings));
});
});
// attribute value
const attrValue = new obsidian.Setting(this.contentEl)
.setName("Value to match")
.setDesc("TODO")
.addText(t => {
t.setValue(cssLink.value);
t.onChange(value => {
cssLink.value = value;
saveButton.setDisabled(updateDisplay(preview, cssLink, plugin.settings));
});
});
this.contentEl.createEl('h4', { text: 'Advanced' });
// matching type
const matchingType = new obsidian.Setting(this.contentEl)
.setName("Matching type")
.setDesc("How to compare the attribute or path with the given value.")
.addDropdown(dc => {
Object.keys(matchTypes).forEach((key) => {
dc.addOption(key, matchTypes[key]);
if (key == cssLink.match) {
dc.setValue(key);
}
});
dc.onChange((value) => {
cssLink.match = value;
saveButton.setDisabled(updateDisplay(preview, cssLink, plugin.settings));
});
});
// case sensitive
const caseSensitiveTogglerContainer = new obsidian.Setting(this.contentEl)
.setName("Case sensitive matching")
.setDesc("Should the matching of the value be case sensitive?")
.addToggle(b => {
b.setValue(cssLink.matchCaseSensitive);
b.onChange(value => {
cssLink.matchCaseSensitive = value;
b.setDisabled(updateDisplay(preview, cssLink, plugin.settings));
});
});
if (!this.cssLink.name && this.plugin.settings.targetAttributes.length > 0) {
this.cssLink.name = this.plugin.settings.targetAttributes[0];
}
const updateContainer = function (type) {
if (type === 'attribute') {
attrName.settingEl.show();
attrValue.nameEl.setText(matchAttrTxt);
attrValue.descEl.setText(matchAttrPlaceholder);
matchingType.settingEl.show();
caseSensitiveTogglerContainer.settingEl.show();
}
else if (type === 'tag') {
attrName.settingEl.hide();
attrValue.nameEl.setText(matchTagTxt);
attrValue.descEl.setText(matchTagPlaceholder);
matchingType.settingEl.hide();
caseSensitiveTogglerContainer.settingEl.hide();
}
else {
attrName.settingEl.hide();
attrValue.nameEl.setText(matchPathTxt);
attrValue.descEl.setText(matchPathPlaceholder);
matchingType.settingEl.show();
caseSensitiveTogglerContainer.settingEl.show();
}
};
new obsidian.Setting(this.contentEl)
.setName("Style options")
.setDesc("What styling options are active? " +
"Disabling options you won't use can improve performance slightly.")
.addToggle(t => {
t.onChange(value => {
cssLink.selectText = value;
});
t.setValue(cssLink.selectText);
t.setTooltip("Style link text");
})
.addToggle(t => {
t.onChange(value => {
cssLink.selectPrepend = value;
});
t.setValue(cssLink.selectPrepend);
t.setTooltip("Add content before link");
})
.addToggle(t => {
t.onChange(value => {
cssLink.selectAppend = value;
});
t.setValue(cssLink.selectAppend);
t.setTooltip("Add content after link");
})
.addToggle(t => {
t.onChange(value => {
cssLink.selectBackground = value;
});
t.setValue(cssLink.selectBackground);
t.setTooltip("Add optional background or underline to link");
});
this.contentEl.createEl('h4', { text: 'Result' });
const modal = this;
const saveButton = new obsidian.Setting(this.contentEl)
.setName("Preview")
.setDesc("")
.addButton(b => {
b.setButtonText("Save");
b.onClick(() => {
modal.saveCallback(cssLink);
modal.close();
});
});
// generate button
const preview = saveButton.nameEl;
updateContainer(cssLink.type);
saveButton.setDisabled(updateDisplay(preview, this.cssLink, this.plugin.settings));
}
}
const colorSet = [[
'#0089BA',
'#2C73D2',
'#008E9B',
'#0081CF',
'#008F7A',
'#008E9B',
], [
'#D65DB1',
'#0082C1',
'#9270D3',
'#007F93',
'#007ED9',
'#007660',
], [
'#FF9671',
'#A36AAA',
'#F27D88',
'#6967A9',
'#D26F9D',
'#1b6299',
], [
'#FFC75F',
'#4C9A52',
'#C3BB4E',
'#00855B',
'#88AC4B',
'#006F61',
], [
'#FF6F91',
'#6F7F22',
'#E07250',
'#257A3E',
'#AC7C26',
'#006F5F',
], [
'#d9d867',
'#2FAB63',
'#B8E067',
'#008E63',
'#78C664',
'#007160',
]];
const colors = [];
for (const i of Array(6).keys()) {
for (const j of Array(6).keys()) {
colors.push(colorSet[j][i]);
}
}
function hash(uid) {
let hash = 0;
for (let i = 0; i < uid.length; i++) {
const char = uid.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
hash = Math.abs(hash);
return hash;
}
function buildCSS(selectors, plugin) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const instructions = [
"/* WARNING: This file will be overwritten by the plugin.",
"Do not edit this file directly! First copy this file and rename it if you want to edit things. */",
"",
":root {"
];
selectors.forEach((selector, i) => {
if (selector.selectText) {
instructions.push(` --${selector.uid}-color: ${colors[hash(selector.uid) % 36]};`);
instructions.push(` --${selector.uid}-weight: initial;`);
}
if (selector.selectPrepend) {
instructions.push(` --${selector.uid}-before: '';`);
}
if (selector.selectAppend) {
instructions.push(` --${selector.uid}-after: '';`);
}
if (selector.selectBackground) {
instructions.push(` --${selector.uid}-background-color: #ffffff;`);
instructions.push(` --${selector.uid}-decoration: initial;`);
}
});
instructions.push("}");
selectors.forEach(selector => {
let cssSelector;
if (selector.type === 'attribute') {
cssSelector = `[data-link-${selector.name}${matchSign[selector.match]}="${selector.value}" ${selector.matchCaseSensitive ? "" : " i"}]`;
}
else if (selector.type === 'tag') {
cssSelector = `[data-link-tags*="${selector.value}" i]`;
}
else {
cssSelector = `[data-link-path${matchSign[selector.match]}="${selector.value}" ${selector.matchCaseSensitive ? "" : "i"}]`;
}
if (selector.selectText) {
instructions.push(...[
"",
`div[data-id="${selector.uid}"] div.setting-item-description,`,
cssSelector + " {",
` color: var(--${selector.uid}-color) !important;`,
` font-weight: var(--${selector.uid}-weight);`,
"}"
]);
}
if (selector.selectBackground) {
instructions.push(...["",
`.c-${selector.uid}-use-background div[data-id="${selector.uid}"] div.setting-item-description,`,
`.c-${selector.uid}-use-background .data-link-text${cssSelector} {`,
` background-color: var(--${selector.uid}-background-color) !important;`,
` border-radius: 5px;`,
` padding-left: 2px;`,
` padding-right: 2px;`,
` text-decoration: var(--${selector.uid}-decoration) !important;`,
"}"]);
}
if (selector.selectPrepend) {
instructions.push(...["",
`div[data-id="${selector.uid}"] div.setting-item-description::before,`,
`.data-link-icon${cssSelector}::before {`,
` content: var(--${selector.uid}-before);`,
"}"]);
}
if (selector.selectAppend) {
instructions.push(...["",
`div[data-id="${selector.uid}"] div.setting-item-description::after,`,
`.data-link-icon-after${cssSelector}::after {`,
` content: var(--${selector.uid}-after);`,
"}"]);
}
});
instructions.push(...[
"/* @settings",
"name: Supercharged Links",
"id: supercharged-links",
"settings:",
]);
selectors.forEach((selector, i) => {
let name = selector.name;
let value = selector.value;
if (selector.type === 'tag') {
name = 'tag';
// value = "\#" + value;
}
else if (selector.type === 'path') {
name = 'path';
}
instructions.push(...[
" - ",
` id: ${selector.uid}`,
` title: ${name} is ${value}`,
` description: Example note`,
" type: heading",
" collapsed: true",
" level: 3"
]);
if (selector.selectText) {
instructions.push(...[
" - ",
` id: ${selector.uid}-color`,
` title: Link color`,
" type: variable-color",
" format: hex",
` default: '${colors[hash(selector.uid) % 36]}'`,
" - ",
` id: ${selector.uid}-weight`,
` title: Font weight`,
" type: variable-select",
` default: initial`,
` options:`,
` - initial`,
` - lighter`,
` - normal`,
` - bold`,
` - bolder`,
" - ",
` id: ${selector.uid}-decoration`,
` title: Font decoration`,
" type: variable-select",
` default: initial`,
` options:`,
` - initial`,
` - underline`,
` - overline`,
` - line-through`
]);
}
if (selector.selectPrepend) {
instructions.push(...[" - ",
` id: ${selector.uid}-before`,
` title: Prepend text`,
` description: Add some text, such as an emoji, before the links.`,
" type: variable-text",
` default: ''`,
` quotes: true`]);
}
if (selector.selectAppend) {
instructions.push(...[" - ",
` id: ${selector.uid}-after`,
` title: Append text`,
` description: Add some text, such as an emoji, after the links.`,
" type: variable-text",
` default: ''`,
` quotes: true`]);
}
if (selector.selectBackground) {
instructions.push(...[" - ",
` id: c-${selector.uid}-use-background`,
` title: Use background color`,
` description: Adds a background color to the link. This can look buggy in live preview.`,
" type: class-toggle",
" - ",
` id: ${selector.uid}-background-color`,
` title: Background color`,
" type: variable-color",
" format: hex",
` default: '#ffffff'`]);
}
});
instructions.push("*/");
const vault = plugin.app.vault;
const configDir = (_a = vault.configDir) !== null && _a !== void 0 ? _a : ".obsidian";
const pathDir = configDir + "/snippets";
yield vault.adapter.mkdir(pathDir);
const path = pathDir + "/supercharged-links-gen.css";
if (yield vault.adapter.exists(path)) {
yield vault.adapter.remove(path);
}
yield plugin.app.vault.create(path, instructions.join('\n'));
// Activate snippet
if (plugin.settings.activateSnippet) {
// @ts-ignore
const customCss = plugin.app.customCss;
customCss.enabledSnippets.add('supercharged-links-gen');
customCss.requestLoadSnippets();
}
// Ensure Style Settings reads changes
plugin.app.workspace.trigger("parse-style-settings");
});
}
function clearExtraAttributes(link) {
Object.values(link.attributes).forEach(attr => {
if (attr.name.includes("data-link")) {
link.removeAttribute(attr.name);
}
});
}
function fetchTargetAttributesSync(app, settings, dest, addDataHref) {
var _a;
let new_props = { tags: "" };
const cache = app.metadataCache.getFileCache(dest);
if (!cache)
return new_props;
const frontmatter = cache.frontmatter;
if (frontmatter) {
settings.targetAttributes.forEach(attribute => {
if (Object.keys(frontmatter).includes(attribute)) {
if (attribute === 'tag' || attribute === 'tags') {
new_props['tags'] += frontmatter[attribute];
}
else {
new_props[attribute] = frontmatter[attribute];
}
}
});
}
if (settings.targetTags) {
new_props["tags"] += obsidian.getAllTags(cache).join(' ');
}
if (addDataHref) {
new_props['data-href'] = dest.basename;
}
new_props['path'] = dest.path;
//@ts-ignore
const getResults = (api) => {
const page = api.page(dest.path);
if (!page) {
return;
}
settings.targetAttributes.forEach((field) => {
const value = page[field];
if (value)
new_props[field] = value;
});
};
if (settings.getFromInlineField && app.plugins.enabledPlugins.has("dataview")) {
const api = (_a = app.plugins.plugins.dataview) === null || _a === void 0 ? void 0 : _a.api;
if (api) {
getResults(api);
}
else
this.plugin.registerEvent(this.app.metadataCache.on("dataview:api-ready", (api) => getResults(api)));
}
return new_props;
}
function setLinkNewProps(link, new_props) {
// @ts-ignore
for (const a of link.attributes) {
if (a.name.includes("data-link") && !(a.name in new_props)) {
link.removeAttribute(a.name);
}
}
Object.keys(new_props).forEach(key => {
var _a;
const name = "data-link-" + key;
const newValue = new_props[key];
const curValue = link.getAttribute(name);
// Only update if value is different
if (!newValue || curValue != newValue) {
link.setAttribute("data-link-" + key, new_props[key]);
if (((_a = new_props[key]) === null || _a === void 0 ? void 0 : _a.startsWith) && (new_props[key].startsWith('http') || new_props[key].startsWith('data:'))) {
link.style.setProperty(`--data-link-${key}`, `url(${new_props[key]})`);
}
else {
link.style.setProperty(`--data-link-${key}`, new_props[key]);
}
}
});
if (!link.hasClass("data-link-icon")) {
link.addClass("data-link-icon");
}
if (!link.hasClass("data-link-icon-after")) {
link.addClass("data-link-icon-after");
}
if (!link.hasClass("data-link-text")) {
link.addClass("data-link-text");
}
}
function updateLinkExtraAttributes(app, settings, link, destName) {
var _a, _b;
const linkHref = (_b = (_a = link.getAttribute('href')) === null || _a === void 0 ? void 0 : _a.split('#')) === null || _b === void 0 ? void 0 : _b[0];
if (linkHref) {
const dest = app.metadataCache.getFirstLinkpathDest(linkHref, destName);
if (dest) {
const new_props = fetchTargetAttributesSync(app, settings, dest, false);
setLinkNewProps(link, new_props);
}
}
}
function updateDivExtraAttributes(app, settings, link, destName, linkName) {
if (link.parentElement.getAttribute("class").contains('mod-collapsible'))
return; // Bookmarks Folder
if (!linkName) {
linkName = link.textContent;
}
if (!!link.parentElement.getAttribute('data-path')) {
// File Browser
linkName = link.parentElement.getAttribute('data-path');
}
else if (link.parentElement.getAttribute("class") == "suggestion-content" && !!link.nextElementSibling) {
// Auto complete
linkName = link.nextElementSibling.textContent + linkName;
}
const dest = app.metadataCache.getFirstLinkpathDest(obsidian.getLinkpath(linkName), destName);
if (dest) {
const new_props = fetchTargetAttributesSync(app, settings, dest, true);
setLinkNewProps(link, new_props);
}
}
function updateElLinks(app, plugin, el, ctx) {
const settings = plugin.settings;
const links = el.querySelectorAll('a.internal-link');
const destName = ctx.sourcePath.replace(/(.*).md/, "$1");
links.forEach((link) => {
updateLinkExtraAttributes(app, settings, link, destName);
});
}
function updatePropertiesPane(propertiesEl, file, app, plugin) {
var _a;
const frontmatter = (_a = app.metadataCache.getCache(file.path)) === null || _a === void 0 ? void 0 : _a.frontmatter;
if (!!frontmatter) {
const nodes = propertiesEl.querySelectorAll("div.internal-link > .multi-select-pill-content");
for (let i = 0; i < nodes.length; ++i) {
const el = nodes[i];
const linkText = el.textContent;
const keyEl = el.parentElement.parentElement.parentElement.parentElement.children[0].children[1];
// @ts-ignore
const key = keyEl.value;
const listOfLinks = frontmatter[key];
let foundS = null;
if (!listOfLinks) {
continue;
}
for (const s of listOfLinks) {
if (s.length > 4 && s.startsWith("[[") && s.endsWith("]]")) {
const slicedS = s.slice(2, -2);
const split = slicedS.split("|");
if (split.length == 1 && split[0] == linkText) {
foundS = split[0];
break;
}
else if (split.length == 2 && split[1] == linkText) {
foundS = split[0];
break;
}
}
}
if (!!foundS) {
updateDivExtraAttributes(plugin.app, plugin.settings, el, "", foundS);
}
}
const singleNodes = propertiesEl.querySelectorAll("div.metadata-link-inner");
for (let i = 0; i < singleNodes.length; ++i) {
const el = singleNodes[i];
const linkText = el.textContent;
const keyEl = el.parentElement.parentElement.parentElement.children[0].children[1];
// @ts-ignore
const key = keyEl.value;
const link = frontmatter[key];
if (!link) {
continue;
}
let foundS = null;
if ((link === null || link === void 0 ? void 0 : link.length) > 4 && link.startsWith("[[") && link.endsWith("]]")) {
const slicedS = link.slice(2, -2);
const split = slicedS.split("|");
if (split.length == 1 && split[0] == linkText) {
foundS = split[0];
}
else if (split.length == 2 && split[1] == linkText) {
foundS = split[0];
}
}
if (!!foundS) {
updateDivExtraAttributes(plugin.app, plugin.settings, el, "", foundS);
}
}
}
}
function updateVisibleLinks(app, plugin) {
const settings = plugin.settings;
app.workspace.iterateRootLeaves((leaf) => {
var _a, _b;
if (leaf.view instanceof obsidian.MarkdownView && leaf.view.file) {
const file = leaf.view.file;
const cachedFile = app.metadataCache.getFileCache(file);
// @ts-ignore
const metadata = (_b = (_a = leaf.view) === null || _a === void 0 ? void 0 : _a.metadataEditor) === null || _b === void 0 ? void 0 : _b.contentEl;
if (!!metadata) {
updatePropertiesPane(metadata, file, app, plugin);
}
//@ts-ignore
const tabHeader = leaf.tabHeaderInnerTitleEl;
if (settings.enableTabHeader) {
// Supercharge tab headers
updateDivExtraAttributes(app, settings, tabHeader, "", file.path);
}
else {
clearExtraAttributes(tabHeader);
}
if (cachedFile === null || cachedFile === void 0 ? void 0 : cachedFile.links) {
cachedFile.links.forEach((link) => {
const fileName = file.path.replace(/(.*).md/, "$1");
const dest = app.metadataCache.getFirstLinkpathDest(link.link, fileName);
if (dest) {
const new_props = fetchTargetAttributesSync(app, settings, dest, false);
const internalLinks = leaf.view.containerEl.querySelectorAll(`a.internal-link[href="${link.link}"]`);
internalLinks.forEach((internalLink) => setLinkNewProps(internalLink, new_props));
}
});
}
}
});
}
class SuperchargedLinksSettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
this.debouncedGenerate = obsidian.debounce(this._generateSnippet, 1000, true);
// Generate CSS immediately rather than 1 second - feels laggy
this._generateSnippet();
}
display() {
let { containerEl } = this;
containerEl.empty();
/* Managing extra attirbutes for a.internal-link */
new obsidian.Setting(containerEl)
.setName('Target Attributes for styling')
.setDesc('Frontmatter attributes to target, comma separated')
.addTextArea((text) => {
text
.setPlaceholder('Enter attributes as string, comma separated')
.setValue(this.plugin.settings.targetAttributes.join(', '))
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.targetAttributes = value.replace(/\s/g, '').split(',');
if (this.plugin.settings.targetAttributes.length === 1 && !this.plugin.settings.targetAttributes[0]) {
this.plugin.settings.targetAttributes = [];
}
yield this.plugin.saveSettings();
}));
text.inputEl.rows = 6;
text.inputEl.cols = 25;
});
containerEl.createEl('h4', { text: 'Styling' });
const styleSettingDescription = containerEl.createDiv();
styleSettingDescription.innerHTML = `
Styling can be done using the Style Settings plugin.
<ol>
<li>Create selectors down below.</li>
<li>Go to the Style Settings tab and style your links!</li>
</ol>`;
const selectorDiv = containerEl.createDiv();
this.drawSelectors(selectorDiv);
containerEl.createEl('h4', { text: 'Settings' });
new obsidian.Setting(containerEl)
.setName('Enable in Editor')
.setDesc('If true, this will also supercharge internal links in the editor view of a note.')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.enableEditor);
toggle.onChange(value => {
this.plugin.settings.enableEditor = value;
this.plugin.saveSettings();
updateVisibleLinks(this.app, this.plugin);
});
});
new obsidian.Setting(containerEl)
.setName('Enable in tab headers')
.setDesc('If true, this will also supercharge the headers of a tab.')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.enableTabHeader);
toggle.onChange(value => {
this.plugin.settings.enableTabHeader = value;
this.plugin.saveSettings();
updateVisibleLinks(this.app, this.plugin);
});
});
new obsidian.Setting(containerEl)
.setName('Enable in File Browser')
.setDesc('If true, this will also supercharge the file browser.')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.enableFileList);
toggle.onChange(value => {
this.plugin.settings.enableFileList = value;
this.plugin.saveSettings();
});
});
new obsidian.Setting(containerEl)
.setName('Enable in Plugins')
.setDesc('If true, this will also supercharge plugins like the backlinks and forward links panels.')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.enableBacklinks);
toggle.onChange(value => {
this.plugin.settings.enableBacklinks = value;
this.plugin.saveSettings();
});
});
new obsidian.Setting(containerEl)
.setName('Enable in Quick Switcher')
.setDesc('If true, this will also supercharge the quick switcher.')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.enableQuickSwitcher);
toggle.onChange(value => {
this.plugin.settings.enableQuickSwitcher = value;
this.plugin.saveSettings();
});
});
new obsidian.Setting(containerEl)
.setName('Enable in Link Autocompleter')
.setDesc('If true, this will also supercharge the link autocompleter.')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.enableSuggestor);
toggle.onChange(value => {
this.plugin.settings.enableSuggestor = value;
this.plugin.saveSettings();
});
});
containerEl.createEl('h4', { text: 'Advanced' });
// Managing choice wether you want to parse tags both from normal tags and in the frontmatter
new obsidian.Setting(containerEl)
.setName('Parse all tags in the file')
.setDesc('Sets the `data-link-tags`-attribute to look for tags both in the frontmatter and in the file as #tag-name')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.targetTags);
toggle.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.targetTags = value;
yield this.plugin.saveSettings();
}));
});
// Managing choice wether you get attributes from inline fields and frontmatter or only frontmater
new obsidian.Setting(containerEl)
.setName('Search for attribute in Inline fields like <field::>')
.setDesc('Sets the `data-link-<field>`-attribute to the value of inline fields')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.getFromInlineField);
toggle.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.getFromInlineField = value;
yield this.plugin.saveSettings();
}));
});
// Automatically activate snippet
new obsidian.Setting(containerEl)
.setName('Automatically activate snippet')
.setDesc('If true, this will automatically activate the generated CSS snippet "supercharged-links-gen.css". ' +
'Turn this off if you don\'t want this to happen.')
.addToggle(toggle => {
toggle.setValue(this.plugin.settings.activateSnippet);
toggle.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.activateSnippet = value;
yield this.plugin.saveSettings();
}));
});
/* Managing predefined values for properties */
/* Manage menu options display*/
new obsidian.Setting(containerEl)
.setName("Display field options in context menu")
.setDesc("This feature has been migrated to metadata-menu plugin https://github.com/mdelobelle/metadatamenu");
}
generateSnippet() {
this.debouncedGenerate();
}
_generateSnippet() {
return __awaiter(this, void 0, void 0, function* () {
yield buildCSS(this.plugin.settings.selectors, this.plugin);
// new Notice("Generated supercharged-links-gen.css");
});
}
drawSelectors(div) {
div.empty();
this.generateSnippet();
const selectors = this.plugin.settings.selectors;
selectors.forEach((selector, i) => {
const s = new obsidian.Setting(div)
.addButton(button => {
button.onClick(() => {
const oldSelector = selectors[i + 1];
selectors[i + 1] = selector;
selectors[i] = oldSelector;
this.drawSelectors(div);
});
button.setIcon("down-arrow-with-tail");
button.setTooltip("Move selector down");
if (i === selectors.length - 1) {
button.setDisabled(true);
}
})
.addButton(button => {
button.onClick(() => {
const oldSelector = selectors[i - 1];
selectors[i - 1] = selector;
selectors[i] = oldSelector;
this.drawSelectors(div);
});
button.setIcon("up-arrow-with-tail");
button.setTooltip("Move selector up");
if (i === 0) {
button.setDisabled(true);
}
})
.addButton(button => {
button.onClick(() => {
const formModal = new CSSBuilderModal(this.plugin, (newSelector) => {
this.plugin.settings.selectors[i] = newSelector;
this.plugin.saveSettings();
updateDisplay(s.nameEl, selector, this.plugin.settings);
this.generateSnippet();
}, selector);
formModal.open();
});
button.setIcon("pencil");
button.setTooltip("Edit selector");
})
.addButton(button => {
button.onClick(() => {
this.plugin.settings.selectors.remove(selector);
this.plugin.saveSettings();
this.drawSelectors(div);
});
button.setIcon("cross");
button.setTooltip("Remove selector");
});
updateDisplay(s.nameEl, selector, this.plugin.settings);
});
new obsidian.Setting(div)
.setName("New selector")
.setDesc("Create a new selector to style with Style Settings.")
.addButton(button => {
button.onClick(() => {
const formModal = new CSSBuilderModal(this.plugin, (newSelector) => {
this.plugin.settings.selectors.push(newSelector);
this.plugin.saveSettings();
this.drawSelectors(div);
// TODO: Force redraw somehow?
});
formModal.open();
});
button.setButtonText("New");
});
}
}
const DEFAULT_SETTINGS = {
targetAttributes: [],
targetTags: true,
getFromInlineField: true,
enableTabHeader: true,
activateSnippet: true,
enableEditor: true,
enableFileList: true,
enableBacklinks: true,
enableQuickSwitcher: true,
enableSuggestor: true,
selectors: []
};
function buildCMViewPlugin(app, _settings) {
// Implements the live preview supercharging
// Code structure based on https://github.com/nothingislost/obsidian-cm6-attributes/blob/743d71b0aa616407149a0b6ea5ffea28e2154158/src/main.ts
// Code help credits to @NothingIsLost! They have been a great help getting this to work properly.
class HeaderWidget extends view.WidgetType {
constructor(attributes, after) {
super();
this.attributes = attributes;
this.after = after;
}
toDOM() {
var _a;
let headerEl = document.createElement("span");
headerEl.setAttrs(this.attributes);
for (let key in this.attributes) {
// CSS doesn't allow interpolation of variables for URLs, so do it beforehand to be nice.
if (((_a = this.attributes[key]) === null || _a === void 0 ? void 0 : _a.startsWith) && (this.attributes[key].startsWith('http') || this.attributes[key].startsWith('data:'))) {
headerEl.style.setProperty(`--${key}`, `url(${this.attributes[key]})`);
}
else {
headerEl.style.setProperty(`--${key}`, this.attributes[key]);
}
}
if (this.after) {
headerEl.addClass('data-link-icon-after');
}
else {
headerEl.addClass('data-link-icon');
}
// create a naive bread crumb
return headerEl;
}
ignoreEvent() {
return true;
}
}
const settings = _settings;
const viewPlugin = view.ViewPlugin.fromClass(class {
constructor(view) {
this.decorations = this.buildDecorations(view);
}
update(update) {
if (update.docChanged) {
this.decorations = this.decorations.map(update.changes);
update.changes.iterChanges((fromA, toA, fromB, toB, t) => {
// Update all 'line blocks' between the range changed. Prevents weird graphical bugs
const minFrom = update.view.lineBlockAt(fromB).from;
const maxTo = update.view.lineBlockAt(toB).to;
// remove things within bounds
this.decorations = this.decorations.update({
filter: (from, to) => to < minFrom || from > maxTo
});
// Update decorations within bounds
this.decorations = state.RangeSet.join([this.decorations,
this.buildDecorations(update.view, minFrom, maxTo)]);
});
}
else if (update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
destroy() {
}
buildDecorations(view$1, updateFrom = -1, updateTo = -1) {
let builder = new state.RangeSetBuilder();
if (!settings.enableEditor) {
return builder.finish();
}
const mdView = view$1.state.field(obsidian.editorViewField);
let lastAttributes = {};
let iconDecoAfter = null;
let iconDecoAfterWhere = null;
let mdAliasFrom = null;
let mdAliasTo = null;
for (let { from, to } of view$1.visibleRanges) {
// When updating, only changes the range given.
if (updateFrom !== -1 && (to < updateFrom || from > updateTo))
continue;
language.syntaxTree(view$1.state).iterate({
from,
to,
enter: (node) => {
if (updateFrom !== -1 && (node.to < updateFrom || node.from > updateTo))
return;
const tokenProps = node.type.prop(language.tokenClassNodeProp);
if (tokenProps) {
const props = new Set(tokenProps.split(" "));
// Square Brackets of links both internal (`[[`, `]]`) and md link (`[`, `]`)
const isMDFormatting = props.has('formatting-link') || props.has('formatting-link-string');
if (isMDFormatting)
return;
// Parts of internal links
const isLink = props.has("hmd-internal-link"); // [[`Note` or `|` or `Alias`]]
const isAlias = props.has("link-alias"); // [[Note| `Alias`]]
const isPipe = props.has("link-alias-pipe"); // [[Note `|` Alias]]
// The 'alias' of the md link (or its brackets)
const isMDLink = props.has('link'); // `[` or `Alias` or `]`(URL)
// The 'internal link' of the md link (or its brackets)
const isMDUrl = props.has('url'); // [Alias]`(` or `URL` or `)`
if (isMDLink) {
// This catches the alias of md links i.e. [ `Alias` ](URL)
// We'll apply the styling in the next iteration when we analyze the `URL`
mdAliasFrom = node.from;
mdAliasTo = node.to;
}
if (!isPipe && !isAlias) {
if (iconDecoAfter) {
builder.add(iconDecoAfterWhere, iconDecoAfterWhere, iconDecoAfter);
iconDecoAfter = null;
iconDecoAfterWhere = null;
}
}
if (isLink && !isAlias && !isPipe || isMDUrl) {
let linkText = view$1.state.doc.sliceString(node.from, node.to);
linkText = linkText.split("#")[0];
let file = app.metadataCache.getFirstLinkpathDest(linkText, mdView.file.basename);
if (isMDUrl && !file) {
try {
file = app.vault.getAbstractFileByPath(decodeURIComponent(linkText));
}
catch (e) { }
}
if (file) {
let _attributes = fetchTargetAttributesSync(app, settings, file, true);
let attributes = {};
for (let key in _attributes) {
attributes["data-link-" + key] = _attributes[key];
}
let deco = view.Decoration.mark({
attributes,
class: "data-link-text"
});
let iconDecoBefore = view.Decoration.widget({
widget: new HeaderWidget(attributes, false),
});
iconDecoAfter = view.Decoration.widget({
widget: new HeaderWidget(attributes, true),
});
if (isMDUrl) {
// Apply retroactively to the alias found before
let deco = view.Decoration.mark({
attributes: attributes,
class: "data-link-text"
});
builder.add(mdAliasFrom, mdAliasFrom, iconDecoBefore);
builder.add(mdAliasFrom, mdAliasTo, deco);
if (iconDecoAfter) {
builder.add(mdAliasTo, mdAliasTo, iconDecoAfter);
iconDecoAfter = null;
iconDecoAfterWhere = null;
mdAliasFrom = null;
mdAliasTo = null;
}
}
else {
builder.add(node.from, node.from, iconDecoBefore);
}
builder.add(node.from, node.to, deco);
lastAttributes = attributes;
iconDecoAfterWhere = node.to;
}
}
else if (isLink && isAlias) {
let deco = view.Decoration.mark({
attributes: lastAttributes,
class: "data-link-text"
});
builder.add(node.from, node.to, deco);
if (iconDecoAfter) {
builder.add(node.to, node.to, iconDecoAfter);
iconDecoAfter = null;
iconDecoAfterWhere = null;
}
}
}
}
});
}
return builder.finish();
}
}, {
decorations: v => v.decorations
});
return viewPlugin;
}
class SuperchargedLinks extends obsidian.Plugin {
constructor() {
super(...arguments);
this.modalObservers = [];
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
console.log('Supercharged links loaded');
yield this.loadSettings();
this.addSettingTab(new SuperchargedLinksSettingTab(this.app, this));
this.registerMarkdownPostProcessor((el, ctx) => {
updateElLinks(this.app, this, el, ctx);
});
const plugin = this;
const updateLinks = function (_file) {
updateVisibleLinks(plugin.app, plugin);
plugin.observers.forEach(([observer, type, own_class]) => {
const leaves = plugin.app.workspace.getLeavesOfType(type);
leaves.forEach(leaf => {
plugin.updateContainer(leaf.view.containerEl, plugin, own_class);
});
});
};
// Live preview
const ext = state.Prec.lowest(buildCMViewPlugin(this.app, this.settings));
this.registerEditorExtension(ext);
this.observers = [];
this.app.workspace.onLayoutReady(() => {
this.initViewObservers(this);
this.initModalObservers(this, document);
updateVisibleLinks(this.app, this);
});
// Initialization
this.registerEvent(this.app.workspace.on("window-open", (window, win) => this.initModalObservers(this, window.getContainer().doc)));
// Update when
// Debounced to prevent lag when writing
this.registerEvent(this.app.metadataCache.on('changed', obsidian.debounce(updateLinks, 500, true)));
// Update when layout changes
// @ts-ignore
this.registerEvent(this.app.workspace.on("layout-change", obsidian.debounce(updateLinks, 10, true)));
// Update plugin views when layout changes
// TODO: This is an expensive operation that seems like it is called fairly frequently. Maybe we can do this more efficiently?
this.registerEvent(this.app.workspace.on("layout-change", () => this.initViewObservers(this)));
// DEBUG: When adding a new view, to get the proper id of that view, uncomment this and reload the plugin
this.app.workspace.iterateAllLeaves(leaf => {
console.log(leaf.view.getViewType());
});
});
}
initViewObservers(plugin) {
var _a, _b, _c, _d, _e, _f;
// Reset observers
plugin.observers.forEach(([observer, type]) => {
observer.disconnect();
});
plugin.observers = [];
// Register new observers
plugin.registerViewType('backlink', plugin, ".tree-item-inner", true);
plugin.registerViewType('outgoing-link', plugin, ".tree-item-inner", true);
plugin.registerViewType('search', plugin, ".tree-item-inner");
plugin.registerViewType('bc-matrix-view', plugin, 'span.internal-link');
plugin.registerViewType('BC-ducks', plugin, '.internal-link');
plugin.registerViewType('bc-tree-view', plugin, 'span.internal-link');
plugin.registerViewType('graph-analysis', plugin, '.internal-link');
plugin.registerViewType('starred', plugin, '.nav-file-title-content');
plugin.registerViewType('file-explorer', plugin, '.nav-file-title-content');
plugin.registerViewType('recent-files', plugin, '.nav-file-title-content');
plugin.registerViewType('bookmarks', plugin, '.tree-item-inner');
plugin.registerViewType('bases', plugin, '.internal-link');
// If backlinks in editor is on
// @ts-ignore
if ((_f = (_e = (_d = (_c = (_b = (_a = plugin.app) === null || _a === void 0 ? void 0 : _a.internalPlugins) === null || _b === void 0 ? void 0 : _b.plugins) === null || _c === void 0 ? void 0 : _c.backlink) === null || _d === void 0 ? void 0 : _d.instance) === null || _e === void 0 ? void 0 : _e.options) === null || _f === void 0 ? void 0 : _f.backlinkInDocument) {
plugin.registerViewType('markdown', plugin, '.tree-item-inner', true);
}
const propertyLeaves = this.app.workspace.getLeavesOfType("file-properties");
for (let i = 0; i < propertyLeaves.length; i++) {
const container = propertyLeaves[i].view.containerEl;
let observer = new MutationObserver((records, _) => {
const file = this.app.workspace.getActiveFile();
if (!!file) {
updatePropertiesPane(container, this.app.workspace.getActiveFile(), this.app, plugin);
}
});
observer.observe(container, { subtree: true, childList: true, attributes: false });
plugin.observers.push([observer, "file-properties" + i, ""]);
// TODO: No proper unloading!
}
plugin.registerViewType('file-properties', plugin, 'div.internal-link > .multi-select-pill-content');
}
initModalObservers(plugin, doc) {
const config = {
subtree: false,
childList: true,
attributes: false
};
this.modalObservers.push(new MutationObserver(records => {
records.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(n => {
if ('className' in n &&
// @ts-ignore
(n.className.includes('modal-container') && plugin.settings.enableQuickSwitcher
// @ts-ignore
|| n.className.includes('suggestion-container') && plugin.settings.enableSuggestor)) {
let selector = ".suggestion-title, .suggestion-note, .another-quick-switcher__item__title, .omnisearch-result__title";
// @ts-ignore
if (n.className.includes('suggestion-container')) {
selector = ".suggestion-title, .suggestion-note";
}
plugin.updateContainer(n, plugin, selector);
plugin._watchContainer(null, n, plugin, selector);
}
});
}
});
}));
this.modalObservers.last().observe(doc.body, config);
}
registerViewType(viewTypeName, plugin, selector, updateDynamic = false) {
const leaves = this.app.workspace.getLeavesOfType(viewTypeName);
// if (leaves.length > 1) {
for (let i = 0; i < leaves.length; i++) {
const container = leaves[i].view.containerEl;
if (updateDynamic) {
plugin._watchContainerDynamic(viewTypeName + i, container, plugin, selector);
}
else {
plugin._watchContainer(viewTypeName + i, container, plugin, selector);
}
}
// }
// else if (leaves.length < 1) return;
// else {
// const container = leaves[0].view.containerEl;
// this.updateContainer(container, plugin, selector);
// if (updateDynamic) {
// plugin._watchContainerDynamic(viewTypeName, container, plugin, selector)
// }
// else {
// plugin._watchContainer(viewTypeName, container, plugin, selector);
// }
// }
}
updateContainer(container, plugin, selector) {
if (!plugin.settings.enableBacklinks && container.getAttribute("data-type") !== "file-explorer")
return;
if (!plugin.settings.enableFileList && container.getAttribute("data-type") === "file-explorer")
return;
const nodes = container.findAll(selector);
for (let i = 0; i < nodes.length; ++i) {
const el = nodes[i];
updateDivExtraAttributes(plugin.app, plugin.settings, el, "");
}
}
removeFromContainer(container, selector) {
const nodes = container.findAll(selector);
for (let i = 0; i < nodes.length; ++i) {
const el = nodes[i];
clearExtraAttributes(el);
}
}
_watchContainer(viewType, container, plugin, selector) {
let observer = new MutationObserver((records, _) => {
plugin.updateContainer(container, plugin, selector);
});
observer.observe(container, { subtree: true, childList: true, attributes: false });
if (viewType) {
plugin.observers.push([observer, viewType, selector]);
}
}
_watchContainerDynamic(viewType, container, plugin, selector, own_class = 'tree-item-inner', parent_class = 'tree-item') {
// Used for efficient updating of the backlinks panel
// Only loops through newly added DOM nodes instead of changing all of them
if (!plugin.settings.enableBacklinks)
return;
let observer = new MutationObserver((records, _) => {
records.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((n) => {
if ('className' in n) {
// @ts-ignore
if (n.className.includes && typeof n.className.includes === 'function' && n.className.includes(parent_class)) {
const fileDivs = n.getElementsByClassName(own_class);
for (let i = 0; i < fileDivs.length; ++i) {
const link = fileDivs[i];
updateDivExtraAttributes(plugin.app, plugin.settings, link, "");
}
}
}
});
}
});
});
observer.observe(container, { subtree: true, childList: true, attributes: false });
plugin.observers.push([observer, viewType, selector]);
}
onunload() {
this.observers.forEach(([observer, type, own_class]) => {
observer.disconnect();
const leaves = this.app.workspace.getLeavesOfType(type);
leaves.forEach(leaf => {
this.removeFromContainer(leaf.view.containerEl, own_class);
});
});
for (const observer of this.modalObservers) {
observer.disconnect();
}
console.log('Supercharged links unloaded');
}
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 = SuperchargedLinks;
/* nosourcemap */