/* @flow */
import {load} from 'cheerio';
import {createElement} from 'react';
import reject from 'lodash/reject';
import map from 'lodash/map';
import transform from 'lodash/transform';
import has from 'lodash/has';
import flattenDeep from 'lodash/flattenDeep';
import isString from 'lodash/isString';
import trim from 'lodash/trim';
import replace from 'lodash/replace';
import toUpper from 'lodash/toUpper';
import toLower from 'lodash/toLower';
import split from 'lodash/split';
/**
* Useful utilities for working with JSX.
*
* @module jsx
*/
/**
* Parses an HTML string.
*
* @memberof module:jsx
* @private
* @method parseHTML
* @param {string} html - an arbitrary HTML string
* @return {Object} an object containing a CheerioDOM object and a CheerioCollection of the `pre.CodeMirror-line`
* elements
*/
export function parseHTML(html: string): Object {
const dom = load(html),
{children} = dom.root().toArray()[0];
return {dom, children};
}
/**
* Convert the CSS style key to a JSX style key.
*
* @memberof module:jsx
* @private
* @method toJSXKey
* @param {string} key - CSS style key
* @return {string} JSX style key
*/
export function toJSXKey(key: string): string {
return replace(/^-ms-/.test(key) ? key.substr(1) : key, /-(.)/g, (match, chr) => toUpper(chr));
}
/**
* Parse the specified inline style attribute value.
*
* @memberof module:jsx
* @private
* @method transformStyle
* @param {Object} object - the object to perform replacements on
*/
export function transformStyle(object: Object) {
if (has(object, 'style')) {
object.style = transform(split(object.style, ';'), (result, style) => {
const firstColon = style.indexOf(':'),
key = trim(style.substr(0, firstColon));
if (key) {
result[toJSXKey(toLower(key))] = trim(style.substr(firstColon + 1));
}
}, {});
}
}
/**
* Renames specified attributes if present.
*
* @memberof module:jsx
* @private
* @method rename
* @param {Object} object - the object to perform replacements on
* @param {string} fromKey - a key to look for
* @param {string} toKey - a key to rename to
*/
export function rename(object: Object, fromKey: string, toKey: string) {
if (has(object, fromKey)) {
object[toKey] = object[fromKey];
delete object[fromKey];
}
}
/**
* Converts a Cheerio Element into an object that can later be used to create a React Element.
*
* @memberof module:jsx
* @private
* @method transformElement
* @param {Object} element - a Cheerio Element
* @return {Object} a plain object describing a React Element
*/
export function transformElement({name: type, attribs: props, children: childElements}: Object): Object {
transformStyle(props);
rename(props, 'for', 'htmlFor');
rename(props, 'class', 'className');
if ('input' === type) {
rename(props, 'checked', 'defaultChecked');
rename(props, 'value', 'defaultValue');
}
let children = transformElements(childElements);
if ('textarea' === type && children.length) {
props.defaultValue = children[0];
children = [];
}
return {type, props, children};
}
/**
* Converts an array of Cheerio Elements to an array of plain objects describing React Elements for easy
* serialization/unserialization.
*
* @memberof module:jsx
* @private
* @method transformElements
* @param {Array<Object>} [elements=[]] - Cheerio Elements
* @return {Array<string|Object>} an array of plain objects describing React Elements
*/
export function transformElements(elements: Object[] = []): Array<string | Object> {
return map(reject(elements, ['type', 'comment']), el => 'text' === el.type ? el.data : transformElement(el));
}
/**
* Recursively flattens `args`, removes falsy values and combines string values.
*
* Can be used as a simple optimization step on the JSX children-to-be to simplify the resulting DOM structure by
* joining adjacent text nodes together.
*
* @memberof module:jsx
* @method flatten
* @param {...*} args - the input values
* @return {Array<*>} the flattened result
* @example
* import {flatten} from 'webcompiler';
* // or - import {flatten} from 'webcompiler/lib/jsx';
* // or - var flatten = require('webcompiler').flatten;
* // or - var flatten = require('webcompiler/lib/jsx').flatten;
*
* flatten('lorem ', ['ipsum ', null, ['dolor ', ['sit ', ['amet']]]]); // ["lorem ipsum dolor sit amet"]
*/
export function flatten(...args: any[]): any[] {
return transform(flattenDeep(args), (accumulator, value) => {
if (!value) {
return;
}
const lastIndex = accumulator.length - 1;
if (isString(value) && isString(accumulator[lastIndex])) {
accumulator[lastIndex] += value;
} else {
accumulator.push(value);
}
}, []);
}
/**
* Converts an array of plain objects describing React Elements to an array of React Elements.
*
* @memberof module:jsx
* @method arrayToJSX
* @param {Array<string|Object>} [arr=[]] - an array of plain objects describing React Elements
* @return {Array<ReactElement>} an array of React Elements
* @example
* import {arrayToJSX} from 'webcompiler';
* // or - import {arrayToJSX} from 'webcompiler/lib/jsx';
* // or - var arrayToJSX = require('webcompiler').arrayToJSX;
* // or - var arrayToJSX = require('webcompiler/lib/jsx').arrayToJSX;
*
* <div>{arrayToJSX([{type: 'h1', children: ['Hello world!']}])}</div>
*/
export function arrayToJSX(arr: Array<string | Object> = []): any[] {
return map(arr, (el, key: number) => {
if (isString(el)) {
return el;
}
const {type, props, children} = el;
return createElement(type, {...props, key}, ...arrayToJSX(children));
});
}
/**
* Converts an arbitrary HTML string to an array of plain objects describing React Elements for easy
* serialization/unserialization.
*
* @memberof module:jsx
* @method htmlToArray
* @param {string} [html=""] - an arbitrary HTML string
* @return {Array<string|Object>} an array of plain objects describing React Elements
* @example
* import {htmlToArray} from 'webcompiler';
* // or - import {htmlToArray} from 'webcompiler/lib/jsx';
* // or - var htmlToArray = require('webcompiler').htmlToArray;
* // or - var htmlToArray = require('webcompiler/lib/jsx').htmlToArray;
*
* htmlToArray('<h1>Hello world!</h1>'); // [{type: 'h1', children: ['Hello world!']}]
*/
export function htmlToArray(html: string = ''): Array<string | Object> {
html = trim(html);
return html ? transformElements(parseHTML(html).children) : [];
}
/**
* Converts an arbitrary HTML string to an array of React Elements.
*
* @memberof module:jsx
* @method htmlToJSX
* @param {string} [html=""] - an arbitrary HTML string
* @return {Array<ReactElement>} an array of React Elements
* @example
* import {htmlToJSX} from 'webcompiler';
* // or - import {htmlToJSX} from 'webcompiler/lib/jsx';
* // or - var htmlToJSX = require('webcompiler').htmlToJSX;
* // or - var htmlToJSX = require('webcompiler/lib/jsx').htmlToJSX;
*
* <div>{htmlToJSX('<h1>Hello world!</h1>')}</div>
*/
export function htmlToJSX(html: string = ''): any[] {
return arrayToJSX(htmlToArray(html));
}