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.
429 lines
9.7 KiB
429 lines
9.7 KiB
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
|
|
|