1437 lines
61 KiB
JavaScript
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 */ |