library mustache.scanner; | |
List<Token> scan(String source) => new _Scanner(source).scan(); | |
abstract class Token { | |
int get type; | |
String get value; | |
} | |
const int TEXT = 1; | |
const int VARIABLE = 2; | |
const int PARTIAL = 3; | |
const int OPEN_SECTION = 4; | |
const int OPEN_INV_SECTION = 5; | |
const int CLOSE_SECTION = 6; | |
const int COMMENT = 7; | |
tokenTypeString(int type) => ['?', 'Text', 'Var', 'Par', 'Open', 'OpenInv', 'Close', 'Comment'][type]; | |
const int _EOF = -1; | |
const int _NEWLINE = 10; | |
const int _EXCLAIM = 33; | |
const int _QUOTE = 34; | |
const int _HASH = 35; | |
const int _AMP = 38; | |
const int _APOS = 39; | |
const int _FORWARD_SLASH = 47; | |
const int _LT = 60; | |
const int _GT = 62; | |
const int _CARET = 94; | |
const int _OPEN_MUSTACHE = 123; | |
const int _CLOSE_MUSTACHE = 125; | |
class _Token implements Token { | |
_Token(this.type, this.value); | |
_Token.fromChar(this.type, int charCode) | |
: value = new String.fromCharCode(charCode); | |
final int type; | |
final String value; | |
toString() => "${tokenTypeString(type)}: \"${value.replaceAll('\n', '\\n')}\""; | |
} | |
class _Scanner { | |
_Scanner(String source) : _r = new _CharReader(source); | |
_CharReader _r; | |
List<Token> _tokens = new List<Token>(); | |
int _read() => _r.read(); | |
int _peek() => _r.peek(); | |
_add(Token t) => _tokens.add(t); | |
_expect(int c) { | |
if (c != _read()) | |
throw new FormatException('Expected character: ${new String.fromCharCode(c)}'); | |
} | |
String _readString() => _r.readWhile( | |
(c) => c != _OPEN_MUSTACHE && c != _CLOSE_MUSTACHE && c != _EOF); | |
List<Token> scan() { | |
while(true) { | |
switch(_peek()) { | |
case _EOF: | |
return _tokens; | |
case _OPEN_MUSTACHE: | |
_scanMustacheTag(); | |
break; | |
default: | |
_scanText(); | |
} | |
} | |
} | |
_scanText() { | |
while(true) { | |
switch(_peek()) { | |
case _EOF: | |
return; | |
case _OPEN_MUSTACHE: | |
return; | |
case _CLOSE_MUSTACHE: | |
_read(); | |
_add(new _Token.fromChar(TEXT, _CLOSE_MUSTACHE)); | |
break; | |
default: | |
_add(new _Token(TEXT, _readString())); | |
} | |
} | |
} | |
_scanMustacheTag() { | |
assert(_peek() == _OPEN_MUSTACHE); | |
_read(); | |
if (_peek() != _OPEN_MUSTACHE) { | |
_add(new _Token.fromChar(TEXT, _OPEN_MUSTACHE)); | |
return; | |
} | |
_read(); | |
switch(_peek()) { | |
case _EOF: | |
throw new FormatException('Unexpected EOF.'); | |
// Escaped text {{{ ... }}} | |
case _OPEN_MUSTACHE: | |
_read(); | |
_add(new _Token(TEXT, _readString())); | |
_expect(_CLOSE_MUSTACHE); | |
break; | |
// Escaped text {{& ... }} | |
case _AMP: | |
_read(); | |
_add(new _Token(TEXT, _readString())); | |
break; | |
// Comment {{! ... }} | |
case _EXCLAIM: | |
_read(); | |
_add(new _Token(COMMENT, _readString())); | |
break; | |
// Partial {{> ... }} | |
case _GT: | |
_read(); | |
_add(new _Token(PARTIAL, _readString())); | |
break; | |
// Open section {{# ... }} | |
case _HASH: | |
_read(); | |
_add(new _Token(OPEN_SECTION, _readString())); | |
break; | |
// Open inverted section {{^ ... }} | |
case _CARET: | |
_read(); | |
_add(new _Token(OPEN_INV_SECTION, _readString())); | |
break; | |
// Close section {{/ ... }} | |
case _FORWARD_SLASH: | |
_read(); | |
_add(new _Token(CLOSE_SECTION, _readString())); | |
break; | |
// Variable {{ ... }} | |
default: | |
_add(new _Token(VARIABLE, _readString())); | |
} | |
_expect(_CLOSE_MUSTACHE); | |
_expect(_CLOSE_MUSTACHE); | |
} | |
} | |
//FIXME return _EOF | |
class _CharReader { | |
_CharReader(String source) | |
: _source = source, | |
_itr = source.runes.iterator { //FIXME runes etc. Not sure if this is the right count. | |
if (source == null) | |
throw new ArgumentError('Source is null.'); | |
_i = 0; | |
if (source == '') { | |
_c = _EOF; | |
} else { | |
_itr.moveNext(); | |
_c = _itr.current; | |
} | |
} | |
String _source; | |
Iterator<int> _itr; | |
int _i, _c; | |
int read() { | |
var c = _c; | |
if (_itr.moveNext()) { | |
_i++; | |
_c = _itr.current; | |
} else { | |
_c = _EOF; | |
} | |
return c; | |
} | |
int peek() => _c; | |
String readWhile(bool test(int charCode)) { | |
if (peek() == _EOF) | |
throw new FormatException('Unexpected end of input: $_i'); | |
int start = _i; | |
while (peek() != _EOF && test(peek())) { | |
read(); | |
} | |
int end = peek() == _EOF ? _source.length : _i; | |
return _source.slice(start, end); | |
} | |
} |