// @flow /** * A `Namespace` refers to a space of nameable things like macros or lengths, * which can be `set` either globally or local to a nested group, using an * undo stack similar to how TeX implements this functionality. * Performance-wise, `get` and local `set` take constant time, while global * `set` takes time proportional to the depth of group nesting. */ import ParseError from "./ParseError"; export type Mapping = {[string]: Value}; export default class Namespace { current: Mapping; builtins: Mapping; undefStack: Mapping[]; /** * Both arguments are optional. The first argument is an object of * built-in mappings which never change. The second argument is an object * of initial (global-level) mappings, which will constantly change * according to any global/top-level `set`s done. */ constructor(builtins: Mapping = {}, globalMacros: Mapping = {}) { this.current = globalMacros; this.builtins = builtins; this.undefStack = []; } /** * Start a new nested group, affecting future local `set`s. */ beginGroup() { this.undefStack.push({}); } /** * End current nested group, restoring values before the group began. */ endGroup() { if (this.undefStack.length === 0) { throw new ParseError("Unbalanced namespace destruction: attempt " + "to pop global namespace; please report this as a bug"); } const undefs = this.undefStack.pop(); for (const undef in undefs) { if (undefs.hasOwnProperty(undef)) { if (undefs[undef] == null) { delete this.current[undef]; } else { this.current[undef] = undefs[undef]; } } } } /** * Ends all currently nested groups (if any), restoring values before the * groups began. Useful in case of an error in the middle of parsing. */ endGroups() { while (this.undefStack.length > 0) { this.endGroup(); } } /** * Detect whether `name` has a definition. Equivalent to * `get(name) != null`. */ has(name: string): boolean { return this.current.hasOwnProperty(name) || this.builtins.hasOwnProperty(name); } /** * Get the current value of a name, or `undefined` if there is no value. * * Note: Do not use `if (namespace.get(...))` to detect whether a macro * is defined, as the definition may be the empty string which evaluates * to `false` in JavaScript. Use `if (namespace.get(...) != null)` or * `if (namespace.has(...))`. */ get(name: string): ?Value { if (this.current.hasOwnProperty(name)) { return this.current[name]; } else { return this.builtins[name]; } } /** * Set the current value of a name, and optionally set it globally too. * Local set() sets the current value and (when appropriate) adds an undo * operation to the undo stack. Global set() may change the undo * operation at every level, so takes time linear in their number. * A value of undefined means to delete existing definitions. */ set(name: string, value: ?Value, global: boolean = false) { if (global) { // Global set is equivalent to setting in all groups. Simulate this // by destroying any undos currently scheduled for this name, // and adding an undo with the *new* value (in case it later gets // locally reset within this environment). for (let i = 0; i < this.undefStack.length; i++) { delete this.undefStack[i][name]; } if (this.undefStack.length > 0) { this.undefStack[this.undefStack.length - 1][name] = value; } } else { // Undo this set at end of this group (possibly to `undefined`), // unless an undo is already in place, in which case that older // value is the correct one. const top = this.undefStack[this.undefStack.length - 1]; if (top && !top.hasOwnProperty(name)) { top[name] = this.current[name]; } } if (value == null) { delete this.current[name]; } else { this.current[name] = value; } } }