From ccb0a7a3297c6d898091cab61bb96b2b5eedacd0 Mon Sep 17 00:00:00 2001 From: Felix Boehm <188768+fb55@users.noreply.github.com> Date: Sat, 3 Oct 2026 22:55:44 +0100 Subject: [PATCH 1/2] fix(parser): count chunks before invoking tokenizer callbacks Co-Authored-By: Claude Fable 5.1 --- src/Parser.spec.ts | 17 +++++++++++++++++ src/Parser.ts | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Parser.spec.ts b/src/Parser.spec.ts index 87a2fd2b..3be6304a 100644 --- a/src/Parser.spec.ts +++ b/src/Parser.spec.ts @@ -334,6 +334,23 @@ describe("API", () => { expect(events).toStrictEqual(["×", "bar", "end"]); }); + it("should parse chunks queued after reset in a callback", () => { + const texts: string[] = []; + const parser = new Parser({ + ontext(text) { + texts.push(text); + if (text === "first") parser.reset(); + else if (text === "second") parser.pause(); + }, + }); + parser.write("first"); + parser.write("second"); + parser.write("third"); + parser.resume(); + parser.end(); + expect(texts).toStrictEqual(["first", "second", "third"]); + }); + it("should support custom tokenizer", () => { class CustomTokenizer extends Tokenizer {} diff --git a/src/Parser.ts b/src/Parser.ts index 84581c4f..1c32a925 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -790,8 +790,9 @@ export class Parser implements Callbacks { this.buffers.push(chunk); if (this.tokenizer.running) { - this.tokenizer.write(chunk); + // Count the chunk first, as a callback may call `reset()`. this.writeIndex++; + this.tokenizer.write(chunk); } } From 35ba8a26b4e05ede4c6309b28cd29e5fa1415f78 Mon Sep 17 00:00:00 2001 From: Felix Boehm <188768+fb55@users.noreply.github.com> Date: Sat, 3 Oct 2026 23:14:18 +0100 Subject: [PATCH 2/2] fix(parser): stop closing a stack reset by a callback Use the matched element's position as the loop bound, so a reset inside a close callback ends the loop without an extra condition per implied close. Co-Authored-By: Claude Fable 5.1 --- src/Parser.spec.ts | 12 ++++++++++++ src/Parser.ts | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Parser.spec.ts b/src/Parser.spec.ts index 3be6304a..e159a100 100644 --- a/src/Parser.spec.ts +++ b/src/Parser.spec.ts @@ -351,6 +351,18 @@ describe("API", () => { expect(texts).toStrictEqual(["first", "second", "third"]); }); + it("should stop closing elements after reset in onclosetag", () => { + const closed: string[] = []; + const parser = new Parser({ + onclosetag(name) { + closed.push(name); + if (name === "c") parser.reset(); + }, + }); + parser.write(""); + expect(closed).toStrictEqual(["c"]); + }); + it("should support custom tokenizer", () => { class CustomTokenizer extends Tokenizer {} diff --git a/src/Parser.ts b/src/Parser.ts index 1c32a925..58fff47e 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -553,10 +553,11 @@ export class Parser implements Callbacks { if (!this.isVoidElement(name)) { const pos = this.stack.lastIndexOf(name); if (pos !== -1) { - for (let index = this.stack.length - 1; index > pos; index--) { + // A callback may have reset the stack. + while (this.stack.length > pos + 1) { this.popElement(true); } - this.popElement(false); + if (this.stack.length === pos + 1) this.popElement(false); } else if (this.htmlMode && name === "p") { // Implicit open before close this.emitOpenTag("p");