// @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<Value> = {[string]: Value}; | |
export default class Namespace<Value> { | |
current: Mapping<Value>; | |
builtins: Mapping<Value>; | |
undefStack: Mapping<?Value>[]; | |
/** | |
* 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<Value> = {}, | |
globalMacros: Mapping<Value> = {}) { | |
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; | |
} | |
} | |
} | |