@openhab-ui/setup-and-maintenance
Version:
Configuration and maintenance interface for openHAB
1,260 lines (1,248 loc) • 69.3 kB
JavaScript
class StringUtilities {
static format(template, ...values) {
return template.replace(/%s/g, function () {
return values.shift();
});
}
static containsAny(text, searchStrings) {
return searchStrings.some(c => {
return text.indexOf(c) > -1;
});
}
}
class CronParser {
constructor(expression, dayOfWeekStartIndexZero = true) {
this.expression = expression;
this.dayOfWeekStartIndexZero = dayOfWeekStartIndexZero;
}
parse() {
let parsed = this.extractParts(this.expression);
this.normalize(parsed);
this.validate(parsed);
return parsed;
}
extractParts(expression) {
if (!this.expression) {
throw new Error("Expression is empty");
}
let parsed = expression.trim().split(" ");
if (parsed.length < 5) {
throw new Error(`Expression has only ${parsed.length} part${parsed.length == 1 ? "" : "s"}. At least 5 parts are required.`);
}
else if (parsed.length == 5) {
parsed.unshift("");
parsed.push("");
}
else if (parsed.length == 6) {
if (/\d{4}$/.test(parsed[5])) {
parsed.unshift("");
}
else {
parsed.push("");
}
}
else if (parsed.length > 7) {
throw new Error(`Expression has ${parsed.length} parts; too many!`);
}
return parsed;
}
normalize(expressionParts) {
expressionParts[3] = expressionParts[3].replace("?", "*");
expressionParts[5] = expressionParts[5].replace("?", "*");
if (expressionParts[0].indexOf("0/") == 0) {
expressionParts[0] = expressionParts[0].replace("0/", "*/");
}
if (expressionParts[1].indexOf("0/") == 0) {
expressionParts[1] = expressionParts[1].replace("0/", "*/");
}
if (expressionParts[2].indexOf("0/") == 0) {
expressionParts[2] = expressionParts[2].replace("0/", "*/");
}
if (expressionParts[3].indexOf("1/") == 0) {
expressionParts[3] = expressionParts[3].replace("1/", "*/");
}
if (expressionParts[4].indexOf("1/") == 0) {
expressionParts[4] = expressionParts[4].replace("1/", "*/");
}
if (expressionParts[5].indexOf("1/") == 0) {
expressionParts[5] = expressionParts[5].replace("1/", "*/");
}
if (expressionParts[6].indexOf("1/") == 0) {
expressionParts[6] = expressionParts[6].replace("1/", "*/");
}
expressionParts[5] = expressionParts[5].replace(/(^\d)|([^#/\s]\d)/g, t => {
let dowDigits = t.replace(/\D/, "");
let dowDigitsAdjusted = dowDigits;
if (this.dayOfWeekStartIndexZero) {
if (dowDigits == "7") {
dowDigitsAdjusted = "0";
}
}
else {
dowDigitsAdjusted = (parseInt(dowDigits) - 1).toString();
}
return t.replace(dowDigits, dowDigitsAdjusted);
});
if (expressionParts[5] == "L") {
expressionParts[5] = "6";
}
if (expressionParts[3] == "?") {
expressionParts[3] = "*";
}
if (expressionParts[3].indexOf("W") > -1 &&
(expressionParts[3].indexOf(",") > -1 || expressionParts[3].indexOf("-") > -1)) {
throw new Error("The 'W' character can be specified only when the day-of-month is a single day, not a range or list of days.");
}
var days = {
SUN: 0,
MON: 1,
TUE: 2,
WED: 3,
THU: 4,
FRI: 5,
SAT: 6
};
for (let day in days) {
expressionParts[5] = expressionParts[5].replace(new RegExp(day, "gi"), days[day].toString());
}
var months = {
JAN: 1,
FEB: 2,
MAR: 3,
APR: 4,
MAY: 5,
JUN: 6,
JUL: 7,
AUG: 8,
SEP: 9,
OCT: 10,
NOV: 11,
DEC: 12
};
for (let month in months) {
expressionParts[4] = expressionParts[4].replace(new RegExp(month, "gi"), months[month].toString());
}
if (expressionParts[0] == "0") {
expressionParts[0] = "";
}
if (!/\*|\-|\,|\//.test(expressionParts[2]) &&
(/\*|\//.test(expressionParts[1]) || /\*|\//.test(expressionParts[0]))) {
expressionParts[2] += `-${expressionParts[2]}`;
}
for (let i = 0; i < expressionParts.length; i++) {
if (expressionParts[i] == "*/1") {
expressionParts[i] = "*";
}
if (expressionParts[i].indexOf("/") > -1 && !/^\*|\-|\,/.test(expressionParts[i])) {
let stepRangeThrough = null;
switch (i) {
case 4:
stepRangeThrough = "12";
break;
case 5:
stepRangeThrough = "6";
break;
case 6:
stepRangeThrough = "9999";
break;
default:
stepRangeThrough = null;
break;
}
if (stepRangeThrough != null) {
let parts = expressionParts[i].split("/");
expressionParts[i] = `${parts[0]}-${stepRangeThrough}/${parts[1]}`;
}
}
}
}
validate(parsed) {
this.assertNoInvalidCharacters("DOW", parsed[5]);
this.assertNoInvalidCharacters("DOM", parsed[3]);
}
assertNoInvalidCharacters(partDescription, expression) {
let invalidChars = expression.match(/[A-KM-VX-Z]+/gi);
if (invalidChars && invalidChars.length) {
throw new Error(`${partDescription} part contains invalid values: '${invalidChars.toString()}'`);
}
}
}
class ExpressionDescriptor {
constructor(expression, options) {
this.expression = expression;
this.options = options;
this.expressionParts = new Array(5);
if (ExpressionDescriptor.locales[options.locale]) {
this.i18n = ExpressionDescriptor.locales[options.locale];
}
else {
console.warn(`Locale '${options.locale}' could not be found; falling back to 'en'.`);
this.i18n = ExpressionDescriptor.locales["en"];
}
if (options.use24HourTimeFormat === undefined) {
options.use24HourTimeFormat = this.i18n.use24HourTimeFormatByDefault();
}
}
static toString(expression, { throwExceptionOnParseError = true, verbose = false, dayOfWeekStartIndexZero = true, use24HourTimeFormat, locale = "en" } = {}) {
let options = {
throwExceptionOnParseError: throwExceptionOnParseError,
verbose: verbose,
dayOfWeekStartIndexZero: dayOfWeekStartIndexZero,
use24HourTimeFormat: use24HourTimeFormat,
locale: locale
};
let descripter = new ExpressionDescriptor(expression, options);
return descripter.getFullDescription();
}
static initialize(localesLoader) {
ExpressionDescriptor.specialCharacters = ["/", "-", ",", "*"];
localesLoader.load(ExpressionDescriptor.locales);
}
getFullDescription() {
let description = "";
try {
let parser = new CronParser(this.expression, this.options.dayOfWeekStartIndexZero);
this.expressionParts = parser.parse();
var timeSegment = this.getTimeOfDayDescription();
var dayOfMonthDesc = this.getDayOfMonthDescription();
var monthDesc = this.getMonthDescription();
var dayOfWeekDesc = this.getDayOfWeekDescription();
var yearDesc = this.getYearDescription();
description += timeSegment + dayOfMonthDesc + dayOfWeekDesc + monthDesc + yearDesc;
description = this.transformVerbosity(description, this.options.verbose);
description = description.charAt(0).toLocaleUpperCase() + description.substr(1);
}
catch (ex) {
if (!this.options.throwExceptionOnParseError) {
description = this.i18n.anErrorOccuredWhenGeneratingTheExpressionD();
}
else {
throw `${ex}`;
}
}
return description;
}
getTimeOfDayDescription() {
let secondsExpression = this.expressionParts[0];
let minuteExpression = this.expressionParts[1];
let hourExpression = this.expressionParts[2];
let description = "";
if (!StringUtilities.containsAny(minuteExpression, ExpressionDescriptor.specialCharacters) &&
!StringUtilities.containsAny(hourExpression, ExpressionDescriptor.specialCharacters) &&
!StringUtilities.containsAny(secondsExpression, ExpressionDescriptor.specialCharacters)) {
description += this.i18n.atSpace() + this.formatTime(hourExpression, minuteExpression, secondsExpression);
}
else if (!secondsExpression &&
minuteExpression.indexOf("-") > -1 &&
!(minuteExpression.indexOf(",") > -1) &&
!(minuteExpression.indexOf("/") > -1) &&
!StringUtilities.containsAny(hourExpression, ExpressionDescriptor.specialCharacters)) {
let minuteParts = minuteExpression.split("-");
description += StringUtilities.format(this.i18n.everyMinuteBetweenX0AndX1(), this.formatTime(hourExpression, minuteParts[0], ""), this.formatTime(hourExpression, minuteParts[1], ""));
}
else if (!secondsExpression &&
hourExpression.indexOf(",") > -1 &&
hourExpression.indexOf("-") == -1 &&
hourExpression.indexOf("/") == -1 &&
!StringUtilities.containsAny(minuteExpression, ExpressionDescriptor.specialCharacters)) {
let hourParts = hourExpression.split(",");
description += this.i18n.at();
for (let i = 0; i < hourParts.length; i++) {
description += " ";
description += this.formatTime(hourParts[i], minuteExpression, "");
if (i < hourParts.length - 2) {
description += ",";
}
if (i == hourParts.length - 2) {
description += this.i18n.spaceAnd();
}
}
}
else {
let secondsDescription = this.getSecondsDescription();
let minutesDescription = this.getMinutesDescription();
let hoursDescription = this.getHoursDescription();
description += secondsDescription;
if (description.length > 0 && minutesDescription.length > 0) {
description += ", ";
}
description += minutesDescription;
if (description.length > 0 && hoursDescription.length > 0) {
description += ", ";
}
description += hoursDescription;
}
return description;
}
getSecondsDescription() {
let description = this.getSegmentDescription(this.expressionParts[0], this.i18n.everySecond(), s => {
return s;
}, s => {
return StringUtilities.format(this.i18n.everyX0Seconds(), s);
}, s => {
return this.i18n.secondsX0ThroughX1PastTheMinute();
}, s => {
return s == "0"
? ""
: parseInt(s) < 20
? this.i18n.atX0SecondsPastTheMinute()
: this.i18n.atX0SecondsPastTheMinuteGt20() || this.i18n.atX0SecondsPastTheMinute();
});
return description;
}
getMinutesDescription() {
const secondsExpression = this.expressionParts[0];
let description = this.getSegmentDescription(this.expressionParts[1], this.i18n.everyMinute(), s => {
return s;
}, s => {
return StringUtilities.format(this.i18n.everyX0Minutes(), s);
}, s => {
return this.i18n.minutesX0ThroughX1PastTheHour();
}, s => {
try {
return s == "0" && secondsExpression == ""
? ""
: parseInt(s) < 20
? this.i18n.atX0MinutesPastTheHour()
: this.i18n.atX0MinutesPastTheHourGt20() || this.i18n.atX0MinutesPastTheHour();
}
catch (e) {
return this.i18n.atX0MinutesPastTheHour();
}
});
return description;
}
getHoursDescription() {
let expression = this.expressionParts[2];
let description = this.getSegmentDescription(expression, this.i18n.everyHour(), s => {
return this.formatTime(s, "0", "");
}, s => {
return StringUtilities.format(this.i18n.everyX0Hours(), s);
}, s => {
return this.i18n.betweenX0AndX1();
}, s => {
return this.i18n.atX0();
});
return description;
}
getDayOfWeekDescription() {
var daysOfWeekNames = this.i18n.daysOfTheWeek();
let description = null;
if (this.expressionParts[5] == "*") {
description = "";
}
else {
description = this.getSegmentDescription(this.expressionParts[5], this.i18n.commaEveryDay(), s => {
let exp = s;
if (s.indexOf("#") > -1) {
exp = s.substr(0, s.indexOf("#"));
}
else if (s.indexOf("L") > -1) {
exp = exp.replace("L", "");
}
return daysOfWeekNames[parseInt(exp)];
}, s => {
return StringUtilities.format(this.i18n.commaEveryX0DaysOfTheWeek(), s);
}, s => {
return this.i18n.commaX0ThroughX1();
}, s => {
let format = null;
if (s.indexOf("#") > -1) {
let dayOfWeekOfMonthNumber = s.substring(s.indexOf("#") + 1);
let dayOfWeekOfMonthDescription = null;
switch (dayOfWeekOfMonthNumber) {
case "1":
dayOfWeekOfMonthDescription = this.i18n.first();
break;
case "2":
dayOfWeekOfMonthDescription = this.i18n.second();
break;
case "3":
dayOfWeekOfMonthDescription = this.i18n.third();
break;
case "4":
dayOfWeekOfMonthDescription = this.i18n.fourth();
break;
case "5":
dayOfWeekOfMonthDescription = this.i18n.fifth();
break;
}
format = this.i18n.commaOnThe() + dayOfWeekOfMonthDescription + this.i18n.spaceX0OfTheMonth();
}
else if (s.indexOf("L") > -1) {
format = this.i18n.commaOnTheLastX0OfTheMonth();
}
else {
const domSpecified = this.expressionParts[3] != "*";
format = domSpecified ? this.i18n.commaAndOnX0() : this.i18n.commaOnlyOnX0();
}
return format;
});
}
return description;
}
getMonthDescription() {
var monthNames = this.i18n.monthsOfTheYear();
let description = this.getSegmentDescription(this.expressionParts[4], "", s => {
return monthNames[parseInt(s) - 1];
}, s => {
return StringUtilities.format(this.i18n.commaEveryX0Months(), s);
}, s => {
return this.i18n.commaMonthX0ThroughMonthX1() || this.i18n.commaX0ThroughX1();
}, s => {
return this.i18n.commaOnlyInX0();
});
return description;
}
getDayOfMonthDescription() {
let description = null;
let expression = this.expressionParts[3];
switch (expression) {
case "L":
description = this.i18n.commaOnTheLastDayOfTheMonth();
break;
case "WL":
case "LW":
description = this.i18n.commaOnTheLastWeekdayOfTheMonth();
break;
default:
let weekDayNumberMatches = expression.match(/(\d{1,2}W)|(W\d{1,2})/);
if (weekDayNumberMatches) {
let dayNumber = parseInt(weekDayNumberMatches[0].replace("W", ""));
let dayString = dayNumber == 1
? this.i18n.firstWeekday()
: StringUtilities.format(this.i18n.weekdayNearestDayX0(), dayNumber.toString());
description = StringUtilities.format(this.i18n.commaOnTheX0OfTheMonth(), dayString);
break;
}
else {
let lastDayOffSetMatches = expression.match(/L-(\d{1,2})/);
if (lastDayOffSetMatches) {
let offSetDays = lastDayOffSetMatches[1];
description = StringUtilities.format(this.i18n.commaDaysBeforeTheLastDayOfTheMonth(), offSetDays);
break;
}
else {
description = this.getSegmentDescription(expression, this.i18n.commaEveryDay(), s => {
return s == "L" ? this.i18n.lastDay() : s;
}, s => {
return s == "1" ? this.i18n.commaEveryDay() : this.i18n.commaEveryX0Days();
}, s => {
return this.i18n.commaBetweenDayX0AndX1OfTheMonth();
}, s => {
return this.i18n.commaOnDayX0OfTheMonth();
});
}
break;
}
}
return description;
}
getYearDescription() {
let description = this.getSegmentDescription(this.expressionParts[6], "", s => {
return /^\d+$/.test(s) ? new Date(parseInt(s), 1).getFullYear().toString() : s;
}, s => {
return StringUtilities.format(this.i18n.commaEveryX0Years(), s);
}, s => {
return this.i18n.commaYearX0ThroughYearX1() || this.i18n.commaX0ThroughX1();
}, s => {
return this.i18n.commaOnlyInX0();
});
return description;
}
getSegmentDescription(expression, allDescription, getSingleItemDescription, getIntervalDescriptionFormat, getBetweenDescriptionFormat, getDescriptionFormat) {
let description = null;
if (!expression) {
description = "";
}
else if (expression === "*") {
description = allDescription;
}
else if (!StringUtilities.containsAny(expression, ["/", "-", ","])) {
description = StringUtilities.format(getDescriptionFormat(expression), getSingleItemDescription(expression));
}
else if (expression.indexOf("/") > -1) {
let segments = expression.split("/");
description = StringUtilities.format(getIntervalDescriptionFormat(segments[1]), getSingleItemDescription(segments[1]));
if (segments[0].indexOf("-") > -1) {
let betweenSegmentDescription = this.generateBetweenSegmentDescription(segments[0], getBetweenDescriptionFormat, getSingleItemDescription);
if (betweenSegmentDescription.indexOf(", ") != 0) {
description += ", ";
}
description += betweenSegmentDescription;
}
else if (!StringUtilities.containsAny(segments[0], ["*", ","])) {
let rangeItemDescription = StringUtilities.format(getDescriptionFormat(segments[0]), getSingleItemDescription(segments[0]));
rangeItemDescription = rangeItemDescription.replace(", ", "");
description += StringUtilities.format(this.i18n.commaStartingX0(), rangeItemDescription);
}
}
else if (expression.indexOf(",") > -1) {
let segments = expression.split(",");
let descriptionContent = "";
for (let i = 0; i < segments.length; i++) {
if (i > 0 && segments.length > 2) {
descriptionContent += ",";
if (i < segments.length - 1) {
descriptionContent += " ";
}
}
if (i > 0 && segments.length > 1 && (i == segments.length - 1 || segments.length == 2)) {
descriptionContent += `${this.i18n.spaceAnd()} `;
}
if (segments[i].indexOf("-") > -1) {
let betweenSegmentDescription = this.generateBetweenSegmentDescription(segments[i], s => {
return this.i18n.commaX0ThroughX1();
}, getSingleItemDescription);
betweenSegmentDescription = betweenSegmentDescription.replace(", ", "");
descriptionContent += betweenSegmentDescription;
}
else {
descriptionContent += getSingleItemDescription(segments[i]);
}
}
description = StringUtilities.format(getDescriptionFormat(expression), descriptionContent);
}
else if (expression.indexOf("-") > -1) {
description = this.generateBetweenSegmentDescription(expression, getBetweenDescriptionFormat, getSingleItemDescription);
}
return description;
}
generateBetweenSegmentDescription(betweenExpression, getBetweenDescriptionFormat, getSingleItemDescription) {
let description = "";
let betweenSegments = betweenExpression.split("-");
let betweenSegment1Description = getSingleItemDescription(betweenSegments[0]);
let betweenSegment2Description = getSingleItemDescription(betweenSegments[1]);
betweenSegment2Description = betweenSegment2Description.replace(":00", ":59");
let betweenDescriptionFormat = getBetweenDescriptionFormat(betweenExpression);
description += StringUtilities.format(betweenDescriptionFormat, betweenSegment1Description, betweenSegment2Description);
return description;
}
formatTime(hourExpression, minuteExpression, secondExpression) {
let hour = parseInt(hourExpression);
let period = "";
if (!this.options.use24HourTimeFormat) {
period = hour >= 12 ? " PM" : " AM";
if (hour > 12) {
hour -= 12;
}
if (hour === 0) {
hour = 12;
}
}
let minute = minuteExpression;
let second = "";
if (secondExpression) {
second = `:${("00" + secondExpression).substring(secondExpression.length)}`;
}
return `${("00" + hour.toString()).substring(hour.toString().length)}:${("00" + minute.toString()).substring(minute.toString().length)}${second}${period}`;
}
transformVerbosity(description, useVerboseFormat) {
if (!useVerboseFormat) {
description = description.replace(new RegExp(this.i18n.commaEveryMinute(), "g"), "");
description = description.replace(new RegExp(this.i18n.commaEveryHour(), "g"), "");
description = description.replace(new RegExp(this.i18n.commaEveryDay(), "g"), "");
description = description.replace(/\, ?$/, "");
}
return description;
}
}
ExpressionDescriptor.locales = {};
class en {
atX0SecondsPastTheMinuteGt20() {
return null;
}
atX0MinutesPastTheHourGt20() {
return null;
}
commaMonthX0ThroughMonthX1() {
return null;
}
commaYearX0ThroughYearX1() {
return null;
}
use24HourTimeFormatByDefault() {
return false;
}
anErrorOccuredWhenGeneratingTheExpressionD() {
return "An error occured when generating the expression description. Check the cron expression syntax.";
}
everyMinute() {
return "every minute";
}
everyHour() {
return "every hour";
}
atSpace() {
return "At ";
}
everyMinuteBetweenX0AndX1() {
return "Every minute between %s and %s";
}
at() {
return "At";
}
spaceAnd() {
return " and";
}
everySecond() {
return "every second";
}
everyX0Seconds() {
return "every %s seconds";
}
secondsX0ThroughX1PastTheMinute() {
return "seconds %s through %s past the minute";
}
atX0SecondsPastTheMinute() {
return "at %s seconds past the minute";
}
everyX0Minutes() {
return "every %s minutes";
}
minutesX0ThroughX1PastTheHour() {
return "minutes %s through %s past the hour";
}
atX0MinutesPastTheHour() {
return "at %s minutes past the hour";
}
everyX0Hours() {
return "every %s hours";
}
betweenX0AndX1() {
return "between %s and %s";
}
atX0() {
return "at %s";
}
commaEveryDay() {
return ", every day";
}
commaEveryX0DaysOfTheWeek() {
return ", every %s days of the week";
}
commaX0ThroughX1() {
return ", %s through %s";
}
first() {
return "first";
}
second() {
return "second";
}
third() {
return "third";
}
fourth() {
return "fourth";
}
fifth() {
return "fifth";
}
commaOnThe() {
return ", on the ";
}
spaceX0OfTheMonth() {
return " %s of the month";
}
lastDay() {
return "the last day";
}
commaOnTheLastX0OfTheMonth() {
return ", on the last %s of the month";
}
commaOnlyOnX0() {
return ", only on %s";
}
commaAndOnX0() {
return ", and on %s";
}
commaEveryX0Months() {
return ", every %s months";
}
commaOnlyInX0() {
return ", only in %s";
}
commaOnTheLastDayOfTheMonth() {
return ", on the last day of the month";
}
commaOnTheLastWeekdayOfTheMonth() {
return ", on the last weekday of the month";
}
commaDaysBeforeTheLastDayOfTheMonth() {
return ", %s days before the last day of the month";
}
firstWeekday() {
return "first weekday";
}
weekdayNearestDayX0() {
return "weekday nearest day %s";
}
commaOnTheX0OfTheMonth() {
return ", on the %s of the month";
}
commaEveryX0Days() {
return ", every %s days";
}
commaBetweenDayX0AndX1OfTheMonth() {
return ", between day %s and %s of the month";
}
commaOnDayX0OfTheMonth() {
return ", on day %s of the month";
}
commaEveryMinute() {
return ", every minute";
}
commaEveryHour() {
return ", every hour";
}
commaEveryX0Years() {
return ", every %s years";
}
commaStartingX0() {
return ", starting %s";
}
daysOfTheWeek() {
return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
}
monthsOfTheYear() {
return [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
}
}
class enLocaleLoader {
load(availableLocales) {
availableLocales["en"] = new en();
}
}
ExpressionDescriptor.initialize(new enLocaleLoader());
let toString = ExpressionDescriptor.toString;
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
const directives = new WeakMap();
const isDirective = (o) => {
return typeof o === 'function' && directives.has(o);
};
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* True if the custom elements polyfill is in use.
*/
const isCEPolyfill = window.customElements !== undefined &&
window.customElements.polyfillWrapFlushCallback !==
undefined;
/**
* Removes nodes, starting from `startNode` (inclusive) to `endNode`
* (exclusive), from `container`.
*/
const removeNodes = (container, startNode, endNode = null) => {
let node = startNode;
while (node !== endNode) {
const n = node.nextSibling;
container.removeChild(node);
node = n;
}
};
/**
* @license
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* A sentinel value that signals that a value was handled by a directive and
* should not be written to the DOM.
*/
const noChange = {};
/**
* A sentinel value that signals a NodePart to fully clear its content.
*/
const nothing = {};
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* An expression marker with embedded unique key to avoid collision with
* possible text in templates.
*/
const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
/**
* An expression marker used text-positions, multi-binding attributes, and
* attributes with markup-like text values.
*/
const nodeMarker = `<!--${marker}-->`;
const markerRegex = new RegExp(`${marker}|${nodeMarker}`);
/**
* Suffix appended to all bound attribute names.
*/
const boundAttributeSuffix = '$lit$';
/**
* An updateable Template that tracks the location of dynamic parts.
*/
class Template {
constructor(result, element) {
this.parts = [];
this.element = element;
let index = -1;
let partIndex = 0;
const nodesToRemove = [];
const _prepareTemplate = (template) => {
const content = template.content;
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be
// null
const walker = document.createTreeWalker(content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
// Keeps track of the last index associated with a part. We try to delete
// unnecessary nodes, but we never want to associate two different parts
// to the same index. They must have a constant node between.
let lastPartIndex = 0;
while (walker.nextNode()) {
index++;
const node = walker.currentNode;
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
if (node.hasAttributes()) {
const attributes = node.attributes;
// Per
// https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap,
// attributes are not guaranteed to be returned in document order.
// In particular, Edge/IE can return them out of order, so we cannot
// assume a correspondance between part index and attribute index.
let count = 0;
for (let i = 0; i < attributes.length; i++) {
if (attributes[i].value.indexOf(marker) >= 0) {
count++;
}
}
while (count-- > 0) {
// Get the template literal section leading up to the first
// expression in this attribute
const stringForPart = result.strings[partIndex];
// Find the attribute name
const name = lastAttributeNameRegex.exec(stringForPart)[2];
// Find the corresponding attribute
// All bound attributes have had a suffix added in
// TemplateResult#getHTML to opt out of special attribute
// handling. To look up the attribute value we also need to add
// the suffix.
const attributeLookupName = name.toLowerCase() + boundAttributeSuffix;
const attributeValue = node.getAttribute(attributeLookupName);
const strings = attributeValue.split(markerRegex);
this.parts.push({ type: 'attribute', index, name, strings });
node.removeAttribute(attributeLookupName);
partIndex += strings.length - 1;
}
}
if (node.tagName === 'TEMPLATE') {
_prepareTemplate(node);
}
}
else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
const data = node.data;
if (data.indexOf(marker) >= 0) {
const parent = node.parentNode;
const strings = data.split(markerRegex);
const lastIndex = strings.length - 1;
// Generate a new text node for each literal section
// These nodes are also used as the markers for node parts
for (let i = 0; i < lastIndex; i++) {
parent.insertBefore((strings[i] === '') ? createMarker() :
document.createTextNode(strings[i]), node);
this.parts.push({ type: 'node', index: ++index });
}
// If there's no text, we must insert a comment to mark our place.
// Else, we can trust it will stick around after cloning.
if (strings[lastIndex] === '') {
parent.insertBefore(createMarker(), node);
nodesToRemove.push(node);
}
else {
node.data = strings[lastIndex];
}
// We have a part for each match found
partIndex += lastIndex;
}
}
else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {
if (node.data === marker) {
const parent = node.parentNode;
// Add a new marker node to be the startNode of the Part if any of
// the following are true:
// * We don't have a previousSibling
// * The previousSibling is already the start of a previous part
if (node.previousSibling === null || index === lastPartIndex) {
index++;
parent.insertBefore(createMarker(), node);
}
lastPartIndex = index;
this.parts.push({ type: 'node', index });
// If we don't have a nextSibling, keep this node so we have an end.
// Else, we can remove it to save future costs.
if (node.nextSibling === null) {
node.data = '';
}
else {
nodesToRemove.push(node);
index--;
}
partIndex++;
}
else {
let i = -1;
while ((i = node.data.indexOf(marker, i + 1)) !==
-1) {
// Comment node has a binding marker inside, make an inactive part
// The binding won't work, but subsequent bindings will
// TODO (justinfagnani): consider whether it's even worth it to
// make bindings in comments work
this.parts.push({ type: 'node', index: -1 });
}
}
}
}
};
_prepareTemplate(element);
// Remove text binding nodes after the walk to not disturb the TreeWalker
for (const n of nodesToRemove) {
n.parentNode.removeChild(n);
}
}
}
const isTemplatePartActive = (part) => part.index !== -1;
// Allows `document.createComment('')` to be renamed for a
// small manual size-savings.
const createMarker = () => document.createComment('');
/**
* This regex extracts the attribute name preceding an attribute-position
* expression. It does this by matching the syntax allowed for attributes
* against the string literal directly preceding the expression, assuming that
* the expression is in an attribute-value position.
*
* See attributes in the HTML spec:
* https://www.w3.org/TR/html5/syntax.html#attributes-0
*
* "\0-\x1F\x7F-\x9F" are Unicode control characters
*
* " \x09\x0a\x0c\x0d" are HTML space characters:
* https://www.w3.org/TR/html5/infrastructure.html#space-character
*
* So an attribute is:
* * The name: any character except a control character, space character, ('),
* ("), ">", "=", or "/"
* * Followed by zero or more space characters
* * Followed by "="
* * Followed by zero or more space characters
* * Followed by:
* * Any character except space, ('), ("), "<", ">", "=", (`), or
* * (") then any non-("), or
* * (') then any non-(')
*/
const lastAttributeNameRegex = /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* An instance of a `Template` that can be attached to the DOM and updated
* with new values.
*/
class TemplateInstance {
constructor(template, processor, options) {
this._parts = [];
this.template = template;
this.processor = processor;
this.options = options;
}
update(values) {
let i = 0;
for (const part of this._parts) {
if (part !== undefined) {
part.setValue(values[i]);
}
i++;
}
for (const part of this._parts) {
if (part !== undefined) {
part.commit();
}
}
}
_clone() {
// When using the Custom Elements polyfill, clone the node, rather than
// importing it, to keep the fragment in the template's document. This
// leaves the fragment inert so custom elements won't upgrade and
// potentially modify their contents by creating a polyfilled ShadowRoot
// while we traverse the tree.
const fragment = isCEPolyfill ?
this.template.element.content.cloneNode(true) :
document.importNode(this.template.element.content, true);
const parts = this.template.parts;
let partIndex = 0;
let nodeIndex = 0;
const _prepareInstance = (fragment) => {
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be
// null
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
let node = walker.nextNode();
// Loop through all the nodes and parts of a template
while (partIndex < parts.length && node !== null) {
const part = parts[partIndex];
// Consecutive Parts may have the same node index, in the case of
// multiple bound attributes on an element. So each iteration we either
// increment the nodeIndex, if we aren't on a node with a part, or the
// partIndex if we are. By not incrementing the nodeIndex when we find a
// part, we allow for the next part to be associated with the current
// node if neccessasry.
if (!isTemplatePartActive(part)) {
this._parts.push(undefined);
partIndex++;
}
else if (nodeIndex === part.index) {
if (part.type === 'node') {
const part = this.processor.handleTextExpression(this.options);
part.insertAfterNode(node.previousSibling);
this._parts.push(part);
}
else {
this._parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options));
}
partIndex++;
}
else {
nodeIndex++;
if (node.nodeName === 'TEMPLATE') {
_prepareInstance(node.content);
}
node = walker.nextNode();
}
}
};
_prepareInstance(fragment);
if (isCEPolyfill) {
document.adoptNode(fragment);
customElements.upgrade(fragment);
}
return fragment;
}
}
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
/**
* The return type of `html`, which holds a Template and the values from
* interpolated expressions.
*/
class TemplateResult {
constructor(strings, values, type, processor) {
this.strings = strings;
this.values = values;
this.type = type;
this.processor = processor;
}
/**
* Returns a string of HTML used to create a `<template>` element.
*/
getHTML() {
const endIndex = this.strings.length - 1;
let html = '';
for (let i = 0; i < endIndex; i++) {
const s = this.strings[i];
// This exec() call does two things:
// 1) Appends a suffix to the bound attribute name to opt out of special
// attribute value parsing that IE11 and Edge do, like for style and
// many SVG attributes. The Template class also appends the same suffix
// when looking up attributes to create Parts.
// 2) Adds an unquoted-attribute-safe marker for the first expression in
// an attribute. Subsequent attribute expressions will use node markers,
// and this is safe since attributes with multiple expressions are
// guaranteed to be quoted.
const match = lastAttributeNameRegex.exec(s);
if (match) {
// We're starting a new bound attribute.
// Add the safe attribute suffix, and use unquoted-attribute-safe
// marker.
html += s.substr(0, match.index) + match[1] + match[2] +
boundAttributeSuffix + match[3] + marker;
}
else {
// We're either in a bound node, or trailing bound attribute.
// Either way, nodeMarker is safe to use.
html += s + nodeMarker;
}
}
return html + this.strings[endIndex];
}
getTemplateElement() {
const template = document.createElement('template');
template.innerHTML = this.getHTML();
return template;
}
}
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
const isPrimitive = (value) => {
return (value === null ||
!(typeof value === 'object' || typeof value === 'function'));
};
/**
* Sets attribute values for AttributeParts, so that the value is only set once
* even if there are multiple parts for an attribute.
*/
class AttributeCommitter {
constructor(element, name, strings) {
this.dirty = true;
this.element = element;
this.name = name;
this.strings = strings;
this.parts = [];
for (let i = 0; i < strings.length - 1; i++) {
this.parts[i] = this._createPart();
}
}
/**
* Creates a single part. Override this to create a differnt type of part.
*/
_createPart() {
return new AttributePart(this);
}
_getValue() {
const strings = this.strings;
const l = strings.length - 1;
let text = '';
for (let i = 0; i < l; i++) {
text += strings[i];
const part = this.parts[i];
if (part !== undefined) {
const v = part.value;
if (v != null &&
(Array.isArray(v) ||
// tslint:disable-next-line:no-any
typeof v !== 'string' && v[Symbol.iterator])) {
for (const t of v) {
text += typeof t === 'string' ? t : String(t);
}
}
else {
text += typeof v === 'string' ? v : String(v);
}
}
}
text += strings[l];
return text;
}
commit() {
if (this.dirty) {
this.dirty = false;
this.element.setAttribute(this.name, this._getValue());
}
}
}
class AttributePart {
constructor(comitter) {
this.value = undefined;
this.committer = comitter;
}
setValue(value) {
if (value !== noChange && (!isPrimitive(value) || value !== this.value)) {
this.value = value;
// If the value is a not a directive, dirty the committer so that it'll
// call setAttribute. If the value is a directive, it'll dirty the
// committer if it calls setValue().
if (!isDirective(value)) {
this.committer.dirty = true;
}
}
}
commit() {
while (isDirective(this.value)) {
const directive = this.value;
this.value = noChange;
directive(this);
}
if (this.value === noChange) {
return;
}
this.committer.commit();
}
}
class NodePart {
constructor(options) {