123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- // @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] === undefined) {
- delete this.current[undef];
- } else {
- this.current[undef] = undefs[undef];
- }
- }
- }
- }
- /**
- * 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.
- */
- 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];
- }
- }
- this.current[name] = value;
- }
- }
|