UNPKG

8.75 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 rangeBy(opts) {
208 let start = {
209 column: this.source.start.column,
210 line: this.source.start.line
211 }
212 let end = this.source.end
213 ? {
214 column: this.source.end.column + 1,
215 line: this.source.end.line
216 }
217 : {
218 column: start.column + 1,
219 line: start.line
220 }
221
222 if (opts.word) {
223 let stringRepresentation = this.toString()
224 let index = stringRepresentation.indexOf(opts.word)
225 if (index !== -1) {
226 start = this.positionInside(index, stringRepresentation)
227 end = this.positionInside(index + opts.word.length, stringRepresentation)
228 }
229 } else {
230 if (opts.start) {
231 start = {
232 column: opts.start.column,
233 line: opts.start.line
234 }
235 } else if (opts.index) {
236 start = this.positionInside(opts.index)
237 }
238
239 if (opts.end) {
240 end = {
241 column: opts.end.column,
242 line: opts.end.line
243 }
244 } else if (typeof opts.endIndex === 'number') {
245 end = this.positionInside(opts.endIndex)
246 } else if (opts.index) {
247 end = this.positionInside(opts.index + 1)
248 }
249 }
250
251 if (
252 end.line < start.line ||
253 (end.line === start.line && end.column <= start.column)
254 ) {
255 end = { column: start.column + 1, line: start.line }
256 }
257
258 return { end, start }
259 }
260
261 raw(prop, defaultType) {
262 let str = new Stringifier()
263 return str.raw(this, prop, defaultType)
264 }
265
266 remove() {
267 if (this.parent) {
268 this.parent.removeChild(this)
269 }
270 this.parent = undefined
271 return this
272 }
273
274 replaceWith(...nodes) {
275 if (this.parent) {
276 let bookmark = this
277 let foundSelf = false
278 for (let node of nodes) {
279 if (node === this) {
280 foundSelf = true
281 } else if (foundSelf) {
282 this.parent.insertAfter(bookmark, node)
283 bookmark = node
284 } else {
285 this.parent.insertBefore(bookmark, node)
286 }
287 }
288
289 if (!foundSelf) {
290 this.remove()
291 }
292 }
293
294 return this
295 }
296
297 root() {
298 let result = this
299 while (result.parent && result.parent.type !== 'document') {
300 result = result.parent
301 }
302 return result
303 }
304
305 toJSON(_, inputs) {
306 let fixed = {}
307 let emitInputs = inputs == null
308 inputs = inputs || new Map()
309 let inputsNextIndex = 0
310
311 for (let name in this) {
312 if (!Object.prototype.hasOwnProperty.call(this, name)) {
313 /* c8 ignore next 2 */
314 continue
315 }
316 if (name === 'parent' || name === 'proxyCache') continue
317 let value = this[name]
318
319 if (Array.isArray(value)) {
320 fixed[name] = value.map(i => {
321 if (typeof i === 'object' && i.toJSON) {
322 return i.toJSON(null, inputs)
323 } else {
324 return i
325 }
326 })
327 } else if (typeof value === 'object' && value.toJSON) {
328 fixed[name] = value.toJSON(null, inputs)
329 } else if (name === 'source') {
330 let inputId = inputs.get(value.input)
331 if (inputId == null) {
332 inputId = inputsNextIndex
333 inputs.set(value.input, inputsNextIndex)
334 inputsNextIndex++
335 }
336 fixed[name] = {
337 end: value.end,
338 inputId,
339 start: value.start
340 }
341 } else {
342 fixed[name] = value
343 }
344 }
345
346 if (emitInputs) {
347 fixed.inputs = [...inputs.keys()].map(input => input.toJSON())
348 }
349
350 return fixed
351 }
352
353 toProxy() {
354 if (!this.proxyCache) {
355 this.proxyCache = new Proxy(this, this.getProxyProcessor())
356 }
357 return this.proxyCache
358 }
359
360 toString(stringifier = stringify) {
361 if (stringifier.stringify) stringifier = stringifier.stringify
362 let result = ''
363 stringifier(this, i => {
364 result += i
365 })
366 return result
367 }
368
369 warn(result, text, opts) {
370 let data = { node: this }
371 for (let i in opts) data[i] = opts[i]
372 return result.warn(text, data)
373 }
374
375 get proxyOf() {
376 return this
377 }
378}
379
380module.exports = Node
381Node.default = Node