UNPKG

4.11 kBJavaScriptView Raw
1// @flow
2
3/**
4 * A `Namespace` refers to a space of nameable things like macros or lengths,
5 * which can be `set` either globally or local to a nested group, using an
6 * undo stack similar to how TeX implements this functionality.
7 * Performance-wise, `get` and local `set` take constant time, while global
8 * `set` takes time proportional to the depth of group nesting.
9 */
10
11import ParseError from "./ParseError";
12
13export type Mapping<Value> = {[string]: Value};
14
15export default class Namespace<Value> {
16 current: Mapping<Value>;
17 builtins: Mapping<Value>;
18 undefStack: Mapping<Value>[];
19
20 /**
21 * Both arguments are optional. The first argument is an object of
22 * built-in mappings which never change. The second argument is an object
23 * of initial (global-level) mappings, which will constantly change
24 * according to any global/top-level `set`s done.
25 */
26 constructor(builtins: Mapping<Value> = {},
27 globalMacros: Mapping<Value> = {}) {
28 this.current = globalMacros;
29 this.builtins = builtins;
30 this.undefStack = [];
31 }
32
33 /**
34 * Start a new nested group, affecting future local `set`s.
35 */
36 beginGroup() {
37 this.undefStack.push({});
38 }
39
40 /**
41 * End current nested group, restoring values before the group began.
42 */
43 endGroup() {
44 if (this.undefStack.length === 0) {
45 throw new ParseError("Unbalanced namespace destruction: attempt " +
46 "to pop global namespace; please report this as a bug");
47 }
48 const undefs = this.undefStack.pop();
49 for (const undef in undefs) {
50 if (undefs.hasOwnProperty(undef)) {
51 if (undefs[undef] === undefined) {
52 delete this.current[undef];
53 } else {
54 this.current[undef] = undefs[undef];
55 }
56 }
57 }
58 }
59
60 /**
61 * Detect whether `name` has a definition. Equivalent to
62 * `get(name) != null`.
63 */
64 has(name: string): boolean {
65 return this.current.hasOwnProperty(name) ||
66 this.builtins.hasOwnProperty(name);
67 }
68
69 /**
70 * Get the current value of a name, or `undefined` if there is no value.
71 *
72 * Note: Do not use `if (namespace.get(...))` to detect whether a macro
73 * is defined, as the definition may be the empty string which evaluates
74 * to `false` in JavaScript. Use `if (namespace.get(...) != null)` or
75 * `if (namespace.has(...))`.
76 */
77 get(name: string): ?Value {
78 if (this.current.hasOwnProperty(name)) {
79 return this.current[name];
80 } else {
81 return this.builtins[name];
82 }
83 }
84
85 /**
86 * Set the current value of a name, and optionally set it globally too.
87 * Local set() sets the current value and (when appropriate) adds an undo
88 * operation to the undo stack. Global set() may change the undo
89 * operation at every level, so takes time linear in their number.
90 */
91 set(name: string, value: Value, global: boolean = false) {
92 if (global) {
93 // Global set is equivalent to setting in all groups. Simulate this
94 // by destroying any undos currently scheduled for this name,
95 // and adding an undo with the *new* value (in case it later gets
96 // locally reset within this environment).
97 for (let i = 0; i < this.undefStack.length; i++) {
98 delete this.undefStack[i][name];
99 }
100 if (this.undefStack.length > 0) {
101 this.undefStack[this.undefStack.length - 1][name] = value;
102 }
103 } else {
104 // Undo this set at end of this group (possibly to `undefined`),
105 // unless an undo is already in place, in which case that older
106 // value is the correct one.
107 const top = this.undefStack[this.undefStack.length - 1];
108 if (top && !top.hasOwnProperty(name)) {
109 top[name] = this.current[name];
110 }
111 }
112 this.current[name] = value;
113 }
114}