| /** @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 {Record<string, string[]>} UnitDefinitions */ |
| /** @typedef {{atrules?: AtRuleDefinitions | undefined, properties?: PropertyDefinitions | undefined, types?: TypeDefinitions | undefined, cssWideKeywords?: CSSWideKeywords | undefined, units?: UnitDefinitions | 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); |
| target.units = mergeUnits(source1.units, source2.units); |
| |
| 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 {UnitDefinitions | undefined} source1 |
| * @param {UnitDefinitions | undefined} source2 |
| * @returns {UnitDefinitions | undefined} |
| */ |
| function mergeUnits(source1, source2) { |
| if (typeof source1 === 'undefined' || typeof source2 === 'undefined') |
| return source1 ?? source2 ?? {}; |
| |
| /** @type {UnitDefinitions} */ |
| const target = { ...source1 }; |
| |
| for (const [category, units] of Object.entries(source2)) { |
| const existing = target[category]; |
| |
| target[category] = existing ? Array.from(new Set([...existing, ...units])) : units; |
| } |
| |
| return target; |
| } |
| |
| /** |
| * @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}`; |
| } |