Skip to content

Commit

Permalink
release: 0.40.18
Browse files Browse the repository at this point in the history
  • Loading branch information
RyotaUshio committed Dec 25, 2024
1 parent e10b3e8 commit 682ae51
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 55 deletions.
2 changes: 1 addition & 1 deletion manifest-beta.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "pdf-plus",
"name": "PDF++",
"version": "0.40.17",
"version": "0.40.18",
"minAppVersion": "1.5.8",
"description": "The most Obsidian-native PDF annotation tool ever.",
"author": "Ryota Ushio",
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": "pdf-plus",
"name": "PDF++",
"version": "0.40.17",
"version": "0.40.18",
"minAppVersion": "1.5.8",
"description": "The most Obsidian-native PDF annotation tool ever.",
"author": "Ryota Ushio",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-pdf-plus",
"version": "0.40.17",
"version": "0.40.18",
"description": "The most Obsidian-native PDF annotation tool ever.",
"scripts": {
"dev": "node esbuild.config.mjs",
Expand Down
66 changes: 45 additions & 21 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { App, Component, EditableFileView, FileView, MarkdownView, Notice, Platform, TFile, TextFileView, View, base64ToArrayBuffer, normalizePath, parseLinktext, requestUrl } from 'obsidian';
import { CanvasFileData } from 'obsidian/canvas';
import { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import { EncryptedPDFError, PDFArray, PDFDict, PDFDocument, PDFName, PDFNumber, PDFRef } from '@cantoo/pdf-lib';

Expand Down Expand Up @@ -561,28 +562,50 @@ export class PDFPlusLib {
return null;
}

getPDFEmbedsInComponent(component: Component, firstOnly: boolean): PDFEmbed[] {
const embeds: PDFEmbed[] = [];
// It is not enough to look through the direct children of the component.
// We have to recurse through all descendants in order to capture PDF embeds
// in an embedded markdown file inside another markdown file, or those embedded in
// a canvas text node, etc.
utils.walkDescendantComponents(component, (child) => {
if (firstOnly && embeds.length) return false;

if (this.isPDFEmbed(child)) {
embeds.push(child);
return false;
}
});
return embeds;
}

getPDFEmbedInMarkdownView(view: MarkdownView): PDFEmbed | null {
// @ts-ignore
const children = view.currentMode._children as any[];
const pdfEmbed = children.find((component): component is PDFEmbed => this.isPDFEmbed(component));
return pdfEmbed ?? null;
return this.getPDFEmbedsInComponent(view.currentMode as unknown as Component, true).first() ?? null;
}

getAllPDFEmbedInMarkdownView(view: MarkdownView): PDFEmbed[] {
// @ts-ignore
const children = view.currentMode._children as any[];
return children.filter((component): component is PDFEmbed => this.isPDFEmbed(component));
getAllPDFEmbedsInMarkdownView(view: MarkdownView): PDFEmbed[] {
return this.getPDFEmbedsInComponent(view.currentMode as unknown as Component, false);
}

getPDFEmbedInCanvasView(view: CanvasView): PDFEmbed | null {
const canvasPDFFileNode = Array.from(view.canvas.nodes.values()).find((node): node is CanvasFileNode => this.isCanvasPDFNode(node));
return (canvasPDFFileNode?.child as PDFEmbed | undefined) ?? null;
const nodes = Array.from(view.canvas.nodes.values());
for (const node of nodes) {
if ('child' in node && node.child instanceof Component) {
const embeds = this.getPDFEmbedsInComponent(node.child, true);
if (embeds.length) return embeds[0];
}
}
return null;
}

getAllPDFEmbedInCanvasView(view: CanvasView): PDFEmbed[] {
getAllPDFEmbedsInCanvasView(view: CanvasView): PDFEmbed[] {
return Array.from(view.canvas.nodes.values())
.filter((node): node is CanvasFileNode => this.isCanvasPDFNode(node))
.map(node => node.child as PDFEmbed);
.flatMap((node) => {
if ('child' in node && node.child instanceof Component) {
return this.getPDFEmbedsInComponent(node.child, false);
}
return [];
});
}

getPDFEmbedInActiveView(): PDFEmbed | null {
Expand Down Expand Up @@ -610,7 +633,7 @@ export class PDFPlusLib {

const view = leaf.view;

// We should not use view.getViewType() here because it cannot detect deferred views.
// We should not use view.getViewType() here because it cannot detect deferred views.
if (view instanceof MarkdownView) {
pdfEmbed = this.getPDFEmbedInMarkdownView(view);
} else if (this.isCanvasView(view)) {
Expand Down Expand Up @@ -828,12 +851,13 @@ export class PDFPlusLib {
}

isCanvasPDFNode(node: CanvasNode): node is CanvasFileNode {
if ('file' in node
&& node.file instanceof TFile
&& node.file.extension === 'pdf'
&& node.child instanceof Component
&& this.isPDFEmbed(node.child)) {
return true;
// What is the optimal way to write this function in TypeScript?
// Ideally, I want TypeScript to understand that if the `data.type ==='file'` check passes,
// then `node` is a CanvasFileNode and `node.getData()` is a CanvasFileData.
const data = node.getData();
if (data.type === 'file') {
const path = (data as CanvasFileData).file;
return path.endsWith('.pdf');
}
return false;
}
Expand All @@ -847,7 +871,7 @@ export class PDFPlusLib {
return this.app.vault.getAvailablePath(removeExtension(file.path), file.extension);
}



get metadataCacheUpdatePromise() {
return new Promise<void>((resolve) => this.app.metadataCache.onCleanCache(resolve));
Expand Down
19 changes: 16 additions & 3 deletions src/lib/workspace-lib.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HoverParent, MarkdownView, OpenViewState, PaneType, Platform, Pos, TFile, View, WorkspaceItem, WorkspaceLeaf, WorkspaceSidedock, WorkspaceSplit, WorkspaceTabs, parseLinktext, requireApiVersion } from 'obsidian';

import { PDFPlusLibSubmodule } from './submodule';
import { BacklinkView, CanvasView, PDFView, PDFViewerChild, PDFViewerComponent } from 'typings';
import { BacklinkView, CanvasView, PDFEmbed, PDFView, PDFViewerChild, PDFViewerComponent } from 'typings';


// Split right, left, down, or up
Expand Down Expand Up @@ -56,17 +56,30 @@ export class WorkspaceLib extends PDFPlusLibSubmodule {
});
}

iteratePDFEmbeds(callback: (embed: PDFEmbed) => any): void {
this.app.workspace.iterateAllLeaves((leaf) => {
const view = leaf.view;
if (view instanceof MarkdownView) {
const embeds = this.lib.getAllPDFEmbedsInMarkdownView(view);
embeds.forEach(callback);
} else if (this.lib.isCanvasView(view)) {
const embeds = this.lib.getAllPDFEmbedsInCanvasView(view);
embeds.forEach(callback);
}
});
}

iteratePDFViewerComponents(callback: (pdfViewerComponent: PDFViewerComponent, file: TFile | null) => any): void {
this.app.workspace.iterateAllLeaves((leaf) => {
const view = leaf.view;

if (this.lib.isPDFView(view)) {
callback(view.viewer, view.file);
} else if (view instanceof MarkdownView) {
this.lib.getAllPDFEmbedInMarkdownView(view)
this.lib.getAllPDFEmbedsInMarkdownView(view)
.forEach((embed) => callback(embed.viewer, embed.file));
} else if (this.lib.isCanvasView(view)) {
this.lib.getAllPDFEmbedInCanvasView(view)
this.lib.getAllPDFEmbedsInCanvasView(view)
.forEach((embed) => callback(embed.viewer, embed.file));
}
});
Expand Down
5 changes: 4 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ export default class PDFPlus extends Plugin {
citationIdRegex: RegExp;
/** Maps a `div.pdf-container` element to the corresponding `PDFViewerChild` object. */
// In most use cases of this map, the goal is also achieved by using lib.workspace.iteratePDFViewerChild.
// However, a PDF embed inside a Canvas text node cannot be handled by the function, so we need this map.
// However, **before PDF++ 0.40.18**, a PDF embed inside a Canvas text node cannot be handled by the function, so we needed this map.
// As of 0.40.18, the function can handle it, but I will keep this map as it could be advantageous
// in terms of performance (it can avoid iteration over all PDFViewerChild objects).
// Also, there is a saying "if it ain't broke, don't fix it."
pdfViewerChildren: Map<HTMLElement, PDFViewerChild> = new Map();
/** Stores all the shown context menu objects. Used to close all visible menus programatically. */
shownMenus: Set<Menu> = new Set();
Expand Down
35 changes: 21 additions & 14 deletions src/patchers/pdf-internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,29 @@ export const patchPDFInternals = async (plugin: PDFPlus, pdfViewerComponent: PDF

function onPDFInternalsPatchSuccess(plugin: PDFPlus) {
const { lib } = plugin;
lib.workspace.iteratePDFViewerComponents((viewer, file) => {
// reflect the patch to existing PDF views
// especially reflesh the "contextmenu" event handler (PDFViewerChild.prototype.onContextMenu/onThumbnailContext)
viewer.unload();

// Clean up the old keymaps already registered by PDFViewerChild,
// which causes an error because the listener references the old instance of PDFFindBar.
// This keymap hanldler will be re-registered in `PDFViewerChild.load` by the following `viewer.load()`.
const oldEscapeHandler = viewer.scope.keys.find((handler) => handler.modifiers === '' && handler.key === 'Escape');
if (oldEscapeHandler) viewer.scope.unregister(oldEscapeHandler);

viewer.load();
if (file) viewer.loadFile(file, plugin.subpathWhenPatched);
});
// For the detail of `plugin.subpathWhenPatched`, see its docstring.
lib.workspace.iteratePDFViews((view) => reloadPDFViewerComponent(view.viewer, view.file, plugin.subpathWhenPatched));
// Without passing `embed.subpath`, the embed will display the first page of the PDF file regardless of the subpath,
// in which case the subpath like `...#page=5` will be ignored.
// See https://github.com/RyotaUshio/obsidian-pdf-plus/issues/322
lib.workspace.iteratePDFEmbeds((embed) => reloadPDFViewerComponent(embed.viewer, embed.file, embed.subpath));
}

const reloadPDFViewerComponent = (viewer: PDFViewerComponent, file: TFile | null, subpath?: string) => {
// reflect the patch to existing PDF views
// especially reflesh the "contextmenu" event handler (PDFViewerChild.prototype.onContextMenu/onThumbnailContext)
viewer.unload();

// Clean up the old keymaps already registered by PDFViewerChild,
// which causes an error because the listener references the old instance of PDFFindBar.
// This keymap hanldler will be re-registered in `PDFViewerChild.load` by the following `viewer.load()`.
const oldEscapeHandler = viewer.scope.keys.find((handler) => handler.modifiers === '' && handler.key === 'Escape');
if (oldEscapeHandler) viewer.scope.unregister(oldEscapeHandler);

viewer.load();
if (file) viewer.loadFile(file, subpath);
};

const patchPDFViewerComponent = (plugin: PDFPlus, pdfViewerComponent: PDFViewerComponent) => {
plugin.register(around(pdfViewerComponent.constructor.prototype, {
loadFile(old) {
Expand Down
35 changes: 24 additions & 11 deletions src/typings.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { App, CachedMetadata, Component, Debouncer, EditableFileView, FileView, Modal, PluginSettingTab, Scope, SearchComponent, SearchMatches, SettingTab, TFile, SearchMatchPart, IconName, TFolder, TAbstractFile, MarkdownView, MarkdownFileInfo, Events, TextFileView, Reference, ViewStateResult, HoverPopover, Hotkey, KeymapEventHandler, Constructor } from 'obsidian';
import { CanvasData } from 'obsidian/canvas';
import { CanvasData, CanvasFileData, CanvasGroupData, CanvasLinkData, CanvasNodeData, CanvasTextData } from 'obsidian/canvas';
import { EditorView } from '@codemirror/view';
import { PDFDocumentProxy, PDFPageProxy, PageViewport } from 'pdfjs-dist';
import { AnnotationStorage } from 'pdfjs-dist/types/src/display/annotation_storage';
Expand Down Expand Up @@ -966,7 +966,7 @@ interface CanvasView extends TextFileView {
}

interface Canvas {
nodes: Map<string, CanvasNode>;
nodes: Map<string, CanvasTextNode | CanvasFileNode | CanvasLinkNode | CanvasGroupNode>;
createTextNode(config: {
pos: { x: number, y: number };
position?: 'center' | 'top' | 'right' | 'bottom' | 'left';
Expand All @@ -988,26 +988,39 @@ interface Canvas {
getData(): CanvasData;
}

type CanvasNode = CanvasFileNode | CanvasTextNode | CanvasLinkNode | CanvasGroupNode;

interface CanvasFileNode {
interface CanvasNode {
id: string;
x: number;
y: number;
width: number;
height: number;
color: string;
app: App;
canvas: Canvas;
nodeEl: HTMLElement;
getData(): CanvasNodeData;
}

interface CanvasFileNode extends CanvasNode {
file: TFile | null;
subpath: string
child: Embed;
getData(): CanvasFileData;
}

interface CanvasTextNode {
app: App;
interface CanvasTextNode extends CanvasNode {
text: string;
child: Component;
getData(): CanvasTextData;
}

interface CanvasLinkNode {
app: App;
interface CanvasLinkNode extends CanvasNode {
url: string;
getData(): CanvasLinkData;
}

interface CanvasGroupNode {
app: App;
interface CanvasGroupNode extends CanvasNode {
getData(): CanvasGroupData;
}

interface HotkeyManager {
Expand Down
13 changes: 13 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,3 +499,16 @@ export function repeatable(func: () => any) {
export function evalInContext(code: string, ctx?: any) {
return (new Function(code.includes('await') ? '(async () => {' + code + '})()' : code)).call(ctx);
}

/**
* Performs a depth-first traversal of the component tree rooted at the given component and calls the callback on each component.
* @param component The component to start walking from.
* @param callback Return `false` to stop walking the subtree rooted at the current component.
*/
export function walkDescendantComponents(component: Component, callback: (component: Component) => boolean | void) {
const ret = callback(component);
if (ret === false) return;
for (const child of component._children) {
walkDescendantComponents(child, callback);
}
}

0 comments on commit 682ae51

Please sign in to comment.