diff --git a/src/Parser.events.spec.ts b/src/Parser.events.spec.ts
index 40c4c85e0..1abee6fd9 100644
--- a/src/Parser.events.spec.ts
+++ b/src/Parser.events.spec.ts
@@ -301,6 +301,15 @@ describe("Events", () => {
'
first
second
", { openImpliesClose: false })); + + it("openImpliesClose=false: no implicit p open before close", () => + runTest("` will implicitly close another + * `
` that is still open, `
` will implicitly open `` if none is + * open). Set to `false` to disable all implicit HTML changes, preserving + * the original document structure more closely. + * + * This option has no effect when `xmlMode` is `true`. + * @default !xmlMode + */ + openImpliesClose?: boolean; } /** @@ -280,6 +292,8 @@ export class Parser implements Callbacks { private readonly recognizeSelfClosing: boolean; /** We are parsing HTML. Inverse of the `xmlMode` option. */ private readonly htmlMode: boolean; + /** Whether implicit open/close logic is active (requires HTML mode + openImpliesClose option). */ + private readonly impliesCloseEnabled: boolean; private readonly tokenizer: Tokenizer; private readonly buffers: string[] = []; @@ -295,6 +309,7 @@ export class Parser implements Callbacks { ) { this.cbs = cbs ?? {}; this.htmlMode = !this.options.xmlMode; + this.impliesCloseEnabled = this.htmlMode && (options.openImpliesClose ?? this.htmlMode); this.lowerCaseTagNames = options.lowerCaseTags ?? this.htmlMode; this.lowerCaseAttributeNames = options.lowerCaseAttributeNames ?? this.htmlMode; @@ -410,12 +425,12 @@ export class Parser implements Callbacks { * stays null so endOpenTag is a no-op, and closeCurrentTag can't * match "" on the stack. */ - if (this.htmlMode && name === "form" && this.stack.includes("form")) { + if (this.impliesCloseEnabled && name === "form" && this.stack.includes("form")) { this.tagname = ""; return; } - const impliesClose = this.htmlMode && openImpliesClose.get(name); + const impliesClose = this.impliesCloseEnabled && impliesCloseMap.get(name); if (impliesClose) { while (this.stack.length > 0 && impliesClose.has(this.stack[0])) { @@ -481,12 +496,12 @@ export class Parser implements Callbacks { this.popElement(true); } this.popElement(false); - } else if (this.htmlMode && name === "p") { + } else if (this.impliesCloseEnabled && name === "p") { // Implicit open before close this.emitOpenTag("p"); this.closeCurrentTag(true); } - } else if (this.htmlMode && name === "br") { + } else if (this.impliesCloseEnabled && name === "br") { // We can't use `emitOpenTag` for implicit open, as `br` would be implicitly closed. this.cbs.onopentagname?.("br"); this.cbs.onopentag?.("br", {}, true); diff --git a/src/__snapshots__/Parser.events.spec.ts.snap b/src/__snapshots__/Parser.events.spec.ts.snap index 686fcafcc..33a711b7a 100644 --- a/src/__snapshots__/Parser.events.spec.ts.snap +++ b/src/__snapshots__/Parser.events.spec.ts.snap @@ -5304,6 +5304,161 @@ exports[`Events > open-implies-close case of (non-br) void close tag in non-XML ] `; +exports[`Events > openImpliesClose=false: no implicit br open before close 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "div", + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "div", + {}, + false, + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "text", + "data": [ + "text", + ], + "endIndex": 8, + "startIndex": 5, + }, + { + "$event": "closetag", + "data": [ + "div", + false, + ], + "endIndex": 19, + "startIndex": 14, + }, +] +`; + +exports[`Events > openImpliesClose=false: no implicit close via map 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "p", + ], + "endIndex": 2, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "p", + {}, + false, + ], + "endIndex": 2, + "startIndex": 0, + }, + { + "$event": "text", + "data": [ + "first", + ], + "endIndex": 7, + "startIndex": 3, + }, + { + "$event": "opentagname", + "data": [ + "p", + ], + "endIndex": 10, + "startIndex": 8, + }, + { + "$event": "opentag", + "data": [ + "p", + {}, + false, + ], + "endIndex": 10, + "startIndex": 8, + }, + { + "$event": "text", + "data": [ + "second", + ], + "endIndex": 16, + "startIndex": 11, + }, + { + "$event": "closetag", + "data": [ + "p", + false, + ], + "endIndex": 20, + "startIndex": 17, + }, + { + "$event": "closetag", + "data": [ + "p", + false, + ], + "endIndex": 24, + "startIndex": 21, + }, +] +`; + +exports[`Events > openImpliesClose=false: no implicit p open before close 1`] = ` +[ + { + "$event": "opentagname", + "data": [ + "div", + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "opentag", + "data": [ + "div", + {}, + false, + ], + "endIndex": 4, + "startIndex": 0, + }, + { + "$event": "text", + "data": [ + "text", + ], + "endIndex": 8, + "startIndex": 5, + }, + { + "$event": "closetag", + "data": [ + "div", + false, + ], + "endIndex": 18, + "startIndex": 13, + }, +] +`; + exports[`Events > plaintext 1`] = ` [ {