/* String streams are the things fed to parsers (which can feed them * to a tokenizer if they want). They provide peek and next methods * for looking at the current character (next 'consumes' this * character, peek does not), and a get method for retrieving all the * text that was consumed since the last time get was called. * * An easy mistake to make is to let a StopIteration exception finish * the token stream while there are still characters pending in the * string stream (hitting the end of the buffer while parsing a * token). To make it easier to detect such errors, the strings throw * an exception when this happens. */ // Make a string stream out of an iterator that returns strings. This // is applied to the result of traverseDOM (see codemirror.js), and // the resulting stream is fed to the parser. window.stringStream = function(source){ // String that's currently being iterated over. var current = ""; // Position in that string. var pos = 0; // Accumulator for strings that have been iterated over but not // get()-ed yet. var accum = ""; // Make sure there are more characters ready, or throw // StopIteration. function ensureChars() { while (pos == current.length) { accum += current; current = ""; // In case source.next() throws pos = 0; try {current = source.next();} catch (e) { if (e != StopIteration) throw e; else return false; } } return true; } return { // Return the next character in the stream. peek: function() { if (!ensureChars()) return null; return current.charAt(pos); }, // Get the next character, throw StopIteration if at end, check // for unused content. next: function() { if (!ensureChars()) { if (accum.length > 0) throw "End of stringstream reached without emptying buffer ('" + accum + "')."; else throw StopIteration; } return current.charAt(pos++); }, // Return the characters iterated over since the last call to // .get(). get: function() { var temp = accum; accum = ""; if (pos > 0){ temp += current.slice(0, pos); current = current.slice(pos); pos = 0; } return temp; }, // Push a string back into the stream. push: function(str) { current = current.slice(0, pos) + str + current.slice(pos); }, lookAhead: function(str, consume, skipSpaces, caseInsensitive) { function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} str = cased(str); var found = false; var _accum = accum, _pos = pos; if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/); while (true) { var end = pos + str.length, left = current.length - pos; if (end <= current.length) { found = str == cased(current.slice(pos, end)); pos = end; break; } else if (str.slice(0, left) == cased(current.slice(pos))) { accum += current; current = ""; try {current = source.next();} catch (e) {break;} pos = 0; str = str.slice(left); } else { break; } } if (!(found && consume)) { current = accum.slice(_accum.length) + current; pos = _pos; accum = _accum; } return found; }, // Utils built on top of the above more: function() { return this.peek() !== null; }, applies: function(test) { var next = this.peek(); return (next !== null && test(next)); }, nextWhile: function(test) { var next; while ((next = this.peek()) !== null && test(next)) this.next(); }, matches: function(re) { var next = this.peek(); return (next !== null && re.test(next)); }, nextWhileMatches: function(re) { var next; while ((next = this.peek()) !== null && re.test(next)) this.next(); }, equals: function(ch) { return ch === this.peek(); }, endOfLine: function() { var next = this.peek(); return next == null || next == "\n"; } }; };