UNPKG

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