From cf684db08fa221cbbcf4597445b7956494e1b558 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Fri, 8 Aug 2025 11:36:33 -0700 Subject: [PATCH 1/2] chore: update build process and add esbuild support - Added .eslintignore to exclude dist and coverage directories. - Updated package.json to include esbuild as a dependency and added build scripts for generating distribution files. - Modified README to clarify the usage of the generated dist files. - Removed the faintly.js source file as part of the transition to a build process. --- .eslintignore | 3 + README.md | 4 +- {src => dist}/faintly.js | 316 +++--------- dist/faintly.min.js | 2 + dist/faintly.min.js.map | 7 + package-lock.json | 449 ++++++++++++++++++ package.json | 9 +- src/directives.js | 212 +++++++++ src/expressions.js | 73 +++ src/index.js | 1 + src/render.js | 99 ++++ src/templates.js | 34 ++ .../attributes/processAttributes.test.js | 4 +- .../directives/content/processContent.test.js | 4 +- .../directives/include/processInclude.test.js | 4 +- test/directives/repeat/processRepeat.test.js | 4 +- test/directives/test/processTest.test.js | 4 +- test/directives/unwrap/processUnwrap.test.js | 4 +- .../processTextExpressions.test.js | 4 +- test/expressions/resolveExpression.test.js | 4 +- test/expressions/resolveExpressions.test.js | 4 +- test/fixtures/blocks/accordion/accordion.js | 2 +- .../blocks/article-feed/article-feed.js | 2 +- test/fixtures/blocks/cards/cards.js | 2 +- test/templates/resolveTemplate.test.js | 4 +- 25 files changed, 969 insertions(+), 286 deletions(-) rename {src => dist}/faintly.js (51%) create mode 100644 dist/faintly.min.js create mode 100644 dist/faintly.min.js.map create mode 100644 src/directives.js create mode 100644 src/expressions.js create mode 100644 src/index.js create mode 100644 src/render.js create mode 100644 src/templates.js diff --git a/.eslintignore b/.eslintignore index e69de29..a7e5fab 100644 --- a/.eslintignore +++ b/.eslintignore @@ -0,0 +1,3 @@ +dist/ +coverage/ + diff --git a/README.md b/README.md index 9fc5b34..0e1140f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ I've experimented with other existing libraries (ejs templates, etc.) but wanted ## Getting Started -1. copy the faintly.js file to the scripts directory of your project +1. copy the /dist/faintly.js file (or minified version if you wish) to the scripts directory of your project 2. in the folder for your block, add a `blockName.html` file for the block template 3. in your block javascript, call the `renderBlock` function: @@ -96,4 +96,4 @@ Faintly supports a simple expression syntax for resolving data from the renderin For `data-fly-include`, HTML text, and normal attributes, wrap your expression in `${}`. -In all other `data-fly-*` attributes, just set the expression directly as the attribute value, no wrapping needed. +In all other `data-fly-*` attributes, just set the expression directly as the attribute value, no wrapping needed. \ No newline at end of file diff --git a/src/faintly.js b/dist/faintly.js similarity index 51% rename from src/faintly.js rename to dist/faintly.js index 39e575b..761451f 100644 --- a/src/faintly.js +++ b/dist/faintly.js @@ -1,122 +1,77 @@ -/** - * resolve the template to render - * - * @param {object} context the rendering context - * @returns {Promise} the template element - */ +// src/templates.js async function resolveTemplate(context) { context.template = context.template || {}; context.template.path = context.template.path || `${context.codeBasePath}/blocks/${context.blockName}/${context.blockName}.html`; - - const templateId = `faintly-template-${context.template.path}#${context.template.name || ''}`.toLowerCase().replace(/[^0-9a-z]/gi, '-'); + const templateId = `faintly-template-${context.template.path}#${context.template.name || ""}`.toLowerCase().replace(/[^0-9a-z]/gi, "-"); let template = document.getElementById(templateId); if (!template) { const resp = await fetch(context.template.path); if (!resp.ok) throw new Error(`Failed to fetch template from ${context.template.path} for block ${context.blockName}.`); - const markup = await resp.text(); - const dp = new DOMParser(); - const templateDom = dp.parseFromString(markup, 'text/html'); - - templateDom.querySelectorAll('template').forEach((t) => { - const name = t.getAttribute('data-fly-name') || ''; - t.id = `faintly-template-${context.template.path}#${name}`.toLowerCase().replace(/[^0-9a-z]/gi, '-'); - + const templateDom = dp.parseFromString(markup, "text/html"); + templateDom.querySelectorAll("template").forEach((t) => { + const name = t.getAttribute("data-fly-name") || ""; + t.id = `faintly-template-${context.template.path}#${name}`.toLowerCase().replace(/[^0-9a-z]/gi, "-"); document.body.append(t); }); } - template = document.getElementById(templateId); if (!template) throw new Error(`Failed to find template with id ${templateId}.`); - return template; } -/** - * resolves and returns data from the rendering context - * - * @param {string} expression the name of the data - * @param {Object} context the rendering context - * @returns {Promise} the data that was resolved - */ +// src/expressions.js async function resolveExpression(expression, context) { let resolved = context; - let prevResolved; - - const parts = expression.split('.'); + let previousResolvedValue; + const parts = expression.split("."); for (let i = 0; i < parts.length; i += 1) { - if (typeof resolved === 'undefined') break; - + if (typeof resolved === "undefined") break; const part = parts[i]; - prevResolved = resolved; + previousResolvedValue = resolved; resolved = resolved[part]; - - if (typeof resolved === 'function') { + if (typeof resolved === "function") { const functionParams = [{ ...context }]; - // eslint-disable-next-line no-await-in-loop - resolved = await resolved.apply(prevResolved, functionParams); + resolved = await resolved.apply(previousResolvedValue, functionParams); } } - return resolved; } - -/** - * resolves expressions in a string - * - * @param {string} str the string that may contain expressions - * @param {Object} context the rendering context - */ async function resolveExpressions(str, context) { const regexp = /(\\)?\${([a-z0-9\\.\s]+)}/dgi; - const promises = []; str.replaceAll(regexp, (match, escapeChar, expression) => { if (escapeChar) { promises.push(Promise.resolve(match.slice(1))); } - promises.push(resolveExpression(expression.trim(), context)); - return match; }); - if (promises.length > 0) { const promiseResults = await Promise.all(promises); const updatedText = str.replaceAll(regexp, () => { const result = promiseResults.shift(); return result; }); - return { updated: true, updatedText }; } - return { updated: false, updatedText: str }; } - -/** - * process text expressions within a text node, updating the node's textContent - * - * @param {Node} node the text node - * @param {Object} context the rendering context - */ async function processTextExpressions(node, context) { const { updated, updatedText } = await resolveExpressions(node.textContent, context); - if (updated) node.textContent = updatedText; } +// src/directives.js async function processAttributesDirective(el, context) { - if (!el.hasAttribute('data-fly-attributes')) return; - - const attrsExpression = el.getAttribute('data-fly-attributes'); + if (!el.hasAttribute("data-fly-attributes")) return; + const attrsExpression = el.getAttribute("data-fly-attributes"); const attrsData = await resolveExpression(attrsExpression, context); - - el.removeAttribute('data-fly-attributes'); + el.removeAttribute("data-fly-attributes"); if (attrsData) { Object.entries(attrsData).forEach(([k, v]) => { - if (v === undefined) { + if (v === void 0) { el.removeAttribute(k); } else { el.setAttribute(k, v); @@ -124,296 +79,159 @@ async function processAttributesDirective(el, context) { }); } } - -/** - * process the attributes directive, as well as any expressions in non `data-fly-*` attributes - * - * @param {Element} el the element to process - * @param {Object} context the rendering context - */ async function processAttributes(el, context) { processAttributesDirective(el, context); - - const attrPromises = el.getAttributeNames() - .filter((attrName) => !attrName.startsWith('data-fly-')) - .map(async (attrName) => { - const { updated, updatedText } = await resolveExpressions(el.getAttribute(attrName), context); - if (updated) el.setAttribute(attrName, updatedText); - }); + const attrPromises = el.getAttributeNames().filter((attrName) => !attrName.startsWith("data-fly-")).map(async (attrName) => { + const { updated, updatedText } = await resolveExpressions(el.getAttribute(attrName), context); + if (updated) el.setAttribute(attrName, updatedText); + }); await Promise.all(attrPromises); } - -/** - * processes the test directive - * - * @param {Element} el the element to process - * @param {Object} context the rendering context - * @returns {Promise} indicator if node should be rendered - */ async function processTest(el, context) { - const testAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-test') || attrName.startsWith('data-fly-not')); + const testAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith("data-fly-test") || attrName.startsWith("data-fly-not")); if (!testAttrName) return true; - - const nameParts = testAttrName.split('.'); - const contextName = nameParts[1] || ''; - + const nameParts = testAttrName.split("."); + const contextName = nameParts[1] || ""; const testExpression = el.getAttribute(testAttrName); const testData = await resolveExpression(testExpression, context); - el.removeAttribute(testAttrName); - - const testResult = testAttrName.startsWith('data-fly-not') ? !testData : !!testData; - + const testResult = testAttrName.startsWith("data-fly-not") ? !testData : !!testData; if (contextName) context[contextName.toLowerCase()] = testResult; - if (!testResult) { el.remove(); } - return testResult; } - -/** - * process the unwrap directive, leavving the attribute only if it resolves to true - * - * @param {Element} el the element to process - * @param {Object} context the rendering context - * @returns {Promise} - */ -async function resolveUnwrap(el, context) { - if (!el.hasAttribute('data-fly-unwrap')) return; - - const unwrapExpression = el.getAttribute('data-fly-unwrap'); - if (unwrapExpression) { - const unwrapVal = !!(await resolveExpression(unwrapExpression, context)); - - if (!unwrapVal) { - el.removeAttribute('data-fly-unwrap'); - } - } -} - -function processUnwraps(el) { - el.querySelectorAll('[data-fly-unwrap]').forEach((unwrapEl) => { - unwrapEl.before(...unwrapEl.childNodes); - unwrapEl.remove(); - }); -} - -/** - * process the content directive - * - * @param {Element} el the element to process - * @param {Object} context the rendering context - * @returns {Promise} if there was a content directive - */ async function processContent(el, context) { - if (!el.hasAttribute('data-fly-content')) return false; - - const contentExpression = el.getAttribute('data-fly-content'); + if (!el.hasAttribute("data-fly-content")) return false; + const contentExpression = el.getAttribute("data-fly-content"); const content = await resolveExpression(contentExpression, context); - - el.removeAttribute('data-fly-content'); - - if (content !== undefined) { + el.removeAttribute("data-fly-content"); + if (content !== void 0) { if (content instanceof Node) { el.replaceChildren(content); - } else if (Array.isArray(content) - || content instanceof NodeList || content instanceof HTMLCollection) { + } else if (Array.isArray(content) || content instanceof NodeList || content instanceof HTMLCollection) { el.replaceChildren(...content); } else { const textNode = document.createTextNode(content); el.replaceChildren(textNode); } } else { - el.textContent = ''; + el.textContent = ""; } - return true; } - -/** - * processes the repeat directive - * - * @param {Element} el the element to potentially be repeated - * @param {Object} context the rendering context - * @returns {Promise} if the node was repeated - * the net number of nodes added/removed as a result of the repeat directive - */ async function processRepeat(el, context) { - const repeatAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-repeat')); + const repeatAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith("data-fly-repeat")); if (!repeatAttrName) return false; - - const nameParts = repeatAttrName.split('.'); - const contextName = nameParts[1] || 'item'; - + const nameParts = repeatAttrName.split("."); + const contextName = nameParts[1] || "item"; const repeatExpression = el.getAttribute(repeatAttrName); const arr = await resolveExpression(repeatExpression, context); if (!arr || Object.keys(arr).length === 0) { el.remove(); return true; } - el.removeAttribute(repeatAttrName); const repeatedNodes = await Promise.all(Object.entries(arr).map(async ([key, item], i) => { const cloned = el.cloneNode(true); - const repeatContext = { ...context }; repeatContext[contextName.toLowerCase()] = item; repeatContext[`${contextName.toLowerCase()}Index`] = i; repeatContext[`${contextName.toLowerCase()}Number`] = i + 1; repeatContext[`${contextName.toLowerCase()}Key`] = key; - - // eslint-disable-next-line no-use-before-define await processNode(cloned, repeatContext); - return cloned; })); - let afterEL = el; repeatedNodes.forEach((node) => { afterEL.after(node); afterEL = node; }); - el.remove(); - return true; } - -/** - * process the include directive - * - * @param {Element} el the element to process - * @param {Object} context the rendering context - * @returns {Promise} if there was a include directive - */ async function processInclude(el, context) { - if (!el.hasAttribute('data-fly-include')) return false; - - const includeValue = el.getAttribute('data-fly-include'); - el.removeAttribute('data-fly-include'); + if (!el.hasAttribute("data-fly-include")) return false; + const includeValue = el.getAttribute("data-fly-include"); + el.removeAttribute("data-fly-include"); const { updatedText } = await resolveExpressions(includeValue, context); - - let templatePath = context.template ? context.template.path : ''; + let templatePath = context.template ? context.template.path : ""; let templateName = updatedText; - if (templateName.startsWith('/')) { - const [path, name] = templateName.split('#'); + if (templateName.startsWith("/")) { + const [path, name] = templateName.split("#"); templatePath = path; templateName = name; } - const includeContext = { ...context, template: { name: templateName, - path: templatePath, - }, + path: templatePath + } }; - - // eslint-disable-next-line no-use-before-define await renderElement(el, includeContext); - return true; } +async function resolveUnwrap(el, context) { + if (!el.hasAttribute("data-fly-unwrap")) return; + const unwrapExpression = el.getAttribute("data-fly-unwrap"); + if (unwrapExpression) { + const unwrapVal = !!await resolveExpression(unwrapExpression, context); + if (!unwrapVal) { + el.removeAttribute("data-fly-unwrap"); + } + } +} +function processUnwraps(el) { + el.querySelectorAll("[data-fly-unwrap]").forEach((unwrapEl) => { + unwrapEl.before(...unwrapEl.childNodes); + unwrapEl.remove(); + }); +} -/** - * recursively renders a dom node, processing all directives - * - * @param {Node} node the node to render - * @param {Object} context the rendering context - * @returns {Promise} a promise that resolves when the node has been rendered - */ +// src/render.js async function processNode(node, context) { context.currentNode = node; let processChildren = [Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(node.nodeType); if (node.nodeType === Node.ELEMENT_NODE) { const shouldRender = await processTest(node, context); if (!shouldRender) return; - const repeated = await processRepeat(node, context); if (repeated) return; - await processAttributes(node, context); - - processChildren = (await processContent(node, context)) - || (await processInclude(node, context)) || true; - + processChildren = await processContent(node, context) || await processInclude(node, context) || true; await resolveUnwrap(node, context); } else if (node.nodeType === Node.TEXT_NODE) { await processTextExpressions(node, context); } - const children = !processChildren ? [] : [...node.childNodes]; - for (let i = 0; i < children.length; i += 1) { const child = children[i]; - // eslint-disable-next-line no-await-in-loop await processNode(child, context); } } - -/** - * Render a template - * @param {Element} template the template to render - * @param {Object} context the rendering context - */ async function renderTemplate(template, context) { const templateClone = template.cloneNode(true); await processNode(templateClone.content, context); - processUnwraps(templateClone.content); - return templateClone; } - -/** - * transform the element, replacing it's children with the content from the template - * @param {Element} el the element - * @param {Element} template the template element - * @param {Object} context the rendering context - */ async function renderElementWithTemplate(el, template, context) { const rendered = await renderTemplate(template, context); el.replaceChildren(rendered.content); } - -/** - * Transform an element using an HTML template - * - * @param {Element} block the block element - * @param {Object} context the rendering context - */ -export async function renderElement(el, context) { +async function renderElement(el, context) { const template = await resolveTemplate(context); - await renderElementWithTemplate(el, template, context); } - -/** - * Transform a block using an HTML template - * - * @param {Element} block the block element - * @param {Object} context the rendering context - */ -export async function renderBlock(block, context = {}) { +async function renderBlock(block, context = {}) { context.block = block; context.blockName = block.dataset.blockName; - context.codeBasePath = context.codeBasePath || (window.hlx ? window.hlx.codeBasePath : ''); - + context.codeBasePath = context.codeBasePath || (window.hlx ? window.hlx.codeBasePath : ""); await renderElement(block, context); } - -export const exportForTesting = { - resolveTemplate, - resolveExpression, - resolveExpressions, - processTextExpressions, - processAttributes, - processTest, - processContent, - processInclude, - processRepeat, - resolveUnwrap, - processUnwraps, +export { + renderBlock, + renderElement }; diff --git a/dist/faintly.min.js b/dist/faintly.min.js new file mode 100644 index 0000000..f6d2bc5 --- /dev/null +++ b/dist/faintly.min.js @@ -0,0 +1,2 @@ +async function h(t){t.template=t.template||{},t.template.path=t.template.path||`${t.codeBasePath}/blocks/${t.blockName}/${t.blockName}.html`;let e=`faintly-template-${t.template.path}#${t.template.name||""}`.toLowerCase().replace(/[^0-9a-z]/gi,"-"),a=document.getElementById(e);if(!a){let r=await fetch(t.template.path);if(!r.ok)throw new Error(`Failed to fetch template from ${t.template.path} for block ${t.blockName}.`);let s=await r.text();new DOMParser().parseFromString(s,"text/html").querySelectorAll("template").forEach(i=>{let p=i.getAttribute("data-fly-name")||"";i.id=`faintly-template-${t.template.path}#${p}`.toLowerCase().replace(/[^0-9a-z]/gi,"-"),document.body.append(i)})}if(a=document.getElementById(e),!a)throw new Error(`Failed to find template with id ${e}.`);return a}async function c(t,e){let a=e,r,s=t.split(".");for(let n=0;n"u");n+=1){let o=s[n];if(r=a,a=a[o],typeof a=="function"){let i=[{...e}];a=await a.apply(r,i)}}return a}async function d(t,e){let a=/(\\)?\${([a-z0-9\\.\s]+)}/dgi,r=[];if(t.replaceAll(a,(s,n,o)=>(n&&r.push(Promise.resolve(s.slice(1))),r.push(c(o.trim(),e)),s)),r.length>0){let s=await Promise.all(r);return{updated:!0,updatedText:t.replaceAll(a,()=>s.shift())}}return{updated:!1,updatedText:t}}async function b(t,e){let{updated:a,updatedText:r}=await d(t.textContent,e);a&&(t.textContent=r)}async function P(t,e){if(!t.hasAttribute("data-fly-attributes"))return;let a=t.getAttribute("data-fly-attributes"),r=await c(a,e);t.removeAttribute("data-fly-attributes"),r&&Object.entries(r).forEach(([s,n])=>{n===void 0?t.removeAttribute(s):t.setAttribute(s,n)})}async function A(t,e){P(t,e);let a=t.getAttributeNames().filter(r=>!r.startsWith("data-fly-")).map(async r=>{let{updated:s,updatedText:n}=await d(t.getAttribute(r),e);s&&t.setAttribute(r,n)});await Promise.all(a)}async function N(t,e){let a=t.getAttributeNames().find(p=>p.startsWith("data-fly-test")||p.startsWith("data-fly-not"));if(!a)return!0;let s=a.split(".")[1]||"",n=t.getAttribute(a),o=await c(n,e);t.removeAttribute(a);let i=a.startsWith("data-fly-not")?!o:!!o;return s&&(e[s.toLowerCase()]=i),i||t.remove(),i}async function E(t,e){if(!t.hasAttribute("data-fly-content"))return!1;let a=t.getAttribute("data-fly-content"),r=await c(a,e);if(t.removeAttribute("data-fly-content"),r!==void 0)if(r instanceof Node)t.replaceChildren(r);else if(Array.isArray(r)||r instanceof NodeList||r instanceof HTMLCollection)t.replaceChildren(...r);else{let s=document.createTextNode(r);t.replaceChildren(s)}else t.textContent="";return!0}async function T(t,e){let a=t.getAttributeNames().find(l=>l.startsWith("data-fly-repeat"));if(!a)return!1;let s=a.split(".")[1]||"item",n=t.getAttribute(a),o=await c(n,e);if(!o||Object.keys(o).length===0)return t.remove(),!0;t.removeAttribute(a);let i=await Promise.all(Object.entries(o).map(async([l,x],w)=>{let y=t.cloneNode(!0),u={...e};return u[s.toLowerCase()]=x,u[`${s.toLowerCase()}Index`]=w,u[`${s.toLowerCase()}Number`]=w+1,u[`${s.toLowerCase()}Key`]=l,await f(y,u),y})),p=t;return i.forEach(l=>{p.after(l),p=l}),t.remove(),!0}async function v(t,e){if(!t.hasAttribute("data-fly-include"))return!1;let a=t.getAttribute("data-fly-include");t.removeAttribute("data-fly-include");let{updatedText:r}=await d(a,e),s=e.template?e.template.path:"",n=r;if(n.startsWith("/")){let[i,p]=n.split("#");s=i,n=p}let o={...e,template:{name:n,path:s}};return await m(t,o),!0}async function g(t,e){if(!t.hasAttribute("data-fly-unwrap"))return;let a=t.getAttribute("data-fly-unwrap");a&&(await c(a,e)||t.removeAttribute("data-fly-unwrap"))}function C(t){t.querySelectorAll("[data-fly-unwrap]").forEach(e=>{e.before(...e.childNodes),e.remove()})}async function f(t,e){e.currentNode=t;let a=[Node.ELEMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE].includes(t.nodeType);if(t.nodeType===Node.ELEMENT_NODE){if(!await N(t,e)||await T(t,e))return;await A(t,e),a=await E(t,e)||await v(t,e)||!0,await g(t,e)}else t.nodeType===Node.TEXT_NODE&&await b(t,e);let r=a?[...t.childNodes]:[];for(let s=0;s} the template element\n */\nexport default async function resolveTemplate(context) {\n context.template = context.template || {};\n context.template.path = context.template.path || `${context.codeBasePath}/blocks/${context.blockName}/${context.blockName}.html`;\n\n const templateId = `faintly-template-${context.template.path}#${context.template.name || ''}`.toLowerCase().replace(/[^0-9a-z]/gi, '-');\n let template = document.getElementById(templateId);\n if (!template) {\n const resp = await fetch(context.template.path);\n if (!resp.ok) throw new Error(`Failed to fetch template from ${context.template.path} for block ${context.blockName}.`);\n\n const markup = await resp.text();\n\n const dp = new DOMParser();\n const templateDom = dp.parseFromString(markup, 'text/html');\n\n templateDom.querySelectorAll('template').forEach((t) => {\n const name = t.getAttribute('data-fly-name') || '';\n t.id = `faintly-template-${context.template.path}#${name}`.toLowerCase().replace(/[^0-9a-z]/gi, '-');\n\n document.body.append(t);\n });\n }\n\n template = document.getElementById(templateId);\n if (!template) throw new Error(`Failed to find template with id ${templateId}.`);\n\n return template;\n}\n", "/**\n * resolves and returns data from the rendering context\n *\n * @param {string} expression the name of the data\n * @param {Object} context the rendering context\n * @returns {Promise} the data that was resolved\n */\nexport async function resolveExpression(expression, context) {\n let resolved = context;\n let previousResolvedValue;\n\n const parts = expression.split('.');\n for (let i = 0; i < parts.length; i += 1) {\n if (typeof resolved === 'undefined') break;\n\n const part = parts[i];\n previousResolvedValue = resolved;\n resolved = resolved[part];\n\n if (typeof resolved === 'function') {\n const functionParams = [{ ...context }];\n // eslint-disable-next-line no-await-in-loop\n resolved = await resolved.apply(previousResolvedValue, functionParams);\n }\n }\n\n return resolved;\n}\n\n/**\n * resolves expressions in a string\n *\n * @param {string} str the string that may contain expressions\n * @param {Object} context the rendering context\n */\nexport async function resolveExpressions(str, context) {\n const regexp = /(\\\\)?\\${([a-z0-9\\\\.\\s]+)}/dgi;\n\n const promises = [];\n str.replaceAll(regexp, (match, escapeChar, expression) => {\n if (escapeChar) {\n promises.push(Promise.resolve(match.slice(1)));\n }\n\n promises.push(resolveExpression(expression.trim(), context));\n\n return match;\n });\n\n if (promises.length > 0) {\n const promiseResults = await Promise.all(promises);\n const updatedText = str.replaceAll(regexp, () => {\n const result = promiseResults.shift();\n return result;\n });\n\n return { updated: true, updatedText };\n }\n\n return { updated: false, updatedText: str };\n}\n\n/**\n * process text expressions within a text node, updating the node's textContent\n *\n * @param {Node} node the text node\n * @param {Object} context the rendering context\n */\nexport async function processTextExpressions(node, context) {\n const { updated, updatedText } = await resolveExpressions(node.textContent, context);\n\n if (updated) node.textContent = updatedText;\n}\n", "import { resolveExpression, resolveExpressions } from './expressions.js';\nimport { processNode, renderElement } from './render.js';\n\nasync function processAttributesDirective(el, context) {\n if (!el.hasAttribute('data-fly-attributes')) return;\n\n const attrsExpression = el.getAttribute('data-fly-attributes');\n const attrsData = await resolveExpression(attrsExpression, context);\n\n el.removeAttribute('data-fly-attributes');\n if (attrsData) {\n Object.entries(attrsData).forEach(([k, v]) => {\n if (v === undefined) {\n el.removeAttribute(k);\n } else {\n el.setAttribute(k, v);\n }\n });\n }\n}\n\n/**\n * process the attributes directive, as well as any expressions in non `data-fly-*` attributes\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n */\nexport async function processAttributes(el, context) {\n processAttributesDirective(el, context);\n\n const attrPromises = el.getAttributeNames()\n .filter((attrName) => !attrName.startsWith('data-fly-'))\n .map(async (attrName) => {\n const { updated, updatedText } = await resolveExpressions(el.getAttribute(attrName), context);\n if (updated) el.setAttribute(attrName, updatedText);\n });\n await Promise.all(attrPromises);\n}\n\n/**\n * processes the test directive\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise} indicator if node should be rendered\n */\nexport async function processTest(el, context) {\n const testAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-test') || attrName.startsWith('data-fly-not'));\n if (!testAttrName) return true;\n\n const nameParts = testAttrName.split('.');\n const contextName = nameParts[1] || '';\n\n const testExpression = el.getAttribute(testAttrName);\n const testData = await resolveExpression(testExpression, context);\n\n el.removeAttribute(testAttrName);\n\n const testResult = testAttrName.startsWith('data-fly-not') ? !testData : !!testData;\n\n if (contextName) context[contextName.toLowerCase()] = testResult;\n\n if (!testResult) {\n el.remove();\n }\n\n return testResult;\n}\n\n/**\n * process the content directive\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise} if there was a content directive\n */\nexport async function processContent(el, context) {\n if (!el.hasAttribute('data-fly-content')) return false;\n\n const contentExpression = el.getAttribute('data-fly-content');\n const content = await resolveExpression(contentExpression, context);\n\n el.removeAttribute('data-fly-content');\n\n if (content !== undefined) {\n if (content instanceof Node) {\n el.replaceChildren(content);\n } else if (Array.isArray(content)\n || content instanceof NodeList || content instanceof HTMLCollection) {\n el.replaceChildren(...content);\n } else {\n const textNode = document.createTextNode(content);\n el.replaceChildren(textNode);\n }\n } else {\n el.textContent = '';\n }\n\n return true;\n}\n\n/**\n * processes the repeat directive\n *\n * @param {Element} el the element to potentially be repeated\n * @param {Object} context the rendering context\n * @returns {Promise} if the node was repeated\n * the net number of nodes added/removed as a result of the repeat directive\n */\nexport async function processRepeat(el, context) {\n const repeatAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-repeat'));\n if (!repeatAttrName) return false;\n\n const nameParts = repeatAttrName.split('.');\n const contextName = nameParts[1] || 'item';\n\n const repeatExpression = el.getAttribute(repeatAttrName);\n const arr = await resolveExpression(repeatExpression, context);\n if (!arr || Object.keys(arr).length === 0) {\n el.remove();\n return true;\n }\n\n el.removeAttribute(repeatAttrName);\n const repeatedNodes = await Promise.all(Object.entries(arr).map(async ([key, item], i) => {\n const cloned = el.cloneNode(true);\n\n const repeatContext = { ...context };\n repeatContext[contextName.toLowerCase()] = item;\n repeatContext[`${contextName.toLowerCase()}Index`] = i;\n repeatContext[`${contextName.toLowerCase()}Number`] = i + 1;\n repeatContext[`${contextName.toLowerCase()}Key`] = key;\n\n // eslint-disable-next-line no-use-before-define\n await processNode(cloned, repeatContext);\n\n return cloned;\n }));\n\n let afterEL = el;\n repeatedNodes.forEach((node) => {\n afterEL.after(node);\n afterEL = node;\n });\n\n el.remove();\n\n return true;\n}\n\n/**\n * process the include directive\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise} if there was a include directive\n */\nexport async function processInclude(el, context) {\n if (!el.hasAttribute('data-fly-include')) return false;\n\n const includeValue = el.getAttribute('data-fly-include');\n el.removeAttribute('data-fly-include');\n const { updatedText } = await resolveExpressions(includeValue, context);\n\n let templatePath = context.template ? context.template.path : '';\n let templateName = updatedText;\n if (templateName.startsWith('/')) {\n const [path, name] = templateName.split('#');\n templatePath = path;\n templateName = name;\n }\n\n const includeContext = {\n ...context,\n template: {\n name: templateName,\n path: templatePath,\n },\n };\n\n await renderElement(el, includeContext);\n\n return true;\n}\n\n/**\n * process the unwrap directive, leavving the attribute only if it resolves to true\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise}\n */\nexport async function resolveUnwrap(el, context) {\n if (!el.hasAttribute('data-fly-unwrap')) return;\n\n const unwrapExpression = el.getAttribute('data-fly-unwrap');\n if (unwrapExpression) {\n const unwrapVal = !!(await resolveExpression(unwrapExpression, context));\n\n if (!unwrapVal) {\n el.removeAttribute('data-fly-unwrap');\n }\n }\n}\n\nexport function processUnwraps(el) {\n el.querySelectorAll('[data-fly-unwrap]').forEach((unwrapEl) => {\n unwrapEl.before(...unwrapEl.childNodes);\n unwrapEl.remove();\n });\n}\n", "import resolveTemplate from './templates.js';\nimport { processTextExpressions } from './expressions.js';\nimport {\n processAttributes,\n processContent,\n processInclude,\n processRepeat,\n processTest,\n processUnwraps,\n resolveUnwrap,\n} from './directives.js';\n\n/**\n * recursively renders a dom node, processing all directives\n *\n * @param {Node} node the node to render\n * @param {Object} context the rendering context\n * @returns {Promise} a promise that resolves when the node has been rendered\n */\nexport async function processNode(node, context) {\n context.currentNode = node;\n let processChildren = [Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(node.nodeType);\n if (node.nodeType === Node.ELEMENT_NODE) {\n const shouldRender = await processTest(node, context);\n if (!shouldRender) return;\n\n const repeated = await processRepeat(node, context);\n if (repeated) return;\n\n await processAttributes(node, context);\n\n processChildren = (await processContent(node, context))\n || (await processInclude(node, context)) || true;\n\n await resolveUnwrap(node, context);\n } else if (node.nodeType === Node.TEXT_NODE) {\n await processTextExpressions(node, context);\n }\n\n const children = !processChildren ? [] : [...node.childNodes];\n\n for (let i = 0; i < children.length; i += 1) {\n const child = children[i];\n // eslint-disable-next-line no-await-in-loop\n await processNode(child, context);\n }\n}\n\n/**\n * Render a template\n * @param {Element} template the template to render\n * @param {Object} context the rendering context\n */\nexport async function renderTemplate(template, context) {\n const templateClone = template.cloneNode(true);\n await processNode(templateClone.content, context);\n\n processUnwraps(templateClone.content);\n\n return templateClone;\n}\n\n/**\n * transform the element, replacing it's children with the content from the template\n * @param {Element} el the element\n * @param {Element} template the template element\n * @param {Object} context the rendering context\n */\nexport async function renderElementWithTemplate(el, template, context) {\n const rendered = await renderTemplate(template, context);\n el.replaceChildren(rendered.content);\n}\n\n/**\n * Transform an element using an HTML template\n *\n * @param {Element} block the block element\n * @param {Object} context the rendering context\n */\nexport async function renderElement(el, context) {\n const template = await resolveTemplate(context);\n\n await renderElementWithTemplate(el, template, context);\n}\n\n/**\n * Transform a block using an HTML template\n *\n * @param {Element} block the block element\n * @param {Object} context the rendering context\n */\nexport async function renderBlock(block, context = {}) {\n context.block = block;\n context.blockName = block.dataset.blockName;\n context.codeBasePath = context.codeBasePath || (window.hlx ? window.hlx.codeBasePath : '');\n\n await renderElement(block, context);\n}\n"], + "mappings": "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", + "names": ["resolveTemplate", "context", "templateId", "template", "resp", "markup", "t", "name", "resolveExpression", "expression", "context", "resolved", "previousResolvedValue", "parts", "i", "part", "functionParams", "resolveExpressions", "str", "regexp", "promises", "match", "escapeChar", "promiseResults", "processTextExpressions", "node", "updated", "updatedText", "processAttributesDirective", "el", "context", "attrsExpression", "attrsData", "resolveExpression", "k", "v", "processAttributes", "attrPromises", "attrName", "updated", "updatedText", "resolveExpressions", "processTest", "testAttrName", "contextName", "testExpression", "testData", "testResult", "processContent", "contentExpression", "content", "textNode", "processRepeat", "repeatAttrName", "repeatExpression", "arr", "repeatedNodes", "key", "item", "i", "cloned", "repeatContext", "processNode", "afterEL", "node", "processInclude", "includeValue", "templatePath", "templateName", "path", "name", "includeContext", "renderElement", "resolveUnwrap", "unwrapExpression", "processUnwraps", "unwrapEl", "processNode", "node", "context", "processChildren", "processTest", "processRepeat", "processAttributes", "processContent", "processInclude", "resolveUnwrap", "processTextExpressions", "children", "i", "child", "renderTemplate", "template", "templateClone", "processUnwraps", "renderElementWithTemplate", "el", "rendered", "renderElement", "resolveTemplate", "renderBlock", "block"] +} diff --git a/package-lock.json b/package-lock.json index 99d07a8..992f4d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@web/test-runner": "0.19.0", "@web/test-runner-commands": "0.9.0", "@web/test-runner-mocha": "0.9.0", + "esbuild": "0.23.0", "eslint": "8.57.1", "eslint-config-airbnb-base": "15.0.0", "eslint-plugin-import": "2.31.0" @@ -296,6 +297,414 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -2918,6 +3327,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", diff --git a/package.json b/package.json index 169f766..9b790dd 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "test:perf": "web-test-runner --node-resolve --group perf", "test:perf:watch": "web-test-runner --node-resolve --watch --group perf", "lint": "eslint .", - "lint:fix": "eslint . --fix" + "lint:fix": "eslint . --fix", + "build:dist": "esbuild src/index.js --bundle --format=esm --platform=browser --outfile=dist/faintly.js", + "build:dist:min": "esbuild src/index.js --bundle --format=esm --platform=browser --minify --sourcemap --legal-comments=none --outfile=dist/faintly.min.js", + "build": "npm run build:dist && npm run build:dist:min", + "clean": "rm -rf dist" }, "author": "Sean Steimer", "license": "Apache-2.0", @@ -21,6 +25,7 @@ "@web/test-runner-mocha": "0.9.0", "eslint": "8.57.1", "eslint-config-airbnb-base": "15.0.0", - "eslint-plugin-import": "2.31.0" + "eslint-plugin-import": "2.31.0", + "esbuild": "0.23.0" } } \ No newline at end of file diff --git a/src/directives.js b/src/directives.js new file mode 100644 index 0000000..468c7e0 --- /dev/null +++ b/src/directives.js @@ -0,0 +1,212 @@ +import { resolveExpression, resolveExpressions } from './expressions.js'; +// eslint-disable-next-line import/no-cycle +import { processNode, renderElement } from './render.js'; + +async function processAttributesDirective(el, context) { + if (!el.hasAttribute('data-fly-attributes')) return; + + const attrsExpression = el.getAttribute('data-fly-attributes'); + const attrsData = await resolveExpression(attrsExpression, context); + + el.removeAttribute('data-fly-attributes'); + if (attrsData) { + Object.entries(attrsData).forEach(([k, v]) => { + if (v === undefined) { + el.removeAttribute(k); + } else { + el.setAttribute(k, v); + } + }); + } +} + +/** + * process the attributes directive, as well as any expressions in non `data-fly-*` attributes + * + * @param {Element} el the element to process + * @param {Object} context the rendering context + */ +export async function processAttributes(el, context) { + processAttributesDirective(el, context); + + const attrPromises = el.getAttributeNames() + .filter((attrName) => !attrName.startsWith('data-fly-')) + .map(async (attrName) => { + const { updated, updatedText } = await resolveExpressions(el.getAttribute(attrName), context); + if (updated) el.setAttribute(attrName, updatedText); + }); + await Promise.all(attrPromises); +} + +/** + * processes the test directive + * + * @param {Element} el the element to process + * @param {Object} context the rendering context + * @returns {Promise} indicator if node should be rendered + */ +export async function processTest(el, context) { + const testAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-test') || attrName.startsWith('data-fly-not')); + if (!testAttrName) return true; + + const nameParts = testAttrName.split('.'); + const contextName = nameParts[1] || ''; + + const testExpression = el.getAttribute(testAttrName); + const testData = await resolveExpression(testExpression, context); + + el.removeAttribute(testAttrName); + + const testResult = testAttrName.startsWith('data-fly-not') ? !testData : !!testData; + + if (contextName) context[contextName.toLowerCase()] = testResult; + + if (!testResult) { + el.remove(); + } + + return testResult; +} + +/** + * process the content directive + * + * @param {Element} el the element to process + * @param {Object} context the rendering context + * @returns {Promise} if there was a content directive + */ +export async function processContent(el, context) { + if (!el.hasAttribute('data-fly-content')) return false; + + const contentExpression = el.getAttribute('data-fly-content'); + const content = await resolveExpression(contentExpression, context); + + el.removeAttribute('data-fly-content'); + + if (content !== undefined) { + if (content instanceof Node) { + el.replaceChildren(content); + } else if (Array.isArray(content) + || content instanceof NodeList || content instanceof HTMLCollection) { + el.replaceChildren(...content); + } else { + const textNode = document.createTextNode(content); + el.replaceChildren(textNode); + } + } else { + el.textContent = ''; + } + + return true; +} + +/** + * processes the repeat directive + * + * @param {Element} el the element to potentially be repeated + * @param {Object} context the rendering context + * @returns {Promise} if the node was repeated + * the net number of nodes added/removed as a result of the repeat directive + */ +export async function processRepeat(el, context) { + const repeatAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-repeat')); + if (!repeatAttrName) return false; + + const nameParts = repeatAttrName.split('.'); + const contextName = nameParts[1] || 'item'; + + const repeatExpression = el.getAttribute(repeatAttrName); + const arr = await resolveExpression(repeatExpression, context); + if (!arr || Object.keys(arr).length === 0) { + el.remove(); + return true; + } + + el.removeAttribute(repeatAttrName); + const repeatedNodes = await Promise.all(Object.entries(arr).map(async ([key, item], i) => { + const cloned = el.cloneNode(true); + + const repeatContext = { ...context }; + repeatContext[contextName.toLowerCase()] = item; + repeatContext[`${contextName.toLowerCase()}Index`] = i; + repeatContext[`${contextName.toLowerCase()}Number`] = i + 1; + repeatContext[`${contextName.toLowerCase()}Key`] = key; + + // eslint-disable-next-line no-use-before-define + await processNode(cloned, repeatContext); + + return cloned; + })); + + let afterEL = el; + repeatedNodes.forEach((node) => { + afterEL.after(node); + afterEL = node; + }); + + el.remove(); + + return true; +} + +/** + * process the include directive + * + * @param {Element} el the element to process + * @param {Object} context the rendering context + * @returns {Promise} if there was a include directive + */ +export async function processInclude(el, context) { + if (!el.hasAttribute('data-fly-include')) return false; + + const includeValue = el.getAttribute('data-fly-include'); + el.removeAttribute('data-fly-include'); + const { updatedText } = await resolveExpressions(includeValue, context); + + let templatePath = context.template ? context.template.path : ''; + let templateName = updatedText; + if (templateName.startsWith('/')) { + const [path, name] = templateName.split('#'); + templatePath = path; + templateName = name; + } + + const includeContext = { + ...context, + template: { + name: templateName, + path: templatePath, + }, + }; + + await renderElement(el, includeContext); + + return true; +} + +/** + * process the unwrap directive, leavving the attribute only if it resolves to true + * + * @param {Element} el the element to process + * @param {Object} context the rendering context + * @returns {Promise} + */ +export async function resolveUnwrap(el, context) { + if (!el.hasAttribute('data-fly-unwrap')) return; + + const unwrapExpression = el.getAttribute('data-fly-unwrap'); + if (unwrapExpression) { + const unwrapVal = !!(await resolveExpression(unwrapExpression, context)); + + if (!unwrapVal) { + el.removeAttribute('data-fly-unwrap'); + } + } +} + +export function processUnwraps(el) { + el.querySelectorAll('[data-fly-unwrap]').forEach((unwrapEl) => { + unwrapEl.before(...unwrapEl.childNodes); + unwrapEl.remove(); + }); +} diff --git a/src/expressions.js b/src/expressions.js new file mode 100644 index 0000000..109edeb --- /dev/null +++ b/src/expressions.js @@ -0,0 +1,73 @@ +/** + * resolves and returns data from the rendering context + * + * @param {string} expression the name of the data + * @param {Object} context the rendering context + * @returns {Promise} the data that was resolved + */ +export async function resolveExpression(expression, context) { + let resolved = context; + let previousResolvedValue; + + const parts = expression.split('.'); + for (let i = 0; i < parts.length; i += 1) { + if (typeof resolved === 'undefined') break; + + const part = parts[i]; + previousResolvedValue = resolved; + resolved = resolved[part]; + + if (typeof resolved === 'function') { + const functionParams = [{ ...context }]; + // eslint-disable-next-line no-await-in-loop + resolved = await resolved.apply(previousResolvedValue, functionParams); + } + } + + return resolved; +} + +/** + * resolves expressions in a string + * + * @param {string} str the string that may contain expressions + * @param {Object} context the rendering context + */ +export async function resolveExpressions(str, context) { + const regexp = /(\\)?\${([a-z0-9\\.\s]+)}/dgi; + + const promises = []; + str.replaceAll(regexp, (match, escapeChar, expression) => { + if (escapeChar) { + promises.push(Promise.resolve(match.slice(1))); + } + + promises.push(resolveExpression(expression.trim(), context)); + + return match; + }); + + if (promises.length > 0) { + const promiseResults = await Promise.all(promises); + const updatedText = str.replaceAll(regexp, () => { + const result = promiseResults.shift(); + return result; + }); + + return { updated: true, updatedText }; + } + + return { updated: false, updatedText: str }; +} + +/** + * process text expressions within a text node, updating the node's textContent + * + * @param {Node} node the text node + * @param {Object} context the rendering context + */ +export async function processTextExpressions(node, context) { + const { updated, updatedText } = await resolveExpressions(node.textContent, context); + + if (updated) node.textContent = updatedText; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1afd0b5 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +export { renderElement, renderBlock } from './render.js'; diff --git a/src/render.js b/src/render.js new file mode 100644 index 0000000..10328a4 --- /dev/null +++ b/src/render.js @@ -0,0 +1,99 @@ +import resolveTemplate from './templates.js'; +import { processTextExpressions } from './expressions.js'; +// eslint-disable-next-line import/no-cycle +import { + processAttributes, + processContent, + processInclude, + processRepeat, + processTest, + processUnwraps, + resolveUnwrap, +} from './directives.js'; + +/** + * recursively renders a dom node, processing all directives + * + * @param {Node} node the node to render + * @param {Object} context the rendering context + * @returns {Promise} a promise that resolves when the node has been rendered + */ +export async function processNode(node, context) { + context.currentNode = node; + let processChildren = [Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(node.nodeType); + if (node.nodeType === Node.ELEMENT_NODE) { + const shouldRender = await processTest(node, context); + if (!shouldRender) return; + + const repeated = await processRepeat(node, context); + if (repeated) return; + + await processAttributes(node, context); + + processChildren = (await processContent(node, context)) + || (await processInclude(node, context)) || true; + + await resolveUnwrap(node, context); + } else if (node.nodeType === Node.TEXT_NODE) { + await processTextExpressions(node, context); + } + + const children = !processChildren ? [] : [...node.childNodes]; + + for (let i = 0; i < children.length; i += 1) { + const child = children[i]; + // eslint-disable-next-line no-await-in-loop + await processNode(child, context); + } +} + +/** + * Render a template + * @param {Element} template the template to render + * @param {Object} context the rendering context + */ +export async function renderTemplate(template, context) { + const templateClone = template.cloneNode(true); + await processNode(templateClone.content, context); + + processUnwraps(templateClone.content); + + return templateClone; +} + +/** + * transform the element, replacing it's children with the content from the template + * @param {Element} el the element + * @param {Element} template the template element + * @param {Object} context the rendering context + */ +export async function renderElementWithTemplate(el, template, context) { + const rendered = await renderTemplate(template, context); + el.replaceChildren(rendered.content); +} + +/** + * Transform an element using an HTML template + * + * @param {Element} block the block element + * @param {Object} context the rendering context + */ +export async function renderElement(el, context) { + const template = await resolveTemplate(context); + + await renderElementWithTemplate(el, template, context); +} + +/** + * Transform a block using an HTML template + * + * @param {Element} block the block element + * @param {Object} context the rendering context + */ +export async function renderBlock(block, context = {}) { + context.block = block; + context.blockName = block.dataset.blockName; + context.codeBasePath = context.codeBasePath || (window.hlx ? window.hlx.codeBasePath : ''); + + await renderElement(block, context); +} diff --git a/src/templates.js b/src/templates.js new file mode 100644 index 0000000..b852135 --- /dev/null +++ b/src/templates.js @@ -0,0 +1,34 @@ +/** + * resolve the template to render + * + * @param {object} context the rendering context + * @returns {Promise} the template element + */ +export default async function resolveTemplate(context) { + context.template = context.template || {}; + context.template.path = context.template.path || `${context.codeBasePath}/blocks/${context.blockName}/${context.blockName}.html`; + + const templateId = `faintly-template-${context.template.path}#${context.template.name || ''}`.toLowerCase().replace(/[^0-9a-z]/gi, '-'); + let template = document.getElementById(templateId); + if (!template) { + const resp = await fetch(context.template.path); + if (!resp.ok) throw new Error(`Failed to fetch template from ${context.template.path} for block ${context.blockName}.`); + + const markup = await resp.text(); + + const dp = new DOMParser(); + const templateDom = dp.parseFromString(markup, 'text/html'); + + templateDom.querySelectorAll('template').forEach((t) => { + const name = t.getAttribute('data-fly-name') || ''; + t.id = `faintly-template-${context.template.path}#${name}`.toLowerCase().replace(/[^0-9a-z]/gi, '-'); + + document.body.append(t); + }); + } + + template = document.getElementById(templateId); + if (!template) throw new Error(`Failed to find template with id ${templateId}.`); + + return template; +} diff --git a/test/directives/attributes/processAttributes.test.js b/test/directives/attributes/processAttributes.test.js index 56a5130..68cab34 100644 --- a/test/directives/attributes/processAttributes.test.js +++ b/test/directives/attributes/processAttributes.test.js @@ -2,9 +2,7 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../../src/faintly.js'; - -const { processAttributes } = exportForTesting; +import { processAttributes } from '../../../src/directives.js'; describe('processAttributes', () => { it('resolves expressions in non data-fly-* attributes', async () => { diff --git a/test/directives/content/processContent.test.js b/test/directives/content/processContent.test.js index 8b49ee3..558cea6 100644 --- a/test/directives/content/processContent.test.js +++ b/test/directives/content/processContent.test.js @@ -2,11 +2,9 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../../src/faintly.js'; +import { processContent } from '../../../src/directives.js'; import { compareDom, compareDomInline } from '../../test-utils.js'; -const { processContent } = exportForTesting; - describe('processContent', () => { it('returns false when children the directive is absent', async () => { const el = document.createElement('div'); diff --git a/test/directives/include/processInclude.test.js b/test/directives/include/processInclude.test.js index 1a8931e..98fea1c 100644 --- a/test/directives/include/processInclude.test.js +++ b/test/directives/include/processInclude.test.js @@ -1,11 +1,9 @@ /* eslint-env mocha */ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../../src/faintly.js'; +import { processInclude } from '../../../src/directives.js'; import { compareDom } from '../../test-utils.js'; -const { processInclude } = exportForTesting; - describe('processInclude', () => { it('returns false when the directive is absent', async () => { const el = document.createElement('div'); diff --git a/test/directives/repeat/processRepeat.test.js b/test/directives/repeat/processRepeat.test.js index 52c777b..cd74eab 100644 --- a/test/directives/repeat/processRepeat.test.js +++ b/test/directives/repeat/processRepeat.test.js @@ -2,9 +2,7 @@ /* eslint-env mocha */ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../../src/faintly.js'; - -const { processRepeat } = exportForTesting; +import { processRepeat } from '../../../src/directives.js'; describe('processRepeat', () => { it('returns false when repeat the directive is absent', async () => { diff --git a/test/directives/test/processTest.test.js b/test/directives/test/processTest.test.js index 8a79417..73b726a 100644 --- a/test/directives/test/processTest.test.js +++ b/test/directives/test/processTest.test.js @@ -2,9 +2,7 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../../src/faintly.js'; - -const { processTest } = exportForTesting; +import { processTest } from '../../../src/directives.js'; describe('processTest', () => { describe('data-fly-test', () => { diff --git a/test/directives/unwrap/processUnwrap.test.js b/test/directives/unwrap/processUnwrap.test.js index c151cc8..98a87ec 100644 --- a/test/directives/unwrap/processUnwrap.test.js +++ b/test/directives/unwrap/processUnwrap.test.js @@ -2,11 +2,9 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../../src/faintly.js'; +import { processUnwraps, resolveUnwrap } from '../../../src/directives.js'; import { compareDomInline } from '../../test-utils.js'; -const { processUnwraps, resolveUnwrap } = exportForTesting; - describe('resolveUnwrap', () => { it('unwraps all the unwrap elements', async () => { const div = document.createElement('div'); diff --git a/test/expressions/processTextExpressions.test.js b/test/expressions/processTextExpressions.test.js index b16c683..ce80019 100644 --- a/test/expressions/processTextExpressions.test.js +++ b/test/expressions/processTextExpressions.test.js @@ -2,9 +2,7 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../src/faintly.js'; - -const { processTextExpressions } = exportForTesting; +import { processTextExpressions } from '../../src/expressions.js'; describe('processTextExpressions', () => { it('updates text node content', async () => { diff --git a/test/expressions/resolveExpression.test.js b/test/expressions/resolveExpression.test.js index f78c490..133a374 100644 --- a/test/expressions/resolveExpression.test.js +++ b/test/expressions/resolveExpression.test.js @@ -2,9 +2,7 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../src/faintly.js'; - -const { resolveExpression } = exportForTesting; +import { resolveExpression } from '../../src/expressions.js'; describe('resolveExpression', () => { it('resolves object expressions from the rendering context', async () => { diff --git a/test/expressions/resolveExpressions.test.js b/test/expressions/resolveExpressions.test.js index 6aefcd5..2ff0816 100644 --- a/test/expressions/resolveExpressions.test.js +++ b/test/expressions/resolveExpressions.test.js @@ -3,9 +3,7 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../src/faintly.js'; - -const { resolveExpressions } = exportForTesting; +import { resolveExpressions } from '../../src/expressions.js'; describe('resolveExpressions', () => { it('resolves multiple expressions in a string', async () => { diff --git a/test/fixtures/blocks/accordion/accordion.js b/test/fixtures/blocks/accordion/accordion.js index fde7b0e..5af433c 100644 --- a/test/fixtures/blocks/accordion/accordion.js +++ b/test/fixtures/blocks/accordion/accordion.js @@ -1,4 +1,4 @@ -import { renderBlock } from '../../../../src/faintly.js'; +import { renderBlock } from '../../../../src/render.js'; export default async function decorate(block) { await renderBlock(block); diff --git a/test/fixtures/blocks/article-feed/article-feed.js b/test/fixtures/blocks/article-feed/article-feed.js index ba47b70..fc7abb9 100644 --- a/test/fixtures/blocks/article-feed/article-feed.js +++ b/test/fixtures/blocks/article-feed/article-feed.js @@ -1,4 +1,4 @@ -import { renderBlock } from '../../../../src/faintly.js'; +import { renderBlock } from '../../../../src/render.js'; async function fetchArticles() { const response = await fetch(`${window.hlx.codeBasePath}/blocks/article-feed/articles.json`); diff --git a/test/fixtures/blocks/cards/cards.js b/test/fixtures/blocks/cards/cards.js index 1f92ce2..180cb90 100644 --- a/test/fixtures/blocks/cards/cards.js +++ b/test/fixtures/blocks/cards/cards.js @@ -1,5 +1,5 @@ import { createOptimizedPicture } from '../../scripts/aem.js'; -import { renderBlock } from '../../../../src/faintly.js'; +import { renderBlock } from '../../../../src/render.js'; function transformCardColumn(context) { const col = context.card; diff --git a/test/templates/resolveTemplate.test.js b/test/templates/resolveTemplate.test.js index 8155e7a..941c36f 100644 --- a/test/templates/resolveTemplate.test.js +++ b/test/templates/resolveTemplate.test.js @@ -2,11 +2,9 @@ /* eslint-disable no-unused-expressions */ import { expect } from '@esm-bundle/chai'; -import { exportForTesting } from '../../src/faintly.js'; +import resolveTemplate from '../../src/templates.js'; import { compareDom } from '../test-utils.js'; -const { resolveTemplate } = exportForTesting; - describe('resolveTemplates', () => { it('loads the default template for a block', async () => { const template = await resolveTemplate({ From 408e2a88edd8639b6b51d5b28d65995212774bf3 Mon Sep 17 00:00:00 2001 From: Sean Steimer Date: Fri, 8 Aug 2025 11:43:02 -0700 Subject: [PATCH 2/2] remove minified version, serves no real purpose --- README.md | 2 +- dist/faintly.min.js | 2 -- dist/faintly.min.js.map | 7 ------- package.json | 4 +--- 4 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 dist/faintly.min.js delete mode 100644 dist/faintly.min.js.map diff --git a/README.md b/README.md index 0e1140f..393e55a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ I've experimented with other existing libraries (ejs templates, etc.) but wanted ## Getting Started -1. copy the /dist/faintly.js file (or minified version if you wish) to the scripts directory of your project +1. copy the /dist/faintly.js file to the scripts directory of your project 2. in the folder for your block, add a `blockName.html` file for the block template 3. in your block javascript, call the `renderBlock` function: diff --git a/dist/faintly.min.js b/dist/faintly.min.js deleted file mode 100644 index f6d2bc5..0000000 --- a/dist/faintly.min.js +++ /dev/null @@ -1,2 +0,0 @@ -async function h(t){t.template=t.template||{},t.template.path=t.template.path||`${t.codeBasePath}/blocks/${t.blockName}/${t.blockName}.html`;let e=`faintly-template-${t.template.path}#${t.template.name||""}`.toLowerCase().replace(/[^0-9a-z]/gi,"-"),a=document.getElementById(e);if(!a){let r=await fetch(t.template.path);if(!r.ok)throw new Error(`Failed to fetch template from ${t.template.path} for block ${t.blockName}.`);let s=await r.text();new DOMParser().parseFromString(s,"text/html").querySelectorAll("template").forEach(i=>{let p=i.getAttribute("data-fly-name")||"";i.id=`faintly-template-${t.template.path}#${p}`.toLowerCase().replace(/[^0-9a-z]/gi,"-"),document.body.append(i)})}if(a=document.getElementById(e),!a)throw new Error(`Failed to find template with id ${e}.`);return a}async function c(t,e){let a=e,r,s=t.split(".");for(let n=0;n"u");n+=1){let o=s[n];if(r=a,a=a[o],typeof a=="function"){let i=[{...e}];a=await a.apply(r,i)}}return a}async function d(t,e){let a=/(\\)?\${([a-z0-9\\.\s]+)}/dgi,r=[];if(t.replaceAll(a,(s,n,o)=>(n&&r.push(Promise.resolve(s.slice(1))),r.push(c(o.trim(),e)),s)),r.length>0){let s=await Promise.all(r);return{updated:!0,updatedText:t.replaceAll(a,()=>s.shift())}}return{updated:!1,updatedText:t}}async function b(t,e){let{updated:a,updatedText:r}=await d(t.textContent,e);a&&(t.textContent=r)}async function P(t,e){if(!t.hasAttribute("data-fly-attributes"))return;let a=t.getAttribute("data-fly-attributes"),r=await c(a,e);t.removeAttribute("data-fly-attributes"),r&&Object.entries(r).forEach(([s,n])=>{n===void 0?t.removeAttribute(s):t.setAttribute(s,n)})}async function A(t,e){P(t,e);let a=t.getAttributeNames().filter(r=>!r.startsWith("data-fly-")).map(async r=>{let{updated:s,updatedText:n}=await d(t.getAttribute(r),e);s&&t.setAttribute(r,n)});await Promise.all(a)}async function N(t,e){let a=t.getAttributeNames().find(p=>p.startsWith("data-fly-test")||p.startsWith("data-fly-not"));if(!a)return!0;let s=a.split(".")[1]||"",n=t.getAttribute(a),o=await c(n,e);t.removeAttribute(a);let i=a.startsWith("data-fly-not")?!o:!!o;return s&&(e[s.toLowerCase()]=i),i||t.remove(),i}async function E(t,e){if(!t.hasAttribute("data-fly-content"))return!1;let a=t.getAttribute("data-fly-content"),r=await c(a,e);if(t.removeAttribute("data-fly-content"),r!==void 0)if(r instanceof Node)t.replaceChildren(r);else if(Array.isArray(r)||r instanceof NodeList||r instanceof HTMLCollection)t.replaceChildren(...r);else{let s=document.createTextNode(r);t.replaceChildren(s)}else t.textContent="";return!0}async function T(t,e){let a=t.getAttributeNames().find(l=>l.startsWith("data-fly-repeat"));if(!a)return!1;let s=a.split(".")[1]||"item",n=t.getAttribute(a),o=await c(n,e);if(!o||Object.keys(o).length===0)return t.remove(),!0;t.removeAttribute(a);let i=await Promise.all(Object.entries(o).map(async([l,x],w)=>{let y=t.cloneNode(!0),u={...e};return u[s.toLowerCase()]=x,u[`${s.toLowerCase()}Index`]=w,u[`${s.toLowerCase()}Number`]=w+1,u[`${s.toLowerCase()}Key`]=l,await f(y,u),y})),p=t;return i.forEach(l=>{p.after(l),p=l}),t.remove(),!0}async function v(t,e){if(!t.hasAttribute("data-fly-include"))return!1;let a=t.getAttribute("data-fly-include");t.removeAttribute("data-fly-include");let{updatedText:r}=await d(a,e),s=e.template?e.template.path:"",n=r;if(n.startsWith("/")){let[i,p]=n.split("#");s=i,n=p}let o={...e,template:{name:n,path:s}};return await m(t,o),!0}async function g(t,e){if(!t.hasAttribute("data-fly-unwrap"))return;let a=t.getAttribute("data-fly-unwrap");a&&(await c(a,e)||t.removeAttribute("data-fly-unwrap"))}function C(t){t.querySelectorAll("[data-fly-unwrap]").forEach(e=>{e.before(...e.childNodes),e.remove()})}async function f(t,e){e.currentNode=t;let a=[Node.ELEMENT_NODE,Node.DOCUMENT_FRAGMENT_NODE].includes(t.nodeType);if(t.nodeType===Node.ELEMENT_NODE){if(!await N(t,e)||await T(t,e))return;await A(t,e),a=await E(t,e)||await v(t,e)||!0,await g(t,e)}else t.nodeType===Node.TEXT_NODE&&await b(t,e);let r=a?[...t.childNodes]:[];for(let s=0;s} the template element\n */\nexport default async function resolveTemplate(context) {\n context.template = context.template || {};\n context.template.path = context.template.path || `${context.codeBasePath}/blocks/${context.blockName}/${context.blockName}.html`;\n\n const templateId = `faintly-template-${context.template.path}#${context.template.name || ''}`.toLowerCase().replace(/[^0-9a-z]/gi, '-');\n let template = document.getElementById(templateId);\n if (!template) {\n const resp = await fetch(context.template.path);\n if (!resp.ok) throw new Error(`Failed to fetch template from ${context.template.path} for block ${context.blockName}.`);\n\n const markup = await resp.text();\n\n const dp = new DOMParser();\n const templateDom = dp.parseFromString(markup, 'text/html');\n\n templateDom.querySelectorAll('template').forEach((t) => {\n const name = t.getAttribute('data-fly-name') || '';\n t.id = `faintly-template-${context.template.path}#${name}`.toLowerCase().replace(/[^0-9a-z]/gi, '-');\n\n document.body.append(t);\n });\n }\n\n template = document.getElementById(templateId);\n if (!template) throw new Error(`Failed to find template with id ${templateId}.`);\n\n return template;\n}\n", "/**\n * resolves and returns data from the rendering context\n *\n * @param {string} expression the name of the data\n * @param {Object} context the rendering context\n * @returns {Promise} the data that was resolved\n */\nexport async function resolveExpression(expression, context) {\n let resolved = context;\n let previousResolvedValue;\n\n const parts = expression.split('.');\n for (let i = 0; i < parts.length; i += 1) {\n if (typeof resolved === 'undefined') break;\n\n const part = parts[i];\n previousResolvedValue = resolved;\n resolved = resolved[part];\n\n if (typeof resolved === 'function') {\n const functionParams = [{ ...context }];\n // eslint-disable-next-line no-await-in-loop\n resolved = await resolved.apply(previousResolvedValue, functionParams);\n }\n }\n\n return resolved;\n}\n\n/**\n * resolves expressions in a string\n *\n * @param {string} str the string that may contain expressions\n * @param {Object} context the rendering context\n */\nexport async function resolveExpressions(str, context) {\n const regexp = /(\\\\)?\\${([a-z0-9\\\\.\\s]+)}/dgi;\n\n const promises = [];\n str.replaceAll(regexp, (match, escapeChar, expression) => {\n if (escapeChar) {\n promises.push(Promise.resolve(match.slice(1)));\n }\n\n promises.push(resolveExpression(expression.trim(), context));\n\n return match;\n });\n\n if (promises.length > 0) {\n const promiseResults = await Promise.all(promises);\n const updatedText = str.replaceAll(regexp, () => {\n const result = promiseResults.shift();\n return result;\n });\n\n return { updated: true, updatedText };\n }\n\n return { updated: false, updatedText: str };\n}\n\n/**\n * process text expressions within a text node, updating the node's textContent\n *\n * @param {Node} node the text node\n * @param {Object} context the rendering context\n */\nexport async function processTextExpressions(node, context) {\n const { updated, updatedText } = await resolveExpressions(node.textContent, context);\n\n if (updated) node.textContent = updatedText;\n}\n", "import { resolveExpression, resolveExpressions } from './expressions.js';\nimport { processNode, renderElement } from './render.js';\n\nasync function processAttributesDirective(el, context) {\n if (!el.hasAttribute('data-fly-attributes')) return;\n\n const attrsExpression = el.getAttribute('data-fly-attributes');\n const attrsData = await resolveExpression(attrsExpression, context);\n\n el.removeAttribute('data-fly-attributes');\n if (attrsData) {\n Object.entries(attrsData).forEach(([k, v]) => {\n if (v === undefined) {\n el.removeAttribute(k);\n } else {\n el.setAttribute(k, v);\n }\n });\n }\n}\n\n/**\n * process the attributes directive, as well as any expressions in non `data-fly-*` attributes\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n */\nexport async function processAttributes(el, context) {\n processAttributesDirective(el, context);\n\n const attrPromises = el.getAttributeNames()\n .filter((attrName) => !attrName.startsWith('data-fly-'))\n .map(async (attrName) => {\n const { updated, updatedText } = await resolveExpressions(el.getAttribute(attrName), context);\n if (updated) el.setAttribute(attrName, updatedText);\n });\n await Promise.all(attrPromises);\n}\n\n/**\n * processes the test directive\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise} indicator if node should be rendered\n */\nexport async function processTest(el, context) {\n const testAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-test') || attrName.startsWith('data-fly-not'));\n if (!testAttrName) return true;\n\n const nameParts = testAttrName.split('.');\n const contextName = nameParts[1] || '';\n\n const testExpression = el.getAttribute(testAttrName);\n const testData = await resolveExpression(testExpression, context);\n\n el.removeAttribute(testAttrName);\n\n const testResult = testAttrName.startsWith('data-fly-not') ? !testData : !!testData;\n\n if (contextName) context[contextName.toLowerCase()] = testResult;\n\n if (!testResult) {\n el.remove();\n }\n\n return testResult;\n}\n\n/**\n * process the content directive\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise} if there was a content directive\n */\nexport async function processContent(el, context) {\n if (!el.hasAttribute('data-fly-content')) return false;\n\n const contentExpression = el.getAttribute('data-fly-content');\n const content = await resolveExpression(contentExpression, context);\n\n el.removeAttribute('data-fly-content');\n\n if (content !== undefined) {\n if (content instanceof Node) {\n el.replaceChildren(content);\n } else if (Array.isArray(content)\n || content instanceof NodeList || content instanceof HTMLCollection) {\n el.replaceChildren(...content);\n } else {\n const textNode = document.createTextNode(content);\n el.replaceChildren(textNode);\n }\n } else {\n el.textContent = '';\n }\n\n return true;\n}\n\n/**\n * processes the repeat directive\n *\n * @param {Element} el the element to potentially be repeated\n * @param {Object} context the rendering context\n * @returns {Promise} if the node was repeated\n * the net number of nodes added/removed as a result of the repeat directive\n */\nexport async function processRepeat(el, context) {\n const repeatAttrName = el.getAttributeNames().find((attrName) => attrName.startsWith('data-fly-repeat'));\n if (!repeatAttrName) return false;\n\n const nameParts = repeatAttrName.split('.');\n const contextName = nameParts[1] || 'item';\n\n const repeatExpression = el.getAttribute(repeatAttrName);\n const arr = await resolveExpression(repeatExpression, context);\n if (!arr || Object.keys(arr).length === 0) {\n el.remove();\n return true;\n }\n\n el.removeAttribute(repeatAttrName);\n const repeatedNodes = await Promise.all(Object.entries(arr).map(async ([key, item], i) => {\n const cloned = el.cloneNode(true);\n\n const repeatContext = { ...context };\n repeatContext[contextName.toLowerCase()] = item;\n repeatContext[`${contextName.toLowerCase()}Index`] = i;\n repeatContext[`${contextName.toLowerCase()}Number`] = i + 1;\n repeatContext[`${contextName.toLowerCase()}Key`] = key;\n\n // eslint-disable-next-line no-use-before-define\n await processNode(cloned, repeatContext);\n\n return cloned;\n }));\n\n let afterEL = el;\n repeatedNodes.forEach((node) => {\n afterEL.after(node);\n afterEL = node;\n });\n\n el.remove();\n\n return true;\n}\n\n/**\n * process the include directive\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise} if there was a include directive\n */\nexport async function processInclude(el, context) {\n if (!el.hasAttribute('data-fly-include')) return false;\n\n const includeValue = el.getAttribute('data-fly-include');\n el.removeAttribute('data-fly-include');\n const { updatedText } = await resolveExpressions(includeValue, context);\n\n let templatePath = context.template ? context.template.path : '';\n let templateName = updatedText;\n if (templateName.startsWith('/')) {\n const [path, name] = templateName.split('#');\n templatePath = path;\n templateName = name;\n }\n\n const includeContext = {\n ...context,\n template: {\n name: templateName,\n path: templatePath,\n },\n };\n\n await renderElement(el, includeContext);\n\n return true;\n}\n\n/**\n * process the unwrap directive, leavving the attribute only if it resolves to true\n *\n * @param {Element} el the element to process\n * @param {Object} context the rendering context\n * @returns {Promise}\n */\nexport async function resolveUnwrap(el, context) {\n if (!el.hasAttribute('data-fly-unwrap')) return;\n\n const unwrapExpression = el.getAttribute('data-fly-unwrap');\n if (unwrapExpression) {\n const unwrapVal = !!(await resolveExpression(unwrapExpression, context));\n\n if (!unwrapVal) {\n el.removeAttribute('data-fly-unwrap');\n }\n }\n}\n\nexport function processUnwraps(el) {\n el.querySelectorAll('[data-fly-unwrap]').forEach((unwrapEl) => {\n unwrapEl.before(...unwrapEl.childNodes);\n unwrapEl.remove();\n });\n}\n", "import resolveTemplate from './templates.js';\nimport { processTextExpressions } from './expressions.js';\nimport {\n processAttributes,\n processContent,\n processInclude,\n processRepeat,\n processTest,\n processUnwraps,\n resolveUnwrap,\n} from './directives.js';\n\n/**\n * recursively renders a dom node, processing all directives\n *\n * @param {Node} node the node to render\n * @param {Object} context the rendering context\n * @returns {Promise} a promise that resolves when the node has been rendered\n */\nexport async function processNode(node, context) {\n context.currentNode = node;\n let processChildren = [Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(node.nodeType);\n if (node.nodeType === Node.ELEMENT_NODE) {\n const shouldRender = await processTest(node, context);\n if (!shouldRender) return;\n\n const repeated = await processRepeat(node, context);\n if (repeated) return;\n\n await processAttributes(node, context);\n\n processChildren = (await processContent(node, context))\n || (await processInclude(node, context)) || true;\n\n await resolveUnwrap(node, context);\n } else if (node.nodeType === Node.TEXT_NODE) {\n await processTextExpressions(node, context);\n }\n\n const children = !processChildren ? [] : [...node.childNodes];\n\n for (let i = 0; i < children.length; i += 1) {\n const child = children[i];\n // eslint-disable-next-line no-await-in-loop\n await processNode(child, context);\n }\n}\n\n/**\n * Render a template\n * @param {Element} template the template to render\n * @param {Object} context the rendering context\n */\nexport async function renderTemplate(template, context) {\n const templateClone = template.cloneNode(true);\n await processNode(templateClone.content, context);\n\n processUnwraps(templateClone.content);\n\n return templateClone;\n}\n\n/**\n * transform the element, replacing it's children with the content from the template\n * @param {Element} el the element\n * @param {Element} template the template element\n * @param {Object} context the rendering context\n */\nexport async function renderElementWithTemplate(el, template, context) {\n const rendered = await renderTemplate(template, context);\n el.replaceChildren(rendered.content);\n}\n\n/**\n * Transform an element using an HTML template\n *\n * @param {Element} block the block element\n * @param {Object} context the rendering context\n */\nexport async function renderElement(el, context) {\n const template = await resolveTemplate(context);\n\n await renderElementWithTemplate(el, template, context);\n}\n\n/**\n * Transform a block using an HTML template\n *\n * @param {Element} block the block element\n * @param {Object} context the rendering context\n */\nexport async function renderBlock(block, context = {}) {\n context.block = block;\n context.blockName = block.dataset.blockName;\n context.codeBasePath = context.codeBasePath || (window.hlx ? window.hlx.codeBasePath : '');\n\n await renderElement(block, context);\n}\n"], - "mappings": "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", - "names": ["resolveTemplate", "context", "templateId", "template", "resp", "markup", "t", "name", "resolveExpression", "expression", "context", "resolved", "previousResolvedValue", "parts", "i", "part", "functionParams", "resolveExpressions", "str", "regexp", "promises", "match", "escapeChar", "promiseResults", "processTextExpressions", "node", "updated", "updatedText", "processAttributesDirective", "el", "context", "attrsExpression", "attrsData", "resolveExpression", "k", "v", "processAttributes", "attrPromises", "attrName", "updated", "updatedText", "resolveExpressions", "processTest", "testAttrName", "contextName", "testExpression", "testData", "testResult", "processContent", "contentExpression", "content", "textNode", "processRepeat", "repeatAttrName", "repeatExpression", "arr", "repeatedNodes", "key", "item", "i", "cloned", "repeatContext", "processNode", "afterEL", "node", "processInclude", "includeValue", "templatePath", "templateName", "path", "name", "includeContext", "renderElement", "resolveUnwrap", "unwrapExpression", "processUnwraps", "unwrapEl", "processNode", "node", "context", "processChildren", "processTest", "processRepeat", "processAttributes", "processContent", "processInclude", "resolveUnwrap", "processTextExpressions", "children", "i", "child", "renderTemplate", "template", "templateClone", "processUnwraps", "renderElementWithTemplate", "el", "rendered", "renderElement", "resolveTemplate", "renderBlock", "block"] -} diff --git a/package.json b/package.json index 9b790dd..46058c2 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,7 @@ "test:perf:watch": "web-test-runner --node-resolve --watch --group perf", "lint": "eslint .", "lint:fix": "eslint . --fix", - "build:dist": "esbuild src/index.js --bundle --format=esm --platform=browser --outfile=dist/faintly.js", - "build:dist:min": "esbuild src/index.js --bundle --format=esm --platform=browser --minify --sourcemap --legal-comments=none --outfile=dist/faintly.min.js", - "build": "npm run build:dist && npm run build:dist:min", + "build": "esbuild src/index.js --bundle --format=esm --platform=browser --outfile=dist/faintly.js", "clean": "rm -rf dist" }, "author": "Sean Steimer",