1 | var os = require('os');
|
2 | var path = require('path');
|
3 | var fs = require('fs');
|
4 | var util = require('util');
|
5 | var Promise = require('bluebird');
|
6 | var whenReadFile = Promise.promisify(fs.readFile);
|
7 |
|
8 | var minimatch = require('./lib/fnmatch');
|
9 | var iniparser = require('./lib/ini');
|
10 | var Version = require('./lib/version');
|
11 | var pkg = require('./package.json');
|
12 |
|
13 | var knownProps = [
|
14 | 'end_of_line',
|
15 | 'indent_style',
|
16 | 'indent_size',
|
17 | 'insert_final_newline',
|
18 | 'trim_trailing_whitespace',
|
19 | 'charset'
|
20 | ].reduce(function (set, prop) {
|
21 | set[prop] = true;
|
22 | return set;
|
23 | }, {});
|
24 |
|
25 | function fnmatch(filepath, glob) {
|
26 | var matchOptions = {matchBase: true, dot: true, noext: true};
|
27 | glob = glob.replace(/\*\*/g, '{*,**/**/**}');
|
28 | return minimatch(filepath, glob, matchOptions);
|
29 | }
|
30 |
|
31 | function getConfigFileNames(filepath, options) {
|
32 | var paths = [];
|
33 | do {
|
34 | filepath = path.dirname(filepath);
|
35 | paths.push(path.join(filepath, options.config));
|
36 | } while (filepath !== options.root);
|
37 | return paths;
|
38 | }
|
39 |
|
40 | function getFilepathRoot(filepath) {
|
41 | if (path.parse !== undefined) {
|
42 |
|
43 | return path.parse(filepath).root;
|
44 | }
|
45 | if (os.platform() === 'win32') {
|
46 | return path.resolve(filepath).match(/^(\\\\[^\\]+\\)?[^\\]+\\/)[0];
|
47 | }
|
48 | return '/';
|
49 | }
|
50 |
|
51 | function processMatches(matches, version) {
|
52 |
|
53 |
|
54 | if ("indent_style" in matches && matches.indent_style === "tab" &&
|
55 | !("indent_size" in matches) && version.gte(new Version(0, 10))) {
|
56 | matches.indent_size = "tab";
|
57 | }
|
58 |
|
59 |
|
60 |
|
61 | if ("indent_size" in matches && !("tab_width" in matches) &&
|
62 | matches.indent_size !== "tab")
|
63 | matches.tab_width = matches.indent_size;
|
64 |
|
65 |
|
66 | if("indent_size" in matches && "tab_width" in matches &&
|
67 | matches.indent_size === "tab")
|
68 | matches.indent_size = matches.tab_width;
|
69 |
|
70 | return matches;
|
71 | }
|
72 |
|
73 | function processOptions(options, filepath) {
|
74 | options = options || {};
|
75 | return {
|
76 | config: options.config || '.editorconfig',
|
77 | version: new Version(options.version || pkg.version),
|
78 | root: path.resolve(options.root || getFilepathRoot(filepath))
|
79 | };
|
80 | }
|
81 |
|
82 | function buildFullGlob(pathPrefix, glob) {
|
83 | switch (glob.indexOf('/')) {
|
84 | case -1: glob = "**/" + glob; break;
|
85 | case 0: glob = glob.substring(1); break;
|
86 | }
|
87 | return path.join(pathPrefix, glob);
|
88 | }
|
89 |
|
90 | function extendProps(props, options) {
|
91 | for (var key in options) {
|
92 | var value = options[key];
|
93 | key = key.toLowerCase();
|
94 | if (knownProps[key]) {
|
95 | value = value.toLowerCase();
|
96 | }
|
97 | try {
|
98 | value = JSON.parse(value);
|
99 | } catch(e) {}
|
100 | if (typeof value === 'undefined' || value === null) {
|
101 |
|
102 |
|
103 | value = String(value);
|
104 | }
|
105 | props[key] = value;
|
106 | }
|
107 | return props;
|
108 | }
|
109 |
|
110 | function parseFromFiles(filepath, files, options) {
|
111 | return getConfigsForFiles(files).then(function (configs) {
|
112 | return configs.reverse();
|
113 | }).reduce(function (matches, file) {
|
114 | var pathPrefix = path.dirname(file.name);
|
115 | file.contents.forEach(function (section) {
|
116 | var glob = section[0], options = section[1];
|
117 | if (!glob) return;
|
118 | var fullGlob = buildFullGlob(pathPrefix, glob);
|
119 | if (!fnmatch(filepath, fullGlob)) return;
|
120 | matches = extendProps(matches, options);
|
121 | });
|
122 | return matches;
|
123 | }, {}).then(function (matches) {
|
124 | return processMatches(matches, options.version);
|
125 | });
|
126 | }
|
127 |
|
128 | function parseFromFilesSync(filepath, files, options) {
|
129 | var configs = getConfigsForFilesSync(files);
|
130 | configs.reverse();
|
131 | var matches = {};
|
132 | configs.forEach(function(config) {
|
133 | var pathPrefix = path.dirname(config.name);
|
134 | config.contents.forEach(function(section) {
|
135 | var glob = section[0], options = section[1];
|
136 | if (!glob) return;
|
137 | var fullGlob = buildFullGlob(pathPrefix, glob);
|
138 | if (!fnmatch(filepath, fullGlob)) return;
|
139 | matches = extendProps(matches, options);
|
140 | });
|
141 | });
|
142 | return processMatches(matches, options.version);
|
143 | }
|
144 |
|
145 | function StopReduce(array) {
|
146 | this.array = array;
|
147 | }
|
148 |
|
149 | StopReduce.prototype = Object.create(Error.prototype);
|
150 |
|
151 | function getConfigsForFiles(files) {
|
152 | return Promise.reduce(files, function (configs, file) {
|
153 | var contents = iniparser.parseString(file.contents);
|
154 | configs.push({
|
155 | name: file.name,
|
156 | contents: contents
|
157 | });
|
158 | if ((contents[0][1].root || '').toLowerCase() === 'true') {
|
159 | return Promise.reject(new StopReduce(configs));
|
160 | }
|
161 | return configs;
|
162 | }, []).catch(StopReduce, function (stop) {
|
163 | return stop.array;
|
164 | });
|
165 | }
|
166 |
|
167 | function getConfigsForFilesSync(files) {
|
168 | var configs = [];
|
169 | for (var i in files) {
|
170 | var file = files[i];
|
171 | var contents = iniparser.parseString(file.contents);
|
172 | configs.push({
|
173 | name: file.name,
|
174 | contents: contents
|
175 | });
|
176 | if ((contents[0][1].root || '').toLowerCase() === 'true') {
|
177 | break;
|
178 | }
|
179 | };
|
180 | return configs;
|
181 | }
|
182 |
|
183 | function readConfigFiles(filepaths) {
|
184 | return Promise.map(filepaths, function (path) {
|
185 | return whenReadFile(path, 'utf-8').catch(function () {
|
186 | return '';
|
187 | }).then(function (contents) {
|
188 | return {name: path, contents: contents};
|
189 | });
|
190 | });
|
191 | }
|
192 |
|
193 | function readConfigFilesSync(filepaths) {
|
194 | var files = [];
|
195 | var file;
|
196 | filepaths.forEach(function(filepath) {
|
197 | try {
|
198 | file = fs.readFileSync(filepath, 'utf8');
|
199 | } catch (e) {
|
200 | file = '';
|
201 | }
|
202 | files.push({name: filepath, contents: file});
|
203 | });
|
204 | return files;
|
205 | }
|
206 |
|
207 | module.exports.parseFromFiles = function (filepath, files, options) {
|
208 | return new Promise (function (resolve, reject) {
|
209 | filepath = path.resolve(filepath);
|
210 | options = processOptions(options, filepath);
|
211 | resolve(parseFromFiles(filepath, files, options));
|
212 | });
|
213 | };
|
214 |
|
215 | module.exports.parseFromFilesSync = function (filepath, files, options) {
|
216 | filepath = path.resolve(filepath);
|
217 | options = processOptions(options, filepath);
|
218 | return parseFromFilesSync(filepath, files, options);
|
219 | };
|
220 |
|
221 | module.exports.parse = function (filepath, options) {
|
222 | return new Promise (function (resolve, reject) {
|
223 | filepath = path.resolve(filepath);
|
224 | options = processOptions(options, filepath);
|
225 | var filepaths = getConfigFileNames(filepath, options);
|
226 | var files = readConfigFiles(filepaths);
|
227 | resolve(parseFromFiles(filepath, files, options));
|
228 | });
|
229 | };
|
230 |
|
231 | module.exports.parseSync = function (filepath, options) {
|
232 | filepath = path.resolve(filepath);
|
233 | options = processOptions(options, filepath);
|
234 | var filepaths = getConfigFileNames(filepath, options);
|
235 | var files = readConfigFilesSync(filepaths);
|
236 | return parseFromFilesSync(filepath, files, options);
|
237 | };
|