let featureQueries = require('caniuse-lite/data/features/css-featurequeries.js') let { feature } = require('caniuse-lite') let { parse } = require('postcss') let Browsers = require('./browsers') let brackets = require('./brackets') let Value = require('./value') let utils = require('./utils') let data = feature(featureQueries) let supported = [] for (let browser in data.stats) { let versions = data.stats[browser] for (let version in versions) { let support = versions[version] if (/y/.test(support)) { supported.push(browser + ' ' + version) } } } class Supports { constructor(Prefixes, all) { this.Prefixes = Prefixes this.all = all } /** * Return prefixer only with @supports supported browsers */ prefixer() { if (this.prefixerCache) { return this.prefixerCache } let filtered = this.all.browsers.selected.filter(i => { return supported.includes(i) }) let browsers = new Browsers( this.all.browsers.data, filtered, this.all.options ) this.prefixerCache = new this.Prefixes( this.all.data, browsers, this.all.options ) return this.prefixerCache } /** * Parse string into declaration property and value */ parse(str) { let parts = str.split(':') let prop = parts[0] let value = parts[1] if (!value) value = '' return [prop.trim(), value.trim()] } /** * Create virtual rule to process it by prefixer */ virtual(str) { let [prop, value] = this.parse(str) let rule = parse('a{}').first rule.append({ prop, value, raws: { before: '' } }) return rule } /** * Return array of Declaration with all necessary prefixes */ prefixed(str) { let rule = this.virtual(str) if (this.disabled(rule.first)) { return rule.nodes } let result = { warn: () => null } let prefixer = this.prefixer().add[rule.first.prop] prefixer && prefixer.process && prefixer.process(rule.first, result) for (let decl of rule.nodes) { for (let value of this.prefixer().values('add', rule.first.prop)) { value.process(decl) } Value.save(this.all, decl) } return rule.nodes } /** * Return true if brackets node is "not" word */ isNot(node) { return typeof node === 'string' && /not\s*/i.test(node) } /** * Return true if brackets node is "or" word */ isOr(node) { return typeof node === 'string' && /\s*or\s*/i.test(node) } /** * Return true if brackets node is (prop: value) */ isProp(node) { return ( typeof node === 'object' && node.length === 1 && typeof node[0] === 'string' ) } /** * Return true if prefixed property has no unprefixed */ isHack(all, unprefixed) { let check = new RegExp(`(\\(|\\s)${utils.escapeRegexp(unprefixed)}:`) return !check.test(all) } /** * Return true if we need to remove node */ toRemove(str, all) { let [prop, value] = this.parse(str) let unprefixed = this.all.unprefixed(prop) let cleaner = this.all.cleaner() if ( cleaner.remove[prop] && cleaner.remove[prop].remove && !this.isHack(all, unprefixed) ) { return true } for (let checker of cleaner.values('remove', unprefixed)) { if (checker.check(value)) { return true } } return false } /** * Remove all unnecessary prefixes */ remove(nodes, all) { let i = 0 while (i < nodes.length) { if ( !this.isNot(nodes[i - 1]) && this.isProp(nodes[i]) && this.isOr(nodes[i + 1]) ) { if (this.toRemove(nodes[i][0], all)) { nodes.splice(i, 2) continue } i += 2 continue } if (typeof nodes[i] === 'object') { nodes[i] = this.remove(nodes[i], all) } i += 1 } return nodes } /** * Clean brackets with one child */ cleanBrackets(nodes) { return nodes.map(i => { if (typeof i !== 'object') { return i } if (i.length === 1 && typeof i[0] === 'object') { return this.cleanBrackets(i[0]) } return this.cleanBrackets(i) }) } /** * Add " or " between properties and convert it to brackets format */ convert(progress) { let result = [''] for (let i of progress) { result.push([`${i.prop}: ${i.value}`]) result.push(' or ') } result[result.length - 1] = '' return result } /** * Compress value functions into a string nodes */ normalize(nodes) { if (typeof nodes !== 'object') { return nodes } nodes = nodes.filter(i => i !== '') if (typeof nodes[0] === 'string') { let firstNode = nodes[0].trim() if ( firstNode.includes(':') || firstNode === 'selector' || firstNode === 'not selector' ) { return [brackets.stringify(nodes)] } } return nodes.map(i => this.normalize(i)) } /** * Add prefixes */ add(nodes, all) { return nodes.map(i => { if (this.isProp(i)) { let prefixed = this.prefixed(i[0]) if (prefixed.length > 1) { return this.convert(prefixed) } return i } if (typeof i === 'object') { return this.add(i, all) } return i }) } /** * Add prefixed declaration */ process(rule) { let ast = brackets.parse(rule.params) ast = this.normalize(ast) ast = this.remove(ast, rule.params) ast = this.add(ast, rule.params) ast = this.cleanBrackets(ast) rule.params = brackets.stringify(ast) } /** * Check global options */ disabled(node) { if (!this.all.options.grid) { if (node.prop === 'display' && node.value.includes('grid')) { return true } if (node.prop.includes('grid') || node.prop === 'justify-items') { return true } } if (this.all.options.flexbox === false) { if (node.prop === 'display' && node.value.includes('flex')) { return true } let other = ['order', 'justify-content', 'align-items', 'align-content'] if (node.prop.includes('flex') || other.includes(node.prop)) { return true } } return false } } module.exports = Supports