Electron Adventures: Episode 79: Svelte Unicodizer
Tags: javascript, electron, svelte, unicode
Published: 2021-10-12 14:38:38

In previous episode we created a packaged for an app consisting of just static files. Let's try one that needs to be dynamically generated.

This app is a fun one - you write some text in normal letters, and it returns you various Unicode funny character versions like 🅗🅔🅛🅛🅞 or 𝔀𝓸𝓻𝓭𝓵𝓭 or ʇdıɹɔsɐʌɐɾ.

This is a Svelte port of Imba 1 app I once wrote. [You can check the original here](https://taw.github.io/imba-unicodizer/).

### JavaScript and Unicode

Every now and then I complain about JavaScript, and here's another such case. JavaScript "strings" do not support Unicode. `"💩".length` is 2. As every reasonable language like Ruby 2+ or Python 3+ knows, that's a single character.

The real issue is that Ruby 1 and Python 2 used to make similar mistakes, they can be fixed - JavaScript is basically unfixable, and forced to live with its early bad design choices forever.

As this app requires a lot of Unicode manipulation, we'll need to use `punycode` package to convert strings into arrays of Unicode code point numbers and back, in particular `punycode.ucs2`. Nasty code.

### Transforming ranges

The core to how our transformation works is that Unicode characters in various groups are generally in same order, so we don't need to list every character individually - we can list the source range, and first character of target range.

So in this case "a" maps to "ⓐ", next character "b" maps to whatever follows "ⓐ" (as you might expect, that would be "ⓑ"), and so on until "z" maps to "ⓩ". In this case unfortunately "⓪" does not follow the pattern so we need to list it separately.

```javascript
  new TextTransform(
    "White Circles",
    [
      ["ⓐ", "a", "z"],
      ["Ⓐ", "A", "Z"],
      ["⓪", "0", "0"],
      ["①", "1", "9"],
    ]
  )
```

`src/TextTransforms.js` is over 1000 lines of various such transforms.

### `src/TextTransform.js`

Each transform takes two arguments, name and transform map. That map is expanded to character to character mapping.

Some notable things - we need to require `punycode/` with extra slash due to conflict between `punycode` package and builtin node module.

`usc2.decode` and `usc2.encode` are used to convert between JavaScript strings and arrays of Unicode code points. If JavaScript supported Unicode, we would need no such thing, but that will likely never happen.

There's also helpful `debug` getter that returns all transformed text.

```javascript
import {ucs2} from "punycode/"

export default class TextTransform {
  constructor(name, map_data) {
    this.name = name
    this.cmap = this.compile_map(map_data)
  }

  compile_map(map_data) {
    let result = {}
    for (let group of map_data) {
      let target_start = ucs2.decode(group[0])[0]
      let source_start = ucs2.decode(group[1])[0]
      let source_end = ucs2.decode(group[2] || group[1])[0]
      for (let i=source_start; i<=source_end; i++) {
        let j=target_start - source_start + i
        result[i] = j
      }
    }
    return result
  }

  apply(text) {
    let result = []
    let utext = ucs2.decode(text)
    for (let c of utext) {
      if (this.cmap[c]) {
        result.push(this.cmap[c])
      } else {
        result.push(c)
      }
    }
    return ucs2.encode(result)
  }

  get debug() {
    let keys = Object.keys(this.cmap)
    keys.sort((a, b) => (a - b))
    let values = keys.map((i) => this.cmap[i])
    return ucs2.encode(values)
  }
}
```

### `src/BackwardsTextTransform.js`

For a few transforms we need to not just map the characters, but also flip the order. Some classic inheritance can do that. It's been a while since I last needed to use class inheritance in JavaScript, it's such an unpopular feature these days.

```javascript
import {ucs2} from "punycode/"
import TextTransform from "./TextTransform.js"

export default class BackwardsTextTransform extends TextTransform {
  apply(text) {
    let result = []
    let utext = ucs2.decode(text)
    for (let c of utext) {
      if (this.cmap[c]) {
        result.push(this.cmap[c])
      } else {
        result.push(c)
      }
    }
    result.reverse()
    return ucs2.encode(result)
  }

  get debug() {
    let keys = Object.keys(this.cmap)
    keys.sort((a, b) => (a - b))
    let values = keys.map(i => this.cmap[i])
    values.reverse()
    return ucs2.encode(values)
  }
}
```

### `src/App.svelte`

The app has two inputs - a checkbox for displaying debug values, and text you want to transform. Then it loops through all the transforms and displays the results.

```html
<script>
  import TransformedText from "./TransformedText.svelte"
  import TransformDebugger from "./TransformDebugger.svelte"
  import TextTransforms from "./TextTransforms.js"

  let text = "Happy New Year 2022!"
  let debug = false
</script>

<div class="app">
  <header>Unicodizer!</header>
  <p>Text goes in. Fancy Unicode goes out. Enjoy.</p>

  <input bind:value={text} type="text">
  <p>
    <label>
      Debug mode
      <input bind:checked={debug} type="checkbox">
    </label>
  </p>

  {#if debug}
    <h2>Debug</h2>
    {#each TextTransforms as map}
      <TransformDebugger {map} />
    {/each}
  {/if}

  <h2>Fancy</h2>
  {#each TextTransforms as map}
    <TransformedText {map} {text} />
  {/each}
</div>

<style>
  :global(body) {
    background-color: #444;
    color: #fff;
  }

  .app {
    max-width: 80em;
    margin: auto;
    font-family: 'Noto Serif', serif;
  }

  input[type="text"] {
    width: 100%;
  }

  input[type="checkbox"] {
    margin-left: 1em;
  }

  header {
    font-size: 64px;
    text-align: center;
  }
</style>
```

### `src/TransformedText.svelte`

Very simple component to display transform name, and the output:

```html
<script>
  export let map, text

  $: transformed = map.apply(text)
</script>

<div>
  {#if text !== transformed}
    <b>{map.name}</b>
    <div>{transformed}</div>
  {/if}
</div>

<style>
  div {
    margin-bottom: 1em;
  }
  b ~ div {
    margin-left: 1em;
  }
</style>
```

### `src/TransformDebugger.svelte`

And another one for displaying simple debug information. There's a bit of style duplication, but not enough to bother extracting it out.

```html
<script>
  export let map
</script>

<div>
  <b>{map.name}</b>
  <div>{map.debug}</div>
</div>

<style>
  div {
    margin-bottom: 1em;
  }
  b ~ div {
    margin-left: 1em;
  }
</style>
```

### Results

To run this we need to start two terminals and do:

```
$ npm run dev
$ npx electron .
```

And the results:

![Episode 79 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f244qr5fvxhfj19hfu9e.png)

This is of course not what we want to tell the users - we'd like the users to be able to run it with a single click. In the next episode we'll try to package it.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-79-svelte-unicodizer).