'use strict' const cheerio = require('cheerio') const _ = require('lodash') const esprima = require('esprima') const escodegen = require('escodegen') const reactDOMSupport = require('./reactDOMSupport') const reactNativeSupport = require('./reactNativeSupport') const reactPropTemplates = require('./reactPropTemplates') const rtError = require('./RTCodeError') const reactSupport = require('./reactSupport') const templates = reactSupport.templates const utils = require('./utils') const validateJS = utils.validateJS const RTCodeError = rtError.RTCodeError const repeatTemplate = _.template('_.map(<%= collection %>,<%= repeatFunction %>.bind(<%= repeatBinds %>))') const ifTemplate = _.template('((<%= condition %>)?(<%= body %>):null)') const propsTemplateSimple = _.template('_.assign({}, <%= generatedProps %>, <%= rtProps %>)') const propsTemplate = _.template('mergeProps( <%= generatedProps %>, <%= rtProps %>)') const propsMergeFunction = `function mergeProps(inline,external) { var res = _.assign({},inline,external) if (inline.hasOwnProperty('style')) { res.style = _.defaults(res.style, inline.style); } if (inline.hasOwnProperty('className') && external.hasOwnProperty('className')) { res.className = external.className + ' ' + inline.className; } return res; } ` const classSetTemplate = _.template('_.transform(<%= classSet %>, function(res, value, key){ if(value){ res.push(key); } }, []).join(" ")') function getTagTemplateString(simpleTagTemplate, shouldCreateElement) { if (simpleTagTemplate) { return shouldCreateElement ? 'React.createElement(<%= name %>,<%= props %><%= children %>)' : '<%= name %>(<%= props %><%= children %>)' } return shouldCreateElement ? 'React.createElement.apply(this, [<%= name %>,<%= props %><%= children %>])' : '<%= name %>.apply(this, [<%= props %><%= children %>])' } const commentTemplate = _.template(' /* <%= data %> */ ') const repeatAttr = 'rt-repeat' const ifAttr = 'rt-if' const classSetAttr = 'rt-class' const classAttr = 'class' const scopeAttr = 'rt-scope' const propsAttr = 'rt-props' const templateNode = 'rt-template' const virtualNode = 'rt-virtual' const includeNode = 'rt-include' const includeSrcAttr = 'src' const requireAttr = 'rt-require' const importAttr = 'rt-import' const statelessAttr = 'rt-stateless' const preAttr = 'rt-pre' const reactTemplatesSelfClosingTags = [includeNode] /** * @param {Options} options * @return {Options} */ function getOptions(options) { options = options || {} const defaultOptions = { version: false, force: false, format: 'stylish', targetVersion: reactDOMSupport.default, lodashImportPath: 'lodash', native: false, nativeTargetVersion: reactNativeSupport.default } const finalOptions = _.defaults({}, options, defaultOptions) finalOptions.reactImportPath = reactImport(finalOptions) finalOptions.modules = finalOptions.modules || (finalOptions.native ? 'commonjs' : 'none') const defaultPropTemplates = finalOptions.native ? reactPropTemplates.native[finalOptions.nativeTargetVersion] : reactPropTemplates.dom[finalOptions.targetVersion] finalOptions.propTemplates = _.defaults({}, options.propTemplates, defaultPropTemplates) return finalOptions } function reactImport(options) { if (options.native) { return reactNativeSupport[options.nativeTargetVersion].react.module } if (!options.reactImportPath) { const isNewReact = _.includes(['0.14.0', '0.15.0', '15.0.0', '15.0.1'], options.targetVersion) return isNewReact ? 'react' : 'react/addons' } return options.reactImportPath } /** * @param {Context} context * @param {string} namePrefix * @param {string} body * @param {*?} params * @return {string} */ function generateInjectedFunc(context, namePrefix, body, params) { params = params || context.boundParams const funcName = namePrefix.replace(',', '') + (context.injectedFunctions.length + 1) const funcText = `function ${funcName}(${params.join(',')}) { ${body} } ` context.injectedFunctions.push(funcText) return funcName } function generateTemplateProps(node, context) { let templatePropCount = 0 const propTemplateDefinition = context.options.propTemplates[node.name] const propertiesTemplates = _(node.children) .map((child, index) => { let templateProp = null if (child.name === templateNode) { // Generic explicit template tag if (!_.has(child.attribs, 'prop')) { throw RTCodeError.build(context, child, 'rt-template must have a prop attribute') } if (_.filter(child.children, {type: 'tag'}).length !== 1) { throw RTCodeError.build(context, child, "'rt-template' should have a single non-text element as direct child") } const childTemplate = _.find(context.options.propTemplates, {prop: child.attribs.prop}) || {arguments: []} templateProp = { prop: child.attribs.prop, arguments: (child.attribs.arguments ? child.attribs.arguments.split(',') : childTemplate.arguments) || [] } } else if (propTemplateDefinition && propTemplateDefinition[child.name]) { // Implicit child template from configuration templateProp = { prop: propTemplateDefinition[child.name].prop, arguments: child.attribs.arguments ? child.attribs.arguments.split(',') : propTemplateDefinition[child.name].arguments } } if (templateProp) { _.assign(templateProp, {childIndex: index - templatePropCount++, content: _.find(child.children, {type: 'tag'})}) } return templateProp }) .compact() .value() return _.transform(propertiesTemplates, (props, templateProp) => { const functionParams = _.values(context.boundParams).concat(templateProp.arguments) const oldBoundParams = context.boundParams context.boundParams = context.boundParams.concat(templateProp.arguments) const functionBody = 'return ' + convertHtmlToReact(templateProp.content, context) context.boundParams = oldBoundParams const generatedFuncName = generateInjectedFunc(context, templateProp.prop, functionBody, functionParams) props[templateProp.prop] = genBind(generatedFuncName, _.values(context.boundParams)) // Remove the template child from the children definition. node.children.splice(templateProp.childIndex, 1) }, {}) } /** * @param node * @param {Context} context * @return {string} */ function generateProps(node, context) { const props = {} _.forOwn(node.attribs, (val, key) => { const propKey = reactSupport.attributesMapping[key.toLowerCase()] || key if (props.hasOwnProperty(propKey) && propKey !== reactSupport.classNameProp) { throw RTCodeError.build(context, node, `duplicate definition of ${propKey} ${JSON.stringify(node.attribs)}`) } if (_.startsWith(key, 'on') && !utils.isStringOnlyCode(val)) { props[propKey] = handleEventHandler(val, context, node, key) } else if (key === 'style' && !utils.isStringOnlyCode(val)) { props[propKey] = handleStyleProp(val, node, context) } else if (propKey === reactSupport.classNameProp) { // Processing for both class and rt-class conveniently return strings that // represent JS expressions, each evaluating to a space-separated set of class names. // We can just join them with another space here. const existing = props[propKey] ? `${props[propKey]} + " " + ` : '' if (key === classSetAttr) { props[propKey] = existing + classSetTemplate({classSet: val}) } else if (key === classAttr || key === reactSupport.classNameProp) { props[propKey] = existing + utils.convertText(node, context, val.trim()) } } else if (!_.startsWith(key, 'rt-')) { props[propKey] = utils.convertText(node, context, val.trim()) } }) _.assign(props, generateTemplateProps(node, context)) // map 'className' back into 'class' for custom elements if (props[reactSupport.classNameProp] && isCustomElement(node.name)) { props[classAttr] = props[reactSupport.classNameProp] delete props[reactSupport.classNameProp] } const propStr = _.map(props, (v, k) => `${JSON.stringify(k)} : ${v}`).join(',') return `{${propStr}}` } function handleEventHandler(val, context, node, key) { let handlerString if (_.startsWith(val, 'this.')) { if (context.options.autobind) { handlerString = `${val}.bind(this)` } else { throw RTCodeError.build(context, node, "'this.handler' syntax allowed only when the --autobind is on, use {} to return a callback function.") } } else { const funcParts = val.split('=>') if (funcParts.length !== 2) { throw RTCodeError.build(context, node, `when using 'on' events, use lambda '(p1,p2)=>body' notation or 'this.handler'; otherwise use {} to return a callback function. error: [${key}='${val}']`) } const evtParams = funcParts[0].replace('(', '').replace(')', '').trim() const funcBody = funcParts[1].trim() let params = context.boundParams if (evtParams.trim() !== '') { params = params.concat([evtParams.trim()]) } const generatedFuncName = generateInjectedFunc(context, key, funcBody, params) handlerString = genBind(generatedFuncName, context.boundParams) } return handlerString } function genBind(func, args) { const bindArgs = ['this'].concat(args) return `${func}.bind(${bindArgs.join(',')})` } function handleStyleProp(val, node, context) { const styleStr = _(val) .split(';') .map(_.trim) .filter(i => _.includes(i, ':')) .map(i => { const pair = i.split(':') const key = pair[0].trim() if (/\{|}/g.test(key)) { throw RTCodeError.build(context, node, 'style attribute keys cannot contain { } expressions') } const value = pair.slice(1).join(':').trim() const parsedKey = /(^-moz-)|(^-o-)|(^-webkit-)/ig.test(key) ? _.upperFirst(_.camelCase(key)) : _.camelCase(key) return parsedKey + ' : ' + utils.convertText(node, context, value.trim()) }) .join(',') return `{${styleStr}}` } /** * @param {string} tagName * @param context * @return {string} */ function convertTagNameToConstructor(tagName, context) { if (context.options.native) { const targetSupport = reactNativeSupport[context.options.nativeTargetVersion] return _.includes(targetSupport.components, tagName) ? `${targetSupport.reactNative.name}.${tagName}` : tagName } let isHtmlTag = _.includes(reactDOMSupport[context.options.targetVersion], tagName) || isCustomElement(tagName) if (reactSupport.shouldUseCreateElement(context)) { isHtmlTag = isHtmlTag || tagName.match(/^\w+(-\w+)+$/) return isHtmlTag ? `'${tagName}'` : tagName } return isHtmlTag ? `React.DOM.${tagName}` : tagName } function isCustomElement(tagName) { return tagName.match(/^\w+(-\w+)+$/) } /** * @param {string} html * @param options * @param reportContext * @return {Context} */ function defaultContext(html, options, reportContext) { const defaultDefines = [ {moduleName: options.reactImportPath, alias: 'React', member: '*'}, {moduleName: options.lodashImportPath, alias: '_', member: '*'} ] if (options.native) { const targetSupport = reactNativeSupport[options.nativeTargetVersion] if (targetSupport.reactNative.module !== targetSupport.react.module) { defaultDefines.splice(0, 0, {moduleName: targetSupport.reactNative.module, alias: targetSupport.reactNative.name, member: '*'}) } } return { boundParams: [], injectedFunctions: [], html, options, defines: options.defines ? _.clone(options.defines) : defaultDefines, reportContext } } /** * @param node * @return {boolean} */ function hasNonSimpleChildren(node) { return _.some(node.children, child => child.type === 'tag' && child.attribs[repeatAttr]) } /** * Trims a string the same way as String.prototype.trim(), but preserving all non breaking spaces ('\xA0') * @param {string} text * @return {string} */ function trimHtmlText(text) { return text.replace(/^[ \f\n\r\t\v\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+|[ \f\n\r\t\v\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+$/g, '') } /** * @param node * @param {Context} context * @return {string} */ function convertHtmlToReact(node, context) { if (node.type === 'tag' || node.type === 'style') { context = _.defaults({ boundParams: _.clone(context.boundParams) }, context) if (node.type === 'tag' && node.name === importAttr) { throw RTCodeError.build(context, node, "'rt-import' must be a toplevel node") } if (node.type === 'tag' && node.name === includeNode) { const srcFile = node.attribs[includeSrcAttr] if (!srcFile) { throw RTCodeError.build(context, node, 'rt-include must supply a source attribute') } if (!context.options.readFileSync) { throw RTCodeError.build(context, node, 'rt-include needs a readFileSync polyfill on options') } try { context.html = context.options.readFileSync(srcFile) } catch (e) { console.error(e) throw RTCodeError.build(context, node, `rt-include failed to read file '${srcFile}'`) } return parseAndConvertHtmlToReact(context.html, context) } const data = {name: convertTagNameToConstructor(node.name, context)} // Order matters. We need to add the item and itemIndex to context.boundParams before // the rt-scope directive is processed, lest they are not passed to the child scopes if (node.attribs[repeatAttr]) { const arr = node.attribs[repeatAttr].split(' in ') if (arr.length !== 2) { throw RTCodeError.build(context, node, `rt-repeat invalid 'in' expression '${node.attribs[repeatAttr]}'`) } const repeaterParams = arr[0].split(',').map(s => s.trim()) data.item = repeaterParams[0] data.index = repeaterParams[1] || `${data.item}Index` data.collection = arr[1].trim() const bindParams = [data.item, data.index] _.forEach(bindParams, param => { validateJS(param, node, context) }) validateJS(`(${data.collection})`, node, context) _.forEach(bindParams, param => { if (!_.includes(context.boundParams, param)) { context.boundParams.push(param) } }) } if (node.attribs[scopeAttr]) { handleScopeAttribute(node, context, data) } if (node.attribs[ifAttr]) { validateIfAttribute(node, context, data) data.condition = node.attribs[ifAttr].trim() if (!node.attribs.key && node.name !== virtualNode) { _.set(node, ['attribs', 'key'], `${node.startIndex}`) } } data.props = generateProps(node, context) if (node.attribs[propsAttr]) { if (data.props === '{}') { data.props = node.attribs[propsAttr] } else if (!node.attribs.style && !node.attribs.class) { data.props = propsTemplateSimple({generatedProps: data.props, rtProps: node.attribs[propsAttr]}) } else { data.props = propsTemplate({generatedProps: data.props, rtProps: node.attribs[propsAttr]}) if (!_.includes(context.injectedFunctions, propsMergeFunction)) { context.injectedFunctions.push(propsMergeFunction) } } } if (node.name === virtualNode) { const invalidAttributes = _.without(_.keys(node.attribs), scopeAttr, ifAttr, repeatAttr) if (invalidAttributes.length > 0) { throw RTCodeError.build(context, node, " may not contain attributes other than 'rt-scope', 'rt-if' and 'rt-repeat'") } // provide a key to virtual node children if missing if (node.children.length > 1) { _(node.children) .reject('attribs.key') .forEach((child, i) => { if (child.type === 'tag' && child.name !== virtualNode) { _.set(child, ['attribs', 'key'], `${node.startIndex}${i}`) } }) } } const children = _.map(node.children, child => { const code = convertHtmlToReact(child, context) validateJS(code, child, context) return code }) data.children = utils.concatChildren(children) if (node.name === virtualNode) { //eslint-disable-line wix-editor/prefer-ternary data.body = `[${_.compact(children).join(',')}]` } else { data.body = _.template(getTagTemplateString(!hasNonSimpleChildren(node), reactSupport.shouldUseCreateElement(context)))(data) } if (node.attribs[scopeAttr]) { const functionBody = _.values(data.innerScope.innerMapping).join('\n') + `return ${data.body}` const generatedFuncName = generateInjectedFunc(context, 'scope' + data.innerScope.scopeName, functionBody, _.keys(data.innerScope.outerMapping)) data.body = `${generatedFuncName}.apply(this, [${_.values(data.innerScope.outerMapping).join(',')}])` } // Order matters here. Each rt-repeat iteration wraps over the rt-scope, so // the scope variables are evaluated in context of the current iteration. if (node.attribs[repeatAttr]) { data.repeatFunction = generateInjectedFunc(context, 'repeat' + _.upperFirst(data.item), 'return ' + data.body) data.repeatBinds = ['this'].concat(_.reject(context.boundParams, p => p === data.item || p === data.index || data.innerScope && p in data.innerScope.innerMapping)) data.body = repeatTemplate(data) } if (node.attribs[ifAttr]) { data.body = ifTemplate(data) } return data.body } else if (node.type === 'comment') { const sanitizedComment = node.data.split('*/').join('* /') return commentTemplate({data: sanitizedComment}) } else if (node.type === 'text') { const parentNode = node.parent const overrideNormalize = parentNode !== undefined && (parentNode.name === 'pre' || parentNode.name === 'textarea' || _.has(parentNode.attribs, preAttr)) const normalizeWhitespaces = context.options.normalizeHtmlWhitespace && !overrideNormalize const text = node.data return trimHtmlText(text) ? utils.convertText(node, context, text, normalizeWhitespaces) : '' } } /** * Parses the rt-scope attribute returning an array of parsed sections * * @param {String} scope The scope attribute to parse * @returns {Array} an array of {expression,identifier} * @throws {String} the part of the string that failed to parse */ function parseScopeSyntax(text) { // the regex below was built using the following pseudo-code: // double_quoted_string = `"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"` // single_quoted_string = `'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'` // text_out_of_quotes = `[^"']*?` // expr_parts = double_quoted_string + "|" + single_quoted_string + "|" + text_out_of_quotes // expression = zeroOrMore(nonCapture(expr_parts)) + "?" // id = "[$_a-zA-Z]+[$_a-zA-Z0-9]*" // as = " as" + OneOrMore(" ") // optional_spaces = zeroOrMore(" ") // semicolon = nonCapture(or(text(";"), "$")) // // regex = capture(expression) + as + capture(id) + optional_spaces + semicolon + optional_spaces const regex = RegExp("((?:(?:\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^\"']*?))*?) as(?: )+([$_a-zA-Z]+[$_a-zA-Z0-9]*)(?: )*(?:;|$)(?: )*", 'g') const res = [] do { const idx = regex.lastIndex const match = regex.exec(text) if (regex.lastIndex === idx || match === null) { throw text.substr(idx) } if (match.index === regex.lastIndex) { regex.lastIndex++ } res.push({expression: match[1].trim(), identifier: match[2]}) } while (regex.lastIndex < text.length) return res } function handleScopeAttribute(node, context, data) { data.innerScope = { scopeName: '', innerMapping: {}, outerMapping: {} } data.innerScope.outerMapping = _.zipObject(context.boundParams, context.boundParams) let scopes try { scopes = parseScopeSyntax(node.attribs[scopeAttr]) } catch (scopePart) { throw RTCodeError.build(context, node, `invalid scope part '${scopePart}'`) } scopes.forEach(({expression, identifier}) => { validateJS(identifier, node, context) // this adds both parameters to the list of parameters passed further down // the scope chain, as well as variables that are locally bound before any // function call, as with the ones we generate for rt-scope. if (!_.includes(context.boundParams, identifier)) { context.boundParams.push(identifier) } data.innerScope.scopeName += _.upperFirst(identifier) data.innerScope.innerMapping[identifier] = `var ${identifier} = ${expression};` validateJS(data.innerScope.innerMapping[identifier], node, context) }) } function validateIfAttribute(node, context, data) { const innerMappingKeys = _.keys(data.innerScope && data.innerScope.innerMapping || {}) let ifAttributeTree = null try { ifAttributeTree = esprima.parse(node.attribs[ifAttr]) } catch (e) { throw new RTCodeError(e.message, e.index, -1) } if (ifAttributeTree && ifAttributeTree.body && ifAttributeTree.body.length === 1 && ifAttributeTree.body[0].type === 'ExpressionStatement') { // make sure that rt-if does not use an inner mapping if (ifAttributeTree.body[0].expression && utils.usesScopeName(innerMappingKeys, ifAttributeTree.body[0].expression)) { throw RTCodeError.buildFormat(context, node, "invalid scope mapping used in if part '%s'", node.attribs[ifAttr]) } } else { throw RTCodeError.buildFormat(context, node, "invalid if part '%s'", node.attribs[ifAttr]) } } function handleSelfClosingHtmlTags(nodes) { return _.flatMap(nodes, node => { let externalNodes = [] node.children = handleSelfClosingHtmlTags(node.children) if (node.type === 'tag' && (_.includes(reactSupport.htmlSelfClosingTags, node.name) || _.includes(reactTemplatesSelfClosingTags, node.name))) { externalNodes = _.filter(node.children, {type: 'tag'}) _.forEach(externalNodes, i => {i.parent = node}) node.children = _.reject(node.children, {type: 'tag'}) } return [node].concat(externalNodes) }) } function handleRequire(tag, context) { let moduleName let alias let member if (tag.children.length) { throw RTCodeError.build(context, tag, `'${requireAttr}' may have no children`) } else if (tag.attribs.dependency && tag.attribs.as) { moduleName = tag.attribs.dependency member = '*' alias = tag.attribs.as } if (!moduleName) { throw RTCodeError.build(context, tag, `'${requireAttr}' needs 'dependency' and 'as' attributes`) } context.defines.push({moduleName, member, alias}) } function handleImport(tag, context) { let moduleName let alias let member if (tag.children.length) { throw RTCodeError.build(context, tag, `'${importAttr}' may have no children`) } else if (tag.attribs.name && tag.attribs.from) { moduleName = tag.attribs.from member = tag.attribs.name alias = tag.attribs.as if (!alias) { if (member === '*') { throw RTCodeError.build(context, tag, "'*' imports must have an 'as' attribute") } else if (member === 'default') { throw RTCodeError.build(context, tag, "default imports must have an 'as' attribute") } alias = member } } if (!moduleName) { throw RTCodeError.build(context, tag, `'${importAttr}' needs 'name' and 'from' attributes`) } context.defines.push({moduleName, member, alias}) } function convertTemplateToReact(html, options) { const context = require('./context') return convertRT(html, context, options) } function parseAndConvertHtmlToReact(html, context) { const rootNode = cheerio.load(html, { lowerCaseTags: false, lowerCaseAttributeNames: false, xmlMode: true, withStartIndices: true }) utils.validate(context.options, context, context.reportContext, rootNode.root()[0]) let rootTags = _.filter(rootNode.root()[0].children, {type: 'tag'}) rootTags = handleSelfClosingHtmlTags(rootTags) if (!rootTags || rootTags.length === 0) { throw new RTCodeError('Document should have a root element') } let firstTag = null _.forEach(rootTags, tag => { if (tag.name === requireAttr) { handleRequire(tag, context) } else if (tag.name === importAttr) { handleImport(tag, context) } else if (firstTag === null) { firstTag = tag if (_.hasIn(tag, ['attribs', statelessAttr])) { context.stateless = true } } else { throw RTCodeError.build(context, tag, 'Document should have no more than a single root element') } }) if (firstTag === null) { throw RTCodeError.build(context, rootNode.root()[0], 'Document should have a single root element') } else if (firstTag.name === virtualNode) { throw RTCodeError.build(context, firstTag, `Document should not have <${virtualNode}> as root element`) } else if (_.includes(_.keys(firstTag.attribs), repeatAttr)) { throw RTCodeError.build(context, firstTag, "root element may not have a 'rt-repeat' attribute") } return convertHtmlToReact(firstTag, context) } /** * @param {string} html * @param {CONTEXT} reportContext * @param {Options?} options * @return {string} */ function convertRT(html, reportContext, options) { options = getOptions(options) const context = defaultContext(html, options, reportContext) const body = parseAndConvertHtmlToReact(html, context) const injectedFunctions = context.injectedFunctions.join('\n') const statelessParams = context.stateless ? 'props, context' : '' const renderFunction = `function(${statelessParams}) { ${injectedFunctions}return ${body} }` const requirePaths = _.map(context.defines, d => `"${d.moduleName}"`).join(',') const requireNames = _.map(context.defines, d => `${d.alias}`).join(',') const AMDArguments = _.map(context.defines, (d, i) => d.member === '*' ? `${d.alias}` : `$${i}`).join(',') //eslint-disable-line no-confusing-arrow const AMDSubstitutions = _.map(context.defines, (d, i) => d.member === '*' ? null : `var ${d.alias} = $${i}.${d.member};`).join('\n') //eslint-disable-line no-confusing-arrow const buildImport = reactSupport.buildImport[options.modules] || reactSupport.buildImport.commonjs const requires = _.map(context.defines, buildImport).join('\n') const header = options.flow ? '/* @flow */\n' : '' const vars = header + requires const data = { renderFunction, requireNames, requirePaths, AMDArguments, AMDSubstitutions, vars, name: options.name } let code = templates[options.modules](data) if (options.modules !== 'typescript' && options.modules !== 'jsrt') { code = parseJS(code, options) } return code } function parseJS(code, options) { try { let tree = esprima.parse(code, {range: true, tokens: true, comment: true, sourceType: 'module'}) // fix for https://github.com/wix/react-templates/issues/157 // do not include comments for es6 modules due to bug in dependency "escodegen" // to be removed when https://github.com/estools/escodegen/issues/263 will be fixed // remove also its test case "test/data/comment.rt.es6.js" if (options.modules !== 'es6') { tree = escodegen.attachComments(tree, tree.comments, tree.tokens) } return escodegen.generate(tree, {comment: true}) } catch (e) { throw new RTCodeError(e.message, e.index, -1) } } function convertJSRTToJS(text, reportContext, options) { options = getOptions(options) options.modules = 'jsrt' const templateMatcherJSRT = /