@navinc/base-react-components
Version:
Nav's Pattern Library
126 lines • 5.01 kB
JavaScript
/* eslint-env browser */
// Based on https://github.com/reactjs/react-magic/blob/master/src/htmltojsx.js
import React from 'react';
import mapAttribute from './mapAttribute.js';
const TABLE_ELEMENTS = ['table', 'tbody', 'thead', 'tfoot', 'tr'];
// map HTML attribute to react props, and optionally DOM prop by using array
// if DOM prop is same as attribute name, use single item array
const attributePropMap = {
for: 'htmlFor',
class: 'className',
// react prop and DOM prop have different casing
allowfullscreen: ['allowFullScreen', 'allowFullscreen'],
autocomplete: 'autoComplete',
autofocus: ['autoFocus'],
contenteditable: 'contentEditable',
spellcheck: 'spellCheck',
srcdoc: 'srcDoc',
srcset: 'srcSet',
itemscope: 'itemScope',
itemprop: 'itemProp',
itemtype: 'itemType',
};
// some attribute can't be checked whether it's a boolean attribute
// or not by using setAttribute trick
const SPECIAL_BOOLEAN_ATTRIBUTES = ['itemScope'];
function checkBooleanAttribute(el, prop) {
el.setAttribute(prop, '');
// @ts-expect-error -- copied code
return el[prop] === true || SPECIAL_BOOLEAN_ATTRIBUTES.indexOf(prop) > -1;
}
/**
* In browser we can get equivalent React props by creating a temporary DOM node,
* and iterating over its properties. This means we don't have to ship entire
* HTML attributes mapping to React props.
*
* For other edge cases we can use specialPropsMaps
*/
function getPropInfo(tagName, attributeName) {
const propName = attributePropMap[attributeName];
const el = document.createElement(tagName);
// handle edge cases first
if (propName) {
const reactProp = Array.isArray(propName) ? propName[0] : propName;
const domProp = Array.isArray(propName) ? propName[1] || attributeName : propName;
return { name: reactProp, isBoolean: checkBooleanAttribute(el, domProp) };
}
for (const propName in el) {
if (propName.toLowerCase() === attributeName.toLowerCase()) {
return { name: propName, isBoolean: checkBooleanAttribute(el, propName) };
}
}
return {
name: attributeName,
isBoolean: checkBooleanAttribute(el, attributeName),
};
}
function reactCreateElement(tag, props, transform, children = null) {
const customElement = transform[tag];
const defaultTransform = transform._;
return customElement
? React.createElement(customElement, props, children)
: defaultTransform
? defaultTransform(tag, props, children)
: React.createElement(tag, props, children);
}
function toReactNode(node, key, options) {
const transform = options.transform || {};
const preserveAttributes = options.preserveAttributes || [];
const dangerouslySetChildren = options.dangerouslySetChildren || ['style'];
const defaultTransform = transform._;
if (node.nodeType === 8) {
return null;
}
else if (node.nodeType === 3) {
const text = node.textContent;
return defaultTransform ? defaultTransform(text) : text;
}
const attrs = {};
const nodeAttributes = node.attributes;
for (let i = 0; i < nodeAttributes.length; i++) {
attrs[nodeAttributes[i].name] = nodeAttributes[i].value;
}
attrs.key = key.toString();
const tag = node.tagName.toLowerCase();
const props = mapAttribute(tag, attrs, preserveAttributes, getPropInfo);
const children = Array.from(node.childNodes)
.map((childNode, i) => {
if (TABLE_ELEMENTS.indexOf(tag) > -1 && childNode.nodeType === 3) {
childNode.textContent = childNode.textContent.trim();
if (childNode.textContent === '') {
return null;
}
}
return toReactNode(childNode, key + '.' + i, options);
})
.filter(Boolean);
if (dangerouslySetChildren.indexOf(tag) > -1) {
let html = node.innerHTML;
// Tag can have empty children
if (html) {
// we need to preserve quote inside style & script tag
if (tag !== 'style' && tag !== 'script') {
html = html.replace(/"/g, '"');
}
props.dangerouslySetInnerHTML = { __html: html.trim() };
}
return reactCreateElement(tag, props, transform);
}
// self closing tag shouldn't have children
const reactChildren = children.length === 0 ? null : children;
return reactCreateElement(tag, props, transform, reactChildren);
}
export function htmr(html, options = {}) {
if (typeof html !== 'string') {
throw new TypeError('Expected HTML string');
}
const container = document.createElement('div');
container.innerHTML = html.trim();
const nodes = Array.from(container.childNodes)
.map((childNode, index) => {
return toReactNode(childNode, String(index), options);
})
.filter(Boolean);
return nodes.length === 1 ? nodes[0] : nodes;
}
//# sourceMappingURL=index.js.map