diff --git a/_js/html-jsx-lib.js b/_js/html-jsx-lib.js index 02b36bcf..1ce79ecb 100644 --- a/_js/html-jsx-lib.js +++ b/_js/html-jsx-lib.js @@ -1,482 +1 @@ -/** - * Copyright 2013-2014 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * 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. - */ -;(function(global) { - 'use strict'; - - // 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' - }; - - /** - * 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 specified string consists entirely of numeric characters. - */ - function isNumeric(input) { - return input !== undefined - && input !== null - && (typeof input === 'number' || parseInt(input, 10) == input); - } - - var HTMLtoJSX = function(config) { - this.config = config || {}; - - if (this.config.createClass === undefined) { - this.config.createClass = true; - } - if (!this.config.indent) { - this.config.indent = ' '; - } - if (!this.config.outputClassName) { - this.config.outputClassName = 'NewComponent'; - } - }; - HTMLtoJSX.prototype = { - /** - * Reset the internal state of the converter - */ - reset: function() { - this.output = ''; - this.level = 0; - }, - /** - * 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 - */ - convert: function(html) { - this.reset(); - - // It turns out browsers have good HTML parsers (imagine that). - // Let's take advantage of it. - var containerEl = document.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 - */ - _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(//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} - */ - _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} - */ - _getIndentedNewline: function() { - return '\n' + repeatString(this.config.indent, this.level + 2); - }, - - /** - * Handles processing the specified node - * - * @param {Node} node - */ - _visit: function(node) { - this._beginVisit(node); - this._traverse(node); - this._endVisit(node); - }, - - /** - * Traverses all the children of the specified node - * - * @param {Node} node - */ - _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 - */ - _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 - */ - _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 - */ - _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])); - } - - this.output += '<' + tagName; - if (attributes.length > 0) { - this.output += ' ' + attributes.join(' '); - } - if (node.firstChild) { - this.output += '>'; - } - }, - - /** - * Handles post-visit behaviour for the specified element node - * - * @param {Node} node - */ - _endVisitElement: function(node) { - // De-indent a bit - // TODO: It's inefficient to do it this way :/ - this.output = trimEnd(this.output, this.config.indent); - if (node.firstChild) { - this.output += ''; - } else { - this.output += ' />'; - } - }, - - /** - * Handles processing of the specified text node - * - * @param {TextNode} node - */ - _visitText: function(node) { - var text = node.textContent; - // If there's a newline in the text, adjust the indent level - if (text.indexOf('\n') > -1) { - text = node.textContent.replace(/\n\s*/g, this._getIndentedNewline()); - } - this.output += text; - }, - - /** - * Handles processing of the specified text node - * - * @param {Text} node - */ - _visitComment: function(node) { - // Do not render the comment - // Since we remove comments, we also need to remove the next line break so we - // don't end up with extra whitespace after every comment - //if (node.nextSibling && node.nextSibling.nodeType === NODE_TYPE.TEXT) { - // node.nextSibling.textContent = node.nextSibling.textContent.replace(/\n\s*/, ''); - //} - 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} - */ - _getElementAttribute: function(node, attribute) { - switch (attribute.name) { - case 'style': - return this._getStyleAttribute(attribute.value); - default: - var 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 { - result += '"' + attribute.value.replace('"', '"') + '"'; - } - return result; - } - }, - - /** - * Gets a JSX formatted version of the specified element styles - * - * @param {string} styles - * @return {string} - */ - _getStyleAttribute: function(styles) { - var jsxStyles = new StyleParser(styles).toJSXString(); - return 'style={{' + jsxStyles + '}}'; - } - }; - - /** - * Handles parsing of inline styles - * - * @param {string} rawStyle Raw style attribute - * @constructor - */ - var StyleParser = function(rawStyle) { - this.parse(rawStyle); - }; - StyleParser.prototype = { - /** - * Parse the specified inline style attribute value - * @param {string} rawStyle Raw style attribute - */ - parse: function(rawStyle) { - this.styles = {}; - 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 !== '') { - this.styles[key] = value; - } - }, this); - }, - - /** - * Convert the style information represented by this parser into a JSX - * string - * - * @return {string} - */ - 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 - */ - toJSXKey: function(key) { - return hyphenToCamelCase(key); - }, - - /** - * Convert the CSS style value to a JSX style value - * - * @param {string} value CSS style value - * @return {string} JSX style value - */ - toJSXValue: function(value) { - if (isNumeric(value)) { - // If numeric, no quotes - return value; - } else if (endsWith(value, 'px')) { - // "500px" -> 500 - return trimEnd(value, 'px'); - } else { - // Proably a string, wrap it in quotes - return '\'' + value.replace(/'/g, '"') + '\''; - } - } - }; - - // Expose public API - global.HTMLtoJSX = HTMLtoJSX; -}(window)); \ No newline at end of file +// This file has moved to http://reactjs.github.io/react-magic/htmltojsx.min.js diff --git a/html-jsx.md b/html-jsx.md index b013b545..0d16c5a5 100644 --- a/html-jsx.md +++ b/html-jsx.md @@ -6,6 +6,6 @@ id: html-jsx

HTML to JSX Compiler

- +