How I Built a 100% Client-Side Word Processor
Tags: webdev, programming, vue, typescript
Published: 2023-05-17 19:19:47

After launching version 1.1 of [betterwrite.io](https://github.com/Novout/betterwrite) (and probably the last major one), deciding to write on a trajectory that lasts for almost 2 (two) years and which may last longer.

Warning from before that this project is not an example of *software* architecture, given its many technology changes, *features* that have been completely removed, forgotten or built without use; *typos*, dubious decisions and missed conversations.

Treat the word processor as a mere project for personal use, built by just one person and gone further than it should have. So, you won't be annoyed by the following review.

![Better Write Cover](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/20dr1we5czjma5i8ry5n.png)


### A Word Processor Focused on Creative Writing

The idea came up during the peak of the *COVID-19* pandemic, where I divided my time between *freelances* without much desire and procrastination in the sheer boredom of social isolation.

Without further details, the idle time made me want to continue or start something personal that might or might not be useful for my work, but one factor was decisive for choosing a project that involved both programming and writing: the slowness of *Google Documents*.

#### The Cursed Canvas

As much as there are other considerably good options on the market, such as *Microsoft Word*, *Overleaf*, thousands of *WYSIWYG* editors that transform markdown into PDF and even *notepad++*, at the end of the day, being hostage to practicality and tools from *Google Docs* was a daily action.

But a problem was recurrent: When surpassing about 100 (one hundred) pages with large paragraphs and color and spacing customizations, the editor was considerably slow and required me to separate the project into other files and join them only at the end.

Consisting of a single canvas, all manipulation, and transformation depends on it, which means that even what is not being displayed is being considered valid in the context. Treating as a unit, not as a block, has its advantages, such as free customization and extensibility, but inserting a line break, page break or changing text positions shouldn't be costly.

These difficulties, added to the fact that practically all extensions (which considerably improve the editor as a whole) broke every *release* (like the beloved Dark Theme) made me decide: build my own or continue an existing project that was precisely what I wanted.

> Obviously *Google Docs* is much more robust than *Betterwrite*, in addition to being generalist. Comparing them directly doesn't make much sense, as they have completely different focuses (and investments).


#### Fontaine Editor — A modern, multi-platform manuscript editor.

An initiative of this scale is a difficult idea to take seriously, especially when whoever is building the tool has already had several projects abandoned for several different reasons, but when finding [Fontaine](https://github.com/EndaHallahan/Fontaine-Editor) I ended up changing my opinion a little.

*Fontaine* consisted of a manuscript editor and provided control tools and an easy-to-use interface. However, the project was stopped, and its status did not allow me to use it due to the lack of document export.

Having it discontinued unfinished was a clear negative sign, but its simplicity in displaying relevant information, even though it was not of a high level of complexity, gave me enough courage to start a project with [*Vue 2*](https://vuejs.org/), [Vite](https://vitejs.dev/), [Typescript](https://www.typescriptlang.org/), [PDFMake](http://pdfmake.org/) and [TailwindCSS](https://tailwindcss.com/ ).


### MVP

The above version, *v0.1.2*, was the first versioned (recovered for this, thanks *git tag*) and developed relatively quickly (rushing), consisting of a block editor where each line was a ` <textarea />` that barely worked properly, a cloud save with a `.json` via the *SDK* of *Dropbox* (one of *features* removed), shortcuts for each sidebar item (also removed), a list of chapters and the dreamed PDF generator.

#### Event System

Firstly, the entire logic of the application consists of issuing events anonymously and carrying out each job individually. Using [mitt](https://github.com/developit/mitt) for this, all events of a given set (currently called plugin) are registered by a key and called even though they are not inside a `setup()`.

Registered plugins have access to stores (*pinia stores*) and *vue hooks*, making it possible to decouple each functionality and facilitate maintenance.

The following is the flow of the PDF:

```ts
export const PDFPlugin = (): PluginTypes.Plugin =>
  createPlugin({ name: 'pdf' }, [PluginPDFBase, PluginPDFSet])

// ...
const PluginEntityInputInitial = (
  emitter: PluginTypes.PluginEmitter,
  content: PluginTypes.PluginContentOn
) => {
  emitter.on(
    'plugin-input-watch-initial',
    (item: PluginTypes.PluginLoggerDefault) => {
      if (!item.data) return

      const created = content[0]

      created && created(item)
    }
  )

  // ...
}

// in pdf monorepo
On.externals().PluginPDFGenerate(emitter, [
  (options: PDFDocOptions) => {
    // ...

    hooks.storage.normalize().then(() => {
      create(options)
    })
  },
  () => {},
])
```

```html
// App.vue
<template>
  <router-view></router-view>
  <PWAPrompt />
</template>

<script setup lang="ts">
  import { useStart } from '@/use/start'
  import { ThemePlugin } from 'better-write-plugin-theme'
  import { ImporterPlugin } from 'better-write-plugin-importer'
  import { PDFPlugin } from 'better-write-plugin-exporter-pdf'
  import { DocxPlugin } from 'better-write-plugin-exporter-docx'
  import { TxtPlugin } from 'better-write-plugin-exporter-txt'
  import { HtmlPlugin } from 'better-write-plugin-exporter-html'
  import { AnnotationsPlugin } from 'better-write-plugin-annotations'
  import { EpubPlugin } from 'better-write-plugin-exporter-epub'

  useStart([
    ThemePlugin(),
    ImporterPlugin(),
    PDFPlugin(),
    DocxPlugin(),
    EpubPlugin(),
    TxtPlugin(),
    HtmlPlugin(),
    AnnotationsPlugin(),
  ]).init()
</script>
```

> As you can see, this system enabled the migration to mono-repositories, using [turborepo](https://turbo.build) and [lerna](https://lerna.js.org/) to adjust the flow and enable the decentralization of internal packages.

#### The PDF Generator

Speaking of the generator, it implied *10 (ten) mb* of *.tff* in the *bundle* and had an unbearable *bug* when attaching external sources (and which continued for a long time), which consisted of not allowing the use for being a *readonly* method and that made its customization usable only in a development environment. However, it was (and still is, as it has not been completely rewritten) useful to me.

> At the time I didn't know about [*skipTsLibCheck*](https://www.typescriptlang.org/tsconfig#skipLibCheck).

> [pdfeasy](https://github.com/betterwrite/pdfeasy) was built to replace [pdfmake](http://pdfmake.org/#/), but since *pdfmake* received updates that tweaked the most of the problems ended up being maintained.

> The other generators implemented follow a flow very similar to that of the PDF.

#### Model-Entity

As much as it has undergone severe changes in the new versions, the concept is the same: a list of entities, and each entity consists of blocks of different types such as paragraphs, list, image, line/page break, among others.

```html
<template>
  //...
  <EditorEntityDefault
    v-for="(element, index) in CONTEXT.entities"
    :id="`entity-${index}`"
    :key="index"
    :entity="element"
  />
  // ...
</template>
```

Its big difference to *block-style* editors is that each event is emitted individually, and each event entry is triggered only when needed.

For example, text blocks do not use *Vue*'s *v-model*, [explained in this file](https://github.com/Novout/betterwrite/blob/main/docs/ENTITY_MODEL.md), where the value of each `el.innerHTML` (new blocks use *contenteditable* in place of `<input />`) is computed only when a newline is added or another chapter is loaded.

```typescript
await storage.normalize()

// ...
const setData = (val: string) => {
  input.value.innerHTML = val
}

const save = (target: number, raw: string) => {
  if (raw === null) return

  CONTEXT.entities[target].raw = raw
}
```

It may seem like a silly approach, but this allows massive bodies of entities being rendered in the same chapter to have fluid typing in text entries, which is a common problem in *WYSIWYG* editors.

> The display sidebar is only changed when a block needs to be updated, which is a limitation of this approach, but something to overlook.

> The list uses the key-index of the list itself for each entity to infer which block is at the front or back in O(1), such as adding an extra empty line after a page/line break or for deleting multiple items.

> The find/replace text uses a different list view as it has a different view proposition:

```html
<template>
  <EditorBaseRenderFinder v-if="ABSOLUTE.shortcuts.finder" />
  <EditorBaseRenderSwitcher v-else-if="ABSOLUTE.shortcuts.switcher" />
  <EditorBaseRenderDefault v-else />
</template>

<script setup lang="ts">
  import { useAbsoluteStore } from '@/store/absolute'

  const ABSOLUTE = useAbsoluteStore()
</script>
```

> Other features follow a similar philosophy, such as the annotations markdown editor that uses *debounce time* to save changes and automatic saving that only occurs in case of context destruction:

```ts
// annotations monorepo
const fn = hooks.vueuse.core.useDebounceFn(doc => {
  setFile(file.id, doc.toJSON())
}, 500)
```

```ts
// editor.ts
const inForcedSave = async () => {
  if (EDITOR.configuration.autosave) {
    await storage.normalize()
    await local.onSaveProject(false)
  }

  if (EDITOR.configuration.cloudAutosave && online.value) {
    await cloud.saveProject()
  }
}

useEventListener('beforeunload', async () => {
  await inForcedSave()
})

onBeforeRouteLeave(async () => {
  await inForcedSave()
})
```

#### 100% Client-Side

*Betterwrite* has a simple concept as an application: only static files, that is, without the need for [SSR](https://solutionshub.epam.com/blog/post/what-is-server-side-rendering). This is only possible due to the *polyfills* that allow the import and generation of documents using the API of the browsers themselves. By *Vite*, [node-stdlib-browser](https://github.com/niksy/node-stdlib-browser) was used to accomplish this task:

```ts
import inject from '@rollup/plugin-inject'
import { Plugin } from 'vite'

export const viteStdlib = (): Plugin => {
  return {...inject({
    global: [
      require.resolve(
        'node-stdlib-browser/helpers/esbuild/shim'
      ),
      'global'
    ],
    process: [
      require.resolve(
        'node-stdlib-browser/helpers/esbuild/shim'
      ),
      'process'
    ],
    Buffer: [
      require.resolve(
        'node-stdlib-browser/helpers/esbuild/shim'
      ),
      'Buffer'
    ]
  }),
  enforce: 'post'
}}
```

> As the word processor uses most *polyfills*, it turns out to be worth injecting them all at once.

Another important point is the migration of the SDK from *Dropbox* to [Supabase](https://supabase.com/), which made it possible to authenticate users and save the project in the cloud [safely on the client-side](https://subase.com/security).

> Even the word processor, being static, is hosted for free. Thank you [Vercel](https://vercel.com/)!

> If you want more information about *100% Client-Side*, [see the application's technical documentation](https://github.com/Novout/betterwrite/tree/main/docs).

### The Progress

When I decided to go ahead with this project, I made some drastic changes that (feel) paid off.

To validate the editor and his idea, some book would have to serve as a guinea pig, and that is why [Restos da Geada - Entre Plano](https://www.amazon.com/-/es/Giovane-Silva-Cardoso/dp/6500469992) was chosen. *Self-publish*, being my first book released in my name that was completed, consequently gave me the freedom to do whatever I wanted with the editor (like losing paragraphs due to editor error without fearing deadlines). It was started in *Google Docs*, but was migrated to *Betterwrite*, having its project initialized in *v0.0.1*.

A war survivor, he was responsible for compatibility between versions, the need for authentication, all the *features* focused on creativity and the ability to place images on break lines.

> A curious fact is that support for importing in other extensions only existed in *v0.10*, so a *copy-paste* of *raw HTML* between blocks exclusively for this task was implemented, *feature* also removed .

> Another curious fact is that your project was lost in a test in production, but it was recovered in an old locally saved project (`.bw`) and its values ​​were copied from `.json` by hand.

#### Electron

Before adopting [PWA](https://en.wikipedia.org/wiki/Progressive_web_app), [Electron](https://www.electronjs.org/pt/) was implemented almost completely (with support to other operating systems and automatic updates), but due to some limitations of *Vue* (and the *Vite* plugin) it was decided to "throw out" the entire implementation and make room for a hybrid application.

> The PWA went hand in hand by allowing caching in *service-worker* certain requests (gstatic) and required files (a `.ttf` font for *pdfmake* and a *typeface* font used by *threejs * on the landing).

#### WindiCSS

The project was migrated from [TailwindCSS](https://tailwindcss.com/) to [WindiCSS](https://windicss.org/) due to the delayed release of *TailwindCSS 3.0*, with [HeadlessUI]( https://headlessui.com/) being a big part of *material design*. The *hot-reload* saved me hours in development mode.

> A migration to [a proprietary Atomic CSS lib](https://ununura.com/#/) almost occurred, but the process was paused.

#### Bundle Size

The size of the final application was never of great concern, but when migrating to *PWA*, the loading time was drastically off the curve (negatively).

It is currently acceptable for your proposal, but it can be improved.

> The actual cache can reach up to 20 (twenty) mb depending on usage.

#### 1.1

Its *major* version has been released, but *1.1* is the true stable version, fixing chronic application problems and bringing with it the editor's true initial objective: **the focus on creativity**.