Skip to content

Instantly share code, notes, and snippets.

@nyteshade
Last active December 16, 2023 09:26
Show Gist options
  • Save nyteshade/0e36a8e004ef86b241f8d1968c8c7bfb to your computer and use it in GitHub Desktop.
Save nyteshade/0e36a8e004ef86b241f8d1968c8c7bfb to your computer and use it in GitHub Desktop.
/**
* 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