You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

430 lines
9.7 KiB

2 years ago
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