UNPKG

8.22 kBJavaScriptView Raw
1'use strict'
2
3const DEFAULT_RAW = {
4 colon: ': ',
5 indent: ' ',
6 beforeDecl: '\n',
7 beforeRule: '\n',
8 beforeOpen: ' ',
9 beforeClose: '\n',
10 beforeComment: '\n',
11 after: '\n',
12 emptyBody: '',
13 commentLeft: ' ',
14 commentRight: ' ',
15 semicolon: false
16}
17
18function capitalize(str) {
19 return str[0].toUpperCase() + str.slice(1)
20}
21
22class Stringifier {
23 constructor(builder) {
24 this.builder = builder
25 }
26
27 stringify(node, semicolon) {
28 /* c8 ignore start */
29 if (!this[node.type]) {
30 throw new Error(
31 'Unknown AST node type ' +
32 node.type +
33 '. ' +
34 'Maybe you need to change PostCSS stringifier.'
35 )
36 }
37 /* c8 ignore stop */
38 this[node.type](node, semicolon)
39 }
40
41 document(node) {
42 this.body(node)
43 }
44
45 root(node) {
46 this.body(node)
47 if (node.raws.after) this.builder(node.raws.after)
48 }
49
50 comment(node) {
51 let left = this.raw(node, 'left', 'commentLeft')
52 let right = this.raw(node, 'right', 'commentRight')
53 this.builder('/*' + left + node.text + right + '*/', node)
54 }
55
56 decl(node, semicolon) {
57 let between = this.raw(node, 'between', 'colon')
58 let string = node.prop + between + this.rawValue(node, 'value')
59
60 if (node.important) {
61 string += node.raws.important || ' !important'
62 }
63
64 if (semicolon) string += ';'
65 this.builder(string, node)
66 }
67
68 rule(node) {
69 this.block(node, this.rawValue(node, 'selector'))
70 if (node.raws.ownSemicolon) {
71 this.builder(node.raws.ownSemicolon, node, 'end')
72 }
73 }
74
75 atrule(node, semicolon) {
76 let name = '@' + node.name
77 let params = node.params ? this.rawValue(node, 'params') : ''
78
79 if (typeof node.raws.afterName !== 'undefined') {
80 name += node.raws.afterName
81 } else if (params) {
82 name += ' '
83 }
84
85 if (node.nodes) {
86 this.block(node, name + params)
87 } else {
88 let end = (node.raws.between || '') + (semicolon ? ';' : '')
89 this.builder(name + params + end, node)
90 }
91 }
92
93 body(node) {
94 let last = node.nodes.length - 1
95 while (last > 0) {
96 if (node.nodes[last].type !== 'comment') break
97 last -= 1
98 }
99
100 let semicolon = this.raw(node, 'semicolon')
101 for (let i = 0; i < node.nodes.length; i++) {
102 let child = node.nodes[i]
103 let before = this.raw(child, 'before')
104 if (before) this.builder(before)
105 this.stringify(child, last !== i || semicolon)
106 }
107 }
108
109 block(node, start) {
110 let between = this.raw(node, 'between', 'beforeOpen')
111 this.builder(start + between + '{', node, 'start')
112
113 let after
114 if (node.nodes && node.nodes.length) {
115 this.body(node)
116 after = this.raw(node, 'after')
117 } else {
118 after = this.raw(node, 'after', 'emptyBody')
119 }
120
121 if (after) this.builder(after)
122 this.builder('}', node, 'end')
123 }
124
125 raw(node, own, detect) {
126 let value
127 if (!detect) detect = own
128
129 // Already had
130 if (own) {
131 value = node.raws[own]
132 if (typeof value !== 'undefined') return value
133 }
134
135 let parent = node.parent
136
137 if (detect === 'before') {
138 // Hack for first rule in CSS
139 if (!parent || (parent.type === 'root' && parent.first === node)) {
140 return ''
141 }
142
143 // `root` nodes in `document` should use only their own raws
144 if (parent && parent.type === 'document') {
145 return ''
146 }
147 }
148
149 // Floating child without parent
150 if (!parent) return DEFAULT_RAW[detect]
151
152 // Detect style by other nodes
153 let root = node.root()
154 if (!root.rawCache) root.rawCache = {}
155 if (typeof root.rawCache[detect] !== 'undefined') {
156 return root.rawCache[detect]
157 }
158
159 if (detect === 'before' || detect === 'after') {
160 return this.beforeAfter(node, detect)
161 } else {
162 let method = 'raw' + capitalize(detect)
163 if (this[method]) {
164 value = this[method](root, node)
165 } else {
166 root.walk(i => {
167 value = i.raws[own]
168 if (typeof value !== 'undefined') return false
169 })
170 }
171 }
172
173 if (typeof value === 'undefined') value = DEFAULT_RAW[detect]
174
175 root.rawCache[detect] = value
176 return value
177 }
178
179 rawSemicolon(root) {
180 let value
181 root.walk(i => {
182 if (i.nodes && i.nodes.length && i.last.type === 'decl') {
183 value = i.raws.semicolon
184 if (typeof value !== 'undefined') return false
185 }
186 })
187 return value
188 }
189
190 rawEmptyBody(root) {
191 let value
192 root.walk(i => {
193 if (i.nodes && i.nodes.length === 0) {
194 value = i.raws.after
195 if (typeof value !== 'undefined') return false
196 }
197 })
198 return value
199 }
200
201 rawIndent(root) {
202 if (root.raws.indent) return root.raws.indent
203 let value
204 root.walk(i => {
205 let p = i.parent
206 if (p && p !== root && p.parent && p.parent === root) {
207 if (typeof i.raws.before !== 'undefined') {
208 let parts = i.raws.before.split('\n')
209 value = parts[parts.length - 1]
210 value = value.replace(/\S/g, '')
211 return false
212 }
213 }
214 })
215 return value
216 }
217
218 rawBeforeComment(root, node) {
219 let value
220 root.walkComments(i => {
221 if (typeof i.raws.before !== 'undefined') {
222 value = i.raws.before
223 if (value.includes('\n')) {
224 value = value.replace(/[^\n]+$/, '')
225 }
226 return false
227 }
228 })
229 if (typeof value === 'undefined') {
230 value = this.raw(node, null, 'beforeDecl')
231 } else if (value) {
232 value = value.replace(/\S/g, '')
233 }
234 return value
235 }
236
237 rawBeforeDecl(root, node) {
238 let value
239 root.walkDecls(i => {
240 if (typeof i.raws.before !== 'undefined') {
241 value = i.raws.before
242 if (value.includes('\n')) {
243 value = value.replace(/[^\n]+$/, '')
244 }
245 return false
246 }
247 })
248 if (typeof value === 'undefined') {
249 value = this.raw(node, null, 'beforeRule')
250 } else if (value) {
251 value = value.replace(/\S/g, '')
252 }
253 return value
254 }
255
256 rawBeforeRule(root) {
257 let value
258 root.walk(i => {
259 if (i.nodes && (i.parent !== root || root.first !== i)) {
260 if (typeof i.raws.before !== 'undefined') {
261 value = i.raws.before
262 if (value.includes('\n')) {
263 value = value.replace(/[^\n]+$/, '')
264 }
265 return false
266 }
267 }
268 })
269 if (value) value = value.replace(/\S/g, '')
270 return value
271 }
272
273 rawBeforeClose(root) {
274 let value
275 root.walk(i => {
276 if (i.nodes && i.nodes.length > 0) {
277 if (typeof i.raws.after !== 'undefined') {
278 value = i.raws.after
279 if (value.includes('\n')) {
280 value = value.replace(/[^\n]+$/, '')
281 }
282 return false
283 }
284 }
285 })
286 if (value) value = value.replace(/\S/g, '')
287 return value
288 }
289
290 rawBeforeOpen(root) {
291 let value
292 root.walk(i => {
293 if (i.type !== 'decl') {
294 value = i.raws.between
295 if (typeof value !== 'undefined') return false
296 }
297 })
298 return value
299 }
300
301 rawColon(root) {
302 let value
303 root.walkDecls(i => {
304 if (typeof i.raws.between !== 'undefined') {
305 value = i.raws.between.replace(/[^\s:]/g, '')
306 return false
307 }
308 })
309 return value
310 }
311
312 beforeAfter(node, detect) {
313 let value
314 if (node.type === 'decl') {
315 value = this.raw(node, null, 'beforeDecl')
316 } else if (node.type === 'comment') {
317 value = this.raw(node, null, 'beforeComment')
318 } else if (detect === 'before') {
319 value = this.raw(node, null, 'beforeRule')
320 } else {
321 value = this.raw(node, null, 'beforeClose')
322 }
323
324 let buf = node.parent
325 let depth = 0
326 while (buf && buf.type !== 'root') {
327 depth += 1
328 buf = buf.parent
329 }
330
331 if (value.includes('\n')) {
332 let indent = this.raw(node, null, 'indent')
333 if (indent.length) {
334 for (let step = 0; step < depth; step++) value += indent
335 }
336 }
337
338 return value
339 }
340
341 rawValue(node, prop) {
342 let value = node[prop]
343 let raw = node.raws[prop]
344 if (raw && raw.value === value) {
345 return raw.raw
346 }
347
348 return value
349 }
350}
351
352module.exports = Stringifier
353Stringifier.default = Stringifier