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 
28 private import temple.delims;
29 
30 package:
31 
32 bool validBeforeShort(string str) {
33 	// Check that the tail of str is whitespace
34 	// before a newline, or nothing.
35 	foreach_reverse(dchar chr; str) {
36 		if(chr == '\n') { return true; }
37 		if(!chr.isWhite()) { return false; }
38 	}
39 	return true;
40 }
41 
42 unittest
43 {
44 	static assert("   ".validBeforeShort() == true);
45 	static assert(" \t".validBeforeShort() == true);
46 	static assert("foo\n".validBeforeShort() == true);
47 	static assert("foo\n  ".validBeforeShort() == true);
48 	static assert("foo\n  \t".validBeforeShort() == true);
49 
50 	static assert("foo  \t".validBeforeShort() == false);
51 	static assert("foo".validBeforeShort() == false);
52 	static assert("\nfoo".validBeforeShort() == false);
53 }
54 
55 void munchHeadOf(ref string a, ref string b, size_t amt) {
56 	// Transfers amt of b's head onto a's tail
57 	a = a ~ b[0..amt];
58 	b = b[amt..$];
59 }
60 
61 unittest
62 {
63 	auto a = "123";
64 	auto b = "abc";
65 	a.munchHeadOf(b, 1);
66 	assert(a == "123a");
67 	assert(b == "bc");
68 }
69 unittest
70 {
71 	auto a = "123";
72 	auto b = "abc";
73 	a.munchHeadOf(b, b.length);
74 	assert(a == "123abc");
75 	assert(b == "");
76 }
77 
78 /// Returns the next matching delimeter in 'delims' found in the haystack,
79 /// or null
80 DelimPos!(D)* nextDelim(D)(string haystack, const(D)[] delims)
81 if(is(D : Delim))
82 {
83 
84 	alias Tuple!(Delim, "delim", string, "str") DelimStrPair;
85 
86 	/// The foreach is there to get around some DMD bugs
87 	/// Preferrably, one of the next two lines would be used instead
88 	//auto delims_strs =      delims.map!(a => new DelimStrPair(a, a.toString()) )().array();
89 	//auto delim_strs  = delims_strs.map!(a => a.str)().array();
90 	DelimStrPair[] delims_strs;
91 	foreach(delim; delims)
92 	{
93 		delims_strs ~= DelimStrPair(delim, toString(delim));
94 	}
95 
96 	// Map delims into their string representations
97 	// e.g. OpenDelim.OpenStr => `<%=`
98 	string[] delim_strs;
99 	foreach(delim; delims)
100 	{
101 		// Would use ~= here, but CTFE in 2.063 can't handle it
102 		delim_strs = delim_strs ~ toString(delim);
103 	}
104 
105 	// Find the first occurance of any of the delimers in the haystack
106 	immutable atPos = countUntilAny(haystack, delim_strs);
107 	if(atPos == -1)
108 	{
109 		return null;
110 	}
111 
112 	// Jump to where the delim is on haystack
113 	haystack = haystack[atPos .. $];
114 
115 	// Make sure that we match the longest of the delimers first,
116 	// e.g. `<%=` is matched before `<%`
117 	// Think of this as laxy lexing for maximal munch.
118 	auto sorted = delims_strs.sort!("a.str.length > b.str.length")();
119 	foreach(s; sorted)
120 	{
121 		if(startsWith(haystack, s.str))
122 		{
123 			return new DelimPos!D(atPos, cast(D) s.delim);
124 		}
125 	}
126 
127 	// invariant
128 	assert(false, "Internal bug");
129 }
130 
131 unittest
132 {
133 	const haystack = Delim.Open.toString();
134 	static assert(*(haystack.nextDelim([Delim.Open])) == DelimPos!Delim(0, Delim.Open));
135 }
136 unittest
137 {
138 	const haystack = "foo";
139 	static assert(haystack.nextDelim([Delim.Open]) is null);
140 }
141 
142 /// Returns the location of the first occurance of any of 'subs' found in
143 /// haystack, or -1 if none are found
144 ptrdiff_t countUntilAny(string haystack, string[] subs) {
145 	// First, calculate first occurance for all subs
146 	auto indexes_of = subs.map!( sub => haystack.countUntil(sub) );
147 	ptrdiff_t min_index = -1;
148 
149 	// Then find smallest index that isn't -1
150 	foreach(index_of; indexes_of)
151 	{
152 		if(index_of != -1)
153 		{
154 			if(min_index == -1)
155 			{
156 				min_index = index_of;
157 			}
158 			else
159 			{
160 				min_index = min(min_index, index_of);
161 			}
162 		}
163 	}
164 
165 	return min_index;
166 }
167 unittest
168 {
169 	enum a = "1, 2, 3, 4";
170 	static assert(a.countUntilAny(["1", "2"]) == 0);
171 	static assert(a.countUntilAny(["2", "1"]) == 0);
172 	static assert(a.countUntilAny(["4", "2"]) == 3);
173 }
174 unittest
175 {
176 	enum a = "1, 2, 3, 4";
177 	static assert(a.countUntilAny(["5", "1"]) == 0);
178 	static assert(a.countUntilAny(["5", "6"]) == -1);
179 }
180 unittest
181 {
182 	enum a = "%>";
183 	static assert(a.countUntilAny(["<%", "<%="]) == -1);
184 }
185 
186 string escapeQuotes(string unclean)
187 {
188 	unclean = unclean.replace(`"`, `\"`);
189 	unclean = unclean.replace(`'`, `\'`);
190 	return unclean;
191 }
192 unittest
193 {
194 	static assert(escapeQuotes(`"`) == `\"`);
195 	static assert(escapeQuotes(`'`) == `\'`);
196 }
197 
198 // Internal, inefficiant function for removing the whitespace from
199 // a string (for comparing that templates generate the same output,
200 // ignoring whitespace exactnes)
201 string stripWs(string unclean) {
202 	return unclean
203 	.filter!(a => !isWhite(a) )
204 	.map!( a => cast(char) a )
205 	.array
206 	.idup;
207 }
208 unittest
209 {
210 	static assert(stripWs("") == "");
211 	static assert(stripWs("    \t") == "");
212 	static assert(stripWs(" a s d f ") == "asdf");
213 	static assert(stripWs(" a\ns\rd f ") == "asdf");
214 }
215 
216 // Checks if haystack ends with needle, ignoring the whitespace in either
217 // of them
218 bool endsWithIgnoreWhitespace(string haystack, string needle)
219 {
220 	haystack = haystack.stripWs;
221 	needle   = needle.stripWs;
222 
223 	return haystack.endsWith(needle);
224 }
225 
226 unittest
227 {
228 	static assert(endsWithIgnoreWhitespace(")   {  ", "){"));
229 	static assert(!endsWithIgnoreWhitespace(")   {}", "){"));
230 }
231 
232 bool startsWithBlockClose(string haystack)
233 {
234 	haystack = haystack.stripWs;
235 
236 	// something that looks like }<something>); passes this
237 	if(haystack.startsWith("}") && haystack.canFind(");")) return true;
238 	return false;
239 }
240 
241 unittest
242 {
243 	static assert(startsWithBlockClose(`}, 10);`));
244 	static assert(startsWithBlockClose(`});`));
245 	static assert(startsWithBlockClose(`}, "foo");`));
246 	static assert(startsWithBlockClose(`}); auto a = "foo";`));
247 
248 	static assert(!startsWithBlockClose(`if() {}`));
249 	static assert(!startsWithBlockClose(`};`));
250 }
251 
252 bool isBlockStart(string haystack)
253 {
254 	return haystack.endsWithIgnoreWhitespace("){");
255 }
256 
257 bool isBlockEnd(string haystack)
258 {
259 	return haystack.startsWithBlockClose();
260 }