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 }