Skip to content

Commit

Permalink
Merge pull request #57 from HEmile/cm6-life-preview
Browse files Browse the repository at this point in the history
Proper implementation of Cm6 live preview supercharging
  • Loading branch information
HEmile authored Dec 21, 2021
2 parents bb24670 + 65aab7b commit 2d0cfd8
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 84 deletions.
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
## Obsidian Internal Links supercharger
## Supercharged Links

Internal links adds attributes to HTMLElements with the attributes and values of the target file's frontmatter.
Combined with css snippets, it allows a very flexible way to customize your links! It supports the note preview, edit mode, backlinks and outgoing links panel, the file browser and the search panel, and also supports the Breadcrumbs plugin.
This plugin gives you huge control style links and references in Obsidian to notes in your vault!
You can, for example, automatically add colors and emojis to the links.

<img src=https://raw.githubusercontent.com/mdelobelle/obsidian_supercharged_links/master/images/link-styling-workspace.png alt="drawing" style="width:600px;"/>

Why is this useful?
If the note you are referring to represents something, like a paper, a location, a person or a day in the week, you can make this type of note stand out in Obsidian.
Supercharged links will make sure that you can make those different links stand out.
This visual feedback helps you find the right link back quickly!


Now how does this work? The plugin adds CSS attributes to the links.
Those attributes will be based on the tags, frontmatter and Dataview inline links in your notes.
Combined with css snippets, you will have full control over customizing your links!
It supports note preview, live preview (!), backlinks panel, the file browser, the search panel, and supports the Breadcrumbs plugin.


It also adds context menu items to modifiy target note's frontmatter properties and "inline fields" (dataview syntax) by right-clicking on the link
The preset values for those properties can be managed globally in the plugin's settings or on a file-by-file basis thanks to fileClass definition (see section 4)

Expand Down Expand Up @@ -92,13 +104,13 @@ This will target all HTML elements that contain the `data-link-tags` property, t

To put a fancy 👤 emoji before the name of each link to a "category: people" note:
```css
:not(.cm-hmd-internal-link)[data-link-category$="People" i]::before{
.data-link-icon[data-link-category$="People" i]::before{
content: "👤 "
}
```
<img src="https://raw.githubusercontent.com/mdelobelle/obsidian_supercharged_links/master/images/link-styling-in-note.png" style="width:500px">

We make sure not to target links in the editor view using `:not(.cm-hmd-internal-link)`, since this will break the cursor positioning!
Selecting specifically `.data-link-icon` is required to prevent bugs in Live Preview.

To highlight the link in a tag-like blue rounded rectangle when there is a property next-actions in the target file:

Expand Down
141 changes: 115 additions & 26 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { Plugin, MarkdownView, Notice, debounce, Platform } from 'obsidian';
import {Plugin, MarkdownView, Notice, App, editorViewField} from 'obsidian';
import SuperchargedLinksSettingTab from "src/settings/SuperchargedLinksSettingTab"
import {
updateElLinks,
updateVisibleLinks,
updateDivLinks,
updateEditorLinks,
clearExtraAttributes, updateDivExtraAttributes
clearExtraAttributes,
updateDivExtraAttributes,
fetchFrontmatterTargetAttributes,
fetchFrontmatterTargetAttributesSync
} from "src/linkAttributes/linkAttributes"
import { SuperchargedLinksSettings, DEFAULT_SETTINGS } from "src/settings/SuperchargedLinksSettings"
import Field from 'src/Field';
import linkContextMenu from "src/options/linkContextMenu"
import NoteFieldsCommandsModal from "src/options/NoteFieldsCommandsModal"
import FileClassAttributeSelectModal from 'src/fileClass/FileClassAttributeSelectModal';
import { CSSBuilderModal } from 'src/cssBuilder/cssBuilderModal'
import {Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate, WidgetType} from "@codemirror/view";
import {RangeSetBuilder} from "@codemirror/rangeset";
import {syntaxTree} from "@codemirror/language";
import {tokenClassNodeProp} from "@codemirror/stream-parser";
import {Prec} from "@codemirror/state";

export default class SuperchargedLinks extends Plugin {
settings: SuperchargedLinksSettings;
Expand Down Expand Up @@ -42,30 +49,9 @@ export default class SuperchargedLinks extends Plugin {
updateDivLinks(this.app, this.settings);
}));

const updateEditor = (markdownView: MarkdownView) => {
updateEditorLinks(this.app, this.settings, markdownView.containerEl, markdownView.file)
}
const dbUpdateEditor = debounce(updateEditor, 300, true)

this.registerEvent(this.app.workspace.on('editor-change', (editor, markdownView) => {
if (this.settings.enableEditor && markdownView.getMode() !== "preview" && Platform.isDesktop) {
dbUpdateEditor(markdownView)

}
}));
this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => {
if (this.settings.enableEditor && leaf.view instanceof MarkdownView && Platform.isDesktop) {
updateEditorLinks(this.app, this.settings, leaf.view.containerEl, (leaf.view as MarkdownView).file)
}
}));
// this.registerCodeMirror((cm) => {
// console.log(cm)
// cm.on("update", () => {
// if (this.settings.enableEditor) {
// updateEditorLinks(this.app, this.settings);
// }
// })
// });
const ext = Prec.lowest(this.buildCMViewPlugin(this.app, this.settings));
this.registerEditorExtension(ext);

this.observers = [];

Expand Down Expand Up @@ -177,6 +163,109 @@ export default class SuperchargedLinks extends Plugin {
plugin.observers.push(observer);
}

buildCMViewPlugin(app: App, _settings: SuperchargedLinksSettings) {
// Implements the live preview supercharging
// Code structure based on https://github.com/nothingislost/obsidian-cm6-attributes/blob/743d71b0aa616407149a0b6ea5ffea28e2154158/src/main.ts
// Code help credits to @NothingIsLost! They have been a great help getting this to work properly.
class HeaderWidget extends WidgetType {
attributes: Record<string, string>

constructor(attributes: Record<string, string>) {
super();
this.attributes = attributes
}

toDOM() {
let headerEl = document.createElement("span");
headerEl.setAttrs(this.attributes);
headerEl.addClass('data-link-icon');
// create a naive bread crumb
return headerEl;
}

ignoreEvent() {
return true;
}
}
const settings = _settings;
const viewPlugin = ViewPlugin.fromClass(
class{
decorations: DecorationSet;

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

update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}

destroy() {}

buildDecorations(view: EditorView) {
let builder = new RangeSetBuilder<Decoration>();
if (!settings.enableEditor) {
builder.finish();
return;
}
const mdView = view.state.field(editorViewField) as MarkdownView;
let lastAttributes = {};
for (let {from, to} of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (type, from, to) => {
const tokenProps = type.prop(tokenClassNodeProp);
if (tokenProps) {
const props = new Set(tokenProps.split(" "));
const isLink = props.has("hmd-internal-link");
const isAlias = props.has("link-alias");
const isPipe = props.has("link-alias-pipe");
// if (props.has("hmd-internal-link")) {console.log("props", type, from, to)}
if (isLink && !isAlias && !isPipe) {
let linkText = view.state.doc.sliceString(from, to);
linkText = linkText.split("#")[0];
let file = app.metadataCache.getFirstLinkpathDest(linkText, mdView.file.basename);
if (file) {
let _attributes = fetchFrontmatterTargetAttributesSync(app, settings, file, true);
let attributes: Record<string, string> = {};
for (let key in _attributes) {
attributes["data-link-" + key] = _attributes[key];
}
let deco = Decoration.mark({
attributes
});
let iconDeco = Decoration.widget({
widget: new HeaderWidget(attributes),
});
builder.add(from, from, iconDeco);
builder.add(from, to, deco);
lastAttributes = attributes;
}
}
else if (isLink && isAlias) {
let deco = Decoration.mark({
attributes: lastAttributes
})
builder.add(from, to, deco);
}
}
}
})

}
return builder.finish();
}
},
{
decorations: v => v.decorations
}
);
return viewPlugin;
}

onunload() {
this.observers.forEach((observer) => observer.disconnect());
console.log('Supercharged links unloaded');
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "supercharged-links-obsidian",
"name": "Supercharged Links",
"version": "0.3.7",
"version": "0.4.0",
"minAppVersion": "0.12.7",
"description": "Adds properties and menu options to internal links",
"author": "mdelobelle",
Expand Down
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,26 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@codemirror/rangeset": "^0.19.0",
"@codemirror/state": "^0.19.0",
"@codemirror/view": "^0.19.0",
"@codemirror/commands": "^0.19.0",
"@codemirror/fold": "0.19.0",
"@codemirror/history": "^0.19.0",
"@codemirror/language": "^0.19.0",
"@codemirror/matchbrackets": "^0.19.0",
"@codemirror/panel": "^0.19.0",
"@codemirror/search": "^0.19.0",
"@codemirror/stream-parser": "https://github.com/lishid/stream-parser",
"@rollup/plugin-commonjs": "^18.0.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-typescript": "^8.2.1",
"@types/node": "^14.14.37",
"obsidian": "^0.12.17",
"rollup": "^2.32.1",
"tslib": "^2.2.0",
"typescript": "^4.2.4"
"typescript": "^4.2.4",
"obsidian": "^0.13.8"
},
"dependencies": {
}
}
20 changes: 19 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,25 @@ export default {
exports: 'default',
banner,
},
external: ['obsidian'],
external: [
"obsidian",
"electron",
"codemirror",
"@codemirror/autocomplete",
"@codemirror/closebrackets",
"@codemirror/commands",
"@codemirror/fold",
"@codemirror/gutter",
"@codemirror/history",
"@codemirror/language",
"@codemirror/rangeset",
"@codemirror/rectangular-selection",
"@codemirror/search",
"@codemirror/state",
"@codemirror/stream-parser",
"@codemirror/text",
"@codemirror/view",
],
plugins: [
typescript(),
nodeResolve({browser: true}),
Expand Down
74 changes: 26 additions & 48 deletions src/linkAttributes/linkAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,36 @@ export function clearExtraAttributes(link: HTMLElement) {
})
}

function fetchFrontmatterTargetAttributes(app: App, settings: SuperchargedLinksSettings, dest: TFile, addDataHref: boolean): Promise<Record<string, string>> {
export function fetchFrontmatterTargetAttributesSync(app: App, settings: SuperchargedLinksSettings, dest: TFile, addDataHref: boolean): Record<string, string> {
let new_props: Record<string, string> = {}
const cache = app.metadataCache.getFileCache(dest)
if (!cache) return;

const frontmatter = cache.frontmatter
if (frontmatter) {
settings.targetAttributes.forEach(attribute => {
if (Object.keys(frontmatter).includes(attribute)) {
new_props[attribute] = frontmatter[attribute]
}
})
}

if (settings.targetTags) {
new_props["tags"] = getAllTags(cache).join(' ');
}

if (addDataHref) {
new_props['data-href'] = dest.basename;
}
return new_props
}

export function fetchFrontmatterTargetAttributes(app: App, settings: SuperchargedLinksSettings, dest: TFile, addDataHref: boolean): Promise<Record<string, string>> {
return new Promise(async (resolve, reject) => {
const cache = app.metadataCache.getFileCache(dest)
if (!cache) return;
const new_props = fetchFrontmatterTargetAttributesSync(app, settings, dest, addDataHref);

const frontmatter = cache.frontmatter
if (frontmatter) {
settings.targetAttributes.forEach(attribute => {
if (Object.keys(frontmatter).includes(attribute)) {
new_props[attribute] = frontmatter[attribute]
}
})
}
if (settings.getFromInlineField) {
const regex = new RegExp(`(${settings.targetAttributes.join("|")})::(.+)?`, "g");
await app.vault.cachedRead(dest).then((result) => {
Expand All @@ -34,13 +50,6 @@ function fetchFrontmatterTargetAttributes(app: App, settings: SuperchargedLinksS
}
})
}
if (settings.targetTags) {
new_props["tags"] = getAllTags(cache).join(' ');
}

if (addDataHref) {
new_props['data-href'] = dest.basename;
}

resolve(new_props)
})
Expand All @@ -49,6 +58,7 @@ function fetchFrontmatterTargetAttributes(app: App, settings: SuperchargedLinksS
function setLinkNewProps(link: HTMLElement, new_props: Record<string, string>) {
Object.keys(new_props).forEach(key => {
link.setAttribute("data-link-" + key, new_props[key])
link.addClass("data-link-icon");
})
}

Expand Down Expand Up @@ -91,38 +101,6 @@ export function updateDivLinks(app: App, settings: SuperchargedLinksSettings) {
})
}

export function updateEditorLinks(app: App, settings: SuperchargedLinksSettings, el: HTMLElement, file: TFile) {
const internalLinks = el.querySelectorAll('span.cm-hmd-internal-link');
internalLinks.forEach((link: HTMLElement) => {
clearExtraAttributes(link);
updateDivExtraAttributes(app, settings, link, "", link.textContent.split('|')[0].split('#')[0]);
})

// Aliased elements do not have an attribute to find the original link.
// So iterate through the array of links to find all aliased links and match them to the html elements
let aliasedElements = Array.from(el.querySelectorAll("span.cm-link-alias"))
?.filter(el => {
// Remove zero-width space which are added in live preview
return (el as HTMLElement).innerText !== "\u200B"
});
if (!aliasedElements) {
return
}
let cache = app.metadataCache.getFileCache(file)
if (cache && cache.links) {
let aliasedLinks = cache.links.filter(eachLink => eachLink.displayText !== eachLink.link);
aliasedLinks.forEach((linkCache, index) => {
let linkElement = aliasedElements[index] as HTMLElement
if (linkElement && linkElement.innerText === linkCache.displayText) {
clearExtraAttributes(linkElement);
updateDivExtraAttributes(app, settings, linkElement, '', linkCache.link)
}
})
}


}

export function updateElLinks(app: App, settings: SuperchargedLinksSettings, el: HTMLElement, ctx: MarkdownPostProcessorContext) {
const links = el.querySelectorAll('a.internal-link');
const destName = ctx.sourcePath.replace(/(.*).md/, "$1");
Expand Down
3 changes: 2 additions & 1 deletion versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"0.2.9": "0.12.4",
"0.2.10": "0.12.3",
"0.2.11": "0.12.3",
"0.2.12": "0.12.3"
"0.2.12": "0.12.3",
"0.4.0": "0.13.10"
}

0 comments on commit 2d0cfd8

Please sign in to comment.