(position: number)
| 281 | } |
| 282 | |
| 283 | private readName(position: number): NameToken { |
| 284 | // Skip the leading / |
| 285 | this.scanner.advance(); |
| 286 | |
| 287 | const data = this.scanner.bytes; |
| 288 | const start = this.scanner.position; |
| 289 | let pos = start; |
| 290 | const len = data.length; |
| 291 | |
| 292 | // Fast path: scan for end of name, checking for # escapes |
| 293 | let hasEscape = false; |
| 294 | |
| 295 | while (pos < len) { |
| 296 | const byte = data[pos]; |
| 297 | |
| 298 | if (!isRegularChar(byte)) { |
| 299 | break; |
| 300 | } |
| 301 | |
| 302 | if (byte === CHAR_HASH) { |
| 303 | hasEscape = true; |
| 304 | break; |
| 305 | } |
| 306 | |
| 307 | pos++; |
| 308 | } |
| 309 | |
| 310 | if (!hasEscape) { |
| 311 | // Common case: pure ASCII name with no escapes. |
| 312 | // Build string directly from byte range — no intermediate array, |
| 313 | // no Uint8Array allocation, no TextDecoder. |
| 314 | this.scanner.moveTo(pos); |
| 315 | |
| 316 | let value = ""; |
| 317 | |
| 318 | for (let i = start; i < pos; i++) { |
| 319 | value += String.fromCharCode(data[i]); |
| 320 | } |
| 321 | |
| 322 | return { type: "name", value, position }; |
| 323 | } |
| 324 | |
| 325 | // Slow path: name contains # escapes, need byte-by-byte processing. |
| 326 | // Reset scanner to start and use the original accumulation approach. |
| 327 | this.scanner.moveTo(start); |
| 328 | |
| 329 | const bytes: number[] = []; |
| 330 | |
| 331 | while (true) { |
| 332 | const byte = this.scanner.peek(); |
| 333 | |
| 334 | if (!isRegularChar(byte)) { |
| 335 | break; |
| 336 | } |
| 337 | |
| 338 | this.scanner.advance(); |
| 339 | |
| 340 | // Handle # hex escape |
no test coverage detected