supports.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. let featureQueries = require('caniuse-lite/data/features/css-featurequeries.js')
  2. let feature = require('caniuse-lite/dist/unpacker/feature')
  3. let { parse } = require('postcss')
  4. let Browsers = require('./browsers')
  5. let brackets = require('./brackets')
  6. let Value = require('./value')
  7. let utils = require('./utils')
  8. let data = feature(featureQueries)
  9. let supported = []
  10. for (let browser in data.stats) {
  11. let versions = data.stats[browser]
  12. for (let version in versions) {
  13. let support = versions[version]
  14. if (/y/.test(support)) {
  15. supported.push(browser + ' ' + version)
  16. }
  17. }
  18. }
  19. class Supports {
  20. constructor(Prefixes, all) {
  21. this.Prefixes = Prefixes
  22. this.all = all
  23. }
  24. /**
  25. * Add prefixes
  26. */
  27. add(nodes, all) {
  28. return nodes.map(i => {
  29. if (this.isProp(i)) {
  30. let prefixed = this.prefixed(i[0])
  31. if (prefixed.length > 1) {
  32. return this.convert(prefixed)
  33. }
  34. return i
  35. }
  36. if (typeof i === 'object') {
  37. return this.add(i, all)
  38. }
  39. return i
  40. })
  41. }
  42. /**
  43. * Clean brackets with one child
  44. */
  45. cleanBrackets(nodes) {
  46. return nodes.map(i => {
  47. if (typeof i !== 'object') {
  48. return i
  49. }
  50. if (i.length === 1 && typeof i[0] === 'object') {
  51. return this.cleanBrackets(i[0])
  52. }
  53. return this.cleanBrackets(i)
  54. })
  55. }
  56. /**
  57. * Add " or " between properties and convert it to brackets format
  58. */
  59. convert(progress) {
  60. let result = ['']
  61. for (let i of progress) {
  62. result.push([`${i.prop}: ${i.value}`])
  63. result.push(' or ')
  64. }
  65. result[result.length - 1] = ''
  66. return result
  67. }
  68. /**
  69. * Check global options
  70. */
  71. disabled(node) {
  72. if (!this.all.options.grid) {
  73. if (node.prop === 'display' && node.value.includes('grid')) {
  74. return true
  75. }
  76. if (node.prop.includes('grid') || node.prop === 'justify-items') {
  77. return true
  78. }
  79. }
  80. if (this.all.options.flexbox === false) {
  81. if (node.prop === 'display' && node.value.includes('flex')) {
  82. return true
  83. }
  84. let other = ['order', 'justify-content', 'align-items', 'align-content']
  85. if (node.prop.includes('flex') || other.includes(node.prop)) {
  86. return true
  87. }
  88. }
  89. return false
  90. }
  91. /**
  92. * Return true if prefixed property has no unprefixed
  93. */
  94. isHack(all, unprefixed) {
  95. let check = new RegExp(`(\\(|\\s)${utils.escapeRegexp(unprefixed)}:`)
  96. return !check.test(all)
  97. }
  98. /**
  99. * Return true if brackets node is "not" word
  100. */
  101. isNot(node) {
  102. return typeof node === 'string' && /not\s*/i.test(node)
  103. }
  104. /**
  105. * Return true if brackets node is "or" word
  106. */
  107. isOr(node) {
  108. return typeof node === 'string' && /\s*or\s*/i.test(node)
  109. }
  110. /**
  111. * Return true if brackets node is (prop: value)
  112. */
  113. isProp(node) {
  114. return (
  115. typeof node === 'object' &&
  116. node.length === 1 &&
  117. typeof node[0] === 'string'
  118. )
  119. }
  120. /**
  121. * Compress value functions into a string nodes
  122. */
  123. normalize(nodes) {
  124. if (typeof nodes !== 'object') {
  125. return nodes
  126. }
  127. nodes = nodes.filter(i => i !== '')
  128. if (typeof nodes[0] === 'string') {
  129. let firstNode = nodes[0].trim()
  130. if (
  131. firstNode.includes(':') ||
  132. firstNode === 'selector' ||
  133. firstNode === 'not selector'
  134. ) {
  135. return [brackets.stringify(nodes)]
  136. }
  137. }
  138. return nodes.map(i => this.normalize(i))
  139. }
  140. /**
  141. * Parse string into declaration property and value
  142. */
  143. parse(str) {
  144. let parts = str.split(':')
  145. let prop = parts[0]
  146. let value = parts[1]
  147. if (!value) value = ''
  148. return [prop.trim(), value.trim()]
  149. }
  150. /**
  151. * Return array of Declaration with all necessary prefixes
  152. */
  153. prefixed(str) {
  154. let rule = this.virtual(str)
  155. if (this.disabled(rule.first)) {
  156. return rule.nodes
  157. }
  158. let result = { warn: () => null }
  159. let prefixer = this.prefixer().add[rule.first.prop]
  160. prefixer && prefixer.process && prefixer.process(rule.first, result)
  161. for (let decl of rule.nodes) {
  162. for (let value of this.prefixer().values('add', rule.first.prop)) {
  163. value.process(decl)
  164. }
  165. Value.save(this.all, decl)
  166. }
  167. return rule.nodes
  168. }
  169. /**
  170. * Return prefixer only with @supports supported browsers
  171. */
  172. prefixer() {
  173. if (this.prefixerCache) {
  174. return this.prefixerCache
  175. }
  176. let filtered = this.all.browsers.selected.filter(i => {
  177. return supported.includes(i)
  178. })
  179. let browsers = new Browsers(
  180. this.all.browsers.data,
  181. filtered,
  182. this.all.options
  183. )
  184. this.prefixerCache = new this.Prefixes(
  185. this.all.data,
  186. browsers,
  187. this.all.options
  188. )
  189. return this.prefixerCache
  190. }
  191. /**
  192. * Add prefixed declaration
  193. */
  194. process(rule) {
  195. let ast = brackets.parse(rule.params)
  196. ast = this.normalize(ast)
  197. ast = this.remove(ast, rule.params)
  198. ast = this.add(ast, rule.params)
  199. ast = this.cleanBrackets(ast)
  200. rule.params = brackets.stringify(ast)
  201. }
  202. /**
  203. * Remove all unnecessary prefixes
  204. */
  205. remove(nodes, all) {
  206. let i = 0
  207. while (i < nodes.length) {
  208. if (
  209. !this.isNot(nodes[i - 1]) &&
  210. this.isProp(nodes[i]) &&
  211. this.isOr(nodes[i + 1])
  212. ) {
  213. if (this.toRemove(nodes[i][0], all)) {
  214. nodes.splice(i, 2)
  215. continue
  216. }
  217. i += 2
  218. continue
  219. }
  220. if (typeof nodes[i] === 'object') {
  221. nodes[i] = this.remove(nodes[i], all)
  222. }
  223. i += 1
  224. }
  225. return nodes
  226. }
  227. /**
  228. * Return true if we need to remove node
  229. */
  230. toRemove(str, all) {
  231. let [prop, value] = this.parse(str)
  232. let unprefixed = this.all.unprefixed(prop)
  233. let cleaner = this.all.cleaner()
  234. if (
  235. cleaner.remove[prop] &&
  236. cleaner.remove[prop].remove &&
  237. !this.isHack(all, unprefixed)
  238. ) {
  239. return true
  240. }
  241. for (let checker of cleaner.values('remove', unprefixed)) {
  242. if (checker.check(value)) {
  243. return true
  244. }
  245. }
  246. return false
  247. }
  248. /**
  249. * Create virtual rule to process it by prefixer
  250. */
  251. virtual(str) {
  252. let [prop, value] = this.parse(str)
  253. let rule = parse('a{}').first
  254. rule.append({ prop, raws: { before: '' }, value })
  255. return rule
  256. }
  257. }
  258. module.exports = Supports