[CS2] Support for CSX - equivalent of JSX (#4551)

* CSX implementation

* fixed comment, used toJS, added error tests, fixed error in identifier regex, fixed interpolation inside attributes value and added test

* added missing test for bare attributes, split attribute and indentifier regex, fixed checking for closing tags closing angle bracket

* Refactor tests that compare expected generated JavaScript with actual generated JavaScript to use common helper; add colors to error message to make differences easier to read

* Better match the style of the rest of the codebase

* Remove unused function

* More style fixes

* Allow unspaced less-than operator when not using CSX

* Replace includesCSX with a counter and simplify the unspaced operator logic

* Fixed indexing and realized that I completely enabled the tight spacing, added a test for it too

* Style fixes
This commit is contained in:
Michal Srb
2017-06-07 07:33:46 +01:00
committed by Geoffrey Booth
parent 63b109a4f5
commit dc0fb85fd3
17 changed files with 1712 additions and 609 deletions

View File

@@ -213,57 +213,50 @@ test "#2916: block comment before implicit call with implicit object", ->
a: yes
test "#3132: Format single-line block comment nicely", ->
input = """
### Single-line block comment without additional space here => ###"""
output = """
eqJS """
### Single-line block comment without additional space here => ###""",
"""
/* Single-line block comment without additional space here => */
"""
eq toJS(input), output
test "#3132: Format multi-line block comment nicely", ->
input = """
eqJS """
###
# Multi-line
# block
# comment
###"""
output = """
###""",
"""
/*
* Multi-line
* block
* comment
*/
"""
eq toJS(input), output
test "#3132: Format simple block comment nicely", ->
input = """
eqJS """
###
No
Preceding hash
###"""
output = """
###""",
"""
/*
No
Preceding hash
*/
"""
eq toJS(input), output
test "#3132: Format indented block-comment nicely", ->
input = """
eqJS """
fn = () ->
###
# Indented
Multiline
###
1"""
output = """
1""",
"""
var fn;
fn = function() {
@@ -275,21 +268,19 @@ test "#3132: Format indented block-comment nicely", ->
return 1;
};
"""
eq toJS(input), output
# Although adequately working, block comment-placement is not yet perfect.
# (Considering a case where multiple variables have been declared …)
test "#3132: Format jsdoc-style block-comment nicely", ->
input = """
eqJS """
###*
# Multiline for jsdoc-"@doctags"
#
# @type {Function}
###
fn = () -> 1
""",
"""
output = """
/**
* Multiline for jsdoc-"@doctags"
*
@@ -300,21 +291,19 @@ test "#3132: Format jsdoc-style block-comment nicely", ->
fn = function() {
return 1;
};"""
eq toJS(input), output
# Although adequately working, block comment-placement is not yet perfect.
# (Considering a case where multiple variables have been declared …)
test "#3132: Format hand-made (raw) jsdoc-style block-comment nicely", ->
input = """
eqJS """
###*
* Multiline for jsdoc-"@doctags"
*
* @type {Function}
###
fn = () -> 1
""",
"""
output = """
/**
* Multiline for jsdoc-"@doctags"
*
@@ -325,12 +314,11 @@ test "#3132: Format hand-made (raw) jsdoc-style block-comment nicely", ->
fn = function() {
return 1;
};"""
eq toJS(input), output
# Although adequately working, block comment-placement is not yet perfect.
# (Considering a case where multiple variables have been declared …)
test "#3132: Place block-comments nicely", ->
input = """
eqJS """
###*
# A dummy class definition
#
@@ -350,9 +338,8 @@ test "#3132: Place block-comments nicely", ->
###
@instance = new DummyClass()
""",
"""
output = """
/**
* A dummy class definition
*
@@ -383,22 +370,19 @@ test "#3132: Place block-comments nicely", ->
return DummyClass;
})();"""
eq toJS(input), output
test "#3638: Demand a whitespace after # symbol", ->
input = """
eqJS """
###
#No
#whitespace
###"""
output = """
###""",
"""
/*
#No
#whitespace
*/"""
eq toJS(input), output
test "#3761: Multiline comment at end of an object", ->
anObject =

726
test/csx.coffee Normal file
View File

@@ -0,0 +1,726 @@
# We usually do not check the actual JS output from the compiler, but since
# JSX is not natively supported by Node, we do it in this case.
test 'self closing', ->
eqJS '''
<div />
''', '''
<div />;
'''
test 'self closing formatting', ->
eqJS '''
<div/>
''', '''
<div />;
'''
test 'self closing multiline', ->
eqJS '''
<div
/>
''', '''
<div />;
'''
test 'regex attribute', ->
eqJS '''
<div x={/>asds/} />
''', '''
<div x={/>asds/} />;
'''
test 'string attribute', ->
eqJS '''
<div x="a" />
''', '''
<div x="a" />;
'''
test 'simple attribute', ->
eqJS '''
<div x={42} />
''', '''
<div x={42} />;
'''
test 'assignment attribute', ->
eqJS '''
<div x={y = 42} />
''', '''
var y;
<div x={y = 42} />;
'''
test 'object attribute', ->
eqJS '''
<div x={{y: 42}} />
''', '''
<div x={{
y: 42
}} />;
'''
test 'attribute without value', ->
eqJS '''
<div checked x="hello" />
''', '''
<div checked x="hello" />;
'''
test 'paired', ->
eqJS '''
<div></div>
''', '''
<div></div>;
'''
test 'simple content', ->
eqJS '''
<div>Hello world</div>
''', '''
<div>Hello world</div>;
'''
test 'content interpolation', ->
eqJS '''
<div>Hello {42}</div>
''', '''
<div>Hello {42}</div>;
'''
test 'nested tag', ->
eqJS '''
<div><span /></div>
''', '''
<div><span /></div>;
'''
test 'tag inside interpolation formatting', ->
eqJS '''
<div>Hello {<span />}</div>
''', '''
<div>Hello <span /></div>;
'''
test 'tag inside interpolation, tags are callable', ->
eqJS '''
<div>Hello {<span /> x}</div>
''', '''
<div>Hello {<span />(x)}</div>;
'''
test 'tags inside interpolation, tags trigger implicit calls', ->
eqJS '''
<div>Hello {f <span />}</div>
''', '''
<div>Hello {f(<span />)}</div>;
'''
test 'regex in interpolation', ->
eqJS '''
<div x={/>asds/}><div />{/>asdsad</}</div>
''', '''
<div x={/>asds/}><div />{/>asdsad</}</div>;
'''
test 'interpolation in string attribute value', ->
eqJS '''
<div x="Hello #{world}" />
''', '''
<div x={`Hello ${world}`} />;
'''
# Unlike in `coffee-react-transform`.
test 'bare numbers not allowed', ->
throws -> CoffeeScript.compile '<div x=3 />'
test 'bare expressions not allowed', ->
throws -> CoffeeScript.compile '<div x=y />'
test 'bare complex expressions not allowed', ->
throws -> CoffeeScript.compile '<div x=f(3) />'
test 'unescaped opening tag angle bracket disallowed', ->
throws -> CoffeeScript.compile '<Person><<</Person>'
test 'space around equal sign', ->
eqJS '''
<div popular = "yes" />
''', '''
<div popular="yes" />;
'''
# The following tests were adopted from James Friends
# [https://github.com/jsdf/coffee-react-transform](https://github.com/jsdf/coffee-react-transform).
test 'ambiguous tag-like expression', ->
throws -> CoffeeScript.compile 'x = a <b > c'
test 'ambiguous tag', ->
eqJS '''
a <b > c </b>
''', '''
a(<b> c </b>);
'''
test 'escaped CoffeeScript attribute', ->
eqJS '''
<Person name={if test() then 'yes' else 'no'} />
''', '''
<Person name={test() ? 'yes' : 'no'} />;
'''
test 'escaped CoffeeScript attribute over multiple lines', ->
eqJS '''
<Person name={
if test()
'yes'
else
'no'
} />
''', '''
<Person name={test() ? 'yes' : 'no'} />;
'''
test 'multiple line escaped CoffeeScript with nested CSX', ->
eqJS '''
<Person name={
if test()
'yes'
else
'no'
}>
{
for n in a
<div> a
asf
<li xy={"as"}>{ n+1 }<a /> <a /> </li>
</div>
}
</Person>
''', '''
var n;
<Person name={test() ? 'yes' : 'no'}>
{(function() {
var i, len, results;
results = [];
for (i = 0, len = a.length; i < len; i++) {
n = a[i];
results.push(<div> a
asf
<li xy={"as"}>{n + 1}<a /> <a /> </li>
</div>);
}
return results;
})()}
</Person>;
'''
test 'nested CSX within an attribute, with object attr value', ->
eqJS '''
<Company>
<Person name={<NameComponent attr3={ {'a': {}, b: '{'} } />} />
</Company>
''', '''
<Company>
<Person name={<NameComponent attr3={{
'a': {},
b: '{'
}} />} />
</Company>;
'''
test 'complex nesting', ->
eqJS '''
<div code={someFunc({a:{b:{}, C:'}{}{'}})} />
''', '''
<div code={someFunc({
a: {
b: {},
C: '}{}{'
}
})} />;
'''
test 'multiline tag with nested CSX within an attribute', ->
eqJS '''
<Person
name={
name = formatName(user.name)
<NameComponent name={name.toUppercase()} />
}
>
blah blah blah
</Person>
''', '''
var name;
<Person name={name = formatName(user.name), <NameComponent name={name.toUppercase()} />}>
blah blah blah
</Person>;
'''
test 'escaped CoffeeScript with nested object literals', ->
eqJS '''
<Person>
blah blah blah {
{'a' : {}, 'asd': 'asd'}
}
</Person>
''', '''
<Person>
blah blah blah {{
'a': {},
'asd': 'asd'
}}
</Person>;
'''
test 'multiline tag attributes with escaped CoffeeScript', ->
eqJS '''
<Person name={if isActive() then 'active' else 'inactive'}
someattr='on new line' />
''', '''
<Person name={isActive() ? 'active' : 'inactive'} someattr='on new line' />;
'''
test 'lots of attributes', ->
eqJS '''
<Person eyes={2} friends={getFriends()} popular = "yes"
active={ if isActive() then 'active' else 'inactive' } data-attr='works' checked check={me_out}
/>
''', '''
<Person eyes={2} friends={getFriends()} popular="yes" active={isActive() ? 'active' : 'inactive'} data-attr='works' checked check={me_out} />;
'''
# TODO: fix partially indented CSX
# test 'multiline elements', ->
# eqJS '''
# <div something={
# do ->
# test = /432/gm # this is a regex
# 6 /432/gm # this is division
# }
# >
# <div>
# <div>
# <div>
# <article name={ new Date() } number={203}
# range={getRange()}
# >
# </article>
# </div>
# </div>
# </div>
# </div>
# ''', '''
# bla
# '''
test 'complex regex', ->
eqJS '''
<Person />
/\\/\\/<Person \\/>\\>\\//
''', '''
<Person />;
/\\/\\/<Person \\/>\\>\\//;
'''
test 'heregex', ->
eqJS '''
test = /432/gm # this is a regex
6 /432/gm # this is division
<Tag>
{test = /<Tag>/} this is a regex containing something which looks like a tag
</Tag>
<Person />
REGEX = /// ^
(/ (?! [\s=] ) # comment comment <comment>comment</comment>
[^ [ / \n \\ ]* # comment comment
(?:
<Tag />
(?: \\[\s\S] # comment comment
| \[ # comment comment
[^ \] \n \\ ]*
(?: \\[\s\S] [^ \] \n \\ ]* )*
<Tag>tag</Tag>
]
) [^ [ / \n \\ ]*
)*
/) ([imgy]{0,4}) (?!\w)
///
<Person />
''', '''
var REGEX, test;
test = /432/gm;
6 / 432 / gm;
<Tag>
{(test = /<Tag>/)} this is a regex containing something which looks like a tag
</Tag>;
<Person />;
REGEX = /^(\\/(?![s=])[^[\\/ ]*(?:<Tag\\/>(?:\\[sS]|[[^] ]*(?:\\[sS][^] ]*)*<Tag>tag<\\/Tag>])[^[\\/ ]*)*\\/)([imgy]{0,4})(?!w)/;
<Person />;
'''
test 'comment within CSX is not treated as comment', ->
eqJS '''
<Person>
# i am not a comment
</Person>
''', '''
<Person>
# i am not a comment
</Person>;
'''
test 'comment at start of CSX escape', ->
eqJS '''
<Person>
{# i am a comment
"i am a string"
}
</Person>
''', '''
<Person>
{"i am a string"}
</Person>;
'''
test 'CSX comment cannot be used inside interpolation', ->
throws -> CoffeeScript.compile '''
<Person>
{# i am a comment}
</Person>
'''
test 'comment syntax cannot be used inline', ->
throws -> CoffeeScript.compile '''
<Person>{#comment inline}</Person>
'''
test 'string within CSX is ignored', ->
eqJS '''
<Person> "i am not a string" 'nor am i' </Person>
''', '''
<Person> "i am not a string" 'nor am i' </Person>;
'''
test 'special chars within CSX are ignored', ->
eqJS """
<Person> a,/';][' a\''@$%^&˚¬∑˜˚∆å∂¬˚*()*&^%$>> '"''"'''\'\'m' i </Person>
""", """
<Person> a,/';][' a''@$%^&˚¬∑˜˚∆å∂¬˚*()*&^%$>> '"''"'''''m' i </Person>;
"""
test 'html entities (name, decimal, hex) within CSX', ->
eqJS '''
<Person> &&&&euro; &#8364; &#x20AC;;; </Person>
''', '''
<Person> &&&&euro; &#8364; &#x20AC;;; </Person>;
'''
test 'tag with {{}}', ->
eqJS '''
<Person name={{value: item, key, item}} />
''', '''
<Person name={{
value: item,
key,
item
}} />;
'''
test 'tag with namespace', ->
eqJS '''
<Something.Tag></Something.Tag>
''', '''
<Something.Tag></Something.Tag>;
'''
test 'tag with lowercase namespace', ->
eqJS '''
<something.tag></something.tag>
''', '''
<something.tag></something.tag>;
'''
test 'self closing tag with namespace', ->
eqJS '''
<Something.Tag />
''', '''
<Something.Tag />;
'''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'self closing tag with spread attribute', ->
# eqJS '''
# <Component a={b} {... x } b="c" />
# ''', '''
# React.createElement(Component, Object.assign({"a": (b)}, x , {"b": "c"}))
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'complex spread attribute', ->
# eqJS '''
# <Component {...x} a={b} {... x } b="c" {...$my_xtraCoolVar123 } />
# ''', '''
# React.createElement(Component, Object.assign({}, x, {"a": (b)}, x , {"b": "c"}, $my_xtraCoolVar123 ))
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'multiline spread attribute', ->
# eqJS '''
# <Component {...
# x } a={b} {... x } b="c" {...z }>
# </Component>
# ''', '''
# React.createElement(Component, Object.assign({},
# x , {"a": (b)}, x , {"b": "c"}, z )
# )
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'multiline tag with spread attribute', ->
# eqJS '''
# <Component
# z="1"
# {...x}
# a={b}
# b="c"
# >
# </Component>
# ''', '''
# React.createElement(Component, Object.assign({ \
# "z": "1"
# }, x, { \
# "a": (b), \
# "b": "c"
# })
# )
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'multiline tag with spread attribute first', ->
# eqJS '''
# <Component
# {...
# x}
# z="1"
# a={b}
# b="c"
# >
# </Component>
# ''', '''
# React.createElement(Component, Object.assign({}, \
# x, { \
# "z": "1", \
# "a": (b), \
# "b": "c"
# })
# )
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'complex multiline spread attribute', ->
# eqJS '''
# <Component
# {...
# y} a={b} {... x } b="c" {...z }>
# <div code={someFunc({a:{b:{}, C:'}'}})} />
# </Component>
# ''', '''
# React.createElement(Component, Object.assign({}, \
# y, {"a": (b)}, x , {"b": "c"}, z ),
# React.createElement("div", {"code": (someFunc({a:{b:{}, C:'}'}}))})
# )
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'self closing spread attribute on single line', ->
# eqJS '''
# <Component a="b" c="d" {...@props} />
# ''', '''
# React.createElement(Component, Object.assign({"a": "b", "c": "d"}, @props ))
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'self closing spread attribute on new line', ->
# eqJS '''
# <Component
# a="b"
# c="d"
# {...@props}
# />
# ''', '''
# React.createElement(Component, Object.assign({ \
# "a": "b", \
# "c": "d"
# }, @props
# ))
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'self closing spread attribute on same line', ->
# eqJS '''
# <Component
# a="b"
# c="d"
# {...@props} />
# ''', '''
# React.createElement(Component, Object.assign({ \
# "a": "b", \
# "c": "d"
# }, @props ))
# '''
# TODO: Uncomment the following test once destructured object spreads are supported.
# test 'self closing spread attribute on next line', ->
# eqJS '''
# <Component
# a="b"
# c="d"
# {...@props}
# />
# ''', '''
# React.createElement(Component, Object.assign({ \
# "a": "b", \
# "c": "d"
# }, @props
# ))
# '''
test 'empty strings are not converted to true', ->
eqJS '''
<Component val="" />
''', '''
<Component val="" />;
'''
test 'CoffeeScript @ syntax in tag name', ->
throws -> CoffeeScript.compile '''
<@Component>
<Component />
</@Component>
'''
test 'hyphens in tag names', ->
eqJS '''
<paper-button className="button">{text}</paper-button>
''', '''
<paper-button className="button">{text}</paper-button>;
'''
test 'closing tags must be closed', ->
throws -> CoffeeScript.compile '''
<a></a
'''
# Tests for allowing less than operator without spaces when ther is no CSX
test 'unspaced less than without CSX: identifier', ->
a = 3
div = 5
ok a<div
test 'unspaced less than without CSX: number', ->
div = 5
ok 3<div
test 'unspaced less than without CSX: paren', ->
div = 5
ok (3)<div
test 'unspaced less than without CSX: index', ->
div = 5
a = [3]
ok a[0]<div
test 'tag inside CSX works following: identifier', ->
eqJS '''
<span>a<div /></span>
''', '''
<span>a<div /></span>;
'''
test 'tag inside CSX works following: number', ->
eqJS '''
<span>3<div /></span>
''', '''
<span>3<div /></span>;
'''
test 'tag inside CSX works following: paren', ->
eqJS '''
<span>(3)<div /></span>
''', '''
<span>(3)<div /></span>;
'''
test 'tag inside CSX works following: square bracket', ->
eqJS '''
<span>]<div /></span>
''', '''
<span>]<div /></span>;
'''
test 'unspaced less than inside CSX works but is not encouraged', ->
eqJS '''
a = 3
div = 5
html = <span>{a<div}</span>
''', '''
var a, div, html;
a = 3;
div = 5;
html = <span>{a < div}</span>;
'''
test 'unspaced less than before CSX works but is not encouraged', ->
eqJS '''
div = 5
res = 2<div
html = <span />
''', '''
var div, html, res;
div = 5;
res = 2 < div;
html = <span />;
'''
test 'unspaced less than after CSX works but is not encouraged', ->
eqJS '''
div = 5
html = <span />
res = 2<div
''', '''
var div, html, res;
div = 5;
html = <span />;
res = 2 < div;
'''

View File

@@ -1545,3 +1545,43 @@ test "#4248: Unicode code point escapes", ->
'\\u{a}\\u{1111110000}'
\ ^\^^^^^^^^^^^^^
'''
test "CSX error: non-matching tag names", ->
assertErrorFormat '''
<div><span></div></span>
''',
'''
[stdin]:1:7: error: expected corresponding CSX closing tag for span
<div><span></div></span>
^^^^
'''
test "CSX error: bare expressions not allowed", ->
assertErrorFormat '''
<div x=3 />
''',
'''
[stdin]:1:8: error: expected wrapped or quoted CSX attribute
<div x=3 />
^
'''
test "CSX error: unescaped opening tag angle bracket disallowed", ->
assertErrorFormat '''
<Person><<</Person>
''',
'''
[stdin]:1:9: error: unexpected <<
<Person><<</Person>
^^
'''
test "CSX error: ambiguous tag-like expression", ->
assertErrorFormat '''
x = a <b > c
''',
'''
[stdin]:1:10: error: missing </
x = a <b > c
^
'''

File diff suppressed because it is too large Load Diff

View File

@@ -300,18 +300,16 @@ test "#4248: Unicode code point escapes", ->
ok /a\u{12345}c/.test 'a\ud808\udf45c'
# rewrite code point escapes unless u flag is set
input = """
eqJS """
/\\u{bcdef}\\u{abc}/u
"""
output = """
""",
"""
/\\u{bcdef}\\u{abc}/u;
"""
eq toJS(input), output
input = """
eqJS """
///#{ 'a' }\\u{bcdef}///
"""
output = """
""",
"""
/a\\udab3\\uddef/;
"""
eq toJS(input), output

View File

@@ -415,18 +415,16 @@ test "#4248: Unicode code point escapes", ->
eq '\\u{123456}', "#{'\\'}#{'u{123456}'}"
# don't rewrite code point escapes
input = """
eqJS """
'\\u{bcdef}\\u{abc}'
"""
output = """
""",
"""
'\\u{bcdef}\\u{abc}';
"""
eq toJS(input), output
input = """
eqJS """
"#{ 'a' }\\u{bcdef}"
"""
output = """
""",
"""
"a\\u{bcdef}";
"""
eq toJS(input), output

View File

@@ -1,4 +1,4 @@
# See http://wiki.ecmascript.org/doku.php?id=harmony:egal
# See [http://wiki.ecmascript.org/doku.php?id=harmony:egal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
egal = (a, b) ->
if a is b
a isnt 0 or 1/a is 1/b
@@ -13,9 +13,20 @@ arrayEgal = (a, b) ->
return no for el, idx in a when not arrayEgal el, b[idx]
yes
exports.eq = (a, b, msg) -> ok egal(a, b), msg or "Expected #{a} to equal #{b}"
exports.arrayEq = (a, b, msg) -> ok arrayEgal(a,b), msg or "Expected #{a} to deep equal #{b}"
exports.eq = (a, b, msg) ->
ok egal(a, b), msg or
"Expected #{reset}#{a}#{red} to equal #{reset}#{b}#{red}"
exports.toJS = (str) ->
CoffeeScript.compile str, bare: yes
.replace /^\s+|\s+$/g, '' # Trim leading/trailing whitespace
exports.arrayEq = (a, b, msg) ->
ok arrayEgal(a,b), msg or
"Expected #{reset}#{a}#{red} to deep equal #{reset}#{b}#{red}"
exports.eqJS = (input, expectedOutput, msg) ->
actualOutput = CoffeeScript.compile input, bare: yes
.replace /^\s+|\s+$/g, '' # Trim leading/trailing whitespace.
ok egal(expectedOutput, actualOutput), msg or
"""Expected generated JavaScript to be:
#{reset}#{expectedOutput}#{red}
but instead it was:
#{reset}#{actualOutput}#{red}"""