Skip to content

Commit d604652

Browse files
committed
feat: add preserveTagNesting option to maintain invalid HTML nesting #295
1 parent f7a190c commit d604652

File tree

3 files changed

+26
-2
lines changed

3 files changed

+26
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Parse the data provided, wrap the result in a new node, and return the root of t
8686
comment: false, // retrieve comments (hurts performance slightly)
8787
fixNestedATags: false, // fix invalid nested <a> HTML tags
8888
parseNoneClosedTags: false, // close none closed HTML tags instead of removing them
89+
preserveTagNesting: false, // preserve invalid HTML nesting instead of auto-closing tags (e.g. <p><p>bar</p></p>)
8990
voidTag: {
9091
tags: ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'], // optional and case insensitive, default value is ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']
9192
closingSlash: true // optional, default false. void tag serialisation, add a final slash <br/>

src/nodes/html.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,10 @@ export interface Options {
10291029
*/
10301030
fixNestedATags?: boolean;
10311031
parseNoneClosedTags?: boolean;
1032+
/**
1033+
* When true, preserves invalid HTML nesting (e.g., <p><p>bar</p></p>) instead of auto-closing tags
1034+
*/
1035+
preserveTagNesting?: boolean;
10321036
blockTextElements: {
10331037
[tag: string]: boolean;
10341038
};
@@ -1135,7 +1139,7 @@ export function base_parse(data: string, options = {} as Partial<Options>) {
11351139

11361140
const parentTagName = currentParent.rawTagName as IRawTagName;
11371141

1138-
if (!closingSlash && kElementsClosedByOpening[parentTagName]) {
1142+
if (!closingSlash && !options.preserveTagNesting && kElementsClosedByOpening[parentTagName]) {
11391143
if (kElementsClosedByOpening[parentTagName][tagName]) {
11401144
stack.pop();
11411145
currentParent = arr_back(stack);
@@ -1216,7 +1220,7 @@ export function base_parse(data: string, options = {} as Partial<Options>) {
12161220
if (
12171221
possibleContainer &&
12181222
possibleContainer.rawTagName &&
1219-
possibleContainer.rawTagName.toLowerCase() === closingTag &&
1223+
possibleContainer.rawTagName.toLowerCase() === closingTag &&
12201224
!kElementsClosedByClosingExcept[openTag][closingTag]
12211225
) {
12221226
// Update range end for closed tag

test/tests/issues/295.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const { parse } = require('@test/test-target');
2+
3+
describe('issue 295 Invalid HTML element nesting', function () {
4+
it('Valid HTML nesting kept', function () {
5+
// valid HTML - nesting kept
6+
const root = parse(`<div>foo<div>bar</div></div>`);
7+
root.outerHTML.should.equal('<div>foo<div>bar</div></div>');
8+
});
9+
it('Valid HTML p nested inside of p', function () {
10+
// invalid HTML (p nested inside of p) - nesting changed
11+
const root = parse(`<p>foo<p>bar</p></p>`, { preserveTagNesting: true });
12+
root.outerHTML.should.equal('<p>foo<p>bar</p></p>');
13+
});
14+
it('Valid HTML ul nested inside of p', function () {
15+
// invalid HTML (ul nested inside of p) - nesting kept
16+
const root = parse(`<p>foo<ul><li>bar</li></ul></p>`);
17+
root.outerHTML.should.equal('<p>foo<ul><li>bar</li></ul></p>');
18+
});
19+
});

0 commit comments

Comments
 (0)