Skip to content

Commit 76e1d94

Browse files
committed
Allow to render TrustedHTML objects similar to SafeString
1 parent 1cb493b commit 76e1d94

File tree

7 files changed

+75
-3
lines changed

7 files changed

+75
-3
lines changed

packages/@glimmer-workspace/integration-tests/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@simple-dom/document": "^1.4.0",
3030
"@simple-dom/serializer": "^1.4.0",
3131
"@simple-dom/void-map": "^1.4.0",
32+
"@types/trusted-types": "^2.0.7",
3233
"js-reporters": "^2.1.0",
3334
"qunit": "^2.19.4",
3435
"simple-html-tokenizer": "^0.5.11"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { TrustedTypePolicy, TrustedTypesWindow } from 'trusted-types/lib';
2+
3+
import { jitSuite, RenderTest, test } from '..';
4+
5+
let policy: TrustedTypePolicy | undefined;
6+
if (typeof window !== 'undefined') {
7+
let trustedTypes = (window as unknown as TrustedTypesWindow).trustedTypes;
8+
if (trustedTypes?.createPolicy) {
9+
policy = trustedTypes.createPolicy('test', {
10+
createHTML: (s: string) => s,
11+
createScript: (s: string) => s,
12+
createScriptURL: (s: string) => s,
13+
});
14+
}
15+
}
16+
17+
export class TrustedHTMLTests extends RenderTest {
18+
static suiteName = 'TrustedHTML';
19+
20+
@test
21+
'renders TrustedHTML similar to SafeString'() {
22+
if (!policy) return;
23+
24+
let html = '<b>test\'"&quot;</b>';
25+
this.registerHelper('trustedHTML', () => {
26+
return policy?.createHTML(html);
27+
});
28+
29+
this.render('<div>{{trustedHTML}}</div>');
30+
this.assertHTML('<div><b>test\'""</b></div');
31+
this.assertStableRerender();
32+
}
33+
34+
@test
35+
'renders TrustedHTML in attribute context as string'() {
36+
if (!policy) return;
37+
38+
let html = '<b>test\'"&quot;</b>';
39+
this.registerHelper('trustedHTML', () => {
40+
return policy?.createHTML(html);
41+
});
42+
43+
this.render('<a title="{{trustedHTML}}">{{trustedHTML}}</a>');
44+
this.assertHTML('<a title="<b>test\'&quot;&amp;quot;</b>"><b>test\'""</b></a>');
45+
this.assertStableRerender();
46+
}
47+
}
48+
49+
jitSuite(TrustedHTMLTests);

packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts

+5
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export function StdAppend(
7474
op(Op.AppendSafeHTML);
7575
});
7676

77+
when(ContentType.TrustedHTML, () => {
78+
op(Op.AssertSame);
79+
op(Op.AppendHTML);
80+
});
81+
7782
when(ContentType.Fragment, () => {
7883
op(Op.AssertSame);
7984
op(Op.AppendDocumentFragment);

packages/@glimmer/runtime/lib/compiled/opcodes/content.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { isObject } from '@glimmer/util';
1111
import { ContentType, CurriedType, Op } from '@glimmer/vm';
1212

1313
import { isCurriedType } from '../../curried-value';
14-
import { isEmpty, isFragment, isNode, isSafeString, shouldCoerce } from '../../dom/normalize';
14+
import { isEmpty, isFragment, isNode, isSafeString, isTrustedHTML, shouldCoerce } from '../../dom/normalize';
1515
import { APPEND_OPCODES } from '../../opcodes';
1616
import DynamicTextContent from '../../vm/content/text';
1717
import { CheckReference } from './-debug-strip';
@@ -32,6 +32,8 @@ function toContentType(value: unknown) {
3232
return ContentType.Helper;
3333
} else if (isSafeString(value)) {
3434
return ContentType.SafeString;
35+
} else if (isTrustedHTML(value)) {
36+
return ContentType.TrustedHTML;
3537
} else if (isFragment(value)) {
3638
return ContentType.Fragment;
3739
} else if (isNode(value)) {
@@ -87,7 +89,7 @@ APPEND_OPCODES.add(Op.AppendHTML, (vm) => {
8789
let reference = check(vm.stack.pop(), CheckReference);
8890

8991
let rawValue = valueForRef(reference);
90-
let value = isEmpty(rawValue) ? '' : String(rawValue);
92+
let value = isEmpty(rawValue) ? '' : isTrustedHTML(rawValue) ? rawValue as string : String(rawValue);
9193

9294
vm.elements().appendDynamicHTML(value);
9395
});

packages/@glimmer/runtime/lib/dom/normalize.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Dict, SimpleDocumentFragment, SimpleNode } from '@glimmer/interfaces';
2+
import type { TrustedTypesWindow } from 'trusted-types/lib';
23

34
export interface SafeString {
45
toHTML(): string;
@@ -43,6 +44,18 @@ export function isEmpty(value: unknown): boolean {
4344
return value === null || value === undefined || typeof (value as Dict).toString !== 'function';
4445
}
4546

47+
let isHTML: ((value: unknown) => boolean) | undefined;
48+
if (typeof window !== 'undefined') {
49+
let trustedTypes = (window as unknown as TrustedTypesWindow).trustedTypes;
50+
if (trustedTypes?.isHTML) {
51+
isHTML = trustedTypes?.isHTML.bind(trustedTypes);
52+
}
53+
}
54+
55+
export function isTrustedHTML(value: unknown): boolean {
56+
return isHTML ? isHTML(value) : false;
57+
}
58+
4659
export function isSafeString(value: unknown): value is SafeString {
4760
return typeof value === 'object' && value !== null && typeof (value as any).toHTML === 'function';
4861
}

packages/@glimmer/runtime/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"@glimmer/util": "workspace:^",
4444
"@glimmer/validator": "workspace:^",
4545
"@glimmer/vm": "workspace:^",
46-
"@glimmer/wire-format": "workspace:^"
46+
"@glimmer/wire-format": "workspace:^",
47+
"@types/trusted-types": "^2.0.7"
4748
},
4849
"devDependencies": {
4950
"@glimmer-workspace/build-support": "workspace:^",

packages/@glimmer/vm/lib/content.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export const ContentType = {
77
Fragment: 5,
88
Node: 6,
99
Other: 8,
10+
TrustedHTML: 9,
1011
} as const;

0 commit comments

Comments
 (0)