blob: d26ef1fdd0465d256336b23bd38be29520e60dbe [file] [log] [blame]
Tim van der Lippe16b82282021-11-08 13:50:26 +00001'use strict'
Mathias Bynens79e2cf02020-05-29 16:46:17 +02002
Tim van der Lippe16b82282021-11-08 13:50:26 +00003let { isClean, my } = require('./symbols')
4let CssSyntaxError = require('./css-syntax-error')
5let Stringifier = require('./stringifier')
6let stringify = require('./stringify')
Mathias Bynens79e2cf02020-05-29 16:46:17 +02007
8function cloneNode(obj, parent) {
Tim van der Lippe16b82282021-11-08 13:50:26 +00009 let cloned = new obj.constructor()
Mathias Bynens79e2cf02020-05-29 16:46:17 +020010
Tim van der Lippe16b82282021-11-08 13:50:26 +000011 for (let i in obj) {
12 if (!Object.prototype.hasOwnProperty.call(obj, i)) {
13 // istanbul ignore next
14 continue
15 }
16 if (i === 'proxyCache') continue
17 let value = obj[i]
18 let type = typeof value
Mathias Bynens79e2cf02020-05-29 16:46:17 +020019
20 if (i === 'parent' && type === 'object') {
Tim van der Lippe16b82282021-11-08 13:50:26 +000021 if (parent) cloned[i] = parent
Mathias Bynens79e2cf02020-05-29 16:46:17 +020022 } else if (i === 'source') {
Tim van der Lippe16b82282021-11-08 13:50:26 +000023 cloned[i] = value
24 } else if (Array.isArray(value)) {
25 cloned[i] = value.map(j => cloneNode(j, cloned))
Mathias Bynens79e2cf02020-05-29 16:46:17 +020026 } else {
Tim van der Lippe16b82282021-11-08 13:50:26 +000027 if (type === 'object' && value !== null) value = cloneNode(value)
28 cloned[i] = value
Mathias Bynens79e2cf02020-05-29 16:46:17 +020029 }
30 }
31
Tim van der Lippe16b82282021-11-08 13:50:26 +000032 return cloned
Mathias Bynens79e2cf02020-05-29 16:46:17 +020033}
Mathias Bynens79e2cf02020-05-29 16:46:17 +020034
Tim van der Lippe16b82282021-11-08 13:50:26 +000035class Node {
36 constructor(defaults = {}) {
37 this.raws = {}
38 this[isClean] = false
39 this[my] = true
Mathias Bynens79e2cf02020-05-29 16:46:17 +020040
Tim van der Lippe16b82282021-11-08 13:50:26 +000041 for (let name in defaults) {
42 if (name === 'nodes') {
43 this.nodes = []
44 for (let node of defaults[name]) {
45 if (typeof node.clone === 'function') {
46 this.append(node.clone())
Tim van der Lippef2ea2c92021-11-08 10:55:56 +000047 } else {
Tim van der Lippe16b82282021-11-08 13:50:26 +000048 this.append(node)
Tim van der Lippef2ea2c92021-11-08 10:55:56 +000049 }
Tim van der Lippe16b82282021-11-08 13:50:26 +000050 }
51 } else {
52 this[name] = defaults[name]
53 }
54 }
55 }
56
57 error(message, opts = {}) {
58 if (this.source) {
59 let pos = this.positionBy(opts)
60 return this.source.input.error(message, pos.line, pos.column, opts)
61 }
62 return new CssSyntaxError(message)
63 }
64
65 warn(result, text, opts) {
66 let data = { node: this }
67 for (let i in opts) data[i] = opts[i]
68 return result.warn(text, data)
69 }
70
71 remove() {
72 if (this.parent) {
73 this.parent.removeChild(this)
74 }
75 this.parent = undefined
76 return this
77 }
78
79 toString(stringifier = stringify) {
80 if (stringifier.stringify) stringifier = stringifier.stringify
81 let result = ''
82 stringifier(this, i => {
83 result += i
84 })
85 return result
86 }
87
88 assign(overrides = {}) {
89 for (let name in overrides) {
90 this[name] = overrides[name]
91 }
92 return this
93 }
94
95 clone(overrides = {}) {
96 let cloned = cloneNode(this)
97 for (let name in overrides) {
98 cloned[name] = overrides[name]
99 }
100 return cloned
101 }
102
103 cloneBefore(overrides = {}) {
104 let cloned = this.clone(overrides)
105 this.parent.insertBefore(this, cloned)
106 return cloned
107 }
108
109 cloneAfter(overrides = {}) {
110 let cloned = this.clone(overrides)
111 this.parent.insertAfter(this, cloned)
112 return cloned
113 }
114
115 replaceWith(...nodes) {
116 if (this.parent) {
117 let bookmark = this
118 let foundSelf = false
119 for (let node of nodes) {
120 if (node === this) {
121 foundSelf = true
122 } else if (foundSelf) {
123 this.parent.insertAfter(bookmark, node)
124 bookmark = node
125 } else {
126 this.parent.insertBefore(bookmark, node)
127 }
128 }
129
130 if (!foundSelf) {
131 this.remove()
132 }
133 }
134
135 return this
136 }
137
138 next() {
139 if (!this.parent) return undefined
140 let index = this.parent.index(this)
141 return this.parent.nodes[index + 1]
142 }
143
144 prev() {
145 if (!this.parent) return undefined
146 let index = this.parent.index(this)
147 return this.parent.nodes[index - 1]
148 }
149
150 before(add) {
151 this.parent.insertBefore(this, add)
152 return this
153 }
154
155 after(add) {
156 this.parent.insertAfter(this, add)
157 return this
158 }
159
160 root() {
161 let result = this
162 while (result.parent && result.parent.type !== 'document') {
163 result = result.parent
164 }
165 return result
166 }
167
168 raw(prop, defaultType) {
169 let str = new Stringifier()
170 return str.raw(this, prop, defaultType)
171 }
172
173 cleanRaws(keepBetween) {
174 delete this.raws.before
175 delete this.raws.after
176 if (!keepBetween) delete this.raws.between
177 }
178
179 toJSON(_, inputs) {
180 let fixed = {}
181 let emitInputs = inputs == null
182 inputs = inputs || new Map()
183 let inputsNextIndex = 0
184
185 for (let name in this) {
186 if (!Object.prototype.hasOwnProperty.call(this, name)) {
187 // istanbul ignore next
188 continue
189 }
190 if (name === 'parent' || name === 'proxyCache') continue
191 let value = this[name]
192
193 if (Array.isArray(value)) {
194 fixed[name] = value.map(i => {
195 if (typeof i === 'object' && i.toJSON) {
196 return i.toJSON(null, inputs)
197 } else {
198 return i
199 }
200 })
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200201 } else if (typeof value === 'object' && value.toJSON) {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000202 fixed[name] = value.toJSON(null, inputs)
203 } else if (name === 'source') {
204 let inputId = inputs.get(value.input)
205 if (inputId == null) {
206 inputId = inputsNextIndex
207 inputs.set(value.input, inputsNextIndex)
208 inputsNextIndex++
209 }
210 fixed[name] = {
211 inputId,
212 start: value.start,
213 end: value.end
214 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200215 } else {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000216 fixed[name] = value
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200217 }
218 }
219
Tim van der Lippe16b82282021-11-08 13:50:26 +0000220 if (emitInputs) {
221 fixed.inputs = [...inputs.keys()].map(input => input.toJSON())
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200222 }
223
Tim van der Lippe16b82282021-11-08 13:50:26 +0000224 return fixed
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200225 }
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200226
Tim van der Lippe16b82282021-11-08 13:50:26 +0000227 positionInside(index) {
228 let string = this.toString()
229 let column = this.source.start.column
230 let line = this.source.start.line
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200231
Tim van der Lippe16b82282021-11-08 13:50:26 +0000232 for (let i = 0; i < index; i++) {
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200233 if (string[i] === '\n') {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000234 column = 1
235 line += 1
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200236 } else {
Tim van der Lippe16b82282021-11-08 13:50:26 +0000237 column += 1
Mathias Bynens79e2cf02020-05-29 16:46:17 +0200238 }
239 }
240
Tim van der Lippe16b82282021-11-08 13:50:26 +0000241 return { line, column }
Tim van der Lippef2ea2c92021-11-08 10:55:56 +0000242 }
Alex Rudenko6c0f1612021-11-05 06:28:44 +0000243
Tim van der Lippe16b82282021-11-08 13:50:26 +0000244 positionBy(opts) {
245 let pos = this.source.start
246 if (opts.index) {
247 pos = this.positionInside(opts.index)
248 } else if (opts.word) {
249 let index = this.toString().indexOf(opts.word)
250 if (index !== -1) pos = this.positionInside(index)
251 }
252 return pos
253 }
Alex Rudenko6c0f1612021-11-05 06:28:44 +0000254
Tim van der Lippe16b82282021-11-08 13:50:26 +0000255 getProxyProcessor() {
256 return {
257 set(node, prop, value) {
258 if (node[prop] === value) return true
259 node[prop] = value
260 if (
261 prop === 'prop' ||
262 prop === 'value' ||
263 prop === 'name' ||
264 prop === 'params' ||
265 prop === 'important' ||
266 prop === 'text'
267 ) {
268 node.markDirty()
269 }
270 return true
271 },
Alex Rudenko6c0f1612021-11-05 06:28:44 +0000272
Tim van der Lippe16b82282021-11-08 13:50:26 +0000273 get(node, prop) {
274 if (prop === 'proxyOf') {
275 return node
276 } else if (prop === 'root') {
277 return () => node.root().toProxy()
278 } else {
279 return node[prop]
280 }
281 }
282 }
283 }
Tim van der Lippef2ea2c92021-11-08 10:55:56 +0000284
Tim van der Lippe16b82282021-11-08 13:50:26 +0000285 toProxy() {
286 if (!this.proxyCache) {
287 this.proxyCache = new Proxy(this, this.getProxyProcessor())
288 }
289 return this.proxyCache
290 }
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +0000291
Tim van der Lippe16b82282021-11-08 13:50:26 +0000292 addToError(error) {
293 error.postcssNode = this
294 if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) {
295 let s = this.source
296 error.stack = error.stack.replace(
297 /\n\s{4}at /,
298 `$&${s.input.from}:${s.start.line}:${s.start.column}$&`
299 )
300 }
301 return error
302 }
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +0000303
Tim van der Lippe16b82282021-11-08 13:50:26 +0000304 markDirty() {
305 if (this[isClean]) {
306 this[isClean] = false
307 let next = this
308 while ((next = next.parent)) {
309 next[isClean] = false
310 }
311 }
312 }
Tim van der Lippe2b4a9df2021-11-08 12:58:12 +0000313
Tim van der Lippe16b82282021-11-08 13:50:26 +0000314 get proxyOf() {
315 return this
316 }
317}
318
319module.exports = Node
320Node.default = Node