diff --git a/packages/@ember/helper/index.ts b/packages/@ember/helper/index.ts index bdf745a9974..06f93955567 100644 --- a/packages/@ember/helper/index.ts +++ b/packages/@ember/helper/index.ts @@ -12,6 +12,10 @@ import { get as glimmerGet, fn as glimmerFn, neq as glimmerNeq, + gt as glimmerGt, + gte as glimmerGte, + lt as glimmerLt, + lte as glimmerLte, } from '@glimmer/runtime'; import { element as glimmerElement, uniqueId as glimmerUniqueId } from '@ember/-internals/glimmer'; import { type Opaque } from '@ember/-internals/utility-types'; @@ -472,6 +476,102 @@ export interface GetHelper extends Opaque<'helper:get'> {} export const fn = glimmerFn as FnHelper; export interface FnHelper extends Opaque<'helper:fn'> {} +/** + * The `{{gt}}` helper returns `true` if the first argument is greater than + * the second argument. + * + * ```js + * import { gt } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `gt` is available as a keyword and + * does not need to be imported. + * + * @method gt + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const gt = glimmerGt as unknown as GtHelper; +export interface GtHelper extends Opaque<'helper:gt'> {} + +/** + * The `{{gte}}` helper returns `true` if the first argument is greater than + * or equal to the second argument. + * + * ```js + * import { gte } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `gte` is available as a keyword and + * does not need to be imported. + * + * @method gte + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const gte = glimmerGte as unknown as GteHelper; +export interface GteHelper extends Opaque<'helper:gte'> {} + +/** + * The `{{lt}}` helper returns `true` if the first argument is less than + * the second argument. + * + * ```js + * import { lt } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `lt` is available as a keyword and + * does not need to be imported. + * + * @method lt + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const lt = glimmerLt as unknown as LtHelper; +export interface LtHelper extends Opaque<'helper:lt'> {} + +/** + * The `{{lte}}` helper returns `true` if the first argument is less than + * or equal to the second argument. + * + * ```js + * import { lte } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `lte` is available as a keyword and + * does not need to be imported. + * + * @method lte + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const lte = glimmerLte as unknown as LteHelper; +export interface LteHelper extends Opaque<'helper:lte'> {} + /** * The `element` helper lets you dynamically set the tag name of an element. * diff --git a/packages/@ember/template-compiler/lib/compile-options.ts b/packages/@ember/template-compiler/lib/compile-options.ts index 10fb8e0bf1a..03a5f3e78a6 100644 --- a/packages/@ember/template-compiler/lib/compile-options.ts +++ b/packages/@ember/template-compiler/lib/compile-options.ts @@ -1,4 +1,4 @@ -import { array, eq, fn, hash, neq } from '@ember/helper'; +import { array, eq, fn, hash, neq, lt, lte, gt, gte } from '@ember/helper'; import { on } from '@ember/modifier'; import { assert } from '@ember/debug'; import { @@ -30,6 +30,10 @@ export const keywords: Record = { fn, hash, neq, + gt, + gte, + lt, + lte, on, }; diff --git a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts index bf3ee5b993d..3ae501b7e6c 100644 --- a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts +++ b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts @@ -42,6 +42,18 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP if (isNeq(node, hasLocal)) { rewriteKeyword(env, node, 'neq', '@ember/helper'); } + if (isGt(node, hasLocal)) { + rewriteKeyword(env, node, 'gt', '@ember/helper'); + } + if (isGte(node, hasLocal)) { + rewriteKeyword(env, node, 'gte', '@ember/helper'); + } + if (isLt(node, hasLocal)) { + rewriteKeyword(env, node, 'lt', '@ember/helper'); + } + if (isLte(node, hasLocal)) { + rewriteKeyword(env, node, 'lte', '@ember/helper'); + } }, MustacheStatement(node: AST.MustacheStatement) { if (isArray(node, hasLocal)) { @@ -59,6 +71,18 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP if (isNeq(node, hasLocal)) { rewriteKeyword(env, node, 'neq', '@ember/helper'); } + if (isGt(node, hasLocal)) { + rewriteKeyword(env, node, 'gt', '@ember/helper'); + } + if (isGte(node, hasLocal)) { + rewriteKeyword(env, node, 'gte', '@ember/helper'); + } + if (isLt(node, hasLocal)) { + rewriteKeyword(env, node, 'lt', '@ember/helper'); + } + if (isLte(node, hasLocal)) { + rewriteKeyword(env, node, 'lte', '@ember/helper'); + } }, }, }; @@ -120,3 +144,31 @@ function isNeq( ): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { return isPath(node.path) && node.path.original === 'neq' && !hasLocal('neq'); } + +function isGt( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'gt' && !hasLocal('gt'); +} + +function isGte( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'gte' && !hasLocal('gte'); +} + +function isLt( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'lt' && !hasLocal('lt'); +} + +function isLte( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'lte' && !hasLocal('lte'); +} diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts new file mode 100644 index 00000000000..a4503e4979b --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts @@ -0,0 +1,70 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordGtRuntime extends RenderTest { + static suiteName = 'keyword helper: gt (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 3, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gt: () => false, a: 3, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { + let a = 3; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + static { + template('{{if (gt this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + a = 3; + b = 2; + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordGtRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts new file mode 100644 index 00000000000..530ba7b1c2d --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts @@ -0,0 +1,110 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; + +class KeywordGt extends RenderTest { + static suiteName = 'keyword helper: gt'; + + @test + 'explicit scope'() { + let a = 3; + let b = 2; + + const compiled = template('{{gt a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 3; + let b = 2; + let gt = () => 'surprise'; + const compiled = template('{{gt a b}}', { + strictMode: true, + scope: () => ({ gt, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 3; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'returns false when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{gt a}}', { + strictMode: true, + scope: () => ({ a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`gt` expects exactly two arguments/); + } +} + +jitSuite(KeywordGt); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts new file mode 100644 index 00000000000..9edbba57685 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts @@ -0,0 +1,70 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordGteRuntime extends RenderTest { + static suiteName = 'keyword helper: gte (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gte: () => false, a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + static { + template('{{if (gte this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + a = 2; + b = 2; + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordGteRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts new file mode 100644 index 00000000000..b13d97b0524 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts @@ -0,0 +1,110 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; + +class KeywordGte extends RenderTest { + static suiteName = 'keyword helper: gte'; + + @test + 'explicit scope'() { + let a = 2; + let b = 2; + + const compiled = template('{{gte a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 3; + let b = 2; + let gte = () => 'surprise'; + const compiled = template('{{gte a b}}', { + strictMode: true, + scope: () => ({ gte, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{gte a}}', { + strictMode: true, + scope: () => ({ a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`gte` expects exactly two arguments/); + } +} + +jitSuite(KeywordGte); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts new file mode 100644 index 00000000000..5b0913c5e1c --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts @@ -0,0 +1,70 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordLtRuntime extends RenderTest { + static suiteName = 'keyword helper: lt (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 1, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lt: () => false, a: 1, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { + let a = 1; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + static { + template('{{if (lt this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + a = 1; + b = 2; + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordLtRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts new file mode 100644 index 00000000000..49e14c56170 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts @@ -0,0 +1,110 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; + +class KeywordLt extends RenderTest { + static suiteName = 'keyword helper: lt'; + + @test + 'explicit scope'() { + let a = 1; + let b = 2; + + const compiled = template('{{lt a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 1; + let b = 2; + let lt = () => 'surprise'; + const compiled = template('{{lt a b}}', { + strictMode: true, + scope: () => ({ lt, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 1; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'returns false when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{lt a}}', { + strictMode: true, + scope: () => ({ a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`lt` expects exactly two arguments/); + } +} + +jitSuite(KeywordLt); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts new file mode 100644 index 00000000000..98145ec893f --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts @@ -0,0 +1,70 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordLteRuntime extends RenderTest { + static suiteName = 'keyword helper: lte (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lte: () => false, a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + static { + template('{{if (lte this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + a = 2; + b = 2; + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordLteRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts new file mode 100644 index 00000000000..7cd01eeaf4b --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts @@ -0,0 +1,110 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; + +class KeywordLte extends RenderTest { + static suiteName = 'keyword helper: lte'; + + @test + 'explicit scope'() { + let a = 2; + let b = 2; + + const compiled = template('{{lte a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 1; + let b = 2; + let lte = () => 'surprise'; + const compiled = template('{{lte a b}}', { + strictMode: true, + scope: () => ({ lte, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{lte a}}', { + strictMode: true, + scope: () => ({ a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`lte` expects exactly two arguments/); + } +} + +jitSuite(KeywordLte); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts index 617edd8ae30..2012ced0f31 100644 --- a/packages/@glimmer/runtime/index.ts +++ b/packages/@glimmer/runtime/index.ts @@ -36,8 +36,12 @@ export { concat } from './lib/helpers/concat'; export { eq } from './lib/helpers/eq'; export { fn } from './lib/helpers/fn'; export { get } from './lib/helpers/get'; +export { gt } from './lib/helpers/gt'; +export { gte } from './lib/helpers/gte'; export { hash } from './lib/helpers/hash'; export { invokeHelper } from './lib/helpers/invoke'; +export { lt } from './lib/helpers/lt'; +export { lte } from './lib/helpers/lte'; export { neq } from './lib/helpers/neq'; export { on } from './lib/modifiers/on'; export { renderComponent, renderMain, renderSync } from './lib/render'; diff --git a/packages/@glimmer/runtime/lib/helpers/gt.ts b/packages/@glimmer/runtime/lib/helpers/gt.ts new file mode 100644 index 00000000000..cca687dceb6 --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/gt.ts @@ -0,0 +1,14 @@ +import { DEBUG } from '@glimmer/env'; + +/** + * Performs a greater than comparison. + * + * left > right + */ +export function gt(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`gt\` expects exactly two arguments, but received ${arguments.length}.`); + } + + return (left as number) > (right as number); +} diff --git a/packages/@glimmer/runtime/lib/helpers/gte.ts b/packages/@glimmer/runtime/lib/helpers/gte.ts new file mode 100644 index 00000000000..78621a3383d --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/gte.ts @@ -0,0 +1,14 @@ +import { DEBUG } from '@glimmer/env'; + +/** + * Performs a greater than or equal comparison. + * + * left >= right + */ +export function gte(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`gte\` expects exactly two arguments, but received ${arguments.length}.`); + } + + return (left as number) >= (right as number); +} diff --git a/packages/@glimmer/runtime/lib/helpers/lt.ts b/packages/@glimmer/runtime/lib/helpers/lt.ts new file mode 100644 index 00000000000..95e7e39035b --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/lt.ts @@ -0,0 +1,14 @@ +import { DEBUG } from '@glimmer/env'; + +/** + * Performs a less than comparison. + * + * left < right + */ +export function lt(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`lt\` expects exactly two arguments, but received ${arguments.length}.`); + } + + return (left as number) < (right as number); +} diff --git a/packages/@glimmer/runtime/lib/helpers/lte.ts b/packages/@glimmer/runtime/lib/helpers/lte.ts new file mode 100644 index 00000000000..5393cf76139 --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/lte.ts @@ -0,0 +1,14 @@ +import { DEBUG } from '@glimmer/env'; + +/** + * Performs a less than or equal comparison. + * + * left <= right + */ +export function lte(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`lte\` expects exactly two arguments, but received ${arguments.length}.`); + } + + return (left as number) <= (right as number); +} diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index e1c1afc3cc3..587b3bbcaf3 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -442,6 +442,57 @@ function basicTest(scenarios: Scenarios, appName: string) { }); }); `, + 'lt-lte-gt-gte-as-keyword-test.gjs': ` + import { module, test } from 'qunit'; + import { setupRenderingTest } from 'ember-qunit'; + import { render } from '@ember/test-helpers'; + + module('{{lt}} / {{lte}} / {{gt}} / {{gte}} as keywords', function(hooks) { + setupRenderingTest(hooks); + + test('it works', async function(assert) { + let a = 1; + let b = 2; + + await render( + + ); + + assert.dom('[data-lt]').hasText('true'); + assert.dom('[data-lte]').hasText('true'); + assert.dom('[data-gt]').hasText('true'); + assert.dom('[data-gte]').hasText('true'); + }); + + test('can be shadowed', async function (assert) { + let a = 1; + let b = 2; + let lt = () => 'surprise:lt'; + let lte = () => 'surprise:lte'; + let gt = () => 'surprise:gt'; + let gte = () => 'surprise:gte'; + + await render( + + ); + + assert.dom('[data-lt]').hasText('surprise:lt'); + assert.dom('[data-lte]').hasText('surprise:lte'); + assert.dom('[data-gt]').hasText('surprise:gt'); + assert.dom('[data-gte]').hasText('surprise:gte'); + }); + }); + `, 'fn-as-keyword-but-its-shadowed-test.gjs': ` import QUnit, { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit';