1 | var utils = require('util');
|
2 | var EventEmitter = require('events').EventEmitter;
|
3 | var Token = require('cst').Token;
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | function TokenAssert(file) {
|
12 | EventEmitter.call(this);
|
13 |
|
14 | this._file = file;
|
15 | }
|
16 |
|
17 | utils.inherits(TokenAssert, EventEmitter);
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | TokenAssert.prototype.whitespaceBetween = function(options) {
|
30 | options.atLeast = 1;
|
31 | return this.spacesBetween(options);
|
32 | };
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | TokenAssert.prototype.noWhitespaceBetween = function(options) {
|
45 | options.exactly = 0;
|
46 | return this.spacesBetween(options);
|
47 | };
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 | TokenAssert.prototype.spacesBetween = function(options) {
|
63 | var token = options.token;
|
64 | var nextToken = options.nextToken;
|
65 | var atLeast = options.atLeast;
|
66 | var atMost = options.atMost;
|
67 | var exactly = options.exactly;
|
68 |
|
69 | if (!token || !nextToken) {
|
70 | return false;
|
71 | }
|
72 |
|
73 | this._validateOptions(options);
|
74 |
|
75 | if (!options.disallowNewLine && !this._file.isOnTheSameLine(token, nextToken)) {
|
76 | return false;
|
77 | }
|
78 |
|
79 |
|
80 |
|
81 | var fixed = !options.token.getNextNonWhitespaceToken().isComment;
|
82 |
|
83 | var emitError = function(countPrefix, spaceCount) {
|
84 | var fix = function() {
|
85 | this._file.setWhitespaceBefore(nextToken, new Array(spaceCount + 1).join(' '));
|
86 | }.bind(this);
|
87 |
|
88 | var msgPostfix = token.value + ' and ' + nextToken.value;
|
89 |
|
90 | if (!options.message) {
|
91 | if (exactly === 0) {
|
92 |
|
93 | options.message = 'Unexpected whitespace between ' + msgPostfix;
|
94 | } else if (exactly !== undefined) {
|
95 |
|
96 | options.message = spaceCount + ' spaces required between ' + msgPostfix;
|
97 | } else if (atLeast === 1 && atMost === undefined) {
|
98 |
|
99 | options.message = 'Missing space between ' + msgPostfix;
|
100 | } else {
|
101 | options.message = countPrefix + ' ' + spaceCount + ' spaces required between ' + msgPostfix;
|
102 | }
|
103 | }
|
104 |
|
105 | this.emit('error', {
|
106 | message: options.message,
|
107 | element: token,
|
108 | offset: token.getSourceCodeLength(),
|
109 | fix: fixed ? fix : undefined
|
110 | });
|
111 | }.bind(this);
|
112 |
|
113 | var spacesBetween = this._file.getDistanceBetween(token, nextToken);
|
114 |
|
115 | if (atLeast !== undefined && spacesBetween < atLeast) {
|
116 | emitError('at least', atLeast);
|
117 | return true;
|
118 | }
|
119 |
|
120 | if (atMost !== undefined && spacesBetween > atMost) {
|
121 | emitError('at most', atMost);
|
122 | return true;
|
123 | }
|
124 |
|
125 | if (exactly !== undefined && spacesBetween !== exactly) {
|
126 | emitError('exactly', exactly);
|
127 | return true;
|
128 | }
|
129 |
|
130 | return false;
|
131 | };
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | TokenAssert.prototype.indentation = function(options) {
|
144 | var token = options.token;
|
145 | var lineNumber = options.lineNumber;
|
146 | var actual = options.actual;
|
147 | var expected = options.expected;
|
148 | var indentChar = options.indentChar;
|
149 |
|
150 | if (actual === expected) {
|
151 | return false;
|
152 | }
|
153 |
|
154 | this.emit('error', {
|
155 | message: 'Expected indentation of ' + expected + ' characters',
|
156 | line: lineNumber,
|
157 | column: expected,
|
158 | fix: function() {
|
159 | var newWhitespace = (new Array(expected + 1)).join(indentChar);
|
160 |
|
161 | this._updateWhitespaceByLine(token, function(lines) {
|
162 | lines[lines.length - 1] = newWhitespace;
|
163 | return lines;
|
164 | });
|
165 |
|
166 | if (token.isComment) {
|
167 | this._updateCommentWhitespace(token, indentChar, actual, expected);
|
168 | }
|
169 | }.bind(this)
|
170 | });
|
171 |
|
172 | return true;
|
173 | };
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 | TokenAssert.prototype._updateWhitespaceByLine = function(token, callback) {
|
183 | var lineBreak = this._file.getLineBreakStyle();
|
184 | var lines = this._file.getWhitespaceBefore(token).split(/\r\n|\r|\n/);
|
185 |
|
186 | lines = callback(lines);
|
187 | this._file.setWhitespaceBefore(token, lines.join(lineBreak));
|
188 | };
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 | TokenAssert.prototype._updateCommentWhitespace = function(token, indentChar, actual, expected) {
|
200 | var difference = expected - actual;
|
201 | var tokenLines = token.value.split(/\r\n|\r|\n/);
|
202 | var i = 1;
|
203 | if (difference >= 0) {
|
204 | var lineWhitespace = (new Array(difference + 1)).join(indentChar);
|
205 | for (; i < tokenLines.length; i++) {
|
206 | tokenLines[i] = tokenLines[i] === '' ? '' : lineWhitespace + tokenLines[i];
|
207 | }
|
208 | } else {
|
209 | for (; i < tokenLines.length; i++) {
|
210 | tokenLines[i] = tokenLines[i].substring(-difference);
|
211 | }
|
212 | }
|
213 |
|
214 | var newComment = new Token('CommentBlock', tokenLines.join(this._file.getLineBreakStyle()));
|
215 | token.parentElement.replaceChild(newComment, token);
|
216 | };
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 | TokenAssert.prototype.sameLine = function(options) {
|
229 | options.exactly = 0;
|
230 |
|
231 | return this.linesBetween(options);
|
232 | };
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 | TokenAssert.prototype.differentLine = function(options) {
|
244 | options.atLeast = 1;
|
245 |
|
246 | return this.linesBetween(options);
|
247 | };
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | TokenAssert.prototype.linesBetween = function(options) {
|
265 | var token = options.token;
|
266 | var nextToken = options.nextToken;
|
267 | var atLeast = options.atLeast;
|
268 | var atMost = options.atMost;
|
269 | var exactly = options.exactly;
|
270 |
|
271 | if (!token || !nextToken) {
|
272 | return false;
|
273 | }
|
274 |
|
275 | this._validateOptions(options);
|
276 |
|
277 |
|
278 |
|
279 | var fixed = !options.token.getNextNonWhitespaceToken().isComment;
|
280 |
|
281 | var linesBetween = this._file.getLineCountBetween(token, nextToken);
|
282 |
|
283 | var emitError = function(countPrefix, lineCount) {
|
284 | var msgPrefix = token.value + ' and ' + nextToken.value;
|
285 |
|
286 | var fix = function() {
|
287 | this._augmentLineCount(options, lineCount);
|
288 | }.bind(this);
|
289 |
|
290 | if (!options.message) {
|
291 | if (exactly === 0) {
|
292 |
|
293 | options.message = msgPrefix + ' should be on the same line';
|
294 | } else if (atLeast === 1 && atMost === undefined) {
|
295 |
|
296 | options.message = msgPrefix + ' should be on different lines';
|
297 | } else {
|
298 |
|
299 | options.message = msgPrefix + ' should have ' + countPrefix + ' ' + lineCount + ' line(s) between them';
|
300 | }
|
301 | }
|
302 |
|
303 | this.emit('error', {
|
304 | message: options.message,
|
305 | element: token,
|
306 | offset: token.getSourceCodeLength(),
|
307 | fix: fixed ? fix : undefined
|
308 | });
|
309 | }.bind(this);
|
310 |
|
311 | if (atLeast !== undefined && linesBetween < atLeast) {
|
312 | emitError('at least', atLeast);
|
313 | return true;
|
314 | }
|
315 |
|
316 | if (atMost !== undefined && linesBetween > atMost) {
|
317 | emitError('at most', atMost);
|
318 | return true;
|
319 | }
|
320 |
|
321 | if (exactly !== undefined && linesBetween !== exactly) {
|
322 | emitError('exactly', exactly);
|
323 | return true;
|
324 | }
|
325 |
|
326 | return false;
|
327 | };
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 | TokenAssert.prototype._validateOptions = function(options) {
|
343 | var token = options.token;
|
344 | var nextToken = options.nextToken;
|
345 | var atLeast = options.atLeast;
|
346 | var atMost = options.atMost;
|
347 | var exactly = options.exactly;
|
348 |
|
349 | if (token === nextToken) {
|
350 | throw new Error('You cannot specify the same token as both token and nextToken');
|
351 | }
|
352 |
|
353 | if (atLeast === undefined &&
|
354 | atMost === undefined &&
|
355 | exactly === undefined) {
|
356 | throw new Error('You must specify at least one option');
|
357 | }
|
358 |
|
359 | if (exactly !== undefined && (atLeast !== undefined || atMost !== undefined)) {
|
360 | throw new Error('You cannot specify atLeast or atMost with exactly');
|
361 | }
|
362 |
|
363 | if (atLeast !== undefined && atMost !== undefined && atMost < atLeast) {
|
364 | throw new Error('atLeast and atMost are in conflict');
|
365 | }
|
366 | };
|
367 |
|
368 |
|
369 |
|
370 |
|
371 |
|
372 |
|
373 |
|
374 |
|
375 |
|
376 |
|
377 | TokenAssert.prototype._augmentLineCount = function(options, lineCount) {
|
378 | var token = options.nextToken;
|
379 | if (lineCount === 0) {
|
380 | if (options.stickToPreviousToken) {
|
381 | var nextToken = this._file.getNextToken(token, {
|
382 | includeComments: true
|
383 | });
|
384 | this._file.setWhitespaceBefore(nextToken, this._file.getWhitespaceBefore(token));
|
385 | }
|
386 |
|
387 | this._file.setWhitespaceBefore(token, ' ');
|
388 | return;
|
389 | }
|
390 |
|
391 | this._updateWhitespaceByLine(token, function(lines) {
|
392 | var currentLineCount = lines.length;
|
393 | var lastLine = lines[lines.length - 1];
|
394 |
|
395 | if (currentLineCount <= lineCount) {
|
396 |
|
397 | for (; currentLineCount <= lineCount; currentLineCount++) {
|
398 | lines[lines.length - 1] = '';
|
399 | lines.push(lastLine);
|
400 | }
|
401 | } else {
|
402 |
|
403 | lines = lines.slice(0, lineCount + 1);
|
404 | lines[lines.length - 1] = lastLine;
|
405 | }
|
406 |
|
407 | return lines;
|
408 | });
|
409 | };
|
410 |
|
411 | module.exports = TokenAssert;
|