| 191 | } |
| 192 | |
| 193 | function _parseString(text, pos) { |
| 194 | const quote = text[pos]; |
| 195 | let i = pos + 1; |
| 196 | let out = ''; |
| 197 | while (i < text.length) { |
| 198 | const c = text[i]; |
| 199 | if (c === '\\') { |
| 200 | const next = text[i + 1]; |
| 201 | switch (next) { |
| 202 | case 'n': out += '\n'; i += 2; break; |
| 203 | case 't': out += '\t'; i += 2; break; |
| 204 | case 'r': out += '\r'; i += 2; break; |
| 205 | case '\\': out += '\\'; i += 2; break; |
| 206 | case "'": out += "'"; i += 2; break; |
| 207 | case '"': out += '"'; i += 2; break; |
| 208 | case '0': out += '\0'; i += 2; break; |
| 209 | case 'a': out += '\x07'; i += 2; break; |
| 210 | case 'b': out += '\b'; i += 2; break; |
| 211 | case 'f': out += '\f'; i += 2; break; |
| 212 | case 'v': out += '\v'; i += 2; break; |
| 213 | case 'x': { |
| 214 | const hex = text.slice(i + 2, i + 4); |
| 215 | if (/^[0-9a-fA-F]{2}$/.test(hex)) { |
| 216 | out += String.fromCharCode(parseInt(hex, 16)); |
| 217 | i += 4; |
| 218 | } else { out += next; i += 2; } |
| 219 | break; |
| 220 | } |
| 221 | case 'u': { |
| 222 | const hex = text.slice(i + 2, i + 6); |
| 223 | if (/^[0-9a-fA-F]{4}$/.test(hex)) { |
| 224 | out += String.fromCharCode(parseInt(hex, 16)); |
| 225 | i += 6; |
| 226 | } else { out += next; i += 2; } |
| 227 | break; |
| 228 | } |
| 229 | default: |
| 230 | // Unknown escape — preserve both chars (Python's behaviour for |
| 231 | // non-recognised escapes is to keep them literal). |
| 232 | out += '\\' + (next || ''); |
| 233 | i += 2; |
| 234 | } |
| 235 | continue; |
| 236 | } |
| 237 | if (c === quote) { |
| 238 | return { value: out, next: i + 1 }; |
| 239 | } |
| 240 | out += c; |
| 241 | i++; |
| 242 | } |
| 243 | return null; // unterminated |
| 244 | } |
| 245 | |
| 246 | function _parseList(text, pos) { |
| 247 | // Find matching ] |