impl/typescript.ts
2,482 bytes · the TypeScript implementation · view raw
import type { CharacterCount, CountUnit } from "./form_character_count_types.ts";
/**
* The Unicode White_Space property, spelled out so all three languages agree
* on what separates words (JavaScript's \s, Python's str.isspace and Rust's
* char::is_whitespace each draw the line slightly differently).
*/
function isWhitespace(cp: number): boolean {
return (
(cp >= 0x09 && cp <= 0x0d) ||
cp === 0x20 ||
cp === 0x85 ||
cp === 0xa0 ||
cp === 0x1680 ||
(cp >= 0x2000 && cp <= 0x200a) ||
cp === 0x2028 ||
cp === 0x2029 ||
cp === 0x202f ||
cp === 0x205f ||
cp === 0x3000
);
}
function countWords(text: string): number {
let words = 0;
let inWord = false;
for (const ch of text) {
if (isWhitespace(ch.codePointAt(0) as number)) {
inWord = false;
} else if (!inWord) {
inWord = true;
words++;
}
}
return words;
}
/**
* GOV.UK's character count: how many characters (code points) or words have
* been typed against a limit, and the sentence to show under the text box.
*/
export function characterCount(text: string, limit: number, unit: CountUnit, threshold: number | null): CharacterCount {
if (typeof text !== "string") throw new TypeError("text must be a string");
if (!Number.isInteger(limit) || limit < 1) throw new RangeError(`limit must be a whole number of at least 1, received ${limit}`);
if (unit !== "characters" && unit !== "words") throw new RangeError(`unit must be characters or words, received ${unit}`);
if (threshold !== null && (!Number.isInteger(threshold) || threshold < 0 || threshold > 100)) {
throw new RangeError(`threshold must be a whole number from 0 to 100, received ${threshold}`);
}
// Code points, not UTF-16 units: an emoji is one character, as in Python and Rust.
const count = unit === "words" ? countWords(text) : Array.from(text).length;
const remaining = limit - count;
const over = remaining < 0;
// count >= limit * threshold / 100, kept in whole numbers so 50% of 3 is 1.5, not 1.
const visible = over || threshold === null || count * 100 >= limit * threshold;
const one = unit === "words" ? "word" : "character";
const noun = (n: number) => (n === 1 ? one : unit);
const message = over ? `You have ${-remaining} ${noun(-remaining)} too many` : `You have ${remaining} ${noun(remaining)} remaining`;
return { count, remaining, over, visible, message, description: `You can enter up to ${limit} ${noun(limit)}` };
}