Skip to content

Commit

Permalink
v1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Aleksandr Strikha committed Aug 22, 2016
0 parents commit 7d02e36
Show file tree
Hide file tree
Showing 10 changed files with 657 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
presets: ["es2015"]
}

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
.idea
node_modules

52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Active HTML for ReactJS
Convert HTML string to React Components

## The problem
The most of CMS provide content as pure html from WYSIWYG editors:
```json
{
"content": "<a href='/hello'>Hello World</a><img src='image.png' class='main-image' alt='' /><p>Lorem ipsum...</p>"
}
```
In this case you lose advantage of using React components in content.

## Solution
```jsx
import activeHtml from 'active-html';

class Html extends Component {

render() {

const components = {
// replace <img> tags by custom react component
img: (attributes) => {
return (<Image {...attributes} />);
},
// replace <a> tags by React Router Link component
a: (attributes) => {
return (<Link to={attributes.href} {...attributes} />);
},
// add lazy load to all iframes
iframe: (attributes) => {
return (
<LazyLoad>
<iframe {...attributes} />
</LazyLoad>
);
}
};

// convert string property "content" to React components
let nodes = activeHtml(this.props.content, components);

return (<div className="html">{nodes}</div>);
}

```
## Installation
#### Frontend
npm install active-html --save-dev
#### Backend (NodeJS)
npm install active-html xmldom --save
150 changes: 150 additions & 0 deletions lib/HtmlSerializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var HtmlSerializer = function () {
function HtmlSerializer() {
_classCallCheck(this, HtmlSerializer);

// if NodeJS
if (typeof DOMParser === 'undefined') {
var DOMParser = require('xmldom').DOMParser;
if (DOMParser) {
var options = {
errorHandler: {
warning: function warning(w) {}
}
};
this.parser = new DOMParser(options);
}
}
// if browser
else {
this.parser = new DOMParser();
}

this._removeEmptyStrings = false;
this._attributesAdapter = null;
}

_createClass(HtmlSerializer, [{
key: 'removeEmptyStrings',
value: function removeEmptyStrings() {
var remove = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0];

this._removeEmptyStrings = remove;
}
}, {
key: 'setAttributesAdapter',
value: function setAttributesAdapter(adapter) {
this._attributesAdapter = adapter;
}
}, {
key: 'parseInlineCss',
value: function parseInlineCss(css) {
var urls = [];
var tmpCss = css.replace(/url(\s+)?\(.*\)/i, function (match) {
var key = urls.length;
urls.push(match);
return '%%' + key + '%%';
});

var arr = tmpCss.split(';').filter(function (item) {
return item.indexOf(':') !== -1;
});

var obj = {};
for (var i = 0; i < arr.length; i++) {
var item = arr[i].split(':');
var key = item[0].trim();
var value = item[1].trim();

if (/%%\d+%%/.test(value)) {
value = value.replace(/%%\d+%%/, function (m) {
return urls[parseInt(m.replace(/\%/g, ''))];
});
}
obj[key] = value;
}

return obj;
}
}, {
key: 'parseHtml',
value: function parseHtml(html) {
if (typeof html !== 'string' || html == '') {
return {};
}

var doc = this.parser.parseFromString(html, "text/html");

if (!doc.childNodes) {
return {};
}

return this._parseNodes(doc.childNodes);
}
}, {
key: '_parseNodes',
value: function _parseNodes(nodes) {
var i = void 0,
k = void 0,
node = void 0;
var objects = [];

for (i = 0; i < nodes.length; i++) {
node = nodes[i];

var obj = {};

if (typeof node.tagName === 'undefined' && typeof node.nodeValue === 'string') {
obj.node = 'text';
obj.text = node.nodeValue;

if (this._removeEmptyStrings && obj.text.trim().length == 0) {
continue;
}
} else {
obj.node = node.tagName;
}

if (_typeof(node.attributes) === 'object' && node.attributes && node.attributes.length > 0) {
var attributes = {};
for (k = 0; k < node.attributes.length; k++) {
var attribute = node.attributes[k];
attributes[attribute.name.toLowerCase()] = attribute.value;
}

if (typeof attributes.style !== 'undefined') {
attributes.style = this.parseInlineCss(attributes.style);
}

if (typeof this._attributesAdapter === 'function') {
attributes = this._attributesAdapter(attributes);
}

obj.attributes = attributes;
}

if (_typeof(node.childNodes) === 'object' && node.childNodes && node.childNodes.length > 0) {
obj.children = this._parseNodes(node.childNodes);
}
objects.push(obj);
}

return objects;
}
}]);

return HtmlSerializer;
}();

exports.default = HtmlSerializer;
73 changes: 73 additions & 0 deletions lib/adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

exports.default = function (attributes) {
var newAttributes = {};

for (var key in attributes) {
if (attributes.hasOwnProperty(key)) {
var value = attributes[key];

if (key == 'style' && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') {
var camelCaseCss = {};
for (var cssAttr in value) {
if (value.hasOwnProperty(cssAttr)) {
camelCaseCss[(0, _toCamelCase2.default)(cssAttr)] = value[cssAttr];
}
}
newAttributes[key] = camelCaseCss;
} else if (typeof keyMap[key] !== 'undefined') {
newAttributes[keyMap[key]] = value;
} else {
newAttributes[key] = value;
}
}
}

return newAttributes;
};

var _HTMLDOMPropertyConfig = require('react/lib/HTMLDOMPropertyConfig');

var _HTMLDOMPropertyConfig2 = _interopRequireDefault(_HTMLDOMPropertyConfig);

var _SVGDOMPropertyConfig = require('react/lib/SVGDOMPropertyConfig');

var _SVGDOMPropertyConfig2 = _interopRequireDefault(_SVGDOMPropertyConfig);

var _toCamelCase = require('to-camel-case');

var _toCamelCase2 = _interopRequireDefault(_toCamelCase);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var keyMap = {};

for (var validAttr in _HTMLDOMPropertyConfig2.default.Properties) {
if (_HTMLDOMPropertyConfig2.default.Properties.hasOwnProperty(validAttr)) {

var lowerAttr = validAttr.toLowerCase();
if (lowerAttr !== validAttr) {
keyMap[lowerAttr] = validAttr;
}
}
}

for (var reactAttr in _HTMLDOMPropertyConfig2.default.DOMAttributeNames) {
if (_HTMLDOMPropertyConfig2.default.DOMAttributeNames.hasOwnProperty(reactAttr)) {
var attr = _HTMLDOMPropertyConfig2.default.DOMAttributeNames[reactAttr];
keyMap[attr.toLowerCase()] = reactAttr;
}
}

for (var _reactAttr in _SVGDOMPropertyConfig2.default.DOMAttributeNames) {
if (_SVGDOMPropertyConfig2.default.DOMAttributeNames.hasOwnProperty(_reactAttr)) {
var _attr = _SVGDOMPropertyConfig2.default.DOMAttributeNames[_reactAttr];
keyMap[_attr.toLowerCase()] = _reactAttr;
}
}
93 changes: 93 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';

Object.defineProperty(exports, "__esModule", {
value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };

exports.default = function (htmlSting) {
var componentsMap = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];

if (typeof htmlSting !== 'string') {
if (debug) {
console && console.error("Html should be string, " + (typeof htmlSting === 'undefined' ? 'undefined' : _typeof(htmlSting)) + " was given.");
return null;
}
}

var removeEmptyStrings = options && options.removeEmptyStrings === false ? false : true;
var serializer = getSerializer();
serializer.removeEmptyStrings(removeEmptyStrings);

var tree = serializer.parseHtml(htmlSting);

return htmlTreeToComponents(tree, componentsMap);
};

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _HtmlSerializer = require('./HtmlSerializer');

var _HtmlSerializer2 = _interopRequireDefault(_HtmlSerializer);

var _adapter = require('./adapter');

var _adapter2 = _interopRequireDefault(_adapter);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var debug = process.env.NODE_ENV !== 'production';

var _serializer = null;

function getSerializer() {
if (_serializer === null) {
_serializer = new _HtmlSerializer2.default();
_serializer.setAttributesAdapter(_adapter2.default);
}
return _serializer;
}

function htmlTreeToComponents(tree, componentsMap) {
var index = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2];


return tree.map(function (item) {

if (item.node === 'text') {
return item.text;
}

var componentProps = {
key: index
};

if (typeof item.attributes !== 'undefined') {
componentProps = Object.assign(componentProps, item.attributes);
}

if (typeof item.children !== 'undefined') {
componentProps = Object.assign(componentProps, {
children: htmlTreeToComponents(item.children, componentsMap, ++index)
});
}

if (typeof componentsMap[item.node] === 'undefined') {
return _react2.default.createElement(item.node, componentProps);
}

if (typeof componentsMap[item.node] === 'function') {
return componentsMap[item.node](componentProps);
}

if (debug) {
console && console.warn("Component mapping should return a function, " + _typeof(componentsMap[item.node]) + " was given.");
}

return componentsMap[item.node];
});
}
Loading

0 comments on commit 7d02e36

Please sign in to comment.