1 | 'use strict';
|
2 | const cheerio = require('cheerio');
|
3 | const _ = require('lodash');
|
4 | const esprima = require('esprima');
|
5 | const escodegen = require('escodegen');
|
6 | const reactDOMSupport = require('./reactDOMSupport');
|
7 | const reactNativeSupport = require('./reactNativeSupport');
|
8 | const reactPropTemplates = require('./reactPropTemplates');
|
9 | const rtError = require('./RTCodeError');
|
10 | const reactSupport = require('./reactSupport');
|
11 | const templates = reactSupport.templates;
|
12 | const utils = require('./utils');
|
13 | const validateJS = utils.validateJS;
|
14 | const RTCodeError = rtError.RTCodeError;
|
15 |
|
16 | const repeatTemplate = _.template('_.map(<%= collection %>,<%= repeatFunction %>.bind(<%= repeatBinds %>))');
|
17 | const ifTemplate = _.template('((<%= condition %>)?(<%= body %>):null)');
|
18 | const propsTemplateSimple = _.template('_.assign({}, <%= generatedProps %>, <%= rtProps %>)');
|
19 | const propsTemplate = _.template('mergeProps( <%= generatedProps %>, <%= rtProps %>)');
|
20 |
|
21 | const propsMergeFunction = `function mergeProps(inline,external) {
|
22 | var res = _.assign({},inline,external)
|
23 | if (inline.hasOwnProperty('style')) {
|
24 | res.style = _.defaults(res.style, inline.style);
|
25 | }
|
26 | if (inline.hasOwnProperty('className') && external.hasOwnProperty('className')) {
|
27 | res.className = external.className + ' ' + inline.className;
|
28 | }
|
29 | return res;
|
30 | }
|
31 | `;
|
32 |
|
33 | const classSetTemplate = _.template('_(<%= classSet %>).transform(function(res, value, key){ if(value){ res.push(key); } }, []).join(" ")');
|
34 |
|
35 | function getTagTemplateString(simpleTagTemplate, shouldCreateElement) {
|
36 | if (simpleTagTemplate) {
|
37 | return shouldCreateElement ? 'React.createElement(<%= name %>,<%= props %><%= children %>)' : '<%= name %>(<%= props %><%= children %>)';
|
38 | }
|
39 | return shouldCreateElement ? 'React.createElement.apply(this, [<%= name %>,<%= props %><%= children %>])' : '<%= name %>.apply(this, [<%= props %><%= children %>])';
|
40 | }
|
41 |
|
42 |
|
43 | const commentTemplate = _.template(' /* <%= data %> */ ');
|
44 |
|
45 | const repeatAttr = 'rt-repeat';
|
46 | const ifAttr = 'rt-if';
|
47 | const classSetAttr = 'rt-class';
|
48 | const classAttr = 'class';
|
49 | const scopeAttr = 'rt-scope';
|
50 | const propsAttr = 'rt-props';
|
51 | const templateNode = 'rt-template';
|
52 | const virtualNode = 'rt-virtual';
|
53 | const includeNode = 'rt-include';
|
54 | const includeSrcAttr = 'src';
|
55 | const requireAttr = 'rt-require';
|
56 | const importAttr = 'rt-import';
|
57 | const statelessAttr = 'rt-stateless';
|
58 |
|
59 | const reactTemplatesSelfClosingTags = [includeNode];
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | function getOptions(options) {
|
66 | options = options || {};
|
67 | const defaultOptions = {
|
68 | version: false,
|
69 | force: false,
|
70 | format: 'stylish',
|
71 | targetVersion: reactDOMSupport.default,
|
72 | lodashImportPath: 'lodash',
|
73 | native: false,
|
74 | nativeTargetVersion: reactNativeSupport.default
|
75 | };
|
76 |
|
77 | const finalOptions = _.defaults({}, options, defaultOptions);
|
78 | finalOptions.reactImportPath = finalOptions.reactImportPath || reactImport(finalOptions);
|
79 | finalOptions.modules = finalOptions.modules || (finalOptions.native ? 'commonjs' : 'amd');
|
80 |
|
81 | const defaultPropTemplates = finalOptions.native ?
|
82 | reactPropTemplates.native[finalOptions.nativeTargetVersion] :
|
83 | reactPropTemplates.dom[finalOptions.targetVersion];
|
84 |
|
85 | finalOptions.propTemplates = _.defaults({}, options.propTemplates, defaultPropTemplates);
|
86 | return finalOptions;
|
87 | }
|
88 |
|
89 | function reactImport(options) {
|
90 | if (options.native) {
|
91 | return 'react-native';
|
92 | }
|
93 | if (options.targetVersion === '0.14.0' || options.targetVersion === '0.15.0' || options.targetVersion === '15.0.0' || options.targetVersion === '15.0.1') {
|
94 | return 'react';
|
95 | }
|
96 | return 'react/addons';
|
97 | }
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | function generateInjectedFunc(context, namePrefix, body, params) {
|
107 | params = params || context.boundParams;
|
108 | const funcName = namePrefix.replace(',', '') + (context.injectedFunctions.length + 1);
|
109 | const funcText = `function ${funcName}(${params.join(',')}) {
|
110 | ${body}
|
111 | }
|
112 | `;
|
113 | context.injectedFunctions.push(funcText);
|
114 | return funcName;
|
115 | }
|
116 |
|
117 | function generateTemplateProps(node, context) {
|
118 | const propTemplateDefinition = context.options.propTemplates[node.name];
|
119 | const propertiesTemplates = _(node.children)
|
120 | .map((child, index) => {
|
121 | let templateProp = null;
|
122 | if (child.name === templateNode) {
|
123 | if (!_.has(child.attribs, 'prop')) {
|
124 | throw RTCodeError.build(context, child, 'rt-template must have a prop attribute');
|
125 | }
|
126 |
|
127 | const childTemplate = _.find(context.options.propTemplates, {prop: child.attribs.prop}) || {arguments: []};
|
128 | templateProp = {
|
129 | prop: child.attribs.prop,
|
130 | arguments: (child.attribs.arguments ? child.attribs.arguments.split(',') : childTemplate.arguments) || []
|
131 | };
|
132 | } else if (propTemplateDefinition && propTemplateDefinition[child.name]) {
|
133 | templateProp = {
|
134 | prop: propTemplateDefinition[child.name].prop,
|
135 | arguments: child.attribs.arguments ? child.attribs.arguments.split(',') : propTemplateDefinition[child.name].arguments
|
136 | };
|
137 | }
|
138 |
|
139 | if (templateProp) {
|
140 | _.assign(templateProp, {childIndex: index, content: _.find(child.children, {type: 'tag'})});
|
141 | }
|
142 |
|
143 | return templateProp;
|
144 | })
|
145 | .compact()
|
146 | .value();
|
147 |
|
148 | return _.transform(propertiesTemplates, (props, templateProp) => {
|
149 | const functionParams = _.values(context.boundParams).concat(templateProp.arguments);
|
150 |
|
151 | const oldBoundParams = context.boundParams;
|
152 | context.boundParams = context.boundParams.concat(templateProp.arguments);
|
153 |
|
154 | const functionBody = 'return ' + convertHtmlToReact(templateProp.content, context);
|
155 | context.boundParams = oldBoundParams;
|
156 |
|
157 | const generatedFuncName = generateInjectedFunc(context, templateProp.prop, functionBody, functionParams);
|
158 | props[templateProp.prop] = genBind(generatedFuncName, _.values(context.boundParams));
|
159 |
|
160 |
|
161 | node.children.splice(templateProp.childIndex, 1);
|
162 | }, {});
|
163 | }
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 | function generateProps(node, context) {
|
171 | const props = {};
|
172 | _.forOwn(node.attribs, (val, key) => {
|
173 | const propKey = reactSupport.attributesMapping[key.toLowerCase()] || key;
|
174 | if (props.hasOwnProperty(propKey) && propKey !== reactSupport.classNameProp) {
|
175 | throw RTCodeError.build(context, node, `duplicate definition of ${propKey} ${JSON.stringify(node.attribs)}`);
|
176 | }
|
177 | if (_.startsWith(key, 'on') && !utils.isStringOnlyCode(val)) {
|
178 | props[propKey] = handleEventHandler(val, context, node, key);
|
179 | } else if (key === 'style' && !utils.isStringOnlyCode(val)) {
|
180 | props[propKey] = handleStyleProp(val, node, context);
|
181 | } else if (propKey === reactSupport.classNameProp) {
|
182 |
|
183 |
|
184 |
|
185 | const existing = props[propKey] ? `${props[propKey]} + " " + ` : '';
|
186 | if (key === classSetAttr) {
|
187 | props[propKey] = existing + classSetTemplate({classSet: val});
|
188 | } else if (key === classAttr || key === reactSupport.classNameProp) {
|
189 | props[propKey] = existing + utils.convertText(node, context, val.trim());
|
190 | }
|
191 | } else if (!_.startsWith(key, 'rt-')) {
|
192 | props[propKey] = utils.convertText(node, context, val.trim());
|
193 | }
|
194 | });
|
195 | _.assign(props, generateTemplateProps(node, context));
|
196 |
|
197 | const propStr = _.map(props, (v, k) => `${JSON.stringify(k)} : ${v}`).join(',');
|
198 | return `{${propStr}}`;
|
199 | }
|
200 |
|
201 | function handleEventHandler(val, context, node, key) {
|
202 | const funcParts = val.split('=>');
|
203 | if (funcParts.length !== 2) {
|
204 | throw RTCodeError.build(context, node, `when using 'on' events, use lambda '(p1,p2)=>body' notation or use {} to return a callback function. error: [${key}='${val}']`);
|
205 | }
|
206 | const evtParams = funcParts[0].replace('(', '').replace(')', '').trim();
|
207 | const funcBody = funcParts[1].trim();
|
208 | let params = context.boundParams;
|
209 | if (evtParams.trim() !== '') {
|
210 | params = params.concat([evtParams.trim()]);
|
211 | }
|
212 | const generatedFuncName = generateInjectedFunc(context, key, funcBody, params);
|
213 | return genBind(generatedFuncName, context.boundParams);
|
214 | }
|
215 |
|
216 | function genBind(func, args) {
|
217 | const bindArgs = ['this'].concat(args);
|
218 | return `${func}.bind(${bindArgs.join(',')})`;
|
219 | }
|
220 |
|
221 | function handleStyleProp(val, node, context) {
|
222 | const styleStr = _(val)
|
223 | .split(';')
|
224 | .map(_.trim)
|
225 | .filter(i => _.includes(i, ':'))
|
226 | .map(i => {
|
227 | const pair = i.split(':');
|
228 |
|
229 | const value = pair.slice(1).join(':').trim();
|
230 | return _.camelCase(pair[0].trim()) + ' : ' + utils.convertText(node, context, value.trim());
|
231 | })
|
232 | .join(',');
|
233 | return `{${styleStr}}`;
|
234 | }
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | function convertTagNameToConstructor(tagName, context) {
|
242 | if (context.options.native) {
|
243 | return _.includes(reactNativeSupport[context.options.nativeTargetVersion], tagName) ? 'React.' + tagName : tagName;
|
244 | }
|
245 | let isHtmlTag = _.includes(reactDOMSupport[context.options.targetVersion], tagName);
|
246 | if (reactSupport.shouldUseCreateElement(context)) {
|
247 | isHtmlTag = isHtmlTag || tagName.match(/^\w+(-\w+)$/);
|
248 | return isHtmlTag ? `'${tagName}'` : tagName;
|
249 | }
|
250 | return isHtmlTag ? 'React.DOM.' + tagName : tagName;
|
251 | }
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 | function defaultContext(html, options, reportContext) {
|
260 | const defaultDefines = [
|
261 | {moduleName: options.reactImportPath, alias: 'React', member: '*'},
|
262 | {moduleName: options.lodashImportPath, alias: '_', member: '*'}
|
263 | ];
|
264 | return {
|
265 | boundParams: [],
|
266 | injectedFunctions: [],
|
267 | html,
|
268 | options,
|
269 | defines: options.defines ? _.clone(options.defines) : defaultDefines,
|
270 | reportContext
|
271 | };
|
272 | }
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 | function hasNonSimpleChildren(node) {
|
279 | return _.some(node.children, child => child.type === 'tag' && child.attribs[repeatAttr]);
|
280 | }
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 | function convertHtmlToReact(node, context) {
|
288 | if (node.type === 'tag' || node.type === 'style') {
|
289 | context = _.defaults({
|
290 | boundParams: _.clone(context.boundParams)
|
291 | }, context);
|
292 |
|
293 | if (node.type === 'tag' && node.name === includeNode) {
|
294 | const srcFile = node.attribs[includeSrcAttr];
|
295 | if (!srcFile) {
|
296 | throw RTCodeError.build(context, node, 'rt-include must supply a source attribute');
|
297 | }
|
298 | if (!context.options.readFileSync) {
|
299 | throw RTCodeError.build(context, node, 'rt-include needs a readFileSync polyfill on options');
|
300 | }
|
301 | try {
|
302 | context.html = context.options.readFileSync(srcFile);
|
303 | } catch (e) {
|
304 | console.error(e);
|
305 | throw RTCodeError.build(context, node, `rt-include failed to read file '${srcFile}'`);
|
306 | }
|
307 | return parseAndConvertHtmlToReact(context.html, context);
|
308 | }
|
309 |
|
310 | const data = {name: convertTagNameToConstructor(node.name, context)};
|
311 |
|
312 |
|
313 |
|
314 | if (node.attribs[repeatAttr]) {
|
315 | const arr = node.attribs[repeatAttr].split(' in ');
|
316 | if (arr.length !== 2) {
|
317 | throw RTCodeError.build(context, node, `rt-repeat invalid 'in' expression '${node.attribs[repeatAttr]}'`);
|
318 | }
|
319 | const repeaterParams = arr[0].split(',').map(s => s.trim());
|
320 | data.item = repeaterParams[0];
|
321 | data.index = repeaterParams[1] || `${data.item}Index`;
|
322 | data.collection = arr[1].trim();
|
323 | const bindParams = [data.item, data.index];
|
324 | _.forEach(bindParams, param => {
|
325 | validateJS(param, node, context);
|
326 | });
|
327 | validateJS(`(${data.collection})`, node, context);
|
328 | _.forEach(bindParams, param => {
|
329 | if (!_.includes(context.boundParams, param)) {
|
330 | context.boundParams.push(param);
|
331 | }
|
332 | });
|
333 | }
|
334 |
|
335 | if (node.attribs[scopeAttr]) {
|
336 | handleScopeAttribute(node, context, data);
|
337 | }
|
338 |
|
339 | if (node.attribs[ifAttr]) {
|
340 | validateIfAttribute(node, context, data);
|
341 | data.condition = node.attribs[ifAttr].trim();
|
342 | if (!node.attribs.key) {
|
343 | _.set(node, ['attribs', 'key'], `${node.startIndex}`);
|
344 | }
|
345 | }
|
346 |
|
347 | data.props = generateProps(node, context);
|
348 | if (node.attribs[propsAttr]) {
|
349 | if (data.props === '{}') {
|
350 | data.props = node.attribs[propsAttr];
|
351 | } else if (!node.attribs.style && !node.attribs.class) {
|
352 | data.props = propsTemplateSimple({generatedProps: data.props, rtProps: node.attribs[propsAttr]});
|
353 | } else {
|
354 | data.props = propsTemplate({generatedProps: data.props, rtProps: node.attribs[propsAttr]});
|
355 | if (!_.includes(context.injectedFunctions, propsMergeFunction)) {
|
356 | context.injectedFunctions.push(propsMergeFunction);
|
357 | }
|
358 | }
|
359 | }
|
360 |
|
361 |
|
362 | if (node.name === virtualNode && node.children.length > 1) {
|
363 | _(node.children)
|
364 | .reject('attribs.key')
|
365 | .forEach((child, i) => {
|
366 | _.set(child, ['attribs', 'key'], `${node.startIndex}${i}`);
|
367 | });
|
368 | }
|
369 |
|
370 | const children = _.map(node.children, child => {
|
371 | const code = convertHtmlToReact(child, context);
|
372 | validateJS(code, child, context);
|
373 | return code;
|
374 | });
|
375 |
|
376 | data.children = utils.concatChildren(children);
|
377 |
|
378 | if (node.name === virtualNode) {
|
379 | data.body = `[${_.compact(children).join(',')}]`;
|
380 | } else {
|
381 | data.body = _.template(getTagTemplateString(!hasNonSimpleChildren(node), reactSupport.shouldUseCreateElement(context)))(data);
|
382 | }
|
383 |
|
384 | if (node.attribs[scopeAttr]) {
|
385 | const functionBody = _.values(data.innerScope.innerMapping).join('\n') + `return ${data.body}`;
|
386 | const generatedFuncName = generateInjectedFunc(context, 'scope' + data.innerScope.scopeName, functionBody, _.keys(data.innerScope.outerMapping));
|
387 | data.body = `${generatedFuncName}.apply(this, [${_.values(data.innerScope.outerMapping).join(',')}])`;
|
388 | }
|
389 |
|
390 |
|
391 |
|
392 | if (node.attribs[repeatAttr]) {
|
393 | data.repeatFunction = generateInjectedFunc(context, 'repeat' + _.upperFirst(data.item), 'return ' + data.body);
|
394 | data.repeatBinds = ['this'].concat(_.reject(context.boundParams, p => p === data.item || p === data.item + 'Index' || data.innerScope && p in data.innerScope.innerMapping));
|
395 | data.body = repeatTemplate(data);
|
396 | }
|
397 | if (node.attribs[ifAttr]) {
|
398 | data.body = ifTemplate(data);
|
399 | }
|
400 | return data.body;
|
401 | } else if (node.type === 'comment') {
|
402 | return commentTemplate(node);
|
403 | } else if (node.type === 'text') {
|
404 | return node.data.trim() ? utils.convertText(node, context, node.data) : '';
|
405 | }
|
406 | }
|
407 |
|
408 | function handleScopeAttribute(node, context, data) {
|
409 | data.innerScope = {
|
410 | scopeName: '',
|
411 | innerMapping: {},
|
412 | outerMapping: {}
|
413 | };
|
414 |
|
415 | data.innerScope.outerMapping = _.zipObject(context.boundParams, context.boundParams);
|
416 |
|
417 | _(node.attribs[scopeAttr]).split(';').invokeMap('trim').compact().forEach(scopePart => {
|
418 | const scopeSubParts = _(scopePart).split(' as ').invokeMap('trim').value();
|
419 | if (scopeSubParts.length < 2) {
|
420 | throw RTCodeError.build(context, node, `invalid scope part '${scopePart}'`);
|
421 | }
|
422 | const alias = scopeSubParts[1];
|
423 | const value = scopeSubParts[0];
|
424 | validateJS(alias, node, context);
|
425 |
|
426 |
|
427 |
|
428 |
|
429 | if (!_.includes(context.boundParams, alias)) {
|
430 | context.boundParams.push(alias);
|
431 | }
|
432 |
|
433 | data.innerScope.scopeName += _.upperFirst(alias);
|
434 | data.innerScope.innerMapping[alias] = `var ${alias} = ${value};`;
|
435 | validateJS(data.innerScope.innerMapping[alias], node, context);
|
436 | });
|
437 | }
|
438 |
|
439 | function validateIfAttribute(node, context, data) {
|
440 | const innerMappingKeys = _.keys(data.innerScope && data.innerScope.innerMapping || {});
|
441 | let ifAttributeTree = null;
|
442 | try {
|
443 | ifAttributeTree = esprima.parse(node.attribs[ifAttr]);
|
444 | } catch (e) {
|
445 | throw new RTCodeError(e.message, e.index, -1);
|
446 | }
|
447 | if (ifAttributeTree && ifAttributeTree.body && ifAttributeTree.body.length === 1 && ifAttributeTree.body[0].type === 'ExpressionStatement') {
|
448 |
|
449 | if (ifAttributeTree.body[0].expression && utils.usesScopeName(innerMappingKeys, ifAttributeTree.body[0].expression)) {
|
450 | throw RTCodeError.buildFormat(context, node, "invalid scope mapping used in if part '%s'", node.attribs[ifAttr]);
|
451 | }
|
452 | } else {
|
453 | throw RTCodeError.buildFormat(context, node, "invalid if part '%s'", node.attribs[ifAttr]);
|
454 | }
|
455 | }
|
456 |
|
457 | function handleSelfClosingHtmlTags(nodes) {
|
458 | return _.flatMap(nodes, node => {
|
459 | let externalNodes = [];
|
460 | node.children = handleSelfClosingHtmlTags(node.children);
|
461 | if (node.type === 'tag' && (_.includes(reactSupport.htmlSelfClosingTags, node.name) ||
|
462 | _.includes(reactTemplatesSelfClosingTags, node.name))) {
|
463 | externalNodes = _.filter(node.children, {type: 'tag'});
|
464 | _.forEach(externalNodes, i => {i.parent = node;});
|
465 | node.children = _.reject(node.children, {type: 'tag'});
|
466 | }
|
467 | return [node].concat(externalNodes);
|
468 | });
|
469 | }
|
470 |
|
471 | function handleRequire(tag, context) {
|
472 | let moduleName;
|
473 | let alias;
|
474 | let member;
|
475 | if (tag.children.length) {
|
476 | throw RTCodeError.build(context, tag, `'${requireAttr}' may have no children`);
|
477 | } else if (tag.attribs.dependency && tag.attribs.as) {
|
478 | moduleName = tag.attribs.dependency;
|
479 | member = '*';
|
480 | alias = tag.attribs.as;
|
481 | }
|
482 | if (!moduleName) {
|
483 | throw RTCodeError.build(context, tag, `'${requireAttr}' needs 'dependency' and 'as' attributes`);
|
484 | }
|
485 | context.defines.push({moduleName, member, alias});
|
486 | }
|
487 |
|
488 | function handleImport(tag, context) {
|
489 | let moduleName;
|
490 | let alias;
|
491 | let member;
|
492 | if (tag.children.length) {
|
493 | throw RTCodeError.build(context, tag, `'${importAttr}' may have no children`);
|
494 | } else if (tag.attribs.name && tag.attribs.from) {
|
495 | moduleName = tag.attribs.from;
|
496 | member = tag.attribs.name;
|
497 | alias = tag.attribs.as;
|
498 | if (!alias) {
|
499 | if (member === '*') {
|
500 | throw RTCodeError.build(context, tag, "'*' imports must have an 'as' attribute");
|
501 | } else if (member === 'default') {
|
502 | throw RTCodeError.build(context, tag, "default imports must have an 'as' attribute");
|
503 | }
|
504 | alias = member;
|
505 | }
|
506 | }
|
507 | if (!moduleName) {
|
508 | throw RTCodeError.build(context, tag, `'${importAttr}' needs 'name' and 'from' attributes`);
|
509 | }
|
510 | context.defines.push({moduleName, member, alias});
|
511 | }
|
512 |
|
513 | function convertTemplateToReact(html, options) {
|
514 | const context = require('./context');
|
515 | return convertRT(html, context, options);
|
516 | }
|
517 |
|
518 | function parseAndConvertHtmlToReact(html, context) {
|
519 | const rootNode = cheerio.load(html, {
|
520 | lowerCaseTags: false,
|
521 | lowerCaseAttributeNames: false,
|
522 | xmlMode: true,
|
523 | withStartIndices: true
|
524 | });
|
525 | utils.validate(context.options, context, context.reportContext, rootNode.root()[0]);
|
526 | let rootTags = _.filter(rootNode.root()[0].children, {type: 'tag'});
|
527 | rootTags = handleSelfClosingHtmlTags(rootTags);
|
528 | if (!rootTags || rootTags.length === 0) {
|
529 | throw new RTCodeError('Document should have a root element');
|
530 | }
|
531 | let firstTag = null;
|
532 | _.forEach(rootTags, tag => {
|
533 | if (tag.name === requireAttr) {
|
534 | handleRequire(tag, context);
|
535 | } else if (tag.name === importAttr) {
|
536 | handleImport(tag, context);
|
537 | } else if (firstTag === null) {
|
538 | firstTag = tag;
|
539 | if (_.hasIn(tag, ['attribs', statelessAttr])) {
|
540 | context.stateless = true;
|
541 | }
|
542 | } else {
|
543 | throw RTCodeError.build(context, tag, 'Document should have no more than a single root element');
|
544 | }
|
545 | });
|
546 | if (firstTag === null) {
|
547 | throw RTCodeError.build(context, rootNode.root()[0], 'Document should have a single root element');
|
548 | } else if (firstTag.name === virtualNode) {
|
549 | throw RTCodeError.build(context, firstTag, `Document should not have <${virtualNode}> as root element`);
|
550 | }
|
551 | return convertHtmlToReact(firstTag, context);
|
552 | }
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 | function convertRT(html, reportContext, options) {
|
561 | options = getOptions(options);
|
562 |
|
563 | const context = defaultContext(html, options, reportContext);
|
564 | const body = parseAndConvertHtmlToReact(html, context);
|
565 |
|
566 | const requirePaths = _.map(context.defines, d => `"${d.moduleName}"`).join(',');
|
567 | const requireNames = _.map(context.defines, d => `${d.alias}`).join(',');
|
568 | const buildImport = reactSupport.buildImport[options.modules] || reactSupport.buildImport.commonjs;
|
569 | const requires = _.map(context.defines, buildImport).join('\n');
|
570 | const header = options.flow ? '/* @flow */\n' : '';
|
571 | const vars = header + requires;
|
572 | const data = {
|
573 | body,
|
574 | injectedFunctions: context.injectedFunctions.join('\n'),
|
575 | requireNames,
|
576 | requirePaths,
|
577 | vars,
|
578 | name: options.name,
|
579 | statelessProps: context.stateless ? 'props' : ''
|
580 | };
|
581 | let code = templates[options.modules](data);
|
582 | if (options.modules !== 'typescript' && options.modules !== 'jsrt') {
|
583 | code = parseJS(code);
|
584 | }
|
585 | return code;
|
586 | }
|
587 |
|
588 | function parseJS(code) {
|
589 | try {
|
590 | let tree = esprima.parse(code, {range: true, tokens: true, comment: true, sourceType: 'module'});
|
591 | tree = escodegen.attachComments(tree, tree.comments, tree.tokens);
|
592 | return escodegen.generate(tree, {comment: true});
|
593 | } catch (e) {
|
594 | throw new RTCodeError(e.message, e.index, -1);
|
595 | }
|
596 | }
|
597 |
|
598 | function convertJSRTToJS(text, reportContext, options) {
|
599 | options = getOptions(options);
|
600 | options.modules = 'jsrt';
|
601 | const templateMatcherJSRT = /<template>([^]*?)<\/template>/gm;
|
602 | const code = text.replace(templateMatcherJSRT, (template, html) => convertRT(html, reportContext, options).replace(/;$/, ''));
|
603 |
|
604 | return parseJS(code);
|
605 | }
|
606 |
|
607 | module.exports = {
|
608 | convertTemplateToReact,
|
609 | convertRT,
|
610 | convertJSRTToJS,
|
611 | RTCodeError,
|
612 | normalizeName: utils.normalizeName
|
613 | };
|