1/* global define, require */
2import { isArray } from '../utils';
3
4let SourceNode;
5
6try {
7  /* istanbul ignore next */
8  if (typeof define !== 'function' || !define.amd) {
9    // We don't support this in AMD environments. For these environments, we assume that
10    // they are running on the browser and thus have no need for the source-map library.
11    let SourceMap = require('source-map');
12    SourceNode = SourceMap.SourceNode;
13  }
14} catch (err) {
15  /* NOP */
16}
17
18/* istanbul ignore if: tested but not covered in istanbul due to dist build  */
19if (!SourceNode) {
20  SourceNode = function(line, column, srcFile, chunks) {
21    this.src = '';
22    if (chunks) {
23      this.add(chunks);
24    }
25  };
26  /* istanbul ignore next */
27  SourceNode.prototype = {
28    add: function(chunks) {
29      if (isArray(chunks)) {
30        chunks = chunks.join('');
31      }
32      this.src += chunks;
33    },
34    prepend: function(chunks) {
35      if (isArray(chunks)) {
36        chunks = chunks.join('');
37      }
38      this.src = chunks + this.src;
39    },
40    toStringWithSourceMap: function() {
41      return { code: this.toString() };
42    },
43    toString: function() {
44      return this.src;
45    }
46  };
47}
48
49function castChunk(chunk, codeGen, loc) {
50  if (isArray(chunk)) {
51    let ret = [];
52
53    for (let i = 0, len = chunk.length; i < len; i++) {
54      ret.push(codeGen.wrap(chunk[i], loc));
55    }
56    return ret;
57  } else if (typeof chunk === 'boolean' || typeof chunk === 'number') {
58    // Handle primitives that the SourceNode will throw up on
59    return chunk + '';
60  }
61  return chunk;
62}
63
64function CodeGen(srcFile) {
65  this.srcFile = srcFile;
66  this.source = [];
67}
68
69CodeGen.prototype = {
70  isEmpty() {
71    return !this.source.length;
72  },
73  prepend: function(source, loc) {
74    this.source.unshift(this.wrap(source, loc));
75  },
76  push: function(source, loc) {
77    this.source.push(this.wrap(source, loc));
78  },
79
80  merge: function() {
81    let source = this.empty();
82    this.each(function(line) {
83      source.add(['  ', line, '\n']);
84    });
85    return source;
86  },
87
88  each: function(iter) {
89    for (let i = 0, len = this.source.length; i < len; i++) {
90      iter(this.source[i]);
91    }
92  },
93
94  empty: function() {
95    let loc = this.currentLocation || { start: {} };
96    return new SourceNode(loc.start.line, loc.start.column, this.srcFile);
97  },
98  wrap: function(chunk, loc = this.currentLocation || { start: {} }) {
99    if (chunk instanceof SourceNode) {
100      return chunk;
101    }
102
103    chunk = castChunk(chunk, this, loc);
104
105    return new SourceNode(
106      loc.start.line,
107      loc.start.column,
108      this.srcFile,
109      chunk
110    );
111  },
112
113  functionCall: function(fn, type, params) {
114    params = this.generateList(params);
115    return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']);
116  },
117
118  quotedString: function(str) {
119    return (
120      '"' +
121      (str + '')
122        .replace(/\\/g, '\\\\')
123        .replace(/"/g, '\\"')
124        .replace(/\n/g, '\\n')
125        .replace(/\r/g, '\\r')
126        .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
127        .replace(/\u2029/g, '\\u2029') +
128      '"'
129    );
130  },
131
132  objectLiteral: function(obj) {
133    let pairs = [];
134
135    Object.keys(obj).forEach(key => {
136      let value = castChunk(obj[key], this);
137      if (value !== 'undefined') {
138        pairs.push([this.quotedString(key), ':', value]);
139      }
140    });
141
142    let ret = this.generateList(pairs);
143    ret.prepend('{');
144    ret.add('}');
145    return ret;
146  },
147
148  generateList: function(entries) {
149    let ret = this.empty();
150
151    for (let i = 0, len = entries.length; i < len; i++) {
152      if (i) {
153        ret.add(',');
154      }
155
156      ret.add(castChunk(entries[i], this));
157    }
158
159    return ret;
160  },
161
162  generateArray: function(entries) {
163    let ret = this.generateList(entries);
164    ret.prepend('[');
165    ret.add(']');
166
167    return ret;
168  }
169};
170
171export default CodeGen;
172