1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// @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;
}
}
}