54 lines
1.8 KiB
TypeScript
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> = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
};
|
|
|
|
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 `<` 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();
|