gradient.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. let parser = require('postcss-value-parser')
  2. let range = require('normalize-range')
  3. let OldValue = require('../old-value')
  4. let Value = require('../value')
  5. let utils = require('../utils')
  6. let IS_DIRECTION = /top|left|right|bottom/gi
  7. class Gradient extends Value {
  8. /**
  9. * Do not add non-webkit prefixes for list-style and object
  10. */
  11. add(decl, prefix) {
  12. let p = decl.prop
  13. if (p.includes('mask')) {
  14. if (prefix === '-webkit-' || prefix === '-webkit- old') {
  15. return super.add(decl, prefix)
  16. }
  17. } else if (
  18. p === 'list-style' ||
  19. p === 'list-style-image' ||
  20. p === 'content'
  21. ) {
  22. if (prefix === '-webkit-' || prefix === '-webkit- old') {
  23. return super.add(decl, prefix)
  24. }
  25. } else {
  26. return super.add(decl, prefix)
  27. }
  28. return undefined
  29. }
  30. /**
  31. * Get div token from exists parameters
  32. */
  33. cloneDiv(params) {
  34. for (let i of params) {
  35. if (i.type === 'div' && i.value === ',') {
  36. return i
  37. }
  38. }
  39. return { after: ' ', type: 'div', value: ',' }
  40. }
  41. /**
  42. * Change colors syntax to old webkit
  43. */
  44. colorStops(params) {
  45. let result = []
  46. for (let i = 0; i < params.length; i++) {
  47. let pos
  48. let param = params[i]
  49. let item
  50. if (i === 0) {
  51. continue
  52. }
  53. let color = parser.stringify(param[0])
  54. if (param[1] && param[1].type === 'word') {
  55. pos = param[1].value
  56. } else if (param[2] && param[2].type === 'word') {
  57. pos = param[2].value
  58. }
  59. let stop
  60. if (i === 1 && (!pos || pos === '0%')) {
  61. stop = `from(${color})`
  62. } else if (i === params.length - 1 && (!pos || pos === '100%')) {
  63. stop = `to(${color})`
  64. } else if (pos) {
  65. stop = `color-stop(${pos}, ${color})`
  66. } else {
  67. stop = `color-stop(${color})`
  68. }
  69. let div = param[param.length - 1]
  70. params[i] = [{ type: 'word', value: stop }]
  71. if (div.type === 'div' && div.value === ',') {
  72. item = params[i].push(div)
  73. }
  74. result.push(item)
  75. }
  76. return result
  77. }
  78. /**
  79. * Change new direction to old
  80. */
  81. convertDirection(params) {
  82. if (params.length > 0) {
  83. if (params[0].value === 'to') {
  84. this.fixDirection(params)
  85. } else if (params[0].value.includes('deg')) {
  86. this.fixAngle(params)
  87. } else if (this.isRadial(params)) {
  88. this.fixRadial(params)
  89. }
  90. }
  91. return params
  92. }
  93. /**
  94. * Add 90 degrees
  95. */
  96. fixAngle(params) {
  97. let first = params[0].value
  98. first = parseFloat(first)
  99. first = Math.abs(450 - first) % 360
  100. first = this.roundFloat(first, 3)
  101. params[0].value = `${first}deg`
  102. }
  103. /**
  104. * Replace `to top left` to `bottom right`
  105. */
  106. fixDirection(params) {
  107. params.splice(0, 2)
  108. for (let param of params) {
  109. if (param.type === 'div') {
  110. break
  111. }
  112. if (param.type === 'word') {
  113. param.value = this.revertDirection(param.value)
  114. }
  115. }
  116. }
  117. /**
  118. * Fix radial direction syntax
  119. */
  120. fixRadial(params) {
  121. let first = []
  122. let second = []
  123. let a, b, c, i, next
  124. for (i = 0; i < params.length - 2; i++) {
  125. a = params[i]
  126. b = params[i + 1]
  127. c = params[i + 2]
  128. if (a.type === 'space' && b.value === 'at' && c.type === 'space') {
  129. next = i + 3
  130. break
  131. } else {
  132. first.push(a)
  133. }
  134. }
  135. let div
  136. for (i = next; i < params.length; i++) {
  137. if (params[i].type === 'div') {
  138. div = params[i]
  139. break
  140. } else {
  141. second.push(params[i])
  142. }
  143. }
  144. params.splice(0, i, ...second, div, ...first)
  145. }
  146. /**
  147. * Look for at word
  148. */
  149. isRadial(params) {
  150. let state = 'before'
  151. for (let param of params) {
  152. if (state === 'before' && param.type === 'space') {
  153. state = 'at'
  154. } else if (state === 'at' && param.value === 'at') {
  155. state = 'after'
  156. } else if (state === 'after' && param.type === 'space') {
  157. return true
  158. } else if (param.type === 'div') {
  159. break
  160. } else {
  161. state = 'before'
  162. }
  163. }
  164. return false
  165. }
  166. /**
  167. * Replace old direction to new
  168. */
  169. newDirection(params) {
  170. if (params[0].value === 'to') {
  171. return params
  172. }
  173. IS_DIRECTION.lastIndex = 0 // reset search index of global regexp
  174. if (!IS_DIRECTION.test(params[0].value)) {
  175. return params
  176. }
  177. params.unshift(
  178. {
  179. type: 'word',
  180. value: 'to'
  181. },
  182. {
  183. type: 'space',
  184. value: ' '
  185. }
  186. )
  187. for (let i = 2; i < params.length; i++) {
  188. if (params[i].type === 'div') {
  189. break
  190. }
  191. if (params[i].type === 'word') {
  192. params[i].value = this.revertDirection(params[i].value)
  193. }
  194. }
  195. return params
  196. }
  197. /**
  198. * Normalize angle
  199. */
  200. normalize(nodes, gradientName) {
  201. if (!nodes[0]) return nodes
  202. if (/-?\d+(.\d+)?grad/.test(nodes[0].value)) {
  203. nodes[0].value = this.normalizeUnit(nodes[0].value, 400)
  204. } else if (/-?\d+(.\d+)?rad/.test(nodes[0].value)) {
  205. nodes[0].value = this.normalizeUnit(nodes[0].value, 2 * Math.PI)
  206. } else if (/-?\d+(.\d+)?turn/.test(nodes[0].value)) {
  207. nodes[0].value = this.normalizeUnit(nodes[0].value, 1)
  208. } else if (nodes[0].value.includes('deg')) {
  209. let num = parseFloat(nodes[0].value)
  210. num = range.wrap(0, 360, num)
  211. nodes[0].value = `${num}deg`
  212. }
  213. if (
  214. gradientName === 'linear-gradient' ||
  215. gradientName === 'repeating-linear-gradient'
  216. ) {
  217. let direction = nodes[0].value
  218. // Unitless zero for `<angle>` values are allowed in CSS gradients and transforms.
  219. // Spec: https://github.com/w3c/csswg-drafts/commit/602789171429b2231223ab1e5acf8f7f11652eb3
  220. if (direction === '0deg' || direction === '0') {
  221. nodes = this.replaceFirst(nodes, 'to', ' ', 'top')
  222. } else if (direction === '90deg') {
  223. nodes = this.replaceFirst(nodes, 'to', ' ', 'right')
  224. } else if (direction === '180deg') {
  225. nodes = this.replaceFirst(nodes, 'to', ' ', 'bottom') // default value
  226. } else if (direction === '270deg') {
  227. nodes = this.replaceFirst(nodes, 'to', ' ', 'left')
  228. }
  229. }
  230. return nodes
  231. }
  232. /**
  233. * Convert angle unit to deg
  234. */
  235. normalizeUnit(str, full) {
  236. let num = parseFloat(str)
  237. let deg = (num / full) * 360
  238. return `${deg}deg`
  239. }
  240. /**
  241. * Remove old WebKit gradient too
  242. */
  243. old(prefix) {
  244. if (prefix === '-webkit-') {
  245. let type
  246. if (this.name === 'linear-gradient') {
  247. type = 'linear'
  248. } else if (this.name === 'repeating-linear-gradient') {
  249. type = 'repeating-linear'
  250. } else if (this.name === 'repeating-radial-gradient') {
  251. type = 'repeating-radial'
  252. } else {
  253. type = 'radial'
  254. }
  255. let string = '-gradient'
  256. let regexp = utils.regexp(
  257. `-webkit-(${type}-gradient|gradient\\(\\s*${type})`,
  258. false
  259. )
  260. return new OldValue(this.name, prefix + this.name, string, regexp)
  261. } else {
  262. return super.old(prefix)
  263. }
  264. }
  265. /**
  266. * Change direction syntax to old webkit
  267. */
  268. oldDirection(params) {
  269. let div = this.cloneDiv(params[0])
  270. if (params[0][0].value !== 'to') {
  271. return params.unshift([
  272. { type: 'word', value: Gradient.oldDirections.bottom },
  273. div
  274. ])
  275. } else {
  276. let words = []
  277. for (let node of params[0].slice(2)) {
  278. if (node.type === 'word') {
  279. words.push(node.value.toLowerCase())
  280. }
  281. }
  282. words = words.join(' ')
  283. let old = Gradient.oldDirections[words] || words
  284. params[0] = [{ type: 'word', value: old }, div]
  285. return params[0]
  286. }
  287. }
  288. /**
  289. * Convert to old webkit syntax
  290. */
  291. oldWebkit(node) {
  292. let { nodes } = node
  293. let string = parser.stringify(node.nodes)
  294. if (this.name !== 'linear-gradient') {
  295. return false
  296. }
  297. if (nodes[0] && nodes[0].value.includes('deg')) {
  298. return false
  299. }
  300. if (
  301. string.includes('px') ||
  302. string.includes('-corner') ||
  303. string.includes('-side')
  304. ) {
  305. return false
  306. }
  307. let params = [[]]
  308. for (let i of nodes) {
  309. params[params.length - 1].push(i)
  310. if (i.type === 'div' && i.value === ',') {
  311. params.push([])
  312. }
  313. }
  314. this.oldDirection(params)
  315. this.colorStops(params)
  316. node.nodes = []
  317. for (let param of params) {
  318. node.nodes = node.nodes.concat(param)
  319. }
  320. node.nodes.unshift(
  321. { type: 'word', value: 'linear' },
  322. this.cloneDiv(node.nodes)
  323. )
  324. node.value = '-webkit-gradient'
  325. return true
  326. }
  327. /**
  328. * Change degrees for webkit prefix
  329. */
  330. replace(string, prefix) {
  331. let ast = parser(string)
  332. for (let node of ast.nodes) {
  333. let gradientName = this.name // gradient name
  334. if (node.type === 'function' && node.value === gradientName) {
  335. node.nodes = this.newDirection(node.nodes)
  336. node.nodes = this.normalize(node.nodes, gradientName)
  337. if (prefix === '-webkit- old') {
  338. let changes = this.oldWebkit(node)
  339. if (!changes) {
  340. return false
  341. }
  342. } else {
  343. node.nodes = this.convertDirection(node.nodes)
  344. node.value = prefix + node.value
  345. }
  346. }
  347. }
  348. return ast.toString()
  349. }
  350. /**
  351. * Replace first token
  352. */
  353. replaceFirst(params, ...words) {
  354. let prefix = words.map(i => {
  355. if (i === ' ') {
  356. return { type: 'space', value: i }
  357. }
  358. return { type: 'word', value: i }
  359. })
  360. return prefix.concat(params.slice(1))
  361. }
  362. revertDirection(word) {
  363. return Gradient.directions[word.toLowerCase()] || word
  364. }
  365. /**
  366. * Round float and save digits under dot
  367. */
  368. roundFloat(float, digits) {
  369. return parseFloat(float.toFixed(digits))
  370. }
  371. }
  372. Gradient.names = [
  373. 'linear-gradient',
  374. 'repeating-linear-gradient',
  375. 'radial-gradient',
  376. 'repeating-radial-gradient'
  377. ]
  378. Gradient.directions = {
  379. bottom: 'top',
  380. left: 'right',
  381. right: 'left',
  382. top: 'bottom' // default value
  383. }
  384. // Direction to replace
  385. Gradient.oldDirections = {
  386. 'bottom': 'left top, left bottom',
  387. 'bottom left': 'right top, left bottom',
  388. 'bottom right': 'left top, right bottom',
  389. 'left': 'right top, left top',
  390. 'left bottom': 'right top, left bottom',
  391. 'left top': 'right bottom, left top',
  392. 'right': 'left top, right top',
  393. 'right bottom': 'left top, right bottom',
  394. 'right top': 'left bottom, right top',
  395. 'top': 'left bottom, left top',
  396. 'top left': 'right bottom, left top',
  397. 'top right': 'left bottom, right top'
  398. }
  399. module.exports = Gradient