diff --git a/package-lock.json b/package-lock.json index c3c451b..4a0b6bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "archiver": "^8.0.0", "markdown-it": "^15.0.0" }, "devDependencies": { + "@types/archiver": "^8.0.0", "@types/node": "^22.5.0", "concurrently": "^9.0.0", "electron": "^32.0.0", @@ -554,6 +556,17 @@ "node": ">=10" } }, + "node_modules/@types/archiver": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-8.0.0.tgz", + "integrity": "sha512-YpXPbEuv9+eUIPPQWUPahj3cvs9isWRuF+J4z+KbdYVDO3rWorWQFxUVHnwPu2AgKwvgpki5F2VMX0Xx+mX45A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/readdir-glob": "*" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -594,6 +607,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/readdir-glob": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", + "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -615,6 +638,18 @@ "@types/node": "*" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -654,6 +689,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/archiver": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-8.0.0.tgz", + "integrity": "sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==", + "license": "MIT", + "dependencies": { + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "is-stream": "^4.0.0", + "lazystream": "^1.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^3.0.0", + "tar-stream": "^3.0.0", + "zip-stream": "^7.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/argparse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.0.tgz", @@ -670,6 +734,12 @@ ], "license": "Python-2.0" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -690,6 +760,129 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", + "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "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/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -699,6 +892,42 @@ "license": "MIT", "optional": true }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -843,6 +1072,22 @@ "node": ">= 0.8" } }, + "node_modules/compress-commons": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz", + "integrity": "sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^7.0.1", + "is-stream": "^4.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/concurrently": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", @@ -868,6 +1113,37 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "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/crc32-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-7.0.1.tgz", + "integrity": "sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1186,6 +1462,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -1207,6 +1510,12 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -1543,6 +1852,32 @@ "node": ">= 6" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "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", @@ -1553,6 +1888,24 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/joi": { "version": "18.2.3", "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz", @@ -1607,6 +1960,48 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/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/lazystream/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/lazystream/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/linkify-it": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", @@ -1733,6 +2128,21 @@ "node": ">=4" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1750,6 +2160,15 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -1801,6 +2220,21 @@ "dev": true, "license": "MIT" }, + "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", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -1854,6 +2288,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-3.0.0.tgz", + "integrity": "sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/yqnn" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1913,6 +2378,26 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1969,6 +2454,26 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "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-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2026,6 +2531,36 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2094,6 +2629,12 @@ "node": ">= 4.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/wait-on": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", @@ -2188,6 +2729,20 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/zip-stream": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-7.0.5.tgz", + "integrity": "sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==", + "license": "MIT", + "dependencies": { + "compress-commons": "^7.0.0", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">=18" + } } } } diff --git a/package.json b/package.json index 141afd7..208ad65 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "test:e2e": "sh tests/run-e2e.sh" }, "devDependencies": { + "@types/archiver": "^8.0.0", "@types/node": "^22.5.0", "concurrently": "^9.0.0", "electron": "^32.0.0", @@ -26,6 +27,7 @@ "wait-on": "^8.0.0" }, "dependencies": { + "archiver": "^8.0.0", "markdown-it": "^15.0.0" } } diff --git a/src/main/export.ts b/src/main/export.ts new file mode 100644 index 0000000..f264db5 --- /dev/null +++ b/src/main/export.ts @@ -0,0 +1,77 @@ +import { ZipArchive } from "archiver"; +import fs from "fs"; +import path from "path"; +import { loadBook } from "./project.js"; +import { listChaptersInOrder } from "./chapters.js"; + +export function combineMarkdown(bookPath: string): string { + const meta = loadBook(bookPath); + const chapters = listChaptersInOrder(meta); + const parts: string[] = []; + parts.push(`# ${meta.title}`); + parts.push(""); + parts.push("_A book compiled with Folio._"); + for (const ch of chapters) { + let content = ""; + try { + content = fs.readFileSync(path.join(bookPath, ch.file), "utf-8"); + } catch { + content = ""; + } + const body = content.replace(/\s+$/, ""); + parts.push(""); + parts.push("---"); + parts.push(""); + parts.push(`## ${ch.title}`); + parts.push(""); + parts.push(body); + } + parts.push(""); + return parts.join("\n"); +} + +export async function buildZip(bookPath: string): Promise { + const meta = loadBook(bookPath); + const chapters = listChaptersInOrder(meta); + const chunks: Buffer[] = []; + const archive = new ZipArchive({ zlib: { level: 9 } }); + + return new Promise((resolve, reject) => { + archive.on("error", reject); + archive.on("data", (c: Buffer) => chunks.push(c)); + archive.on("end", () => resolve(Buffer.concat(chunks))); + + archive.append(`# ${meta.title}\n\n_A book compiled with Folio._\n`, { + name: "title_page.md", + }); + + archive.append(combineMarkdown(bookPath), { name: "book.md" }); + + for (const ch of chapters) { + let content = ""; + try { + content = fs.readFileSync(path.join(bookPath, ch.file), "utf-8"); + } catch { + content = ""; + } + const entryName = path.posix.join("chapters", path.basename(ch.file)); + archive.append(content, { name: entryName }); + } + + const manifest = { + title: meta.title, + app: meta.app, + version: meta.version, + generatedAt: new Date().toISOString(), + chapterCount: chapters.length, + chapters: chapters.map((c) => ({ + id: c.id, + title: c.title, + file: path.basename(c.file), + })), + }; + archive.append(JSON.stringify(manifest, null, 2), { name: "manifest.json" }); + + void archive.finalize(); + }); +} \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index 83eb3e6..1b3e612 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,5 +1,5 @@ import { app, BrowserWindow, dialog, ipcMain, shell } from "electron"; -import type { OpenDialogOptions } from "electron"; +import type { OpenDialogOptions, SaveDialogOptions } from "electron"; import fs from "fs"; import path from "path"; import { @@ -22,6 +22,7 @@ import { listChaptersInOrder, setChapterContent, } from "./chapters.js"; +import { combineMarkdown, buildZip } from "./export.js"; let mainWindow: BrowserWindow | null = null; let bookPath: string | null = null; @@ -35,6 +36,11 @@ function showOpenDialog(options: OpenDialogOptions) { return win ? dialog.showOpenDialog(win, options) : dialog.showOpenDialog(options); } +function showSaveDialog(options: SaveDialogOptions) { + const win = getWindow(); + return win ? dialog.showSaveDialog(win, options) : dialog.showSaveDialog(options); +} + function getState() { return { bookPath }; } @@ -251,6 +257,39 @@ ipcMain.handle("folio:getChapterList", () => { } }); +function currentBookTitle(): string { + try { + return loadBook(requireBook()).title; + } catch { + return "book"; + } +} + +ipcMain.handle("folio:exportMarkdown", async () => { + const bp = requireBook(); + const { canceled, filePath } = await showSaveDialog({ + title: "Export combined Markdown", + defaultPath: `${currentBookTitle()}.md`, + filters: [{ name: "Markdown", extensions: ["md"] }], + }); + if (canceled || !filePath) return { canceled: true } as const; + fs.writeFileSync(filePath, combineMarkdown(bp), "utf-8"); + return { ok: true, filePath }; +}); + +ipcMain.handle("folio:exportZip", async () => { + const bp = requireBook(); + const { canceled, filePath } = await showSaveDialog({ + title: "Export book bundle (ZIP)", + defaultPath: `${currentBookTitle()}.zip`, + filters: [{ name: "ZIP Archive", extensions: ["zip"] }], + }); + if (canceled || !filePath) return { canceled: true } as const; + const buf = await buildZip(bp); + fs.writeFileSync(filePath, buf); + return { ok: true, filePath }; +}); + ipcMain.handle("folio:saveChapter", (_e, id: string, content: string) => { try { setChapterContent(requireBook(), id, content); diff --git a/src/main/menu.ts b/src/main/menu.ts index dc6a377..b9596c9 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -42,6 +42,18 @@ export function buildMenu( }, }, { type: "separator" }, + { + label: "Export Markdown…", + accelerator: "CmdOrCtrl+Shift+E", + enabled: !!getState().bookPath, + click: () => getWindow()?.webContents.send("folio:export-markdown-requested"), + }, + { + label: "Export Bundle (ZIP)…", + enabled: !!getState().bookPath, + click: () => getWindow()?.webContents.send("folio:export-zip-requested"), + }, + { type: "separator" }, { label: "Close", accelerator: isMac ? "Cmd+W" : "Ctrl+W", diff --git a/src/main/preload.ts b/src/main/preload.ts index ad43bc5..201893c 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -25,6 +25,8 @@ contextBridge.exposeInMainWorld("folio", { ipcRenderer.invoke("folio:reorderChapters", newOrder), saveChapter: (id: string, content: string) => ipcRenderer.invoke("folio:saveChapter", id, content), + exportMarkdown: () => ipcRenderer.invoke("folio:exportMarkdown"), + exportZip: () => ipcRenderer.invoke("folio:exportZip"), // events onBookOpened: (cb: (data: { path: string; title: string }) => void) => @@ -41,4 +43,8 @@ contextBridge.exposeInMainWorld("folio", { ipcRenderer.on("folio:open-recent", (_e, p) => cb(p)), onRevealRequested: (cb: (p: string) => void) => ipcRenderer.on("folio:reveal-requested", (_e, p) => cb(p)), + onExportMarkdownRequested: (cb: () => void) => + ipcRenderer.on("folio:export-markdown-requested", () => cb()), + onExportZipRequested: (cb: () => void) => + ipcRenderer.on("folio:export-zip-requested", () => cb()), }); diff --git a/src/renderer/book.ts b/src/renderer/book.ts index 1090477..393d6cf 100644 --- a/src/renderer/book.ts +++ b/src/renderer/book.ts @@ -17,6 +17,8 @@ export type FolioAPI = { duplicateChapter: (id: string, title?: string) => Promise; reorderChapters: (newOrder: string[]) => Promise; saveChapter: (id: string, content: string) => Promise; + exportMarkdown: () => Promise; + exportZip: () => Promise; onBookOpened: (cb: (data: { path: string; title: string }) => void) => void; onBookClosed: (cb: () => void) => void; onChaptersChanged: (cb: () => void) => void; @@ -24,6 +26,8 @@ export type FolioAPI = { onOpenBookRequested: (cb: () => void) => void; onOpenRecent: (cb: (p: string) => void) => void; onRevealRequested: (cb: (p: string) => void) => void; + onExportMarkdownRequested: (cb: () => void) => void; + onExportZipRequested: (cb: () => void) => void; }; const $ = (id: string) => document.getElementById(id) as HTMLElement; @@ -68,6 +72,8 @@ export function initBook(api: FolioAPI) { const findPrevBtn = $("findPrev") as HTMLButtonElement; const findNextBtn = $("findNext") as HTMLButtonElement; const findCloseBtn = $("findClose") as HTMLButtonElement; + const exportBtn = $("exportBtn") as HTMLButtonElement; + const exportZipBtn = $("exportZipBtn") as HTMLButtonElement; const addChapterBtn = $("addChapterBtn") as HTMLButtonElement; const projectBar = $("projectBar"); @@ -282,6 +288,35 @@ export function initBook(api: FolioAPI) { sourceArea.focus(); } + function status(text: string, ms = 3000) { + saveStateEl.textContent = text; + saveStateEl.classList.add("unsaved"); + setTimeout(() => { + saveStateEl.textContent = dirty ? "Unsaved" : "Saved"; + saveStateEl.classList.toggle("unsaved", dirty); + }, ms); + } + + async function exportMarkdown() { + if (!currentBook) return; + const res = await api.exportMarkdown(); + if (res && "error" in res) alert((res as { error: string }).error); + else if (res && "filePath" in res) status(`Exported to ${basename(res.filePath)}`); + else if (res && "canceled" in res) status("Export canceled"); + } + + async function exportZip() { + if (!currentBook) return; + const res = await api.exportZip(); + if (res && "error" in res) alert((res as { error: string }).error); + else if (res && "filePath" in res) status(`Bundle saved to ${basename(res.filePath)}`); + else if (res && "canceled" in res) status("Export canceled"); + } + + function basename(p: string): string { + return p.split(/[/\\]/).pop() || p; + } + function countWords(text: string) { const trimmed = text.trim(); if (!trimmed) return 0; @@ -479,6 +514,8 @@ export function initBook(api: FolioAPI) { $("welcomeOpenBook").addEventListener("click", () => void startOpenBook()); newChapterBtn.addEventListener("click", () => void create()); addChapterBtn.addEventListener("click", () => void create()); + exportBtn.addEventListener("click", () => void exportMarkdown()); + exportZipBtn.addEventListener("click", () => void exportZip()); $("revealBtn").addEventListener("click", () => { if (currentBook) void api.revealInFolder(currentBook.path); }); @@ -519,6 +556,8 @@ export function initBook(api: FolioAPI) { api.onOpenBookRequested(() => void startOpenBook()); api.onOpenRecent((p) => void openPath(p)); api.onRevealRequested((p) => void api.revealInFolder(p)); + api.onExportMarkdownRequested(() => void exportMarkdown()); + api.onExportZipRequested(() => void exportZip()); void renderRecents(); } diff --git a/src/renderer/index.html b/src/renderer/index.html index 67e1b28..f8713d7 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -44,6 +44,8 @@
Saved words: 0 + +
diff --git a/tests/run-chapters-test.mjs b/tests/run-chapters-test.mjs index 0bfffe1..f5d0244 100644 --- a/tests/run-chapters-test.mjs +++ b/tests/run-chapters-test.mjs @@ -119,6 +119,41 @@ ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 0, 2)) === JSON.stringify([" ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 3, 0)) === JSON.stringify(["d", "a", "b", "c"]), "move last to top"); ok(JSON.stringify(moveInOrder(["a", "b", "c", "d"], 1, 1)) === JSON.stringify(["a", "b", "c", "d"]), "no-op on same index"); +// ---- export (combineMarkdown + buildZip) ---- +const exportOut = path.join(os.tmpdir(), `folio-export-${process.pid}.cjs`); +await esbuild.build({ + entryPoints: [path.resolve("src/main/export.ts")], + bundle: true, + platform: "node", + format: "cjs", + outfile: exportOut, + write: true, + logLevel: "silent", +}); +const exportMod = (await import(url.pathToFileURL(exportOut).href)).default; +const { combineMarkdown, buildZip } = exportMod; + +const b1 = createChapter(dir, "Intro"); +setChapterContent(dir, b1.id, "# Intro\n\nHello world\n\n- a\n- b"); +const b2 = createChapter(dir, "Scene"); +setChapterContent(dir, b2.id, "## Scene one\n\nIt was a night."); +const combined = combineMarkdown(dir); +ok(combined.includes("# Test Book"), "combined markdown starts with book title"); +ok(combined.includes("## Intro"), "combined markdown has chapter heading (Intro)"); +ok(combined.includes("## Scene"), "combined markdown has chapter heading (Scene)"); +ok(combined.includes("- a") && combined.includes("It was a night"), "combined markdown includes chapter content in order"); +ok(combined.startsWith("# Test Book"), "combined markdown first line is book title"); + +const zip = await buildZip(dir); +ok(Buffer.isBuffer(zip), "buildZip returns a Buffer"); +ok(zip.length > 0, "zip buffer is non-empty"); +ok(zip.includes("book.md"), "zip contains book.md"); +ok(zip.includes("title_page.md"), "zip contains title_page.md"); +ok(zip.includes("manifest.json"), "zip contains manifest.json"); +ok(zip.includes("chapters"), "zip contains a chapters/ folder"); +ok(zip.slice(0, 2).toString("ascii") === "PK", "zip starts with PK signature"); +fs.rmSync(exportOut, { force: true }); + fs.rmSync(projOut, { force: true }); fs.rmSync(chapOut, { force: true }); fs.rmSync(dir, { recursive: true, force: true });