forked from rDrama/rDrama
354 lines
8.0 KiB
JavaScript
354 lines
8.0 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const DEFAULT_RAW = {
|
||
|
colon: ': ',
|
||
|
indent: ' ',
|
||
|
beforeDecl: '\n',
|
||
|
beforeRule: '\n',
|
||
|
beforeOpen: ' ',
|
||
|
beforeClose: '\n',
|
||
|
beforeComment: '\n',
|
||
|
after: '\n',
|
||
|
emptyBody: '',
|
||
|
commentLeft: ' ',
|
||
|
commentRight: ' ',
|
||
|
semicolon: false
|
||
|
}
|
||
|
|
||
|
function capitalize(str) {
|
||
|
return str[0].toUpperCase() + str.slice(1)
|
||
|
}
|
||
|
|
||
|
class Stringifier {
|
||
|
constructor(builder) {
|
||
|
this.builder = builder
|
||
|
}
|
||
|
|
||
|
stringify(node, semicolon) {
|
||
|
/* c8 ignore start */
|
||
|
if (!this[node.type]) {
|
||
|
throw new Error(
|
||
|
'Unknown AST node type ' +
|
||
|
node.type +
|
||
|
'. ' +
|
||
|
'Maybe you need to change PostCSS stringifier.'
|
||
|
)
|
||
|
}
|
||
|
/* c8 ignore stop */
|
||
|
this[node.type](node, semicolon)
|
||
|
}
|
||
|
|
||
|
document(node) {
|
||
|
this.body(node)
|
||
|
}
|
||
|
|
||
|
root(node) {
|
||
|
this.body(node)
|
||
|
if (node.raws.after) this.builder(node.raws.after)
|
||
|
}
|
||
|
|
||
|
comment(node) {
|
||
|
let left = this.raw(node, 'left', 'commentLeft')
|
||
|
let right = this.raw(node, 'right', 'commentRight')
|
||
|
this.builder('/*' + left + node.text + right + '*/', node)
|
||
|
}
|
||
|
|
||
|
decl(node, semicolon) {
|
||
|
let between = this.raw(node, 'between', 'colon')
|
||
|
let string = node.prop + between + this.rawValue(node, 'value')
|
||
|
|
||
|
if (node.important) {
|
||
|
string += node.raws.important || ' !important'
|
||
|
}
|
||
|
|
||
|
if (semicolon) string += ';'
|
||
|
this.builder(string, node)
|
||
|
}
|
||
|
|
||
|
rule(node) {
|
||
|
this.block(node, this.rawValue(node, 'selector'))
|
||
|
if (node.raws.ownSemicolon) {
|
||
|
this.builder(node.raws.ownSemicolon, node, 'end')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
atrule(node, semicolon) {
|
||
|
let name = '@' + node.name
|
||
|
let params = node.params ? this.rawValue(node, 'params') : ''
|
||
|
|
||
|
if (typeof node.raws.afterName !== 'undefined') {
|
||
|
name += node.raws.afterName
|
||
|
} else if (params) {
|
||
|
name += ' '
|
||
|
}
|
||
|
|
||
|
if (node.nodes) {
|
||
|
this.block(node, name + params)
|
||
|
} else {
|
||
|
let end = (node.raws.between || '') + (semicolon ? ';' : '')
|
||
|
this.builder(name + params + end, node)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
body(node) {
|
||
|
let last = node.nodes.length - 1
|
||
|
while (last > 0) {
|
||
|
if (node.nodes[last].type !== 'comment') break
|
||
|
last -= 1
|
||
|
}
|
||
|
|
||
|
let semicolon = this.raw(node, 'semicolon')
|
||
|
for (let i = 0; i < node.nodes.length; i++) {
|
||
|
let child = node.nodes[i]
|
||
|
let before = this.raw(child, 'before')
|
||
|
if (before) this.builder(before)
|
||
|
this.stringify(child, last !== i || semicolon)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
block(node, start) {
|
||
|
let between = this.raw(node, 'between', 'beforeOpen')
|
||
|
this.builder(start + between + '{', node, 'start')
|
||
|
|
||
|
let after
|
||
|
if (node.nodes && node.nodes.length) {
|
||
|
this.body(node)
|
||
|
after = this.raw(node, 'after')
|
||
|
} else {
|
||
|
after = this.raw(node, 'after', 'emptyBody')
|
||
|
}
|
||
|
|
||
|
if (after) this.builder(after)
|
||
|
this.builder('}', node, 'end')
|
||
|
}
|
||
|
|
||
|
raw(node, own, detect) {
|
||
|
let value
|
||
|
if (!detect) detect = own
|
||
|
|
||
|
// Already had
|
||
|
if (own) {
|
||
|
value = node.raws[own]
|
||
|
if (typeof value !== 'undefined') return value
|
||
|
}
|
||
|
|
||
|
let parent = node.parent
|
||
|
|
||
|
if (detect === 'before') {
|
||
|
// Hack for first rule in CSS
|
||
|
if (!parent || (parent.type === 'root' && parent.first === node)) {
|
||
|
return ''
|
||
|
}
|
||
|
|
||
|
// `root` nodes in `document` should use only their own raws
|
||
|
if (parent && parent.type === 'document') {
|
||
|
return ''
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Floating child without parent
|
||
|
if (!parent) return DEFAULT_RAW[detect]
|
||
|
|
||
|
// Detect style by other nodes
|
||
|
let root = node.root()
|
||
|
if (!root.rawCache) root.rawCache = {}
|
||
|
if (typeof root.rawCache[detect] !== 'undefined') {
|
||
|
return root.rawCache[detect]
|
||
|
}
|
||
|
|
||
|
if (detect === 'before' || detect === 'after') {
|
||
|
return this.beforeAfter(node, detect)
|
||
|
} else {
|
||
|
let method = 'raw' + capitalize(detect)
|
||
|
if (this[method]) {
|
||
|
value = this[method](root, node)
|
||
|
} else {
|
||
|
root.walk(i => {
|
||
|
value = i.raws[own]
|
||
|
if (typeof value !== 'undefined') return false
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (typeof value === 'undefined') value = DEFAULT_RAW[detect]
|
||
|
|
||
|
root.rawCache[detect] = value
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawSemicolon(root) {
|
||
|
let value
|
||
|
root.walk(i => {
|
||
|
if (i.nodes && i.nodes.length && i.last.type === 'decl') {
|
||
|
value = i.raws.semicolon
|
||
|
if (typeof value !== 'undefined') return false
|
||
|
}
|
||
|
})
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawEmptyBody(root) {
|
||
|
let value
|
||
|
root.walk(i => {
|
||
|
if (i.nodes && i.nodes.length === 0) {
|
||
|
value = i.raws.after
|
||
|
if (typeof value !== 'undefined') return false
|
||
|
}
|
||
|
})
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawIndent(root) {
|
||
|
if (root.raws.indent) return root.raws.indent
|
||
|
let value
|
||
|
root.walk(i => {
|
||
|
let p = i.parent
|
||
|
if (p && p !== root && p.parent && p.parent === root) {
|
||
|
if (typeof i.raws.before !== 'undefined') {
|
||
|
let parts = i.raws.before.split('\n')
|
||
|
value = parts[parts.length - 1]
|
||
|
value = value.replace(/\S/g, '')
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawBeforeComment(root, node) {
|
||
|
let value
|
||
|
root.walkComments(i => {
|
||
|
if (typeof i.raws.before !== 'undefined') {
|
||
|
value = i.raws.before
|
||
|
if (value.includes('\n')) {
|
||
|
value = value.replace(/[^\n]+$/, '')
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
})
|
||
|
if (typeof value === 'undefined') {
|
||
|
value = this.raw(node, null, 'beforeDecl')
|
||
|
} else if (value) {
|
||
|
value = value.replace(/\S/g, '')
|
||
|
}
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawBeforeDecl(root, node) {
|
||
|
let value
|
||
|
root.walkDecls(i => {
|
||
|
if (typeof i.raws.before !== 'undefined') {
|
||
|
value = i.raws.before
|
||
|
if (value.includes('\n')) {
|
||
|
value = value.replace(/[^\n]+$/, '')
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
})
|
||
|
if (typeof value === 'undefined') {
|
||
|
value = this.raw(node, null, 'beforeRule')
|
||
|
} else if (value) {
|
||
|
value = value.replace(/\S/g, '')
|
||
|
}
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawBeforeRule(root) {
|
||
|
let value
|
||
|
root.walk(i => {
|
||
|
if (i.nodes && (i.parent !== root || root.first !== i)) {
|
||
|
if (typeof i.raws.before !== 'undefined') {
|
||
|
value = i.raws.before
|
||
|
if (value.includes('\n')) {
|
||
|
value = value.replace(/[^\n]+$/, '')
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
if (value) value = value.replace(/\S/g, '')
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawBeforeClose(root) {
|
||
|
let value
|
||
|
root.walk(i => {
|
||
|
if (i.nodes && i.nodes.length > 0) {
|
||
|
if (typeof i.raws.after !== 'undefined') {
|
||
|
value = i.raws.after
|
||
|
if (value.includes('\n')) {
|
||
|
value = value.replace(/[^\n]+$/, '')
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
if (value) value = value.replace(/\S/g, '')
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawBeforeOpen(root) {
|
||
|
let value
|
||
|
root.walk(i => {
|
||
|
if (i.type !== 'decl') {
|
||
|
value = i.raws.between
|
||
|
if (typeof value !== 'undefined') return false
|
||
|
}
|
||
|
})
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawColon(root) {
|
||
|
let value
|
||
|
root.walkDecls(i => {
|
||
|
if (typeof i.raws.between !== 'undefined') {
|
||
|
value = i.raws.between.replace(/[^\s:]/g, '')
|
||
|
return false
|
||
|
}
|
||
|
})
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
beforeAfter(node, detect) {
|
||
|
let value
|
||
|
if (node.type === 'decl') {
|
||
|
value = this.raw(node, null, 'beforeDecl')
|
||
|
} else if (node.type === 'comment') {
|
||
|
value = this.raw(node, null, 'beforeComment')
|
||
|
} else if (detect === 'before') {
|
||
|
value = this.raw(node, null, 'beforeRule')
|
||
|
} else {
|
||
|
value = this.raw(node, null, 'beforeClose')
|
||
|
}
|
||
|
|
||
|
let buf = node.parent
|
||
|
let depth = 0
|
||
|
while (buf && buf.type !== 'root') {
|
||
|
depth += 1
|
||
|
buf = buf.parent
|
||
|
}
|
||
|
|
||
|
if (value.includes('\n')) {
|
||
|
let indent = this.raw(node, null, 'indent')
|
||
|
if (indent.length) {
|
||
|
for (let step = 0; step < depth; step++) value += indent
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
rawValue(node, prop) {
|
||
|
let value = node[prop]
|
||
|
let raw = node.raws[prop]
|
||
|
if (raw && raw.value === value) {
|
||
|
return raw.raw
|
||
|
}
|
||
|
|
||
|
return value
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Stringifier
|
||
|
Stringifier.default = Stringifier
|