'use strict' let CssSyntaxError = require('./css-syntax-error') let Stringifier = require('./stringifier') let { isClean } = require('./symbols') 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)) { // istanbul ignore next 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 } class Node { constructor (defaults = {}) { this.raws = {} this[isClean] = false 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] } } } 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) } warn (result, text, opts) { let data = { node: this } for (let i in opts) data[i] = opts[i] return result.warn(text, data) } remove () { if (this.parent) { this.parent.removeChild(this) } this.parent = undefined return this } toString (stringifier = stringify) { if (stringifier.stringify) stringifier = stringifier.stringify let result = '' stringifier(this, i => { result += i }) return result } clone (overrides = {}) { let cloned = cloneNode(this) for (let name in overrides) { cloned[name] = overrides[name] } return cloned } cloneBefore (overrides = {}) { let cloned = this.clone(overrides) this.parent.insertBefore(this, cloned) return cloned } cloneAfter (overrides = {}) { let cloned = this.clone(overrides) this.parent.insertAfter(this, cloned) return cloned } replaceWith (...nodes) { if (this.parent) { let bookmark = this let foundSelf = false for (let node of nodes) { if (node === this) { foundSelf = true } else if (foundSelf) { this.parent.insertAfter(bookmark, node) bookmark = node } else { this.parent.insertBefore(bookmark, node) } } if (!foundSelf) { this.remove() } } return this } next () { if (!this.parent) return undefined let index = this.parent.index(this) return this.parent.nodes[index + 1] } prev () { if (!this.parent) return undefined let index = this.parent.index(this) return this.parent.nodes[index - 1] } before (add) { this.parent.insertBefore(this, add) return this } after (add) { this.parent.insertAfter(this, add) return this } root () { let result = this while (result.parent) result = result.parent return result } raw (prop, defaultType) { let str = new Stringifier() return str.raw(this, prop, defaultType) } cleanRaws (keepBetween) { delete this.raws.before delete this.raws.after if (!keepBetween) delete this.raws.between } toJSON () { let fixed = {} for (let name in this) { if (!Object.prototype.hasOwnProperty.call(this, name)) { // istanbul ignore next continue } if (name === 'parent') 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 } 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) { if (node[prop] === value) return true node[prop] = value if ( prop === 'prop' || prop === 'value' || prop === 'name' || prop === 'params' || prop === 'important' || prop === 'text' ) { node.markDirty() } return true }, get (node, prop) { if (prop === 'proxyOf') { return node } else if (prop === 'root') { return () => node.root().toProxy() } 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 () { if (this[isClean]) { this[isClean] = false let next = this while ((next = next.parent)) { next[isClean] = false } } } get proxyOf () { return this } } module.exports = Node