Skip to content

Commit ecc64dc

Browse files
chriskrychokategengler
authored andcommitted
Make the type for SafeString public
This type has been effectively "intimate" for many years, and fits in the same bucket as e.g. `Transition`: it is not user-constructible, but will be constructed by the framework and users need to be able to name it. For example, `ember-intl` needs to be able to see that a string has been marked as trusted to do the right thing to emit it. Internally, clean up a few long-standing TS issues (`any` etc.), make `SafeString` explicitly implement the contract from Glimmer so that if that contract changes, we will know at the definition site, and make the implementation details of how `SafeString` handles the string it wraps `private`. (This does not use a `#`-private field because private class fields have some non-trivial overhead in transpiled contexts, and `SafeString` can appear in fairly hot rendering paths.) (cherry picked from commit 00c69d2)
1 parent 91b5719 commit ecc64dc

File tree

2 files changed

+31
-13
lines changed

2 files changed

+31
-13
lines changed

packages/@ember/-internals/glimmer/lib/utils/string.ts

+28-12
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
@module @ember/template
33
*/
44

5-
export class SafeString {
6-
public string: string;
5+
import type { SafeString as GlimmerSafeString } from '@glimmer/runtime';
6+
7+
export class SafeString implements GlimmerSafeString {
8+
private __string: string;
79

810
constructor(string: string) {
9-
this.string = string;
11+
this.__string = string;
1012
}
1113

1214
toString(): string {
13-
return `${this.string}`;
15+
return `${this.__string}`;
1416
}
1517

1618
toHTML(): string {
@@ -35,10 +37,11 @@ function escapeChar(chr: keyof typeof escape) {
3537
return escape[chr];
3638
}
3739

38-
export function escapeExpression(string: any): string {
40+
export function escapeExpression(string: unknown): string {
41+
let s: string;
3942
if (typeof string !== 'string') {
4043
// don't escape SafeStrings, since they're already safe
41-
if (string && string.toHTML) {
44+
if (isHTMLSafe(string)) {
4245
return string.toHTML();
4346
} else if (string === null || string === undefined) {
4447
return '';
@@ -49,13 +52,23 @@ export function escapeExpression(string: any): string {
4952
// Force a string conversion as this will be done by the append regardless and
5053
// the regex test will do this transparently behind the scenes, causing issues if
5154
// an object's to string has escaped characters in it.
52-
string = String(string);
55+
s = String(string);
56+
} else {
57+
s = string;
5358
}
5459

55-
if (!possible.test(string)) {
56-
return string;
60+
if (!possible.test(s)) {
61+
return s;
5762
}
58-
return string.replace(badChars, escapeChar);
63+
64+
// SAFETY: this is technically a lie, but it's a true lie as long as the
65+
// invariant it depends on is upheld: `escapeChar` will always return a string
66+
// as long as its input is one of the characters in `escape`, and it will only
67+
// be called if it matches one of the characters in the `badChar` regex, which
68+
// is hand-maintained to match the set escaped. (It would be nice if TS could
69+
// "see" into the regex to see how this works, but that'd be quite a lot of
70+
// extra fanciness.)
71+
return s.replace(badChars, escapeChar as (s: string) => string);
5972
}
6073

6174
/**
@@ -82,6 +95,7 @@ export function escapeExpression(string: any): string {
8295
8396
@method htmlSafe
8497
@for @ember/template
98+
@param str {String} The string to treat as trusted.
8599
@static
86100
@return {SafeString} A string that will not be HTML escaped by Handlebars.
87101
@public
@@ -114,6 +128,8 @@ export function htmlSafe(str: string): SafeString {
114128
@return {Boolean} `true` if the string was decorated with `htmlSafe`, `false` otherwise.
115129
@public
116130
*/
117-
export function isHTMLSafe(str: any | null | undefined): str is SafeString {
118-
return str !== null && typeof str === 'object' && typeof str.toHTML === 'function';
131+
export function isHTMLSafe(str: unknown): str is SafeString {
132+
return (
133+
str !== null && typeof str === 'object' && 'toHTML' in str && typeof str.toHTML === 'function'
134+
);
119135
}

packages/@ember/template/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
export { htmlSafe, isHTMLSafe } from '@ember/-internals/glimmer';
1+
// NOTE: this intentionally *only* exports the *type* `SafeString`, not its
2+
// value, since it should not be constructed by users.
3+
export { htmlSafe, isHTMLSafe, type SafeString } from '@ember/-internals/glimmer';

0 commit comments

Comments
 (0)