diff --git a/CHANGELOG.md b/CHANGELOG.md index 360a85469..265f56ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,38 @@ -# Changelog (03/11/2025) +# Changelog (15/06/2026) Changelog was generated by [Generi](https://github.com/betterwrite/generi). Any questions, consult the documentation. +### v1.4.0 + +* **πŸ”§ fix(pdf):** cover with correct creator field size - [[d398c258](https://github.com/Novout/betterwrite/commit/d398c258)] +* **🚧 chore:** update epub-gen3 - [[316d0ebe](https://github.com/Novout/betterwrite/commit/316d0ebe)] +* **πŸ—Ώ ci:** fix stubs in runner text - [[ead66f46](https://github.com/Novout/betterwrite/commit/ead66f46)] +* **πŸŽ‰ feat:** landing background - [[8d09f5d1](https://github.com/Novout/betterwrite/commit/8d09f5d1)] +* **πŸŽ‰ feat:** some features for editor and preferences - [[8f3b8091](https://github.com/Novout/betterwrite/commit/8f3b8091)] +* **πŸ”§ test:** use file in setup - [[249d0738](https://github.com/Novout/betterwrite/commit/249d0738)] +* **🚧 chore:** epub in app package field - [[5fd1435b](https://github.com/Novout/betterwrite/commit/5fd1435b)] +* **🚧 chore:** update vite to v8 - [[34f392d8](https://github.com/Novout/betterwrite/commit/34f392d8)] +* **🚩 refactor:** choice microphone - [[003e717f](https://github.com/Novout/betterwrite/commit/003e717f)] +* **🚩 refactor!:** voice typing implement - [[9db0712a](https://github.com/Novout/betterwrite/commit/9db0712a)] +* **🚩 refactor:** unique bionic option - [[537c0d57](https://github.com/Novout/betterwrite/commit/537c0d57)] +* **πŸ”§ fix:** gap in header principal item - [[e3099f3b](https://github.com/Novout/betterwrite/commit/e3099f3b)] +* **🚧 chore:** lint - [[43d38269](https://github.com/Novout/betterwrite/commit/43d38269)] +* **🚩 refactor:** alert erros in def components - [[d117e8b1](https://github.com/Novout/betterwrite/commit/d117e8b1)] +* **πŸŽ‰ feat!:** encrypt option for .bw - [[d4a88e04](https://github.com/Novout/betterwrite/commit/d4a88e04)] +* **πŸŽ‰ feat:** bionic reading docx and pdf - [[3c5b472e](https://github.com/Novout/betterwrite/commit/3c5b472e)] +* **πŸŽ‰ feat(epub):** ignore fixed and image case - [[a06d38b2](https://github.com/Novout/betterwrite/commit/a06d38b2)] +* **🚧 chore:** update epub-gen-rs - [[21f27acd](https://github.com/Novout/betterwrite/commit/21f27acd)] +* **🚧 chore(epub):** toc title - [[bb3f4aa8](https://github.com/Novout/betterwrite/commit/bb3f4aa8)] +* **πŸŽ‰ feat:** initial .epub implementation - [[fb0b5db8](https://github.com/Novout/betterwrite/commit/fb0b5db8)] +* **πŸ”§ fix!:** clean save in before set entity raw - [[6be2b614](https://github.com/Novout/betterwrite/commit/6be2b614)] +* **🚩 refactor:** remove head dynamic set - [[3d3ef9b2](https://github.com/Novout/betterwrite/commit/3d3ef9b2)] +* **🚧 chore:** remove dropbox support - [[86628245](https://github.com/Novout/betterwrite/commit/86628245)] +* **🚧 chore:** ignore deprecation typescript - [[6a512ff2](https://github.com/Novout/betterwrite/commit/6a512ff2)] +* **🚧 chore:** update vite - [[739e444f](https://github.com/Novout/betterwrite/commit/739e444f)] +* **🚧 chore:** added claude.md - [[e10104cd](https://github.com/Novout/betterwrite/commit/e10104cd)] +* **πŸŽ‰ feat:** top file - [[7db91b93](https://github.com/Novout/betterwrite/commit/7db91b93)] +* **🚧 chore:** readme - [[c94ebf14](https://github.com/Novout/betterwrite/commit/c94ebf14)] + ### v1.3.27 * **🚧 chore:** update generi - [[9929a27e](https://github.com/Novout/betterwrite/commit/9929a27e)] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..74e9cd57a --- /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 08d7063cb..0054ce154 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/editor/entity/default/blocks/item/EditorEntityDefaultText.vue b/packages/app/src/components/page/editor/entity/default/blocks/item/EditorEntityDefaultText.vue index ce2a9087b..fcd9a52d6 100644 --- a/packages/app/src/components/page/editor/entity/default/blocks/item/EditorEntityDefaultText.vue +++ b/packages/app/src/components/page/editor/entity/default/blocks/item/EditorEntityDefaultText.vue @@ -7,9 +7,11 @@ fontFamily: EDITOR.styles.text?.fontFamily || 'Raleway', fontWeight: EDITOR.styles.text?.fontWeight || 500, fontSize: `${EDITOR.styles.text?.fontSize || 16}px`, + lineHeight: EDITOR.styles.text?.lineHeight ?? 1.5, + letterSpacing: `${EDITOR.styles.text?.letterSpacing ?? 0}px`, }" class="editable whitespace-pre-line text-justify text-theme-editor-entity-text hover:text-theme-editor-entity-text-hover active:text-theme-editor-entity-text-active" - :spellcheck="true" + :spellcheck="EDITOR.configuration.entity?.spellcheck ?? true" :contenteditable="true" :data-placeholder="focused ? t('editor.text.placeholder.base') : ''" @input="block.onInput" 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 5df2fe29a..f1431633a 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.bionicReading, }) }, 100) } diff --git a/packages/app/src/components/page/editor/header/EditorHeaderButton.vue b/packages/app/src/components/page/editor/header/EditorHeaderButton.vue index 69c7a1028..90fe4a532 100644 --- a/packages/app/src/components/page/editor/header/EditorHeaderButton.vue +++ b/packages/app/src/components/page/editor/header/EditorHeaderButton.vue @@ -1,18 +1,15 @@ - - diff --git a/packages/app/src/components/page/editor/main/EditorBase.vue b/packages/app/src/components/page/editor/main/EditorBase.vue index 1fc025093..1d70e2a46 100644 --- a/packages/app/src/components/page/editor/main/EditorBase.vue +++ b/packages/app/src/components/page/editor/main/EditorBase.vue @@ -4,9 +4,7 @@ class="flex flex-col wb-edit bg-theme-editor-background hover:bg-theme-editor-background-hover active:bg-theme-editor-background-active" :class="[ EDITOR.configuration.draggable ? 'fixed' : 'inline-block', - EDITOR.configuration.bars - ? 'w-full lg:w-3/4 sm:w-10/12 shadow-xl' - : 'w-full', + EDITOR.configuration.bars ? editorWidthClass : 'w-full', TUTORIAL.counter === 2 ? 'z-umax' : 'z-20', ]" > @@ -23,7 +21,7 @@ diff --git a/packages/app/src/components/page/editor/main/EditorBaseHeader.vue b/packages/app/src/components/page/editor/main/EditorBaseHeader.vue index fee654bf7..b2bc1b80a 100644 --- a/packages/app/src/components/page/editor/main/EditorBaseHeader.vue +++ b/packages/app/src/components/page/editor/main/EditorBaseHeader.vue @@ -9,7 +9,7 @@ ]" class="flex pr-2 sm:pr-5 h-12 justify-between items-center w-full shadow-lg bg-theme-editor-background hover:bg-theme-editor-background-hover active:bg-theme-editor-background-active" > -
+
-
diff --git a/packages/app/src/components/page/editor/main/render/history/EditorBaseRenderHistory.vue b/packages/app/src/components/page/editor/main/render/history/EditorBaseRenderHistory.vue index fbcfcdd49..15c24c856 100644 --- a/packages/app/src/components/page/editor/main/render/history/EditorBaseRenderHistory.vue +++ b/packages/app/src/components/page/editor/main/render/history/EditorBaseRenderHistory.vue @@ -14,13 +14,13 @@ class="flex-1 font-bold flex justify-around items-center font-raleway min-w-12 h-10 md:(h-9)" @click.prevent.stop="history.onLoad(item)" > -
+ -

{{ item.name || item.id }}

-
- +
+
+

+ {{ t('editor.aside.configuration.entity.spellcheck') }} +

+ +
diff --git a/packages/app/src/components/page/editor/project/preferences/container/EditorProjectPreferencesContainerStyles.vue b/packages/app/src/components/page/editor/project/preferences/container/EditorProjectPreferencesContainerStyles.vue index ac0ac5929..b611e0f0d 100644 --- a/packages/app/src/components/page/editor/project/preferences/container/EditorProjectPreferencesContainerStyles.vue +++ b/packages/app/src/components/page/editor/project/preferences/container/EditorProjectPreferencesContainerStyles.vue @@ -99,6 +99,21 @@
+
+

+ {{ t('editor.preferences.configuration.editor.layout') }} +

+
+

+ {{ t('editor.preferences.configuration.editor.editorWidth') }} +

+ +
+

{{ t('editor.preferences.configuration.editor.text') }} @@ -135,6 +150,28 @@ :min="8" />

+
+

+ {{ t('editor.preferences.configuration.editor.lineHeight') }} +

+ +
+
+

+ {{ t('editor.preferences.configuration.editor.letterSpacing') }} +

+ +

diff --git a/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue b/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue index 4f85e61bd..09089a5c4 100644 --- a/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue +++ b/packages/app/src/components/page/editor/tools/EditorToolsSpeechRecognition.vue @@ -6,24 +6,27 @@ :initial="{ opacity: 0 }" :enter="{ opacity: 1 }" :delay="0" - class="flex z-50 bg-rgba-blur bg-theme-background-3 items-center justify-center absolute z-50 right-0 transform m-5 p-2 shadow-lg w-60 absolute transform -translate-y-1/2 top-1/2" + class="flex z-50 bg-rgba-blur bg-theme-background-3 items-center justify-center absolute z-50 right-0 transform m-5 p-2 shadow-lg w-72 absolute transform -translate-y-1/2 top-1/2" >

-
-
+
+
+
+ +
@@ -35,12 +38,21 @@ import useEmitter from '@/use/emitter' import { VueI18nAllISO } from 'better-write-languages' import { usePlugin } from 'better-write-plugin-core' - import { onMounted, ref } from 'vue' + import { onMounted, ref, computed } from 'vue' + import { useDevicesList } from '@vueuse/core' + import { useI18n } from 'vue-i18n' const plugin = usePlugin() const emitter = useEmitter() + const { t } = useI18n() + + const { audioInputs } = useDevicesList({ requestPermissions: true }) + const microphones = computed(() => + audioInputs.value.filter((d) => d.deviceId && d.deviceId !== 'default'), + ) const lang = ref(VueI18nAllISO[0]) + const selectedDeviceId = ref(t('editor.tools.speechRecognition.defaultMic')) const listening = ref(false) onMounted(() => { diff --git a/packages/app/src/pages/Landing.vue b/packages/app/src/pages/Landing.vue index f68fe2ed8..e3447fb2e 100644 --- a/packages/app/src/pages/Landing.vue +++ b/packages/app/src/pages/Landing.vue @@ -5,11 +5,34 @@ >
+
+
+
+
+
+
+
{ plugin.emit('call-landing-unmounted') }) + + const rand = (min: number, max: number) => + Math.random() * (max - min) + min + + interface BookBlock { + lines: { width: string; height: string }[] + style: string + } + + interface BookColumn { + id: number + blocks: BookBlock[] + style: string + responsiveClass: string + } + + const NUM_COLUMNS = 9 + + const bookColumns = Array.from({ length: NUM_COLUMNS }, (_, i): BookColumn => { + const leftPct = (i / NUM_COLUMNS) * 100 + rand(-2, 2) + const duration = rand(18, 32) + const delay = rand(-duration, 0) + const maxOpacity = rand(0.07, 0.16) + + const blocks: BookBlock[] = Array.from({ length: rand(5, 15) | 0 }, () => { + const lineCount = (rand(2, 7) | 0) + const blockDelay = rand(0, 8) + const blockFadeDuration = rand(3, 6) + + return { + lines: Array.from({ length: lineCount }, (_, li) => { + const isLast = li === lineCount - 1 + return { + width: isLast ? `${rand(40, 75)}%` : `${rand(85, 100)}%`, + height: '2px', + } + }), + style: [ + `animation: bookFadeInOut ${blockFadeDuration}s ${blockDelay}s ease-in-out infinite alternate`, + `--max-op: ${maxOpacity}`, + ].join(';'), + } + }) + + // mobile: 3 cols, sm: 5 cols, lg: all 9 + const responsiveClass = + i < 3 ? 'flex' : i < 5 ? 'hidden sm:flex' : 'hidden lg:flex' + + return { + id: i, + blocks, + responsiveClass, + style: [ + `left: ${leftPct}%`, + `width: ${rand(60, 110)}px`, + `animation: bookDrift ${duration}s ${delay}s linear infinite`, + ].join(';'), + } + }) + + diff --git a/packages/app/src/router.ts b/packages/app/src/router.ts index 3cd25493e..f0b3edaf2 100644 --- a/packages/app/src/router.ts +++ b/packages/app/src/router.ts @@ -15,7 +15,13 @@ router.beforeResolve(async (to, from, next) => { const token = url.includes('access_token') const isOnline = useNetwork().isOnline.value - if (to.name === 'Landing' && token && !env.isDev()) { + if(env.isDev()) { + next() + + return + } + + if (to.name === 'Landing' && token) { next({ name: 'Main' }) return @@ -33,7 +39,7 @@ router.beforeResolve(async (to, from, next) => { return } - if (to.name === 'Landing' && user && !env.isDev()) { + if (to.name === 'Landing' && user) { next({ name: 'Main' }) return diff --git a/packages/app/src/store/absolute.ts b/packages/app/src/store/absolute.ts index 1603cfd93..b486d884b 100644 --- a/packages/app/src/store/absolute.ts +++ b/packages/app/src/store/absolute.ts @@ -31,6 +31,7 @@ export const useAbsoluteStore = defineStore('absolute', { configuration: false, generate: false, }, + epub: {}, docx: { configuration: false, }, @@ -59,6 +60,10 @@ export const useAbsoluteStore = defineStore('absolute', { create: false, template: false, }, + bw: { + encryptOnExport: false, + }, + bionicReading: false, } }, }) diff --git a/packages/app/src/store/editor.ts b/packages/app/src/store/editor.ts index 95aeb0f7f..b131f7442 100644 --- a/packages/app/src/store/editor.ts +++ b/packages/app/src/store/editor.ts @@ -15,6 +15,8 @@ export const useEditorStore = defineStore('editor', { fontFamily: 'Raleway', fontWeight: 400, fontSize: 16, + lineHeight: 1.5, + letterSpacing: 0, }, header: { fontFamily: 'Raleway', @@ -33,6 +35,7 @@ export const useEditorStore = defineStore('editor', { backgroundGrayscale: false, backgroundSaturate: false, backgroundSepia: false, + editorWidth: 'medium', }, }, configuration: { @@ -57,6 +60,7 @@ export const useEditorStore = defineStore('editor', { topBar: true, entity: { insertEntityInParagraphBreakLine: true, + spellcheck: true, }, commands: { paragraph: { diff --git a/packages/app/src/use/__tests__/env.test.ts b/packages/app/src/use/__tests__/env.test.ts new file mode 100644 index 000000000..3c8531470 --- /dev/null +++ b/packages/app/src/use/__tests__/env.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest' +import { useEnv } from '../env' + +// import.meta.env is stubbed in setup.ts + +describe('useEnv', () => { + const env = useEnv() + + it('getSentryDsn() returns the Sentry DSN', () => { + expect(env.getSentryDsn()).toBe('https://sentry.example.com') + }) + + it('getProdUrl() returns the base URL', () => { + expect(env.getProdUrl()).toBe('https://betterwrite.io') + }) + + it('getCorrectLocalUrl() returns localhost in dev mode', () => { + expect(env.getCorrectLocalUrl()).toBe('http://localhost:3000') + }) + + it('projectEmpty() returns the empty project sentinel', () => { + expect(env.projectEmpty()).toBe('__EMPTY__') + }) + + it('isEmptyProject() detects the empty project name', () => { + expect(env.isEmptyProject('__EMPTY__')).toBe(true) + expect(env.isEmptyProject('My Project')).toBe(false) + }) + + it('storageLimitSaver() returns the limit saver key', () => { + expect(env.storageLimitSaver()).toBe('__BW_LIMIT_SAVER__') + }) + + it('emptyLine() returns the empty line sentinel', () => { + expect(env.emptyLine()).toBe('__EMPTY_LINE__') + }) + + it('lineBreak() returns the line break sentinel', () => { + expect(env.lineBreak()).toBe('__LINE_BREAK__') + }) + + it('pageBreak() returns the page break sentinel', () => { + expect(env.pageBreak()).toBe('__PAGE_BREAK__') + }) + + it('packageVersion() returns the version string', () => { + expect(env.packageVersion()).toBe('1.0.0') + }) + + it('isDev() returns true in test environment', () => { + expect(env.isDev()).toBe(true) + }) + + it('production() returns false in test environment', () => { + expect(env.production()).toBe(false) + }) + + it('getAccountPlanLimit() returns limits per plan', () => { + expect(env.getAccountPlanLimit('beginner')).toBe('10') + expect(env.getAccountPlanLimit('intermediate')).toBe('50') + expect(env.getAccountPlanLimit('advanced')).toBe('100') + expect(env.getAccountPlanLimit('unlimited')).toBe('999') + }) +}) diff --git a/packages/app/src/use/__tests__/format.test.ts b/packages/app/src/use/__tests__/format.test.ts new file mode 100644 index 000000000..b4a48b672 --- /dev/null +++ b/packages/app/src/use/__tests__/format.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useFormat } from '../format' + +describe('useFormat', () => { + const { simple, actually, lastTime } = useFormat() + + describe('simple()', () => { + const date = new Date(2024, 5, 15, 9, 5, 3) // 2024-06-15 09:05:03 + + it('formats as default (YYYY-M-D H:M:S)', () => { + const result = simple(date, 'default') + expect(result).toBe(`2024-5-6 9:5:3`) + }) + + it('formats as resume (HH:MM)', () => { + expect(simple(date, 'resume')).toBe('09:05') + }) + + it('formats as iso', () => { + expect(simple(date, 'iso')).toBe(date.toISOString()) + }) + + it('defaults to "default" format when type is omitted', () => { + const result = simple(date) + expect(result).toMatch(/^\d{4}-\d+-\d+ \d+:\d+:\d+$/) + }) + }) + + describe('actually()', () => { + it('returns a string for the current date', () => { + const result = actually() + expect(typeof result).toBe('string') + expect(result.length).toBeGreaterThan(0) + }) + + it('returns ISO string when type is iso', () => { + const before = new Date().toISOString().slice(0, 10) + expect(actually('iso')).toContain(before) + }) + + it('returns HH:MM format for resume', () => { + expect(actually('resume')).toMatch(/^\d{2}:\d{2}$/) + }) + }) + + describe('lastTime()', () => { + it('returns the passed updatedAt value', () => { + expect(lastTime('2024-01-01T00:00:00.000Z')).toBe('2024-01-01T00:00:00.000Z') + }) + + it('falls back to actually() when updatedAt is undefined', () => { + const result = lastTime(undefined as any) + expect(typeof result).toBe('string') + }) + }) +}) diff --git a/packages/app/src/use/__tests__/input.test.ts b/packages/app/src/use/__tests__/input.test.ts new file mode 100644 index 000000000..bb2f8f941 --- /dev/null +++ b/packages/app/src/use/__tests__/input.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi } from 'vitest' +import { useInput } from '../input' + +describe('useInput', () => { + const { pasteText, expandTextArea, getScrollHeight } = useInput() + + describe('pasteText()', () => { + const makeEvent = (text: string) => ({ + clipboardData: { getData: () => text }, + }) + + it('splits by newline and trims', () => { + expect(pasteText(makeEvent('hello\nworld'))).toEqual(['hello', 'world']) + }) + + it('filters out empty lines', () => { + expect(pasteText(makeEvent('a\n\nb'))).toEqual(['a', 'b']) + }) + + it('normalises multiple spaces', () => { + expect(pasteText(makeEvent(' foo bar '))).toEqual(['foo bar']) + }) + + it('returns empty array for blank-only text', () => { + expect(pasteText(makeEvent('\n\n\n'))).toEqual([]) + }) + }) + + describe('expandTextArea()', () => { + it('sets element height to scrollHeight', () => { + const el = document.createElement('textarea') + Object.defineProperty(el, 'scrollHeight', { value: 120, configurable: true }) + expandTextArea(el) + expect(el.style.height).toBe('120px') + }) + + it('does nothing when element is falsy', () => { + expect(() => expandTextArea(null as any)).not.toThrow() + }) + }) + + describe('getScrollHeight()', () => { + it('stores _baseScrollHeight and resets value', () => { + const el = { value: 'hello', scrollHeight: 80, _baseScrollHeight: 0 } + getScrollHeight(el) + expect(el._baseScrollHeight).toBe(80) + expect(el.value).toBe('hello') + }) + }) +}) diff --git a/packages/app/src/use/__tests__/populate.test.ts b/packages/app/src/use/__tests__/populate.test.ts new file mode 100644 index 000000000..125197411 --- /dev/null +++ b/packages/app/src/use/__tests__/populate.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi } from 'vitest' + +// usePopulate depends on useDefines which uses vue-i18n β€” mock it +vi.mock('vue-i18n', () => ({ + createI18n: () => ({ global: { t: (k: string) => k } }), + useI18n: () => ({ + t: (k: string) => k, + availableLocales: ['en-US'], + getLocaleMessage: () => ({ + generator: { + block: { alignment: { both: 'Justify', left: 'Left', right: 'Right', center: 'Center' } }, + flow: { item: { annotation: 'Annotation', 'break-page': 'Break page', bw: 'BW', content: 'Content' } }, + }, + editor: { + pdf: { configuration: { alignment: { justify: 'Justify', left: 'Left', center: 'Center', right: 'Right' } } }, + characters: { item: { nameCaseStrict: 'Strict', nameCaseDefault: 'Default', nameCaseAll: 'All' } }, + presence: { type: { owner: 'Owner', visit: 'Visit', collaborator: 'Collaborator' } }, + schemas: { + types: { characters: { target: 'Characters' }, default: { target: 'Default' } }, + create: { templates: { simple: { title: 'Simple' }, enthusiast: { title: 'Enthusiast' } } }, + }, + }, + }), + }), +})) + +import { usePopulate } from '../populate' + +describe('usePopulate', () => { + const populate = usePopulate() + + describe('cmd()', () => { + it('returns the entity purge command', () => { + expect(populate.cmd()).toContain('entity purge') + }) + }) + + describe('debug()', () => { + it('paragraph() returns a non-empty string', () => { + expect(populate.debug().names().paragraph()).toBeTruthy() + }) + + it('text() returns a lorem ipsum string', () => { + expect(populate.debug().names().text()).toContain('Lorem ipsum') + }) + }) + + describe('docx()', () => { + it('flow() returns default flow items', () => { + const flow = populate.docx().flow() + expect(flow.length).toBeGreaterThan(0) + expect(flow.some((f) => f.type === 'content')).toBe(true) + expect(flow.some((f) => f.type === 'bw')).toBe(true) + }) + + it('styles() has paragraph with expected defaults', () => { + const styles = populate.docx().styles() + expect(styles.paragraph.bold).toBe(false) + expect(styles.paragraph.size).toBe(14) + expect(typeof styles.paragraph.color).toBe('string') + }) + + it('styles() has headingOne with bold true', () => { + expect(populate.docx().styles().headingOne.bold).toBe(true) + }) + }) + + describe('pdf()', () => { + it('styles() returns paragraph and headings', () => { + const styles = populate.pdf().styles() + expect(styles).toHaveProperty('paragraph') + expect(styles).toHaveProperty('headingOne') + expect(styles).toHaveProperty('headingTwo') + expect(styles).toHaveProperty('headingThree') + }) + + it('paragraph fontSize is a number', () => { + const { paragraph } = populate.pdf().styles() + expect(typeof paragraph.fontSize).toBe('number') + }) + }) +}) diff --git a/packages/app/src/use/__tests__/scroll.test.ts b/packages/app/src/use/__tests__/scroll.test.ts new file mode 100644 index 000000000..6c8d7e20f --- /dev/null +++ b/packages/app/src/use/__tests__/scroll.test.ts @@ -0,0 +1,120 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useScroll } from '../scroll' + +describe('useScroll', () => { + const scroll = useScroll() + + beforeEach(() => { + document.body.innerHTML = '' + }) + + describe('to()', () => { + it('calls scrollIntoView on the matched element', () => { + const div = document.createElement('div') + div.id = 'target' + const spy = vi.fn() + div.scrollIntoView = spy + document.body.appendChild(div) + + scroll.to('#target') + expect(spy).toHaveBeenCalledOnce() + }) + + it('calls scrollIntoView with center block when display is center', () => { + const div = document.createElement('div') + div.id = 'center-target' + const spy = vi.fn() + div.scrollIntoView = spy + document.body.appendChild(div) + + scroll.to('#center-target', 'center') + expect(spy).toHaveBeenCalledWith({ behavior: 'auto', block: 'center', inline: 'center' }) + }) + + it('does not throw when element does not exist', () => { + expect(() => scroll.to('#nonexistent')).not.toThrow() + }) + }) + + describe('entity()', () => { + it('scrolls entity element into view', () => { + const div = document.createElement('div') + div.id = 'entity-42' + const spy = vi.fn() + div.scrollIntoView = spy + document.body.appendChild(div) + + scroll.entity(42) + expect(spy).toHaveBeenCalledOnce() + }) + + it('scrolls to center when display is center', () => { + const div = document.createElement('div') + div.id = 'entity-7' + const spy = vi.fn() + div.scrollIntoView = spy + document.body.appendChild(div) + + scroll.entity(7, 'center') + expect(spy).toHaveBeenCalledWith({ behavior: 'auto', block: 'center', inline: 'center' }) + }) + + it('scrolls to top when display is top', () => { + const div = document.createElement('div') + div.id = 'entity-3' + const spy = vi.fn() + div.scrollIntoView = spy + document.body.appendChild(div) + + scroll.entity(3, 'top') + expect(spy).toHaveBeenCalledWith({ behavior: 'auto', block: 'start', inline: 'start' }) + }) + + it('scrolls to bottom when display is bottom', () => { + const div = document.createElement('div') + div.id = 'entity-5' + const spy = vi.fn() + div.scrollIntoView = spy + document.body.appendChild(div) + + scroll.entity(5, 'bottom') + expect(spy).toHaveBeenCalledWith({ behavior: 'auto', block: 'end', inline: 'end' }) + }) + + it('does not throw for missing entity', () => { + expect(() => scroll.entity(9999)).not.toThrow() + }) + }) + + describe('editor()', () => { + it('get() returns 0 when #edit does not exist', () => { + expect(scroll.editor().get()).toBe(0) + }) + + it('set() does nothing when #edit does not exist', () => { + expect(() => scroll.editor().set(100)).not.toThrow() + }) + + it('get() and set() work when #edit exists', () => { + const div = document.createElement('div') + div.id = 'edit' + Object.defineProperty(div, 'scrollTop', { + writable: true, + value: 0, + }) + document.body.appendChild(div) + + scroll.editor().set(200) + expect(div.scrollTop).toBe(200) + }) + }) + + describe('force()', () => { + it('does not throw when element does not exist', () => { + vi.useFakeTimers() + expect(() => scroll.force('#nonexistent')).not.toThrow() + vi.runAllTimers() + vi.useRealTimers() + }) + }) +}) diff --git a/packages/app/src/use/__tests__/setup.ts b/packages/app/src/use/__tests__/setup.ts new file mode 100644 index 000000000..d6f72afd7 --- /dev/null +++ b/packages/app/src/use/__tests__/setup.ts @@ -0,0 +1,19 @@ +import { vi } from 'vitest' + +// stub import.meta.env values used by useEnv +vi.stubEnv('VITE_SENTRY_DSN', 'https://sentry.example.com') +vi.stubEnv('VITE_BASE_URL', 'https://betterwrite.io') +vi.stubEnv('VITE_PROJECT_EMPTY', '__EMPTY__') +vi.stubEnv('VITE_LOCAL_STORAGE', '__BW_LOCAL__') +vi.stubEnv('VITE_EMPTY_LINE', '__EMPTY_LINE__') +vi.stubEnv('VITE_LINE_BREAK', '__LINE_BREAK__') +vi.stubEnv('VITE_PAGE_BREAK', '__PAGE_BREAK__') +vi.stubEnv('VITE_INITIAL_LOAD', '__INITIAL_LOAD__') +vi.stubEnv('VITE_DROPBOX_APP_KEY', 'dropbox-key') +vi.stubEnv('VITE_GOOGLE_FONTS_MAX_FONTS', '100') +vi.stubEnv('VITE_GOOGLE_FONTS_KEY', 'google-key') +vi.stubEnv('VITE_BEGINEER_LIMIT', '10') +vi.stubEnv('VITE_INTERMEDIATE_LIMIT', '50') +vi.stubEnv('VITE_ADVANCED_LIMIT', '100') +vi.stubEnv('VITE_UNLIMITED_LIMIT', '999') +vi.stubEnv('PACKAGE_VERSION', '1.0.0') diff --git a/packages/app/src/use/__tests__/transformer.test.ts b/packages/app/src/use/__tests__/transformer.test.ts new file mode 100644 index 000000000..9ecfdeaf8 --- /dev/null +++ b/packages/app/src/use/__tests__/transformer.test.ts @@ -0,0 +1,140 @@ +import { describe, it, expect, vi } from 'vitest' +import { DOCXAlignmentType } from 'better-write-types' + +const mockT = (k: string) => { + const map: Record = { + 'generator.block.alignment.both': 'Justify', + 'generator.block.alignment.left': 'Left', + 'generator.block.alignment.right': 'Right', + 'generator.block.alignment.center': 'Center', + 'generator.flow.item.annotation': 'Annotation', + 'generator.flow.item.break-page': 'Break page', + 'generator.flow.item.bw': 'BW', + 'generator.flow.item.content': 'Content', + 'editor.characters.item.nameCaseStrict': 'Strict', + 'editor.characters.item.nameCaseDefault': 'Default', + 'editor.characters.item.nameCaseAll': 'All', + 'editor.presence.type.owner': 'Owner', + 'editor.presence.type.visit': 'Visit', + 'editor.presence.type.collaborator': 'Collaborator', + 'editor.schemas.types.characters.target': 'Characters', + 'editor.schemas.types.default.target': 'Default', + 'editor.schemas.create.templates.simple.title': 'Simple', + 'editor.schemas.create.templates.enthusiast.title': 'Enthusiast', + } + return map[k] ?? k +} + +const mockLocaleMessage = () => ({ + generator: { + block: { alignment: { justify: 'Justify', left: 'Left', center: 'Center', right: 'Right' } }, + flow: { item: { annotation: 'Annotation', 'break-page': 'Break page', bw: 'BW', content: 'Content' } }, + }, + editor: { + pdf: { configuration: { alignment: { justify: 'Justify', left: 'Left', center: 'Center', right: 'Right' } } }, + characters: { item: { nameCaseStrict: 'Strict', nameCaseDefault: 'Default', nameCaseAll: 'All' } }, + presence: { type: { owner: 'Owner', visit: 'Visit', collaborator: 'Collaborator' } }, + schemas: { + types: { characters: { target: 'Characters' }, default: { target: 'Default' } }, + create: { templates: { simple: { title: 'Simple' }, enthusiast: { title: 'Enthusiast' } } }, + }, + }, +}) + +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: mockT, + availableLocales: ['en-US'], + getLocaleMessage: () => mockLocaleMessage(), + }), +})) + +import { useTransformer } from '../generator/transformer' + +describe('useTransformer', () => { + const transformer = useTransformer() + + describe('docx().entityAlignment()', () => { + it('getter: returns translated label for "both"', () => { + expect(transformer.docx().entityAlignment('both', 'getter')).toBe('Justify') + }) + + it('getter: returns translated label for "left"', () => { + expect(transformer.docx().entityAlignment('left', 'getter')).toBe('Left') + }) + + it('setter: translates label back to DOCX alignment type', () => { + expect(transformer.docx().entityAlignment('Justify', 'setter')).toBe(DOCXAlignmentType.JUSTIFIED) + }) + + it('setter: falls back to JUSTIFIED for unknown value', () => { + expect(transformer.docx().entityAlignment('unknown', 'setter')).toBe(DOCXAlignmentType.JUSTIFIED) + }) + }) + + describe('docx().flowItem()', () => { + it('getter: maps "content" to locale string', () => { + expect(transformer.docx().flowItem('content', 'getter')).toBe('Content') + }) + + it('setter: maps locale string back to "content"', () => { + expect(transformer.docx().flowItem('Content', 'setter')).toBe('content') + }) + + it('setter: falls back to "annotation" for unknown value', () => { + expect(transformer.docx().flowItem('unknown', 'setter')).toBe('annotation') + }) + }) + + describe('characters().nameCase()', () => { + it('getter: maps "strict" to locale string', () => { + expect(transformer.characters().nameCase('strict', 'getter')).toBe('Strict') + }) + + it('setter: maps locale string back to "default"', () => { + expect(transformer.characters().nameCase('Default', 'setter')).toBe('default') + }) + + it('setter: falls back to "default" for unknown value', () => { + expect(transformer.characters().nameCase('unknown', 'setter')).toBe('default') + }) + }) + + describe('presence().type()', () => { + it('getter: maps "owner" to locale string', () => { + expect(transformer.presence().type('owner', 'getter')).toBe('Owner') + }) + + it('setter: maps locale string to "visit"', () => { + expect(transformer.presence().type('Visit', 'setter')).toBe('visit') + }) + + it('setter: falls back to "visit" for unknown value', () => { + expect(transformer.presence().type('unknown', 'setter')).toBe('visit') + }) + }) + + describe('schemas().type()', () => { + it('getter: maps "characters" to locale string', () => { + expect(transformer.schemas().type('characters', 'getter')).toBe('Characters') + }) + + it('setter: maps locale string to "default"', () => { + expect(transformer.schemas().type('Default', 'setter')).toBe('default') + }) + }) + + describe('schemas().template()', () => { + it('getter: maps "simple" to locale string', () => { + expect(transformer.schemas().template('simple', 'getter')).toBe('Simple') + }) + + it('setter: maps locale string to "enthusiast"', () => { + expect(transformer.schemas().template('Enthusiast', 'setter')).toBe('enthusiast') + }) + + it('setter: falls back to "simple" for unknown value', () => { + expect(transformer.schemas().template('unknown', 'setter')).toBe('simple') + }) + }) +}) diff --git a/packages/app/src/use/__tests__/utils.test.ts b/packages/app/src/use/__tests__/utils.test.ts new file mode 100644 index 000000000..1e9c12e00 --- /dev/null +++ b/packages/app/src/use/__tests__/utils.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from 'vitest' +import { useUtils } from '../utils' + +describe('useUtils', () => { + const utils = useUtils() + + describe('array()', () => { + const { insert, mapToObject } = utils.array() + + it('inserts items at the given index', () => { + expect(insert([1, 2, 4], 2, 3)).toEqual([1, 2, 3, 4]) + }) + + it('inserts at index 0', () => { + expect(insert([2, 3], 0, 1)).toEqual([1, 2, 3]) + }) + + it('inserts at end', () => { + expect(insert([1, 2], 2, 3)).toEqual([1, 2, 3]) + }) + + it('converts a Map to an object', () => { + const map = new Map([['a', 1], ['b', 2]]) + expect(mapToObject(map)).toEqual({ a: 1, b: 2 }) + }) + }) + + describe('join()', () => { + it('joins path parts with /', () => { + expect(utils.join(['a', 'b', 'c'])).toBe('a/b/c') + }) + + it('collapses duplicate separators', () => { + expect(utils.join(['a/', '/b'])).toBe('a/b') + }) + + it('accepts custom separator', () => { + expect(utils.join(['a', 'b'], '-')).toBe('a-b') + }) + }) + + describe('id()', () => { + const { uuidv4, nano } = utils.id() + + it('uuidv4 returns a valid UUID format', () => { + expect(uuidv4()).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + ) + }) + + it('uuidv4 generates unique values', () => { + expect(uuidv4()).not.toBe(uuidv4()) + }) + + it('nano generates a string of default length', () => { + expect(nano().length).toBeGreaterThan(0) + }) + + it('nano uses prefix when provided', () => { + expect(nano({ prefix: 'entity' })).toMatch(/^entity-/) + }) + + it('nano uses suffix when provided', () => { + expect(nano({ suffix: 'end' })).toMatch(/-end$/) + }) + + it('nano respects custom length', () => { + const id = nano({ length: 10 }) + // length = 10 chars + possible prefix/suffix + expect(id.length).toBeGreaterThanOrEqual(10) + }) + }) + + describe('text()', () => { + const { kebab, defaultWhitespace, randomLetter, randomColor } = utils.text() + + it('converts text to kebab-case', () => { + expect(kebab('Hello World')).toBe('hello-world') + }) + + it('converts multiple spaces to kebab dashes', () => { + expect(kebab('foo bar baz')).toBe('foo-bar-baz') + }) + + it('replaces   with space', () => { + expect(defaultWhitespace('hello world')).toBe('hello world') + }) + + it('replaces   with space', () => { + expect(defaultWhitespace('a b')).toBe('a b') + }) + + it('randomLetter returns an uppercase letter', () => { + const letter = randomLetter() + expect(letter).toMatch(/^[A-Z]$/) + }) + + it('randomColor returns a hex color string', () => { + expect(randomColor()).toMatch(/^#[0-9a-f]{1,6}$/i) + }) + }) + + describe('regex()', () => { + const r = utils.regex() + + it('links() matches http URLs', () => { + expect('https://betterwrite.io'.match(r.links())).toBeTruthy() + }) + + it('links() matches www URLs', () => { + expect('www.example.com'.match(r.links())).toBeTruthy() + }) + + it('htmlTags() matches html tags', () => { + expect('text'.match(r.htmlTags())).toBeTruthy() + }) + + it('divTag() matches a div', () => { + expect('
content
'.match(r.divTag())).toBeTruthy() + }) + }) + + describe('delay()', () => { + it('resolves after the given time', async () => { + const start = Date.now() + await utils.delay(50) + expect(Date.now() - start).toBeGreaterThanOrEqual(40) + }) + }) +}) diff --git a/packages/app/src/use/block/text.ts b/packages/app/src/use/block/text.ts index bd5aa9078..449dff200 100644 --- a/packages/app/src/use/block/text.ts +++ b/packages/app/src/use/block/text.ts @@ -51,7 +51,9 @@ 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') @@ -121,6 +123,15 @@ export const useBlockText = ({ if (id === index.value) CONTEXT.entities[id].raw = result }) + emitter.on('entity-speech-interim', ({ id, base, interim }) => { + if (id !== index.value) return + + // mostra o texto final acumulado + interim em itΓ‘lico/opaco, sem salvar no store + input.value.innerHTML = base + ? `${base} ${interim}` + : `${interim}` + }) + emitter.on('entity-text-force-save', () => { if (isSalvageable.value && input.value && input.value.innerHTML) { save(index.value, input.value.innerHTML) diff --git a/packages/app/src/use/project.ts b/packages/app/src/use/project.ts index 7422e98e6..8bfbdbe19 100644 --- a/packages/app/src/use/project.ts +++ b/packages/app/src/use/project.ts @@ -27,7 +27,7 @@ import { useFileSystemAccess } from '@vueuse/core' import { useI18n } from 'vue-i18n' import { read } from 'better-write-plugin-importer' import useEmitter from './emitter' -import { writeBW } from 'better-write-extension' +import { writeBW, encryptBW } from 'better-write-extension' import { useAuthStore } from '@/store/auth' import { useDOCXStore } from '@/store/docx' import { useGlobalStore } from '@/store/global' @@ -197,12 +197,29 @@ export const useProject = () => { 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/app/src/use/storage/storage.ts b/packages/app/src/use/storage/storage.ts index 98f87dacd..c2937c04a 100644 --- a/packages/app/src/use/storage/storage.ts +++ b/packages/app/src/use/storage/storage.ts @@ -197,6 +197,28 @@ export const useStorage = () => { } } + if (!_.editor.styles.text.lineHeight) { + _.editor.styles.text = { + ..._.editor.styles.text, + lineHeight: 1.5, + letterSpacing: 0, + } + } + + if (!_.editor.styles.base.editorWidth) { + _.editor.styles.base = { + ..._.editor.styles.base, + editorWidth: 'medium', + } + } + + if (!_.editor.configuration.entity.hasOwnProperty('spellcheck')) { + _.editor.configuration.entity = { + ..._.editor.configuration.entity, + spellcheck: true, + } + } + if (!_.project.schemas) { _.project.schemas = [] if (_.project.annotations) { diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index db0f4db09..c17d66f7e 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -16,7 +16,14 @@ import { viteStdlib } from './scripts/vite' import { FontaineTransform as CSSFontaine } from 'fontaine' import windiCSS from 'vite-plugin-windicss' import stdLibBrowser from 'node-stdlib-browser' -import { routes } from './src/routes' +const sitemapRoutes = [ + '/landing', + '/about', + '/terms-of-use', + '/support', + '/questions', + '/privacy', +] export default ({ mode }: any) => { process.env = { ...process.env, ...loadEnv(mode, process.cwd()) } @@ -31,6 +38,7 @@ export default ({ mode }: any) => { }, optimizeDeps: { include: ['buffer', 'process'], + exclude: ['epub-gen3'] }, define: { __VUE_I18N_FULL_INSTALL__: true, @@ -59,12 +67,7 @@ export default ({ mode }: any) => { viteSitemap({ hostname: process.env.VITE_BASE_URL, outDir: 'dist/render', - dynamicRoutes: routes - .filter( - (m) => - !m.redirect && m.path !== '/:pathMatch(.*)*' && m.path !== '/', - ) - .map(({ path }) => path), + dynamicRoutes: sitemapRoutes, robots: [{ userAgent: '*', allow: '/' }], }), vitePersist(), diff --git a/packages/client-storage/package.json b/packages/client-storage/package.json index 3522fe6cf..3354d146d 100644 --- a/packages/client-storage/package.json +++ b/packages/client-storage/package.json @@ -1,6 +1,6 @@ { "name": "better-write-client-storage", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -16,7 +16,7 @@ "build": "tsup" }, "dependencies": { - "better-write-types": "^1.3.27" + "better-write-types": "^1.4.0" }, "files": [ "dist/**/*", diff --git a/packages/color-converter/package.json b/packages/color-converter/package.json index f573b82ca..47d5dc52c 100644 --- a/packages/color-converter/package.json +++ b/packages/color-converter/package.json @@ -1,6 +1,6 @@ { "name": "better-write-color-converter", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -18,7 +18,7 @@ "build": "tsup" }, "dependencies": { - "better-write-types": "^1.3.27" + "better-write-types": "^1.4.0" }, "files": [ "dist/**/*", diff --git a/packages/contenteditable-ast/package.json b/packages/contenteditable-ast/package.json index e15908c2b..1f3d374c2 100644 --- a/packages/contenteditable-ast/package.json +++ b/packages/contenteditable-ast/package.json @@ -1,6 +1,6 @@ { "name": "better-write-contenteditable-ast", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -18,7 +18,7 @@ "build": "tsup" }, "dependencies": { - "better-write-types": "^1.3.27" + "better-write-types": "^1.4.0" }, "files": [ "dist/**/*", diff --git a/packages/extension/package.json b/packages/extension/package.json index 0f0d7d604..29f7796fb 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "better-write-extension", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -16,7 +16,7 @@ "build": "tsup" }, "dependencies": { - "better-write-types": "^1.3.27" + "better-write-types": "^1.4.0" }, "files": [ "dist/**/*", diff --git a/packages/extension/src/index.ts b/packages/extension/src/index.ts index c1f690ea4..fa905eb46 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,11 +34,140 @@ export const writeBW = async (data: string) => { return target } -export const readBW = async (blob: Blob): Promise => { - const zipFileReader = new BlobReader(blob) +// --- WebCrypto helpers --- - const writer = new TextWriter() +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 encEntry = entries.find((e) => e.filename === 'encrypted.bin') + 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() diff --git a/packages/google-fonts-api/package.json b/packages/google-fonts-api/package.json index a7e1f955a..b7b75f7cb 100644 --- a/packages/google-fonts-api/package.json +++ b/packages/google-fonts-api/package.json @@ -1,6 +1,6 @@ { "name": "better-write-google-fonts-api", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -16,7 +16,7 @@ "build": "tsup" }, "dependencies": { - "better-write-types": "^1.3.27" + "better-write-types": "^1.4.0" }, "files": [ "dist/**/*", diff --git a/packages/image-converter/package.json b/packages/image-converter/package.json index 4a7e93fd2..fcf18de79 100644 --- a/packages/image-converter/package.json +++ b/packages/image-converter/package.json @@ -1,6 +1,6 @@ { "name": "better-write-image-converter", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -17,7 +17,7 @@ "build": "tsup" }, "dependencies": { - "better-write-types": "^1.3.27" + "better-write-types": "^1.4.0" }, "files": [ "dist/**/*", diff --git a/packages/languages/package.json b/packages/languages/package.json index d694fbb1a..735528c51 100644 --- a/packages/languages/package.json +++ b/packages/languages/package.json @@ -1,6 +1,6 @@ { "name": "better-write-languages", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -17,8 +17,8 @@ "build": "tsup" }, "dependencies": { - "better-write-plugin-exporter-pdf": "^1.3.27", - "better-write-types": "^1.3.27" + "better-write-plugin-exporter-pdf": "^1.4.0", + "better-write-types": "^1.4.0" }, "files": [ "dist/**/*", diff --git a/packages/languages/src/en-US.ts b/packages/languages/src/en-US.ts index 8a2550bb2..a0ba724a1 100644 --- a/packages/languages/src/en-US.ts +++ b/packages/languages/src/en-US.ts @@ -173,9 +173,13 @@ export default { header: 'Top Bar', graph: 'Sidebar', text: 'Editor Text', + layout: 'Layout', + editorWidth: 'Editor Width', fontFamily: 'Font Family', fontWeight: 'Font Weight', fontSize: 'Font Size', + lineHeight: 'Line Height', + letterSpacing: 'Letter Spacing', }, }, styles: { @@ -235,6 +239,9 @@ export default { load: 'Cloud Load', save: 'Cloud Save', }, + export: { + bionicReading: 'Enable Bionic Reading', + }, pdf: { preview: 'Preview (.PDF)', generate: 'Generate (.PDF)', @@ -270,6 +277,7 @@ export default { import: 'Import', export: 'Export', exportAs: 'Export as...', + encryptBW: 'Encrypt .bw', }, chapter: { drafts: 'Drafts', @@ -304,6 +312,12 @@ export default { substitutions: 'Substitutions', }, }, + tools: { + speechRecognition: { + defaultMic: 'Default microphone', + unknownMic: 'Unknown microphone', + }, + }, window: { confirmConfiguration: 'Do you want to quit saving the settings?', saveDropbox: 'Are you sure to save this file to Dropbox?', @@ -537,6 +551,7 @@ export default { entity: { title: 'Editor', insertEntityInParagraphBreakLine: 'Insert Paragraph at line breaks', + spellcheck: 'Spell Check', }, }, entity: { @@ -1076,6 +1091,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!', }, @@ -1166,7 +1188,7 @@ export default { 4: 'Use creativity in the best way.', }, editor: { - website: 'Login', + website: 'Enter', about: 'About', }, support: 'Follow the project!', diff --git a/packages/languages/src/pt-BR.ts b/packages/languages/src/pt-BR.ts index 26200ce63..28936fb43 100644 --- a/packages/languages/src/pt-BR.ts +++ b/packages/languages/src/pt-BR.ts @@ -174,9 +174,13 @@ export default { header: 'Barra Superior', graph: 'Barra Lateral', text: 'Texto do Editor', + layout: 'Layout', + editorWidth: 'Largura do Editor', fontFamily: 'Fonte PadrΓ£o', fontWeight: 'Estilo da Fonte', fontSize: 'Tamanho da Fonte', + lineHeight: 'Altura da Linha', + letterSpacing: 'EspaΓ§amento entre Letras', }, }, styles: { @@ -234,6 +238,9 @@ export default { load: 'Carregar da Nuvem', save: 'Salvar na Nuvem', }, + export: { + bionicReading: 'Bionic Reading', + }, pdf: { preview: 'Simular (.PDF)', generate: 'Gerar (.PDF)', @@ -267,6 +274,7 @@ export default { import: 'Importar', export: 'Exportar', exportAs: 'Exportar como...', + encryptBW: 'Criptografar .bw', }, chapter: { drafts: 'Rascunhos', @@ -301,6 +309,12 @@ export default { substitutions: 'SubstituiΓ§Γ΅es', }, }, + tools: { + speechRecognition: { + defaultMic: 'Microfone padrΓ£o', + unknownMic: 'Microfone desconhecido', + }, + }, window: { confirmConfiguration: 'VocΓͺ quer sair salvando as configuraΓ§Γ΅es?', saveDropbox: 'VocΓͺ tem certeza em salvar este arquivo no Dropbox?', @@ -537,6 +551,7 @@ export default { title: 'Editor', insertEntityInParagraphBreakLine: 'Inserir ParΓ‘grafo em quebras de linha', + spellcheck: 'VerificaΓ§Γ£o OrtogrΓ‘fica', }, }, entity: { @@ -1084,6 +1099,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-characters/package.json b/packages/plugin-characters/package.json index a1d82db17..7c22d9a3f 100644 --- a/packages/plugin-characters/package.json +++ b/packages/plugin-characters/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-characters", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,10 +23,10 @@ "README.md" ], "dependencies": { - "better-write-color-converter": "^1.3.27", - "better-write-contenteditable-ast": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-color-converter": "^1.4.0", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-core/package.json b/packages/plugin-core/package.json index 16fa307f5..5215d6554 100644 --- a/packages/plugin-core/package.json +++ b/packages/plugin-core/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-core", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,7 +17,7 @@ "README.md" ], "dependencies": { - "better-write-types": "^1.3.27", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-core/src/on.ts b/packages/plugin-core/src/on.ts index f6eb11f3b..e0725b1a5 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) }) } @@ -492,10 +494,10 @@ export const externals = () => { emitter: PluginTypes.PluginEmitter, content: PluginTypes.PluginContentOn, ) => { - emitter.on('plugin-voice-start', () => { + emitter.on('plugin-voice-start', (...args: any[]) => { const created = content[0] - created && created() + created && created(...args) }) } diff --git a/packages/plugin-dropbox/.gitignore b/packages/plugin-dropbox/.gitignore deleted file mode 100644 index 25322f728..000000000 --- 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 a9c1b1419..000000000 --- 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 22b9f29d5..000000000 --- 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 3fd2c30fd..000000000 --- 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 ae807eca3..000000000 --- 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 096e43833..000000000 --- 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 bdc00469a..000000000 --- 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 b0833c89f..000000000 --- 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/package.json b/packages/plugin-editor-window/package.json index 9b42db55d..4bd5d2cc5 100644 --- a/packages/plugin-editor-window/package.json +++ b/packages/plugin-editor-window/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-editor-window", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,11 +17,11 @@ "README.md" ], "dependencies": { - "better-write-extension": "^1.3.27", - "better-write-image-converter": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-plugin-importer": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-extension": "^1.4.0", + "better-write-image-converter": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-plugin-importer": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-editor-window/src/drop.ts b/packages/plugin-editor-window/src/drop.ts index f9dd4c16a..6f679ea5a 100644 --- a/packages/plugin-editor-window/src/drop.ts +++ b/packages/plugin-editor-window/src/drop.ts @@ -22,9 +22,22 @@ 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-editor-window/src/seo.ts b/packages/plugin-editor-window/src/seo.ts index 35ce7399e..8737b0814 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`, diff --git a/packages/plugin-entity-history/package.json b/packages/plugin-entity-history/package.json index 7f0d86085..80b9e3a19 100644 --- a/packages/plugin-entity-history/package.json +++ b/packages/plugin-entity-history/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-entity-history", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,8 +23,8 @@ "README.md" ], "dependencies": { - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-exporter-docx/package.json b/packages/plugin-exporter-docx/package.json index 08090fbf6..7cdfb12df 100644 --- a/packages/plugin-exporter-docx/package.json +++ b/packages/plugin-exporter-docx/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-docx", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,9 +17,9 @@ "README.md" ], "dependencies": { - "better-write-contenteditable-ast": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-exporter-docx/src/set.ts b/packages/plugin-exporter-docx/src/set.ts index 0c7293182..84dcb509d 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,35 @@ 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 +93,7 @@ export const PluginDocxSet = ( return { bw } } - const create = () => { + const create = (bionicReading = false) => { const properties = (): docx.ISectionPropertiesOptions => { return { page: { @@ -252,7 +278,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 +368,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 +410,8 @@ export const PluginDocxSet = ( } On.externals().PluginDocxGenerate(emitter, [ - () => { - download(doc()) + (options: DOCXDocOptions) => { + download(doc(options ?? { bionicReading: false })) }, () => {}, ]) diff --git a/packages/plugin-exporter-epub/package.json b/packages/plugin-exporter-epub/package.json index 041788e6a..a9bfd88f5 100644 --- a/packages/plugin-exporter-epub/package.json +++ b/packages/plugin-exporter-epub/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-epub", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,9 +17,9 @@ "README.md" ], "dependencies": { - "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" + "better-write-contenteditable-ast": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", + "epub-gen3": "0.3.0" } } diff --git a/packages/plugin-exporter-epub/src/set.ts b/packages/plugin-exporter-epub/src/set.ts index 548e93f07..0b6aeea01 100644 --- a/packages/plugin-exporter-epub/src/set.ts +++ b/packages/plugin-exporter-epub/src/set.ts @@ -1,186 +1,94 @@ -import { saveAs } from 'file-saver' -import { ContextState, Entity, PluginTypes } from 'better-write-types' +import { + ContextState, + Entity, + EPUBDocOptions, + 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' +// @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, 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 isValidType = (val: Entity) => { + return ( + !hooks.entity.utils().isFixedRaw(val.raw) && + val.type !== 'image' && + val.type !== 'drau' + ) } - const contents = (): Chapter[] => { - const chapters: Chapter[] = [] + const contents = (options: EPUBDocOptions): 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 + if (isValidType(entity)) { + const purged = hooks.substitution.purge(entity.raw) + data.push(options.bionicReading ? bionicHtml(purged) : purged) } } - chapters.push(chapter) + raw.push(data) }) - return chapters + return raw } - const generate = () => { - EPUB( + const generate = async (options: EPUBDocOptions) => { + 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, + tocTitle: stores.PROJECT.nameRaw ?? undefined, + lang: 'en', + fonts: [], + version: 3, }, - contents(), - ).then( - (content) => download(content), - (_) => hooks.toast.error(hooks.i18n.t('toast.generics.error')), + contents(options), ) + + 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')) } On.externals().PluginEpubGenerate(emitter, [ - () => { - generate() + (options: EPUBDocOptions) => { + generate(options ?? { bionicReading: false }) }, () => {}, ]) diff --git a/packages/plugin-exporter-html/package.json b/packages/plugin-exporter-html/package.json index 0ecbc7010..44d02380d 100644 --- a/packages/plugin-exporter-html/package.json +++ b/packages/plugin-exporter-html/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-html", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,9 +17,9 @@ "README.md" ], "dependencies": { - "better-write-contenteditable-ast": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-exporter-pdf/package.json b/packages/plugin-exporter-pdf/package.json index 22e47e040..16c4b9a4a 100644 --- a/packages/plugin-exporter-pdf/package.json +++ b/packages/plugin-exporter-pdf/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-pdf", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,13 +17,13 @@ "README.md" ], "dependencies": { - "better-write-color-converter": "^1.3.27", - "better-write-contenteditable-ast": "^1.3.27", - "better-write-google-fonts-api": "^1.3.27", - "better-write-image-converter": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-plugin-theme": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-color-converter": "^1.4.0", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-google-fonts-api": "^1.4.0", + "better-write-image-converter": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-plugin-theme": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-exporter-pdf/src/generate.ts b/packages/plugin-exporter-pdf/src/generate.ts index bc0f30d1d..eda7c1456 100644 --- a/packages/plugin-exporter-pdf/src/generate.ts +++ b/packages/plugin-exporter-pdf/src/generate.ts @@ -358,12 +358,29 @@ 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 @@ -590,7 +607,7 @@ export const PluginPDFSet = ( text: stores.PROJECT.creator, fontSize: 11, font: utils().correctFontInject(stores.PDF.styles.paragraph.font), - margin: [base().pageMargins[0], 250, base().pageMargins[2], 0], + margin: [base().pageMargins[0], 180, base().pageMargins[2], 0], color: isTheme.value ? theme.paragraph : defColor, alignment: 'left', pageBreak: 'after', diff --git a/packages/plugin-exporter-txt/package.json b/packages/plugin-exporter-txt/package.json index 6dfe7b8b8..9c8b992f6 100644 --- a/packages/plugin-exporter-txt/package.json +++ b/packages/plugin-exporter-txt/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-exporter-txt", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,9 +17,9 @@ "README.md" ], "dependencies": { - "better-write-contenteditable-ast": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-contenteditable-ast": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-importer/package.json b/packages/plugin-importer/package.json index 8a1558749..62f4f2ef6 100644 --- a/packages/plugin-importer/package.json +++ b/packages/plugin-importer/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-importer", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,9 +23,9 @@ "README.md" ], "dependencies": { - "better-write-extension": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-extension": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-importer/src/bw.ts b/packages/plugin-importer/src/bw.ts index 870cfb6c7..a26f9472b 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/plugin-progress-bar/package.json b/packages/plugin-progress-bar/package.json index 12539187c..922bbaaec 100644 --- a/packages/plugin-progress-bar/package.json +++ b/packages/plugin-progress-bar/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-progress-bar", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,9 +23,9 @@ "README.md" ], "dependencies": { - "better-write-extension": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-extension": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-schemas/package.json b/packages/plugin-schemas/package.json index e216f0af1..a27411fbf 100644 --- a/packages/plugin-schemas/package.json +++ b/packages/plugin-schemas/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-schemas", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "main": "dist/index.js", @@ -17,8 +17,8 @@ "README.md" ], "dependencies": { - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-shortcuts/package.json b/packages/plugin-shortcuts/package.json index f4a609ebe..22cea2057 100644 --- a/packages/plugin-shortcuts/package.json +++ b/packages/plugin-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-shortcuts", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,9 +23,9 @@ "README.md" ], "dependencies": { - "better-write-extension": "^1.3.27", - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-extension": "^1.4.0", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-theme/package.json b/packages/plugin-theme/package.json index c7b26353b..9cd9929b6 100644 --- a/packages/plugin-theme/package.json +++ b/packages/plugin-theme/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-theme", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,8 +23,8 @@ "README.md" ], "dependencies": { - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "peerDependencies": { diff --git a/packages/plugin-voice-typing/package.json b/packages/plugin-voice-typing/package.json index 7001284c7..263c5ff64 100644 --- a/packages/plugin-voice-typing/package.json +++ b/packages/plugin-voice-typing/package.json @@ -1,6 +1,6 @@ { "name": "better-write-plugin-voice-typing", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ @@ -23,8 +23,8 @@ "README.md" ], "dependencies": { - "better-write-plugin-core": "^1.3.27", - "better-write-types": "^1.3.27", + "better-write-plugin-core": "^1.4.0", + "better-write-types": "^1.4.0", "vue-demi": "latest" }, "devDependencies": { diff --git a/packages/plugin-voice-typing/src/collector.ts b/packages/plugin-voice-typing/src/collector.ts index 9dd10e357..f70890c37 100644 --- a/packages/plugin-voice-typing/src/collector.ts +++ b/packages/plugin-voice-typing/src/collector.ts @@ -1,32 +1,46 @@ import { On } from 'better-write-plugin-core' import { PluginTypes } from 'better-write-types' -import { computed, ref, watch } from 'vue-demi' +import { computed, watch } from 'vue-demi' + +type SR = { + lang: string + continuous: boolean + interimResults: boolean + onresult: ((event: SREvent) => void) | null + onend: (() => void) | null + onerror: (() => void) | null + start(): void + stop(): void +} + +type SRResult = { isFinal: boolean; [index: number]: { transcript: string } } +type SREvent = { resultIndex: number; results: SRResult[] & { length: number } } + +// Minimum gap (ms) between final results to insert a period vs comma +const GAP_PERIOD = 2000 +const GAP_COMMA = 700 export const CollectorSet = ( emitter: PluginTypes.PluginEmitter, stores: PluginTypes.PluginStores, hooks: PluginTypes.PluginHooks, ) => { - const instance = ref(null) + let recognition: SR | null = null + let activeStream: MediaStream | null = null + + const stop = () => { + recognition?.stop() + recognition = null + activeStream?.getTracks().forEach((t) => t.stop()) + activeStream = null + hooks.project.utils().resetAllVisual() + } On.externals().PluginVoiceStart(emitter, [ - (lang: string) => { - const speech = hooks.vueuse.core.useSpeechRecognition({ - lang, - continuous: true, - }) + async (lang: string, deviceId?: string) => { + const { isSupported } = hooks.vueuse.core.useSpeechRecognition({ lang, continuous: true }) const { audioInputs: microphones } = hooks.vueuse.core.useDevicesList() - const id = computed(() => stores.EDITOR.actives.entity.index) - - const { isListening, isSupported, stop, result, start } = speech - - const onStop = () => { - stop() - - hooks.project.utils().resetAllVisual() - } - if (!isSupported.value) { hooks.toast(hooks.i18n.t('toast.generics.supported')) return @@ -37,47 +51,151 @@ export const CollectorSet = ( return } - hooks.vueuse.core.tryOnMounted(() => { - if (stores.CONTEXT.entities[id.value]) - stores.CONTEXT.entities[id.value].visual.warning = true - }) + // toggle off if already running + if (recognition) { + stop() + emitter.emit('plugin-voice-is-listening', false) + return + } - hooks.vueuse.core.tryOnUnmounted(() => { - hooks.project.utils().resetAllVisual() - }) + // force specific microphone via getUserMedia before starting SpeechRecognition + if (deviceId) { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { deviceId: { exact: deviceId } }, + }) + activeStream = stream + } catch { + // ignore β€” fall through to default mic + } + } - watch(isListening, () => { - emitter.emit('plugin-voice-is-listening', isListening.value) - }) + // melhoria 2: id reativo ao cursor + const id = computed(() => stores.EDITOR.actives.entity.index) - watch(result, (_result) => { - emitter.emit('plugin-voice-mutate', { id: id.value, result: _result }) - }) + const getEntityBase = (entityId: number): string => { + const raw = stores.CONTEXT.entities[entityId]?.raw ?? '' + const emptyLine = hooks.env.emptyLine() + return raw === emptyLine ? '' : raw.trim() + } - if (!isListening.value) { - result.value = '' - start() + let baseText = getEntityBase(id.value) + let accumulatedFinal = '' + let lastFinalTime = 0 - hooks.project.utils().resetAllVisual() + // melhoria 2: quando o foco muda de entidade, salva o acumulado na antiga + // e reinicia o contexto para a nova + const stopIdWatch = watch(id, (newId, oldId) => { + if (newId === oldId) return + + // confirma texto acumulado na entidade anterior + const committed = baseText + ? `${baseText} ${accumulatedFinal}`.trim() + : accumulatedFinal.trim() + + if (committed) { + emitter.emit('plugin-voice-mutate', { id: oldId, result: committed }) + stores.CONTEXT.entities[oldId].visual.warning = false + } + + // reinicia para a nova entidade + baseText = getEntityBase(newId) + accumulatedFinal = '' + lastFinalTime = 0 + stores.CONTEXT.entities[newId].visual.warning = true + }) + + // melhoria visual de qual entidade estΓ‘ sendo ditada + hooks.project.utils().resetAllVisual() + if (stores.CONTEXT.entities[id.value]) { stores.CONTEXT.entities[id.value].visual.warning = true + } - return + // usa Web Speech API diretamente para controle total sobre interim e isFinal + const SRCtor = (window as any).SpeechRecognition ?? (window as any).webkitSpeechRecognition + recognition = new SRCtor() as SR + recognition.lang = lang + recognition.continuous = true + recognition.interimResults = true // melhoria 5 + + recognition.onresult = (event: SREvent) => { + let interimTranscript = '' + + for (let i = event.resultIndex; i < event.results.length; i++) { + const transcript = event.results[i][0].transcript + + if (event.results[i].isFinal) { + // melhoria 3: pontuaΓ§Γ£o automΓ‘tica baseada na pausa + const now = Date.now() + const gap = lastFinalTime ? now - lastFinalTime : 0 + lastFinalTime = now + + let segment = transcript.trim() + + if (gap >= GAP_PERIOD && accumulatedFinal) { + // pausa longa β†’ ponto final + segment = '. ' + capitalize(segment) + } else if (gap >= GAP_COMMA && accumulatedFinal) { + // pausa curta β†’ vΓ­rgula + segment = ', ' + segment + } else if (accumulatedFinal) { + segment = ' ' + segment + } + + accumulatedFinal += segment + } else { + interimTranscript += transcript + } + } + + // melhoria 1: append ao texto prΓ©-existente + const fullFinal = baseText + ? `${baseText} ${accumulatedFinal}`.trim() + : accumulatedFinal.trim() + + if (accumulatedFinal) { + emitter.emit('plugin-voice-mutate', { id: id.value, result: fullFinal }) + } + + // melhoria 5: exibe interim no DOM sem salvar no store + if (interimTranscript) { + ;(emitter.emit as (...a: any[]) => void)('entity-speech-interim', { + id: id.value, + base: fullFinal, + interim: interimTranscript.trim(), + }) + } } - onStop() + recognition.onend = () => { + emitter.emit('plugin-voice-is-listening', false) + stopIdWatch() + hooks.project.utils().resetAllVisual() + } + + recognition.onerror = () => { + emitter.emit('plugin-voice-is-listening', false) + stopIdWatch() + hooks.project.utils().resetAllVisual() + recognition = null + } + + recognition.start() + emitter.emit('plugin-voice-is-listening', true) }, () => {}, ]) On.externals().PluginVoiceStop(emitter, [ () => { - instance.value?.stop() - - hooks.project.utils().resetAllVisual() - + stop() + emitter.emit('plugin-voice-is-listening', false) stores.ABSOLUTE.tools.speechRecognition = false }, () => {}, ]) } + +const capitalize = (s: string): string => + s.length === 0 ? s : s[0].toUpperCase() + s.slice(1) diff --git a/packages/types/package.json b/packages/types/package.json index c6033b309..98447d20a 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "better-write-types", - "version": "1.3.27", + "version": "1.4.0", "author": "Novout", "license": "MIT", "keywords": [ diff --git a/packages/types/src/absolute.ts b/packages/types/src/absolute.ts index 0c388188d..2667675c7 100644 --- a/packages/types/src/absolute.ts +++ b/packages/types/src/absolute.ts @@ -31,6 +31,10 @@ export interface AbsoluteStateDOCX { configuration: boolean } +export interface AbsoluteStateEPUB { + _placeholder?: never +} + export interface AbsoluteStateAuth { supabase: boolean } @@ -58,6 +62,10 @@ export interface AbsoluteStateSchemas { template: boolean } +export interface AbsoluteStateBW { + encryptOnExport: boolean +} + export interface AbsoluteState { cmd: boolean commands: boolean @@ -67,6 +75,7 @@ export interface AbsoluteState { modal: AbsoluteStateModal shortcuts: AbsoluteStateShortcuts pdf: AbsoluteStatePDF + epub: AbsoluteStateEPUB docx: AbsoluteStateDOCX auth: AbsoluteStateAuth entity: AbsoluteStateEntity @@ -75,4 +84,6 @@ export interface AbsoluteState { generator: AbsoluteStateGenerator spinner: boolean schemas: AbsoluteStateSchemas + bw: AbsoluteStateBW + bionicReading: boolean } diff --git a/packages/types/src/docx.ts b/packages/types/src/docx.ts index 10612905d..4e91ea244 100644 --- a/packages/types/src/docx.ts +++ b/packages/types/src/docx.ts @@ -59,3 +59,7 @@ export interface DOCXState { flow: DOCXStateFlow styles: DOCXStateStyles } + +export interface DOCXDocOptions { + bionicReading: boolean +} diff --git a/packages/types/src/editor.ts b/packages/types/src/editor.ts index 5bd6f23eb..02aff0477 100644 --- a/packages/types/src/editor.ts +++ b/packages/types/src/editor.ts @@ -38,6 +38,7 @@ export interface EditorStateActives { export interface EditorStateConfigurationEntity { insertEntityInParagraphBreakLine: boolean + spellcheck: boolean } export interface EditorStateConfigurationCommands { @@ -114,14 +115,19 @@ export interface EditorStateBase { backgroundGrayscale: boolean backgroundSaturate: boolean backgroundSepia: boolean + editorWidth: EditorStateEditorWidth } export interface EditorStateText { fontFamily: string fontWeight: number fontSize: number + lineHeight: number + letterSpacing: number } +export type EditorStateEditorWidth = 'narrow' | 'medium' | 'wide' | 'full' + export interface EditorStateHeading { fontFamily: string fontWeight: number diff --git a/packages/types/src/emitter.ts b/packages/types/src/emitter.ts index 30407d7a3..ab026c8b8 100644 --- a/packages/types/src/emitter.ts +++ b/packages/types/src/emitter.ts @@ -24,6 +24,7 @@ export type EventsEntityTextFocus = { export type Events = { 'entity-external-comment-save': any 'entity-speech-recognition': any + 'entity-speech-interim': { id: number; base: string; interim: string } 'entity-text-focus': EventsEntityTextFocus 'entity-text-force-save': void 'project-creative-drafts-set-info': any diff --git a/packages/types/src/epub.ts b/packages/types/src/epub.ts new file mode 100644 index 000000000..3c79a0e25 --- /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 94f2f864c..d8fb85d16 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 e8cb48335..ea43246dc 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 { diff --git a/packages/types/src/plugin/core.ts b/packages/types/src/plugin/core.ts index 8877137f5..514a9329b 100644 --- a/packages/types/src/plugin/core.ts +++ b/packages/types/src/plugin/core.ts @@ -57,6 +57,7 @@ export type PluginEmitterName = | 'plugin-voice-stop' | 'plugin-voice-mutate' | 'plugin-voice-is-listening' + | 'entity-speech-interim' | 'plugin-editor-header-project-open' | 'plugin-editor-header-create-open' | 'plugin-editor-header-help-open' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d69bd7970..56237e66e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,8 +17,8 @@ importers: specifier: 3.6.2 version: 3.6.2 vitest: - specifier: 4.0.6 - version: 4.0.6(happy-dom@20.0.10) + specifier: 4.1.8 + version: 4.1.8(happy-dom@20.0.10)(vite@7.1.12) packages/app: dependencies: @@ -107,76 +107,73 @@ importers: specifier: 2.8.8 version: 2.8.8 better-write-client-storage: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../client-storage better-write-color-converter: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../color-converter better-write-contenteditable-ast: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../contenteditable-ast better-write-extension: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../extension better-write-google-fonts-api: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../google-fonts-api better-write-image-converter: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../image-converter better-write-languages: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../languages better-write-plugin-characters: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-characters better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 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 + specifier: ^1.4.0 version: link:../plugin-editor-window better-write-plugin-entity-history: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-entity-history better-write-plugin-exporter-docx: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-exporter-docx better-write-plugin-exporter-epub: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-exporter-epub better-write-plugin-exporter-html: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-exporter-html better-write-plugin-exporter-pdf: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-exporter-pdf better-write-plugin-exporter-txt: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-exporter-txt better-write-plugin-importer: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-importer better-write-plugin-progress-bar: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-progress-bar better-write-plugin-schemas: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-schemas better-write-plugin-shortcuts: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-shortcuts better-write-plugin-theme: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-theme better-write-plugin-voice-typing: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-voice-typing better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types compressorjs: specifier: 1.2.1 @@ -193,6 +190,9 @@ importers: dropbox: specifier: 10.34.0 version: 10.34.0(@types/node-fetch@2.6.13) + epub-gen3: + specifier: 0.4.0 + version: 0.4.0 file-saver: specifier: 2.0.5 version: 2.0.5 @@ -345,46 +345,46 @@ importers: packages/client-storage: dependencies: better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types packages/color-converter: dependencies: better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types packages/contenteditable-ast: dependencies: better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types packages/extension: dependencies: better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types packages/google-fonts-api: dependencies: better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types packages/image-converter: dependencies: better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types packages/languages: dependencies: better-write-plugin-exporter-pdf: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-exporter-pdf better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types packages/plugin-characters: @@ -393,16 +393,16 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-color-converter: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../color-converter better-write-contenteditable-ast: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../contenteditable-ast better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -424,7 +424,7 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -433,50 +433,25 @@ 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': specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-extension: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../extension better-write-image-converter: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../image-converter better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-plugin-importer: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-importer better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -491,10 +466,10 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -516,13 +491,13 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-contenteditable-ast: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../contenteditable-ast better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -534,17 +509,17 @@ importers: packages/plugin-exporter-epub: dependencies: better-write-contenteditable-ast: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../contenteditable-ast better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types - epub-gen-memory: - specifier: 1.0.10 - version: 1.0.10 + epub-gen3: + specifier: 0.3.0 + version: 0.3.0 packages/plugin-exporter-html: dependencies: @@ -552,13 +527,13 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-contenteditable-ast: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../contenteditable-ast better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -573,25 +548,25 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-color-converter: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../color-converter better-write-contenteditable-ast: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../contenteditable-ast better-write-google-fonts-api: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../google-fonts-api better-write-image-converter: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../image-converter better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-plugin-theme: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-theme better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -606,13 +581,13 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-contenteditable-ast: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../contenteditable-ast better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -627,13 +602,13 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-extension: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../extension better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -655,13 +630,13 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-extension: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../extension better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -683,10 +658,10 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -701,13 +676,13 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-extension: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../extension better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -729,10 +704,10 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -747,10 +722,10 @@ importers: specifier: ^1.0.0-rc.1 version: 1.0.0-rc.1(vue@3.5.14) better-write-plugin-core: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../plugin-core better-write-types: - specifier: ^1.3.27 + specifier: ^1.4.0 version: link:../types vue: specifier: ^2.0.0 || >=3.0.0 @@ -3622,8 +3597,8 @@ packages: escape-string-regexp: 5.0.0 dev: false - /@standard-schema/spec@1.0.0: - resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + /@standard-schema/spec@1.1.0: + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} dev: true /@surma/rollup-plugin-off-main-thread@2.2.3: @@ -3968,64 +3943,66 @@ packages: vue: 3.5.14(typescript@5.9.3) dev: true - /@vitest/expect@4.0.6: - resolution: {integrity: sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg==} + /@vitest/expect@4.1.8: + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} dependencies: - '@standard-schema/spec': 1.0.0 + '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.6 - '@vitest/utils': 4.0.6 - chai: 6.2.0 - tinyrainbow: 3.0.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 dev: true - /@vitest/mocker@4.0.6(vite@7.1.12): - resolution: {integrity: sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA==} + /@vitest/mocker@4.1.8(vite@7.1.12): + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true dependencies: - '@vitest/spy': 4.0.6 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 vite: 7.1.12 dev: true - /@vitest/pretty-format@4.0.6: - resolution: {integrity: sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g==} + /@vitest/pretty-format@4.1.8: + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 dev: true - /@vitest/runner@4.0.6: - resolution: {integrity: sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og==} + /@vitest/runner@4.1.8: + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} dependencies: - '@vitest/utils': 4.0.6 + '@vitest/utils': 4.1.8 pathe: 2.0.3 dev: true - /@vitest/snapshot@4.0.6: - resolution: {integrity: sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g==} + /@vitest/snapshot@4.1.8: + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} dependencies: - '@vitest/pretty-format': 4.0.6 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 dev: true - /@vitest/spy@4.0.6: - resolution: {integrity: sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ==} + /@vitest/spy@4.1.8: + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} dev: true - /@vitest/utils@4.0.6: - resolution: {integrity: sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A==} + /@vitest/utils@4.1.8: + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} dependencies: - '@vitest/pretty-format': 4.0.6 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 dev: true /@volar/language-core@1.10.10: @@ -4089,7 +4066,7 @@ packages: '@vue/shared': 3.5.14 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.6 + postcss: 8.5.15 source-map-js: 1.2.1 /@vue/compiler-ssr@3.5.14: @@ -4471,13 +4448,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: @@ -4694,6 +4664,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==} @@ -4762,6 +4733,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==} @@ -4820,6 +4792,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==} @@ -4832,6 +4805,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==} @@ -4967,8 +4941,8 @@ packages: jsonc-parser: 3.3.1 package-manager-detector: 1.5.0 semver: 7.7.3 - tinyexec: 1.0.1 - tinyglobby: 0.2.15 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 yaml: 2.8.1 transitivePeerDependencies: - magicast @@ -5088,6 +5062,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==} @@ -5107,8 +5082,8 @@ packages: resolution: {integrity: sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==} dev: true - /chai@6.2.0: - resolution: {integrity: sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==} + /chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} dev: true @@ -5569,16 +5544,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} @@ -5587,11 +5552,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'} @@ -5766,10 +5726,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'} @@ -5801,38 +5757,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'} @@ -5845,6 +5774,7 @@ packages: engines: {node: '>=10'} dependencies: is-obj: 2.0.0 + dev: true /dotenv@10.0.0: resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} @@ -5896,6 +5826,7 @@ packages: hasBin: true dependencies: jake: 10.9.4 + dev: true /electron-to-chromium@1.5.244: resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} @@ -5954,15 +5885,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'} @@ -5982,25 +5904,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.3.0: + resolution: {integrity: sha512-Vi14IzFEUF2OdwPybbJdiOXUzXJPv76aADvOrQ4FqfmSYanpqw08bw3WM0o4q55SPagGszo2y9GwOsRG+8xVQg==} + dev: false + + /epub-gen3@0.4.0: + resolution: {integrity: sha512-tslbcUxHahPGLpm3ghhmAeeQtraoZU2Uhm3FifXVL2TqcM9RqFnfKlE2Tg2cnIu+v3wCSJfKbDnDbQSUxA7oSw==} dev: false /err-code@2.0.3: @@ -6081,8 +5990,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - /es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + /es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} dev: true /es-object-atoms@1.1.1: @@ -6314,11 +6223,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 @@ -6365,8 +6269,8 @@ packages: strip-final-newline: 2.0.0 dev: true - /expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + /expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} dev: true @@ -6434,7 +6338,7 @@ packages: reusify: 1.1.0 dev: true - /fdir@6.5.0(picomatch@4.0.3): + /fdir@6.5.0(picomatch@4.0.4): resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} peerDependencies: @@ -6443,7 +6347,7 @@ packages: picomatch: optional: true dependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 dev: true /figures@3.2.0: @@ -6473,6 +6377,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==} @@ -7194,15 +7099,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 @@ -7606,6 +7502,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==} @@ -7787,6 +7684,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==} @@ -8186,11 +8084,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 @@ -8666,12 +8559,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'} @@ -8707,6 +8594,7 @@ packages: engines: {node: '>=10'} dependencies: brace-expansion: 2.0.2 + dev: true /minimatch@6.2.0: resolution: {integrity: sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==} @@ -8900,6 +8788,12 @@ packages: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + dev: false + + /nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true /nanoid@4.0.2: resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} @@ -9268,6 +9162,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==} @@ -9340,7 +9235,7 @@ packages: consola: 3.4.2 pathe: 2.0.3 pkg-types: 2.3.0 - tinyexec: 1.0.1 + tinyexec: 1.2.4 dev: true /object-assign@4.1.1: @@ -9378,6 +9273,11 @@ packages: object-keys: 1.1.1 dev: true + /obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + dev: true + /ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} dev: true @@ -9444,17 +9344,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'} @@ -9778,6 +9667,11 @@ packages: engines: {node: '>=12'} dev: true + /picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + dev: true + /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -9894,11 +9788,11 @@ packages: util-deprecate: 1.0.2 dev: true - /postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + /postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -10810,11 +10704,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'} @@ -10948,8 +10837,8 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + /std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} dev: true /stop-iteration-iterator@1.1.0: @@ -11293,16 +11182,25 @@ packages: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} dev: true - /tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + /tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} dev: true /tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + dev: true + + /tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 dev: true /tinygradient@1.1.5: @@ -11312,8 +11210,8 @@ packages: tinycolor2: 1.6.0 dev: true - /tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + /tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} dev: true @@ -11705,11 +11603,11 @@ packages: magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 pkg-types: 2.3.0 scule: 1.3.0 strip-literal: 3.1.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 unplugin: 2.3.10 unplugin-utils: 0.3.1 dev: true @@ -11816,7 +11714,7 @@ packages: engines: {node: '>=20.19.0'} dependencies: pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 dev: true /unplugin-vue-components@30.0.0(vue@3.5.14): @@ -11851,7 +11749,7 @@ packages: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 dev: true @@ -11923,11 +11821,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: @@ -12141,33 +12034,36 @@ packages: optional: true dependencies: esbuild: 0.25.11 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 rollup: 4.52.5 - tinyglobby: 0.2.15 + 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==} + /vitest@4.1.8(happy-dom@20.0.10)(vite@7.1.12): + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 + '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.6 - '@vitest/browser-preview': 4.0.6 - '@vitest/browser-webdriverio': 4.0.6 - '@vitest/ui': 4.0.6 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@types/debug': + '@opentelemetry/api': optional: true '@types/node': optional: true @@ -12177,6 +12073,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -12184,40 +12084,29 @@ packages: jsdom: optional: true dependencies: - '@vitest/expect': 4.0.6 - '@vitest/mocker': 4.0.6(vite@7.1.12) - '@vitest/pretty-format': 4.0.6 - '@vitest/runner': 4.0.6 - '@vitest/snapshot': 4.0.6 - '@vitest/spy': 4.0.6 - '@vitest/utils': 4.0.6 - debug: 4.4.3 - es-module-lexer: 1.7.0 - expect-type: 1.2.2 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@7.1.12) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 happy-dom: 20.0.10 magic-string: 0.30.21 + obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 vite: 7.1.12 why-is-node-running: 2.3.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml dev: true /vm-browserify@1.1.2: diff --git a/vitest.config.ts b/vitest.config.ts index 8faa4fe2c..00ab5fdca 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,28 @@ import { defineConfig } from 'vitest/config' +import { fileURLToPath } from 'url' + +const pkg = (path: string) => + fileURLToPath(new URL(`./packages/${path}`, import.meta.url)) export default defineConfig({ test: { - environment: 'happy-dom' + environment: 'happy-dom', + globals: true, + include: ['**/src/**/*.test.ts'], + setupFiles: ['packages/app/src/use/__tests__/setup.ts'], + }, + resolve: { + alias: { + '@': pkg('app/src'), + 'better-write-types': pkg('types/src/index.ts'), + 'better-write-languages': pkg('languages/src/index.ts'), + 'better-write-plugin-core': pkg('plugin-core/src/index.ts'), + 'better-write-plugin-theme': pkg('plugin-theme/src/index.ts'), + 'better-write-client-storage': pkg('client-storage/src/index.ts'), + 'better-write-contenteditable-ast': pkg('contenteditable-ast/src/index.ts'), + 'better-write-extension': pkg('extension/src/index.ts'), + 'better-write-image-converter': pkg('image-converter/src/index.ts'), + 'better-write-plugin-importer': pkg('plugin-importer/src/index.ts'), + }, }, -}) \ No newline at end of file +})