Namespace.js 4.0 KB

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