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 |
|
11 | import ParseError from "./ParseError";
|
12 |
|
13 | export type Mapping<Value> = {[string]: Value};
|
14 |
|
15 | export 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 | }
|