Skip to content

Instantly share code, notes, and snippets.

@composite
Last active May 25, 2023 09:18
Show Gist options
  • Save composite/dcdec1ae24b595193a2d1458952fac2a to your computer and use it in GitHub Desktop.
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.)
// 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