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
135 changes: 74 additions & 61 deletions __tests__/client/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,74 @@
// @vitest-environment jsdom
import htmlToDOM from '../../src/client/html-to-dom';
import htmlCases from '../cases/html';
import {
isBrowser,
parseDOM,
runTests,
testCaseSensitiveTags,
throwErrors,
} from '../helpers';

describe('client parser', () => {
// @ts-expect-error argument of type is not assignable
throwErrors(htmlToDOM);
// @ts-expect-error argument of type is not assignable
runTests(htmlToDOM, parseDOM, htmlCases);
testCaseSensitiveTags(htmlToDOM);

if (isBrowser()) {
describe('trustedTypePolicy', () => {
it('uses policy before setting template innerHTML', () => {
const trustedTypePolicy = {
createHTML: vi.fn((input: string) => input),
};

htmlToDOM('<div>test</div>', { trustedTypePolicy });

expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(
'<div>test</div>',
);
});

it('uses policy before setting document innerHTML', () => {
const trustedTypePolicy = {
createHTML: vi.fn((input: string) => input),
};

htmlToDOM('<body><div>test</div></body>', { trustedTypePolicy });

expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(
'<body><div>test</div></body>',
);
});
});

describe('performance', () => {
it('executes 1000 times in less than 50ms', () => {
let times = 1000;
const start = performance.now();
while (--times) {
htmlToDOM('<div>test</div>');
}
const end = performance.now();
const elapsed = end - start;
expect(elapsed).below(50);
});
});
}
});
// @vitest-environment jsdom
import htmlToDOM from '../../src/client/html-to-dom';
import htmlCases from '../cases/html';
import {
isBrowser,
parseDOM,
runTests,
testCaseSensitiveTags,
throwErrors,
} from '../helpers';

describe('client parser', () => {
// @ts-expect-error argument of type is not assignable
throwErrors(htmlToDOM);
// @ts-expect-error argument of type is not assignable
runTests(htmlToDOM, parseDOM, htmlCases);
testCaseSensitiveTags(htmlToDOM);

it('uses policy before setting innerHTML', () => {
const trustedTypePolicy = {
createHTML: vi.fn((input: string) => input),
};

htmlToDOM('<div>test</div>', { trustedTypePolicy });

expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(
'<div>test</div>',
);
});

if (isBrowser()) {
describe('trustedTypePolicy', () => {
it('uses policy before setting template innerHTML', () => {
const trustedTypePolicy = {
createHTML: vi.fn((input: string) => input),
};

htmlToDOM('<div>test</div>', { trustedTypePolicy });

expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(
'<div>test</div>',
);
});

it('uses policy before setting document innerHTML', () => {
const trustedTypePolicy = {
createHTML: vi.fn((input: string) => input),
};

htmlToDOM('<body><div>test</div></body>', { trustedTypePolicy });

expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(
'<body><div>test</div></body>',
);
});
});

describe('performance', () => {
it('executes 1000 times in less than 50ms', () => {
let times = 1000;
const start = performance.now();
while (--times) {
htmlToDOM('<div>test</div>');
}
const end = performance.now();
const elapsed = end - start;
expect(elapsed).below(50);
});
});
}
});
7 changes: 7 additions & 0 deletions eslint.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,11 @@ export default defineConfig([
'@typescript-eslint/no-require-imports': 'off',
},
},
{
files: ['__tests__/types/**'],

rules: {
'@typescript-eslint/no-unsafe-call': 'off',
},
},
]);
18 changes: 9 additions & 9 deletions src/client/domparser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { escapeSpecialCharacters, hasOpenTag } from './utilities';
import type { TrustedTypePolicyLike } from '../types';
import type { TrustedTypePolicy } from '../types';

// constants
const HTML = 'html';
Expand All @@ -9,7 +9,7 @@ const FIRST_TAG_REGEX = /<([a-zA-Z]+[0-9]?)/; // e.g., <h1>

function getHTMLForInnerHTML(
html: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
) {
return trustedTypePolicy ? trustedTypePolicy.createHTML(html) : html;
}
Expand All @@ -20,7 +20,7 @@ function getHTMLForInnerHTML(
let parseFromDocument = (
html: string,
tagName?: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
): Document => {
throw new Error(
'This browser does not support `document.implementation.createHTMLDocument`',
Expand All @@ -30,7 +30,7 @@ let parseFromDocument = (
let parseFromString = (
html: string,
tagName?: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
): Document => {
void trustedTypePolicy;
throw new Error(
Expand Down Expand Up @@ -59,7 +59,7 @@ if (typeof DOMParser === 'function') {
parseFromString = (
html: string,
tagName?: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
): Document => {
void trustedTypePolicy;
if (tagName) {
Expand Down Expand Up @@ -91,7 +91,7 @@ if (typeof document === 'object' && document.implementation) {
parseFromDocument = function (
html: string,
tagName?: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
): Document {
if (tagName) {
const element = htmlDocument.documentElement.querySelector(tagName);
Expand Down Expand Up @@ -124,7 +124,7 @@ const template =

let parseFromTemplate: (
html: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
) => NodeList;

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Expand All @@ -137,7 +137,7 @@ if (template && template.content) {
*/
parseFromTemplate = (
html: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
): NodeList => {
template.innerHTML = getHTMLForInnerHTML(
html,
Expand All @@ -159,7 +159,7 @@ const createNodeList = () => document.createDocumentFragment().childNodes;
*/
export default function domparser(
html: string,
trustedTypePolicy?: TrustedTypePolicyLike,
trustedTypePolicy?: TrustedTypePolicy,
): NodeList {
// Escape special characters before parsing
html = escapeSpecialCharacters(html);
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export type { Comment, Element, ProcessingInstruction, Text };

export type DOMNode = Comment | Element | ProcessingInstruction | Text;

export interface TrustedTypePolicyLike {
export interface TrustedTypePolicy {
createHTML(input: string): unknown;
}

export type HTMLDOMParserOptions = ParserOptions &
DomHandlerOptions & {
trustedTypePolicy?: TrustedTypePolicyLike;
trustedTypePolicy?: TrustedTypePolicy;
};