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/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
57
packages/safe-content/src/text.ts
Normal file
57
packages/safe-content/src/text.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue