let parser = require('postcss-value-parser') let range = require('normalize-range') let OldValue = require('../old-value') let Value = require('../value') let utils = require('../utils') let IS_DIRECTION = /top|left|right|bottom/gi class Gradient extends Value { /** * Change degrees for webkit prefix */ replace(string, prefix) { let ast = parser(string) for (let node of ast.nodes) { if (node.type === 'function' && node.value === this.name) { node.nodes = this.newDirection(node.nodes) node.nodes = this.normalize(node.nodes) if (prefix === '-webkit- old') { let changes = this.oldWebkit(node) if (!changes) { return false } } else { node.nodes = this.convertDirection(node.nodes) node.value = prefix + node.value } } } return ast.toString() } /** * Replace first token */ replaceFirst(params, ...words) { let prefix = words.map(i => { if (i === ' ') { return { type: 'space', value: i } } return { type: 'word', value: i } }) return prefix.concat(params.slice(1)) } /** * Convert angle unit to deg */ normalizeUnit(str, full) { let num = parseFloat(str) let deg = (num / full) * 360 return `${deg}deg` } /** * Normalize angle */ normalize(nodes) { if (!nodes[0]) return nodes if (/-?\d+(.\d+)?grad/.test(nodes[0].value)) { nodes[0].value = this.normalizeUnit(nodes[0].value, 400) } else if (/-?\d+(.\d+)?rad/.test(nodes[0].value)) { nodes[0].value = this.normalizeUnit(nodes[0].value, 2 * Math.PI) } else if (/-?\d+(.\d+)?turn/.test(nodes[0].value)) { nodes[0].value = this.normalizeUnit(nodes[0].value, 1) } else if (nodes[0].value.includes('deg')) { let num = parseFloat(nodes[0].value) num = range.wrap(0, 360, num) nodes[0].value = `${num}deg` } if (nodes[0].value === '0deg') { nodes = this.replaceFirst(nodes, 'to', ' ', 'top') } else if (nodes[0].value === '90deg') { nodes = this.replaceFirst(nodes, 'to', ' ', 'right') } else if (nodes[0].value === '180deg') { nodes = this.replaceFirst(nodes, 'to', ' ', 'bottom') } else if (nodes[0].value === '270deg') { nodes = this.replaceFirst(nodes, 'to', ' ', 'left') } return nodes } /** * Replace old direction to new */ newDirection(params) { if (params[0].value === 'to') { return params } IS_DIRECTION.lastIndex = 0 // reset search index of global regexp if (!IS_DIRECTION.test(params[0].value)) { return params } params.unshift( { type: 'word', value: 'to' }, { type: 'space', value: ' ' } ) for (let i = 2; i < params.length; i++) { if (params[i].type === 'div') { break } if (params[i].type === 'word') { params[i].value = this.revertDirection(params[i].value) } } return params } /** * Look for at word */ isRadial(params) { let state = 'before' for (let param of params) { if (state === 'before' && param.type === 'space') { state = 'at' } else if (state === 'at' && param.value === 'at') { state = 'after' } else if (state === 'after' && param.type === 'space') { return true } else if (param.type === 'div') { break } else { state = 'before' } } return false } /** * Change new direction to old */ convertDirection(params) { if (params.length > 0) { if (params[0].value === 'to') { this.fixDirection(params) } else if (params[0].value.includes('deg')) { this.fixAngle(params) } else if (this.isRadial(params)) { this.fixRadial(params) } } return params } /** * Replace `to top left` to `bottom right` */ fixDirection(params) { params.splice(0, 2) for (let param of params) { if (param.type === 'div') { break } if (param.type === 'word') { param.value = this.revertDirection(param.value) } } } /** * Add 90 degrees */ fixAngle(params) { let first = params[0].value first = parseFloat(first) first = Math.abs(450 - first) % 360 first = this.roundFloat(first, 3) params[0].value = `${first}deg` } /** * Fix radial direction syntax */ fixRadial(params) { let first = [] let second = [] let a, b, c, i, next for (i = 0; i < params.length - 2; i++) { a = params[i] b = params[i + 1] c = params[i + 2] if (a.type === 'space' && b.value === 'at' && c.type === 'space') { next = i + 3 break } else { first.push(a) } } let div for (i = next; i < params.length; i++) { if (params[i].type === 'div') { div = params[i] break } else { second.push(params[i]) } } params.splice(0, i, ...second, div, ...first) } revertDirection(word) { return Gradient.directions[word.toLowerCase()] || word } /** * Round float and save digits under dot */ roundFloat(float, digits) { return parseFloat(float.toFixed(digits)) } /** * Convert to old webkit syntax */ oldWebkit(node) { let { nodes } = node let string = parser.stringify(node.nodes) if (this.name !== 'linear-gradient') { return false } if (nodes[0] && nodes[0].value.includes('deg')) { return false } if ( string.includes('px') || string.includes('-corner') || string.includes('-side') ) { return false } let params = [[]] for (let i of nodes) { params[params.length - 1].push(i) if (i.type === 'div' && i.value === ',') { params.push([]) } } this.oldDirection(params) this.colorStops(params) node.nodes = [] for (let param of params) { node.nodes = node.nodes.concat(param) } node.nodes.unshift( { type: 'word', value: 'linear' }, this.cloneDiv(node.nodes) ) node.value = '-webkit-gradient' return true } /** * Change direction syntax to old webkit */ oldDirection(params) { let div = this.cloneDiv(params[0]) if (params[0][0].value !== 'to') { return params.unshift([ { type: 'word', value: Gradient.oldDirections.bottom }, div ]) } else { let words = [] for (let node of params[0].slice(2)) { if (node.type === 'word') { words.push(node.value.toLowerCase()) } } words = words.join(' ') let old = Gradient.oldDirections[words] || words params[0] = [{ type: 'word', value: old }, div] return params[0] } } /** * Get div token from exists parameters */ cloneDiv(params) { for (let i of params) { if (i.type === 'div' && i.value === ',') { return i } } return { type: 'div', value: ',', after: ' ' } } /** * Change colors syntax to old webkit */ colorStops(params) { let result = [] for (let i = 0; i < params.length; i++) { let pos let param = params[i] let item if (i === 0) { continue } let color = parser.stringify(param[0]) if (param[1] && param[1].type === 'word') { pos = param[1].value } else if (param[2] && param[2].type === 'word') { pos = param[2].value } let stop if (i === 1 && (!pos || pos === '0%')) { stop = `from(${color})` } else if (i === params.length - 1 && (!pos || pos === '100%')) { stop = `to(${color})` } else if (pos) { stop = `color-stop(${pos}, ${color})` } else { stop = `color-stop(${color})` } let div = param[param.length - 1] params[i] = [{ type: 'word', value: stop }] if (div.type === 'div' && div.value === ',') { item = params[i].push(div) } result.push(item) } return result } /** * Remove old WebKit gradient too */ old(prefix) { if (prefix === '-webkit-') { let type = this.name === 'linear-gradient' ? 'linear' : 'radial' let string = '-gradient' let regexp = utils.regexp( `-webkit-(${type}-gradient|gradient\\(\\s*${type})`, false ) return new OldValue(this.name, prefix + this.name, string, regexp) } else { return super.old(prefix) } } /** * Do not add non-webkit prefixes for list-style and object */ add(decl, prefix) { let p = decl.prop if (p.includes('mask')) { if (prefix === '-webkit-' || prefix === '-webkit- old') { return super.add(decl, prefix) } } else if ( p === 'list-style' || p === 'list-style-image' || p === 'content' ) { if (prefix === '-webkit-' || prefix === '-webkit- old') { return super.add(decl, prefix) } } else { return super.add(decl, prefix) } return undefined } } Gradient.names = [ 'linear-gradient', 'repeating-linear-gradient', 'radial-gradient', 'repeating-radial-gradient' ] Gradient.directions = { top: 'bottom', left: 'right', bottom: 'top', right: 'left' } // Direction to replace Gradient.oldDirections = { 'top': 'left bottom, left top', 'left': 'right top, left top', 'bottom': 'left top, left bottom', 'right': 'left top, right top', 'top right': 'left bottom, right top', 'top left': 'right bottom, left top', 'right top': 'left bottom, right top', 'right bottom': 'left top, right bottom', 'bottom right': 'left top, right bottom', 'bottom left': 'right top, left bottom', 'left top': 'right bottom, left top', 'left bottom': 'right top, left bottom' } module.exports = Gradient