diff --git a/README.md b/README.md
index aae5f7c1..66b0c5d4 100644
--- a/README.md
+++ b/README.md
@@ -169,7 +169,24 @@ The server parser is a wrapper of [htmlparser2](https://github.com/fb55/htmlpars
The client parser mimics the server parser by using the [DOM](https://developer.mozilla.org/docs/Web/API/Document_Object_Model/Introduction) API to parse the HTML string.
-## Options (server only)
+## Options
+
+### trustedTypePolicy (browser only)
+
+When running in the browser, you can pass a Trusted Types policy. The parser
+uses `trustedTypePolicy.createHTML` right before assigning to `innerHTML`.
+
+```js
+const trustedTypePolicy = window.trustedTypes?.createPolicy('my-policy', {
+ createHTML(input) {
+ return input;
+ },
+});
+
+parse('
Hello
', { trustedTypePolicy });
+```
+
+### Server parser options
Because the server parser is a wrapper of [htmlparser2](https://github.com/fb55/htmlparser2), which implements [domhandler](https://github.com/fb55/domhandler), you can alter how the server parser parses your code with the options:
diff --git a/__tests__/client/domparser.test.ts b/__tests__/client/domparser.test.ts
new file mode 100644
index 00000000..ab3c7c23
--- /dev/null
+++ b/__tests__/client/domparser.test.ts
@@ -0,0 +1,67 @@
+import { getHTMLForInnerHTML } from '../../src/client/domparser';
+
+describe('getHTMLForInnerHTML', () => {
+ it('returns the html string as-is when no trusted type policy is provided', () => {
+ const html = 'test
';
+ const result = getHTMLForInnerHTML(html);
+
+ expect(result).toBe(html);
+ });
+
+ it('returns the html string when trustedTypePolicy is undefined', () => {
+ const html = 'Hello World
';
+ const result = getHTMLForInnerHTML(html, undefined);
+
+ expect(result).toBe(html);
+ });
+
+ it('calls trustedTypePolicy.createHTML with the html string when policy is provided', () => {
+ const html = 'content';
+ const trustedTypePolicy = {
+ createHTML: vi.fn((input: string) => input),
+ };
+
+ const result = getHTMLForInnerHTML(html, trustedTypePolicy);
+
+ expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
+ expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(html);
+ expect(result).toBe(html);
+ });
+
+ it('returns the result of trustedTypePolicy.createHTML when policy is provided', () => {
+ const html = 'test
';
+ const trustedHtml = 'TRUSTED_HTML_VALUE';
+ const trustedTypePolicy = {
+ createHTML: vi.fn(() => trustedHtml),
+ };
+
+ const result = getHTMLForInnerHTML(html, trustedTypePolicy);
+
+ expect(result).toBe(trustedHtml);
+ });
+
+ it('handles empty html string', () => {
+ const html = '';
+ const result = getHTMLForInnerHTML(html);
+
+ expect(result).toBe('');
+ });
+
+ it('handles html string with special characters', () => {
+ const html = 'Test & "quotes" \'apostrophe\'
';
+ const result = getHTMLForInnerHTML(html);
+
+ expect(result).toBe(html);
+ });
+
+ it('calls trustedTypePolicy with complex html', () => {
+ const html = '';
+ const trustedTypePolicy = {
+ createHTML: vi.fn((input: string) => input),
+ };
+
+ getHTMLForInnerHTML(html, trustedTypePolicy);
+
+ expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(html);
+ });
+});
diff --git a/__tests__/client/index.test.ts b/__tests__/client/index.test.ts
index 11b4c618..4efe9484 100644
--- a/__tests__/client/index.test.ts
+++ b/__tests__/client/index.test.ts
@@ -17,6 +17,34 @@ describe('client parser', () => {
testCaseSensitiveTags(htmlToDOM);
if (isBrowser()) {
+ describe('trustedTypePolicy', () => {
+ it('uses policy before setting template innerHTML', () => {
+ const trustedTypePolicy = {
+ createHTML: vi.fn((input: string) => input),
+ };
+
+ htmlToDOM('test
', { trustedTypePolicy });
+
+ expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
+ expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(
+ 'test
',
+ );
+ });
+
+ it('uses policy before setting document innerHTML', () => {
+ const trustedTypePolicy = {
+ createHTML: vi.fn((input: string) => input),
+ };
+
+ htmlToDOM('test
', { trustedTypePolicy });
+
+ expect(trustedTypePolicy.createHTML).toHaveBeenCalledOnce();
+ expect(trustedTypePolicy.createHTML).toHaveBeenCalledWith(
+ 'test
',
+ );
+ });
+ });
+
describe('performance', () => {
it('executes 1000 times in less than 50ms', () => {
let times = 1000;
diff --git a/__tests__/types/index.test.ts b/__tests__/types/index.test.ts
index 3efd0007..7e6949da 100644
--- a/__tests__/types/index.test.ts
+++ b/__tests__/types/index.test.ts
@@ -12,5 +12,14 @@ parse('text
', { decodeEntities: false });
// $ExpectType (Element | Text | Comment | ProcessingInstruction)[]
parse('text
', { lowerCaseTags: true });
+// $ExpectType (Element | Text | Comment | ProcessingInstruction)[]
+parse('text
', {
+ trustedTypePolicy: {
+ createHTML(input: string) {
+ return input;
+ },
+ },
+});
+
// $ExpectType (Element | Text | Comment | ProcessingInstruction)[]
parse('');
diff --git a/src/client/domparser.ts b/src/client/domparser.ts
index 8329fe35..29807ad7 100644
--- a/src/client/domparser.ts
+++ b/src/client/domparser.ts
@@ -1,3 +1,4 @@
+import type { TrustedTypePolicyLike } from '../types';
import { escapeSpecialCharacters, hasOpenTag } from './utilities';
// constants
@@ -6,16 +7,32 @@ const HEAD = 'head';
const BODY = 'body';
const FIRST_TAG_REGEX = /<([a-zA-Z]+[0-9]?)/; // e.g.,
+export function getHTMLForInnerHTML(
+ html: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+) {
+ return trustedTypePolicy ? trustedTypePolicy.createHTML(html) : html;
+}
+
// falls back to `parseFromString` if `createHTMLDocument` cannot be used
/* eslint-disable @typescript-eslint/no-unused-vars */
/* v8 ignore start */
-let parseFromDocument = (html: string, tagName?: string): Document => {
+let parseFromDocument = (
+ html: string,
+ tagName?: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+): Document => {
throw new Error(
'This browser does not support `document.implementation.createHTMLDocument`',
);
};
-let parseFromString = (html: string, tagName?: string): Document => {
+let parseFromString = (
+ html: string,
+ tagName?: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+): Document => {
+ void trustedTypePolicy;
throw new Error(
'This browser does not support `DOMParser.prototype.parseFromString`',
);
@@ -39,7 +56,12 @@ if (typeof DOMParser === 'function') {
* @param tagName - The element to render the HTML (with 'body' as fallback).
* @returns - Document.
*/
- parseFromString = (html: string, tagName?: string): Document => {
+ parseFromString = (
+ html: string,
+ tagName?: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+ ): Document => {
+ void trustedTypePolicy;
if (tagName) {
html = `<${tagName}>${html}${tagName}>`;
}
@@ -66,18 +88,28 @@ if (typeof document === 'object' && document.implementation) {
* @param tagName - The element to render the HTML (with 'body' as fallback).
* @returns - Document
*/
- parseFromDocument = function (html: string, tagName?: string): Document {
+ parseFromDocument = function (
+ html: string,
+ tagName?: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+ ): Document {
if (tagName) {
const element = htmlDocument.documentElement.querySelector(tagName);
if (element) {
- element.innerHTML = html;
+ element.innerHTML = getHTMLForInnerHTML(
+ html,
+ trustedTypePolicy,
+ ) as string;
}
return htmlDocument;
}
- htmlDocument.documentElement.innerHTML = html;
+ htmlDocument.documentElement.innerHTML = getHTMLForInnerHTML(
+ html,
+ trustedTypePolicy,
+ ) as string;
return htmlDocument;
};
}
@@ -90,7 +122,10 @@ if (typeof document === 'object' && document.implementation) {
const template =
typeof document === 'object' && document.createElement('template');
-let parseFromTemplate: (html: string) => NodeList;
+let parseFromTemplate: (
+ html: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+) => NodeList;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (template && template.content) {
@@ -100,8 +135,11 @@ if (template && template.content) {
* @param html - HTML string.
* @returns - Nodes.
*/
- parseFromTemplate = (html: string): NodeList => {
- template.innerHTML = html;
+ parseFromTemplate = (
+ html: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+ ): NodeList => {
+ template.innerHTML = getHTMLForInnerHTML(html, trustedTypePolicy) as string;
return template.content.childNodes;
};
}
@@ -113,9 +151,13 @@ const createNodeList = () => document.createDocumentFragment().childNodes;
* Parses HTML string to DOM nodes.
*
* @param html - HTML markup.
+ * @param trustedTypePolicy - Trusted Types policy.
* @returns - DOM nodes.
*/
-export default function domparser(html: string): NodeList {
+export default function domparser(
+ html: string,
+ trustedTypePolicy?: TrustedTypePolicyLike,
+): NodeList {
// Escape special characters before parsing
html = escapeSpecialCharacters(html);
@@ -143,7 +185,11 @@ export default function domparser(html: string): NodeList {
case HEAD:
case BODY: {
- const elements = parseFromDocument(html).querySelectorAll(firstTagName);
+ const elements = parseFromDocument(
+ html,
+ undefined,
+ trustedTypePolicy,
+ ).querySelectorAll(firstTagName);
// if there's a sibling element, then return both elements
/* v8 ignore next */
@@ -159,10 +205,14 @@ export default function domparser(html: string): NodeList {
default: {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (parseFromTemplate) {
- return parseFromTemplate(html);
+ return parseFromTemplate(html, trustedTypePolicy);
}
- const element = parseFromDocument(html, BODY).querySelector(BODY);
+ const element = parseFromDocument(
+ html,
+ BODY,
+ trustedTypePolicy,
+ ).querySelector(BODY);
return element?.childNodes ?? createNodeList();
}
diff --git a/src/client/html-to-dom.ts b/src/client/html-to-dom.ts
index 4eb5583d..36f8caab 100644
--- a/src/client/html-to-dom.ts
+++ b/src/client/html-to-dom.ts
@@ -1,3 +1,4 @@
+import type { HTMLDOMParserOptions } from '../types';
import domparser from './domparser';
import { formatDOM } from './utilities';
@@ -7,9 +8,13 @@ const DIRECTIVE_REGEX = /<(![a-zA-Z\s]+)>/; // e.g.,
* Parses HTML string to DOM nodes in browser.
*
* @param html - HTML markup.
+ * @param options - Parser options.
* @returns - DOM elements.
*/
-export default function HTMLDOMParser(html: string) {
+export default function HTMLDOMParser(
+ html: string,
+ options?: HTMLDOMParserOptions,
+) {
if (typeof html !== 'string') {
throw new TypeError('First argument must be a string');
}
@@ -22,5 +27,9 @@ export default function HTMLDOMParser(html: string) {
const match = DIRECTIVE_REGEX.exec(html);
const directive = match ? match[1] : undefined;
- return formatDOM(domparser(html), null, directive);
+ return formatDOM(
+ domparser(html, options?.trustedTypePolicy),
+ null,
+ directive,
+ );
}
diff --git a/src/server/html-to-dom.ts b/src/server/html-to-dom.ts
index df491506..4147c30f 100644
--- a/src/server/html-to-dom.ts
+++ b/src/server/html-to-dom.ts
@@ -1,7 +1,7 @@
import { DomHandler } from 'domhandler';
-import type { ParserOptions } from 'htmlparser2';
import { Parser } from 'htmlparser2';
+import type { HTMLDOMParserOptions } from '../types';
import { unsetRootParent } from './utilities';
/**
@@ -16,7 +16,10 @@ import { unsetRootParent } from './utilities';
* @param options - Parser options.
* @returns - DOM nodes.
*/
-export default function HTMLDOMParser(html: string, options?: ParserOptions) {
+export default function HTMLDOMParser(
+ html: string,
+ options?: HTMLDOMParserOptions,
+) {
if (typeof html !== 'string') {
throw new TypeError('First argument must be a string.');
}
diff --git a/src/types.ts b/src/types.ts
index 4be12091..2b42ef9c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,5 +1,16 @@
+import type { DomHandlerOptions } from 'domhandler';
import type { Comment, Element, ProcessingInstruction, Text } from 'domhandler';
+import type { ParserOptions } from 'htmlparser2';
export type { Comment, Element, ProcessingInstruction, Text };
export type DOMNode = Comment | Element | ProcessingInstruction | Text;
+
+export interface TrustedTypePolicyLike {
+ createHTML(input: string): { toString(): string };
+}
+
+export type HTMLDOMParserOptions = ParserOptions &
+ DomHandlerOptions & {
+ trustedTypePolicy?: TrustedTypePolicyLike;
+ };