mirror of
https://github.com/bobwen-dev/react-templates
synced 2025-04-12 00:56:39 +02:00
Initial version
This commit is contained in:
parent
a66b57f2a4
commit
e7ef23971d
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
### Operating systems ###
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
|
||||||
|
### Regular dev ###
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
### bower ###
|
||||||
|
/bower_components/*
|
||||||
|
|
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "react-templates",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Light weight templates for react -> write html get valid react code",
|
||||||
|
"main": "reactTemplates.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/wix/react-templates.git"
|
||||||
|
},
|
||||||
|
"author": "Avi Marcus",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wix/react-templates/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wix/react-templates",
|
||||||
|
"dependencies": {
|
||||||
|
"cheerio": "^0.18.0",
|
||||||
|
"escodegen": "^1.4.1",
|
||||||
|
"esprima": "^1.2.2"
|
||||||
|
}
|
||||||
|
}
|
239
reactTemplates.js
Normal file
239
reactTemplates.js
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
/**
|
||||||
|
* Created by avim on 11/9/2014.
|
||||||
|
*/
|
||||||
|
var cheerio = require('cheerio');
|
||||||
|
var fs = require('fs');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var esprima = require('esprima');
|
||||||
|
var escodegen = require('escodegen');
|
||||||
|
var path = require('path');
|
||||||
|
var React = require('react');
|
||||||
|
|
||||||
|
|
||||||
|
var repeatTemplate = _.template("_.map(<%= collection %>,function (<%= item %>,<%= item %>Index) {\n return <%= body %>}, this)");
|
||||||
|
var ifTemplate = _.template("((<%= condition %>)?(<%= body %>):null)");
|
||||||
|
var classSetTemplate = _.template("React.addons.classSet(<%= classSet %>)");
|
||||||
|
var tagTemplate = _.template("<%= name %>.apply(this,_.flatten([<%= props %>].concat([<%= children %>])))");
|
||||||
|
var commentTemplate = _.template(" /* <%= data %> */ ");
|
||||||
|
var templateTemplate = _.template("define([<%= requirePaths %>], function (<%= requireNames %>) {\n <%= injectedFunctions %>\nreturn <%= body %>\n});")
|
||||||
|
|
||||||
|
var templateProp = "rt-repeat";
|
||||||
|
var ifProp = "rt-if";
|
||||||
|
var classSetProp = "rt-class";
|
||||||
|
|
||||||
|
var reactSupportedAttributes = ['accept', 'acceptCharset', 'accessKey', 'action', 'allowFullScreen', 'allowTransparency', 'alt', 'async', 'autoComplete', 'autoPlay', 'cellPadding', 'cellSpacing', 'charSet', 'checked', 'classID', 'className', 'cols', 'colSpan', 'content', 'contentEditable', 'contextMenu', 'controls', 'coords', 'crossOrigin', 'data', 'dateTime', 'defer', 'dir', 'disabled', 'download', 'draggable', 'encType', 'form', 'formNoValidate', 'frameBorder', 'height', 'hidden', 'href', 'hrefLang', 'htmlFor', 'httpEquiv', 'icon', 'id', 'label', 'lang', 'list', 'loop', 'manifest', 'max', 'maxLength', 'media', 'mediaGroup', 'method', 'min', 'multiple', 'muted', 'name', 'noValidate', 'open', 'pattern', 'placeholder', 'poster', 'preload', 'radioGroup', 'readOnly', 'rel', 'required', 'role', 'rows', 'rowSpan', 'sandbox', 'scope', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'span', 'spellCheck', 'src', 'srcDoc', 'srcSet', 'start', 'step', 'style', 'tabIndex', 'target', 'title', 'type', 'useMap', 'value', 'width', 'wmode'];
|
||||||
|
var attributesMapping = {'class':'className','rt-class':'className'};
|
||||||
|
_.forEach(reactSupportedAttributes,function (attributeReactName) {
|
||||||
|
if (attributeReactName !== attributeReactName.toLowerCase()) {
|
||||||
|
attributesMapping[attributeReactName.toLowerCase()] = attributeReactName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function concatChildren(children) {
|
||||||
|
var res = "";
|
||||||
|
var first = true;
|
||||||
|
_.forEach(children, function (child) {
|
||||||
|
if (child.indexOf(" /*") !== 0 && child) {
|
||||||
|
res += (first?"":",") + child;
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
res += child;
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
var curlyMap = {'{':1, '}': -1};
|
||||||
|
|
||||||
|
function convertText(txt) {
|
||||||
|
txt = txt.trim();
|
||||||
|
var res = "";
|
||||||
|
var first = true;
|
||||||
|
while (txt.indexOf('{') !== -1) {
|
||||||
|
var start = txt.indexOf('{');
|
||||||
|
var pre = txt.substr(0,start);
|
||||||
|
if (pre) {
|
||||||
|
res += (first?"":"+") + JSON.stringify(pre);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
var curlyCounter = 1;
|
||||||
|
for (var end = start + 1;end < txt.length && curlyCounter > 0;end++) {
|
||||||
|
curlyCounter += curlyMap[txt.charAt(end)] || 0;
|
||||||
|
}
|
||||||
|
if (curlyCounter !== 0) {
|
||||||
|
throw "Failed to parse text";
|
||||||
|
} else {
|
||||||
|
res += (first?"":"+") + txt.substr(start+1,end-start-2);
|
||||||
|
first = false;
|
||||||
|
txt = txt.substr(end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (txt) {
|
||||||
|
res += (first?"":"+") + JSON.stringify(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStringOnlyCode(txt) {
|
||||||
|
txt = txt.trim();
|
||||||
|
return txt.length && txt.charAt(0) === '{' && txt.charAt(txt.length - 1) === '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateProps(node,context) {
|
||||||
|
var props = {};
|
||||||
|
_.forOwn(node.attribs, function (val,key) {
|
||||||
|
var propKey = attributesMapping[key.toLowerCase()] || key;
|
||||||
|
if (props.hasOwnProperty(propKey)) {
|
||||||
|
throw "duplicate definition of "+propKey+" "+JSON.stringify(node.attribs);
|
||||||
|
}
|
||||||
|
if (key.indexOf("on") === 0 && !isStringOnlyCode(val)) {
|
||||||
|
var funcParts = val.split("=>");
|
||||||
|
var evtParams = funcParts[0].replace("(","").replace(")","").trim();
|
||||||
|
var funcBody = funcParts[1].trim();
|
||||||
|
var generatedFuncName = "generated"+(context.injectedFunctions.length + 1);
|
||||||
|
var params = context.boundParams;
|
||||||
|
if (evtParams.trim().length > 0) {
|
||||||
|
params = params.concat(evtParams.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
var funcText = "function "+generatedFuncName+"("+ params.join(",");
|
||||||
|
funcText += ") {\n"+funcBody+"\n}\n";
|
||||||
|
context.injectedFunctions.push(funcText);
|
||||||
|
props[propKey] = generatedFuncName+".bind("+(["this"].concat(context.boundParams)).join(",")+")";
|
||||||
|
} else if (key === "style" && !isStringOnlyCode(val)) {
|
||||||
|
var styleParts = val.trim().split(";");
|
||||||
|
styleParts = _.compact(_.map(styleParts, function (str) {
|
||||||
|
str = str.trim();
|
||||||
|
if (!str || str.indexOf(':') === -1) {
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
var res = str.split(":");
|
||||||
|
res[0] = res[0].trim();
|
||||||
|
res[1] = res[1].trim();
|
||||||
|
return res;
|
||||||
|
}));
|
||||||
|
var styleArray = [];
|
||||||
|
_.forEach(styleParts, function (stylePart) {
|
||||||
|
styleArray.push(stylePart[0] + " : " + convertText(stylePart[1]))
|
||||||
|
});
|
||||||
|
props[propKey] = "{" + styleArray.join(",") + "}";
|
||||||
|
} else if (key === classSetProp) {
|
||||||
|
props[propKey] = classSetTemplate({classSet:val});
|
||||||
|
} else if (key.indexOf("rt-") !== 0) {
|
||||||
|
props[propKey] = convertText(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return "{"+ _.map(props, function (val,key) {
|
||||||
|
return JSON.stringify(key) + " : "+val;
|
||||||
|
}).join(",")+"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertTagNameToConsstructor(tagName) {
|
||||||
|
return React.DOM.hasOwnProperty(tagName)?"React.DOM."+tagName:tagName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultContext() {
|
||||||
|
return {
|
||||||
|
boundParams: [],
|
||||||
|
injectedFunctions: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function convertHtmlToReact(node,context) {
|
||||||
|
if (node.type === "tag") {
|
||||||
|
context.boundParams = _.clone(context.boundParams);
|
||||||
|
|
||||||
|
var data = {name: convertTagNameToConsstructor(node.name)};
|
||||||
|
if (node.attribs[templateProp]) {
|
||||||
|
data.item = node.attribs[templateProp].split(" in ")[0].trim();
|
||||||
|
data.collection = node.attribs[templateProp].split(" in ")[1].trim();
|
||||||
|
context.boundParams.push(data.item);
|
||||||
|
}
|
||||||
|
data.props = generateProps(node,context);
|
||||||
|
if (node.attribs[ifProp]) {
|
||||||
|
data.condition = node.attribs[ifProp].trim();
|
||||||
|
}
|
||||||
|
data.children = concatChildren(_.map(node.children,function (child) {
|
||||||
|
return convertHtmlToReact(child,context);
|
||||||
|
}));
|
||||||
|
|
||||||
|
data.body = tagTemplate(data);
|
||||||
|
|
||||||
|
if (node.attribs[templateProp]) {
|
||||||
|
data.body = repeatTemplate(data);
|
||||||
|
}
|
||||||
|
if (node.attribs[ifProp]) {
|
||||||
|
data.body = ifTemplate(data);
|
||||||
|
}
|
||||||
|
return data.body;
|
||||||
|
} else if (node.type === "comment") {
|
||||||
|
return (commentTemplate(node));
|
||||||
|
} else if (node.type === "text") {
|
||||||
|
if (node.data.trim()) {
|
||||||
|
return convertText(node.data.trim());
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDefinesFromJSXTag(html,defines) {
|
||||||
|
html = html.replace(/\<\!doctype jsx\s*(.*?)\s*\>/, function(full, reqStr) {
|
||||||
|
var match = true;
|
||||||
|
while (match) {
|
||||||
|
match = false;
|
||||||
|
reqStr = reqStr.replace(/\s*(\w+)\s*\=\s*\"([^\"]*)\"\s*/, function(full, varName, reqPath) {
|
||||||
|
defines[reqPath] = varName;
|
||||||
|
match = true;
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertTemplateToReact(html) {
|
||||||
|
var defines = {"react":"React","lodash":"_"};
|
||||||
|
html = extractDefinesFromJSXTag(html,defines);
|
||||||
|
var rootNode = cheerio.load(html.trim(),{lowerCaseTags:false,lowerCaseAttributeNames:false,xmlMode:true});
|
||||||
|
var context = defaultContext();
|
||||||
|
var body = convertHtmlToReact(rootNode.root()[0].children[0],context);
|
||||||
|
var requirePaths = _(defines).keys().map(function (reqName) {return '"'+reqName+'"'}).value().join(",");
|
||||||
|
var requireVars = _(defines).values().value().join(",");
|
||||||
|
var data = {"body":body,injectedFunctions:"",requireNames:requireVars,requirePaths:requirePaths};
|
||||||
|
data.injectedFunctions = context.injectedFunctions.join("\n");
|
||||||
|
var code = templateTemplate(data);
|
||||||
|
try {
|
||||||
|
var tree = esprima.parse(code, {range: true, tokens: true, comment: true});
|
||||||
|
tree = escodegen.attachComments(tree, tree.comments, tree.tokens);
|
||||||
|
code = escodegen.generate(tree,{comment:true});
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSingleFile(filename) {
|
||||||
|
if (path.extname(filename) !== ".html") {
|
||||||
|
return;// only handle html files
|
||||||
|
}
|
||||||
|
var html = fs.readFileSync(filename).toString();
|
||||||
|
if (!html.match(/\<\!doctype jsx/)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var js = convertTemplateToReact(html);
|
||||||
|
fs.writeFileSync(filename.replace(".html",".js"),js);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv.length > 2) {
|
||||||
|
_.forEach(process.argv.slice(2),handleSingleFile);
|
||||||
|
} else {
|
||||||
|
console.log("Usage:node reactTemplates.js <filename>");
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user