1 module workspaced.helpers;
2 
3 import std.ascii;
4 import std.string;
5 
6 string determineIndentation(scope const(char)[] code) @safe
7 {
8 	const(char)[] indent = null;
9 	foreach (line; code.lineSplitter)
10 	{
11 		if (line.strip.length == 0)
12 			continue;
13 		indent = line[0 .. $ - line.stripLeft.length];
14 	}
15 	return indent.idup;
16 }
17 
18 int stripLineEndingLength(scope const(char)[] code) @safe @nogc
19 {
20 	switch (code.length)
21 	{
22 		case 0:
23 			return 0;
24 		case 1:
25 			return code[0] == '\r' || code[0] == '\n' ? 1 : 0;
26 		default:
27 			if (code[$ - 2 .. $] == "\r\n")
28 				return 2;
29 			else if (code[$ - 1] == '\r' || code[$ - 1] == '\n')
30 				return 1;
31 			else
32 				return 0;
33 	}
34 }
35 
36 bool isIdentifierChar(dchar c) @safe @nogc
37 {
38 	return c.isAlphaNum || c == '_';
39 }
40 
41 ptrdiff_t indexOfKeyword(scope const(char)[] code, string keyword, ptrdiff_t start = 0) @safe @nogc
42 {
43 	ptrdiff_t index = start;
44 	while (true)
45 	{
46 		index = code.indexOf(keyword, index);
47 		if (index == -1)
48 			break;
49 
50 		if ((index > 0 && code[index - 1].isIdentifierChar)
51 				|| (index + keyword.length < code.length && code[index + keyword.length].isIdentifierChar))
52 		{
53 			index++;
54 			continue;
55 		}
56 		else
57 			break;
58 	}
59 	return index;
60 }
61 
62 bool endsWithKeyword(scope const(char)[] code, string keyword) @safe @nogc
63 {
64 	return code == keyword || (code.endsWith(keyword) && code[$ - 1 - keyword.length]
65 			.isIdentifierChar);
66 }
67 
68 bool isIdentifierSeparatingChar(dchar c) @safe @nogc
69 {
70 	return c < 48 || (c > 57 && c < 65) || c == '[' || c == '\\' || c == ']'
71 		|| c == '`' || (c > 122 && c < 128) || c == '\u2028' || c == '\u2029'; // line separators
72 }
73 
74 version (unittest)
75 {
76 	import std.json;
77 
78 	/// Iterates over all files in the given folder, reads them as D files until
79 	/// a __EOF__ token is encountered, then parses the following lines in this
80 	/// format per file:
81 	/// - If the line is empty or starts with `//` ignore it
82 	/// - If the line starts with `:` it's a variable assignment in form `:variable=JSON`
83 	/// - Otherwise it's a tab separated line like `1	2	3`
84 	/// Finally, it's tested that at least one test has been tested.
85 	void runTestDataFileTests(string dir,
86 		void delegate() onFileStart,
87 		void delegate(string code, string variable, JSONValue value) setVariable,
88 		void delegate(string code, string[] parts, string line) onTestLine,
89 		void delegate(string code) onFileFinished,
90 		string __file = __FILE__,
91 		size_t __line = __LINE__)
92 	{
93 		import core.exception;
94 		import std.algorithm;
95 		import std.array;
96 		import std.conv;
97 		import std.file;
98 		import std.stdio;
99 
100 		int noTested = 0;
101 		foreach (testFile; dirEntries(dir, SpanMode.shallow))
102 		{
103 			int lineNo = 0;
104 			try
105 			{
106 				auto testCode = appender!string;
107 				bool inCode = true;
108 				if (onFileStart)
109 					onFileStart();
110 				foreach (line; File(testFile, "r").byLine)
111 				{
112 					lineNo++;
113 					if (line == "__EOF__")
114 					{
115 						inCode = false;
116 						continue;
117 					}
118 
119 					if (inCode)
120 					{
121 						testCode ~= line;
122 						testCode ~= '\n'; // normalize CRLF to LF
123 					}
124 					else if (!line.length || line.startsWith("//"))
125 					{
126 						continue;
127 					}
128 					else if (line[0] == ':')
129 					{
130 						auto variable = line[1 .. $].idup.findSplit("=");
131 						if (setVariable)
132 							setVariable(testCode.data, variable[0], parseJSON(variable[2]));
133 					}
134 					else
135 					{
136 						if (onTestLine)
137 						{
138 							string lineDup = line.idup;
139 							onTestLine(testCode.data, lineDup.split("\t"), lineDup);
140 						}
141 					}
142 				}
143 
144 				if (onFileFinished)
145 					onFileFinished(testCode.data);
146 				noTested++;
147 			}
148 			catch (AssertError e)
149 			{
150 				e.file = __file;
151 				e.line = __line;
152 				e.msg = "in " ~ testFile ~ "(" ~ lineNo.to!string ~ "): " ~ e.msg;
153 				throw e;
154 			}
155 		}
156 
157 		assert(noTested > 0);
158 	}
159 }