From 257c76b6e2ea9e6e59ea14dd66432544216ba8c1 Mon Sep 17 00:00:00 2001 From: deadstrobe5 Date: Mon, 9 Nov 2026 22:56:10 +0000 Subject: [PATCH] perf(tokenizer): skip runs of text in one loop when decoding entities --- src/Tokenizer.spec.ts | 21 +++++++++++++++++++++ src/Tokenizer.ts | 16 ++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Tokenizer.spec.ts b/src/Tokenizer.spec.ts index e596b976..f259c1a2 100644 --- a/src/Tokenizer.spec.ts +++ b/src/Tokenizer.spec.ts @@ -191,6 +191,27 @@ describe("Tokenizer", () => { it("for multi-byte entities", () => expect(tokenize("≧̸")).toMatchSnapshot()); + + it("for text and entities split across chunks", () => + expect( + tokenize((tokenizer) => { + tokenizer.write("one & two & three"); + tokenizer.write(" fourfivesix <"); + tokenizer.end(); + }), + ).toEqual([ + ["ontext", 0, 10], + ["ontextentity", 38, 15], + ["ontext", 15, 21], + ["ontext", 21, 26], + ["onopentagname", 27, 28], + ["onopentagend", 28], + ["ontext", 29, 33], + ["onclosetag", 35, 36], + ["ontext", 37, 41], + ["ontextentity", 60, 44], + ["onend"], + ])); }); it("should close comments on --!>", () => { diff --git a/src/Tokenizer.ts b/src/Tokenizer.ts index 6f983e1b..08a0933a 100644 --- a/src/Tokenizer.ts +++ b/src/Tokenizer.ts @@ -247,8 +247,20 @@ export default class Tokenizer { } this.state = State.BeforeTagName; this.sectionStart = this.index; - } else if (this.decodeEntities && c === CharCodes.Amp) { - this.startEntity(); + } else if (this.decodeEntities) { + if (c === CharCodes.Amp) { + this.startEntity(); + return; + } + // Skip to the next `<` or `&`, or the end of the buffer. + const { buffer, offset } = this; + let index = this.index - offset + 1; + while (index < buffer.length) { + const next = buffer.charCodeAt(index); + if (next === CharCodes.Lt || next === CharCodes.Amp) break; + index++; + } + this.index = index + offset - 1; } }