Skip to content

Instantly share code, notes, and snippets.

@oscarmarina
Last active July 17, 2020 06:32
Show Gist options
  • Select an option

  • Save oscarmarina/47e8b48b3bdcc0e105030d5e8d3f4674 to your computer and use it in GitHub Desktop.

Select an option

Save oscarmarina/47e8b48b3bdcc0e105030d5e8d3f4674 to your computer and use it in GitHub Desktop.
/* eslint-disable complexity */
const stringsCache = new WeakMap();
/**
* https://github.com/Polymer/lit-html/pull/274
*
* https://github.com/corpusculejs/corpuscule/blob/master/packages/lit-html-renderer/docs/withCustomElement.md
* https://github.com/bashmish/carehtml
*
* A value that's interpolated directly into the template before parsing.
*
* Static values cannot be updated, since they don't define a part and are
* effectively merged into the literal part of a lit-html template. Because
* they are interpolated before the template is parsed as HTML, static values
* may occupy positions in the template that regular interpolations may not,
* such as tag and attribute names.
*
* UnsafeStatic values are inherently very unsafe, as the name states. They
* can break well-formedness assumptions and aren't escaped, and thus a
* potential XSS vulnerability if created from user-provided data.
*
* It's recommended that no user templates ever use UnsafeStatic directly,
* but directive-like functions are written by library authors to validate
* and sanitize values for a specific purpose, before wrapping in an
* UnsafeStatic value.
*
* An example would be a `tag()` directive that lets a template contain tags
* whose names aren't known until runtime, like:
*
* html`<${tag(myTagName)}>Whoa</${tag(MyElement)}>`
*
* Here, `tag()` should validate that `myTagName` is a valid HTML tag name,
* and throw if it contains any illegal characters.
*/
export class UnsafeStatic {
constructor(value) {
this.value = value;
}
}
/**
* Interpolates a value before template parsing and making it available to
* template pre-processing steps.
*
* Static values cannot be updated since they don't define a part and are
* effectively merged into the literal part of a lit-html template. Because
* they are interpolated before the template is parsed as HTML, static values
* may occupy positions in the template that regular interpolations may not,
* such as tag and attribute names.
*
* @param {*} value convertable to string
* @returns {UnsafeStatic} value wrapped with UnsafeStatic
*/
export const unsafeStatic = value => new UnsafeStatic(value);
/**
* Decorates initial `html` function to produce preprocessed templates that
* include unsafe static values.
*
* No sanitization provided. Any static value is considered as a string and
* merged to a template, so it can lead to undefined behavior if you use
* angle brackets or any other html-specific symbols.
*
* Updating static values is impossible. If you try to replace one static
* value with another, it will be ignored, and if it is a dynamic value,
* the error will be thrown.
*
* @param {Function} processor `html` function
* @returns {Function} decorated `html` function
*/
export const withUnsafeStatic = processor => (strings, ...values) => {
let updateValues = false;
let finalStrings = stringsCache.get(strings);
if (finalStrings) {
for (let i = 0; i < finalStrings.finalValuesTotal.length; i++) {
if (values[i] instanceof UnsafeStatic && finalStrings.finalValuesTotal[i] !== values[i].value) {
updateValues = true;
}
}
}
if (!finalStrings || updateValues) {
// Convert the initial array of strings into a new one with merged
// static values. Values array is used only to know where the static
// value is placed.
if (values.some(v => v instanceof UnsafeStatic)) {
finalStrings = {
finalStringsTotal: [],
finalValuesTotal: []
};
let previousValueWasStatic = false;
for (let i = 0; i < strings.length; i++) {
if (previousValueWasStatic) {
// Append the string part that follows static value.
finalStrings.finalStringsTotal[finalStrings.finalStringsTotal.length - 1] += strings[i];
} else {
finalStrings.finalStringsTotal.push(strings[i]);
}
// Since length of values array is N and strings array is N+1,
// it is necessary to check if we have crossed the values
// boundaries.
if (i < values.length && values[i] instanceof UnsafeStatic) {
// Append static value.
finalStrings.finalStringsTotal[finalStrings.finalStringsTotal.length - 1] += String(values[i].value);
finalStrings.finalValuesTotal.push(String(values[i].value));
previousValueWasStatic = true;
} else {
previousValueWasStatic = false;
}
}
} else {
// If there is no static value remember original strings array
finalStrings.finalStringsTotal = strings;
}
stringsCache.set(strings, finalStrings);
}
// If there is static values remove all statics from it. Otherwise,
// just use original values array.
const finalValues =
finalStrings.finalStringsTotal !== strings ? values.filter(v => !(v instanceof UnsafeStatic)) : values;
// If user try to replace static value with dynamic one we cannot filter
// it. It produces different amount of filtered values we have so we can
// catch it and throw an error to avoid undefined behavior during template
// update.
if (finalValues.length >= finalStrings.finalStringsTotal.length) {
throw new Error(
'Amount of values provided does not fit amount of available parts. ' +
'It could happen if you try to change your UnsafeStatic value to a dynamic one.',
);
}
return processor(finalStrings.finalStringsTotal, ...finalValues);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment