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 }