Last active
December 16, 2023 09:26
-
-
Save nyteshade/0e36a8e004ef86b241f8d1968c8c7bfb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The Walker class provides a mechanism for iterating through a string and | |
* counting occurrences of specified opening and closing characters. It's designed | |
* to start counting when the first instance of the 'open' character is encountered | |
* and stops when the count drops back to zero. | |
* | |
* @example | |
* const walker = new Walker("{a{b}c}", {when: () => {}, searchFor: Walker.squigly}); | |
* walker.until(); // Process the string | |
* | |
* @property {number} index - Current position in the string. | |
* @property {number} value - Current count of open-minus-close characters. | |
* @property {string} open - Character marking the start of a segment. | |
* @property {string} close - Character marking the end of a segment. | |
* @property {boolean} started - Flag indicating if counting has started. | |
* @property {boolean} done - Flag indicating if counting has finished. | |
* @property {Function} when - Callback function called when counting is done. | |
* @property {string} string - The string to process. | |
* @property {number[]} range - Array holding start and end indices of the span. | |
*/ | |
globalThis.Walker = class Walker { | |
// Initialization of default properties | |
index = 0 | |
value = 0 | |
open = '{' | |
close = '}' | |
started = false | |
range = [0, Infinity] | |
done = false | |
when = undefined | |
/** | |
* Constructs an instance of the Walker class. | |
* | |
* @param {string} string - The string to be processed. | |
* @param {Object} options - Configuration options for the instance. | |
* @param {Function} [options.when] - Callback function for when counting is complete. | |
* @param {string[]} [options.searchFor] - Array with open and close characters. | |
* @param {number} [options.index] - Starting index in the string. | |
*/ | |
constructor( | |
string, | |
options | |
) { | |
let { when, searchFor, index } = options | |
Object.assign(this, { | |
index: index ?? 0, | |
open: searchFor?.[0] ?? 0, | |
close: searchFor?.[1] ?? Infinity, | |
string, | |
when, | |
}); | |
} | |
/** | |
* Returns the class name as a String | |
* | |
* @returns {number} class name as a String | |
*/ | |
get [Symbol.toStringTag]() { | |
return this.constructor.name | |
} | |
/** | |
* Returns the current index value. | |
* | |
* @returns {number} index within the string | |
*/ | |
[Symbol.toPrimitive](hint) { | |
return this.index | |
} | |
/** | |
* Returns the current index value. | |
* | |
* @returns {number} index within the string | |
*/ | |
valueOf() { return this.index } | |
/** | |
* Increases the count. | |
* | |
* @param {number} [by=1] - Amount to increase the count. | |
* @returns {Walker} Instance for chaining. | |
*/ | |
up(by = 1) { this.value += by; return this } | |
/** | |
* Decreases the count. | |
* | |
* @param {number} [by=1] - Amount to decrease the count. | |
* @returns {Walker} Instance for chaining. | |
*/ | |
down(by = 1) { this.value -= by; return this } | |
/** | |
* Extracts and returns a substring from the current string based on the specified | |
* range. The method allows for both inclusive and exclusive extraction of the | |
* substring. | |
* | |
* The `range` property of the class is expected to hold the start and end indices | |
* for the extraction. If `range` is not defined or if any of the indices are | |
* missing, the method falls back to default values: start defaults to 0 and end | |
* defaults to the length of the string minus one. | |
* | |
* @param {boolean} [inclusive=false] - Determines the nature of the extraction: | |
* If true, the characters at the start and end indices are included in the result. | |
* If false, the characters at the start and end indices are excluded from the result. | |
* @returns {string} The extracted substring based on the specified range and | |
* inclusivity. If the range is invalid or if the end index is less than the start | |
* index, an empty string is returned. | |
* @example | |
* // Assuming this.string = "Hello, World!" and this.range = [7, 12] | |
* this.span(true); // Returns "World!" | |
* this.span(false); // Returns "orld" | |
*/ | |
span(inclusive = false) { | |
const end = this.string.length - 1; | |
const start = Math.max(this?.range?.[0] ?? 0, 0); | |
const stop = Math.min(this?.range?.[1] ?? end, end); | |
return (inclusive | |
? this.string.slice(start, stop + 1) | |
: this.string.slice(start + 1, stop) | |
); | |
} | |
/** | |
* Processes the next character in the string. It increments or decrements the | |
* counter based on whether the character matches the open or close character. | |
* | |
* @returns {boolean} False if the iteration is complete, true otherwise. | |
*/ | |
next() { | |
if (this.done) { return false } | |
const [START, STOP] = [0, 1] | |
const currentChar = this.string[this.index] | |
if (currentChar === this.open) { | |
if (!this.started) { | |
this.started = true | |
this.range[START] = this.index | |
} | |
this.up() | |
} else if (currentChar === this.close) { | |
if (this.started) { | |
this.down() | |
if (this.value <= 0) { | |
this.done = true | |
this.range[STOP] = this.index | |
this.when?.(this.span(false)) | |
return false | |
} | |
} | |
} | |
this.index++ | |
if (this.index >= this.string.length) { | |
this.done = true | |
return false | |
} | |
return true | |
} | |
/** | |
* Resets the Walker instance with new parameters. This allows reusing the same | |
* instance with different settings. | |
* | |
* @param {string} [string] - New string to process. | |
* @param {Function} [when] - New callback function. | |
* @param {string[]} [searchFor] - New open and close characters. | |
*/ | |
reset(options = {}) { | |
const { index, string, when, searchFor } = options | |
this.string = (string || this.string) | |
this.when = (when || this.when) | |
this.open = searchFor?.[0] ?? '(' | |
this.close = searchFor?.[1] ?? ')' | |
this.index = (index || this.index) | |
this.value = 0 | |
this.started = false | |
this.done = false | |
this.range = [this.index, Infinity] | |
return this | |
} | |
/** | |
* Continues processing the string until completion or until the count returns | |
* to zero after having started. It iterates over the string, applying the logic | |
* defined in the `next` method, until the iteration is complete. | |
* | |
* @returns {string} The substring from the start to the end of the span found. | |
*/ | |
until() { | |
while (this.next()) {} | |
return this.span(false); | |
} | |
/** | |
* Static getter for the default parenthesis characters. | |
* | |
* @returns {string[]} An array containing the open and close characters for parenthesis. | |
* @example | |
* const walker = new Walker("function (arg) { return arg; }", {searchFor: Walker.parenthesis}); | |
* walker.until(); | |
*/ | |
static get parenthesis() { return ['(', ')']; } | |
/** | |
* Static getter for the default squiggly (curly brace) characters. | |
* | |
* @returns {string[]} An array containing the open and close characters for curly braces. | |
* @example | |
* const walker = new Walker("{ let a = 1; }", {searchFor: Walker.squigly}); | |
* walker.until(); | |
*/ | |
static get squigly() { return ['{', '}']; } | |
/** | |
* Static getter for the default square bracket characters. | |
* | |
* @returns {string[]} An array containing the open and close characters for square brackets. | |
* @example | |
* const walker = new Walker("[1, 2, 3]", {searchFor: Walker.square}); | |
* walker.until(); | |
*/ | |
static get square() { return ['[', ']']; } | |
} | |
globalThis.Descriptor = class Descriptor { | |
static base(enumerable = true, configurable = true) { | |
return { enumerable, configurable } | |
} | |
static data(value, writable = true, { enumerable, configurable } = this.base()) { | |
return { value, enumerable, configurable, writable } | |
} | |
static accessor( getter, setter, { store, enumerable, configurable } = this.base()) { | |
if (Object.isObject(store)) { | |
// controversial | |
const injectStore = (fn, store) => { | |
let source = String(fn) | |
source = source.replace(/(?:get|set)(\s*)(\w+)?\((.*?)\)/, 'function$1$2($3)') | |
// We need to find where the opening bracket of the function is, regardless of | |
// whether or not there are newlines or other conflicting characters in the | |
// block. | |
let walker = new Walker(source, { searchFor: Walker.parenthesis }) | |
// Go until paremeters are done | |
walker.until() | |
// Reset to follow { } characters | |
walker.reset({searchFor: Walker.squigly}) | |
walker.until() | |
// Set the range to 1 plus the current index. | |
walker.range[0] + 1 | |
let _store = store | |
source = source.replace(/{/, '{let store = _store;') | |
return eval(`(${source})`) | |
} | |
return { | |
...this.base(), | |
enumerable, | |
configurable, | |
get: getter ? injectStore(getter) : undefined, | |
set: setter ? injectStore(setter) : undefined, | |
} | |
} | |
else { | |
return { ...this.base(), enumerable, configurable, get: getter, set: setter, } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment