UNPKG

element-vir

Version:

Heroic. Reactive. Declarative. Type safe. Web components without compromise.

118 lines (117 loc) 5.19 kB
/* eslint-disable @typescript-eslint/no-deprecated */ import { check } from '@augment-vir/assert'; import { getOrSet, getOrSetFromMap } from '@augment-vir/common'; import { assign } from '../../declarative-element/directives/assign.directive.js'; import { hasTagName, isMinimalDefinitionWithInputs, } from '../minimal-element-definition.js'; import { transformTemplate } from '../transform-template.js'; import { tagNameKeys } from './tag-name-keys.js'; /** * Which interpolation slots of a template sit in a tag name position. This depends only on the * template's strings, which never change for a given template literal site, so it is computed once * per site: {@link classifyValue} costs a `trimEnd` allocation and up to two regular expression * tests, and it would otherwise run for every interpolated string on every render. */ const tagNameSlotsByTemplate = new WeakMap(); function getTagNameSlots(inputTemplateStrings) { return getOrSetFromMap(tagNameSlotsByTemplate, inputTemplateStrings, () => { /** A template's strings always outnumber its interpolations by exactly one. */ return inputTemplateStrings.slice(0, -1).map((lastString, valueIndex) => { const nextString = inputTemplateStrings[valueIndex + 1]; return (!!lastString && !!nextString && classifyValue({ lastNewString: lastString, currentTemplateString: nextString, }).shouldHaveTagNameHere); }); }); } export function mapHtmlValues(inputTemplateStrings, inputValues) { const tagNameSlots = getTagNameSlots(inputTemplateStrings); /** Only a string can be an interpolated tag name. */ const hasTagNameString = tagNameSlots.some((isTagNameSlot, valueIndex) => { return isTagNameSlot && check.isString(inputValues[valueIndex]); }); /** Most templates interpolate no tag name strings, so most renders allocate nothing here. */ if (!hasTagNameString) { return inputValues; } return inputValues.map((currentValue, currentValueIndex) => { if (!tagNameSlots[currentValueIndex] || !check.isString(currentValue)) { return currentValue; } const replacement = { tagName: currentValue, tagInterpolationKey: getOrSet(tagNameKeys, currentValue, () => { return { tagName: currentValue, }; }), }; return replacement; }); } /** Characters that are allowed to directly follow an interpolated opening tag name. */ const openingTagNameFollower = /^[\s>/]/; /** An interpolated closing tag name must be followed by the tag's `>`. */ const closingTagNameFollower = /^\s*>/; function classifyValue({ lastNewString, currentTemplateString, }) { /** Only the end of this string is ever checked, so only its end needs trimming. */ const trimmedLastNewString = lastNewString.trimEnd(); const isOpeningTag = trimmedLastNewString.endsWith('<') && openingTagNameFollower.test(currentTemplateString); const isClosingTag = trimmedLastNewString.endsWith('</') && closingTagNameFollower.test(currentTemplateString); const shouldHaveTagNameHere = isOpeningTag || isClosingTag; return { isOpeningTag, shouldHaveTagNameHere, }; } function transformHtml(...[lastNewString, currentTemplateString, rawCurrentValue,]) { const currentValue = isMinimalDefinitionWithInputs(rawCurrentValue) ? rawCurrentValue.definition : rawCurrentValue; const { isOpeningTag, shouldHaveTagNameHere } = classifyValue({ lastNewString, currentTemplateString, }); const isTagNameWrapper = hasTagName(currentValue); if (isTagNameWrapper && shouldHaveTagNameHere && currentValue.tagInterpolationKey) { return { replacement: currentValue.tagName, getExtraValues: undefined, }; } else if (shouldHaveTagNameHere && !isTagNameWrapper) { console.error({ lastNewString, currentTemplateString, currentValue, }); throw new Error(`Got interpolated tag name but found no tag name on the given value: '${currentValue?.tagName || currentValue?.prototype?.constructor?.name || currentValue?.constructor?.name}'`); } else if (!shouldHaveTagNameHere || !isTagNameWrapper) { return undefined; } else { return { replacement: currentValue.tagName, getExtraValues(extraValueCurrentValue) { const assignedInputs = isMinimalDefinitionWithInputs(extraValueCurrentValue) ? extraValueCurrentValue.inputs : undefined; return [ isOpeningTag && assignedInputs ? assign(assignedInputs) : undefined, ].filter(check.isTruthy); }, }; } } export function transformHtmlTemplate(litTemplate) { return transformTemplate({ inputTemplateStrings: litTemplate.strings, inputValues: litTemplate.values, transformValue: transformHtml, }); }