Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
cache: npm

- run: npm ci
- run: npm run lint:obsidian-warnings
- run: npx tsc -noEmit -skipLibCheck
- run: npm run build
- run: npm test
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "document-exporter",
"name": "Document Exporter",
"version": "0.4.8",
"version": "0.4.9",
"minAppVersion": "1.4.0",
"description": "Export notes, folders, and query results into Markdown bundles, HTML documents, and print-ready exports.",
"author": "Roger Deng",
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.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "document-exporter",
"version": "0.4.8",
"version": "0.4.9",
"description": "Export notes, folders, and query results into Markdown bundles, HTML documents, and print-ready exports.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"lint:obsidian-warnings": "eslint 'src/**/*.ts'",
"test": "vitest run",
"test:watch": "vitest",
"version": "node version-bump.mjs && git add manifest.json versions.json"
Expand Down
2 changes: 1 addition & 1 deletion src/export/ExportRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ describe("ExportRunner", () => {
);

// Attachment must land under the target folder, not the export root.
const destPaths = copySpy.mock.calls.map((c) => c[1] as string);
const destPaths = copySpy.mock.calls.map((c) => c[1]);
expect(destPaths).toContain("exports/notes/assets/img.png");
expect(destPaths).not.toContain("exports/assets/img.png");
copySpy.mockRestore();
Expand Down
18 changes: 14 additions & 4 deletions src/formats/docx.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { describe, expect, it, vi } from "vitest";
import { TFile } from "obsidian";
import { renderDocx } from "@/formats/docx";
import { AssembledDocument, ExportPlan } from "@/types";

function makeTFile(path: string, extension: string): TFile {
const file = new TFile();
file.path = path;
file.name = path.split("/").pop() ?? path;
file.basename = file.name.replace(new RegExp(`\\.${extension}$`), "");
file.extension = extension;
return file;
}

describe("DOCX rendering", () => {
it("writes a minimal DOCX package without external dependencies", async () => {
let writtenPath = "";
Expand Down Expand Up @@ -116,7 +126,7 @@ describe("DOCX rendering", () => {
vault: {
getAbstractFileByPath: vi.fn((path: string) => {
if (path === "assets/image.png") {
return { path, extension: "png", name: "image.png" };
return makeTFile(path, "png");
}
return null;
}),
Expand Down Expand Up @@ -163,7 +173,7 @@ describe("DOCX rendering", () => {
vault: {
getAbstractFileByPath: vi.fn((path: string) => {
if (path === "assets/wide.png") {
return { path, extension: "png", name: "wide.png" };
return makeTFile(path, "png");
}
return null;
}),
Expand Down Expand Up @@ -204,7 +214,7 @@ describe("DOCX rendering", () => {
vault: {
getAbstractFileByPath: vi.fn((path: string) => {
if (path === "assets/image.png") {
return { path, extension: "png", name: "image.png" };
return makeTFile(path, "png");
}
return null;
}),
Expand Down Expand Up @@ -247,7 +257,7 @@ describe("DOCX rendering", () => {
vault: {
getAbstractFileByPath: vi.fn((path: string) => {
if (path === "attachments/one.png" || path === "attachments/two.png") {
return { path, extension: "png", name: path.split("/").pop() };
return makeTFile(path, "png");
}
return null;
}),
Expand Down
9 changes: 6 additions & 3 deletions src/formats/docx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ async function collectImages(

try {
const file = app.vault.getAbstractFileByPath(att.sourcePath);
if (!file || !("extension" in file)) continue;
if (!(file instanceof TFile)) continue;

const buffer = await app.vault.readBinary(file as TFile);
const buffer = await app.vault.readBinary(file);
const data = new Uint8Array(buffer);
const ext = att.sourcePath.split(".").pop()?.toLowerCase() ?? "png";
const dims = readImageDimensions(data, ext);
Expand Down Expand Up @@ -379,7 +379,10 @@ function findImage(ref: string, imageMap: Map<string, DocxImage>): DocxImage | n
return img;
}
}
if (imageMap.size === 1) return imageMap.values().next().value!;
if (imageMap.size === 1) {
const onlyImage = imageMap.values().next();
return onlyImage.done ? null : onlyImage.value;
}
return null;
}

Expand Down
23 changes: 22 additions & 1 deletion src/formats/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,27 @@ function mimeFromExt(ext: string): string {

export function encodeAttachmentDataUri(buffer: ArrayBuffer, ext: string): string {
const bytes = new Uint8Array(buffer);
const base64 = Buffer.from(bytes).toString("base64");
const base64 = encodeBase64(bytes);
return `data:${mimeFromExt(ext)};base64,${base64}`;
}

function encodeBase64(bytes: Uint8Array): string {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let output = "";

for (let i = 0; i < bytes.length; i += 3) {
const first = bytes[i];
const second = bytes[i + 1];
const third = bytes[i + 2];
const hasSecond = i + 1 < bytes.length;
const hasThird = i + 2 < bytes.length;
const value = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);

output += alphabet[(value >> 18) & 0x3F];
output += alphabet[(value >> 12) & 0x3F];
output += hasSecond ? alphabet[(value >> 6) & 0x3F] : "=";
output += hasThird ? alphabet[value & 0x3F] : "=";
}

return output;
}
65 changes: 56 additions & 9 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Plugin, TFile, TFolder, Menu } from "obsidian";
import { Plugin, TFile, TFolder, Menu, MenuItem } from "obsidian";
import { ExportSettings } from "@/types";
import { loadSettings, saveSettings } from "@/settings/settings";
import { DocumentExporterSettingTab } from "@/settings/settings-tab";
Expand All @@ -8,6 +8,37 @@ import { ExportPlanBuilder, validatePlan } from "@/export/ExportPlan";
import { ExportRunner, ExportProgressCallbacks, SINGLE_FILE_PHASES } from "@/export/ExportRunner";
import { ProgressNotice } from "@/ui/ProgressNotice";

type NotebookNavigatorMenus = {
registerFileMenu?: (callback: (context: NotebookNavigatorFileContext) => void) => () => void;
registerFolderMenu?: (callback: (context: NotebookNavigatorFolderContext) => void) => () => void;
};

type NotebookNavigatorFileContext = {
selection?: { mode?: string };
file?: unknown;
addItem: (callback: (item: MenuItem) => void) => void;
};

type NotebookNavigatorFolderContext = {
folder?: unknown;
addItem: (callback: (item: MenuItem) => void) => void;
};

type AppWithPluginRegistry = typeof Plugin.prototype.app & {
plugins?: {
plugins?: Record<string, unknown>;
};
};

function isNotebookNavigatorMenus(value: unknown): value is NotebookNavigatorMenus {
if (!value || typeof value !== "object") return false;
const menus = value as NotebookNavigatorMenus;
return (
(typeof menus.registerFileMenu === "function" || typeof menus.registerFileMenu === "undefined")
&& (typeof menus.registerFolderMenu === "function" || typeof menus.registerFolderMenu === "undefined")
);
}

export default class DocumentExporterPlugin extends Plugin {
settings!: ExportSettings;

Expand Down Expand Up @@ -66,16 +97,15 @@ export default class DocumentExporterPlugin extends Plugin {
onunload() {}

private registerNotebookNavigatorMenus() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nnMenus = (this.app as any)?.plugins?.plugins?.["notebook-navigator"]?.api?.menus;
const nnMenus = this.getNotebookNavigatorMenus();
if (!nnMenus) return;

if (typeof nnMenus.registerFileMenu === "function") {
const dispose = nnMenus.registerFileMenu((context: any) => {
const dispose = nnMenus.registerFileMenu((context) => {
if (context.selection?.mode !== "single") return;
const file = context.file;
if (!file || !("extension" in file) || file.extension !== "md") return;
context.addItem((item: any) => {
if (!(file instanceof TFile) || file.extension !== "md") return;
context.addItem((item) => {
item.setTitle("Export this file")
.setIcon("file-output")
.onClick(() => this.openExportModal(file, undefined));
Expand All @@ -85,10 +115,10 @@ export default class DocumentExporterPlugin extends Plugin {
}

if (typeof nnMenus.registerFolderMenu === "function") {
const dispose = nnMenus.registerFolderMenu((context: any) => {
const dispose = nnMenus.registerFolderMenu((context) => {
const folder = context.folder;
if (!folder) return;
context.addItem((item: any) => {
if (!(folder instanceof TFolder)) return;
context.addItem((item) => {
item.setTitle("Export this folder")
.setIcon("file-output")
.onClick(() => this.openExportModal(undefined, folder));
Expand All @@ -98,6 +128,23 @@ export default class DocumentExporterPlugin extends Plugin {
}
}

private getNotebookNavigatorMenus(): NotebookNavigatorMenus | null {
const app = this.app as AppWithPluginRegistry;
const registry = app.plugins?.plugins;
if (!registry) return null;

const notebookNavigator = registry["notebook-navigator"];
if (!notebookNavigator || typeof notebookNavigator !== "object") return null;

const api = (notebookNavigator as { api?: unknown }).api;
if (!api || typeof api !== "object") return null;

const menus = (api as { menus?: unknown }).menus;
if (!isNotebookNavigatorMenus(menus)) return null;

return menus;
}

async saveSettings() {
await saveSettings(this, this.settings);
}
Expand Down
15 changes: 8 additions & 7 deletions src/ui/ProgressNotice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("ProgressNotice", () => {
value: window.document,
configurable: true,
});
Object.defineProperty(document, "hasFocus", {
Object.defineProperty(activeDocument, "hasFocus", {
value: () => true,
configurable: true,
});
Expand Down Expand Up @@ -130,7 +130,7 @@ describe("ProgressNotice", () => {
notify(title, options);
});
window.Notification.permission = "granted";
Object.defineProperty(document, "hasFocus", {
Object.defineProperty(activeDocument, "hasFocus", {
value: () => false,
configurable: true,
});
Expand All @@ -149,16 +149,17 @@ describe("ProgressNotice", () => {
window.Notification = { permission: "default" };
window.require = vi.fn((moduleId: string) => {
if (moduleId !== "electron") return undefined;
function TestNotification(options: { title: string; body?: string }): { show: () => void } {
notify(options);
return { show: vi.fn() };
}
return {
remote: {
Notification: vi.fn(function (options: { title: string; body?: string }) {
notify(options);
this.show = vi.fn();
}),
Notification: TestNotification,
},
};
});
Object.defineProperty(document, "hasFocus", {
Object.defineProperty(activeDocument, "hasFocus", {
value: () => false,
configurable: true,
});
Expand Down
2 changes: 1 addition & 1 deletion src/ui/ProgressNotice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class ProgressNotice {
}

private notifyWhenUnfocused(message: string): void {
if (typeof document === "undefined" || document.hasFocus()) return;
if (typeof activeDocument === "undefined" || activeDocument.hasFocus()) return;

try {
if (this.notifyViaWebNotification(message)) return;
Expand Down
3 changes: 2 additions & 1 deletion versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"0.4.5": "1.4.0",
"0.4.6": "1.4.0",
"0.4.7": "1.4.0",
"0.4.8": "1.4.0"
"0.4.8": "1.4.0",
"0.4.9": "1.4.0"
}
Loading