| 'use strict'
|
|
|
|
|
|
|
|
|
| const STYLE_TAG = /(<)(\/?style\b)/gi
|
| const COMMENT_OPEN = /(<)(!--)/g
|
|
|
|
|
|
|
| const AT_NAME_END = /[\t\n\f\r "#'()/;[\\\]{}]/
|
|
|
| function escapeHTMLInCSS(str) {
|
| if (typeof str !== 'string') return str
|
| if (!str.includes('<')) return str
|
| return str.replace(STYLE_TAG, '\\3c $2').replace(COMMENT_OPEN, '\\3c $2')
|
| }
|
|
|
| const DEFAULT_RAW = {
|
| after: '\n',
|
| beforeClose: '\n',
|
| beforeComment: '\n',
|
| beforeDecl: '\n',
|
| beforeOpen: ' ',
|
| beforeRule: '\n',
|
| colon: ': ',
|
| commentLeft: ' ',
|
| commentRight: ' ',
|
| emptyBody: '',
|
| indent: ' ',
|
| semicolon: false
|
| }
|
|
|
| function capitalize(str) {
|
| return str[0].toUpperCase() + str.slice(1)
|
| }
|
|
|
| function atruleStart(str, node) {
|
| let name = '@' + node.name
|
| let params = node.params ? str.rawValue(node, 'params') : ''
|
| let afterName = node.raws.afterName
|
|
|
| if (typeof afterName === 'undefined') {
|
| afterName = params ? ' ' : ''
|
| } else if (afterName === '' && params && !AT_NAME_END.test(params[0])) {
|
| afterName = ' '
|
| }
|
|
|
| return name + afterName + params
|
| }
|
|
|
| function pushBody(str, stack, node) {
|
| let nodes = node.nodes
|
| let last = nodes.length - 1
|
| while (last > 0) {
|
| if (nodes[last].type !== 'comment') break
|
| last -= 1
|
| }
|
|
|
| let semicolon = str.raw(node, 'semicolon')
|
| let isDocument = node.type === 'document'
|
| for (let i = nodes.length - 1; i >= 0; i--) {
|
| let child = nodes[i]
|
| let childSemicolon = last !== i || semicolon
|
|
|
|
|
|
|
|
|
| if (
|
| !childSemicolon &&
|
| i < nodes.length - 1 &&
|
| ((child.type === 'atrule' && !child.nodes) ||
|
| (child.type === 'decl' && child.prop.startsWith('--')))
|
| ) {
|
| childSemicolon = true
|
| }
|
| stack.push({
|
| document: isDocument,
|
| node: child,
|
| semicolon: childSemicolon
|
| })
|
| }
|
| }
|
|
|
| function pushBlock(str, stack, node, start) {
|
| let between = str.raw(node, 'between', 'beforeOpen')
|
| str.builder(escapeHTMLInCSS(start + between) + '{', node, 'start')
|
|
|
| let hasNodes = node.nodes && node.nodes.length
|
| let close = () => {
|
| let after = hasNodes
|
| ? str.raw(node, 'after')
|
| : str.raw(node, 'after', 'emptyBody')
|
| if (after) str.builder(escapeHTMLInCSS(after))
|
| str.builder('}', node, 'end')
|
| if (node.type === 'rule' && node.raws.ownSemicolon) {
|
| str.builder(escapeHTMLInCSS(node.raws.ownSemicolon), node, 'end')
|
| }
|
| }
|
|
|
| if (hasNodes) {
|
| stack.push(close)
|
| pushBody(str, stack, node)
|
| } else {
|
| close()
|
| }
|
| }
|
|
|
| class Stringifier {
|
| constructor(builder) {
|
| this.builder = builder
|
| }
|
|
|
| atrule(node, semicolon) {
|
| let start = atruleStart(this, node)
|
| if (node.nodes) {
|
| this.block(node, start)
|
| } else {
|
| let end = (node.raws.between || '') + (semicolon ? ';' : '')
|
| this.builder(escapeHTMLInCSS(start + end), node)
|
| }
|
| }
|
|
|
| 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
|
| }
|
|
|
| block(node, start) {
|
| let between = this.raw(node, 'between', 'beforeOpen')
|
| this.builder(escapeHTMLInCSS(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(escapeHTMLInCSS(after))
|
| this.builder('}', node, 'end')
|
| }
|
|
|
| body(node) {
|
|
|
|
|
|
|
|
|
| let proto = Stringifier.prototype
|
| let expandable = ['atrule', 'block', 'body', 'rule', 'stringify'].every(
|
| method => this[method] === proto[method]
|
| )
|
|
|
| let stack = []
|
| pushBody(this, stack, node)
|
|
|
| while (stack.length > 0) {
|
| let entry = stack.pop()
|
| if (typeof entry === 'function') {
|
| entry()
|
| continue
|
| }
|
|
|
| let child = entry.node
|
| let before = this.raw(child, 'before')
|
| if (before) {
|
| this.builder(entry.document ? before : escapeHTMLInCSS(before))
|
| }
|
|
|
| if (expandable && child.type === 'rule') {
|
| pushBlock(this, stack, child, this.rawValue(child, 'selector'))
|
| } else if (expandable && child.type === 'atrule' && child.nodes) {
|
| pushBlock(this, stack, child, atruleStart(this, child))
|
| } else {
|
| this.stringify(child, entry.semicolon)
|
| }
|
| }
|
| }
|
|
|
| comment(node) {
|
| let left = this.raw(node, 'left', 'commentLeft')
|
| let right = this.raw(node, 'right', 'commentRight')
|
| this.builder(escapeHTMLInCSS('/*' + left + node.text + right + '*/'), node)
|
| }
|
|
|
| decl(node, semicolon) {
|
| let raws = node.raws
|
| let between = this.raw(node, 'between', 'colon')
|
|
|
| let string = node.prop + between + this.rawValue(node, 'value')
|
|
|
| if (node.important) {
|
| string += raws.important || ' !important'
|
| }
|
|
|
| if (semicolon) string += ';'
|
| this.builder(escapeHTMLInCSS(string), node)
|
| }
|
|
|
| document(node) {
|
| this.body(node)
|
| }
|
|
|
| raw(node, own, detect) {
|
| let value
|
| if (!detect) detect = own
|
|
|
|
|
| if (own) {
|
| value = node.raws[own]
|
| if (typeof value !== 'undefined') return value
|
| }
|
|
|
| let parent = node.parent
|
|
|
| if (detect === 'before') {
|
|
|
| if (!parent || (parent.type === 'root' && parent.first === node)) {
|
| return ''
|
| }
|
|
|
|
|
| if (parent && parent.type === 'document') {
|
| return ''
|
| }
|
| }
|
|
|
|
|
| if (!parent) return DEFAULT_RAW[detect]
|
|
|
|
|
| let root = node.root()
|
| let cache = root.rawCache || (root.rawCache = {})
|
| if (typeof cache[detect] !== 'undefined') {
|
| return cache[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]
|
|
|
| cache[detect] = value
|
| 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
|
| }
|
|
|
| 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
|
| }
|
|
|
| rawBeforeOpen(root) {
|
| let value
|
| root.walk(i => {
|
| if (i.type !== 'decl') {
|
| value = i.raws.between
|
| if (typeof value !== 'undefined') return false
|
| }
|
| })
|
| 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
|
| }
|
|
|
| rawColon(root) {
|
| let value
|
| root.walkDecls(i => {
|
| if (typeof i.raws.between !== 'undefined') {
|
| value = i.raws.between.replace(/[^\s:]/g, '')
|
| 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
|
| }
|
|
|
| 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
|
| }
|
|
|
| rawValue(node, prop) {
|
| let value = node[prop]
|
| let raw = node.raws[prop]
|
| if (raw && raw.value === value) {
|
| return raw.raw
|
| }
|
|
|
| return value
|
| }
|
|
|
| root(node) {
|
| if (node.source && node.source.input.hasBOM) {
|
| this.builder('\uFEFF', node, 'start')
|
| }
|
| this.body(node)
|
| if (node.raws.after) {
|
| let after = node.raws.after
|
| let isDocument = node.parent && node.parent.type === 'document'
|
| this.builder(isDocument ? after : escapeHTMLInCSS(after))
|
| }
|
| }
|
|
|
| rule(node) {
|
| this.block(node, this.rawValue(node, 'selector'))
|
| if (node.raws.ownSemicolon) {
|
| this.builder(escapeHTMLInCSS(node.raws.ownSemicolon), node, 'end')
|
| }
|
| }
|
|
|
| stringify(node, semicolon) {
|
|
|
| if (!this[node.type]) {
|
| throw new Error(
|
| 'Unknown AST node type ' +
|
| node.type +
|
| '. ' +
|
| 'Maybe you need to change PostCSS stringifier.'
|
| )
|
| }
|
|
|
| this[node.type](node, semicolon)
|
| }
|
| }
|
|
|
| module.exports = Stringifier
|
| Stringifier.default = Stringifier
|
|
|