'use strict' let CssSyntaxError = require('./css-syntax-error') let Stringifier = require('./stringifier') let stringify = require('./stringify') let { isClean, my } = require('./symbols') function cloneNode(obj, parent) { let cloned = new obj.constructor() for (let i in obj) { if (!Object.prototype.hasOwnProperty.call(obj, i)) { /* c8 ignore next 2 */ 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 this[my] = true 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] } } } 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 } after(add) { this.parent.insertAfter(this, add) return this } assign(overrides = {}) { for (let name in overrides) { this[name] = overrides[name] } return this } before(add) { this.parent.insertBefore(this, add) return this } cleanRaws(keepBetween) { delete this.raws.before delete this.raws.after if (!keepBetween) delete this.raws.between } clone(overrides = {}) { let cloned = cloneNode(this) for (let name in overrides) { cloned[name] = overrides[name] } return cloned } cloneAfter(overrides = {}) { let cloned = this.clone(overrides) this.parent.insertAfter(this, cloned) return cloned } cloneBefore(overrides = {}) { let cloned = this.clone(overrides) this.parent.insertBefore(this, cloned) return cloned } error(message, opts = {}) { if (this.source) { let { end, start } = this.rangeBy(opts) return this.source.input.error( message, { column: start.column, line: start.line }, { column: end.column, line: end.line }, opts ) } return new CssSyntaxError(message) } getProxyProcessor() { return { get(node, prop) { if (prop === 'proxyOf') { return node } else if (prop === 'root') { return () => node.root().toProxy() } else { return node[prop] } }, set(node, prop, value) { if (node[prop] === value) return true node[prop] = value if ( prop === 'prop' || prop === 'value' || prop === 'name' || prop === 'params' || prop === 'important' || /* c8 ignore next */ prop === 'text' ) { node.markDirty() } return true } } } /* c8 ignore next 3 */ markClean() { this[isClean] = true } markDirty() { if (this[isClean]) { this[isClean] = false let next = this while ((next = next.parent)) { next[isClean] = false } } } next() { if (!this.parent) return undefined let index = this.parent.index(this) return this.parent.nodes[index + 1] } positionBy(opts, stringRepresentation) { let pos = this.source.start if (opts.index) { pos = this.positionInside(opts.index, stringRepresentation) } else if (opts.word) { stringRepresentation = this.toString() let index = stringRepresentation.indexOf(opts.word) if (index !== -1) pos = this.positionInside(index, stringRepresentation) } return pos } positionInside(index, stringRepresentation) { let string = stringRepresentation || 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 { column, line } } prev() { if (!this.parent) return undefined let index = this.parent.index(this) return this.parent.nodes[index - 1] } rangeBy(opts) { let start = { column: this.source.start.column, line: this.source.start.line } let end = this.source.end ? { column: this.source.end.column + 1, line: this.source.end.line } : { column: start.column + 1, line: start.line } if (opts.word) { let stringRepresentation = this.toString() let index = stringRepresentation.indexOf(opts.word) if (index !== -1) { start = this.positionInside(index, stringRepresentation) end = this.positionInside( index + opts.word.length, stringRepresentation ) } } else { if (opts.start) { start = { column: opts.start.column, line: opts.start.line } } else if (opts.index) { start = this.positionInside(opts.index) } if (opts.end) { end = { column: opts.end.column, line: opts.end.line } } else if (typeof opts.endIndex === 'number') { end = this.positionInside(opts.endIndex) } else if (opts.index) { end = this.positionInside(opts.index + 1) } } if ( end.line < start.line || (end.line === start.line && end.column <= start.column) ) { end = { column: start.column + 1, line: start.line } } return { end, start } } raw(prop, defaultType) { let str = new Stringifier() return str.raw(this, prop, defaultType) } remove() { if (this.parent) { this.parent.removeChild(this) } this.parent = undefined return this } 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 } root() { let result = this while (result.parent && result.parent.type !== 'document') { result = result.parent } return result } toJSON(_, inputs) { let fixed = {} let emitInputs = inputs == null inputs = inputs || new Map() let inputsNextIndex = 0 for (let name in this) { if (!Object.prototype.hasOwnProperty.call(this, name)) { /* c8 ignore next 2 */ continue } if (name === 'parent' || name === 'proxyCache') continue let value = this[name] if (Array.isArray(value)) { fixed[name] = value.map(i => { if (typeof i === 'object' && i.toJSON) { return i.toJSON(null, inputs) } else { return i } }) } else if (typeof value === 'object' && value.toJSON) { fixed[name] = value.toJSON(null, inputs) } else if (name === 'source') { let inputId = inputs.get(value.input) if (inputId == null) { inputId = inputsNextIndex inputs.set(value.input, inputsNextIndex) inputsNextIndex++ } fixed[name] = { end: value.end, inputId, start: value.start } } else { fixed[name] = value } } if (emitInputs) { fixed.inputs = [...inputs.keys()].map(input => input.toJSON()) } return fixed } toProxy() { if (!this.proxyCache) { this.proxyCache = new Proxy(this, this.getProxyProcessor()) } return this.proxyCache } toString(stringifier = stringify) { if (stringifier.stringify) stringifier = stringifier.stringify let result = '' stringifier(this, i => { result += i }) return result } warn(result, text, opts) { let data = { node: this } for (let i in opts) data[i] = opts[i] return result.warn(text, data) } get proxyOf() { return this } } module.exports = Node Node.default = Node