mirror of
https://git.sr.ht/~cadence/cloudtube
synced 2024-11-22 15:47:30 +00:00
176 lines
4.5 KiB
JavaScript
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
|