Permalink
Cannot retrieve contributors at this time
590 lines (547 sloc)
16 KB
| let { isComplete, isClean } = require('./symbols') | |
| let CssSyntaxError = require('./css-syntax-error') | |
| let Stringifier = require('./stringifier') | |
| let stringify = require('./stringify') | |
| function cloneNode (obj, parent) { | |
| let cloned = new obj.constructor() | |
| for (let i in obj) { | |
| if (!Object.prototype.hasOwnProperty.call(obj, i)) continue | |
| if (i === 'proxyCache') continue | |
| let value = obj[i] | |
| let type = typeof value | |
| if (i === 'parent' && type === 'object') { | |
| if (parent) cloned[i] = parent | |
| } else if (i === 'source') { | |
| cloned[i] = value | |
| } else if (Array.isArray(value)) { | |
| cloned[i] = value.map(j => cloneNode(j, cloned)) | |
| } else { | |
| if (type === 'object' && value !== null) value = cloneNode(value) | |
| cloned[i] = value | |
| } | |
| } | |
| return cloned | |
| } | |
| /** | |
| * All node classes inherit the following common methods. | |
| * | |
| * @abstract | |
| */ | |
| class Node { | |
| /** | |
| * @param {object} [defaults] Value for node properties. | |
| */ | |
| constructor (defaults = { }) { | |
| this.raws = { } | |
| this[isComplete] = false | |
| this[isClean] = false | |
| if (process.env.NODE_ENV !== 'production') { | |
| if (typeof defaults !== 'object' && typeof defaults !== 'undefined') { | |
| throw new Error( | |
| 'PostCSS nodes constructor accepts object, not ' + | |
| JSON.stringify(defaults) | |
| ) | |
| } | |
| } | |
| for (let name in defaults) { | |
| if (name === 'nodes') { | |
| this.nodes = [] | |
| for (let node of defaults[name]) { | |
| if (typeof node.clone === 'function') { | |
| this.append(node.clone()) | |
| } else { | |
| this.append(node) | |
| } | |
| } | |
| } else { | |
| this[name] = defaults[name] | |
| } | |
| } | |
| } | |
| /** | |
| * Returns a `CssSyntaxError` instance containing the original position | |
| * of the node in the source, showing line and column numbers and also | |
| * a small excerpt to facilitate debugging. | |
| * | |
| * If present, an input source map will be used to get the original position | |
| * of the source, even from a previous compilation step | |
| * (e.g., from Sass compilation). | |
| * | |
| * This method produces very useful error messages. | |
| * | |
| * @param {string} message Error description. | |
| * @param {object} [opts] Options. | |
| * @param {string} opts.plugin Plugin name that created this error. | |
| * PostCSS will set it automatically. | |
| * @param {string} opts.word A word inside a node’s string that should | |
| * be highlighted as the source of the error. | |
| * @param {number} opts.index An index inside a node’s string that should | |
| * be highlighted as the source of the error. | |
| * | |
| * @return {CssSyntaxError} Error object to throw it. | |
| * | |
| * @example | |
| * if (!variables[name]) { | |
| * throw decl.error('Unknown variable ' + name, { word: name }) | |
| * // CssSyntaxError: postcss-vars:a.sass:4:3: Unknown variable $black | |
| * // color: $black | |
| * // a | |
| * // ^ | |
| * // background: white | |
| * } | |
| */ | |
| error (message, opts = { }) { | |
| if (this.source) { | |
| let pos = this.positionBy(opts) | |
| return this.source.input.error(message, pos.line, pos.column, opts) | |
| } | |
| return new CssSyntaxError(message) | |
| } | |
| /** | |
| * This method is provided as a convenience wrapper for {@link Result#warn}. | |
| * | |
| * @param {Result} result The {@link Result} instance | |
| * that will receive the warning. | |
| * @param {string} text Warning message. | |
| * @param {object} [opts] Options | |
| * @param {string} opts.plugin Plugin name that created this warning. | |
| * PostCSS will set it automatically. | |
| * @param {string} opts.word A word inside a node’s string that should | |
| * be highlighted as the source of the warning. | |
| * @param {number} opts.index An index inside a node’s string that should | |
| * be highlighted as the source of the warning. | |
| * | |
| * @return {Warning} Created warning object. | |
| * | |
| * @example | |
| * const plugin = postcss.plugin('postcss-deprecated', () => { | |
| * return (root, result) => { | |
| * root.walkDecls('bad', decl => { | |
| * decl.warn(result, 'Deprecated property bad') | |
| * }) | |
| * } | |
| * }) | |
| */ | |
| warn (result, text, opts) { | |
| let data = { node: this } | |
| for (let i in opts) data[i] = opts[i] | |
| return result.warn(text, data) | |
| } | |
| /** | |
| * Removes the node from its parent and cleans the parent properties | |
| * from the node and its children. | |
| * | |
| * @example | |
| * if (decl.prop.match(/^-webkit-/)) { | |
| * decl.remove() | |
| * } | |
| * | |
| * @return {Node} Node to make calls chain. | |
| */ | |
| remove () { | |
| if (this.parent) { | |
| this.parent.removeChild(this) | |
| } | |
| this.parent = undefined | |
| return this | |
| } | |
| /** | |
| * Returns a CSS string representing the node. | |
| * | |
| * @param {stringifier|syntax} [stringifier] A syntax to use | |
| * in string generation. | |
| * | |
| * @return {string} CSS string of this node. | |
| * | |
| * @example | |
| * postcss.rule({ selector: 'a' }).toString() //=> "a {}" | |
| */ | |
| toString (stringifier = stringify) { | |
| if (stringifier.stringify) stringifier = stringifier.stringify | |
| let result = '' | |
| stringifier(this, i => { | |
| result += i | |
| }) | |
| return result | |
| } | |
| /** | |
| * Returns an exact clone of the node. | |
| * | |
| * The resulting cloned node and its (cloned) children will retain | |
| * code style properties. | |
| * | |
| * @param {object} [overrides] New properties to override in the clone. | |
| * | |
| * @example | |
| * decl.raws.before //=> "\n " | |
| * const cloned = decl.clone({ prop: '-moz-' + decl.prop }) | |
| * cloned.raws.before //=> "\n " | |
| * cloned.toString() //=> -moz-transform: scale(0) | |
| * | |
| * @return {Node} Clone of the node. | |
| */ | |
| clone (overrides = { }) { | |
| let cloned = cloneNode(this) | |
| for (let name in overrides) { | |
| cloned[name] = overrides[name] | |
| } | |
| return cloned | |
| } | |
| /** | |
| * Shortcut to clone the node and insert the resulting cloned node | |
| * before the current node. | |
| * | |
| * @param {object} [overrides] Mew properties to override in the clone. | |
| * | |
| * @example | |
| * decl.cloneBefore({ prop: '-moz-' + decl.prop }) | |
| * | |
| * @return {Node} New node | |
| */ | |
| cloneBefore (overrides = { }) { | |
| let cloned = this.clone(overrides) | |
| this.parent.insertBefore(this, cloned) | |
| return cloned | |
| } | |
| /** | |
| * Shortcut to clone the node and insert the resulting cloned node | |
| * after the current node. | |
| * | |
| * @param {object} [overrides] New properties to override in the clone. | |
| * | |
| * @return {Node} New node. | |
| */ | |
| cloneAfter (overrides = { }) { | |
| let cloned = this.clone(overrides) | |
| this.parent.insertAfter(this, cloned) | |
| return cloned | |
| } | |
| /** | |
| * Inserts node(s) before the current node and removes the current node. | |
| * | |
| * @param {...Node} nodes Mode(s) to replace current one. | |
| * | |
| * @example | |
| * if (atrule.name === 'mixin') { | |
| * atrule.replaceWith(mixinRules[atrule.params]) | |
| * } | |
| * | |
| * @return {Node} Current node to methods chain. | |
| */ | |
| replaceWith (...nodes) { | |
| if (this.parent) { | |
| for (let node of nodes) { | |
| this.parent.insertBefore(this, node) | |
| } | |
| this.remove() | |
| } | |
| return this | |
| } | |
| /** | |
| * Returns the next child of the node’s parent. | |
| * Returns `undefined` if the current node is the last child. | |
| * | |
| * @return {Node|undefined} Next node. | |
| * | |
| * @example | |
| * if (comment.text === 'delete next') { | |
| * const next = comment.next() | |
| * if (next) { | |
| * next.remove() | |
| * } | |
| * } | |
| */ | |
| next () { | |
| if (!this.parent) return undefined | |
| let index = this.parent.index(this) | |
| return this.parent.nodes[index + 1] | |
| } | |
| /** | |
| * Returns the previous child of the node’s parent. | |
| * Returns `undefined` if the current node is the first child. | |
| * | |
| * @return {Node|undefined} Previous node. | |
| * | |
| * @example | |
| * const annotation = decl.prev() | |
| * if (annotation.type === 'comment') { | |
| * readAnnotation(annotation.text) | |
| * } | |
| */ | |
| prev () { | |
| if (!this.parent) return undefined | |
| let index = this.parent.index(this) | |
| return this.parent.nodes[index - 1] | |
| } | |
| /** | |
| * Insert new node before current node to current node’s parent. | |
| * | |
| * Just alias for `node.parent.insertBefore(node, add)`. | |
| * | |
| * @param {Node|object|string|Node[]} add New node. | |
| * | |
| * @return {Node} This node for methods chain. | |
| * | |
| * @example | |
| * decl.before('content: ""') | |
| */ | |
| before (add) { | |
| this.parent.insertBefore(this, add) | |
| return this | |
| } | |
| /** | |
| * Insert new node after current node to current node’s parent. | |
| * | |
| * Just alias for `node.parent.insertAfter(node, add)`. | |
| * | |
| * @param {Node|object|string|Node[]} add New node. | |
| * | |
| * @return {Node} This node for methods chain. | |
| * | |
| * @example | |
| * decl.after('color: black') | |
| */ | |
| after (add) { | |
| this.parent.insertAfter(this, add) | |
| return this | |
| } | |
| toJSON () { | |
| let fixed = { } | |
| for (let name in this) { | |
| if (!Object.prototype.hasOwnProperty.call(this, name)) continue | |
| if (name === 'parent') continue | |
| if (name === 'listeners') continue | |
| let value = this[name] | |
| if (Array.isArray(value)) { | |
| fixed[name] = value.map(i => { | |
| if (typeof i === 'object' && i.toJSON) { | |
| return i.toJSON() | |
| } else { | |
| return i | |
| } | |
| }) | |
| } else if (typeof value === 'object' && value.toJSON) { | |
| fixed[name] = value.toJSON() | |
| } else { | |
| fixed[name] = value | |
| } | |
| } | |
| return fixed | |
| } | |
| /** | |
| * Returns a {@link Node#raws} value. If the node is missing | |
| * the code style property (because the node was manually built or cloned), | |
| * PostCSS will try to autodetect the code style property by looking | |
| * at other nodes in the tree. | |
| * | |
| * @param {string} prop Name of code style property. | |
| * @param {string} [defaultType] Name of default value, it can be missed | |
| * if the value is the same as prop. | |
| * | |
| * @example | |
| * const root = postcss.parse('a { background: white }') | |
| * root.nodes[0].append({ prop: 'color', value: 'black' }) | |
| * root.nodes[0].nodes[1].raws.before //=> undefined | |
| * root.nodes[0].nodes[1].raw('before') //=> ' ' | |
| * | |
| * @return {string} Code style value. | |
| */ | |
| raw (prop, defaultType) { | |
| let str = new Stringifier() | |
| return str.raw(this, prop, defaultType) | |
| } | |
| /** | |
| * Finds the Root instance of the node’s tree. | |
| * | |
| * @example | |
| * root.nodes[0].nodes[0].root() === root | |
| * | |
| * @return {Root} Root parent. | |
| */ | |
| root () { | |
| let result = this | |
| while (result.parent) result = result.parent | |
| return result | |
| } | |
| /** | |
| * Clear the code style properties for the node and its children. | |
| * | |
| * @param {boolean} [keepBetween] Keep the raws.between symbols. | |
| * | |
| * @return {undefined} | |
| * | |
| * @example | |
| * node.raws.before //=> ' ' | |
| * node.cleanRaws() | |
| * node.raws.before //=> undefined | |
| */ | |
| cleanRaws (keepBetween) { | |
| delete this.raws.before | |
| delete this.raws.after | |
| if (!keepBetween) delete this.raws.between | |
| } | |
| positionInside (index) { | |
| let string = this.toString() | |
| let column = this.source.start.column | |
| let line = this.source.start.line | |
| for (let i = 0; i < index; i++) { | |
| if (string[i] === '\n') { | |
| column = 1 | |
| line += 1 | |
| } else { | |
| column += 1 | |
| } | |
| } | |
| return { line, column } | |
| } | |
| positionBy (opts) { | |
| let pos = this.source.start | |
| if (opts.index) { | |
| pos = this.positionInside(opts.index) | |
| } else if (opts.word) { | |
| let index = this.toString().indexOf(opts.word) | |
| if (index !== -1) pos = this.positionInside(index) | |
| } | |
| return pos | |
| } | |
| getProxyProcessor () { | |
| return { | |
| set (node, prop, value) { | |
| node[prop] = value | |
| if ( | |
| prop === 'prop' || prop === 'value' || prop === 'important' || | |
| prop === 'text' | |
| ) { | |
| node.markDirty() | |
| } | |
| return true | |
| }, | |
| get (node, prop) { | |
| if (prop === 'proxyOf') { | |
| return node | |
| } else { | |
| return node[prop] | |
| } | |
| } | |
| } | |
| } | |
| toProxy () { | |
| if (!this.proxyCache) { | |
| this.proxyCache = new Proxy(this, this.getProxyProcessor()) | |
| } | |
| return this.proxyCache | |
| } | |
| addToError (error) { | |
| error.postcssNode = this | |
| if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) { | |
| let s = this.source | |
| error.stack = error.stack.replace(/\n\s{4}at /, | |
| `$&${ s.input.from }:${ s.start.line }:${ s.start.column }$&`) | |
| } | |
| return error | |
| } | |
| markDirty () { | |
| this[isClean] = false | |
| if (this[isComplete]) { | |
| this[isComplete] = false | |
| if (this.parent) this.parent.markDirty() | |
| } | |
| } | |
| /** | |
| * @memberof Node# | |
| * @member {string} type String representing the node’s type. | |
| * Possible values are `root`, `atrule`, `rule`, | |
| * `decl`, or `comment`. | |
| * | |
| * @example | |
| * postcss.decl({ prop: 'color', value: 'black' }).type //=> 'decl' | |
| */ | |
| /** | |
| * @memberof Node# | |
| * @member {Container} parent The node’s parent node. | |
| * | |
| * @example | |
| * root.nodes[0].parent === root | |
| */ | |
| /** | |
| * @memberof Node# | |
| * @member {source} source The input source of the node. | |
| * | |
| * The property is used in source map generation. | |
| * | |
| * If you create a node manually (e.g., with `postcss.decl()`), | |
| * that node will not have a `source` property and will be absent | |
| * from the source map. For this reason, the plugin developer should | |
| * consider cloning nodes to create new ones (in which case the new node’s | |
| * source will reference the original, cloned node) or setting | |
| * the `source` property manually. | |
| * | |
| * ```js | |
| * // Bad | |
| * const prefixed = postcss.decl({ | |
| * prop: '-moz-' + decl.prop, | |
| * value: decl.value | |
| * }) | |
| * | |
| * // Good | |
| * const prefixed = decl.clone({ prop: '-moz-' + decl.prop }) | |
| * ``` | |
| * | |
| * ```js | |
| * if (atrule.name === 'add-link') { | |
| * const rule = postcss.rule({ selector: 'a', source: atrule.source }) | |
| * atrule.parent.insertBefore(atrule, rule) | |
| * } | |
| * ``` | |
| * | |
| * @example | |
| * decl.source.input.from //=> '/home/ai/a.sass' | |
| * decl.source.start //=> { line: 10, column: 2 } | |
| * decl.source.end //=> { line: 10, column: 12 } | |
| */ | |
| /** | |
| * @memberof Node# | |
| * @member {object} raws Information to generate byte-to-byte equal | |
| * node string as it was in the origin input. | |
| * | |
| * Every parser saves its own properties, | |
| * but the default CSS parser uses: | |
| * | |
| * * `before`: the space symbols before the node. It also stores `*` | |
| * and `_` symbols before the declaration (IE hack). | |
| * * `after`: the space symbols after the last child of the node | |
| * to the end of the node. | |
| * * `between`: the symbols between the property and value | |
| * for declarations, selector and `{` for rules, or last parameter | |
| * and `{` for at-rules. | |
| * * `semicolon`: contains true if the last child has | |
| * an (optional) semicolon. | |
| * * `afterName`: the space between the at-rule name and its parameters. | |
| * * `left`: the space symbols between `/*` and the comment’s text. | |
| * * `right`: the space symbols between the comment’s text | |
| * and <code>*/</code>. | |
| * * `important`: the content of the important statement, | |
| * if it is not just `!important`. | |
| * | |
| * PostCSS cleans selectors, declaration values and at-rule parameters | |
| * from comments and extra spaces, but it stores origin content in raws | |
| * properties. As such, if you don’t change a declaration’s value, | |
| * PostCSS will use the raw value with comments. | |
| * | |
| * @example | |
| * const root = postcss.parse('a {\n color:black\n}') | |
| * root.first.first.raws //=> { before: '\n ', between: ':' } | |
| */ | |
| } | |
| module.exports = Node | |
| /** | |
| * @typedef {object} position | |
| * @property {number} line Source line in file. | |
| * @property {number} column Source column in file. | |
| */ | |
| /** | |
| * @typedef {object} source | |
| * @property {Input} input {@link Input} with input file | |
| * @property {position} start The starting position of the node’s source. | |
| * @property {position} end The ending position of the node’s source. | |
| */ |