diff --git a/package-lock.json b/package-lock.json index 4ce386e..3bd3439 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "license": "MIT", "dependencies": { "better-sqlite3": "^9.4.3", - "chrome-remote-interface": "^0.33.0" + "chrome-remote-interface": "^0.33.0", + "mammoth": "^1.12.0", + "pdf-parse": "^2.4.5" }, "devDependencies": { "@electron/rebuild": "^3.6.0", @@ -19,6 +21,7 @@ "@types/better-sqlite3": "^7.6.8", "@types/chrome-remote-interface": "^0.34.0", "@types/node": "^20.11.5", + "@types/pdf-parse": "^1.1.5", "electron": "^29.0.0", "electron-builder": "^24.9.1", "eslint": "^9.39.4", @@ -825,6 +828,190 @@ "node": ">=10" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@npmcli/fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", @@ -1002,6 +1189,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pdf-parse": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.5.tgz", + "integrity": "sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/plist": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", @@ -2277,7 +2474,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT" }, "node_modules/crc": { @@ -2491,6 +2687,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "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/dir-compare": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", @@ -2588,6 +2790,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "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", @@ -3680,6 +3891,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3838,9 +4055,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.7", @@ -3974,6 +4189,48 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4055,6 +4312,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4142,6 +4408,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -4203,6 +4480,69 @@ "node": ">=12" } }, + "node_modules/mammoth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", + "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "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/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.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/mammoth/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/mammoth/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/mammoth/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/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -4557,6 +4897,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -4664,6 +5010,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4691,7 +5043,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4741,6 +5092,38 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4850,9 +5233,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", @@ -5219,6 +5600,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5729,6 +6116,12 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "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": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/package.json b/package.json index 01e8319..2f6f194 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ }, "dependencies": { "better-sqlite3": "^9.4.3", - "chrome-remote-interface": "^0.33.0" + "chrome-remote-interface": "^0.33.0", + "mammoth": "^1.12.0", + "pdf-parse": "^2.4.5" }, "devDependencies": { "@electron/rebuild": "^3.6.0", @@ -28,6 +30,7 @@ "@types/better-sqlite3": "^7.6.8", "@types/chrome-remote-interface": "^0.34.0", "@types/node": "^20.11.5", + "@types/pdf-parse": "^1.1.5", "electron": "^29.0.0", "electron-builder": "^24.9.1", "eslint": "^9.39.4", diff --git a/src/main/db/applications.ts b/src/main/db/applications.ts index e7a8c97..7529566 100644 --- a/src/main/db/applications.ts +++ b/src/main/db/applications.ts @@ -17,6 +17,14 @@ export interface ApplicationFull extends ApplicationSummary { matches: { keyword_id: number; name: string; mention_count: number; blurb_id: number | null }[]; } +// TODO: add more stats and a potential nice graph using MatPlotLib's TS equivalent, gotta research on that what is +// though. +export interface AppStats { + total: number; + byStatus: Record; + avgMatches: number; +} + export function createApplication( db: Database.Database, data: { @@ -128,3 +136,23 @@ export function updateApplicationStatus(db: Database.Database, id: number, statu export function updateApplicationNotes(db: Database.Database, id: number, notes: string): void { db.prepare('UPDATE applications SET notes = ? WHERE id = ?').run(notes, id); } + +export function getStats(db: Database.Database): AppStats { + const total = (db.prepare('SELECT COUNT(*) as c FROM applications').get() as { c: number }).c; + + const statusRows = db + .prepare('SELECT status, COUNT(*) as c FROM applications GROUP BY status') + .all() as { status: string; c: number }[]; + const byStatus: Record = {}; + statusRows.forEach((r) => (byStatus[r.status] = r.c)); + + const avg = db + .prepare( + `SELECT AVG(cnt) as avg FROM ( + SELECT application_id, COUNT(*) as cnt FROM application_keyword_matches GROUP BY application_id + )` + ) + .get() as { avg: number | null }; + + return { total, byStatus, avgMatches: Math.round((avg.avg ?? 0) * 10) / 10 }; +} diff --git a/src/main/index.ts b/src/main/index.ts index df4a61a..7ce6fe4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,6 +5,8 @@ import { initDatabase, getDatabase } from './db/schema'; import { registerScraperIPC } from './ipc/scraper'; import { registerFileIPC } from './ipc/file'; import { registerDatabaseIPC } from './ipc/database'; +import { registerImporterIPC } from './ipc/importer'; +import { registerLibraryIPC } from './ipc/library'; import { seedKeywords } from './db/keywords'; import { seedTemplates } from './db/templates'; @@ -65,7 +67,11 @@ app.whenReady().then(() => { registerDatabaseIPC(); createWindow(); - if (mainWindow) registerScraperIPC(mainWindow); + if (mainWindow) { + registerScraperIPC(mainWindow); + registerImporterIPC(mainWindow); + registerLibraryIPC(mainWindow); + } app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); diff --git a/src/main/ipc/database.ts b/src/main/ipc/database.ts index bd7b174..2886ef8 100644 --- a/src/main/ipc/database.ts +++ b/src/main/ipc/database.ts @@ -44,6 +44,7 @@ export function registerDatabaseIPC(): void { ipcMain.handle('app:updateNotes', (_e, id: number, notes: string) => APP.updateApplicationNotes(db, id, notes) ); + ipcMain.handle('app:stats', () => APP.getStats(db)); ipcMain.handle('app:saveCoverLetter', (_e, id: number, html: string) => APP.saveCoverLetter(db, id, html) ); diff --git a/src/main/ipc/importer.ts b/src/main/ipc/importer.ts new file mode 100644 index 0000000..0a6a9e2 --- /dev/null +++ b/src/main/ipc/importer.ts @@ -0,0 +1,46 @@ +import { ipcMain, dialog, BrowserWindow } from 'electron'; +import * as fs from 'fs'; +import mammoth from 'mammoth'; + +export function registerImporterIPC(win: BrowserWindow): void { + ipcMain.handle('import:docx', async () => { + const { filePaths, canceled } = await dialog.showOpenDialog(win, { + title: 'Import DOCX', + filters: [{ name: 'Word Document', extensions: ['docx'] }], + properties: ['openFile'], + }); + if (canceled || !filePaths[0]) return { success: false, cancelled: true }; + + try { + const result = await mammoth.convertToHtml({ path: filePaths[0] }); + return { success: true, html: result.value }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : String(e) }; + } + }); + + ipcMain.handle('import:pdf', async () => { + const { filePaths, canceled } = await dialog.showOpenDialog(win, { + title: 'Import PDF', + filters: [{ name: 'PDF', extensions: ['pdf'] }], + properties: ['openFile'], + }); + if (canceled || !filePaths[0]) return { success: false, cancelled: true }; + + try { + + const pdfParseModule = await import('pdf-parse'); + const pdfParse = (pdfParseModule as unknown as { default: (buf: Buffer) => Promise<{ text: string }> }).default + ?? (pdfParseModule as unknown as (buf: Buffer) => Promise<{ text: string }>); + const buffer = fs.readFileSync(filePaths[0]); + const data = await pdfParse(buffer); + const html = data.text + .split(/\n{2,}/) + .map((p: string) => `

${p.trim().replace(/\n/g, ' ')}

`) + .join('\n'); + return { success: true, html }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : String(e) }; + } + }); +} diff --git a/src/main/ipc/library.ts b/src/main/ipc/library.ts new file mode 100644 index 0000000..f6b0879 --- /dev/null +++ b/src/main/ipc/library.ts @@ -0,0 +1,62 @@ +import { ipcMain, dialog, BrowserWindow, app as electronApp } from 'electron'; +import * as fs from 'fs'; +import { getDatabase } from '../db/schema'; +import { createKeyword, addBlurb, getKeywordsDetails } from '../db/keywords'; + +interface V1Blurb { keywords: string[]; description: string } +type V1Format = Record; + +interface V2Format { + keywords: { name: string; triggers: string[]; blurbs: { label: string; content_html: string; is_default: boolean }[] }[]; +} + +export function registerLibraryIPC(win: BrowserWindow): void { + const db = getDatabase(); + + ipcMain.handle('kw:importFile', async () => { + const { filePaths, canceled } = await dialog.showOpenDialog(win, { + title: 'Import Blurb Library', + filters: [{ name: 'JSON', extensions: ['json'] }], + properties: ['openFile'], + }); + if (canceled || !filePaths[0]) return { success: false, cancelled: true }; + + try { + const raw = JSON.parse(fs.readFileSync(filePaths[0], 'utf8')); + let imported = 0; + + if (Array.isArray(raw.keywords)) { + const data = raw as V2Format; + for (const kw of data.keywords) { + const id = createKeyword(db, kw.name, kw.triggers); + for (const b of kw.blurbs) addBlurb(db, id, b.label, b.content_html, b.is_default); + imported++; + } + } else { + const data = raw as V1Format; + for (const [name, entry] of Object.entries(data)) { + const id = createKeyword(db, name, entry.keywords); + addBlurb(db, id, 'Imported', entry.description, true); + imported++; + } + } + + return { success: true, imported }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : String(e) }; + } + }); + + ipcMain.handle('kw:exportFile', async () => { + const { filePath, canceled } = await dialog.showSaveDialog(win, { + title: 'Export Blurb Library', + defaultPath: `blurb-library-${new Date().toISOString().slice(0, 10)}.json`, + filters: [{ name: 'JSON', extensions: ['json'] }], + }); + if (canceled || !filePath) return { success: false, cancelled: true }; + + const keywords = getKeywordsDetails(db); + fs.writeFileSync(filePath, JSON.stringify({ keywords }, null, 2), 'utf8'); + return { success: true, filePath }; + }); +} diff --git a/src/main/preload.ts b/src/main/preload.ts index be72d72..36c5a85 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -22,7 +22,9 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('kw:updateBlurb', id, label, contentHtml), deleteBlurb: (id: number) => ipcRenderer.invoke('kw:deleteBlurb', id), setDefaultBlurb: (keywordId: number, blurbId: number) => - ipcRenderer.invoke('kw:setDefaultBlurb', keywordId, blurbId) + ipcRenderer.invoke("kw:setDefaultBlurb", keywordId, blurbId), + importFile: () => ipcRenderer.invoke('kw:importFile'), + exportFile: () => ipcRenderer.invoke('kw:exportFile'), }, templates: { @@ -42,6 +44,12 @@ contextBridge.exposeInMainWorld('electronAPI', { updateNotes: (id: number, notes: string) => ipcRenderer.invoke('app:updateNotes', id, notes), saveCoverLetter: (id: number, html: string) => - ipcRenderer.invoke('app:saveCoverLetter', id, html) - } + ipcRenderer.invoke('app:saveCoverLetter', id, html), + stats: () => ipcRenderer.invoke('app:stats') + }, + + importFile: { + docx: () => ipcRenderer.invoke('import:docx'), + pdf: () => ipcRenderer.invoke('import:pdf'), + }, }); diff --git a/src/renderer/editor.ts b/src/renderer/editor.ts index ca23621..6c8ae7b 100644 --- a/src/renderer/editor.ts +++ b/src/renderer/editor.ts @@ -1,13 +1,44 @@ export const Editor = (() => { let root: HTMLElement; + let textArea: HTMLTextAreaElement; + let previewFrame: HTMLIFrameElement; + let mode: 'visual' | 'text' | 'preview' = 'visual'; let undoStack: string[] = []; let redoStack: string[] = []; const MAX_STACK = 20; // TODO: migrate this to be its own proper config/settings thing let pushTimer: ReturnType | null = null; - - function init(elementId: string): void { + const KATEX_HEAD = ` + + + `; + + function stripDocument(html: string): string { + const bodyMatch = html.match(/]*>([\s\S]*)<\/body>/i); + const inner = bodyMatch ? bodyMatch[1] : html; + return inner + .replace(//gi, '') + .replace(//gi, '') + .replace(/<\/?(html|head|body)[^>]*>/gi, '') + .trim(); + } + + function init(elementId: string, textAreaId: string, previewId: string): void { root = document.getElementById(elementId) as HTMLElement; - root.contentEditable = 'true'; + textArea = document.getElementById(textAreaId) as HTMLTextAreaElement; + previewFrame = document.getElementById(previewId) as HTMLIFrameElement; + root.contentEditable = "true"; resetWithMarker(); pushSnapshot(true); @@ -86,7 +117,8 @@ export const Editor = (() => { root.focus(); const marker = root.querySelector('.insertion-marker'); const wrapper = document.createElement('div'); - wrapper.innerHTML = html; + // See comment under setHTML + wrapper.innerHTML = stripDocument(html); const newMarker = 'insert here'; @@ -107,16 +139,59 @@ export const Editor = (() => { function getHTML(): string { // strip the marker otherwise it appears on the actual final output - return root.innerHTML.replace(/]*>.*?<\/span>/g, ''); + const source = mode === 'text' ? textArea.value : root.innerHTML; + return source.replace(/]*>.*?<\/span>/g, ''); } function setHTML(html: string): void { - root.innerHTML = html; + // Pretty sure the reason why the UI was breaking was because it'd try to render the full base template which + // had HTML elements of its own, this should now strip it clean. + const clean = stripDocument(html) + root.innerHTML = clean; + textArea.value = clean; undoStack = []; redoStack = []; pushSnapshot(true); } + function insertAtExp(html: string): void { + const clean = stripDocument(html); + if (mode === 'text') { + const start = textArea.selectionStart; + const end = textArea.selectionEnd; + textArea.value = textArea.value.slice(0, start) + clean + textArea.value.slice(end); + } else { + insertBlurbAtMarker(clean); + } + } + + function setMode(next: 'visual' | 'text' | 'preview'): void { + if (mode === 'visual' && next !== 'visual') syncFromVisual(); + if (mode === 'text' && next !== 'text') syncFromText(); + + mode = next; + root.style.display = mode === 'visual' ? 'block' : 'none'; + textArea.style.display = mode === 'text' ? 'block' : 'none'; + previewFrame.style.display = mode === 'preview' ? 'block' : 'none'; + + if (mode === 'preview') { + previewFrame.srcdoc = `${KATEX_HEAD}${getHTML()}`; + } + } + + function syncFromVisual(): void { + textArea.value = getHTML(); + } + + function syncFromText(): void { + root.innerHTML = textArea.value; + pushSnapshot(); + } + + function getMode(): string { + return mode; + } + return { init, exec, @@ -127,6 +202,9 @@ export const Editor = (() => { getHTML, setHTML, undo, - redo + redo, + insertAtExp, + setMode, + getMode, }; })(); diff --git a/src/renderer/index.html b/src/renderer/index.html index 46fd768..2c06526 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -11,54 +11,148 @@ -
-
- - -
-
-
-
-
- - +
+
+

Keyword Confidence

+
+
+ +
+

Cover Letter

+ +
+
+ + +
- + + + + + + + + + + + + + + +
-
-
Scan Terminal
-

+							
+ + + +
+ + + +
+
+
+ + + +