UNPKG

opencolor

Version:

A collection of functions to parse Open Color files, construct them via code and write them

342 lines (318 loc) 9.82 kB
'use strict' import ParserError from './parser_error' import MetaProxy from './meta_proxy' /** @ignore **/ function flatten (ary) { var ret = [] for (var i = 0; i < ary.length; i++) { if (Array.isArray(ary[i])) { ret = ret.concat(flatten(ary[i])) } else { ret.push(ary[i]) } } return ret } /** * Generic Entry. Can be either a Palette or a Color */ export default class Entry { constructor (name, children, type, position) { this._name = name || 'Root' this.position = position this.children = [] this.parent = null this.metadata = new MetaProxy(this) this.type = type || 'Palette' this.addChildren(flatten(children || []), false) this.validateType() this.forEach = Array.prototype.forEach.bind(this.children) // the magic of JavaScript. } /** * @type {string} */ set name (newName) { newName = newName.replace(/[\.\/]/g, '') this._name = newName } /** * @type {string} */ get name () { return this._name } /** * Rename an Entry. This can also mean to move the entry from one point to another in the tree * @param {string} newName the new name/path for the renamed entry */ rename (newName) { newName = newName.replace(/[\.\/]/g, '') if (this.isRoot()) { this._name = newName } else { let newPath = [this.parent.path(), newName].filter((e) => e !== '').join('.') this.moveTo(newPath) } } /** * Get child of entry, either by name or index * @param {string|number} nameOrIndex name or index of child. Can also be a dotpath that will be recursively looked up. * @return Child */ get (nameOrIndex) { if (typeof nameOrIndex === 'string') { if (nameOrIndex.match(/\./)) { // dotpath, so we need to do a deep lookup var pathspec = nameOrIndex.split('.') var first = this.get(pathspec.shift()) if (!first) { return undefined } return first.get(pathspec.join('.')) } return this.children.filter((child) => child.name === nameOrIndex).pop() } else { return this.children[nameOrIndex] } } /** * Find out if this is the root node of the OCO tree * @return {boolean} true if this entry is the root of the OCO tree */ isRoot () { return !this.parent } /** * Find the root node of this OCO tree, recursively if needed * @return Root node of OCO tree */ root () { if (this.isRoot()) { return this } else { return this.parent.root() } } /** * Remove a child specified by path or name * @param {string} path of entry to be removed from subtree */ remove (path) { var entry = this.get(path) if (!entry) { return } entry.parent.removeChild(entry) // if there are multiple children with the same dotpath this.remove(path) } /** * Set child, by name or dotpath. Will create sub nodes if needed (think mkdir -p) * @param {string} path the path of the new child to set * @param entry entry to set as child */ set (path, entry) { if (path.match(/\./)) { // dotpath, so we need to do a deep lookup var pathspec = path.split('.') var firstPart = pathspec.shift() var existingEntry = this.get(firstPart) if (existingEntry && existingEntry.type === 'Palette') { existingEntry.set(pathspec.join('.'), entry) } else { var newGroup = new Entry(firstPart) this.set(firstPart, newGroup) newGroup.set(pathspec.join('.'), entry) } } else { if (path !== entry.name) { entry.name = path } entry.parent = this if (this.get(path)) { // replace existing entries this.children.filter((child) => child.name === path).forEach((child) => { this.replaceChild(child, entry) }) } else { // add entry this.children.push(entry) } } } /** @ignore */ updateReferences (oldPath, newPath) { this.traverseTree(['Reference'], (entry) => { if (entry.absoluteRefName && entry.absoluteRefName.indexOf(oldPath) === 0) { entry.refName = entry.absoluteRefName.replace(oldPath, newPath) this.set(entry.path(), entry) } }) } /** * Move this entry to a new place in the tree * @param {string} newPath new path to move the entry to * @param {boolean} [maintainReferences=true] if true, references will be updated to the new path */ moveTo (newPath, maintainReferences = true) { let oldPath = this.path() if (maintainReferences) { this.root().updateReferences(oldPath, newPath) } this.parent.removeChild(this) this.root().set(newPath, this) } /** * Returns full dotpath in OCO tree of this entry * @return {string} path of this entry */ path () { if (!this.parent) { return '' } return [this.parent.path(), this.name].filter((e) => e !== '').join('.') } /** @ignore */ addParent (element) { if (element['refName']) { element.parent = this } } /** * Add metadata to entry * @param {Object} metadata Hash with metadata */ addMetadata (metadata) { this.metadata.add(metadata) // transitioning } /** Remove a child by value * @param child Child to remove */ removeChild (child) { var index = this.children.indexOf(child) this.children = this.children.slice(0, index).concat(this.children.slice(index + 1)) } /** * Replace a child with a new entry * @param child Old Entry * @param newEntry New Entry */ replaceChild (child, newEntry) { var currentPosition = this.children.indexOf(child) this.children.splice(currentPosition, 1, newEntry) } /** * Add a child * @param child Entry to add as a child * @param {boolean} [validate=true] Validate type of entry * @param {number} [position=-1] position to add the child at. */ addChild (child, validate = true, position = -1) { if (!child) { return } var type = child.type // we're basically only separating meta data. if (type === 'Metadata') { throw (new Error('API error, please use .addMetadata or .metadata.add instead')) } else if (type === 'Metablock') { throw (new Error('API error, please use .addMetadata or .metadata.add instead')) } else { child.parent = this if (position === -1) { this.children.push(child) } else { this.children.splice(position, 0, child) } } if (validate) { this.validateType() } } /** * Add multiple entries as children * @param {[*]} children to add */ addChildren (children) { children.forEach((child) => { this.addChild(child, false) }) } /** * Validate if children added are valid type combinations * @throws {ParserError} if illegal nesting is detected */ validateType () { var types = [] this.children.forEach((child) => { let type = child.type if (types.indexOf(type) === -1) { types.push(type) } }) types = types.sort() if (types.indexOf('ColorValue') !== -1 && types.indexOf('Color') !== -1) { let message = `Palette "${this.name}" cannot contain colors and color values at the same level (line: ${this.position.first_line - 1})` throw (new ParserError(message, { line: this.position.first_line })) } if (types.indexOf('Palette') !== -1 && types.indexOf('ColorValue') !== -1) { let message = `Palette "${this.name}" cannot contain palette and color values at the same level (line: ${this.position.first_line - 1})` throw (new ParserError(message, { line: this.position.first_line })) } if (types.indexOf('ColorValue') !== -1 && this.type === 'Palette') { this.type = 'Color' } } /** * Get a metadata entry * @param {string} key Key of Metadata entry * @return {number|boolean|string|Reference} Metadatum or null if not found */ getMetadata (key) { return this.metadata.get(key) } /** * Traverse the subtree * @param {string[]|string} filters List of types to filter against * @param {function} callback Callback that will be called for each tree entry */ traverseTree (filters, callback) { var filter = false if (typeof filters === 'string') { filters = [filters] } if (filters && filters.length > 0) { filter = true } this.children.forEach((child) => { if (child.type !== 'ColorValue' && (!filter || filters.indexOf(child.type) !== -1)) { callback(child) } if (child.children && child.children.length > 0) { child.traverseTree(filters, callback) } }) } /** * Shortcut to return hexcolor if type of current Entry is a Color * @param {boolean} [withAlpha=false] if true, color will be returned with alpha channel * @return {?string} hexcolor value as string or null if not a color */ hexcolor (withAlpha = false) { if (this.type !== 'Color') { return null } var identifiedColor = this.children.filter((child) => child.isHexExpressable())[0] if (identifiedColor) { return identifiedColor.hexcolor(withAlpha) } return null } /** * Clone Entry, including metadata * @return {Entry} deep clone of entry */ clone () { var children = this.children.map((child) => child.clone()) var clone = new Entry(this.name, children, this.type, this.position) this.metadata.keys().forEach((key) => { let meta = this.metadata.get(key) clone.metadata.set(key, meta.clone ? meta.clone() : meta) }) return clone } /** @ignore */ toString () { return JSON.stringify(this, function (key, value) { if (key === 'parent' && value) { return value.path() } else { return value } }, ' ') } }