File: lib/parsers/string.js
/*
* niViz -- snow profiles visualization
* Copyright (C) 2015 WSL/SLF - Fluelastrasse 11 - 7260 Davos Dorf - Switzerland.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function (niviz) {
'use strict';
// --- Module Dependencies ---
var properties = Object.defineProperties;
var Parser = niviz.Parser;
var util = niviz.util;
var format = util.format;
/** @module niviz */
/**
* A simple string/stream parser.
*
* @class StringParser
* @constructor
*
* @extends Parser
*
* @param {String} [data=''] The data to parse.
* @param {String} [delimiter='\n'] The token delimiter.
* @param {String} [trim='\r'] The token trim value.
*/
function StringParser(data, delimiter, trim) {
Parser.call(this, data || '');
/**
* The token delimiter; defaults to a newline.
*
* @property delimiter
* @type String
*/
this.delimiter = delimiter || '\n';
/**
* The token trim value; defaults to a carriage
* return. If present, this value will be cut off
* at the end of each token during parsing.
*
* @property trim
* @type String
*/
this.trim = trim || '\r';
/**
* The current position in the input data.
*
* @property position
* @type Number
* @private
*/
this.position = 0;
/**
* The number of lines/tokens parsed so far.
*
* @property lines
* @type Number
* @private
*/
this.lines = 0;
}
Parser.implement(StringParser, 'string');
properties(StringParser.prototype, {
/**
* Whether or not all data has been parsed.
*
* @property done
* @type Boolean
*/
done: {
get: function () {
return this.position >= this.data.length;
}
}
});
/**
* Returns the next line/token; updates the parser's current
* position pointer and line number as a side effect.
*
* @method next
* @returns {String|undefined} The next token.
*/
StringParser.prototype.next = function () {
if (this.done) return undefined;
var idx = this.data.indexOf(this.delimiter, this.position);
if (idx < 0) idx = this.data.length;
var token = this.data.substring(this.position, idx);
token = trimr(token, this.trim);
this.position = idx + this.delimiter.length;
this.lines++;
return token;
};
/**
* @method peek
* @returns {String} The next k characters.
*/
StringParser.prototype.peek = function (k) {
return this.data.substr(this.position, k || 1);
};
StringParser.prototype.error = function () {
var message = format(Array.prototype.slice.apply(arguments));
var template = 'parser error at %d:0: %s';
return new Error(format(template, this.lines, message));
};
/**
* Like `next()` but skips lines matching `over`
* until the end of data is reached or, if given,
* the next line starts with `until`.
*
* @method skip
*
* @param {RegExp} over Lines to skip.
* @param {String} [until]
*
* @returns {String|undefined} The next line.
*/
StringParser.prototype.skip = function (over, until) {
var next;
do {
next = (until && this.check(until)) ?
undefined : this.next();
} while (next !== undefined && over.test(next));
return next;
};
/**
* Returns true if the next line starts with the
* given string `against`.
*
* @method check
*
* @param {String} against
*
* @returns {Boolean}
*/
StringParser.prototype.check = function (against) {
return this.peek(against.length) === against;
};
/**
* @property _parse
* @returns {Object} The parse result.
*/
StringParser.prototype._parse = function () {
var i = 0;
try {
while (!this.done) {
if (i++ > 500) { // pause after 500 lines
this.pause();
return this.result;
}
this.parse_line(this.next());
}
} catch (error) {
error.message = 'Line ' + this.lines + ': ' + error.message;
return this.emit('error', error);
}
try {
this.verify();
} catch (error) {
error.message = 'Verification error: ' + error.message;
return this.emit('error', error);
}
this.end();
return this.result;
};
/**
* Resumes _parse method after a short timeout; these
* small timeouts effectively give the browser more time
* to breathe and are a remedy for the 'long running script'
* warnings in Firefox.
*
* @method pause
*/
StringParser.prototype.pause = function () {
var that = this;
setTimeout(function () {
that._parse();
}, 15);
};
/**
* Parses a single line. This method should be
* implemented by all subclasses.
*
* @method parse_line
* @chainable
*
* @abstract
*
* @param {String} line The line to parse.
*/
StringParser.prototype.parse_line = function (line) {
throw new Error('not implemented');
};
// --- Helpers ---
function trimr(string, value) {
var k = string.length;
if (k === 0 || string[k - 1] !== value)
return string;
return string.slice(0, k - 1);
}
}(niviz));