select.js

/* jslint browser: true */

aeq = ( function ( aeq ) {
/**
 * @typedef {String} aeq.SelectorString
 * @description The selectorString has 3 expression types:
 *
 * - type
 * - props
 * - pseudo
 *
 * #### Type
 *
 * The type of object to find, one of:
 *
 * - `item`: Finds items in the project panel
 * - `activecomp`: Finds the active composition
 * - `comp`/`composition`: Finds CompItems
 * - `layer`: Finds Layers
 * - `propertygroup`/`propgrp`/`propgroup`: Finds property groups
 * - `prop`/`property`: Finds properties`
 * - `effect`: Finds effects property groups
 * - `key`: Finds keyframes on properties. Returns aeq.Key objects
 *
 * The types can be chained after each other, but must be in the order above,
 * but all of them are optional. Only the objects of the last specified `type`
 * will be returned.
 *
 * Type is the only expression type that is required. All other expression
 * types are optional.
 *
 * #### Props
 * written right after the type, without a space, and inside square brackes
 * (`[ ]`). The props are a list attribute names and values, separated by `=`.
 * The objects must have an attribute with the specified value to qualify as
 * a match. Attributes are separated by a space.
 *
 * #### Pseudo
 * Psoudo are a bit like `props` but start with a colon, `:`, followed by a
 * keyword specifying how the attributes should match. The attributes are
 * placed inside parenthesis `()`.
 *
 * The keywords that are currently supported are:
 *
 * - `:is()`: all attributes must match.
 * - `:has()`: same as `:is()`
 * - `:not()`: objects should not have any attributes matching the props.
 * - `:isnot()`: same as `:not()`
 *
 * Psoudo selectors can be chained.
 *
 * @example <caption>Get all comps with width and height of 1920x1080</caption>
 *     aeq("comp[width=1920 heigth=1080]")
 *
 * @example <caption>Get all properties of layers that are selected and
 *          does not have audio:</caption>
 *     aeq("layer[selected hasAudio=false] prop")
 *
 * @example <caption>Get properties that have `PropertyValueType.OneD` and are
 *          not selected.</caption>
 *     aeq("prop[propertyValueType=" + PropertyValueType.OneD + "]:not(selected)");
 *
 * @example <caption>Get layers that do not have audio inside comps
 *          that are selected:</caption>
 *    aeq("comp:is(selected) layer:not(hasAudio)")
 */

/**
 * Gets objects by looking at a string and finding objects in After Effects
 * matching the description. The context is used to determine a starting point
 * for where the function starts looking for elements.
 * @memberof aeq
 * @method
 * @param  {aeq.SelectorString} selector A string containing a
 *         selector expression
 * @param  {CompItem|FolderItem|Layer|PropertyGroup|Array} [context] The object
 *         to start looking from
 * @return {ArrayEx} The found After Effects objects
 */
// TODO: Fix complexity of this function
// eslint-disable-next-line
aeq.select = function ( selector, context ) {
	var results = [];

	var parsedSelector = cssselector.parse( selector );
	var parts = parsedSelector;

	if ( context !== undefined ) {
		if ( aeq.isString( context ) ) {
			results = aeq.select( context );
		} else if ( aeq.isArray( context ) ) {
			results = context;
		} else {
			results = [ context ];
		}
	}

	var part;

	while ( parts.length > 0 ) {
		part = parts[0];
		var unshifted = false;

		switch ( part.type ) {
		case 'activecomp':
			results = filterResults( aeq.arrayEx( [ aeq.getActiveComposition() ] ) );
			results.type = 'comp';
			break;
		case 'composition':
		case 'comp':
			results = filterResults( aeq.getCompositions() );
			results.type = 'comp';
			break;

		case 'layer':
			if ( results.type === 'comp' || aeq.isComp( results[0] ) ) {
				results = filterResults( aeq.getLayers( results ) );
				results.type = 'layer';
			} else if ( results.type !== 'comp' ) {
				parts.unshift({ type: 'comp' });
				unshifted = true;
			}
			break;

		case 'propertygroup':
		case 'propgrp':
		case 'propgroup':
			if ( results.type === 'layer' || results.type === 'propertygroup' ||
					aeq.isLayer( results[0] ) || aeq.isPropertyGroup( results[0] ) ) {
				results = filterResults( aeq.getProperties( results,
					{ separate: false, groups: true, props: false }) );
				results.type = 'propertygroup';
			} else if ( results.type !== 'layer' ) {
				parts.unshift({ type: 'layer' });
				unshifted = true;
			}
			break;

		case 'property':
		case 'prop':
			if ( results.type === 'layer' || results.type === 'propertygroup' ||
					aeq.isLayer( results[0] ) || aeq.isPropertyGroup( results[0] ) ) {
				results = filterResults( aeq.getProperties( results, { separate: false }) );
				results.type = 'property';
			} else if ( results.type !== 'layer' ) {
				parts.unshift({ type: 'layer' });
				unshifted = true;
			}
			break;

		case 'effect':
			if ( results.type === 'layer' || aeq.isLayer( results[0] ) ) {
				results = filterResults( aeq.getEffects( results ) );
				results.type = 'effect';
			} else if ( results.type !== 'layer' ) {
				parts.unshift({ type: 'layer' });
				unshifted = true;
			}
			break;

		case 'key':
		case 'keys':
			if ( results.type === 'property' || aeq.isProperty( results[0] ) ) {
				results = filterResults( aeq.getKeys( results ) );
				results.type = 'key';
			} else if ( results.type !== 'property' ) {
				parts.unshift({ type: 'property' });
				unshifted = true;
			}
			break;

		case 'item':
			results = filterResults( aeq.getItems() );
			results.type = 'item';
			break;

		default:
			throw new Error( 'Unrecognized token ' + part.type );
		}
		if ( !unshifted ) {
			parts.shift();
		}
	}
	function filterResults( arr ) {
		// Only filter if there is something to filter
		if ( part.props || part.pseudo ) {
			return arr.filter( filter );
		}
		return arr;
	}

	function filter( obj ) {
		var ret = true,
			len, pseudo;
		if ( part.props !== null ) {
			if ( !hasAllAttributes( obj, part.props, false ) ) {
				return false;
			}
		}
		if ( !part.pseudo ) {
			return true;
		}
		len = part.pseudo.length;

		for ( var i = 0; i < len; i++ ) {
			pseudo = part.pseudo[i];

			if ( pseudo.type === 'not' || pseudo.type === 'isnot' ) {
				ret = hasAllAttributes( obj, pseudo.props, true );
			} else if ( pseudo.type === 'is' || pseudo.type === 'has' ) {
				ret = hasAllAttributes( obj, pseudo.props, false );
			}

			if ( ret === false ) {
				return false;
			}
		}
		return true;
	}

	return aeq.arrayEx( results );
};


function hasAllAttributes( obj, attributes, not ) {
	var attributeValue;
	for ( var attribute in attributes ) {
		if ( !attributes.hasOwnProperty( attribute ) ) {
			continue;
		}
		attributeValue = attributes[attribute];

		if ( !obj.hasOwnProperty( attribute ) ) {
			throw new Error( 'The attribute ' + attribute + ' does not exist on a ' + typeof ( obj ) );
		}

		var isSame = compare( attributeValue, obj[attribute] );

		// Return false if it is the same and it should not be,
		// or if it isn't the same and it should be
		if ( ( isSame && not ) || ( !isSame && not === false ) ) {
			return false;
		}
	}
	return true;
}

function compare( value, attribute ) {
	if ( value.type === 'Array' ) {
		return valueInArray( value, attribute );
	} else if ( value.type === 'RegExp' ) {
		return value.value.test( attribute );

	// For numbers, strings, booleans etc.
	}
	return value.value.toString() === attribute.toString();
}

function valueInArray( value, attribute ) {
	// Check if value is in array
	for ( var i = 0, il = value.value.length; i < il; i++ ) {
		if ( compare( value.value[i], attribute ) ) {
			return true;
		}
	}
	return false;
}

return aeq;
}( aeq || {}) );