Skip to content

Commit

Permalink
user providable additional context, minor test refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerwyn committed Dec 8, 2023
1 parent c01270d commit 04c1421
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 39 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion dist/renderTemplate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 3 additions & 2 deletions dist/renderTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
7 changes: 6 additions & 1 deletion src/renderTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
Expand Down
6 changes: 4 additions & 2 deletions tests/hass.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const hassTestObject = {
import { HomeAssistant } from 'custom-card-helpers';

export const hass = {
states: {
'light.lounge': {
state: 'on',
Expand Down Expand Up @@ -35,4 +37,4 @@ export const hassTestObject = {
},
},
},
};
} as unknown as HomeAssistant;
51 changes: 40 additions & 11 deletions tests/renderTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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 }}';
Expand All @@ -55,21 +51,21 @@ 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('');

str = '{{ hass["states"]["light.lounge"]["state"] == "off" }}';
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"] }}';
Expand All @@ -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');
Expand All @@ -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);
});
14 changes: 5 additions & 9 deletions tests/utils/iif.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
Expand All @@ -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';
Expand All @@ -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';
Expand Down
16 changes: 6 additions & 10 deletions tests/utils/states.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { HomeAssistant } from 'custom-card-helpers';

import { hassTestObject } from '../hass';
import { hass } from '../hass';
import { renderTemplate } from '../../src';
import {
states,
Expand All @@ -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);
Expand All @@ -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(
Expand All @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 04c1421

Please sign in to comment.