'use strict'; const postcss = require('postcss'); const selectorParser = require('postcss-selector-parser'); const hasOwnProperty = Object.prototype.hasOwnProperty; function getSingleLocalNamesForComposes(root) { return root.nodes.map(node => { if (node.type !== 'selector' || node.nodes.length !== 1) { throw new Error( `composition is only allowed when selector is single :local class name not in "${root}"` ); } node = node.nodes[0]; if ( node.type !== 'pseudo' || node.value !== ':local' || node.nodes.length !== 1 ) { throw new Error( 'composition is only allowed when selector is single :local class name not in "' + root + '", "' + node + '" is weird' ); } node = node.first; if (node.type !== 'selector' || node.length !== 1) { throw new Error( 'composition is only allowed when selector is single :local class name not in "' + root + '", "' + node + '" is weird' ); } node = node.first; if (node.type !== 'class') { // 'id' is not possible, because you can't compose ids throw new Error( 'composition is only allowed when selector is single :local class name not in "' + root + '", "' + node + '" is weird' ); } return node.value; }); } const whitespace = '[\\x20\\t\\r\\n\\f]'; const unescapeRegExp = new RegExp( '\\\\([\\da-f]{1,6}' + whitespace + '?|(' + whitespace + ')|.)', 'ig' ); function unescape(str) { return str.replace(unescapeRegExp, (_, escaped, escapedWhitespace) => { const high = '0x' + escaped - 0x10000; // NaN means non-codepoint // Workaround erroneous numeric interpretation of +"0x" return high !== high || escapedWhitespace ? escaped : high < 0 ? // BMP codepoint String.fromCharCode(high + 0x10000) : // Supplemental Plane codepoint (surrogate pair) String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00); }); } const processor = postcss.plugin('postcss-modules-scope', function(options) { return css => { const generateScopedName = (options && options.generateScopedName) || processor.generateScopedName; const generateExportEntry = (options && options.generateExportEntry) || processor.generateExportEntry; const exportGlobals = options && options.exportGlobals; const exports = Object.create(null); function exportScopedName(name, rawName) { const scopedName = generateScopedName( rawName ? rawName : name, css.source.input.from, css.source.input.css ); const exportEntry = generateExportEntry( rawName ? rawName : name, scopedName, css.source.input.from, css.source.input.css ); const { key, value } = exportEntry; exports[key] = exports[key] || []; if (exports[key].indexOf(value) < 0) { exports[key].push(value); } return scopedName; } function localizeNode(node) { switch (node.type) { case 'selector': node.nodes = node.map(localizeNode); return node; case 'class': return selectorParser.className({ value: exportScopedName( node.value, node.raws && node.raws.value ? node.raws.value : null ), }); case 'id': { return selectorParser.id({ value: exportScopedName( node.value, node.raws && node.raws.value ? node.raws.value : null ), }); } } throw new Error( `${node.type} ("${node}") is not allowed in a :local block` ); } function traverseNode(node) { switch (node.type) { case 'pseudo': if (node.value === ':local') { if (node.nodes.length !== 1) { throw new Error('Unexpected comma (",") in :local block'); } const selector = localizeNode(node.first, node.spaces); // move the spaces that were around the psuedo selector to the first // non-container node selector.first.spaces = node.spaces; const nextNode = node.next(); if ( nextNode && nextNode.type === 'combinator' && nextNode.value === ' ' && /\\[A-F0-9]{1,6}$/.test(selector.last.value) ) { selector.last.spaces.after = ' '; } node.replaceWith(selector); return; } /* falls through */ case 'root': case 'selector': { node.each(traverseNode); break; } case 'id': case 'class': if (exportGlobals) { exports[node.value] = [node.value]; } break; } return node; } // Find any :import and remember imported names const importedNames = {}; css.walkRules(rule => { if (/^:import\(.+\)$/.test(rule.selector)) { rule.walkDecls(decl => { importedNames[decl.prop] = true; }); } }); // Find any :local classes css.walkRules(rule => { if ( rule.nodes && rule.selector.slice(0, 2) === '--' && rule.selector.slice(-1) === ':' ) { // ignore custom property set return; } let parsedSelector = selectorParser().astSync(rule); rule.selector = traverseNode(parsedSelector.clone()).toString(); rule.walkDecls(/composes|compose-with/, decl => { const localNames = getSingleLocalNamesForComposes(parsedSelector); const classes = decl.value.split(/\s+/); classes.forEach(className => { const global = /^global\(([^\)]+)\)$/.exec(className); if (global) { localNames.forEach(exportedName => { exports[exportedName].push(global[1]); }); } else if (hasOwnProperty.call(importedNames, className)) { localNames.forEach(exportedName => { exports[exportedName].push(className); }); } else if (hasOwnProperty.call(exports, className)) { localNames.forEach(exportedName => { exports[className].forEach(item => { exports[exportedName].push(item); }); }); } else { throw decl.error( `referenced class name "${className}" in ${decl.prop} not found` ); } }); decl.remove(); }); rule.walkDecls(decl => { let tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/); tokens = tokens.map((token, idx) => { if (idx === 0 || tokens[idx - 1] === ',') { const localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token); if (localMatch) { return ( localMatch[1] + exportScopedName(localMatch[2]) + token.substr(localMatch[0].length) ); } else { return token; } } else { return token; } }); decl.value = tokens.join(''); }); }); // Find any :local keyframes css.walkAtRules(atrule => { if (/keyframes$/i.test(atrule.name)) { const localMatch = /^\s*:local\s*\((.+?)\)\s*$/.exec(atrule.params); if (localMatch) { atrule.params = exportScopedName(localMatch[1]); } } }); // If we found any :locals, insert an :export rule const exportedNames = Object.keys(exports); if (exportedNames.length > 0) { const exportRule = postcss.rule({ selector: ':export' }); exportedNames.forEach(exportedName => exportRule.append({ prop: exportedName, value: exports[exportedName].join(' '), raws: { before: '\n ' }, }) ); css.append(exportRule); } }; }); processor.generateScopedName = function(name, path) { const sanitisedPath = path .replace(/\.[^\.\/\\]+$/, '') .replace(/[\W_]+/g, '_') .replace(/^_|_$/g, ''); return `_${sanitisedPath}__${name}`.trim(); }; processor.generateExportEntry = function(name, scopedName) { return { key: unescape(name), value: unescape(scopedName), }; }; module.exports = processor;