1 module fmt.editorconfig;
2 
3 import std;
4 import fmt.formatter;
5 
6 class Option {
7     string pattern;
8     Formatter.Option option;
9     this(string pattern, Formatter.Option option) {
10         this.pattern = pattern;
11         this.option = option;
12     }
13 }
14 
15 struct EditorConfig {
16     bool root;
17     Option[] options;
18 
19     Formatter.Option opIndex(string pattern) {
20         return options.find!(op => op.pattern == pattern).front.option;
21     }
22 
23     Nullable!(Formatter.Option) find(string fileName) {
24         foreach_reverse(op; options) {
25             if (sectionMatch(fileName, op.pattern))
26                 return op.option.nullable;
27         }
28         return typeof(return).init;
29     }
30 }
31 
32 EditorConfig loadConfig(string fileContent) {
33     EditorConfig result;
34     Option option;
35     foreach (line; fileContent.split("\n")) {
36         if (line.isBlank) continue;
37         if (line.isComment) continue;
38         if (line.isSectionHeader) {
39             auto sectionHeader = line.getSectionHeader();
40             option =  new Option(sectionHeader, Formatter.Option.init);
41             result.options ~= option;
42             continue;
43         }
44         if (line.isKeyValuePair) {
45             auto key = line.getKey();
46             auto value = line.getValue();
47             enforce(key == "root" || option, "Section header is not specified.");
48             switch (key) {
49                 case "brace_style":
50                     option.option.braceStyle = value.to!(Formatter.BraceStyle);
51                     continue;
52                 case "indent_style":
53                     option.option.indentStyle = value.to!(Formatter.IndentStyle);
54                     continue;
55                 case "indent_size":
56                     option.option.indentSize = value.to!size_t;
57                     continue;
58                 case "end_of_line":
59                     option.option.eol = value.to!(Formatter.EOL);
60                     continue;
61                 case "root":
62                     result.root = value.to!bool;
63                     continue;
64                 default:
65                     continue;
66             }
67         }
68         enforce(false, "Invalid line: " ~ line);
69     }
70     return result;
71 }
72 
73 bool sectionMatch(string fileName, string sectionHeader) {
74     auto pattern = sectionHeader
75         .replaceAll(ctRegex!`\{\d+\.\.\d+\}`, "(\\d+)")
76         .replace("?", ".")
77         .replace(".", "\\.")
78         .replace("**", ".__star__")
79         .replace("*", "[^/]*")
80         .replace("__star__", "*")
81         .replaceAll(ctRegex!`\{.*?\}`, "(.*)");
82 
83     auto r = fileName.matchAll(regex(pattern));
84     if (!r) return false;
85     auto matchResult = r.front.map!(to!string).array;
86     if (matchResult.empty) return false;
87     if (matchResult.length == 1) return true;
88     string[] originalPatterns = sectionHeader.matchAll(ctRegex!`\{(.*?)\}`).front.map!(to!string).array;
89 
90     foreach (originalPattern, filePart; zip(originalPatterns[1..$], matchResult[1..$])) {
91         if (originalPattern.canFind("..")) {
92             try {
93                 auto range = originalPattern.split("..").to!(int[]);
94                 if (range.length != 2) return false;
95                 if (!(range[0] <= filePart.to!int && filePart.to!int <= range[1])) return false;
96             } catch(ConvException) {
97                 return false;
98             }
99         } else {
100             if (!originalPattern.split(",").canFind(filePart)) return false;
101         }
102     }
103     return true;
104 }
105 
106 private {
107     bool isBlank(string line) {
108         return line.chomp == "";
109     }
110 
111     bool isComment(string line) {
112         return cast(bool)line.match(ctRegex!`^#`);
113     }
114 
115     bool isSectionHeader(string line) {
116         return cast(bool)line.match(ctRegex!` *\[.*\] *`);
117     }
118 
119     bool isKeyValuePair(string line) {
120         return cast(bool)line.match(ctRegex!`.*=.*`);
121     }
122 
123     string getSectionHeader(string line) {
124         return line.matchFirst(ctRegex!` *\[(.*)\] *`)[1];
125     }
126 
127     string getKey(string line) {
128         return line.split("=")[0].strip;
129     }
130 
131     string getValue(string line) {
132         return line.split("=")[1].strip;
133     }
134 }
135 
136 unittest {
137     auto configs = loadConfig(readText("test/.editorconfig"));
138     assert(configs.root is true);
139 
140     assert(configs["*"].eol == Formatter.EOL.lf);
141 
142     assert(configs["*.py"].indentStyle == Formatter.IndentStyle.space);
143     assert(configs["*.py"].indentSize == 4);
144 
145     assert(configs["Makefile"].indentStyle == Formatter.IndentStyle.tab);
146 
147     assert(configs["lib/**.js"].indentStyle == Formatter.IndentStyle.space);
148     assert(configs["lib/**.js"].indentSize == 2);
149 
150     assert(configs["{package.json,.travis.yml}"].indentStyle == Formatter.IndentStyle.space);
151     assert(configs["{package.json,.travis.yml}"].indentSize == 2);
152 }
153 
154 unittest {
155     assert( sectionMatch("poyo3.d", "*"));
156 
157     assert( sectionMatch("poyo3.d", "*.d"));
158     assert(!sectionMatch("poyo3.d", "*.py"));
159 
160     assert( sectionMatch("poyo3.d", "*.{js,d}"));
161     assert(!sectionMatch("poyo3.d", "*.{js,py}"));
162 
163     assert( sectionMatch("poyo3.d", "*{0..5}.{js,d}"));
164     assert(!sectionMatch("poyo3.d", "*{0..2}.{js,d}"));
165 
166     assert( sectionMatch("foo/poyo3.d", "foo/poyo3.d"));
167     assert(!sectionMatch("foo/poyo3.d", "foo/poyo2.d"));
168 
169     assert( sectionMatch("foo/poyo3.d", "{foo/poyo3.d,bar/poyo.d}"));
170     assert(!sectionMatch("foo/poyo3.d", "{foo/poyo2.d,bar/poyo3.d}"));
171 }