DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 79: Svelte Unicodizer

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.

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.

  new TextTransform(
    "White Circles",
    [
      ["", "a", "z"],
      ["", "A", "Z"],
      ["", "0", "0"],
      ["", "1", "9"],
    ]
  )
Enter fullscreen mode Exit fullscreen mode

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.

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

src/TransformedText.svelte

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

<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>
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

Results

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

$ npm run dev
$ npx electron .
Enter fullscreen mode Exit fullscreen mode

And the results:

Episode 79 Screenshot

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.

Top comments (0)