Skip to content

Commit

Permalink
Allow to render TrustedHTML objects similar to SafeString
Browse files Browse the repository at this point in the history
  • Loading branch information
crypto committed Feb 21, 2024
1 parent d9b5045 commit 613799e
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/@glimmer-workspace/integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = '<b>test\'"&quot;</b>';
this.registerHelper('trustedHTML', () => {
return policy?.createHTML(html);
});

this.render('<div>{{trustedHTML}}</div>');
this.assertHTML('<div><b>test\'""</b></div');
this.assertStableRerender();
}

@test
'renders TrustedHTML in attribute context as string'() {
if (!policy) return;

let html = '<b>test\'"&quot;</b>';
this.registerHelper('trustedHTML', () => {
return policy?.createHTML(html);
});

this.render('<a title="{{trustedHTML}}">{{trustedHTML}}</a>');
this.assertHTML('<a title="<b>test\'&quot;&amp;quot;</b>"><b>test\'""</b></a>');
this.assertStableRerender();
}
}

jitSuite(TrustedHTMLTests);
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions packages/@glimmer/runtime/lib/compiled/opcodes/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)) {
Expand Down Expand Up @@ -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);
});
Expand Down
13 changes: 13 additions & 0 deletions packages/@glimmer/runtime/lib/dom/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Dict, SimpleDocumentFragment, SimpleNode } from '@glimmer/interfaces';
import type { TrustedTypesWindow } from 'trusted-types/lib';

export interface SafeString {
toHTML(): string;
Expand Down Expand Up @@ -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';
}
Expand Down
3 changes: 2 additions & 1 deletion packages/@glimmer/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
1 change: 1 addition & 0 deletions packages/@glimmer/vm/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const ContentType = {
Fragment: 5,
Node: 6,
Other: 8,
TrustedHTML: 9,
} as const;

0 comments on commit 613799e

Please sign in to comment.