From e10104cd2099cc4840e393985c06e4bf0bf8a6bd Mon Sep 17 00:00:00 2001 From: Novout Date: Wed, 10 Jun 2026 18:55:36 -0300 Subject: [PATCH 01/30] chore: added claude.md --- CLAUDE.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..74e9cd57 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,113 @@ +# betterwrite — CLAUDE.md + +## Project Overview + +**Better Write** is a 100% client-side creative word processor built as a Vue 3 + TypeScript monorepo. There is no backend — all data lives in the browser (IndexedDB). Exports (PDF, DOCX, HTML, TXT, EPUB) are generated entirely in the browser. The project is deployed as a PWA at betterwrite.io. + +## Monorepo Structure + +Managed with **pnpm workspaces** + **Lerna** + **Nx** (task caching). + +``` +packages/ + app/ # Main Vue 3 SPA — the entry point for everything + better-write-types/ # Shared TypeScript types — check here first before defining new types + contenteditable-ast/ # Custom AST for the entity-model text editor + client-storage/ # IndexedDB abstraction layer + extension/ # .bw file format (ZIP + data.json) + plugin-*/ # Feature plugins (characters, theme, shortcuts, voice, etc.) + plugin-exporter-*/ # Export generators (pdf, docx, html, txt, epub) + plugin-importer/ # File import handling + google-fonts-api/ # Google Fonts integration + color-converter/ # Color utility + image-converter/ # Image processing + languages/ # i18n translation files +``` + +## Key Architecture Concepts + +### Entity Model +The document is not stored as raw HTML. Content is a tree of **entities** (paragraphs, headings, images, page breaks, etc.), each with a `type`, `raw` text, and optional visual overrides. See `packages/better-write-types` and `docs/ENTITY_MODEL.md`. + +### Plugin System +Plugins receive three injection points: `emitter` (mitt event bus), `stores` (Pinia), and `hooks` (lifecycle). All plugins initialize in `packages/app/src/App.vue`. When adding a feature, check whether it belongs in an existing plugin or as a new `plugin-*` package. See `docs/PLUGIN_SYSTEM.md`. + +### State Management +Pinia stores live in `packages/app/src/store/`. There are ~14 stores. Prefer composables in `packages/app/src/use/` over direct store access in components. + +### Export Pipeline +Each export format is a separate `plugin-exporter-*` package. Generators receive the entity tree and produce browser-downloadable output. See `docs/GENERATOR_FLOW.md`. + +### .bw Extension Format +Project files saved as `.bw` are a ZIP archive containing `data.json`. Handled by `packages/extension/`. See `docs/PROJECT_FLOW.md`. + +## Development Setup + +```bash +# Install dependencies +pnpm install + +# Start dev server (port 3000) +pnpm dev + +# Build all packages +pnpm build + +# Run tests +pnpm test + +# Format code +pnpm lint +``` + +Requires a `.env.local` file in `packages/app/` — copy from `.env.example`. + +Node 16+ and pnpm 8+ required. + +## Coding Conventions + +- **TypeScript strict mode** — no `any` without justification. +- **No semicolons**, **single quotes**, **2-space indent** (Prettier enforced). +- **Conventional Commits** required: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:`, `ci:`. +- **Vue 3 Composition API** — use ` diff --git a/packages/app/src/components/page/about/AboutInfo.vue b/packages/app/src/components/page/about/AboutInfo.vue index 08d7063c..0054ce15 100644 --- a/packages/app/src/components/page/about/AboutInfo.vue +++ b/packages/app/src/components/page/about/AboutInfo.vue @@ -70,9 +70,6 @@
- - - diff --git a/packages/app/src/components/page/about/AboutPortability.vue b/packages/app/src/components/page/about/AboutPortability.vue index 781c2f8d..ca3f4cfc 100644 --- a/packages/app/src/components/page/about/AboutPortability.vue +++ b/packages/app/src/components/page/about/AboutPortability.vue @@ -110,7 +110,9 @@ class="flex flex-col gap-5" > .DOCX + .TXT .PDF .BW diff --git a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderVault.vue b/packages/app/src/components/page/editor/header/items/EditorBaseHeaderVault.vue deleted file mode 100644 index 660fef73..00000000 --- a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderVault.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/packages/app/src/components/page/editor/main/EditorBaseHeader.vue b/packages/app/src/components/page/editor/main/EditorBaseHeader.vue index fee654bf..81c027ae 100644 --- a/packages/app/src/components/page/editor/main/EditorBaseHeader.vue +++ b/packages/app/src/components/page/editor/main/EditorBaseHeader.vue @@ -22,7 +22,6 @@ -
diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index 2cc22326..337a4f30 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "baseUrl": ".", - "ignoreDeprecations": "6.0", "target": "esnext", "useDefineForClassFields": true, "module": "esnext", diff --git a/packages/languages/tsconfig.json b/packages/languages/tsconfig.json index 1cb154a5..1d60880f 100644 --- a/packages/languages/tsconfig.json +++ b/packages/languages/tsconfig.json @@ -11,7 +11,6 @@ "exclude": ["dist", "node_modules"], "compilerOptions": { "baseUrl": ".", - "ignoreDeprecations": "6.0", "rootDir": ".", "outDir": "dist", "sourceMap": false, diff --git a/packages/plugin-dropbox/.gitignore b/packages/plugin-dropbox/.gitignore deleted file mode 100644 index 25322f72..00000000 --- a/packages/plugin-dropbox/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -.vscode -.DS_Store - -node_modules -dist -yarn-error.log \ No newline at end of file diff --git a/packages/plugin-dropbox/.prettierrc.json b/packages/plugin-dropbox/.prettierrc.json deleted file mode 100644 index a9c1b141..00000000 --- a/packages/plugin-dropbox/.prettierrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "tabWidth": 2, - "singleQuote": true, - "semi": false, - "vueIndentScriptAndStyle": true -} diff --git a/packages/plugin-dropbox/package.json b/packages/plugin-dropbox/package.json deleted file mode 100644 index 22b9f29d..00000000 --- a/packages/plugin-dropbox/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "better-write-plugin-dropbox", - "version": "1.3.27", - "author": "Novout", - "license": "MIT", - "keywords": [ - "vue", - "hook", - "text", - "editor" - ], - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "scripts": { - "test": "echo \"Error: no test specified\"", - "build": "tsup" - }, - "files": [ - "dist/**/*", - "package.json", - "LICENSE", - "README.md" - ], - "dependencies": { - "better-write-client-storage": "^1.3.27", - "better-write-extension": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", - "vue-demi": "latest" - }, - "devDependencies": { - "prettier": "2.5.1" - }, - "peerDependencies": { - "vue": "^2.0.0 || >=3.0.0" - } -} diff --git a/packages/plugin-dropbox/src/index.ts b/packages/plugin-dropbox/src/index.ts deleted file mode 100644 index 3fd2c30f..00000000 --- a/packages/plugin-dropbox/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PluginTypes } from 'better-write-types' -import { createPlugin } from 'better-write-plugin-core' -import { DropboxSet } from './set' -import { DropboxToken } from './token' - -export const DropboxPlugin = (): PluginTypes.Plugin => - createPlugin({ name: 'dropbox' }, [DropboxSet, DropboxToken]) diff --git a/packages/plugin-dropbox/src/set.ts b/packages/plugin-dropbox/src/set.ts deleted file mode 100644 index ae807eca..00000000 --- a/packages/plugin-dropbox/src/set.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { - Dropbox as DBX, - DropboxAuth, - DropboxResponse, - DropboxResponseError, - files, -} from 'dropbox' -import { On } from 'better-write-plugin-core' -import { ClientStorageOptions, PluginTypes } from 'better-write-types' -import { readBW, writeBW } from 'better-write-extension' -import { exclude, set } from 'better-write-client-storage' - -export const DropboxSet = ( - emitter: PluginTypes.PluginEmitter, - stores: PluginTypes.PluginStores, - hooks: PluginTypes.PluginHooks, -) => { - On.externals().PluginDropboxSave(emitter, [ - () => { - if (!stores.AUTH.account.dropboxAccessToken) { - return - } - - const dbx = new DBX({ - clientId: hooks.env.dropboxKey(), - accessToken: stores.AUTH.account.dropboxAccessToken, - }) - - const path = `/${hooks.project.utils().exportFullName('bw')}` - - hooks.toast.info(hooks.i18n.t('toast.generics.load')) - - emitter.emit('plugin-progress-start') - - hooks.storage.normalize().then(async () => { - const payload = hooks.storage.getProjectObject() - payload.project.externalProvider = 'dropbox' - - const obj = JSON.stringify(payload) - const zip = await writeBW(obj) - - dbx - .filesUpload({ - path, - contents: zip, - mode: { - '.tag': 'overwrite', - }, - }) - .then(({ result }) => { - if (!stores.VAULT.dropboxFiles.some(({ id }) => id === result.id)) - stores.VAULT.dropboxFiles.unshift( - result as files.FileMetadataReference, - ) - - hooks.toast.success(hooks.i18n.t('toast.dropbox.save')) - }) - .catch(() => { - hooks.toast.error(hooks.i18n.t('toast.project.error')) - }) - .finally(() => { - emitter.emit('plugin-progress-end') - }) - }) - }, - () => {}, - ]) - - On.externals().PluginDropboxLoad(emitter, [ - async () => { - if (!stores.AUTH.account.dropboxAccessToken) { - return - } - - const dbx = new DBX({ - accessToken: stores.AUTH.account.dropboxAccessToken, - }) - - hooks.plugin.emit('plugin-progress-start') - - const files = await dbx.filesListFolder({ path: '' }) - - if (!files || files.result.entries.length === 0) { - hooks.toast.warning(hooks.i18n.t('toast.dropbox.empty')) - - hooks.plugin.emit('plugin-progress-end') - - return - } - - const targets = (files.result.entries.filter( - // @ts-expect-error - (file) => file?.is_downloadable && file?.['.tag'] === 'file', - ) ?? []) as files.FileMetadataReference[] - - stores.VAULT.dropboxFiles.unshift(...targets) - - hooks.plugin.emit('plugin-progress-end') - - hooks.toast.success(hooks.i18n.t('toast.generics.successChanged')) - }, - () => {}, - ]) - - On.externals().PluginDropboxDelete(emitter, [ - (file: files.FileMetadataReference) => { - if (!stores.AUTH.account.dropboxAccessToken) return - - if (!confirm(hooks.i18n.t('toast.generics.fileDelete'))) return - - const dbx = new DBX({ - accessToken: stores.AUTH.account.dropboxAccessToken, - }) - - hooks.plugin.emit('plugin-progress-start') - - dbx - .filesDeleteV2({ path: file.id }) - .then(() => { - stores.VAULT.dropboxFiles = stores.VAULT.dropboxFiles.filter( - ({ id }) => id !== file.id, - ) - }) - .catch((err: DropboxResponseError) => { - hooks.toast.error(hooks.i18n.t('toast.project.error')) - }) - .finally(() => { - hooks.plugin.emit('plugin-progress-end') - }) - }, - () => {}, - ]) - - On.externals().PluginDropboxSet(emitter, [ - (file: files.FileMetadataReference) => { - if (!stores.AUTH.account.dropboxAccessToken) return - - const dbx = new DBX({ - accessToken: stores.AUTH.account.dropboxAccessToken, - }) - - hooks.plugin.emit('plugin-progress-start') - - dbx - .filesDownload({ path: file.id }) - .then(async (data: DropboxResponse) => { - const bw = await readBW(data.result.fileBlob as Blob) - const content = hooks.storage.support(bw) - - hooks.project.onLoadProject(content) - }) - .catch(() => { - hooks.toast.error(hooks.i18n.t('toast.project.error')) - }) - .finally(() => { - hooks.plugin.emit('plugin-progress-end') - }) - }, - () => {}, - ]) - - On.externals().PluginDropboxConnect(emitter, [ - async () => { - const dbxAuth = new DropboxAuth({ - clientId: hooks.env.dropboxKey(), - }) - - const url = await dbxAuth.getAuthenticationUrl( - hooks.env.getCorrectLocalUrl(), - 'dropboxbw', - 'code', - 'online', - [ - 'account_info.read', - 'files.metadata.write', - 'files.metadata.read', - 'files.content.write', - 'files.content.read', - 'file_requests.read', - ], - undefined, - true, - ) - - exclude('code_verifier') - await set( - 'code_verifier', - dbxAuth.getCodeVerifier(), - stores.EDITOR.configuration.clientStorage as ClientStorageOptions, - ) - - window.open(url as string, '_self') - }, - () => {}, - ]) -} diff --git a/packages/plugin-dropbox/src/token.ts b/packages/plugin-dropbox/src/token.ts deleted file mode 100644 index 096e4383..00000000 --- a/packages/plugin-dropbox/src/token.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { exclude, get } from 'better-write-client-storage' -import { ClientStorageOptions, PluginTypes } from 'better-write-types' -import { DropboxAuth } from 'dropbox' - -export const DropboxToken = ( - emitter: PluginTypes.PluginEmitter, - stores: PluginTypes.PluginStores, - hooks: PluginTypes.PluginHooks, -) => { - emitter.on('call-editor-mounted', async () => { - const params = hooks.vueuse.core.useUrlSearchParams() - const code = params?.code - const state = params?.state - const code_verifier = await get( - 'code_verifier', - stores.EDITOR.configuration.clientStorage as ClientStorageOptions, - ) - - if (code && code_verifier && state === 'dropboxbw') { - const dbxAuth = new DropboxAuth({ - clientId: hooks.env.dropboxKey(), - }) - dbxAuth.setCodeVerifier(code_verifier) - - const token = await dbxAuth.getAccessTokenFromCode( - hooks.env.getCorrectLocalUrl(), - code, - ) - - stores.AUTH.account.dropboxAccessToken = token?.result // @ts-expect-error - ?.access_token as string - - await exclude('code_verifier') - - hooks.toast.success(hooks.i18n.t('toast.dropbox.load')) - } - }) -} diff --git a/packages/plugin-dropbox/tsconfig.json b/packages/plugin-dropbox/tsconfig.json deleted file mode 100644 index bdc00469..00000000 --- a/packages/plugin-dropbox/tsconfig.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "include": ["src/**/*.ts"], - "exclude": ["dist", "node_modules"], - "compilerOptions": { - "baseUrl": ".", - "rootDir": ".", - "outDir": "dist", - "sourceMap": false, - "noEmit": true, - "declaration": true, - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "skipLibCheck": true, - "noUnusedLocals": true, - "strictNullChecks": true, - "noImplicitAny": true, - "noImplicitThis": true, - "noImplicitReturns": true, - "strict": true, - "isolatedModules": false, - "experimentalDecorators": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "removeComments": false, - "strictPropertyInitialization": false, - "jsx": "preserve", - "lib": ["esnext", "dom"], - "types": ["node"], - "plugins": [ - { - "name": "@vuedx/typescript-plugin-vue" - } - ] - } -} diff --git a/packages/plugin-dropbox/tsup.config.ts b/packages/plugin-dropbox/tsup.config.ts deleted file mode 100644 index b0833c89..00000000 --- a/packages/plugin-dropbox/tsup.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'tsup' - -export default defineConfig({ - entry: ['src/index.ts'], - format: ['esm', 'cjs'], - clean: true, - dts: true, - external: ['dropbox'], -}) diff --git a/packages/plugin-editor-window/tsconfig.json b/packages/plugin-editor-window/tsconfig.json index b016643a..bdc00469 100644 --- a/packages/plugin-editor-window/tsconfig.json +++ b/packages/plugin-editor-window/tsconfig.json @@ -3,7 +3,6 @@ "exclude": ["dist", "node_modules"], "compilerOptions": { "baseUrl": ".", - "ignoreDeprecations": "6.0", "rootDir": ".", "outDir": "dist", "sourceMap": false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db954d17..7c99dcfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,9 +133,6 @@ importers: better-write-plugin-core: specifier: ^1.3.27 version: link:../plugin-core - better-write-plugin-dropbox: - specifier: ^1.3.27 - version: link:../plugin-dropbox better-write-plugin-editor-window: specifier: ^1.3.27 version: link:../plugin-editor-window @@ -271,7 +268,7 @@ importers: version: 8.46.3(eslint@9.39.1)(typescript@5.9.3) '@vitejs/plugin-vue': specifier: 6.0.1 - version: 6.0.1(vite@8.0.16)(vue@3.5.14) + version: 6.0.1(vite@7.1.12)(vue@3.5.14) '@vue/eslint-config-typescript': specifier: 14.6.0 version: 14.6.0(eslint-plugin-vue@10.5.1)(eslint@9.39.1)(typescript@5.9.3) @@ -315,29 +312,29 @@ importers: specifier: 30.0.0 version: 30.0.0(vue@3.5.14) vite: - specifier: 8.0.16 - version: 8.0.16(esbuild@0.25.11) + specifier: 7.1.12 + version: 7.1.12 vite-plugin-checker: specifier: 0.11.0 - version: 0.11.0(eslint@9.39.1)(typescript@5.9.3)(vite@8.0.16)(vue-tsc@1.8.8) + version: 0.11.0(eslint@9.39.1)(typescript@5.9.3)(vite@7.1.12)(vue-tsc@1.8.8) vite-plugin-optimize-persist: specifier: 0.1.2 - version: 0.1.2(vite-plugin-package-config@0.1.1)(vite@8.0.16) + version: 0.1.2(vite-plugin-package-config@0.1.1)(vite@7.1.12) vite-plugin-package-config: specifier: 0.1.1 - version: 0.1.1(vite@8.0.16) + version: 0.1.1(vite@7.1.12) vite-plugin-package-version: specifier: 1.1.0 - version: 1.1.0(vite@8.0.16) + version: 1.1.0(vite@7.1.12) vite-plugin-pwa: specifier: 1.1.0 - version: 1.1.0(vite@8.0.16)(workbox-build@7.3.0)(workbox-window@7.3.0) + version: 1.1.0(vite@7.1.12)(workbox-build@7.3.0)(workbox-window@7.3.0) vite-plugin-sitemap: specifier: 0.4.2 version: 0.4.2 vite-plugin-windicss: specifier: 1.9.4 - version: 1.9.4(vite@8.0.16) + version: 1.9.4(vite@7.1.12) vue-tsc: specifier: 1.8.8 version: 1.8.8(typescript@5.9.3) @@ -433,31 +430,6 @@ importers: specifier: latest version: 0.14.10(@vue/composition-api@1.0.0-rc.1)(vue@3.5.14) - packages/plugin-dropbox: - dependencies: - better-write-client-storage: - specifier: ^1.3.27 - version: link:../client-storage - better-write-extension: - specifier: ^1.3.27 - version: link:../extension - better-write-plugin-core: - specifier: ^1.3.27 - version: link:../plugin-core - better-write-types: - specifier: ^1.3.27 - version: link:../types - vue: - specifier: ^2.0.0 || >=3.0.0 - version: 3.5.14(typescript@5.9.3) - vue-demi: - specifier: latest - version: 0.14.10(@vue/composition-api@1.0.0-rc.1)(vue@3.5.14) - devDependencies: - prettier: - specifier: 2.5.1 - version: 2.5.1 - packages/plugin-editor-window: dependencies: '@vue/composition-api': @@ -1869,31 +1841,6 @@ packages: resolution: {integrity: sha512-MmFKN0DEIS+78wtfag7DiQDuE7eSpHRt4tYh0m8bEUnxbH1v2pieQ6Ir+1WZ3Xxkkf5L5tmDfeYQtCSwUz1Hyg==} dev: false - /@emnapi/core@1.10.0: - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - requiresBuild: true - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - dev: true - optional: true - - /@emnapi/runtime@1.10.0: - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - requiresBuild: true - dependencies: - tslib: 2.8.1 - dev: true - optional: true - - /@emnapi/wasi-threads@1.2.1: - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - requiresBuild: true - dependencies: - tslib: 2.8.1 - dev: true - optional: true - /@esbuild/aix-ppc64@0.25.11: resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} @@ -2758,19 +2705,6 @@ packages: tslib: 2.8.1 dev: false - /@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): - resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} - requiresBuild: true - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - dev: true - optional: true - /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3238,10 +3172,6 @@ packages: '@octokit/openapi-types': 18.1.1 dev: true - /@oxc-project/types@0.133.0: - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} - dev: true - /@parcel/watcher@2.0.4: resolution: {integrity: sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==} engines: {node: '>= 10.0.0'} @@ -3262,152 +3192,10 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false - /@rolldown/binding-android-arm64@1.0.3: - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-darwin-arm64@1.0.3: - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-darwin-x64@1.0.3: - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-freebsd-x64@1.0.3: - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-linux-arm-gnueabihf@1.0.3: - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-linux-arm64-gnu@1.0.3: - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-linux-arm64-musl@1.0.3: - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-linux-ppc64-gnu@1.0.3: - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-linux-s390x-gnu@1.0.3: - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-linux-x64-gnu@1.0.3: - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-linux-x64-musl@1.0.3: - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-openharmony-arm64@1.0.3: - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-wasm32-wasi@1.0.3: - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - requiresBuild: true - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - dev: true - optional: true - - /@rolldown/binding-win32-arm64-msvc@1.0.3: - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rolldown/binding-win32-x64-msvc@1.0.3: - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@rolldown/pluginutils@1.0.0-beta.29: resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} dev: true - /@rolldown/pluginutils@1.0.1: - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - dev: true - /@rollup/plugin-babel@5.3.1(@babel/core@7.28.5)(rollup@2.79.2): resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -3842,14 +3630,6 @@ packages: minimatch: 9.0.5 dev: true - /@tybys/wasm-util@0.10.2: - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - requiresBuild: true - dependencies: - tslib: 2.8.1 - dev: true - optional: true - /@types/chai@5.2.3: resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} dependencies: @@ -4148,7 +3928,7 @@ packages: vue: 3.5.14(typescript@5.9.3) dev: false - /@vitejs/plugin-vue@6.0.1(vite@8.0.16)(vue@3.5.14): + /@vitejs/plugin-vue@6.0.1(vite@7.1.12)(vue@3.5.14): resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: @@ -4156,7 +3936,7 @@ packages: vue: ^3.2.25 dependencies: '@rolldown/pluginutils': 1.0.0-beta.29 - vite: 8.0.16(esbuild@0.25.11) + vite: 7.1.12 vue: 3.5.14(typescript@5.9.3) dev: true @@ -5955,11 +5735,6 @@ packages: engines: {node: '>=4'} dev: true - /detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - dev: true - /dfa@1.2.0: resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} @@ -8303,124 +8078,6 @@ packages: immediate: 3.0.6 dev: false - /lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} - engines: {node: '>= 12.0.0'} - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - dev: true - /lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -10838,31 +10495,6 @@ packages: inherits: 2.0.4 dev: true - /rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - dependencies: - '@oxc-project/types': 0.133.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 - dev: true - /rollup@2.79.2: resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} engines: {node: '>=10.0.0'} @@ -12335,7 +11967,7 @@ packages: vfile-message: 3.1.4 dev: false - /vite-plugin-checker@0.11.0(eslint@9.39.1)(typescript@5.9.3)(vite@8.0.16)(vue-tsc@1.8.8): + /vite-plugin-checker@0.11.0(eslint@9.39.1)(typescript@5.9.3)(vite@7.1.12)(vue-tsc@1.8.8): resolution: {integrity: sha512-iUdO9Pl9UIBRPAragwi3as/BXXTtRu4G12L3CMrjx+WVTd9g/MsqNakreib9M/2YRVkhZYiTEwdH2j4Dm0w7lw==} engines: {node: '>=16.11'} peerDependencies: @@ -12381,12 +12013,12 @@ packages: tiny-invariant: 1.3.3 tinyglobby: 0.2.15 typescript: 5.9.3 - vite: 8.0.16(esbuild@0.25.11) + vite: 7.1.12 vscode-uri: 3.1.0 vue-tsc: 1.8.8(typescript@5.9.3) dev: true - /vite-plugin-optimize-persist@0.1.2(vite-plugin-package-config@0.1.1)(vite@8.0.16): + /vite-plugin-optimize-persist@0.1.2(vite-plugin-package-config@0.1.1)(vite@7.1.12): resolution: {integrity: sha512-H/Ebn2kZO8PvwUF08SsT5K5xMJNCWKoGX71+e9/ER3yNj7GHiFjNQlvGg5ih/zEx09MZ9m7WCxOwmEKbeIVzww==} peerDependencies: vite: ^2.0.0 @@ -12394,32 +12026,32 @@ packages: dependencies: debug: 4.4.3 fs-extra: 10.1.0 - vite: 8.0.16(esbuild@0.25.11) - vite-plugin-package-config: 0.1.1(vite@8.0.16) + vite: 7.1.12 + vite-plugin-package-config: 0.1.1(vite@7.1.12) transitivePeerDependencies: - supports-color dev: true - /vite-plugin-package-config@0.1.1(vite@8.0.16): + /vite-plugin-package-config@0.1.1(vite@7.1.12): resolution: {integrity: sha512-w9B3I8ZnqoyhlbzimXjXNk85imrMZgvI9m8f6j3zonK5IVA5KXzpT+PZOHlDz8lqh1vqvoEI1uhy+ZDoLAiA/w==} peerDependencies: vite: ^2.0.0 dependencies: debug: 4.4.3 - vite: 8.0.16(esbuild@0.25.11) + vite: 7.1.12 transitivePeerDependencies: - supports-color dev: true - /vite-plugin-package-version@1.1.0(vite@8.0.16): + /vite-plugin-package-version@1.1.0(vite@7.1.12): resolution: {integrity: sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==} peerDependencies: vite: '>=2.0.0-beta.69' dependencies: - vite: 8.0.16(esbuild@0.25.11) + vite: 7.1.12 dev: true - /vite-plugin-pwa@1.1.0(vite@8.0.16)(workbox-build@7.3.0)(workbox-window@7.3.0): + /vite-plugin-pwa@1.1.0(vite@7.1.12)(workbox-build@7.3.0)(workbox-window@7.3.0): resolution: {integrity: sha512-VsSpdubPzXhHWVINcSx6uHRMpOHVHQcHsef1QgkOlEoaIDAlssFEW88LBq1a59BuokAhsh2kUDJbaX1bZv4Bjw==} engines: {node: '>=16.0.0'} peerDependencies: @@ -12434,7 +12066,7 @@ packages: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.15 - vite: 8.0.16(esbuild@0.25.11) + vite: 7.1.12 workbox-build: 7.3.0 workbox-window: 7.3.0 transitivePeerDependencies: @@ -12445,7 +12077,7 @@ packages: resolution: {integrity: sha512-xNTYPcNW+XthOJqI459DYyCZ6nPDKpnFFnvMHVlshtDn+UUp1y37HgmvmWEsCdLp0LZILnzHfrjVrH44V2krAg==} dev: true - /vite-plugin-windicss@1.9.4(vite@8.0.16): + /vite-plugin-windicss@1.9.4(vite@7.1.12): resolution: {integrity: sha512-3t1AUVrs2XBXGc2BefRPRvy1CLy8qA/5A1J1Z73Ej1DIx+puXn39MQSWluxZ2FHEz8z9OEIvsoIIPc/s/P3OmQ==} peerDependencies: vite: '*' @@ -12453,7 +12085,7 @@ packages: '@windicss/plugin-utils': 1.9.4 debug: 4.4.3 kolorist: 1.8.0 - vite: 8.0.16(esbuild@0.25.11) + vite: 7.1.12 windicss: 3.5.6 transitivePeerDependencies: - supports-color @@ -12509,59 +12141,6 @@ packages: fsevents: 2.3.3 dev: true - /vite@8.0.16(esbuild@0.25.11): - resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - dependencies: - esbuild: 0.25.11 - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 - rolldown: 1.0.3 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - dev: true - /vitest@4.0.6(happy-dom@20.0.10): resolution: {integrity: sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} diff --git a/tsconfig.json b/tsconfig.json index d3afb00d..25e020f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,3 @@ { - "extends": "./packages/app/tsconfig.json", - "compilerOptions": { - "ignoreDeprecations": "6.0" - } + "extends": "./packages/app/tsconfig.json" } \ No newline at end of file From 3d3ef9b2a86f880e9fed6a29b757b1402a88e57c Mon Sep 17 00:00:00 2001 From: Novout Date: Wed, 10 Jun 2026 19:38:08 -0300 Subject: [PATCH 06/30] refactor: remove head dynamic set --- packages/plugin-editor-window/src/seo.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/plugin-editor-window/src/seo.ts b/packages/plugin-editor-window/src/seo.ts index 35ce7399..8737b081 100644 --- a/packages/plugin-editor-window/src/seo.ts +++ b/packages/plugin-editor-window/src/seo.ts @@ -7,22 +7,11 @@ export const PluginSeoSet = ( hooks: PluginTypes.PluginHooks, ) => { emitter.on('call-editor-created', () => { - // dynamic head title const title = computed(() => hooks.i18n.t('seo.editor.title')) const description = computed(() => hooks.i18n.t('seo.editor.description')) - const _title = computed(() => - stores.PROJECT.nameRaw === hooks.env.projectEmpty() || - !stores.CONTEXT.entities[0] || - stores.CONTEXT.entities[0].raw === hooks.env.emptyLine() || - hooks.entity.utils().isFixed(0) - ? title.value - : stores.PROJECT.nameRaw + - (stores.CONTEXT.entities[0]?.raw ? ' - ' : '') + - stores.CONTEXT.entities[0]?.raw, - ) hooks.vueuse.head({ - title: _title, + title, meta: [ { name: `description`, From 6be2b614dd71eba82e4783d129183a1a6c2689d6 Mon Sep 17 00:00:00 2001 From: Novout Date: Wed, 10 Jun 2026 19:46:43 -0300 Subject: [PATCH 07/30] fix!: clean save in before set entity raw --- packages/app/src/use/block/text.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/app/src/use/block/text.ts b/packages/app/src/use/block/text.ts index bd5aa907..2d75c2da 100644 --- a/packages/app/src/use/block/text.ts +++ b/packages/app/src/use/block/text.ts @@ -51,7 +51,11 @@ export const useBlockText = ({ const save = (target: number, raw: string) => { if (raw === null) return - CONTEXT.entities[target].raw = raw + const clean = raw + .replaceAll(//gi, '') + .replaceAll('&', '&') + + CONTEXT.entities[target].raw = clean if (EDITOR.configuration.trackEntities) { CONTEXT.entities[target].updatedAt = format.actually('iso') From fb0b5db8ddb827fb2c8c874ee951003bee64b1f1 Mon Sep 17 00:00:00 2001 From: Novout Date: Fri, 12 Jun 2026 18:33:22 -0300 Subject: [PATCH 08/30] feat: initial .epub implementation --- package.json | 7 +- packages/app/package.json | 1 + packages/app/src/App.vue | 4 +- .../page/about/AboutPortability.vue | 2 - .../header/items/EditorBaseHeaderCreate.vue | 2 - packages/app/vite.config.ts | 1 + packages/plugin-exporter-epub/package.json | 2 +- packages/plugin-exporter-epub/src/set.ts | 177 +++--------------- pnpm-lock.yaml | 158 +++------------- 9 files changed, 66 insertions(+), 288 deletions(-) diff --git a/package.json b/package.json index 763e966d..beebd6f5 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,12 @@ "devDependencies": { "generi": "2.0.4", "happy-dom": "20.0.10", - "prettier": "3.6.2", "lerna": "6.6.2", + "prettier": "3.6.2", "vitest": "4.0.6" }, - "packageManager": "pnpm@8.5.1" + "packageManager": "pnpm@8.5.1", + "dependencies": { + "epub-gen3": "^0.2.0" + } } diff --git a/packages/app/package.json b/packages/app/package.json index 9e3093c3..8dffb43b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -72,6 +72,7 @@ "docx": "7.5.0", "drauu": "0.4.3", "dropbox": "10.34.0", + "epub-gen3": "0.2.2", "file-saver": "2.0.5", "floating-vue": "2.0.0-beta.20", "hast": "1.0.0", diff --git a/packages/app/src/App.vue b/packages/app/src/App.vue index 1a15465c..880429a2 100644 --- a/packages/app/src/App.vue +++ b/packages/app/src/App.vue @@ -9,7 +9,7 @@ import { ImporterPlugin } from 'better-write-plugin-importer' import { PDFPlugin } from 'better-write-plugin-exporter-pdf' import { DocxPlugin } from 'better-write-plugin-exporter-docx' - // import { EpubPlugin } from 'better-write-plugin-exporter-epub' + import { EpubPlugin } from 'better-write-plugin-exporter-epub' import { TxtPlugin } from 'better-write-plugin-exporter-txt' import { HtmlPlugin } from 'better-write-plugin-exporter-html' import { SchemasPlugin } from 'better-write-plugin-schemas' @@ -25,7 +25,7 @@ ImporterPlugin(), PDFPlugin(), DocxPlugin(), - // EpubPlugin(), + EpubPlugin(), TxtPlugin(), HtmlPlugin(), SchemasPlugin(), diff --git a/packages/app/src/components/page/about/AboutPortability.vue b/packages/app/src/components/page/about/AboutPortability.vue index ca3f4cfc..781c2f8d 100644 --- a/packages/app/src/components/page/about/AboutPortability.vue +++ b/packages/app/src/components/page/about/AboutPortability.vue @@ -110,9 +110,7 @@ class="flex flex-col gap-5" > .DOCX - .TXT .PDF .BW diff --git a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue b/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue index d881d870..b06a494c 100644 --- a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue +++ b/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue @@ -120,8 +120,6 @@ const onEPUBGenerate = async () => { await storage.normalize() - toast.warning(t('toast.epub.disabled')) - plugin.emit('plugin-epub-generate') } diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index db0f4db0..b59e536f 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -31,6 +31,7 @@ export default ({ mode }: any) => { }, optimizeDeps: { include: ['buffer', 'process'], + exclude: ['epub-gen3'] }, define: { __VUE_I18N_FULL_INSTALL__: true, diff --git a/packages/plugin-exporter-epub/package.json b/packages/plugin-exporter-epub/package.json index 041788e6..6191cc0f 100644 --- a/packages/plugin-exporter-epub/package.json +++ b/packages/plugin-exporter-epub/package.json @@ -20,6 +20,6 @@ "better-write-contenteditable-ast": "^1.3.27", "better-write-plugin-core": "^1.3.27", "better-write-types": "^1.3.27", - "epub-gen-memory": "1.0.10" + "epub-gen3": "0.2.2" } } diff --git a/packages/plugin-exporter-epub/src/set.ts b/packages/plugin-exporter-epub/src/set.ts index 548e93f0..6651c0ec 100644 --- a/packages/plugin-exporter-epub/src/set.ts +++ b/packages/plugin-exporter-epub/src/set.ts @@ -1,179 +1,58 @@ -import { saveAs } from 'file-saver' -import { ContextState, Entity, PluginTypes } from 'better-write-types' +import { ContextState, PluginTypes } from 'better-write-types' import { On } from 'better-write-plugin-core' -import { getRows, parse } from 'better-write-contenteditable-ast' -import EPUB, { Chapter } from 'epub-gen-memory/bundle' -import { getStyles } from './styles' +import { Epub, ready } from 'epub-gen3/browser' export const PluginEpubSet = ( emitter: PluginTypes.PluginEmitter, stores: PluginTypes.PluginStores, hooks: PluginTypes.PluginHooks, ) => { - const entities = () => { - const headingOne = (entity: Entity) => { - return hooks.substitution.purge(entity.raw) - } - - const headingTwo = (entity: Entity) => { - return `

${hooks.substitution.purge(entity.raw)}

` - } - - const headingThree = (entity: Entity) => { - return `

${hooks.substitution.purge(entity.raw)}

` - } - - const image = (entity: Entity) => { - return `
` - } - - const svg = (entity: Entity) => { - return `
${entity.raw}
` - } - - const checkbox = (entity: Entity) => { - const id = String(hooks.utils.id().uuidv4()) - - return `
-
` - } - - const list = (entity: Entity) => { - return `
  • ${hooks.substitution.purge( - entity.raw, - )}
` - } - - const paragraph = (entity: Entity): string[] => { - if ( - hooks.env.emptyLine() === entity.raw || - entity.raw === '' || - entity.raw === ' ' - ) - return [lineBreak()] - - return getRows(entity.raw).map((row) => { - const target = parse(hooks.substitution.purge(row)) - - return target.reduce((acc, item) => { - // boldItalics - if (item.italic && item.bold) - return (acc += item.text.trim() ? `${item.text}` : '') - - // italics - if (item.italic) - return (acc += item.text.trim() ? `${item.text}` : '') - - // bold - if (item.bold) - return (acc += item.text.trim() ? `${item.text}` : '') - - // common case - return (acc += item.text.trim() ? `${item.text}` : '') - }, '') - }) - } - - const pageBreak = () => { - return `
` - } - - const lineBreak = () => { - return '
' - } - - return { - paragraph, - headingOne, - headingTwo, - headingThree, - image, - svg, - checkbox, - list, - pageBreak, - lineBreak, - } - } - - const contents = (): Chapter[] => { - const chapters: Chapter[] = [] + const contents = (): string[][] => { + const raw: string[][] = [] stores.PROJECT.chapters.forEach(({ entities: list }: ContextState) => { - const chapter = { - title: '', - content: '', - } + let data = [] for (const entity of list) { - switch (entity.type) { - case 'checkbox': - chapter.content += entities().checkbox(entity) - break - case 'list': - chapter.content += entities().list(entity) - break - case 'paragraph': - entities() - .paragraph(entity) - ?.forEach( - (paragraph) => (chapter.content += `

${paragraph}

`), - ) - break - case 'heading-one': - chapter.title = entities().headingOne(entity) - break - case 'heading-two': - chapter.content += entities().headingTwo(entity) - break - case 'heading-three': - chapter.content += entities().headingThree(entity) - break - case 'page-break': - chapter.content += entities().pageBreak() - break - case 'line-break': - chapter.content += entities().lineBreak() - break - case 'image': - chapter.content += entities().image(entity) - break - case 'drau': - chapter.content += entities().svg(entity) - break - } + data.push(hooks.substitution.purge(entity.raw)) } - chapters.push(chapter) + raw.push(data) }) - return chapters + return raw } - const generate = () => { - EPUB( + const generate = async () => { + await ready + + const epub = new Epub( { title: stores.PROJECT.nameRaw, author: stores.PROJECT.creator, description: stores.PROJECT.subject, publisher: stores.PROJECT.producer, tocTitle: hooks.i18n.t('editor.bar.epub.table') ?? undefined, - tocInTOC: stores.PROJECT.type === 'creative', - date: new Date().toString(), - css: getStyles(stores, hooks), - cover: undefined, + lang: 'en', + fonts: [], + version: 3, + // TODO: support CSS styles + // css: getStyles(stores, hooks), }, - contents(), - ).then( - (content) => download(content), - (_) => hooks.toast.error(hooks.i18n.t('toast.generics.error')), + [ + contents(), + ] ) + + download(epub, stores.PROJECT.nameRaw) } - const download = (blob: Blob) => { - saveAs(blob, hooks.project.utils().exportFullName('epub')) + const download = (epub: Epub, title: string) => { + const bytes = epub.archive() + const blob = new Blob([bytes as any], { type: 'application/epub+zip' }) + const url = URL.createObjectURL(blob) + const a = Object.assign(document.createElement('a'), { href: url, download: `${title}.epub` }) + a.click() hooks.toast.success(hooks.i18n.t('toast.project.epub.generate')) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c99dcfe..57107c01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,6 +3,10 @@ lockfileVersion: '6.0' importers: .: + dependencies: + epub-gen3: + specifier: ^0.2.0 + version: 0.2.0 devDependencies: generi: specifier: 2.0.4 @@ -190,6 +194,9 @@ importers: dropbox: specifier: 10.34.0 version: 10.34.0(@types/node-fetch@2.6.13) + epub-gen3: + specifier: 0.2.2 + version: 0.2.2 file-saver: specifier: 2.0.5 version: 2.0.5 @@ -514,9 +521,9 @@ importers: better-write-types: specifier: ^1.3.27 version: link:../types - epub-gen-memory: - specifier: 1.0.10 - version: 1.0.10 + epub-gen3: + specifier: 0.2.2 + version: 0.2.2 packages/plugin-exporter-html: dependencies: @@ -4443,13 +4450,6 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} dev: true - /abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - dependencies: - event-target-shim: 5.0.1 - dev: false - /acorn-jsx@5.3.2(acorn@8.15.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4666,6 +4666,7 @@ packages: /async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + dev: true /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -4734,6 +4735,7 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true /base64-js@0.0.8: resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} @@ -4792,6 +4794,7 @@ packages: /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true /brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -4804,6 +4807,7 @@ packages: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} dependencies: balanced-match: 1.0.2 + dev: true /braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -5060,6 +5064,7 @@ packages: /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + dev: true /camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} @@ -5541,16 +5546,6 @@ packages: engines: {node: '>=8'} dev: true - /css-select@4.3.0: - resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 - dev: false - /css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -5559,11 +5554,6 @@ packages: source-map-js: 1.2.1 dev: true - /css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - dev: false - /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -5738,10 +5728,6 @@ packages: /dfa@1.2.0: resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} - /diacritics@1.3.0: - resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} - dev: false - /diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} @@ -5773,38 +5759,11 @@ packages: xml-js: 1.6.11 dev: false - /dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 - dev: false - /domain-browser@4.22.0: resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==} engines: {node: '>=10'} dev: true - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: false - - /domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} - dependencies: - domelementtype: 2.3.0 - dev: false - - /domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 - dev: false - /dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -5817,6 +5776,7 @@ packages: engines: {node: '>=10'} dependencies: is-obj: 2.0.0 + dev: true /dotenv@10.0.0: resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} @@ -5868,6 +5828,7 @@ packages: hasBin: true dependencies: jake: 10.9.4 + dev: true /electron-to-chromium@1.5.244: resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} @@ -5926,15 +5887,6 @@ packages: ansi-colors: 4.1.3 dev: true - /entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - dev: false - - /entities@3.0.1: - resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} - engines: {node: '>=0.12'} - dev: false - /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -5954,25 +5906,12 @@ packages: hasBin: true dev: true - /epub-gen-memory@1.0.10: - resolution: {integrity: sha512-QS4rEGCO/AUaaofhvAzXAxz0bJ/u23ZGdsdfjVBsWE6LHqM1Mi4R0rs+f6BbGDrHWBvASZkadvrfMc73TFpC6Q==} - engines: {node: '>=10.0.0'} - dependencies: - abort-controller: 3.0.0 - css-select: 4.3.0 - diacritics: 1.3.0 - dom-serializer: 1.4.1 - domhandler: 4.3.1 - domutils: 2.8.0 - ejs: 3.1.10 - htmlparser2: 7.2.0 - jszip: 3.10.1 - mime: 2.6.0 - node-fetch: 2.7.0 - ow: 0.28.2 - slugify: 1.6.6 - transitivePeerDependencies: - - encoding + /epub-gen3@0.2.0: + resolution: {integrity: sha512-l1LWgYNGWYf5O/xLSEpHY9y96jlkMQugiF4kmf6q9WsmHHLdAkhs1xfeTCTmSlHRK0v9+q73eILSj0KvvsyibQ==} + dev: false + + /epub-gen3@0.2.2: + resolution: {integrity: sha512-lLgYwbEJWDfTlVIIrsO6lU97sIDEO4EcZNAHT54qkwayZNXjGbahY026Yg1Tin0vJrnKMArwWJ7MALyFyPoykg==} dev: false /err-code@2.0.3: @@ -6286,11 +6225,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - dev: false - /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: true @@ -6445,6 +6379,7 @@ packages: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: minimatch: 5.1.6 + dev: true /fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} @@ -7166,15 +7101,6 @@ packages: resolution: {integrity: sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==} dev: false - /htmlparser2@7.2.0: - resolution: {integrity: sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==} - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - domutils: 2.8.0 - entities: 3.0.1 - dev: false - /http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} dev: true @@ -7578,6 +7504,7 @@ packages: /is-obj@2.0.0: resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} engines: {node: '>=8'} + dev: true /is-path-cwd@2.2.0: resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} @@ -7759,6 +7686,7 @@ packages: async: 3.2.6 filelist: 1.0.4 picocolors: 1.1.1 + dev: true /jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} @@ -8158,11 +8086,6 @@ packages: /lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. - dev: false - /lodash.ismatch@4.4.0: resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} dev: true @@ -8638,12 +8561,6 @@ packages: dependencies: mime-db: 1.52.0 - /mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - dev: false - /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -8679,6 +8596,7 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.2 + dev: true /minimatch@6.2.0: resolution: {integrity: sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==} @@ -9246,6 +9164,7 @@ packages: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} dependencies: boolbase: 1.0.0 + dev: true /nx@15.9.7: resolution: {integrity: sha512-1qlEeDjX9OKZEryC8i4bA+twNg+lB5RKrozlNwWx/lLJHqWPUfvUTvxh+uxlPYL9KzVReQjUuxMLFMsHNqWUrA==} @@ -9422,17 +9341,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /ow@0.28.2: - resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} - engines: {node: '>=12'} - dependencies: - '@sindresorhus/is': 4.6.0 - callsites: 3.1.0 - dot-prop: 6.0.1 - lodash.isequal: 4.5.0 - vali-date: 1.0.0 - dev: false - /own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -10793,11 +10701,6 @@ packages: engines: {node: '>=8'} dev: true - /slugify@1.6.6: - resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} - engines: {node: '>=8.0.0'} - dev: false - /smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -11914,11 +11817,6 @@ packages: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true - /vali-date@1.0.0: - resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} - engines: {node: '>=0.10.0'} - dev: false - /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: From bb3f4aa88910998a21b2420540de5370391f2aac Mon Sep 17 00:00:00 2001 From: Novout Date: Fri, 12 Jun 2026 18:46:59 -0300 Subject: [PATCH 09/30] chore(epub): toc title --- packages/plugin-exporter-epub/src/set.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-exporter-epub/src/set.ts b/packages/plugin-exporter-epub/src/set.ts index 6651c0ec..35b34309 100644 --- a/packages/plugin-exporter-epub/src/set.ts +++ b/packages/plugin-exporter-epub/src/set.ts @@ -32,7 +32,7 @@ export const PluginEpubSet = ( author: stores.PROJECT.creator, description: stores.PROJECT.subject, publisher: stores.PROJECT.producer, - tocTitle: hooks.i18n.t('editor.bar.epub.table') ?? undefined, + tocTitle: stores.PROJECT.nameRaw ?? undefined, lang: 'en', fonts: [], version: 3, From 21f27acd0d2ce005ae9bf4bab2edf59df57f8838 Mon Sep 17 00:00:00 2001 From: Novout Date: Fri, 12 Jun 2026 20:00:22 -0300 Subject: [PATCH 10/30] chore: update epub-gen-rs --- packages/app/package.json | 2 +- packages/plugin-exporter-epub/package.json | 2 +- packages/plugin-exporter-epub/src/set.ts | 6 +----- pnpm-lock.yaml | 12 ++++++------ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 8dffb43b..d852fd73 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -72,7 +72,7 @@ "docx": "7.5.0", "drauu": "0.4.3", "dropbox": "10.34.0", - "epub-gen3": "0.2.2", + "epub-gen3": "0.3.0", "file-saver": "2.0.5", "floating-vue": "2.0.0-beta.20", "hast": "1.0.0", diff --git a/packages/plugin-exporter-epub/package.json b/packages/plugin-exporter-epub/package.json index 6191cc0f..27cd98d9 100644 --- a/packages/plugin-exporter-epub/package.json +++ b/packages/plugin-exporter-epub/package.json @@ -20,6 +20,6 @@ "better-write-contenteditable-ast": "^1.3.27", "better-write-plugin-core": "^1.3.27", "better-write-types": "^1.3.27", - "epub-gen3": "0.2.2" + "epub-gen3": "0.3.0" } } diff --git a/packages/plugin-exporter-epub/src/set.ts b/packages/plugin-exporter-epub/src/set.ts index 35b34309..39d6d108 100644 --- a/packages/plugin-exporter-epub/src/set.ts +++ b/packages/plugin-exporter-epub/src/set.ts @@ -36,12 +36,8 @@ export const PluginEpubSet = ( lang: 'en', fonts: [], version: 3, - // TODO: support CSS styles - // css: getStyles(stores, hooks), }, - [ - contents(), - ] + contents() ) download(epub, stores.PROJECT.nameRaw) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57107c01..d5ee648d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,8 +195,8 @@ importers: specifier: 10.34.0 version: 10.34.0(@types/node-fetch@2.6.13) epub-gen3: - specifier: 0.2.2 - version: 0.2.2 + specifier: 0.3.0 + version: 0.3.0 file-saver: specifier: 2.0.5 version: 2.0.5 @@ -522,8 +522,8 @@ importers: specifier: ^1.3.27 version: link:../types epub-gen3: - specifier: 0.2.2 - version: 0.2.2 + specifier: 0.3.0 + version: 0.3.0 packages/plugin-exporter-html: dependencies: @@ -5910,8 +5910,8 @@ packages: resolution: {integrity: sha512-l1LWgYNGWYf5O/xLSEpHY9y96jlkMQugiF4kmf6q9WsmHHLdAkhs1xfeTCTmSlHRK0v9+q73eILSj0KvvsyibQ==} dev: false - /epub-gen3@0.2.2: - resolution: {integrity: sha512-lLgYwbEJWDfTlVIIrsO6lU97sIDEO4EcZNAHT54qkwayZNXjGbahY026Yg1Tin0vJrnKMArwWJ7MALyFyPoykg==} + /epub-gen3@0.3.0: + resolution: {integrity: sha512-Vi14IzFEUF2OdwPybbJdiOXUzXJPv76aADvOrQ4FqfmSYanpqw08bw3WM0o4q55SPagGszo2y9GwOsRG+8xVQg==} dev: false /err-code@2.0.3: From a06d38b224df1e6d702cad4ba3414282eac13b74 Mon Sep 17 00:00:00 2001 From: Novout Date: Sat, 13 Jun 2026 18:14:51 -0300 Subject: [PATCH 11/30] feat(epub): ignore fixed and image case --- packages/plugin-exporter-epub/src/set.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/plugin-exporter-epub/src/set.ts b/packages/plugin-exporter-epub/src/set.ts index 39d6d108..a490ee00 100644 --- a/packages/plugin-exporter-epub/src/set.ts +++ b/packages/plugin-exporter-epub/src/set.ts @@ -1,5 +1,6 @@ -import { ContextState, PluginTypes } from 'better-write-types' +import { ContextState, Entity, PluginTypes } from 'better-write-types' import { On } from 'better-write-plugin-core' +// @ts-ignore import { Epub, ready } from 'epub-gen3/browser' export const PluginEpubSet = ( @@ -7,6 +8,14 @@ export const PluginEpubSet = ( stores: PluginTypes.PluginStores, hooks: PluginTypes.PluginHooks, ) => { + const isValidType = (val: Entity) => { + return ( + !hooks.entity.utils().isFixedRaw(val.raw) && + val.type !== 'image' && + val.type !== 'drau' + ) + } + const contents = (): string[][] => { const raw: string[][] = [] @@ -14,7 +23,9 @@ export const PluginEpubSet = ( let data = [] for (const entity of list) { - data.push(hooks.substitution.purge(entity.raw)) + if(isValidType(entity)) { + data.push(hooks.substitution.purge(entity.raw)) + } } raw.push(data) From 3c5b472e9bafc5bf2f374c95eaa77c2e92dcc73f Mon Sep 17 00:00:00 2001 From: Novout Date: Sat, 13 Jun 2026 19:07:49 -0300 Subject: [PATCH 12/30] feat: bionic reading docx and pdf --- .../generator/pdf/generate/PDFGenerate.vue | 1 + .../header/items/EditorBaseHeaderCreate.vue | 31 +++++++++- packages/app/src/store/absolute.ts | 5 ++ packages/languages/src/en-US.ts | 3 + packages/languages/src/pt-BR.ts | 3 + packages/plugin-core/src/on.ts | 10 +-- packages/plugin-exporter-docx/src/set.ts | 62 +++++++++++++------ packages/plugin-exporter-epub/src/set.ts | 30 ++++++--- packages/plugin-exporter-pdf/src/generate.ts | 19 ++++-- packages/types/src/absolute.ts | 7 +++ packages/types/src/docx.ts | 5 ++ packages/types/src/epub.ts | 3 + packages/types/src/index.ts | 1 + packages/types/src/pdf.ts | 1 + 14 files changed, 142 insertions(+), 39 deletions(-) create mode 100644 packages/types/src/epub.ts diff --git a/packages/app/src/components/page/editor/generator/pdf/generate/PDFGenerate.vue b/packages/app/src/components/page/editor/generator/pdf/generate/PDFGenerate.vue index 5df2fe29..6a1db818 100644 --- a/packages/app/src/components/page/editor/generator/pdf/generate/PDFGenerate.vue +++ b/packages/app/src/components/page/editor/generator/pdf/generate/PDFGenerate.vue @@ -106,6 +106,7 @@ plugin.emit('plugin-pdf-generate', { chapters: chapters.value, color: color.value, + bionicReading: ABSOLUTE.pdf.bionicReading, }) }, 100) } diff --git a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue b/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue index b06a494c..7ee79c65 100644 --- a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue +++ b/packages/app/src/components/page/editor/header/items/EditorBaseHeaderCreate.vue @@ -20,6 +20,14 @@ + { await storage.normalize() - plugin.emit('plugin-epub-generate') + plugin.emit('plugin-epub-generate', { bionicReading: ABSOLUTE.epub.bionicReading }) } const onPDFGenerate = async () => { @@ -134,6 +159,8 @@ plugin.emit('plugin-pdf-generate', { chapters: project.utils().getChaptersSelection(), + color: 'RGB', + bionicReading: ABSOLUTE.pdf.bionicReading, }) } diff --git a/packages/app/src/store/absolute.ts b/packages/app/src/store/absolute.ts index 1603cfd9..336a68d3 100644 --- a/packages/app/src/store/absolute.ts +++ b/packages/app/src/store/absolute.ts @@ -30,9 +30,14 @@ export const useAbsoluteStore = defineStore('absolute', { pdf: { configuration: false, generate: false, + bionicReading: false, + }, + epub: { + bionicReading: false, }, docx: { configuration: false, + bionicReading: false, }, // deprecated auth: { diff --git a/packages/languages/src/en-US.ts b/packages/languages/src/en-US.ts index d16898f9..205d135d 100644 --- a/packages/languages/src/en-US.ts +++ b/packages/languages/src/en-US.ts @@ -239,10 +239,12 @@ export default { preview: 'Preview (.PDF)', generate: 'Generate (.PDF)', configuration: 'Configure (.PDF)', + bionicReading: 'Enable Bionic Reading', }, epub: { generate: 'Generate (.EPUB)', table: 'Table of Content', + bionicReading: 'Enable Bionic Reading', }, txt: { generate: 'Generate (.TXT)', @@ -253,6 +255,7 @@ export default { docx: { configuration: 'Configure (.DOCX)', generate: 'Generate (.DOCX)', + bionicReading: 'Enable Bionic Reading', }, project: { new: 'New Project', diff --git a/packages/languages/src/pt-BR.ts b/packages/languages/src/pt-BR.ts index 26200ce6..9545de5d 100644 --- a/packages/languages/src/pt-BR.ts +++ b/packages/languages/src/pt-BR.ts @@ -238,10 +238,12 @@ export default { preview: 'Simular (.PDF)', generate: 'Gerar (.PDF)', configuration: 'Configurar (.PDF)', + bionicReading: 'Bionic Reading', }, epub: { generate: 'Gerar (.EPUB)', table: 'Tabela de Conteúdos', + bionicReading: 'Bionic Reading', }, txt: { generate: 'Gerar (.TXT)', @@ -252,6 +254,7 @@ export default { docx: { configuration: 'Configurar (.DOCX)', generate: 'Gerar (.DOCX)', + bionicReading: 'Bionic Reading', }, project: { new: 'Novo Projeto', diff --git a/packages/plugin-core/src/on.ts b/packages/plugin-core/src/on.ts index f6eb11f3..2c8536bb 100644 --- a/packages/plugin-core/src/on.ts +++ b/packages/plugin-core/src/on.ts @@ -1,5 +1,7 @@ import type { PDFDocOptions, + EPUBDocOptions, + DOCXDocOptions, PluginTypes, ImporterParams, ProjectStateSchemaCreate, @@ -315,10 +317,10 @@ export const externals = () => { emitter: PluginTypes.PluginEmitter, content: PluginTypes.PluginContentOn, ) => { - emitter.on('plugin-docx-generate', () => { + emitter.on('plugin-docx-generate', (options: DOCXDocOptions) => { const created = content[0] - created && created() + created && created(options) }) } @@ -337,10 +339,10 @@ export const externals = () => { emitter: PluginTypes.PluginEmitter, content: PluginTypes.PluginContentOn, ) => { - emitter.on('plugin-epub-generate', () => { + emitter.on('plugin-epub-generate', (options: EPUBDocOptions) => { const created = content[0] - created && created() + created && created(options) }) } diff --git a/packages/plugin-exporter-docx/src/set.ts b/packages/plugin-exporter-docx/src/set.ts index 0c729318..9c1b2d90 100644 --- a/packages/plugin-exporter-docx/src/set.ts +++ b/packages/plugin-exporter-docx/src/set.ts @@ -4,6 +4,7 @@ import { Entity, PluginTypes, ProjectStateTemplatesGenerator, + DOCXDocOptions, } from 'better-write-types' import { On } from 'better-write-plugin-core' import { getRows, parse } from 'better-write-contenteditable-ast' @@ -11,6 +12,17 @@ import { ContextState } from 'better-write-types' type DocxPurge = Array +const bionicRuns = ( + word: string, + base: Record, +): docx.TextRun[] => { + const len = Math.ceil(word.length * 0.45) + return [ + new docx.TextRun({ ...base, text: word.slice(0, len), bold: true }), + new docx.TextRun({ ...base, text: word.slice(len) }), + ] +} + export const PluginDocxSet = ( emitter: PluginTypes.PluginEmitter, stores: PluginTypes.PluginStores, @@ -18,21 +30,31 @@ export const PluginDocxSet = ( ) => { const { isLoading } = hooks.vueuse.integration.progress - const purge = (raw: string, custom: Record): DocxPurge => { + const purge = (raw: string, custom: Record, bionicReading = false): DocxPurge => { const arr: DocxPurge = [] const ast = parse(hooks.substitution.purge(raw)) ast.forEach((node) => { - arr.push( - new docx.TextRun({ - text: node.text, - italics: node.italic, - bold: node.bold, - underline: custom.isUnderline(node.underline), - ...custom.textRun, - }), - ) + const base = { + italics: node.italic, + bold: node.bold, + underline: custom.isUnderline(node.underline), + ...custom.textRun, + } + + if (bionicReading) { + const words = node.text.split(/(\s+)/) + words.forEach((chunk) => { + if (/^\s+$/.test(chunk) || chunk === '') { + arr.push(new docx.TextRun({ ...base, text: chunk })) + } else { + bionicRuns(chunk, base).forEach((r) => arr.push(r)) + } + }) + } else { + arr.push(new docx.TextRun({ ...base, text: node.text })) + } }) return arr @@ -67,7 +89,7 @@ export const PluginDocxSet = ( return { bw } } - const create = () => { + const create = (bionicReading = false) => { const properties = (): docx.ISectionPropertiesOptions => { return { page: { @@ -252,7 +274,7 @@ export const PluginDocxSet = ( return getRows(entity.raw).map((row) => { return new docx.Paragraph({ - children: purge(row, custom), + children: purge(row, custom, bionicReading), ...custom.paragraph, ...custom.isList, }) @@ -342,19 +364,21 @@ export const PluginDocxSet = ( return { properties, footer, styles, entities, content, flow } } - const doc = (): docx.File => { + const doc = (options: DOCXDocOptions): docx.File => { + const c = create(options.bionicReading) + return new docx.Document({ creator: stores.PROJECT.creator, title: stores.PROJECT.nameRaw, description: stores.PROJECT.subject, subject: stores.PROJECT.subject, keywords: stores.PROJECT.keywords, - styles: create().styles(), + styles: c.styles(), sections: [ { - properties: create().properties(), - children: create().flow(), - footers: create().footer(), + properties: c.properties(), + children: c.flow(), + footers: c.footer(), }, ], }) @@ -382,8 +406,8 @@ export const PluginDocxSet = ( } On.externals().PluginDocxGenerate(emitter, [ - () => { - download(doc()) + (options: DOCXDocOptions) => { + download(doc(options ?? { bionicReading: false })) }, () => {}, ]) diff --git a/packages/plugin-exporter-epub/src/set.ts b/packages/plugin-exporter-epub/src/set.ts index a490ee00..f9b204fc 100644 --- a/packages/plugin-exporter-epub/src/set.ts +++ b/packages/plugin-exporter-epub/src/set.ts @@ -1,8 +1,21 @@ -import { ContextState, Entity, PluginTypes } from 'better-write-types' +import { ContextState, Entity, EPUBDocOptions, PluginTypes } from 'better-write-types' import { On } from 'better-write-plugin-core' // @ts-ignore import { Epub, ready } from 'epub-gen3/browser' +const bionicWord = (word: string): string => { + const len = Math.ceil(word.length * 0.45) + return `${word.slice(0, len)}${word.slice(len)}` +} + +const bionicHtml = (text: string): string => { + return text.replace(/(\S+)/g, (word) => { + // skip html tags + if (word.startsWith('<')) return word + return bionicWord(word) + }) +} + export const PluginEpubSet = ( emitter: PluginTypes.PluginEmitter, stores: PluginTypes.PluginStores, @@ -16,15 +29,16 @@ export const PluginEpubSet = ( ) } - const contents = (): string[][] => { + const contents = (options: EPUBDocOptions): string[][] => { const raw: string[][] = [] stores.PROJECT.chapters.forEach(({ entities: list }: ContextState) => { let data = [] for (const entity of list) { - if(isValidType(entity)) { - data.push(hooks.substitution.purge(entity.raw)) + if (isValidType(entity)) { + const purged = hooks.substitution.purge(entity.raw) + data.push(options.bionicReading ? bionicHtml(purged) : purged) } } @@ -34,7 +48,7 @@ export const PluginEpubSet = ( return raw } - const generate = async () => { + const generate = async (options: EPUBDocOptions) => { await ready const epub = new Epub( @@ -48,7 +62,7 @@ export const PluginEpubSet = ( fonts: [], version: 3, }, - contents() + contents(options) ) download(epub, stores.PROJECT.nameRaw) @@ -65,8 +79,8 @@ export const PluginEpubSet = ( } On.externals().PluginEpubGenerate(emitter, [ - () => { - generate() + (options: EPUBDocOptions) => { + generate(options ?? { bionicReading: false }) }, () => {}, ]) diff --git a/packages/plugin-exporter-pdf/src/generate.ts b/packages/plugin-exporter-pdf/src/generate.ts index bc0f30d1..9618a3d8 100644 --- a/packages/plugin-exporter-pdf/src/generate.ts +++ b/packages/plugin-exporter-pdf/src/generate.ts @@ -358,12 +358,19 @@ export const PluginPDFSet = ( ast.forEach(({ text, italic, bold, underline }) => { const und = underline ? { decoration: 'underline' } : {} - arr.push({ - text, - italics: italic, - bold, - ...und, - }) + if (options.bionicReading) { + text.split(/(\s+)/).forEach((chunk) => { + if (/^\s+$/.test(chunk) || chunk === '') { + arr.push({ text: chunk, italics: italic, bold, ...und }) + } else { + const len = Math.ceil(chunk.length * 0.45) + arr.push({ text: chunk.slice(0, len), italics: italic, bold: true, ...und }) + arr.push({ text: chunk.slice(len), italics: italic, bold, ...und }) + } + }) + } else { + arr.push({ text, italics: italic, bold, ...und }) + } }) return arr diff --git a/packages/types/src/absolute.ts b/packages/types/src/absolute.ts index 0c388188..a9c9414e 100644 --- a/packages/types/src/absolute.ts +++ b/packages/types/src/absolute.ts @@ -25,10 +25,16 @@ export interface AbsoluteStateShortcuts { export interface AbsoluteStatePDF { configuration: boolean generate: boolean + bionicReading: boolean } export interface AbsoluteStateDOCX { configuration: boolean + bionicReading: boolean +} + +export interface AbsoluteStateEPUB { + bionicReading: boolean } export interface AbsoluteStateAuth { @@ -67,6 +73,7 @@ export interface AbsoluteState { modal: AbsoluteStateModal shortcuts: AbsoluteStateShortcuts pdf: AbsoluteStatePDF + epub: AbsoluteStateEPUB docx: AbsoluteStateDOCX auth: AbsoluteStateAuth entity: AbsoluteStateEntity diff --git a/packages/types/src/docx.ts b/packages/types/src/docx.ts index 10612905..a98146b9 100644 --- a/packages/types/src/docx.ts +++ b/packages/types/src/docx.ts @@ -59,3 +59,8 @@ export interface DOCXState { flow: DOCXStateFlow styles: DOCXStateStyles } + + +export interface DOCXDocOptions { + bionicReading: boolean +} diff --git a/packages/types/src/epub.ts b/packages/types/src/epub.ts new file mode 100644 index 00000000..3c79a0e2 --- /dev/null +++ b/packages/types/src/epub.ts @@ -0,0 +1,3 @@ +export interface EPUBDocOptions { + bionicReading: boolean +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 94f2f864..d8fb85d1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -13,6 +13,7 @@ export * from './global' export * from './google' export * from './image' export * from './pdf' +export * from './epub' export * from './docx' export * from './project' export * from './raw' diff --git a/packages/types/src/pdf.ts b/packages/types/src/pdf.ts index e8cb4833..ea43246d 100644 --- a/packages/types/src/pdf.ts +++ b/packages/types/src/pdf.ts @@ -131,6 +131,7 @@ export interface PDFDocOptions { select: boolean }[] color: ColorSchema + bionicReading: boolean } export interface PDFState { From d4a88e04a39652039ed6000c03879c7e3b0e2c7e Mon Sep 17 00:00:00 2001 From: Novout Date: Sat, 13 Jun 2026 19:27:42 -0300 Subject: [PATCH 13/30] feat!: encrypt option for .bw --- .../header/items/EditorBaseHeaderProject.vue | 11 ++ packages/app/src/store/absolute.ts | 3 + packages/app/src/use/project.ts | 23 +++- packages/extension/src/index.ts | 116 +++++++++++++++++- packages/languages/src/en-US.ts | 8 ++ packages/languages/src/pt-BR.ts | 8 ++ packages/plugin-editor-window/src/drop.ts | 15 ++- packages/plugin-importer/src/bw.ts | 15 ++- packages/types/src/absolute.ts | 5 + 9 files changed, 192 insertions(+), 12 deletions(-) diff --git a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderProject.vue b/packages/app/src/components/page/editor/header/items/EditorBaseHeaderProject.vue index 96559990..7d5c9aaf 100644 --- a/packages/app/src/components/page/editor/header/items/EditorBaseHeaderProject.vue +++ b/packages/app/src/components/page/editor/header/items/EditorBaseHeaderProject.vue @@ -56,6 +56,17 @@ @action="ABSOLUTE.project.preferences = true" /> + + + { ABSOLUTE.project.tutorial = !localStorage.getItem('tutorial') } + const getBWBlob = async (target: string): Promise => { + if (!ABSOLUTE.bw.encryptOnExport) return writeBW(target) + + const password = window.prompt(t('toast.project.bw.passwordSet')) + + if (!password) throw new Error('cancelled') + + const confirm = window.prompt(t('toast.project.bw.passwordConfirm')) + + if (confirm !== password) { + toast.error(t('toast.project.bw.passwordMismatch')) + throw new Error('mismatch') + } + + return encryptBW(target, password) + } + const onExportProject = () => { storage .normalize() .then(async () => { const target = JSON.stringify(storage.getProjectObject()) - const zip = await writeBW(target) + const zip = await getBWBlob(target) await saveAs(zip, utils().exportName('bw')) @@ -233,7 +250,7 @@ export const useProject = () => { if (!res.isSupported.value) return const target = JSON.stringify(storage.getProjectObject()) - const zip = await writeBW(target) + const zip = await getBWBlob(target) res.data.value = zip res.fileName.value = utils().exportName('bw') diff --git a/packages/extension/src/index.ts b/packages/extension/src/index.ts index c1f690ea..90d60177 100644 --- a/packages/extension/src/index.ts +++ b/packages/extension/src/index.ts @@ -1,6 +1,8 @@ import { BlobReader, BlobWriter, + Entry, + FileEntry, TextReader, TextWriter, ZipReader, @@ -32,13 +34,117 @@ export const writeBW = async (data: string) => { return target } -export const readBW = async (blob: Blob): Promise => { - const zipFileReader = new BlobReader(blob) +// --- WebCrypto helpers --- + +const PBKDF2_ITERATIONS = 200_000 +const SALT_LEN = 16 +const IV_LEN = 12 + +const deriveKey = async (password: string, salt: Uint8Array): Promise => { + const keyMaterial = await crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(password), + 'PBKDF2', + false, + ['deriveKey'], + ) + + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: salt.buffer as ArrayBuffer, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'], + ) +} + +export const encryptBW = async (data: string, password: string): Promise => { + const salt = crypto.getRandomValues(new Uint8Array(SALT_LEN)) + const iv = crypto.getRandomValues(new Uint8Array(IV_LEN)) + const key = await deriveKey(password, salt) + + const ciphertext = new Uint8Array( + await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + new TextEncoder().encode(data), + ), + ) + + // layout: [16 salt][12 iv][...ciphertext] + const payload = new Uint8Array(SALT_LEN + IV_LEN + ciphertext.byteLength) + payload.set(salt, 0) + payload.set(iv, SALT_LEN) + payload.set(ciphertext, SALT_LEN + IV_LEN) + + const blobWriter = new BlobWriter() + const zipWriter = new ZipWriter(blobWriter) + + await zipWriter.add('meta.json', new TextReader(JSON.stringify({ encrypted: true, version: 1 }))) + await zipWriter.add('encrypted.bin', new BlobReader(new Blob([payload]))) + await zipWriter.add('mimetype', writeMimetype()) + await zipWriter.close() + + return blobWriter.getData() +} + +const isEncryptedZip = async (zipReader: ZipReader): Promise => { + const entries = await zipReader.getEntries() + + return entries.some((e: Entry) => e.filename === 'meta.json') +} + +export const decryptBW = async (blob: Blob, password: string): Promise => { + const zipReader = new ZipReader(new BlobReader(blob)) + const entries = await zipReader.getEntries() - const writer = new TextWriter() + const encEntry = entries.find((e) => e.filename === 'encrypted.bin') - const zipReader = new ZipReader(zipFileReader) - const firstEntry = (await zipReader.getEntries()).shift() + if (!encEntry) throw new Error('encrypted.bin not found') + + const payloadBlob = await (encEntry as FileEntry).getData(new BlobWriter()) + const payload = new Uint8Array(await payloadBlob.arrayBuffer()) + + const salt = payload.slice(0, SALT_LEN) + const iv = payload.slice(SALT_LEN, SALT_LEN + IV_LEN) + const ciphertext = payload.slice(SALT_LEN + IV_LEN) + + const key = await deriveKey(password, salt) + + let plaintext: ArrayBuffer + + try { + plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext) + } catch { + throw new Error('wrong-password') + } + + await zipReader.close() + + return destr(new TextDecoder().decode(plaintext)) +} + +export const readBW = async (blob: Blob, getPassword?: () => Promise): Promise => { + // peek at entries to decide the path + const peekReader = new ZipReader(new BlobReader(blob)) + const encrypted = await isEncryptedZip(peekReader) + await peekReader.close() + + if (encrypted) { + if (!getPassword) throw new Error('File is encrypted but no password provider was given') + + const password = await getPassword() + + if (password === null) throw new Error('cancelled') + + return decryptBW(blob, password) + } + + // original unencrypted path + const zipFileReader = new BlobReader(blob) + const writer = new TextWriter() + const zipReader = new ZipReader(zipFileReader) + const firstEntry = (await zipReader.getEntries()).shift() // @ts-expect-error const data = await firstEntry.getData(writer) diff --git a/packages/languages/src/en-US.ts b/packages/languages/src/en-US.ts index 205d135d..4bc61829 100644 --- a/packages/languages/src/en-US.ts +++ b/packages/languages/src/en-US.ts @@ -273,6 +273,7 @@ export default { import: 'Import', export: 'Export', exportAs: 'Export as...', + encryptBW: 'Encrypt .bw', }, chapter: { drafts: 'Drafts', @@ -1079,6 +1080,13 @@ export default { export: 'Project exported to extension (.bw) successfully!', delete: 'Project successfully deleted!', unsupportedExtension: 'This extension is not supported by Better Write!', + bw: { + passwordSet: 'Set a password to encrypt the file:', + passwordConfirm: 'Confirm the password:', + passwordMismatch: 'Passwords do not match. Export cancelled.', + passwordGet: 'This file is encrypted. Enter the password:', + passwordWrong: 'Wrong password. Could not open the file.', + }, docx: { generate: 'Successfully Downloaded DOCX!', }, diff --git a/packages/languages/src/pt-BR.ts b/packages/languages/src/pt-BR.ts index 9545de5d..bf6ffc97 100644 --- a/packages/languages/src/pt-BR.ts +++ b/packages/languages/src/pt-BR.ts @@ -270,6 +270,7 @@ export default { import: 'Importar', export: 'Exportar', exportAs: 'Exportar como...', + encryptBW: 'Criptografar .bw', }, chapter: { drafts: 'Rascunhos', @@ -1087,6 +1088,13 @@ export default { export: 'Projeto exportado para a extensão (.bw) com sucesso!', unsupportedExtension: 'Esta extensão não é suportada por Better Write!', delete: 'Projeto deletado com sucesso!', + bw: { + passwordSet: 'Defina uma senha para criptografar o arquivo:', + passwordConfirm: 'Confirme a senha:', + passwordMismatch: 'As senhas não coincidem. Exportação cancelada.', + passwordGet: 'Este arquivo está criptografado. Digite a senha:', + passwordWrong: 'Senha incorreta. Não foi possível abrir o arquivo.', + }, docx: { generate: 'DOCX Baixado com Sucesso!', }, diff --git a/packages/plugin-editor-window/src/drop.ts b/packages/plugin-editor-window/src/drop.ts index f9dd4c16..c05bbd07 100644 --- a/packages/plugin-editor-window/src/drop.ts +++ b/packages/plugin-editor-window/src/drop.ts @@ -22,9 +22,20 @@ export const PluginDropSet = ( if ( confirm(hooks.i18n.t('toast.project.import', { name: file.name })) ) { - const data = await readBW(file) + const getPassword = async () => + window.prompt(hooks.i18n.t('toast.project.bw.passwordGet')) - hooks.project.onLoadProject(data, false) + try { + const data = await readBW(file, getPassword) + + hooks.project.onLoadProject(data, false) + } catch (e: any) { + if (e?.message === 'wrong-password') { + hooks.toast.error(hooks.i18n.t('toast.project.bw.passwordWrong')) + } else if (e?.message !== 'cancelled') { + hooks.toast.error(hooks.i18n.t('toast.generics.error')) + } + } } return diff --git a/packages/plugin-importer/src/bw.ts b/packages/plugin-importer/src/bw.ts index 870cfb6c..a26f9472 100644 --- a/packages/plugin-importer/src/bw.ts +++ b/packages/plugin-importer/src/bw.ts @@ -9,9 +9,20 @@ export const BWSet = ( ) => { On.externals().PluginImporterBW(emitter, [ async ({ data }: ImporterParams) => { - const content = await readBW(data as any) + const getPassword = async () => + window.prompt(hooks.i18n.t('toast.project.bw.passwordGet')) - hooks.project.onLoadProject(content) + try { + const content = await readBW(data as any, getPassword) + + hooks.project.onLoadProject(content) + } catch (e: any) { + if (e?.message === 'wrong-password') { + hooks.toast.error(hooks.i18n.t('toast.project.bw.passwordWrong')) + } else if (e?.message !== 'cancelled') { + hooks.toast.error(hooks.i18n.t('toast.generics.error')) + } + } }, () => {}, ]) diff --git a/packages/types/src/absolute.ts b/packages/types/src/absolute.ts index a9c9414e..097a78ae 100644 --- a/packages/types/src/absolute.ts +++ b/packages/types/src/absolute.ts @@ -64,6 +64,10 @@ export interface AbsoluteStateSchemas { template: boolean } +export interface AbsoluteStateBW { + encryptOnExport: boolean +} + export interface AbsoluteState { cmd: boolean commands: boolean @@ -82,4 +86,5 @@ export interface AbsoluteState { generator: AbsoluteStateGenerator spinner: boolean schemas: AbsoluteStateSchemas + bw: AbsoluteStateBW } From d117e8b122540993a027d7a6279583a61ea67b32 Mon Sep 17 00:00:00 2001 From: Novout Date: Sat, 13 Jun 2026 19:45:30 -0300 Subject: [PATCH 14/30] refactor: alert erros in def components --- .../page/editor/header/EditorHeaderButton.vue | 8 ++++---- .../main/render/history/EditorBaseRenderHistory.vue | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/page/editor/header/EditorHeaderButton.vue b/packages/app/src/components/page/editor/header/EditorHeaderButton.vue index 69c7a102..70c775fa 100644 --- a/packages/app/src/components/page/editor/header/EditorHeaderButton.vue +++ b/packages/app/src/components/page/editor/header/EditorHeaderButton.vue @@ -1,6 +1,6 @@