Skip to content
Merged
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
65 changes: 48 additions & 17 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,57 @@ function isWhitespaceTextNode(node) {
}

export const stripWrappingParagraphs = (html) => {
const parsedHtml = parse5.parseFragment(html);
parsedHtml.childNodes = parsedHtml.childNodes.flatMap((node) => {
if (node.nodeName !== 'p') {
return node;
}
const isFullHtmlDoc = (/^<(!DOCTYPE )?html>/i).test(html);
const parsedHtml = isFullHtmlDoc ? parse5.parse(html) : parse5.parseFragment(html);

const rootNode = chooseRootNode(parsedHtml);
rootNode.childNodes = rootNode.childNodes.map(traverseNodes);

return parse5.serialize(parsedHtml);
};

// Ignore whitespace-only text nodes
const meaningfulChildren = node.childNodes.filter(
(child) => !isWhitespaceTextNode(child)
);
function chooseRootNode(parsedHtml, isFullHtmlDoc) {
if (isFullHtmlDoc) {
const rootNode = parsedHtml.childNodes
.find((x) => x.nodeName === 'html')
?.childNodes?.find((x) => x.nodeName === 'body');

if (
meaningfulChildren.length === 1 &&
meaningfulChildren[0].nodeName.includes('-')
) {
return meaningfulChildren[0];
if (!rootNode) {
throw new Error('html output is missing the body tag');
}

return rootNode;
} else {
return parsedHtml;
}
}

function traverseNodes(node) {
node = stripWrappingParagraph(node);

// Don't traverse children of custom elements
if (node.childNodes && !node.nodeName.includes('-')) {
node.childNodes = node.childNodes.map(traverseNodes);
}

return node;
}

function stripWrappingParagraph(node) {
if (node.nodeName !== 'p') {
return node;
});
}

// Ignore whitespace-only text nodes
const meaningfulChildren = node.childNodes.filter(
(child) => !isWhitespaceTextNode(child)
);

if (meaningfulChildren.length === 1 &&
meaningfulChildren[0].nodeName.includes('-')) {
return meaningfulChildren[0];
}

return node;
}

return parse5.serialize(parsedHtml);
};
106 changes: 73 additions & 33 deletions test/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,96 @@ import assert from 'node:assert/strict';
import { describe, it } from 'node:test';

describe('stripWrappingParagraphs', () => {
it('removes wrapping p tags', async () => {
const input = '<p><x-greeting></x-greeting></p>';
const expected = '<x-greeting></x-greeting>';
describe('Html fragment', () => {
it('removes wrapping p tags', async () => {
const input = '<p><x-greeting></x-greeting></p>';
const expected = '<x-greeting></x-greeting>';

const result = stripWrappingParagraphs(input);
const result = stripWrappingParagraphs(input);

assert.equal(result, expected);
});
assert.equal(result, expected);
});

it('removes wrapping p tags (inner content)', async () => {
const input = '<p><x-greeting>inner content</x-greeting></p>';
const expected = '<x-greeting>inner content</x-greeting>';
it('removes wrapping p tags (inner content)', async () => {
const input = '<p><x-greeting>inner content</x-greeting></p>';
const expected = '<x-greeting>inner content</x-greeting>';

const result = stripWrappingParagraphs(input);
const result = stripWrappingParagraphs(input);

assert.equal(result, expected);
});
assert.equal(result, expected);
});

it('removes wrapping p tags (inner content, newlines)', async () => {
const input = '<p><x-greeting>inner\ncontent</x-greeting></p>';
const expected = '<x-greeting>inner\ncontent</x-greeting>';
it('removes wrapping p tags (inner content, newlines)', async () => {
const input = '<p><x-greeting>inner\ncontent</x-greeting></p>';
const expected = '<x-greeting>inner\ncontent</x-greeting>';

const result = stripWrappingParagraphs(input);
const result = stripWrappingParagraphs(input);

assert.equal(result, expected);
});
assert.equal(result, expected);
});

it('removes wrapping p tags (whitespace)', async () => {
const input = '<p> \n\t<x-greeting></x-greeting> \n</p>';
const expected = '<x-greeting></x-greeting>';
it('removes wrapping p tags (whitespace)', async () => {
const input = '<p> \n\t<x-greeting></x-greeting> \n</p>';
const expected = '<x-greeting></x-greeting>';

const result = stripWrappingParagraphs(input);
const result = stripWrappingParagraphs(input);

assert.equal(result, expected);
});
assert.equal(result, expected);
});

it('does not remove wrapping p tags if it includes other content', async () => {
const input = '<p>Hello <x-greeting></x-greeting></p>';

const result = stripWrappingParagraphs(input);

assert.equal(result, input);
});

it('removes wrapping p tags (multiple)', async () => {
const input =
'<p><x-greeting></x-greeting></p>\n<p><x-test></x-test></p>';
const expected = '<x-greeting></x-greeting>\n<x-test></x-test>';

it('does not remove wrapping p tags if it includes other content', async () => {
const input = '<p>Hello <x-greeting></x-greeting></p>';
const result = stripWrappingParagraphs(input);

const result = stripWrappingParagraphs(input);
assert.equal(result, expected);
});

assert.equal(result, input);
it('removes nested wrapping p tags', async () => {
const input = '<main><p><x-greeting></x-greeting></p></main>';
const expected = '<main><x-greeting></x-greeting></main>';

const result = stripWrappingParagraphs(input);

assert.equal(result, expected);
});

it('removes double nested wrapping p tags', async () => {
const input = '<main><div><p><x-greeting></x-greeting></p></div></main>';
const expected = '<main><div><x-greeting></x-greeting></div></main>';

const result = stripWrappingParagraphs(input);

assert.equal(result, expected);
});
});

it('removes wrapping p tags (multiple)', async () => {
const input = '<p><x-greeting></x-greeting></p>\n<p><x-test></x-test></p>';
const expected = '<x-greeting></x-greeting>\n<x-test></x-test>';
describe('Html document', () => {
it('preserves html, head, and body tags', () => {
const input = '<!DOCTYPE html><html lang="en"><head></head><body><p>Hello</p></body></html>';

const result = stripWrappingParagraphs(input);

assert.equal(result, input);
});

it('strips p tags in body', () => {
const input = '<!DOCTYPE html><html lang="en"><head></head><body><p><x-greeting></x-greeting></p></body></html>';
const expected = '<!DOCTYPE html><html lang="en"><head></head><body><x-greeting></x-greeting></body></html>';

const result = stripWrappingParagraphs(input);
const result = stripWrappingParagraphs(input);

assert.equal(result, expected);
assert.equal(result, expected);
});
});
});