Skip to content

Commit

Permalink
Only save empty DocumentFragments as keys to check that the previous …
Browse files Browse the repository at this point in the history
…render with with unsafeHTML.
  • Loading branch information
justinfagnani committed Dec 20, 2018
1 parent dcc7872 commit f35e2bb
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 37 deletions.
55 changes: 21 additions & 34 deletions src/directives/unsafe-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,42 @@
* http://polymer.github.io/PATENTS.txt
*/

import {isPrimitive} from '../lib/parts.js';
import {directive, NodePart, Part} from '../lit-html.js';

// For each part, remember the value that was last rendered to the part by the
// unsafeHTML directive, and the DocumentFragment that was last set as a value.
// The DocumentFragment is used as a unique key to check if the last value
// rendered to the part was with unsafeHTML. If not, we'll always re-render the
// value passed to unsafeHTML.
const previousValues = new WeakMap < NodePart, {
value: any;
fragment: DocumentFragment;
}
> ();

/**
* Renders the result as HTML, rather than text.
*
* Note, this is unsafe to use with any user-provided input that hasn't been
* sanitized or escaped, as it may lead to cross-site-scripting
* vulnerabilities.
*/

type CachedTemplate = {
template: HTMLTemplateElement; fragment: DocumentFragment;
};

// Use a cache for TemplateElements so we don't have to parse the same HTML
// string twice
const templateCache = new Map<string, HTMLTemplateElement>();

// For each part, remember the TemplateElement that was last used to render in
// that part, and the DocumentFragment that was last set as a value.
const partValues = new WeakMap<NodePart, CachedTemplate>();

export const unsafeHTML = directive((value: any) => (part: Part): void => {
if (!(part instanceof NodePart)) {
throw new Error('unsafeHTML can only be used in text bindings');
}

// Cast value to String only if necessary, to improve cache lookups
const htmlString = typeof value === 'string' ? value : String(value);
const previousValue = previousValues.get(part);

// Get a TemplateElement that represents this htmlString
let template = templateCache.get(htmlString);
if (!template) {
template = document.createElement('template');
template.innerHTML = htmlString;
templateCache.set(htmlString, template);
if (previousValue !== undefined && isPrimitive(value) &&
value === previousValue.value && part.value === previousValue.fragment) {
return;
}

const previousValue = partValues.get(part);
/**
* Need to render only if one of the following is true
* - This part never rendered unsafeHTML previously
* - The new template is different from the previousl template
* - The current value of the part is different from the previous fragment
*/
if (!previousValue || template !== previousValue.template ||
part.value !== previousValue.fragment) {
const fragment = document.importNode(template.content, true);
part.setValue(fragment);
partValues.set(part, {template, fragment});
}
const template = document.createElement('template');
template.innerHTML = value;
const fragment = document.importNode(template.content, true);
part.setValue(fragment);
previousValues.set(part, {value, fragment});
});
6 changes: 3 additions & 3 deletions src/test/lib/render_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@ suite('render()', () => {
test('renders noChange', () => {
const template = (i: any) => html`<div>${i}</div>`;
render(template('foo'), container);
render(template(noChange), container)
render(template(noChange), container);
assert.equal(
stripExpressionMarkers(container.innerHTML), '<div>foo</div>');
});

test('renders nothing', () => {
const template = (i: any) => html`<div>${i}</div>`;
render(template('foo'), container);
render(template(nothing), container)
render(template(nothing), container);
const children = Array.from(container.querySelector('div')!.childNodes);
assert.isEmpty(
children.filter(node => node.nodeType !== Node.COMMENT_NODE));
children.filter((node) => node.nodeType !== Node.COMMENT_NODE));
});

testIfHasSymbol('renders a Symbol', () => {
Expand Down

0 comments on commit f35e2bb

Please sign in to comment.