Skip to content

Instantly share code, notes, and snippets.

@nyteshade
Last active April 20, 2024 05:41
Show Gist options
  • Save nyteshade/940fca6e64533a93a53c626aa2dc66f4 to your computer and use it in GitHub Desktop.
Save nyteshade/940fca6e64533a93a53c626aa2dc66f4 to your computer and use it in GitHub Desktop.
ParamParser and HTML
function ship(code) {
const _ne = Object.entries(code)
.reduce((a, [k,v]) => ({ ...a, [k]:v }), {});
const { defaults = {} } = code;
const [_den, _de] = Object.entries(defaults)?.[0];
if (typeof module !== 'undefined' && module.exports) {
module.exports = _ne || {};
}
else if (_den && _de) { globalThis[_den] = _de; }
}
let {Symkeys} = require('./symkeys');
let {ParamParser} = require('./param.parser');
const HTML = new Proxy(
class HTML {
/**
* Creates an HTML element based on provided arguments, which can
* include element name, content, styles, attributes,
* webComponentName, useDocument, and children. This method
* dynamically parses the arguments using the configured parsers
* and constructs the element accordingly.
*
* @param {...any} args - Arguments that can include various
* configurations for the element creation such as name, content,
* styles, attributes, webComponentName, useDocument, and children.
* @returns {Element} The newly created HTML element.
*
* @example
* // There are three primary signatures for this function; each
* // has its pros and cons but is most ideal with the Proxy
* // getter applied.
*
* // - tagName is the only required parameter
* // - content becomes a TextNode element
* // - styles is a JavaScript style CSS object
* // - attributes is a JavaScript object with attribute key/value
* // pairs defined inside
* // - webComponentName is the name of the custom registered HTML
* // element if that is the type of element being created.
* // - document defaults to `top.document` but can be pointed to
* // another such as one from within an `<iframe>`
* HTML.create(
* tagName, content, styles, attributes,
* webComponentName, document
* )
*
* HTML.create(tagName, {
* content, styles, attributes, webComponentName, document
* })
*
* HTML.create(
* tagName, [Element, Element, Element, ...], styles,
* attributes, webComponentName, document
* )
*
* @example
* // With the Proxy in place, however, HTML becomes even more
* // exciting.
* HTML.div([
* HTML.h1('Title'),
* HTML.p('lorem ipsum dolor...'),
* ])
* // creates <div><h1>Title</h1><p>lorem ipsum dolor...</p></div>
*/
static create(...args) {
const parsers = HTML.Parsers
const { success, _opts } = ParamParser.tryParsers(args, parsers)
const doc = top.document;
const options = _opts.webComponentName
? { is: _opts.webComponentName }
: undefined;
const element = doc.createElement(name, options);
for (const [key, value] of Object.entries(_opts.attributes)) {
element.setAttribute(key, value);
}
for (const [key, value] of Object.entries(_opts.style)) {
element.style[key] = value;
}
if (typeof _opts.content === 'string' && _opts.content) {
element.append(doc.createTextNode(_opts.content));
}
for (const child of _opts.children) {
element.append(child);
}
return element;
}
/**
* Appends a newly created HTML element to a specified target element
* identified by a CSS selector. This method provides a fluent interface
* allowing for the chaining of `create` and `appendTo` calls.
*
* The `create` method dynamically creates an HTML element based on
* provided arguments, which can include element name, content, styles,
* attributes, webComponentName, useDocument, and children. The created
* element is then appended to the target element.
*
* The `appendTo` method allows for appending additional elements to the
* target, facilitating the creation of complex DOM structures in a
* declarative manner.
*
* @param {string} selector - The CSS selector of the target element to
* which the created element will be appended.
* @returns {Object} An object containing the `create` and `appendTo`
* methods for chaining.
*
* @example
* HTML.appendTo('#container').create('div', {
* content: 'Hello, World!',
* style: { color: 'blue' }
* });
* // This will find the element with id 'container' and append a new
* // div element with the text 'Hello, World!' and text color blue.
*/
static appendTo(selector) {
const deferred = { promise: null, resolve: null, reject: null }
deferred.promise = new Promise((resolve, reject) =>
Object.assign(deferred, { resolve, reject })
)
const target = document.querySelector(selector)
return {
create(...args) {
const element = HTML.create(...args);
target?.append(element);
return element;
},
appendTo(...args) {
return HTML.appendTo(...args)
},
}
}
static get HTMLInvalidParametersError() {
return class HTMLInvalidParametersError extends Error { }
}
static get Parsers() {
return [
this.OrderedParser,
this.ObjectLiteralParser,
this.ChildrenArrayParser,
]
}
/**
* Represents a parser that processes parameters in a predefined
* order. This class extends `ParamParser` to implement validation
* and parsing logic specific to ordered parameter sets for HTML
* element creation.
*/
static OrderedParser = class extends ParamParser {
/**
* Validates the parameters for creating an HTML element to
* ensure they meet the expected criteria. Specifically, it
* checks that the name is a non-empty string and that the
* content is neither an object nor an array, which aligns with
* the requirements for simple text or HTML content.
*
* @param {Array} params - An array containing the parameters
* to validate, structured as [name, content, style, attributes,
* children, webComponentName, useDocument].
* @returns {boolean} - True if the parameters meet the
* validation criteria, false otherwise.
*
* @example
* validate([
* 'div', 'Hello, World!', {}, {}, [],
* 'my-web-component', document
* ]);
* // returns true if 'name' is a non-empty string and
* // 'content' is not an object or array, false otherwise.
*/
validate([
name, content, style, attributes,
children, webComponentName, useDocument
]) {
return (
(typeof name === 'string' && name.length) &&
(typeof content !== 'object' && !Array.isArray(content))
)
}
/**
* Parses the provided parameters into an object suitable for HTML
* element creation. This method organizes the parameters into a
* structured format, facilitating their use in element creation
* processes.
*
* @param {Array} params - The parameters for HTML element creation,
* structured as [name, content, style, attributes, children,
* webComponentName, useDocument].
* @returns {Object} An object containing the parsed parameters.
*
* @example
* const parsedParams = parse([
* 'div', 'Hello, World!', {color: 'red'}, {'id': 'greeting'},
* [], 'my-web-component', document
* ]);
* // Returns:
* // {
* // name: 'div',
* // content: 'Hello, World!',
* // style: {color: 'red'},
* // attributes: {'id': 'greeting'},
* // children: [],
* // webComponentName: 'my-web-component',
* // useDocument: document
* // }
*/
parse([
name, content, style, attributes,
children, webComponentName, useDocument
]) {
return {
name, content, style, attributes,
children, webComponentName, useDocument
}
}
}
static ObjectLiteralParser = class extends ParamParser {
validate([
name, content, style, attributes,
children, webComponentName, useDocument
]) {
// list of valid and expected parameters for the HTML instance
const params = [
'name', 'style', 'attributes', 'webComponentName', 'content',
'useDocument', 'children',
];
return (
// validate name is supplied and is a valid string
(typeof name === 'string' && name.length) &&
// validate content was supplied as an object and not a string
(typeof content === 'object' && content !== null) &&
// validate content object contains at least some of the
// expected ordered parameters
params.some(param => Reflect.has(content, param))
)
}
parse([
name, content, style, attributes,
children, webComponentName, useDocument
]) {
// validate the values supplied to the parser for
// further consumption.
const _style = style || {}
const _attrs = attributes || {}
const _children = children || []
const _doc = useDocument || top.document;
return {
name, content, style: _style, attributes: _attrs,
children: _children, webComponentName, useDocument: _doc
}
}
}
static ChildrenArrayParser = class extends ParamParser {
validate([
name, content, style, attributes,
children, webComponentName, useDocument
]) {
// For this parser to succeed, name must be valid
const validName =
typeof name === 'string' && name.length;
const contentsAreChildren = (
// The supplied content parameter must be an array
(Array.isArray(content) && content.length) &&
// And at least one of the elements must be of type Element
(content.filter(e => e instanceof Element)?.length)
);
return validName && contentsAreChildren
}
parse([
name, content, style, attributes,
children, webComponentName, useDocument
]) {
return {
name,
content: undefined,
style: style || {},
attributes: attributes || {},
children: content.filter(e => e instanceof Element),
webComponentName,
useDocument: useDocument || top.document
}
}
}
},
{
get(target, property, receiver) {
if (typeof property === 'string' && property !== 'create') {
return target.create?.bind(target, property);
}
if (property === 'prototype') {
return Object.getPrototypeOf(_TagBoundProxy)
}
return Reflect.get(target, property, receiver);
}
}
)
console.log('Trying to ship ', {HTML})
ship({
HTML,
defaults: { HTML },
})
class ParamParser {
/**
*
*
* @param {any[]} parameters arguments passed in by the process.
* @param {((any[]) => boolean)?} validator an optional way to
* specify a validator without subclassing ParamParser
* @param {((any[]) => object)?} parser an optional way to specify
* a parser without subclassing ParamParser
*/
constructor(parameters, validator = () => {}, parser = () => {}) {
this.args = parameters
this.parser = parser
this.validator = validator
this.result = undefined
this.success = this.validate(this.args)
if (this.success) {
this.results = this.parse(this.args)
}
}
/**
* @param {object} args arguments that were previously validated
* by either the overloaded validate() method or the supplied
* validator closure.
* @returns {object} returns the output object, or an empty
* object, after parsing the input arguments or parameters.
*/
parse(args) {
return this.parser?.(args);
}
/**
* Walk the arguments and determine if the supplied input is
* a valid parsing.
*
* @param {any[]} args arguments supplied by the process.
* @returns {boolean} `true` if the validation is successful,
* `false` otherwise.
*/
validate(args) {
return this.validator?.(args);
}
/**
* Attempts to parse the given parameters using the provided parsers, throwing an
* error if no valid parser is found. This method serves as a convenience wrapper
* around `safeTryParsers`, enforcing strict parsing by automatically enabling
* error throwing on failure.
*
* @param {any[]} parameters - The parameters to be parsed.
* @param {Function[]} parsers - An array of `ParamParser` subclasses to attempt
* parsing with.
* @returns {Object} An object containing the parsing result, with a `success`
* property indicating if parsing was successful, and a `data` property containing
* the parsed data if successful.
* @example
* const parameters = ['param1', 'param2'];
* const parsers = [Parser1, Parser2];
* const result = ParamParser.tryParsers(parameters, parsers);
* if (result.success) {
* console.log('Parsing successful:', result.data);
* } else {
* console.error('Parsing failed.');
* }
*/
static tryParsers(parameters, parsers) {
return this.safeTryParsers(parameters, parsers, true)
}
/**
* Tries parsing `parameters` with each parser in `parsers`. If
* `throwOnFail` is true, throws an error when validation fails or
* no valid parser is found.
*
* This method attempts to parse the given parameters using the
* provided list of parsers. It validates the input to ensure both
* `parameters` and `parsers` are arrays and that `parsers`
* contains at least one valid `ParamParser` subclass. If
* `throwOnFail` is set to true, it will throw specific errors for
* invalid inputs or when no parser succeeds. Otherwise, it returns
* an object indicating the success status and the result of
* parsing, if successful.
*
* @param {any[]} parameters - The parameters to be parsed.
* @param {Function[]} parsers - An array of `ParamParser`
* subclasses to attempt parsing with.
* @param {boolean} [throwOnFail=false] - Whether to throw an
* error on failure.
* @returns {{success: boolean, data: any}} An object with a
* `success` flag and `data` containing the parsing result, if
* successful.
* @throws {ParametersMustBeArrayError} If `parameters` or
* `parsers` are not arrays when `throwOnFail` is true.
* @throws {ParsersArrayMustContainParsersError} If `parsers`
* does not contain at least one valid `ParamParser` subclass
* when `throwOnFail` is true.
* @throws {NoValidParsersFound} If no valid parser is found
* and `throwOnFail` is true.
* @example
* const parameters = ['param1', 'param2'];
* const parsers = [Parser1, Parser2];
* const result = ParamParser.safeTryParsers(
* parameters, parsers, true
* );
*
* if (result.success) {
* console.log('Parsing successful:', result.data);
* } else {
* console.error('Parsing failed.');
* }
*/
static safeTryParsers(parameters, parsers, throwOnFail = false) {
if (!Array.isArray(parameters) || !Array.isArray(parsers)) {
if (throwOnFail) {
throw new this.ParametersMustBeArrayError(
`${this.name}.tryParsers must receive two arrays as args`
);
}
}
if (!parsers.some(parser => parser?.prototype instanceof ParamParser &&
typeof parser === 'function')) {
if (throwOnFail) {
throw new this.ParsersArrayMustContainParsersError(
`${this.name}.tryParsers parsers argument must contain at least one ` +
`ParamParser derived class`
);
}
}
let success = false;
let result = undefined;
for (let Parser of parsers) {
const parser = new Parser(parameters);
if (parser.success) {
success = true;
result = parser.result;
break;
}
}
if (!success && throwOnFail) {
throw new this.NoValidParsersFound('No valid parsers found');
}
return { success, data: result };
}
/**
* A custom error class that signifies no valid parsers were found
* during the parsing process. This error is thrown when all
* parsers fail to parse the given parameters and the `throwOnFail`
* flag is set to true in the `safeTryParsers` method.
*
* @returns {Function} A class extending Error, representing a
* specific error when no valid parsers are found.ound.
*
* @example
* try {
* const result = ParamParser.safeTryParsers(
* parameters, parsers, true
* );
* } catch (error) {
* if (error instanceof ParamParser.NoValidParsersFound) {
* console.error(
* 'No valid parsers could process the parameters.'
* );
* }
* }
*/
static get NoValidParsersFound() {
return class NoValidParsersFound extends Error { }
}
/**
* Represents an error thrown when the parameters provided to a method
* are not in an array format as expected. This class extends the
* native JavaScript `Error` class, allowing for instances of this
* error to be thrown and caught using standard error handling
* mechanisms in JavaScript.
*
* This error is specifically used in scenarios where a method
* expects its arguments to be provided as an array, and the
* validation of those arguments fails because they were not
* provided in an array format. It serves as a clear indicator
* of the nature of the error to developers, enabling them to
* quickly identify and rectify the issue in their code.
*
* @example
* try {
* ParamParser.safeTryParsers(nonArrayParameters, parsers, true);
* } catch (error) {
* if (error instanceof ParamParser.ParametersMustBeArrayError) {
* console.error('Parameters must be provided as an array.');
* }
* }
*/
static get ParametersMustBeArrayError() {
return class ParametersMustBeArrayError extends Error { }
}
/**
* A custom error class indicating that the parsers array does not
* contain valid parser functions. This error is thrown when the
* validation of parsers within `ParamParser.safeTryParsers` fails
* to find any instance that is a subclass of `ParamParser`. It
* extends the native JavaScript `Error` class, allowing it to be
* thrown and caught using standard error handling mechanisms.
*
* This error serves as a clear indicator to developers that the
* provided array of parsers does not meet the expected criteria,
* specifically that it must contain at least one valid parser
* that extends `ParamParser`. This ensures that the parsing
* process can be executed with at least one valid parser function.
*
* @example
* try {
* ParamParser.safeTryParsers(parameters, [], true);
* } catch (error) {
* const { ParsersArrayMustContainParsersError } = ParamParser
* if (error instanceof ParsersArrayMustContainParsersError) {
* console.error(
* 'The parsers array must contain at least one valid parser.'
* );
* }
* }
*/
static get ParsersArrayMustContainParsersError() {
return class ParsersArrayMustContainParsersError extends Error { }
}
}
ship({ ParamParser, defaults: { ParamParser } })
function ship(code) {
const _ne = Object.entries(code)
.reduce((a, [k,v]) => ({ ...a, [k]:v }), {});
const { defaults = {} } = code;
const [_den, _de] = Object.entries(defaults)?.[0];
if (typeof module !== 'undefined' && module.exports) {
module.exports = _ne || {};
}
else if (_den && _de) { globalThis[_den] = _de; }
}
/**
* Represents a secure container for storing and retrieving unique symbols
* associated with data. This class provides methods to add new symbols to
* the Symkeys and to retrieve data associated with a particular symbol.
*
* @example
* // Create a new Symkeys instance
* const symkeys = new Symkeys();
*
* // Add a symbol with associated data to the Symkeys
* const mySymbol = Symkeys.add('myIdentifier', { foo: 'bar' });
*
* // Retrieve the data using the symbol
* const myData = Symkeys.dataFor(mySymbol);
* console.log(myData); // Output: { foo: 'bar' }
*/
class Symkeys {
/**
* Adds a new entry to the Symkeys with a unique symbol based on the provided
* name and associates it with the given data.
*
* @param named - The base name for the symbol to be created.
* @param [associatedData={}] - The data to associate with the symbol.
* @returns The unique symbol created for the entry.
*
* @example
* // Add an entry with associated data
* const symbol = Symkeys.add('myEntry', { foo: 'bar' });
* // Retrieve the associated data using the symbol
* const data = Symkeys.dataFor(symbol);
* console.log(data); // Output: { foo: 'bar' }
*/
add(named, associatedData = {}) {
// Generate a unique token for the symbol
const token = Symkeys.token;
// Calculate a name (optionally with domain and separator)
const symName = this.calculateName(named)
// Create a symbol using the provided name and the unique token
const symbol = Symbol.for(`${symName} #${token}`);
// Store the symbol and associated data in the Symkeys's internal map
this[Symkeys.kDataKey].set(symbol, associatedData);
// Return the unique symbol for external use
return symbol;
}
/**
* Retrieves the data associated with a given symbol from the Symkeys.
*
* This method allows access to the data that has been associated with a
* particular symbol in the Symkeys. It is useful for retrieving stored
* information when only the symbol is known.
*
* @param symbol - The symbol whose associated data is to be
* retrieved.
* @returns The data associated with the symbol, or undefined if
* the symbol is not found in the Symkeys.
*
* @example
* // Assuming 'mySymbol' is a symbol that has been added to the Symkeys
* // with associated data
* const data = Symkeys.dataFor(mySymbol);
* console.log(data); // Output: The data associated with 'mySymbol'
*/
data(forSymbol) {
return this[Symkeys.kDataKey].get(forSymbol);
}
/**
* Extracts the token part from a symbol created by the `add` method.
*
* This method parses the string representation of a symbol to retrieve
* the unique token that was appended to the symbol's name upon creation.
* It is useful for debugging or for operations that require knowledge of
* the token associated with a symbol.
*
* @param symbol - The symbol whose token is to be extracted.
* @returns The extracted token or undefined if the
* token cannot be extracted.
*
* @example
* // Assuming 'mySymbol' is a symbol created with the name 'myEntry'
* // and a token 'agftofxob6f'
* const token = Symkeys.tokenFor(mySymbol);
* console.log(token); // Output: 'agftofxob6f'
*/
token(forSymbol) {
// Use a regular expression to match the token pattern in the symbol
// description exists on symbol but our JS output target is too old
return /^.* \#(.*?)$/.exec(forSymbol).description?.[1];
}
/**
* Retrieves an iterator for the symbols stored in the Symkeys.
*
* This method provides access to the symbols that have been stored in
* the Symkeys. It returns an iterator which can be used to loop over
* all the symbols. This is particularly useful for iterating through
* all stored data without knowing the individual symbols in advance.
*
* @returns An iterator that yields all the symbols
* stored in the Symkeys.
*
* @example
* // Assuming the Symkeys has symbols stored
* for (const symbol of Symkeys.symbols()) {
* console.log(symbol);
* }
*/
symbols() {
// Retrieve the keys (symbols) from the Symkeys data map and return
// the iterator.
return this[Symkeys.kDataKey].keys();
}
calculateName(providedName, useDomain, useSeparator) {
let domain = String(useDomain || this[Symkeys.kDomain])
let separator = String(useSeparator || this[Symkeys.kSeparator])
let postfix = (String(providedName).startsWith(separator)
? providedName.substring(1)
: providedName
)
if (domain.length) {
if (domain.endsWith(separator)) {
domain = domain.substring(0, domain.length - 1)
}
}
else {
separator = ''
}
return `${domain}${separator}${postfix}`
}
/**
* Constructs a new instance of the Symkeys, setting up a proxy to
* intercept and manage access to properties.
*
* This constructor initializes the Symkeys with a proxy that
* overrides the default behavior for property access, checking, and
* enumeration. It allows the Symkeys to behave like a map for its
* own properties, while also maintaining the prototype chain.
*
* @param {string} domain an optional prefix string, to which the
* `separator` parameter value will be guaranteed to have in between
* the domain (if truthy) and the name of the added key.
* @param {string} separator defaults to a period. So if your domain
* is 'symkeys.internal' then calling {@link add()} with a name of
* `"feature"` will result in the full name being
* `"symkeys.internal.feature"`
*
* @example
* const Symkeys = new Symkeys();
* Symkeys[Symbol.for('myProperty')] = 'myValue';
* console.log(Symkeys[Symbol.for('myProperty')]); // 'myValue'
*/
constructor(domain = '', separator = '.') {
// Create a prototype from the parent class to maintain the chain.
const prototype = Object.create(Object.getPrototypeOf(this))
// Store the original prototype for potential future use.
this[Symkeys.kPrototype] = prototype
// Create map for this instance
this[Symkeys.kDataKey] = new Map()
// Store the domain
this[Symkeys.kDomain] = (typeof domain === 'string' && domain)
// Store the separator
this[Symkeys.kSeparator] = separator
// Access the internal map that stores Symkeys data.
const map = this[Symkeys.kDataKey];
// Replace the instance's prototype with a proxy that manages
// property access and manipulation.
Object.setPrototypeOf(
this,
new Proxy(Object.create(prototype), {
// Return the stored prototype for the target.
getPrototypeOf(_) {
return prototype;
},
// Intercept property access.
get(target, property, receiver) {
// If the property exists in the Symkeys map, return its value.
if (map.has(property)) {
return map.get(property);
}
// Otherwise, perform the default behavior.
return Reflect.get(target, property, receiver);
},
// Check for property existence. Check both the Symkeys map and the target for
// the property.
has(target, property) {
return map.has(property) || Reflect.has(target, property);
},
// Retrieve all property keys. Combine keys from the Symkeys map and the target.
ownKeys(target) {
return [...Array.from(map.keys()), ...Reflect.ownKeys(target)];
},
// Intercept property assignment.
set(_, property, value, __) {
// If the property exists in the Symkeys map, set its value.
if (map.has(property)) {
map.set(property, value);
return true;
}
// If not, the operation is not allowed.
return false;
},
// Retrieve property descriptors.
getOwnPropertyDescriptor(_, property) {
// Convert the Symkeys map to an object to use with
// Object.getOwnPropertyDescriptor.
const object = [...map.entries()].reduce(
(a, e) => Object.assign(a, { [e[0]]: e[1] }),
{},
);
// Retrieve the descriptor from the object.
return Object.getOwnPropertyDescriptor(object, property);
},
}),
);
}
/**
* Generates a random token string.
*
* This method creates a pseudo-random token that can be used for various
* purposes within the library, such as generating unique identifiers or
* keys. The token is generated using a base 36 encoding, which includes
* numbers and lowercase letters.
*
* @returns A random token string.
*
* @example
* // Example of getting a random token:
* const token = MyClass.token;
* console.log(token); // Outputs a string like 'qg6k1zr0is'
*/
static get token() {
return Math.random().toString(36).slice(2);
}
/**
* Reusable publicly static key for identifying where data is stored.
*/
static get kDataKey() {
return Symbol.for('symkeys.data');
}
/**
* Reusable publicly static key for identifying where data is stored.
*/
static get kPrototype() {
return Symbol.for('symkeys.prototype')
}
static get kDomain() {
return Symbol.for('symkeys.domain')
}
static get kSeparator() {
return Symbol.for('symkeys.separator')
}
}
// Prevent the need for transpiler/packer or ES modules
ship({ Symkeys, defaults: { Symkeys }})
function ship(code) {
const _ne = Object.entries(code)
.reduce((a, [k,v]) => ({ ...a, [k]:v }), {});
const { defaults = {} } = code;
const [_den, _de] = Object.entries(defaults)?.[0];
if (typeof module !== 'undefined' && module.exports) {
module.exports = _ne || {};
}
else if (_den && _de) { globalThis[_den] = _de; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment