mirror of
https://github.com/hanzoai/postcss.git
synced 2026-09-24 17:01:56 +00:00
662 lines
18 KiB
JavaScript
662 lines
18 KiB
JavaScript
import Declaration from './declaration'
|
||
import Comment from './comment'
|
||
import Node from './node'
|
||
|
||
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
|
||
}
|
||
|
||
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) {
|
||
e.postcssNode = child
|
||
if (e.stack && child.source && /\n\s{4}at /.test(e.stack)) {
|
||
let s = child.source
|
||
e.stack = e.stack.replace(/\n\s{4}at /,
|
||
`$&${ s.input.from }:${ s.start.line }:${ s.start.column }$&`)
|
||
}
|
||
throw 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)
|
||
}
|
||
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
|
||
}
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
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 = []
|
||
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.indexOf(decl.prop) === -1) return
|
||
if (opts.fast && decl.value.indexOf(opts.fast) === -1) return
|
||
|
||
decl.value = decl.value.replace(pattern, callback)
|
||
})
|
||
|
||
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
|
||
}
|
||
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') {
|
||
let parse = require('./parse')
|
||
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) {
|
||
let Rule = require('./rule')
|
||
nodes = [new Rule(nodes)]
|
||
} else if (nodes.name) {
|
||
let AtRule = require('./at-rule')
|
||
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
|
||
}
|
||
|
||
/**
|
||
* @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'
|
||
*/
|
||
}
|
||
|
||
export default 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.
|
||
*/
|