You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
379 lines
8.3 KiB
379 lines
8.3 KiB
'use strict' |
|
|
|
let { isClean, my } = 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)) { |
|
/* 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] |
|
} |
|
} |
|
} |
|
|
|
error(message, opts = {}) { |
|
if (this.source) { |
|
let { start, end } = this.rangeBy(opts) |
|
return this.source.input.error( |
|
message, |
|
{ line: start.line, column: start.column }, |
|
{ line: end.line, column: end.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 |
|
} |
|
|
|
assign(overrides = {}) { |
|
for (let name in overrides) { |
|
this[name] = overrides[name] |
|
} |
|
return this |
|
} |
|
|
|
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.parent.type !== 'document') { |
|
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(_, 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] = { |
|
inputId, |
|
start: value.start, |
|
end: value.end |
|
} |
|
} else { |
|
fixed[name] = value |
|
} |
|
} |
|
|
|
if (emitInputs) { |
|
fixed.inputs = [...inputs.keys()].map(input => input.toJSON()) |
|
} |
|
|
|
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 |
|
} |
|
|
|
rangeBy(opts) { |
|
let start = { |
|
line: this.source.start.line, |
|
column: this.source.start.column |
|
} |
|
let end = this.source.end |
|
? { |
|
line: this.source.end.line, |
|
column: this.source.end.column + 1 |
|
} |
|
: { |
|
line: start.line, |
|
column: start.column + 1 |
|
} |
|
|
|
if (opts.word) { |
|
let index = this.toString().indexOf(opts.word) |
|
if (index !== -1) { |
|
start = this.positionInside(index) |
|
end = this.positionInside(index + opts.word.length) |
|
} |
|
} else { |
|
if (opts.start) { |
|
start = { |
|
line: opts.start.line, |
|
column: opts.start.column |
|
} |
|
} else if (opts.index) { |
|
start = this.positionInside(opts.index) |
|
} |
|
|
|
if (opts.end) { |
|
end = { |
|
line: opts.end.line, |
|
column: opts.end.column |
|
} |
|
} else if (opts.endIndex) { |
|
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 = { line: start.line, column: start.column + 1 } |
|
} |
|
|
|
return { start, end } |
|
} |
|
|
|
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' || |
|
/* c8 ignore next */ |
|
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 |
|
Node.default = Node
|
|
|