OLD | NEW |
(Empty) | |
| 1 part of mustache; |
| 2 |
| 3 //FIXME Temporarily made public for testing. |
| 4 //List<_Token> scan(String source, bool lenient) => _scan(source, lenient); |
| 5 //List<_Token> trim(List<_Token> tokens) => _trim(tokens); |
| 6 |
| 7 List<_Token> _scan(String source, bool lenient) => _trim(new _Scanner(source).sc
an()); |
| 8 |
| 9 const int _TEXT = 1; |
| 10 const int _VARIABLE = 2; |
| 11 const int _PARTIAL = 3; |
| 12 const int _OPEN_SECTION = 4; |
| 13 const int _OPEN_INV_SECTION = 5; |
| 14 const int _CLOSE_SECTION = 6; |
| 15 const int _COMMENT = 7; |
| 16 const int _UNESC_VARIABLE = 8; |
| 17 const int _WHITESPACE = 9; // Should be filtered out, before returned by scan. |
| 18 const int _LINE_END = 10; // Should be filtered out, before returned by scan. |
| 19 |
| 20 //FIXME make private |
| 21 tokenTypeString(int type) => [ |
| 22 '?', |
| 23 'Text', |
| 24 'Var', |
| 25 'Par', |
| 26 'Open', |
| 27 'OpenInv', |
| 28 'Close', |
| 29 'Comment', |
| 30 'UnescVar', |
| 31 'Whitespace', |
| 32 'LineEnd'][type]; |
| 33 |
| 34 const int _EOF = -1; |
| 35 const int _TAB = 9; |
| 36 const int _NEWLINE = 10; |
| 37 const int _RETURN = 13; |
| 38 const int _SPACE = 32; |
| 39 const int _EXCLAIM = 33; |
| 40 const int _QUOTE = 34; |
| 41 const int _APOS = 39; |
| 42 const int _HASH = 35; |
| 43 const int _AMP = 38; |
| 44 const int _PERIOD = 46; |
| 45 const int _FORWARD_SLASH = 47; |
| 46 const int _LT = 60; |
| 47 const int _GT = 62; |
| 48 const int _CARET = 94; |
| 49 |
| 50 const int _OPEN_MUSTACHE = 123; |
| 51 const int _CLOSE_MUSTACHE = 125; |
| 52 |
| 53 // Takes a list of tokens, and removes _NEWLINE, and _WHITESPACE tokens. |
| 54 // This is used to implement mustache standalone lines. |
| 55 // Where TAG is one of: OPEN_SECTION, INV_SECTION, CLOSE_SECTION |
| 56 // LINE_END, [WHITESPACE], TAG, [WHITESPACE], LINE_END => LINE_END, TAG |
| 57 // WHITESPACE => TEXT |
| 58 // LINE_END => TEXT |
| 59 //TODO Consecutive text tokens will also be merged into a single token. (Do in a
separate merge func). |
| 60 List<_Token> _trim(List<_Token> tokens) { |
| 61 int i = 0; |
| 62 _Token read() { var ret = i < tokens.length ? tokens[i++] : null; /* pri
nt('Read: $ret'); */ return ret; } |
| 63 _Token peek([int n = 0]) => i + n < tokens.length ? tokens[i + n] : null
; |
| 64 |
| 65 bool isTag(token) => |
| 66 token != null |
| 67 && (token.type == _OPEN_SECTION |
| 68 || token.type == _OPEN_INV_SECTION |
| 69 || token.type == _CLOSE_SECTION |
| 70 || token.type == _COMMENT); |
| 71 |
| 72 bool isWhitespace(token) => token != null && token.type == _WHITESPACE; |
| 73 bool isLineEnd(token) => token != null && token.type == _LINE_END; |
| 74 |
| 75 var result = new List<_Token>(); |
| 76 add(token) => result.add(token); |
| 77 |
| 78 standaloneLineCheck() { |
| 79 // Swallow leading whitespace |
| 80 // Note, the scanner will only ever create a single whitespace t
oken. There |
| 81 // is no need to handle multiple whitespace tokens. |
| 82 if (isWhitespace(peek()) |
| 83 && isTag(peek(1)) |
| 84 && (isLineEnd(peek(2)) || peek(2) == null)) { // null
== EOF |
| 85 read(); |
| 86 } else if (isWhitespace(peek()) |
| 87 && isTag(peek(1)) |
| 88 && isWhitespace(peek(2)) |
| 89 && (isLineEnd(peek(3)) || peek(3) == null)) { |
| 90 read(); |
| 91 } |
| 92 |
| 93 if ((isTag(peek()) && isLineEnd(peek(1))) |
| 94 || (isTag(peek()) |
| 95 && isWhitespace(peek(1)) |
| 96 && (isLineEnd(peek(2)) || peek(2) == null))) {
|
| 97 |
| 98 // Add tag |
| 99 add(read()); |
| 100 |
| 101 // Swallow trailing whitespace. |
| 102 if (isWhitespace(peek())) |
| 103 read(); |
| 104 |
| 105 // Swallow line end. |
| 106 assert(isLineEnd(peek())); |
| 107 read(); |
| 108 |
| 109 standaloneLineCheck(); //FIXME don't use recursion. |
| 110 } |
| 111 } |
| 112 |
| 113 // Handle case where first line is a standalone tag. |
| 114 standaloneLineCheck(); |
| 115 |
| 116 var t; |
| 117 while ((t = read()) != null) { |
| 118 if (t.type == _LINE_END) { |
| 119 // Convert line end to text token |
| 120 add(new _Token(_TEXT, t.value, t.line, t.column)); |
| 121 standaloneLineCheck(); |
| 122 } else if (t.type == _WHITESPACE) { |
| 123 // Convert whitespace to text token |
| 124 add(new _Token(_TEXT, t.value, t.line, t.column)); |
| 125 } else { |
| 126 // Preserve token |
| 127 add(t); |
| 128 } |
| 129 } |
| 130 |
| 131 return result; |
| 132 } |
| 133 |
| 134 class _Token { |
| 135 _Token(this.type, this.value, this.line, this.column); |
| 136 final int type; |
| 137 final String value; |
| 138 final int line; |
| 139 final int column; |
| 140 toString() => "${tokenTypeString(type)}: \"${value.replaceAll('\n', '\\n
')}\" $line:$column"; |
| 141 } |
| 142 |
| 143 class _Scanner { |
| 144 _Scanner(String source) : _r = new _CharReader(source); |
| 145 |
| 146 _CharReader _r; |
| 147 List<_Token> _tokens = new List<_Token>(); |
| 148 |
| 149 int _read() => _r.read(); |
| 150 int _peek() => _r.peek(); |
| 151 |
| 152 _addStringToken(int type) { |
| 153 int l = _r.line, c = _r.column; |
| 154 var value = type == _TEXT ? _readLine() : _readString(); |
| 155 if (type != _TEXT && type != _COMMENT) value = value.trim();
|
| 156 _tokens.add(new _Token(type, value, l, c)); |
| 157 } |
| 158 |
| 159 _addCharToken(int type, int charCode) { |
| 160 int l = _r.line, c = _r.column; |
| 161 var value = new String.fromCharCode(charCode); |
| 162 _tokens.add(new _Token(type, value, l, c)); |
| 163 } |
| 164 |
| 165 _expect(int expectedCharCode) { |
| 166 int c = _read(); |
| 167 |
| 168 if (c == _EOF) { |
| 169 throw new MustacheFormatException('Unexpected end of inp
ut.', _r.line, _r.column); |
| 170 |
| 171 } else if (c != expectedCharCode) { |
| 172 throw new MustacheFormatException('Unexpected character,
' |
| 173 'expected: ${new String.fromCharCode(expectedCha
rCode)} ($expectedCharCode), ' |
| 174 'was: ${new String.fromCharCode(c)} ($c), ' |
| 175 'at: ${_r.line}:${_r.column}', _r.line, _r.colum
n); |
| 176 } |
| 177 } |
| 178 |
| 179 String _readString() => _r.readWhile( |
| 180 (c) => c != _OPEN_MUSTACHE |
| 181 && c != _CLOSE_MUSTACHE |
| 182 && c != _EOF); |
| 183 |
| 184 String _readLine() => _r.readWhile( |
| 185 (c) => c != _OPEN_MUSTACHE |
| 186 && c != _CLOSE_MUSTACHE |
| 187 && c != _EOF |
| 188 && c != _NEWLINE); |
| 189 |
| 190 // Actually excludes newlines. |
| 191 String _readWhitespace() => _r.readWhile( |
| 192 (c) => c == _SPACE |
| 193 || c == _TAB); |
| 194 |
| 195 List<_Token> scan() { |
| 196 while(true) { |
| 197 switch(_peek()) { |
| 198 case _EOF: |
| 199 return _tokens; |
| 200 case _OPEN_MUSTACHE: |
| 201 _scanMustacheTag(); |
| 202 break; |
| 203 default: |
| 204 _scanText(); |
| 205 } |
| 206 } |
| 207 } |
| 208 |
| 209 _scanText() { |
| 210 while(true) { |
| 211 switch(_peek()) { |
| 212 case _EOF: |
| 213 return; |
| 214 case _OPEN_MUSTACHE: |
| 215 return; |
| 216 case _CLOSE_MUSTACHE: |
| 217 _read(); |
| 218 _addCharToken(_TEXT, _CLOSE_MUSTACHE); |
| 219 break; |
| 220 case _RETURN: |
| 221 _read(); |
| 222 if (_peek() == _NEWLINE) { |
| 223 _read(); |
| 224 _tokens.add(new _Token(_LINE_END
, '\r\n', _r.line, _r.column)); |
| 225 } else { |
| 226 _addCharToken(_TEXT, _RETURN); |
| 227 } |
| 228 break; |
| 229 case _NEWLINE: |
| 230 _read(); |
| 231 _addCharToken(_LINE_END, _NEWLINE); //TO
DO handle \r\n |
| 232 break; |
| 233 case _SPACE: |
| 234 case _TAB: |
| 235 var value = _readWhitespace(); |
| 236 _tokens.add(new _Token(_WHITESPACE, valu
e, _r.line, _r.column)); |
| 237 break; |
| 238 default: |
| 239 _addStringToken(_TEXT); |
| 240 } |
| 241 } |
| 242 } |
| 243 |
| 244 _scanMustacheTag() { |
| 245 _expect(_OPEN_MUSTACHE); |
| 246 |
| 247 // If just a single mustache, return this as a text token. |
| 248 if (_peek() != _OPEN_MUSTACHE) { |
| 249 _addCharToken(_TEXT, _OPEN_MUSTACHE); |
| 250 return; |
| 251 } |
| 252 |
| 253 _expect(_OPEN_MUSTACHE); |
| 254 |
| 255 switch(_peek()) { |
| 256 case _EOF: |
| 257 throw new MustacheFormatException('Unexpected en
d of input.', _r.line, _r.column); |
| 258 |
| 259 // Escaped text {{{ ... }}} |
| 260 case _OPEN_MUSTACHE: |
| 261 _read(); |
| 262 _addStringToken(_UNESC_VARIABLE); |
| 263 _expect(_CLOSE_MUSTACHE); |
| 264 break; |
| 265 |
| 266 // Escaped text {{& ... }} |
| 267 case _AMP: |
| 268 _read(); |
| 269 _addStringToken(_UNESC_VARIABLE); |
| 270 break; |
| 271 |
| 272 // Comment {{! ... }} |
| 273 case _EXCLAIM: |
| 274 _read(); |
| 275 _addStringToken(_COMMENT); |
| 276 break; |
| 277 |
| 278 // Partial {{> ... }} |
| 279 case _GT: |
| 280 _read(); |
| 281 _addStringToken(_PARTIAL); |
| 282 break; |
| 283 |
| 284 // Open section {{# ... }} |
| 285 case _HASH: |
| 286 _read(); |
| 287 _addStringToken(_OPEN_SECTION); |
| 288 break; |
| 289 |
| 290 // Open inverted section {{^ ... }} |
| 291 case _CARET: |
| 292 _read(); |
| 293 _addStringToken(_OPEN_INV_SECTION); |
| 294 break; |
| 295 |
| 296 // Close section {{/ ... }} |
| 297 case _FORWARD_SLASH: |
| 298 _read(); |
| 299 _addStringToken(_CLOSE_SECTION); |
| 300 break; |
| 301 |
| 302 // Variable {{ ... }} |
| 303 default: |
| 304 _addStringToken(_VARIABLE); |
| 305 } |
| 306 |
| 307 _expect(_CLOSE_MUSTACHE); |
| 308 _expect(_CLOSE_MUSTACHE); |
| 309 } |
| 310 } |
| 311 |
OLD | NEW |