UNPKG

node-sketch

Version:

Javascript library to manipulate sketch files

431 lines (357 loc) 11 kB
const _parent = Symbol.for('Parent'); const lib = require('../'); const Sketch = require('./Sketch'); const layers = { classes: [ 'artboard', 'bitmap', 'group', 'oval', 'page', 'polygon', 'rectangle', 'shapeGroup', 'shapePath', 'slice', 'star', 'symbolInstance', 'symbolMaster', 'text', 'triangle' ], page: ['symbolMaster', 'artboard'] }; /** * Abstract class that it's used by all other classes, providing basic functionalities. * * @abstract */ class Node { /** * @constructor * * @param {Node|Sketch} parent - The parent of the element * @param {Object} data - The raw data from the sketch file */ constructor(parent, data) { this[_parent] = parent; Object.keys(data).forEach(key => this.set(key, data[key])); } /** * Returns the parent element * * @return {Node|Sketch|undefined} */ get parent() { return this[_parent]; } /** * Find a node ascendent matching with the type and condition * * @param {String} [type] - The node type * @param {Function|String} [condition] - The node name or a callback to be executed on each parent and must return true or false. If it's not provided, only the type argument is be used. * @return {Node|Sketch|undefined} */ getParent(type, condition) { let parent = this[_parent]; condition = getCondition(type, condition); if (!condition) { return parent; } while (parent && !condition(parent)) { parent = parent[_parent]; } return parent; } /** * Get the sketch element associated with this node * * @return {Sketch|undefined} */ getSketch() { let parent = this; while (parent[_parent]) { parent = parent[_parent]; } if (parent instanceof Sketch) { return parent; } } /** * Add/replace new childrens in this node * @param {string} key The node key * @param {Node|Object|Array} node The node/s to insert */ set(key, node) { if (node instanceof Node) { node = node.toJson(); } if (isPlainObject(node)) { //is a subclass if ('_class' in node) { this[key] = lib.create(this, node); return; } this[key] = {}; Object.keys(node).forEach(k => { if (isClassObject(node[k])) { this[key][k] = lib.create(this, node[k]); } else { this[key][k] = node[k]; } }); return; } //is an array of subclasses if (Array.isArray(node) && isClassObject(node[0])) { this[key] = node.map(child => lib.create(this, child)); return; } this[key] = node; } /** * Push a new children in this node * @param {string} key The node key * @param {Node|Object} node The node/s to insert * * @return {Node} The new node inserted */ push(key, node) { if (node instanceof Node) { node = node.toJson(); } if (!Array.isArray(this[key])) { throw new Error(`Unable to push new children. ${key} must be an array`); } //is a subclass if (isClassObject(node)) { node = lib.create(this, node); } this[key].push(node); return node; } /** * Search and returns the first descendant node that match the type and condition. * * @param {String} type - The Node type * @param {Function|String} [condition] - The node name or a callback to be executed on each node that must return true or false. If it's not provided, only the type argument is be used. * @return {Node|undefined} */ get(type, condition) { //page has always as direct children artboard and symbolMasters if (layers[this._class] && layers[this._class].indexOf(type) !== -1) { return this.layers.find(getCondition(type, condition)); } if (layers.classes.indexOf(type) === -1) { return findNode(this, getCondition(type, condition)); } return findLayer(this, getCondition(type, condition)); } /** * Search and returns all descendant nodes matching with the type and condition. * @example * //Get the first page * const page = sketch.pages[0]; * * //Get all colors found in this page * const colors = page.getAll('color'); * * //Get all colors with specific values * const blueColors = page.getAll('color', (color) => { * return color.blue > 0.5 && color.red < 0.33 * }); * * @param {String} type - The Node type * @param {Function|String} [condition] - The node name or a callback to be executed on each node that must return true or false. If it's not provided, only the type argument is be used. * @return {Node[]} */ getAll(type, condition, result) { //page has always as direct children artboard and symbolMasters if (layers[this._class] && layers[this._class].indexOf(type) !== -1) { return this.layers.filter(getCondition(type, condition)); } result = result || []; if (layers.classes.indexOf(type) === -1) { return findNode(this, getCondition(type, condition), result); } return findLayer(this, getCondition(type, condition), result); } /** * Removes the node from its parent * * @return {Node} */ detach() { const parent = this[_parent]; this[_parent] = undefined; if (!parent) { throw new Error('Unable to detach a node without parent'); } for (let [key, value] of Object.entries(parent)) { if (value === this) { parent[key] = undefined; return this; } if (Array.isArray(value)) { const index = value.indexOf(this); if (index !== -1) { value.splice(index, 1); return this; } } } throw new Error('Unable to detach a node with incorrect parent'); } /** * Replace this node with other * * @param {Node} node - The node to use * * @return {Node} The new node */ replaceWith(node) { const parent = this[_parent]; if (!parent) { throw new Error('Unable to replace a node without parent'); } node[_parent] = parent; for (let [key, value] of Object.entries(parent)) { if (value === this) { parent[key] = node; return node; } if (Array.isArray(value)) { const index = value.indexOf(this); if (index !== -1) { value[index] = node; return node; } } } throw new Error('Unable to replace a node with incorrect parent'); } /** * Creates a deep clone of this node * * @param {Node|undefined} parent - The new parent of the clone. If it's not defined use the current parent. * * @return {Node} */ clone(parent) { return lib.create(parent || this.parent, this.toJson()); } /** * Returns a json with the node data * * @return {Object} */ toJson() { return JSON.parse(JSON.stringify(this)); } } module.exports = Node; function getCondition(type, condition) { if (!type) { return false; } if (typeof type === 'function') { return type; } if (!condition) { return node => node._class === type; } if (typeof condition === 'string') { return node => node._class === type && node.name === condition; } return node => node._class === type && condition(node); } function findNode(target, condition, result) { for (let [key, value] of Object.entries(target)) { if (value instanceof Node) { if (!condition || condition(value)) { if (result) { result.push(value); continue; } return value; } if (result) { findNode(value, condition, result); continue; } const found = findNode(value, condition); if (found) { return found; } continue; } if (Array.isArray(value)) { for (let child of value) { if (child instanceof Node) { if (!condition || condition(child)) { if (result) { result.push(child); continue; } return child; } if (result) { findNode(child, condition, result); continue; } const found = findNode(child, condition); if (found) { return found; } } } continue; } if (isPlainObject(value)) { if (result) { findNode(value, condition, result); continue; } const found = findNode(value, condition); if (found) { return found; } } } return result; } function findLayer(target, condition, result) { if (result) { target.layers.filter(layer => !condition || condition(layer)).forEach(layer => result.push(layer)); } else { let layer = target.layers.find(layer => !condition || condition(layer)); if (layer) { return layer; } } for (let [key, value] of Object.entries(target.layers)) { if ('layers' in value) { if (result) { findLayer(value, condition, result); } else { const found = findLayer(value, condition); if (found) { return found; } } } } return result; } //https://stackoverflow.com/a/38555871 function isPlainObject(obj) { return ( typeof obj === 'object' && obj !== null && obj.constructor === Object && Object.prototype.toString.call(obj) === '[object Object]' ); } function isClassObject(obj) { return isPlainObject(obj) && '_class' in obj; }