Files
kongruity/backend/lib/slackText.ts
2026-08-02 06:13:01 -04:00

54 lines
1.8 KiB
TypeScript

/**
* Slack delivers mrkdwn, not plain text. Link syntax and HTML entities that
* survive into a note degrade embedding quality and read as noise on a sticky,
* so they are unwrapped here. Pure and dependency-free so both the normalizer
* and the API client can use it.
*/
const ENTITIES: Record<string, string> = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
};
const unescapeEntities = (text: string): string =>
text.replace(/&(?:amp|lt|gt);/g, (match) => ENTITIES[match] ?? match);
const splitOnce = (value: string, separator: string): [string, string | undefined] => {
const index = value.indexOf(separator);
if (index === -1) return [value, undefined];
return [value.slice(0, index), value.slice(index + separator.length)];
};
const BROADCASTS: readonly string[] = ['here', 'channel', 'everyone'];
const MARKUP = /<([^<>]*)>/g;
const unwrap = (inner: string): string => {
const [target, rawLabel] = splitOnce(inner, '|');
const label = rawLabel !== undefined && rawLabel.length > 0 ? rawLabel : undefined;
if (target.startsWith('@')) return `@${label ?? target.slice(1)}`;
if (target.startsWith('#')) return `#${label ?? target.slice(1)}`;
if (target.startsWith('!')) {
const special = target.slice(1);
if (BROADCASTS.includes(special)) return `@${special}`;
return label ?? `@${special}`;
}
if (target.startsWith('mailto:')) return label ?? target.slice('mailto:'.length);
return label ?? target;
};
/**
* Entities are unescaped only after markup is unwrapped. Slack escapes a
* literal `<` as `&lt;` precisely so it is not read as markup, and reversing
* that order would turn user text into parsed link syntax.
*/
export const cleanSlackText = (input: string): string =>
unescapeEntities(input.replace(MARKUP, (_match, inner: string) => unwrap(inner)))
.replace(/[ \t]+$/gm, '')
.trim();