UNPKG

stylelint-taro-rn

Version:

A collection of React Native specific rules for stylelint

374 lines (350 loc) 11.9 kB
'use strict'; 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