Skip to content
Permalink
b63eeba8fa
Switch branches/tags
Go to file
 
 
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.
*/