1 /** 2 * Temple (C) Dylan Knutson, 2013, distributed under the: 3 * Boost Software License - Version 1.0 - August 17th, 2003 4 * 5 * Permission is hereby granted, free of charge, to any person or organization 6 * obtaining a copy of the software and accompanying documentation covered by 7 * this license (the "Software") to use, reproduce, display, distribute, 8 * execute, and transmit the Software, and to prepare derivative works of the 9 * Software, and to permit third-parties to whom the Software is furnished to 10 * do so, all subject to the following: 11 * 12 * The copyright notices in the Software and this entire statement, including 13 * the above license grant, this restriction and the following disclaimer, 14 * must be included in all copies of the Software, in whole or in part, and 15 * all derivative works of the Software, unless such copies or derivative 16 * works are solely in the form of machine-executable object code generated by 17 * a source language processor. 18 */ 19 20 module temple.util; 21 22 private import 23 std.algorithm, 24 std.typecons, 25 std.array, 26 std.uni, 27 std.conv, 28 std.utf; 29 30 private import temple.delims; 31 32 package: 33 34 bool validBeforeShort(string str) { 35 // Check that the tail of str is whitespace 36 // before a newline, or nothing. 37 foreach_reverse(dchar chr; str) { 38 if(chr == '\n') { return true; } 39 if(!chr.isWhite()) { return false; } 40 } 41 return true; 42 } 43 44 unittest 45 { 46 static assert(" ".validBeforeShort()); 47 static assert(" \t".validBeforeShort()); 48 static assert("foo\n".validBeforeShort()); 49 static assert("foo\n ".validBeforeShort()); 50 static assert("foo\n \t".validBeforeShort); 51 52 static assert("foo \t".validBeforeShort() == false); 53 static assert("foo".validBeforeShort() == false); 54 static assert("\nfoo".validBeforeShort() == false); 55 } 56 57 void munchHeadOf(ref string a, ref string b, size_t amt) { 58 // Transfers amt of b's head onto a's tail 59 a = a ~ b[0..amt]; 60 b = b[amt..$]; 61 } 62 63 unittest 64 { 65 auto a = "123"; 66 auto b = "abc"; 67 a.munchHeadOf(b, 1); 68 assert(a == "123a"); 69 assert(b == "bc"); 70 } 71 unittest 72 { 73 auto a = "123"; 74 auto b = "abc"; 75 a.munchHeadOf(b, b.length); 76 assert(a == "123abc"); 77 assert(b == ""); 78 } 79 80 /// Returns the next matching delimeter in 'delims' found in the haystack, 81 /// or null 82 DelimPos!(D)* nextDelim(D)(string haystack, const(D)[] delims) 83 if(is(D : Delim)) 84 { 85 struct DelimStrPair { Delim delim; string str; } 86 87 /// The foreach is there to get around some DMD bugs 88 /// Preferrably, one of the next two lines would be used instead 89 //auto delims_strs = delims.map!(a => new DelimStrPair(a, a.toString()) )().array(); 90 //auto delim_strs = delims_strs.map!(a => a.str)().array(); 91 DelimStrPair[] delims_strs; 92 foreach(delim; delims) 93 { 94 delims_strs ~= DelimStrPair(delim, toString(delim)); 95 } 96 97 // Map delims into their string representations 98 // e.g. OpenDelim.OpenStr => `<%=` 99 string[] delim_strs; 100 foreach(delim; delims) 101 { 102 // BUG: Would use ~= here, but CTFE in 2.063 can't handle it 103 delim_strs = delim_strs ~ toString(delim); 104 } 105 106 // Find the first occurance of any of the delimers in the haystack 107 immutable index = countUntilAny(haystack, delim_strs); 108 if(index == -1) 109 { 110 return null; 111 } 112 113 // Jump to where the delim is on haystack, using stride to handle 114 // unicode correctly 115 size_t pos = 0; 116 foreach(_; 0 .. index) { 117 auto size = stride(haystack, 0); 118 119 haystack = haystack[size .. $]; 120 pos += size; 121 } 122 123 // Make sure that we match the longest of the delimers first, 124 // e.g. `<%=` is matched before `<%` for maximal munch 125 auto sorted = delims_strs.sort!("a.str.length > b.str.length")(); 126 foreach(s; sorted) 127 { 128 if(startsWith(haystack, s.str)) 129 { 130 return new DelimPos!D(pos, cast(D) s.delim); 131 } 132 } 133 134 // invariant 135 assert(false, "internal bug: \natPos: " ~ index.to!string ~ "\nhaystack: " ~ haystack); 136 } 137 138 unittest 139 { 140 const haystack = "% Я"; 141 static assert(*(haystack.nextDelim([Delim.OpenShort])) == 142 DelimPos!Delim(0, Delim.OpenShort)); 143 } 144 unittest 145 { 146 const haystack = "Я"; 147 static assert(haystack.nextDelim([Delim.OpenShort]) == null); 148 } 149 unittest 150 { 151 const haystack = "Я%"; 152 static assert(*(haystack.nextDelim([Delim.OpenShort])) == 153 DelimPos!Delim(codeLength!char('Я'), Delim.OpenShort)); 154 } 155 unittest 156 { 157 const haystack = Delim.Open.toString(); 158 static assert(*(haystack.nextDelim([Delim.Open])) == DelimPos!Delim(0, Delim.Open)); 159 } 160 unittest 161 { 162 const haystack = "foo"; 163 static assert(haystack.nextDelim([Delim.Open]) is null); 164 } 165 166 /// Returns the location of the first occurance of any of 'subs' found in 167 /// haystack, or -1 if none are found 168 ptrdiff_t countUntilAny(string haystack, string[] subs) { 169 // First, calculate first occurance for all subs 170 auto indexes_of = subs.map!( sub => haystack.countUntil(sub) ); 171 ptrdiff_t min_index = -1; 172 173 // Then find smallest index that isn't -1 174 foreach(index_of; indexes_of) 175 { 176 if(index_of != -1) 177 { 178 if(min_index == -1) 179 { 180 min_index = index_of; 181 } 182 else 183 { 184 min_index = min(min_index, index_of); 185 } 186 } 187 } 188 189 return min_index; 190 } 191 unittest 192 { 193 enum a = "1, 2, 3, 4"; 194 static assert(a.countUntilAny(["1", "2"]) == 0); 195 static assert(a.countUntilAny(["2", "1"]) == 0); 196 static assert(a.countUntilAny(["4", "2"]) == 3); 197 } 198 unittest 199 { 200 enum a = "1, 2, 3, 4"; 201 static assert(a.countUntilAny(["5", "1"]) == 0); 202 static assert(a.countUntilAny(["5", "6"]) == -1); 203 } 204 unittest 205 { 206 enum a = "%>"; 207 static assert(a.countUntilAny(["<%", "<%="]) == -1); 208 } 209 210 string escapeQuotes(string unclean) 211 { 212 unclean = unclean.replace(`"`, `\"`); 213 unclean = unclean.replace(`'`, `\'`); 214 return unclean; 215 } 216 unittest 217 { 218 static assert(escapeQuotes(`"`) == `\"`); 219 static assert(escapeQuotes(`'`) == `\'`); 220 } 221 222 // Internal, inefficiant function for removing the whitespace from 223 // a string (for comparing that templates generate the same output, 224 // ignoring whitespace exactnes) 225 string stripWs(string unclean) { 226 return unclean 227 .filter!(a => !isWhite(a) ) 228 .map!( a => cast(char) a ) 229 .array 230 .idup; 231 } 232 unittest 233 { 234 static assert(stripWs("") == ""); 235 static assert(stripWs(" \t") == ""); 236 static assert(stripWs(" a s d f ") == "asdf"); 237 static assert(stripWs(" a\ns\rd f ") == "asdf"); 238 } 239 240 // Checks if haystack ends with needle, ignoring the whitespace in either 241 // of them 242 bool endsWithIgnoreWhitespace(string haystack, string needle) 243 { 244 haystack = haystack.stripWs; 245 needle = needle.stripWs; 246 247 return haystack.endsWith(needle); 248 } 249 250 unittest 251 { 252 static assert(endsWithIgnoreWhitespace(") { ", "){")); 253 static assert(!endsWithIgnoreWhitespace(") {}", "){")); 254 } 255 256 bool startsWithBlockClose(string haystack) 257 { 258 haystack = haystack.stripWs; 259 260 // something that looks like }<something>); passes this 261 if(haystack.startsWith("}") && haystack.canFind(");")) return true; 262 return false; 263 } 264 265 unittest 266 { 267 static assert(startsWithBlockClose(`}, 10);`)); 268 static assert(startsWithBlockClose(`});`)); 269 static assert(startsWithBlockClose(`}, "foo");`)); 270 static assert(startsWithBlockClose(`}); auto a = "foo";`)); 271 272 static assert(!startsWithBlockClose(`if() {}`)); 273 static assert(!startsWithBlockClose(`};`)); 274 } 275 276 bool isBlockStart(string haystack) 277 { 278 return haystack.endsWithIgnoreWhitespace("){"); 279 } 280 281 bool isBlockEnd(string haystack) 282 { 283 return haystack.startsWithBlockClose(); 284 }