| 160 | }) |
| 161 | .replace(/&(amp|lt|gt|quot|apos|nbsp);/gi, (match, entity) => { |
| 162 | switch (entity.toLowerCase()) { |
| 163 | case "amp": |
| 164 | return "&"; |
| 165 | case "lt": |
| 166 | return "<"; |
| 167 | case "gt": |
| 168 | return ">"; |
| 169 | case "quot": |
| 170 | return '"'; |
| 171 | case "apos": |
| 172 | return "'"; |
| 173 | case "nbsp": |
| 174 | return " "; |
| 175 | default: |
| 176 | return match; |
| 177 | } |
| 178 | }); |
| 179 | |
| 180 | const sanitizePlainText = (text: string): string => |
| 181 | decodeHtmlEntities(text) |
| 182 | // Strip control characters that Telegram may render poorly while preserving common whitespace. |
| 183 | .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "") |
| 184 | .replace(/\r\n/g, "\n"); |
| 185 | |
| 186 | const trimTrailingSlash = (url: string): string => url.replace(/\/+$/, ""); |
| 187 | |
| 188 | const clampTemperature = (value: number, fallback: number): number => { |
| 189 | if (!Number.isFinite(value)) { |
| 190 | return fallback; |
| 191 | } |
| 192 | if (value < TEMPERATURE_MIN) return TEMPERATURE_MIN; |