Skip to content

Instantly share code, notes, and snippets.

@jlongster
Last active August 29, 2015 14:02
Show Gist options
  • Save jlongster/5858882fac03afbadf20 to your computer and use it in GitHub Desktop.
Save jlongster/5858882fac03afbadf20 to your computer and use it in GitHub Desktop.
function JSXBailError(message) {
this.message = message;
}
function JSXReadError(message, parser) {
var sx = syn.syntaxFromToken({
type: null,
value: 'nothing matters',
lineStart: parser.lineStart(),
lineNumber: parser.lineNumber(),
range: [parser.index(), parser.index()+1]
});
var err = new syn.MacroSyntaxError('JSX', message, sx)
return new SyntaxError(syn.printSyntaxError(parser.source(), err));
}
var JSXReader = Reader.create({
read: function() {
try {
this.readElement();
}
catch(e) {
if(!(e instanceof JSXBailError)) {
throw e;
}
this.reset();
//console.log('bailed: ' + this.parser.ch());
}
return this.finish();
},
readElement: function() {
var parser = this.parser;
var Token = parser.Token;
this.shadowBuffer(function(prevBuffer) {
var selfClosing = this.readOpeningElement();
var openingName = this.buffer[this.buffer.length-2];
if(!selfClosing) {
while(this.parser.index() < this.parser.length()) {
if(this.match('<', '/')) {
break;
}
this.readChild();
}
this.readClosingElement();
var closingName = this.buffer[this.buffer.length-1];
if(openingName.value !== closingName.value) {
throw new JSXReadError(
"Expected correspoding closing tag for " + openingName.value,
this.parser
)
}
}
var firstTok = this.buffer[0];
var lastTok = this.buffer[this.buffer.length-1];
prevBuffer.push(parser.withLoc(parser.index(), {
type: Token.Identifier,
value: 'DOM',
}));
prevBuffer.push(parser.withLoc(parser.index(), {
type: Token.Delimiter,
value: '{}',
startLineNumber: firstTok.lineNumber,
startLineStart: firstTok.lineStart,
startRange: firstTok.range,
inner: this.buffer,
endLineNumber: lastTok.lineNumber,
endLineStart: lastTok.lineStart,
endRange: lastTok.range,
}));
}.bind(this));
},
readOpeningElement: function() {
var parser = this.parser;
var selfClosing = false;
this.expect('<');
this.readElementName();
var openingName = this.buffer[this.buffer.length - 1];
if(JSXTAGS[openingName.value]) {
openingName.value = 'React.DOM.' + openingName.value;
}
parser.skipComment();
this.shadowBuffer(function(prevBuffer) {
var end = false;
while(parser.index() < parser.length() &&
!this.match('/') &&
!this.match('>')) {
this.readAttribute();
end = this.match('/') || this.match('>');
if(!end) {
this.add({
type: parser.Token.Punctuator,
value: ','
});
}
}
if(this.buffer.length) {
prevBuffer.push(parser.withLoc(parser.index(), {
type: parser.Token.Delimiter,
value: '{}',
inner: this.buffer
}));
}
else {
prevBuffer.push({
type: parser.Token.NullLiteral,
value: 'null'
});
}
}.bind(this));
parser.skipComment();
if(this.match('/')) {
selfClosing = true;
this.expect('/');
}
this.expect('>');
return selfClosing;
},
readClosingElement: function() {
this.expect('<');
this.expect('/');
this.readElementName();
this.expect('>');
},
readChild: function() {
if(this.match('{')) {
this.readExpressionContainer();
}
else if(this.match('<')) {
this.readElement();
}
else {
this.readText(['{', '<']);
}
},
readText: function(stopChars) {
var parser = this.parser;
var ch, str = '';
var index = parser.index();
var source = parser.source();
var start = index;
while(index < parser.length()) {
ch = source[index];
if(stopChars.indexOf(ch) !== -1) {
break;
}
if(ch === '&') {
parser.index(index);
str += this.readEntity(index);
index = parser.index();
}
else {
index++;
if(parser.isLineTerminator(ch.charCodeAt(0))) {
parser.lineNumber(parser.lineNumber()+1);
parser.lineStart(index);
}
str += ch;
}
}
parser.index(index);
this.add(parser.withLoc(start, {
type: parser.Token.StringLiteral,
value: str
}));
},
readEntity: function() {
var parser = this.parser;
var source = parser.source();
var index = parser.index();
var ch = source[index];
var str = '', count = 0, entity;
parser.assert(ch === '&', 'Entity must start with an ampersand');
index++;
while(index < parser.length() && count < 10) {
ch = source[index++];
if(ch === ';') {
break;
}
str += ch;
count++;
}
if (str[0] === '#' && str[1] === 'x') {
entity = String.fromCharCode(parseInt(str.substr(2), 16));
} else if (str[0] === '#') {
entity = String.fromCharCode(parseInt(str.substr(1), 10));
} else {
entity = XHTMLEntities[str];
}
parser.index(index);
return entity;
},
readElementName: function() {
var parser = this.parser;
if(!parser.isIdentifierStart(parser.ch().charCodeAt(0))) {
throw new JSXBailError('bailed while reading element ' +
'name: ' + parser.ch().charCodeAt(0));
}
this.readIdentifier();
if(this.match(':')) {
this.readPunc();
this.readIdentifier();
}
if(this.match('.')) {
this.readMemberParts();
}
},
readAttribute: function() {
var hasValue = false;
this.readIdentifier();
if(this.match(':')) {
this.readPunc();
this.readIdentifier();
}
if((hasValue = this.match('='))) {
this.expect('=');
}
this.add({
type: this.parser.Token.Punctuator,
value: ':'
});
if(hasValue) {
this.readAttributeValue();
}
else {
this.add({
type: this.parser.Token.BooleanLiteral,
value: 'true'
});
}
this.parser.skipComment();
},
readAttributeValue: function() {
if(this.match('{')) {
this.readExpressionContainer();
// TODO
//throw new Error("can't be empty");
}
else if(this.match('<')) {
this.read();
}
else {
var quote = this.parser.ch();
var start = this.parser.index();
if(quote !== '"' && quote !== "'") {
throw new JSXReadError(
'attributes should be an expression or quoted text',
this.parser
);
}
this.parser.index(start + 1);
this.readText([quote]);
if(quote !== this.parser.source()[this.parser.index()]) {
throw new JSXReadError(
'badly quoted string',
this.parser
);
}
this.parser.index(this.parser.index() + 1);
}
},
readExpressionContainer: function() {
this.expect('{');
var expr;
while(!this.match('}')) {
this.readToken();
}
this.expect('}');
},
readMemberParts: function() {
while(this.match('.')) {
this.readPunc();
this.readIdentifier();
}
},
readIdentifier: function() {
var parser = this.parser;
parser.skipComment();
var index = parser.index();
var source = parser.source();
var ch, start, value = '';
var chCode = parser.ch().charCodeAt(0);
if(chCode === 92 || !parser.isIdentifierStart(chCode)) {
throw new JSXBailError('bailed while reading identifier: ' +
parser.ch());
}
start = index;
while(index < parser.length()) {
ch = source.charCodeAt(index);
// exclude backslash (\) and add hyphen (-)
if(ch === 92 ||
!(ch === 45 || parser.isIdentifierPart(ch))) {
break;
}
value += source[index++];
}
this.parser.index(index);
this.add(parser.withLoc(start, {
type: parser.Token.Identifier,
value: value
}));
}
});
function Reader(parser) {
this.buffer = [];
this.parser = parser;
this.origLineNumber = parser.lineNumber();
this.origLineStart = parser.lineStart();
this.origIndex = parser.index();
}
Reader.create = function(cls) {
function _Reader() {
Reader.apply(this, arguments);
}
_Reader.prototype = _.extend(Object.create(Reader.prototype), cls);
return _Reader;
};
Reader.prototype.expect = function(ch) {
var parser = this.parser;
var punc = parser.scanPunctuator();
if(punc.value !== ch) {
throw new Error('unexpected: ' + punc.value +
', wanted: ' + ch);
}
return punc;
};
Reader.prototype.match = function(/* ch1, ch2, ... chN */) {
var chars = Array.prototype.slice.call(arguments);
var parser = this.parser;
var idx = parser.index();
var matched = false;
try {
var punc = parser.scanPunctuator();
matched = punc.value === chars[0];
chars.shift();
if(chars.length) {
matched = matched && this.match.apply(this, chars);
}
}
catch(e) {
// TODO: check specific error type here
//console.log(e);
}
parser.index(idx);
return matched;
}
Reader.prototype.advance = function() {
this.add(this.parser.advance());
}
Reader.prototype.readToken = function() {
this.add(this.parser.readToken([], false, false));
}
Reader.prototype.reset = function() {
this.parser.index(this.origIndex);
this.parser.lineStart(this.origLineStart);
this.parser.lineNumber(this.origLineNumber);
this.finish();
};
Reader.prototype.add = function(toks) {
if(Array.isArray(toks)) {
this.buffer = this.buffer.concat(toks);
}
else {
this.buffer.push(toks);
}
};
Reader.prototype.shadowBuffer = function(func) {
var prev = this.buffer;
this.buffer = [];
func(prev);
this.buffer = prev;
};
Reader.prototype.finish = function() {
var buf = this.buffer;
this.buffer = [];
return buf;
};
Reader.prototype.readPunc = function(ch) {
var tok;
if(ch) {
tok = this.expect(ch);
}
else {
tok = this.parser.scanPunctuator();
}
this.add(tok);
};
Reader.prototype.readIdent = function() {
this.add(this.parser.scanIdentifier());
};
Reader.prototype.readIdent = function() {
this.add(this.parser.scanIdentifier());
};
Reader.prototype.readLiteral = function() {
//this.add(this.parser.scanIdentifier());
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment