blob: 61218b4ae63de0f1a56e36eaad8cc27be5b9651d [file] [log] [blame]
part of mustache;
const int _EOF = -1;
const int _TAB = 9;
const int _NEWLINE = 10;
const int _RETURN = 13;
const int _SPACE = 32;
const int _EXCLAIM = 33;
const int _QUOTE = 34;
const int _APOS = 39;
const int _HASH = 35;
const int _AMP = 38;
const int _PERIOD = 46;
const int _FORWARD_SLASH = 47;
const int _LT = 60;
const int _EQUAL = 61;
const int _GT = 62;
const int _CARET = 94;
const int _OPEN_MUSTACHE = 123;
const int _CLOSE_MUSTACHE = 125;
class _Scanner {
_Scanner(String source, this._templateName, Delimiters initial, {bool lenient: true})
: _r = new _CharReader(source),
_lenient = lenient,
_openDelimiter = (initial == null) ? _OPEN_MUSTACHE : initial.open,
_openDelimiterInner =
(initial == null) ? _OPEN_MUSTACHE : initial.openInner,
_closeDelimiterInner =
(initial == null) ? _CLOSE_MUSTACHE : initial.closeInner,
_closeDelimiter = (initial == null) ? _CLOSE_MUSTACHE : initial.close;
final String _templateName;
//FIXME not used yet.
final bool _lenient;
_CharReader _r;
List<_Token> _tokens = new List<_Token>();
// These can be changed by the change delimiter tag.
int _openDelimiter;
int _openDelimiterInner;
int _closeDelimiterInner;
int _closeDelimiter;
List<_Token> scan() {
while(true) {
int c = _peek();
if (c == _EOF) break;
else if (c == _openDelimiter) _scanMustacheTag();
else _scanText();
}
return _tokens;
}
int _read() => _r.read();
int _peek() => _r.peek();
_addStringToken(int type) {
int l = _r.line, c = _r.column;
var value = type == _TEXT ? _readLine() : _readString();
if (type != _TEXT && type != _COMMENT) value = value.trim();
_tokens.add(new _Token(type, value, l, c));
}
_addCharToken(int type, int charCode) {
int l = _r.line, c = _r.column;
var value = new String.fromCharCode(charCode);
_tokens.add(new _Token(type, value, l, c));
}
_addPartialToken() {
// Capture whitespace preceding a partial tag so it can used for indentation during rendering.
var indent = '';
if (_tokens.isNotEmpty) {
if (_tokens.length == 1 && _tokens.last.type == _WHITESPACE) {
indent = _tokens.last.value;
} else if (_tokens.length > 1) {
if (_tokens.last.type == _WHITESPACE
&& _tokens[_tokens.length - 2].type == _NEWLINE) {
indent = _tokens.last.value;
}
}
}
int l = _r.line, c = _r.column;
var value = _readString().trim();
_tokens.add(new _Token(_PARTIAL, value, l, c, indent: indent));
}
_expect(int expectedCharCode) {
int c = _read();
if (c == _EOF) {
throw new TemplateException('Unexpected end of input',
_templateName, _r.line, _r.column);
} else if (c != expectedCharCode) {
throw new TemplateException('Unexpected character, '
'expected: ${new String.fromCharCode(expectedCharCode)} ($expectedCharCode), '
'was: ${new String.fromCharCode(c)} ($c)',
_templateName, _r.line, _r.column);
}
}
// FIXME probably need to differentiate between searching for open, or close
// delimiter.
String _readString() => _r.readWhile(
(c) => c != _closeDelimiterInner
//FIXME && (_closeDelimiterInner == null && c != _closeDelimiter)
&& c != _closeDelimiter
&& c != _openDelimiter
&& c != _openDelimiterInner
&& c != _EOF); //FIXME EOF should be error.
// FIXME probably need to differentiate between searching for open, or close
// delimiter.
String _readLine() => _r.readWhile(
(c) => c != _closeDelimiterInner
//FIXME && (_closeDelimiterInner == null && c != _closeDelimiter)
&& c != _closeDelimiter
&& c != _openDelimiter
&& c != _openDelimiterInner
&& c != _EOF //FIXME EOF should be error.
&& c != _NEWLINE);
//FIXME unless in lenient mode only allow spaces.
String _readTagWhitespace() => _r.readWhile(_isWhitespace);
bool _isWhitespace(int c)
=> const [_SPACE, _TAB , _NEWLINE, _RETURN].contains(c);
_scanText() {
while(true) {
int c = _peek();
if (c == _EOF) {
return;
} else if (c == _openDelimiter) {
return;
} else if (c == _RETURN) {
_read();
if (_peek() == _NEWLINE) {
_read();
_tokens.add(new _Token(_LINE_END, '\r\n', _r.line, _r.column));
} else {
_addCharToken(_TEXT, _RETURN);
}
} else if (c == _NEWLINE) {
_read();
_addCharToken(_LINE_END, _NEWLINE);
} else if (c == _SPACE || c == _TAB) {
var value = _r.readWhile((c) => c == _SPACE || c == _TAB);
_tokens.add(new _Token(_WHITESPACE, value, _r.line, _r.column));
//FIXME figure out why this is required
} else if (c == _closeDelimiter || c == _closeDelimiterInner) {
_read();
_addCharToken(_TEXT, c);
} else {
_addStringToken(_TEXT);
}
}
}
//TODO consider changing the parsing here to use a regexp. It will probably
// be simpler to read.
_scanChangeDelimiterTag() {
// Open delimiter characters have already been read.
_expect(_EQUAL);
int line = _r.line;
int col = _r.column;
var delimiterInner = _closeDelimiterInner;
var delimiter = _closeDelimiter;
_readTagWhitespace();
int c;
c = _r.read();
if (c == _EQUAL) throw 'syntax error'; //FIXME
_openDelimiter = c;
c = _r.read();
if (_isWhitespace(c)) {
_openDelimiterInner = null;
} else {
_openDelimiterInner = c;
}
_readTagWhitespace();
c = _r.read();
if (_isWhitespace(c) || c == _EQUAL) throw 'syntax error'; //FIXME
if (_isWhitespace(_peek()) || _peek() == _EQUAL) {
_closeDelimiterInner = null;
_closeDelimiter = c;
} else {
_closeDelimiterInner = c;
_closeDelimiter = _read();
}
_readTagWhitespace();
_expect(_EQUAL);
_readTagWhitespace();
_expect(delimiterInner);
_expect(delimiter);
var value = new Delimiters(
_openDelimiter,
_openDelimiterInner,
_closeDelimiterInner,
_closeDelimiter).toString();
_tokens.add(new _Token(_CHANGE_DELIMITER, value, line, col));
}
_scanMustacheTag() {
int startOffset = _r.offset;
_expect(_openDelimiter);
// If just a single mustache, return this as a text token.
//FIXME is this missing a read call to advance ??
if (_openDelimiterInner != null && _peek() != _openDelimiterInner) {
_addCharToken(_TEXT, _openDelimiter);
return;
}
if (_openDelimiterInner != null) _expect(_openDelimiterInner);
// Escaped text {{{ ... }}}
if (_peek() == _OPEN_MUSTACHE) {
_read();
_addStringToken(_UNESC_VARIABLE);
_expect(_CLOSE_MUSTACHE);
_expect(_closeDelimiterInner);
_expect(_closeDelimiter);
return;
}
// Skip whitespace at start of tag. i.e. {{ # foo }} {{ / foo }}
_readTagWhitespace();
switch(_peek()) {
case _EOF:
throw new TemplateException('Unexpected end of input',
_templateName, _r.line, _r.column);
// Escaped text {{& ... }}
case _AMP:
_read();
_addStringToken(_UNESC_VARIABLE);
break;
// Comment {{! ... }}
case _EXCLAIM:
_read();
_addStringToken(_COMMENT);
break;
// Partial {{> ... }}
case _GT:
_read();
_addPartialToken();
break;
// Open section {{# ... }}
case _HASH:
_read();
_addStringToken(_OPEN_SECTION);
break;
// Open inverted section {{^ ... }}
case _CARET:
_read();
_addStringToken(_OPEN_INV_SECTION);
break;
// Close section {{/ ... }}
case _FORWARD_SLASH:
_read();
_addStringToken(_CLOSE_SECTION);
// Store source file offset, so source substrings can be extracted for
// lambdas.
_tokens.last.offset = startOffset;
break;
// Change delimiter {{= ... =}}
case _EQUAL:
_scanChangeDelimiterTag();
return;
// Variable {{ ... }}
default:
_addStringToken(_VARIABLE);
}
if (_closeDelimiterInner != null) _expect(_closeDelimiterInner);
_expect(_closeDelimiter);
// Store source file offset, so source substrings can be extracted for
// lambdas.
if (_tokens.isNotEmpty) {
var t = _tokens.last;
if (t.type == _OPEN_SECTION || t.type == _OPEN_INV_SECTION) {
t.offset = _r.offset;
}
}
}
}