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