Skip to content

Commit d6a7ab1

Browse files
author
Andrew Marcuse
committed
Upside-down text
1 parent 1fe18d1 commit d6a7ab1

File tree

6 files changed

+260
-1
lines changed

6 files changed

+260
-1
lines changed

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>Vite + TS</title>
7+
<title>Online File Format Tools</title>
88
</head>
99
<body>
1010
<div id="app"></div>

src/asciify.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
2424
</div>
2525
<textarea id="output-text" class="textarea textarea-bordered min-h-40 w-full" readonly></textarea>
2626
</label>
27+
28+
<div>
29+
<button id="copy-button" type="button" class="btn btn-sm">Copy to clipboard</button>
30+
</div>
2731
</form>
2832
</section>
2933
</section>
@@ -32,6 +36,7 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
3236

3337
const inputText = document.querySelector<HTMLTextAreaElement>('#input-text')
3438
const outputText = document.querySelector<HTMLTextAreaElement>('#output-text')
39+
const copyButton = document.querySelector<HTMLButtonElement>('#copy-button')
3540

3641
type AnyAsciiFn = (value: string) => string
3742
let anyAsciiPromise: Promise<AnyAsciiFn> | undefined
@@ -57,3 +62,20 @@ inputText?.addEventListener('input', () => {
5762
void updateOutput()
5863
})
5964
void updateOutput()
65+
66+
copyButton?.addEventListener('click', async () => {
67+
if (!outputText) {
68+
return
69+
}
70+
71+
try {
72+
await navigator.clipboard.writeText(outputText.value)
73+
copyButton.textContent = 'Copied!'
74+
} catch {
75+
copyButton.textContent = 'Copy failed'
76+
}
77+
78+
window.setTimeout(() => {
79+
copyButton.textContent = 'Copy to clipboard'
80+
}, 1500)
81+
})

src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
2121
<a href="/runecount.html" class="btn btn-primary">Rune Count</a>
2222
<div class="ps-2">count which characters are in a file</div>
2323
</div>
24+
<div class="flex flex-row items-center">
25+
<a href="/upside-down.html" class="btn btn-primary">Upside Down</a>
26+
<div class="ps-2">flip text upside down</div>
27+
</div>
2428
</div>
2529
</div>
2630

src/upside-down.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import './style.css'
2+
import { renderHeader } from './components/header.ts'
3+
4+
const baseFlipTable: Record<string, string> = {
5+
'\u0021': '\u00A1',
6+
'\u0022': '\u201E',
7+
'\u0026': '\u214B',
8+
'\u0027': '\u002C',
9+
'\u0028': '\u0029',
10+
'\u002E': '\u02D9',
11+
'\u0033': '\u0190',
12+
'\u0034': '\u152D',
13+
'\u0036': '\u0039',
14+
'\u0037': '\u2C62',
15+
'\u003B': '\u061B',
16+
'\u003C': '\u003E',
17+
'\u003F': '\u00BF',
18+
'\u0041': '\u2200',
19+
'\u0042': '\u{10412}',
20+
'\u0043': '\u2183',
21+
'\u0044': '\u25D6',
22+
'\u0045': '\u018E',
23+
'\u0046': '\u2132',
24+
'\u0047': '\u2141',
25+
'\u004A': '\u017F',
26+
'\u004B': '\u22CA',
27+
'\u004C': '\u2142',
28+
'\u004D': '\u0057',
29+
'\u004E': '\u1D0E',
30+
'\u0050': '\u0500',
31+
'\u0051': '\u038C',
32+
'\u0052': '\u1D1A',
33+
'\u0054': '\u22A5',
34+
'\u0055': '\u2229',
35+
'\u0056': '\u1D27',
36+
'\u0059': '\u2144',
37+
'\u005B': '\u005D',
38+
'\u005F': '\u203E',
39+
'\u0061': '\u0250',
40+
'\u0062': '\u0071',
41+
'\u0063': '\u0254',
42+
'\u0064': '\u0070',
43+
'\u0065': '\u01DD',
44+
'\u0066': '\u025F',
45+
'\u0067': '\u0183',
46+
'\u0068': '\u0265',
47+
'\u0069': '\u0131',
48+
'\u006A': '\u027E',
49+
'\u006B': '\u029E',
50+
'\u006C': '\u0283',
51+
'\u006D': '\u026F',
52+
'\u006E': '\u0075',
53+
'\u0072': '\u0279',
54+
'\u0074': '\u0287',
55+
'\u0076': '\u028C',
56+
'\u0077': '\u028D',
57+
'\u0079': '\u028E',
58+
'\u007B': '\u007D',
59+
'\u203F': '\u2040',
60+
'\u2045': '\u2046',
61+
'\u2234': '\u2235',
62+
}
63+
64+
const flipTable: Record<string, string> = { ...baseFlipTable }
65+
for (const [left, right] of Object.entries(baseFlipTable)) {
66+
if (!(right in flipTable)) {
67+
flipTable[right] = left
68+
}
69+
}
70+
71+
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
72+
<main class="min-h-screen bg-base-200" data-theme="light">
73+
<section class="mx-auto flex max-w-5xl flex-col gap-8 px-4 py-8 md:px-8 md:py-12">
74+
${renderHeader()}
75+
76+
<section class="rounded-box border border-base-300 bg-base-100 p-8 shadow-sm">
77+
<div class="flex items-center justify-between gap-3">
78+
<h1 class="text-3xl font-bold">Upside Down</h1>
79+
<button id="demo-button" type="button" class="btn btn-sm">Demo</button>
80+
</div>
81+
<p class="mt-3 text-base-content/70">Type text below to flip it upside down in your browser.</p>
82+
83+
<form class="mt-6 flex flex-col gap-4" action="#" method="post" onsubmit="return false;">
84+
<label class="form-control w-full">
85+
<div class="label">
86+
<span class="label-text">Input text</span>
87+
</div>
88+
<textarea id="input-text" class="textarea textarea-bordered min-h-40 w-full" placeholder="Paste or type text here"></textarea>
89+
</label>
90+
91+
<label class="form-control w-full">
92+
<div class="label">
93+
<span class="label-text">Upside-down output</span>
94+
</div>
95+
<textarea id="output-text" class="textarea textarea-bordered min-h-40 w-full" readonly></textarea>
96+
</label>
97+
98+
<div>
99+
<button id="copy-button" type="button" class="btn btn-sm">Copy to clipboard</button>
100+
</div>
101+
</form>
102+
</section>
103+
</section>
104+
</main>
105+
`
106+
107+
const inputText = document.querySelector<HTMLTextAreaElement>('#input-text')
108+
const outputText = document.querySelector<HTMLTextAreaElement>('#output-text')
109+
const copyButton = document.querySelector<HTMLButtonElement>('#copy-button')
110+
const demoButton = document.querySelector<HTMLButtonElement>('#demo-button')
111+
112+
const extractFortuneRaw = (payload: unknown): string | null => {
113+
if (!payload || typeof payload !== 'object') {
114+
return null
115+
}
116+
117+
const raw = (payload as Record<string, unknown>).raw
118+
119+
return typeof raw === 'string' ? raw : null
120+
}
121+
122+
const fetchFortuneJsonp = (): Promise<string> =>
123+
new Promise((resolve, reject) => {
124+
const windowRecord = window as unknown as Record<string, unknown>
125+
const callbackName = `fortuneJsonp_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`
126+
const script = document.createElement('script')
127+
const timeout = window.setTimeout(() => {
128+
cleanup()
129+
reject(new Error('Timed out loading fortune'))
130+
}, 10000)
131+
132+
const cleanup = () => {
133+
window.clearTimeout(timeout)
134+
script.remove()
135+
delete windowRecord[callbackName]
136+
}
137+
138+
windowRecord[callbackName] = (payload: unknown) => {
139+
cleanup()
140+
141+
const text = extractFortuneRaw(payload)
142+
if (!text) {
143+
reject(new Error('No raw value in response'))
144+
return
145+
}
146+
147+
resolve(text.trim())
148+
}
149+
150+
script.onerror = () => {
151+
cleanup()
152+
reject(new Error('Unable to load fortune JSONP script'))
153+
}
154+
155+
script.src = `http://www.fortune.ninja/fortune/bsd_linux.json?callback=${encodeURIComponent(callbackName)}`
156+
document.body.appendChild(script)
157+
})
158+
159+
const toUpsideDown = (value: string): string => {
160+
let result = ''
161+
162+
for (const character of value) {
163+
const flipped = flipTable[character] ?? character
164+
result = flipped + result
165+
}
166+
167+
return result
168+
}
169+
170+
const updateOutput = () => {
171+
if (!inputText || !outputText) {
172+
return
173+
}
174+
175+
outputText.value = toUpsideDown(inputText.value)
176+
}
177+
178+
inputText?.addEventListener('input', updateOutput)
179+
updateOutput()
180+
181+
demoButton?.addEventListener('click', async () => {
182+
if (!inputText) {
183+
return
184+
}
185+
186+
demoButton.disabled = true
187+
demoButton.textContent = 'Loading...'
188+
189+
try {
190+
const fortune = await fetchFortuneJsonp()
191+
inputText.value = fortune
192+
updateOutput()
193+
demoButton.textContent = 'Loaded'
194+
} catch {
195+
demoButton.textContent = 'Failed'
196+
}
197+
198+
window.setTimeout(() => {
199+
demoButton.disabled = false
200+
demoButton.textContent = 'Demo'
201+
}, 1500)
202+
})
203+
204+
copyButton?.addEventListener('click', async () => {
205+
if (!outputText) {
206+
return
207+
}
208+
209+
try {
210+
await navigator.clipboard.writeText(outputText.value)
211+
copyButton.textContent = 'Copied!'
212+
} catch {
213+
copyButton.textContent = 'Copy failed'
214+
}
215+
216+
window.setTimeout(() => {
217+
copyButton.textContent = 'Copy to clipboard'
218+
}, 1500)
219+
})

upside-down.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Upside Down Text</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/upside-down.ts"></script>
12+
</body>
13+
</html>

vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default defineConfig({
1111
bytecount: resolve(__dirname, 'bytecount.html'),
1212
runecount: resolve(__dirname, 'runecount.html'),
1313
asciify: resolve(__dirname, 'asciify.html'),
14+
upsideDown: resolve(__dirname, 'upside-down.html'),
1415
},
1516
},
1617
},

0 commit comments

Comments
 (0)