Skip to content

Commit

Permalink
Add literal tag to static module. Fixes lit#1713 (lit#1714)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpschaaf authored Apr 1, 2021
1 parent 20b4dd3 commit 63ca33b
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 14 deletions.
42 changes: 40 additions & 2 deletions packages/lit-html/src/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {html as coreHtml, svg as coreSvg, TemplateResult} from './lit-html.js';
* Wraps a string so that it behaves like part of the static template
* strings instead of a dynamic value.
*
* This is a very unsafe operation and may break templates if changes
* the structure of a template. Do not pass user input to this function
* Users must take care to ensure that adding the static string to the template
* results in well-formed HTML, or else templates may break unexpectedly.
*
* Note that this function is unsafe to use on untrusted content, as it will be
* directly parsed into HTML. Do not pass user input to this function
* without sanitizing it.
*
* Static values can be changed, but they will cause a complete re-render
Expand All @@ -20,6 +23,41 @@ export const unsafeStatic = (value: string) => ({
_$litStatic$: value,
});

const textFromStatic = (value: StaticValue) => {
if (value._$litStatic$ !== undefined) {
return value._$litStatic$;
} else {
throw new Error(
`Value passed to 'literal' function must be a 'literal' result: ${value}. Use 'unsafeStatic' to pass non-literal values, but
take care to ensure page security.`
);
}
};

/**
* Tags a string literal so that it behaves like part of the static template
* strings instead of a dynamic value.
*
* The only values that may be used in template expressions are other tagged
* `literal` results or `unsafeStatic` values (note that untrusted content
* should never be passed to `unsafeStatic`).
*
* Users must take care to ensure that adding the static string to the template
* results in well-formed HTML, or else templates may break unexpectedly.
*
* Static values can be changed, but they will cause a complete re-render since
* they effectively create a new template.
*/
export const literal = (
strings: TemplateStringsArray,
...values: unknown[]
) => ({
_$litStatic$: values.reduce(
(acc, v, idx) => acc + textFromStatic(v as StaticValue) + strings[idx + 1],
strings[0]
),
});

type StaticValue = ReturnType<typeof unsafeStatic>;

const stringsCache = new Map<string, TemplateStringsArray>();
Expand Down
62 changes: 50 additions & 12 deletions packages/lit-html/src/test/static_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
*/
import {render} from '../lit-html.js';
import {html, unsafeStatic} from '../static.js';
import {html, literal, unsafeStatic} from '../static.js';
import {assert} from '@esm-bundle/chai';
import {stripExpressionComments} from './test-utils/strip-markers.js';

Expand All @@ -16,13 +16,13 @@ suite('static', () => {
});

test('static text binding', () => {
render(html`${unsafeStatic('<p>Hello</p>')}`, container);
render(html`${literal`<p>Hello</p>`}`, container);
// If this were a dynamic binding, the tags would be escaped
assert.equal(stripExpressionComments(container.innerHTML), '<p>Hello</p>');
});

test('static attribute binding', () => {
render(html`<div class="${unsafeStatic('cool')}"></div>`, container);
render(html`<div class="${literal`cool`}"></div>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div class="cool"></div>'
Expand All @@ -32,45 +32,42 @@ suite('static', () => {
});

test('static tag binding', () => {
const tagName = unsafeStatic('div');
const tagName = literal`div`;
render(html`<${tagName}>${'A'}</${tagName}>`, container);
assert.equal(stripExpressionComments(container.innerHTML), '<div>A</div>');
});

test('static attribute name binding', () => {
render(html`<div ${unsafeStatic('foo')}="${'bar'}"></div>`, container);
render(html`<div ${literal`foo`}="${'bar'}"></div>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div foo="bar"></div>'
);

render(html`<div x-${unsafeStatic('foo')}="${'bar'}"></div>`, container);
render(html`<div x-${literal`foo`}="${'bar'}"></div>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div x-foo="bar"></div>'
);
});

test('static attribute name binding', () => {
render(
html`<div ${unsafeStatic('foo')}="${unsafeStatic('bar')}"></div>`,
container
);
render(html`<div ${literal`foo`}="${literal`bar`}"></div>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div foo="bar"></div>'
);
});

test('dynamic binding after static text binding', () => {
render(html`${unsafeStatic('<p>Hello</p>')}${'<p>World</p>'}`, container);
render(html`${literal`<p>Hello</p>`}${'<p>World</p>'}`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<p>Hello</p>&lt;p&gt;World&lt;/p&gt;'
);

// Make sure `null` is handled
render(html`${unsafeStatic('<p>Hello</p>')}${null}`, container);
render(html`${literal`<p>Hello</p>`}${null}`, container);
assert.equal(stripExpressionComments(container.innerHTML), '<p>Hello</p>');
});

Expand Down Expand Up @@ -126,4 +123,45 @@ suite('static', () => {
// previously used value does not restore static DOM
assert.notStrictEqual(div3, div);
});

test('interpolating statics into statics', () => {
const start = literal`<${literal`sp${literal`an`}`}>`;
const end = literal`</${unsafeStatic('span')}>`;
render(html`<div>a${start}b${end}c</div>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div>a<span>b</span>c</div>'
);
});

test('interpolating non-statics into statics throws', () => {
assert.throws(() => {
literal`a${literal`bar`}b${'shouldthrow'}`;
});
});

suite('unsafe', () => {
test('static tag binding', () => {
const tagName = unsafeStatic('div');
render(html`<${tagName}>${'A'}</${tagName}>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div>A</div>'
);
});

test('static attribute name binding', () => {
render(html`<div ${unsafeStatic('foo')}="${'bar'}"></div>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div foo="bar"></div>'
);

render(html`<div x-${unsafeStatic('foo')}="${'bar'}"></div>`, container);
assert.equal(
stripExpressionComments(container.innerHTML),
'<div x-foo="bar"></div>'
);
});
});
});

0 comments on commit 63ca33b

Please sign in to comment.