@foxy.io/sdk
Version:
Universal SDK for a full server-side and a limited in-browser access to Foxy hAPI.
404 lines (403 loc) • 16.2 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.BooleanSelector = void 0;
/**
* Boolean selector is an HTML boolean attribute value format that allows
* developers to write configurations for elements deep in a shadow DOM. Here's
* what it looks like:
*
* ```text
* direct-child-one:nested-child:not=descendant-one,descendant-two direct-child-two
* ```
*
* When used with the "disabled" attribute, the code above could translate to: "Disable
* everything except for the descendant-one and descendant-two in the nested-child that
* belongs to direct-child-one; disable direct-child-two entirely."
*
* Boolean selector is always a list, where items are separated by whitespace (as much
* as you need, including line breaks):
*
* ```text
* item-one item-two item-three
* ```
*
* Each item is a path that consists of identifiers (lowercase characters from a to z or a dash)
* separated by a colon:
*
* ```text
* parent:child:nested-child
* ```
*
* By default, only specified paths will be selected. To select everything except for certain paths,
* add the `not=` modifier to the end of the path (or at the top level):
*
* ```text
* parent:child:not=exception
* ```
*
* You can specify multiple values by separating them with a comma and optionally a whitespace:
*
* ```text
* parent:child:not=exception-one, exception-two
* ```
*
* Only lowercase a-z letters, colon, comma, dash and whitespace are allowed in the selectors. An attempt
* to use a character outside of this set will result in a `SyntaxError`.
*/
class BooleanSelector {
/**
* Parses the boolean selector value and creates an instance
* of the `BooleanSelector` class.
*
* @param value boolean selector value, e.g. `foo:bar baz:not=qux`
*/
constructor(value) {
this.__tree = BooleanSelector.__parse(value);
}
/**
* Helper selector that matches any identifier on any level.
*
* @example
* BooleanSelector.True.matches('anything') // => true
* BooleanSelector.True.zoom('thing').matches('stuff') // => true
*
* @returns `BooleanSelector` singleton
*/
static get True() {
return trueBooleanSelectorSingleton;
}
/**
* Helper selector that doesn't match any identifier on any level.
*
* @example
* BooleanSelector.False.matches('anything') // => false
* BooleanSelector.False.zoom('thing').matches('stuff') // => false
*
* @returns `BooleanSelector` singleton
*/
static get False() {
return falseBooleanSelectorSingleton;
}
/**
* Creates a `BooleanSelector` instance from an attribute value according to the following rules:
*
* - null will be parsed as empty string;
* - empty string or `truthyValue` will be parsed as `:not=*` ;
* - in every other case attribute value will be parsed as regular boolean selector.
*
* @example
* const value = element.getAttribute('disabled');
* BooleanSelector.fromAttribute(value) // => [object BooleanSelector]
*
* @param value attribite value
* @param truthyValue additional attribute value that must be treated as truthy (use attribute name here to be spec-compliant)
* @returns `BooleanSelector` instance constructed from the given attribite value
*/
static fromAttribute(value, truthyValue) {
if (value === null)
return BooleanSelector.False;
if (value === '' || value === truthyValue)
return BooleanSelector.True;
return new BooleanSelector(value);
}
/**
* Checks if current selector includes rules for the given top-level identifier.
*
* @example
* new BooleanSelector('foo').matches('foo') // => true
* new BooleanSelector('foo').matches('foo', true) // => true
* new BooleanSelector('foo').matches('bar') // => false
* new BooleanSelector('foo:bar').matches('foo') // => true
* new BooleanSelector('foo:bar').matches('foo', true) // => false
* new BooleanSelector('foo:bar').matches('bar') // => false
*
* @param id identifier to look for
* @param [isFullMatch=false] if true, will match only if the entire namespace is selected
* @returns `true` is current selector includes rules for the given identifier
*/
matches(id, isFullMatch = false) {
const selector = this.zoom(id).toString();
return isFullMatch ? selector === 'not=*' : selector !== '';
}
/**
* Zooms on the given top-level identifier or follows a path.
*
* @example
* new BooleanSelector('foo:bar:baz').zoom('foo:bar').toString() // => "baz"
* new BooleanSelector('foo:bar:baz').zoom('foo').toString() // => "bar:baz"
* new BooleanSelector('not=foo').zoom('bar').toString() // => "not=*"
* new BooleanSelector('not=foo').zoom('foo').toString() // => ""
*
* @param path path to look for
* @returns zoomed BooleanSelector
*/
zoom(path) {
const zoomedSelector = new BooleanSelector('');
zoomedSelector.__tree = path.split(':').reduce((currentTree, id) => {
let zoomedTree;
if ('include' in currentTree) {
zoomedTree = currentTree.include[id];
if (zoomedTree === undefined)
return { include: {} };
if (zoomedTree === true)
return { exclude: { '*': true } };
}
else {
zoomedTree = currentTree.exclude[id];
if (zoomedTree === undefined)
return { exclude: { '*': true } };
if (zoomedTree === true)
return { include: {} };
}
return zoomedTree;
}, this.__tree);
return zoomedSelector;
}
/**
* Converts this selector to string.
*
* @example
* new BooleanSelector('foo:bar').toString() // => "foo:bar"
*
* @returns serialized representation of this selector
*/
toString() {
return BooleanSelector.__stringifyTree(this.__tree);
}
/**
* Converts this selector to an attribute value.
*
* @example
* new BooleanSelector('foo:bar').toAttribute() // => "foo:bar"
* BooleanSelector.False.toAttribute() // => null
* BooleanSelector.True.toAttribute("disabled") // => "disabled"
* BooleanSelector.True.toAttribute() // => ""
*
* @param truthyValue attribute value for wildcard selectors (use attribute name here to be spec-compliant)
* @returns attribute value representing this selector.
*/
toAttribute(truthyValue = '') {
const serializedSelector = this.toString();
if (serializedSelector === 'not=*')
return truthyValue;
return serializedSelector.length === 0 ? null : serializedSelector;
}
static __parsePath(path, tree) {
const firstSeparatorIndex = path.indexOf(':');
const topLevelId = path.substring(0, firstSeparatorIndex);
const nestedPath = path.substring(firstSeparatorIndex + 1);
if ('exclude' in tree) {
const subTree = tree.exclude[topLevelId];
if (subTree)
tree.exclude[topLevelId] = this.__parseListItem(nestedPath, subTree === true ? void 0 : subTree);
}
else {
const subTree = tree.include[topLevelId];
if (subTree !== true)
tree.include[topLevelId] = this.__parseListItem(nestedPath, subTree);
}
return tree;
}
static __parseSet(set, tree) {
const setItems = set.split(',');
if ('include' in tree) {
tree = { exclude: tree.include };
for (const id in tree.exclude)
if (!setItems.includes(id))
delete tree.exclude[id];
for (const item of setItems) {
if (item in tree.exclude) {
delete tree.exclude[item];
}
else {
tree.exclude[item] = true;
}
}
}
else {
for (const id in tree.exclude)
if (!setItems.includes(id))
delete tree.exclude[id];
}
return tree;
}
static __parseListItem(listItem, tree = { include: {} }) {
if (listItem.includes(':'))
return this.__parsePath(listItem, tree);
if (listItem.startsWith('not='))
return this.__parseSet(listItem.substring(4), tree);
if ('include' in tree) {
tree.include[listItem] = true;
}
else {
for (const id in tree.exclude)
if (id === listItem)
delete tree.exclude[id];
}
return tree;
}
static __parseList(list, tree = { include: {} }) {
return list.split(' ').reduce((newTree, listItem) => this.__parseListItem(listItem, newTree), tree);
}
static __lintList(list) {
var _a;
let position = 'list';
let result = '';
for (let i = 0; i < list.length; ++i) {
const character = list.charAt(i);
try {
if (position === 'list') {
if (/^\s$/.test(character)) {
if (!/^\s$/.test((_a = list[i - 1]) !== null && _a !== void 0 ? _a : ' '))
result += ' ';
continue;
}
if (/^[a-z]$/.test(character)) {
result += character;
position = 'path';
continue;
}
throw new SyntaxError(`Expected [a-z] or a whitespace, but got "${character}" instead.`);
}
if (position === 'path') {
if (/^[a-z]$/.test(character)) {
result += character;
continue;
}
if (character === '-') {
if (list[i - 1] === '-' || list[i - 1] === ':') {
throw new SyntaxError(`Expected [a-z], but got "${character}" instead.`);
}
else {
result += character;
continue;
}
}
if (character === ':') {
if (list[i - 1] === ':' || list[i - 1] === '-') {
throw new SyntaxError(`Expected [a-z], but got "${character}" instead.`);
}
else {
result += character;
continue;
}
}
if (character === '=') {
if (list[i - 1] === '=' || list[i - 1] === ':' || list[i - 1] === '-') {
throw new SyntaxError(`Expected [a-z], but got "${character}" instead.`);
}
if (result.endsWith('not') && (result.length === 3 || !/[a-z]|-/.test(result[i - 4]))) {
result += character;
position = 'set';
continue;
}
else {
throw new SyntaxError(`Expected [a-z] or ":", but got "${character}" instead.`);
}
}
if (/^\s$/.test(character)) {
result += ' ';
position = 'list';
continue;
}
throw new SyntaxError(`Expected [a-z], ",", ":", ":" or a whitespace, but got "${character}" instead.`);
}
if (position === 'set') {
if (/^\s$/.test(character))
continue;
if (/^[a-z]|\*$/.test(character)) {
position = 'set-item';
result += character;
continue;
}
throw new SyntaxError(`Expected [a-z] or a whitespace, but got "${character}" instead.`);
}
if (position === 'set-item') {
if (list[i - 1] === '*') {
if (character === ',') {
result += character;
position = 'set';
continue;
}
if (/^\s$/.test(character)) {
if (i !== list.length - 1)
result += ' ';
position = 'list';
continue;
}
throw new SyntaxError(`Expected "," or a whitespace, but got "${character}" instead.`);
}
else {
if (/^[a-z]$/.test(character)) {
result += character;
continue;
}
if (character === '-') {
if (list[i - 1] === '-' || list[i - 1] === ':' || list[i - 1] === '=') {
throw new SyntaxError(`Expected [a-z], but got "${character}" instead.`);
}
else {
result += character;
continue;
}
}
if (character === ',') {
result += character;
position = 'set';
continue;
}
if (/^\s$/.test(character)) {
if (i !== list.length - 1)
result += ' ';
position = 'list';
continue;
}
throw new SyntaxError(`Expected [a-z], "," or a whitespace, but got "${character}" instead.`);
}
}
}
catch (err) {
const hint = 'This error occured at: ';
const trim = (v) => v.substring(Math.max(0, i - 30), i + 30);
const preview = trim(list);
const pointer = ' '.repeat(hint.length) + trim('^'.padStart(i + 1, ' '));
throw new SyntaxError([err.message, `${hint}${preview}`, pointer].join('\n'));
}
}
return result.trimEnd();
}
static __parse(list) {
return this.__parseList(this.__lintList(list));
}
static __stringifyTree(tree, path) {
const parts = [];
if ('include' in tree) {
for (const id in tree.include) {
const nestedTree = tree.include[id];
const newPath = path ? [path, id].join(':') : id;
if (nestedTree === true) {
parts.push(newPath);
}
else {
parts.push(this.__stringifyTree(nestedTree, newPath));
}
}
}
else {
const ids = [];
const partsToPush = [];
for (const id in tree.exclude) {
const nestedTree = tree.exclude[id];
const newPath = path ? [path, id].join(':') : id;
ids.push(id);
if (nestedTree !== true)
partsToPush.push(this.__stringifyTree(nestedTree, newPath));
}
parts.push(`${path ? `${path}:` : ''}not=${ids.join(',')}`, ...partsToPush);
}
return parts.join(' ');
}
}
exports.BooleanSelector = BooleanSelector;
const falseBooleanSelectorSingleton = new BooleanSelector('');
const trueBooleanSelectorSingleton = new BooleanSelector('not=*');
;