Last active
May 25, 2023 09:18
-
-
Save composite/dcdec1ae24b595193a2d1458952fac2a to your computer and use it in GitHub Desktop.
Safely append plain HTML content from your raw html string for React (caveats: no SSR/SSG friendly. you should handle before use it.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// other allowed node types | |
const allowNodes: number[] = [Node.TEXT_NODE, Node.DOCUMENT_FRAGMENT_NODE] | |
// allowed tags | |
const allowTags = ['div', 'span', 'p', 'img', 'strong', 'i', 'b', 's', 'br', 'a', 'blockquote', 'code', 'del', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'kbd', 'ol', 'ul', 'li', 'sub', 'sup'] | |
// allowed tag's attributes | |
const allowAttrs = ['src', 'width', 'height', 'alt', 'title', 'href', 'style'] | |
// generate almost unique hash that avoid key warning | |
const unsafeHash = () => Math.random().toString(36).substring(2) | |
/** | |
* Safely append plain HTML content from your raw html string | |
* | |
* Usage for JSX/TSX: `<SafeHtml html="<p>The <strong>HTML</strong> content!</p>" />` | |
* | |
* @param props.html HTML content string | |
* @returns React Element or null if html content not exists | |
*/ | |
export const SafeHtml = ({ html }: { html: string }): ReactElement | null => { | |
const filterChild = (children: NodeListOf<ChildNode>): ReactNode => { | |
return [...children] | |
.filter(node => | |
node.nodeType === Node.ELEMENT_NODE | |
? allowTags.includes(node.nodeName.toLowerCase()) | |
: allowNodes.includes(node.nodeType), | |
) | |
.map(node => { | |
console.log(node, node.nodeName, node.nodeType) | |
if (node.nodeType === Node.ELEMENT_NODE) { | |
const el = node as Element | |
return React.createElement( | |
node.nodeName.toLowerCase(), | |
{ | |
key: 'SAFEHTML_ELEMENT#' + unsafeHash(), | |
...Object.fromEntries( | |
allowAttrs | |
.filter(attr => el.hasAttribute(attr)) | |
.map(attr => [attr, el.getAttribute(attr) ?? true]), | |
), | |
}, | |
filterChild(node.childNodes), | |
) | |
} else if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { | |
return React.createElement( | |
React.Fragment, | |
{ key: 'SAFEHTML_FRAGMENT#' + unsafeHash() }, | |
filterChild(node.childNodes), | |
) | |
} else return node.textContent | |
}) | |
} | |
if (html) { | |
const tmpl = document.createElement('template') | |
tmpl.innerHTML = String(html).trim() | |
return React.createElement(React.Fragment, null, filterChild(tmpl.content.childNodes)) | |
} else return null | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment