From 04c142129de622bcc4f6f366ee3f79d7fee4cef2 Mon Sep 17 00:00:00 2001 From: Nerwyn Singh Date: Thu, 7 Dec 2023 20:07:39 -0500 Subject: [PATCH] user providable additional context, minor test refactor --- README.md | 15 +++++++++++ dist/renderTemplate.d.ts | 3 ++- dist/renderTemplate.js | 5 ++-- package-lock.json | 4 +-- package.json | 2 +- src/renderTemplate.ts | 7 ++++- tests/hass.ts | 6 +++-- tests/renderTemplate.test.ts | 51 ++++++++++++++++++++++++++++-------- tests/utils/iif.test.ts | 14 ++++------ tests/utils/states.test.ts | 16 +++++------ 10 files changed, 84 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 91feeff..c05d960 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,21 @@ const renderedString = renderTemplate(this.hass, templateString); Rather than rendering templates on the backend, nunjucks renders templates on the frontend. This repository uses the Home Assistant object present in all custom cards to read entity state data. +You can also provide additional context to the `renderTemplate` function to pass to nunjucks if you want to make additional variables or project specific functions available to your users for use in templates. + +```typescript +import { renderTemplate } from 'ha-nunjucks'; + +const context = { + foo: 'bar', + doThing(thing: string) { + return `doing ${thing}!`; + }, +}; + +const renderedString = renderTemplate(this.hass, templateString, context); +``` + ## Available Extensions The catch to this approach of rendering jinja2/nunjucks templates is that we have to reimplement all of the [Home Assistant template extension](https://www.home-assistant.io/docs/configuration/templating/#home-assistant-template-extensions) functions and filters. If there are functions or filters that you use that are not currently supported, please make a feature request or try adding it to the project yourself and create a pull request. diff --git a/dist/renderTemplate.d.ts b/dist/renderTemplate.d.ts index b94a1f3..a4a68fd 100644 --- a/dist/renderTemplate.d.ts +++ b/dist/renderTemplate.d.ts @@ -3,6 +3,7 @@ import { HomeAssistant } from 'custom-card-helpers'; * Render a Home Assistant template string using nunjucks * @param {HomeAssistant} hass The Home Assistant object * @param {string} str The template string to render + * @param {object} [context] Additional context to expose to nunjucks * @returns {string} The rendered template string if a string was provided, otherwise the unaltered input */ -export declare function renderTemplate(hass: HomeAssistant, str: string): string | number | boolean; +export declare function renderTemplate(hass: HomeAssistant, str: string, context?: object): string | number | boolean; diff --git a/dist/renderTemplate.js b/dist/renderTemplate.js index 6086128..56375e8 100644 --- a/dist/renderTemplate.js +++ b/dist/renderTemplate.js @@ -7,13 +7,14 @@ const context_1 = require("./context"); * Render a Home Assistant template string using nunjucks * @param {HomeAssistant} hass The Home Assistant object * @param {string} str The template string to render + * @param {object} [context] Additional context to expose to nunjucks * @returns {string} The rendered template string if a string was provided, otherwise the unaltered input */ -function renderTemplate(hass, str) { +function renderTemplate(hass, str, context) { if (typeof str == 'string' && ((str.includes('{{') && str.includes('}}')) || (str.includes('{%') && str.includes('%}')))) { - str = (0, nunjucks_1.renderString)(structuredClone(str), (0, context_1.CONTEXT)(hass)).trim(); + str = (0, nunjucks_1.renderString)(structuredClone(str), Object.assign(Object.assign({}, (0, context_1.CONTEXT)(hass)), context)).trim(); if ([undefined, null, 'undefined', 'null', 'None'].includes(str)) { return ''; } diff --git a/package-lock.json b/package-lock.json index 938eb9f..0211c44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ha-nunjucks", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ha-nunjucks", - "version": "1.1.0", + "version": "1.2.0", "license": "ISC", "dependencies": { "@types/nunjucks": "^3.2.6", diff --git a/package.json b/package.json index afda577..446a245 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ha-nunjucks", - "version": "1.1.0", + "version": "1.2.0", "description": "Wrapper for nunjucks for use with Home Assistant frontend custom components to render templates", "main": "./dist/index.js", "scripts": { diff --git a/src/renderTemplate.ts b/src/renderTemplate.ts index c0fbd73..724d6ec 100644 --- a/src/renderTemplate.ts +++ b/src/renderTemplate.ts @@ -7,18 +7,23 @@ import { CONTEXT } from './context'; * Render a Home Assistant template string using nunjucks * @param {HomeAssistant} hass The Home Assistant object * @param {string} str The template string to render + * @param {object} [context] Additional context to expose to nunjucks * @returns {string} The rendered template string if a string was provided, otherwise the unaltered input */ export function renderTemplate( hass: HomeAssistant, str: string, + context?: object, ): string | number | boolean { if ( typeof str == 'string' && ((str.includes('{{') && str.includes('}}')) || (str.includes('{%') && str.includes('%}'))) ) { - str = renderString(structuredClone(str), CONTEXT(hass)).trim(); + str = renderString(structuredClone(str), { + ...CONTEXT(hass), + ...context, + }).trim(); if ([undefined, null, 'undefined', 'null', 'None'].includes(str)) { return ''; diff --git a/tests/hass.ts b/tests/hass.ts index 6ba6402..e85ed4a 100644 --- a/tests/hass.ts +++ b/tests/hass.ts @@ -1,4 +1,6 @@ -export const hassTestObject = { +import { HomeAssistant } from 'custom-card-helpers'; + +export const hass = { states: { 'light.lounge': { state: 'on', @@ -35,4 +37,4 @@ export const hassTestObject = { }, }, }, -}; +} as unknown as HomeAssistant; diff --git a/tests/renderTemplate.test.ts b/tests/renderTemplate.test.ts index 3d9d765..49cee45 100644 --- a/tests/renderTemplate.test.ts +++ b/tests/renderTemplate.test.ts @@ -1,10 +1,6 @@ -import { HomeAssistant } from 'custom-card-helpers'; - -import { hassTestObject } from './hass'; +import { hass } from './hass'; import { renderTemplate } from '../src'; -const hass = hassTestObject as unknown as HomeAssistant; - test('Returns input if it is not a string.', () => { expect(renderTemplate(hass, 5 as unknown as string)).toBe(5); expect(renderTemplate(hass, 0 as unknown as string)).toBe(0); @@ -32,12 +28,12 @@ test('Returns input if it is not a string.', () => { ).toStrictEqual({ foo: 'bar', baz: 'bah' }); }); -test('Returns input string if it is a string but does not include a template', () => { +test('Returns input string if it is a string but does not include a template.', () => { expect(renderTemplate(hass, 'foobar')).toBe('foobar'); expect(renderTemplate(hass, '')).toBe(''); }); -test('Returns input string if it is a string but does not contain a complete template', () => { +test('Returns input string if it is a string but does not contain a complete template.', () => { let str = '{{ not a template'; expect(renderTemplate(hass, str)).toBe(str); str = 'not a template }}'; @@ -55,13 +51,13 @@ test('Returns input string if it is a string but does not contain a complete tem expect(renderTemplate(hass, str)).toBe(str); }); -test('Returns result of simple templates and does not modify the input', () => { +test('Returns result of simple templates and does not modify the input.', () => { const str = '{{ hass["states"]["light.lounge"]["state"] }}'; expect(renderTemplate(hass, str)).toBe('on'); expect(str).toBe('{{ hass["states"]["light.lounge"]["state"] }}'); }); -test('Returns empty string if result of template is undefined or null, but not if it is falsey', () => { +test('Returns empty string if result of template is undefined or null, but not if it is falsey.', () => { let str = '{{ hass["states"]["light.lounge"]["status"] }}'; expect(renderTemplate(hass, str)).toBe(''); @@ -69,7 +65,7 @@ test('Returns empty string if result of template is undefined or null, but not i expect(renderTemplate(hass, str)).toBe(false); }); -test('Return type should be number if original value is a number', () => { +test('Return type should be number if original value is a number.', () => { let value = hass['states']['light.lounge']['attributes']['brightness']; let str = '{{ hass["states"]["light.lounge"]["attributes"]["brightness"] }}'; @@ -84,7 +80,7 @@ test('Return type should be number if original value is a number', () => { expect(renderTemplate(hass, str)).toBe(value); }); -test('Return type should be boolean if original value is a boolean', () => { +test('Return type should be boolean if original value is a boolean.', () => { let value = 'foo' == 'foo'; let str = '{{ "foo" == "foo" }}'; expect(typeof value).toBe('boolean'); @@ -95,3 +91,36 @@ test('Return type should be boolean if original value is a boolean', () => { expect(typeof value).toBe('boolean'); expect(renderTemplate(hass, str)).toBe(false); }); + +test('Users should be able to add additional context and reference it in templates.', () => { + const context = { + foo: 'bar', + doThing(thing: string) { + return `doing ${thing}!`; + }, + }; + + let str = 'Testing that foo is {{ foo }}.'; + expect(renderTemplate(hass, str, context)).toBe('Testing that foo is bar.'); + + str = 'I am {{ doThing("the dishes") }}'; + expect(renderTemplate(hass, str, context)).toBe('I am doing the dishes!'); +}); + +test('Users should be able to still use the built in context when adding additional context.', () => { + const context = { + min: 'minimum', + doThing(thing: string) { + return `doing ${thing}!`; + }, + }; + + let value = hass['states']['light.lounge']['attributes']['min_mireds']; + let str = '{{ hass.states["light.lounge"].attributes.min_mireds }}'; + expect(renderTemplate(hass, str, context)).toBe(value); + + value = `The minimum color temperature is ${hass['states']['light.lounge']['attributes']['min_mireds']} mireds. Also I'm doing my taxes!`; + str = + 'The {{ min }} color temperature is {{ hass.states["light.lounge"].attributes.min_mireds }} mireds. Also I\'m {{ doThing("my taxes") }}'; + expect(renderTemplate(hass, str, context)).toBe(value); +}); diff --git a/tests/utils/iif.test.ts b/tests/utils/iif.test.ts index 8715a77..830b61b 100644 --- a/tests/utils/iif.test.ts +++ b/tests/utils/iif.test.ts @@ -1,12 +1,8 @@ -import { HomeAssistant } from 'custom-card-helpers'; - -import { hassTestObject } from '../hass'; +import { hass } from '../hass'; import { renderTemplate } from '../../src'; import { iif } from '../../src/utils/iif'; -const hass = hassTestObject as unknown as HomeAssistant; - -test('Function iif should return true or false if only condition is given', () => { +test('Function iif should return true or false if only condition is given.', () => { let condition = '"foo" == "foo"'; expect(iif(hass, condition)).toBe(true); expect(renderTemplate(hass, `{{ iif(${condition}) }}`)).toBe(true); @@ -16,7 +12,7 @@ test('Function iif should return true or false if only condition is given', () = expect(renderTemplate(hass, `{{ iif(${condition}) }}`)).toBe(false); }); -test('Function iif should return if_true if condition is true or false otherwise', () => { +test('Function iif should return if_true if condition is true or false otherwise.', () => { let condition = '"foo" == "foo"'; const isTrue = 'is foo'; expect(iif(hass, condition, isTrue)).toBe(isTrue); @@ -31,7 +27,7 @@ test('Function iif should return if_true if condition is true or false otherwise ); }); -test('Function iif should return if_true if condition is true or if_false otherwise', () => { +test('Function iif should return if_true if condition is true or if_false otherwise.', () => { let condition = '"foo" == "foo"'; const isTrue = 'is foo'; const isFalse = 'is not foo'; @@ -53,7 +49,7 @@ test('Function iif should return if_true if condition is true or if_false otherw ).toBe(isFalse); }); -test('Function iif should return is_none if comparison', () => { +test('Function iif should return is_none if comparison.', () => { const condition = 'None'; const isTrue = 'is true'; const isFalse = 'is false'; diff --git a/tests/utils/states.test.ts b/tests/utils/states.test.ts index ad07cab..55e95ea 100644 --- a/tests/utils/states.test.ts +++ b/tests/utils/states.test.ts @@ -1,6 +1,4 @@ -import { HomeAssistant } from 'custom-card-helpers'; - -import { hassTestObject } from '../hass'; +import { hass } from '../hass'; import { renderTemplate } from '../../src'; import { states, @@ -10,9 +8,7 @@ import { has_value, } from '../../src/utils/states'; -const hass = hassTestObject as unknown as HomeAssistant; - -test('Function states should return state of an entity', () => { +test('Function states should return state of an entity.', () => { const value = hass['states']['light.lounge']['state']; expect(states(hass, 'light.lounge')).toBe(value); expect(renderTemplate(hass, '{{ states("light.lounge") }}')).toBe(value); @@ -21,7 +17,7 @@ test('Function states should return state of an entity', () => { expect(renderTemplate(hass, '{{ states("foobar") }}')).toBe(''); }); -test('Function is_state should return boolean', () => { +test('Function is_state should return boolean.', () => { const value = hass['states']['light.lounge']['state']; expect(is_state(hass, 'light.lounge', value)).toBe(true); expect( @@ -37,7 +33,7 @@ test('Function is_state should return boolean', () => { ); }); -test('Function state_attr should return attribute of an entity', () => { +test('Function state_attr should return attribute of an entity.', () => { let attribute = 'color_mode'; let value = hass['states']['light.lounge']['attributes'][attribute]; expect(typeof value).toBe('string'); @@ -66,7 +62,7 @@ test('Function state_attr should return attribute of an entity', () => { ); }); -test('Function is_state_attr should return boolean', () => { +test('Function is_state_attr should return boolean.', () => { let attribute = 'color_mode'; let value = hass['states']['light.lounge']['attributes'][attribute]; expect(is_state_attr(hass, 'light.lounge', attribute, value)).toBe(true); @@ -96,7 +92,7 @@ test('Function is_state_attr should return boolean', () => { ).toBe(false); }); -test('Function has_value should return boolean', () => { +test('Function has_value should return boolean.', () => { let entity = 'light.lounge'; expect(has_value(hass, entity)).toBe(true); expect(renderTemplate(hass, `{{ has_value("${entity}") }}`)).toBe(true);