hybrids
Version:
A JavaScript framework for creating fully-featured web applications, components libraries, and single web components with unique declarative and functional architecture
585 lines (493 loc) • 16.7 kB
JavaScript
import { stringifyElement } from "../utils.js";
import { get as getMessage, isLocalizeEnabled } from "../localize.js";
import * as layout from "./layout.js";
import {
getMeta,
getPlaceholder,
getTemplateEnd,
removeTemplate,
} from "./utils.js";
import resolveValue from "./resolvers/value.js";
import resolveProperty from "./resolvers/property.js";
const PLACEHOLDER_REGEXP_TEXT = getPlaceholder("(\\d+)");
const PLACEHOLDER_REGEXP_EQUAL = new RegExp(`^${PLACEHOLDER_REGEXP_TEXT}$`);
const PLACEHOLDER_REGEXP_ALL = new RegExp(PLACEHOLDER_REGEXP_TEXT, "g");
const PLACEHOLDER_REGEXP_ONLY = /^[^A-Za-z]+$/;
const PLACEHOLDER_REGEXP_MSG = new RegExp(getPlaceholder("") + "\\d+");
function createContents(parts) {
let signature = parts[0];
let tableMode = false;
for (let index = 1; index < parts.length; index += 1) {
tableMode =
tableMode ||
signature.match(
/<\s*(table|th|tr|td|thead|tbody|tfoot|caption|colgroup)([^<>]|"[^"]*"|'[^']*')*>\s*$/,
);
signature +=
(tableMode
? `<!--${getPlaceholder(index - 1)}-->`
: getPlaceholder(index - 1)) + parts[index];
tableMode =
tableMode &&
!signature.match(
/<\/\s*(table|th|tr|td|thead|tbody|tfoot|caption|colgroup)\s*>/,
);
}
return signature;
}
function getPropertyName(string) {
return string
.replace(/\s*=\s*['"]*$/g, "")
.split(/\s+/)
.pop();
}
function createWalker(context) {
return globalThis.document.createTreeWalker(
context,
globalThis.NodeFilter.SHOW_ELEMENT |
globalThis.NodeFilter.SHOW_TEXT |
globalThis.NodeFilter.SHOW_COMMENT,
null,
false,
);
}
function normalizeWhitespace(input, startIndent = 0) {
input = input.replace(/(^[\n\s\t ]+)|([\n\s\t ]+$)+/g, "");
let i = input.indexOf("\n");
if (i > -1) {
let indent = 0 - startIndent - 2;
for (i += 1; input[i] === " " && i < input.length; i += 1) {
indent += 1;
}
return input.replace(/\n +/g, (t) =>
t.substr(0, Math.max(t.length - indent, 1)),
);
}
return input;
}
function beautifyTemplateLog(input, index) {
const placeholder = getPlaceholder(index);
const output = normalizeWhitespace(input)
.split("\n")
.filter((i) => i)
.map((line) => {
const startIndex = line.indexOf(placeholder);
if (startIndex > -1) {
return `| ${line}\n--${"-".repeat(startIndex)}${"^".repeat(6)}`;
}
return `| ${line}`;
})
.join("\n")
.replace(PLACEHOLDER_REGEXP_ALL, "${...}");
return `${output}`;
}
const styleSheetsMap = new Map();
const prevStylesMap = new WeakMap();
const prevStyleSheetsMap = new WeakMap();
function updateAdoptedStylesheets(target, styles) {
const prevStyles = prevStylesMap.get(target);
if (
(!prevStyles && !styles) ||
(styles?.length &&
prevStyles?.length &&
styles?.every((s, i) => prevStyles[i] === s))
) {
return;
}
let styleSheets = null;
if (styles) {
styleSheets = [];
for (const style of styles) {
let styleSheet = style;
if (!(styleSheet instanceof globalThis.CSSStyleSheet)) {
styleSheet = styleSheetsMap.get(style);
if (!styleSheet) {
styleSheet = new globalThis.CSSStyleSheet();
styleSheet.replaceSync(style);
styleSheetsMap.set(style, styleSheet);
}
}
styleSheets.push(styleSheet);
}
}
let adoptedStyleSheets;
const prevStyleSheets = prevStyleSheetsMap.get(target);
if (prevStyleSheets) {
adoptedStyleSheets = [];
for (const styleSheet of target.adoptedStyleSheets) {
if (!prevStyleSheets.includes(styleSheet)) {
adoptedStyleSheets.push(styleSheet);
}
}
}
if (styleSheets) {
adoptedStyleSheets =
adoptedStyleSheets || target.adoptedStyleSheets.length
? [...target.adoptedStyleSheets]
: [];
for (const styleSheet of styleSheets) {
adoptedStyleSheets.push(styleSheet);
}
}
target.adoptedStyleSheets = adoptedStyleSheets;
prevStylesMap.set(target, styles);
prevStyleSheetsMap.set(target, styleSheets);
}
const styleElementMap = new WeakMap();
function updateStyleElement(target, styles) {
let styleEl = styleElementMap.get(target);
if (styles) {
const prevStyles = prevStylesMap.get(target);
if (prevStyles && styles.every((s, i) => prevStyles[i] === s)) return;
if (!styleEl || styleEl.parentNode !== target) {
styleEl = globalThis.document.createElement("style");
styleElementMap.set(target, styleEl);
target = getTemplateEnd(target);
if (target.nodeType === globalThis.Node.TEXT_NODE) {
target.parentNode.insertBefore(styleEl, target.nextSibling);
} else {
target.appendChild(styleEl);
}
}
styleEl.textContent = styles.join("\n/*------*/\n");
prevStylesMap.set(target, styles);
} else if (styleEl) {
styleEl.parentNode.removeChild(styleEl);
styleElementMap.set(target, null);
}
}
export function compileTemplate(rawParts, isSVG, isMsg, useLayout) {
let contents = "";
if (isMsg) {
contents = rawParts;
rawParts = rawParts.split(PLACEHOLDER_REGEXP_MSG);
} else {
contents = createContents(rawParts);
}
let template = globalThis.document.createElement("template");
if (isSVG) {
template.innerHTML = `<svg>${contents}</svg>`;
const svgRoot = template.content.firstChild;
template.content.removeChild(svgRoot);
for (const node of Array.from(svgRoot.childNodes)) {
template.content.appendChild(node);
}
} else {
template.innerHTML = contents;
}
let hostLayout;
const layoutTemplate = template.content.children[0];
if (layoutTemplate instanceof globalThis.HTMLTemplateElement) {
for (const attr of Array.from(layoutTemplate.attributes)) {
const value = attr.value.trim();
if (value && attr.name.startsWith("layout")) {
if (value.match(PLACEHOLDER_REGEXP_ALL)) {
throw Error("Layout attribute cannot contain expressions");
}
hostLayout = layout.insertRule(
layoutTemplate,
attr.name.substr(6),
value,
true,
);
}
}
if (hostLayout !== undefined && template.content.children.length > 1) {
throw Error(
"Template, which uses layout system must have only the '<template>' root element",
);
}
useLayout = hostLayout || layoutTemplate.hasAttribute("layout");
template = layoutTemplate;
}
const compileWalker = createWalker(template.content);
const parts = {};
const notDefinedElements = [];
let compileIndex = 0;
let noTranslate = null;
let useShadow = false;
let slotDetected = false;
while (compileWalker.nextNode()) {
let node = compileWalker.currentNode;
if (noTranslate && !noTranslate.contains(node)) {
noTranslate = null;
}
if (node.nodeType === globalThis.Node.COMMENT_NODE) {
if (PLACEHOLDER_REGEXP_EQUAL.test(node.textContent)) {
node.parentNode.insertBefore(
globalThis.document.createTextNode(node.textContent),
node.nextSibling,
);
compileWalker.nextNode();
node.parentNode.removeChild(node);
node = compileWalker.currentNode;
}
}
if (node.nodeType === globalThis.Node.TEXT_NODE) {
let text = node.textContent;
const equal = text.match(PLACEHOLDER_REGEXP_EQUAL);
if (equal) {
node.textContent = "";
parts[equal[1]] = [compileIndex, resolveValue];
} else {
if (
isLocalizeEnabled() &&
!isMsg &&
!noTranslate &&
!text.match(/^\s*$/)
) {
let offset;
const key = text.trim();
const localizedKey = key
.replace(/\s+/g, " ")
.replace(PLACEHOLDER_REGEXP_ALL, (_, index) => {
index = Number(index);
if (offset === undefined) offset = index;
return `\${${index - offset}}`;
});
if (!localizedKey.match(PLACEHOLDER_REGEXP_ONLY)) {
let context =
node.previousSibling &&
node.previousSibling.nodeType === globalThis.Node.COMMENT_NODE
? node.previousSibling
: "";
if (context) {
context.parentNode.removeChild(context);
compileIndex -= 1;
context = (context.textContent.split("|")[1] || "")
.trim()
.replace(/\s+/g, " ");
}
const resultKey = getMessage(localizedKey, context).replace(
/\${(\d+)}/g,
(_, index) => getPlaceholder(Number(index) + offset),
);
text = text.replace(key, resultKey);
node.textContent = text;
}
}
const results = text.match(PLACEHOLDER_REGEXP_ALL);
if (results) {
let currentNode = node;
results
.reduce(
(acc, placeholder) => {
const [before, next] = acc.pop().split(placeholder);
if (before) acc.push(before);
acc.push(placeholder);
if (next) acc.push(next);
return acc;
},
[text],
)
.forEach((part, index) => {
if (index === 0) {
currentNode.textContent = part;
} else {
currentNode = currentNode.parentNode.insertBefore(
globalThis.document.createTextNode(part),
currentNode.nextSibling,
);
compileWalker.currentNode = currentNode;
compileIndex += 1;
}
const equal = currentNode.textContent.match(
PLACEHOLDER_REGEXP_EQUAL,
);
if (equal) {
currentNode.textContent = "";
parts[equal[1]] = [compileIndex, resolveValue];
}
});
}
}
} else {
/* istanbul ignore else */
if (node.nodeType === globalThis.Node.ELEMENT_NODE) {
if (
node.tagName === "STYLE" ||
node.tagName === "SLOT" ||
(node.tagName === "LINK" && node.rel === "stylesheet")
) {
useShadow = true;
slotDetected = slotDetected || node.tagName === "SLOT";
}
if (
!noTranslate &&
(node.getAttribute("translate") === "no" ||
node.tagName.toLowerCase() === "script" ||
node.tagName.toLowerCase() === "style")
) {
noTranslate = node;
}
const tagName = node.tagName.toLowerCase();
if (
tagName.match(/.+-.+/) &&
!globalThis.customElements.get(tagName) &&
!notDefinedElements.includes(tagName)
) {
notDefinedElements.push(tagName);
}
for (const attr of Array.from(node.attributes)) {
const value = attr.value.trim();
/* istanbul ignore next */
const name = attr.name;
if (useLayout && name.startsWith("layout") && value) {
if (value.match(PLACEHOLDER_REGEXP_ALL)) {
throw Error("Layout attribute cannot contain expressions");
}
const className = layout.insertRule(node, name.substr(6), value);
node.removeAttribute(name);
node.classList.add(className);
continue;
}
const equal = value.match(PLACEHOLDER_REGEXP_EQUAL);
if (equal) {
const propertyName = getPropertyName(rawParts[equal[1]]);
parts[equal[1]] = [
compileIndex,
resolveProperty(name, propertyName, isSVG),
];
node.removeAttribute(attr.name);
} else {
const results = value.match(PLACEHOLDER_REGEXP_ALL);
if (results) {
const partialName = `attr__${name}`;
for (const [index, placeholder] of results.entries()) {
const [, id] = placeholder.match(PLACEHOLDER_REGEXP_EQUAL);
let isProp = false;
parts[id] = [
compileIndex,
(host, target, attrValue) => {
const meta = getMeta(target);
meta[partialName] = (meta[partialName] || value).replace(
placeholder,
attrValue ?? "",
);
if (results.length === 1 || index + 1 === results.length) {
isProp =
isProp ||
(!isSVG &&
!(target instanceof globalThis.SVGElement) &&
name in target);
if (isProp) {
target[name] = meta[partialName];
} else {
target.setAttribute(name, meta[partialName]);
}
meta[partialName] = undefined;
}
},
true,
];
}
attr.value = "";
}
}
}
}
}
compileIndex += 1;
}
if (notDefinedElements.length) {
console.warn(
`Not defined ${notDefinedElements
.map((e) => `<${e}>`)
.join(", ")} element${
notDefinedElements.length > 1 ? "s" : ""
} found in the template:\n${beautifyTemplateLog(contents, -1)}`,
);
}
const partsKeys = Object.keys(parts);
return function updateTemplateInstance(host, target, args, styleSheets) {
if (target) {
if (slotDetected && !host.shadowRoot) {
throw TypeError(
`The <slot> element found - use 'shadow' options to explicitly define Shadow DOM mode`,
);
}
} else {
target =
host.shadowRoot ||
((useShadow || styleSheets) && host.attachShadow({ mode: "open" })) ||
host;
}
let meta = getMeta(target);
if (template !== meta.template) {
const fragment = globalThis.document.importNode(template.content, true);
const renderWalker = createWalker(fragment);
const markers = [];
let renderIndex = 0;
let keyIndex = 0;
let currentPart = parts[partsKeys[keyIndex]];
while (renderWalker.nextNode()) {
const node = renderWalker.currentNode;
while (currentPart && currentPart[0] === renderIndex) {
markers.push({
index: partsKeys[keyIndex],
node,
fn: currentPart[1],
forceUpdate: currentPart[2],
});
keyIndex += 1;
currentPart = parts[partsKeys[keyIndex]];
}
renderIndex += 1;
}
if (meta.hostLayout) {
host.classList.remove(meta.hostLayout);
}
removeTemplate(target);
meta = getMeta(target);
meta.template = template;
meta.markers = markers;
if (target.nodeType === globalThis.Node.TEXT_NODE) {
updateStyleElement(target);
meta.startNode = fragment.childNodes[0];
meta.endNode = fragment.childNodes[fragment.childNodes.length - 1];
let previousChild = target;
let child = fragment.childNodes[0];
while (child) {
target.parentNode.insertBefore(child, previousChild.nextSibling);
previousChild = child;
child = fragment.childNodes[0];
}
} else {
if (useLayout && hostLayout) {
const className = `${hostLayout}-${host === target ? "c" : "s"}`;
host.classList.add(className);
meta.hostLayout = className;
}
target.appendChild(fragment);
}
if (useLayout) layout.inject(target);
}
if (target.adoptedStyleSheets) {
updateAdoptedStylesheets(target, styleSheets);
} else {
updateStyleElement(target, styleSheets);
}
for (const marker of meta.markers) {
const value = args[marker.index];
let prevValue = undefined;
if (meta.prevArgs) {
prevValue = meta.prevArgs[marker.index];
if (prevValue === value && !marker.forceUpdate) {
continue;
}
}
try {
marker.fn(host, marker.node, value, prevValue, useLayout);
} catch (error) {
console.error(
`Error while updating template expression in ${stringifyElement(
host,
)}:\n${beautifyTemplateLog(contents, marker.index)}`,
);
throw error;
}
}
meta.prevArgs = args;
return target;
};
}