1 module squelch.write;
2 
3 import std.algorithm.searching;
4 import std.array;
5 import std.stdio : File;
6 import std.string;
7 import std.sumtype : match;
8 
9 import ae.utils.array;
10 
11 import squelch.common;
12 
13 void save(Token[] tokens, File output)
14 {
15 	foreach (token; tokens)
16 	{
17 		token.match!(
18 			(ref TokenWhiteSpace t)
19 			{
20 				output.write(t.text);
21 			},
22 			(ref TokenComment t)
23 			{
24 				output.write("--", t.text.length ? " " : "", t.text);
25 			},
26 			(ref TokenKeyword t)
27 			{
28 				output.write(t.text);
29 			},
30 			(ref TokenIdentifier t)
31 			{
32 				output.write(encode(t.text, true));
33 			},
34 			(ref TokenNamedParameter t)
35 			{
36 				output.write(`@`, t.text);
37 			},
38 			(ref TokenOperator t)
39 			{
40 				output.write(t.text);
41 			},
42 			(ref TokenAngleBracket t)
43 			{
44 				output.write(t.text);
45 			},
46 			(ref TokenString t)
47 			{
48 				if (t.bytes)
49 					output.write('b');
50 				output.write(encode(t.text, false));
51 			},
52 			(ref TokenNumber t)
53 			{
54 				output.write(t.text);
55 			},
56 			(ref TokenDbtStatement t)
57 			{
58 				output.write("{%", t.text, "%}");
59 			},
60 			(ref TokenDbtComment t)
61 			{
62 				output.write("{#", t.text, "#}");
63 			},
64 		);
65 	}
66 }
67 
68 string encode(ref const scope DbtString str, bool identifier)
69 {
70 	// Try all encodings, and pick the shortest one.
71 	string bestEnc;
72 	foreach (quote; identifier ? ['\0', '`'] : ['\'', '"'])
73 		foreach (raw; quote ? [false, true] : [false])
74 		  encLoop:
75 			foreach (triple; quote ? [false, true] : [false])
76 			{
77 				import squelch.lex : isIdentifierStart, isIdentifierContinuation, keywords;
78 
79 				auto delimeter = replicate([DbtStringElem(quote)], quote ? triple ? 3 : 1 : 0);
80 				auto delimeterStr = delimeter.tryToString();
81 
82 				if (raw && str.canFind(delimeter))
83 					continue; // not representable in this encoding
84 
85 				string enc = "";
86 				if (raw)
87 					enc ~= 'r';
88 				enc ~= delimeterStr;
89 
90 				auto s = str[];
91 				while (s.length)
92 				{
93 					auto rest = s;
94 					bool ok = s.shift.match!(
95 						(dchar c)
96 						{
97 							if (!quote)
98 							{
99 								bool first = rest.length == str.length;
100 								if (c != cast(char)c)
101 									return false;
102 								bool ok = first
103 									? isIdentifierStart(cast(char)c)
104 									: isIdentifierContinuation(cast(char)c);
105 								if (!ok)
106 									return false;
107 							}
108 
109 							if (c == '\n')
110 							{
111 								if (triple)
112 								{
113 									enc ~= '\n';
114 									return true;
115 								}
116 								if (!raw)
117 								{
118 									enc ~= `\n`;
119 									return true;
120 								}
121 							}
122 
123 							if (raw && c < 0x20)
124 								return false;
125 
126 							if (!raw)
127 							{
128 								switch (c)
129 								{
130 									case '\a': enc ~= `\a`; return true;
131 									case '\b': enc ~= `\b`; return true;
132 									case '\f': enc ~= `\f`; return true;
133 									case '\n': enc ~= `\n`; return true;
134 									case '\r': enc ~= `\r`; return true;
135 									case '\t': enc ~= `\t`; return true;
136 									case '\v': enc ~= `\v`; return true;
137 									default:
138 										if (c < 0x20)
139 										{
140 											enc ~= format(`\x%02x`, uint(c));
141 											return true;
142 										}
143 										if ((delimeter.length && rest.startsWith(delimeter)) || c == '\\')
144 										{
145 											enc ~= '\\';
146 											enc ~= c;
147 											return true;
148 										}
149 								}
150 							}
151 
152 							enc ~= c;
153 							return true;
154 						},
155 						(DbtExpression e)
156 						{
157 							if (e.quoting != QuotingContext(quote, raw, triple))
158 								return false;
159 							enc ~= "{{" ~ e.expr ~ "}}";
160 							return true;
161 						}
162 					);
163 					if (!ok)
164 						continue encLoop;
165 				}
166 				enc ~= delimeterStr;
167 
168 				if (!quote)
169 				{
170 					if (enc.length == 0 || keywords.canFind!(kwd => kwd.icmp(enc) == 0))
171 						continue encLoop;
172 				}
173 
174 				if (!bestEnc || enc.length < bestEnc.length)
175 					bestEnc = enc;
176 			}
177 	assert(bestEnc, "Failed to encode string: " ~ format("%(%s%)", [str]));
178 	return bestEnc;
179 }