gradient.js 9.7 KB

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