import sanitizeHtml from 'sanitize-html'; const allowedTags = [ 'p', 'br', 'h2', 'h3', 'h4', 'strong', 'em', 'u', 's', 'ul', 'ol', 'li', 'blockquote', 'a', 'figure', 'figcaption', 'img', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td' ]; const blockTags = /<\/?(?:p|h[2-4]|ul|ol|li|blockquote|br|figure|figcaption|table|thead|tbody|tfoot|tr|th|td)\b[^>]*>/gi; function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function decodeTextEntities(value) { const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", '#39': "'", nbsp: ' ' }; const codePoint = (code, radix) => { const parsed = Number.parseInt(code, radix); return Number.isInteger(parsed) && parsed >= 0 && parsed <= 0x10ffff && !(parsed >= 0xd800 && parsed <= 0xdfff) ? String.fromCodePoint(parsed) : '�'; }; return String(value) .replace(/&#x([0-9a-f]+);/gi, (_, code) => codePoint(code, 16)) .replace(/&#(\d+);/g, (_, code) => codePoint(code, 10)) .replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/gi, (_, name) => named[name.toLowerCase()]); } export function sanitizeNoticeContent(value) { const source = String(value ?? '').trim().slice(0, 20000); return sanitizeHtml(source, { allowedTags, allowedAttributes: { a: ['href', 'target', 'rel'], figure: ['class'], img: ['src', 'alt'], th: ['colspan', 'rowspan'], td: ['colspan', 'rowspan'] }, allowedClasses: { figure: [ 'image', 'table', 'image-style-inline', 'image-style-block', 'image-style-side', 'image-style-align-left', 'image-style-align-right', 'image-style-block-align-left', 'image-style-block-align-right' ] }, allowedSchemes: ['http', 'https', 'mailto', 'tel'], allowProtocolRelative: false, transformTags: { a(tagName, attributes) { const safeAttributes = {}; if (attributes.href) safeAttributes.href = attributes.href; if (attributes.target === '_blank') safeAttributes.target = '_blank'; safeAttributes.rel = 'noopener noreferrer'; return { tagName, attribs: safeAttributes }; }, img(tagName, attributes) { const safeAttributes = {}; if (/^https?:\/\//i.test(attributes.src || '')) safeAttributes.src = attributes.src; if (attributes.alt) safeAttributes.alt = attributes.alt; return { tagName, attribs: safeAttributes }; } } }); } export function noticePlainText(value) { const sanitized = sanitizeNoticeContent(value).replace(blockTags, ' '); const withoutTags = sanitizeHtml(sanitized, { allowedTags: [], allowedAttributes: {} }); return decodeTextEntities(withoutTags).replace(/\s+/g, ' ').trim(); } export function noticeContentHtml(value) { const source = String(value ?? '').trim(); if (!source) return ''; if (!/<\/?(?:p|h[2-4]|strong|em|u|s|ul|ol|li|blockquote|a|br|figure|figcaption|img|table|thead|tbody|tfoot|tr|th|td)\b/i.test(source)) { return source .split(/\r?\n{2,}/) .map(paragraph => `

${escapeHtml(paragraph).replace(/\r?\n/g, '
')}

`) .join(''); } return sanitizeNoticeContent(source); } export function noticeForClient(notice) { return { ...notice, content: sanitizeNoticeContent(notice.content), contentHtml: noticeContentHtml(notice.content) }; }