5 var _tk = require('rocambole-token');
6 var debug = require('debug');
7 var debugBefore = debug('rocambole:br:before');
8 var debugAfter = debug('rocambole:br:after');
9 var debugBetween = debug('rocambole:br:between');
11 // yeah, we use semver to parse integers. it's lame but works and will give
12 // more flexibility while still keeping a format that is easy to read
13 var semver = require('semver');
15 // fallback in case plugin author forgets to call setOptions
24 exports.setOptions = setOptions;
25 function setOptions(opts) {
30 exports.limit = limit;
31 function limit(token, typeOrValue) {
32 limitBefore(token, typeOrValue);
33 limitAfter(token, typeOrValue);
37 exports.limitBefore = limitBefore;
38 function limitBefore(token, typeOrValue) {
39 var expected = expectedBefore(typeOrValue);
41 'typeOrValue: %s, expected: %s, value: %s',
42 typeOrValue, expected, token && token.value
44 if (expected < 0) return; // noop
45 var start = getStartToken(token);
46 limitInBetween('before', start, token, expected);
50 exports.limitAfter = limitAfter;
51 function limitAfter(token, typeOrValue) {
52 var expected = expectedAfter(typeOrValue);
54 'typeOrValue: %s, expected: %s, value: %s',
55 typeOrValue, expected, token && token.value
57 if (expected < 0) return; // noop
58 var end = getEndToken(token);
59 limitInBetween('after', token, end, expected);
62 exports.expectedBefore = expectedBefore;
63 function expectedBefore(typeOrValue) {
64 return getExpect('before', typeOrValue);
67 exports.expectedAfter = expectedAfter;
68 function expectedAfter(typeOrValue) {
69 return getExpect('after', typeOrValue);
73 function getExpect(location, typeOrValue) {
76 // we allow expected value (number) as 2nd argument or the node type (string)
77 if (typeof typeOrValue === 'string') {
78 expected = _curOpts[location][typeOrValue];
80 expected = typeOrValue;
83 // default is noop, explicit is better than implicit
84 expected = expected != null ? expected : -1;
86 if (typeof expected === 'boolean') {
87 // if user sets booleans by mistake we simply add one if missing (true)
88 // or remove all if false
89 expected = expected ? '>=1' : 0;
95 } else if (typeof expected === 'number') {
96 return String(expected);
103 function limitInBetween(location, start, end, expected) {
104 var n = getDiff(start, end, expected);
105 debugBetween('diff: %d', n);
107 _tk.removeInBetween(start, end, 'WhiteSpace');
110 _tk.removeInBetween(start, end, function(token) {
111 return token.type === 'LineBreak' && n++ < 0 &&
112 !siblingIsComment(location, token);
115 var target = location === 'after' ? start : end;
116 var insertNextTo = _tk[location];
118 insertNextTo(target, {
120 value: _curOpts.value
127 function siblingIsComment(location, token) {
128 var prop = location === 'before' ? 'prev' : 'next';
129 return _tk.isComment(token[prop]);
133 function getDiff(start, end, expected) {
134 // start will only be equal to end if it's start or file
135 if (start === end) return 0;
136 var count = countBrInBetween(start, end);
137 // yeah, it's ugly to strings to compare integers but was quickest solution
138 var vCount = String(count) + '.0.0';
139 if (semver.satisfies(vCount, expected)) {
142 return getSatisfyingMatch(count, vCount, expected) - count;
147 function getSatisfyingMatch(count, vCount, expected) {
149 var diff = semver.gtr(vCount, expected) ? -1 : 1;
151 while (result == null && count >= 0 && count < 100) {
152 if (semver.satisfies(String(count) + '.0.0', expected)) {
157 return parseInt(result, 10);
161 function countBrInBetween(start, end) {
163 _tk.eachInBetween(start, end, function(token) {
164 if (_tk.isBr(token)) count++;
170 function getEndToken(token) {
171 var end = _tk.findNextNonEmpty(token);
172 if (shouldSkipToken(end)) {
173 end = _tk.findNextNonEmpty(end);
175 return end ? end : token.root.endToken;
179 function shouldSkipToken(token) {
180 // if comment is at same line we skip it unless it has a specific rule that
181 // would add line breaks
182 var result = _tk.isComment(token) && !isOnSeparateLine(token);
183 return result && getExpect('before', token.type) <= 0;
187 function isOnSeparateLine(token) {
188 return _tk.isBr(token.prev) || (
189 _tk.isEmpty(token.prev) && _tk.isBr(token.prev.prev)
194 function getStartToken(token) {
195 var end = _tk.findPrevNonEmpty(token);
196 return end ? end : token.root.startToken;
200 exports.limitBeforeEndOfFile = function(ast, amount) {
201 var typeOrValue = amount != null ? amount : 'EndOfFile';
202 var expected = getExpect('before', typeOrValue);
204 if (expected < 0) return; // noop
206 var lastNonEmpty = _tk.isEmpty(ast.endToken) ?
207 _tk.findPrevNonEmpty(ast.endToken) :
211 limitInBetween('after', lastNonEmpty, null, expected);
216 value: _curOpts.value
218 if (ast.startToken) {
219 _tk.after(ast.startToken, br);
221 ast.startToken = ast.endToken = br;
223 } while (--expected);