T-HUB/packages/safe-content/src/text.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

57 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Bidi embedding/override/isolate controls (U+202A–U+202E, U+2066–U+2069) can
* visually reorder text ("evil" + U+202E + "txt.exe"); they are always removed.
*/
const BIDI_CONTROLS = /[\u202A-\u202E\u2066-\u2069]/g;
/** Marks and zero-width characters used for spoofing look-alike names. */
const INVISIBLES = /[\u200B-\u200F\u061C\u180E\u2060-\u2064\uFEFF\u034F\u115F\u1160\u3164\uFFA0]/g;
/** C0/C1 controls except tab and newline; lone surrogates. */
const CONTROLS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
/** Normalises body text: strips controls and bidi overrides, keeps ZWJ (emoji sequences). */
export function neutralizeText(input: string): string {
return input.replace(CONTROLS, "").replace(BIDI_CONTROLS, "").replace(/\r\n?/g, "\n");
}
/**
* Display names, usernames, channel names: single line, no bidi or zero-width
* characters at all, whitespace collapsed, length-capped.
*/
export function neutralizeDisplayName(input: string, maxLength = 100): string {
const cleaned = input
.normalize("NFC")
.replace(CONTROLS, "")
.replace(BIDI_CONTROLS, "")
.replace(INVISIBLES, "")
.replace(/\s+/g, " ")
.trim();
return truncateCodePoints(cleaned, maxLength);
}
export function truncateCodePoints(input: string, max: number): string {
if (input.length <= max) return input;
const chars = Array.from(input);
return chars.length <= max ? input : chars.slice(0, max).join("") + "…";
}
const SCRIPT_RUNS: readonly [string, RegExp][] = [
["Latin", /\p{Script=Latin}/u],
["Cyrillic", /\p{Script=Cyrillic}/u],
["Greek", /\p{Script=Greek}/u],
["Armenian", /\p{Script=Armenian}/u],
["Cherokee", /\p{Script=Cherokee}/u],
];
/**
* Homoglyph heuristic: true when a single word mixes Latin with a script that
* has Latin look-alikes ("pаypal" with Cyrillic "а"). The UI should flag such
* names rather than trust them; it is a warning, not a sanitiser.
*/
export function hasMixedScriptConfusables(input: string): boolean {
for (const word of input.normalize("NFKC").split(/[\s\p{P}]+/u)) {
let seen = 0;
for (const [, re] of SCRIPT_RUNS) if (re.test(word)) seen++;
if (seen > 1) return true;
}
return false;
}