diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 519f729..08e8f47 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,10 +1,6 @@ import esbuild from "esbuild"; import process from "process"; -import fs from "fs"; -import { builtinModules, createRequire } from 'node:module'; -import { fileURLToPath } from 'node:url'; - -const require = createRequire(import.meta.url); +import { builtinModules } from 'node:module'; const banner = `/* @@ -15,51 +11,6 @@ if you want to view the source, please visit the github repository of this plugi const prod = (process.argv[2] === "production"); -// punycode is a deprecated Node.js built-in; filter it from externals so the -// npm `punycode` package gets bundled instead. Some transitive deps use the -// package-path form `require('punycode/')` which Electron can't resolve at -// runtime unless it is inlined by esbuild. -const externalModules = builtinModules.filter(m => m !== 'punycode'); - -/** Resolves `punycode/` (trailing-slash package-path form) to the npm package. */ -const punycodePlugin = { - name: 'punycode-fix', - setup(build) { - build.onResolve({ filter: /^punycode\/$/ }, async (args) => { - const result = await build.resolve('punycode', { - resolveDir: args.resolveDir, - kind: args.kind, - }); - return result; - }); - }, -}; - -/** - * Inlines pdfjs-dist's worker file as a JS string module. - * PdfView.ts imports it and creates a Blob URL at runtime — no external - * worker file needed, works in Electron's sandboxed renderer. - */ -const pdfWorkerPlugin = { - name: 'pdf-worker-inline', - setup(build) { - const workerPath = require.resolve('pdfjs-dist/build/pdf.worker.min.js'); - - build.onResolve({ filter: /^pdfjs-worker-src$/ }, () => ({ - path: workerPath, - namespace: 'pdf-worker-text', - })); - - build.onLoad({ filter: /.*/, namespace: 'pdf-worker-text' }, async (args) => { - const content = await fs.promises.readFile(args.path, 'utf-8'); - return { - contents: `module.exports = ${JSON.stringify(content)};`, - loader: 'js', - }; - }); - }, -}; - const context = await esbuild.context({ banner: { js: banner, @@ -81,8 +32,7 @@ const context = await esbuild.context({ "@lezer/common", "@lezer/highlight", "@lezer/lr", - ...externalModules], - plugins: [punycodePlugin, pdfWorkerPlugin], + ...builtinModules], format: "cjs", target: "es2018", logLevel: "info", diff --git a/manifest.json b/manifest.json index a598587..c19967f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "viewitall", "name": "View It All", - "version": "1.5.0", + "version": "2.0.1", "minAppVersion": "0.15.0", - "description": "View and edit MS (.docx - .xlsx - .csv - .pptx ) and more file types directly inside the app — no external apps needed.", + "description": "View and edit Word documents (.docx) natively in Obsidian.", "author": "ROOCKY.dev", "authorUrl": "https://roocky.dev", "fundingUrl": "https://ko-fi.com/r00cky", diff --git a/package-lock.json b/package-lock.json index bd0ce9c..3f0a261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,16 @@ { "name": "viewitall-md", - "version": "1.4.2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "viewitall-md", - "version": "1.4.2", + "version": "2.0.0", "license": "MIT", "dependencies": { - "chart.js": "^4.5.1", - "html-to-docx": "^1.8.0", "jszip": "^3.10.1", - "mammoth": "^1.11.0", - "obsidian": "latest", - "pdf-lib": "^1.17.1", - "pdfjs-dist": "^3.11.174", - "pptxviewjs": "^1.1.8", - "xlsx": "^0.18.5" + "obsidian": "latest" }, "devDependencies": { "@eslint/js": "9.30.1", @@ -704,46 +697,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "license": "MIT" - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", @@ -827,111 +780,6 @@ "node": ">= 8" } }, - "node_modules/@oozcitak/dom": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.6.tgz", - "integrity": "sha512-k4uEIa6DI3FCrFJMGq/05U/59WnS9DjME0kaPqBRCJAqBTkmopbYV1Xs4qFKbDJ/9wOg8W97p+1E0heng/LH7g==", - "license": "MIT", - "dependencies": { - "@oozcitak/infra": "1.0.5", - "@oozcitak/url": "1.0.0", - "@oozcitak/util": "8.3.4" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@oozcitak/infra": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.5.tgz", - "integrity": "sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==", - "license": "MIT", - "dependencies": { - "@oozcitak/util": "8.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@oozcitak/infra/node_modules/@oozcitak/util": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz", - "integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@oozcitak/url": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.0.tgz", - "integrity": "sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==", - "license": "MIT", - "dependencies": { - "@oozcitak/infra": "1.0.3", - "@oozcitak/util": "1.0.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@oozcitak/url/node_modules/@oozcitak/infra": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.3.tgz", - "integrity": "sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==", - "license": "MIT", - "dependencies": { - "@oozcitak/util": "1.0.1" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@oozcitak/url/node_modules/@oozcitak/infra/node_modules/@oozcitak/util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.1.tgz", - "integrity": "sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@oozcitak/url/node_modules/@oozcitak/util": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.2.tgz", - "integrity": "sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/@oozcitak/util": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.4.tgz", - "integrity": "sha512-6gH/bLQJSJEg7OEpkH4wGQdA8KXHRbzL1YkGyUO12YNAgV3jxKy4K9kvfXj4+9T0OLug5k58cnPCKSSIKzp7pg==", - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/@pdf-lib/standard-fonts": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", - "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", - "license": "MIT", - "dependencies": { - "pako": "^1.0.6" - } - }, - "node_modules/@pdf-lib/upng": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", - "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", - "license": "MIT", - "dependencies": { - "pako": "^1.0.10" - } - }, "node_modules/@pkgr/core": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz", @@ -1278,22 +1126,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC", - "optional": true - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1317,28 +1149,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1356,16 +1166,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1382,28 +1182,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1601,40 +1379,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1654,12 +1406,6 @@ "node": ">=8" } }, - "node_modules/browser-split": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.1.tgz", - "integrity": "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==", - "license": "MIT" - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1683,6 +1429,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1696,6 +1443,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1718,44 +1466,6 @@ "node": ">=6" } }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1773,37 +1483,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "license": "MIT", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1821,50 +1500,22 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC", - "optional": true - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -1945,7 +1596,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1959,19 +1610,6 @@ } } }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2015,29 +1653,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", - "license": "BSD-2-Clause" - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2051,80 +1666,11 @@ "node": ">=0.10.0" } }, - "node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/dom-serializer/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" - }, - "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "1" - } - }, - "node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "license": "BSD", - "dependencies": { - "underscore": "^1.13.1" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2135,13 +1681,6 @@ "node": ">= 0.4" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, "node_modules/empathic": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", @@ -2166,43 +1705,6 @@ "node": ">=10.13.0" } }, - "node_modules/ent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", - "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "punycode": "^1.4.1", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ent/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "license": "MIT" - }, - "node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "license": "BSD-2-Clause" - }, - "node_modules/error": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz", - "integrity": "sha512-SNDKualLUtT4StGFP7xNfuFybL2f6iJujFtrWuvJqGbVQGaN+adE23veqzPz1hjUjTunLi2EnJ+0SJxtbJreKw==", - "dependencies": { - "camelize": "^1.0.0", - "string-template": "~0.2.0", - "xtend": "~4.0.0" - } - }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -2276,6 +1778,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2285,6 +1788,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2322,6 +1826,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3058,14 +2563,6 @@ "node": ">=0.10.0" } }, - "node_modules/ev-store": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ev-store/-/ev-store-7.0.0.tgz", - "integrity": "sha512-otazchNRnGzp2YarBJ+GXKVGvhxVATB1zmaStxJBYet0Dyq7A9VhH8IUEB/gRcL6Ch52lfpgPTRJ2m49epyMsQ==", - "dependencies": { - "individual": "^3.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3224,52 +2721,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3306,28 +2762,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -3342,6 +2776,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3366,6 +2801,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3406,28 +2842,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3441,16 +2855,6 @@ "node": ">=10.13.0" } }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "license": "MIT", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -3485,6 +2889,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3563,6 +2968,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3575,6 +2981,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3586,17 +2993,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC", - "optional": true - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3605,81 +3006,6 @@ "node": ">= 0.4" } }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/html-to-docx": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/html-to-docx/-/html-to-docx-1.8.0.tgz", - "integrity": "sha512-IiMBWIqXM4+cEsW//RKoonWV7DlXAJBmmKI73XJSVWTIXjGUaxSr2ck1jqzVRZknpvO8xsFnVicldKVAWrBYBA==", - "license": "MIT", - "dependencies": { - "@oozcitak/dom": "1.15.6", - "@oozcitak/util": "8.3.4", - "color-name": "^1.1.4", - "html-entities": "^2.3.3", - "html-to-vdom": "^0.7.0", - "image-size": "^1.0.0", - "image-to-base64": "^2.2.0", - "jszip": "^3.7.1", - "lodash": "^4.17.21", - "mime-types": "^2.1.35", - "nanoid": "^3.1.25", - "virtual-dom": "^2.1.1", - "xmlbuilder2": "2.1.2" - } - }, - "node_modules/html-to-vdom": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/html-to-vdom/-/html-to-vdom-0.7.0.tgz", - "integrity": "sha512-k+d2qNkbx0JO00KezQsNcn6k2I/xSBP4yXYFLvXbcasTTDh+RDLUJS3puxqyNnpdyXWRHFGoKU7cRmby8/APcQ==", - "license": "ISC", - "dependencies": { - "ent": "^2.0.0", - "htmlparser2": "^3.8.2" - } - }, - "node_modules/htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "license": "MIT", - "dependencies": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3690,30 +3016,6 @@ "node": ">= 4" } }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/image-to-base64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/image-to-base64/-/image-to-base64-2.2.0.tgz", - "integrity": "sha512-Z+aMwm/91UOQqHhrz7Upre2ytKhWejZlWV/JxUTD1sT7GWWKFDJUEV5scVQKnkzSgPHFuQBUEWcanO+ma0PSVw==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.0" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -3747,23 +3049,6 @@ "node": ">=0.8.19" } }, - "node_modules/individual": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz", - "integrity": "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3946,16 +3231,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4042,19 +3317,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", - "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -4501,12 +3768,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4527,70 +3788,11 @@ "loose-envify": "cli.js" } }, - "node_modules/lop": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", - "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", - "license": "BSD-2-Clause", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "optional": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mammoth": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz", - "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", - "license": "BSD-2-Clause", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/mammoth/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4620,54 +3822,11 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/min-document": { - "version": "2.19.2", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", - "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", - "license": "MIT", - "dependencies": { - "dom-walk": "^0.1.0" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4686,56 +3845,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/module-replacements": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/module-replacements/-/module-replacements-2.10.1.tgz", @@ -4756,34 +3865,9 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "devOptional": true, + "dev": true, "license": "MIT" }, - "node_modules/nan": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", - "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", - "license": "MIT", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4791,67 +3875,11 @@ "dev": true, "license": "MIT" }, - "node_modules/next-tick": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", - "integrity": "sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4984,22 +4012,6 @@ "@codemirror/view": "6.38.6" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", - "license": "BSD-2-Clause" - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5097,15 +4109,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5123,47 +4126,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path2d-polyfill": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", - "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pdf-lib": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", - "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", - "license": "MIT", - "dependencies": { - "@pdf-lib/standard-fonts": "^1.0.0", - "@pdf-lib/upng": "^1.0.1", - "pako": "^1.0.11", - "tslib": "^1.11.1" - } - }, - "node_modules/pdf-lib/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/pdfjs-dist": { - "version": "3.11.174", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", - "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" - } - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5187,27 +4149,6 @@ "node": ">= 0.4" } }, - "node_modules/pptxviewjs": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pptxviewjs/-/pptxviewjs-1.1.8.tgz", - "integrity": "sha512-Nk3uIg1H7WkigKIKZPcTrcmV4RMpRSHvG4jWAO9aKPD1MWkOF8fwqtypsF+kzUZvIzO0BA/eKK+zNK7/R7WrDg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "chart.js": ">=4.4.1", - "jszip": ">=3.10.1" - }, - "peerDependenciesMeta": { - "chart.js": { - "optional": true - }, - "jszip": { - "optional": true - } - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5234,15 +4175,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5271,15 +4203,6 @@ "node": ">=6" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5308,20 +4231,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5448,23 +4357,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5513,6 +4405,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -5560,6 +4453,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5577,19 +4471,12 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC", - "optional": true - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5744,64 +4631,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "license": "MIT", - "optional": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -5816,35 +4645,6 @@ "node": ">= 0.4" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -5943,19 +4743,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6050,25 +4837,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6111,12 +4879,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -6310,12 +5072,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/underscore": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -6339,22 +5095,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/virtual-dom": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/virtual-dom/-/virtual-dom-2.1.1.tgz", - "integrity": "sha512-wb6Qc9Lbqug0kRqo/iuApfBpJJAq14Sk1faAnSmtqXiwahg7PVTvWMs9L02Z8nNIMqbwsxzBAA90bbtRLbw0zg==", - "license": "MIT", - "dependencies": { - "browser-split": "0.0.1", - "error": "^4.3.0", - "ev-store": "^7.0.0", - "global": "^4.3.0", - "is-object": "^1.0.1", - "next-tick": "^0.2.2", - "x-is-array": "0.1.0", - "x-is-string": "0.1.0" - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -6362,22 +5102,6 @@ "license": "MIT", "peer": true }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6483,34 +5207,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -6521,115 +5217,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC", - "optional": true - }, - "node_modules/x-is-array": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/x-is-array/-/x-is-array-0.1.0.tgz", - "integrity": "sha512-goHPif61oNrr0jJgsXRfc8oqtYzvfiMJpTqwE7Z4y9uH+T3UozkGqQ4d2nX9mB9khvA8U2o/UbPOFjgC7hLWIA==" - }, - "node_modules/x-is-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", - "integrity": "sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==" - }, - "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, - "bin": { - "xlsx": "bin/xlsx.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlbuilder2": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-2.1.2.tgz", - "integrity": "sha512-PI710tmtVlQ5VmwzbRTuhmVhKnj9pM8Si+iOZCV2g2SNo3gCrpzR2Ka9wNzZtqfD+mnP+xkrqoNy0sjKZqP4Dg==", - "license": "MIT", - "dependencies": { - "@oozcitak/dom": "1.15.5", - "@oozcitak/infra": "1.0.5", - "@oozcitak/util": "8.3.3" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/xmlbuilder2/node_modules/@oozcitak/dom": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.5.tgz", - "integrity": "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==", - "license": "MIT", - "dependencies": { - "@oozcitak/infra": "1.0.5", - "@oozcitak/url": "1.0.0", - "@oozcitak/util": "8.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/xmlbuilder2/node_modules/@oozcitak/dom/node_modules/@oozcitak/util": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz", - "integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/xmlbuilder2/node_modules/@oozcitak/util": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.3.tgz", - "integrity": "sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC", - "optional": true - }, "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", diff --git a/package.json b/package.json index a08c445..a2985a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "viewitall-md", - "version": "1.5.0", - "description": "Open and edit PDF, Word, Excel, CSV, and PowerPoint files directly inside Obsidian.", + "version": "2.0.0", + "description": "View and edit Word documents (.docx) natively in Obsidian.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", @@ -10,9 +10,6 @@ "version:patch": "npm version patch --no-git-tag-version && node version-bump.mjs && git add package.json manifest.json versions.json", "version:minor": "npm version minor --no-git-tag-version && node version-bump.mjs && git add package.json manifest.json versions.json", "version:major": "npm version major --no-git-tag-version && node version-bump.mjs && git add package.json manifest.json versions.json", - "release:patch": "npm version patch", - "release:minor": "npm version minor", - "release:major": "npm version major", "lint": "eslint .", "lint:css": "node scripts/check-css.js", "lint:all": "eslint . && node scripts/check-css.js" @@ -32,14 +29,7 @@ "typescript-eslint": "8.35.1" }, "dependencies": { - "chart.js": "^4.5.1", - "html-to-docx": "^1.8.0", "jszip": "^3.10.1", - "mammoth": "^1.11.0", - "obsidian": "latest", - "pdf-lib": "^1.17.1", - "pdfjs-dist": "^3.11.174", - "pptxviewjs": "^1.1.8", - "xlsx": "^0.18.5" + "obsidian": "latest" } } diff --git a/src/docx/model.ts b/src/docx/model.ts new file mode 100644 index 0000000..f398e4e --- /dev/null +++ b/src/docx/model.ts @@ -0,0 +1,196 @@ +/** + * ViewItAll — DOCX Document Model + * + * Pure TypeScript interfaces representing the structure of a .docx (OOXML) + * document. No DOM, no Obsidian — this is a data-only layer. + */ + +// ── Block-level elements ──────────────────────────────────────────────────── + +export type DocxBlockElement = + | DocxParagraph + | DocxTable + | DocxPageBreak + | DocxSectionBreak; + +export interface DocxParagraph { + type: "paragraph"; + styleId: string | undefined; + properties: DocxParagraphProperties; + children: DocxInlineElement[]; + numberingId: string | undefined; + numberingLevel: number; +} + +export interface DocxParagraphProperties { + alignment: "left" | "center" | "right" | "both" | undefined; + headingLevel: number | undefined; + indentation: + | { left: number; right: number; firstLine: number; hanging: number } + | undefined; + spacing: + | { before: number; after: number; line: number; lineRule: string } + | undefined; + /** Numbering ID from style or direct pPr */ + numberingId: string | undefined; + /** Numbering indent level */ + numberingLevel: number | undefined; +} + +export interface DocxPageBreak { + type: "pageBreak"; +} + +export interface DocxSectionBreak { + type: "sectionBreak"; +} + +// ── Inline elements ───────────────────────────────────────────────────────── + +export type DocxInlineElement = + | DocxRun + | DocxHyperlink + | DocxImage + | DocxBreak + | DocxTab; + +export interface DocxRun { + type: "run"; + text: string; + /** Run-level overrides — only properties explicitly in the XML are set. */ + properties: Partial; +} + +export interface DocxRunProperties { + bold: boolean; + italic: boolean; + underline: boolean; + strikethrough: boolean; + fontFamily: string | undefined; + fontSize: number | undefined; + color: string | undefined; + highlight: string | undefined; + vertAlign: "superscript" | "subscript" | undefined; +} + +export interface DocxHyperlink { + type: "hyperlink"; + url: string; + children: DocxRun[]; +} + +export interface DocxImage { + type: "image"; + relationshipId: string; + width: number; + height: number; + altText: string | undefined; +} + +export interface DocxBreak { + type: "break"; + breakType: "line" | "page" | "column"; +} + +export interface DocxTab { + type: "tab"; +} + +// ── Table ─────────────────────────────────────────────────────────────────── + +export interface DocxTable { + type: "table"; + rows: DocxTableRow[]; + properties: DocxTableProperties; +} + +export interface DocxTableRow { + cells: DocxTableCell[]; + isHeader: boolean; +} + +export interface DocxTableCell { + paragraphs: DocxParagraph[]; + properties: DocxTableCellProperties; +} + +export interface DocxTableProperties { + width: number | undefined; + alignment: "left" | "center" | "right" | undefined; +} + +export interface DocxTableCellProperties { + width: number | undefined; + verticalMerge: "restart" | "continue" | undefined; + gridSpan: number; + shading: string | undefined; + verticalAlign: "top" | "center" | "bottom" | undefined; +} + +// ── Styles ────────────────────────────────────────────────────────────────── + +export interface DocxStyle { + id: string; + name: string; + type: "paragraph" | "character" | "table" | "numbering"; + basedOn: string | undefined; + paragraphProperties: Partial; + runProperties: Partial; +} + +// ── Numbering ─────────────────────────────────────────────────────────────── + +export interface DocxNumberingLevel { + format: + | "decimal" + | "bullet" + | "lowerLetter" + | "upperLetter" + | "lowerRoman" + | "upperRoman" + | "none"; + text: string; + indentLeft: number; +} + +export interface DocxNumberingDef { + abstractNumId: string; + levels: Map; +} + +// ── Root document ─────────────────────────────────────────────────────────── + +export interface DocxDocument { + body: DocxBlockElement[]; + styles: Map; + images: Map; + numbering: Map; + relationships: Map; +} + +// ── Default factories ─────────────────────────────────────────────────────── + +export function defaultRunProperties(): DocxRunProperties { + return { + bold: false, + italic: false, + underline: false, + strikethrough: false, + fontFamily: undefined, + fontSize: undefined, + color: undefined, + highlight: undefined, + vertAlign: undefined, + }; +} + +export function defaultParagraphProperties(): DocxParagraphProperties { + return { + alignment: undefined, + headingLevel: undefined, + indentation: undefined, + spacing: undefined, + numberingId: undefined, + numberingLevel: undefined, + }; +} diff --git a/src/docx/numbering.ts b/src/docx/numbering.ts new file mode 100644 index 0000000..82904be --- /dev/null +++ b/src/docx/numbering.ts @@ -0,0 +1,105 @@ +/** + * ViewItAll — OOXML Numbering Parser + * + * Parses `word/numbering.xml` to resolve list definitions. + */ + +import type { DocxNumberingDef, DocxNumberingLevel } from "./model"; +import { + NS_W, + getElements, + getDirectChild, + getDirectChildren, + getVal, + getWAttr, + parseXml, +} from "../utils/xml"; +import { safeParseInt } from "../utils/units"; + +/** + * Parse numbering.xml content into a map of numId → DocxNumberingDef. + */ +export function parseNumbering( + numberingXml: string, +): Map { + const doc = parseXml(numberingXml); + const result = new Map(); + + const abstractMap = new Map>(); + const abstractElements = getElements(doc, NS_W, "abstractNum"); + + for (const absEl of abstractElements) { + const absId = getWAttr(absEl, "abstractNumId"); + if (!absId) continue; + + const levels = new Map(); + const lvlElements = getDirectChildren(absEl, NS_W, "lvl"); + + for (const lvlEl of lvlElements) { + const ilvl = safeParseInt(getWAttr(lvlEl, "ilvl")); + if (ilvl === undefined) continue; + + const numFmtEl = getDirectChild(lvlEl, NS_W, "numFmt"); + const lvlTextEl = getDirectChild(lvlEl, NS_W, "lvlText"); + const pPrEl = getDirectChild(lvlEl, NS_W, "pPr"); + + const format = parseNumFormat(numFmtEl ? getVal(numFmtEl) : null); + const text = lvlTextEl ? (getVal(lvlTextEl) ?? "") : ""; + + let indentLeft = 0; + if (pPrEl) { + const indEl = getDirectChild(pPrEl, NS_W, "ind"); + if (indEl) { + indentLeft = safeParseInt(getWAttr(indEl, "left")) ?? 0; + } + } + + levels.set(ilvl, { format, text, indentLeft }); + } + + abstractMap.set(absId, levels); + } + + const numElements = getElements(doc, NS_W, "num"); + for (const numEl of numElements) { + const numId = getWAttr(numEl, "numId"); + if (!numId) continue; + + const abstractNumIdEl = getDirectChild(numEl, NS_W, "abstractNumId"); + const abstractNumId = abstractNumIdEl + ? (getVal(abstractNumIdEl) ?? "") + : ""; + + const levels = abstractMap.get(abstractNumId) ?? new Map(); + + result.set(numId, { + abstractNumId: abstractNumId, + levels, + }); + } + + return result; +} + +function parseNumFormat( + val: string | null, +): DocxNumberingLevel["format"] { + switch (val) { + case "decimal": + return "decimal"; + case "bullet": + return "bullet"; + case "lowerLetter": + return "lowerLetter"; + case "upperLetter": + return "upperLetter"; + case "lowerRoman": + return "lowerRoman"; + case "upperRoman": + return "upperRoman"; + case "none": + return "none"; + default: + return "bullet"; + } +} diff --git a/src/docx/parser.ts b/src/docx/parser.ts new file mode 100644 index 0000000..0d050f1 --- /dev/null +++ b/src/docx/parser.ts @@ -0,0 +1,490 @@ +/** + * ViewItAll — OOXML Document Parser + * + * Main orchestrator: extracts ZIP → parses XML → builds DocxDocument model. + * Uses JSZip (dynamic import) for ZIP extraction and browser DOMParser for XML. + */ + +import type { + DocxDocument, + DocxBlockElement, + DocxParagraph, + DocxInlineElement, + DocxRun, + DocxHyperlink, + DocxImage, + DocxTable, + DocxTableRow, + DocxTableCell, + DocxRunProperties, +} from "./model"; +import { defaultParagraphProperties } from "./model"; +import { + NS_W, + NS_R, + NS_WP, + NS_A, + NS_PIC, + getElement, + getElements, + getDirectChild, + getDirectChildren, + getVal, + getWAttr, + getAttr, + parseXml, +} from "../utils/xml"; +import { emuToPx, safeParseInt } from "../utils/units"; +import { parseRelationships, REL_HYPERLINK, REL_IMAGE } from "./relationships"; +import { parseStyles, parseParagraphProperties, parseRunProperties } from "./styles"; +import { parseNumbering } from "./numbering"; + +/** + * Parse a .docx ArrayBuffer into a DocxDocument model. + */ +export async function parseDocx(data: ArrayBuffer): Promise { + // Dynamic import — JSZip is a heavy lib + const JSZip = (await import("jszip")).default; + const zip = await JSZip.loadAsync(data); + + // ── Extract core XML files ────────────────────────────────────────── + const documentXml = await readZipText(zip, "word/document.xml"); + if (!documentXml) { + throw new Error("Invalid .docx: missing word/document.xml"); + } + + const relsXml = await readZipText(zip, "word/_rels/document.xml.rels"); + const stylesXml = await readZipText(zip, "word/styles.xml"); + const numberingXml = await readZipText(zip, "word/numbering.xml"); + + // ── Parse supporting structures ───────────────────────────────────── + const relationships = relsXml + ? parseRelationships(relsXml) + : new Map(); + + const styles = stylesXml ? parseStyles(stylesXml) : new Map(); + const numbering = numberingXml ? parseNumbering(numberingXml) : new Map(); + + // ── Extract images ────────────────────────────────────────────────── + const images = new Map(); + for (const [id, rel] of relationships) { + if (rel.type === REL_IMAGE) { + const imagePath = `word/${rel.target}`; + const imageFile = zip.file(imagePath); + if (imageFile) { + const imageData = await imageFile.async("arraybuffer"); + const ext = rel.target.split(".").pop()?.toLowerCase() ?? ""; + const mimeType = getMimeType(ext); + images.set(id, new Blob([imageData], { type: mimeType })); + } + } + } + + // ── Parse document body ───────────────────────────────────────────── + const docXml = parseXml(documentXml); + const bodyEl = getElement(docXml, NS_W, "body"); + if (!bodyEl) { + throw new Error("Invalid .docx: missing w:body element"); + } + + const relMap = new Map(); + for (const [id, rel] of relationships) { + relMap.set(id, { type: rel.type, target: rel.target }); + } + + const body = parseBody(bodyEl, relationships); + + return { + body, + styles, + images, + numbering, + relationships: relMap, + }; +} + +// ── Body Parser ───────────────────────────────────────────────────────────── + +function parseBody( + bodyEl: Element, + relationships: Map, +): DocxBlockElement[] { + const blocks: DocxBlockElement[] = []; + + for (let i = 0; i < bodyEl.childNodes.length; i++) { + const node = bodyEl.childNodes.item(i); + if (!node || node.nodeType !== Node.ELEMENT_NODE) continue; + const el = node as Element; + + if (el.localName === "p" && el.namespaceURI === NS_W) { + blocks.push(parseParagraph(el, relationships)); + } else if (el.localName === "tbl" && el.namespaceURI === NS_W) { + blocks.push(parseTable(el, relationships)); + } else if (el.localName === "sdt" && el.namespaceURI === NS_W) { + // Structured document tags — unwrap and parse content + const sdtContent = getDirectChild(el, NS_W, "sdtContent"); + if (sdtContent) { + const inner = parseBody(sdtContent, relationships); + blocks.push(...inner); + } + } + } + + return blocks; +} + +// ── Paragraph Parser ──────────────────────────────────────────────────────── + +function parseParagraph( + pEl: Element, + relationships: Map, +): DocxParagraph { + const pPrEl = getDirectChild(pEl, NS_W, "pPr"); + + // Style ID + let styleId: string | undefined; + if (pPrEl) { + const pStyleEl = getDirectChild(pPrEl, NS_W, "pStyle"); + if (pStyleEl) { + styleId = getVal(pStyleEl) ?? undefined; + } + } + + // Paragraph properties (includes numbering from direct pPr) + const properties = pPrEl + ? { ...defaultParagraphProperties(), ...parseParagraphProperties(pPrEl) } + : defaultParagraphProperties(); + + // Numbering: use direct pPr numbering if present + const numberingId = properties.numberingId; + const numberingLevel = properties.numberingLevel ?? 0; + + // Check for page break in paragraph properties + const children: DocxInlineElement[] = []; + + // Parse inline children + for (let i = 0; i < pEl.childNodes.length; i++) { + const node = pEl.childNodes.item(i); + if (!node || node.nodeType !== Node.ELEMENT_NODE) continue; + const el = node as Element; + + if (el.localName === "r" && el.namespaceURI === NS_W) { + const inlines = parseRun(el); + children.push(...inlines); + } else if (el.localName === "hyperlink" && el.namespaceURI === NS_W) { + const hyperlink = parseHyperlink(el, relationships); + if (hyperlink) children.push(hyperlink); + } else if (el.localName === "sdt" && el.namespaceURI === NS_W) { + // Structured document tags — unwrap and parse inline content + const sdtContent = getDirectChild(el, NS_W, "sdtContent"); + if (sdtContent) { + for (let j = 0; j < sdtContent.childNodes.length; j++) { + const sdtNode = sdtContent.childNodes.item(j); + if (!sdtNode || sdtNode.nodeType !== Node.ELEMENT_NODE) continue; + const sdtEl = sdtNode as Element; + if (sdtEl.localName === "r" && sdtEl.namespaceURI === NS_W) { + children.push(...parseRun(sdtEl)); + } else if (sdtEl.localName === "hyperlink" && sdtEl.namespaceURI === NS_W) { + const hl = parseHyperlink(sdtEl, relationships); + if (hl) children.push(hl); + } + } + } + } + } + + return { + type: "paragraph", + styleId, + properties, + children, + numberingId, + numberingLevel, + }; +} + +// ── Run Parser ────────────────────────────────────────────────────────────── + +function parseRun(rEl: Element): DocxInlineElement[] { + const results: DocxInlineElement[] = []; + + // Run properties — keep as Partial so mergeRunProps knows what was explicit + const rPrEl = getDirectChild(rEl, NS_W, "rPr"); + const properties: Partial = rPrEl + ? parseRunProperties(rPrEl) + : {}; + + for (let i = 0; i < rEl.childNodes.length; i++) { + const node = rEl.childNodes.item(i); + if (!node || node.nodeType !== Node.ELEMENT_NODE) continue; + const child = node as Element; + + if (child.localName === "t" && child.namespaceURI === NS_W) { + const text = child.textContent ?? ""; + if (text) { + results.push({ type: "run", text, properties }); + } + } else if (child.localName === "br" && child.namespaceURI === NS_W) { + const breakType = getWAttr(child, "type"); + if (breakType === "page") { + results.push({ type: "break", breakType: "page" }); + } else if (breakType === "column") { + results.push({ type: "break", breakType: "column" }); + } else { + results.push({ type: "break", breakType: "line" }); + } + } else if (child.localName === "tab" && child.namespaceURI === NS_W) { + results.push({ type: "tab" }); + } else if (child.localName === "drawing" && child.namespaceURI === NS_W) { + const image = parseDrawing(child); + if (image) results.push(image); + } else if (child.localName === "pict" && child.namespaceURI === NS_W) { + // Legacy VML images — skip for MVP + } + } + + return results; +} + +// ── Hyperlink Parser ──────────────────────────────────────────────────────── + +function parseHyperlink( + hlEl: Element, + relationships: Map, +): DocxHyperlink | null { + // Resolve URL from relationship ID + const rId = + getAttr(hlEl, NS_R, "id") ?? + hlEl.getAttribute("r:id"); + + let url = ""; + if (rId) { + const rel = relationships.get(rId); + if (rel && rel.type === REL_HYPERLINK) { + url = rel.target; + } + } + + // Also check for anchor (internal bookmark) + if (!url) { + const anchor = getWAttr(hlEl, "anchor"); + if (anchor) { + url = `#${anchor}`; + } + } + + // Parse child runs + const children: DocxRun[] = []; + const runElements = getDirectChildren(hlEl, NS_W, "r"); + for (const rEl of runElements) { + const inlines = parseRun(rEl); + for (const inline of inlines) { + if (inline.type === "run") { + children.push(inline); + } + } + } + + if (children.length === 0) return null; + + return { type: "hyperlink", url, children }; +} + +// ── Drawing (Image) Parser ────────────────────────────────────────────────── + +function parseDrawing(drawingEl: Element): DocxImage | null { + // Inline images: wp:inline or wp:anchor + const inlineEl = + getElement(drawingEl, NS_WP, "inline") ?? + getElement(drawingEl, NS_WP, "anchor"); + if (!inlineEl) return null; + + // Dimensions from wp:extent + const extentEl = getElement(inlineEl, NS_WP, "extent"); + let width = 0; + let height = 0; + if (extentEl) { + const cx = safeParseInt(extentEl.getAttribute("cx")); + const cy = safeParseInt(extentEl.getAttribute("cy")); + if (cx) width = emuToPx(cx); + if (cy) height = emuToPx(cy); + } + + // Alt text from wp:docPr + const docPrEl = getElement(inlineEl, NS_WP, "docPr"); + const altText = docPrEl?.getAttribute("descr") ?? undefined; + + // Find the blip (actual image reference) + const blipEl = getElement(drawingEl, NS_A, "blip"); + if (!blipEl) return null; + + const rId = + getAttr(blipEl, NS_R, "embed") ?? + blipEl.getAttribute("r:embed") ?? + getAttr(blipEl, NS_R, "link") ?? + blipEl.getAttribute("r:link"); + + if (!rId) return null; + + return { + type: "image", + relationshipId: rId, + width, + height, + altText, + }; +} + +// ── Table Parser ──────────────────────────────────────────────────────────── + +function parseTable( + tblEl: Element, + relationships: Map, +): DocxTable { + // Table properties + const tblPrEl = getDirectChild(tblEl, NS_W, "tblPr"); + let tableWidth: number | undefined; + let tableAlignment: "left" | "center" | "right" | undefined; + + if (tblPrEl) { + const tblWEl = getDirectChild(tblPrEl, NS_W, "tblW"); + if (tblWEl) { + tableWidth = safeParseInt(getWAttr(tblWEl, "w")) ?? undefined; + } + const jcEl = getDirectChild(tblPrEl, NS_W, "jc"); + if (jcEl) { + const val = getVal(jcEl); + if (val === "left" || val === "center" || val === "right") { + tableAlignment = val; + } + } + } + + // Parse rows + const rows: DocxTableRow[] = []; + const trElements = getDirectChildren(tblEl, NS_W, "tr"); + + for (const trEl of trElements) { + const trPrEl = getDirectChild(trEl, NS_W, "trPr"); + const isHeader = trPrEl + ? getDirectChild(trPrEl, NS_W, "tblHeader") !== null + : false; + + const cells: DocxTableCell[] = []; + const tcElements = getDirectChildren(trEl, NS_W, "tc"); + + for (const tcEl of tcElements) { + cells.push(parseTableCell(tcEl, relationships)); + } + + rows.push({ cells, isHeader }); + } + + return { + type: "table", + rows, + properties: { + width: tableWidth, + alignment: tableAlignment, + }, + }; +} + +function parseTableCell( + tcEl: Element, + relationships: Map, +): DocxTableCell { + const tcPrEl = getDirectChild(tcEl, NS_W, "tcPr"); + + let width: number | undefined; + let verticalMerge: "restart" | "continue" | undefined; + let gridSpan = 1; + let shading: string | undefined; + let verticalAlign: "top" | "center" | "bottom" | undefined; + + if (tcPrEl) { + const tcWEl = getDirectChild(tcPrEl, NS_W, "tcW"); + if (tcWEl) { + width = safeParseInt(getWAttr(tcWEl, "w")) ?? undefined; + } + + const vMergeEl = getDirectChild(tcPrEl, NS_W, "vMerge"); + if (vMergeEl) { + const val = getVal(vMergeEl); + verticalMerge = val === "restart" ? "restart" : "continue"; + } + + const gridSpanEl = getDirectChild(tcPrEl, NS_W, "gridSpan"); + if (gridSpanEl) { + gridSpan = safeParseInt(getVal(gridSpanEl)) ?? 1; + } + + const shdEl = getDirectChild(tcPrEl, NS_W, "shd"); + if (shdEl) { + const fill = getWAttr(shdEl, "fill"); + if (fill && fill !== "auto") { + shading = fill; + } + } + + const vAlignEl = getDirectChild(tcPrEl, NS_W, "vAlign"); + if (vAlignEl) { + const val = getVal(vAlignEl); + if (val === "top" || val === "center" || val === "bottom") { + verticalAlign = val; + } + } + } + + // Parse paragraphs inside the cell + const paragraphs: DocxParagraph[] = []; + const pElements = getDirectChildren(tcEl, NS_W, "p"); + for (const pEl of pElements) { + paragraphs.push(parseParagraph(pEl, relationships)); + } + + return { + paragraphs, + properties: { + width, + verticalMerge, + gridSpan, + shading, + verticalAlign, + }, + }; +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +async function readZipText( + zip: { file(path: string): { async(type: "string"): Promise } | null }, + path: string, +): Promise { + const file = zip.file(path); + if (!file) return null; + return file.async("string"); +} + +function getMimeType(ext: string): string { + switch (ext) { + case "png": + return "image/png"; + case "jpg": + case "jpeg": + return "image/jpeg"; + case "gif": + return "image/gif"; + case "svg": + return "image/svg+xml"; + case "bmp": + return "image/bmp"; + case "tiff": + case "tif": + return "image/tiff"; + case "webp": + return "image/webp"; + default: + return "application/octet-stream"; + } +} diff --git a/src/docx/relationships.ts b/src/docx/relationships.ts new file mode 100644 index 0000000..0385d6c --- /dev/null +++ b/src/docx/relationships.ts @@ -0,0 +1,59 @@ +/** + * ViewItAll — OOXML Relationships Parser + * + * Parses `word/_rels/document.xml.rels` to resolve hyperlink URLs + * and image file paths referenced by relationship IDs. + */ + +import { NS_RELS, parseXml } from "../utils/xml"; + +export interface Relationship { + type: string; + target: string; + targetMode: string | undefined; +} + +/** Relationship type URIs */ +export const REL_HYPERLINK = + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"; +export const REL_IMAGE = + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"; +export const REL_NUMBERING = + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"; +export const REL_STYLES = + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"; + +/** + * Parse a .rels XML string into a map of relationship ID → Relationship. + */ +export function parseRelationships( + relsXml: string, +): Map { + const doc = parseXml(relsXml); + const map = new Map(); + + const elements = doc.getElementsByTagNameNS(NS_RELS, "Relationship"); + const fallback = + elements.length === 0 + ? doc.getElementsByTagName("Relationship") + : elements; + + for (let i = 0; i < fallback.length; i++) { + const el = fallback[i]; + if (!el) continue; + + const id = el.getAttribute("Id"); + const type = el.getAttribute("Type"); + const target = el.getAttribute("Target"); + + if (id && type && target) { + map.set(id, { + type, + target, + targetMode: el.getAttribute("TargetMode") ?? undefined, + }); + } + } + + return map; +} diff --git a/src/docx/renderer.ts b/src/docx/renderer.ts new file mode 100644 index 0000000..46a2446 --- /dev/null +++ b/src/docx/renderer.ts @@ -0,0 +1,599 @@ +/** + * ViewItAll — DOCX Document Renderer + * + * Converts a DocxDocument model into native Obsidian DOM using createEl(). + * Never uses innerHTML — all elements are built programmatically. + */ + +import type { + DocxDocument, + DocxBlockElement, + DocxParagraph, + DocxInlineElement, + DocxRun, + DocxHyperlink, + DocxImage, + DocxTable, + DocxTableRow, + DocxTableCell, + DocxRunProperties, + DocxParagraphProperties, +} from "./model"; +import { defaultRunProperties, defaultParagraphProperties } from "./model"; +import { resolveStyle, resolveHeadingLevel } from "./styles"; +import { halfPointsToPt, twipsToPx, dxaToPx } from "../utils/units"; + +// ── Word highlight color → CSS color map ──────────────────────────────────── + +const HIGHLIGHT_COLORS: Record = { + yellow: "rgba(255, 255, 0, 0.4)", + green: "rgba(0, 255, 0, 0.4)", + cyan: "rgba(0, 255, 255, 0.4)", + magenta: "rgba(255, 0, 255, 0.4)", + blue: "rgba(0, 0, 255, 0.4)", + red: "rgba(255, 0, 0, 0.4)", + darkBlue: "rgba(0, 0, 139, 0.4)", + darkCyan: "rgba(0, 139, 139, 0.4)", + darkGreen: "rgba(0, 100, 0, 0.4)", + darkMagenta: "rgba(139, 0, 139, 0.4)", + darkRed: "rgba(139, 0, 0, 0.4)", + darkYellow: "rgba(139, 139, 0, 0.4)", + darkGray: "rgba(169, 169, 169, 0.4)", + lightGray: "rgba(211, 211, 211, 0.4)", + black: "rgba(0, 0, 0, 0.4)", +}; + +/** Render context passed through the rendering pipeline. */ +interface RenderContext { + doc: DocxDocument; + blobUrls: string[]; + styleCache: Map; + /** + * Numbering counters: key = "numId:level", value = current count. + * Reset when a different numId is encountered at the same level. + */ + numberingCounters: Map; + /** Track last numId seen at each level to detect resets */ + lastNumIdAtLevel: Map; +} + +/** + * Render a DocxDocument into a DOM container element. + * Returns an array of blob URLs that must be revoked on cleanup. + */ +export function renderDocument( + doc: DocxDocument, + container: HTMLElement, +): string[] { + const ctx: RenderContext = { + doc, + blobUrls: [], + styleCache: new Map(), + numberingCounters: new Map(), + lastNumIdAtLevel: new Map(), + }; + + renderBlocks(doc.body, container, ctx); + + return ctx.blobUrls; +} + +// ── Block Rendering ───────────────────────────────────────────────────────── + +function renderBlocks( + blocks: DocxBlockElement[], + container: HTMLElement, + ctx: RenderContext, +): void { + for (const block of blocks) { + if (!block) continue; + + if (block.type === "paragraph") { + renderParagraph(block, container, ctx); + } else if (block.type === "table") { + renderTable(block, container, ctx); + } else if (block.type === "pageBreak") { + container.createEl("hr", { cls: "via-docx-page-break" }); + } + } +} + +// ── Paragraph Rendering ───────────────────────────────────────────────────── + +function renderParagraph( + para: DocxParagraph, + container: HTMLElement, + ctx: RenderContext, +): void { + // Resolve heading level from style + let headingLevel = para.properties.headingLevel; + if (!headingLevel && para.styleId) { + const style = ctx.doc.styles.get(para.styleId); + if (style) { + headingLevel = resolveHeadingLevel(style); + } + } + + // Also resolve full style properties + let resolvedRProps = defaultRunProperties(); + let resolvedPProps = defaultParagraphProperties(); + if (para.styleId) { + const resolved = resolveStyle(para.styleId, ctx.doc.styles, ctx.styleCache); + resolvedRProps = resolved.rProps; + resolvedPProps = resolved.pProps; + // Merge paragraph-level heading from style + if (!headingLevel && resolved.pProps.headingLevel) { + headingLevel = resolved.pProps.headingLevel; + } + // Merge alignment from style if not set on paragraph + if (!para.properties.alignment && resolved.pProps.alignment) { + para.properties.alignment = resolved.pProps.alignment; + } + } + + // Resolve numbering: direct paragraph > style-inherited + let numId = para.numberingId; + let numLevel = para.numberingLevel; + if (!numId && resolvedPProps.numberingId) { + numId = resolvedPProps.numberingId; + numLevel = resolvedPProps.numberingLevel ?? 0; + } + + // Generate numbering text prefix if applicable + let numberingPrefix = ""; + if (numId && numId !== "0") { + numberingPrefix = getNumberingText(numId, numLevel, ctx); + } + + // Create the element + let el: HTMLElement; + if (headingLevel && headingLevel >= 1 && headingLevel <= 6) { + const tag = `h${headingLevel}` as keyof HTMLElementTagNameMap; + el = container.createEl(tag, { cls: "via-docx-heading" }); + // Inline style beats any Obsidian theme CSS that colors headings + el.style.color = "inherit"; + } else { + el = container.createEl("p"); + } + + // Apply paragraph styles + applyParagraphStyles(el, para.properties); + + // Check if paragraph is empty (just whitespace/empty runs) + const hasContent = para.children.some((child) => { + if (child.type === "run") return child.text.trim().length > 0; + if (child.type === "hyperlink") return child.children.length > 0; + if (child.type === "image") return true; + if (child.type === "break") return true; + return false; + }); + + if (!hasContent && para.children.length === 0) { + // Empty paragraph — add a zero-width space to preserve spacing + el.createEl("br"); + return; + } + + // Prepend numbering text if present + if (numberingPrefix) { + const numSpan = el.createEl("span", { + cls: "via-docx-num-prefix", + }); + numSpan.textContent = numberingPrefix; + numSpan.style.color = "inherit"; + } + + // Render inline children + for (const child of para.children) { + renderInline(child, el, ctx, resolvedRProps); + } +} + +function applyParagraphStyles( + el: HTMLElement, + props: DocxParagraphProperties, +): void { + if (props.alignment) { + const align = props.alignment === "both" ? "justify" : props.alignment; + el.style.textAlign = align; + } + + if (props.indentation) { + const left = props.indentation.left + ? twipsToPx(props.indentation.left) + : 0; + const right = props.indentation.right + ? twipsToPx(props.indentation.right) + : 0; + const firstLine = props.indentation.firstLine + ? twipsToPx(props.indentation.firstLine) + : 0; + const hanging = props.indentation.hanging + ? twipsToPx(props.indentation.hanging) + : 0; + + if (left > 0) el.style.marginLeft = `${left}px`; + if (right > 0) el.style.marginRight = `${right}px`; + if (firstLine > 0) el.style.textIndent = `${firstLine}px`; + if (hanging > 0) el.style.textIndent = `-${hanging}px`; + } + + if (props.spacing) { + if (props.spacing.before > 0) { + el.style.marginTop = `${twipsToPx(props.spacing.before)}px`; + } + if (props.spacing.after > 0) { + el.style.marginBottom = `${twipsToPx(props.spacing.after)}px`; + } + } +} + +// ── Inline Rendering ──────────────────────────────────────────────────────── + +function renderInline( + inline: DocxInlineElement, + container: HTMLElement, + ctx: RenderContext, + styleRProps: DocxRunProperties, +): void { + switch (inline.type) { + case "run": + renderRun(inline, container, styleRProps); + break; + case "hyperlink": + renderHyperlink(inline, container, ctx, styleRProps); + break; + case "image": + renderImage(inline, container, ctx); + break; + case "break": + renderBreak(inline, container); + break; + case "tab": + container.createEl("span", { + cls: "via-docx-tab", + text: "\u00A0\u00A0\u00A0\u00A0", + }); + break; + } +} + +function renderRun( + run: DocxRun, + container: HTMLElement, + styleRProps: DocxRunProperties, +): void { + if (!run.text) return; + // Merge style-level run properties with run-level overrides + const props = mergeRunProps(styleRProps, run.properties); + + let el: HTMLElement = container.createEl("span"); + el.textContent = run.text; + + // Apply formatting by wrapping in semantic elements + if (props.bold) { + const strong = container.createEl("strong"); + strong.appendChild(el); + el = strong; + } + if (props.italic) { + const em = container.createEl("em"); + em.appendChild(el); + el = em; + } + if (props.underline) { + const u = container.createEl("u"); + u.appendChild(el); + el = u; + } + if (props.strikethrough) { + const s = container.createEl("s"); + s.appendChild(el); + el = s; + } + if (props.vertAlign === "superscript") { + const sup = container.createEl("sup"); + sup.appendChild(el); + el = sup; + } else if (props.vertAlign === "subscript") { + const sub = container.createEl("sub"); + sub.appendChild(el); + el = sub; + } + + // Force color inheritance so Obsidian themes can't override via CSS on + // , , etc. applyRunStyles will overwrite if doc sets color. + el.style.color = "inherit"; + + // Apply inline styles for color, size, highlight, font + applyRunStyles(el, props); + + // Highlight wraps the run in a + if (props.highlight) { + const cssColor = HIGHLIGHT_COLORS[props.highlight]; + if (cssColor) { + const mark = container.createEl("mark", { cls: "via-docx-highlight" }); + mark.style.backgroundColor = cssColor; + mark.appendChild(el); + el = mark; + } + } + + // The element is already appended by createEl, but if we wrapped it, + // we need to ensure the outermost wrapper is in the container + if (el.parentElement !== container) { + container.appendChild(el); + } +} + +function applyRunStyles(el: HTMLElement, props: DocxRunProperties): void { + if (props.color && props.color !== "auto") { + el.style.color = `#${props.color}`; + } + + if (props.fontSize) { + const pt = halfPointsToPt(props.fontSize); + // Use em units relative to parent for native feel + const em = pt / 12; // Assume 12pt base + if (Math.abs(em - 1) > 0.05) { + el.style.fontSize = `${em.toFixed(2)}em`; + } + } + + if (props.fontFamily) { + el.style.fontFamily = `"${props.fontFamily}", var(--font-text)`; + } +} + +function renderHyperlink( + link: DocxHyperlink, + container: HTMLElement, + ctx: RenderContext, + styleRProps: DocxRunProperties, +): void { + const a = container.createEl("a", { + cls: "via-docx-link", + attr: { href: link.url }, + }); + + // External links open in browser + if (link.url.startsWith("http://") || link.url.startsWith("https://")) { + a.setAttribute("target", "_blank"); + a.setAttribute("rel", "noopener noreferrer"); + } + + for (const run of link.children) { + renderRun(run, a, styleRProps); + } +} + +function renderImage( + image: DocxImage, + container: HTMLElement, + ctx: RenderContext, +): void { + const blob = ctx.doc.images.get(image.relationshipId); + if (!blob) { + // Unsupported or missing image — show placeholder + container.createEl("span", { + cls: "via-docx-image-placeholder", + text: `[Image: ${image.altText ?? "missing"}]`, + }); + return; + } + + const url = URL.createObjectURL(blob); + ctx.blobUrls.push(url); + + const img = container.createEl("img", { + cls: "via-docx-image", + attr: { + src: url, + alt: image.altText ?? "", + }, + }); + + if (image.width > 0) img.style.maxWidth = `${image.width}px`; + if (image.height > 0) img.style.maxHeight = `${image.height}px`; +} + +function renderBreak( + brk: { breakType: string }, + container: HTMLElement, +): void { + if (brk.breakType === "page") { + container.createEl("hr", { cls: "via-docx-page-break" }); + } else { + container.createEl("br"); + } +} + +// ── Numbering Text Generation ─────────────────────────────────────────────── + +/** + * Generate the numbering text prefix for a paragraph (e.g., "Part 1:", "Step 2:", "a."). + * Tracks and increments counters per numId+level combination. + */ +function getNumberingText( + numId: string, + level: number, + ctx: RenderContext, +): string { + const numDef = ctx.doc.numbering.get(numId); + if (!numDef) return ""; + + const lvlDef = numDef.levels.get(level); + if (!lvlDef) return ""; + + // format "none" means no visible numbering + if (lvlDef.format === "none") return ""; + + // Bullet format — return a bullet character + if (lvlDef.format === "bullet") { + return "\u2022 "; + } + + // Increment the counter for this numId+level + const counterKey = `${numId}:${level}`; + const prev = ctx.numberingCounters.get(counterKey) ?? 0; + const current = prev + 1; + ctx.numberingCounters.set(counterKey, current); + + // Reset sub-level counters when a higher level increments + for (const [key] of ctx.numberingCounters) { + const [kNumId, kLevel] = key.split(":"); + if (kNumId === numId && Number(kLevel) > level) { + ctx.numberingCounters.delete(key); + } + } + + // Build the numbering text from lvlText template + // lvlText uses %1, %2, %3 etc. where %N refers to level N's counter + let text = lvlDef.text; + if (!text) { + // Fallback: just use the counter + return `${formatNumber(current, lvlDef.format)} `; + } + + // Replace %N placeholders with the counter value for that level + text = text.replace(/%(\d+)/g, (_match: string, levelStr: string) => { + const refLevel = parseInt(levelStr, 10) - 1; // %1 = level 0, %2 = level 1 + const refKey = `${numId}:${refLevel}`; + const refCount = ctx.numberingCounters.get(refKey) ?? 1; + // Use the format of the referenced level + const refLvlDef = numDef.levels.get(refLevel); + const refFormat = refLvlDef?.format ?? "decimal"; + return formatNumber(refCount, refFormat); + }); + + return text + " "; +} + +/** Format a number according to the numbering format type. */ +function formatNumber( + num: number, + format: string, +): string { + switch (format) { + case "decimal": + return String(num); + case "lowerLetter": + return String.fromCharCode(96 + ((num - 1) % 26) + 1); + case "upperLetter": + return String.fromCharCode(64 + ((num - 1) % 26) + 1); + case "lowerRoman": + return toRoman(num).toLowerCase(); + case "upperRoman": + return toRoman(num); + default: + return String(num); + } +} + +/** Convert integer to Roman numeral. */ +function toRoman(num: number): string { + const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]; + const syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]; + let result = ""; + let remaining = num; + for (let i = 0; i < vals.length; i++) { + const v = vals[i]; + const s = syms[i]; + if (v === undefined || s === undefined) continue; + while (remaining >= v) { + result += s; + remaining -= v; + } + } + return result; +} + +// ── Table Rendering ───────────────────────────────────────────────────────── + +function renderTable( + table: DocxTable, + container: HTMLElement, + ctx: RenderContext, +): void { + const tableEl = container.createEl("table", { cls: "via-docx-table" }); + + // Track vertical merge state: column index → "consumed" flag + const vMergeState: Map = new Map(); + + for (const row of table.rows) { + renderTableRow(row, tableEl, ctx, vMergeState); + } +} + +function renderTableRow( + row: DocxTableRow, + tableEl: HTMLElement, + ctx: RenderContext, + vMergeState: Map, +): void { + const trEl = tableEl.createEl("tr"); + let colIdx = 0; + + for (const cell of row.cells) { + // Skip cells that are vertically merged continuations + if (cell.properties.verticalMerge === "continue") { + // Increment the rowspan of the cell that started this merge + colIdx += cell.properties.gridSpan; + continue; + } + + const tag = row.isHeader ? "th" : "td"; + const tdEl = trEl.createEl(tag, { cls: "via-docx-cell" }); + + // Column span + if (cell.properties.gridSpan > 1) { + tdEl.setAttribute("colspan", String(cell.properties.gridSpan)); + } + + // Cell shading (background color) + if (cell.properties.shading) { + tdEl.style.backgroundColor = `#${cell.properties.shading}`; + } + + // Vertical alignment + if (cell.properties.verticalAlign) { + tdEl.style.verticalAlign = cell.properties.verticalAlign; + } + + // Cell width + if (cell.properties.width) { + tdEl.style.width = `${dxaToPx(cell.properties.width)}px`; + } + + // Render cell content + for (const para of cell.paragraphs) { + renderParagraph(para, tdEl, ctx); + } + + colIdx += cell.properties.gridSpan; + } +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +/** + * Merge style-level run properties with run-level overrides. + * Only properties that were explicitly set in the run XML take precedence. + * Uses Partial so that `false` (explicitly turned off) beats the base. + */ +function mergeRunProps( + base: DocxRunProperties, + override: Partial, +): DocxRunProperties { + return { + bold: override.bold !== undefined ? override.bold : base.bold, + italic: override.italic !== undefined ? override.italic : base.italic, + underline: override.underline !== undefined ? override.underline : base.underline, + strikethrough: override.strikethrough !== undefined ? override.strikethrough : base.strikethrough, + fontFamily: override.fontFamily !== undefined ? override.fontFamily : base.fontFamily, + fontSize: override.fontSize !== undefined ? override.fontSize : base.fontSize, + color: override.color !== undefined ? override.color : base.color, + highlight: override.highlight !== undefined ? override.highlight : base.highlight, + vertAlign: override.vertAlign !== undefined ? override.vertAlign : base.vertAlign, + }; +} diff --git a/src/docx/styles.ts b/src/docx/styles.ts new file mode 100644 index 0000000..c902b7a --- /dev/null +++ b/src/docx/styles.ts @@ -0,0 +1,287 @@ +/** + * ViewItAll — OOXML Styles Parser + * + * Parses `word/styles.xml` and resolves style inheritance chains + * to produce fully-resolved DocxStyle objects. + */ + +import type { + DocxStyle, + DocxParagraphProperties, + DocxRunProperties, +} from "./model"; +import { defaultParagraphProperties, defaultRunProperties } from "./model"; +import { + NS_W, + getElements, + getDirectChild, + getVal, + getWAttr, + parseXml, +} from "../utils/xml"; +import { safeParseInt } from "../utils/units"; + +/** + * Parse styles.xml content into a map of style ID → DocxStyle. + */ +export function parseStyles(stylesXml: string): Map { + const doc = parseXml(stylesXml); + const styleMap = new Map(); + + const styleElements = getElements(doc, NS_W, "style"); + for (const el of styleElements) { + const id = getWAttr(el, "styleId"); + if (!id) continue; + + const typeAttr = getWAttr(el, "type") ?? "paragraph"; + const nameEl = getDirectChild(el, NS_W, "name"); + const name = nameEl ? (getVal(nameEl) ?? id) : id; + const basedOnEl = getDirectChild(el, NS_W, "basedOn"); + const basedOn = basedOnEl ? (getVal(basedOnEl) ?? undefined) : undefined; + + const pPr = getDirectChild(el, NS_W, "pPr"); + const rPr = getDirectChild(el, NS_W, "rPr"); + + styleMap.set(id, { + id, + name, + type: typeAttr as DocxStyle["type"], + basedOn, + paragraphProperties: pPr + ? parseParagraphProperties(pPr) + : {}, + runProperties: rPr ? parseRunProperties(rPr) : {}, + }); + } + + return styleMap; +} + +/** + * Resolve a style's full properties by walking the basedOn chain. + * Caches resolved results to avoid repeated walks. + */ +export function resolveStyle( + styleId: string, + styles: Map, + cache: Map, +): { pProps: DocxParagraphProperties; rProps: DocxRunProperties } { + const cached = cache.get(styleId); + if (cached) return cached; + + const style = styles.get(styleId); + if (!style) { + const fallback = { + pProps: defaultParagraphProperties(), + rProps: defaultRunProperties(), + }; + cache.set(styleId, fallback); + return fallback; + } + + let pProps: DocxParagraphProperties; + let rProps: DocxRunProperties; + + if (style.basedOn && style.basedOn !== styleId) { + const parent = resolveStyle(style.basedOn, styles, cache); + pProps = { ...parent.pProps }; + rProps = { ...parent.rProps }; + } else { + pProps = defaultParagraphProperties(); + rProps = defaultRunProperties(); + } + + mergeParagraphProperties(pProps, style.paragraphProperties); + mergeRunProperties(rProps, style.runProperties); + + const result = { pProps, rProps }; + cache.set(styleId, result); + return result; +} + +/** + * Determine heading level from a style name or outlineLvl. + */ +export function resolveHeadingLevel( + style: DocxStyle, +): number | undefined { + const nameMatch = /^heading\s+(\d)$/i.exec(style.name); + if (nameMatch && nameMatch[1]) { + const level = parseInt(nameMatch[1], 10); + if (level >= 1 && level <= 6) return level; + } + + const headingLevel = style.paragraphProperties.headingLevel; + if (headingLevel !== undefined && headingLevel >= 0 && headingLevel <= 5) { + return headingLevel + 1; + } + + return undefined; +} + +// ── Property Parsers ──────────────────────────────────────────────────────── + +/** + * Parse paragraph properties from a w:pPr element. + */ +export function parseParagraphProperties( + pPr: Element, +): Partial { + const props: Partial = {}; + + const jcEl = getDirectChild(pPr, NS_W, "jc"); + if (jcEl) { + const val = getVal(jcEl); + if (val === "left" || val === "center" || val === "right" || val === "both") { + props.alignment = val; + } + } + + const outlineLvlEl = getDirectChild(pPr, NS_W, "outlineLvl"); + if (outlineLvlEl) { + const val = safeParseInt(getVal(outlineLvlEl)); + if (val !== undefined && val >= 0 && val <= 5) { + props.headingLevel = val + 1; + } + } + + // Numbering from pPr (used by styles and direct paragraph formatting) + const numPrEl = getDirectChild(pPr, NS_W, "numPr"); + if (numPrEl) { + const numIdEl = getDirectChild(numPrEl, NS_W, "numId"); + const ilvlEl = getDirectChild(numPrEl, NS_W, "ilvl"); + const numId = numIdEl ? (getVal(numIdEl) ?? undefined) : undefined; + const ilvl = ilvlEl ? safeParseInt(getVal(ilvlEl)) : undefined; + if (numId) { + props.numberingId = numId; + props.numberingLevel = ilvl ?? 0; + } + } + + const indEl = getDirectChild(pPr, NS_W, "ind"); + if (indEl) { + props.indentation = { + left: safeParseInt(getWAttr(indEl, "left")) ?? 0, + right: safeParseInt(getWAttr(indEl, "right")) ?? 0, + firstLine: safeParseInt(getWAttr(indEl, "firstLine")) ?? 0, + hanging: safeParseInt(getWAttr(indEl, "hanging")) ?? 0, + }; + } + + const spacingEl = getDirectChild(pPr, NS_W, "spacing"); + if (spacingEl) { + props.spacing = { + before: safeParseInt(getWAttr(spacingEl, "before")) ?? 0, + after: safeParseInt(getWAttr(spacingEl, "after")) ?? 0, + line: safeParseInt(getWAttr(spacingEl, "line")) ?? 0, + lineRule: getWAttr(spacingEl, "lineRule") ?? "auto", + }; + } + + return props; +} + +/** + * Parse run properties from a w:rPr element. + */ +export function parseRunProperties( + rPr: Element, +): Partial { + const props: Partial = {}; + + const bEl = getDirectChild(rPr, NS_W, "b"); + if (bEl) { + const val = getVal(bEl); + props.bold = val !== "0" && val !== "false"; + } + + const iEl = getDirectChild(rPr, NS_W, "i"); + if (iEl) { + const val = getVal(iEl); + props.italic = val !== "0" && val !== "false"; + } + + const uEl = getDirectChild(rPr, NS_W, "u"); + if (uEl) { + const val = getVal(uEl); + props.underline = val !== "none" && val !== undefined; + } + + const strikeEl = getDirectChild(rPr, NS_W, "strike"); + if (strikeEl) { + const val = getVal(strikeEl); + props.strikethrough = val !== "0" && val !== "false"; + } + + const rFontsEl = getDirectChild(rPr, NS_W, "rFonts"); + if (rFontsEl) { + props.fontFamily = + getWAttr(rFontsEl, "ascii") ?? + getWAttr(rFontsEl, "hAnsi") ?? + getWAttr(rFontsEl, "cs") ?? + undefined; + } + + const szEl = getDirectChild(rPr, NS_W, "sz"); + if (szEl) { + props.fontSize = safeParseInt(getVal(szEl)); + } + + const colorEl = getDirectChild(rPr, NS_W, "color"); + if (colorEl) { + const val = getVal(colorEl); + if (val === "auto") { + // "auto" means reset to default (black) — override inherited colors + props.color = "auto"; + } else if (val) { + props.color = val; + } + } + + const highlightEl = getDirectChild(rPr, NS_W, "highlight"); + if (highlightEl) { + props.highlight = getVal(highlightEl) ?? undefined; + } + + const vertAlignEl = getDirectChild(rPr, NS_W, "vertAlign"); + if (vertAlignEl) { + const val = getVal(vertAlignEl); + if (val === "superscript" || val === "subscript") { + props.vertAlign = val; + } + } + + return props; +} + +// ── Merge Helpers ─────────────────────────────────────────────────────────── + +function mergeParagraphProperties( + target: DocxParagraphProperties, + source: Partial, +): void { + if (source.alignment !== undefined) target.alignment = source.alignment; + if (source.headingLevel !== undefined) target.headingLevel = source.headingLevel; + if (source.indentation !== undefined) target.indentation = source.indentation; + if (source.spacing !== undefined) target.spacing = source.spacing; + if (source.numberingId !== undefined) target.numberingId = source.numberingId; + if (source.numberingLevel !== undefined) target.numberingLevel = source.numberingLevel; +} + +function mergeRunProperties( + target: DocxRunProperties, + source: Partial, +): void { + if (source.bold !== undefined) target.bold = source.bold; + if (source.italic !== undefined) target.italic = source.italic; + if (source.underline !== undefined) target.underline = source.underline; + if (source.strikethrough !== undefined) target.strikethrough = source.strikethrough; + if (source.fontFamily !== undefined) target.fontFamily = source.fontFamily; + if (source.fontSize !== undefined) target.fontSize = source.fontSize; + if (source.color !== undefined) { + // "auto" resets color to default, overriding any inherited color + target.color = source.color === "auto" ? undefined : source.color; + } + if (source.highlight !== undefined) target.highlight = source.highlight; + if (source.vertAlign !== undefined) target.vertAlign = source.vertAlign; +} diff --git a/src/main.ts b/src/main.ts index 64fe990..069ca16 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,26 +1,11 @@ -import { App, Plugin } from "obsidian"; +import { Plugin } from "obsidian"; import { DEFAULT_SETTINGS, PluginSettings, ViewItAllSettingTab, } from "./settings"; -import { - VIEW_TYPE_DOCX, - VIEW_TYPE_PDF, - VIEW_TYPE_SPREADSHEET, - VIEW_TYPE_PPTX, -} from "./types"; +import { VIEW_TYPE_DOCX } from "./types"; import { DocxView } from "./views/DocxView"; -import { PdfView } from "./views/PdfView"; -import { SpreadsheetView } from "./views/SpreadsheetView"; -import { PptxView } from "./views/PptxView"; - -interface AppWithRegistry extends App { - viewRegistry: { - unregisterExtensions(exts: string[]): void; - registerExtensions(exts: string[], viewType: string): void; - }; -} export default class ViewItAllPlugin extends Plugin { settings: PluginSettings; @@ -28,62 +13,15 @@ export default class ViewItAllPlugin extends Plugin { async onload() { await this.loadSettings(); - // ── Register view types ─────────────────────────────────────────── this.registerView(VIEW_TYPE_DOCX, (leaf) => new DocxView(leaf, this)); - this.registerView(VIEW_TYPE_PDF, (leaf) => new PdfView(leaf, this)); - this.registerView( - VIEW_TYPE_SPREADSHEET, - (leaf) => new SpreadsheetView(leaf, this), - ); - this.registerView(VIEW_TYPE_PPTX, (leaf) => new PptxView(leaf, this)); - // ── Register file extensions (respecting enable toggles) ────────── if (this.settings.enableDocx) { this.registerExtensions(["docx"], VIEW_TYPE_DOCX); } - if (this.settings.enablePdf) { - this.overridePdfExtension(); - } - - const sheetExts: string[] = []; - if (this.settings.enableXlsx) sheetExts.push("xlsx"); - if (this.settings.enableCsv) sheetExts.push("csv"); - if (sheetExts.length > 0) { - this.registerExtensions(sheetExts, VIEW_TYPE_SPREADSHEET); - } - - if (this.settings.enablePptx) { - this.registerExtensions(["pptx"], VIEW_TYPE_PPTX); - } - this.addSettingTab(new ViewItAllSettingTab(this.app, this)); } - onunload() { - // Restore Obsidian's built-in PDF viewer when this plugin is disabled. - if (this.settings.enablePdf) { - try { - const reg = (this.app as AppWithRegistry).viewRegistry; - reg.unregisterExtensions(["pdf"]); - reg.registerExtensions(["pdf"], "pdf"); - } catch { - // ignore — built-in will be re-registered on app restart - } - } - } - - private overridePdfExtension() { - try { - (this.app as AppWithRegistry).viewRegistry.unregisterExtensions([ - "pdf", - ]); - } catch { - // already unregistered or API unavailable — proceed anyway - } - this.registerExtensions(["pdf"], VIEW_TYPE_PDF); - } - async loadSettings() { this.settings = Object.assign( {}, diff --git a/src/settings.ts b/src/settings.ts index 174489d..2b69b55 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,115 +1,20 @@ import { App, PluginSettingTab, Setting } from "obsidian"; import type ViewItAllPlugin from "./main"; -export type OpenMode = "tab" | "sidebar-right"; export type ToolbarPosition = "top" | "bottom"; -export type SnapModifier = "Alt" | "Shift"; -export type SnapDirection = "horizontal" | "vertical" | "slope"; -export type PptxRendererMode = "custom" | "library"; export interface PluginSettings { - // ── File-type toggles ───────────────────────────────────────────────── - enablePdf: boolean; enableDocx: boolean; - enableXlsx: boolean; - enableCsv: boolean; - enablePptx: boolean; - - // ── DOCX ───────────────────────────────────────────────────────────── - docxOpenMode: OpenMode; docxToolbarPosition: ToolbarPosition; - docxDefaultEditMode: boolean; - confirmOnSave: boolean; - - // ── Spreadsheet (.xlsx / .csv) ──────────────────────────────────────── - spreadsheetToolbarPosition: ToolbarPosition; - - // ── PowerPoint (.pptx) ──────────────────────────────────────────────── - pptxToolbarPosition: ToolbarPosition; - pptxRendererMode: PptxRendererMode; - - // ── PDF — general ───────────────────────────────────────────────────── - pdfOpenMode: OpenMode; - pdfToolbarPosition: ToolbarPosition; - pdfDefaultTool: "none" | "pen" | "highlighter"; - pdfDefaultZoom: number; - showTocOnOpen: boolean; - - // ── PDF — annotation tools ──────────────────────────────────────────── - penColor: string; - penWidth: number; - highlighterColor: string; - highlighterWidth: number; - highlighterOpacity: number; - eraserWidth: number; - - // ── PDF — notes ─────────────────────────────────────────────────────── - noteDefaultColor: string; - - // ── PDF — snap ──────────────────────────────────────────────────────── - snapActivateKey: SnapModifier; - snapDefaultDirection: SnapDirection; - - // ── Keyboard shortcuts (PDF) ────────────────────────────────────────── - keyToolView: string; // default 'v' - keyToolPen: string; // default 'p' - keyToolHighlight: string; // default 'h' - keyToolErase: string; // default 'e' - keyToolNote: string; // default 'n' - keySnapCycle: string; // default 's' (with snapActivateKey held) - keySearch: string; // default 'f' (with Ctrl/Cmd held) + docxDefaultZoom: number; } export const DEFAULT_SETTINGS: PluginSettings = { - enablePdf: true, enableDocx: true, - enableXlsx: true, - enableCsv: true, - enablePptx: true, - - docxOpenMode: "tab", docxToolbarPosition: "top", - docxDefaultEditMode: false, - confirmOnSave: true, - - spreadsheetToolbarPosition: "top", - - pptxToolbarPosition: "top", - pptxRendererMode: "library", - - pdfOpenMode: "tab", - pdfToolbarPosition: "top", - pdfDefaultTool: "none", - pdfDefaultZoom: 1.0, - showTocOnOpen: false, - - penColor: "#e03131", - penWidth: 2, - highlighterColor: "#ffd43b", - highlighterWidth: 16, - highlighterOpacity: 0.4, - eraserWidth: 20, - - noteDefaultColor: "#ffd43b", - - snapActivateKey: "Alt", - snapDefaultDirection: "horizontal", - - keyToolView: "v", - keyToolPen: "p", - keyToolHighlight: "h", - keyToolErase: "e", - keyToolNote: "n", - keySnapCycle: "s", - keySearch: "f", + docxDefaultZoom: 1.0, }; -// ── Helper ──────────────────────────────────────────────────────────────────── -/** Validate and normalise a single-character shortcut key entered by the user. */ -function normKey(raw: string): string { - return raw.trim().toLowerCase().slice(0, 1) || ""; -} - export class ViewItAllSettingTab extends PluginSettingTab { plugin: ViewItAllPlugin; @@ -122,77 +27,20 @@ export class ViewItAllSettingTab extends PluginSettingTab { const { containerEl } = this; containerEl.empty(); - // ── File Types ─────────────────────────────────────────────────────── new Setting(containerEl).setName("File types").setHeading(); - containerEl.createEl("p", { - text: "Enable or disable support for each file type. Changes take effect after reloading Obsidian.", - cls: "setting-item-description", - }); - - new Setting(containerEl).setName("PDF").addToggle((t) => - t.setValue(this.plugin.settings.enablePdf).onChange(async (v) => { - this.plugin.settings.enablePdf = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl).setName("Word (.docx)").addToggle((t) => - t.setValue(this.plugin.settings.enableDocx).onChange(async (v) => { - this.plugin.settings.enableDocx = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl).setName("Excel (.xlsx)").addToggle((t) => - t.setValue(this.plugin.settings.enableXlsx).onChange(async (v) => { - this.plugin.settings.enableXlsx = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl).setName("CSV").addToggle((t) => - t.setValue(this.plugin.settings.enableCsv).onChange(async (v) => { - this.plugin.settings.enableCsv = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl).setName("Presentations (.pptx)").addToggle((t) => - t.setValue(this.plugin.settings.enablePptx).onChange(async (v) => { - this.plugin.settings.enablePptx = v; - await this.plugin.saveSettings(); - }), - ); - - // ── DOCX ───────────────────────────────────────────────────────────── - new Setting(containerEl).setName("Word documents (.docx)").setHeading(); new Setting(containerEl) - .setName("Open mode") - .setDesc("Where to open .docx files.") - .addDropdown((dd) => - dd - .addOption("tab", "New tab") - .addOption("sidebar-right", "Right sidebar") - .setValue(this.plugin.settings.docxOpenMode) - .onChange(async (v) => { - this.plugin.settings.docxOpenMode = v as OpenMode; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Open in edit mode by default") - .setDesc("When enabled, .docx files open ready to edit.") + .setName("Word (.docx)") + .setDesc("Enable viewing .docx files. Restart required after change.") .addToggle((t) => - t - .setValue(this.plugin.settings.docxDefaultEditMode) - .onChange(async (v) => { - this.plugin.settings.docxDefaultEditMode = v; - await this.plugin.saveSettings(); - }), + t.setValue(this.plugin.settings.enableDocx).onChange(async (v) => { + this.plugin.settings.enableDocx = v; + await this.plugin.saveSettings(); + }), ); + new Setting(containerEl).setName("Word documents (.docx)").setHeading(); + new Setting(containerEl) .setName("Toolbar position") .setDesc("Where to pin the toolbar.") @@ -208,108 +56,9 @@ export class ViewItAllSettingTab extends PluginSettingTab { }), ); - new Setting(containerEl) - .setName("Confirm before saving") - .setDesc( - "Show a confirmation dialog before overwriting the original .docx file.", - ) - .addToggle((t) => - t - .setValue(this.plugin.settings.confirmOnSave) - .onChange(async (v) => { - this.plugin.settings.confirmOnSave = v; - await this.plugin.saveSettings(); - }), - ); - - // ── Spreadsheet ────────────────────────────────────────────────── - new Setting(containerEl) - .setName("Spreadsheets (.xlsx / .csv)") - .setHeading(); - - new Setting(containerEl) - .setName("Toolbar position") - .setDesc("Where to pin the spreadsheet toolbar.") - .addDropdown((dd) => - dd - .addOption("top", "Top") - .addOption("bottom", "Bottom") - .setValue(this.plugin.settings.spreadsheetToolbarPosition) - .onChange(async (v) => { - this.plugin.settings.spreadsheetToolbarPosition = - v as ToolbarPosition; - await this.plugin.saveSettings(); - }), - ); - - // ── PowerPoint ──────────────────────────────────────────────────── - new Setting(containerEl).setName("Presentations (.pptx)").setHeading(); - - new Setting(containerEl) - .setName("Toolbar position") - .setDesc("Where to pin the slide toolbar.") - .addDropdown((dd) => - dd - .addOption("top", "Top") - .addOption("bottom", "Bottom") - .setValue(this.plugin.settings.pptxToolbarPosition) - .onChange(async (v) => { - this.plugin.settings.pptxToolbarPosition = - v as ToolbarPosition; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Renderer mode") - .setDesc( - "Choose the pptx rendering engine. Library mode prioritizes visual fidelity; custom mode keeps the in-plugin editable shape pipeline.", - ) - .addDropdown((dd) => - dd - .addOption("library", "Library (high fidelity)") - .addOption("custom", "Custom (editable pipeline)") - .setValue(this.plugin.settings.pptxRendererMode) - .onChange(async (v) => { - this.plugin.settings.pptxRendererMode = - v as PptxRendererMode; - await this.plugin.saveSettings(); - }), - ); - - // ── PDF — General ───────────────────────────────────────────────── - new Setting(containerEl) - .setName("Open mode") - .setDesc("Where to open PDF files.") - .addDropdown((dd) => - dd - .addOption("tab", "New tab") - .addOption("sidebar-right", "Right sidebar") - .setValue(this.plugin.settings.pdfOpenMode) - .onChange(async (v) => { - this.plugin.settings.pdfOpenMode = v as OpenMode; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Toolbar position") - .setDesc("Where to pin the PDF toolbar.") - .addDropdown((dd) => - dd - .addOption("top", "Top") - .addOption("bottom", "Bottom") - .setValue(this.plugin.settings.pdfToolbarPosition) - .onChange(async (v) => { - this.plugin.settings.pdfToolbarPosition = - v as ToolbarPosition; - await this.plugin.saveSettings(); - }), - ); - new Setting(containerEl) .setName("Default zoom") - .setDesc("Zoom level when a PDF is first opened.") + .setDesc("Zoom level when a .docx file is first opened.") .addDropdown((dd) => dd .addOption("0.5", "50%") @@ -318,248 +67,11 @@ export class ViewItAllSettingTab extends PluginSettingTab { .addOption("1.25", "125%") .addOption("1.5", "150%") .addOption("2.0", "200%") - .setValue(String(this.plugin.settings.pdfDefaultZoom)) - .onChange(async (v) => { - this.plugin.settings.pdfDefaultZoom = parseFloat(v); - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Default annotation tool") - .setDesc("Tool to activate when a PDF is opened.") - .addDropdown((dd) => - dd - .addOption("none", "None (view only)") - .addOption("pen", "Pen") - .addOption("highlighter", "Highlighter") - .setValue(this.plugin.settings.pdfDefaultTool) + .setValue(String(this.plugin.settings.docxDefaultZoom)) .onChange(async (v) => { - this.plugin.settings.pdfDefaultTool = v as - | "none" - | "pen" - | "highlighter"; + this.plugin.settings.docxDefaultZoom = parseFloat(v); await this.plugin.saveSettings(); }), ); - - new Setting(containerEl) - .setName("Show table of contents on open") - .setDesc( - "Automatically expand the table of contents sidebar when a PDF is opened (only if the PDF has an outline).", - ) - .addToggle((t) => - t - .setValue(this.plugin.settings.showTocOnOpen) - .onChange(async (v) => { - this.plugin.settings.showTocOnOpen = v; - await this.plugin.saveSettings(); - }), - ); - - // ── PDF — Annotation Tools ──────────────────────────────────────── - new Setting(containerEl) - .setName("PDF files — annotation tools") - .setHeading(); - - new Setting(containerEl).setName("Pen color").addColorPicker((cp) => - cp.setValue(this.plugin.settings.penColor).onChange(async (v) => { - this.plugin.settings.penColor = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl).setName("Pen width").addSlider((sl) => - sl - .setLimits(1, 20, 1) - .setValue(this.plugin.settings.penWidth) - .setDynamicTooltip() - .onChange(async (v) => { - this.plugin.settings.penWidth = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Highlighter color") - .addColorPicker((cp) => - cp - .setValue(this.plugin.settings.highlighterColor) - .onChange(async (v) => { - this.plugin.settings.highlighterColor = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl).setName("Highlighter width").addSlider((sl) => - sl - .setLimits(10, 40, 2) - .setValue(this.plugin.settings.highlighterWidth) - .setDynamicTooltip() - .onChange(async (v) => { - this.plugin.settings.highlighterWidth = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Highlighter opacity") - .addSlider((sl) => - sl - .setLimits(0.1, 1.0, 0.05) - .setValue(this.plugin.settings.highlighterOpacity) - .setDynamicTooltip() - .onChange(async (v) => { - this.plugin.settings.highlighterOpacity = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl).setName("Eraser width").addSlider((sl) => - sl - .setLimits(5, 60, 5) - .setValue(this.plugin.settings.eraserWidth) - .setDynamicTooltip() - .onChange(async (v) => { - this.plugin.settings.eraserWidth = v; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Default note color") - .setDesc("Background color for newly placed text notes.") - .addColorPicker((cp) => - cp - .setValue(this.plugin.settings.noteDefaultColor) - .onChange(async (v) => { - this.plugin.settings.noteDefaultColor = v; - await this.plugin.saveSettings(); - }), - ); - - // ── PDF — Snap ──────────────────────────────────────────────────── - new Setting(containerEl).setName("PDF files — snap").setHeading(); - - new Setting(containerEl) - .setName("Snap activation key") - .setDesc( - "Hold this key while drawing to constrain the stroke direction.", - ) - .addDropdown((dd) => - dd - .addOption("Alt", "Alt") - .addOption("Shift", "Shift") - .setValue(this.plugin.settings.snapActivateKey) - .onChange(async (v) => { - this.plugin.settings.snapActivateKey = - v as SnapModifier; - await this.plugin.saveSettings(); - }), - ); - - new Setting(containerEl) - .setName("Default snap direction") - .setDesc("Snap direction applied when opening a PDF.") - .addDropdown((dd) => - dd - .addOption("horizontal", "⟷ horizontal") - .addOption("vertical", "↕ vertical") - .addOption("slope", "↗ 45°") - .setValue(this.plugin.settings.snapDefaultDirection) - .onChange(async (v) => { - this.plugin.settings.snapDefaultDirection = - v as SnapDirection; - await this.plugin.saveSettings(); - }), - ); - - // ── Keyboard Shortcuts (PDF) ────────────────────────────────────── - new Setting(containerEl).setName("PDF keyboard shortcuts").setHeading(); - containerEl.createEl("p", { - text: "Single character keys (no modifier). Avoid letters already used by Obsidian globally.", - cls: "setting-item-description", - }); - - const shortcutEntry = ( - name: string, - desc: string, - get: () => string, - set: (v: string) => void, - ) => { - new Setting(containerEl) - .setName(name) - .setDesc(desc) - .addText((t) => - t - .setValue(get()) - .setPlaceholder("Single key") - .onChange(async (raw) => { - const k = normKey(raw); - if (k) { - set(k); - t.setValue(k); - await this.plugin.saveSettings(); - } - }), - ); - }; - - shortcutEntry( - "View tool", - "Switch to view/pan mode.", - () => this.plugin.settings.keyToolView, - (v) => { - this.plugin.settings.keyToolView = v; - }, - ); - shortcutEntry( - "Pen tool", - "Switch to freehand pen.", - () => this.plugin.settings.keyToolPen, - (v) => { - this.plugin.settings.keyToolPen = v; - }, - ); - shortcutEntry( - "Highlighter tool", - "Switch to highlighter.", - () => this.plugin.settings.keyToolHighlight, - (v) => { - this.plugin.settings.keyToolHighlight = v; - }, - ); - shortcutEntry( - "Eraser tool", - "Switch to eraser.", - () => this.plugin.settings.keyToolErase, - (v) => { - this.plugin.settings.keyToolErase = v; - }, - ); - shortcutEntry( - "Note tool", - "Switch to sticky-note placement.", - () => this.plugin.settings.keyToolNote, - (v) => { - this.plugin.settings.keyToolNote = v; - }, - ); - shortcutEntry( - "Cycle snap direction", - `Press together with the snap activation key (e.g. ${this.plugin.settings.snapActivateKey}+key) to cycle snap mode.`, - () => this.plugin.settings.keySnapCycle, - (v) => { - this.plugin.settings.keySnapCycle = v; - }, - ); - shortcutEntry( - "Open search bar", - "Press with Ctrl/Cmd to open the text search bar.", - () => this.plugin.settings.keySearch, - (v) => { - this.plugin.settings.keySearch = v; - }, - ); } } diff --git a/src/types.ts b/src/types.ts index deac298..3229694 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,33 +1 @@ export const VIEW_TYPE_DOCX = "viewitall-docx"; -export const VIEW_TYPE_PDF = "viewitall-pdf"; -export const VIEW_TYPE_SPREADSHEET = "viewitall-spreadsheet"; -export const VIEW_TYPE_PPTX = "viewitall-pptx"; - -export interface AnnotationPath { - tool: "pen" | "highlighter" | "eraser"; - color: string; - width: number; - opacity?: number; - points: { x: number; y: number }[]; -} - -export interface PageAnnotations { - page: number; - paths: AnnotationPath[]; -} - -/** A text note pinned to a normalised (0-1) position on a PDF page. */ -export interface TextNote { - id: string; - page: number; - x: number; // normalised 0-1 - y: number; // normalised 0-1 - text: string; - color?: string; // background swatch color, defaults to '#ffd43b' -} - -export interface AnnotationFile { - version: 1; - pages: PageAnnotations[]; - notes?: TextNote[]; -} diff --git a/src/utils/annotations.ts b/src/utils/annotations.ts deleted file mode 100644 index 9930284..0000000 --- a/src/utils/annotations.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { App, TFile } from "obsidian"; -import { AnnotationFile, PageAnnotations } from "../types"; -import { getCompanionPath } from "./fileUtils"; - -const SUFFIX = ".annotations.json"; - -export async function loadAnnotations( - app: App, - file: TFile, -): Promise { - const path = getCompanionPath(file, SUFFIX); - try { - const exists = await app.vault.adapter.exists(path); - if (!exists) return { version: 1, pages: [] }; - const raw = await app.vault.adapter.read(path); - return JSON.parse(raw) as AnnotationFile; - } catch { - return { version: 1, pages: [] }; - } -} - -export async function saveAnnotations( - app: App, - file: TFile, - data: AnnotationFile, -): Promise { - const path = getCompanionPath(file, SUFFIX); - const raw = JSON.stringify(data, null, 2); - const exists = await app.vault.adapter.exists(path); - if (exists) { - await app.vault.adapter.write(path, raw); - } else { - await app.vault.adapter.write(path, raw); - } -} - -export function getPageAnnotations( - data: AnnotationFile, - pageNum: number, -): PageAnnotations { - return ( - data.pages.find((p) => p.page === pageNum) ?? { - page: pageNum, - paths: [], - } - ); -} - -export function setPageAnnotations( - data: AnnotationFile, - pageAnnotations: PageAnnotations, -): AnnotationFile { - const pages = data.pages.filter((p) => p.page !== pageAnnotations.page); - if (pageAnnotations.paths.length > 0) pages.push(pageAnnotations); - pages.sort((a, b) => a.page - b.page); - return { ...data, pages }; -} diff --git a/src/utils/docxUtils.ts b/src/utils/docxUtils.ts deleted file mode 100644 index a396fd2..0000000 --- a/src/utils/docxUtils.ts +++ /dev/null @@ -1,61 +0,0 @@ -import mammoth from "mammoth"; -// @ts-expect-error — html-to-docx has no bundled type declarations -import htmlToDocxModule from "html-to-docx"; - -/** Shape of a Node.js Buffer as returned by html-to-docx in Electron/Node contexts. */ -interface NodeJsBuffer { - buffer: ArrayBuffer; - byteOffset: number; - byteLength: number; -} - -type HtmlToDocxFn = ( - html: string, - headerHtml: string | undefined, - options: Record, -) => Promise; - -// Cast the untyped import to its known function signature. -const htmlToDocx = htmlToDocxModule as unknown as HtmlToDocxFn; - -/** - * Converts a .docx ArrayBuffer to an HTML string for display/editing. - * Returns both the html content and any conversion messages/warnings. - */ -export async function readDocxAsHtml( - buffer: ArrayBuffer, -): Promise<{ html: string; messages: string[] }> { - const result = await mammoth.convertToHtml({ arrayBuffer: buffer }); - // Filter out pure style-mapping noise — "Unrecognised paragraph/run style" messages - // only mean mammoth doesn't know the custom Word style name; content is unaffected. - const messages = result.messages - .filter((m) => m.type === "warning") - .filter( - (m) => - !m.message.startsWith("Unrecognised paragraph style") && - !m.message.startsWith("Unrecognised run style"), - ) - .map((m) => m.message); - return { html: result.value, messages }; -} - -/** - * Converts an HTML string back to a .docx ArrayBuffer for saving. - * Note: Complex formatting (merged table cells, custom styles) may be simplified. - */ -export async function saveHtmlAsDocx(html: string): Promise { - // htmlToDocx returns Blob in browser contexts, NodeJsBuffer in Electron/Node - const result = await htmlToDocx(html, undefined, { - table: { row: { cantSplit: true } }, - footer: false, - pageNumber: false, - }); - if (result instanceof Blob) { - return result.arrayBuffer(); - } - // Zero-copy slice of the Node.js Buffer backing array - return result.buffer.slice( - result.byteOffset, - result.byteOffset + result.byteLength, - ); -} diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts deleted file mode 100644 index 9a17759..0000000 --- a/src/utils/fileUtils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { App, TFile } from "obsidian"; - -/** Returns the app:// resource URL Obsidian/Electron uses to serve vault files. */ -export function getResourcePath(app: App, file: TFile): string { - return app.vault.adapter.getResourcePath(file.path); -} - -/** Returns the absolute OS path for a vault file. */ -export function getAbsolutePath(app: App, file: TFile): string { - const adapter = app.vault.adapter as { basePath?: string }; - if (adapter.basePath) { - return `${adapter.basePath}/${file.path}`; - } - return file.path; -} - -/** Returns the vault-relative path for a companion file (e.g. .annotations.json). */ -export function getCompanionPath(file: TFile, suffix: string): string { - return file.path + suffix; -} diff --git a/src/utils/formulaEval.ts b/src/utils/formulaEval.ts deleted file mode 100644 index fb2f061..0000000 --- a/src/utils/formulaEval.ts +++ /dev/null @@ -1,635 +0,0 @@ -/** - * Lightweight spreadsheet formula evaluator. - * Supports basic arithmetic, cell/range references, and common functions. - */ - -type CellValue = string | number | boolean | null | undefined; - -interface Cell { - v?: CellValue; - f?: string; - t?: string; - w?: string; -} - -type Sheet = Record; - -interface CellAddr { - r: number; - c: number; -} -interface Range { - s: CellAddr; - e: CellAddr; -} - -interface Utils { - encode_cell(addr: CellAddr): string; - decode_cell(addr: string): CellAddr; - decode_range(range: string): Range; -} - -// ── Public API ──────────────────────────────────────────────────────────────── - -/** - * Evaluate every formula cell in the sheet (multi-pass to resolve dependencies). - * Modifies cell `.v`, `.w`, `.t` in place. - */ -export function evaluateSheet( - sheet: Sheet, - utils: Utils, - rangeStr: string | undefined, -): void { - if (!rangeStr) return; - const range = utils.decode_range(rangeStr); - - // Up to 3 passes so that formulas referencing other formulas stabilise - for (let pass = 0; pass < 3; pass++) { - for (let r = range.s.r; r <= range.e.r; r++) { - for (let c = range.s.c; c <= range.e.c; c++) { - const ref = utils.encode_cell({ r, c }); - const cell = sheet[ref] as Cell | undefined; - if (!cell?.f) continue; - try { - const val = evalFormula(cell.f, sheet, utils, ref); - cell.v = val; - cell.w = fmtVal(val); - cell.t = - typeof val === "number" - ? "n" - : typeof val === "boolean" - ? "b" - : "s"; - } catch { - // Leave pre-computed value if evaluation fails - } - } - } - } -} - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -function fmtVal(v: number | string | boolean): string { - if (typeof v === "number") { - return Number.isInteger(v) - ? String(v) - : parseFloat(v.toFixed(10)).toString(); - } - return String(v); -} - -function cellValue(sheet: Sheet, ref: string): number | string | boolean { - const c = sheet[ref] as Cell | undefined; - if (!c) return 0; - - // Prefer the raw value if it's a clean number or boolean - if (typeof c.v === "number") return c.v; - if (typeof c.v === "boolean") return c.v; - - // v might be a formatted string ("20.00%", "€100.00") — try to extract a number - const raw = c.v != null ? String(c.v) : (c.w ?? ""); - return parseFormattedNumber(raw); -} - -/** Parse a potentially formatted string into a number, or return it as-is. */ -function parseFormattedNumber(s: string): number | string { - const trimmed = s.trim(); - if (trimmed === "") return 0; - - // Percentage: "20.00%" → 0.2 - if (trimmed.endsWith("%")) { - const n = Number(trimmed.slice(0, -1)); - if (!isNaN(n)) return n / 100; - } - - // Strip common currency symbols / thousand separators then try parse - const stripped = trimmed - .replace(/^[€$£¥₹₽¥₩₪₫₴₸₺₼₻₥₦₧₨₩₿\s]+/, "") - .replace(/,/g, ""); - if (stripped !== "" && !isNaN(Number(stripped))) return Number(stripped); - - // Plain number - const n = Number(trimmed); - if (!isNaN(n)) return n; - - // Not numeric — return as string - return trimmed; -} - -function resolveRange( - sheet: Sheet, - utils: Utils, - rangeStr: string, -): (number | string | boolean)[] { - const rng = utils.decode_range(rangeStr); - const out: (number | string | boolean)[] = []; - for (let r = rng.s.r; r <= rng.e.r; r++) { - for (let c = rng.s.c; c <= rng.e.c; c++) { - out.push(cellValue(sheet, utils.encode_cell({ r, c }))); - } - } - return out; -} - -// ── Tokeniser ───────────────────────────────────────────────────────────────── - -type Token = - | { type: "num"; v: number } - | { type: "str"; v: string } - | { type: "bool"; v: boolean } - | { type: "cell"; ref: string } - | { type: "range"; ref: string } - | { type: "func"; name: string } - | { type: "op"; v: string } - | { type: "paren"; v: "(" | ")" } - | { type: "comma" }; - -const RE_RANGE = /^\$?[A-Z]{1,3}\$?\d{1,7}:\$?[A-Z]{1,3}\$?\d{1,7}/i; -const RE_CELL = /^\$?[A-Z]{1,3}\$?\d{1,7}/i; -const RE_FUNC = /^[A-Z_][A-Z0-9_.]*(?=\()/i; -const RE_NUM = /^\d+(\.\d+)?([eE][+-]?\d+)?/; - -function tokenise(f: string): Token[] { - const tokens: Token[] = []; - let i = 0; - while (i < f.length) { - const s = f.slice(i); - - if (/^\s/.test(s)) { - i++; - continue; - } - - // String literal - if (s[0] === '"') { - let j = 1; - while (j < s.length && s[j] !== '"') j++; - tokens.push({ type: "str", v: s.slice(1, j) }); - i += j + 1; - continue; - } - - // Boolean - const bm = s.match(/^(TRUE|FALSE)\b/i); - if (bm) { - tokens.push({ type: "bool", v: bm[1]!.toUpperCase() === "TRUE" }); - i += bm[0].length; - continue; - } - - // Function (before cell/range so SUM( is caught) - const fm = s.match(RE_FUNC); - if (fm) { - tokens.push({ type: "func", name: fm[0].toUpperCase() }); - i += fm[0].length; - continue; - } - - // Range - const rm = s.match(RE_RANGE); - if (rm) { - tokens.push({ - type: "range", - ref: rm[0].replace(/\$/g, "").toUpperCase(), - }); - i += rm[0].length; - continue; - } - - // Cell ref - const cm = s.match(RE_CELL); - if (cm) { - tokens.push({ - type: "cell", - ref: cm[0].replace(/\$/g, "").toUpperCase(), - }); - i += cm[0].length; - continue; - } - - // Number - const nm = s.match(RE_NUM); - if (nm) { - tokens.push({ type: "num", v: parseFloat(nm[0]) }); - i += nm[0].length; - continue; - } - - // Two-char operators - const two = s.slice(0, 2); - if ([">=", "<=", "<>", "!="].includes(two)) { - tokens.push({ type: "op", v: two }); - i += 2; - continue; - } - - // Single-char operators - if ("+-*/^%&=<>".includes(s[0]!)) { - tokens.push({ type: "op", v: s[0]! }); - i++; - continue; - } - - if (s[0] === "(" || s[0] === ")") { - tokens.push({ type: "paren", v: s[0] }); - i++; - continue; - } - - if (s[0] === ",") { - tokens.push({ type: "comma" }); - i++; - continue; - } - - // Skip unknown - i++; - } - return tokens; -} - -// ── Recursive-descent parser / evaluator ────────────────────────────────────── - -type Arg = number | string | boolean | (number | string | boolean)[]; - -function evalFormula( - formula: string, - sheet: Sheet, - utils: Utils, - selfRef: string, -): number | string | boolean { - const tokens = tokenise(formula); - if (tokens.length === 0) return 0; - let pos = 0; - - const peek = (): Token | undefined => tokens[pos]; - const next = (): Token => tokens[pos++]!; - - function expect(type: string, val?: string): Token { - const t = next(); - if (!t || t.type !== type) throw new Error("Unexpected token"); - if (val !== undefined && "v" in t && (t as { v: unknown }).v !== val) - throw new Error("Expected " + val); - return t; - } - - // ── Precedence layers ───────────────────────────────────────────────── - - function parseExpr(): number | string | boolean { - return parseComparison(); - } - - function parseComparison(): number | string | boolean { - let left = parseConcat(); - while ( - peek()?.type === "op" && - ["=", "<", ">", ">=", "<=", "<>", "!="].includes( - (peek() as { v: string }).v, - ) - ) { - const op = (next() as { v: string }).v; - const right = parseConcat(); - const l = typeof left === "number" ? left : Number(left); - const r = typeof right === "number" ? right : Number(right); - switch (op) { - case "=": - left = left === right; - break; - case "<": - left = l < r; - break; - case ">": - left = l > r; - break; - case ">=": - left = l >= r; - break; - case "<=": - left = l <= r; - break; - case "<>": - case "!=": - left = left !== right; - break; - } - } - return left; - } - - function parseConcat(): number | string | boolean { - let left = parseAddSub(); - while (peek()?.type === "op" && (peek() as { v: string }).v === "&") { - next(); - const right = parseAddSub(); - left = String(left) + String(right); - } - return left; - } - - function parseAddSub(): number | string | boolean { - let left = parseMulDiv(); - while ( - peek()?.type === "op" && - ["+", "-"].includes((peek() as { v: string }).v) - ) { - const op = (next() as { v: string }).v; - const right = parseMulDiv(); - const l = typeof left === "number" ? left : Number(left); - const r = typeof right === "number" ? right : Number(right); - left = op === "+" ? l + r : l - r; - } - return left; - } - - function parseMulDiv(): number | string | boolean { - let left = parseUnary(); - while ( - peek()?.type === "op" && - ["*", "/"].includes((peek() as { v: string }).v) - ) { - const op = (next() as { v: string }).v; - const right = parseUnary(); - const l = typeof left === "number" ? left : Number(left); - const r = typeof right === "number" ? right : Number(right); - left = op === "*" ? l * r : r === 0 ? NaN : l / r; - } - return left; - } - - function parseUnary(): number | string | boolean { - if (peek()?.type === "op" && (peek() as { v: string }).v === "-") { - next(); - const v = parsePower(); - return typeof v === "number" ? -v : -Number(v); - } - if (peek()?.type === "op" && (peek() as { v: string }).v === "+") { - next(); - return parsePower(); - } - return parsePower(); - } - - function parsePower(): number | string | boolean { - let left = parsePostfix(); - if (peek()?.type === "op" && (peek() as { v: string }).v === "^") { - next(); - const right = parseUnary(); - left = Math.pow( - typeof left === "number" ? left : Number(left), - typeof right === "number" ? right : Number(right), - ); - } - return left; - } - - /** Handle postfix `%` operator: 50% → 0.5, A1% → A1/100 */ - function parsePostfix(): number | string | boolean { - let left = parsePrimary(); - while (peek()?.type === "op" && (peek() as { v: string }).v === "%") { - next(); - left = (typeof left === "number" ? left : Number(left)) / 100; - } - return left; - } - - function parsePrimary(): number | string | boolean { - const t = peek(); - if (!t) throw new Error("Unexpected end"); - - switch (t.type) { - case "num": - next(); - return t.v; - case "str": - next(); - return t.v; - case "bool": - next(); - return t.v; - - case "cell": { - next(); - if (t.ref === selfRef) return 0; // self-ref guard - return cellValue(sheet, t.ref); - } - - case "range": { - next(); - const rng = utils.decode_range(t.ref); - return cellValue(sheet, utils.encode_cell(rng.s)); - } - - case "func": - return parseFunc(); - - case "paren": - if (t.v === "(") { - next(); - const v = parseExpr(); - expect("paren", ")"); - return v; - } - throw new Error("Unexpected )"); - - default: - throw new Error("Unexpected token"); - } - } - - // ── Function calls ──────────────────────────────────────────────────── - - function parseFunc(): number | string | boolean { - const name = (next() as { name: string }).name; - expect("paren", "("); - const args = parseFuncArgs(); - expect("paren", ")"); - return evalFunc(name, args); - } - - function parseFuncArgs(): Arg[] { - const args: Arg[] = []; - if (peek()?.type === "paren" && (peek() as { v: string }).v === ")") - return args; - args.push(parseFuncArg()); - while (peek()?.type === "comma") { - next(); - args.push(parseFuncArg()); - } - return args; - } - - function parseFuncArg(): Arg { - if (peek()?.type === "range") { - const t = next() as { ref: string }; - return resolveRange(sheet, utils, t.ref); - } - return parseExpr(); - } - - function flatNums(args: Arg[]): number[] { - const out: number[] = []; - for (const a of args) { - if (Array.isArray(a)) { - for (const v of a) { - if (typeof v === "number") out.push(v); - else if (typeof v === "boolean") out.push(v ? 1 : 0); - } - } else if (typeof a === "number") { - out.push(a); - } else if (typeof a === "boolean") { - out.push(a ? 1 : 0); - } else { - const n = Number(a); - if (!isNaN(n)) out.push(n); - } - } - return out; - } - - function evalFunc(name: string, args: Arg[]): number | string | boolean { - switch (name) { - case "SUM": - return flatNums(args).reduce((a, b) => a + b, 0); - - case "AVERAGE": { - const n = flatNums(args); - return n.length > 0 - ? n.reduce((a, b) => a + b, 0) / n.length - : 0; - } - - case "MIN": { - const n = flatNums(args); - return n.length > 0 ? Math.min(...n) : 0; - } - case "MAX": { - const n = flatNums(args); - return n.length > 0 ? Math.max(...n) : 0; - } - - case "COUNT": - return flatNums(args).length; - case "COUNTA": { - let c = 0; - for (const a of args) c += Array.isArray(a) ? a.length : 1; - return c; - } - - case "ABS": - return Math.abs(Number(args[0] ?? 0)); - case "INT": - return Math.floor(Number(args[0] ?? 0)); - case "SQRT": - return Math.sqrt(Number(args[0] ?? 0)); - case "POWER": - return Math.pow(Number(args[0] ?? 0), Number(args[1] ?? 0)); - case "PI": - return Math.PI; - case "LOG": { - const val = Number(args[0] ?? 1); - const base = args[1] !== undefined ? Number(args[1]) : 10; - return Math.log(val) / Math.log(base); - } - case "LN": - return Math.log(Number(args[0] ?? 1)); - - case "ROUND": { - const v = Number(args[0] ?? 0), - p = Number(args[1] ?? 0), - f = Math.pow(10, p); - return Math.round(v * f) / f; - } - case "ROUNDUP": { - const v = Number(args[0] ?? 0), - p = Number(args[1] ?? 0), - f = Math.pow(10, p); - return Math.ceil(v * f) / f; - } - case "ROUNDDOWN": { - const v = Number(args[0] ?? 0), - p = Number(args[1] ?? 0), - f = Math.pow(10, p); - return Math.floor(v * f) / f; - } - - case "MOD": { - const n = Number(args[0] ?? 0), - d = Number(args[1] ?? 1); - return d === 0 ? NaN : n % d; - } - - case "IF": { - const cond = args[0]; - const truthy = - typeof cond === "number" - ? cond !== 0 - : typeof cond === "boolean" - ? cond - : Boolean(cond); - return truthy - ? ((args[1] ?? true) as number | string | boolean) - : ((args[2] ?? false) as number | string | boolean); - } - - case "AND": { - for (const a of args) { - if (Array.isArray(a)) { - if (a.some((v) => !v)) return false; - } else if (!a && a !== 0) return false; - } - return true; - } - - case "OR": { - for (const a of args) { - if (Array.isArray(a)) { - if (a.some((v) => !!v || v === 0)) return true; - } else if (a || a === 0) return true; - } - return false; - } - - case "NOT": - return !args[0]; - - case "LEN": - return String(args[0] ?? "").length; - case "UPPER": - return String(args[0] ?? "").toUpperCase(); - case "LOWER": - return String(args[0] ?? "").toLowerCase(); - case "TRIM": - return String(args[0] ?? "").trim(); - case "CONCATENATE": - case "CONCAT": - return args - .map((a) => - Array.isArray(a) ? a.map(String).join("") : String(a), - ) - .join(""); - case "LEFT": - return String(args[0] ?? "").slice(0, Number(args[1] ?? 1)); - case "RIGHT": { - const s = String(args[0] ?? ""); - return s.slice(Math.max(0, s.length - Number(args[1] ?? 1))); - } - case "MID": { - const s = String(args[0] ?? ""); - const start = Number(args[1] ?? 1) - 1; - return s.slice(start, start + Number(args[2] ?? 1)); - } - - case "TODAY": - return new Date().toISOString().split("T")[0]!; - case "NOW": - return new Date().toISOString(); - - default: - throw new Error(`Unknown function: ${name}`); - } - } - - // ── Run ─────────────────────────────────────────────────────────────── - return parseExpr(); -} diff --git a/src/utils/pdfExport.ts b/src/utils/pdfExport.ts deleted file mode 100644 index cf0c2f9..0000000 --- a/src/utils/pdfExport.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { App, TFile, Notice } from "obsidian"; -import * as pdfjsLib from "pdfjs-dist"; -import { PDFDocument, rgb, LineCapStyle } from "pdf-lib"; -import type { AnnotationFile } from "../types"; - -/** - * Embed pen/highlighter annotations as vector SVG paths into a copy of the - * source PDF and save it as `.annotated.pdf` next to the original. - */ -export async function exportAnnotatedPdf( - app: App, - currentFile: TFile, - pdfDoc: pdfjsLib.PDFDocumentProxy, - annotData: AnnotationFile, -): Promise { - new Notice("Exporting PDF with annotations…"); - - try { - const srcBuffer = await app.vault.adapter.readBinary(currentFile.path); - const pdfLibDoc = await PDFDocument.load(srcBuffer); - const pages = pdfLibDoc.getPages(); - - const hexToRgb = (hex: string) => { - const n = parseInt(hex.replace("#", ""), 16); - return rgb( - ((n >> 16) & 255) / 255, - ((n >> 8) & 255) / 255, - (n & 255) / 255, - ); - }; - - for (const pa of annotData.pages) { - const libPage = pages[pa.page - 1]; - if (!libPage || pa.paths.length === 0) continue; - - const pjsPage = await pdfDoc.getPage(pa.page); - const vp = pjsPage.getViewport({ scale: 1.0 }); - const pdfW = libPage.getWidth(); - const pdfH = libPage.getHeight(); - const scaleX = pdfW / vp.width; - const scaleY = pdfH / vp.height; - - for (const path of pa.paths) { - if (path.tool === "eraser" || path.points.length < 2) continue; - - const pts = path.points.map((p: { x: number; y: number }) => ({ - px: p.x * vp.width * scaleX, - // PDF coordinate system has bottom-left origin (y-flipped vs canvas) - py: pdfH - p.y * vp.height * scaleY, - })); - - const lineWidth = - (path.width * scaleX + path.width * scaleY) / 2; - const opacity = - path.tool === "highlighter" ? (path.opacity ?? 0.35) : 1; - - let d = `M ${pts[0]!.px.toFixed(2)} ${pts[0]!.py.toFixed(2)}`; - for (let i = 1; i < pts.length; i++) { - d += ` L ${pts[i]!.px.toFixed(2)} ${pts[i]!.py.toFixed(2)}`; - } - - libPage.drawSvgPath(d, { - borderColor: hexToRgb(path.color), - borderWidth: lineWidth, - borderOpacity: opacity, - borderLineCap: LineCapStyle.Round, - opacity: 0, // fill: none — stroke-only path - }); - } - } - - const exportBytes = await pdfLibDoc.save(); - const exportBuffer = exportBytes.buffer as ArrayBuffer; - - const dir = currentFile.parent?.path ?? ""; - const base = currentFile.basename; - const exportPath = dir - ? `${dir}/${base}.annotated.pdf` - : `${base}.annotated.pdf`; - - const existing = app.vault.getAbstractFileByPath(exportPath); - if (existing && existing instanceof TFile) { - await app.vault.modifyBinary(existing, exportBuffer); - } else { - await app.vault.createBinary(exportPath, exportBuffer); - } - - new Notice(`✅ Exported to "${base}.annotated.pdf"`); - } catch (err) { - new Notice(`❌ Export failed: ${String(err)}`); - } -} diff --git a/src/utils/pdfSnap.ts b/src/utils/pdfSnap.ts deleted file mode 100644 index 014ae58..0000000 --- a/src/utils/pdfSnap.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { SnapDirection } from "../settings"; - -export type Point = { x: number; y: number }; - -/** - * Constrain `raw` to the given snap direction relative to `origin`. - * All coordinates are normalised 0-1 fractions of the canvas. - */ -export function snapPoint( - origin: Point, - raw: Point, - direction: SnapDirection, -): Point { - const dx = raw.x - origin.x; - const dy = raw.y - origin.y; - - switch (direction) { - case "horizontal": - return { x: raw.x, y: origin.y }; - - case "vertical": - return { x: origin.x, y: raw.y }; - - case "slope": { - // Snap to nearest 45° increment (8 directions) - const angle = Math.atan2(dy, dx); - const snapped = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4); - const dist = Math.sqrt(dx * dx + dy * dy); - return { - x: origin.x + dist * Math.cos(snapped), - y: origin.y + dist * Math.sin(snapped), - }; - } - } -} diff --git a/src/utils/units.ts b/src/utils/units.ts new file mode 100644 index 0000000..9e00fd1 --- /dev/null +++ b/src/utils/units.ts @@ -0,0 +1,68 @@ +/** + * ViewItAll — Unit Conversion Utilities + * + * OOXML uses several unit systems. These helpers convert to + * CSS-friendly values (px, pt, em). + * Pure functions — no Obsidian state. + */ + +/** 1 inch = 914400 EMU (English Metric Units) */ +const EMU_PER_INCH = 914400; + +/** Standard CSS pixels per inch */ +const PX_PER_INCH = 96; + +/** Convert EMU to CSS pixels. */ +export function emuToPx(emu: number): number { + return Math.round((emu / EMU_PER_INCH) * PX_PER_INCH); +} + +/** + * Convert half-points to CSS points. + * OOXML `w:sz` stores font size in half-points (e.g. 24 = 12pt). + */ +export function halfPointsToPt(halfPts: number): number { + return halfPts / 2; +} + +/** + * Convert twips to CSS pixels. + * 1 inch = 1440 twips. Used for indentation, spacing, table widths. + */ +export function twipsToPx(twips: number): number { + return Math.round((twips / 1440) * PX_PER_INCH); +} + +/** + * Convert twentieths of a point to CSS points. + * Same as twips but expressed differently in OOXML docs. + */ +export function twipsToPt(twips: number): number { + return twips / 20; +} + +/** + * Convert DXA (twentieths of a point) to CSS pixels. + * DXA is used for table cell widths in OOXML. + */ +export function dxaToPx(dxa: number): number { + return twipsToPx(dxa); +} + +/** + * Parse a numeric string safely, returning undefined if not a valid number. + */ +export function safeParseInt(value: string | null | undefined): number | undefined { + if (value == null) return undefined; + const n = parseInt(value, 10); + return isNaN(n) ? undefined : n; +} + +/** + * Parse a numeric float string safely. + */ +export function safeParseFloat(value: string | null | undefined): number | undefined { + if (value == null) return undefined; + const n = parseFloat(value); + return isNaN(n) ? undefined : n; +} diff --git a/src/utils/xml.ts b/src/utils/xml.ts new file mode 100644 index 0000000..015f9ea --- /dev/null +++ b/src/utils/xml.ts @@ -0,0 +1,134 @@ +/** + * ViewItAll — XML Utility Helpers + * + * OOXML namespace constants and typed DOM helpers. + * Pure functions — no Obsidian state. + */ + +/** Word processing main namespace (w:) */ +export const NS_W = + "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; + +/** Relationships namespace (r:) */ +export const NS_R = + "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; + +/** DrawingML main namespace (a:) */ +export const NS_A = + "http://schemas.openxmlformats.org/drawingml/2006/main"; + +/** Word Drawing namespace (wp:) */ +export const NS_WP = + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"; + +/** Picture namespace (pic:) */ +export const NS_PIC = + "http://schemas.openxmlformats.org/drawingml/2006/picture"; + +/** Package relationships namespace */ +export const NS_RELS = + "http://schemas.openxmlformats.org/package/2006/relationships"; + +/** + * Get all child elements matching a namespace + local name. + * Returns a plain array (not a live NodeList). + */ +export function getElements( + parent: Element | Document, + ns: string, + localName: string, +): Element[] { + return Array.from(parent.getElementsByTagNameNS(ns, localName)); +} + +/** + * Get the first child element matching a namespace + local name, or null. + */ +export function getElement( + parent: Element | Document, + ns: string, + localName: string, +): Element | null { + return parent.getElementsByTagNameNS(ns, localName).item(0); +} + +/** + * Get an attribute value from an element in a specific namespace. + * Falls back to checking without namespace (some parsers strip ns prefixes). + */ +export function getAttr( + el: Element, + ns: string, + localName: string, +): string | null { + return el.getAttributeNS(ns, localName) ?? el.getAttribute(localName); +} + +/** + * Get an attribute value from the w: namespace. + */ +export function getWAttr(el: Element, localName: string): string | null { + const val = el.getAttributeNS(NS_W, localName); + if (val) return val; + return el.getAttribute(`w:${localName}`) ?? el.getAttribute(localName); +} + +/** + * Get the `w:val` attribute from an element (very common in OOXML). + */ +export function getVal(el: Element): string | null { + return getWAttr(el, "val"); +} + +/** + * Get direct child elements (not descendants) matching namespace + local name. + */ +export function getDirectChildren( + parent: Element, + ns: string, + localName: string, +): Element[] { + const results: Element[] = []; + for (let i = 0; i < parent.childNodes.length; i++) { + const node = parent.childNodes.item(i); + if ( + node && + node.nodeType === Node.ELEMENT_NODE && + (node as Element).localName === localName && + (node as Element).namespaceURI === ns + ) { + results.push(node as Element); + } + } + return results; +} + +/** + * Get the first direct child element matching namespace + local name. + */ +export function getDirectChild( + parent: Element, + ns: string, + localName: string, +): Element | null { + for (let i = 0; i < parent.childNodes.length; i++) { + const node = parent.childNodes.item(i); + if ( + node && + node.nodeType === Node.ELEMENT_NODE && + (node as Element).localName === localName && + (node as Element).namespaceURI === ns + ) { + return node as Element; + } + } + return null; +} + +/** + * Parse an XML string into a Document using the browser's DOMParser. + */ +export function parseXml(xmlString: string): Document { + const parser = new DOMParser(); + return parser.parseFromString(xmlString, "application/xml"); +} diff --git a/src/views/DocxView.ts b/src/views/DocxView.ts index a450b8d..038abfe 100644 --- a/src/views/DocxView.ts +++ b/src/views/DocxView.ts @@ -1,30 +1,27 @@ -import { - FileView, - TFile, - WorkspaceLeaf, - Notice, - Modal, - App, - setIcon, - setTooltip, -} from "obsidian"; -import { VIEW_TYPE_DOCX } from "../types"; -import { readDocxAsHtml, saveHtmlAsDocx } from "../utils/docxUtils"; +/** + * ViewItAll — DocxView + * + * FileView subclass that renders .docx files natively using the + * OOXML parser + DOM renderer pipeline. + */ + +import { FileView, Notice, TFile, WorkspaceLeaf, setIcon, setTooltip } from "obsidian"; import type ViewItAllPlugin from "../main"; +import { VIEW_TYPE_DOCX } from "../types"; +import type { DocxDocument } from "../docx/model"; +import { parseDocx } from "../docx/parser"; +import { renderDocument } from "../docx/renderer"; export class DocxView extends FileView { - private plugin: ViewItAllPlugin; - private editMode = false; - private isDirty = false; - private contentDiv: HTMLElement | null = null; - private editToggleBtn: HTMLElement | null = null; - private saveBtn: HTMLElement | null = null; - private undoBtn: HTMLElement | null = null; - private redoBtn: HTMLElement | null = null; - private dirtyIndicator: HTMLElement | null = null; - private currentFile: TFile | null = null; - private undoStack: string[] = []; - private redoStack: string[] = []; + plugin: ViewItAllPlugin; + + private wrapperEl: HTMLElement | null = null; + private toolbarEl: HTMLElement | null = null; + private scrollEl: HTMLElement | null = null; + private contentEl_: HTMLElement | null = null; + private blobUrls: string[] = []; + private docModel: DocxDocument | null = null; + private currentZoom = 1.0; constructor(leaf: WorkspaceLeaf, plugin: ViewItAllPlugin) { super(leaf); @@ -34,264 +31,156 @@ export class DocxView extends FileView { getViewType(): string { return VIEW_TYPE_DOCX; } + getDisplayText(): string { - return this.file?.basename ?? "Word Document"; + return this.file?.basename ?? "Word document"; } + getIcon(): string { return "file-text"; } - canAcceptExtension(extension: string): boolean { - return extension === "docx"; - } - async onLoadFile(file: TFile): Promise { - this.currentFile = file; - this.editMode = this.plugin.settings.docxDefaultEditMode; - this.isDirty = false; - await this.renderFile(file); - } + const s = this.plugin.settings; + this.currentZoom = s.docxDefaultZoom; - // No async work needed — returns resolved promise for type compatibility - onUnloadFile(_file: TFile): Promise { + // Clear previous content this.contentEl.empty(); - this.contentDiv = null; - this.editToggleBtn = null; - this.saveBtn = null; - this.undoBtn = null; - this.redoBtn = null; - this.dirtyIndicator = null; - return Promise.resolve(); - } - private async renderFile(file: TFile): Promise { - this.contentEl.empty(); + // Build layout + const isBottom = s.docxToolbarPosition === "bottom"; + this.wrapperEl = this.contentEl.createEl("div", { + cls: `via-docx-wrapper${isBottom ? " via-docx-wrapper--toolbar-bottom" : ""}`, + }); - const isBottom = this.plugin.settings.docxToolbarPosition === "bottom"; + this.toolbarEl = this.wrapperEl.createEl("div", { + cls: "via-docx-toolbar", + }); - // Wrapper — flex column so toolbar can be ordered top or bottom - const wrapper = this.contentEl.createEl("div", { - cls: "via-docx-wrapper", + this.scrollEl = this.wrapperEl.createEl("div", { + cls: "via-docx-scroll", }); - if (isBottom) wrapper.classList.add("via-docx-wrapper--toolbar-bottom"); - - // Scroll container - const scrollEl = wrapper.createEl("div", { cls: "via-docx-scroll" }); - - // ── Toolbar ──────────────────────────────────────────────────────── - const toolbar = wrapper.createEl("div", { cls: "via-docx-toolbar" }); - - // Edit / View toggle - this.editToggleBtn = toolbar.createEl("div", { cls: "clickable-icon" }); - setIcon(this.editToggleBtn, this.editMode ? "eye" : "pencil"); - setTooltip( - this.editToggleBtn, - this.editMode ? "Switch to view mode" : "Switch to edit mode", - ); - this.editToggleBtn.classList.toggle("is-active", this.editMode); - this.editToggleBtn.addEventListener("click", () => this.toggleEdit()); - - toolbar.createEl("div", { cls: "via-toolbar-sep" }); - - // Undo / redo (visible in edit mode only) - this.undoBtn = toolbar.createEl("div", { cls: "clickable-icon" }); - setIcon(this.undoBtn, "undo-2"); - setTooltip(this.undoBtn, "Undo (Ctrl+Z)"); - this.undoBtn.classList.toggle("via-hidden", !this.editMode); - this.undoBtn.addEventListener("click", () => this.undo()); - - this.redoBtn = toolbar.createEl("div", { cls: "clickable-icon" }); - setIcon(this.redoBtn, "redo-2"); - setTooltip(this.redoBtn, "Redo (Ctrl+Shift+Z)"); - this.redoBtn.classList.toggle("via-hidden", !this.editMode); - this.redoBtn.addEventListener("click", () => this.redo()); - - // Dirty indicator (yellow dot when unsaved changes exist) - this.dirtyIndicator = toolbar.createEl("div", { - cls: "via-docx-dirty-dot via-hidden", + + this.contentEl_ = this.scrollEl.createEl("div", { + cls: "via-docx-content", }); - setTooltip(this.dirtyIndicator, "Unsaved changes"); - // Spacer - toolbar.createEl("div", { cls: "via-toolbar-spacer" }); + // Build toolbar + this.buildToolbar(); - // Save button - this.saveBtn = toolbar.createEl("div", { - cls: "clickable-icon via-icon-save", - }); - setIcon(this.saveBtn, "save"); - setTooltip(this.saveBtn, "Save (overwrite original)"); - this.saveBtn.classList.toggle("via-hidden", !this.editMode); - this.saveBtn.addEventListener("click", () => { void this.saveFile(); }); - - // ── Conversion warnings ──────────────────────────────────────────── - let html: string; - let messages: string[]; + // Apply initial zoom + this.applyZoom(); + + // Parse and render try { - const buffer = await this.app.vault.adapter.readBinary(file.path); - ({ html, messages } = await readDocxAsHtml(buffer)); - } catch (err) { - scrollEl.createEl("p", { + const data = await this.app.vault.readBinary(file); + this.docModel = await parseDocx(data); + this.blobUrls = renderDocument(this.docModel, this.contentEl_); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + new Notice(`Failed to open .docx: ${msg}`); + this.contentEl_.createEl("div", { cls: "via-error", - text: `Failed to read file: ${String(err)}`, + text: `Error loading document: ${msg}`, }); - return; } + } - if (messages.length > 0) { - const warn = scrollEl.createEl("div", { cls: "via-warning" }); - warn.createEl("strong", { text: "Conversion notes: " }); - warn.createEl("span", { text: messages.join("; ") }); + async onUnloadFile(): Promise { + // Revoke all blob URLs to prevent memory leaks + for (const url of this.blobUrls) { + URL.revokeObjectURL(url); } + this.blobUrls = []; - // ── Content ──────────────────────────────────────────────────────── - this.contentDiv = scrollEl.createEl("div", { cls: "via-docx-content" }); - const parser = new DOMParser(); - const parsed = parser.parseFromString(html, "text/html"); - while (parsed.body.firstChild) { - this.contentDiv.appendChild( - this.contentDiv.ownerDocument.importNode( - parsed.body.firstChild, - true, - ), - ); - } - this.contentDiv.contentEditable = this.editMode ? "true" : "false"; - if (this.editMode) this.contentDiv.classList.add("via-editable"); - - // Track dirty state and undo history - this.undoStack = [html]; - this.redoStack = []; - this.contentDiv.addEventListener("input", () => { - this.setDirty(true); - if (this.contentDiv) { - this.undoStack.push(this.contentDiv.innerHTML); - this.redoStack = []; - } - }); + // Clear DOM refs + this.contentEl.empty(); + this.wrapperEl = null; + this.toolbarEl = null; + this.scrollEl = null; + this.contentEl_ = null; + this.docModel = null; } - private undo(): void { - if (!this.contentDiv || this.undoStack.length <= 1) return; - const current = this.undoStack.pop()!; - this.redoStack.push(current); - const prev = this.undoStack[this.undoStack.length - 1]!; - this.setContentFromHtml(this.contentDiv, prev); - this.setDirty(this.undoStack.length > 1); + canAcceptExtension(extension: string): boolean { + return extension === "docx" && this.plugin.settings.enableDocx; } - private redo(): void { - if (!this.contentDiv || this.redoStack.length === 0) return; - const next = this.redoStack.pop()!; - this.undoStack.push(next); - this.setContentFromHtml(this.contentDiv, next); - this.setDirty(true); - } + // ── Toolbar ───────────────────────────────────────────────────────────── - private toggleEdit(): void { - this.editMode = !this.editMode; - if (!this.contentDiv || !this.editToggleBtn || !this.saveBtn) return; - this.contentDiv.contentEditable = this.editMode ? "true" : "false"; - this.contentDiv.classList.toggle("via-editable", this.editMode); - setIcon(this.editToggleBtn, this.editMode ? "eye" : "pencil"); - setTooltip( - this.editToggleBtn, - this.editMode ? "Switch to view mode" : "Switch to edit mode", - ); - this.editToggleBtn.classList.toggle("is-active", this.editMode); - this.saveBtn.classList.toggle("via-hidden", !this.editMode); - if (this.undoBtn) - this.undoBtn.classList.toggle("via-hidden", !this.editMode); - if (this.redoBtn) - this.redoBtn.classList.toggle("via-hidden", !this.editMode); - // Hide dirty indicator when leaving edit mode without saving - if (!this.editMode) this.setDirty(false); - } + private buildToolbar(): void { + if (!this.toolbarEl) return; - private setDirty(dirty: boolean): void { - this.isDirty = dirty; - if (this.dirtyIndicator) - this.dirtyIndicator.classList.toggle("via-hidden", !dirty); - } + // File name label + const fileLabel = this.toolbarEl.createEl("div", { + cls: "via-docx-file-label", + }); + const fileIcon = fileLabel.createEl("div", { cls: "clickable-icon" }); + setIcon(fileIcon, "file-text"); + fileLabel.createEl("span", { + cls: "via-docx-file-name", + text: this.file?.name ?? "Untitled", + }); - /** Replace an element's children with parsed HTML without using innerHTML. */ - private setContentFromHtml(el: HTMLElement, html: string): void { - el.empty(); - const parsed = new DOMParser().parseFromString(html, "text/html"); - while (parsed.body.firstChild) { - el.appendChild(el.ownerDocument.importNode(parsed.body.firstChild, true)); - } - } + // Separator + this.toolbarEl.createEl("div", { cls: "via-toolbar-sep" }); - private async saveFile(): Promise { - if (!this.currentFile || !this.contentDiv) return; + // Zoom out + const zoomOut = this.toolbarEl.createEl("div", { cls: "clickable-icon" }); + setIcon(zoomOut, "minus"); + setTooltip(zoomOut, "Zoom out"); + zoomOut.addEventListener("click", () => this.adjustZoom(-0.25)); - if (this.plugin.settings.confirmOnSave) { - const confirmed = await confirmModal( - this.app, - `Overwrite "${this.currentFile.name}"?`, - "This will replace the original file. Complex formatting may be simplified.", - ); - if (!confirmed) return; - } + // Zoom label + const zoomLabel = this.toolbarEl.createEl("button", { + cls: "via-btn via-btn-zoom-label", + text: this.formatZoom(), + }); + setTooltip(zoomLabel, "Reset zoom"); + zoomLabel.addEventListener("click", () => { + this.currentZoom = 1.0; + this.applyZoom(); + zoomLabel.textContent = this.formatZoom(); + }); - try { - const buffer = await saveHtmlAsDocx(this.contentDiv.innerHTML); - await this.app.vault.modifyBinary(this.currentFile, buffer); - this.setDirty(false); - new Notice(`Saved "${this.currentFile.name}"`); - } catch (err) { - new Notice(`Save failed: ${String(err)}`); - } + // Zoom in + const zoomIn = this.toolbarEl.createEl("div", { cls: "clickable-icon" }); + setIcon(zoomIn, "plus"); + setTooltip(zoomIn, "Zoom in"); + zoomIn.addEventListener("click", () => this.adjustZoom(0.25)); + + // Spacer + this.toolbarEl.createEl("div", { cls: "via-toolbar-spacer" }); + + // Store zoom label ref for updates + this.zoomLabelEl = zoomLabel; } -} -// ── Simple confirmation modal ────────────────────────────────────────────── -function confirmModal( - app: App, - title: string, - message: string, -): Promise { - return new Promise((resolve) => { - const modal = new ConfirmModal(app, title, message, resolve); - modal.open(); - }); -} + private zoomLabelEl: HTMLElement | null = null; -class ConfirmModal extends Modal { - constructor( - app: App, - private title: string, - private message: string, - private resolve: (v: boolean) => void, - ) { - super(app); + private adjustZoom(delta: number): void { + const newZoom = Math.max(0.25, Math.min(3.0, this.currentZoom + delta)); + this.currentZoom = newZoom; + this.applyZoom(); + if (this.zoomLabelEl) { + this.zoomLabelEl.textContent = this.formatZoom(); + } } - onOpen(): void { - this.setTitle(this.title); - const { contentEl } = this; - contentEl.createEl("p", { text: this.message }); - const btnRow = contentEl.createEl("div", { - cls: "modal-button-container", - }); - btnRow - .createEl("button", { text: "Cancel" }) - .addEventListener("click", () => { - this.resolve(false); - this.close(); - }); - const overwriteBtn = btnRow.createEl("button", { - text: "Overwrite", - cls: "mod-cta via-btn-danger", - }); - overwriteBtn.addEventListener("click", () => { - this.resolve(true); - this.close(); - }); + private applyZoom(): void { + if (this.contentEl_) { + this.contentEl_.style.transform = `scale(${this.currentZoom})`; + this.contentEl_.style.transformOrigin = "top center"; + // Adjust width to compensate for scaling + if (this.currentZoom !== 1) { + this.contentEl_.style.width = `${100 / this.currentZoom}%`; + } else { + this.contentEl_.style.width = ""; + } + } } - onClose(): void { - this.contentEl.empty(); + private formatZoom(): string { + return `${Math.round(this.currentZoom * 100)}%`; } } diff --git a/src/views/PdfView.ts b/src/views/PdfView.ts deleted file mode 100644 index cdbbad5..0000000 --- a/src/views/PdfView.ts +++ /dev/null @@ -1,1517 +0,0 @@ -import { - FileView, - TFile, - WorkspaceLeaf, - Notice, - setIcon, - setTooltip, -} from "obsidian"; -import * as pdfjsLib from "pdfjs-dist"; -import type { RefProxy as PdfRefProxy } from "pdfjs-dist/types/src/display/api"; -import { VIEW_TYPE_PDF } from "../types"; -import type { - PageAnnotations, - AnnotationPath, - AnnotationFile, - TextNote, -} from "../types"; -import { - loadAnnotations, - saveAnnotations, - getPageAnnotations, - setPageAnnotations, -} from "../utils/annotations"; -import { snapPoint } from "../utils/pdfSnap"; -import { exportAnnotatedPdf } from "../utils/pdfExport"; -import { PdfSearchController } from "./pdf/PdfSearchController"; -import type { PageCtx, PageRenderState } from "./pdf/pdfTypes"; -import type ViewItAllPlugin from "../main"; -import type { SnapDirection } from "../settings"; - -// Virtual module resolved by esbuild's pdfWorkerPlugin — inlines the pdf.js worker. -declare const require: (id: string) => string; -const _pdfWorkerSrc: string = require("pdfjs-worker-src"); -let _workerBlobUrl: string | null = null; -function getPdfWorkerUrl(): string { - if (!_workerBlobUrl) { - const blob = new Blob([_pdfWorkerSrc], { - type: "application/javascript", - }); - _workerBlobUrl = URL.createObjectURL(blob); - } - return _workerBlobUrl; -} - -type AnnotTool = "none" | "pen" | "highlighter" | "eraser" | "note"; - -interface PdfOutlineItem { - title: string; - dest: string | unknown[] | null; - items: PdfOutlineItem[]; -} - -// PageRenderState imported from pdfTypes - -export class PdfView extends FileView { - private plugin: ViewItAllPlugin; - private pdfDoc: pdfjsLib.PDFDocumentProxy | null = null; - private annotData: AnnotationFile = { version: 1, pages: [] }; - private pages: PageCtx[] = []; - private currentTool: AnnotTool = "none"; - private isDrawing = false; - private currentPath: AnnotationPath | null = null; - private currentFile: TFile | null = null; - - // Zoom - private currentScale = 1.0; - private readonly ZOOM_STEPS = [ - 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0, - ]; - private scrollAreaEl: HTMLElement | null = null; - private zoomLabelEl: HTMLElement | null = null; - private _zoomDebounceTimer: ReturnType | null = null; - - // Lazy rendering - private pageIndicatorEl: HTMLElement | null = null; - private pageObserver: IntersectionObserver | null = null; - private renderObserver: IntersectionObserver | null = null; - private _renderGen = 0; - - // Color dot & floating popover - private colorDotBtnEl: HTMLButtonElement | null = null; - private colorPopoverEl: HTMLElement | null = null; - - private readonly PEN_PRESETS = [ - "#e03131", - "#1971c2", - "#2f9e44", - "#212529", - "#e8590c", - "#7048e8", - ]; - private readonly HIGHLIGHT_PRESETS = [ - "#ffd43b", - "#22b8cf", - "#f783ac", - "#69db7c", - "#ffa94d", - "#da77f2", - ]; - - // Snapping - private snapDirection: SnapDirection = "horizontal"; - private snapDirBtnEl: HTMLButtonElement | null = null; - - // Layout containers - private wrapperEl: HTMLElement | null = null; - private bodyEl: HTMLElement | null = null; - private tocSidebarEl: HTMLElement | null = null; - private tocVisible = false; - private _outline: PdfOutlineItem[] = []; - - // Text notes overlay (keyed by note id) - private noteEls = new Map(); - - // Search controller - private search = new PdfSearchController(); - - constructor(leaf: WorkspaceLeaf, plugin: ViewItAllPlugin) { - super(leaf); - this.plugin = plugin; - pdfjsLib.GlobalWorkerOptions.workerSrc = getPdfWorkerUrl(); - } - - onload(): void { - super.onload(); - - const isActiveLeaf = () => - this.app.workspace.getActiveViewOfType(PdfView) === this; - - // ── Document-level snap key handler ────────────────────────────────── - // Registered at document level so it fires regardless of focus, guarded - // by the active-leaf check. - this.registerDomEvent( - document as unknown as HTMLElement, - "keydown", - (e: KeyboardEvent) => { - if (!isActiveLeaf()) return; - const s = this.plugin.settings; - const snapKey = s.snapActivateKey; // 'Alt' | 'Shift' - const snapPressed = snapKey === "Alt" ? e.altKey : e.shiftKey; - - if (e.key === snapKey) { - e.preventDefault(); - const drawing = - this.currentTool === "pen" || - this.currentTool === "highlighter" || - this.currentTool === "eraser"; - if (drawing) - this.snapDirBtnEl?.classList.add("via-btn-snap-active"); - } else if ( - snapPressed && - e.key.toLowerCase() === s.keySnapCycle - ) { - // Snap modifier + cycle key → cycle snap direction - e.preventDefault(); - const dirs: SnapDirection[] = [ - "horizontal", - "vertical", - "slope", - ]; - this.snapDirection = - dirs[ - (dirs.indexOf(this.snapDirection) + 1) % dirs.length - ]!; - this.updateSnapDirBtn(); - } - }, - ); - - this.registerDomEvent( - document as unknown as HTMLElement, - "keyup", - (e: KeyboardEvent) => { - if (!isActiveLeaf()) return; - if (e.key === this.plugin.settings.snapActivateKey) { - this.snapDirBtnEl?.classList.remove("via-btn-snap-active"); - } - }, - ); - - // ── Container-level shortcuts (zoom, tools, search) ────────────────── - this.registerDomEvent( - this.containerEl, - "keydown", - (e: KeyboardEvent) => { - const s = this.plugin.settings; - - // Ctrl/Cmd shortcuts - if (e.ctrlKey || e.metaKey) { - if (e.key === "0") { - e.preventDefault(); - void this.setZoom( - s.pdfDefaultZoom, - this.viewportCenterFrac(), - ); - } else if (e.key === "=" || e.key === "+") { - e.preventDefault(); - this.stepZoom(+1); - } else if (e.key === "-") { - e.preventDefault(); - this.stepZoom(-1); - } else if (e.key.toLowerCase() === s.keySearch) { - e.preventDefault(); - this.search.open(); - } - return; - } - - // Skip snap modifier combos — handled at document level - if ( - e.key === s.snapActivateKey || - (s.snapActivateKey === "Alt" && e.altKey) || - (s.snapActivateKey === "Shift" && e.shiftKey) - ) - return; - - // Tool shortcuts — skip if an input element has focus - const target = e.target as HTMLElement; - if ( - target.tagName === "INPUT" || - target.tagName === "TEXTAREA" || - target.isContentEditable - ) - return; - - const toolMap: Record = { - [s.keyToolView]: "none", - [s.keyToolPen]: "pen", - [s.keyToolHighlight]: "highlighter", - [s.keyToolErase]: "eraser", - [s.keyToolNote]: "note", - }; - const tool = toolMap[e.key.toLowerCase()]; - if (tool !== undefined) { - e.preventDefault(); - this.setTool(tool); - } - }, - ); - - // ── View header actions (top-right of the leaf) ─────────────────────── - const s0 = this.plugin.settings; - this.addAction( - "search", - `Search PDF (Ctrl+${s0.keySearch.toUpperCase()})`, - () => this.search.open(), - ); - this.addAction("list", "Table of contents", () => this.toggleToc()); - this.addAction("file-output", "Export annotated PDF", () => { - if (this.currentFile && this.pdfDoc) - void exportAnnotatedPdf( - this.app, - this.currentFile, - this.pdfDoc, - this.annotData, - ); - }); - } - - getViewType(): string { - return VIEW_TYPE_PDF; - } - getDisplayText(): string { - return this.file?.basename ?? "PDF"; - } - getIcon(): string { - return "file"; - } - canAcceptExtension(extension: string): boolean { - return extension === "pdf"; - } - - async onLoadFile(file: TFile): Promise { - this.currentFile = file; - this.currentTool = this.plugin.settings.pdfDefaultTool; - this.currentScale = this.plugin.settings.pdfDefaultZoom; - this.snapDirection = this.plugin.settings.snapDefaultDirection; - this.annotData = await loadAnnotations(this.app, file); - await this.renderPdf(file); - } - - // No async work needed — returns resolved promise for type compatibility - onUnloadFile(_file: TFile): Promise { - this._renderGen++; - this.hideColorPopover(); - if (this._zoomDebounceTimer !== null) { - clearTimeout(this._zoomDebounceTimer); - this._zoomDebounceTimer = null; - } - this.renderObserver?.disconnect(); - this.renderObserver = null; - this.pageObserver?.disconnect(); - this.pageObserver = null; - if (this.pdfDoc) { - void this.pdfDoc.destroy(); - this.pdfDoc = null; - } - this.pages = []; - this.search.destroy(); - this.noteEls.clear(); - this.tocSidebarEl = null; - this.tocVisible = false; - this.bodyEl = null; - this.snapDirBtnEl = null; - this.colorDotBtnEl = null; - this.contentEl.empty(); - return Promise.resolve(); - } - - private async renderPdf(file: TFile): Promise { - this._renderGen++; - this.contentEl.empty(); - this.pages = []; - this.search.destroy(); - this.noteEls.clear(); - this.tocSidebarEl = null; - this.tocVisible = false; - this.renderObserver?.disconnect(); - this.renderObserver = null; - this.pageObserver?.disconnect(); - this.pageObserver = null; - - const isBottom = this.plugin.settings.pdfToolbarPosition === "bottom"; - const wrapper = this.contentEl.createEl("div", { - cls: "via-pdf-wrapper", - }); - if (isBottom) wrapper.classList.add("via-pdf-wrapper--toolbar-bottom"); - this.wrapperEl = wrapper; - - const toolbar = this.buildToolbar(); - wrapper.appendChild(toolbar); - - const bodyEl = wrapper.createEl("div", { cls: "via-pdf-body" }); - this.bodyEl = bodyEl; - - const scrollArea = bodyEl.createEl("div", { cls: "via-pdf-scroll" }); - this.scrollAreaEl = scrollArea; - scrollArea.addEventListener( - "wheel", - (e: WheelEvent) => this.handleWheelZoom(e), - { passive: false }, - ); - - const loadingEl = scrollArea.createEl("div", { - cls: "via-pdf-loading", - }); - loadingEl.createEl("div", { cls: "via-pdf-loading-spinner" }); - loadingEl.createEl("span", { text: "Loading PDF…" }); - - let buffer: ArrayBuffer; - try { - buffer = await this.app.vault.adapter.readBinary(file.path); - } catch (err) { - loadingEl.remove(); - scrollArea.createEl("p", { - cls: "via-error", - text: `Cannot read file: ${String(err)}`, - }); - return; - } - - this.pdfDoc = await pdfjsLib.getDocument({ - data: new Uint8Array(buffer), - }).promise; - const numPages = this.pdfDoc.numPages; - - const sizes = await Promise.all( - Array.from({ length: numPages }, async (_, i) => { - const page = await this.pdfDoc!.getPage(i + 1); - const vp = page.getViewport({ scale: this.currentScale }); - return { w: Math.ceil(vp.width), h: Math.ceil(vp.height) }; - }), - ); - - loadingEl.remove(); - - for (let i = 0; i < numPages; i++) { - const { w, h } = sizes[i]!; - const container = scrollArea.createEl("div", { - cls: "via-pdf-page", - }); - container.style.cssText = `width:${w}px;height:${h}px;min-width:${w}px;min-height:${h}px`; - scrollArea.createEl("div", { - cls: "via-pdf-page-label", - text: `${i + 1} / ${numPages}`, - }); - this.pages.push({ - pageNum: i + 1, - state: "placeholder" as PageRenderState, - container, - pdfCanvas: null, - annotCanvas: null, - searchCanvas: null, - w, - h, - }); - } - - if (this.pageIndicatorEl) - this.pageIndicatorEl.textContent = `1 / ${numPages}`; - - // Wire search controller to current document - this.search.setContext(this.pdfDoc, this.pages, wrapper, bodyEl); - - this.attachRenderObserver(); - this.attachPageObserver(); - - // Render existing text notes - for (const note of this.annotData.notes ?? []) this.renderNoteEl(note); - - // Load TOC; auto-open if configured - this.loadToc() - .then(() => { - if ( - this.plugin.settings.showTocOnOpen && - this._outline?.length > 0 - ) { - this.toggleToc(); - } - }) - .catch(console.error); - } - - // Lazy rendering --------------------------------------------------------- - - private attachRenderObserver(): void { - this.renderObserver?.disconnect(); - if (!this.scrollAreaEl || this.pages.length === 0) return; - - const pageMap = new Map( - this.pages.map((p) => [p.container, p]), - ); - - this.renderObserver = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - const ctx = pageMap.get(entry.target); - if (!ctx) continue; - if (entry.isIntersecting) - this.renderPageCanvas(ctx).catch(console.error); - else this.unloadPageCanvas(ctx); - } - }, - { root: this.scrollAreaEl, rootMargin: "200% 0px" }, - ); - - for (const ctx of this.pages) - this.renderObserver.observe(ctx.container); - } - - private async renderPageCanvas(ctx: PageCtx): Promise { - if (ctx.state !== "placeholder") return; - ctx.state = "rendering"; - const gen = this._renderGen; - - const page = await this.pdfDoc!.getPage(ctx.pageNum); - const viewport = page.getViewport({ scale: this.currentScale }); - - if (gen !== this._renderGen) { - ctx.state = "placeholder"; - return; - } - - const pdfCanvas = ctx.container.createEl("canvas", { - cls: "via-pdf-canvas", - }); - pdfCanvas.width = ctx.w; - pdfCanvas.height = ctx.h; - - const annotCanvas = ctx.container.createEl("canvas", { - cls: "via-pdf-annot-canvas", - }); - annotCanvas.width = ctx.w; - annotCanvas.height = ctx.h; - - const searchCanvas = ctx.container.createEl("canvas", { - cls: "via-pdf-search-canvas", - }); - searchCanvas.width = ctx.w; - searchCanvas.height = ctx.h; - - await page.render({ - canvasContext: pdfCanvas.getContext("2d")!, - viewport, - }).promise; - - if (gen !== this._renderGen) { - pdfCanvas.remove(); - annotCanvas.remove(); - searchCanvas.remove(); - ctx.state = "placeholder"; - return; - } - - ctx.pdfCanvas = pdfCanvas; - ctx.annotCanvas = annotCanvas; - ctx.searchCanvas = searchCanvas; - ctx.state = "rendered"; - - this.redrawAnnotations(ctx); - if (this.search.hasMatches) this.search.drawHighlightsForPage(ctx); - this.attachDrawListeners(ctx); - this.updateCanvasInteraction(); - } - - private unloadPageCanvas(ctx: PageCtx): void { - if (ctx.state !== "rendered") return; - ctx.pdfCanvas?.remove(); - ctx.pdfCanvas = null; - ctx.annotCanvas?.remove(); - ctx.annotCanvas = null; - ctx.searchCanvas?.remove(); - ctx.searchCanvas = null; - ctx.state = "placeholder"; - } - - // Toolbar ---------------------------------------------------------------- - - private buildToolbar(): HTMLElement { - const bar = document.createElement("div"); - bar.className = "via-pdf-toolbar"; - const s = this.plugin.settings; - - // ── Tool group (pill) ────────────────────────────────────────────────── - const toolGroup = bar.createEl("div", { cls: "via-tool-group" }); - const toolDefs: { id: AnnotTool; icon: string; label: string }[] = [ - { - id: "none", - icon: "eye", - label: `View (${s.keyToolView.toUpperCase()})`, - }, - { - id: "pen", - icon: "pencil", - label: `Pen (${s.keyToolPen.toUpperCase()})`, - }, - { - id: "highlighter", - icon: "highlighter", - label: `Highlight (${s.keyToolHighlight.toUpperCase()})`, - }, - { - id: "eraser", - icon: "eraser", - label: `Erase (${s.keyToolErase.toUpperCase()})`, - }, - { - id: "note", - icon: "sticky-note", - label: `Note (${s.keyToolNote.toUpperCase()})`, - }, - ]; - for (const t of toolDefs) { - const btn = toolGroup.createEl("div", { - cls: "clickable-icon via-tool-btn", - }); - btn.dataset.tool = t.id; - setIcon(btn, t.icon); - setTooltip(btn, t.label); - if (t.id === this.currentTool) btn.classList.add("is-active"); - btn.addEventListener("click", () => this.setTool(t.id)); - } - - bar.createEl("div", { cls: "via-toolbar-sep" }); - - // ── Color dot (shown for pen/highlighter) ────────────────────────────── - const showColors = - this.currentTool === "pen" || this.currentTool === "highlighter"; - this.colorDotBtnEl = bar.createEl("button", { - cls: "via-color-dot-btn", - }); - const initColor = - this.currentTool === "pen" ? s.penColor : s.highlighterColor; - this.colorDotBtnEl.style.background = initColor; - this.colorDotBtnEl.style.display = showColors ? "" : "none"; - setTooltip(this.colorDotBtnEl, "Color & size options"); - this.colorDotBtnEl.addEventListener("click", (e) => - this.toggleColorPopover(e), - ); - - bar.createEl("div", { cls: "via-toolbar-sep" }); - - // ── Snap button ──────────────────────────────────────────────────────── - this.snapDirBtnEl = bar.createEl("button", { cls: "via-pdf-snap-btn" }); - this.updateSnapDirBtn(); - this.snapDirBtnEl.addEventListener("click", () => { - const dirs: SnapDirection[] = ["horizontal", "vertical", "slope"]; - this.snapDirection = - dirs[(dirs.indexOf(this.snapDirection) + 1) % dirs.length]!; - this.updateSnapDirBtn(); - }); - - bar.createEl("div", { cls: "via-toolbar-sep" }); - - // ── Zoom ─────────────────────────────────────────────────────────────── - const zoomOutBtn = bar.createEl("div", { cls: "clickable-icon" }); - setIcon(zoomOutBtn, "zoom-out"); - setTooltip(zoomOutBtn, "Zoom out (Ctrl+−)"); - zoomOutBtn.addEventListener("click", () => this.stepZoom(-1)); - - this.zoomLabelEl = bar.createEl("button", { - cls: "via-btn via-btn-zoom-label", - text: `${Math.round(this.currentScale * 100)}%`, - }); - setTooltip(this.zoomLabelEl, "Reset zoom (Ctrl+0)"); - this.zoomLabelEl.addEventListener("click", () => { - void this.setZoom(s.pdfDefaultZoom, this.viewportCenterFrac()); - }); - - const zoomInBtn = bar.createEl("div", { cls: "clickable-icon" }); - setIcon(zoomInBtn, "zoom-in"); - setTooltip(zoomInBtn, "Zoom in (Ctrl+=)"); - zoomInBtn.addEventListener("click", () => this.stepZoom(+1)); - - bar.createEl("div", { cls: "via-toolbar-sep" }); - - // ── Page indicator ───────────────────────────────────────────────────── - this.pageIndicatorEl = bar.createEl("button", { - cls: "via-pdf-page-indicator", - text: "— / —", - }); - setTooltip(this.pageIndicatorEl, "Jump to page"); - this.pageIndicatorEl.addEventListener("click", () => - this.openPageJumpInput(), - ); - - // ── Spacer ───────────────────────────────────────────────────────────── - bar.createEl("div", { cls: "via-toolbar-spacer" }); - - // ── Clear & Save ─────────────────────────────────────────────────────── - const clearBtn = bar.createEl("div", { - cls: "clickable-icon via-icon-danger", - }); - setIcon(clearBtn, "trash-2"); - setTooltip(clearBtn, "Clear page annotations"); - clearBtn.addEventListener("click", () => - this.clearCurrentPageAnnotations(), - ); - - const saveBtn = bar.createEl("div", { - cls: "clickable-icon via-icon-save", - }); - setIcon(saveBtn, "save"); - setTooltip(saveBtn, "Save annotations"); - saveBtn.addEventListener("click", () => { void this.persistAnnotations(); }); - - return bar; - } - - // Zoom ------------------------------------------------------------------- - - private stepZoom(direction: -1 | 1): void { - const idx = this.ZOOM_STEPS.findIndex( - (s) => Math.abs(s - this.currentScale) < 0.01, - ); - const next = - this.ZOOM_STEPS[ - Math.max( - 0, - Math.min(this.ZOOM_STEPS.length - 1, idx + direction), - ) - ]; - if (next !== undefined) void this.setZoom(next, this.viewportCenterFrac()); - } - - private async setZoom( - scale: number, - frac?: { x: number; y: number; pX: number; pY: number }, - ): Promise { - if (Math.abs(scale - this.currentScale) < 0.001) return; - this.currentScale = scale; - if (this.zoomLabelEl) - this.zoomLabelEl.textContent = `${Math.round(scale * 100)}%`; - await this.reRenderPages(frac); - } - - private handleWheelZoom(e: WheelEvent): void { - if (!e.ctrlKey && !e.metaKey) return; - e.preventDefault(); - const scrollEl = this.scrollAreaEl; - if (!scrollEl) return; - - const rect = scrollEl.getBoundingClientRect(); - const pX = e.clientX - rect.left; - const pY = e.clientY - rect.top; - const frac = { - x: (scrollEl.scrollLeft + pX) / (scrollEl.scrollWidth || 1), - y: (scrollEl.scrollTop + pY) / (scrollEl.scrollHeight || 1), - pX, - pY, - }; - - const idx = this.ZOOM_STEPS.findIndex( - (s) => Math.abs(s - this.currentScale) < 0.01, - ); - const next = - this.ZOOM_STEPS[ - Math.max( - 0, - Math.min( - this.ZOOM_STEPS.length - 1, - idx + (e.deltaY < 0 ? 1 : -1), - ), - ) - ]; - if (next === undefined || Math.abs(next - this.currentScale) < 0.001) - return; - - this.currentScale = next; - if (this.zoomLabelEl) - this.zoomLabelEl.textContent = `${Math.round(next * 100)}%`; - - if (this._zoomDebounceTimer !== null) - clearTimeout(this._zoomDebounceTimer); - this._zoomDebounceTimer = setTimeout(() => { - this._zoomDebounceTimer = null; - this.reRenderPages(frac).catch(console.error); - }, 250); - } - - private viewportCenterFrac(): - | { x: number; y: number; pX: number; pY: number } - | undefined { - const el = this.scrollAreaEl; - if (!el) return undefined; - const pX = el.clientWidth / 2, - pY = el.clientHeight / 2; - return { - x: (el.scrollLeft + pX) / (el.scrollWidth || 1), - y: (el.scrollTop + pY) / (el.scrollHeight || 1), - pX, - pY, - }; - } - - private async reRenderPages(frac?: { - x: number; - y: number; - pX: number; - pY: number; - }): Promise { - if (!this.pdfDoc || !this.scrollAreaEl) return; - this._renderGen++; - const scrollEl = this.scrollAreaEl; - - this.renderObserver?.disconnect(); - this.renderObserver = null; - this.pageObserver?.disconnect(); - this.pageObserver = null; - - const sizes = await Promise.all( - this.pages.map(async (ctx) => { - const page = await this.pdfDoc!.getPage(ctx.pageNum); - const vp = page.getViewport({ scale: this.currentScale }); - return { w: Math.ceil(vp.width), h: Math.ceil(vp.height) }; - }), - ); - - for (let i = 0; i < this.pages.length; i++) { - const ctx = this.pages[i]!; - const { w, h } = sizes[i]!; - ctx.w = w; - ctx.h = h; - ctx.container.style.cssText = `width:${w}px;height:${h}px;min-width:${w}px;min-height:${h}px`; - this.unloadPageCanvas(ctx); - } - - if (frac) { - scrollEl.scrollLeft = frac.x * scrollEl.scrollWidth - frac.pX; - scrollEl.scrollTop = frac.y * scrollEl.scrollHeight - frac.pY; - } - - this.attachRenderObserver(); - this.attachPageObserver(); - } - - // Page indicator --------------------------------------------------------- - - private attachPageObserver(): void { - this.pageObserver?.disconnect(); - if (!this.scrollAreaEl || this.pages.length === 0) return; - - const total = this.pdfDoc!.numPages; - const pageMap = new Map( - this.pages.map((p) => [p.container, p.pageNum]), - ); - const visibleRatio = new Map(); - - this.pageObserver = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - const num = pageMap.get(entry.target); - if (num !== undefined) - visibleRatio.set(num, entry.intersectionRatio); - } - let bestPage = 1, - bestRatio = -1; - for (const [num, ratio] of visibleRatio) { - if (ratio > bestRatio) { - bestRatio = ratio; - bestPage = num; - } - } - if (this.pageIndicatorEl) - this.pageIndicatorEl.textContent = `${bestPage} / ${total}`; - }, - { - root: this.scrollAreaEl, - threshold: Array.from({ length: 11 }, (_, i) => i / 10), - }, - ); - - for (const ctx of this.pages) this.pageObserver.observe(ctx.container); - if (this.pageIndicatorEl) - this.pageIndicatorEl.textContent = `1 / ${total}`; - } - - private updateCanvasInteraction(): void { - for (const ctx of this.pages) { - if (!ctx.annotCanvas) continue; - const drawing = - this.currentTool !== "none" && this.currentTool !== "note"; - ctx.annotCanvas.style.pointerEvents = drawing ? "auto" : "none"; - ctx.annotCanvas.style.cursor = drawing ? "crosshair" : "default"; - ctx.container.style.cursor = - this.currentTool === "note" ? "text" : ""; - } - } - - private setTool(tool: AnnotTool): void { - this.hideColorPopover(); - this.currentTool = tool; - this.containerEl - .querySelectorAll(".via-pdf-toolbar .via-tool-btn") - .forEach((b) => - b.classList.toggle( - "is-active", - (b as HTMLElement).dataset.tool === tool, - ), - ); - this.updateCanvasInteraction(); - const showColors = tool === "pen" || tool === "highlighter"; - if (this.colorDotBtnEl) { - this.colorDotBtnEl.style.display = showColors ? "" : "none"; - if (showColors) { - const color = - tool === "pen" - ? this.plugin.settings.penColor - : this.plugin.settings.highlighterColor; - this.colorDotBtnEl.style.background = color; - } - } - } - - // Color dot & popover ---------------------------------------------------- - - private toggleColorPopover(e: MouseEvent): void { - if (this.colorPopoverEl) { - this.hideColorPopover(); - return; - } - e.stopPropagation(); - this.showColorPopover(e.currentTarget as HTMLElement); - } - - private showColorPopover(anchor: HTMLElement): void { - const tool = this.currentTool as "pen" | "highlighter"; - if (tool !== "pen" && tool !== "highlighter") return; - - const popover = document.createElement("div"); - popover.className = "via-color-popover"; - this.colorPopoverEl = popover; - - // Swatch row - const swatchRow = popover.createEl("div", { - cls: "via-color-popover-swatches", - }); - const presets = - tool === "pen" ? this.PEN_PRESETS : this.HIGHLIGHT_PRESETS; - const activeColor = ( - tool === "pen" - ? this.plugin.settings.penColor - : this.plugin.settings.highlighterColor - ).toLowerCase(); - - for (const color of presets) { - const sw = swatchRow.createEl("button", { - cls: "via-color-swatch", - }); - sw.style.background = color; - sw.dataset.color = color; - sw.classList.toggle( - "via-color-swatch-active", - color.toLowerCase() === activeColor, - ); - setTooltip(sw, color); - sw.addEventListener("click", () => { - this.applyColor(color); - this.hideColorPopover(); - }); - } - - const customLabel = swatchRow.createEl("label", { - cls: "via-color-swatch via-color-custom", - }); - setTooltip(customLabel as unknown as HTMLElement, "Custom colour"); - const customInput = customLabel.createEl("input"); - customInput.type = "color"; - customInput.className = "via-color-custom-input"; - customInput.value = activeColor; - const isCustom = !presets.some((c) => c.toLowerCase() === activeColor); - customLabel.classList.toggle("via-color-swatch-active", isCustom); - - customInput.addEventListener("input", () => { - const c = customInput.value.toLowerCase(); - swatchRow - .querySelectorAll(".via-color-swatch") - .forEach((s) => - s.classList.toggle( - "via-color-swatch-active", - s.dataset.color?.toLowerCase() === c, - ), - ); - customLabel.classList.toggle( - "via-color-swatch-active", - !presets.some((p) => p.toLowerCase() === c), - ); - }); - customInput.addEventListener("change", () => { - this.applyColor(customInput.value); - this.hideColorPopover(); - }); - - popover.createEl("hr", { cls: "via-color-popover-sep" }); - - // Size slider - const sizeRow = popover.createEl("div", { - cls: "via-popover-slider-row", - }); - sizeRow.createEl("span", { - cls: "via-popover-slider-label", - text: "Size", - }); - const sizeSlider = sizeRow.createEl("input"); - sizeSlider.type = "range"; - sizeSlider.className = "via-popover-slider"; - if (tool === "pen") { - sizeSlider.min = "1"; - sizeSlider.max = "20"; - sizeSlider.step = "1"; - sizeSlider.value = String(this.plugin.settings.penWidth); - } else { - sizeSlider.min = "10"; - sizeSlider.max = "40"; - sizeSlider.step = "2"; - sizeSlider.value = String(this.plugin.settings.highlighterWidth); - } - const sizeLabel = sizeRow.createEl("span", { - cls: "via-popover-slider-value", - text: `${sizeSlider.value}px`, - }); - sizeSlider.addEventListener( - "input", - () => (sizeLabel.textContent = `${sizeSlider.value}px`), - ); - sizeSlider.addEventListener("change", () => - this.applyWidth(Number(sizeSlider.value)), - ); - - // Opacity slider (highlighter only) - if (tool === "highlighter") { - const opRow = popover.createEl("div", { - cls: "via-popover-slider-row", - }); - opRow.createEl("span", { - cls: "via-popover-slider-label", - text: "Opacity", - }); - const opSlider = opRow.createEl("input"); - opSlider.type = "range"; - opSlider.min = "0.1"; - opSlider.max = "1.0"; - opSlider.step = "0.05"; - opSlider.className = "via-popover-slider"; - opSlider.value = String(this.plugin.settings.highlighterOpacity); - const opLabel = opRow.createEl("span", { - cls: "via-popover-slider-value", - text: `${Math.round(this.plugin.settings.highlighterOpacity * 100)}%`, - }); - opSlider.addEventListener( - "input", - () => - (opLabel.textContent = `${Math.round(Number(opSlider.value) * 100)}%`), - ); - opSlider.addEventListener("change", () => - this.applyOpacity(Number(opSlider.value)), - ); - } - - // Append to body and position below anchor - document.body.appendChild(popover); - const rect = anchor.getBoundingClientRect(); - popover.style.top = `${rect.bottom + 6}px`; - popover.style.left = `${Math.max(4, rect.left)}px`; - - // Dismiss on outside click - const dismiss = (ev: MouseEvent) => { - if (!popover.contains(ev.target as Node)) { - this.hideColorPopover(); - document.removeEventListener("mousedown", dismiss, true); - } - }; - setTimeout( - () => document.addEventListener("mousedown", dismiss, true), - 0, - ); - } - - private hideColorPopover(): void { - this.colorPopoverEl?.remove(); - this.colorPopoverEl = null; - } - - private applyColor(color: string): void { - const tool = this.currentTool; - if (tool !== "pen" && tool !== "highlighter") return; - if (tool === "pen") this.plugin.settings.penColor = color; - else this.plugin.settings.highlighterColor = color; - void this.plugin.saveSettings(); - if (this.colorDotBtnEl) this.colorDotBtnEl.style.background = color; - } - - // Width / opacity --------------------------------------------------------- - - private applyWidth(value: number): void { - const tool = this.currentTool; - if (tool !== "pen" && tool !== "highlighter") return; - if (tool === "pen") this.plugin.settings.penWidth = value; - else this.plugin.settings.highlighterWidth = value; - void this.plugin.saveSettings(); - } - - private applyOpacity(value: number): void { - this.plugin.settings.highlighterOpacity = value; - void this.plugin.saveSettings(); - } - - // Snap ------------------------------------------------------------------- - - private updateSnapDirBtn(): void { - if (!this.snapDirBtnEl) return; - const iconMap: Record = { - horizontal: "arrow-left-right", - vertical: "arrow-up-down", - slope: "arrow-up-right", - }; - const labelMap: Record = { - horizontal: "H", - vertical: "V", - slope: "45°", - }; - const { snapActivateKey, keySnapCycle } = this.plugin.settings; - this.snapDirBtnEl.empty(); - const iconSpan = this.snapDirBtnEl.createEl("span", { - cls: "via-snap-icon", - }); - setIcon(iconSpan, iconMap[this.snapDirection]); - this.snapDirBtnEl.createSpan({ - cls: "via-snap-label", - text: labelMap[this.snapDirection], - }); - setTooltip( - this.snapDirBtnEl, - `Snap: ${this.snapDirection} — hold ${snapActivateKey} to activate · ${snapActivateKey}+${keySnapCycle.toUpperCase()} to cycle`, - ); - } - - // Page jump -------------------------------------------------------------- - - private openPageJumpInput(): void { - if (!this.pdfDoc || !this.pageIndicatorEl) return; - const total = this.pdfDoc.numPages; - const currentPage = this.getVisiblePageNum(); - const indicator = this.pageIndicatorEl; - - const input = document.createElement("input"); - input.type = "number"; - input.min = "1"; - input.max = String(total); - input.value = String(currentPage); - input.className = "via-pdf-page-jump-input"; - - indicator.parentElement!.insertBefore(input, indicator); - indicator.classList.add("via-hidden"); - input.focus(); - input.select(); - - const cleanup = () => { - input.remove(); - indicator.classList.remove("via-hidden"); - }; - const commit = () => { - const val = parseInt(input.value, 10); - if (!isNaN(val)) - this.scrollToPage(Math.max(1, Math.min(total, val))); - cleanup(); - }; - - input.addEventListener("keydown", (e) => { - e.stopPropagation(); - if (e.key === "Enter") { - e.preventDefault(); - commit(); - } - if (e.key === "Escape") { - e.preventDefault(); - cleanup(); - } - }); - input.addEventListener("blur", cleanup); - } - - private scrollToPage(pageNum: number): void { - const ctx = this.pages.find((p) => p.pageNum === pageNum); - if (ctx) - ctx.container.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - } - - // Drawing ---------------------------------------------------------------- - - private attachDrawListeners(ctx: PageCtx): void { - const annotCanvas = ctx.annotCanvas; - if (!annotCanvas) return; - const { pageNum } = ctx; - - const getPos = (e: MouseEvent | PointerEvent) => { - const rect = annotCanvas.getBoundingClientRect(); - return { - x: (e.clientX - rect.left) / rect.width, - y: (e.clientY - rect.top) / rect.height, - }; - }; - - const isSnapActive = (e: PointerEvent) => { - const key = this.plugin.settings.snapActivateKey; - return key === "Alt" ? e.altKey : e.shiftKey; - }; - - annotCanvas.addEventListener("pointerdown", (e) => { - if (this.currentTool === "none") return; - annotCanvas.setPointerCapture(e.pointerId); - this.isDrawing = true; - this.currentPath = { - tool: - this.currentTool === "pen" - ? "pen" - : this.currentTool === "eraser" - ? "eraser" - : "highlighter", - color: - this.currentTool === "pen" - ? this.plugin.settings.penColor - : this.currentTool === "highlighter" - ? this.plugin.settings.highlighterColor - : "#ffffff", - width: - this.currentTool === "pen" - ? this.plugin.settings.penWidth - : this.currentTool === "highlighter" - ? this.plugin.settings.highlighterWidth - : this.plugin.settings.eraserWidth, - opacity: - this.currentTool === "highlighter" - ? this.plugin.settings.highlighterOpacity - : 1, - points: [getPos(e)], - }; - }); - - annotCanvas.addEventListener("pointermove", (e) => { - if (!this.isDrawing || !this.currentPath) return; - const raw = getPos(e); - if (isSnapActive(e) && this.currentPath.points.length >= 1) { - const origin = this.currentPath.points[0]!; - const snapped = snapPoint(origin, raw, this.snapDirection); - // Replace trailing point to keep a clean constrained stroke - if (this.currentPath.points.length > 1) { - this.currentPath.points[ - this.currentPath.points.length - 1 - ] = snapped; - } else { - this.currentPath.points.push(snapped); - } - this.snapDirBtnEl?.classList.add("via-btn-snap-active"); - } else { - this.currentPath.points.push(raw); - if (!isSnapActive(e)) - this.snapDirBtnEl?.classList.remove("via-btn-snap-active"); - } - this.redrawAnnotations(ctx, this.currentPath); - }); - - const finishDraw = () => { - if (!this.isDrawing || !this.currentPath) return; - this.isDrawing = false; - this.snapDirBtnEl?.classList.remove("via-btn-snap-active"); - let pa = getPageAnnotations(this.annotData, pageNum); - pa = { ...pa, paths: [...pa.paths, this.currentPath] }; - this.annotData = setPageAnnotations(this.annotData, pa); - this.currentPath = null; - this.redrawAnnotations(ctx); - }; - - annotCanvas.addEventListener("pointerup", finishDraw); - annotCanvas.addEventListener("pointercancel", finishDraw); - - // Note tool: click on page container to place a note - ctx.container.addEventListener("click", (e: MouseEvent) => { - if (this.currentTool !== "note") return; - if ((e.target as HTMLElement).closest(".via-pdf-note")) return; - const rect = ctx.container.getBoundingClientRect(); - const x = (e.clientX - rect.left) / rect.width; - const y = (e.clientY - rect.top) / rect.height; - this.createNote(pageNum, x, y); - }); - } - - // Annotations ------------------------------------------------------------ - - private redrawAnnotations( - ctx: PageCtx, - inProgressPath?: AnnotationPath, - ): void { - if (!ctx.annotCanvas) return; - const canvas = ctx.annotCanvas; - const c = canvas.getContext("2d")!; - c.clearRect(0, 0, canvas.width, canvas.height); - - const drawPath = (path: AnnotationPath) => { - if (path.points.length < 2) return; - c.save(); - if (path.tool === "highlighter") { - c.globalAlpha = - path.opacity ?? this.plugin.settings.highlighterOpacity; - c.globalCompositeOperation = "multiply"; - } else if (path.tool === "eraser") { - c.globalCompositeOperation = "destination-out"; - c.globalAlpha = 1; - } else { - c.globalAlpha = 1; - c.globalCompositeOperation = "source-over"; - } - c.strokeStyle = path.color; - c.lineWidth = path.width * this.currentScale; - c.lineCap = "round"; - c.lineJoin = "round"; - c.beginPath(); - c.moveTo( - path.points[0]!.x * canvas.width, - path.points[0]!.y * canvas.height, - ); - for (let i = 1; i < path.points.length; i++) { - c.lineTo( - path.points[i]!.x * canvas.width, - path.points[i]!.y * canvas.height, - ); - } - c.stroke(); - c.restore(); - }; - - const pa: PageAnnotations = getPageAnnotations( - this.annotData, - ctx.pageNum, - ); - for (const path of pa.paths) drawPath(path); - if (inProgressPath) drawPath(inProgressPath); - } - - // Persistence ------------------------------------------------------------ - - private clearCurrentPageAnnotations(): void { - const visiblePage = this.getVisiblePageNum(); - this.annotData = setPageAnnotations(this.annotData, { - page: visiblePage, - paths: [], - }); - const ctx = this.pages.find((p) => p.pageNum === visiblePage); - if (ctx) this.redrawAnnotations(ctx); - } - - private getVisiblePageNum(): number { - let best = 1, - bestVisible = -Infinity; - for (const ctx of this.pages) { - const rect = ctx.container.getBoundingClientRect(); - const visible = - Math.min(rect.bottom, window.innerHeight) - - Math.max(rect.top, 0); - if (visible > bestVisible) { - bestVisible = visible; - best = ctx.pageNum; - } - } - return best; - } - - private async persistAnnotations(): Promise { - if (!this.currentFile) return; - try { - await saveAnnotations(this.app, this.currentFile, this.annotData); - new Notice("Annotations saved"); - } catch (err) { - new Notice(`Failed to save annotations: ${String(err)}`); - } - } - - // TOC / Outline ---------------------------------------------------------- - - private async loadToc(): Promise { - if (!this.pdfDoc) return; - try { - const outline = await this.pdfDoc.getOutline(); - if (!outline || outline.length === 0) return; - this._outline = outline; - } catch { - // Some PDFs throw on getOutline — ignore - } - } - - private toggleToc(): void { - if (!this.bodyEl) return; - this.tocVisible = !this.tocVisible; - - if (!this.tocVisible) { - this.tocSidebarEl?.remove(); - this.tocSidebarEl = null; - return; - } - - const sidebar = this.bodyEl.createEl("div", { cls: "via-pdf-toc" }); - this.tocSidebarEl = sidebar; - this.bodyEl.insertBefore(sidebar, this.scrollAreaEl); - - const header = sidebar.createEl("div", { cls: "via-pdf-toc-header" }); - header.createEl("span", { text: "Contents" }); - const closeBtn = header.createEl("div", { cls: "clickable-icon" }); - setIcon(closeBtn, "x"); - setTooltip(closeBtn, "Close table of contents"); - closeBtn.addEventListener("click", () => { - this.tocVisible = false; - sidebar.remove(); - this.tocSidebarEl = null; - }); - - const list = sidebar.createEl("div", { cls: "via-pdf-toc-list" }); - const outline = this._outline; - - if (!outline || outline.length === 0) { - list.createEl("p", { - cls: "via-pdf-toc-empty", - text: "No outline available for this PDF.", - }); - return; - } - - const renderItems = ( - items: typeof outline, - parentEl: HTMLElement, - depth = 0, - ) => { - for (const item of items) { - const row = parentEl.createEl("div", { - cls: "via-pdf-toc-item", - }); - row.style.paddingLeft = `${8 + depth * 14}px`; - - if (item.items && item.items.length > 0) { - const toggle = row.createEl("span", { - cls: "via-pdf-toc-toggle", - text: "▾", - }); - let collapsed = false; - const childList = parentEl.createEl("div"); - renderItems(item.items, childList, depth + 1); - - toggle.addEventListener("click", (e) => { - e.stopPropagation(); - collapsed = !collapsed; - childList.classList.toggle("via-hidden", collapsed); - toggle.textContent = collapsed ? "▸" : "▾"; - }); - } - - const label = row.createEl("span", { - cls: "via-pdf-toc-label", - text: item.title ?? "(untitled)", - }); - label.addEventListener("click", () => { - if (!this.pdfDoc) return; - const pdfDoc = this.pdfDoc; - void (async () => { - try { - let dest = item.dest; - if (typeof dest === "string") - dest = await pdfDoc.getDestination(dest); - if (!Array.isArray(dest) || dest.length === 0) return; - const pageIdx = await pdfDoc.getPageIndex( - dest[0] as PdfRefProxy, - ); - this.scrollToPage(pageIdx + 1); - } catch { - // Destination lookup failed — silently ignore - } - })(); - }); - } - }; - - renderItems(outline, list); - } - - // Text notes ------------------------------------------------------------- - - private createNote(pageNum: number, x: number, y: number): void { - const note: TextNote = { - id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, - page: pageNum, - x, - y, - text: "", - color: this.plugin.settings.noteDefaultColor, - }; - this.annotData = { - ...this.annotData, - notes: [...(this.annotData.notes ?? []), note], - }; - this.renderNoteEl(note, true); - } - - private renderNoteEl(note: TextNote, focusImmediately = false): void { - const ctx = this.pages.find((p) => p.pageNum === note.page); - if (!ctx) return; - - const noteColor = note.color ?? this.plugin.settings.noteDefaultColor; - const el = ctx.container.createEl("div", { cls: "via-pdf-note" }); - el.style.left = `${note.x * 100}%`; - el.style.top = `${note.y * 100}%`; - el.style.setProperty("--note-color", noteColor); - el.dataset.noteId = note.id; - - const header = el.createEl("div", { cls: "via-pdf-note-header" }); - - // Drag handle - const dragHandle = header.createEl("span", { - cls: "via-pdf-note-drag", - }); - setIcon(dragHandle, "grip-vertical"); - setTooltip(dragHandle, "Drag note"); - dragHandle.addEventListener("mousedown", (e) => { - e.preventDefault(); - const startX = e.clientX, - startY = e.clientY; - const origLeft = note.x, - origTop = note.y; - const rect = ctx.container.getBoundingClientRect(); - - const onMove = (me: MouseEvent) => { - const dx = (me.clientX - startX) / rect.width; - const dy = (me.clientY - startY) / rect.height; - note.x = Math.max(0, Math.min(0.9, origLeft + dx)); - note.y = Math.max(0, Math.min(0.95, origTop + dy)); - el.style.left = `${note.x * 100}%`; - el.style.top = `${note.y * 100}%`; - }; - const onUp = () => { - window.removeEventListener("mousemove", onMove); - window.removeEventListener("mouseup", onUp); - }; - window.addEventListener("mousemove", onMove); - window.addEventListener("mouseup", onUp); - }); - - // Color dot (center of header — visual identity at a glance) - header.createEl("span", { cls: "via-pdf-note-color-dot" }); - - const deleteBtn = header.createEl("button", { - cls: "via-pdf-note-delete", - }); - setIcon(deleteBtn, "x"); - setTooltip(deleteBtn, "Delete note"); - deleteBtn.addEventListener("click", () => { - this.annotData = { - ...this.annotData, - notes: (this.annotData.notes ?? []).filter( - (n) => n.id !== note.id, - ), - }; - el.remove(); - this.noteEls.delete(note.id); - }); - - const textarea = el.createEl("textarea", { cls: "via-pdf-note-text" }); - textarea.value = note.text; - textarea.placeholder = "Note…"; - textarea.addEventListener("input", () => { - note.text = textarea.value; - }); - textarea.addEventListener("keydown", (e) => e.stopPropagation()); - - this.noteEls.set(note.id, el); - if (focusImmediately) textarea.focus(); - } -} diff --git a/src/views/PptxView.ts b/src/views/PptxView.ts deleted file mode 100644 index 28a5b22..0000000 --- a/src/views/PptxView.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { FileView, TFile, WorkspaceLeaf } from "obsidian"; -import { PPTXViewer } from "pptxviewjs"; -import { VIEW_TYPE_PPTX } from "../types"; -import ViewItAllPlugin from "../main"; - -// Internal pptxviewjs types for patching theme colors -interface PptxColor { - scheme?: string; - tint?: number; - shade?: number; - lumMod?: number; - lumOff?: number; -} - -interface PptxDrawingDocument { - parseColorToHex: (color: unknown) => string; - applyColorModifications?: (hex: string, color: unknown) => string; -} - -interface PptxSlide { - theme?: Record; - layout?: { master?: { theme?: Record } }; -} - -interface PptxRenderer { - presentation?: { - theme?: { colors?: Record }; - }; - slides?: PptxSlide[]; - drawingDocument?: PptxDrawingDocument; - getSlideDimensions?: () => { cx: number; cy: number } | undefined; -} - -interface PptxProcessor { - processor?: PptxRenderer; - getSlideDimensions?: () => { cx: number; cy: number } | undefined; -} - -export class PptxView extends FileView { - private plugin: ViewItAllPlugin; - private viewer: PPTXViewer | null = null; - private canvasEl: HTMLCanvasElement | null = null; - private canvasWrapper: HTMLElement | null = null; - private statusEl: HTMLElement | null = null; - private zoomLabel: HTMLElement | null = null; - private zoom = 1.0; - private slideWidthPx = 960; - private slideHeightPx = 720; - private resizeObserver: ResizeObserver | null = null; - private rendering = false; - - constructor(leaf: WorkspaceLeaf, plugin: ViewItAllPlugin) { - super(leaf); - this.plugin = plugin; - } - - getViewType(): string { - return VIEW_TYPE_PPTX; - } - - getDisplayText(): string { - return this.file ? this.file.basename : "PPTX"; - } - - getIcon(): string { - return "presentation"; - } - - async onLoadFile(file: TFile): Promise { - this.contentEl.empty(); - this.buildUI(); - await this.loadPresentation(file); - } - - private buildUI() { - this.contentEl.classList.add("via-pptx-wrapper"); - - const toolbar = this.contentEl.createDiv("via-pptx-toolbar"); - - const prevBtn = toolbar.createEl("button", { text: "◀ previous" }); - prevBtn.addEventListener("click", () => { void this.prevSlide(); }); - - const nextBtn = toolbar.createEl("button", { text: "Next ▶" }); - nextBtn.addEventListener("click", () => { void this.nextSlide(); }); - - toolbar.createEl("span", { - text: "|", - cls: "via-separator-faint", - }); - - const zoomOutBtn = toolbar.createEl("button", { text: "−" }); - zoomOutBtn.addEventListener("click", () => { - void this.setZoom(this.zoom - 0.15); - }); - - this.zoomLabel = toolbar.createEl("span", { - text: "100%", - cls: "via-pptx-zoom-label", - }); - - const zoomInBtn = toolbar.createEl("button", { text: "+" }); - zoomInBtn.addEventListener("click", () => { - void this.setZoom(this.zoom + 0.15); - }); - - const fitBtn = toolbar.createEl("button", { text: "Fit" }); - fitBtn.addEventListener("click", () => { void this.fitToContainer(); }); - - toolbar.createEl("span", { - text: "|", - cls: "via-separator-faint", - }); - - this.statusEl = toolbar.createEl("span", { cls: "via-pptx-status" }); - this.statusEl.setText("Loading..."); - - this.canvasWrapper = this.contentEl.createDiv( - "via-pptx-canvas-wrapper", - ); - - this.canvasEl = this.canvasWrapper.createEl("canvas", { - cls: "via-pptx-canvas", - }); - } - - private async loadPresentation(file: TFile) { - const buffer = await this.app.vault.readBinary(file); - - this.viewer?.destroy(); - this.viewer = new PPTXViewer({ - canvas: this.canvasEl, - slideSizeMode: "fit", - autoChartRerenderDelayMs: 400, - }); - - this.viewer.on("slideChanged", () => this.updateStatus()); - - try { - await this.viewer.loadFile(buffer); - this.patchThemeColors(); - this.readSlideDimensions(); - await this.renderCurrentSlide(); - this.updateStatus(); - this.setupResizeObserver(); - } catch (e) { - console.error("PptxView: failed to render PPTX", e); - this.statusEl?.setText("Failed to render presentation"); - } - } - - /** - * Patch pptxviewjs internals to use real theme colors from the PPTX file - * instead of hardcoded fallback scheme colors. - */ - private patchThemeColors() { - try { - const v = this.viewer as unknown as { processor?: PptxProcessor }; - const outerProc = v?.processor; - const renderer = outerProc?.processor; - if (!renderer) return; - - const presentation = renderer.presentation; - if (!presentation) return; - const themeColors: Record | undefined = - presentation.theme?.colors; - if (!themeColors) return; - - // 1. Propagate theme onto every slide so bgRef / currentSlide.theme works - if (Array.isArray(renderer.slides)) { - for (const slide of renderer.slides) { - if (!slide.theme) slide.theme = presentation.theme; - if (slide.layout?.master && !slide.layout.master.theme) { - slide.layout.master.theme = presentation.theme; - } - } - } - - // 2. Patch the DrawingDocument's parseColorToHex to resolve scheme - // colors from the real theme instead of hardcoded defaults. - const drawDoc = renderer.drawingDocument; - if (!drawDoc) return; - - const origParse = drawDoc.parseColorToHex.bind(drawDoc); - drawDoc.parseColorToHex = (color: unknown): string => { - if (!color) return "#ffffff"; - if (typeof color === "string") { - return color.startsWith("#") ? color : `#${color}`; - } - const c = color as PptxColor; - if (c.scheme && themeColors[c.scheme]) { - let hex = themeColors[c.scheme]; - if (typeof hex === "string") { - if (!hex.startsWith("#")) hex = `#${hex}`; - // Apply tint / shade / lumMod / lumOff modifications - if ( - c.tint !== undefined || - c.shade !== undefined || - c.lumMod !== undefined || - c.lumOff !== undefined - ) { - if ( - typeof drawDoc.applyColorModifications === - "function" - ) { - return drawDoc.applyColorModifications( - hex, - color, - ); - } - } - return hex; - } - } - return origParse(color); - }; - } catch { - // non-critical — rendering will still work with hardcoded fallbacks - } - } - - private readSlideDimensions() { - try { - const proc = (this.viewer as unknown as { processor?: PptxProcessor })?.processor; - const dims = - proc?.getSlideDimensions?.() ?? - proc?.processor?.getSlideDimensions?.(); - if (dims && dims.cx && dims.cy) { - this.slideWidthPx = (dims.cx / 914400) * 96; - this.slideHeightPx = (dims.cy / 914400) * 96; - } - } catch { - // keep defaults - } - } - - private setupResizeObserver() { - this.resizeObserver?.disconnect(); - if (!this.canvasWrapper) return; - - let timeout: ReturnType; - this.resizeObserver = new ResizeObserver(() => { - clearTimeout(timeout); - timeout = setTimeout(() => { void this.renderCurrentSlide(); }, 150); - }); - this.resizeObserver.observe(this.canvasWrapper); - } - - private async renderCurrentSlide() { - if ( - !this.viewer || - !this.canvasEl || - !this.canvasWrapper || - this.rendering - ) - return; - this.rendering = true; - - try { - const wrapperRect = this.canvasWrapper.getBoundingClientRect(); - const availW = wrapperRect.width - 32; - const availH = wrapperRect.height - 32; - if (availW <= 0 || availH <= 0) return; - - const fitScale = Math.min( - availW / this.slideWidthPx, - availH / this.slideHeightPx, - ); - const scale = fitScale * this.zoom; - - const cssW = Math.round(this.slideWidthPx * scale); - const cssH = Math.round(this.slideHeightPx * scale); - - // Set only CSS dimensions — the library handles canvas.width/height - // and DPR scaling internally via its init() method - this.canvasEl.setCssProps({ - "--via-canvas-width": `${cssW}px`, - "--via-canvas-height": `${cssH}px`, - }); - - await this.viewer.render(this.canvasEl); - } finally { - this.rendering = false; - } - } - - private async fitToContainer() { - this.zoom = 1.0; - this.updateZoomLabel(); - await this.renderCurrentSlide(); - } - - private updateStatus() { - if (!this.viewer || !this.statusEl) return; - const current = this.viewer.getCurrentSlideIndex() + 1; - const total = this.viewer.getSlideCount(); - this.statusEl.setText(`Slide ${current} / ${total}`); - } - - private updateZoomLabel() { - if (this.zoomLabel) { - this.zoomLabel.setText(`${Math.round(this.zoom * 100)}%`); - } - } - - private async prevSlide() { - if (!this.viewer || this.rendering) return; - await this.viewer.previousSlide(this.canvasEl); - await this.renderCurrentSlide(); - this.updateStatus(); - } - - private async nextSlide() { - if (!this.viewer || this.rendering) return; - await this.viewer.nextSlide(this.canvasEl); - await this.renderCurrentSlide(); - this.updateStatus(); - } - - private async setZoom(newZoom: number) { - this.zoom = Math.max(0.25, Math.min(newZoom, 4.0)); - this.updateZoomLabel(); - await this.renderCurrentSlide(); - } - - // No async work needed — returns resolved promise for type compatibility - protected onClose(): Promise { - this.resizeObserver?.disconnect(); - this.resizeObserver = null; - this.viewer?.destroy(); - this.viewer = null; - this.canvasEl = null; - this.canvasWrapper = null; - this.statusEl = null; - this.zoomLabel = null; - this.contentEl.empty(); - return Promise.resolve(); - } -} diff --git a/src/views/SpreadsheetView.ts b/src/views/SpreadsheetView.ts deleted file mode 100644 index ea7731a..0000000 --- a/src/views/SpreadsheetView.ts +++ /dev/null @@ -1,1078 +0,0 @@ -import { - FileView, - Notice, - TFile, - WorkspaceLeaf, - Menu, - Modal, - App, - setIcon, - setTooltip, -} from "obsidian"; -import { VIEW_TYPE_SPREADSHEET } from "../types"; -import { evaluateSheet } from "../utils/formulaEval"; -import type ViewItAllPlugin from "../main"; - -// ── SheetJS type surface ────────────────────────────────────────────────────── - -type CellValue = string | number | boolean | null | undefined; - -interface XLSXCell { - v?: CellValue; // primitive value - f?: string; // formula (without leading '=') - t?: string; // type: 'n' | 's' | 'b' | 'e' | 'z' - w?: string; // formatted text -} - -/** - * Opaque sheet record: cell refs like "A1" map to XLSXCell, - * and special keys "!ref", "!cols", etc. map to metadata. - */ -type XLSXSheet = Record; - -interface XLSXWorkBook { - SheetNames: string[]; - Sheets: Record; -} - -interface SheetRange { - s: { r: number; c: number }; - e: { r: number; c: number }; -} - -interface XLSXModule { - read(data: Uint8Array | ArrayBuffer, opts: { type: "array" }): XLSXWorkBook; - write(wb: XLSXWorkBook, opts: { type: string; bookType: string }): unknown; - utils: { - encode_cell(addr: { r: number; c: number }): string; - decode_cell(addr: string): { r: number; c: number }; - encode_range(range: SheetRange): string; - decode_range(range: string): SheetRange; - }; -} - -// ── Sheet helpers ───────────────────────────────────────────────────────────── - -function sheetGetRef(sheet: XLSXSheet): string | undefined { - return sheet["!ref"] as string | undefined; -} - -function sheetSetRef(sheet: XLSXSheet, ref: string): void { - (sheet as Record)["!ref"] = ref; -} - -function sheetGetCell(sheet: XLSXSheet, ref: string): XLSXCell | undefined { - return sheet[ref] as XLSXCell | undefined; -} - -function sheetSetCell(sheet: XLSXSheet, ref: string, cell: XLSXCell): void { - (sheet as Record)[ref] = cell; -} - -// ── View ────────────────────────────────────────────────────────────────────── - -export class SpreadsheetView extends FileView { - private plugin: ViewItAllPlugin; - private currentFile: TFile | null = null; - private workbook: XLSXWorkBook | null = null; - private xlsx: XLSXModule | null = null; - private activeSheet = 0; - private isDirty = false; - private editMode = false; - private savedData: ArrayBuffer | null = null; - - // Selection - private selRow = -1; - private selCol = -1; - - // DOM refs - private wrapper: HTMLElement | null = null; - private tabBar: HTMLElement | null = null; - private tableContainer: HTMLElement | null = null; - private formulaRef: HTMLElement | null = null; - private formulaInput: HTMLInputElement | null = null; - private saveBtn: HTMLElement | null = null; - private undoBtn: HTMLElement | null = null; - private editToggleBtn: HTMLElement | null = null; - private dirtyIndicator: HTMLElement | null = null; - private addRowBtn: HTMLElement | null = null; - private addColBtn: HTMLElement | null = null; - private infoEl: HTMLElement | null = null; - - constructor(leaf: WorkspaceLeaf, plugin: ViewItAllPlugin) { - super(leaf); - this.plugin = plugin; - } - - getViewType(): string { - return VIEW_TYPE_SPREADSHEET; - } - getDisplayText(): string { - return this.file?.basename ?? "Spreadsheet"; - } - getIcon(): string { - return "table"; - } - - canAcceptExtension(extension: string): boolean { - if (extension === "xlsx") return this.plugin.settings.enableXlsx; - if (extension === "csv") return this.plugin.settings.enableCsv; - return false; - } - - async onLoadFile(file: TFile): Promise { - this.currentFile = file; - this.activeSheet = 0; - this.isDirty = false; - this.selRow = -1; - this.selCol = -1; - - if (!this.xlsx) { - this.xlsx = (await import("xlsx")) as unknown as XLSXModule; - } - await this.renderFile(file); - } - - // No async work needed — returns resolved promise for type compatibility - onUnloadFile(_file: TFile): Promise { - this.contentEl.empty(); - this.workbook = null; - this.savedData = null; - this.wrapper = null; - this.tabBar = null; - this.tableContainer = null; - this.formulaRef = null; - this.formulaInput = null; - this.saveBtn = null; - this.undoBtn = null; - this.editToggleBtn = null; - this.dirtyIndicator = null; - this.addRowBtn = null; - this.addColBtn = null; - this.infoEl = null; - this.currentFile = null; - this.editMode = false; - return Promise.resolve(); - } - - // ── Render ──────────────────────────────────────────────────────────────── - - private async renderFile(file: TFile): Promise { - this.contentEl.empty(); - if (!this.xlsx) return; - - const isBottom = - this.plugin.settings.spreadsheetToolbarPosition === "bottom"; - const isCsv = file.extension === "csv"; - - // Read raw bytes - let data: ArrayBuffer; - try { - data = await this.app.vault.adapter.readBinary(file.path); - } catch (err) { - this.contentEl.createEl("p", { - cls: "via-error", - text: `Failed to read file: ${String(err)}`, - }); - return; - } - - // Keep a copy for undo/revert - this.savedData = data.slice(0); - - // Parse workbook - try { - this.workbook = this.xlsx.read(new Uint8Array(data), { - type: "array", - }); - } catch (err) { - this.contentEl.createEl("p", { - cls: "via-error", - text: `Failed to parse spreadsheet: ${String(err)}`, - }); - return; - } - - // Evaluate formulas on initial load - this.evaluateAllFormulas(); - - // Root wrapper - this.wrapper = this.contentEl.createEl("div", { - cls: "via-sheet-wrapper", - }); - if (isBottom) - this.wrapper.classList.add("via-sheet-wrapper--toolbar-bottom"); - - // ── Toolbar ─────────────────────────────────────────────────────────── - const toolbar = this.wrapper.createEl("div", { - cls: "via-sheet-toolbar", - }); - - // Edit / View toggle - this.editToggleBtn = toolbar.createEl("div", { cls: "clickable-icon" }); - setIcon(this.editToggleBtn, "pencil"); - setTooltip(this.editToggleBtn, "Switch to edit mode"); - this.editToggleBtn.addEventListener("click", () => - this.toggleEditMode(), - ); - - toolbar.createEl("div", { cls: "via-toolbar-sep" }); - - // Save button - this.saveBtn = toolbar.createEl("div", { - cls: "clickable-icon via-icon-save", - }); - setIcon(this.saveBtn, "save"); - setTooltip(this.saveBtn, "Save (Ctrl+S)"); - this.saveBtn.classList.add("via-hidden"); - this.saveBtn.addEventListener("click", () => { void this.saveFile(); }); - - // Undo (revert to last save) - this.undoBtn = toolbar.createEl("div", { cls: "clickable-icon" }); - setIcon(this.undoBtn, "undo-2"); - setTooltip(this.undoBtn, "Revert to last save"); - this.undoBtn.classList.add("via-hidden"); - this.undoBtn.addEventListener("click", () => this.revertToSaved()); - - // Dirty indicator (yellow dot) - this.dirtyIndicator = toolbar.createEl("div", { - cls: "via-docx-dirty-dot via-hidden", - }); - setTooltip(this.dirtyIndicator, "Unsaved changes"); - - toolbar.createEl("div", { cls: "via-toolbar-sep" }); - - // Add Row - this.addRowBtn = toolbar.createEl("div", { cls: "clickable-icon" }); - setIcon(this.addRowBtn, "plus-square"); - setTooltip(this.addRowBtn, "Add row at bottom"); - this.addRowBtn.classList.add("via-hidden"); - this.addRowBtn.addEventListener("click", () => this.addRow()); - - // Add Column - this.addColBtn = toolbar.createEl("div", { cls: "clickable-icon" }); - setIcon(this.addColBtn, "between-vertical-start"); - setTooltip(this.addColBtn, "Add column at right"); - this.addColBtn.classList.add("via-hidden"); - this.addColBtn.addEventListener("click", () => this.addColumn()); - - toolbar.createEl("div", { cls: "via-toolbar-sep" }); - - // File label - const fileLabel = toolbar.createEl("div", { - cls: "via-sheet-file-label", - }); - setIcon( - fileLabel.createEl("div", { cls: "clickable-icon" }), - isCsv ? "file-text" : "table", - ); - fileLabel.createEl("span", { - text: file.basename, - cls: "via-sheet-file-name", - }); - - toolbar.createEl("div", { cls: "via-toolbar-sep" }); - - // Sheet tabs (multi-sheet xlsx) - this.tabBar = toolbar.createEl("div", { cls: "via-sheet-tabs" }); - if (!isCsv && this.workbook.SheetNames.length > 1) { - this.renderTabs(); - } - - toolbar.createEl("div", { cls: "via-toolbar-spacer" }); - - // Row × Col info - this.infoEl = toolbar.createEl("div", { cls: "via-sheet-info" }); - this.updateInfo(); - - // ── Formula bar ─────────────────────────────────────────────────────── - const fbar = this.wrapper.createEl("div", { - cls: "via-sheet-formula-bar", - }); - this.formulaRef = fbar.createEl("div", { - cls: "via-sheet-formula-ref", - }); - fbar.createEl("div", { cls: "via-sheet-formula-sep" }); - this.formulaInput = fbar.createEl("input", { - cls: "via-sheet-formula-input", - type: "text", - placeholder: "Select a cell to edit…", - }); - this.formulaInput.addEventListener("keydown", (e: KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - this.commitFormulaBar(); - } - if (e.key === "Escape") { - e.preventDefault(); - this.refreshFormulaBar(); - this.formulaInput?.blur(); - } - }); - - // Ctrl/Cmd+S — save - this.registerDomEvent(this.contentEl, "keydown", (e: KeyboardEvent) => { - if ((e.ctrlKey || e.metaKey) && e.key === "s") { - e.preventDefault(); - void this.saveFile(); - } - }); - this.contentEl.tabIndex = 0; - - // ── Scroll + table ──────────────────────────────────────────────────── - const scrollEl = this.wrapper.createEl("div", { - cls: "via-sheet-scroll", - }); - this.tableContainer = scrollEl.createEl("div", { - cls: "via-sheet-table-wrap", - }); - this.renderSheet(); - } - - private renderTabs(): void { - if (!this.tabBar || !this.workbook) return; - this.tabBar.empty(); - for (let i = 0; i < this.workbook.SheetNames.length; i++) { - const name = this.workbook.SheetNames[i] ?? `Sheet ${i + 1}`; - const tab = this.tabBar.createEl("div", { - cls: "via-sheet-tab", - text: name, - }); - if (i === this.activeSheet) tab.classList.add("is-active"); - setTooltip(tab, name); - tab.addEventListener("click", () => { - if (i === this.activeSheet) return; - this.activeSheet = i; - this.selRow = -1; - this.selCol = -1; - this.renderTabs(); - this.renderSheet(); - this.updateInfo(); - this.refreshFormulaBar(); - }); - } - } - - private renderSheet(): void { - if (!this.tableContainer || !this.workbook || !this.xlsx) return; - this.tableContainer.empty(); - - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) { - this.tableContainer.createEl("p", { - cls: "via-sheet-empty", - text: "This sheet is empty.", - }); - return; - } - - const ref = sheetGetRef(sheet); - if (!ref) { - this.tableContainer.createEl("p", { - cls: "via-sheet-empty", - text: "This sheet is empty.", - }); - return; - } - - const range = this.xlsx.utils.decode_range(ref); - const rowCount = range.e.r + 1; - const colCount = range.e.c + 1; - - const table = this.tableContainer.createEl("table", { - cls: "via-sheet-table", - }); - - // Column header row - const thead = table.createEl("thead"); - const headerRow = thead.createEl("tr"); - headerRow.createEl("th", { cls: "via-sheet-row-num", text: "" }); - for (let c = 0; c < colCount; c++) { - const th = headerRow.createEl("th", { text: colLetter(c) }); - if (c === this.selCol) th.classList.add("is-selected-col"); - // Context menu on column header (edit mode only) - const colIdx = c; - th.addEventListener("contextmenu", (e: MouseEvent) => { - if (!this.editMode) return; - e.preventDefault(); - this.showColumnContextMenu(e, colIdx, colCount); - }); - } - - // Data rows - const tbody = table.createEl("tbody"); - for (let r = 0; r < rowCount; r++) { - const tr = tbody.createEl("tr"); - const rowNumTd = tr.createEl("td", { - cls: "via-sheet-row-num", - text: String(r + 1), - }); - if (r === this.selRow) rowNumTd.classList.add("is-selected-row"); - // Context menu on row number (edit mode only) - const rowIdx = r; - rowNumTd.addEventListener("contextmenu", (e: MouseEvent) => { - if (!this.editMode) return; - e.preventDefault(); - this.showRowContextMenu(e, rowIdx, rowCount); - }); - - for (let c = 0; c < colCount; c++) { - const cellRef = this.xlsx.utils.encode_cell({ r, c }); - const cell = sheetGetCell(sheet, cellRef); - const td = tr.createEl("td"); - td.textContent = this.getCellDisplay(cell); - if (r === this.selRow && c === this.selCol) - td.classList.add("is-selected"); - - td.addEventListener("click", () => this.selectCell(r, c)); - td.addEventListener("dblclick", () => { - if (this.editMode) this.beginInlineEdit(td, r, c); - }); - } - } - } - - private getCellDisplay(cell: XLSXCell | undefined): string { - if (!cell) return ""; - if (cell.w !== undefined) return cell.w; - if (cell.v !== undefined) return String(cell.v); - return ""; - } - - // ── Selection ───────────────────────────────────────────────────────────── - - private selectCell(row: number, col: number): void { - this.selRow = row; - this.selCol = col; - this.updateSelectionHighlight(); - this.refreshFormulaBar(); - } - - private updateSelectionHighlight(): void { - if (!this.tableContainer) return; - this.tableContainer - .querySelectorAll( - ".is-selected, .is-selected-col, .is-selected-row", - ) - .forEach((el) => - el.classList.remove( - "is-selected", - "is-selected-col", - "is-selected-row", - ), - ); - - if (this.selRow < 0 || this.selCol < 0) return; - const table = this.tableContainer.querySelector(".via-sheet-table"); - if (!table) return; - - // Column header highlight - const ths = table.querySelectorAll("thead th"); - ths[this.selCol + 1]?.classList.add("is-selected-col"); - - // Row header + cell highlight - const rows = table.querySelectorAll("tbody tr"); - const row = rows[this.selRow]; - if (row) { - row.querySelector(".via-sheet-row-num")?.classList.add( - "is-selected-row", - ); - row.querySelectorAll("td")[this.selCol + 1]?.classList.add( - "is-selected", - ); - } - } - - // ── Formula bar ─────────────────────────────────────────────────────────── - - private refreshFormulaBar(): void { - if (!this.formulaRef || !this.formulaInput || !this.xlsx) return; - if (this.selRow < 0 || this.selCol < 0) { - this.formulaRef.textContent = ""; - this.formulaInput.value = ""; - return; - } - const cellRef = this.xlsx.utils.encode_cell({ - r: this.selRow, - c: this.selCol, - }); - this.formulaRef.textContent = cellRef; - const sheetName = this.workbook?.SheetNames[this.activeSheet]; - const sheet = sheetName ? this.workbook?.Sheets[sheetName] : undefined; - const cell = sheet ? sheetGetCell(sheet, cellRef) : undefined; - this.formulaInput.value = cell?.f - ? "=" + cell.f - : this.getCellDisplay(cell); - } - - private commitFormulaBar(): void { - if (!this.formulaInput || this.selRow < 0 || this.selCol < 0) return; - this.writeCell(this.selRow, this.selCol, this.formulaInput.value); - this.formulaInput.blur(); - } - - // ── Inline cell editing ─────────────────────────────────────────────────── - - private beginInlineEdit(td: HTMLElement, row: number, col: number): void { - if (!this.xlsx || !this.editMode) return; - const sheetName = this.workbook?.SheetNames[this.activeSheet]; - const sheet = sheetName ? this.workbook?.Sheets[sheetName] : undefined; - const cellRef = this.xlsx.utils.encode_cell({ r: row, c: col }); - const cell = sheet ? sheetGetCell(sheet, cellRef) : undefined; - const rawValue = cell?.f ? "=" + cell.f : this.getCellDisplay(cell); - - td.empty(); - const input = td.createEl("input", { - cls: "via-sheet-cell-input", - type: "text", - }); - input.value = rawValue; - input.focus(); - input.select(); - - let committed = false; - const commit = (nextRow?: number, nextCol?: number) => { - if (committed) return; - committed = true; - this.writeCell(row, col, input.value); - // Navigate to next cell if specified - if (nextRow !== undefined && nextCol !== undefined) { - this.selectCell(nextRow, nextCol); - // Auto-start editing the next cell - const nextTd = this.getCellTd(nextRow, nextCol); - if (nextTd) { - setTimeout( - () => this.beginInlineEdit(nextTd, nextRow, nextCol), - 0, - ); - } - } - }; - - input.addEventListener("keydown", (e: KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - commit(row + 1, col); - } - if (e.key === "Tab") { - e.preventDefault(); - if (e.shiftKey) { - commit(row, Math.max(0, col - 1)); - } else { - commit(row, col + 1); - } - } - if (e.key === "Escape") { - e.preventDefault(); - committed = true; - this.renderSheet(); - this.refreshFormulaBar(); - } - }); - input.addEventListener("blur", () => commit()); - } - - /** Get the element for a given row/col in the rendered table. */ - private getCellTd(row: number, col: number): HTMLElement | null { - if (!this.tableContainer) return null; - const rows = this.tableContainer.querySelectorAll("tbody tr"); - const tr = rows[row]; - if (!tr) return null; - // +1 to skip the row number - const tds = tr.querySelectorAll("td"); - return (tds[col + 1] as HTMLElement) ?? null; - } - - private writeCell(row: number, col: number, value: string): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - - const cellRef = this.xlsx.utils.encode_cell({ r: row, c: col }); - let cell: XLSXCell; - if (value.startsWith("=")) { - cell = { f: value.slice(1), t: "n", v: 0 }; - } else if (value.trim() !== "" && !isNaN(Number(value))) { - cell = { v: Number(value), t: "n", w: value }; - } else { - cell = { v: value, t: "s", w: value }; - } - sheetSetCell(sheet, cellRef, cell); - - // Expand sheet !ref if needed - const ref = sheetGetRef(sheet); - if (!ref) { - sheetSetRef( - sheet, - this.xlsx.utils.encode_range({ - s: { r: 0, c: 0 }, - e: { r: row, c: col }, - }), - ); - } else { - const range = this.xlsx.utils.decode_range(ref); - if (row > range.e.r) range.e.r = row; - if (col > range.e.c) range.e.c = col; - sheetSetRef(sheet, this.xlsx.utils.encode_range(range)); - } - - // Re-evaluate all formulas so dependent cells update live - this.evaluateAllFormulas(); - - this.markDirty(); - this.renderSheet(); - this.refreshFormulaBar(); - } - - // ── Edit Mode Toggle ───────────────────────────────────────────────────── - - private evaluateAllFormulas(): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - evaluateSheet(sheet, this.xlsx.utils, sheetGetRef(sheet)); - } - - private toggleEditMode(): void { - this.editMode = !this.editMode; - if (!this.editToggleBtn) return; - setIcon(this.editToggleBtn, this.editMode ? "eye" : "pencil"); - setTooltip( - this.editToggleBtn, - this.editMode ? "Switch to view mode" : "Switch to edit mode", - ); - this.editToggleBtn.classList.toggle("is-active", this.editMode); - - // Show/hide edit-only toolbar buttons - const hidden = !this.editMode; - if (this.saveBtn) this.saveBtn.classList.toggle("via-hidden", hidden); - if (this.undoBtn) this.undoBtn.classList.toggle("via-hidden", hidden); - if (this.addRowBtn) this.addRowBtn.classList.toggle("via-hidden", hidden); - if (this.addColBtn) this.addColBtn.classList.toggle("via-hidden", hidden); - - // Hide dirty indicator when leaving edit mode - if (!this.editMode) { - this.setDirty(false); - } - } - - // ── Add Row / Column ────────────────────────────────────────────────────── - - private addRow(): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - - const ref = sheetGetRef(sheet) ?? "A1:A1"; - const range = this.xlsx.utils.decode_range(ref); - range.e.r += 1; - sheetSetRef(sheet, this.xlsx.utils.encode_range(range)); - - this.markDirty(); - this.renderSheet(); - this.updateInfo(); - new Notice("Row added", 1500); - } - - private addColumn(): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - - const ref = sheetGetRef(sheet) ?? "A1:A1"; - const range = this.xlsx.utils.decode_range(ref); - range.e.c += 1; - sheetSetRef(sheet, this.xlsx.utils.encode_range(range)); - - this.markDirty(); - this.renderSheet(); - this.updateInfo(); - new Notice("Column added", 1500); - } - - // ── Insert / Delete Row ─────────────────────────────────────────────────── - - private insertRowAt(index: number): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - - const ref = sheetGetRef(sheet) ?? "A1:A1"; - const range = this.xlsx.utils.decode_range(ref); - const colCount = range.e.c + 1; - - // Shift rows down from bottom to insertion point - for (let r = range.e.r; r >= index; r--) { - for (let c = 0; c < colCount; c++) { - const srcRef = this.xlsx.utils.encode_cell({ r, c }); - const dstRef = this.xlsx.utils.encode_cell({ r: r + 1, c }); - const cell = sheetGetCell(sheet, srcRef); - if (cell) { - sheetSetCell(sheet, dstRef, cell); - } else { - delete (sheet as Record)[dstRef]; - } - } - } - - // Clear the inserted row - for (let c = 0; c < colCount; c++) { - const ref = this.xlsx.utils.encode_cell({ r: index, c }); - delete (sheet as Record)[ref]; - } - - range.e.r += 1; - sheetSetRef(sheet, this.xlsx.utils.encode_range(range)); - this.markDirty(); - this.renderSheet(); - this.updateInfo(); - } - - private deleteRowAt(index: number): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - - const ref = sheetGetRef(sheet) ?? "A1:A1"; - const range = this.xlsx.utils.decode_range(ref); - if (range.e.r <= 0) return; // Don't delete the last row - const colCount = range.e.c + 1; - - // Shift rows up - for (let r = index; r < range.e.r; r++) { - for (let c = 0; c < colCount; c++) { - const srcRef = this.xlsx.utils.encode_cell({ r: r + 1, c }); - const dstRef = this.xlsx.utils.encode_cell({ r, c }); - const cell = sheetGetCell(sheet, srcRef); - if (cell) { - sheetSetCell(sheet, dstRef, cell); - } else { - delete (sheet as Record)[dstRef]; - } - } - } - - // Clear last row - for (let c = 0; c < colCount; c++) { - const cellRef = this.xlsx.utils.encode_cell({ r: range.e.r, c }); - delete (sheet as Record)[cellRef]; - } - - range.e.r -= 1; - sheetSetRef(sheet, this.xlsx.utils.encode_range(range)); - this.selRow = -1; - this.selCol = -1; - this.markDirty(); - this.renderSheet(); - this.updateInfo(); - this.refreshFormulaBar(); - } - - // ── Insert / Delete Column ──────────────────────────────────────────────── - - private insertColumnAt(index: number): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - - const ref = sheetGetRef(sheet) ?? "A1:A1"; - const range = this.xlsx.utils.decode_range(ref); - const rowCount = range.e.r + 1; - - // Shift columns right from rightmost to insertion point - for (let c = range.e.c; c >= index; c--) { - for (let r = 0; r < rowCount; r++) { - const srcRef = this.xlsx.utils.encode_cell({ r, c }); - const dstRef = this.xlsx.utils.encode_cell({ r, c: c + 1 }); - const cell = sheetGetCell(sheet, srcRef); - if (cell) { - sheetSetCell(sheet, dstRef, cell); - } else { - delete (sheet as Record)[dstRef]; - } - } - } - - // Clear inserted column - for (let r = 0; r < rowCount; r++) { - const cellRef = this.xlsx.utils.encode_cell({ r, c: index }); - delete (sheet as Record)[cellRef]; - } - - range.e.c += 1; - sheetSetRef(sheet, this.xlsx.utils.encode_range(range)); - this.markDirty(); - this.renderSheet(); - this.updateInfo(); - } - - private deleteColumnAt(index: number): void { - if (!this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) return; - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) return; - - const ref = sheetGetRef(sheet) ?? "A1:A1"; - const range = this.xlsx.utils.decode_range(ref); - if (range.e.c <= 0) return; // Don't delete the last column - const rowCount = range.e.r + 1; - - // Shift columns left - for (let c = index; c < range.e.c; c++) { - for (let r = 0; r < rowCount; r++) { - const srcRef = this.xlsx.utils.encode_cell({ r, c: c + 1 }); - const dstRef = this.xlsx.utils.encode_cell({ r, c }); - const cell = sheetGetCell(sheet, srcRef); - if (cell) { - sheetSetCell(sheet, dstRef, cell); - } else { - delete (sheet as Record)[dstRef]; - } - } - } - - // Clear last column - for (let r = 0; r < rowCount; r++) { - const cellRef = this.xlsx.utils.encode_cell({ r, c: range.e.c }); - delete (sheet as Record)[cellRef]; - } - - range.e.c -= 1; - sheetSetRef(sheet, this.xlsx.utils.encode_range(range)); - this.selRow = -1; - this.selCol = -1; - this.markDirty(); - this.renderSheet(); - this.updateInfo(); - this.refreshFormulaBar(); - } - - // ── Context Menus ───────────────────────────────────────────────────────── - - private showRowContextMenu( - e: MouseEvent, - row: number, - rowCount: number, - ): void { - const menu = new Menu(); - menu.addItem((item) => { - item.setTitle("Insert row above") - .setIcon("arrow-up") - .onClick(() => this.insertRowAt(row)); - }); - menu.addItem((item) => { - item.setTitle("Insert row below") - .setIcon("arrow-down") - .onClick(() => this.insertRowAt(row + 1)); - }); - if (rowCount > 1) { - menu.addSeparator(); - menu.addItem((item) => { - item.setTitle("Delete row") - .setIcon("trash-2") - .onClick(() => this.deleteRowAt(row)); - }); - } - menu.showAtMouseEvent(e); - } - - private showColumnContextMenu( - e: MouseEvent, - col: number, - colCount: number, - ): void { - const menu = new Menu(); - menu.addItem((item) => { - item.setTitle("Insert column left") - .setIcon("arrow-left") - .onClick(() => this.insertColumnAt(col)); - }); - menu.addItem((item) => { - item.setTitle("Insert column right") - .setIcon("arrow-right") - .onClick(() => this.insertColumnAt(col + 1)); - }); - if (colCount > 1) { - menu.addSeparator(); - menu.addItem((item) => { - item.setTitle("Delete column") - .setIcon("trash-2") - .onClick(() => this.deleteColumnAt(col)); - }); - } - menu.showAtMouseEvent(e); - } - - // ── Dirty state & Save ──────────────────────────────────────────────────── - - private markDirty(): void { - this.isDirty = true; - this.saveBtn?.classList.add("is-dirty"); - this.setDirty(true); - } - - private setDirty(dirty: boolean): void { - this.isDirty = dirty; - if (this.dirtyIndicator) - this.dirtyIndicator.classList.toggle("via-hidden", !dirty); - if (!dirty) this.saveBtn?.classList.remove("is-dirty"); - } - - private async saveFile(): Promise { - if (!this.workbook || !this.xlsx || !this.currentFile) return; - - if (this.plugin.settings.confirmOnSave) { - const confirmed = await confirmModal( - this.app, - `Overwrite "${this.currentFile.name}"?`, - "This will replace the original file with the current spreadsheet data.", - ); - if (!confirmed) return; - } - - const bookType = this.currentFile.extension === "csv" ? "csv" : "xlsx"; - try { - // Use type:'binary' to avoid readSlice errors in SheetJS CFB writer - const out = this.xlsx.write(this.workbook, { - type: "binary", - bookType, - }) as string; - const ab = binaryStringToArrayBuffer(out); - await this.app.vault.modifyBinary(this.currentFile, ab); - // Update saved snapshot - this.savedData = ab.slice(0); - this.setDirty(false); - new Notice("Saved", 2000); - } catch (err) { - new Notice(`Save failed: ${String(err)}`); - } - } - - private revertToSaved(): void { - if (!this.savedData || !this.xlsx) return; - try { - this.workbook = this.xlsx.read(new Uint8Array(this.savedData), { - type: "array", - }); - this.setDirty(false); - this.selRow = -1; - this.selCol = -1; - this.renderSheet(); - this.updateInfo(); - this.refreshFormulaBar(); - new Notice("Reverted to last save", 1500); - } catch (err) { - new Notice(`Revert failed: ${String(err)}`); - } - } - - private updateInfo(): void { - if (!this.infoEl || !this.workbook || !this.xlsx) return; - const sheetName = this.workbook.SheetNames[this.activeSheet]; - if (!sheetName) { - this.infoEl.textContent = "—"; - return; - } - const sheet = this.workbook.Sheets[sheetName]; - if (!sheet) { - this.infoEl.textContent = "—"; - return; - } - const ref = sheetGetRef(sheet); - if (!ref) { - this.infoEl.textContent = "0 rows"; - return; - } - const range = this.xlsx.utils.decode_range(ref); - this.infoEl.textContent = `${range.e.r + 1} × ${range.e.c + 1}`; - } -} - -// ── Confirm modal ───────────────────────────────────────────────────────────── - -function confirmModal( - app: App, - title: string, - message: string, -): Promise { - return new Promise((resolve) => { - const modal = new ConfirmModal(app, title, message, resolve); - modal.open(); - }); -} - -class ConfirmModal extends Modal { - constructor( - app: App, - private titleText: string, - private message: string, - private resolve: (v: boolean) => void, - ) { - super(app); - } - - onOpen(): void { - this.setTitle(this.titleText); - const { contentEl } = this; - contentEl.createEl("p", { text: this.message }); - const btnRow = contentEl.createEl("div", { - cls: "modal-button-container", - }); - btnRow - .createEl("button", { text: "Cancel" }) - .addEventListener("click", () => { - this.resolve(false); - this.close(); - }); - const overwriteBtn = btnRow.createEl("button", { - text: "Overwrite", - cls: "mod-cta via-btn-danger", - }); - overwriteBtn.addEventListener("click", () => { - this.resolve(true); - this.close(); - }); - } - - onClose(): void { - this.contentEl.empty(); - } -} - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -/** Convert 0-based column index to spreadsheet letter (A, B, …, Z, AA, AB…). */ -function colLetter(index: number): string { - let s = ""; - let n = index; - while (n >= 0) { - s = String.fromCharCode((n % 26) + 65) + s; - n = Math.floor(n / 26) - 1; - } - return s; -} - -/** Convert a SheetJS binary string to an ArrayBuffer (avoids CFB readSlice bug). */ -function binaryStringToArrayBuffer(bin: string): ArrayBuffer { - const buf = new ArrayBuffer(bin.length); - const view = new Uint8Array(buf); - for (let i = 0; i < bin.length; i++) view[i] = bin.charCodeAt(i) & 0xff; - return buf; -} diff --git a/src/views/pdf/PdfSearchController.ts b/src/views/pdf/PdfSearchController.ts deleted file mode 100644 index 279aa2b..0000000 --- a/src/views/pdf/PdfSearchController.ts +++ /dev/null @@ -1,288 +0,0 @@ -import type * as pdfjsLib from "pdfjs-dist"; -import type { TextItem as PdfTextItem } from "pdfjs-dist/types/src/display/api"; -import { setIcon, setTooltip } from "obsidian"; -import type { PageCtx, SearchMatch } from "./pdfTypes"; - -/** - * Manages the text-search bar, match highlighting, and page-text caching for - * a PdfView instance. Call `setContext()` each time a new PDF is loaded. - */ -export class PdfSearchController { - private pdfDoc: pdfjsLib.PDFDocumentProxy | null = null; - private pages: PageCtx[] = []; - private wrapperEl: HTMLElement | null = null; - private bodyEl: HTMLElement | null = null; - - // DOM elements (null when bar is closed) - private searchBarEl: HTMLElement | null = null; - private searchInputEl: HTMLInputElement | null = null; - private searchMatchCountEl: HTMLElement | null = null; - - // State - private matches: SearchMatch[] = []; - private currentIdx = -1; - private textCache = new Map(); - private debounceTimer: ReturnType | null = null; - - // ── Lifecycle ───────────────────────────────────────────────────────── - - /** - * Call this each time a new PDF is loaded into the view. - * Closes any open search bar and resets all state. - */ - setContext( - pdfDoc: pdfjsLib.PDFDocumentProxy, - pages: PageCtx[], - wrapperEl: HTMLElement, - bodyEl: HTMLElement, - ): void { - this.close(); - this.pdfDoc = pdfDoc; - this.pages = pages; - this.wrapperEl = wrapperEl; - this.bodyEl = bodyEl; - this.textCache.clear(); - this.matches = []; - this.currentIdx = -1; - } - - /** Call when the view is unloaded to clean up DOM and timers. */ - destroy(): void { - this.close(); - this.pdfDoc = null; - this.pages = []; - this.wrapperEl = null; - this.bodyEl = null; - } - - // ── Public API ──────────────────────────────────────────────────────── - - open(): void { - if (this.searchBarEl) { - this.searchInputEl?.focus(); - return; - } - if (!this.wrapperEl) return; - - const bar = this.wrapperEl.createEl("div", { - cls: "via-pdf-search-bar", - }); - this.searchBarEl = bar; - - const iconEl = bar.createEl("span", { cls: "via-pdf-search-icon" }); - setIcon(iconEl, "search"); - - this.searchInputEl = bar.createEl("input"); - this.searchInputEl.type = "text"; - this.searchInputEl.placeholder = "Search…"; - this.searchInputEl.className = "via-pdf-search-input"; - - this.searchMatchCountEl = bar.createEl("span", { - cls: "via-pdf-search-count", - text: "", - }); - - const prevBtn = bar.createEl("div", { cls: "clickable-icon" }); - setIcon(prevBtn, "chevron-up"); - setTooltip(prevBtn, "Previous match (Shift+Enter)"); - prevBtn.addEventListener("click", () => - this.goToMatch(this.currentIdx - 1), - ); - - const nextBtn = bar.createEl("div", { cls: "clickable-icon" }); - setIcon(nextBtn, "chevron-down"); - setTooltip(nextBtn, "Next match (Enter)"); - nextBtn.addEventListener("click", () => - this.goToMatch(this.currentIdx + 1), - ); - - const closeBtn = bar.createEl("div", { cls: "clickable-icon" }); - setIcon(closeBtn, "x"); - setTooltip(closeBtn, "Close search (Escape)"); - closeBtn.addEventListener("click", () => this.close()); - - this.searchInputEl.addEventListener("keydown", (e) => { - e.stopPropagation(); - if (e.key === "Enter") { - e.preventDefault(); - if (e.shiftKey) { - this.goToMatch(this.currentIdx - 1); - } else { - this.goToMatch(this.currentIdx + 1); - } - } else if (e.key === "Escape") { - e.preventDefault(); - this.close(); - } - }); - - this.searchInputEl.addEventListener("input", () => { - if (this.debounceTimer) clearTimeout(this.debounceTimer); - this.debounceTimer = setTimeout( - () => { void this.performSearch(this.searchInputEl!.value); }, - 300, - ); - }); - - // Insert search bar between toolbar and body area - if (this.bodyEl) this.wrapperEl.insertBefore(bar, this.bodyEl); - - this.searchInputEl.focus(); - } - - close(): void { - this.matches = []; - this.currentIdx = -1; - this.clearAll(); - this.updateCount(); - if (this.debounceTimer) { - clearTimeout(this.debounceTimer); - this.debounceTimer = null; - } - this.searchBarEl?.remove(); - this.searchBarEl = null; - this.searchInputEl = null; - this.searchMatchCountEl = null; - } - - get hasMatches(): boolean { - return this.matches.length > 0; - } - - /** Call after a page canvas has been rendered (or re-rendered) to draw highlights. */ - drawHighlightsForPage(ctx: PageCtx): void { - if (!ctx.searchCanvas) return; - const canvas = ctx.searchCanvas; - const c = canvas.getContext("2d")!; - c.clearRect(0, 0, canvas.width, canvas.height); - - for (let i = 0; i < this.matches.length; i++) { - const m = this.matches[i]!; - if (m.pageNum !== ctx.pageNum) continue; - c.fillStyle = - i === this.currentIdx - ? "rgba(255, 140, 0, 0.55)" - : "rgba(255, 220, 0, 0.38)"; - c.fillRect( - m.x * canvas.width, - m.y * canvas.height, - m.w * canvas.width, - m.h * canvas.height, - ); - } - } - - /** Redraw highlights on all currently-rendered pages. */ - redrawAll(): void { - for (const ctx of this.pages) { - if (ctx.state === "rendered") this.drawHighlightsForPage(ctx); - } - } - - /** Clear all search highlight canvases. */ - clearAll(): void { - for (const ctx of this.pages) { - if (!ctx.searchCanvas) continue; - const c = ctx.searchCanvas.getContext("2d")!; - c.clearRect(0, 0, ctx.searchCanvas.width, ctx.searchCanvas.height); - } - } - - // ── Private ─────────────────────────────────────────────────────────── - - private async performSearch(query: string): Promise { - this.matches = []; - this.currentIdx = -1; - this.clearAll(); - - if (!query.trim() || !this.pdfDoc) { - this.updateCount(); - return; - } - - const q = query.toLowerCase(); - const total = this.pdfDoc.numPages; - - for (let pn = 1; pn <= total; pn++) { - const items = await this.getPageTextItems(pn); - const page = await this.pdfDoc.getPage(pn); - const vp = page.getViewport({ scale: 1.0 }); - - for (const item of items) { - const str = item.str.toLowerCase(); - let idx = 0; - while ((idx = str.indexOf(q, idx)) !== -1) { - const tx = item.transform[4] ?? 0; - const ty = item.transform[5] ?? 0; - const fontSize = Math.abs(item.transform[3] ?? 12); - const charWidth = - (item.width || 0) / (item.str.length || 1); - const matchX = tx + charWidth * idx; - const matchW = charWidth * query.length; - - const [cx, cy] = vp.convertToViewportPoint(matchX, ty); - const [cx2, cy2] = vp.convertToViewportPoint( - matchX + matchW, - ty - fontSize, - ); - - this.matches.push({ - pageNum: pn, - x: Math.min(cx, cx2) / vp.width, - y: Math.min(cy, cy2) / vp.height, - w: Math.abs(cx2 - cx) / vp.width, - h: Math.abs(cy2 - cy) / vp.height, - }); - idx += q.length; - } - } - } - - if (this.matches.length > 0) { - this.currentIdx = 0; - this.scrollToMatch(0); - } - this.updateCount(); - this.redrawAll(); - } - - private goToMatch(idx: number): void { - if (this.matches.length === 0) return; - this.currentIdx = - ((idx % this.matches.length) + this.matches.length) % - this.matches.length; - this.scrollToMatch(this.currentIdx); - this.updateCount(); - this.redrawAll(); - } - - private scrollToMatch(idx: number): void { - const m = this.matches[idx]; - if (!m) return; - const ctx = this.pages.find((p) => p.pageNum === m.pageNum); - if (ctx) - ctx.container.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - - private updateCount(): void { - if (!this.searchMatchCountEl) return; - if (this.matches.length === 0) { - this.searchMatchCountEl.textContent = - this.searchInputEl?.value.trim() ? "No matches" : ""; - } else { - this.searchMatchCountEl.textContent = `${this.currentIdx + 1} / ${this.matches.length}`; - } - } - - private async getPageTextItems(pageNum: number): Promise { - if (this.textCache.has(pageNum)) return this.textCache.get(pageNum)!; - const page = await this.pdfDoc!.getPage(pageNum); - const tc = await page.getTextContent(); - const items = tc.items.filter((it): it is PdfTextItem => "str" in it); - this.textCache.set(pageNum, items); - return items; - } -} diff --git a/src/views/pdf/pdfTypes.ts b/src/views/pdf/pdfTypes.ts deleted file mode 100644 index 1465572..0000000 --- a/src/views/pdf/pdfTypes.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** Shared types for PDF page rendering, used by PdfView and PdfSearchController. */ - -export type PageRenderState = "placeholder" | "rendering" | "rendered"; - -export interface PageCtx { - pageNum: number; - state: PageRenderState; - container: HTMLElement; - pdfCanvas: HTMLCanvasElement | null; - annotCanvas: HTMLCanvasElement | null; - searchCanvas: HTMLCanvasElement | null; - w: number; - h: number; -} - -export interface SearchMatch { - pageNum: number; - /** Normalised 0-1 fractions of page viewport */ - x: number; - y: number; - w: number; - h: number; -} diff --git a/styles.css b/styles.css index 83b9ffc..9483c35 100644 --- a/styles.css +++ b/styles.css @@ -1,5 +1,5 @@ /* ===================================================================== - ViewItAll — styles.css + ViewItAll v2.0.0 — styles.css Uses Obsidian CSS variables throughout for a native-theme feel. ===================================================================== */ @@ -9,8 +9,6 @@ .via-hidden { display: none; } -.via-separator-faint { opacity: 0.3; } - .via-toolbar-sep { width: 1px; height: 18px; @@ -20,7 +18,6 @@ align-self: center; } -/* Legacy text button (zoom %, page count) */ .via-btn { display: inline-flex; align-items: center; @@ -50,108 +47,11 @@ border-color: var(--background-modifier-border); } -/* Icon buttons with semantic tint */ -.via-icon-save { color: var(--color-green); opacity: 0.85; } -.via-icon-save:hover { color: var(--color-green) !important; opacity: 1; background: var(--background-modifier-hover) !important; } -.via-icon-danger { color: var(--text-muted); } -.via-icon-danger:hover { color: var(--color-red) !important; background: var(--background-modifier-hover) !important; } - -/* Snap direction button */ -.via-pdf-snap-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: var(--size-4-1) var(--size-4-2); - border-radius: var(--radius-s); - border: 1px solid var(--background-modifier-border); - background: transparent; - color: var(--text-muted); - font-size: var(--font-ui-small); - cursor: pointer; - transition: background 80ms, color 80ms, border-color 80ms; -} -.via-pdf-snap-btn:hover { - background: var(--background-modifier-hover); - color: var(--text-normal); - border-color: var(--background-modifier-border-hover); -} -.via-pdf-snap-btn .via-snap-icon { - display: flex; - align-items: center; - width: 14px; - height: 14px; - flex-shrink: 0; -} -.via-pdf-snap-btn .via-snap-icon svg { - width: 14px; - height: 14px; -} -.via-snap-label { - font-variant-numeric: tabular-nums; - font-weight: 600; - font-size: 0.75em; - line-height: 1; -} - -/* Snap active (modifier key held during draw) */ -.via-snap-active { - background: var(--interactive-accent) !important; - color: var(--text-on-accent) !important; - border-color: transparent !important; -} - -/* Page indicator */ -.via-pdf-page-indicator { - font-size: var(--font-ui-small); - font-variant-numeric: tabular-nums; - color: var(--text-muted); - padding: var(--size-4-1) var(--size-4-2); - white-space: nowrap; - user-select: none; - background: transparent; - border: 1px solid transparent; - border-radius: var(--radius-s); - cursor: pointer; - transition: border-color 120ms, color 120ms; -} -.via-pdf-page-indicator:hover { - border-color: var(--background-modifier-border); - color: var(--text-normal); -} - -/* Inline page-jump input */ -.via-pdf-page-jump-input { - width: 5ch; - padding: 2px 4px; - border: 1px solid var(--interactive-accent); - border-radius: var(--radius-s); - background: var(--background-primary); - color: var(--text-normal); - font-size: var(--font-ui-small); - font-variant-numeric: tabular-nums; - text-align: center; - -moz-appearance: textfield; -} -.via-pdf-page-jump-input::-webkit-inner-spin-button, -.via-pdf-page-jump-input::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - /* Error / warning messages */ .via-error { color: var(--text-error); padding: var(--size-4-4); } -.via-warning { - margin: var(--size-4-2) var(--size-4-4); - padding: var(--size-4-2) var(--size-4-3); - background: var(--background-modifier-warning, rgba(255, 167, 0, 0.08)); - border-radius: var(--radius-m); - font-size: var(--font-ui-small); - color: var(--text-warning); - border-left: 3px solid var(--color-yellow); -} /* ── DOCX viewer ─────────────────────────────────────────────────────── */ @@ -180,20 +80,21 @@ border-top: 1px solid var(--background-modifier-border); } -/* Unsaved-changes dot */ -.via-docx-dirty-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--color-yellow); +/* File name label */ +.via-docx-file-label { + display: flex; + align-items: center; + gap: var(--size-4-1); flex-shrink: 0; - opacity: 0.9; - cursor: default; } - -.via-btn-danger { - background: var(--color-red); - border-color: var(--color-red); +.via-docx-file-name { + font-size: var(--font-ui-small); + color: var(--text-muted); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; } /* Document content area */ @@ -206,1175 +107,120 @@ line-height: var(--line-height-normal); } -.via-docx-content.via-editable { - outline: 2px solid var(--interactive-accent); - outline-offset: 4px; - border-radius: var(--radius-m); -} +/* ── DOCX typography ─────────────────────────────────────────────────── */ +/* Override Obsidian theme heading/bold colors — Word content should use + the document's text color unless the .docx explicitly sets a color. + Inline styles in JS are the primary defense; these are the backup. */ .via-docx-content h1, .via-docx-content h2, .via-docx-content h3, -.via-docx-content h4 { font-weight: 700; margin: 1em 0 0.5em; } -.via-docx-content p { margin: 0.5em 0; } -.via-docx-content table { - border-collapse: collapse; - width: 100%; - margin: 1em 0; -} -.via-docx-content table td, -.via-docx-content table th { - border: 1px solid var(--background-modifier-border); - padding: 6px 10px; -} -.via-docx-content img { max-width: 100%; } - -/* ── PDF viewer — layout ─────────────────────────────────────────────── */ - -.via-pdf-wrapper { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} -.via-pdf-wrapper--toolbar-bottom { flex-direction: column-reverse; } - -.via-pdf-toolbar { - display: flex; - align-items: center; - gap: var(--size-4-1); - padding: var(--size-4-1) var(--size-4-2); - border-bottom: 1px solid var(--background-modifier-border); - background: var(--background-primary); - flex-shrink: 0; - min-height: 36px; - overflow-x: auto; - scrollbar-width: none; -} -.via-pdf-toolbar::-webkit-scrollbar { display: none; } -.via-pdf-wrapper--toolbar-bottom .via-pdf-toolbar { - border-bottom: none; - border-top: 1px solid var(--background-modifier-border); -} - -/* Tool group pill */ -.via-tool-group { - display: flex; - align-items: center; - gap: 1px; - background: var(--background-modifier-hover); - border-radius: var(--radius-m); - padding: 2px; +.via-docx-content h4, +.via-docx-content h5, +.via-docx-content h6 { + font-weight: 700; + margin: 1em 0 0.5em; + color: var(--text-normal) !important; +} +.via-docx-content strong, +.via-docx-content b, +.via-docx-content em, +.via-docx-content i, +.via-docx-content u, +.via-docx-content s, +.via-docx-content span, +.via-docx-content sub, +.via-docx-content sup { + color: inherit; +} +.via-docx-content p { + margin: 0.5em 0; } -/* Active state for tool buttons inside the pill */ -.via-tool-btn.is-active { - background: var(--background-primary) !important; - color: var(--interactive-accent) !important; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); -} +/* ── DOCX links ──────────────────────────────────────────────────────── */ -/* PDF body: TOC sidebar + scroll area */ -.via-pdf-body { - display: flex; - flex-direction: row; - flex: 1; - overflow: hidden; +.via-docx-link { + color: var(--text-accent); + text-decoration: underline; + text-decoration-color: var(--text-accent); + text-underline-offset: 2px; + cursor: pointer; } - -.via-pdf-scroll { - flex: 1; - overflow: auto; - padding: var(--size-4-6) var(--size-4-4); - display: flex; - flex-direction: column; - align-items: center; - gap: var(--size-4-5); - background: var(--background-secondary); - position: relative; +.via-docx-link:hover { + color: var(--text-accent-hover); + text-decoration-color: var(--text-accent-hover); } -/* Loading indicator */ -.via-pdf-loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--size-4-3); - padding: var(--size-4-8); - color: var(--text-muted); - font-size: var(--font-ui-medium); -} -@keyframes via-spin { to { transform: rotate(360deg); } } -.via-pdf-loading-spinner { - width: 28px; - height: 28px; - border: 2px solid var(--background-modifier-border); - border-top-color: var(--interactive-accent); - border-radius: 50%; - animation: via-spin 0.7s linear infinite; -} +/* ── DOCX images ─────────────────────────────────────────────────────── */ -/* PDF page container */ -.via-pdf-page { - position: relative; - flex-shrink: 0; - display: block; - border-radius: 2px; - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.28), 0 1px 4px rgba(0, 0, 0, 0.12); -} -.via-pdf-canvas, -.via-pdf-annot-canvas, -.via-pdf-search-canvas { - position: absolute; - top: 0; - left: 0; +.via-docx-image { + max-width: 100%; + height: auto; display: block; + margin: var(--size-4-2) 0; + border-radius: var(--radius-s); } -.via-pdf-canvas { z-index: 1; border-radius: 2px; } -.via-pdf-annot-canvas { z-index: 2; pointer-events: none; } -.via-pdf-search-canvas { z-index: 3; pointer-events: none; } -/* Page number label below each page */ -.via-pdf-page-label { - text-align: center; - font-size: var(--font-ui-small); +.via-docx-image-placeholder { + display: inline-block; + padding: var(--size-4-2) var(--size-4-3); + background: var(--background-secondary); + border: 1px dashed var(--background-modifier-border); + border-radius: var(--radius-s); color: var(--text-faint); - padding: var(--size-4-1) 0 var(--size-4-2); - user-select: none; - flex-shrink: 0; -} - -/* ── Color dot button & popover ─────────────────────────────────────── */ - -.via-color-dot-btn { - width: 22px; - height: 22px; - border-radius: 50%; - border: 2px solid var(--background-primary); - box-shadow: 0 0 0 1.5px var(--background-modifier-border), - inset 0 0 0 1px rgba(0, 0, 0, 0.08); - cursor: pointer; - padding: 0; - flex-shrink: 0; - transition: transform 100ms, box-shadow 100ms; -} -.via-color-dot-btn:hover { - transform: scale(1.18); - box-shadow: 0 0 0 2px var(--interactive-accent), - inset 0 0 0 1px rgba(0, 0, 0, 0.08); -} - -/* Floating color/size popover */ -.via-color-popover { - position: fixed; - z-index: 200; - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-l); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2), 0 2px 8px rgba(0, 0, 0, 0.1); - padding: var(--size-4-3); - display: flex; - flex-direction: column; - gap: var(--size-4-2); - min-width: 208px; -} - -.via-color-popover-swatches { - display: flex; - align-items: center; - gap: var(--size-4-2); - flex-wrap: wrap; -} - -.via-color-popover-sep { - border: none; - border-top: 1px solid var(--background-modifier-border); - margin: 0; -} - -.via-popover-slider-row { - display: flex; - align-items: center; - gap: var(--size-4-2); -} - -.via-popover-slider-label { - font-size: var(--font-ui-small); - color: var(--text-muted); - min-width: 50px; - flex-shrink: 0; -} - -.via-popover-slider { - flex: 1; - accent-color: var(--interactive-accent); - cursor: pointer; -} - -.via-popover-slider-value { font-size: var(--font-ui-small); - font-variant-numeric: tabular-nums; - color: var(--text-muted); - min-width: 36px; - text-align: right; -} - -/* ── Color swatches ─────────────────────────────────────────────────── */ - -.via-color-swatch { - width: 20px; - height: 20px; - border-radius: 50%; - border: 2px solid transparent; - cursor: pointer; - padding: 0; - flex-shrink: 0; - outline: none; - display: inline-flex; - align-items: center; - justify-content: center; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12); - transition: transform 100ms; -} -.via-color-swatch:hover { transform: scale(1.2); } -.via-color-swatch-active { - border-color: var(--background-primary); - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12), - 0 0 0 2px var(--text-normal); -} - -.via-color-custom { - background: conic-gradient(from 0deg, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); - position: relative; -} -.via-color-custom-input { - position: absolute; - width: 0; - height: 0; - opacity: 0; - border: none; - padding: 0; - top: 0; - left: 0; + font-style: italic; } -/* ── PDF search bar ─────────────────────────────────────────────────── */ - -.via-pdf-search-bar { - display: flex; - align-items: center; - gap: var(--size-4-2); - padding: var(--size-4-1) var(--size-4-3); - background: var(--background-primary); - border-bottom: 1px solid var(--background-modifier-border); - flex-shrink: 0; -} +/* ── DOCX tables ─────────────────────────────────────────────────────── */ -.via-pdf-search-icon { - flex-shrink: 0; - color: var(--text-muted); - display: flex; - align-items: center; +.via-docx-table { + border-collapse: collapse; + width: 100%; + margin: 1em 0; } - -.via-pdf-search-input { - flex: 1; - max-width: 260px; - height: 26px; - padding: 0 var(--size-4-2); +.via-docx-cell { border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - background: var(--background-primary); - color: var(--text-normal); - font-size: var(--font-ui-small); - outline: none; - transition: border-color 100ms; -} -.via-pdf-search-input:focus { border-color: var(--interactive-accent); } - -.via-pdf-search-count { - font-size: var(--font-ui-small); - font-variant-numeric: tabular-nums; - color: var(--text-muted); - white-space: nowrap; - min-width: 58px; + padding: var(--size-4-1) var(--size-4-2); + vertical-align: top; } - -/* ── TOC sidebar ─────────────────────────────────────────────────────── */ - -.via-pdf-toc { - width: 220px; - min-width: 160px; - max-width: 320px; - flex-shrink: 0; - display: flex; - flex-direction: column; - overflow: hidden; - border-right: 1px solid var(--background-modifier-border); - background: var(--background-primary); +.via-docx-cell p { + margin: 0.25em 0; } +.via-docx-cell p:first-child { margin-top: 0; } +.via-docx-cell p:last-child { margin-bottom: 0; } -.via-pdf-toc-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--size-4-2) var(--size-4-3); - border-bottom: 1px solid var(--background-modifier-border); - font-size: var(--font-ui-small); +/* Header cells */ +tr:first-child .via-docx-cell { font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; - flex-shrink: 0; -} - -.via-pdf-toc-list { - flex: 1; - overflow-y: auto; - padding: var(--size-4-1) 0; + background: var(--background-secondary); } -.via-pdf-toc-item { - display: flex; - align-items: center; - gap: var(--size-4-1); - padding: 2px var(--size-4-2); - min-height: 26px; -} +/* ── DOCX numbering prefixes ──────────────────────────────────────── */ -.via-pdf-toc-toggle { - font-size: 0.65em; - color: var(--text-muted); - cursor: pointer; - flex-shrink: 0; - width: 14px; - text-align: center; - opacity: 0.7; +.via-docx-num-prefix { + font-weight: inherit; + margin-right: 0.3em; } -.via-pdf-toc-toggle:hover { opacity: 1; } -.via-pdf-toc-label { - font-size: var(--font-ui-small); - color: var(--text-normal); - cursor: pointer; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - border-radius: var(--radius-s); - padding: 2px var(--size-4-2); - transition: background 80ms, color 80ms; -} -.via-pdf-toc-label:hover { - background: var(--background-modifier-hover); - color: var(--text-accent); -} +/* ── DOCX highlights ─────────────────────────────────────────────────── */ -.via-pdf-toc-empty { - padding: var(--size-4-3); - color: var(--text-faint); - font-size: var(--font-ui-small); - text-align: center; +.via-docx-highlight { + padding: 1px 2px; + border-radius: 2px; } -/* ── Text note overlays ─────────────────────────────────────────────── */ +/* ── DOCX tabs ───────────────────────────────────────────────────────── */ -/* ── Text note overlays — redesigned ─────────────────────────────────── */ - -.via-pdf-note { - /* Position & size */ - position: absolute; - width: 192px; - min-width: 140px; - max-width: 400px; - min-height: 108px; - transform: translate(-4px, -4px); - z-index: 10; - resize: both; - overflow: hidden; - - /* Appearance — neutral card with colored top accent */ - background: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-top: 4px solid var(--note-color, #ffd966); - border-radius: 0 0 var(--radius-m) var(--radius-m); - box-shadow: - 0 6px 24px rgba(0, 0, 0, 0.13), - 0 2px 6px rgba(0, 0, 0, 0.07), - 0 0 0 1px rgba(0, 0, 0, 0.04); - - display: flex; - flex-direction: column; - transition: box-shadow 120ms ease; +.via-docx-tab { + white-space: pre; } -/* Lift shadow on hover/focus so the note reads as interactive */ -.via-pdf-note:hover, -.via-pdf-note:focus-within { - box-shadow: - 0 10px 32px rgba(0, 0, 0, 0.18), - 0 3px 10px rgba(0, 0, 0, 0.10), - 0 0 0 2px var(--note-color, #ffd966); - z-index: 12; -} - -/* Header row — invisible until hover for a cleaner default state */ -.via-pdf-note-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 2px var(--size-4-2); - gap: var(--size-4-2); - flex-shrink: 0; - min-height: 22px; - /* Controls fade in on note hover */ - opacity: 0; - transition: opacity 120ms ease; - background: transparent; -} -.via-pdf-note:hover .via-pdf-note-header, -.via-pdf-note:focus-within .via-pdf-note-header { - opacity: 1; -} +/* ── DOCX page breaks ────────────────────────────────────────────────── */ -/* Drag handle */ -.via-pdf-note-drag { - display: flex; - align-items: center; - cursor: grab; - color: var(--text-faint); - user-select: none; - padding: 2px; - border-radius: var(--radius-s); - transition: color 80ms, background 80ms; -} -.via-pdf-note-drag:hover { color: var(--text-muted); background: var(--background-modifier-hover); } -.via-pdf-note-drag:active { cursor: grabbing; } -.via-pdf-note-drag svg { width: 13px; height: 13px; } - -/* Color dot indicator (center of header) */ -.via-pdf-note-color-dot { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--note-color, #ffd966); - flex-shrink: 0; - opacity: 0.7; - border: 1px solid rgba(0, 0, 0, 0.12); -} - -/* Delete button */ -.via-pdf-note-delete { - display: flex; - align-items: center; - justify-content: center; - background: transparent; +.via-docx-page-break { border: none; - cursor: pointer; - width: 20px; - height: 20px; - padding: 0; - border-radius: var(--radius-s); - color: var(--text-faint); - transition: background 80ms, color 80ms; - flex-shrink: 0; -} -.via-pdf-note-delete:hover { - background: var(--background-modifier-error-hover, rgba(255, 80, 80, 0.12)); - color: var(--text-error); -} -.via-pdf-note-delete svg { width: 12px; height: 12px; } - -/* Textarea */ -.via-pdf-note-text { - flex: 1; - border: none; - background: transparent; - resize: none; - padding: var(--size-4-1) var(--size-4-3) var(--size-4-3); - font-size: var(--font-ui-small); - font-family: var(--font-text); - color: var(--text-normal); - min-height: 72px; - outline: none; - line-height: 1.5; -} -.via-pdf-note-text::placeholder { - color: var(--text-faint); - font-style: italic; -} - -/* ── Spreadsheet viewer ──────────────────────────────────────────────── */ - -.via-sheet-wrapper { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} -.via-sheet-wrapper--toolbar-bottom { flex-direction: column-reverse; } - -.via-sheet-toolbar { - display: flex; - align-items: center; - gap: var(--size-4-1); - padding: var(--size-4-1) var(--size-4-3); - border-bottom: 1px solid var(--background-modifier-border); - background: var(--background-primary); - flex-shrink: 0; - min-height: 36px; - overflow-x: auto; - scrollbar-width: none; -} -.via-sheet-toolbar::-webkit-scrollbar { display: none; } -.via-sheet-wrapper--toolbar-bottom .via-sheet-toolbar { - border-bottom: none; - border-top: 1px solid var(--background-modifier-border); -} - -.via-sheet-file-label { - display: flex; - align-items: center; - gap: var(--size-4-1); - flex-shrink: 0; -} -.via-sheet-file-name { - font-size: var(--font-ui-small); - color: var(--text-muted); - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 180px; -} - -/* Sheet tabs */ -.via-sheet-tabs { - display: flex; - align-items: center; - gap: 2px; -} - -.via-sheet-tab { - padding: var(--size-4-1) var(--size-4-2); - font-size: var(--font-ui-small); - color: var(--text-muted); - border-radius: var(--radius-s); - cursor: pointer; - white-space: nowrap; - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; - transition: background 80ms, color 80ms; -} -.via-sheet-tab:hover { - background: var(--background-modifier-hover); - color: var(--text-normal); -} -.via-sheet-tab.is-active { - background: var(--background-modifier-active-hover); - color: var(--text-normal); - font-weight: 600; -} - -.via-sheet-info { - font-size: var(--font-ui-small); - color: var(--text-faint); - white-space: nowrap; - flex-shrink: 0; -} - -.via-sheet-scroll { - flex: 1; - overflow: auto; - background: var(--background-primary); -} - -.via-sheet-table-wrap { - min-width: 100%; -} - -.via-sheet-empty { - padding: var(--size-4-6); - color: var(--text-faint); - font-size: var(--font-ui-medium); - text-align: center; -} - -/* Table */ -.via-sheet-table { - border-collapse: collapse; - width: max-content; - min-width: 100%; - font-size: var(--font-ui-small); - font-family: var(--font-monospace); -} - -.via-sheet-table thead th { - position: sticky; - top: 0; - z-index: 2; - background: var(--background-secondary); - color: var(--text-muted); - font-weight: 600; - text-align: center; - padding: var(--size-4-1) var(--size-4-2); - border: 1px solid var(--background-modifier-border); - white-space: nowrap; - min-width: 64px; -} - -.via-sheet-table tbody td { - padding: var(--size-4-1) var(--size-4-2); - border: 1px solid var(--background-modifier-border); - color: var(--text-normal); - white-space: nowrap; - max-width: 300px; - overflow: hidden; - text-overflow: ellipsis; -} - -.via-sheet-table tbody tr:hover td { - background: var(--background-modifier-hover); -} - -/* Row number column */ -.via-sheet-row-num { - color: var(--text-faint); - font-weight: 600; - text-align: center; - background: var(--background-secondary); - min-width: 40px; - width: 40px; - user-select: none; - position: sticky; - left: 0; - z-index: 1; -} - -thead .via-sheet-row-num { - z-index: 3; -} - -/* ── PPTX canvas wrapper ─────────────────────────────────────────── */ - -.via-pptx-canvas-wrapper { - flex: 1; - overflow: auto; - display: flex; - justify-content: center; - align-items: center; - padding: 16px; - min-height: 0; - background: var(--background-secondary); -} - -.via-pptx-canvas { - box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2); - border-radius: 4px; - width: var(--via-canvas-width, auto); - height: var(--via-canvas-height, auto); -} - -.via-pptx-zoom-label { - min-width: 40px; - text-align: center; -} - -/* ── PowerPoint viewer ───────────────────────────────────────────────── */ - -.via-pptx-wrapper { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} -.via-pptx-wrapper--toolbar-bottom { flex-direction: column-reverse; } - -.via-pptx-toolbar { - display: flex; - align-items: center; - gap: var(--size-4-1); - padding: var(--size-4-1) var(--size-4-3); - border-bottom: 1px solid var(--background-modifier-border); - background: var(--background-primary); - flex-shrink: 0; - min-height: 36px; -} -.via-pptx-wrapper--toolbar-bottom .via-pptx-toolbar { - border-bottom: none; - border-top: 1px solid var(--background-modifier-border); -} - -.via-pptx-counter { - font-size: var(--font-ui-small); - font-variant-numeric: tabular-nums; - color: var(--text-muted); - white-space: nowrap; - min-width: 52px; - text-align: center; - user-select: none; -} - -.via-pptx-file-label { - display: flex; - align-items: center; - gap: var(--size-4-1); - flex-shrink: 0; -} -.via-pptx-file-name { - font-size: var(--font-ui-small); - color: var(--text-muted); - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 180px; -} - -.via-pptx-info { - font-size: var(--font-ui-small); - color: var(--text-faint); - white-space: nowrap; - flex-shrink: 0; -} - -.via-pptx-format-control { - display: flex; - align-items: center; - gap: var(--size-4-1); - flex-shrink: 0; -} - -.via-pptx-format-label { - font-size: var(--font-ui-smaller); - color: var(--text-faint); - user-select: none; -} - -.via-pptx-format-select { - height: 22px; - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - background: var(--background-primary); - color: var(--text-normal); - font-size: var(--font-ui-smaller); - padding: 0 var(--size-4-1); -} - -/* Body: strip + main */ -.via-pptx-body { - display: flex; - flex-direction: row; - flex: 1; - overflow: hidden; -} - -/* Slide strip sidebar */ -.via-pptx-strip { - width: 160px; - min-width: 120px; - flex-shrink: 0; - display: flex; - flex-direction: column; - gap: var(--size-4-1); - overflow-y: auto; - padding: var(--size-4-2); - border-right: 1px solid var(--background-modifier-border); - background: var(--background-secondary); -} - -.via-pptx-thumb { - display: flex; - align-items: flex-start; - gap: var(--size-4-1); - padding: var(--size-4-2); - border-radius: var(--radius-s); - cursor: pointer; - border: 2px solid transparent; - transition: background 80ms, border-color 80ms; -} -.via-pptx-thumb:hover { - background: var(--background-modifier-hover); -} -.via-pptx-thumb.is-active { - border-color: var(--interactive-accent); - background: var(--background-modifier-active-hover); -} - -.via-pptx-thumb-num { - font-size: var(--font-ui-small); - color: var(--text-faint); - font-weight: 600; - min-width: 20px; - flex-shrink: 0; -} - -.via-pptx-thumb-preview { - font-size: var(--font-ui-small); - color: var(--text-muted); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; -} - -/* Main slide scroll area */ -.via-pptx-scroll { - flex: 1; - overflow: auto; - display: flex; - justify-content: center; - align-items: flex-start; - padding: var(--size-4-6) var(--size-4-4); - background: var(--background-secondary); -} - -/* Slide card */ -.via-pptx-slide { - background: var(--background-primary); - border-radius: var(--radius-m); - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15), 0 1px 4px rgba(0, 0, 0, 0.08); - padding: var(--size-4-8); - width: 100%; - max-width: 860px; - min-height: 480px; - display: flex; - flex-direction: column; - gap: var(--size-4-4); - transform-origin: top center; - transition: transform 0.15s ease; -} - -.via-pptx-slide--canvas { - position: relative; - overflow: hidden; - padding: 0; - gap: 0; -} - -.via-pptx-slide[data-slide-bg='lt1'] { background: var(--background-primary); } -.via-pptx-slide[data-slide-bg='lt2'] { background: var(--background-secondary); } -.via-pptx-slide[data-slide-bg='dk1'] { background: var(--background-primary-alt); } -.via-pptx-slide[data-slide-bg='dk2'] { background: var(--background-secondary-alt); } - -.via-pptx-table-wrap { - width: fit-content; - max-width: 100%; - overflow: auto; - border-radius: var(--radius-s); - border: 1px solid var(--background-modifier-border); -} - -.via-pptx-table { - border-collapse: collapse; - font-size: var(--font-ui-small); - color: var(--text-normal); - min-width: 360px; -} - -.via-pptx-table th, -.via-pptx-table td { - border: 1px solid var(--background-modifier-border); - padding: var(--size-4-1) var(--size-4-2); - text-align: left; - vertical-align: top; -} - -.via-pptx-table th { - background: var(--interactive-accent); - color: var(--text-on-accent); - font-weight: 600; -} - -.via-pptx-table td { - background: var(--background-primary); -} - -.via-pptx-slide-img { - max-width: 100%; - border-radius: var(--radius-s); - display: block; - margin: 0 auto; -} - -.via-pptx-slide-picture { - position: absolute; - object-fit: fill; - pointer-events: none; -} - -.via-pptx-library-renderer { - width: 100%; - max-width: 960px; - min-height: 540px; - padding: 0; - overflow: auto; -} - -.via-pptx-library-host { - width: 100%; - min-height: 540px; -} - -/* ── PPTX shape blocks ────────────────────────────────────────────── */ - -.via-pptx-shape { - font-family: var(--font-text); - color: var(--text-normal); - line-height: 1.6; - position: relative; - transform-origin: top left; -} - -.via-pptx-shape--editable { - cursor: move; - border-radius: var(--radius-s); - outline: 1px dashed transparent; - transition: outline-color 80ms, box-shadow 80ms; -} - -.via-pptx-shape--editable:hover { - outline-color: var(--interactive-accent-hover); -} - -.via-pptx-shape--editable.is-selected { - outline-color: var(--interactive-accent); - box-shadow: 0 0 0 1px var(--interactive-accent); -} - -.via-pptx-shape-handle { - position: absolute; - width: var(--size-4-2); - height: var(--size-4-2); - border-radius: var(--radius-2xs); - border: 1px solid var(--background-primary); - background: var(--interactive-accent); - opacity: 0; - pointer-events: none; -} - -.via-pptx-shape--editable.is-selected .via-pptx-shape-handle { - opacity: 1; - pointer-events: auto; -} - -.via-pptx-shape-handle--nw { - left: calc(var(--size-4-1) * -1); - top: calc(var(--size-4-1) * -1); - cursor: nwse-resize; -} - -.via-pptx-shape-handle--ne { - right: calc(var(--size-4-1) * -1); - top: calc(var(--size-4-1) * -1); - cursor: nesw-resize; -} - -.via-pptx-shape-handle--sw { - left: calc(var(--size-4-1) * -1); - bottom: calc(var(--size-4-1) * -1); - cursor: nesw-resize; -} - -.via-pptx-shape-handle--se { - right: calc(var(--size-4-1) * -1); - bottom: calc(var(--size-4-1) * -1); - cursor: nwse-resize; -} - -.via-pptx-shape p { - margin: 0.25em 0; -} - -.via-pptx-shape[data-fill-style='accent'] { - background: var(--interactive-accent-hover); -} - -.via-pptx-shape[data-fill-style='muted'] { - background: var(--background-secondary-alt); -} - -.via-pptx-shape[data-fill-style='none'] { - background: transparent; -} - -.via-pptx-shape[data-stroke-style='accent'] { - border: 1px solid var(--interactive-accent); -} - -.via-pptx-shape[data-stroke-style='muted'] { - border: 1px solid var(--background-modifier-border-hover); -} - -.via-pptx-shape[data-stroke-style='normal'] { - border: 1px solid var(--background-modifier-border); -} - -.via-pptx-shape[data-stroke-style='none'] { - border: 1px solid transparent; -} - -/* Title shapes */ -.via-pptx-shape--title, -.via-pptx-shape--ctrTitle { - font-size: 1.5em; - font-weight: 700; - line-height: 1.3; -} - -.via-pptx-shape--ctrTitle { - text-align: center; -} - -/* Subtitle */ -.via-pptx-shape--subTitle { - font-size: 1.15em; - color: var(--text-muted); - font-weight: 500; -} - -/* Body text */ -.via-pptx-shape--body { - font-size: var(--font-ui-medium); -} - -/* Other / unknown shapes */ -.via-pptx-shape--other { - font-size: var(--font-ui-medium); -} - -/* Bullet lists within shapes */ -.via-pptx-list { - margin: 0.5em 0; - padding-left: 1.5em; - list-style-type: disc; -} -.via-pptx-list li { - margin: 0.25em 0; -} - -.via-pptx-slide-empty { - color: var(--text-faint); - font-style: italic; - text-align: center; - padding: var(--size-4-8); -} - -/* Hidden slide strip */ -.via-pptx-strip.is-hidden { - display: none; -} - -/* ── PPTX fullscreen mode ────────────────────────────────────────── */ - -.via-pptx-fullscreen .via-pptx-strip { - display: none; -} - -.via-pptx-fullscreen .via-pptx-slide { - max-width: none; - min-height: calc(100% - var(--size-4-8)); -} - -.via-pptx-fullscreen .via-pptx-scroll { - align-items: center; - justify-content: center; -} - -/* ── Spreadsheet formula bar ─────────────────────────────────────── */ - -.via-sheet-formula-bar { - display: flex; - align-items: center; - gap: var(--size-4-2); - padding: var(--size-4-1) var(--size-4-3); - border-bottom: 1px solid var(--background-modifier-border); - background: var(--background-primary); - flex-shrink: 0; - min-height: 30px; -} - -.via-sheet-formula-ref { - font-size: var(--font-ui-small); - font-family: var(--font-monospace); - font-weight: 600; - color: var(--text-muted); - min-width: 40px; - text-align: center; - user-select: none; -} - -.via-sheet-formula-sep { - width: 1px; - height: 16px; - background: var(--background-modifier-border); - flex-shrink: 0; -} - -.via-sheet-formula-input { - flex: 1; - height: 24px; - padding: 0 var(--size-4-2); - border: 1px solid var(--background-modifier-border); - border-radius: var(--radius-s); - background: var(--background-primary); - color: var(--text-normal); - font-size: var(--font-ui-small); - font-family: var(--font-monospace); - outline: none; - transition: border-color 100ms; -} -.via-sheet-formula-input:focus { - border-color: var(--interactive-accent); -} - -/* ── Spreadsheet cell inline edit input ──────────────────────────── */ - -.via-sheet-cell-input { - width: 100%; - height: 100%; - padding: var(--size-4-1) var(--size-4-2); - border: 2px solid var(--interactive-accent); - border-radius: 0; - background: var(--background-primary); - color: var(--text-normal); - font-size: var(--font-ui-small); - font-family: var(--font-monospace); - outline: none; - box-sizing: border-box; -} - -/* ── Spreadsheet selection ───────────────────────────────────────── */ - -.via-sheet-table thead th.is-selected-col { - background: var(--background-modifier-active-hover); - color: var(--text-normal); -} - -.via-sheet-table .via-sheet-row-num.is-selected-row { - background: var(--background-modifier-active-hover); - color: var(--text-normal); -} - -.via-sheet-table tbody td.is-selected { - outline: 2px solid var(--interactive-accent); - outline-offset: -1px; - background: var(--background-modifier-active-hover); -} - -/* Dirty state for save button */ -.clickable-icon.is-dirty { - position: relative; -} -.clickable-icon.is-dirty::after { - content: ''; - position: absolute; - top: 2px; - right: 2px; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--color-yellow); + border-top: 1px dashed var(--background-modifier-border); + margin: var(--size-4-6) 0; + opacity: 0.5; } diff --git a/versions.json b/versions.json index f63ea5e..dde9430 100644 --- a/versions.json +++ b/versions.json @@ -11,5 +11,6 @@ "1.4.0": "0.15.0", "1.4.1": "0.15.0", "1.4.2": "0.15.0", - "1.5.0": "0.15.0" + "1.5.0": "0.15.0", + "2.0.0": "0.15.0" }