import { List, walk, keyword as resolveKeyword } from 'css-tree'; const { hasOwnProperty } = Object.prototype; function addRuleToMap(map, item, list, single) { const node = item.data; const name = resolveKeyword(node.name).basename; const id = node.name.toLowerCase() + '/' + (node.prelude ? node.prelude.id : null); if (!hasOwnProperty.call(map, name)) { map[name] = Object.create(null); } if (single) { delete map[name][id]; } if (!hasOwnProperty.call(map[name], id)) { map[name][id] = new List(); } map[name][id].append(list.remove(item)); } function relocateAtrules(ast, options) { const collected = Object.create(null); let topInjectPoint = null; ast.children.forEach(function(node, item, list) { if (node.type === 'Atrule') { const name = resolveKeyword(node.name).basename; switch (name) { case 'keyframes': addRuleToMap(collected, item, list, true); return; case 'media': if (options.forceMediaMerge) { addRuleToMap(collected, item, list, false); return; } break; } if (topInjectPoint === null && name !== 'charset' && name !== 'import') { topInjectPoint = item; } } else { if (topInjectPoint === null) { topInjectPoint = item; } } }); for (const atrule in collected) { for (const id in collected[atrule]) { ast.children.insertList( collected[atrule][id], atrule === 'media' ? null : topInjectPoint ); } } }; function isMediaRule(node) { return node.type === 'Atrule' && node.name === 'media'; } function processAtrule(node, item, list) { if (!isMediaRule(node)) { return; } const prev = item.prev && item.prev.data; if (!prev || !isMediaRule(prev)) { return; } // merge @media with same query if (node.prelude && prev.prelude && node.prelude.id === prev.prelude.id) { prev.block.children.appendList(node.block.children); list.remove(item); // TODO: use it when we can refer to several points in source // prev.loc = { // primary: prev.loc, // merged: node.loc // }; } } export default function rejoinAtrule(ast, options) { relocateAtrules(ast, options); walk(ast, { visit: 'Atrule', reverse: true, enter: processAtrule }); };