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"],
]
)
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)
}
}
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)
}
}
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>
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>
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>
Results
To run this we need to start two terminals and do:
$ npm run dev
$ npx electron .
And the results:
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)