1
0
mirror of https://git.sr.ht/~cadence/cloudtube synced 2024-11-13 20:07:30 +00:00
cloudtube/utils/parser.js
2021-05-12 00:29:44 +12:00

176 lines
4.5 KiB
JavaScript

/**
* @typedef GetOptions
* @property {string} [split] Characters to split on
* @property {string} [mode] "until" or "between"; choose where to get the content from
* @property {function} [transform] Transformation to apply to result before returning
*/
const tf = {
lc: s => s.toLowerCase()
}
class Parser {
constructor(string) {
this.string = string;
this.substore = [];
this.cursor = 0;
this.cursorStore = [];
this.mode = "until";
this.transform = s => s;
this.split = " ";
}
/**
* Return all the remaining text from the buffer, without updating the cursor
* @return {string}
*/
remaining() {
return this.string.slice(this.cursor);
}
/**
* Have we reached the end of the string yet?
* @return {boolean}
*/
hasRemaining() {
return this.cursor < this.string.length
}
/**
* Get the next element from the buffer, either up to a token or between two tokens, and update the cursor.
* @param {GetOptions} [options]
* @returns {string}
*/
get(options = {}) {
["mode", "split", "transform"].forEach(o => {
if (!options[o]) options[o] = this[o];
});
if (options.mode == "until") {
let next = this.string.indexOf(options.split, this.cursor+options.split.length-1);
if (next == -1) {
let result = this.remaining();
this.cursor = this.string.length;
return result;
} else {
let result = this.string.slice(this.cursor, next);
this.cursor = next + options.split.length;
return options.transform(result);
}
} else if (options.mode == "between") {
let start = this.string.indexOf(options.split, this.cursor);
let end = this.string.indexOf(options.split, start+options.split.length);
let result = this.string.slice(start+options.split.length, end);
this.cursor = end + options.split.length;
return options.transform(result);
}
}
/**
* Get a number of chars from the buffer.
* @param {number} length Number of chars to get
* @param {boolean} [move] Whether to update the cursor
*/
slice(length, move = false) {
let result = this.string.slice(this.cursor, this.cursor+length);
if (move) this.cursor += length;
return result;
}
/**
* Repeatedly swallow a character.
* @param {string} char
*/
swallow(char) {
let before = this.cursor;
while (this.string[this.cursor] == char) this.cursor++;
return this.cursor - before;
}
/**
* Push the current cursor position to the store
*/
store() {
this.cursorStore.push(this.cursor);
}
/**
* Pop the previous cursor position from the store
*/
restore() {
this.cursor = this.cursorStore.pop();
}
/**
* Run a get operation, test against an input, return success or failure, and restore the cursor.
* @param {string} value The value to test against
* @param {object} options Options for get
*/
test(value, options) {
this.store();
let next = this.get(options);
let result = next == value;
this.restore();
return result;
}
/**
* Run a get operation, test against an input, return success or failure, and restore the cursor on failure.
* @param {string} value The value to test against
* @param {object} options Options for get
*/
has(value, options) {
this.store();
let next = this.get(options);
let result = next == value;
if (!result) this.restore();
return result;
}
/**
* Run a get operation, test against an input, and throw an error if it doesn't match.
* @param {string} value
* @param {GetOptions} [options]
*/
expect(value, options = {}) {
let next = this.get(options);
if (next != value) throw new Error("Expected "+value+", got "+next);
}
/**
* Seek to or past the next occurance of the string.
* @param {string} toFind
* @param {{moveToMatch?: boolean, useEnd?: boolean}} options both default to false
*/
seek(toFind, options = {}) {
if (options.moveToMatch === undefined) options.moveToMatch = false
if (options.useEnd === undefined) options.useEnd = false
let index = this.string.indexOf(toFind, this.cursor)
if (index !== -1) {
index -= this.cursor
if (options.useEnd) index += toFind.length
if (options.moveToMatch) this.cursor += index
}
return index
}
/**
* Replace the current string, adding the old one to the substore.
* @param {string} string
*/
pushSubstore(string) {
this.substore.push({string: this.string, cursor: this.cursor, cursorStore: this.cursorStore})
this.string = string
this.cursor = 0
this.cursorStore = []
}
/**
* Replace the current string with the first entry from the substore.
*/
popSubstore() {
Object.assign(this, this.substore.pop())
}
}
module.exports.Parser = Parser