Permalink
Cannot retrieve contributors at this time
721 lines (657 sloc)
19.2 KB
| let Declaration = require('./declaration') | |
| let Comment = require('./comment') | |
| let Node = require('./node') | |
| let parse, Rule, AtRule | |
| function cleanSource (nodes) { | |
| return nodes.map(i => { | |
| if (i.nodes) i.nodes = cleanSource(i.nodes) | |
| delete i.source | |
| return i | |
| }) | |
| } | |
| /** | |
| * The {@link Root}, {@link AtRule}, and {@link Rule} container nodes | |
| * inherit some common methods to help work with their children. | |
| * | |
| * Note that all containers can store any content. If you write a rule inside | |
| * a rule, PostCSS will parse it. | |
| * | |
| * @extends Node | |
| * @abstract | |
| */ | |
| class Container extends Node { | |
| push (child) { | |
| child.parent = this | |
| this.nodes.push(child) | |
| return this | |
| } | |
| /** | |
| * Iterates through the container’s immediate children, | |
| * calling `callback` for each child. | |
| * | |
| * Returning `false` in the callback will break iteration. | |
| * | |
| * This method only iterates through the container’s immediate children. | |
| * If you need to recursively iterate through all the container’s descendant | |
| * nodes, use {@link Container#walk}. | |
| * | |
| * Unlike the for `{}`-cycle or `Array#forEach` this iterator is safe | |
| * if you are mutating the array of child nodes during iteration. | |
| * PostCSS will adjust the current index to match the mutations. | |
| * | |
| * @param {childIterator} callback Iterator receives each node and index. | |
| * | |
| * @return {false|undefined} Returns `false` if iteration was broke. | |
| * | |
| * @example | |
| * const root = postcss.parse('a { color: black; z-index: 1 }') | |
| * const rule = root.first | |
| * | |
| * for (const decl of rule.nodes) { | |
| * decl.cloneBefore({ prop: '-webkit-' + decl.prop }) | |
| * // Cycle will be infinite, because cloneBefore moves the current node | |
| * // to the next index | |
| * } | |
| * | |
| * rule.each(decl => { | |
| * decl.cloneBefore({ prop: '-webkit-' + decl.prop }) | |
| * // Will be executed only for color and z-index | |
| * }) | |
| */ | |
| each (callback) { | |
| if (!this.lastEach) this.lastEach = 0 | |
| if (!this.indexes) this.indexes = { } | |
| this.lastEach += 1 | |
| let id = this.lastEach | |
| this.indexes[id] = 0 | |
| if (!this.nodes) return undefined | |
| let index, result | |
| while (this.indexes[id] < this.nodes.length) { | |
| index = this.indexes[id] | |
| result = callback(this.nodes[index], index) | |
| if (result === false) break | |
| this.indexes[id] += 1 | |
| if (result === 'start-again') { | |
| this.indexes[id] = 0 | |
| } | |
| } | |
| delete this.indexes[id] | |
| return result | |
| } | |
| /** | |
| * Traverses the container’s descendant nodes, calling callback | |
| * for each node. | |
| * | |
| * Like container.each(), this method is safe to use | |
| * if you are mutating arrays during iteration. | |
| * | |
| * If you only need to iterate through the container’s immediate children, | |
| * use {@link Container#each}. | |
| * | |
| * @param {childIterator} callback Iterator receives each node and index. | |
| * | |
| * @return {false|undefined} Returns `false` if iteration was broke. | |
| * | |
| * @example | |
| * root.walk(node => { | |
| * // Traverses all descendant nodes. | |
| * }) | |
| */ | |
| walk (callback) { | |
| return this.each((child, i) => { | |
| let result | |
| try { | |
| result = callback(child, i) | |
| } catch (e) { | |
| throw child.addToError(e) | |
| } | |
| if (result !== false && child.walk) { | |
| result = child.walk(callback) | |
| } | |
| return result | |
| }) | |
| } | |
| /** | |
| * Traverses the container’s descendant nodes, calling callback | |
| * for each declaration node. | |
| * | |
| * If you pass a filter, iteration will only happen over declarations | |
| * with matching properties. | |
| * | |
| * Like {@link Container#each}, this method is safe | |
| * to use if you are mutating arrays during iteration. | |
| * | |
| * @param {string|RegExp} [prop] String or regular expression | |
| * to filter declarations by property name. | |
| * @param {childIterator} callback Iterator receives each node and index. | |
| * | |
| * @return {false|undefined} Returns `false` if iteration was broke. | |
| * | |
| * @example | |
| * root.walkDecls(decl => { | |
| * checkPropertySupport(decl.prop) | |
| * }) | |
| * | |
| * root.walkDecls('border-radius', decl => { | |
| * decl.remove() | |
| * }) | |
| * | |
| * root.walkDecls(/^background/, decl => { | |
| * decl.value = takeFirstColorFromGradient(decl.value) | |
| * }) | |
| */ | |
| walkDecls (prop, callback) { | |
| if (!callback) { | |
| callback = prop | |
| return this.walk((child, i) => { | |
| if (child.type === 'decl') { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| if (prop instanceof RegExp) { | |
| return this.walk((child, i) => { | |
| if (child.type === 'decl' && prop.test(child.prop)) { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| return this.walk((child, i) => { | |
| if (child.type === 'decl' && child.prop === prop) { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| /** | |
| * Traverses the container’s descendant nodes, calling callback | |
| * for each rule node. | |
| * | |
| * If you pass a filter, iteration will only happen over rules | |
| * with matching selectors. | |
| * | |
| * Like {@link Container#each}, this method is safe | |
| * to use if you are mutating arrays during iteration. | |
| * | |
| * @param {string|RegExp} [selector] String or regular expression | |
| * to filter rules by selector. | |
| * @param {childIterator} callback Iterator receives each node and index. | |
| * | |
| * @return {false|undefined} returns `false` if iteration was broke. | |
| * | |
| * @example | |
| * const selectors = [] | |
| * root.walkRules(rule => { | |
| * selectors.push(rule.selector) | |
| * }) | |
| * console.log(`Your CSS uses ${ selectors.length } selectors`) | |
| */ | |
| walkRules (selector, callback) { | |
| if (!callback) { | |
| callback = selector | |
| return this.walk((child, i) => { | |
| if (child.type === 'rule') { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| if (selector instanceof RegExp) { | |
| return this.walk((child, i) => { | |
| if (child.type === 'rule' && selector.test(child.selector)) { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| return this.walk((child, i) => { | |
| if (child.type === 'rule' && child.selector === selector) { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| /** | |
| * Traverses the container’s descendant nodes, calling callback | |
| * for each at-rule node. | |
| * | |
| * If you pass a filter, iteration will only happen over at-rules | |
| * that have matching names. | |
| * | |
| * Like {@link Container#each}, this method is safe | |
| * to use if you are mutating arrays during iteration. | |
| * | |
| * @param {string|RegExp} [name] String or regular expression | |
| * to filter at-rules by name. | |
| * @param {childIterator} callback Iterator receives each node and index. | |
| * | |
| * @return {false|undefined} Returns `false` if iteration was broke. | |
| * | |
| * @example | |
| * root.walkAtRules(rule => { | |
| * if (isOld(rule.name)) rule.remove() | |
| * }) | |
| * | |
| * let first = false | |
| * root.walkAtRules('charset', rule => { | |
| * if (!first) { | |
| * first = true | |
| * } else { | |
| * rule.remove() | |
| * } | |
| * }) | |
| */ | |
| walkAtRules (name, callback) { | |
| if (!callback) { | |
| callback = name | |
| return this.walk((child, i) => { | |
| if (child.type === 'atrule') { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| if (name instanceof RegExp) { | |
| return this.walk((child, i) => { | |
| if (child.type === 'atrule' && name.test(child.name)) { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| return this.walk((child, i) => { | |
| if (child.type === 'atrule' && child.name === name) { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| /** | |
| * Traverses the container’s descendant nodes, calling callback | |
| * for each comment node. | |
| * | |
| * Like {@link Container#each}, this method is safe | |
| * to use if you are mutating arrays during iteration. | |
| * | |
| * @param {childIterator} callback Iterator receives each node and index. | |
| * | |
| * @return {false|undefined} Returns `false` if iteration was broke. | |
| * | |
| * @example | |
| * root.walkComments(comment => { | |
| * comment.remove() | |
| * }) | |
| */ | |
| walkComments (callback) { | |
| return this.walk((child, i) => { | |
| if (child.type === 'comment') { | |
| return callback(child, i) | |
| } | |
| }) | |
| } | |
| /** | |
| * Inserts new nodes to the end of the container. | |
| * | |
| * @param {...(Node|object|string|Node[])} children New nodes. | |
| * | |
| * @return {Node} This node for methods chain. | |
| * | |
| * @example | |
| * const decl1 = postcss.decl({ prop: 'color', value: 'black' }) | |
| * const decl2 = postcss.decl({ prop: 'background-color', value: 'white' }) | |
| * rule.append(decl1, decl2) | |
| * | |
| * root.append({ name: 'charset', params: '"UTF-8"' }) // at-rule | |
| * root.append({ selector: 'a' }) // rule | |
| * rule.append({ prop: 'color', value: 'black' }) // declaration | |
| * rule.append({ text: 'Comment' }) // comment | |
| * | |
| * root.append('a {}') | |
| * root.first.append('color: black; z-index: 1') | |
| */ | |
| append (...children) { | |
| for (let child of children) { | |
| let nodes = this.normalize(child, this.last) | |
| for (let node of nodes) this.nodes.push(node) | |
| } | |
| this.markDirty() | |
| return this | |
| } | |
| /** | |
| * Inserts new nodes to the start of the container. | |
| * | |
| * @param {...(Node|object|string|Node[])} children New nodes. | |
| * | |
| * @return {Node} This node for methods chain. | |
| * | |
| * @example | |
| * const decl1 = postcss.decl({ prop: 'color', value: 'black' }) | |
| * const decl2 = postcss.decl({ prop: 'background-color', value: 'white' }) | |
| * rule.prepend(decl1, decl2) | |
| * | |
| * root.append({ name: 'charset', params: '"UTF-8"' }) // at-rule | |
| * root.append({ selector: 'a' }) // rule | |
| * rule.append({ prop: 'color', value: 'black' }) // declaration | |
| * rule.append({ text: 'Comment' }) // comment | |
| * | |
| * root.append('a {}') | |
| * root.first.append('color: black; z-index: 1') | |
| */ | |
| prepend (...children) { | |
| children = children.reverse() | |
| for (let child of children) { | |
| let nodes = this.normalize(child, this.first, 'prepend').reverse() | |
| for (let node of nodes) this.nodes.unshift(node) | |
| for (let id in this.indexes) { | |
| this.indexes[id] = this.indexes[id] + nodes.length | |
| } | |
| } | |
| this.markDirty() | |
| return this | |
| } | |
| cleanRaws (keepBetween) { | |
| super.cleanRaws(keepBetween) | |
| if (this.nodes) { | |
| for (let node of this.nodes) node.cleanRaws(keepBetween) | |
| } | |
| } | |
| /** | |
| * Insert new node before old node within the container. | |
| * | |
| * @param {Node|number} exist Child or child’s index. | |
| * @param {Node|object|string|Node[]} add New node. | |
| * | |
| * @return {Node} This node for methods chain. | |
| * | |
| * @example | |
| * rule.insertBefore(decl, decl.clone({ prop: '-webkit-' + decl.prop })) | |
| */ | |
| insertBefore (exist, add) { | |
| exist = this.index(exist) | |
| let type = exist === 0 ? 'prepend' : false | |
| let nodes = this.normalize(add, this.nodes[exist], type).reverse() | |
| for (let node of nodes) this.nodes.splice(exist, 0, node) | |
| let index | |
| for (let id in this.indexes) { | |
| index = this.indexes[id] | |
| if (exist <= index) { | |
| this.indexes[id] = index + nodes.length | |
| } | |
| } | |
| this.markDirty() | |
| return this | |
| } | |
| /** | |
| * Insert new node after old node within the container. | |
| * | |
| * @param {Node|number} exist Child or child’s index. | |
| * @param {Node|object|string|Node[]} add New node. | |
| * | |
| * @return {Node} This node for methods chain. | |
| */ | |
| insertAfter (exist, add) { | |
| exist = this.index(exist) | |
| let nodes = this.normalize(add, this.nodes[exist]).reverse() | |
| for (let node of nodes) this.nodes.splice(exist + 1, 0, node) | |
| let index | |
| for (let id in this.indexes) { | |
| index = this.indexes[id] | |
| if (exist < index) { | |
| this.indexes[id] = index + nodes.length | |
| } | |
| } | |
| this.markDirty() | |
| return this | |
| } | |
| /** | |
| * Removes node from the container and cleans the parent properties | |
| * from the node and its children. | |
| * | |
| * @param {Node|number} child Child or child’s index. | |
| * | |
| * @return {Node} This node for methods chain | |
| * | |
| * @example | |
| * rule.nodes.length //=> 5 | |
| * rule.removeChild(decl) | |
| * rule.nodes.length //=> 4 | |
| * decl.parent //=> undefined | |
| */ | |
| removeChild (child) { | |
| child = this.index(child) | |
| this.nodes[child].parent = undefined | |
| this.nodes.splice(child, 1) | |
| let index | |
| for (let id in this.indexes) { | |
| index = this.indexes[id] | |
| if (index >= child) { | |
| this.indexes[id] = index - 1 | |
| } | |
| } | |
| this.markDirty() | |
| return this | |
| } | |
| /** | |
| * Removes all children from the container | |
| * and cleans their parent properties. | |
| * | |
| * @return {Node} This node for methods chain. | |
| * | |
| * @example | |
| * rule.removeAll() | |
| * rule.nodes.length //=> 0 | |
| */ | |
| removeAll () { | |
| for (let node of this.nodes) node.parent = undefined | |
| this.nodes = [] | |
| this.markDirty() | |
| return this | |
| } | |
| /** | |
| * Passes all declaration values within the container that match pattern | |
| * through callback, replacing those values with the returned result | |
| * of callback. | |
| * | |
| * This method is useful if you are using a custom unit or function | |
| * and need to iterate through all values. | |
| * | |
| * @param {string|RegExp} pattern Replace pattern. | |
| * @param {object} opts Options to speed up the search. | |
| * @param {string|string[]} opts.props An array of property names. | |
| * @param {string} opts.fast String that’s used to narrow down | |
| * values and speed up the regexp search. | |
| * @param {function|string} callback String to replace pattern or callback | |
| * that returns a new value. The callback | |
| * will receive the same arguments | |
| * as those passed to a function parameter | |
| * of `String#replace`. | |
| * | |
| * @return {Node} This node for methods chain. | |
| * | |
| * @example | |
| * root.replaceValues(/\d+rem/, { fast: 'rem' }, string => { | |
| * return 15 * parseInt(string) + 'px' | |
| * }) | |
| */ | |
| replaceValues (pattern, opts, callback) { | |
| if (!callback) { | |
| callback = opts | |
| opts = { } | |
| } | |
| this.walkDecls(decl => { | |
| if (opts.props && !opts.props.includes(decl.prop)) return | |
| if (opts.fast && !decl.value.includes(opts.fast)) return | |
| decl.value = decl.value.replace(pattern, callback) | |
| }) | |
| this.markDirty() | |
| return this | |
| } | |
| /** | |
| * Returns `true` if callback returns `true` | |
| * for all of the container’s children. | |
| * | |
| * @param {childCondition} condition Iterator returns true or false. | |
| * | |
| * @return {boolean} Is every child pass condition. | |
| * | |
| * @example | |
| * const noPrefixes = rule.every(i => i.prop[0] !== '-') | |
| */ | |
| every (condition) { | |
| return this.nodes.every(condition) | |
| } | |
| /** | |
| * Returns `true` if callback returns `true` for (at least) one | |
| * of the container’s children. | |
| * | |
| * @param {childCondition} condition Iterator returns true or false. | |
| * | |
| * @return {boolean} Is some child pass condition. | |
| * | |
| * @example | |
| * const hasPrefix = rule.some(i => i.prop[0] === '-') | |
| */ | |
| some (condition) { | |
| return this.nodes.some(condition) | |
| } | |
| /** | |
| * Returns a `child`’s index within the {@link Container#nodes} array. | |
| * | |
| * @param {Node} child Child of the current container. | |
| * | |
| * @return {number} Child index. | |
| * | |
| * @example | |
| * rule.index( rule.nodes[2] ) //=> 2 | |
| */ | |
| index (child) { | |
| if (typeof child === 'number') return child | |
| if (child.proxyOf) child = child.proxyOf | |
| return this.nodes.indexOf(child) | |
| } | |
| /** | |
| * The container’s first child. | |
| * | |
| * @type {Node} | |
| * | |
| * @example | |
| * rule.first === rules.nodes[0] | |
| */ | |
| get first () { | |
| if (!this.nodes) return undefined | |
| return this.nodes[0] | |
| } | |
| /** | |
| * The container’s last child. | |
| * | |
| * @type {Node} | |
| * | |
| * @example | |
| * rule.last === rule.nodes[rule.nodes.length - 1] | |
| */ | |
| get last () { | |
| if (!this.nodes) return undefined | |
| return this.nodes[this.nodes.length - 1] | |
| } | |
| normalize (nodes, sample) { | |
| if (typeof nodes === 'string') { | |
| nodes = cleanSource(parse(nodes).nodes) | |
| } else if (Array.isArray(nodes)) { | |
| nodes = nodes.slice(0) | |
| for (let i of nodes) { | |
| if (i.parent) i.parent.removeChild(i, 'ignore') | |
| } | |
| } else if (nodes.type === 'root') { | |
| nodes = nodes.nodes.slice(0) | |
| for (let i of nodes) { | |
| if (i.parent) i.parent.removeChild(i, 'ignore') | |
| } | |
| } else if (nodes.type) { | |
| nodes = [nodes] | |
| } else if (nodes.prop) { | |
| if (typeof nodes.value === 'undefined') { | |
| throw new Error('Value field is missed in node creation') | |
| } else if (typeof nodes.value !== 'string') { | |
| nodes.value = String(nodes.value) | |
| } | |
| nodes = [new Declaration(nodes)] | |
| } else if (nodes.selector) { | |
| nodes = [new Rule(nodes)] | |
| } else if (nodes.name) { | |
| nodes = [new AtRule(nodes)] | |
| } else if (nodes.text) { | |
| nodes = [new Comment(nodes)] | |
| } else { | |
| throw new Error('Unknown node type in node creation') | |
| } | |
| let processed = nodes.map(i => { | |
| if (i.parent) i.parent.removeChild(i) | |
| if (typeof i.raws.before === 'undefined') { | |
| if (sample && typeof sample.raws.before !== 'undefined') { | |
| i.raws.before = sample.raws.before.replace(/\S/g, '') | |
| } | |
| } | |
| i.parent = this | |
| return i | |
| }) | |
| return processed | |
| } | |
| getProxyProcessor () { | |
| return { | |
| set (node, prop, value) { | |
| node[prop] = value | |
| if (prop === 'name' || prop === 'params' || prop === 'selector') { | |
| node.markDirty() | |
| } | |
| return true | |
| }, | |
| get (node, prop) { | |
| if (prop === 'proxyOf') { | |
| return node | |
| } else if (!node[prop]) { | |
| return node[prop] | |
| } else if (prop === 'each' || prop.startsWith('walk')) { | |
| return cb => { | |
| return node[prop]((child, i) => cb(child.toProxy(), i)) | |
| } | |
| } else if (prop === 'every' || prop === 'some') { | |
| return cb => { | |
| return node[prop]((child, ...o) => cb(child.toProxy(), ...o)) | |
| } | |
| } else if (prop === 'nodes') { | |
| return node[prop].map(i => i.toProxy()) | |
| } else if (prop === 'first' || prop === 'last') { | |
| return node[prop].toProxy() | |
| } else { | |
| return node[prop] | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * @memberof Container# | |
| * @member {Node[]} nodes An array containing the container’s children. | |
| * | |
| * @example | |
| * const root = postcss.parse('a { color: black }') | |
| * root.nodes.length //=> 1 | |
| * root.nodes[0].selector //=> 'a' | |
| * root.nodes[0].nodes[0].prop //=> 'color' | |
| */ | |
| } | |
| Container.registerParse = dependant => { | |
| parse = dependant | |
| } | |
| Container.registerRule = dependant => { | |
| Rule = dependant | |
| } | |
| Container.registerAtRule = dependant => { | |
| AtRule = dependant | |
| } | |
| module.exports = Container | |
| /** | |
| * @callback childCondition | |
| * @param {Node} node Container child. | |
| * @param {number} index Child index. | |
| * @param {Node[]} nodes All container children. | |
| * @return {boolean} | |
| */ | |
| /** | |
| * @callback childIterator | |
| * @param {Node} node Container child. | |
| * @param {number} index Child index. | |
| * @return {false|undefined} Returning `false` will break iteration. | |
| */ |