gradient.js 10 KB

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