diff --git a/lib/utils/html-tag.html b/lib/utils/html-tag.html
index 6745e2cce4..e6189cde7c 100644
--- a/lib/utils/html-tag.html
+++ b/lib/utils/html-tag.html
@@ -12,28 +12,68 @@
(function() {
'use strict';
+ /**
+ * Class representing a static string value which can be used to filter
+ * strings by asseting that they have been created via this class. The
+ * `value` property returns the string passed to the constructor.
+ */
+ class LiteralString {
+ constructor(string) {
+ /** @type {string} */
+ this.value = string.toString();
+ }
+ /**
+ * @return {string} LiteralString string value
+ */
+ toString() {
+ return this.value;
+ }
+ }
+
+ /**
+ * @param {*} value Object to stringify into HTML
+ * @return {string} HTML stringified form of `obj`
+ */
+ function literalValue(value) {
+ if (value instanceof LiteralString) {
+ return /** @type {!LiteralString} */(value).value;
+ } else {
+ throw new Error(`non-literal value passed to Polymer.htmlLiteral: ${value}`);
+ }
+ }
+
/**
* @param {*} value Object to stringify into HTML
* @return {string} HTML stringified form of `obj`
*/
function htmlValue(value) {
if (value instanceof HTMLTemplateElement) {
- return /** @type {!HTMLTemplateElement} */(value).innerHTML;
+ return /** @type {!HTMLTemplateElement } */(value).innerHTML;
+ } else if (value instanceof LiteralString) {
+ return literalValue(value);
} else {
- return String(value);
+ throw new Error(`non-template value passed to Polymer.html: ${value}`);
}
}
/**
- * A template literal tag that creates an HTML element from the contents of the string.
+ * A template literal tag that creates an HTML element from the
+ * contents of the string.
*
* This allows you to write a Polymer Template in JavaScript.
*
- * Interpolated values are converted to strings when the template is created,
- * they are not intended as a replacement for Polymer data-binding.
+ * Templates can be composed by interpolating `HTMLTemplateElement`s in
+ * expressions in the JavaScript template literal. The nested template's
+ * `innerHTML` is included in the containing template. The only other
+ * values allowed in expressions are those returned from `Polymer.htmlLiteral`
+ * which ensures only literal values from JS source ever reach the HTML, to
+ * guard against XSS risks.
*
- * There is special support for HTMLTemplateElement values,
- * allowing for easy composition of superclass or partial templates.
+ * All other values are disallowed in expressions to help prevent XSS
+ * attacks; however, `Polymer.htmlLiteral` can be used to compose static
+ * string values into templates. This is useful to compose strings into
+ * places that do not accept html, like the css text of a `style`
+ * element.
*
* Example:
*
@@ -54,8 +94,36 @@
Polymer.html = function html(strings, ...values) {
const template = /** @type {!HTMLTemplateElement} */(document.createElement('template'));
template.innerHTML = values.reduce((acc, v, idx) =>
- acc + htmlValue(v) + strings[idx + 1], strings[0]);
+ acc + htmlValue(v) + strings[idx + 1], strings[0]);
return template;
};
+
+ /**
+ * An html literal tag that can be used with `Polymer.html` to compose.
+ * a literal string.
+ *
+ * Example:
+ *
+ * static get template() {
+ * return Polymer.html`
+ *
+ * ${staticValue}
+ * ${super.template}
+ * `;
+ * }
+ * static get styleTemplate() { return Polymer.htmlLiteral`.shadowed { background: gray; }`; }
+ *
+ * @memberof Polymer
+ * @param {!ITemplateArray} strings Constant parts of tagged template literal
+ * @param {...*} values Variable parts of tagged template literal
+ * @return {!LiteralString} Constructed literal string
+ */
+ Polymer.htmlLiteral = function(strings, ...values) {
+ return new LiteralString(values.reduce((acc, v, idx) =>
+ acc + literalValue(v) + strings[idx + 1], strings[0]));
+ };
})();
diff --git a/test/unit/html-tag.html b/test/unit/html-tag.html
index b633853ff9..223ec51670 100644
--- a/test/unit/html-tag.html
+++ b/test/unit/html-tag.html
@@ -32,19 +32,26 @@
let t = Polymer.html`the \`price\` is \${{foo}}\n`;
assert.equal(t.innerHTML, 'the `price` is ${{foo}}\n');
});
- test('variables work', function() {
- let a = 3;
- let b = 'foo';
- let t = Polymer.html`${a} ${b}
`;
- let inst = document.createElement('div');
- inst.appendChild(t.content.cloneNode(true));
- assert.equal(inst.textContent, `${a} ${b}`);
+ test('interpolation of non-templates throw', function() {
+ assert.throws(() => Polymer.html`${'a'}
`);
});
- test('template variables only include the template contents', function() {
+ test('interpolation of templates include the template contents', function() {
let t1 = Polymer.html``;
let t2 = Polymer.html`${t1}`;
assert.equal(t2.innerHTML, '');
});
+ test('interpolation of literal values', function() {
+ let s1 = Polymer.htmlLiteral`hello`;
+ assert.equal(s1.value, `hello`);
+ let s2 = Polymer.htmlLiteral`hello${Polymer.htmlLiteral` ${Polymer.htmlLiteral`world`}!`}!`;
+ assert.equal(s2.value, 'hello world!!');
+ });
+ test('interpolation of literal values within html values', function() {
+ let s1 = Polymer.htmlLiteral`foo`;
+ let s2 = Polymer.htmlLiteral`display: block;`;
+ let t = Polymer.html``;
+ assert.equal(t.innerHTML, ``);
+ });
});
suite('Polymer + html fn', function() {
suiteSetup(function() {
@@ -131,4 +138,4 @@
});