1
0
Fork 0
mirror of https://github.com/jashkenas/coffeescript.git synced 2022-11-09 12:23:24 -05:00

[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

@ -378,6 +378,10 @@ runTests = (CoffeeScript) ->
# Convenience aliases. # Convenience aliases.
global.CoffeeScript = CoffeeScript global.CoffeeScript = CoffeeScript
global.Repl = require './lib/coffeescript/repl' global.Repl = require './lib/coffeescript/repl'
global.bold = bold
global.red = red
global.green = green
global.reset = reset
# Our test helper function for delimiting different test cases. # Our test helper function for delimiting different test cases.
global.test = (description, fn) -> global.test = (description, fn) ->

View file

@ -68,6 +68,8 @@
Identifier: [ Identifier: [
o('IDENTIFIER', function() { o('IDENTIFIER', function() {
return new IdentifierLiteral($1); return new IdentifierLiteral($1);
}), o('CSX_TAG', function() {
return new CSXTag($1);
}) })
], ],
Property: [ Property: [

View file

@ -1,6 +1,6 @@
// Generated by CoffeeScript 2.0.0-beta2 // Generated by CoffeeScript 2.0.0-beta2
(function() { (function() {
var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARE, COMPOUND_ASSIGN, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError, var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_IDENTIFIER, CSX_INTERPOLATION, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError,
indexOf = [].indexOf; indexOf = [].indexOf;
({Rewriter, INVERSES} = require('./rewriter')); ({Rewriter, INVERSES} = require('./rewriter'));
@ -24,12 +24,13 @@
this.seenExport = false; this.seenExport = false;
this.importSpecifierList = false; this.importSpecifierList = false;
this.exportSpecifierList = false; this.exportSpecifierList = false;
this.csxDepth = 0;
this.chunkLine = opts.line || 0; this.chunkLine = opts.line || 0;
this.chunkColumn = opts.column || 0; this.chunkColumn = opts.column || 0;
code = this.clean(code); code = this.clean(code);
i = 0; i = 0;
while (this.chunk = code.slice(i)) { while (this.chunk = code.slice(i)) {
consumed = this.identifierToken() || this.commentToken() || this.whitespaceToken() || this.lineToken() || this.stringToken() || this.numberToken() || this.regexToken() || this.jsToken() || this.literalToken(); consumed = this.identifierToken() || this.commentToken() || this.whitespaceToken() || this.lineToken() || this.stringToken() || this.numberToken() || this.csxToken() || this.regexToken() || this.jsToken() || this.literalToken();
[this.chunkLine, this.chunkColumn] = this.getLineAndColumnFromChunk(consumed); [this.chunkLine, this.chunkColumn] = this.getLineAndColumnFromChunk(consumed);
i += consumed; i += consumed;
if (opts.untilBalanced && this.ends.length === 0) { if (opts.untilBalanced && this.ends.length === 0) {
@ -65,8 +66,10 @@
} }
identifierToken() { identifierToken() {
var alias, colon, colonOffset, id, idLength, input, match, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, tag, tagToken; var alias, colon, colonOffset, colonToken, id, idLength, inCSXTag, input, match, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, regex, tag, tagToken;
if (!(match = IDENTIFIER.exec(this.chunk))) { inCSXTag = this.atCSXTag();
regex = inCSXTag ? CSX_ATTRIBUTE : IDENTIFIER;
if (!(match = regex.exec(this.chunk))) {
return 0; return 0;
} }
[input, id, colon] = match; [input, id, colon] = match;
@ -180,8 +183,14 @@
[tagToken[2].first_line, tagToken[2].first_column] = [poppedToken[2].first_line, poppedToken[2].first_column]; [tagToken[2].first_line, tagToken[2].first_column] = [poppedToken[2].first_line, poppedToken[2].first_column];
} }
if (colon) { if (colon) {
colonOffset = input.lastIndexOf(':'); colonOffset = input.lastIndexOf(inCSXTag ? '=' : ':');
this.token(':', ':', colonOffset, colon.length); colonToken = this.token(':', ':', colonOffset, colon.length);
if (inCSXTag) {
colonToken.csxColon = true;
}
}
if (inCSXTag && tag === 'IDENTIFIER' && prev[0] !== ':') {
this.token(',', ',', 0, 0, tagToken);
} }
return input.length; return input.length;
} }
@ -313,6 +322,9 @@
return value; return value;
}); });
} }
if (this.atCSXTag()) {
this.token(',', ',', 0, 0, this.prev);
}
return end; return end;
} }
@ -564,6 +576,98 @@
return this; return this;
} }
csxToken() {
var afterTag, colon, csxTag, end, firstChar, id, input, match, origin, prev, ref, token, tokens;
firstChar = this.chunk[0];
if (firstChar === '<') {
match = CSX_IDENTIFIER.exec(this.chunk.slice(1));
if (!(match && (this.csxDepth > 0 || !(prev = this.prev()) || prev.spaced || (ref = prev[0], indexOf.call(COMPARABLE_LEFT_SIDE, ref) < 0)))) {
return 0;
}
[input, id, colon] = match;
origin = this.token('CSX_TAG', id, 1, id.length);
this.token('CALL_START', '(');
this.token('{', '{');
this.ends.push({
tag: '/>',
origin: origin,
name: id
});
this.csxDepth++;
return id.length + 1;
} else if (csxTag = this.atCSXTag()) {
if (this.chunk.slice(0, 2) === '/>') {
this.pair('/>');
this.token('}', '}', 0, 2);
this.token('CALL_END', ')', 0, 2);
this.csxDepth--;
return 2;
} else if (firstChar === '{') {
token = this.token('(', '(');
this.ends.push({
tag: '}',
origin: token
});
return 1;
} else if (firstChar === '>') {
this.pair('/>');
origin = this.token('}', '}');
this.token(',', ',');
({
tokens,
index: end
} = this.matchWithInterpolations(INSIDE_CSX, '>', '</', CSX_INTERPOLATION));
this.mergeInterpolationTokens(tokens, {
delimiter: '"'
}, (value, i) => {
return this.formatString(value, {
delimiter: '>'
});
});
match = CSX_IDENTIFIER.exec(this.chunk.slice(end));
if (!match || match[0] !== csxTag.name) {
this.error(`expected corresponding CSX closing tag for ${csxTag.name}`, csxTag.origin[2]);
}
afterTag = end + csxTag.name.length;
if (this.chunk[afterTag] !== '>') {
this.error("missing closing > after tag name", {
offset: afterTag,
length: 1
});
}
this.token('CALL_END', ')', end, csxTag.name.length + 1);
this.csxDepth--;
return afterTag + 1;
} else {
return 0;
}
} else if (this.atCSXTag(1)) {
if (firstChar === '}') {
this.pair(firstChar);
this.token(')', ')');
this.token(',', ',');
return 1;
} else {
return 0;
}
} else {
return 0;
}
}
atCSXTag(depth = 0) {
var i, last, ref;
if (this.csxDepth === 0) {
return false;
}
i = this.ends.length - 1;
while (((ref = this.ends[i]) != null ? ref.tag : void 0) === 'OUTDENT' || depth-- > 0) {
i--;
}
last = this.ends[i];
return (last != null ? last.tag : void 0) === '/>' && last;
}
literalToken() { literalToken() {
var match, message, origin, prev, ref, ref1, ref2, ref3, skipToken, tag, token, value; var match, message, origin, prev, ref, ref1, ref2, ref3, skipToken, tag, token, value;
if (match = OPERATOR.exec(this.chunk)) { if (match = OPERATOR.exec(this.chunk)) {
@ -652,7 +756,7 @@
case ']': case ']':
this.pair(value); this.pair(value);
} }
this.tokens.push(token); this.tokens.push(this.makeToken(tag, value));
return value.length; return value.length;
} }
@ -691,8 +795,14 @@
return this.outdentToken(this.indent); return this.outdentToken(this.indent);
} }
matchWithInterpolations(regex, delimiter) { matchWithInterpolations(regex, delimiter, closingDelimiter, interpolators) {
var close, column, firstToken, index, lastToken, line, nested, offsetInChunk, open, ref, str, strPart, tokens; var braceInterpolator, close, column, firstToken, index, interpolationOffset, interpolator, lastToken, line, match, nested, offsetInChunk, open, ref, rest, str, strPart, tokens;
if (closingDelimiter == null) {
closingDelimiter = delimiter;
}
if (interpolators == null) {
interpolators = /^#\{/;
}
tokens = []; tokens = [];
offsetInChunk = delimiter.length; offsetInChunk = delimiter.length;
if (this.chunk.slice(0, offsetInChunk) !== delimiter) { if (this.chunk.slice(0, offsetInChunk) !== delimiter) {
@ -708,32 +818,43 @@
tokens.push(this.makeToken('NEOSTRING', strPart, offsetInChunk)); tokens.push(this.makeToken('NEOSTRING', strPart, offsetInChunk));
str = str.slice(strPart.length); str = str.slice(strPart.length);
offsetInChunk += strPart.length; offsetInChunk += strPart.length;
if (str.slice(0, 2) !== '#{') { if (!(match = interpolators.exec(str))) {
break; break;
} }
[line, column] = this.getLineAndColumnFromChunk(offsetInChunk + 1); [interpolator] = match;
interpolationOffset = interpolator.length - 1;
[line, column] = this.getLineAndColumnFromChunk(offsetInChunk + interpolationOffset);
rest = str.slice(interpolationOffset);
({ ({
tokens: nested, tokens: nested,
index index
} = new Lexer().tokenize(str.slice(1), { } = new Lexer().tokenize(rest, {
line: line, line: line,
column: column, column: column,
untilBalanced: true untilBalanced: true
})); }));
index += 1; index += interpolationOffset;
open = nested[0], close = nested[nested.length - 1]; braceInterpolator = str[index - 1] === '}';
open[0] = open[1] = '('; if (braceInterpolator) {
close[0] = close[1] = ')'; open = nested[0], close = nested[nested.length - 1];
close.origin = ['', 'end of interpolation', close[2]]; open[0] = open[1] = '(';
close[0] = close[1] = ')';
close.origin = ['', 'end of interpolation', close[2]];
}
if (((ref = nested[1]) != null ? ref[0] : void 0) === 'TERMINATOR') { if (((ref = nested[1]) != null ? ref[0] : void 0) === 'TERMINATOR') {
nested.splice(1, 1); nested.splice(1, 1);
} }
if (!braceInterpolator) {
open = this.makeToken('(', '(', offsetInChunk, 0);
close = this.makeToken(')', ')', offsetInChunk + index, 0);
nested = [open, ...nested, close];
}
tokens.push(['TOKENS', nested]); tokens.push(['TOKENS', nested]);
str = str.slice(index); str = str.slice(index);
offsetInChunk += index; offsetInChunk += index;
} }
if (str.slice(0, delimiter.length) !== delimiter) { if (str.slice(0, closingDelimiter.length) !== closingDelimiter) {
this.error(`missing ${delimiter}`, { this.error(`missing ${closingDelimiter}`, {
length: delimiter.length length: delimiter.length
}); });
} }
@ -741,16 +862,16 @@
firstToken[2].first_column -= delimiter.length; firstToken[2].first_column -= delimiter.length;
if (lastToken[1].substr(-1) === '\n') { if (lastToken[1].substr(-1) === '\n') {
lastToken[2].last_line += 1; lastToken[2].last_line += 1;
lastToken[2].last_column = delimiter.length - 1; lastToken[2].last_column = closingDelimiter.length - 1;
} else { } else {
lastToken[2].last_column += delimiter.length; lastToken[2].last_column += closingDelimiter.length;
} }
if (lastToken[1].length === 0) { if (lastToken[1].length === 0) {
lastToken[2].last_column -= 1; lastToken[2].last_column -= 1;
} }
return { return {
tokens, tokens,
index: offsetInChunk + delimiter.length index: offsetInChunk + closingDelimiter.length
}; };
} }
@ -1080,6 +1201,10 @@
IDENTIFIER = /^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+)([^\n\S]*:(?!:))?/; IDENTIFIER = /^(?!\d)((?:(?!\s)[$\w\x7f-\uffff])+)([^\n\S]*:(?!:))?/;
CSX_IDENTIFIER = /^(?![\d<])((?:(?!\s)[\.\-$\w\x7f-\uffff])+)/;
CSX_ATTRIBUTE = /^(?!\d)((?:(?!\s)[\-$\w\x7f-\uffff])+)([^\S]*=(?!=))?/;
NUMBER = /^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i; NUMBER = /^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i;
OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/; OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/;
@ -1106,6 +1231,10 @@
HEREDOC_DOUBLE = /^(?:[^\\"#]|\\[\s\S]|"(?!"")|\#(?!\{))*/; HEREDOC_DOUBLE = /^(?:[^\\"#]|\\[\s\S]|"(?!"")|\#(?!\{))*/;
INSIDE_CSX = /^(?:[^\{<])*/;
CSX_INTERPOLATION = /^(?:\{|<(?!\/))/;
STRING_OMIT = /((?:\\\\)+)|\\[^\S\n]*\n\s*/g; STRING_OMIT = /((?:\\\\)+)|\\[^\S\n]*\n\s*/g;
SIMPLE_STRING_OMIT = /\s*\n\s*/g; SIMPLE_STRING_OMIT = /\s*\n\s*/g;
@ -1162,6 +1291,8 @@
INDEXABLE = CALLABLE.concat(['NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_END', 'REGEX', 'REGEX_END', 'BOOL', 'NULL', 'UNDEFINED', '}', '::']); INDEXABLE = CALLABLE.concat(['NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_END', 'REGEX', 'REGEX_END', 'BOOL', 'NULL', 'UNDEFINED', '}', '::']);
COMPARABLE_LEFT_SIDE = ['IDENTIFIER', ')', ']', 'NUMBER'];
NOT_REGEX = INDEXABLE.concat(['++', '--']); NOT_REGEX = INDEXABLE.concat(['++', '--']);
LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR']; LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR'];

View file

@ -1,6 +1,6 @@
// Generated by CoffeeScript 2.0.0-beta2 // Generated by CoffeeScript 2.0.0-beta2
(function() { (function() {
var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, Call, Class, Code, CodeFragment, Comment, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, JS_FORBIDDEN, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, TAB, THIS, TaggedTemplateCall, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isLiteralArguments, isLiteralThis, isUnassignable, locationDataToString, merge, multident, shouldCacheOrIsAssignable, some, starts, throwSyntaxError, unfoldSoak, utility, var Access, Arr, Assign, AwaitReturn, Base, Block, BooleanLiteral, CSXTag, Call, Class, Code, CodeFragment, Comment, ExecutableClassBody, Existence, Expansion, ExportAllDeclaration, ExportDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, ExportSpecifier, ExportSpecifierList, Extends, For, HoistTarget, IdentifierLiteral, If, ImportClause, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, ImportSpecifierList, In, Index, InfinityLiteral, JS_FORBIDDEN, LEVEL_ACCESS, LEVEL_COND, LEVEL_LIST, LEVEL_OP, LEVEL_PAREN, LEVEL_TOP, Literal, ModuleDeclaration, ModuleSpecifier, ModuleSpecifierList, NEGATE, NO, NaNLiteral, NullLiteral, NumberLiteral, Obj, Op, Param, Parens, PassthroughLiteral, PropertyName, Range, RegexLiteral, RegexWithInterpolations, Return, SIMPLENUM, Scope, Slice, Splat, StatementLiteral, StringLiteral, StringWithInterpolations, Super, SuperCall, Switch, TAB, THIS, TaggedTemplateCall, ThisLiteral, Throw, Try, UTILITIES, UndefinedLiteral, Value, While, YES, YieldReturn, addLocationDataFn, compact, del, ends, extend, flatten, fragmentsToText, isLiteralArguments, isLiteralThis, isUnassignable, locationDataToString, merge, multident, shouldCacheOrIsAssignable, some, starts, throwSyntaxError, unfoldSoak, utility,
splice = [].splice, splice = [].splice,
indexOf = [].indexOf, indexOf = [].indexOf,
slice = [].slice; slice = [].slice;
@ -294,7 +294,11 @@
} }
wrapInParentheses(fragments) { wrapInParentheses(fragments) {
return [].concat(this.makeCode('('), fragments, this.makeCode(')')); return [this.makeCode('('), ...fragments, this.makeCode(')')];
}
wrapInBraces(fragments) {
return [this.makeCode('{'), ...fragments, this.makeCode('}')];
} }
joinFragmentArrays(fragmentsList, joinStr) { joinFragmentArrays(fragmentsList, joinStr) {
@ -666,7 +670,23 @@
}; };
exports.StringLiteral = StringLiteral = class StringLiteral extends Literal {}; exports.StringLiteral = StringLiteral = class StringLiteral extends Literal {
compileNode(o) {
var res;
return res = this.csx ? [this.makeCode(this.unquote(true))] : super.compileNode();
}
unquote(literal) {
var unquoted;
unquoted = this.value.slice(1, -1);
if (literal) {
return unquoted.replace(/\\n/g, '\n').replace(/\\"/g, '"');
} else {
return unquoted;
}
}
};
exports.RegexLiteral = RegexLiteral = class RegexLiteral extends Literal {}; exports.RegexLiteral = RegexLiteral = class RegexLiteral extends Literal {};
@ -686,6 +706,8 @@
})(); })();
exports.CSXTag = CSXTag = class CSXTag extends IdentifierLiteral {};
exports.PropertyName = PropertyName = (function() { exports.PropertyName = PropertyName = (function() {
class PropertyName extends Literal {}; class PropertyName extends Literal {};
@ -1060,6 +1082,7 @@
if (this.variable instanceof Value && this.variable.isNotCallable()) { if (this.variable instanceof Value && this.variable.isNotCallable()) {
this.variable.error("literal is not a function"); this.variable.error("literal is not a function");
} }
this.csx = this.variable.base instanceof CSXTag;
} }
updateLocationDataIfMissing(locationData) { updateLocationDataIfMissing(locationData) {
@ -1145,6 +1168,9 @@
compileNode(o) { compileNode(o) {
var arg, argIndex, compiledArgs, fragments, j, len1, ref1, ref2; var arg, argIndex, compiledArgs, fragments, j, len1, ref1, ref2;
if (this.csx) {
return this.compileCSX(o);
}
if ((ref1 = this.variable) != null) { if ((ref1 = this.variable) != null) {
ref1.front = this.front; ref1.front = this.front;
} }
@ -1169,6 +1195,26 @@
return fragments; return fragments;
} }
compileCSX(o) {
var attributes, content, fragments, tag;
[attributes, content] = this.args;
attributes.base.csx = true;
if (content != null) {
content.base.csx = true;
}
fragments = [this.makeCode('<')];
fragments.push(...(tag = this.variable.compileToFragments(o, LEVEL_ACCESS)));
fragments.push(...attributes.compileToFragments(o, LEVEL_PAREN));
if (content) {
fragments.push(this.makeCode('>'));
fragments.push(...content.compileNode(o, LEVEL_LIST));
fragments.push(...[this.makeCode('</'), ...tag, this.makeCode('>')]);
} else {
fragments.push(this.makeCode(' />'));
}
return fragments;
}
}; };
Call.prototype.children = ['variable', 'args']; Call.prototype.children = ['variable', 'args'];
@ -1514,15 +1560,15 @@
ref1 = this.properties; ref1 = this.properties;
for (k = 0, len2 = ref1.length; k < len2; k++) { for (k = 0, len2 = ref1.length; k < len2; k++) {
prop = ref1[k]; prop = ref1[k];
if (prop instanceof Comment || (prop instanceof Assign && prop.context === 'object')) { if (prop instanceof Comment || (prop instanceof Assign && prop.context === 'object' && !this.csx)) {
isCompact = false; isCompact = false;
} }
} }
answer = []; answer = [];
answer.push(this.makeCode(`{${(isCompact ? '' : '\n')}`)); answer.push(this.makeCode(isCompact ? '' : '\n'));
for (i = l = 0, len3 = props.length; l < len3; i = ++l) { for (i = l = 0, len3 = props.length; l < len3; i = ++l) {
prop = props[i]; prop = props[i];
join = i === props.length - 1 ? '' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment ? '\n' : ',\n'; join = i === props.length - 1 ? '' : isCompact && this.csx ? ' ' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment || this.csx ? '\n' : ',\n';
indent = isCompact || prop instanceof Comment ? '' : idt; indent = isCompact || prop instanceof Comment ? '' : idt;
key = prop instanceof Assign && prop.context === 'object' ? prop.variable : prop instanceof Assign ? (!this.lhs ? prop.operatorToken.error(`unexpected ${prop.operatorToken.value}`) : void 0, prop.variable) : !(prop instanceof Comment) ? prop : void 0; key = prop instanceof Assign && prop.context === 'object' ? prop.variable : prop instanceof Assign ? (!this.lhs ? prop.operatorToken.error(`unexpected ${prop.operatorToken.value}`) : void 0, prop.variable) : !(prop instanceof Comment) ? prop : void 0;
if (key instanceof Value && key.hasProperties()) { if (key instanceof Value && key.hasProperties()) {
@ -1546,12 +1592,21 @@
if (indent) { if (indent) {
answer.push(this.makeCode(indent)); answer.push(this.makeCode(indent));
} }
if (this.csx) {
prop.csx = true;
}
if (this.csx && i === 0) {
answer.push(this.makeCode(' '));
}
answer.push(...prop.compileToFragments(o, LEVEL_TOP)); answer.push(...prop.compileToFragments(o, LEVEL_TOP));
if (join) { if (join) {
answer.push(this.makeCode(join)); answer.push(this.makeCode(join));
} }
} }
answer.push(this.makeCode(`${(isCompact ? '' : `\n${this.tab}`)}}`)); answer.push(this.makeCode(isCompact ? '' : `\n${this.tab}`));
if (!this.csx) {
answer = this.wrapInBraces(answer);
}
if (this.front) { if (this.front) {
return this.wrapInParentheses(answer); return this.wrapInParentheses(answer);
} else { } else {
@ -2386,6 +2441,9 @@
} }
} }
} }
if (this.csx) {
this.value.base.csxAttribute = true;
}
val = this.value.compileToFragments(o, LEVEL_LIST); val = this.value.compileToFragments(o, LEVEL_LIST);
compiledName = this.variable.compileToFragments(o, LEVEL_LIST); compiledName = this.variable.compileToFragments(o, LEVEL_LIST);
if (this.context === 'object') { if (this.context === 'object') {
@ -2393,7 +2451,7 @@
compiledName.unshift(this.makeCode('[')); compiledName.unshift(this.makeCode('['));
compiledName.push(this.makeCode(']')); compiledName.push(this.makeCode(']'));
} }
return compiledName.concat(this.makeCode(": "), val); return compiledName.concat(this.makeCode(this.csx ? '=' : ': '), val);
} }
answer = compiledName.concat(this.makeCode(` ${this.context || '='} `), val); answer = compiledName.concat(this.makeCode(` ${this.context || '='} `), val);
if (o.level > LEVEL_LIST || (isValue && this.variable.base instanceof Obj && !this.param)) { if (o.level > LEVEL_LIST || (isValue && this.variable.base instanceof Obj && !this.param)) {
@ -3672,12 +3730,15 @@
compileNode(o) { compileNode(o) {
var bare, expr, fragments; var bare, expr, fragments;
expr = this.body.unwrap(); expr = this.body.unwrap();
if (expr instanceof Value && expr.isAtomic()) { if (expr instanceof Value && expr.isAtomic() && !this.csxAttribute) {
expr.front = this.front; expr.front = this.front;
return expr.compileToFragments(o); return expr.compileToFragments(o);
} }
fragments = expr.compileToFragments(o, LEVEL_PAREN); fragments = expr.compileToFragments(o, LEVEL_PAREN);
bare = o.level < LEVEL_OP && (expr instanceof Op || expr instanceof Call || (expr instanceof For && expr.returns)) && (o.level < LEVEL_COND || fragments.length <= 3); bare = o.level < LEVEL_OP && (expr instanceof Op || expr instanceof Call || (expr instanceof For && expr.returns)) && (o.level < LEVEL_COND || fragments.length <= 3);
if (this.csxAttribute) {
return this.wrapInBraces(fragments);
}
if (bare) { if (bare) {
return fragments; return fragments;
} else { } else {
@ -3709,7 +3770,12 @@
} }
compileNode(o) { compileNode(o) {
var element, elements, expr, fragments, j, len1, value; var code, element, elements, expr, fragments, j, len1, value, wrapped;
if (this.csxAttribute) {
wrapped = new Parens(new StringWithInterpolations(this.body));
wrapped.csxAttribute = true;
return wrapped.compileNode(o);
}
expr = this.body.unwrap(); expr = this.body.unwrap();
elements = []; elements = [];
expr.traverseChildren(false, function(node) { expr.traverseChildren(false, function(node) {
@ -3723,29 +3789,47 @@
return true; return true;
}); });
fragments = []; fragments = [];
fragments.push(this.makeCode('`')); if (!this.csx) {
fragments.push(this.makeCode('`'));
}
for (j = 0, len1 = elements.length; j < len1; j++) { for (j = 0, len1 = elements.length; j < len1; j++) {
element = elements[j]; element = elements[j];
if (element instanceof StringLiteral) { if (element instanceof StringLiteral) {
value = element.value.slice(1, -1); value = element.unquote(this.csx);
value = value.replace(/(\\*)(`|\$\{)/g, function(match, backslashes, toBeEscaped) { if (!this.csx) {
if (backslashes.length % 2 === 0) { value = value.replace(/(\\*)(`|\$\{)/g, function(match, backslashes, toBeEscaped) {
return `${backslashes}\\${toBeEscaped}`; if (backslashes.length % 2 === 0) {
} else { return `${backslashes}\\${toBeEscaped}`;
return match; } else {
} return match;
}); }
});
}
fragments.push(this.makeCode(value)); fragments.push(this.makeCode(value));
} else { } else {
fragments.push(this.makeCode('${')); if (!this.csx) {
fragments.push(...element.compileToFragments(o, LEVEL_PAREN)); fragments.push(this.makeCode('$'));
fragments.push(this.makeCode('}')); }
code = element.compileToFragments(o, LEVEL_PAREN);
if (!this.isNestedTag(element)) {
code = this.wrapInBraces(code);
}
fragments.push(...code);
} }
} }
fragments.push(this.makeCode('`')); if (!this.csx) {
fragments.push(this.makeCode('`'));
}
return fragments; return fragments;
} }
isNestedTag(element) {
var call, exprs, ref1;
exprs = element != null ? (ref1 = element.body) != null ? ref1.expressions : void 0 : void 0;
call = exprs != null ? exprs[0] : void 0;
return this.csx && exprs && exprs.length === 1 && call instanceof Call && call.csx;
}
}; };
StringWithInterpolations.prototype.children = ['body']; StringWithInterpolations.prototype.children = ['body'];

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,10 @@
// Generated by CoffeeScript 2.0.0-beta2 // Generated by CoffeeScript 2.0.0-beta2
(function() { (function() {
var BALANCED_PAIRS, CALL_CLOSERS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, IMPLICIT_UNSPACED_CALL, INVERSES, LINEBREAKS, Rewriter, SINGLE_CLOSERS, SINGLE_LINERS, generate, k, left, len, rite, var BALANCED_PAIRS, CALL_CLOSERS, EXPRESSION_CLOSE, EXPRESSION_END, EXPRESSION_START, IMPLICIT_CALL, IMPLICIT_END, IMPLICIT_FUNC, IMPLICIT_UNSPACED_CALL, INVERSES, LINEBREAKS, Rewriter, SINGLE_CLOSERS, SINGLE_LINERS, generate, k, left, len, rite, throwSyntaxError,
indexOf = [].indexOf; indexOf = [].indexOf;
({throwSyntaxError} = require('./helpers'));
generate = function(tag, value, origin) { generate = function(tag, value, origin) {
var tok; var tok;
tok = [tag, value]; tok = [tag, value];
@ -24,6 +26,7 @@
this.tagPostfixConditionals(); this.tagPostfixConditionals();
this.addImplicitBracesAndParens(); this.addImplicitBracesAndParens();
this.addLocationDataToGeneratedTokens(); this.addLocationDataToGeneratedTokens();
this.enforceValidCSXAttributes();
this.fixOutdentLocationData(); this.fixOutdentLocationData();
return this.tokens; return this.tokens;
} }
@ -362,6 +365,19 @@
}); });
} }
enforceValidCSXAttributes() {
return this.scanTokens(function(token, i, tokens) {
var next, ref;
if (token.csxColon) {
next = tokens[i + 1];
if ((ref = next[0]) !== 'STRING_START' && ref !== 'STRING' && ref !== '(') {
throwSyntaxError('expected wrapped or quoted CSX attribute', next[2]);
}
}
return 1;
});
}
addLocationDataToGeneratedTokens() { addLocationDataToGeneratedTokens() {
return this.scanTokens(function(token, i, tokens) { return this.scanTokens(function(token, i, tokens) {
var column, line, nextLocation, prevLocation, ref, ref1; var column, line, nextLocation, prevLocation, ref, ref1;
@ -528,7 +544,7 @@
IMPLICIT_FUNC = ['IDENTIFIER', 'PROPERTY', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END', '@', 'THIS']; IMPLICIT_FUNC = ['IDENTIFIER', 'PROPERTY', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END', '@', 'THIS'];
IMPLICIT_CALL = ['IDENTIFIER', 'PROPERTY', 'NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START', 'JS', 'NEW', 'PARAM_START', 'CLASS', 'IF', 'TRY', 'SWITCH', 'THIS', 'UNDEFINED', 'NULL', 'BOOL', 'UNARY', 'YIELD', 'AWAIT', 'UNARY_MATH', 'SUPER', 'THROW', '@', '->', '=>', '[', '(', '{', '--', '++']; IMPLICIT_CALL = ['IDENTIFIER', 'CSX_TAG', 'PROPERTY', 'NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_START', 'REGEX', 'REGEX_START', 'JS', 'NEW', 'PARAM_START', 'CLASS', 'IF', 'TRY', 'SWITCH', 'THIS', 'UNDEFINED', 'NULL', 'BOOL', 'UNARY', 'YIELD', 'AWAIT', 'UNARY_MATH', 'SUPER', 'THROW', '@', '->', '=>', '[', '(', '{', '--', '++'];
IMPLICIT_UNSPACED_CALL = ['+', '-']; IMPLICIT_UNSPACED_CALL = ['+', '-'];

View file

@ -142,6 +142,7 @@ grammar =
Identifier: [ Identifier: [
o 'IDENTIFIER', -> new IdentifierLiteral $1 o 'IDENTIFIER', -> new IdentifierLiteral $1
o 'CSX_TAG', -> new CSXTag $1
] ]
Property: [ Property: [

View file

@ -48,6 +48,7 @@ exports.Lexer = class Lexer
@seenExport = no # Used to recognize EXPORT FROM? AS? tokens. @seenExport = no # Used to recognize EXPORT FROM? AS? tokens.
@importSpecifierList = no # Used to identify when in an IMPORT {...} FROM? ... @importSpecifierList = no # Used to identify when in an IMPORT {...} FROM? ...
@exportSpecifierList = no # Used to identify when in an EXPORT {...} FROM? ... @exportSpecifierList = no # Used to identify when in an EXPORT {...} FROM? ...
@csxDepth = 0 # Used to optimize CSX checks, how deep in CSX we are.
@chunkLine = @chunkLine =
opts.line or 0 # The start line for the current @chunk. opts.line or 0 # The start line for the current @chunk.
@ -67,6 +68,7 @@ exports.Lexer = class Lexer
@lineToken() or @lineToken() or
@stringToken() or @stringToken() or
@numberToken() or @numberToken() or
@csxToken() or
@regexToken() or @regexToken() or
@jsToken() or @jsToken() or
@literalToken() @literalToken()
@ -105,7 +107,9 @@ exports.Lexer = class Lexer
# referenced as property names here, so you can still do `jQuery.is()` even # referenced as property names here, so you can still do `jQuery.is()` even
# though `is` means `===` otherwise. # though `is` means `===` otherwise.
identifierToken: -> identifierToken: ->
return 0 unless match = IDENTIFIER.exec @chunk inCSXTag = @atCSXTag()
regex = if inCSXTag then CSX_ATTRIBUTE else IDENTIFIER
return 0 unless match = regex.exec @chunk
[input, id, colon] = match [input, id, colon] = match
# Preserve length of id for location data # Preserve length of id for location data
@ -205,8 +209,11 @@ exports.Lexer = class Lexer
[tagToken[2].first_line, tagToken[2].first_column] = [tagToken[2].first_line, tagToken[2].first_column] =
[poppedToken[2].first_line, poppedToken[2].first_column] [poppedToken[2].first_line, poppedToken[2].first_column]
if colon if colon
colonOffset = input.lastIndexOf ':' colonOffset = input.lastIndexOf if inCSXTag then '=' else ':'
@token ':', ':', colonOffset, colon.length colonToken = @token ':', ':', colonOffset, colon.length
colonToken.csxColon = yes if inCSXTag # used by rewriter
if inCSXTag and tag is 'IDENTIFIER' and prev[0] isnt ':'
@token ',', ',', 0, 0, tagToken
input.length input.length
@ -289,6 +296,9 @@ exports.Lexer = class Lexer
' ' ' '
value value
if @atCSXTag()
@token ',', ',', 0, 0, @prev
end end
# Matches and consumes comments. # Matches and consumes comments.
@ -475,6 +485,76 @@ exports.Lexer = class Lexer
@tokens.pop() if @value() is '\\' @tokens.pop() if @value() is '\\'
this this
# CSX is like JSX but for CoffeeScript.
csxToken: ->
firstChar = @chunk[0]
if firstChar is '<'
match = CSX_IDENTIFIER.exec @chunk[1...]
return 0 unless match and (
@csxDepth > 0 or
# Not the right hand side of an unspaced comparison (i.e. `a<b`).
not (prev = @prev()) or
prev.spaced or
prev[0] not in COMPARABLE_LEFT_SIDE
)
[input, id, colon] = match
origin = @token 'CSX_TAG', id, 1, id.length
@token 'CALL_START', '('
@token '{', '{'
@ends.push tag: '/>', origin: origin, name: id
@csxDepth++
return id.length + 1
else if csxTag = @atCSXTag()
if @chunk[...2] is '/>'
@pair '/>'
@token '}', '}', 0, 2
@token 'CALL_END', ')', 0, 2
@csxDepth--
return 2
else if firstChar is '{'
token = @token '(', '('
@ends.push {tag: '}', origin: token}
return 1
else if firstChar is '>'
# Ignore terminators inside a tag.
@pair '/>' # As if the current tag was self-closing.
origin = @token '}', '}'
@token ',', ','
{tokens, index: end} =
@matchWithInterpolations INSIDE_CSX, '>', '</', CSX_INTERPOLATION
@mergeInterpolationTokens tokens, {delimiter: '"'}, (value, i) =>
@formatString value, delimiter: '>'
match = CSX_IDENTIFIER.exec @chunk[end...]
if not match or match[0] isnt csxTag.name
@error "expected corresponding CSX closing tag for #{csxTag.name}",
csxTag.origin[2]
afterTag = end + csxTag.name.length
if @chunk[afterTag] isnt '>'
@error "missing closing > after tag name", offset: afterTag, length: 1
# +1 for the closing `>`.
@token 'CALL_END', ')', end, csxTag.name.length + 1
@csxDepth--
return afterTag + 1
else
return 0
else if @atCSXTag 1
if firstChar is '}'
@pair firstChar
@token ')', ')'
@token ',', ','
return 1
else
return 0
else
return 0
atCSXTag: (depth = 0) ->
return no if @csxDepth is 0
i = @ends.length - 1
i-- while @ends[i]?.tag is 'OUTDENT' or depth-- > 0 # Ignore indents.
last = @ends[i]
last?.tag is '/>' and last
# We treat all other single characters as a token. E.g.: `( ) , . !` # We treat all other single characters as a token. E.g.: `( ) , . !`
# Multi-character operators are also literal tokens, so that Jison can assign # Multi-character operators are also literal tokens, so that Jison can assign
# the proper order of operations. There are some symbols that we tag specially # the proper order of operations. There are some symbols that we tag specially
@ -535,7 +615,7 @@ exports.Lexer = class Lexer
switch value switch value
when '(', '{', '[' then @ends.push {tag: INVERSES[value], origin: token} when '(', '{', '[' then @ends.push {tag: INVERSES[value], origin: token}
when ')', '}', ']' then @pair value when ')', '}', ']' then @pair value
@tokens.push token @tokens.push @makeToken tag, value
value.length value.length
# Token Manipulators # Token Manipulators
@ -582,10 +662,16 @@ exports.Lexer = class Lexer
# `#{` if interpolations are desired). # `#{` if interpolations are desired).
# - `delimiter` is the delimiter of the token. Examples are `'`, `"`, `'''`, # - `delimiter` is the delimiter of the token. Examples are `'`, `"`, `'''`,
# `"""` and `///`. # `"""` and `///`.
# - `closingDelimiter` is different from `delimiter` only in CSX
# - `interpolators` matches the start of an interpolation, for CSX it's both
# `{` and `<` (i.e. nested CSX tag)
# #
# This method allows us to have strings within interpolations within strings, # This method allows us to have strings within interpolations within strings,
# ad infinitum. # ad infinitum.
matchWithInterpolations: (regex, delimiter) -> matchWithInterpolations: (regex, delimiter, closingDelimiter, interpolators) ->
closingDelimiter ?= delimiter
interpolators ?= /^#\{/
tokens = [] tokens = []
offsetInChunk = delimiter.length offsetInChunk = delimiter.length
return null unless @chunk[...offsetInChunk] is delimiter return null unless @chunk[...offsetInChunk] is delimiter
@ -601,44 +687,55 @@ exports.Lexer = class Lexer
str = str[strPart.length..] str = str[strPart.length..]
offsetInChunk += strPart.length offsetInChunk += strPart.length
break unless str[...2] is '#{' break unless match = interpolators.exec str
[interpolator] = match
# The `1`s are to remove the `#` in `#{`. # To remove the `#` in `#{`.
[line, column] = @getLineAndColumnFromChunk offsetInChunk + 1 interpolationOffset = interpolator.length - 1
[line, column] = @getLineAndColumnFromChunk offsetInChunk + interpolationOffset
rest = str[interpolationOffset..]
{tokens: nested, index} = {tokens: nested, index} =
new Lexer().tokenize str[1..], line: line, column: column, untilBalanced: on new Lexer().tokenize rest, line: line, column: column, untilBalanced: on
# Skip the trailing `}`. # Account for the `#` in `#{`
index += 1 index += interpolationOffset
# Turn the leading and trailing `{` and `}` into parentheses. Unnecessary braceInterpolator = str[index - 1] is '}'
# parentheses will be removed later. if braceInterpolator
[open, ..., close] = nested # Turn the leading and trailing `{` and `}` into parentheses. Unnecessary
open[0] = open[1] = '(' # parentheses will be removed later.
close[0] = close[1] = ')' [open, ..., close] = nested
close.origin = ['', 'end of interpolation', close[2]] open[0] = open[1] = '('
close[0] = close[1] = ')'
close.origin = ['', 'end of interpolation', close[2]]
# Remove leading `'TERMINATOR'` (if any). # Remove leading `'TERMINATOR'` (if any).
nested.splice 1, 1 if nested[1]?[0] is 'TERMINATOR' nested.splice 1, 1 if nested[1]?[0] is 'TERMINATOR'
unless braceInterpolator
# We are not using `{` and `}`, so wrap the interpolated tokens instead.
open = @makeToken '(', '(', offsetInChunk, 0
close = @makeToken ')', ')', offsetInChunk + index, 0
nested = [open, nested..., close]
# Push a fake `'TOKENS'` token, which will get turned into real tokens later. # Push a fake `'TOKENS'` token, which will get turned into real tokens later.
tokens.push ['TOKENS', nested] tokens.push ['TOKENS', nested]
str = str[index..] str = str[index..]
offsetInChunk += index offsetInChunk += index
unless str[...delimiter.length] is delimiter unless str[...closingDelimiter.length] is closingDelimiter
@error "missing #{delimiter}", length: delimiter.length @error "missing #{closingDelimiter}", length: delimiter.length
[firstToken, ..., lastToken] = tokens [firstToken, ..., lastToken] = tokens
firstToken[2].first_column -= delimiter.length firstToken[2].first_column -= delimiter.length
if lastToken[1].substr(-1) is '\n' if lastToken[1].substr(-1) is '\n'
lastToken[2].last_line += 1 lastToken[2].last_line += 1
lastToken[2].last_column = delimiter.length - 1 lastToken[2].last_column = closingDelimiter.length - 1
else else
lastToken[2].last_column += delimiter.length lastToken[2].last_column += closingDelimiter.length
lastToken[2].last_column -= 1 if lastToken[1].length is 0 lastToken[2].last_column -= 1 if lastToken[1].length is 0
{tokens, index: offsetInChunk + delimiter.length} {tokens, index: offsetInChunk + closingDelimiter.length}
# Merge the array `tokens` of the fake token types `'TOKENS'` and `'NEOSTRING'` # Merge the array `tokens` of the fake token types `'TOKENS'` and `'NEOSTRING'`
# (as returned by `matchWithInterpolations`) into the token stream. The value # (as returned by `matchWithInterpolations`) into the token stream. The value
@ -976,6 +1073,17 @@ IDENTIFIER = /// ^
( [^\n\S]* : (?!:) )? # Is this a property name? ( [^\n\S]* : (?!:) )? # Is this a property name?
/// ///
CSX_IDENTIFIER = /// ^
(?![\d<]) # Must not start with `<`.
( (?: (?!\s)[\.\-$\w\x7f-\uffff] )+ ) # Like `IDENTIFIER`, but includes `-`s and `.`s.
///
CSX_ATTRIBUTE = /// ^
(?!\d)
( (?: (?!\s)[\-$\w\x7f-\uffff] )+ ) # Like `IDENTIFIER`, but includes `-`s.
( [^\S]* = (?!=) )? # Is this an attribute with a value?
///
NUMBER = /// NUMBER = ///
^ 0b[01]+ | # binary ^ 0b[01]+ | # binary
^ 0o[0-7]+ | # octal ^ 0o[0-7]+ | # octal
@ -1012,6 +1120,17 @@ STRING_DOUBLE = /// ^(?: [^\\"#] | \\[\s\S] | \#(?!\{) )* ///
HEREDOC_SINGLE = /// ^(?: [^\\'] | \\[\s\S] | '(?!'') )* /// HEREDOC_SINGLE = /// ^(?: [^\\'] | \\[\s\S] | '(?!'') )* ///
HEREDOC_DOUBLE = /// ^(?: [^\\"#] | \\[\s\S] | "(?!"") | \#(?!\{) )* /// HEREDOC_DOUBLE = /// ^(?: [^\\"#] | \\[\s\S] | "(?!"") | \#(?!\{) )* ///
INSIDE_CSX = /// ^(?:
[^
\{ # Start of CoffeeScript interpolation.
< # Maybe CSX tag (`<` not allowed even if bare).
]
)* /// # Similar to `HEREDOC_DOUBLE` but there is no escaping.
CSX_INTERPOLATION = /// ^(?:
\{ # CoffeeScript interpolation.
| <(?!/) # CSX opening tag.
)///
STRING_OMIT = /// STRING_OMIT = ///
((?:\\\\)+) # Consume (and preserve) an even number of backslashes. ((?:\\\\)+) # Consume (and preserve) an even number of backslashes.
| \\[^\S\n]*\n\s* # Remove escaped newlines. | \\[^\S\n]*\n\s* # Remove escaped newlines.
@ -1115,6 +1234,9 @@ INDEXABLE = CALLABLE.concat [
'BOOL', 'NULL', 'UNDEFINED', '}', '::' 'BOOL', 'NULL', 'UNDEFINED', '}', '::'
] ]
# Tokens which can be the left-hand side of a less-than comparison, i.e. `a<b`.
COMPARABLE_LEFT_SIDE = ['IDENTIFIER', ')', ']', 'NUMBER']
# Tokens which a regular expression will never immediately follow (except spaced # Tokens which a regular expression will never immediately follow (except spaced
# CALLABLEs in some cases), but which a division operator can. # CALLABLEs in some cases), but which a division operator can.
# #

View file

@ -280,7 +280,10 @@ exports.Base = class Base
new CodeFragment this, code new CodeFragment this, code
wrapInParentheses: (fragments) -> wrapInParentheses: (fragments) ->
[].concat @makeCode('('), fragments, @makeCode(')') [@makeCode('('), fragments..., @makeCode(')')]
wrapInBraces: (fragments) ->
[@makeCode('{'), fragments..., @makeCode('}')]
# `fragmentsList` is an array of arrays of fragments. Each array in fragmentsList will be # `fragmentsList` is an array of arrays of fragments. Each array in fragmentsList will be
# concatonated together, with `joinStr` added in between each, to produce a final flat array # concatonated together, with `joinStr` added in between each, to produce a final flat array
@ -534,6 +537,16 @@ exports.NaNLiteral = class NaNLiteral extends NumberLiteral
if o.level >= LEVEL_OP then @wrapInParentheses code else code if o.level >= LEVEL_OP then @wrapInParentheses code else code
exports.StringLiteral = class StringLiteral extends Literal exports.StringLiteral = class StringLiteral extends Literal
compileNode: (o) ->
res = if @csx then [@makeCode @unquote yes] else super()
unquote: (literal) ->
unquoted = @value[1...-1]
if literal
unquoted.replace /\\n/g, '\n'
.replace /\\"/g, '"'
else
unquoted
exports.RegexLiteral = class RegexLiteral extends Literal exports.RegexLiteral = class RegexLiteral extends Literal
@ -545,6 +558,8 @@ exports.IdentifierLiteral = class IdentifierLiteral extends Literal
eachName: (iterator) -> eachName: (iterator) ->
iterator @ iterator @
exports.CSXTag = class CSXTag extends IdentifierLiteral
exports.PropertyName = class PropertyName extends Literal exports.PropertyName = class PropertyName extends Literal
isAssignable: YES isAssignable: YES
@ -778,6 +793,8 @@ exports.Call = class Call extends Base
if @variable instanceof Value and @variable.isNotCallable() if @variable instanceof Value and @variable.isNotCallable()
@variable.error "literal is not a function" @variable.error "literal is not a function"
@csx = @variable.base instanceof CSXTag
children: ['variable', 'args'] children: ['variable', 'args']
# When setting the location, we sometimes need to update the start location to # When setting the location, we sometimes need to update the start location to
@ -840,6 +857,7 @@ exports.Call = class Call extends Base
# Compile a vanilla function call. # Compile a vanilla function call.
compileNode: (o) -> compileNode: (o) ->
return @compileCSX o if @csx
@variable?.front = @front @variable?.front = @front
compiledArgs = [] compiledArgs = []
for arg, argIndex in @args for arg, argIndex in @args
@ -854,6 +872,21 @@ exports.Call = class Call extends Base
fragments.push @makeCode('('), compiledArgs..., @makeCode(')') fragments.push @makeCode('('), compiledArgs..., @makeCode(')')
fragments fragments
compileCSX: (o) ->
[attributes, content] = @args
attributes.base.csx = yes
content?.base.csx = yes
fragments = [@makeCode('<')]
fragments.push (tag = @variable.compileToFragments(o, LEVEL_ACCESS))...
fragments.push attributes.compileToFragments(o, LEVEL_PAREN)...
if content
fragments.push @makeCode('>')
fragments.push content.compileNode(o, LEVEL_LIST)...
fragments.push [@makeCode('</'), tag..., @makeCode('>')]...
else
fragments.push @makeCode(' />')
fragments
#### Super #### Super
# Takes care of converting `super()` calls into calls against the prototype's # Takes care of converting `super()` calls into calls against the prototype's
@ -1134,17 +1167,19 @@ exports.Obj = class Obj extends Base
isCompact = yes isCompact = yes
for prop in @properties for prop in @properties
if prop instanceof Comment or (prop instanceof Assign and prop.context is 'object') if prop instanceof Comment or (prop instanceof Assign and prop.context is 'object' and not @csx)
isCompact = no isCompact = no
answer = [] answer = []
answer.push @makeCode "{#{if isCompact then '' else '\n'}" answer.push @makeCode if isCompact then '' else '\n'
for prop, i in props for prop, i in props
join = if i is props.length - 1 join = if i is props.length - 1
'' ''
else if isCompact and @csx
' '
else if isCompact else if isCompact
', ' ', '
else if prop is lastNoncom or prop instanceof Comment else if prop is lastNoncom or prop instanceof Comment or @csx
'\n' '\n'
else else
',\n' ',\n'
@ -1172,9 +1207,12 @@ exports.Obj = class Obj extends Base
prop = new Assign prop, prop, 'object' prop = new Assign prop, prop, 'object'
if indent then answer.push @makeCode indent if indent then answer.push @makeCode indent
prop.csx = yes if @csx
answer.push @makeCode ' ' if @csx and i is 0
answer.push prop.compileToFragments(o, LEVEL_TOP)... answer.push prop.compileToFragments(o, LEVEL_TOP)...
if join then answer.push @makeCode join if join then answer.push @makeCode join
answer.push @makeCode "#{if isCompact then '' else "\n#{@tab}"}}" answer.push @makeCode if isCompact then '' else "\n#{@tab}"
answer = @wrapInBraces answer if not @csx
if @front then @wrapInParentheses answer else answer if @front then @wrapInParentheses answer else answer
assigns: (name) -> assigns: (name) ->
@ -1758,6 +1796,7 @@ exports.Assign = class Assign extends Base
[properties..., prototype, name] = @variable.properties [properties..., prototype, name] = @variable.properties
@value.name = name if prototype.name?.value is 'prototype' @value.name = name if prototype.name?.value is 'prototype'
@value.base.csxAttribute = yes if @csx
val = @value.compileToFragments o, LEVEL_LIST val = @value.compileToFragments o, LEVEL_LIST
compiledName = @variable.compileToFragments o, LEVEL_LIST compiledName = @variable.compileToFragments o, LEVEL_LIST
@ -1765,7 +1804,7 @@ exports.Assign = class Assign extends Base
if @variable.shouldCache() if @variable.shouldCache()
compiledName.unshift @makeCode '[' compiledName.unshift @makeCode '['
compiledName.push @makeCode ']' compiledName.push @makeCode ']'
return compiledName.concat @makeCode(": "), val return compiledName.concat @makeCode(if @csx then '=' else ': '), val
answer = compiledName.concat @makeCode(" #{ @context or '=' } "), val answer = compiledName.concat @makeCode(" #{ @context or '=' } "), val
# Per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assignment_without_declaration, # Per https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Assignment_without_declaration,
@ -2773,13 +2812,14 @@ exports.Parens = class Parens extends Base
compileNode: (o) -> compileNode: (o) ->
expr = @body.unwrap() expr = @body.unwrap()
if expr instanceof Value and expr.isAtomic() if expr instanceof Value and expr.isAtomic() and not @csxAttribute
expr.front = @front expr.front = @front
return expr.compileToFragments o return expr.compileToFragments o
fragments = expr.compileToFragments o, LEVEL_PAREN fragments = expr.compileToFragments o, LEVEL_PAREN
bare = o.level < LEVEL_OP and (expr instanceof Op or expr instanceof Call or bare = o.level < LEVEL_OP and (expr instanceof Op or expr instanceof Call or
(expr instanceof For and expr.returns)) and (o.level < LEVEL_COND or (expr instanceof For and expr.returns)) and (o.level < LEVEL_COND or
fragments.length <= 3) fragments.length <= 3)
return @wrapInBraces fragments if @csxAttribute
if bare then fragments else @wrapInParentheses fragments if bare then fragments else @wrapInParentheses fragments
#### StringWithInterpolations #### StringWithInterpolations
@ -2798,6 +2838,11 @@ exports.StringWithInterpolations = class StringWithInterpolations extends Base
shouldCache: -> @body.shouldCache() shouldCache: -> @body.shouldCache()
compileNode: (o) -> compileNode: (o) ->
if @csxAttribute
wrapped = new Parens new StringWithInterpolations @body
wrapped.csxAttribute = yes
return wrapped.compileNode o
# Assumes that `expr` is `Value` » `StringLiteral` or `Op` # Assumes that `expr` is `Value` » `StringLiteral` or `Op`
expr = @body.unwrap() expr = @body.unwrap()
@ -2812,25 +2857,31 @@ exports.StringWithInterpolations = class StringWithInterpolations extends Base
return yes return yes
fragments = [] fragments = []
fragments.push @makeCode '`' fragments.push @makeCode '`' unless @csx
for element in elements for element in elements
if element instanceof StringLiteral if element instanceof StringLiteral
value = element.value[1...-1] value = element.unquote @csx
# Backticks and `${` inside template literals must be escaped. unless @csx
value = value.replace /(\\*)(`|\$\{)/g, (match, backslashes, toBeEscaped) -> # Backticks and `${` inside template literals must be escaped.
if backslashes.length % 2 is 0 value = value.replace /(\\*)(`|\$\{)/g, (match, backslashes, toBeEscaped) ->
"#{backslashes}\\#{toBeEscaped}" if backslashes.length % 2 is 0
else "#{backslashes}\\#{toBeEscaped}"
match else
match
fragments.push @makeCode value fragments.push @makeCode value
else else
fragments.push @makeCode '${' fragments.push @makeCode '$' unless @csx
fragments.push element.compileToFragments(o, LEVEL_PAREN)... code = element.compileToFragments(o, LEVEL_PAREN)
fragments.push @makeCode '}' code = @wrapInBraces code unless @isNestedTag element
fragments.push @makeCode '`' fragments.push code...
fragments.push @makeCode '`' unless @csx
fragments fragments
isNestedTag: (element) ->
exprs = element?.body?.expressions
call = exprs?[0]
@csx and exprs and exprs.length is 1 and call instanceof Call and call.csx
#### For #### For
# CoffeeScript's replacement for the *for* loop is our array and object # CoffeeScript's replacement for the *for* loop is our array and object

View file

@ -5,6 +5,8 @@
# shorthand into the unambiguous long form, add implicit indentation and # shorthand into the unambiguous long form, add implicit indentation and
# parentheses, and generally clean things up. # parentheses, and generally clean things up.
{throwSyntaxError} = require './helpers'
# Create a generated token: one that exists due to a use of implicit syntax. # Create a generated token: one that exists due to a use of implicit syntax.
generate = (tag, value, origin) -> generate = (tag, value, origin) ->
tok = [tag, value] tok = [tag, value]
@ -31,6 +33,7 @@ exports.Rewriter = class Rewriter
@tagPostfixConditionals() @tagPostfixConditionals()
@addImplicitBracesAndParens() @addImplicitBracesAndParens()
@addLocationDataToGeneratedTokens() @addLocationDataToGeneratedTokens()
@enforceValidCSXAttributes()
@fixOutdentLocationData() @fixOutdentLocationData()
@tokens @tokens
@ -354,6 +357,15 @@ exports.Rewriter = class Rewriter
endImplicitObject i + offset endImplicitObject i + offset
return forward(1) return forward(1)
# Make sure only strings and wrapped expressions are used in CSX attributes
enforceValidCSXAttributes: ->
@scanTokens (token, i, tokens) ->
if token.csxColon
next = tokens[i + 1]
if next[0] not in ['STRING_START', 'STRING', '(']
throwSyntaxError 'expected wrapped or quoted CSX attribute', next[2]
return 1
# Add location data to all tokens generated by the rewriter. # Add location data to all tokens generated by the rewriter.
addLocationDataToGeneratedTokens: -> addLocationDataToGeneratedTokens: ->
@scanTokens (token, i, tokens) -> @scanTokens (token, i, tokens) ->
@ -504,7 +516,7 @@ IMPLICIT_FUNC = ['IDENTIFIER', 'PROPERTY', 'SUPER', ')', 'CALL_END', ']', 'IN
# If preceded by an `IMPLICIT_FUNC`, indicates a function invocation. # If preceded by an `IMPLICIT_FUNC`, indicates a function invocation.
IMPLICIT_CALL = [ IMPLICIT_CALL = [
'IDENTIFIER', 'PROPERTY', 'NUMBER', 'INFINITY', 'NAN' 'IDENTIFIER', 'CSX_TAG', 'PROPERTY', 'NUMBER', 'INFINITY', 'NAN'
'STRING', 'STRING_START', 'REGEX', 'REGEX_START', 'JS' 'STRING', 'STRING_START', 'REGEX', 'REGEX_START', 'JS'
'NEW', 'PARAM_START', 'CLASS', 'IF', 'TRY', 'SWITCH', 'THIS' 'NEW', 'PARAM_START', 'CLASS', 'IF', 'TRY', 'SWITCH', 'THIS'
'UNDEFINED', 'NULL', 'BOOL' 'UNDEFINED', 'NULL', 'BOOL'

View file

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

View file

@ -415,18 +415,16 @@ test "#4248: Unicode code point escapes", ->
eq '\\u{123456}', "#{'\\'}#{'u{123456}'}" eq '\\u{123456}', "#{'\\'}#{'u{123456}'}"
# don't rewrite code point escapes # don't rewrite code point escapes
input = """ eqJS """
'\\u{bcdef}\\u{abc}' '\\u{bcdef}\\u{abc}'
""" """,
output = """ """
'\\u{bcdef}\\u{abc}'; '\\u{bcdef}\\u{abc}';
""" """
eq toJS(input), output
input = """ eqJS """
"#{ 'a' }\\u{bcdef}" "#{ 'a' }\\u{bcdef}"
""" """,
output = """ """
"a\\u{bcdef}"; "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) -> egal = (a, b) ->
if a is b if a is b
a isnt 0 or 1/a is 1/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] return no for el, idx in a when not arrayEgal el, b[idx]
yes yes
exports.eq = (a, b, msg) -> ok egal(a, b), msg or "Expected #{a} to equal #{b}" exports.eq = (a, b, msg) ->
exports.arrayEq = (a, b, msg) -> ok arrayEgal(a,b), msg or "Expected #{a} to deep equal #{b}" ok egal(a, b), msg or
"Expected #{reset}#{a}#{red} to equal #{reset}#{b}#{red}"
exports.toJS = (str) -> exports.arrayEq = (a, b, msg) ->
CoffeeScript.compile str, bare: yes ok arrayEgal(a,b), msg or
.replace /^\s+|\s+$/g, '' # Trim leading/trailing whitespace "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}"""