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 }