1989 lines
84 KiB
JavaScript
1989 lines
84 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
default: () => LinkerPlugin
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian5 = require("obsidian");
|
|
|
|
// linker/readModeLinker.ts
|
|
var import_obsidian3 = require("obsidian");
|
|
|
|
// linker/linkerCache.ts
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// linker/linkerInfo.ts
|
|
var import_obsidian = require("obsidian");
|
|
var LinkerFileMetaInfo = class {
|
|
constructor(fetcher, file) {
|
|
this.fetcher = fetcher;
|
|
var _a;
|
|
this.fetcher = fetcher;
|
|
this.file = file instanceof import_obsidian.TFile ? file : this.fetcher.app.vault.getFileByPath(file.path);
|
|
const settings = this.fetcher.settings;
|
|
this.tags = ((_a = (0, import_obsidian.getAllTags)(this.fetcher.app.metadataCache.getFileCache(this.file))) != null ? _a : []).filter((tag) => tag.trim().length > 0).map((tag) => tag.startsWith("#") ? tag.slice(1) : tag);
|
|
this.includeFile = this.tags.includes(settings.tagToIncludeFile);
|
|
this.excludeFile = this.tags.includes(settings.tagToExcludeFile);
|
|
this.includeAllFiles = fetcher.includeAllFiles;
|
|
this.isInIncludedDir = fetcher.includeDirPattern.test(this.file.path);
|
|
this.isInExcludedDir = fetcher.excludeDirPattern.test(this.file.path);
|
|
}
|
|
};
|
|
var LinkerMetaInfoFetcher = class {
|
|
constructor(app, settings) {
|
|
this.app = app;
|
|
this.settings = settings;
|
|
this.refreshSettings();
|
|
}
|
|
refreshSettings(settings) {
|
|
this.settings = settings != null ? settings : this.settings;
|
|
this.includeAllFiles = this.settings.includeAllFiles;
|
|
this.includeDirPattern = new RegExp(`(^|/)(${this.settings.linkerDirectories.join("|")})/`);
|
|
this.excludeDirPattern = new RegExp(`(^|/)(${this.settings.excludedDirectories.join("|")})/`);
|
|
}
|
|
getMetaInfo(file) {
|
|
return new LinkerFileMetaInfo(this, file);
|
|
}
|
|
};
|
|
|
|
// linker/linkerCache.ts
|
|
var ExternalUpdateManager = class {
|
|
constructor() {
|
|
this.registeredCallbacks = /* @__PURE__ */ new Set();
|
|
}
|
|
registerCallback(callback) {
|
|
this.registeredCallbacks.add(callback);
|
|
}
|
|
update() {
|
|
setTimeout(() => {
|
|
for (const callback of this.registeredCallbacks) {
|
|
callback();
|
|
}
|
|
}, 50);
|
|
}
|
|
};
|
|
var PrefixNode = class {
|
|
constructor() {
|
|
this.children = /* @__PURE__ */ new Map();
|
|
this.files = /* @__PURE__ */ new Set();
|
|
this.charValue = "";
|
|
this.value = "";
|
|
this.requiresCaseMatch = false;
|
|
}
|
|
};
|
|
var VisitedPrefixNode = class {
|
|
constructor(node, caseIsMatched = true, startedAtWordBeginning = false) {
|
|
this.formattingDelta = 0;
|
|
this.node = node;
|
|
this.caseIsMatched = caseIsMatched;
|
|
this.startedAtWordBeginning = startedAtWordBeginning;
|
|
}
|
|
};
|
|
var MatchNode = class {
|
|
constructor() {
|
|
this.start = 0;
|
|
this.length = 0;
|
|
this.files = /* @__PURE__ */ new Set();
|
|
this.value = "";
|
|
this.isAlias = false;
|
|
this.caseIsMatched = true;
|
|
this.startsAtWordBoundary = false;
|
|
this.requiresCaseMatch = false;
|
|
}
|
|
get end() {
|
|
return this.start + this.length;
|
|
}
|
|
};
|
|
var PrefixTree = class {
|
|
constructor(app, settings) {
|
|
this.app = app;
|
|
this.settings = settings;
|
|
this.root = new PrefixNode();
|
|
this._currentNodes = [];
|
|
this.setIndexedFilePaths = /* @__PURE__ */ new Set();
|
|
this.mapIndexedFilePathsToUpdateTime = /* @__PURE__ */ new Map();
|
|
this.mapFilePathToLeaveNodes = /* @__PURE__ */ new Map();
|
|
this.fetcher = new LinkerMetaInfoFetcher(this.app, this.settings);
|
|
this.updateTree();
|
|
}
|
|
clear() {
|
|
this.root = new PrefixNode();
|
|
this._currentNodes = [];
|
|
this.setIndexedFilePaths.clear();
|
|
this.mapIndexedFilePathsToUpdateTime.clear();
|
|
this.mapFilePathToLeaveNodes.clear();
|
|
}
|
|
getCurrentMatchNodes(index, excludedNote) {
|
|
const matchNodes = [];
|
|
if (excludedNote === void 0 && this.settings.excludeLinksToOwnNote) {
|
|
excludedNote = this.app.workspace.getActiveFile();
|
|
}
|
|
for (const node of this._currentNodes) {
|
|
if (node.node.files.size === 0) {
|
|
continue;
|
|
}
|
|
const matchNode = new MatchNode();
|
|
matchNode.length = node.node.value.length + node.formattingDelta;
|
|
matchNode.start = index - matchNode.length;
|
|
matchNode.files = new Set(Array.from(node.node.files).filter((file) => !excludedNote || file.path !== excludedNote.path));
|
|
matchNode.value = node.node.value;
|
|
matchNode.requiresCaseMatch = node.node.requiresCaseMatch;
|
|
const fileNames = Array.from(matchNode.files).map((file) => file.basename);
|
|
const nodeValue = node.node.value;
|
|
matchNode.isAlias = !fileNames.map((n) => n.toLowerCase()).includes(nodeValue.toLowerCase());
|
|
let currentNode = node.node;
|
|
while (currentNode) {
|
|
if (!node.caseIsMatched) {
|
|
matchNode.caseIsMatched = false;
|
|
break;
|
|
}
|
|
currentNode = currentNode.parent;
|
|
}
|
|
matchNode.startsAtWordBoundary = node.startedAtWordBeginning;
|
|
if (matchNode.requiresCaseMatch && !matchNode.caseIsMatched) {
|
|
continue;
|
|
}
|
|
if (matchNode.files.size > 0) {
|
|
matchNodes.push(matchNode);
|
|
}
|
|
}
|
|
matchNodes.sort((a, b) => b.length - a.length);
|
|
return matchNodes;
|
|
}
|
|
addFileWithName(name, file, matchCase) {
|
|
var _a;
|
|
let node = this.root;
|
|
for (let char of name) {
|
|
let child = node.children.get(char);
|
|
if (!child) {
|
|
child = new PrefixNode();
|
|
child.parent = node;
|
|
child.charValue = char;
|
|
child.value = node.value + char;
|
|
node.children.set(char, child);
|
|
}
|
|
node = child;
|
|
}
|
|
node.files.add(file);
|
|
node.requiresCaseMatch = matchCase;
|
|
const path2 = file.path;
|
|
this.mapFilePathToLeaveNodes.set(path2, [node, ...(_a = this.mapFilePathToLeaveNodes.get(path2)) != null ? _a : []]);
|
|
}
|
|
static isNoneEmptyString(value) {
|
|
return value !== null && value !== void 0 && typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
static isUpperCaseString(value, upperCasePart = 0.75) {
|
|
if (!PrefixTree.isNoneEmptyString(value)) {
|
|
return false;
|
|
}
|
|
const length = value.length;
|
|
const upperCaseChars = value.split("").filter((char) => char === char.toUpperCase()).length;
|
|
return upperCaseChars / length >= upperCasePart;
|
|
}
|
|
addFileToTree(file) {
|
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
const path2 = file.path;
|
|
if (!file || !path2) {
|
|
return;
|
|
}
|
|
this.removeFileFromTree(file);
|
|
this.setIndexedFilePaths.add(path2);
|
|
this.mapIndexedFilePathsToUpdateTime.set(path2, file.stat.mtime);
|
|
const metaInfo = this.fetcher.getMetaInfo(file);
|
|
const tags = ((_a = (0, import_obsidian2.getAllTags)(this.app.metadataCache.getFileCache(file))) != null ? _a : []).filter(PrefixTree.isNoneEmptyString).map((tag) => tag.startsWith("#") ? tag.slice(1) : tag);
|
|
const includeFile = metaInfo.includeFile;
|
|
const excludeFile = metaInfo.excludeFile;
|
|
const isInIncludedDir = metaInfo.isInIncludedDir;
|
|
const isInExcludedDir = metaInfo.isInExcludedDir;
|
|
if (excludeFile || isInExcludedDir && !includeFile) {
|
|
return;
|
|
}
|
|
if (!includeFile && !isInIncludedDir && !metaInfo.includeAllFiles) {
|
|
return;
|
|
}
|
|
const metadata = this.app.metadataCache.getFileCache(file);
|
|
let aliases = (_c = (_b = metadata == null ? void 0 : metadata.frontmatter) == null ? void 0 : _b.aliases) != null ? _c : [];
|
|
let aliasesWithMatchCase = new Set((_e = (_d = metadata == null ? void 0 : metadata.frontmatter) == null ? void 0 : _d[this.settings.propertyNameToMatchCase]) != null ? _e : []);
|
|
let aliasesWithIgnoreCase = new Set((_g = (_f = metadata == null ? void 0 : metadata.frontmatter) == null ? void 0 : _f[this.settings.propertyNameToIgnoreCase]) != null ? _g : []);
|
|
if (!Array.isArray(aliases)) {
|
|
aliases = [aliases];
|
|
}
|
|
try {
|
|
aliases = aliases.filter(PrefixTree.isNoneEmptyString);
|
|
} catch (e) {
|
|
console.error("[VL LC] Error filtering aliases", aliases, e);
|
|
}
|
|
let names = [file.basename];
|
|
if (aliases && this.settings.includeAliases) {
|
|
names.push(...aliases);
|
|
}
|
|
names = names.filter(PrefixTree.isNoneEmptyString);
|
|
let namesWithCaseIgnore = new Array();
|
|
let namesWithCaseMatch = new Array();
|
|
if (this.settings.matchCaseSensitive) {
|
|
let lowerCaseNames = new Array();
|
|
if (tags.includes(this.settings.tagToIgnoreCase)) {
|
|
namesWithCaseIgnore = [...names];
|
|
} else {
|
|
namesWithCaseMatch = [...names];
|
|
}
|
|
lowerCaseNames = lowerCaseNames.map((name) => name.toLowerCase());
|
|
names.push(...lowerCaseNames);
|
|
} else {
|
|
let lowerCaseNames = new Array();
|
|
if (tags.includes(this.settings.tagToMatchCase)) {
|
|
namesWithCaseMatch = [...names];
|
|
lowerCaseNames = names.filter((name) => aliasesWithIgnoreCase.has(name));
|
|
} else {
|
|
const prop = this.settings.capitalLetterProportionForAutomaticMatchCase;
|
|
namesWithCaseMatch = [...names].filter((name) => PrefixTree.isUpperCaseString(name, prop) && !aliasesWithIgnoreCase.has(name));
|
|
namesWithCaseIgnore = [...names].filter((name) => !namesWithCaseMatch.includes(name));
|
|
}
|
|
}
|
|
const namesToMoveFromIgnoreToMatch = namesWithCaseIgnore.filter((name) => aliasesWithMatchCase.has(name));
|
|
const namesToMoveFromMatchToIgnore = namesWithCaseMatch.filter((name) => aliasesWithIgnoreCase.has(name));
|
|
namesWithCaseIgnore = namesWithCaseIgnore.filter((name) => !namesToMoveFromIgnoreToMatch.includes(name));
|
|
namesWithCaseMatch = namesWithCaseMatch.filter((name) => !namesToMoveFromMatchToIgnore.includes(name));
|
|
namesWithCaseIgnore.push(...namesToMoveFromMatchToIgnore);
|
|
namesWithCaseMatch.push(...namesToMoveFromIgnoreToMatch);
|
|
namesWithCaseIgnore.push(...namesWithCaseIgnore.map((name) => name.toLowerCase()));
|
|
namesWithCaseIgnore.forEach((name) => {
|
|
this.addFileWithName(name, file, false);
|
|
});
|
|
namesWithCaseMatch.forEach((name) => {
|
|
this.addFileWithName(name, file, true);
|
|
});
|
|
}
|
|
removeFileFromTree(file) {
|
|
var _a;
|
|
const path2 = typeof file === "string" ? file : file.path;
|
|
const nodes = (_a = this.mapFilePathToLeaveNodes.get(path2)) != null ? _a : [];
|
|
for (const node of nodes) {
|
|
node.files = new Set([...node.files].filter((f) => f.path !== path2));
|
|
}
|
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
const node = nodes[i];
|
|
let currentNode = node;
|
|
while (currentNode.files.size === 0 && currentNode.children.size === 0) {
|
|
const parent = currentNode.parent;
|
|
if (!parent || parent === this.root) {
|
|
break;
|
|
}
|
|
parent.children.delete(currentNode.charValue);
|
|
currentNode = parent;
|
|
}
|
|
}
|
|
this.setIndexedFilePaths.delete(path2);
|
|
this.mapFilePathToLeaveNodes.delete(path2);
|
|
this.mapIndexedFilePathsToUpdateTime.delete(path2);
|
|
}
|
|
fileIsUpToDate(file) {
|
|
const mtime = file.stat.mtime;
|
|
const path2 = file.path;
|
|
return this.mapIndexedFilePathsToUpdateTime.has(path2) && this.mapIndexedFilePathsToUpdateTime.get(path2) === mtime;
|
|
}
|
|
updateTree(updateFiles) {
|
|
this.fetcher.refreshSettings();
|
|
const currentVaultFiles = /* @__PURE__ */ new Set();
|
|
let files = new Array();
|
|
const allFiles = this.app.vault.getMarkdownFiles();
|
|
allFiles.forEach((f) => currentVaultFiles.add(f.path));
|
|
if (allFiles.length != this.setIndexedFilePaths.size || !updateFiles || updateFiles.length == 0) {
|
|
files = allFiles;
|
|
} else {
|
|
files = updateFiles.map((f) => f ? this.app.vault.getAbstractFileByPath(f) : null).filter((f) => f !== null && f instanceof import_obsidian2.TFile);
|
|
}
|
|
for (const file of files) {
|
|
const mtime = file.stat.mtime;
|
|
if (this.fileIsUpToDate(file)) {
|
|
continue;
|
|
}
|
|
try {
|
|
this.addFileToTree(file);
|
|
} catch (e) {
|
|
console.error("[VL LC] Error adding file to tree", file, e);
|
|
}
|
|
}
|
|
const filesToRemove = [...this.setIndexedFilePaths].filter((f) => !currentVaultFiles.has(f));
|
|
filesToRemove.forEach((f) => this.removeFileFromTree(f));
|
|
}
|
|
findFiles(prefix) {
|
|
let node = this.root;
|
|
for (const char of prefix) {
|
|
node = node.children.get(char.toLowerCase());
|
|
if (!node) {
|
|
return /* @__PURE__ */ new Set();
|
|
}
|
|
}
|
|
return node.files;
|
|
}
|
|
resetSearch() {
|
|
this._currentNodes = [new VisitedPrefixNode(this.root)];
|
|
}
|
|
pushChar(char) {
|
|
const newNodes = [];
|
|
const chars = [char];
|
|
chars.push(char.toLowerCase());
|
|
chars.forEach((c) => {
|
|
const isBoundary = PrefixTree.checkWordBoundary(c);
|
|
if (this.settings.matchAnyPartsOfWords || isBoundary || this.settings.matchEndOfWords) {
|
|
newNodes.push(new VisitedPrefixNode(this.root, true, isBoundary));
|
|
}
|
|
for (const node of this._currentNodes) {
|
|
const child = node.node.children.get(c);
|
|
const startedAtBoundary = node.startedAtWordBeginning;
|
|
if (child) {
|
|
const newPrefixNodes = newNodes.map((n) => n.node);
|
|
if (!newPrefixNodes.includes(child)) {
|
|
const newVisited = new VisitedPrefixNode(child, char == c, startedAtBoundary);
|
|
newVisited.formattingDelta = node.formattingDelta;
|
|
newNodes.push(newVisited);
|
|
}
|
|
}
|
|
}
|
|
if (false) {
|
|
const isFormatting = PrefixTree.isFormattingChar(char);
|
|
if (isFormatting) {
|
|
this._currentNodes.forEach((node) => {
|
|
node.formattingDelta += 1;
|
|
});
|
|
newNodes.push(...this._currentNodes);
|
|
}
|
|
}
|
|
});
|
|
this._currentNodes = newNodes;
|
|
}
|
|
static checkWordBoundary(char) {
|
|
let pattern = /[^\p{L}]/u;
|
|
return pattern.test(char);
|
|
}
|
|
static isFormattingChar(char) {
|
|
const pattern = /[^\p{L}\p{N}]/u;
|
|
return pattern.test(char);
|
|
}
|
|
};
|
|
var LinkerCache = class {
|
|
constructor(app, settings) {
|
|
this.app = app;
|
|
this.settings = settings;
|
|
const { vault } = app;
|
|
this.vault = vault;
|
|
this.cache = new PrefixTree(app, settings);
|
|
this.updateCache(true);
|
|
}
|
|
static getInstance(app, settings) {
|
|
if (!LinkerCache.instance) {
|
|
LinkerCache.instance = new LinkerCache(app, settings);
|
|
}
|
|
return LinkerCache.instance;
|
|
}
|
|
clearCache() {
|
|
this.cache.clear();
|
|
}
|
|
reset() {
|
|
this.cache.resetSearch();
|
|
}
|
|
updateCache(force = false) {
|
|
var _a, _b, _c;
|
|
if (!((_b = (_a = this.app) == null ? void 0 : _a.workspace) == null ? void 0 : _b.getActiveFile())) {
|
|
return;
|
|
}
|
|
const activeFile = (_c = this.app.workspace.getActiveFile()) == null ? void 0 : _c.path;
|
|
if (activeFile === this.activeFilePath && !force) {
|
|
return;
|
|
}
|
|
this.cache.updateTree(force ? void 0 : [activeFile, this.activeFilePath]);
|
|
this.activeFilePath = activeFile;
|
|
}
|
|
};
|
|
|
|
// linker/virtualLinkDom.ts
|
|
var VirtualMatch = class {
|
|
constructor(id, originText, from, to, files, isAlias, isSubWord, settings) {
|
|
this.id = id;
|
|
this.originText = originText;
|
|
this.from = from;
|
|
this.to = to;
|
|
this.files = files;
|
|
this.isAlias = isAlias;
|
|
this.isSubWord = isSubWord;
|
|
this.settings = settings;
|
|
}
|
|
getCompleteLinkElement() {
|
|
const span = this.getLinkRootSpan();
|
|
const firstPath = this.files.length > 0 ? this.files[0].path : "";
|
|
span.appendChild(this.getLinkAnchorElement(this.originText, firstPath));
|
|
if (this.files.length > 1) {
|
|
if (!this.isSubWord) {
|
|
span.appendChild(this.getMultipleReferencesIndicatorSpan());
|
|
}
|
|
span.appendChild(this.getMultipleReferencesSpan());
|
|
}
|
|
if (!this.isSubWord || !this.settings.suppressSuffixForSubWords) {
|
|
const icon = this.getIconSpan();
|
|
if (icon)
|
|
span.appendChild(icon);
|
|
}
|
|
return span;
|
|
}
|
|
getLinkAnchorElement(linkText, href) {
|
|
const link = document.createElement("a");
|
|
link.href = href;
|
|
link.textContent = linkText;
|
|
link.target = "_blank";
|
|
link.rel = "noopener noreferrer";
|
|
link.setAttribute("from", this.from.toString());
|
|
link.setAttribute("to", this.to.toString());
|
|
link.setAttribute("origin-text", this.originText);
|
|
link.classList.add("internal-link", "virtual-link-a");
|
|
return link;
|
|
}
|
|
getLinkRootSpan() {
|
|
const span = document.createElement("span");
|
|
span.classList.add("glossary-entry", "virtual-link", "virtual-link-span");
|
|
if (this.settings.applyDefaultLinkStyling) {
|
|
span.classList.add("virtual-link-default");
|
|
}
|
|
return span;
|
|
}
|
|
getMultipleReferencesSpan(files) {
|
|
const spanReferences = document.createElement("span");
|
|
if (!this.settings.alwaysShowMultipleReferences) {
|
|
spanReferences.classList.add("multiple-files-references");
|
|
}
|
|
files = files != null ? files : this.files;
|
|
files.forEach((file, index) => {
|
|
if (index === 0) {
|
|
const bracket = document.createElement("span");
|
|
bracket.textContent = this.isSubWord ? "[" : " [";
|
|
spanReferences.appendChild(bracket);
|
|
}
|
|
let linkText = ` ${index + 1} `;
|
|
if (index < files.length - 1) {
|
|
linkText += "|";
|
|
}
|
|
let linkHref = file.path;
|
|
const link = this.getLinkAnchorElement(linkText, linkHref);
|
|
spanReferences.appendChild(link);
|
|
if (index == files.length - 1) {
|
|
const bracket = document.createElement("span");
|
|
bracket.textContent = "]";
|
|
spanReferences.appendChild(bracket);
|
|
}
|
|
});
|
|
return spanReferences;
|
|
}
|
|
getMultipleReferencesIndicatorSpan() {
|
|
const spanIndicator = document.createElement("span");
|
|
spanIndicator.textContent = " [...]";
|
|
spanIndicator.classList.add("multiple-files-indicator");
|
|
return spanIndicator;
|
|
}
|
|
getIconSpan() {
|
|
var _a;
|
|
const suffix = this.isAlias ? this.settings.virtualLinkAliasSuffix : this.settings.virtualLinkSuffix;
|
|
if (((_a = suffix == null ? void 0 : suffix.length) != null ? _a : 0) > 0) {
|
|
let icon = document.createElement("sup");
|
|
icon.textContent = suffix;
|
|
icon.classList.add("linker-suffix-icon");
|
|
return icon;
|
|
}
|
|
return null;
|
|
}
|
|
static compare(a, b) {
|
|
if (a.from === b.from) {
|
|
if (b.to == a.to) {
|
|
return b.files.length - a.files.length;
|
|
}
|
|
return b.to - a.to;
|
|
}
|
|
return a.from - b.from;
|
|
}
|
|
static sort(matches) {
|
|
return Array.from(matches).sort(VirtualMatch.compare);
|
|
}
|
|
static filterAlreadyLinked(matches, linkedFiles, mode = "every") {
|
|
return matches.filter((match) => {
|
|
if (mode === "every") {
|
|
return !match.files.every((file) => linkedFiles.has(file));
|
|
} else {
|
|
return !match.files.some((file) => linkedFiles.has(file));
|
|
}
|
|
});
|
|
}
|
|
static filterOverlapping(matches, onlyLinkOnce = true, excludedIntervalTree) {
|
|
const matchesToDelete = /* @__PURE__ */ new Map();
|
|
for (let i = 0; i < matches.length; i++) {
|
|
const addition = matches[i];
|
|
if (matchesToDelete.has(addition.id)) {
|
|
continue;
|
|
}
|
|
if (excludedIntervalTree) {
|
|
const overlaps = excludedIntervalTree.search([addition.from, addition.to]);
|
|
if (overlaps.length > 0) {
|
|
matchesToDelete.set(addition.id, true);
|
|
continue;
|
|
}
|
|
}
|
|
for (let j = i + 1; j < matches.length; j++) {
|
|
const otherAddition = matches[j];
|
|
if (otherAddition.from >= addition.to) {
|
|
break;
|
|
}
|
|
matchesToDelete.set(otherAddition.id, true);
|
|
}
|
|
if (onlyLinkOnce) {
|
|
for (let j = i + 1; j < matches.length; j++) {
|
|
const otherAddition = matches[j];
|
|
if (matchesToDelete.has(otherAddition.id)) {
|
|
continue;
|
|
}
|
|
if (otherAddition.files.every((f) => addition.files.contains(f))) {
|
|
matchesToDelete.set(otherAddition.id, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return matches.filter((match) => !matchesToDelete.has(match.id));
|
|
}
|
|
};
|
|
|
|
// linker/readModeLinker.ts
|
|
var GlossaryLinker = class extends import_obsidian3.MarkdownRenderChild {
|
|
constructor(app, settings, context, containerEl) {
|
|
super(containerEl);
|
|
this.settings = settings;
|
|
this.app = app;
|
|
this.ctx = context;
|
|
this.linkerCache = LinkerCache.getInstance(app, settings);
|
|
this.load();
|
|
}
|
|
getClosestLinkPath(glossaryName) {
|
|
var _a, _b;
|
|
const destName = this.ctx.sourcePath.replace(/(.*).md/, "$1");
|
|
let currentDestName = destName;
|
|
let currentPath = this.app.metadataCache.getFirstLinkpathDest((0, import_obsidian3.getLinkpath)(glossaryName), currentDestName);
|
|
if (currentPath == null)
|
|
return null;
|
|
while (currentDestName.includes("/")) {
|
|
currentDestName = currentDestName.replace(/\/[^\/]*?$/, "");
|
|
const newPath = this.app.metadataCache.getFirstLinkpathDest((0, import_obsidian3.getLinkpath)(glossaryName), currentDestName);
|
|
if ((((_a = newPath == null ? void 0 : newPath.path) == null ? void 0 : _a.length) || 0) > ((_b = currentPath == null ? void 0 : currentPath.path) == null ? void 0 : _b.length)) {
|
|
currentPath = newPath;
|
|
break;
|
|
}
|
|
}
|
|
return currentPath;
|
|
}
|
|
onload() {
|
|
if (!this.settings.linkerActivated) {
|
|
return;
|
|
}
|
|
const tags = ["p", "li", "td", "th", "span", "em", "strong"];
|
|
if (this.settings.includeHeaders) {
|
|
tags.push("h1", "h2", "h3", "h4", "h5", "h6");
|
|
}
|
|
const linkedFiles = /* @__PURE__ */ new Set();
|
|
const explicitlyLinkedFiles = /* @__PURE__ */ new Set();
|
|
for (const tag of tags) {
|
|
const nodeList = this.containerEl.getElementsByTagName(tag);
|
|
const children = this.containerEl.children;
|
|
for (let index = 0; index <= nodeList.length; index++) {
|
|
const item = index == nodeList.length ? this.containerEl : nodeList.item(index);
|
|
for (let childNodeIndex = 0; childNodeIndex < item.childNodes.length; childNodeIndex++) {
|
|
const childNode = item.childNodes[childNodeIndex];
|
|
if (childNode.nodeType === Node.TEXT_NODE) {
|
|
let text = childNode.textContent || "";
|
|
if (text.length === 0)
|
|
continue;
|
|
this.linkerCache.reset();
|
|
let matches = [];
|
|
let id = 0;
|
|
for (let i = 0; i <= text.length; i) {
|
|
const codePoint = text.codePointAt(i);
|
|
const char = i < text.length ? String.fromCodePoint(codePoint) : "\n";
|
|
const isWordBoundary = PrefixTree.checkWordBoundary(char);
|
|
if (this.settings.matchAnyPartsOfWords || this.settings.matchBeginningOfWords || isWordBoundary) {
|
|
const currentNodes = this.linkerCache.cache.getCurrentMatchNodes(i);
|
|
if (currentNodes.length > 0) {
|
|
currentNodes.forEach((node) => {
|
|
if (!this.settings.matchAnyPartsOfWords) {
|
|
if (this.settings.matchBeginningOfWords && !node.startsAtWordBoundary && this.settings.matchEndOfWords && !isWordBoundary) {
|
|
return;
|
|
}
|
|
}
|
|
const nFrom = node.start;
|
|
const nTo = node.end;
|
|
const name = text.slice(nFrom, nTo);
|
|
matches.push(new VirtualMatch(id++, name, nFrom, nTo, Array.from(node.files), node.isAlias, !isWordBoundary, this.settings));
|
|
});
|
|
}
|
|
}
|
|
this.linkerCache.cache.pushChar(char);
|
|
i += char.length;
|
|
}
|
|
matches = VirtualMatch.sort(matches);
|
|
if (this.settings.excludeLinksToRealLinkedFiles) {
|
|
matches = VirtualMatch.filterAlreadyLinked(matches, explicitlyLinkedFiles);
|
|
}
|
|
if (this.settings.onlyLinkOnce) {
|
|
matches = VirtualMatch.filterAlreadyLinked(matches, linkedFiles);
|
|
}
|
|
matches = VirtualMatch.filterOverlapping(matches, this.settings.onlyLinkOnce);
|
|
const parent = childNode.parentElement;
|
|
let lastTo = 0;
|
|
matches.forEach((match) => {
|
|
match.files.forEach((f) => linkedFiles.add(f));
|
|
const span = match.getCompleteLinkElement();
|
|
if (match.from > 0) {
|
|
parent == null ? void 0 : parent.insertBefore(document.createTextNode(text.slice(lastTo, match.from)), childNode);
|
|
}
|
|
parent == null ? void 0 : parent.insertBefore(span, childNode);
|
|
lastTo = match.to;
|
|
});
|
|
const textLength = text.length;
|
|
if (lastTo < textLength) {
|
|
parent == null ? void 0 : parent.insertBefore(document.createTextNode(text.slice(lastTo)), childNode);
|
|
}
|
|
parent == null ? void 0 : parent.removeChild(childNode);
|
|
childNodeIndex += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// linker/liveLinker.ts
|
|
var import_language = require("@codemirror/language");
|
|
var import_state = require("@codemirror/state");
|
|
var import_view = require("@codemirror/view");
|
|
var import_obsidian4 = require("obsidian");
|
|
|
|
// node_modules/@flatten-js/interval-tree/dist/main.mjs
|
|
var Interval = class Interval2 {
|
|
constructor(low, high) {
|
|
this.low = low;
|
|
this.high = high;
|
|
}
|
|
clone() {
|
|
return new Interval2(this.low, this.high);
|
|
}
|
|
get max() {
|
|
return this.clone();
|
|
}
|
|
less_than(other_interval) {
|
|
return this.low < other_interval.low || this.low === other_interval.low && this.high < other_interval.high;
|
|
}
|
|
equal_to(other_interval) {
|
|
return this.low === other_interval.low && this.high === other_interval.high;
|
|
}
|
|
intersect(other_interval) {
|
|
return !this.not_intersect(other_interval);
|
|
}
|
|
not_intersect(other_interval) {
|
|
return this.high < other_interval.low || other_interval.high < this.low;
|
|
}
|
|
merge(other_interval) {
|
|
return new Interval2(this.low === void 0 ? other_interval.low : this.low < other_interval.low ? this.low : other_interval.low, this.high === void 0 ? other_interval.high : this.high > other_interval.high ? this.high : other_interval.high);
|
|
}
|
|
output() {
|
|
return [this.low, this.high];
|
|
}
|
|
static comparable_max(interval1, interval2) {
|
|
return interval1.merge(interval2);
|
|
}
|
|
static comparable_less_than(val1, val2) {
|
|
return val1 < val2;
|
|
}
|
|
};
|
|
var RB_TREE_COLOR_RED = 0;
|
|
var RB_TREE_COLOR_BLACK = 1;
|
|
var Node2 = class {
|
|
constructor(key = void 0, value = void 0, left = null, right = null, parent = null, color = RB_TREE_COLOR_BLACK) {
|
|
this.left = left;
|
|
this.right = right;
|
|
this.parent = parent;
|
|
this.color = color;
|
|
this.item = { key, value };
|
|
if (key && key instanceof Array && key.length === 2) {
|
|
if (!Number.isNaN(key[0]) && !Number.isNaN(key[1])) {
|
|
let [low, high] = key;
|
|
if (low > high)
|
|
[low, high] = [high, low];
|
|
this.item.key = new Interval(low, high);
|
|
}
|
|
}
|
|
this.max = this.item.key ? this.item.key.max : void 0;
|
|
}
|
|
isNil() {
|
|
return this.item.key === void 0 && this.item.value === void 0 && this.left === null && this.right === null && this.color === RB_TREE_COLOR_BLACK;
|
|
}
|
|
_value_less_than(other_node) {
|
|
return this.item.value && other_node.item.value && this.item.value.less_than ? this.item.value.less_than(other_node.item.value) : this.item.value < other_node.item.value;
|
|
}
|
|
less_than(other_node) {
|
|
if (this.item.value === this.item.key && other_node.item.value === other_node.item.key) {
|
|
return this.item.key.less_than(other_node.item.key);
|
|
} else {
|
|
return this.item.key.less_than(other_node.item.key) || this.item.key.equal_to(other_node.item.key) && this._value_less_than(other_node);
|
|
}
|
|
}
|
|
_value_equal(other_node) {
|
|
return this.item.value && other_node.item.value && this.item.value.equal_to ? this.item.value.equal_to(other_node.item.value) : this.item.value === other_node.item.value;
|
|
}
|
|
equal_to(other_node) {
|
|
if (this.item.value === this.item.key && other_node.item.value === other_node.item.key) {
|
|
return this.item.key.equal_to(other_node.item.key);
|
|
} else {
|
|
return this.item.key.equal_to(other_node.item.key) && this._value_equal(other_node);
|
|
}
|
|
}
|
|
intersect(other_node) {
|
|
return this.item.key.intersect(other_node.item.key);
|
|
}
|
|
copy_data(other_node) {
|
|
this.item.key = other_node.item.key;
|
|
this.item.value = other_node.item.value;
|
|
}
|
|
update_max() {
|
|
this.max = this.item.key ? this.item.key.max : void 0;
|
|
if (this.right && this.right.max) {
|
|
const comparable_max = this.item.key.constructor.comparable_max;
|
|
this.max = comparable_max(this.max, this.right.max);
|
|
}
|
|
if (this.left && this.left.max) {
|
|
const comparable_max = this.item.key.constructor.comparable_max;
|
|
this.max = comparable_max(this.max, this.left.max);
|
|
}
|
|
}
|
|
not_intersect_left_subtree(search_node) {
|
|
const comparable_less_than = this.item.key.constructor.comparable_less_than;
|
|
let high = this.left.max.high !== void 0 ? this.left.max.high : this.left.max;
|
|
return comparable_less_than(high, search_node.item.key.low);
|
|
}
|
|
not_intersect_right_subtree(search_node) {
|
|
const comparable_less_than = this.item.key.constructor.comparable_less_than;
|
|
let low = this.right.max.low !== void 0 ? this.right.max.low : this.right.item.key.low;
|
|
return comparable_less_than(search_node.item.key.high, low);
|
|
}
|
|
};
|
|
var IntervalTree = class {
|
|
constructor() {
|
|
this.root = null;
|
|
this.nil_node = new Node2();
|
|
}
|
|
get size() {
|
|
let count = 0;
|
|
this.tree_walk(this.root, () => count++);
|
|
return count;
|
|
}
|
|
get keys() {
|
|
let res = [];
|
|
this.tree_walk(this.root, (node) => res.push(node.item.key.output ? node.item.key.output() : node.item.key));
|
|
return res;
|
|
}
|
|
get values() {
|
|
let res = [];
|
|
this.tree_walk(this.root, (node) => res.push(node.item.value));
|
|
return res;
|
|
}
|
|
get items() {
|
|
let res = [];
|
|
this.tree_walk(this.root, (node) => res.push({
|
|
key: node.item.key.output ? node.item.key.output() : node.item.key,
|
|
value: node.item.value
|
|
}));
|
|
return res;
|
|
}
|
|
isEmpty() {
|
|
return this.root == null || this.root === this.nil_node;
|
|
}
|
|
clear() {
|
|
this.root = null;
|
|
}
|
|
insert(key, value = key) {
|
|
if (key === void 0)
|
|
return;
|
|
let insert_node = new Node2(key, value, this.nil_node, this.nil_node, null, RB_TREE_COLOR_RED);
|
|
this.tree_insert(insert_node);
|
|
this.recalc_max(insert_node);
|
|
return insert_node;
|
|
}
|
|
exist(key, value = key) {
|
|
let search_node = new Node2(key, value);
|
|
return !!this.tree_search(this.root, search_node);
|
|
}
|
|
remove(key, value = key) {
|
|
let search_node = new Node2(key, value);
|
|
let delete_node = this.tree_search(this.root, search_node);
|
|
if (delete_node) {
|
|
this.tree_delete(delete_node);
|
|
}
|
|
return delete_node;
|
|
}
|
|
search(interval, outputMapperFn = (value, key) => value === key ? key.output() : value) {
|
|
let search_node = new Node2(interval);
|
|
let resp_nodes = [];
|
|
this.tree_search_interval(this.root, search_node, resp_nodes);
|
|
return resp_nodes.map((node) => outputMapperFn(node.item.value, node.item.key));
|
|
}
|
|
intersect_any(interval) {
|
|
let search_node = new Node2(interval);
|
|
return this.tree_find_any_interval(this.root, search_node);
|
|
}
|
|
forEach(visitor) {
|
|
this.tree_walk(this.root, (node) => visitor(node.item.key, node.item.value));
|
|
}
|
|
map(callback) {
|
|
const tree = new IntervalTree();
|
|
this.tree_walk(this.root, (node) => tree.insert(node.item.key, callback(node.item.value, node.item.key)));
|
|
return tree;
|
|
}
|
|
*iterate(interval, outputMapperFn = (value, key) => value === key ? key.output() : value) {
|
|
let node;
|
|
if (interval) {
|
|
node = this.tree_search_nearest_forward(this.root, new Node2(interval));
|
|
} else if (this.root) {
|
|
node = this.local_minimum(this.root);
|
|
}
|
|
while (node) {
|
|
yield outputMapperFn(node.item.value, node.item.key);
|
|
node = this.tree_successor(node);
|
|
}
|
|
}
|
|
recalc_max(node) {
|
|
let node_current = node;
|
|
while (node_current.parent != null) {
|
|
node_current.parent.update_max();
|
|
node_current = node_current.parent;
|
|
}
|
|
}
|
|
tree_insert(insert_node) {
|
|
let current_node = this.root;
|
|
let parent_node = null;
|
|
if (this.root == null || this.root === this.nil_node) {
|
|
this.root = insert_node;
|
|
} else {
|
|
while (current_node !== this.nil_node) {
|
|
parent_node = current_node;
|
|
if (insert_node.less_than(current_node)) {
|
|
current_node = current_node.left;
|
|
} else {
|
|
current_node = current_node.right;
|
|
}
|
|
}
|
|
insert_node.parent = parent_node;
|
|
if (insert_node.less_than(parent_node)) {
|
|
parent_node.left = insert_node;
|
|
} else {
|
|
parent_node.right = insert_node;
|
|
}
|
|
}
|
|
this.insert_fixup(insert_node);
|
|
}
|
|
insert_fixup(insert_node) {
|
|
let current_node;
|
|
let uncle_node;
|
|
current_node = insert_node;
|
|
while (current_node !== this.root && current_node.parent.color === RB_TREE_COLOR_RED) {
|
|
if (current_node.parent === current_node.parent.parent.left) {
|
|
uncle_node = current_node.parent.parent.right;
|
|
if (uncle_node.color === RB_TREE_COLOR_RED) {
|
|
current_node.parent.color = RB_TREE_COLOR_BLACK;
|
|
uncle_node.color = RB_TREE_COLOR_BLACK;
|
|
current_node.parent.parent.color = RB_TREE_COLOR_RED;
|
|
current_node = current_node.parent.parent;
|
|
} else {
|
|
if (current_node === current_node.parent.right) {
|
|
current_node = current_node.parent;
|
|
this.rotate_left(current_node);
|
|
}
|
|
current_node.parent.color = RB_TREE_COLOR_BLACK;
|
|
current_node.parent.parent.color = RB_TREE_COLOR_RED;
|
|
this.rotate_right(current_node.parent.parent);
|
|
}
|
|
} else {
|
|
uncle_node = current_node.parent.parent.left;
|
|
if (uncle_node.color === RB_TREE_COLOR_RED) {
|
|
current_node.parent.color = RB_TREE_COLOR_BLACK;
|
|
uncle_node.color = RB_TREE_COLOR_BLACK;
|
|
current_node.parent.parent.color = RB_TREE_COLOR_RED;
|
|
current_node = current_node.parent.parent;
|
|
} else {
|
|
if (current_node === current_node.parent.left) {
|
|
current_node = current_node.parent;
|
|
this.rotate_right(current_node);
|
|
}
|
|
current_node.parent.color = RB_TREE_COLOR_BLACK;
|
|
current_node.parent.parent.color = RB_TREE_COLOR_RED;
|
|
this.rotate_left(current_node.parent.parent);
|
|
}
|
|
}
|
|
}
|
|
this.root.color = RB_TREE_COLOR_BLACK;
|
|
}
|
|
tree_delete(delete_node) {
|
|
let cut_node;
|
|
let fix_node;
|
|
if (delete_node.left === this.nil_node || delete_node.right === this.nil_node) {
|
|
cut_node = delete_node;
|
|
} else {
|
|
cut_node = this.tree_successor(delete_node);
|
|
}
|
|
if (cut_node.left !== this.nil_node) {
|
|
fix_node = cut_node.left;
|
|
} else {
|
|
fix_node = cut_node.right;
|
|
}
|
|
fix_node.parent = cut_node.parent;
|
|
if (cut_node === this.root) {
|
|
this.root = fix_node;
|
|
} else {
|
|
if (cut_node === cut_node.parent.left) {
|
|
cut_node.parent.left = fix_node;
|
|
} else {
|
|
cut_node.parent.right = fix_node;
|
|
}
|
|
cut_node.parent.update_max();
|
|
}
|
|
this.recalc_max(fix_node);
|
|
if (cut_node !== delete_node) {
|
|
delete_node.copy_data(cut_node);
|
|
delete_node.update_max();
|
|
this.recalc_max(delete_node);
|
|
}
|
|
if (cut_node.color === RB_TREE_COLOR_BLACK) {
|
|
this.delete_fixup(fix_node);
|
|
}
|
|
}
|
|
delete_fixup(fix_node) {
|
|
let current_node = fix_node;
|
|
let brother_node;
|
|
while (current_node !== this.root && current_node.parent != null && current_node.color === RB_TREE_COLOR_BLACK) {
|
|
if (current_node === current_node.parent.left) {
|
|
brother_node = current_node.parent.right;
|
|
if (brother_node.color === RB_TREE_COLOR_RED) {
|
|
brother_node.color = RB_TREE_COLOR_BLACK;
|
|
current_node.parent.color = RB_TREE_COLOR_RED;
|
|
this.rotate_left(current_node.parent);
|
|
brother_node = current_node.parent.right;
|
|
}
|
|
if (brother_node.left.color === RB_TREE_COLOR_BLACK && brother_node.right.color === RB_TREE_COLOR_BLACK) {
|
|
brother_node.color = RB_TREE_COLOR_RED;
|
|
current_node = current_node.parent;
|
|
} else {
|
|
if (brother_node.right.color === RB_TREE_COLOR_BLACK) {
|
|
brother_node.color = RB_TREE_COLOR_RED;
|
|
brother_node.left.color = RB_TREE_COLOR_BLACK;
|
|
this.rotate_right(brother_node);
|
|
brother_node = current_node.parent.right;
|
|
}
|
|
brother_node.color = current_node.parent.color;
|
|
current_node.parent.color = RB_TREE_COLOR_BLACK;
|
|
brother_node.right.color = RB_TREE_COLOR_BLACK;
|
|
this.rotate_left(current_node.parent);
|
|
current_node = this.root;
|
|
}
|
|
} else {
|
|
brother_node = current_node.parent.left;
|
|
if (brother_node.color === RB_TREE_COLOR_RED) {
|
|
brother_node.color = RB_TREE_COLOR_BLACK;
|
|
current_node.parent.color = RB_TREE_COLOR_RED;
|
|
this.rotate_right(current_node.parent);
|
|
brother_node = current_node.parent.left;
|
|
}
|
|
if (brother_node.left.color === RB_TREE_COLOR_BLACK && brother_node.right.color === RB_TREE_COLOR_BLACK) {
|
|
brother_node.color = RB_TREE_COLOR_RED;
|
|
current_node = current_node.parent;
|
|
} else {
|
|
if (brother_node.left.color === RB_TREE_COLOR_BLACK) {
|
|
brother_node.color = RB_TREE_COLOR_RED;
|
|
brother_node.right.color = RB_TREE_COLOR_BLACK;
|
|
this.rotate_left(brother_node);
|
|
brother_node = current_node.parent.left;
|
|
}
|
|
brother_node.color = current_node.parent.color;
|
|
current_node.parent.color = RB_TREE_COLOR_BLACK;
|
|
brother_node.left.color = RB_TREE_COLOR_BLACK;
|
|
this.rotate_right(current_node.parent);
|
|
current_node = this.root;
|
|
}
|
|
}
|
|
}
|
|
current_node.color = RB_TREE_COLOR_BLACK;
|
|
}
|
|
tree_search(node, search_node) {
|
|
if (node == null || node === this.nil_node)
|
|
return void 0;
|
|
if (search_node.equal_to(node)) {
|
|
return node;
|
|
}
|
|
if (search_node.less_than(node)) {
|
|
return this.tree_search(node.left, search_node);
|
|
} else {
|
|
return this.tree_search(node.right, search_node);
|
|
}
|
|
}
|
|
tree_search_nearest_forward(node, search_node) {
|
|
let best;
|
|
let curr = node;
|
|
while (curr && curr !== this.nil_node) {
|
|
if (curr.less_than(search_node)) {
|
|
if (curr.intersect(search_node)) {
|
|
best = curr;
|
|
curr = curr.left;
|
|
} else {
|
|
curr = curr.right;
|
|
}
|
|
} else {
|
|
if (!best || curr.less_than(best))
|
|
best = curr;
|
|
curr = curr.left;
|
|
}
|
|
}
|
|
return best || null;
|
|
}
|
|
tree_search_interval(node, search_node, res) {
|
|
if (node != null && node !== this.nil_node) {
|
|
if (node.left !== this.nil_node && !node.not_intersect_left_subtree(search_node)) {
|
|
this.tree_search_interval(node.left, search_node, res);
|
|
}
|
|
if (node.intersect(search_node)) {
|
|
res.push(node);
|
|
}
|
|
if (node.right !== this.nil_node && !node.not_intersect_right_subtree(search_node)) {
|
|
this.tree_search_interval(node.right, search_node, res);
|
|
}
|
|
}
|
|
}
|
|
tree_find_any_interval(node, search_node) {
|
|
let found = false;
|
|
if (node != null && node !== this.nil_node) {
|
|
if (node.left !== this.nil_node && !node.not_intersect_left_subtree(search_node)) {
|
|
found = this.tree_find_any_interval(node.left, search_node);
|
|
}
|
|
if (!found) {
|
|
found = node.intersect(search_node);
|
|
}
|
|
if (!found && node.right !== this.nil_node && !node.not_intersect_right_subtree(search_node)) {
|
|
found = this.tree_find_any_interval(node.right, search_node);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
local_minimum(node) {
|
|
let node_min = node;
|
|
while (node_min.left != null && node_min.left !== this.nil_node) {
|
|
node_min = node_min.left;
|
|
}
|
|
return node_min;
|
|
}
|
|
local_maximum(node) {
|
|
let node_max = node;
|
|
while (node_max.right != null && node_max.right !== this.nil_node) {
|
|
node_max = node_max.right;
|
|
}
|
|
return node_max;
|
|
}
|
|
tree_successor(node) {
|
|
let node_successor;
|
|
let current_node;
|
|
let parent_node;
|
|
if (node.right !== this.nil_node) {
|
|
node_successor = this.local_minimum(node.right);
|
|
} else {
|
|
current_node = node;
|
|
parent_node = node.parent;
|
|
while (parent_node != null && parent_node.right === current_node) {
|
|
current_node = parent_node;
|
|
parent_node = parent_node.parent;
|
|
}
|
|
node_successor = parent_node;
|
|
}
|
|
return node_successor;
|
|
}
|
|
rotate_left(x) {
|
|
let y = x.right;
|
|
x.right = y.left;
|
|
if (y.left !== this.nil_node) {
|
|
y.left.parent = x;
|
|
}
|
|
y.parent = x.parent;
|
|
if (x === this.root) {
|
|
this.root = y;
|
|
} else {
|
|
if (x === x.parent.left) {
|
|
x.parent.left = y;
|
|
} else {
|
|
x.parent.right = y;
|
|
}
|
|
}
|
|
y.left = x;
|
|
x.parent = y;
|
|
if (x != null && x !== this.nil_node) {
|
|
x.update_max();
|
|
}
|
|
y = x.parent;
|
|
if (y != null && y !== this.nil_node) {
|
|
y.update_max();
|
|
}
|
|
}
|
|
rotate_right(y) {
|
|
let x = y.left;
|
|
y.left = x.right;
|
|
if (x.right !== this.nil_node) {
|
|
x.right.parent = y;
|
|
}
|
|
x.parent = y.parent;
|
|
if (y === this.root) {
|
|
this.root = x;
|
|
} else {
|
|
if (y === y.parent.left) {
|
|
y.parent.left = x;
|
|
} else {
|
|
y.parent.right = x;
|
|
}
|
|
}
|
|
x.right = y;
|
|
y.parent = x;
|
|
if (y !== null && y !== this.nil_node) {
|
|
y.update_max();
|
|
}
|
|
x = y.parent;
|
|
if (x != null && x !== this.nil_node) {
|
|
x.update_max();
|
|
}
|
|
}
|
|
tree_walk(node, action) {
|
|
if (node != null && node !== this.nil_node) {
|
|
this.tree_walk(node.left, action);
|
|
action(node);
|
|
this.tree_walk(node.right, action);
|
|
}
|
|
}
|
|
testRedBlackProperty() {
|
|
let res = true;
|
|
this.tree_walk(this.root, function(node) {
|
|
if (node.color === RB_TREE_COLOR_RED) {
|
|
if (!(node.left.color === RB_TREE_COLOR_BLACK && node.right.color === RB_TREE_COLOR_BLACK)) {
|
|
res = false;
|
|
}
|
|
}
|
|
});
|
|
return res;
|
|
}
|
|
testBlackHeightProperty(node) {
|
|
let height = 0;
|
|
let heightLeft = 0;
|
|
let heightRight = 0;
|
|
if (node.color === RB_TREE_COLOR_BLACK) {
|
|
height++;
|
|
}
|
|
if (node.left !== this.nil_node) {
|
|
heightLeft = this.testBlackHeightProperty(node.left);
|
|
} else {
|
|
heightLeft = 1;
|
|
}
|
|
if (node.right !== this.nil_node) {
|
|
heightRight = this.testBlackHeightProperty(node.right);
|
|
} else {
|
|
heightRight = 1;
|
|
}
|
|
if (heightLeft !== heightRight) {
|
|
throw new Error("Red-black height property violated");
|
|
}
|
|
height += heightLeft;
|
|
return height;
|
|
}
|
|
};
|
|
|
|
// linker/liveLinker.ts
|
|
function isDescendant(parent, child, maxDepth = 10) {
|
|
let node = child.parentNode;
|
|
let depth = 0;
|
|
while (node != null && depth < maxDepth) {
|
|
if (node === parent) {
|
|
return true;
|
|
}
|
|
node = node.parentNode;
|
|
depth++;
|
|
}
|
|
return false;
|
|
}
|
|
var VirtualLinkWidget = class extends import_view.WidgetType {
|
|
constructor(match) {
|
|
super();
|
|
this.match = match;
|
|
}
|
|
toDOM(view) {
|
|
return this.match.getCompleteLinkElement();
|
|
}
|
|
};
|
|
var AutoLinkerPlugin = class {
|
|
constructor(view, app, settings, updateManager) {
|
|
this.lastCursorPos = 0;
|
|
this.lastActiveFile = "";
|
|
this.lastViewUpdate = null;
|
|
this.viewUpdateDomToFileMap = /* @__PURE__ */ new Map();
|
|
this.app = app;
|
|
this.settings = settings;
|
|
const { vault } = this.app;
|
|
this.vault = vault;
|
|
this.linkerCache = LinkerCache.getInstance(app, this.settings);
|
|
this.decorations = this.buildDecorations(view);
|
|
updateManager.registerCallback(() => {
|
|
if (this.lastViewUpdate) {
|
|
this.update(this.lastViewUpdate, true);
|
|
}
|
|
});
|
|
}
|
|
update(update, force = false) {
|
|
var _a;
|
|
const activeView = this.app.workspace.getActiveViewOfType(import_obsidian4.MarkdownView);
|
|
let updateIsOnActiveView = false;
|
|
if (this.settings.fixIMEProblem || this.settings.excludeLinksInCurrentLine || this.settings.excludeLinksToOwnNote) {
|
|
const domFromUpdate = update.view.dom;
|
|
const domFromWorkspace = activeView == null ? void 0 : activeView.contentEl;
|
|
updateIsOnActiveView = domFromWorkspace ? isDescendant(domFromWorkspace, domFromUpdate, 3) : false;
|
|
if (updateIsOnActiveView) {
|
|
this.viewUpdateDomToFileMap.set(domFromUpdate, activeView == null ? void 0 : activeView.file);
|
|
}
|
|
}
|
|
const cursorPos = update.view.state.selection.main.from;
|
|
const activeFile = (_a = this.app.workspace.getActiveFile()) == null ? void 0 : _a.path;
|
|
const fileChanged = activeFile != this.lastActiveFile;
|
|
if (force || this.lastCursorPos != cursorPos || update.docChanged || fileChanged || update.viewportChanged) {
|
|
this.lastCursorPos = cursorPos;
|
|
this.linkerCache.updateCache(force);
|
|
this.decorations = this.buildDecorations(update.view, updateIsOnActiveView);
|
|
this.lastActiveFile = activeFile != null ? activeFile : "";
|
|
}
|
|
this.lastViewUpdate = update;
|
|
}
|
|
destroy() {
|
|
}
|
|
buildDecorations(view, viewIsActive = true) {
|
|
var _a, _b, _c, _d;
|
|
const builder = new import_state.RangeSetBuilder();
|
|
if (!this.settings.linkerActivated) {
|
|
return builder.finish();
|
|
}
|
|
const dom = view.dom;
|
|
const mappedFile = this.viewUpdateDomToFileMap.get(dom);
|
|
const excludedFolders = this.settings.excludedDirectoriesForLinking;
|
|
if (excludedFolders.length > 0) {
|
|
const path2 = (_d = (_a = mappedFile == null ? void 0 : mappedFile.parent) == null ? void 0 : _a.path) != null ? _d : (_c = (_b = this.app.workspace.getActiveFile()) == null ? void 0 : _b.parent) == null ? void 0 : _c.path;
|
|
if (excludedFolders.includes(path2 != null ? path2 : ""))
|
|
return builder.finish();
|
|
}
|
|
const explicitlyLinkedFiles = /* @__PURE__ */ new Set();
|
|
const alreadyLinkedFiles = /* @__PURE__ */ new Set();
|
|
for (let { from, to } of view.visibleRanges) {
|
|
this.linkerCache.reset();
|
|
const text = view.state.doc.sliceString(from, to);
|
|
let matches = [];
|
|
let id = 0;
|
|
for (let i = 0; i <= text.length; i) {
|
|
const codePoint = text.codePointAt(i);
|
|
const char = i < text.length ? String.fromCodePoint(codePoint) : "\n";
|
|
const isWordBoundary = PrefixTree.checkWordBoundary(char);
|
|
if (this.settings.matchAnyPartsOfWords || this.settings.matchBeginningOfWords || isWordBoundary) {
|
|
const currentNodes = this.linkerCache.cache.getCurrentMatchNodes(i, this.settings.excludeLinksToOwnNote ? mappedFile : null);
|
|
if (currentNodes.length > 0) {
|
|
for (const node of currentNodes) {
|
|
if (!this.settings.matchAnyPartsOfWords) {
|
|
if (this.settings.matchBeginningOfWords && !node.startsAtWordBoundary && (this.settings.matchEndOfWords && !isWordBoundary)) {
|
|
continue;
|
|
}
|
|
}
|
|
const nFrom = node.start;
|
|
const nTo = node.end;
|
|
const name = text.slice(nFrom, nTo);
|
|
const isAlias = node.isAlias;
|
|
const aFrom = from + nFrom;
|
|
const aTo = from + nTo;
|
|
matches.push(new VirtualMatch(id++, name, aFrom, aTo, Array.from(node.files), isAlias, !isWordBoundary, this.settings));
|
|
}
|
|
}
|
|
}
|
|
this.linkerCache.cache.pushChar(char);
|
|
i += char.length;
|
|
}
|
|
matches = VirtualMatch.sort(matches);
|
|
const excludedIntervalTree = new IntervalTree();
|
|
const excludedTypes = ["codeblock", "code-block", "inline-code", "internal-link", "link", "url", "hashtag"];
|
|
if (!this.settings.includeHeaders) {
|
|
excludedTypes.push("header-");
|
|
}
|
|
const app = this.app;
|
|
(0, import_language.syntaxTree)(view.state).iterate({
|
|
from,
|
|
to,
|
|
enter(node) {
|
|
const type = node.type.name;
|
|
const types = type.split("_");
|
|
for (const excludedType of excludedTypes) {
|
|
if (type.contains(excludedType)) {
|
|
excludedIntervalTree.insert([node.from, node.to]);
|
|
const isLinkIfHavingTypes = [["string", "url"], "hmd-internal-link", "internal-link"];
|
|
isLinkIfHavingTypes.forEach((t) => {
|
|
var _a2;
|
|
const tList = Array.isArray(t) ? t : [t];
|
|
if (tList.every((tt) => types.includes(tt))) {
|
|
const text2 = view.state.doc.sliceString(node.from, node.to);
|
|
const linkedFile = app.metadataCache.getFirstLinkpathDest(text2, (_a2 = mappedFile == null ? void 0 : mappedFile.path) != null ? _a2 : "");
|
|
if (linkedFile) {
|
|
explicitlyLinkedFiles.add(linkedFile);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (this.settings.excludeLinksToRealLinkedFiles) {
|
|
matches = VirtualMatch.filterAlreadyLinked(matches, explicitlyLinkedFiles);
|
|
}
|
|
if (this.settings.onlyLinkOnce) {
|
|
matches = VirtualMatch.filterAlreadyLinked(matches, alreadyLinkedFiles);
|
|
}
|
|
matches = VirtualMatch.filterOverlapping(matches, this.settings.onlyLinkOnce, excludedIntervalTree);
|
|
matches.forEach((addition) => addition.files.forEach((f) => alreadyLinkedFiles.add(f)));
|
|
const cursorPos = view.state.selection.main.from;
|
|
const excludeLine = viewIsActive && this.settings.excludeLinksInCurrentLine;
|
|
const fixIMEProblem = viewIsActive && this.settings.fixIMEProblem;
|
|
let needImeFix = false;
|
|
const lineStart = view.state.doc.lineAt(cursorPos).from;
|
|
const lineEnd = view.state.doc.lineAt(cursorPos).to;
|
|
matches.forEach((addition) => {
|
|
const [from2, to2] = [addition.from, addition.to];
|
|
const cursorNearby = cursorPos >= from2 - 0 && cursorPos <= to2 + 0;
|
|
const additionIsInCurrentLine = from2 >= lineStart && to2 <= lineEnd;
|
|
if (fixIMEProblem) {
|
|
needImeFix = true;
|
|
if (additionIsInCurrentLine && cursorPos > to2) {
|
|
let gapString = view.state.sliceDoc(to2, cursorPos);
|
|
let strBeforeAdd = view.state.sliceDoc(lineStart, from2);
|
|
const regAddInLineStart = /(^\s*$)|(^\s*- +$)|(^\s*#{1,6} $)|(^\s*>+ *$)|(^\s*- +#{1,6} +$)|(^\s*> \[![\w-]+\][+-]? +$)/;
|
|
if (!regAddInLineStart.test(strBeforeAdd)) {
|
|
needImeFix = false;
|
|
} else {
|
|
const regStrMayIMEon = /^[a-zA-Z]+[a-zA-Z' ]*[a-zA-Z]$|^[a-zA-Z]$/;
|
|
if (!regStrMayIMEon.test(gapString) || /[' ]{2}/.test(gapString)) {
|
|
needImeFix = false;
|
|
}
|
|
}
|
|
} else {
|
|
needImeFix = false;
|
|
}
|
|
}
|
|
if (!cursorNearby && !needImeFix && !(excludeLine && additionIsInCurrentLine)) {
|
|
builder.add(from2, to2, import_view.Decoration.replace({
|
|
widget: new VirtualLinkWidget(addition)
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
return builder.finish();
|
|
}
|
|
};
|
|
var pluginSpec = {
|
|
decorations: (value) => value.decorations
|
|
};
|
|
var liveLinkerPlugin = (app, settings, updateManager) => {
|
|
return import_view.ViewPlugin.define((editorView) => {
|
|
return new AutoLinkerPlugin(editorView, app, settings, updateManager);
|
|
}, pluginSpec);
|
|
};
|
|
|
|
// main.ts
|
|
var path = __toESM(require("path"));
|
|
var DEFAULT_SETTINGS = {
|
|
advancedSettings: false,
|
|
linkerActivated: true,
|
|
matchAnyPartsOfWords: false,
|
|
matchEndOfWords: true,
|
|
matchBeginningOfWords: true,
|
|
suppressSuffixForSubWords: false,
|
|
includeAllFiles: true,
|
|
linkerDirectories: ["Glossary"],
|
|
excludedDirectories: [],
|
|
excludedDirectoriesForLinking: [],
|
|
virtualLinkSuffix: "\u{1F517}",
|
|
virtualLinkAliasSuffix: "\u{1F517}",
|
|
useMarkdownLinks: false,
|
|
linkFormat: "shortest",
|
|
defaultUseMarkdownLinks: false,
|
|
defaultLinkFormat: "shortest",
|
|
useDefaultLinkStyleForConversion: true,
|
|
applyDefaultLinkStyling: true,
|
|
includeHeaders: true,
|
|
matchCaseSensitive: false,
|
|
capitalLetterProportionForAutomaticMatchCase: 0.75,
|
|
tagToIgnoreCase: "linker-ignore-case",
|
|
tagToMatchCase: "linker-match-case",
|
|
propertyNameToMatchCase: "linker-match-case",
|
|
propertyNameToIgnoreCase: "linker-ignore-case",
|
|
tagToExcludeFile: "linker-exclude",
|
|
tagToIncludeFile: "linker-include",
|
|
excludeLinksToOwnNote: true,
|
|
fixIMEProblem: false,
|
|
excludeLinksInCurrentLine: false,
|
|
onlyLinkOnce: true,
|
|
excludeLinksToRealLinkedFiles: true,
|
|
includeAliases: true,
|
|
alwaysShowMultipleReferences: false
|
|
};
|
|
var LinkerPlugin = class extends import_obsidian5.Plugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.updateManager = new ExternalUpdateManager();
|
|
}
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.updateManager.registerCallback(() => {
|
|
LinkerCache.getInstance(this.app, this.settings).clearCache();
|
|
});
|
|
this.registerMarkdownPostProcessor((element, context) => {
|
|
context.addChild(new GlossaryLinker(this.app, this.settings, context, element));
|
|
});
|
|
this.registerEditorExtension(liveLinkerPlugin(this.app, this.settings, this.updateManager));
|
|
this.addSettingTab(new LinkerSettingTab(this.app, this));
|
|
this.registerEvent(this.app.workspace.on("file-menu", (menu, file, source) => this.addContextMenuItem(menu, file, source)));
|
|
this.addCommand({
|
|
id: "activate-virtual-linker",
|
|
name: "Activate Virtual Linker",
|
|
checkCallback: (checking) => {
|
|
if (!this.settings.linkerActivated) {
|
|
if (!checking) {
|
|
this.updateSettings({ linkerActivated: true });
|
|
this.updateManager.update();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "deactivate-virtual-linker",
|
|
name: "Deactivate Virtual Linker",
|
|
checkCallback: (checking) => {
|
|
if (this.settings.linkerActivated) {
|
|
if (!checking) {
|
|
this.updateSettings({ linkerActivated: false });
|
|
this.updateManager.update();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "convert-selected-virtual-links",
|
|
name: "Convert All Virtual Links in Selection to Real Links",
|
|
checkCallback: (checking) => {
|
|
var _a;
|
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian5.MarkdownView);
|
|
const editor = view == null ? void 0 : view.editor;
|
|
if (!editor || !editor.somethingSelected()) {
|
|
return false;
|
|
}
|
|
if (checking)
|
|
return true;
|
|
const from = editor.getCursor("from");
|
|
const to = editor.getCursor("to");
|
|
const cmEditor = editor.cm;
|
|
if (!cmEditor)
|
|
return false;
|
|
const selectionRange = cmEditor.dom.querySelector(".cm-content");
|
|
if (!selectionRange)
|
|
return false;
|
|
const virtualLinks = Array.from(selectionRange.querySelectorAll(".virtual-link-a")).filter((link) => link instanceof HTMLElement).map((link) => ({
|
|
element: link,
|
|
from: parseInt(link.getAttribute("from") || "-1"),
|
|
to: parseInt(link.getAttribute("to") || "-1"),
|
|
text: link.getAttribute("origin-text") || "",
|
|
href: link.getAttribute("href") || ""
|
|
})).filter((link) => {
|
|
const linkFrom = editor.offsetToPos(link.from);
|
|
const linkTo = editor.offsetToPos(link.to);
|
|
return this.isPosWithinRange(linkFrom, linkTo, from, to);
|
|
}).sort((a, b) => a.from - b.from);
|
|
if (virtualLinks.length === 0)
|
|
return;
|
|
const replacements = [];
|
|
for (const link of virtualLinks) {
|
|
const targetFile = this.app.vault.getAbstractFileByPath(link.href);
|
|
if (!(targetFile instanceof import_obsidian5.TFile))
|
|
continue;
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const activeFilePath = (_a = activeFile == null ? void 0 : activeFile.path) != null ? _a : "";
|
|
let absolutePath = targetFile.path;
|
|
let relativePath = path.relative(path.dirname(activeFilePath), path.dirname(absolutePath)) + "/" + path.basename(absolutePath);
|
|
relativePath = relativePath.replace(/\\/g, "/");
|
|
const replacementPath = this.app.metadataCache.fileToLinktext(targetFile, activeFilePath);
|
|
const lastPart = replacementPath.split("/").pop();
|
|
const shortestFile = this.app.metadataCache.getFirstLinkpathDest(lastPart, "");
|
|
let shortestPath = (shortestFile == null ? void 0 : shortestFile.path) === targetFile.path ? lastPart : absolutePath;
|
|
if (!replacementPath.endsWith(".md")) {
|
|
if (absolutePath.endsWith(".md"))
|
|
absolutePath = absolutePath.slice(0, -3);
|
|
if (shortestPath.endsWith(".md"))
|
|
shortestPath = shortestPath.slice(0, -3);
|
|
if (relativePath.endsWith(".md"))
|
|
relativePath = relativePath.slice(0, -3);
|
|
}
|
|
const useMarkdownLinks = this.settings.useDefaultLinkStyleForConversion ? this.settings.defaultUseMarkdownLinks : this.settings.useMarkdownLinks;
|
|
const linkFormat = this.settings.useDefaultLinkStyleForConversion ? this.settings.defaultLinkFormat : this.settings.linkFormat;
|
|
let replacement = "";
|
|
if (replacementPath === link.text && linkFormat === "shortest") {
|
|
replacement = `[[${replacementPath}]]`;
|
|
} else {
|
|
const path2 = linkFormat === "shortest" ? shortestPath : linkFormat === "relative" ? relativePath : absolutePath;
|
|
replacement = useMarkdownLinks ? `[${link.text}](${path2})` : `[[${path2}|${link.text}]]`;
|
|
}
|
|
replacements.push({
|
|
from: link.from,
|
|
to: link.to,
|
|
text: replacement
|
|
});
|
|
}
|
|
for (const replacement of replacements.reverse()) {
|
|
const fromPos = editor.offsetToPos(replacement.from);
|
|
const toPos = editor.offsetToPos(replacement.to);
|
|
editor.replaceRange(replacement.text, fromPos, toPos);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
isPosWithinRange(linkFrom, linkTo, selectionFrom, selectionTo) {
|
|
return (linkFrom.line > selectionFrom.line || linkFrom.line === selectionFrom.line && linkFrom.ch >= selectionFrom.ch) && (linkTo.line < selectionTo.line || linkTo.line === selectionTo.line && linkTo.ch <= selectionTo.ch);
|
|
}
|
|
addContextMenuItem(menu, file, source) {
|
|
if (!file) {
|
|
return;
|
|
}
|
|
const that = this;
|
|
const app = this.app;
|
|
const updateManager = this.updateManager;
|
|
const settings = this.settings;
|
|
const fetcher = new LinkerMetaInfoFetcher(app, settings);
|
|
const isDirectory = app.vault.getAbstractFileByPath(file.path) instanceof import_obsidian5.TFolder;
|
|
if (!isDirectory) {
|
|
let contextMenuHandler = function(event) {
|
|
const targetElement = event.target;
|
|
if (!targetElement || !(targetElement instanceof HTMLElement)) {
|
|
console.error("No target element");
|
|
return;
|
|
}
|
|
const isVirtualLink = targetElement.classList.contains("virtual-link-a");
|
|
const from = parseInt(targetElement.getAttribute("from") || "-1");
|
|
const to = parseInt(targetElement.getAttribute("to") || "-1");
|
|
if (from === -1 || to === -1) {
|
|
menu.addItem((item) => {
|
|
item.setTitle("[Virtual Linker] Converting link is not here.").setIcon("link");
|
|
});
|
|
} else if (isVirtualLink) {
|
|
menu.addItem((item) => {
|
|
item.setTitle("[Virtual Linker] Convert to real link").setIcon("link").onClick(() => {
|
|
var _a, _b;
|
|
const from2 = parseInt(targetElement.getAttribute("from") || "-1");
|
|
const to2 = parseInt(targetElement.getAttribute("to") || "-1");
|
|
if (from2 === -1 || to2 === -1) {
|
|
console.error("No from or to position");
|
|
return;
|
|
}
|
|
const text = targetElement.getAttribute("origin-text") || "";
|
|
const target = file;
|
|
const activeFile = app.workspace.getActiveFile();
|
|
const activeFilePath = (_a = activeFile == null ? void 0 : activeFile.path) != null ? _a : "";
|
|
if (!activeFile) {
|
|
console.error("No active file");
|
|
return;
|
|
}
|
|
let absolutePath = target.path;
|
|
let relativePath = path.relative(path.dirname(activeFile.path), path.dirname(absolutePath)) + "/" + path.basename(absolutePath);
|
|
relativePath = relativePath.replace(/\\/g, "/");
|
|
const replacementPath = app.metadataCache.fileToLinktext(target, activeFilePath);
|
|
const lastPart = replacementPath.split("/").pop();
|
|
const shortestFile = app.metadataCache.getFirstLinkpathDest(lastPart, "");
|
|
let shortestPath = (shortestFile == null ? void 0 : shortestFile.path) == target.path ? lastPart : absolutePath;
|
|
if (!replacementPath.endsWith(".md")) {
|
|
if (absolutePath.endsWith(".md")) {
|
|
absolutePath = absolutePath.slice(0, -3);
|
|
}
|
|
if (shortestPath.endsWith(".md")) {
|
|
shortestPath = shortestPath.slice(0, -3);
|
|
}
|
|
if (relativePath.endsWith(".md")) {
|
|
relativePath = relativePath.slice(0, -3);
|
|
}
|
|
}
|
|
const useMarkdownLinks = settings.useDefaultLinkStyleForConversion ? settings.defaultUseMarkdownLinks : settings.useMarkdownLinks;
|
|
const linkFormat = settings.useDefaultLinkStyleForConversion ? settings.defaultLinkFormat : settings.linkFormat;
|
|
const createLink = (replacementPath2, text2, markdownStyle) => {
|
|
if (markdownStyle) {
|
|
return `[${text2}](${replacementPath2})`;
|
|
} else {
|
|
return `[[${replacementPath2}|${text2}]]`;
|
|
}
|
|
};
|
|
let replacement = "";
|
|
if (replacementPath === text && linkFormat === "shortest") {
|
|
replacement = `[[${replacementPath}]]`;
|
|
} else {
|
|
if (linkFormat === "shortest") {
|
|
replacement = createLink(shortestPath, text, useMarkdownLinks);
|
|
} else if (linkFormat === "relative") {
|
|
replacement = createLink(relativePath, text, useMarkdownLinks);
|
|
} else if (linkFormat === "absolute") {
|
|
replacement = createLink(absolutePath, text, useMarkdownLinks);
|
|
}
|
|
}
|
|
const editor = (_b = app.workspace.getActiveViewOfType(import_obsidian5.MarkdownView)) == null ? void 0 : _b.editor;
|
|
const fromEditorPos = editor == null ? void 0 : editor.offsetToPos(from2);
|
|
const toEditorPos = editor == null ? void 0 : editor.offsetToPos(to2);
|
|
if (!fromEditorPos || !toEditorPos) {
|
|
console.warn("No editor positions");
|
|
return;
|
|
}
|
|
editor == null ? void 0 : editor.replaceRange(replacement, fromEditorPos, toEditorPos);
|
|
});
|
|
});
|
|
}
|
|
document.removeEventListener("contextmenu", contextMenuHandler);
|
|
};
|
|
const metaInfo = fetcher.getMetaInfo(file);
|
|
if (!metaInfo.excludeFile && (metaInfo.includeAllFiles || metaInfo.includeFile || metaInfo.isInIncludedDir)) {
|
|
menu.addItem((item) => {
|
|
item.setTitle("[Virtual Linker] Exclude this file").setIcon("trash").onClick(async () => {
|
|
const target = file;
|
|
const targetFile = app.vault.getFileByPath(target.path);
|
|
if (!targetFile) {
|
|
console.error("No target file");
|
|
return;
|
|
}
|
|
const fileCache = app.metadataCache.getFileCache(targetFile);
|
|
const frontmatter = (fileCache == null ? void 0 : fileCache.frontmatter) || {};
|
|
const tag = settings.tagToExcludeFile;
|
|
let tags = frontmatter["tags"];
|
|
if (typeof tags === "string") {
|
|
tags = [tags];
|
|
}
|
|
if (!Array.isArray(tags)) {
|
|
tags = [];
|
|
}
|
|
if (!tags.includes(tag)) {
|
|
await app.fileManager.processFrontMatter(targetFile, (frontMatter) => {
|
|
if (!frontMatter.tags) {
|
|
frontMatter.tags = /* @__PURE__ */ new Set();
|
|
}
|
|
const currentTags = [...frontMatter.tags];
|
|
frontMatter.tags = /* @__PURE__ */ new Set([...currentTags, tag]);
|
|
const includeTag = settings.tagToIncludeFile;
|
|
if (frontMatter.tags.has(includeTag)) {
|
|
frontMatter.tags.delete(includeTag);
|
|
}
|
|
});
|
|
updateManager.update();
|
|
}
|
|
});
|
|
});
|
|
} else if (!metaInfo.includeFile && (!metaInfo.includeAllFiles || metaInfo.excludeFile || metaInfo.isInExcludedDir)) {
|
|
menu.addItem((item) => {
|
|
item.setTitle("[Virtual Linker] Include this file").setIcon("plus").onClick(async () => {
|
|
const target = file;
|
|
const targetFile = app.vault.getFileByPath(target.path);
|
|
if (!targetFile) {
|
|
console.error("No target file");
|
|
return;
|
|
}
|
|
const fileCache = app.metadataCache.getFileCache(targetFile);
|
|
const frontmatter = (fileCache == null ? void 0 : fileCache.frontmatter) || {};
|
|
const tag = settings.tagToIncludeFile;
|
|
let tags = frontmatter["tags"];
|
|
if (typeof tags === "string") {
|
|
tags = [tags];
|
|
}
|
|
if (!Array.isArray(tags)) {
|
|
tags = [];
|
|
}
|
|
if (!tags.includes(tag)) {
|
|
await app.fileManager.processFrontMatter(targetFile, (frontMatter) => {
|
|
if (!frontMatter.tags) {
|
|
frontMatter.tags = /* @__PURE__ */ new Set();
|
|
}
|
|
const currentTags = [...frontMatter.tags];
|
|
frontMatter.tags = /* @__PURE__ */ new Set([...currentTags, tag]);
|
|
const excludeTag = settings.tagToExcludeFile;
|
|
if (frontMatter.tags.has(excludeTag)) {
|
|
frontMatter.tags.delete(excludeTag);
|
|
}
|
|
});
|
|
updateManager.update();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
document.addEventListener("contextmenu", contextMenuHandler, { once: true });
|
|
} else {
|
|
const path2 = file.path + "/";
|
|
const isInIncludedDir = fetcher.includeDirPattern.test(path2);
|
|
const isInExcludedDir = fetcher.excludeDirPattern.test(path2);
|
|
if (fetcher.includeAllFiles && !isInExcludedDir || isInIncludedDir) {
|
|
menu.addItem((item) => {
|
|
item.setTitle("[Virtual Linker] Exclude this directory").setIcon("trash").onClick(async () => {
|
|
const target = file;
|
|
const targetFolder = app.vault.getAbstractFileByPath(target.path);
|
|
if (!targetFolder) {
|
|
console.error("No target folder");
|
|
return;
|
|
}
|
|
const newExcludedDirs = Array.from(/* @__PURE__ */ new Set([...settings.excludedDirectories, targetFolder.name]));
|
|
const newIncludedDirs = settings.linkerDirectories.filter((dir) => dir !== targetFolder.name);
|
|
await this.updateSettings({ linkerDirectories: newIncludedDirs, excludedDirectories: newExcludedDirs });
|
|
updateManager.update();
|
|
});
|
|
});
|
|
} else if (!fetcher.includeAllFiles && !isInIncludedDir || isInExcludedDir) {
|
|
menu.addItem((item) => {
|
|
item.setTitle("[Virtual Linker] Include this directory").setIcon("plus").onClick(async () => {
|
|
const target = file;
|
|
const targetFolder = app.vault.getAbstractFileByPath(target.path);
|
|
if (!targetFolder) {
|
|
console.error("No target folder");
|
|
return;
|
|
}
|
|
const newExcludedDirs = settings.excludedDirectories.filter((dir) => dir !== targetFolder.name);
|
|
const newIncludedDirs = Array.from(/* @__PURE__ */ new Set([...settings.linkerDirectories, targetFolder.name]));
|
|
await this.updateSettings({ linkerDirectories: newIncludedDirs, excludedDirectories: newExcludedDirs });
|
|
updateManager.update();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
onunload() {
|
|
}
|
|
async loadSettings() {
|
|
var _a;
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
const fileContent = await this.app.vault.adapter.read(this.app.vault.configDir + "/app.json");
|
|
const appSettings = JSON.parse(fileContent);
|
|
this.settings.defaultUseMarkdownLinks = appSettings.useMarkdownLinks;
|
|
this.settings.defaultLinkFormat = (_a = appSettings.newLinkFormat) != null ? _a : "shortest";
|
|
}
|
|
async updateSettings(settings = {}) {
|
|
Object.assign(this.settings, settings);
|
|
await this.saveData(this.settings);
|
|
this.updateManager.update();
|
|
}
|
|
};
|
|
var LinkerSettingTab = class extends import_obsidian5.PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
new import_obsidian5.Setting(containerEl).setName("Activate Virtual Linker").addToggle((toggle) => toggle.setValue(this.plugin.settings.linkerActivated).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ linkerActivated: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Show advanced settings").addToggle((toggle) => toggle.setValue(this.plugin.settings.advancedSettings).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ advancedSettings: value });
|
|
this.display();
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Matching behavior").setHeading();
|
|
new import_obsidian5.Setting(containerEl).setName("Include aliases").setDesc("If activated, the virtual linker will also include aliases for the files.").addToggle((toggle) => toggle.setValue(this.plugin.settings.includeAliases).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ includeAliases: value });
|
|
}));
|
|
if (this.plugin.settings.advancedSettings) {
|
|
new import_obsidian5.Setting(containerEl).setName("Only link once").setDesc("If activated, there will not be several identical virtual links in the same note (Wikipedia style).").addToggle((toggle) => toggle.setValue(this.plugin.settings.onlyLinkOnce).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ onlyLinkOnce: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Exclude links to real linked files").setDesc("If activated, there will be no links to files that are already linked in the note by real links.").addToggle((toggle) => toggle.setValue(this.plugin.settings.excludeLinksToRealLinkedFiles).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ excludeLinksToRealLinkedFiles: value });
|
|
}));
|
|
}
|
|
new import_obsidian5.Setting(containerEl).setName("Include headers").setDesc("If activated, headers (so your lines beginning with at least one `#`) are included for virtual links.").addToggle((toggle) => toggle.setValue(this.plugin.settings.includeHeaders).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ includeHeaders: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Match any part of a word").setDesc("If deactivated, only whole words are matched. Otherwise, every part of a word is found.").addToggle((toggle) => toggle.setValue(this.plugin.settings.matchAnyPartsOfWords).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ matchAnyPartsOfWords: value });
|
|
this.display();
|
|
}));
|
|
if (!this.plugin.settings.matchAnyPartsOfWords) {
|
|
new import_obsidian5.Setting(containerEl).setName("Match the beginning of words").setDesc("If activated, the beginnings of words are also linked, even if it is not a whole match.").addToggle((toggle) => toggle.setValue(this.plugin.settings.matchBeginningOfWords).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ matchBeginningOfWords: value });
|
|
this.display();
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Match the end of words").setDesc("If activated, the ends of words are also linked, even if it is not a whole match.").addToggle((toggle) => toggle.setValue(this.plugin.settings.matchEndOfWords).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ matchEndOfWords: value });
|
|
this.display();
|
|
}));
|
|
}
|
|
if (this.plugin.settings.matchAnyPartsOfWords || this.plugin.settings.matchBeginningOfWords) {
|
|
new import_obsidian5.Setting(containerEl).setName("Suppress suffix for sub words").setDesc("If activated, the suffix is not added to links for subwords, but only for complete matches.").addToggle((toggle) => toggle.setValue(this.plugin.settings.suppressSuffixForSubWords).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ suppressSuffixForSubWords: value });
|
|
}));
|
|
}
|
|
if (this.plugin.settings.advancedSettings) {
|
|
new import_obsidian5.Setting(containerEl).setName("Fix IME problem").setDesc("If activated, there will be no links in the current line start which is followed immediately by the Input Method Editor (IME). This is the recommended setting if you are using IME (input method editor) for typing, e.g. for chinese characters, because instant linking might interfere with IME.").addToggle((toggle) => toggle.setValue(this.plugin.settings.fixIMEProblem).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ fixIMEProblem: value });
|
|
}));
|
|
}
|
|
if (this.plugin.settings.advancedSettings) {
|
|
new import_obsidian5.Setting(containerEl).setName("Avoid linking in current line").setDesc("If activated, there will be no links in the current line.").addToggle((toggle) => toggle.setValue(this.plugin.settings.excludeLinksInCurrentLine).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ excludeLinksInCurrentLine: value });
|
|
}));
|
|
}
|
|
new import_obsidian5.Setting(containerEl).setName("Case sensitivity").setHeading();
|
|
new import_obsidian5.Setting(containerEl).setName("Case sensitive").setDesc("If activated, the matching is case sensitive.").addToggle((toggle) => toggle.setValue(this.plugin.settings.matchCaseSensitive).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ matchCaseSensitive: value });
|
|
this.display();
|
|
}));
|
|
if (this.plugin.settings.advancedSettings) {
|
|
new import_obsidian5.Setting(containerEl).setName("Capital letter percentage for automatic match case").setDesc("The percentage (0 - 100) of capital letters in a file name or alias to be automatically considered as case sensitive.").addText((text) => text.setValue((this.plugin.settings.capitalLetterProportionForAutomaticMatchCase * 100).toFixed(1)).onChange(async (value) => {
|
|
let newValue = parseFloat(value);
|
|
if (isNaN(newValue)) {
|
|
newValue = 75;
|
|
} else if (newValue < 0) {
|
|
newValue = 0;
|
|
} else if (newValue > 100) {
|
|
newValue = 100;
|
|
}
|
|
newValue /= 100;
|
|
await this.plugin.updateSettings({ capitalLetterProportionForAutomaticMatchCase: newValue });
|
|
}));
|
|
if (this.plugin.settings.matchCaseSensitive) {
|
|
new import_obsidian5.Setting(containerEl).setName("Tag to ignore case").setDesc("By adding this tag to a file, the linker will ignore the case for the file.").addText((text) => text.setValue(this.plugin.settings.tagToIgnoreCase).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ tagToIgnoreCase: value });
|
|
}));
|
|
} else {
|
|
new import_obsidian5.Setting(containerEl).setName("Tag to match case").setDesc("By adding this tag to a file, the linker will match the case for the file.").addText((text) => text.setValue(this.plugin.settings.tagToMatchCase).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ tagToMatchCase: value });
|
|
}));
|
|
}
|
|
new import_obsidian5.Setting(containerEl).setName("Property name to ignore case").setDesc("By adding this property to a note, containing a list of names, the linker will ignore the case for the specified names / aliases. This way you can decide, which alias should be insensitive.").addText((text) => text.setValue(this.plugin.settings.propertyNameToIgnoreCase).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ propertyNameToIgnoreCase: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Property name to match case").setDesc("By adding this property to a note, containing a list of names, the linker will match the case for the specified names / aliases. This way you can decide, which alias should be case sensitive.").addText((text) => text.setValue(this.plugin.settings.propertyNameToMatchCase).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ propertyNameToMatchCase: value });
|
|
}));
|
|
}
|
|
new import_obsidian5.Setting(containerEl).setName("Matched files").setHeading();
|
|
new import_obsidian5.Setting(containerEl).setName("Include all files").setDesc("Include all files for the virtual linker.").addToggle((toggle) => toggle.setValue(this.plugin.settings.includeAllFiles).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ includeAllFiles: value });
|
|
this.display();
|
|
}));
|
|
if (!this.plugin.settings.includeAllFiles) {
|
|
new import_obsidian5.Setting(containerEl).setName("Glossary linker directories").setDesc("Directories to include for the virtual linker (separated by new lines).").addTextArea((text) => {
|
|
let setValue = "";
|
|
try {
|
|
setValue = this.plugin.settings.linkerDirectories.join("\n");
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
text.setPlaceholder("List of directory names (separated by new line)").setValue(setValue).onChange(async (value) => {
|
|
this.plugin.settings.linkerDirectories = value.split("\n").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
await this.plugin.updateSettings();
|
|
});
|
|
text.inputEl.addClass("linker-settings-text-box");
|
|
});
|
|
} else {
|
|
if (this.plugin.settings.advancedSettings) {
|
|
new import_obsidian5.Setting(containerEl).setName("Excluded directories").setDesc("Directories from which files are to be excluded for the virtual linker (separated by new lines). Files in these directories will not create any virtual links in other files.").addTextArea((text) => {
|
|
let setValue = "";
|
|
try {
|
|
setValue = this.plugin.settings.excludedDirectories.join("\n");
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
text.setPlaceholder("List of directory names (separated by new line)").setValue(setValue).onChange(async (value) => {
|
|
this.plugin.settings.excludedDirectories = value.split("\n").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
await this.plugin.updateSettings();
|
|
});
|
|
text.inputEl.addClass("linker-settings-text-box");
|
|
});
|
|
}
|
|
}
|
|
if (this.plugin.settings.advancedSettings) {
|
|
new import_obsidian5.Setting(containerEl).setName("Tag to include file").setDesc("Tag to explicitly include the file for the linker.").addText((text) => text.setValue(this.plugin.settings.tagToIncludeFile).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ tagToIncludeFile: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Tag to ignore file").setDesc("Tag to ignore the file for the linker.").addText((text) => text.setValue(this.plugin.settings.tagToExcludeFile).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ tagToExcludeFile: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Exclude self-links to the current note").setDesc("If toggled, links to the note itself are excluded from the linker. (This might not work in preview windows.)").addToggle((toggle) => toggle.setValue(this.plugin.settings.excludeLinksToOwnNote).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ excludeLinksToOwnNote: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Excluded directories for generating virtual links").setDesc("Directories in which the plugin will not create virtual links (separated by new lines).").addTextArea((text) => {
|
|
let setValue = "";
|
|
try {
|
|
setValue = this.plugin.settings.excludedDirectoriesForLinking.join("\n");
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
text.setPlaceholder("List of directory names (separated by new line)").setValue(setValue).onChange(async (value) => {
|
|
this.plugin.settings.excludedDirectoriesForLinking = value.split("\n").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
await this.plugin.updateSettings();
|
|
});
|
|
text.inputEl.addClass("linker-settings-text-box");
|
|
});
|
|
}
|
|
new import_obsidian5.Setting(containerEl).setName("Link style").setHeading();
|
|
new import_obsidian5.Setting(containerEl).setName("Always show multiple references").setDesc("If toggled, if there are multiple matching notes, all references are shown behind the match. If not toggled, the references are only shown if hovering over the match.").addToggle((toggle) => toggle.setValue(this.plugin.settings.alwaysShowMultipleReferences).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ alwaysShowMultipleReferences: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Virtual link suffix").setDesc("The suffix to add to auto generated virtual links.").addText((text) => text.setValue(this.plugin.settings.virtualLinkSuffix).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ virtualLinkSuffix: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Virtual link suffix for aliases").setDesc("The suffix to add to auto generated virtual links for aliases.").addText((text) => text.setValue(this.plugin.settings.virtualLinkAliasSuffix).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ virtualLinkAliasSuffix: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Apply default link styling").setDesc("If toggled, the default link styling will be applied to virtual links. Furthermore, you can style the links yourself with a CSS-snippet affecting the class `virtual-link`. (Find the CSS snippet directory at Appearance -> CSS Snippets -> Open snippets folder)").addToggle((toggle) => toggle.setValue(this.plugin.settings.applyDefaultLinkStyling).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ applyDefaultLinkStyling: value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Use default link style for conversion").setDesc("If toggled, the default link style will be used for the conversion of virtual links to real links.").addToggle((toggle) => toggle.setValue(this.plugin.settings.useDefaultLinkStyleForConversion).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ useDefaultLinkStyleForConversion: value });
|
|
this.display();
|
|
}));
|
|
if (!this.plugin.settings.useDefaultLinkStyleForConversion) {
|
|
new import_obsidian5.Setting(containerEl).setName("Use [[Wiki-links]]").setDesc("If toggled, the virtual links will be created as wiki-links instead of markdown links.").addToggle((toggle) => toggle.setValue(!this.plugin.settings.useMarkdownLinks).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ useMarkdownLinks: !value });
|
|
}));
|
|
new import_obsidian5.Setting(containerEl).setName("Link format").setDesc("The format of the generated links.").addDropdown((dropdown) => dropdown.addOption("shortest", "Shortest").addOption("relative", "Relative").addOption("absolute", "Absolute").setValue(this.plugin.settings.linkFormat).onChange(async (value) => {
|
|
await this.plugin.updateSettings({ linkFormat: value });
|
|
}));
|
|
}
|
|
}
|
|
};
|