Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Parser.events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,15 @@ describe("Events", () => {
'<Page\n title="Hello world"\n actionBarVisible="false"/>',
{ xmlMode: true },
));

it("openImpliesClose=false: no implicit close via map", () =>
runTest("<p>first<p>second</p></p>", { openImpliesClose: false }));

it("openImpliesClose=false: no implicit p open before close", () =>
runTest("<div>text</p></div>", { openImpliesClose: false }));

it("openImpliesClose=false: no implicit br open before close", () =>
runTest("<div>text</br></div>", { openImpliesClose: false }));
});

describe("Helper", () => {
Expand Down
25 changes: 20 additions & 5 deletions src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const tableSectionTags = new Set(["thead", "tbody", "tfoot", "tr", "td", "th"]);
const ddtTags = new Set(["dd", "dt"]);
const rtpTags = new Set(["rt", "rp"]);

const openImpliesClose = new Map<string, Set<string>>([
const impliesCloseMap = new Map<string, Set<string>>([
["tr", new Set(["tr", "th", "td"])],
["th", new Set(["th", "td"])],
["td", new Set(["thead", "th", "td"])],
Expand Down Expand Up @@ -208,6 +208,18 @@ export interface ParserOptions {
* Allows the default tokenizer to be overwritten.
*/
Tokenizer?: typeof Tokenizer;

/**
* If set to `true`, the parser will implicitly open and close HTML tags
* according to the HTML5 spec (e.g. `<p>` will implicitly close another
* `<p>` that is still open, `</p>` will implicitly open `<p>` 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;
}

/**
Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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;
Expand Down Expand Up @@ -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])) {
Expand Down Expand Up @@ -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);
Expand Down
155 changes: 155 additions & 0 deletions src/__snapshots__/Parser.events.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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`] = `
[
{
Expand Down