Skip to content

Commit c724ecb

Browse files
author
Andrew Marcuse
committed
runecount to charcount
1 parent 2201235 commit c724ecb

5 files changed

Lines changed: 50 additions & 34 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ These are some useful tools for viewing, analyzing and fixing file formats. The
77
## Tools
88

99
* [Byte count](https://tools.fileformat.info/bytecount.html) - count the byte values in a file
10-
* [Rune count](https://tools.fileformat.info/runecount.html) - count the characters (UTF-8, UTF-16 or Latin-1) in a file
10+
* [Character count](https://tools.fileformat.info/charcount.html) - count the characters (UTF-8, UTF-16 or Latin-1) in a file
1111

1212
## License
1313

runecount.html renamed to charcount.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
</head>
99
<body>
1010
<div id="app"></div>
11-
<script type="module" src="/src/runecount.ts"></script>
11+
<script type="module" src="/src/charcount.ts"></script>
1212
</body>
1313
</html>

src/runecount.ts renamed to src/charcount.ts

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ type RuneStats = {
77
last: number
88
}
99

10+
type CountRunesResult = {
11+
runeCounts: Map<number, RuneStats>
12+
decodeErrorCount: number
13+
}
14+
1015
type CharsetKey = 'utf8' | 'utf16' | 'latin1'
1116

1217
const charsetToEncoding: Record<CharsetKey, string> = {
@@ -22,9 +27,9 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
2227
2328
<section class="rounded-box border border-base-300 bg-base-100 p-8 shadow-sm">
2429
<h1 class="text-3xl font-bold">Character Count</h1>
25-
<p class="mt-3 text-base-content/70">Choose a text file and start counting Unicode code points.</p>
30+
<p class="mt-3 text-base-content/70">Choose a text file to start counting Unicode code points.</p>
2631
27-
<form id="runecount-form" class="mt-6 flex flex-col gap-4" action="#" method="post">
32+
<form id="charcount-form" class="mt-6 flex flex-col gap-4" action="#" method="post">
2833
<label class="form-control w-full">
2934
<div class="label">
3035
<span class="label-text">Input file</span>
@@ -44,10 +49,6 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
4449
</label>
4550
4651
<div id="form-error" class="alert alert-error hidden" role="alert" aria-live="polite"></div>
47-
48-
<div>
49-
<button id="start-button" type="submit" class="btn btn-primary">Start</button>
50-
</div>
5152
</form>
5253
5354
<section id="results" class="mt-8 hidden">
@@ -74,11 +75,9 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
7475
</main>
7576
`
7677

77-
const form = document.querySelector<HTMLFormElement>('#runecount-form')
7878
const fileInput = document.querySelector<HTMLInputElement>('#input-file')
7979
const charsetSelect = document.querySelector<HTMLSelectElement>('#charset-select')
8080
const formError = document.querySelector<HTMLDivElement>('#form-error')
81-
const startButton = document.querySelector<HTMLButtonElement>('#start-button')
8281
const resultsSection = document.querySelector<HTMLElement>('#results')
8382
const resultsSummary = document.querySelector<HTMLParagraphElement>('#results-summary')
8483
const resultsBody = document.querySelector<HTMLTableSectionElement>('#results-body')
@@ -119,10 +118,11 @@ const showError = (message: string) => {
119118

120119
const isCharsetKey = (value: string): value is CharsetKey => value in charsetToEncoding
121120

122-
const countRunes = async (file: File, charset: CharsetKey): Promise<Map<number, RuneStats>> => {
121+
const countRunes = async (file: File, charset: CharsetKey): Promise<CountRunesResult> => {
123122
const runeCounts = new Map<number, RuneStats>()
124123
const fileBuffer = await file.arrayBuffer()
125124
const text = new TextDecoder(charsetToEncoding[charset]).decode(fileBuffer)
125+
let decodeErrorCount = 0
126126

127127
let runeOffset = 0
128128
for (const character of text) {
@@ -132,6 +132,10 @@ const countRunes = async (file: File, charset: CharsetKey): Promise<Map<number,
132132
continue
133133
}
134134

135+
if (codePoint === 0xfffd) {
136+
decodeErrorCount += 1
137+
}
138+
135139
const existing = runeCounts.get(codePoint)
136140

137141
if (existing) {
@@ -150,7 +154,7 @@ const countRunes = async (file: File, charset: CharsetKey): Promise<Map<number,
150154
runeOffset += 1
151155
}
152156

153-
return runeCounts
157+
return { runeCounts, decodeErrorCount }
154158
}
155159

156160
const formatOffsetHex = (offset: number): string => `0x${offset.toString(16).toUpperCase().padStart(4, '0')}`
@@ -201,7 +205,7 @@ const toCodePointUrl = (codePoint: number): string => {
201205
return `https://www.fileformat.info/info/unicode/char/${hex}/index.htm`
202206
}
203207

204-
const renderRuneTable = (runeCounts: Map<number, RuneStats>, totalRunes: number) => {
208+
const renderRuneTable = (runeCounts: Map<number, RuneStats>, totalRunes: number, decodeErrorCount: number) => {
205209
if (!resultsBody || !resultsSummary || !resultsSection) {
206210
return
207211
}
@@ -214,23 +218,21 @@ const renderRuneTable = (runeCounts: Map<number, RuneStats>, totalRunes: number)
214218
)
215219

216220
resultsBody.innerHTML = rows.join('')
217-
resultsSummary.textContent = `${totalRunes.toLocaleString()} total code points, ${runeCounts.size} distinct values.`
221+
const baseSummary = `${totalRunes.toLocaleString()} total code points, ${runeCounts.size} distinct values.`
222+
resultsSummary.innerHTML =
223+
decodeErrorCount > 0
224+
? `${baseSummary} <span class="inline-flex items-center gap-1"><span aria-hidden="true">⚠️</span><span>${decodeErrorCount.toLocaleString()} decoding errors</span></span>`
225+
: baseSummary
226+
218227
resultsSection.classList.remove('hidden')
219228
}
220229

221-
form?.addEventListener('submit', async (event) => {
222-
event.preventDefault()
223-
230+
const runCount = async () => {
224231
const selectedFile = fileInput?.files?.[0]
225232
const selectedCharset = charsetSelect?.value ?? 'utf8'
226233

227234
if (!selectedFile) {
228-
if (resultsSection) {
229-
resultsSection.classList.add('hidden')
230-
}
231-
232-
showError('Please choose a file before starting.')
233-
235+
resultsSection?.classList.add('hidden')
234236
return
235237
}
236238

@@ -241,21 +243,35 @@ form?.addEventListener('submit', async (event) => {
241243

242244
hideError()
243245

244-
if (startButton) {
245-
startButton.disabled = true
246-
startButton.textContent = 'Counting...'
246+
if (fileInput) {
247+
fileInput.disabled = true
248+
}
249+
250+
if (charsetSelect) {
251+
charsetSelect.disabled = true
247252
}
248253

249254
try {
250-
const runeCounts = await countRunes(selectedFile, selectedCharset)
251-
const totalRunes = Array.from(runeCounts.values()).reduce((total, stats) => total + stats.count, 0)
252-
renderRuneTable(runeCounts, totalRunes)
255+
const countResult = await countRunes(selectedFile, selectedCharset)
256+
const totalRunes = Array.from(countResult.runeCounts.values()).reduce((total, stats) => total + stats.count, 0)
257+
renderRuneTable(countResult.runeCounts, totalRunes, countResult.decodeErrorCount)
253258
} catch {
254259
showError('Unable to read the selected file.')
255260
} finally {
256-
if (startButton) {
257-
startButton.disabled = false
258-
startButton.textContent = 'Start'
261+
if (fileInput) {
262+
fileInput.disabled = false
263+
}
264+
265+
if (charsetSelect) {
266+
charsetSelect.disabled = false
259267
}
260268
}
269+
}
270+
271+
fileInput?.addEventListener('change', () => {
272+
void runCount()
273+
})
274+
275+
charsetSelect?.addEventListener('change', () => {
276+
void runCount()
261277
})

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
1818
<div>count which bytes are in a file</div>
1919
</div>
2020
<div class="flex w-full items-center gap-3">
21-
<a href="/runecount.html" class="btn btn-primary w-40">Character Count</a>
21+
<a href="/charcount.html" class="btn btn-primary w-40">Character Count</a>
2222
<div>count which characters are in a file</div>
2323
</div>
2424
<div class="flex w-full items-center gap-3">

vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineConfig({
99
input: {
1010
main: resolve(__dirname, 'index.html'),
1111
bytecount: resolve(__dirname, 'bytecount.html'),
12-
runecount: resolve(__dirname, 'runecount.html'),
12+
charcount: resolve(__dirname, 'charcount.html'),
1313
asciify: resolve(__dirname, 'asciify.html'),
1414
upsideDown: resolve(__dirname, 'upside-down.html'),
1515
urlencode: resolve(__dirname, 'urlencode.html'),

0 commit comments

Comments
 (0)