UNPKG

takedown

Version:
252 lines (210 loc) 7.62 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var TakedownError = require('../../../error/TakedownError.js'); var arbiter = require('./arbiter.js'); var closer = require('./closer.js'); var pruning = require('./pruning.js'); let edata = ({ name, type, id }) => ({ name, type, id }); function createAgent (config) { let self = { agent: true }, parent, model; // parsing context elements let agent, current, madoe, stream, streamFn; // entity state tracking let state, backstate; // entity index tracker let nendex; // agent data pruning let getPruning = pruning.default(self); // agent entity close handler let close = closer.default(self); let actionLog = (action, chunk) => { if (config.onAction) { let details = { action: String(action), entity: edata(model), chunk: chunk.toString(), index: chunk.index, }; if (parent) details.parent = edata(parent.model); config.onAction(details); } }; self.initialize = (context, entity, branch) => { ({ agent, current, madoe, stream, streamFn } = context); let ent = madoe.withNesters(madoe(entity)); state = { ...ent.state }; backstate = [ { ...state }, { ...state } ]; nendex = 0; parent = branch; model = { ...ent, agent: true, id: performance.now().toString(16).replace('.', ''), chunks: [], current, document: context.document, getPruning, opens, // starting/ending indexes in source index: -1, endex: -1, inliner: ent.type === 'block' ? context.inlineParser(ent) : void 0, stream: streamFn, prune: chunk => chunk ? ent.prune.call(model, chunk, state) : null, toString: () => model.chunk.valueOf(), get chunk() { return streamFn.slice(model.index, model.endex); }, get content() { return model.chunks.join(''); }, get state() { return state; }, }; self.parent = parent; self.model = model; // parentless (root) agent always priority 0 self.priority = parent ? model.priority : 0; // get entity open for `chunk` self.open = chunk => model.open(chunk, state); // get entity response for `chunk` self.action = chunk => model.action(chunk, state); // chunk action resolution self.arbitrate = arbiter.default(parent, self); // chunk processing self.next = next; self.exec = execute; // agent/chunk abortions/additions self.abort = abort; self.apply = apply; return self; }; /* Checks to see if at least one named entity can open as a peer (sibling). This should never be called from root entity. */ let opens = (chunk, ...names) => { let index = names.findIndex(name => { let candidate = agent(name, parent); let canOpen = candidate.open(chunk); agent.repool(candidate); return canOpen; }); return index >= 0; }; /* Advance to next entity, reset state, and rewind iterator. @param { object } restate Retry state. */ let abort = (aborted, restate) => { let { model } = aborted; // repool aborted agent agent.repool(aborted); let retry = typeof restate === 'object'; // advance entity search index (skip `agent`) if (!retry) nendex ++; // return state to moment before acceptance state = { ...backstate[1] }; // move streamer back to index of first chunk in `agent` stream.goto(model.index); if (retry) { let { name, nesters } = model; // retry parsing with modified version of same entity let modded = madoe.custom(name, { state: restate, nesters }); return agent(modded, self); } return self; }; /* Append chunk (or agent), reset entity index, advance entity state. */ let apply = (chunk, value) => { if (chunk.agent) { model.chunks.push(chunk.model); model.endex = chunk.model.endex; // agent.repool(chunk); } else { if (value.valueOf()) model.chunks.push(value); model.endex = chunk.endex; } // reset entity search index nendex = 0; // return state to moment of acceptance state = { ...backstate[0] }; return self; }; /* Look for a nestable entity that can accept `chunk`. */ let spawn = chunk => { for (;nendex<model.nesters.length;nendex++) { let candidate = agent(model.nesters[nendex], self); let response = candidate.open(chunk); // response can be an action here if (response) return candidate.next(chunk, response); // return agent object to pool agent.repool(candidate); } }; /* Processes a document chunk based on the desired `action`. If `action` is not specified, it will be derived from entity action arbitration. @param { object } action Action to be performed. */ let next = (chunk, action) => { current.model = model; current.chunk = chunk; // update starting index if not already set if (model.index < 0) model.index = model.endex = chunk.index; // do arbitration if no action specified if (!action?.action) action = self.arbitrate(chunk); // track previous states for apply/abort backstate = [ { ...state }, backstate[0] ]; return execute(chunk, action); }; /* Executes the specified `action` on behalf of entity on `chunk`. */ let execute = (chunk, action) => { // set next parsing index if >= current index if (typeof action.endex === 'number' && action.endex >= chunk.index) { stream.goto(action.endex); // update parsed chunk to be the slice desired chunk = streamFn.slice(chunk.index, action.endex); // set action value as well if desired if (action.value === true) action.value = chunk; } actionLog(action, chunk); // continue and look for children if (action.accept) return spawn(chunk) || apply(chunk, action.value); // continue without looking for children if (action.block) return apply(chunk, action.value); // exclude chunk from output and continue if (action.censor) return self; // parentless (root) agents must close here if (!parent) return close(); // close and consume current chunk if (action.consume) return (apply(chunk, action.value), close()); // close without consuming chunk if (action.reject) return close(chunk); // abandon everything including current chunk if (action.abort) return parent.abort(self, action.value); // definitely an error by now throw new TakedownError.default(`"${action}" is not a valid entity action response`); }; return self; } exports.default = createAgent;