const { hasOwnProperty } = Object.prototype; const noop = function() {}; function ensureFunction(value) { return typeof value === 'function' ? value : noop; } function invokeForType(fn, type) { return function(node, item, list) { if (node.type === type) { fn.call(this, node, item, list); } }; } function getWalkersFromStructure(name, nodeType) { const structure = nodeType.structure; const walkers = []; for (const key in structure) { if (hasOwnProperty.call(structure, key) === false) { continue; } let fieldTypes = structure[key]; const walker = { name: key, type: false, nullable: false }; if (!Array.isArray(fieldTypes)) { fieldTypes = [fieldTypes]; } for (const fieldType of fieldTypes) { if (fieldType === null) { walker.nullable = true; } else if (typeof fieldType === 'string') { walker.type = 'node'; } else if (Array.isArray(fieldType)) { walker.type = 'list'; } } if (walker.type) { walkers.push(walker); } } if (walkers.length) { return { context: nodeType.walkContext, fields: walkers }; } return null; } function getTypesFromConfig(config) { const types = {}; for (const name in config.node) { if (hasOwnProperty.call(config.node, name)) { const nodeType = config.node[name]; if (!nodeType.structure) { throw new Error('Missed `structure` field in `' + name + '` node type definition'); } types[name] = getWalkersFromStructure(name, nodeType); } } return types; } function createTypeIterator(config, reverse) { const fields = config.fields.slice(); const contextName = config.context; const useContext = typeof contextName === 'string'; if (reverse) { fields.reverse(); } return function(node, context, walk, walkReducer) { let prevContextValue; if (useContext) { prevContextValue = context[contextName]; context[contextName] = node; } for (const field of fields) { const ref = node[field.name]; if (!field.nullable || ref) { if (field.type === 'list') { const breakWalk = reverse ? ref.reduceRight(walkReducer, false) : ref.reduce(walkReducer, false); if (breakWalk) { return true; } } else if (walk(ref)) { return true; } } } if (useContext) { context[contextName] = prevContextValue; } }; } function createFastTraveralMap({ StyleSheet, Atrule, Rule, Block, DeclarationList }) { return { Atrule: { StyleSheet, Atrule, Rule, Block }, Rule: { StyleSheet, Atrule, Rule, Block }, Declaration: { StyleSheet, Atrule, Rule, Block, DeclarationList } }; } export function createWalker(config) { const types = getTypesFromConfig(config); const iteratorsNatural = {}; const iteratorsReverse = {}; const breakWalk = Symbol('break-walk'); const skipNode = Symbol('skip-node'); for (const name in types) { if (hasOwnProperty.call(types, name) && types[name] !== null) { iteratorsNatural[name] = createTypeIterator(types[name], false); iteratorsReverse[name] = createTypeIterator(types[name], true); } } const fastTraversalIteratorsNatural = createFastTraveralMap(iteratorsNatural); const fastTraversalIteratorsReverse = createFastTraveralMap(iteratorsReverse); const walk = function(root, options) { function walkNode(node, item, list) { const enterRet = enter.call(context, node, item, list); if (enterRet === breakWalk) { return true; } if (enterRet === skipNode) { return false; } if (iterators.hasOwnProperty(node.type)) { if (iterators[node.type](node, context, walkNode, walkReducer)) { return true; } } if (leave.call(context, node, item, list) === breakWalk) { return true; } return false; } let enter = noop; let leave = noop; let iterators = iteratorsNatural; let walkReducer = (ret, data, item, list) => ret || walkNode(data, item, list); const context = { break: breakWalk, skip: skipNode, root, stylesheet: null, atrule: null, atrulePrelude: null, rule: null, selector: null, block: null, declaration: null, function: null }; if (typeof options === 'function') { enter = options; } else if (options) { enter = ensureFunction(options.enter); leave = ensureFunction(options.leave); if (options.reverse) { iterators = iteratorsReverse; } if (options.visit) { if (fastTraversalIteratorsNatural.hasOwnProperty(options.visit)) { iterators = options.reverse ? fastTraversalIteratorsReverse[options.visit] : fastTraversalIteratorsNatural[options.visit]; } else if (!types.hasOwnProperty(options.visit)) { throw new Error('Bad value `' + options.visit + '` for `visit` option (should be: ' + Object.keys(types).sort().join(', ') + ')'); } enter = invokeForType(enter, options.visit); leave = invokeForType(leave, options.visit); } } if (enter === noop && leave === noop) { throw new Error('Neither `enter` nor `leave` walker handler is set or both aren\'t a function'); } walkNode(root); }; walk.break = breakWalk; walk.skip = skipNode; walk.find = function(ast, fn) { let found = null; walk(ast, function(node, item, list) { if (fn.call(this, node, item, list)) { found = node; return breakWalk; } }); return found; }; walk.findLast = function(ast, fn) { let found = null; walk(ast, { reverse: true, enter(node, item, list) { if (fn.call(this, node, item, list)) { found = node; return breakWalk; } } }); return found; }; walk.findAll = function(ast, fn) { const found = []; walk(ast, function(node, item, list) { if (fn.call(this, node, item, list)) { found.push(node); } }); return found; }; return walk; };