stylelint-taro-rn
Version:
A collection of React Native specific rules for stylelint
374 lines (350 loc) • 11.9 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var stylelint = require('stylelint');
var reactNativeKnownStylingProperties = require('react-native-known-styling-properties');
var declarationValueIndex = require('stylelint/lib/utils/declarationValueIndex.cjs');
const endsWith = (str, suffix) => str.indexOf(suffix, str.length - suffix.length) !== -1;
/**
* Check whether a string has less interpolation
*
* @param {string} string
* @return {boolean} If `true`, a string has less interpolation
*/
function hasLessInterpolation(string /*: string */) {
if (/@{.+?}/.test(string)) {
return true;
}
return false;
}
/**
* Check whether a string has postcss-simple-vars interpolation
*/
function hasPsvInterpolation(string /*: string */) {
if (/\$\(.+?\)/.test(string)) {
return true;
}
return false;
}
/**
* Check whether a string has scss interpolation
*/
function hasScssInterpolation(string /*: string */) {
if (/#{.+?}/.test(string)) {
return true;
}
return false;
}
/**
* Check whether a string has interpolation
*
* @param {string} string
* @return {boolean} If `true`, a string has interpolation
*/
function hasInterpolation(string /*: string */) {
// SCSS or Less interpolation
if (hasLessInterpolation(string) ||
hasScssInterpolation(string) ||
hasPsvInterpolation(string)) {
return true;
}
return false;
}
/**
* Check whether a property is a custom one
*/
function isCustomProperty(property /*: string */) {
return property.slice(0, 2) === '--';
}
/**
* Check whether a node is an :export block
*/
function isExportBlock(node /*: Object */) {
if (node.type === 'rule' && node.selector && node.selector === ':export') {
return true;
}
return false;
}
/**
* Check whether a declaration is standard
*/
function isStandardSyntaxDeclaration(decl /*: Object */) {
const prop = decl.prop;
const parent = decl.parent;
// Declarations belong in a declaration block
if (parent.type === 'root') {
return false;
}
// Sass var (e.g. $var: x), nested list (e.g. $list: (x)) or nested map (e.g. $map: (key:value))
if (prop[0] === '$') {
return false;
}
// Less var (e.g. @var: x), but exclude variable interpolation (e.g. @{var})
if (prop[0] === '@' && prop[1] !== '{') {
return false;
}
// Sass nested properties (e.g. border: { style: solid; color: red; })
if (parent.selector &&
parent.selector[parent.selector.length - 1] === ':' &&
parent.selector.substring(0, 2) !== '--') {
return false;
}
return true;
}
/**
* Check whether a property is standard
*/
function isStandardSyntaxProperty(property /*: string */) {
// SCSS var (e.g. $var: x), list (e.g. $list: (x)) or map (e.g. $map: (key:value))
if (property[0] === '$') {
return false;
}
// Less var (e.g. @var: x)
if (property[0] === '@') {
return false;
}
// Less append property value with space (e.g. transform+_: scale(2))
if (endsWith(property, '+') || endsWith(property, '+_')) {
return false;
}
// SCSS or Less interpolation
if (hasInterpolation(property)) {
return false;
}
return true;
}
const isString = string => typeof string === 'string';
const kebabCase = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
const prefix = 'taro-rn';
function namespace(ruleName) {
return `${prefix}/${ruleName}`;
}
/**
* Compares a string to a second value that, if it fits a certain convention,
* is converted to a regular expression before the comparison.
* If it doesn't fit the convention, then two strings are compared.
*
* Any strings starting and ending with `/` are interpreted
* as regular expressions.
*/
function matchesStringOrRegExp(input /*: string | Array<string> */, comparison /*: string | Array<string> */) {
if (!Array.isArray(input)) {
return testAgainstStringOrRegExpOrArray(input, comparison);
}
for (const inputItem of input) {
const testResult = testAgainstStringOrRegExpOrArray(inputItem, comparison);
if (testResult) {
return testResult;
}
}
return false;
}
function testAgainstStringOrRegExpOrArray(value, comparison) {
if (!Array.isArray(comparison)) {
return testAgainstStringOrRegExp(value, comparison);
}
for (const comparisonItem of comparison) {
const testResult = testAgainstStringOrRegExp(value, comparisonItem);
if (testResult) {
return testResult;
}
}
return false;
}
function testAgainstStringOrRegExp(value, comparison) {
// If it's a RegExp, test directly
if (comparison instanceof RegExp) {
return comparison.test(value)
? { match: value, pattern: comparison }
: false;
}
// Check if it's RegExp in a string
const firstComparisonChar = comparison[0];
const lastComparisonChar = comparison[comparison.length - 1];
const secondToLastComparisonChar = comparison[comparison.length - 2];
const comparisonIsRegex = firstComparisonChar === '/' &&
(lastComparisonChar === '/' ||
(secondToLastComparisonChar === '/' && lastComparisonChar === 'i'));
const hasCaseInsensitiveFlag = comparisonIsRegex && lastComparisonChar === 'i';
// If so, create a new RegExp from it
if (comparisonIsRegex) {
const valueMatches = hasCaseInsensitiveFlag
? new RegExp(comparison.slice(1, -2), 'i').test(value)
: new RegExp(comparison.slice(1, -1)).test(value);
return valueMatches ? { match: value, pattern: comparison } : false;
}
// Otherwise, it's a string. Do a strict comparison
return value === comparison ? { match: value, pattern: comparison } : false;
}
/**
* Check if an options object's propertyName contains a user-defined string or
* regex that matches the passed in input.
*/
function optionsMatches(options /*: Object */, propertyName /*: string */, input /*: string */) {
return !!(options &&
options[propertyName] &&
typeof input === 'string' &&
matchesStringOrRegExp(input, options[propertyName]));
}
const ruleName$3 = namespace('css-property-no-unknown');
const messages$3 = stylelint.utils.ruleMessages(ruleName$3, {
rejected: (property) => `无效的 React Native 样式属性 "${property}"`
});
const props$1 = reactNativeKnownStylingProperties.allCSS2RNProps.map(kebabCase);
function cssPropertyNoUnknown (actual, options) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName$3, {
actual
}, {
actual: options,
possible: {
ignoreProperties: [isString]
},
optional: true
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const prop = decl.prop;
if (!isStandardSyntaxProperty(prop)) {
return;
}
if (!isStandardSyntaxDeclaration(decl)) {
return;
}
if (isCustomProperty(prop)) {
return;
}
if (isExportBlock(decl.parent)) {
return;
}
if (optionsMatches(options, 'ignoreProperties', prop)) {
return;
}
if (props$1.indexOf(prop.toLowerCase()) !== -1) {
return;
}
stylelint.utils.report({
message: messages$3.rejected(prop),
node: decl,
result,
ruleName: ruleName$3
});
});
};
}
const ruleName$2 = namespace('font-weight-no-ignored-values');
const messages$2 = stylelint.utils.ruleMessages(ruleName$2, {
rejected: (weight) => `Unexpected font-weight "${weight}"`
});
const acceptedWeights = ['400', '700', 'normal', 'bold'];
function fontWeightNoIgnoredValues (actual) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName$2, {
actual
});
if (!validOptions) {
return;
}
root.walkDecls(/^font-weight$/i, (decl) => {
if (acceptedWeights.indexOf(decl.value) > -1) {
return;
}
const weightValueOffset = decl.value.indexOf(decl.value);
const index = declarationValueIndex(decl) + weightValueOffset;
stylelint.utils.report({
message: messages$2.rejected(decl.value),
node: decl,
result,
ruleName: ruleName$2,
index
});
});
};
}
const ruleName$1 = namespace('line-height-no-value-without-unit');
const messages$1 = stylelint.utils.ruleMessages(ruleName$1, {
rejected: (height) => `Unexpected line-height "${height}", expect a value with units`
});
const lengthRe = /^(0$|(?:[+-]?(?:\d*\.)?\d+(?:[Ee][+-]?\d+)?)(?=px|PX|rem$))/;
const viewportUnitRe = /^([+-]?[0-9.]+)(vh|vw|vmin|vmax)$/;
function lineHeightNoValueWithoutUnit (actual) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName$1, {
actual
});
if (!validOptions) {
return;
}
root.walkDecls(/^line-height$/i, (decl) => {
if (lengthRe.test(decl.value) || viewportUnitRe.test(decl.value)) {
return;
}
const valueOffset = decl.value.indexOf(decl.value);
const index = declarationValueIndex(decl) + valueOffset;
stylelint.utils.report({
message: messages$1.rejected(decl.value),
node: decl,
result,
ruleName: ruleName$1,
index
});
});
};
}
const ruleName = namespace('style-property-no-unknown');
const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: (property) => `无效的 React Native 样式属性 "${property}"`
});
const props = reactNativeKnownStylingProperties.allProps.map(kebabCase);
function stylePropertyNoUnknown (actual, options) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName, {
actual
}, {
actual: options,
possible: {
ignoreProperties: [isString]
},
optional: true
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const prop = decl.prop;
if (!isStandardSyntaxProperty(prop)) {
return;
}
if (!isStandardSyntaxDeclaration(decl)) {
return;
}
if (isCustomProperty(prop)) {
return;
}
if (optionsMatches(options, 'ignoreProperties', prop)) {
return;
}
if (props.indexOf(prop.toLowerCase()) !== -1) {
return;
}
stylelint.utils.report({
message: messages.rejected(prop),
node: decl,
result,
ruleName
});
});
};
}
var rules = {
'font-weight-no-ignored-values': fontWeightNoIgnoredValues,
'css-property-no-unknown': cssPropertyNoUnknown,
'style-property-no-unknown': stylePropertyNoUnknown,
'line-height-no-value-without-unit': lineHeightNoValueWithoutUnit
};
const rulesPlugins = Object.keys(rules).map((ruleName) => {
return stylelint.createPlugin(namespace(ruleName), rules[ruleName]);
});
exports.default = rulesPlugins;
//# sourceMappingURL=index.cjs.js.map
;