alm
Version:
The best IDE for TypeScript
538 lines (537 loc) • 19.5 kB
JavaScript
/**
* This is a copy paste of `htmltojsx/src/htmltojsx.js`
* That lib tries to support both node / cli / browser
* So I just removed the `node` stuff from this file instead of taking too many depdencies
*/
;
Object.defineProperty(exports, "__esModule", { value: true });
/**
* This is a very simple HTML to JSX converter. It turns out that browsers
* have good HTML parsers (who would have thought?) so we utilise this by
* inserting the HTML into a temporary DOM node, and then do a breadth-first
* traversal of the resulting DOM tree.
*/
// https://developer.mozilla.org/en-US/docs/Web/API/Node.nodeType
var NODE_TYPE = {
ELEMENT: 1,
TEXT: 3,
COMMENT: 8
};
var ATTRIBUTE_MAPPING = {
'for': 'htmlFor',
'class': 'className'
};
var ELEMENT_ATTRIBUTE_MAPPING = {
'input': {
'checked': 'defaultChecked',
'value': 'defaultValue'
}
};
var HTMLDOMPropertyConfig = require('react-dom/lib/HTMLDOMPropertyConfig');
// Populate property map with ReactJS's attribute and property mappings
// TODO handle/use .Properties value eg: MUST_USE_PROPERTY is not HTML attr
for (var propname in HTMLDOMPropertyConfig.Properties) {
if (!HTMLDOMPropertyConfig.Properties.hasOwnProperty(propname)) {
continue;
}
var mapFrom = HTMLDOMPropertyConfig.DOMAttributeNames[propname] || propname.toLowerCase();
if (!ATTRIBUTE_MAPPING[mapFrom])
ATTRIBUTE_MAPPING[mapFrom] = propname;
}
/**
* Repeats a string a certain number of times.
* Also: the future is bright and consists of native string repetition:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
*
* @param {string} string String to repeat
* @param {number} times Number of times to repeat string. Integer.
* @see http://jsperf.com/string-repeater/2
*/
function repeatString(string, times) {
if (times === 1) {
return string;
}
if (times < 0) {
throw new Error();
}
var repeated = '';
while (times) {
if (times & 1) {
repeated += string;
}
if (times >>= 1) {
string += string;
}
}
return repeated;
}
/**
* Determine if the string ends with the specified substring.
*
* @param {string} haystack String to search in
* @param {string} needle String to search for
* @return {boolean}
*/
function endsWith(haystack, needle) {
return haystack.slice(-needle.length) === needle;
}
/**
* Trim the specified substring off the string. If the string does not end
* with the specified substring, this is a no-op.
*
* @param {string} haystack String to search in
* @param {string} needle String to search for
* @return {string}
*/
function trimEnd(haystack, needle) {
return endsWith(haystack, needle)
? haystack.slice(0, -needle.length)
: haystack;
}
/**
* Convert a hyphenated string to camelCase.
*/
function hyphenToCamelCase(string) {
return string.replace(/-(.)/g, function (match, chr) {
return chr.toUpperCase();
});
}
/**
* Determines if the specified string consists entirely of whitespace.
*/
function isEmpty(string) {
return !/[^\s]/.test(string);
}
/**
* Determines if the CSS value can be converted from a
* 'px' suffixed string to a numeric value
*
* @param {string} value CSS property value
* @return {boolean}
*/
function isConvertiblePixelValue(value) {
return /^\d+px$/.test(value);
}
/**
* Determines if the specified string consists entirely of numeric characters.
*/
function isNumeric(input) {
return input !== undefined
&& input !== null
&& (typeof input === 'number' || parseInt(input, 10) == input);
}
var createElement = function (tag) {
return document.createElement(tag);
};
var tempEl = createElement('div');
/**
* Escapes special characters by converting them to their escaped equivalent
* (eg. "<" to "<"). Only escapes characters that absolutely must be escaped.
*
* @param {string} value
* @return {string}
*/
function escapeSpecialChars(value) {
// Uses this One Weird Trick to escape text - Raw text inserted as textContent
// will have its escaped version in innerHTML.
tempEl.textContent = value;
return tempEl.innerHTML;
}
var HTMLtoJSX = /** @class */ (function () {
function HTMLtoJSX(config) {
var _this = this;
/**
* Reset the internal state of the converter
*/
this.reset = function () {
_this.output = '';
_this.level = 0;
_this._inPreTag = false;
};
/**
* Main entry point to the converter. Given the specified HTML, returns a
* JSX object representing it.
* @param {string} html HTML to convert
* @return {string} JSX
*/
this.convert = function (html) {
_this.reset();
var containerEl = createElement('div');
containerEl.innerHTML = '\n' + _this._cleanInput(html) + '\n';
if (_this.config.createClass) {
if (_this.config.outputClassName) {
_this.output = 'var ' + _this.config.outputClassName + ' = React.createClass({\n';
}
else {
_this.output = 'React.createClass({\n';
}
_this.output += _this.config.indent + 'render: function() {' + "\n";
_this.output += _this.config.indent + _this.config.indent + 'return (\n';
}
if (_this._onlyOneTopLevel(containerEl)) {
// Only one top-level element, the component can return it directly
// No need to actually visit the container element
_this._traverse(containerEl);
}
else {
// More than one top-level element, need to wrap the whole thing in a
// container.
_this.output += _this.config.indent + _this.config.indent + _this.config.indent;
_this.level++;
_this._visit(containerEl);
}
_this.output = _this.output.trim() + '\n';
if (_this.config.createClass) {
_this.output += _this.config.indent + _this.config.indent + ');\n';
_this.output += _this.config.indent + '}\n';
_this.output += '});';
}
return _this.output;
};
/**
* Cleans up the specified HTML so it's in a format acceptable for
* converting.
*
* @param {string} html HTML to clean
* @return {string} Cleaned HTML
*/
this._cleanInput = function (html) {
// Remove unnecessary whitespace
html = html.trim();
// Ugly method to strip script tags. They can wreak havoc on the DOM nodes
// so let's not even put them in the DOM.
html = html.replace(/<script([\s\S]*?)<\/script>/g, '');
return html;
};
/**
* Determines if there's only one top-level node in the DOM tree. That is,
* all the HTML is wrapped by a single HTML tag.
*
* @param {DOMElement} containerEl Container element
* @return {boolean}
*/
this._onlyOneTopLevel = function (containerEl) {
// Only a single child element
if (containerEl.childNodes.length === 1
&& containerEl.childNodes[0].nodeType === NODE_TYPE.ELEMENT) {
return true;
}
// Only one element, and all other children are whitespace
var foundElement = false;
for (var i = 0, count = containerEl.childNodes.length; i < count; i++) {
var child = containerEl.childNodes[i];
if (child.nodeType === NODE_TYPE.ELEMENT) {
if (foundElement) {
// Encountered an element after already encountering another one
// Therefore, more than one element at root level
return false;
}
else {
foundElement = true;
}
}
else if (child.nodeType === NODE_TYPE.TEXT && !isEmpty(child.textContent)) {
// Contains text content
return false;
}
}
return true;
};
/**
* Gets a newline followed by the correct indentation for the current
* nesting level
*
* @return {string}
*/
this._getIndentedNewline = function () {
return '\n' + repeatString(_this.config.indent, _this.level + 2);
};
/**
* Handles processing the specified node
*
* @param {Node} node
*/
this._visit = function (node) {
_this._beginVisit(node);
_this._traverse(node);
_this._endVisit(node);
};
/**
* Traverses all the children of the specified node
*
* @param {Node} node
*/
this._traverse = function (node) {
_this.level++;
for (var i = 0, count = node.childNodes.length; i < count; i++) {
_this._visit(node.childNodes[i]);
}
_this.level--;
};
/**
* Handle pre-visit behaviour for the specified node.
*
* @param {Node} node
*/
this._beginVisit = function (node) {
switch (node.nodeType) {
case NODE_TYPE.ELEMENT:
_this._beginVisitElement(node);
break;
case NODE_TYPE.TEXT:
_this._visitText(node);
break;
case NODE_TYPE.COMMENT:
_this._visitComment(node);
break;
default:
console.warn('Unrecognised node type: ' + node.nodeType);
}
};
/**
* Handles post-visit behaviour for the specified node.
*
* @param {Node} node
*/
this._endVisit = function (node) {
switch (node.nodeType) {
case NODE_TYPE.ELEMENT:
_this._endVisitElement(node);
break;
// No ending tags required for these types
case NODE_TYPE.TEXT:
case NODE_TYPE.COMMENT:
break;
}
};
/**
* Handles pre-visit behaviour for the specified element node
*
* @param {DOMElement} node
*/
this._beginVisitElement = function (node) {
var tagName = node.tagName.toLowerCase();
var attributes = [];
for (var i = 0, count = node.attributes.length; i < count; i++) {
attributes.push(_this._getElementAttribute(node, node.attributes[i]));
}
if (tagName === 'textarea') {
// Hax: textareas need their inner text moved to a "defaultValue" attribute.
attributes.push('defaultValue={' + JSON.stringify(node.value) + '}');
}
if (tagName === 'style') {
// Hax: style tag contents need to be dangerously set due to liberal curly brace usage
attributes.push('dangerouslySetInnerHTML={{__html: ' + JSON.stringify(node.textContent) + ' }}');
}
if (tagName === 'pre') {
_this._inPreTag = true;
}
_this.output += '<' + tagName;
if (attributes.length > 0) {
_this.output += ' ' + attributes.join(' ');
}
if (!_this._isSelfClosing(node)) {
_this.output += '>';
}
};
/**
* Handles post-visit behaviour for the specified element node
*
* @param {Node} node
*/
this._endVisitElement = function (node) {
var tagName = node.tagName.toLowerCase();
// De-indent a bit
// TODO: It's inefficient to do it this way :/
_this.output = trimEnd(_this.output, _this.config.indent);
if (_this._isSelfClosing(node)) {
_this.output += ' />';
}
else {
_this.output += '</' + node.tagName.toLowerCase() + '>';
}
if (tagName === 'pre') {
_this._inPreTag = false;
}
};
/**
* Determines if this element node should be rendered as a self-closing
* tag.
*
* @param {Node} node
* @return {boolean}
*/
this._isSelfClosing = function (node) {
// If it has children, it's not self-closing
// Exception: All children of a textarea are moved to a "defaultValue" attribute, style attributes are dangerously set.
return !node.firstChild || node.tagName.toLowerCase() === 'textarea' || node.tagName.toLowerCase() === 'style';
};
/**
* Handles processing of the specified text node
*
* @param {TextNode} node
*/
this._visitText = function (node) {
var parentTag = node.parentNode && node.parentNode.tagName.toLowerCase();
if (parentTag === 'textarea' || parentTag === 'style') {
// Ignore text content of textareas and styles, as it will have already been moved
// to a "defaultValue" attribute and "dangerouslySetInnerHTML" attribute respectively.
return;
}
var text = escapeSpecialChars(node.textContent);
if (_this._inPreTag) {
// If this text is contained within a <pre>, we need to ensure the JSX
// whitespace coalescing rules don't eat the whitespace. This means
// wrapping newlines and sequences of two or more spaces in variables.
text = text
.replace(/\r/g, '')
.replace(/( {2,}|\n|\t|\{|\})/g, function (whitespace) {
return '{' + JSON.stringify(whitespace) + '}';
});
}
else {
// If there's a newline in the text, adjust the indent level
if (text.indexOf('\n') > -1) {
text = text.replace(/\n\s*/g, _this._getIndentedNewline());
}
}
_this.output += text;
};
/**
* Handles processing of the specified text node
*
* @param {Text} node
*/
this._visitComment = function (node) {
_this.output += '{/*' + node.textContent.replace('*/', '* /') + '*/}';
};
/**
* Gets a JSX formatted version of the specified attribute from the node
*
* @param {DOMElement} node
* @param {object} attribute
* @return {string}
*/
this._getElementAttribute = function (node, attribute) {
switch (attribute.name) {
case 'style':
return _this._getStyleAttribute(attribute.value);
default:
var tagName = node.tagName.toLowerCase();
var name = (ELEMENT_ATTRIBUTE_MAPPING[tagName] &&
ELEMENT_ATTRIBUTE_MAPPING[tagName][attribute.name]) ||
ATTRIBUTE_MAPPING[attribute.name] ||
attribute.name;
var result = name;
// Numeric values should be output as {123} not "123"
if (isNumeric(attribute.value)) {
result += '={' + attribute.value + '}';
}
else if (attribute.value.length > 0) {
result += '="' + attribute.value.replace('"', '"') + '"';
}
return result;
}
};
/**
* Gets a JSX formatted version of the specified element styles
*
* @param {string} styles
* @return {string}
*/
this._getStyleAttribute = function (styles) {
var jsxStyles = new StyleParser(styles).toJSXString();
return 'style={{' + jsxStyles + '}}';
};
this.config = config || {};
if (this.config.createClass === undefined) {
this.config.createClass = true;
}
if (!this.config.indent) {
this.config.indent = ' ';
}
}
return HTMLtoJSX;
}());
exports.HTMLtoJSX = HTMLtoJSX;
;
/**
* Handles parsing of inline styles
*/
var StyleParser = /** @class */ (function () {
/** @param {string} rawStyle Raw style attribute */
function StyleParser(rawStyle) {
var _this = this;
this.styles = {};
/**
* Parse the specified inline style attribute value
* @param {string} rawStyle Raw style attribute
*/
this.parse = function (rawStyle) {
rawStyle.split(';').forEach(function (style) {
style = style.trim();
var firstColon = style.indexOf(':');
var key = style.substr(0, firstColon);
var value = style.substr(firstColon + 1).trim();
if (key !== '') {
// Style key should be case insensitive
key = key.toLowerCase();
this.styles[key] = value;
}
}, _this);
};
/**
* Convert the style information represented by this parser into a JSX
* string
*
* @return {string}
*/
this.toJSXString = function () {
var output = [];
for (var key in _this.styles) {
if (!_this.styles.hasOwnProperty(key)) {
continue;
}
output.push(_this.toJSXKey(key) + ': ' + _this.toJSXValue(_this.styles[key]));
}
return output.join(', ');
};
/**
* Convert the CSS style key to a JSX style key
*
* @param {string} key CSS style key
* @return {string} JSX style key
*/
this.toJSXKey = function (key) {
// Don't capitalize -ms- prefix
if (/^-ms-/.test(key)) {
key = key.substr(1);
}
return hyphenToCamelCase(key);
};
/**
* Convert the CSS style value to a JSX style value
*
* @param {string} value CSS style value
* @return {string} JSX style value
*/
this.toJSXValue = function (value) {
if (isNumeric(value)) {
// If numeric, no quotes
return value;
}
else if (isConvertiblePixelValue(value)) {
// "500px" -> 500
return trimEnd(value, 'px');
}
else {
// Probably a string, wrap it in quotes
return '\'' + value.replace(/'/g, '"') + '\'';
}
};
this.parse(rawStyle);
}
;
return StyleParser;
}());
exports.StyleParser = StyleParser;