taggedjs
Version:
tagged template reactive html
205 lines • 8.21 kB
JavaScript
import { variablePrefix, variableSuffix } from "../../tag/DomTag.type.js";
import { isSpecialAttr } from "../attributes/isSpecialAttribute.function.js";
import { fakeTagsRegEx, findRealTagsRegEx } from "./htmlInterpolationToDomMeta.function.js";
import { placeholderRegex } from "../../render/attributes/getTagVarIndex.function.js";
const fragFindAny = /(:tagvar\d+:)/;
const ondoubleclick = 'ondoubleclick';
const regexAttr = /([:_a-zA-Z0-9\-.]+)\s*(?:=\s*"([^"]*)"|=\s*(\S+))?/g;
const regexTagOrg = /<\/?([a-zA-Z0-9-]+)((?:\s+[a-zA-Z_:*][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)+\s*|\s*)\//g;
/** Main start of string parsing */
export function parseHTML(html) {
const valuePositions = [];
const elements = [];
const stack = [];
let currentElement = null;
let valueIndex = -1;
let position = 0;
const regexTag = new RegExp(regexTagOrg, 'g');
html = preprocessTagsInComments(html);
while (position < html.length) {
const tagMatch = regexTag.exec(html);
if (!tagMatch) {
break;
}
const [fullMatch, tagName, attrString] = tagMatch;
const isClosingTag = fullMatch.startsWith('</');
const isSelfClosing = fullMatch.endsWith('/>');
if (position < tagMatch.index) {
const textContent = html.slice(position, tagMatch.index);
if (textContent.trim()) {
const textVarMatches = splitByTagVar(textContent);
for (let textContent of textVarMatches) {
if (textContent.startsWith(variablePrefix) && textContent.search(fragFindAny) >= 0) {
// if its not fake then lets now consider this a real variable
if (textContent.search(fakeTagsRegEx) === -1) {
textContent = variablePrefix + (++valueIndex) + variableSuffix;
}
}
pushTextTo(currentElement, elements, textContent);
}
}
}
position = tagMatch.index + fullMatch.length;
if (isClosingTag) {
currentElement = stack.pop() || null;
continue;
}
const attributes = [];
let attrMatch;
while ((attrMatch = regexAttr.exec(attrString)) !== null) {
valueIndex = parseAttrString(attrMatch, valueIndex, valuePositions, attributes);
}
const element = {
nn: tagName, // nodeName
};
if (attributes.length) {
element.at = attributes;
}
if (currentElement) {
if (!currentElement.ch) {
currentElement.ch = [];
}
currentElement.ch.push(element);
}
else {
elements.push(element);
}
if (!isSelfClosing) {
stack.push(currentElement);
currentElement = element;
}
}
if (position < html.length) {
const textContent = html.slice(position);
if (textContent.trim()) {
const textVarMatches = splitByTagVar(textContent);
for (const textContent of textVarMatches) {
if (textContent.startsWith(variablePrefix)) {
++valueIndex;
}
pushTextTo(currentElement, elements, textContent);
}
}
}
return elements;
}
const removeCommentRegX = new RegExp('(<!--[\\s\\S]*?-->)', 'g');
function preprocessTagsInComments(html) {
// Use a regex to find all HTML comments
return html.replace(removeCommentRegX, function (match) {
// For each comment found, replace < and > inside it
return match.replace(/\[l t\]/g, '[l t]').replace(/\[g t\]/g, '[g t]').replace(/</g, '[l t]').replace(/>/g, '[g t]');
});
}
function cleanEventName(eventName) {
if (eventName.startsWith('on')) {
const couldByDblClick = eventName.length === ondoubleclick.length && eventName === ondoubleclick;
if (couldByDblClick) {
return 'dblclick';
}
return eventName.slice(2, eventName.length);
}
return eventName;
}
function pushTextTo(currentElement, elements, textContent) {
const textNode = {
nn: 'text', // nodeName
tc: postProcessTagsInComments(textContent), // textContent
};
pushTo(currentElement, elements, textNode);
}
/** TODO: This has got to be too expensive */
function postProcessTagsInComments(html) {
// Use a regex to find all segments that look like processed comments
return html.replace(/(\[l t\]!--[\s\S]*?--\[g t\])/g, function (match) {
// For each processed comment found, replace *lt* and *gt* back to < and >
return match.replace(/\[l t\]/g, '<').replace(/\[g t\]/g, '>').replace(/\[l t\]/g, '[l t]').replace(/\[g t\]/g, '[g t]');
});
}
function pushTo(currentElement, elements, textNode) {
if (currentElement) {
if (!currentElement.ch) {
currentElement.ch = [];
}
currentElement.ch.push(textNode);
}
else {
elements.push(textNode);
}
}
function splitByTagVar(inputString) {
// Split the string using the regular expression, keep delimiters in the output
const parts = inputString.split(fragFindAny);
// Filter out any empty strings from the results
const filteredParts = parts.filter(notEmptyStringMapper);
return filteredParts;
}
function notEmptyStringMapper(part) {
return part !== '';
}
function parseAttrString(attrMatch, valueIndex, valuePositions, attributes) {
const attrName = attrMatch[1] || attrMatch[3] || attrMatch[5];
const attrChoice = attrMatch[2] || attrMatch[4] || attrMatch[6];
let attrValue = attrChoice;
if (attrName === undefined) {
return valueIndex;
}
const notEmpty = attrMatch[2] !== '';
const noValue = attrValue === undefined && notEmpty;
const lowerName = attrName.toLowerCase();
const fixedName = lowerName.startsWith('on') ? cleanEventName(lowerName) : lowerName;
if (noValue) {
const standAloneVar = attrName.slice(0, variablePrefix.length) === variablePrefix;
if (standAloneVar) {
const valueName = variablePrefix + (++valueIndex) + variableSuffix;
valuePositions.push(['at', valueName]);
attributes.push([valueName]); // the name itself is dynamic
return valueIndex;
}
const startMatched = attrMatch[0].startsWith(attrName);
const standAloneAttr = startMatched && attrMatch[0].slice(attrName.length, attrMatch[0].length).search(/\s+$/) >= 0;
if (standAloneAttr) {
attributes.push([fixedName]);
return valueIndex;
}
const wholeValue = attrMatch[3];
const isFakeTag = wholeValue.search(fakeTagsRegEx) >= 0;
if (isFakeTag) {
attrValue = wholeValue;
// to restore: wholeValue.replace(fakeTagsRegEx,variablePrefix+'$1$3$4'+variableSuffix)
const attrSet = [fixedName, attrValue];
attributes.push(attrSet);
return valueIndex;
}
else {
const valueName = variablePrefix + (++valueIndex) + variableSuffix;
attrValue = valueName;
}
}
if (!notEmpty) {
attrValue = attrMatch[2];
}
// concat attributes as array
const attrValueSplit = attrValue.split(findRealTagsRegEx).filter((x) => x.length > 0);
if (attrValueSplit.length > 1) {
attrValue = attrValueSplit;
attrValueSplit.forEach((value) => {
if (value.search(placeholderRegex) >= 0) {
++valueIndex;
}
});
}
const attrSet = [fixedName, attrValue];
const isSpecial = isSpecialAttr(lowerName); // check original name for "oninit" or "autofocus"
if (isSpecial) {
attrSet.push(isSpecial);
}
// force style to be first so other style manipulating attributes do not get overwritten
if (fixedName === 'style') {
attributes.unshift(attrSet);
return valueIndex;
}
attributes.push(attrSet);
return valueIndex;
}
//# sourceMappingURL=parseHTML.function.js.map