diff --git a/packages/@glimmer-workspace/integration-tests/package.json b/packages/@glimmer-workspace/integration-tests/package.json
index 27ac55e35b..22680f0ab7 100644
--- a/packages/@glimmer-workspace/integration-tests/package.json
+++ b/packages/@glimmer-workspace/integration-tests/package.json
@@ -29,6 +29,7 @@
"@simple-dom/document": "^1.4.0",
"@simple-dom/serializer": "^1.4.0",
"@simple-dom/void-map": "^1.4.0",
+ "@types/trusted-types": "^2.0.7",
"js-reporters": "^2.1.0",
"qunit": "^2.19.4",
"simple-html-tokenizer": "^0.5.11"
diff --git a/packages/@glimmer-workspace/integration-tests/test/trusted-html-test.ts b/packages/@glimmer-workspace/integration-tests/test/trusted-html-test.ts
new file mode 100644
index 0000000000..7c28a4c2e5
--- /dev/null
+++ b/packages/@glimmer-workspace/integration-tests/test/trusted-html-test.ts
@@ -0,0 +1,49 @@
+import type { TrustedTypePolicy, TrustedTypesWindow } from 'trusted-types/lib';
+
+import { jitSuite, RenderTest, test } from '..';
+
+let policy: TrustedTypePolicy | undefined;
+if (typeof window !== 'undefined') {
+ let trustedTypes = (window as unknown as TrustedTypesWindow).trustedTypes;
+ if (trustedTypes?.createPolicy) {
+ policy = trustedTypes.createPolicy('test', {
+ createHTML: (s: string) => s,
+ createScript: (s: string) => s,
+ createScriptURL: (s: string) => s,
+ });
+ }
+}
+
+export class TrustedHTMLTests extends RenderTest {
+ static suiteName = 'TrustedHTML';
+
+ @test
+ 'renders TrustedHTML similar to SafeString'() {
+ if (!policy) return;
+
+ let html = 'test\'""';
+ this.registerHelper('trustedHTML', () => {
+ return policy?.createHTML(html);
+ });
+
+ this.render('
{{trustedHTML}}
');
+ this.assertHTML('test\'""
test\'""';
+ this.registerHelper('trustedHTML', () => {
+ return policy?.createHTML(html);
+ });
+
+ this.render('{{trustedHTML}}');
+ this.assertHTML('test\'""');
+ this.assertStableRerender();
+ }
+}
+
+jitSuite(TrustedHTMLTests);
diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts
index 7c7a98e79e..bc080cec42 100644
--- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts
+++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts
@@ -74,6 +74,11 @@ export function StdAppend(
op(Op.AppendSafeHTML);
});
+ when(ContentType.TrustedHTML, () => {
+ op(Op.AssertSame);
+ op(Op.AppendHTML);
+ });
+
when(ContentType.Fragment, () => {
op(Op.AssertSame);
op(Op.AppendDocumentFragment);
diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts
index 03b1c9c855..522c46c593 100644
--- a/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts
+++ b/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts
@@ -11,7 +11,7 @@ import { isObject } from '@glimmer/util';
import { ContentType, CurriedType, Op } from '@glimmer/vm';
import { isCurriedType } from '../../curried-value';
-import { isEmpty, isFragment, isNode, isSafeString, shouldCoerce } from '../../dom/normalize';
+import { isEmpty, isFragment, isNode, isSafeString, isTrustedHTML, shouldCoerce } from '../../dom/normalize';
import { APPEND_OPCODES } from '../../opcodes';
import DynamicTextContent from '../../vm/content/text';
import { CheckReference } from './-debug-strip';
@@ -32,6 +32,8 @@ function toContentType(value: unknown) {
return ContentType.Helper;
} else if (isSafeString(value)) {
return ContentType.SafeString;
+ } else if (isTrustedHTML(value)) {
+ return ContentType.TrustedHTML;
} else if (isFragment(value)) {
return ContentType.Fragment;
} else if (isNode(value)) {
@@ -87,7 +89,7 @@ APPEND_OPCODES.add(Op.AppendHTML, (vm) => {
let reference = check(vm.stack.pop(), CheckReference);
let rawValue = valueForRef(reference);
- let value = isEmpty(rawValue) ? '' : String(rawValue);
+ let value = isEmpty(rawValue) ? '' : isTrustedHTML(rawValue) ? rawValue as string : String(rawValue);
vm.elements().appendDynamicHTML(value);
});
diff --git a/packages/@glimmer/runtime/lib/dom/normalize.ts b/packages/@glimmer/runtime/lib/dom/normalize.ts
index 6de6ec7b4b..638a87ea33 100644
--- a/packages/@glimmer/runtime/lib/dom/normalize.ts
+++ b/packages/@glimmer/runtime/lib/dom/normalize.ts
@@ -1,4 +1,5 @@
import type { Dict, SimpleDocumentFragment, SimpleNode } from '@glimmer/interfaces';
+import type { TrustedTypesWindow } from 'trusted-types/lib';
export interface SafeString {
toHTML(): string;
@@ -43,6 +44,18 @@ export function isEmpty(value: unknown): boolean {
return value === null || value === undefined || typeof (value as Dict).toString !== 'function';
}
+let isHTML: ((value: unknown) => boolean) | undefined;
+if (typeof window !== 'undefined') {
+ let trustedTypes = (window as unknown as TrustedTypesWindow).trustedTypes;
+ if (trustedTypes?.isHTML) {
+ isHTML = trustedTypes?.isHTML.bind(trustedTypes);
+ }
+}
+
+export function isTrustedHTML(value: unknown): boolean {
+ return isHTML ? isHTML(value) : false;
+}
+
export function isSafeString(value: unknown): value is SafeString {
return typeof value === 'object' && value !== null && typeof (value as any).toHTML === 'function';
}
diff --git a/packages/@glimmer/runtime/package.json b/packages/@glimmer/runtime/package.json
index 08147b44cb..e07e0703ce 100644
--- a/packages/@glimmer/runtime/package.json
+++ b/packages/@glimmer/runtime/package.json
@@ -43,7 +43,8 @@
"@glimmer/util": "workspace:^",
"@glimmer/validator": "workspace:^",
"@glimmer/vm": "workspace:^",
- "@glimmer/wire-format": "workspace:^"
+ "@glimmer/wire-format": "workspace:^",
+ "@types/trusted-types": "^2.0.7"
},
"devDependencies": {
"@glimmer-workspace/build-support": "workspace:^",
diff --git a/packages/@glimmer/vm/lib/content.ts b/packages/@glimmer/vm/lib/content.ts
index ddbe0290ce..ac8d805d42 100644
--- a/packages/@glimmer/vm/lib/content.ts
+++ b/packages/@glimmer/vm/lib/content.ts
@@ -7,4 +7,5 @@ export const ContentType = {
Fragment: 5,
Node: 6,
Other: 8,
+ TrustedHTML: 9,
} as const;