Skip to content

Commit

Permalink
Add editor and frontmatter support
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenkuro committed Feb 22, 2023
1 parent a715d81 commit b605e6b
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 179 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Obsidian Base Tag Renderer

This plugin renders only the basename for tags in preview mode while maintaining the nested strucutres elsewhere.
This plugin renders only the basename for tags while maintaining the nested strucutres elsewhere.

It also appends a new class name (`basename-tag`) so it's possible to add custom style to it.

Expand All @@ -19,3 +19,8 @@ a.basename-tag[href*="dog"]::before {
content: "🐶 ";
}
```

# Version 1.1 (Feb 23rd, 2023)

- Add support for editor mode including tags in the frontmatter.

4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"id": "obsidian-basetag",
"name": "Base Tag Renderer",
"version": "1.1.2",
"version": "1.1.3",
"minAppVersion": "0.15.0",
"description": "This plugin renders the basename of tags in preview mode.",
"description": "This plugin renders the basename of tags.",
"author": "Darren Kuro",
"authorUrl": "https://github.com/darrenkuro",
"isDesktopOnly": false
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "obsidian-basetag",
"version": "1.1.1",
"description": "This plugin renders the basename of tags in preview mode.",
"version": "1.1.3",
"description": "This plugin renders the basename for tags.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
Expand Down
214 changes: 178 additions & 36 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,189 @@
import { Plugin } from "obsidian";
import { syntaxTree } from "@codemirror/language";
import { RangeSetBuilder } from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
PluginValue,
ViewPlugin,
ViewUpdate,
WidgetType,
} from "@codemirror/view";
import { livePreviewState } from "obsidian";

const BASETAG = "basename-tag";

/** Get the current vault name. */
const getVaultName = () => window.app.vault.getName();

/** Create a custom tag node from text content (can include #). */
const createTagNode = (text: string | null) => {
const node = document.createElement("a");
if (!text) return node;

// Keep the 'tag' class for consistent css styles.
node.className = `tag ${BASETAG}`;
node.target = "_blank";
node.rel = "noopener";
node.href = text;

const vaultStr = encodeURIComponent(getVaultName());
const queryStr = `tag:${encodeURIComponent(text)}`;
node.dataset.uri = `obsidian://search?vault=${vaultStr}&query=${queryStr}`;

// Remove the hash tags to conform to the same style.
node.textContent = text.slice(text.lastIndexOf("/") + 1).replaceAll("#", "");

node.onclick = () => window.open(node.dataset.uri);

return node;
};

/** Create a tag node in the type of widget from text content. */
class TagWidget extends WidgetType {
constructor(private text: string) {
super();
}

toDOM(view: EditorView): HTMLElement {
return createTagNode(this.text);
}
}

class editorPlugin implements PluginValue {
decorations: DecorationSet;

constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}

update(update: ViewUpdate): void {
if (
update.view.composing ||
update.view.plugin(livePreviewState)?.mousedown
) {
this.decorations = this.decorations.map(update.changes);
} else if (update.selectionSet || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}

private buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();

for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
// Handle tags in the text region.
if (node.name.contains("hashtag-end")) {
// Do not render if falls under selection (cursor) range.
const extendedFrom = node.from - 1;
const extendedTo = node.to + 1;

for (const range of view.state.selection.ranges) {
if (extendedFrom <= range.to && range.from < extendedTo) {
return;
}
}

const text = view.state.sliceDoc(node.from, node.to);

builder.add(
// To include the "#".
node.from - 1,
node.to,
Decoration.replace({
widget: new TagWidget(text),
}),
);
}

// Handle tags in frontmatter.
if (node.name === "hmd-frontmatter") {
// Do not render if falls under selection (cursor) range.
const extendedFrom = node.from;
const extendedTo = node.to + 1;

for (const range of view.state.selection.ranges) {
if (extendedFrom <= range.to && range.from < extendedTo) {
return;
}
}

let frontmatterName = "";
let currentNode = node.node;

// Go up the nodes to find the name for frontmatter, max 20.
for (let i = 0; i < 20; i++) {
currentNode = currentNode.prevSibling ?? node.node;
if (currentNode?.name.contains("atom")) {
frontmatterName = view.state.sliceDoc(
currentNode.from,
currentNode.to,
);
break;
}
}

// Ignore if it's not frontmatter for tags.
if (
frontmatterName.toLowerCase() !== "tags" &&
frontmatterName.toLowerCase() !== "tag"
)
return;

const contentNode = node.node;
const content = view.state.sliceDoc(
contentNode.from,
contentNode.to,
);
const tagsArray = content.split(" ").filter((tag) => tag !== "");

// Loop through the array of tags.
let currentIndex = contentNode.from;
for (let i = 0; i < tagsArray.length; i++) {
builder.add(
currentIndex,
currentIndex + tagsArray[i].length,
Decoration.replace({
widget: new TagWidget(tagsArray[i]),
}),
);

// Length and the space char.
currentIndex += tagsArray[i].length + 1;
}
}
},
});
}

return builder.finish();
}
}

export default class TagRenderer extends Plugin {
async onload() {
this.registerEditorExtension(
ViewPlugin.fromClass(editorPlugin, {
decorations: (value) => value.decorations,
}),
);

this.registerMarkdownPostProcessor((el: HTMLElement) => {
// Find the original tags to render.
el.querySelectorAll(`a.tag:not(.${BASETAG})`).forEach((a) => {
this.formatTag(a as HTMLAnchorElement);
});
el.querySelectorAll(`a.tag:not(.${BASETAG})`).forEach(
(node: HTMLAnchorElement) => {
// Remove class 'tag' so it doesn't get rendered again.
node.removeAttribute("class");
// Hide this node and append the custom tag node in its place.
node.style.display = "none";
node.parentNode?.insertBefore(createTagNode(node.textContent), node);
},
);
});
}

private formatTag(el: HTMLAnchorElement) {
/** Create a custom tag node. */
const createTagNode = (text: string | null) => {
const node = document.createElement("a");
if (!text) return node;

// Keep the 'tag' class for consistent css styles.
node.className = `tag ${BASETAG}`;
node.target = "_blank";
node.rel = "noopener";
node.href = text;

const vaultStr = encodeURIComponent(this.app.vault.getName());
const queryStr = `tag:${encodeURIComponent(text)}`;
node.dataset.uri = `obsidian://search?vault=${vaultStr}&query=${queryStr}`;

// Remove the hash tags to conform to the same style.
node.textContent = text
.slice(text.lastIndexOf("/") + 1)
.replaceAll("#", "");

node.onclick = () => window.open(node.dataset.uri);

return node;
};

// Remove class 'tag' so it doesn't get rendered again.
el.removeAttribute("class");
// Hide this node and append the custom tag node in its place.
el.style.display = "none";
el.parentNode?.insertBefore(createTagNode(el.textContent), el);
}
}
Loading

0 comments on commit b605e6b

Please sign in to comment.