UNPKG

8.48 kBJavaScriptView Raw
1'use strict'
2
3let { isClean, my } = require('./symbols')
4let CssSyntaxError = require('./css-syntax-error')
5let Stringifier = require('./stringifier')
6let stringify = require('./stringify')
7
8function cloneNode(obj, parent) {
9 let cloned = new obj.constructor()
10
11 for (let i in obj) {
12 if (!Object.prototype.hasOwnProperty.call(obj, i)) {
13 /* c8 ignore next 2 */
14 continue
15 }
16 if (i === 'proxyCache') continue
17 let value = obj[i]
18 let type = typeof value
19
20 if (i === 'parent' && type === 'object') {
21 if (parent) cloned[i] = parent
22 } else if (i === 'source') {
23 cloned[i] = value
24 } else if (Array.isArray(value)) {
25 cloned[i] = value.map(j => cloneNode(j, cloned))
26 } else {
27 if (type === 'object' && value !== null) value = cloneNode(value)
28 cloned[i] = value
29 }
30 }
31
32 return cloned
33}
34
35class Node {
36 constructor(defaults = {}) {
37 this.raws = {}
38 this[isClean] = false
39 this[my] = true
40
41 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())
47 } else {
48 this.append(node)
49 }
50 }
51 } else {
52 this[name] = defaults[name]
53 }
54 }
55 }
56
57 error(message, opts = {}) {
58 if (this.source) {
59 let { start, end } = this.rangeBy(opts)
60 return this.source.input.error(
61 message,
62 { line: start.line, column: start.column },
63 { line: end.line, column: end.column },
64 opts
65 )
66 }
67 return new CssSyntaxError(message)
68 }
69
70 warn(result, text, opts) {
71 let data = { node: this }
72 for (let i in opts) data[i] = opts[i]
73 return result.warn(text, data)
74 }
75
76 remove() {
77 if (this.parent) {
78 this.parent.removeChild(this)
79 }
80 this.parent = undefined
81 return this
82 }
83
84 toString(stringifier = stringify) {
85 if (stringifier.stringify) stringifier = stringifier.stringify
86 let result = ''
87 stringifier(this, i => {
88 result += i
89 })
90 return result
91 }
92
93 assign(overrides = {}) {
94 for (let name in overrides) {
95 this[name] = overrides[name]
96 }
97 return this
98 }
99
100 clone(overrides = {}) {
101 let cloned = cloneNode(this)
102 for (let name in overrides) {
103 cloned[name] = overrides[name]
104 }
105 return cloned
106 }
107
108 cloneBefore(overrides = {}) {
109 let cloned = this.clone(overrides)
110 this.parent.insertBefore(this, cloned)
111 return cloned
112 }
113
114 cloneAfter(overrides = {}) {
115 let cloned = this.clone(overrides)
116 this.parent.insertAfter(this, cloned)
117 return cloned
118 }
119
120 replaceWith(...nodes) {
121 if (this.parent) {
122 let bookmark = this
123 let foundSelf = false
124 for (let node of nodes) {
125 if (node === this) {
126 foundSelf = true
127 } else if (foundSelf) {
128 this.parent.insertAfter(bookmark, node)
129 bookmark = node
130 } else {
131 this.parent.insertBefore(bookmark, node)
132 }
133 }
134
135 if (!foundSelf) {
136 this.remove()
137 }
138 }
139
140 return this
141 }
142
143 next() {
144 if (!this.parent) return undefined
145 let index = this.parent.index(this)
146 return this.parent.nodes[index + 1]
147 }
148
149 prev() {
150 if (!this.parent) return undefined
151 let index = this.parent.index(this)
152 return this.parent.nodes[index - 1]
153 }
154
155 before(add) {
156 this.parent.insertBefore(this, add)
157 return this
158 }
159
160 after(add) {
161 this.parent.insertAfter(this, add)
162 return this
163 }
164
165 root() {
166 let result = this
167 while (result.parent && result.parent.type !== 'document') {
168 result = result.parent
169 }
170 return result
171 }
172
173 raw(prop, defaultType) {
174 let str = new Stringifier()
175 return str.raw(this, prop, defaultType)
176 }
177
178 cleanRaws(keepBetween) {
179 delete this.raws.before
180 delete this.raws.after
181 if (!keepBetween) delete this.raws.between
182 }
183
184 toJSON(_, inputs) {
185 let fixed = {}
186 let emitInputs = inputs == null
187 inputs = inputs || new Map()
188 let inputsNextIndex = 0
189
190 for (let name in this) {
191 if (!Object.prototype.hasOwnProperty.call(this, name)) {
192 /* c8 ignore next 2 */
193 continue
194 }
195 if (name === 'parent' || name === 'proxyCache') continue
196 let value = this[name]
197
198 if (Array.isArray(value)) {
199 fixed[name] = value.map(i => {
200 if (typeof i === 'object' && i.toJSON) {
201 return i.toJSON(null, inputs)
202 } else {
203 return i
204 }
205 })
206 } else if (typeof value === 'object' && value.toJSON) {
207 fixed[name] = value.toJSON(null, inputs)
208 } else if (name === 'source') {
209 let inputId = inputs.get(value.input)
210 if (inputId == null) {
211 inputId = inputsNextIndex
212 inputs.set(value.input, inputsNextIndex)
213 inputsNextIndex++
214 }
215 fixed[name] = {
216 inputId,
217 start: value.start,
218 end: value.end
219 }
220 } else {
221 fixed[name] = value
222 }
223 }
224
225 if (emitInputs) {
226 fixed.inputs = [...inputs.keys()].map(input => input.toJSON())
227 }
228
229 return fixed
230 }
231
232 positionInside(index) {
233 let string = this.toString()
234 let column = this.source.start.column
235 let line = this.source.start.line
236
237 for (let i = 0; i < index; i++) {
238 if (string[i] === '\n') {
239 column = 1
240 line += 1
241 } else {
242 column += 1
243 }
244 }
245
246 return { line, column }
247 }
248
249 positionBy(opts) {
250 let pos = this.source.start
251 if (opts.index) {
252 pos = this.positionInside(opts.index)
253 } else if (opts.word) {
254 let index = this.toString().indexOf(opts.word)
255 if (index !== -1) pos = this.positionInside(index)
256 }
257 return pos
258 }
259
260 rangeBy(opts) {
261 let start = {
262 line: this.source.start.line,
263 column: this.source.start.column
264 }
265 let end = this.source.end
266 ? {
267 line: this.source.end.line,
268 column: this.source.end.column + 1
269 }
270 : {
271 line: start.line,
272 column: start.column + 1
273 }
274
275 if (opts.word) {
276 let index = this.toString().indexOf(opts.word)
277 if (index !== -1) {
278 start = this.positionInside(index)
279 end = this.positionInside(index + opts.word.length)
280 }
281 } else {
282 if (opts.start) {
283 start = {
284 line: opts.start.line,
285 column: opts.start.column
286 }
287 } else if (opts.index) {
288 start = this.positionInside(opts.index)
289 }
290
291 if (opts.end) {
292 end = {
293 line: opts.end.line,
294 column: opts.end.column
295 }
296 } else if (opts.endIndex) {
297 end = this.positionInside(opts.endIndex)
298 } else if (opts.index) {
299 end = this.positionInside(opts.index + 1)
300 }
301 }
302
303 if (
304 end.line < start.line ||
305 (end.line === start.line && end.column <= start.column)
306 ) {
307 end = { line: start.line, column: start.column + 1 }
308 }
309
310 return { start, end }
311 }
312
313 getProxyProcessor() {
314 return {
315 set(node, prop, value) {
316 if (node[prop] === value) return true
317 node[prop] = value
318 if (
319 prop === 'prop' ||
320 prop === 'value' ||
321 prop === 'name' ||
322 prop === 'params' ||
323 prop === 'important' ||
324 /* c8 ignore next */
325 prop === 'text'
326 ) {
327 node.markDirty()
328 }
329 return true
330 },
331
332 get(node, prop) {
333 if (prop === 'proxyOf') {
334 return node
335 } else if (prop === 'root') {
336 return () => node.root().toProxy()
337 } else {
338 return node[prop]
339 }
340 }
341 }
342 }
343
344 toProxy() {
345 if (!this.proxyCache) {
346 this.proxyCache = new Proxy(this, this.getProxyProcessor())
347 }
348 return this.proxyCache
349 }
350
351 addToError(error) {
352 error.postcssNode = this
353 if (error.stack && this.source && /\n\s{4}at /.test(error.stack)) {
354 let s = this.source
355 error.stack = error.stack.replace(
356 /\n\s{4}at /,
357 `$&${s.input.from}:${s.start.line}:${s.start.column}$&`
358 )
359 }
360 return error
361 }
362
363 markDirty() {
364 if (this[isClean]) {
365 this[isClean] = false
366 let next = this
367 while ((next = next.parent)) {
368 next[isClean] = false
369 }
370 }
371 }
372
373 get proxyOf() {
374 return this
375 }
376}
377
378module.exports = Node
379Node.default = Node