* Process a chunk of XML text using callbacks. * * This method is synchronous and can be called multiple times with * consecutive chunks of XML input. Callbacks are invoked for each * token, enabling zero-allocation streaming.
(chunk: string, callbacks: XmlTokenCallbacks)
| 1276 | * token, enabling zero-allocation streaming. |
| 1277 | */ |
| 1278 | process(chunk: string, callbacks: XmlTokenCallbacks): void { |
| 1279 | this.#callbacks = callbacks; |
| 1280 | this.#savePartialsBeforeReset(); |
| 1281 | const normalized = this.#normalizeLineEndings(chunk); |
| 1282 | // The main loop always fully consumes the buffer (bufferIndex reaches |
| 1283 | // buffer.length), so the new chunk is assigned directly — no leftover |
| 1284 | // to slice or concatenate. |
| 1285 | this.#buffer = normalized; |
| 1286 | this.#bufferIndex = 0; |
| 1287 | |
| 1288 | // Cache hot variables locally to reduce private field access overhead. |
| 1289 | const buffer = normalized; |
| 1290 | const bufferLen = buffer.length; |
| 1291 | |
| 1292 | // Check for BOM at the very first character (for XML declaration position check) |
| 1293 | if (!this.#checkedFirstChar && bufferLen > 0) { |
| 1294 | this.#checkedFirstChar = true; |
| 1295 | if (buffer.charCodeAt(0) === 0xFEFF) { |
| 1296 | this.#firstCharWasBOM = true; |
| 1297 | } |
| 1298 | } |
| 1299 | |
| 1300 | while (this.#bufferIndex < bufferLen) { |
| 1301 | // Use charCodeAt for faster character comparison in hot path |
| 1302 | const code = buffer.charCodeAt(this.#bufferIndex); |
| 1303 | |
| 1304 | // Switch cases ordered by frequency for better branch prediction. |
| 1305 | switch (this.#state) { |
| 1306 | // === HOT PATH: Most frequently hit states === |
| 1307 | |
| 1308 | case State.INITIAL: { |
| 1309 | // Use dedicated capture method for tight-loop text scanning |
| 1310 | if (this.#captureText(buffer, bufferLen)) { |
| 1311 | // Found '<' - flush text and transition to TAG_OPEN |
| 1312 | this.#flushText(); |
| 1313 | this.#saveTokenPosition(); |
| 1314 | this.#advanceWithCode(CC_LT); |
| 1315 | this.#state = State.TAG_OPEN; |
| 1316 | } |
| 1317 | // If captureText returns false, we've consumed all input |
| 1318 | // and will exit the main loop naturally |
| 1319 | break; |
| 1320 | } |
| 1321 | |
| 1322 | case State.TAG_NAME: { |
| 1323 | // Use dedicated capture method for tight-loop name scanning |
| 1324 | this.#captureNameChars(buffer, bufferLen); |
| 1325 | |
| 1326 | // Check what character ended the name (if any) |
| 1327 | if (this.#bufferIndex >= bufferLen) { |
| 1328 | // End of buffer - need more data, stay in TAG_NAME state |
| 1329 | break; |
| 1330 | } |
| 1331 | |
| 1332 | // Get the terminating character |
| 1333 | const termCode = buffer.charCodeAt(this.#bufferIndex); |
| 1334 | if (this.#isWhitespaceCode(termCode)) { |
| 1335 | this.#callbacks.onStartTagOpen?.( |
no test coverage detected