1import Exception from '../exception';
2
3function Visitor() {
4  this.parents = [];
5}
6
7Visitor.prototype = {
8  constructor: Visitor,
9  mutating: false,
10
11  // Visits a given value. If mutating, will replace the value if necessary.
12  acceptKey: function(node, name) {
13    let value = this.accept(node[name]);
14    if (this.mutating) {
15      // Hacky sanity check: This may have a few false positives for type for the helper
16      // methods but will generally do the right thing without a lot of overhead.
17      if (value && !Visitor.prototype[value.type]) {
18        throw new Exception(
19          'Unexpected node type "' +
20            value.type +
21            '" found when accepting ' +
22            name +
23            ' on ' +
24            node.type
25        );
26      }
27      node[name] = value;
28    }
29  },
30
31  // Performs an accept operation with added sanity check to ensure
32  // required keys are not removed.
33  acceptRequired: function(node, name) {
34    this.acceptKey(node, name);
35
36    if (!node[name]) {
37      throw new Exception(node.type + ' requires ' + name);
38    }
39  },
40
41  // Traverses a given array. If mutating, empty respnses will be removed
42  // for child elements.
43  acceptArray: function(array) {
44    for (let i = 0, l = array.length; i < l; i++) {
45      this.acceptKey(array, i);
46
47      if (!array[i]) {
48        array.splice(i, 1);
49        i--;
50        l--;
51      }
52    }
53  },
54
55  accept: function(object) {
56    if (!object) {
57      return;
58    }
59
60    /* istanbul ignore next: Sanity code */
61    if (!this[object.type]) {
62      throw new Exception('Unknown type: ' + object.type, object);
63    }
64
65    if (this.current) {
66      this.parents.unshift(this.current);
67    }
68    this.current = object;
69
70    let ret = this[object.type](object);
71
72    this.current = this.parents.shift();
73
74    if (!this.mutating || ret) {
75      return ret;
76    } else if (ret !== false) {
77      return object;
78    }
79  },
80
81  Program: function(program) {
82    this.acceptArray(program.body);
83  },
84
85  MustacheStatement: visitSubExpression,
86  Decorator: visitSubExpression,
87
88  BlockStatement: visitBlock,
89  DecoratorBlock: visitBlock,
90
91  PartialStatement: visitPartial,
92  PartialBlockStatement: function(partial) {
93    visitPartial.call(this, partial);
94
95    this.acceptKey(partial, 'program');
96  },
97
98  ContentStatement: function(/* content */) {},
99  CommentStatement: function(/* comment */) {},
100
101  SubExpression: visitSubExpression,
102
103  PathExpression: function(/* path */) {},
104
105  StringLiteral: function(/* string */) {},
106  NumberLiteral: function(/* number */) {},
107  BooleanLiteral: function(/* bool */) {},
108  UndefinedLiteral: function(/* literal */) {},
109  NullLiteral: function(/* literal */) {},
110
111  Hash: function(hash) {
112    this.acceptArray(hash.pairs);
113  },
114  HashPair: function(pair) {
115    this.acceptRequired(pair, 'value');
116  }
117};
118
119function visitSubExpression(mustache) {
120  this.acceptRequired(mustache, 'path');
121  this.acceptArray(mustache.params);
122  this.acceptKey(mustache, 'hash');
123}
124function visitBlock(block) {
125  visitSubExpression.call(this, block);
126
127  this.acceptKey(block, 'program');
128  this.acceptKey(block, 'inverse');
129}
130function visitPartial(partial) {
131  this.acceptRequired(partial, 'name');
132  this.acceptArray(partial.params);
133  this.acceptKey(partial, 'hash');
134}
135
136export default Visitor;
137