Skip to content

Commit

Permalink
Enhancement: Clicking a PDF annotation link now causes an immediate j…
Browse files Browse the repository at this point in the history
…ump to the annotation in markdown-source-view (#258)

* Clicking a PDF annotation link now causes an immediate jump to the annotation in markdown-source-view
  • Loading branch information
HardwayLinka committed Jan 5, 2023
1 parent c5cc31a commit b405940
Show file tree
Hide file tree
Showing 2 changed files with 224 additions and 8 deletions.
35 changes: 27 additions & 8 deletions src/main.tsx
Expand Up @@ -31,6 +31,7 @@ import defineWebAnnotation from 'defineWebAnnotation';
import { awaitResourceLoading, loadResourcesZip, unloadResources } from 'resourcesFolder';
import stringEncodedResourcesFolder from './resources!zipStringEncoded';
import * as jszip from 'jszip';
import SourceViewObserver from 'sourceViewObserver';

export default class AnnotatorPlugin extends Plugin implements IHasAnnotatorSettings {
static instance: AnnotatorPlugin = null;
Expand Down Expand Up @@ -58,6 +59,8 @@ export default class AnnotatorPlugin extends Plugin implements IHasAnnotatorSett
setupPromise: Promise<void>;
styleObserver: StyleObserver;

sourceViewObserver: SourceViewObserver;

async onload() {
AnnotatorPlugin.instance = this;
this.setupPromise = this.onloadImpl();
Expand Down Expand Up @@ -98,6 +101,8 @@ export default class AnnotatorPlugin extends Plugin implements IHasAnnotatorSett
this.addMarkdownPostProcessor();
this.registerMonkeyPatches();
this.registerSettingsTab();
this.sourceViewObserver = new SourceViewObserver(this);
this.sourceViewObserver.watch();

this.registerEditorExtension(this.getDropExtension());

Expand Down Expand Up @@ -146,6 +151,21 @@ export default class AnnotatorPlugin extends Plugin implements IHasAnnotatorSett
}
})
);

this.registerEvent(
this.app.workspace.on('file-open', (file) => {
if (file) {
this.log("file opened");
if (this.sourceViewObserver.getObserver()) {
this.sourceViewObserver.resetTmpLinkInfo();
this.sourceViewObserver.getObserver().disconnect();
} else {
this.sourceViewObserver.initObserver();
}
this.sourceViewObserver.watch();
}
})
);
}

/*
Expand Down Expand Up @@ -209,6 +229,11 @@ export default class AnnotatorPlugin extends Plugin implements IHasAnnotatorSett
this.styleObserver.listerners = null;
this.styleObserver = null;
AnnotatorPlugin.instance = null;

if (this.sourceViewObserver.getObserver()) {
this.sourceViewObserver.getObserver().disconnect();
this.sourceViewObserver.setObserver(null);
}
}

async loadSettings() {
Expand Down Expand Up @@ -358,7 +383,7 @@ export default class AnnotatorPlugin extends Plugin implements IHasAnnotatorSett
private addMarkdownPostProcessor() {
const markdownPostProcessor = async (el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
for (const link of el.getElementsByClassName('internal-link') as HTMLCollectionOf<HTMLAnchorElement>) {
const linkHref = link.getAttribute('data-href');
const linkHref = link.getAttribute('href');
if (linkHref === null) {
continue;
}
Expand All @@ -367,13 +392,7 @@ export default class AnnotatorPlugin extends Plugin implements IHasAnnotatorSett
const file: TFile | null = this.app.metadataCache.getFirstLinkpathDest(parsedLink.path, ctx.sourcePath);

if (file !== null && this.isAnnotationFile(file)) {
link.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation();
const inNewPane = ev.metaKey || ev.ctrlKey || ev.button == 1;
this.openAnnotationTarget(file, inNewPane, annotationid);
});
this.sourceViewObserver.addClickListener(link, annotationid, file, true);
}
}
};
Expand Down
197 changes: 197 additions & 0 deletions src/sourceViewObserver.tsx
@@ -0,0 +1,197 @@
import {
MarkdownView,
TFile,
parseLinktext
} from 'obsidian';
import AnnotatorPlugin from 'main';

export default class SourceViewObserver {
private plugin: AnnotatorPlugin;
private tmpLinkInfos: {linkText: string; count: number}[] = [];
private tmpTargetIndex = -1;
private targetClassNameSet: Set<string> = new Set();
private observer: MutationObserver;

constructor(plugin: AnnotatorPlugin) {
this.plugin = plugin;
this.initClassNameSet();
this.initObserver();
}

initClassNameSet() {
const prefixObservedClassName = [
'cm-highlight ', 'cm-em ',
'cm-header cm-header-1 ', 'cm-header cm-header-2 ', 'cm-header cm-header-3 ',
'cm-header cm-header-4 ', 'cm-header cm-header-5 ', 'cm-header cm-header-6 '
];
const suffixObservedClassName = [
'',
' cm-list-1', ' cm-list-2', ' cm-list-3',
' cm-quote cm-quote-1', ' cm-strong'
];
const baseTargetClassName = 'cm-hmd-internal-link cm-link-alias';
for (const className of prefixObservedClassName) {
this.targetClassNameSet.add(className + baseTargetClassName);
}
for (const className of suffixObservedClassName) {
this.targetClassNameSet.add(baseTargetClassName + className);
}
}

initObserver() {
const activeLeaf = this.plugin.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeLeaf) return;

const that = this;
const observedClassName = 'cm-hmd-internal-link cm-link-has-alias';
const targetClassName = 'cm-hmd-internal-link cm-link-alias';
const filePath = this.plugin.app.workspace.getActiveFile().path;
const observeCallback = function(mutations: MutationRecord[]) {
that.plugin.log('-----------------------------');
for (const mutation of mutations) {
if (mutation.type == 'childList') {
for (const addedNode of mutation.addedNodes) {
const addedElement = addedNode as Element;
if (addedElement.className && addedElement.className.indexOf(observedClassName) >= 0) {
that.linkOnFocus(addedElement);
}
}
for (const removedNode of mutation.removedNodes) {
const removedElement = removedNode as Element;
if (removedElement.className && removedElement.className.indexOf(observedClassName) >= 0) {
const removedLinkText = removedElement.textContent;
that.plugin.log('remove');
that.linkOnBlur(mutation.target as Element, removedLinkText, filePath, targetClassName);
}
}
} else if (mutation.type == 'attributes') {
if (mutation.oldValue && mutation.oldValue.indexOf(observedClassName) >= 0) {
that.plugin.log('attri');
that.linkOnBlur(mutation.target.parentNode as Element, '', filePath, targetClassName);
}
}
}
}
this.observer = new MutationObserver(observeCallback);
}

getObserver(): MutationObserver {
return this.observer;
}

setObserver(observer: MutationObserver) {
this.observer = observer;
}

watch() {
const activeLeaf = this.plugin.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeLeaf) return;

const regex2Find = /(?<=[\[]{2})[^\]]*(?=[\]]{2})/gm;
const filePath = this.plugin.app.workspace.getActiveFile().path;
const view = activeLeaf.leaf.view.containerEl;
const editor = activeLeaf.editor;
const oldText = editor.getValue();
const linkHref = oldText.match(regex2Find);
if(!linkHref || linkHref.length == 0) return;

const tempSourceLinks = view.getElementsByClassName('cm-hmd-internal-link cm-link-alias') as HTMLCollectionOf<HTMLAnchorElement>;
const length = tempSourceLinks.length;
const sourceLinks = [];
if (length > 1) {
let prev = tempSourceLinks[0];
for (let i = 1; i < length; i++) {
if (tempSourceLinks[i - 1].nextElementSibling != tempSourceLinks[i]) {
sourceLinks.push(prev);
prev = tempSourceLinks[i];
if (i == length - 1) {
sourceLinks.push(tempSourceLinks[i]);
}
} else if (i == length - 1) {
sourceLinks.push(prev);
}
}
} else {
sourceLinks.push(tempSourceLinks[0]);
}

const observeConfig = {
attributeFilter: ['class'],
attributes: true,
characterData: true,
characterDataOldValue: true,
childList: true,
attributeOldValue: true,
subtree: true
};

for (let i = 0; i < sourceLinks.length; i++) {
const tempLink = linkHref[i];
if (typeof tempLink != 'string') continue;
const parsedLink = parseLinktext(tempLink.split('|')[0]);
const annotationid = parsedLink.subpath.startsWith('#^') ? parsedLink.subpath.substring(2) : null;
const file: TFile | null = this.plugin.app.metadataCache.getFirstLinkpathDest(parsedLink.path, filePath);

if (this.plugin.isAnnotationFile(file)) {
this.addClickListener(sourceLinks[i], annotationid, file, false);
this.observer.observe(sourceLinks[i].parentNode, observeConfig);
}
}
}

addClickListener(element: HTMLAnchorElement, annotationid: string, file: TFile, isReadingView: boolean) {
const childs = element.children;
if (isReadingView || childs && childs.length == 1) {
element.addEventListener('click', ev => {
this.plugin.log(annotationid);
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation();
const inNewPane = ev.metaKey || ev.ctrlKey || ev.button == 1;
this.plugin.openAnnotationTarget(file, inNewPane, annotationid);
});
}
}

linkOnFocus(element: Element) {
let count = 0;
const linkText = element.textContent;
while(element.className && element.className.indexOf('cm-formatting-link cm-formatting-link-end') == -1) {
if (this.targetClassNameSet.has(element.className)) count++;
element = element.nextElementSibling;
}
this.tmpLinkInfos.push({linkText: linkText, count: count});
}

linkOnBlur(node: Element, rawLinkText: string, filePath: string, className: string) {
const linkIndex = this.tmpTargetIndex + 1;
if (this.tmpLinkInfos.length == 0) return;
const linkInfo = this.tmpLinkInfos[0];

const link = parseLinktext(rawLinkText == '' ? linkInfo.linkText : rawLinkText);
const annotationid = link.subpath.startsWith('#^') ? link.subpath.substring(2) : null;
const file: TFile | null = this.plugin.app.metadataCache.getFirstLinkpathDest(link.path, filePath);

const targets = node.getElementsByClassName(className) as HTMLCollectionOf<HTMLAnchorElement>;
const uniqueTarget = targets[linkIndex];
this.plugin.log(
'linkText: ' + link.path +
' tarIndex: ' + this.tmpTargetIndex +
' linkIndex: ' + linkIndex +
' size: ' + this.tmpLinkInfos.length
);
this.plugin.log(uniqueTarget);

this.tmpTargetIndex += linkInfo.count;

this.addClickListener(uniqueTarget, annotationid, file, false);

this.tmpLinkInfos.splice(0, 1);
if (this.tmpLinkInfos.length == 0) this.resetTmpLinkInfo();
}

resetTmpLinkInfo() {
this.tmpTargetIndex = -1;
this.tmpLinkInfos.length = 0;
}
}

0 comments on commit b405940

Please sign in to comment.