* 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)
| 1227 | * token, enabling zero-allocation streaming. |
| 1228 | */ |
| 1229 | process(chunk: string, callbacks: XmlTokenCallbacks): void { |
| 1230 | this.#callbacks = callbacks; |
| 1231 | this.#savePartialsBeforeReset(); |
| 1232 | const normalized = this.#normalizeLineEndings(chunk); |
| 1233 | // The main loop always fully consumes the buffer (bufferIndex reaches |
| 1234 | // buffer.length), so the new chunk is assigned directly — no leftover |
| 1235 | // to slice or concatenate. |
| 1236 | this.#buffer = normalized; |
| 1237 | this.#bufferIndex = 0; |
| 1238 | |
| 1239 | // Cache hot variables locally to reduce private field access overhead. |
| 1240 | const buffer = normalized; |
| 1241 | const bufferLen = buffer.length; |
| 1242 | |
| 1243 | // Check for BOM at the very first character (for XML declaration position check) |
| 1244 | if (!this.#checkedFirstChar && bufferLen > 0) { |
| 1245 | this.#checkedFirstChar = true; |
| 1246 | if (buffer.charCodeAt(0) === 0xFEFF) { |
| 1247 | this.#firstCharWasBOM = true; |
| 1248 | } |
| 1249 | } |
| 1250 | |
| 1251 | while (this.#bufferIndex < bufferLen) { |
| 1252 | // Use charCodeAt for faster character comparison in hot path |
| 1253 | const code = buffer.charCodeAt(this.#bufferIndex); |
| 1254 | |
| 1255 | // Switch cases ordered by frequency for better branch prediction. |
| 1256 | switch (this.#state) { |
| 1257 | // === HOT PATH: Most frequently hit states === |
| 1258 | |
| 1259 | case State.INITIAL: { |
| 1260 | // Use dedicated capture method for tight-loop text scanning |
| 1261 | if (this.#captureText(buffer, bufferLen)) { |
| 1262 | // Found '<' - flush text and transition to TAG_OPEN |
| 1263 | this.#flushText(); |
| 1264 | this.#saveTokenPosition(); |
| 1265 | this.#advanceWithCode(CC_LT); |
| 1266 | this.#state = State.TAG_OPEN; |
| 1267 | } |
| 1268 | // If captureText returns false, we've consumed all input |
| 1269 | // and will exit the main loop naturally |
| 1270 | break; |
| 1271 | } |
| 1272 | |
| 1273 | case State.TAG_NAME: { |
| 1274 | // Use dedicated capture method for tight-loop name scanning |
| 1275 | this.#captureNameChars(buffer, bufferLen); |
| 1276 | |
| 1277 | // Check what character ended the name (if any) |
| 1278 | if (this.#bufferIndex >= bufferLen) { |
| 1279 | // End of buffer - need more data, stay in TAG_NAME state |
| 1280 | break; |
| 1281 | } |
| 1282 | |
| 1283 | // Get the terminating character |
| 1284 | const termCode = buffer.charCodeAt(this.#bufferIndex); |
| 1285 | if (this.#isWhitespaceCode(termCode)) { |
| 1286 | this.#callbacks.onStartTagOpen?.( |
no test coverage detected