/* eslint-disable @typescript-eslint/quotes */ import { Replacer } from '../internal/replacer.ts'; import { fromCodePoint, getCodePointAt, numericUnicodeMap } from '../internal/codepoints.ts'; import { type Dictionary } from '../other/types.ts'; import { buildFullNamedEntities, fullNamedEntities } from './entity.ts'; const escapeReplacer = /*#__PURE__*/ new Replacer({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"', }); const unescapeReplacer = /*#__PURE__*/ new Replacer({ '&': '&', '&': '&', '<': '<', '<': '<', '>': '>', '>': '>', ''': "'", ''': "'", '"': '"', '"': '"', }); const namedEntities: Record = { 'amp;': '&', 'lt;': '<', 'gt;': '>', 'quot;': '"', 'apos;': `'`, }; /** * Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'` * @param {string} input */ export function escape(input: string): string { return escapeReplacer.replace(input); } /** * Unescape HTML entities such as `&`, `<`, `>`, `"`, and `'` * @param {string} input */ export function unescape(input: string): string { return unescapeReplacer.replace(input); } const outOfBoundsChar = /*#__PURE__*/ String.fromCharCode(65533); const ENCODE_REGEX = /(?:[<>'"&\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g; export function encodeHTMLEntities(input: string): string { if (input == null) { return ''; } return input.replace(ENCODE_REGEX, (entity) => { const code = entity.length > 1 ? getCodePointAt(entity, 0) : entity.charCodeAt(0); return '&#' + String(code) + ';'; }); } const DECODE_REGEX = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+);?/g; function decodeString(input: string, entities: Record): string { if (typeof input !== 'string' || !input) { return ''; } return input.replace(DECODE_REGEX, (entity) => { if (entity[1] === '#') { // We need to have at least "&#.". if (entity.length <= 3) { return entity; } const secondChar = entity.charAt(2); const code = secondChar === 'x' || secondChar === 'X' ? parseInt(entity.substr(3).toLowerCase(), 16) : parseInt(entity.substr(2)); if (code >= 0x10ffff) { return outOfBoundsChar; } if (code > 65535) { return fromCodePoint(code); } return String.fromCharCode(numericUnicodeMap[code] || code); } return entities[entity.slice(1)] || entity; }); } export function decodeHTMLEntitiesDeep(input: T): T { if (typeof input === 'string') { return decodeHTMLEntities(input) as unknown as T; } if (typeof input === 'object') { const correctType = Object.prototype.toString.apply(input); if (correctType === '[object Array]') { return (input as unknown[]).map((item) => { return decodeHTMLEntitiesDeep(item); }) as T; } if (correctType === '[object Object]') { const response: Dictionary = {}; Object.keys(input as Dictionary).forEach((item) => { response[decodeHTMLEntities(item)] = decodeHTMLEntitiesDeep( (input as Dictionary)[item], ); }); return response as unknown as T; } } return input; } /** * `decodeHTMLEntities` декодирует зарезервированные HTML-сущности. * * Если нужна возможность декодировать все сущности, используйте * {@link decodeHTMLFullEntities} * * @param input Текст который необходимо декодировать * @param entities Кастомный словарь сущностей `{'lt;': '<'}` */ export function decodeHTMLEntities( input: string, entities: Record = namedEntities, ): string { return decodeString(input, entities); } /** * `decodeHTMLFullEntities` декодирует все HTML-сущности. * * Если вам нужно декодировать не все сущности, используйте * {@link decodeHTMLEntities} и кастомный словарь. * * @param input Текст который необходимо декодировать */ export function decodeHTMLFullEntities(input: string): string { buildFullNamedEntities(); return decodeString(input, fullNamedEntities); }