Skip to content

Commit

Permalink
Merge pull request #9 from dalcantara7/heading-dev
Browse files Browse the repository at this point in the history
Added support for anchor links to headings
  • Loading branch information
dalcantara7 authored Jul 23, 2023
2 parents e1fdf98 + f9ab077 commit c8d8089
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 44 deletions.
28 changes: 18 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ There are two commands that are currently supported: <br>

2. Import notes based on tagged mentions

- The plugin checks for tags matching the current file's name
- The plugin checks for tags matching the tag selected from a modal popup

* **NEW**: Support for frontmatter tags has been added

After enabling this plugin, place your cursor in an editable markdown note where you want the links to be added.

Expand All @@ -35,7 +37,7 @@ Command 2 can be through the command pallette, or a keyboard shortcut (must be m

![demo](assets/modal-demo.gif)

<br>
### Frontmatter Support

The plugin will check to see if a note has aliases in its frontmatter. If it finds one, it will import the note with the alias so that the link will be

Expand All @@ -49,20 +51,30 @@ ALIAS
in Preview Mode
```

NOTE: you must use the proper YAML frontmatter structure shown below. Otherwise, the alias will not be recognized and the note will be imported without the alias
NOTE: you must use the proper YAML frontmatter structure shown below. Otherwise, the alias/tag will not be recognized

```
aliases: [Example, Example2]
aliases: [alias1, alias2]
tags: tag1, tag2
```

or

```
aliases:
- Example
- Example2
- alias1
- alias2
tags:
- tag1
- tag2
```

Frontmatter tags do not have a "#" prepended to them. Putting a "#" before a frontmatter tag is invalid YAML and will cause the tag not to be recognized

<br>

**NEW**: The plugin now has support for anchor links (linking to headings). This option is available in the plugin settings. When on, the plugin will search for the heading closest to the link or tag specified. This is done a greedy manner

## Known issues

When creating a new link or tag, it may take a while for Obsidian to resolve the link/tag. In this time, the new link/tag may not be visible to the plugin and it will report "No new links found". To avoid this wait a couple of seconds between creating a new link/tag and running the AutoMOC plugin.
Expand All @@ -74,7 +86,3 @@ It is possible there are other bugs. If you come across one, please do report it
This plugin is provided to everyone for free, however if you would like to say thanks or help support continued development, consider getting me a coffee. It keeps my work going.

<a href="https://www.buymeacoffee.com/dalca7" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>

## Notes

This plugin has **not** yet been tested on mobile.
205 changes: 172 additions & 33 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import {

interface AutoMOCSettings {
showRibbonButton: boolean;
linkToHeading: boolean;
}

const DEFAULT_SETTINGS: AutoMOCSettings = {
showRibbonButton: true,
linkToHeading: false,
};

export class TagSuggestModal extends FuzzySuggestModal<string> {
Expand All @@ -25,16 +27,10 @@ export class TagSuggestModal extends FuzzySuggestModal<string> {
constructor(app: App, plugin: AutoMOC) {
super(app);
this.plugin = plugin;

this.modalEl.prepend(
this.modalEl.createEl("h2", {
text: "Import notes with tags matching",
})
);
}

onOpen(): void {
this.setPlaceholder("Type the name of a tag...");
this.setPlaceholder("Import notes with tags matching...");
this.setInstructions([
{ command: "↑↓", purpose: "to navigate" },
{ command: "↵", purpose: "to select tag" },
Expand All @@ -54,14 +50,24 @@ export class TagSuggestModal extends FuzzySuggestModal<string> {
let tagsSet = new Set<string>();

Object.keys(allFiles).forEach((key) => {
let file = this.app.vault.getAbstractFileByPath(key);
const file = this.app.vault.getAbstractFileByPath(key);
if (file instanceof TFile) {
const tags = app.metadataCache.getFileCache(file).tags;
if (tags) {
for (const tag of tags) {
const body_tags = app.metadataCache.getFileCache(file).tags;
const frontmatter =
this.app.metadataCache.getFileCache(file).frontmatter;

if (body_tags) {
for (const tag of body_tags) {
tagsSet.add(tag["tag"]);
}
}

if (frontmatter && String.isString(frontmatter["tags"])) {
const f_tags = frontmatter["tags"].split(", ");
for (const f_tag of f_tags) {
tagsSet.add("#" + f_tag);
}
}
}
});

Expand Down Expand Up @@ -91,6 +97,7 @@ export default class AutoMOC extends Plugin {
getLinkedMentions(currFilePath: string) {
const allFiles = this.app.metadataCache.resolvedLinks;
let linkedMentions: Array<string> = [];

Object.keys(allFiles).forEach((key) => {
if (currFilePath in allFiles[key]) {
linkedMentions.push(key);
Expand All @@ -100,44 +107,57 @@ export default class AutoMOC extends Plugin {
return linkedMentions.sort();
}

getTaggedMentions(currFileName: string) {
getTaggedMentions(tag: string) {
const allFiles = this.app.metadataCache.resolvedLinks;
let taggedMentions: Array<string> = [];
const toCompare = currFileName;
const toCompare = tag.replace("#", "");

Object.keys(allFiles).forEach((key) => {
let file = this.app.vault.getAbstractFileByPath(key);
const file = this.app.vault.getAbstractFileByPath(key);
if (file instanceof TFile) {
const tags = app.metadataCache.getFileCache(file).tags;
if (tags) {
for (const tag of tags) {
const body_tags = app.metadataCache.getFileCache(file).tags;
const frontmatter =
this.app.metadataCache.getFileCache(file).frontmatter;

if (body_tags) {
for (const tag of body_tags) {
if (tag["tag"] === toCompare) {
taggedMentions.push(file.path);
}
}
}

if (frontmatter && String.isString(frontmatter["tags"])) {
const f_tags = frontmatter["tags"].split(", ");
for (const f_tag of f_tags) {
if (f_tag == toCompare) {
taggedMentions.push(file.path);
}
}
}
}
});

return taggedMentions;
}

addMissingLinks(
async addMissingLinks(
activeFileView: MarkdownView,
presentLinks: Array<string>,
allLinkedMentions: Array<string>
allLinkedMentions: Array<string>,
tag?: string
) {
let addFlag = false;

//checks for missing links and adds them
for (const path of allLinkedMentions) {
if (!presentLinks.includes(path)) {
let found = this.app.vault.getAbstractFileByPath(path);
const file = this.app.vault.getAbstractFileByPath(path);

if (found instanceof TFile) {
if (file instanceof TFile) {
//check for aliases
const fileAliases =
this.app.metadataCache.getFileCache(found).frontmatter;
this.app.metadataCache.getFileCache(file).frontmatter;
let alias = "";

if (
Expand All @@ -148,13 +168,44 @@ export default class AutoMOC extends Plugin {
alias = "|" + fileAliases.aliases[0];
}

activeFileView.editor.replaceSelection(
this.app.fileManager.generateMarkdownLink(
found,
activeFileView.file.path,
(alias = alias)
) + "\n"
);
let closestHeading = "";

if (this.settings.linkToHeading) {
const headingsLocations =
await this.getHeadingsLocationsInFile(path);
const linkTagLocation =
await this.getLinkTagLocationInFile(
activeFileView,
path,
tag
);
closestHeading = this.determineClosestHeading(
headingsLocations,
linkTagLocation
);
}

if (closestHeading) {
//if there is a closest heading, link to heading
activeFileView.editor.replaceSelection(
this.app.fileManager.generateMarkdownLink(
file,
activeFileView.file.path,
"#" + closestHeading,
(alias = alias)
) + "\n"
);
} else {
//otherwise just link to note without heading
activeFileView.editor.replaceSelection(
this.app.fileManager.generateMarkdownLink(
file,
activeFileView.file.path,
(alias = alias)
) + "\n"
);
}

addFlag = true;
}
}
Expand All @@ -163,21 +214,95 @@ export default class AutoMOC extends Plugin {
if (!addFlag) new Notice("No new links found");
}

async getHeadingsLocationsInFile(filePath: string) {
const file = this.app.vault.getAbstractFileByPath(filePath);

if (file instanceof TFile) {
const fileContent = await this.app.vault.read(file);
const lines = fileContent.split("\n");
const regXHeader = /#{1,6}\s.+(?=)/g;
let headings: Array<string> = [];

lines.forEach((line) => {
const match = line.match(regXHeader);
if (match) headings.push(match[0].replace(/#{1,6}\s/g, ""));
else headings.push("-1");
});

return headings;
}
}

async getLinkTagLocationInFile(
activeFileView: MarkdownView,
filePath: string,
tag?: string
) {
const file = this.app.vault.getAbstractFileByPath(filePath);

if (file instanceof TFile) {
const fileContent = await this.app.vault.read(file);
const lines = fileContent.split("\n");
let lineContent: Array<string> = [];
const activeFileName = activeFileView.file.name.substring(
0,
activeFileView.file.name.length - 3
);

let toSearch = "";
if (!tag) {
toSearch = "[[" + activeFileName + "]]"; // -3 in order to remove ".md" from filepath
} else toSearch = tag;

lines.forEach((line) => {
if (line.includes(toSearch)) lineContent.push(toSearch);
else lineContent.push("-1");
});

return lineContent.indexOf(toSearch);
}
}

determineClosestHeading(
headingsLocations: Array<string>,
linkTagLocation: number
) {
let distances: Array<number> = [];

headingsLocations.forEach((item, index) => {
if (item != "-1") distances.push(Math.abs(index - linkTagLocation));
else distances.push(-1);
});

let minIndex = -1;
let minValue = Infinity;
for (let len = distances.length, i = 0; i < len; i += 1) {
if (distances[i] != -1) {
if (distances[i] < minValue) {
minIndex = i;
minValue = distances[i];
}
}
}

return headingsLocations[minIndex];
}

runAutoMOC(tag?: string) {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);

if (view != null && view.file.extension === "md") {
new Notice("Linking mentions");
const presentLinks = this.getPresentLinks(view.file.path); // links already in the document

let linkedMentions: Array<string>;
let linkTagMentions: Array<string>;
if (!tag) {
linkedMentions = this.getLinkedMentions(view.file.path); // all linked mentions even those not present
linkTagMentions = this.getLinkedMentions(view.file.path); // all linked mentions even those not present
} else {
linkedMentions = this.getTaggedMentions(tag); // tagged mentions are looked up by basename rather than path
linkTagMentions = this.getTaggedMentions(tag); // tagged mentions are looked up by basename rather than path
}

this.addMissingLinks(view, presentLinks, linkedMentions);
this.addMissingLinks(view, presentLinks, linkTagMentions, tag);
} else {
new Notice(
"Failed to link mentions, file type is not a markdown file"
Expand Down Expand Up @@ -259,5 +384,19 @@ class AutoMOCSettingTab extends PluginSettingTab {
this.plugin.saveSettings();
});
});

new Setting(containerEl)
.setName("Link to heading")
.setDesc(
"Creates the link to the heading closest to the link/tag. This is performed in a greedy manner"
)
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.linkToHeading)
.onChange((linkToHeading) => {
this.plugin.settings.linkToHeading = linkToHeading;
this.plugin.saveSettings();
});
});
}
}
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "auto-moc",
"name": "AutoMOC",
"version": "1.1.1",
"version": "1.3.0",
"minAppVersion": "0.12.0",
"description": "Looks for missing linked mentions or notes with a specific tag and imports them into the current note.",
"author": "Diego Alcantara",
Expand Down

0 comments on commit c8d8089

Please sign in to comment.