1 import DOMPurify from 'dompurify';
3 const addRelNoopenerAndTargetBlank = (node: Node) => {
4 if (node instanceof Element && node.tagName === 'A') {
5 node.setAttribute('rel', 'noopener noreferrer');
6 node.setAttribute('target', '_blank');
10 export const restrictedCalendarSanitize = (source: string) => {
11 DOMPurify.clearConfig();
12 DOMPurify.addHook('afterSanitizeAttributes', addRelNoopenerAndTargetBlank);
14 const sanitizedContent = DOMPurify.sanitize(source, {
15 ALLOWED_TAGS: ['a', 'b', 'em', 'br', 'i', 'u', 'ul', 'ol', 'li', 'span', 'p'],
16 ALLOWED_ATTR: ['href'],
19 DOMPurify.removeHook('afterSanitizeAttributes');
21 return sanitizedContent;
24 const validTags: Set<string> = new Set([
46 // 'center', // Deprecated
149 const escapeBrackets = (input: string): string => {
150 return input.replace(/</g, '<').replace(/>/g, '>');
153 const HTML_TAG_PATTERN: RegExp = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/g;
155 export const escapeInvalidHtmlTags = (input: string): string => {
163 // Replace is only used to iterate over all matches
164 input.replace(HTML_TAG_PATTERN, (match, tagName, offset) => {
165 // Append the part of the string before the current match
166 result += escapeBrackets(input.slice(lastIndex, offset));
167 // Append the current match (HTML tag) if it's valid, otherwise escape it
168 result += validTags.has(tagName.toLowerCase()) ? match : escapeBrackets(match);
169 // Update lastIndex to the end of the current match
170 lastIndex = offset + match.length;
171 // Return the match as is
175 // Append any remaining part of the string after the last match
176 result += escapeBrackets(input.slice(lastIndex));
181 export const stripAllTags = (source: string) => {
182 const html = escapeInvalidHtmlTags(source);
183 const sanitized = restrictedCalendarSanitize(html);
184 const div = document.createElement('DIV');
185 div.style.whiteSpace = 'pre-wrap';
186 div.innerHTML = sanitized;
187 div.querySelectorAll('a').forEach((element) => {
188 element.innerText = element.href || element.innerText;
190 // Append it to force a layout pass so that innerText returns newlines
191 document.body.appendChild(div);
192 const result = div.innerText;
193 document.body.removeChild(div);