449 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			449 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
let range = require('normalize-range')
 | 
						|
let parser = require('postcss-value-parser')
 | 
						|
 | 
						|
let OldValue = require('../old-value')
 | 
						|
let utils = require('../utils')
 | 
						|
let Value = require('../value')
 | 
						|
 | 
						|
let IS_DIRECTION = /top|left|right|bottom/gi
 | 
						|
 | 
						|
class Gradient extends Value {
 | 
						|
  /**
 | 
						|
   * 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
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get div token from exists parameters
 | 
						|
   */
 | 
						|
  cloneDiv(params) {
 | 
						|
    for (let i of params) {
 | 
						|
      if (i.type === 'div' && i.value === ',') {
 | 
						|
        return i
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return { after: ' ', type: 'div', value: ',' }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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`
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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)
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Normalize angle
 | 
						|
   */
 | 
						|
  normalize(nodes, gradientName) {
 | 
						|
    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 (
 | 
						|
      gradientName === 'linear-gradient' ||
 | 
						|
      gradientName === 'repeating-linear-gradient'
 | 
						|
    ) {
 | 
						|
      let direction = nodes[0].value
 | 
						|
 | 
						|
      // Unitless zero for `<angle>` values are allowed in CSS gradients and transforms.
 | 
						|
      // Spec: https://github.com/w3c/csswg-drafts/commit/602789171429b2231223ab1e5acf8f7f11652eb3
 | 
						|
      if (direction === '0deg' || direction === '0') {
 | 
						|
        nodes = this.replaceFirst(nodes, 'to', ' ', 'top')
 | 
						|
      } else if (direction === '90deg') {
 | 
						|
        nodes = this.replaceFirst(nodes, 'to', ' ', 'right')
 | 
						|
      } else if (direction === '180deg') {
 | 
						|
        nodes = this.replaceFirst(nodes, 'to', ' ', 'bottom') // default value
 | 
						|
      } else if (direction === '270deg') {
 | 
						|
        nodes = this.replaceFirst(nodes, 'to', ' ', 'left')
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return nodes
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Convert angle unit to deg
 | 
						|
   */
 | 
						|
  normalizeUnit(str, full) {
 | 
						|
    let num = parseFloat(str)
 | 
						|
    let deg = (num / full) * 360
 | 
						|
    return `${deg}deg`
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Remove old WebKit gradient too
 | 
						|
   */
 | 
						|
  old(prefix) {
 | 
						|
    if (prefix === '-webkit-') {
 | 
						|
      let type
 | 
						|
      if (this.name === 'linear-gradient') {
 | 
						|
        type = 'linear'
 | 
						|
      } else if (this.name === 'repeating-linear-gradient') {
 | 
						|
        type = 'repeating-linear'
 | 
						|
      } else if (this.name === 'repeating-radial-gradient') {
 | 
						|
        type = 'repeating-radial'
 | 
						|
      } else {
 | 
						|
        type = '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)
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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]
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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 degrees for webkit prefix
 | 
						|
   */
 | 
						|
  replace(string, prefix) {
 | 
						|
    let ast = parser(string)
 | 
						|
    for (let node of ast.nodes) {
 | 
						|
      let gradientName = this.name // gradient name
 | 
						|
      if (node.type === 'function' && node.value === gradientName) {
 | 
						|
        node.nodes = this.newDirection(node.nodes)
 | 
						|
        node.nodes = this.normalize(node.nodes, gradientName)
 | 
						|
        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))
 | 
						|
  }
 | 
						|
 | 
						|
  revertDirection(word) {
 | 
						|
    return Gradient.directions[word.toLowerCase()] || word
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Round float and save digits under dot
 | 
						|
   */
 | 
						|
  roundFloat(float, digits) {
 | 
						|
    return parseFloat(float.toFixed(digits))
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
Gradient.names = [
 | 
						|
  'linear-gradient',
 | 
						|
  'repeating-linear-gradient',
 | 
						|
  'radial-gradient',
 | 
						|
  'repeating-radial-gradient'
 | 
						|
]
 | 
						|
 | 
						|
Gradient.directions = {
 | 
						|
  bottom: 'top',
 | 
						|
  left: 'right',
 | 
						|
  right: 'left',
 | 
						|
  top: 'bottom' // default value
 | 
						|
}
 | 
						|
 | 
						|
// Direction to replace
 | 
						|
Gradient.oldDirections = {
 | 
						|
  'bottom': 'left top, left bottom',
 | 
						|
  'bottom left': 'right top, left bottom',
 | 
						|
  'bottom right': 'left top, right bottom',
 | 
						|
  'left': 'right top, left top',
 | 
						|
 | 
						|
  'left bottom': 'right top, left bottom',
 | 
						|
  'left top': 'right bottom, left top',
 | 
						|
  'right': 'left top, right top',
 | 
						|
  'right bottom': 'left top, right bottom',
 | 
						|
  'right top': 'left bottom, right top',
 | 
						|
  'top': 'left bottom, left top',
 | 
						|
  'top left': 'right bottom, left top',
 | 
						|
  'top right': 'left bottom, right top'
 | 
						|
}
 | 
						|
 | 
						|
module.exports = Gradient
 |