From 61ac85caa0c9beab3c2c1e37fc4ab5eb14f3f91d Mon Sep 17 00:00:00 2001 From: Roger Deng <13251150+rogerdigital@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:36:47 +0800 Subject: [PATCH] fix: clear scorecard warnings for 0.4.9 --- .github/workflows/ci.yml | 1 + manifest.json | 2 +- package-lock.json | 4 +- package.json | 3 +- src/export/ExportRunner.test.ts | 2 +- src/formats/docx.test.ts | 18 +++++++-- src/formats/docx.ts | 9 +++-- src/formats/pdf.ts | 23 +++++++++++- src/main.ts | 65 ++++++++++++++++++++++++++++----- src/ui/ProgressNotice.test.ts | 15 ++++---- src/ui/ProgressNotice.ts | 2 +- versions.json | 3 +- 12 files changed, 116 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17e3daf..f70a9e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/manifest.json b/manifest.json index 7156d97..f0f835d 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 1e39bd4..fc501de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "document-exporter", - "version": "0.4.8", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "document-exporter", - "version": "0.4.8", + "version": "0.4.9", "license": "MIT", "devDependencies": { "@types/node": "^20.11.0", diff --git a/package.json b/package.json index a1ec9c0..97c5e52 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/export/ExportRunner.test.ts b/src/export/ExportRunner.test.ts index 89f48df..76042a9 100644 --- a/src/export/ExportRunner.test.ts +++ b/src/export/ExportRunner.test.ts @@ -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(); diff --git a/src/formats/docx.test.ts b/src/formats/docx.test.ts index ac722e2..d796bd3 100644 --- a/src/formats/docx.test.ts +++ b/src/formats/docx.test.ts @@ -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 = ""; @@ -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; }), @@ -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; }), @@ -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; }), @@ -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; }), diff --git a/src/formats/docx.ts b/src/formats/docx.ts index 1236b7e..6eb9af5 100644 --- a/src/formats/docx.ts +++ b/src/formats/docx.ts @@ -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); @@ -379,7 +379,10 @@ function findImage(ref: string, imageMap: Map): 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; } diff --git a/src/formats/pdf.ts b/src/formats/pdf.ts index 6d9e67b..925191c 100644 --- a/src/formats/pdf.ts +++ b/src/formats/pdf.ts @@ -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; +} diff --git a/src/main.ts b/src/main.ts index 0933b60..6e93c93 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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"; @@ -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; + }; +}; + +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; @@ -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)); @@ -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)); @@ -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); } diff --git a/src/ui/ProgressNotice.test.ts b/src/ui/ProgressNotice.test.ts index b083e19..1466daa 100644 --- a/src/ui/ProgressNotice.test.ts +++ b/src/ui/ProgressNotice.test.ts @@ -59,7 +59,7 @@ describe("ProgressNotice", () => { value: window.document, configurable: true, }); - Object.defineProperty(document, "hasFocus", { + Object.defineProperty(activeDocument, "hasFocus", { value: () => true, configurable: true, }); @@ -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, }); @@ -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, }); diff --git a/src/ui/ProgressNotice.ts b/src/ui/ProgressNotice.ts index 57f7e24..7c8b290 100644 --- a/src/ui/ProgressNotice.ts +++ b/src/ui/ProgressNotice.ts @@ -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; diff --git a/versions.json b/versions.json index 4a0745d..81aed12 100644 --- a/versions.json +++ b/versions.json @@ -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" } \ No newline at end of file