1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
import FEATURES from '../data/features.js';
import { performFeatureCheck } from '../utils/util.js';
/**
* @typedef DetectorCallbackArgument
* @prop {!import('postcss').ChildNode} usage
* @prop {keyof FEATURES} feature
* @prop {(keyof FEATURES & string)[]} ignore
*/
/**
* @callback DetectorCallback
* @param {DetectorCallbackArgument} result
* @return {any}
*/
const PLUGIN_OPTION_COMMENT = 'doiuse-';
const DISABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}disable`;
const ENABLE_FEATURE_COMMENT = `${PLUGIN_OPTION_COMMENT}enable`;
/**
* Strip the contents of url literals so they aren't matched
* by our naive substring matching.
* @param {string} input
* @return {string}
*/
function stripUrls(input) {
return input.replaceAll(/url\([^)]*\)/g, 'url()');
}
/**
* Detect the use of any of a given list of CSS features.
* ```
* var detector = new Detector(featureList)
* detector.process(css, cb)
* ```
*
* `featureList`: an array of feature slugs (see caniuse-db)
* `cb`: a callback that gets called for each usage of one of the given features,
* called with an argument like:
* ```
* {
* usage: {} // postcss node where usage was found
* feature: {} // caniuse-db feature slug
* ignore: {} // caniuse-db feature to ignore in current file
* }
* ```
*/
export default class Detector {
/**
* @param {(keyof FEATURES & string)[]} featureList an array of feature slugs (see caniuse-db)
*/
constructor(featureList) {
/** @type {Partial<FEATURES>} */
this.features = {};
for (const feature of featureList) {
if (FEATURES[feature]) {
this.features[feature] = FEATURES[feature];
}
}
/** @type {(keyof FEATURES & string)[]} */
this.ignore = [];
}
/**
* @param {import('postcss').Comment} comment
* @return {void}
*/
comment(comment) {
const text = comment.text.toLowerCase();
if (!text.startsWith(PLUGIN_OPTION_COMMENT)) return;
const option = text.split(' ', 1)[0];
const value = text.replace(option, '').trim();
switch (option) {
case DISABLE_FEATURE_COMMENT: {
if (value === '') {
// @ts-expect-error Skip cast
this.ignore = Object.keys(this.features);
} else {
for (const feat of value.split(',')) {
/** @type {any} */
const f = feat.trim();
if (!this.ignore.includes(f)) {
this.ignore.push(f);
}
}
}
break;
}
case ENABLE_FEATURE_COMMENT: {
if (value === '') {
this.ignore = [];
} else {
const without = new Set(value.split(',').map((feat) => feat.trim()));
this.ignore = this.ignore.filter((index) => !without.has(index));
}
break;
}
default:
}
}
/**
* @param {import('postcss').Container} node
* @param {DetectorCallback} callback
* @return {void}
*/
node(node, callback) {
node.each((child) => {
if (child.type === 'comment') {
this.comment(child);
return;
}
for (const [feat] of Object.entries(this.features).filter(([, featValue]) => {
if (!featValue) return false;
if (typeof featValue === 'function') {
return featValue(child);
}
if (Array.isArray(featValue)) {
return featValue.some((function_) => function_(child));
}
if (child.type !== 'decl') {
return false;
}
return Object.entries(featValue).some(([property, value]) => {
if (property !== '' && property !== child.prop) return false;
if (value === true) return true;
if (value === false) return false;
return performFeatureCheck(value, stripUrls(child.value));
});
})) {
const feature = /** @type {keyof FEATURES} */ (feat);
callback({ usage: child, feature, ignore: this.ignore });
}
if (child.type !== 'decl') {
this.node(child, callback);
}
});
}
/**
* @param {import('postcss').Root} node
* @param {DetectorCallback} callback
* @return {void}
*/
process(node, callback) {
// Reset ignoring rules specified by inline comments per each file
this.ignore = [];
// Recursively walk nodes in file
this.node(node, callback);
}
}
|