From 6179e442d9fc95408251298f41cca9556375657c Mon Sep 17 00:00:00 2001 From: Antonino Porcino Date: Sat, 30 Jul 2016 16:08:08 +0200 Subject: [PATCH 01/16] implements #175, better rt-scope syntax parser --- src/reactTemplates.js | 56 +++++++++++++++++++------ test/data/scope-reserved-tokens.rt | 4 ++ test/data/scope-reserved-tokens.rt.html | 2 + test/src/rt-html-valid.spec.js | 1 + 4 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 test/data/scope-reserved-tokens.rt create mode 100644 test/data/scope-reserved-tokens.rt.html diff --git a/src/reactTemplates.js b/src/reactTemplates.js index 55b1c291..d2478b6c 100644 --- a/src/reactTemplates.js +++ b/src/reactTemplates.js @@ -448,6 +448,35 @@ function convertHtmlToReact(node, context) { } } +/** + * 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) { + // in plain english, this regex scans for: + // any character + one or more space + "as" + one or more space + JavaScript identifier + + // zero or more space + semicolon or end of line + zero or more space + // it captures "any character" as the scope expression, "JavaScript identifier" as the identifier + const regex = RegExp('([\\s\\S]*?)(?: )+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: '', @@ -457,25 +486,26 @@ function handleScopeAttribute(node, context, data) { data.innerScope.outerMapping = _.zipObject(context.boundParams, context.boundParams); - _(node.attribs[scopeAttr]).split(';').invokeMap('trim').compact().forEach(scopePart => { - const scopeSubParts = _(scopePart).split(' as ').invokeMap('trim').value(); - if (scopeSubParts.length < 2) { - throw RTCodeError.build(context, node, `invalid scope part '${scopePart}'`); - } - const alias = scopeSubParts[1]; - const value = scopeSubParts[0]; - validateJS(alias, node, context); + 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, alias)) { - context.boundParams.push(alias); + if (!_.includes(context.boundParams, identifier)) { + context.boundParams.push(identifier); } - data.innerScope.scopeName += _.upperFirst(alias); - data.innerScope.innerMapping[alias] = `var ${alias} = ${value};`; - validateJS(data.innerScope.innerMapping[alias], node, context); + data.innerScope.scopeName += _.upperFirst(identifier); + data.innerScope.innerMapping[identifier] = `var ${identifier} = ${expression};`; + validateJS(data.innerScope.innerMapping[identifier], node, context); }); } diff --git a/test/data/scope-reserved-tokens.rt b/test/data/scope-reserved-tokens.rt new file mode 100644 index 00000000..83d0266f --- /dev/null +++ b/test/data/scope-reserved-tokens.rt @@ -0,0 +1,4 @@ +
+ {message}{semicolon}{as} +
+ diff --git a/test/data/scope-reserved-tokens.rt.html b/test/data/scope-reserved-tokens.rt.html new file mode 100644 index 00000000..e984be6e --- /dev/null +++ b/test/data/scope-reserved-tokens.rt.html @@ -0,0 +1,2 @@ +
as fast as possible;as
+ diff --git a/test/src/rt-html-valid.spec.js b/test/src/rt-html-valid.spec.js index 3d75c0cc..b992f30c 100644 --- a/test/src/rt-html-valid.spec.js +++ b/test/src/rt-html-valid.spec.js @@ -31,6 +31,7 @@ module.exports = { 'scope-evaluated-after-repeat2.rt', 'scope-evaluated-after-if.rt', 'scope-obj.rt', + 'scope-reserved-tokens.rt', 'repeat-literal-collection.rt', 'include.rt' ]; From f8296ceda82bd59cdacc42ecf3634b8fc10d593f Mon Sep 17 00:00:00 2001 From: Antonino Porcino Date: Tue, 2 Aug 2016 16:44:17 +0200 Subject: [PATCH 02/16] fixed broken html tests Fixed html tests that were not running due to changed file name. Failing html tests now dump "actual" html instead of the code that generates it (.actual.html) Fixed title for rtStyle tests Removed compareAndWriteHtml() that is no longer needed --- .gitignore | 2 ++ test/src/rt-html-valid.spec.js | 23 ++++++++++------------- test/src/rtStyle.spec.js | 2 +- test/src/test.js | 2 +- test/src/testUtils.js | 22 +--------------------- 5 files changed, 15 insertions(+), 36 deletions(-) diff --git a/.gitignore b/.gitignore index 714ce619..46833c14 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ npm-debug.log ### Test Output ### test/data/**/*.rt.actual.js test/data/**/*.code.js +test/data/**/*.actual.html + diff --git a/test/src/rt-html-valid.spec.js b/test/src/rt-html-valid.spec.js index b992f30c..5c2f814f 100644 --- a/test/src/rt-html-valid.spec.js +++ b/test/src/rt-html-valid.spec.js @@ -1,9 +1,7 @@ 'use strict'; -// const _ = require('lodash'); const reactTemplates = require('../../src/reactTemplates'); const testUtils = require('./testUtils'); const readFileNormalized = testUtils.readFileNormalized; -const compareAndWriteHtml = testUtils.compareAndWriteHtml; const path = require('path'); const fsUtil = require('../../src/fsUtil'); const fs = require('fs'); @@ -34,8 +32,7 @@ module.exports = { 'scope-reserved-tokens.rt', 'repeat-literal-collection.rt', 'include.rt' - ]; - // t.plan(files.length); + ]; files.forEach(testFile => { const filename = path.join(dataPath, testFile); @@ -43,21 +40,21 @@ module.exports = { readFileSync: fsUtil.createRelativeReadFileSync(filename), modules: 'amd' }; - let code = ''; + let actual = ''; + let equal = false; try { const html = fs.readFileSync(filename).toString(); const expected = testUtils.normalizeHtml(readFileNormalized(filename + '.html')); - code = reactTemplates.convertTemplateToReact(html, options).replace(/\r/g, ''); - const actual = testUtils.normalizeHtml(testUtils.codeToHtml(code)); - const equal = compareAndWriteHtml(t, actual, expected, filename); - if (!equal) { - fs.writeFileSync(filename + '.code.js', code); - } + const code = reactTemplates.convertTemplateToReact(html, options).replace(/\r/g, ''); + actual = testUtils.normalizeHtml(testUtils.codeToHtml(code)); + equal = t.equal(actual, expected); } catch (e) { - console.log(testFile, e); - fs.writeFileSync(filename + '.code.js', code); + console.log(testFile, e); t.fail(e); } + if (!equal) { + fs.writeFileSync(filename + '.actual.html', actual); + } }); t.end(); }); diff --git a/test/src/rtStyle.spec.js b/test/src/rtStyle.spec.js index 3235b811..e33f5810 100644 --- a/test/src/rtStyle.spec.js +++ b/test/src/rtStyle.spec.js @@ -3,7 +3,7 @@ const rtStyle = require('../../src/rtStyle'); module.exports = { runTests(test) { - test('html tests', t => { + test('test rtStyle', t => { const text = '.text { background-color: #00346E; padding: 3px; }'; const expected = '{\n "text": {\n "backgroundColor": "#00346E",\n "padding": 3\n }\n}'; const actual = rtStyle.convertBody(text); diff --git a/test/src/test.js b/test/src/test.js index 12cd47b5..7ed3b56a 100644 --- a/test/src/test.js +++ b/test/src/test.js @@ -3,7 +3,7 @@ const test = require('tape'); const path = require('path'); const dataPath = path.resolve(__dirname, '..', 'data'); -const specs = ['rt.invalid', 'rt.valid', 'utils', 'shell', 'rtStyle', 'fsUtil']; +const specs = ['rt.invalid', 'rt.valid', 'rt-html-valid', 'utils', 'shell', 'rtStyle', 'fsUtil']; specs .map(file => require(`./${file}.spec`)) diff --git a/test/src/testUtils.js b/test/src/testUtils.js index 364985c4..212c5dda 100644 --- a/test/src/testUtils.js +++ b/test/src/testUtils.js @@ -34,25 +34,6 @@ function compareAndWrite(t, actual, expected, filename) { return true; } -/** - * @param {*} t - * @param {string} actual - * @param {string} expected - * @param {string} filename - * @return {boolean} whether actual is equal to expected - */ -function compareAndWriteHtml(t, actual, expected, filename) { - const $actual = cheerio.load(actual, {normalizeWhitespace: true}); - const $expected = cheerio.load(expected, {normalizeWhitespace: true}); - compareNodesList(t, $actual.root(), $expected.root(), filename); - // t.equal(actual, expected, filename); - // if (actual !== expected) { - // fs.writeFileSync(filename + '.actual.js', actual); - // return false; - // } - // return true; -} - function compareNodes(t, a, b, filename) { _.forEach(a.attribs, (v, k) => { if (v !== b.attribs[k]) { @@ -117,6 +98,5 @@ module.exports = { readFile, joinDataPath, rtToHtml, - codeToHtml, - compareAndWriteHtml + codeToHtml }; From 883a10b252a36d57e1ee5789d892a2a5837ac71a Mon Sep 17 00:00:00 2001 From: Antonino Porcino Date: Tue, 2 Aug 2016 16:47:13 +0200 Subject: [PATCH 03/16] improved rt-scope syntax parser rt-scope syntax parser now is able to scan over quoted strings (both single and double quotes) which may be also contain escaped quotes (\" or \') --- src/reactTemplates.js | 4 ++-- test/data/scope-reserved-tokens.rt | 4 ++-- test/data/scope-reserved-tokens.rt.html | 2 +- test/src/rt.invalid.spec.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/reactTemplates.js b/src/reactTemplates.js index d2478b6c..634b00fb 100644 --- a/src/reactTemplates.js +++ b/src/reactTemplates.js @@ -460,12 +460,12 @@ function parseScopeSyntax(text) { // any character + one or more space + "as" + one or more space + JavaScript identifier + // zero or more space + semicolon or end of line + zero or more space // it captures "any character" as the scope expression, "JavaScript identifier" as the identifier - const regex = RegExp('([\\s\\S]*?)(?: )+as(?: )+([$_a-zA-Z]+[$_a-zA-Z0-9]*)(?: )*(?:;|$)(?: )*', 'g'); + 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) { + if (regex.lastIndex === idx || match === null || match.index !== idx) { throw text.substr(idx); } if (match.index === regex.lastIndex) { diff --git a/test/data/scope-reserved-tokens.rt b/test/data/scope-reserved-tokens.rt index 83d0266f..7b4dfcb5 100644 --- a/test/data/scope-reserved-tokens.rt +++ b/test/data/scope-reserved-tokens.rt @@ -1,4 +1,4 @@ -
- {message}{semicolon}{as} +
+ {id}{id2}
diff --git a/test/data/scope-reserved-tokens.rt.html b/test/data/scope-reserved-tokens.rt.html index e984be6e..68fd095e 100644 --- a/test/data/scope-reserved-tokens.rt.html +++ b/test/data/scope-reserved-tokens.rt.html @@ -1,2 +1,2 @@ -
as fast as possible;as
+
x as id;y'y as id2
diff --git a/test/src/rt.invalid.spec.js b/test/src/rt.invalid.spec.js index 3c98c0a6..d2f5bfab 100644 --- a/test/src/rt.invalid.spec.js +++ b/test/src/rt.invalid.spec.js @@ -20,7 +20,7 @@ module.exports = { {file: 'invalid-html.rt', issue: new RTCodeError('Document should have a root element', -1, -1, -1, -1)}, {file: 'invalid-exp.rt', issue: new RTCodeError("Failed to parse text '\n {z\n'", 5, 13, 1, 6)}, {file: 'invalid-lambda.rt', issue: new RTCodeError("when using 'on' events, use lambda '(p1,p2)=>body' notation or use {} to return a callback function. error: [onClick='']", 0, 23, 1, 1)}, - {file: 'invalid-js.rt', issue: new RTCodeError('Unexpected token ILLEGAL', 0, 32, 1, 1)}, + {file: 'invalid-js.rt', issue: new RTCodeError("invalid scope part 'x'a as v'", 0, 32, 1, 1)}, {file: 'invalid-single-root.rt', issue: new RTCodeError('Document should have no more than a single root element', 12, 23, 2, 1)}, {file: 'invalid-repeat-1.rt', issue: new RTCodeError("rt-repeat invalid 'in' expression 'a in b in c'", 9, 44, 2, 4)}, {file: 'invalid-repeat-2.rt', issue: new RTCodeError("root element may not have a 'rt-repeat' attribute", 0, 39, 1, 1)}, From 5519dc24b2bace4f017ad733baf5237c636ed5bc Mon Sep 17 00:00:00 2001 From: Antonino Porcino Date: Tue, 2 Aug 2016 17:14:42 +0200 Subject: [PATCH 04/16] updated comment explaining the regex --- src/reactTemplates.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/reactTemplates.js b/src/reactTemplates.js index 634b00fb..6f2c47e0 100644 --- a/src/reactTemplates.js +++ b/src/reactTemplates.js @@ -456,10 +456,19 @@ function convertHtmlToReact(node, context) { * @throws {String} the part of the string that failed to parse */ function parseScopeSyntax(text) { - // in plain english, this regex scans for: - // any character + one or more space + "as" + one or more space + JavaScript identifier + - // zero or more space + semicolon or end of line + zero or more space - // it captures "any character" as the scope expression, "JavaScript identifier" as the identifier + // 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 { From 97ed342efab3e91028c5e355dc0f1f775d5f2b1b Mon Sep 17 00:00:00 2001 From: Antonino Porcino Date: Wed, 3 Aug 2016 10:11:23 +0200 Subject: [PATCH 05/16] do not use _() in rt-class, fixes #179 --- src/reactTemplates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactTemplates.js b/src/reactTemplates.js index 6f2c47e0..e9fd0de2 100644 --- a/src/reactTemplates.js +++ b/src/reactTemplates.js @@ -30,7 +30,7 @@ const propsMergeFunction = `function mergeProps(inline,external) { } `; -const classSetTemplate = _.template('_(<%= classSet %>).transform(function(res, value, key){ if(value){ res.push(key); } }, []).join(" ")'); +const classSetTemplate = _.template('_.transform(<%= classSet %>, function(res, value, key){ if(value){ res.push(key); } }, []).join(" ")'); function getTagTemplateString(simpleTagTemplate, shouldCreateElement) { if (simpleTagTemplate) { From c9912833a4079c854240b936f8ae428ebfc83713 Mon Sep 17 00:00:00 2001 From: Antonino Porcino Date: Fri, 12 Aug 2016 03:54:41 +0200 Subject: [PATCH 06/16] removed some trainling spaces in docs/cli.md --- docs/cli.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 893d2ce4..8e00d86b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -54,7 +54,7 @@ The option enable or disable color in the output. Use output modules. Valid targets are: `amd`, `commonjs`, `none`, `es6`, `typescript`, or `jsrt`. -### `-n`, `--name` +### `-n`, `--name` When using globals, the name for the variable. The default is the [file name]RT, when using amd, the name of the module. @@ -70,7 +70,7 @@ This option allows to override the output file even if there are no changes. Use a specific output format (`stylish` or `json`). -### `-t`, `--target-version` +### `-t`, `--target-version` React version to generate code for (15.0.1, 15.0.0, 0.14.0, 0.13.1, 0.12.2, 0.12.1, 0.12.0, 0.11.2, 0.11.1, 0.11.0, 0.10.0, default). default: 0.14.0 @@ -92,13 +92,13 @@ Dependency path for importing React. ### `--lodash-import-path` -Dependency path for importing lodash. +Dependency path for importing lodash. ### `--native`, `--rn` Renders react native templates. -### `--native-target-version`, `--rnv` +### `--native-target-version`, `--rnv` React native version to generate code for (0.9.0, 0.29.0, default) - either: 0.9.0, 0.29.0, or default - default: 0.9.0 @@ -106,9 +106,9 @@ React native version to generate code for (0.9.0, 0.29.0, default) - either: 0.9 Add /* @flow */ to the top of the generated file -### `--normalize-html-whitespace` +### `--normalize-html-whitespace` -Remove repeating whitespace from HTML text. +Remove repeating whitespace from HTML text. Repeating whitespaces normally are not displayed and thus can be removed in order to reduce the size of the generated JavaScript file. From 93d96d9422b07a17c21b0900aaac234e0a9f8971 Mon Sep 17 00:00:00 2001 From: Antonino Porcino Date: Fri, 12 Aug 2016 04:08:52 +0200 Subject: [PATCH 07/16] Added CLI option '--create-element-alias' --- docs/cli.md | 17 ++++++++++++++++- src/options.js | 7 ++++++- src/reactTemplates.js | 12 +++++++----- test/data/createElement.rt | 1 + test/data/createElement.rt.amd.js | 10 ++++++++++ test/data/createElement.rt.commonjs.js | 6 ++++++ test/src/rt.valid.spec.js | 9 +++++++++ 7 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 test/data/createElement.rt create mode 100644 test/data/createElement.rt.amd.js create mode 100644 test/data/createElement.rt.commonjs.js diff --git a/docs/cli.md b/docs/cli.md index 8e00d86b..34ebd1f4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -40,6 +40,7 @@ Options: --flow Add /* @flow */ to the top of the generated file --native-target-version, --rnv String React native version to generate code for (0.9.0, 0.29.0, default) - either: 0.9.0, 0.29.0, or default - default: 0.9.0 --normalize-html-whitespace Remove repeating whitespace from HTML text. - default: false + --create-element-alias Use an alias name for "React.createElement()" ``` ### `-h`, `--help` @@ -115,4 +116,18 @@ the size of the generated JavaScript file. Whitespace removal is not applied on `
` and `