stylelint
Version:
A mighty CSS linter that helps you avoid errors and enforce conventions.
138 lines (109 loc) • 4.19 kB
JavaScript
/** @typedef {Record<string, {prelude?: string | undefined, descriptors?: Record<string, string> | undefined}>} AtRuleDefinitions */
/** @typedef {Record<string, string>} PropertyDefinitions */
/** @typedef {Record<string, string>} TypeDefinitions */
/** @typedef {Array<string>} CSSWideKeywords */
/** @typedef {{atrules?: AtRuleDefinitions | undefined, properties?: PropertyDefinitions | undefined, types?: TypeDefinitions | undefined, cssWideKeywords?: CSSWideKeywords | undefined}} SyntaxDefinition */
/**
* Merge multiple separate syntax definitions.
* Individual definitions will override each other unless the second definition starts with a pipe symbol `|`.
*
* @param {...(SyntaxDefinition|undefined)} sources
* @returns {SyntaxDefinition}
*/
export default function mergeSyntaxDefinitions(...sources) {
/** @type {SyntaxDefinition|undefined} */
let target = sources[0] ?? {};
for (let i = 1; i < sources.length; i++) {
const source = sources[i];
if (!source) continue;
target = mergeTwoSyntaxDefinitions(target, source);
}
return target;
}
/**
* Merge two separate syntax definitions.
* Individual definitions will override each other unless the second definition starts with a pipe symbol `|`.
*
* @param {SyntaxDefinition} source1
* @param {SyntaxDefinition} source2
* @returns {SyntaxDefinition}
*/
function mergeTwoSyntaxDefinitions(source1, source2) {
/** @type {SyntaxDefinition} */
const target = {};
target.atrules = mergeAtRuleDefinitions(source1.atrules, source2.atrules);
target.properties = mergePropertyOrTypeDefinitions(source1.properties, source2.properties);
target.types = mergePropertyOrTypeDefinitions(source1.types, source2.types);
target.cssWideKeywords = mergeKeywords(source1.cssWideKeywords, source2.cssWideKeywords);
return target;
}
/**
* @param {AtRuleDefinitions | undefined} source1
* @param {AtRuleDefinitions | undefined} source2
* @returns {AtRuleDefinitions | undefined}
*/
function mergeAtRuleDefinitions(source1, source2) {
if (typeof source1 === 'undefined' || typeof source2 === 'undefined')
return source1 ?? source2 ?? {};
/** @type {AtRuleDefinitions} */
const target = structuredClone(source1);
for (const [atRuleName, atRule] of Object.entries(source2)) {
const targetAtRule = target[atRuleName] ?? {};
targetAtRule.prelude = mergePrelude(targetAtRule.prelude, atRule.prelude);
targetAtRule.descriptors = mergePropertyOrTypeDefinitions(
targetAtRule.descriptors,
atRule.descriptors,
);
target[atRuleName] = targetAtRule;
}
return target;
}
/**
* @param {Record<string, string> | undefined} source1
* @param {Record<string, string> | undefined} source2
* @returns {Record<string, string> | undefined}
*/
function mergePropertyOrTypeDefinitions(source1, source2) {
if (typeof source1 === 'undefined' || typeof source2 === 'undefined')
return source1 ?? source2 ?? {};
/** @type {Record<string, string>} */
const target = {
...source1,
};
for (const [name, syntax] of Object.entries(source2)) {
target[name] = mergeSyntax(target[name], syntax);
}
return target;
}
/**
* @param {Array<string> | undefined} source1
* @param {Array<string> | undefined} source2
* @returns {Array<string> | undefined}
*/
function mergeKeywords(source1, source2) {
if (typeof source1 === 'undefined' || typeof source2 === 'undefined')
return source1 ?? source2 ?? [];
return Array.from(new Set([...source1, ...source2]));
}
/**
* @param {string | undefined} source1
* @param {string} source2
* @returns {string}
*/
function mergeSyntax(source1, source2) {
if (typeof source1 === 'undefined') return source2;
const trimmedSource2 = source2.trimStart();
if (!trimmedSource2.startsWith('|')) return source2;
return `${source1} ${trimmedSource2}`;
}
/**
* @param {string | undefined} source1
* @param {string | undefined} source2
* @returns {string | undefined}
*/
function mergePrelude(source1, source2) {
if (typeof source1 === 'undefined' || typeof source2 === 'undefined') return source1 ?? source2;
const trimmedSource2 = source2.trimStart();
if (!trimmedSource2.startsWith('|')) return source2;
return `${source1} ${trimmedSource2}`;
}