diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a1636..1e74cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # CHANGELOG +## 未发布 + +### 新增 +- **响应式多语言**: 新增 `Cookie` 和 `Lang` 内置状态,`SetTranslator` 支持同步字符串与异步 Promise,并通过原有 State 依赖机制刷新翻译 binding。 +- **翻译 binding**: 支持静态文本、静态属性和动态表达式结果中的 `{#...#}`,并恢复重新插入文本节点的订阅。 + +### 安全性 +- **异步隔离**: 翻译请求按语言和原文去重,丢弃旧语言返回值,并隔离 translator 的同步异常。 + ## v1.0.26 (2026-08-14) ### 修复 diff --git a/README.md b/README.md index ef3afa7..7840a7c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,13 @@ * `obj.__watch(key, cb)`: 建立 key -> callback 的直接依赖。 * `obj.__unwatch(key, cb)`: 解除依赖。 +### 内置状态 + +- `Hash`: 与 URL Hash 同步的响应式状态。 +- `LocalStorage`: 与 `localStorage` 同步的响应式状态。 +- `Cookie`: 与同路径 Cookie 同步的响应式状态,默认写入 `Path=/` 并保留 100 年。`Cookie.language` 默认使用浏览器首选语言。 +- `Lang`: 翻译结果的响应式缓存,以原文为键。修改 `Lang[text]` 会自动更新依赖该翻译的 binding。 + --- ## 2. 指令映射全集 (AI-Ready) @@ -69,7 +76,12 @@ AI 必须根据不同的元素类型执行以下逻辑: ## 5. 国际化 (I18n) 逻辑 - **语法结构**:`{# Key{param} || paramValue #}`。 -- **处理链路**:正则匹配 `{# ... #}` -> 提取 Key -> 注入 `||` 后的参数值 -> 调用全局 `_translator` 函数。 +- **处理链路**:正则匹配 `{# ... #}` -> 提取 Key -> 注入 `||` 后的参数值 -> 读取 `Lang[Key]` -> 缓存缺失时调用 translator。 +- **注册翻译器**:`SetTranslator((text, args) => string | Promise)`。 +- **同步结果**:立即显示 translator 返回的字符串。 +- **异步结果**:Promise 完成前显示原文;完成后写入 `Lang[text]`,由现有 State 依赖机制自动重新执行 binding。 +- **请求去重**:异步翻译按 `Cookie.language + 原文` 去重;语言切换后,旧语言请求不会写入当前 `Lang`。 +- **表达式结果**:`$text` 等指令先按原有规则求值,最终结果为包含 `{#` 的字符串时才翻译。 --- diff --git a/dist/state.js b/dist/state.js index 5792929..ca5b921 100644 --- a/dist/state.js +++ b/dist/state.js @@ -2,7 +2,7 @@ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.ApigoState = global.ApigoState || {})); })(this, function(exports2) { "use strict"; - var _a, _b; + var _a, _b, _c; const Util = { clone: (obj) => JSON.parse(JSON.stringify(obj)), base64: (str) => btoa(String.fromCharCode(...new TextEncoder().encode(str))), @@ -158,11 +158,35 @@ if (oldStr === newStr || oldStr === null && newStr === void 0) return; v === void 0 ? localStorage.removeItem(k) : localStorage.setItem(k, newStr); }); + const readCookies = () => Object.fromEntries(document.cookie.split(";").map((item) => { + const index = item.indexOf("="); + if (index < 0) return [item.trim(), ""]; + return [item.slice(0, index).trim(), item.slice(index + 1)]; + }).filter(([key]) => key)); + const Cookie = NewState({}, (k) => { + const value = readCookies()[k]; + if (value === void 0) return void 0; + try { + return Util.safeJson(decodeURIComponent(value)); + } catch (_) { + return void 0; + } + }, (k, v) => { + const value = v === void 0 ? "" : encodeURIComponent(JSON.stringify(v)); + const maxAge = v === void 0 ? 0 : 31536e5; + document.cookie = `${k}=${value}; Path=/; Max-Age=${maxAge}; SameSite=Lax`; + }); + const Lang = NewState({}); + if (Cookie.language === void 0 && typeof navigator !== "undefined") { + Cookie.language = ((_c = navigator.languages) == null ? void 0 : _c[0]) || navigator.language || "en-US"; + } const State = NewState({ exitBlocks: 0 }); globalThis.Hash = Hash; globalThis.LocalStorage = LocalStorage; + globalThis.Cookie = Cookie; + globalThis.Lang = Lang; globalThis.State = State; let _disableRunCodeError = false; const setDisableRunCodeError = (value) => { @@ -300,6 +324,34 @@ return text.replace(/\{(.+?)\}/g, (match, key) => args.hasOwnProperty(key) ? args[key] : match); }; const SetTranslator = (fn) => _translator = fn; + const _translationLoads = /* @__PURE__ */ new Map(); + const _isPromise = (value) => value && typeof value.then === "function"; + const _fillTranslationArgs = (text, args) => { + if (!text || typeof text !== "string") return text; + return text.replace(/\{(.+?)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(args, key) ? args[key] : match); + }; + const _getTranslation = (text, args) => { + var _a2; + const cached = Lang[text]; + if (Object.prototype.hasOwnProperty.call(Lang, text)) return _fillTranslationArgs(cached, args); + const language = (_a2 = globalThis.Cookie) == null ? void 0 : _a2.language; + const loadKey = `${language}\0${text}`; + if (_translationLoads.has(loadKey)) return text; + let result; + try { + result = _translator(text, args); + } catch (_) { + return text; + } + if (!_isPromise(result)) return typeof result === "string" ? result : text; + const loading = Promise.resolve(result).then((translated) => { + var _a3; + if (((_a3 = globalThis.Cookie) == null ? void 0 : _a3.language) === language && typeof translated === "string") Lang[text] = translated; + return translated; + }).catch(() => text).finally(() => _translationLoads.delete(loadKey)); + _translationLoads.set(loadKey, loading); + return text; + }; const _translate = (text) => { if (!text || typeof text !== "string" || !text.includes("{#")) return text; return text.replace(/\{#(.+?)#\}/g, (m, content) => { @@ -309,7 +361,7 @@ const matches = parts[0].match(/\{(.+?)\}/g); if (matches) matches.forEach((match, i) => args[match.substring(1, match.length - 1)] = parts[i + 1] || ""); } - return _translator(parts[0], args); + return _getTranslation(parts[0], args); }); }; if (typeof document !== "undefined") { @@ -341,6 +393,7 @@ } catch (e) { } } + if (typeof result === "string" && result.includes("{#")) result = _translate(result); _setActiveBinding(null); binding.lastResult = result; if (binding.prop) { @@ -528,7 +581,7 @@ if (node.tagName === "TEMPLATE") { ["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].forEach((n) => node.hasAttribute(n) && attrs.push(node.getAttributeNode(n))); } else { - attrs = Array.from(node.attributes).filter((a) => (a.name.startsWith("$") || a.name.startsWith("st-")) && !["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].includes(a.name) || a.name.includes(".")); + attrs = Array.from(node.attributes).filter((a) => (a.name.startsWith("$") || a.name.startsWith("st-")) && !["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].includes(a.name) || a.name.includes(".") || !a.name.startsWith("$") && !a.name.startsWith("st-") && !a.name.startsWith(".") && !a.name.startsWith("on") && a.name !== "bind" && a.value.includes("{#")); } if (node._thisObj && scanObj.thisObj && node._thisObj !== scanObj.thisObj) node._thisObj.parent = scanObj.thisObj; if (!node._thisObj) node._thisObj = scanObj.thisObj || null; @@ -567,10 +620,7 @@ tpl = node.textContent; node.textContent = ""; } - if (tpl) { - tpl = _translate(tpl); - _initBinding({ node, attr: realAttrName, tpl, exp }); - } + if (tpl) _initBinding({ node, attr: realAttrName, tpl, exp }); } }); if (node._hasOnLoad || node._componentInitialized) Promise.resolve().then(() => node.dispatchEvent(new Event("load", { bubbles: false }))); @@ -579,22 +629,18 @@ } const _scanTree = (node, scanObj = {}) => { if (node.nodeType === 3) { + if (node._bindings) { + node._states = /* @__PURE__ */ new Set(); + node._bindings.forEach((binding) => _updateBinding({ node, ...binding })); + return; + } if (node._stTranslated) return; - const translated = _translate(node.textContent); - if (translated !== node.textContent) node.textContent = translated; + if (node.textContent.includes("{#")) _initBinding({ node, attr: "text", tpl: node.textContent, exp: 0 }); node._stTranslated = true; return; } if (node.nodeType !== 1) return; - if (!node._stTranslated) { - Array.from(node.attributes).forEach((attr) => { - if (!attr.name.startsWith("$") && !attr.name.startsWith("st-") && !attr.name.startsWith(".")) { - const translated = _translate(attr.value); - if (translated !== attr.value) attr.value = translated; - } - }); - node._stTranslated = true; - } + if (!node._stTranslated) node._stTranslated = true; if (node.tagName !== "TEMPLATE" && (node.hasAttribute("$if") || node.hasAttribute("$each") || node.hasAttribute("st-if") || node.hasAttribute("st-each") || node.hasAttribute("$$if") || node.hasAttribute("$$each") || node.hasAttribute("st-st-if") || node.hasAttribute("st-st-each"))) { const template = document.createElement("TEMPLATE"); const attrs = Array.from(node.attributes).filter((attr) => ["$if", "$each", "st-if", "st-each", "$$if", "$$each", "st-st-if", "st-st-each"].includes(attr.name) || (node.hasAttribute("$each") || node.hasAttribute("st-each") || node.hasAttribute("$$each") || node.hasAttribute("st-st-each")) && ["as", "index"].includes(attr.name)); @@ -654,8 +700,8 @@ nodes.forEach((child) => _scanTree(child, { thisObj: node._thisObj ?? scanObj.thisObj, extendVars: { ...node._ref } })); }; const _unbindTree = (node) => { - if (node.nodeType !== 1) return; - if (node._hasOnUnload) node.dispatchEvent(new Event("unload", { bubbles: false })); + if (node.nodeType !== 1 && !node._states) return; + if (node.nodeType === 1 && node._hasOnUnload) node.dispatchEvent(new Event("unload", { bubbles: false })); if (node._states) node._states.forEach((mappings) => { for (const [key, bindingSet] of mappings) { for (const binding of bindingSet) { @@ -694,7 +740,9 @@ exports2.$ = $; exports2.$$ = $$; exports2.Component = Component; + exports2.Cookie = Cookie; exports2.Hash = Hash; + exports2.Lang = Lang; exports2.LocalStorage = LocalStorage; exports2.NewState = NewState; exports2.SetTranslator = SetTranslator; diff --git a/dist/state.min.js b/dist/state.min.js index 19c0d69..3697f48 100644 --- a/dist/state.min.js +++ b/dist/state.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ApigoState=e.ApigoState||{})}(this,function(e){"use strict";var t,s;const n={clone:e=>JSON.parse(JSON.stringify(e)),base64:e=>btoa(String.fromCharCode(...(new TextEncoder).encode(e))),unbase64:e=>(new TextDecoder).decode(Uint8Array.from(atob(e),e=>e.charCodeAt(0))),urlbase64:e=>n.base64(e).replace(/[+/=]/g,e=>({"+":"-","/":"","=":""}[e])),unurlbase64:e=>n.unbase64(e.replace(/[-_.]/g,e=>({"-":"+",_:"/",".":"="}[e])).padEnd(4*Math.ceil(e.length/4),"=")),safeJson:e=>{try{return JSON.parse(e)}catch{return null}},updateDefaults:(e,t)=>{for(const s in t)void 0===e[s]&&(e[s]=t[s])},copyFunction:(e,t,...s)=>{s.forEach(s=>e[s]=t[s].bind(t))},getFunctionBody:e=>{const t=e.toString();return t.slice(t.indexOf("{")+1,t.lastIndexOf("}")).trim()},makeDom:e=>{e.includes(">\n")&&(e=e.replace(/>\s+<").trim());const t=document.createElement("div");return t.innerHTML=e,t.children[0]},newAvg:()=>{let e=0,t=0,s=0;return{add:n=>(e+=n,t++,s=e/t),get:()=>s,clear:()=>{e=0,t=0,s=0}}},newTimeCount:()=>{let e=0,t=0,s=0;return{start:()=>e=(new Date).getTime(),end:()=>{const n=(new Date).getTime(),a=n-e;return e=n,t+=a,s++,a},avg:()=>t/s}}},a=(e,t)=>t?e.querySelector(t):document.querySelector(e),i=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e);globalThis.Util=n,globalThis.$=a,globalThis.$$=i;let r=null,o=null;const l=e=>r=e,c=e=>o=e,d=new Set,h=e=>d.add(e);function u(e={},t=null,s=null){const n={},a=new Map,i=new Map,l=(e,t)=>(i.has(e)||i.set(e,new Set),t?i.get(e).add(t):i.get(e).clear(),()=>i.get(e).delete(t)),c=(e,t)=>{i.has(e)&&i.set(e,new Set),i.get(e).delete(t)},h=t||(e=>n[e]),u=s||((e,t)=>n[e]=t);return Object.assign(n,e),new Proxy(n,{get:(e,t)=>"__watch"===t?l:"__unwatch"===t?c:"__isProxy"===t||(r&&(a.has(t)||a.set(t,new Set),a.get(t).add(r),r.node._states||(r.node._states=new Set),r.node._states.add(a)),h(t)),set(e,t,s){if(h(t)!==s&&u(t,s),i.has(t)&&i.get(t).forEach(n=>{const a=n(s);void 0!==a&&(s=a,e[t]=s)}),i.has(null)&&i.get(null).forEach(e=>e(s)),a.has(t)){const e=a.get(t);for(const t of e)t.node.isConnected?o!==t.node&&d.forEach(e=>e(t)):e.delete(t)}return!0}})}globalThis.NewState=u;let f=new URLSearchParams("undefined"!=typeof globalThis&&(null==(s=null==(t=globalThis.location)?void 0:t.hash)?void 0:s.substring(1))||"");const b=u({},e=>n.safeJson(f.get(e)),(e,t)=>{const s=f.get(e),n=void 0===t?void 0:JSON.stringify(t);s===n||null===s&&void 0===n||(void 0===t?f.delete(e):f.set(e,n),globalThis.location.hash="#"+f.toString())});"undefined"!=typeof globalThis&&globalThis.addEventListener("hashchange",()=>{var e;const t=new URLSearchParams((null==(e=globalThis.location.hash)?void 0:e.substring(1))||""),s=new Set([...f.keys(),...t.keys()]);f=t,s.forEach(e=>b[e]=b[e])});const m=u({},e=>n.safeJson(localStorage.getItem(e)),(e,t)=>{const s=localStorage.getItem(e),n=void 0===t?void 0:JSON.stringify(t);s===n||null===s&&void 0===n||(void 0===t?localStorage.removeItem(e):localStorage.setItem(e,n))}),p=u({exitBlocks:0});globalThis.Hash=b,globalThis.LocalStorage=m,globalThis.State=p;let g=!1;const _=e=>{g=e},A=new Map;function y(e,t,s,n){const a={...n||{},...t||{}},i=Object.keys(a),r=Object.values(a),o=e+i.join(",");try{let t=A.get(o);return t||(t=new Function("Hash","LocalStorage","State",...i,e),A.set(o,t)),t.apply(s,[globalThis.Hash,globalThis.LocalStorage,globalThis.State,...r])}catch(a){return g||console.error(a,n,[e,n,t,s]),null}}function v(e,t,s,n){return e.includes("${")?y("return `"+e+"`",t,s,n):y("return "+e,t,s,n)}const E=new Map,$=[],T={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,s=null,...n)=>{E.set(e.toUpperCase(),t),"loading"!==document.readyState?T._addTemplate(e,s,n):$.push([e,s,n])},exists:e=>E.has(e.toUpperCase()),getSetupFunction:e=>E.get(e.toUpperCase()),_addTemplate:(e,t,s)=>{if(t){const s=document.createElement("TEMPLATE");s.setAttribute("component",e.toUpperCase()),s.content.appendChild(t),document.body.appendChild(s)}s&&s.forEach(e=>document.body.appendChild(e))},_initPending:()=>{$.forEach(([e,t,s])=>T._addTemplate(e,t,s)),$.length=0}};function N(e,t,s,n={}){e.attributes&&Array.from(e.attributes).forEach(e=>{if("class"!==e.name)if("style"===e.name)t.hasAttribute("style")?t.setAttribute("style",`${e.value}; ${t.getAttribute("style")}`):t.setAttribute("style",e.value);else if(t.hasAttribute(e.name)){const s=["$class","st-class"].includes(e.name),n=["$style","st-style"].includes(e.name);if(s||n){const n=t.getAttribute(e.name),a=e.value,i=s?" ":"; ",r=n.includes("${")?n:`\${${n}}`,o=a.includes("${")?a:`\${${a}}`;t.setAttribute(e.name,`${o}${i}${r}`)}}else t.setAttribute(e.name,e.value)});const a=[...t.classList];t.className="",t.classList.add(...e.classList),t.classList.add(...a);const i="TEMPLATE"===t.tagName?t.content:t,r="TEMPLATE"===e.tagName?e.content.childNodes:e.childNodes;Array.from(r).forEach(e=>i.appendChild(e)),e.tagName&&T.exists(e.tagName)&&S(e.tagName,t,s,n)}const O=e=>{const t=[...e.querySelectorAll("[slot-id]")];return e.querySelectorAll("template").forEach(e=>{t.push(...O(e.content))}),t};function S(e,t,s,n={}){if(n[e])return;n[e]=!0;const a=s.thisObj;s.thisObj&&Array.from(t.attributes).forEach(e=>{(e.name.startsWith("$")||e.name.startsWith("st-"))&&e.value.includes("this.")&&(e.value=e.value.replace(/\bthis\./g,"this.parent."))});const i=T.getSetupFunction(e),r={};Array.from(t.childNodes).forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&e.hasAttribute("slot")&&(r[e.getAttribute("slot")]=e,e.removeAttribute("slot"))}),t.innerHTML="",t.state=u(t.state||{});const o=T.getTemplate(e);if(o){const e=o.content.cloneNode(!0);if(e.childNodes.length){const a=e.children[0];a&&N(a,t,s,n),O(t).forEach(e=>{const t=e.getAttribute("slot-id");r[t]&&(e.removeAttribute("slot-id"),e.innerHTML="",N(r[t],e,s,n))})}}i&&i(t),t._thisObj=t,a&&a!==t&&(t._thisObj.parent=a)}let j=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,s)=>t.hasOwnProperty(s)?t[s]:e):e;const x=e=>j=e,C=e=>e&&"string"==typeof e&&e.includes("{#")?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const s=t.split("||").map(e=>e.trim()),n={};if(s.length>1){const e=s[0].match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>n[e.substring(1,e.length-1)]=s[t+1]||"")}return j(s[0],n)}):e;if("undefined"!=typeof document)try{document.createElement("div").setAttribute("$t","1")}catch(e){const t=Element.prototype.setAttribute;Element.prototype.setAttribute=function(e,s){return e.startsWith("$")?t.call(this,"st-"+e.substring(1),s):t.call(this,e,s)}}function M(e){e._renderedNodes&&e._renderedNodes.forEach(e=>e.forEach(e=>{e.remove(),e._renderedNodes&&M(e)}))}function w(e){const t=e.node;if(!t.isConnected&&"TEMPLATE"!==t.tagName)return;l(e);let s=e.exp?e.tpl?v(e.tpl,{thisNode:t},t._thisObj||t,t._ref||null):null:e.tpl;if(2===e.exp&&"string"==typeof s)try{s=v(s,{thisNode:t},t._thisObj||t,t._ref||null)}catch(e){}if(l(null),e.lastResult=s,e.prop){const n=e.prop;let a=t;for(let e=0;e{const s=e.cloneNode(!0);return t.parentNode.insertBefore(s,t),s._ref={...t._ref},s._thisObj=t._thisObj,s});t._renderedNodes=[e]}}else M(t),t._renderedNodes=[];else if("each"===n)if(s&&"object"==typeof s){const e=t.getAttribute("as")||"item",n=t.getAttribute("index")||"index",a=t.getAttribute("key");let i,r;if(s instanceof Map)i=Array.from(s.keys()),r=e=>s.get(e);else if("function"==typeof s[Symbol.iterator]){const e=Array.isArray(s)?s:Array.from(s);i=new Array(e.length);for(let t=0;te[t]}else i=Object.keys(s),r=e=>s[e];t._keyedNodes||(t._keyedNodes=new Map);const o=new Map,l=[];i.forEach((s,i)=>{const c=r(s),d=a?c&&"object"==typeof c?c[a]:c:s,h=null==d||o.has(d)?`st_key_${i}`:d;let u=t._keyedNodes.get(h);u?(t._keyedNodes.delete(h),u.forEach(a=>{t.parentNode.insertBefore(a,t),a._ref[n]=s,a._ref[e]=c,k(a)})):(u=[],t._children.forEach(a=>{const i=a.cloneNode(!0);i._ref={...t._ref,[n]:s,[e]:c},i._thisObj=t._thisObj,t.parentNode.insertBefore(i,t),u.push(i)})),o.set(h,u),l.push(u)}),t._keyedNodes.forEach(e=>e.forEach(e=>{M(e),e.remove()})),t._keyedNodes=o,t._renderedNodes=l}else M(t),t._renderedNodes=[];else if("bind"===n){if(["INPUT","SELECT","TEXTAREA"].includes(t.tagName)&&!t.hasAttribute("autocomplete")&&t.setAttribute("autocomplete","off"),"checkbox"===t.type){"on"===t.value||s||(y(`${e.tpl} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),s=[]),t._checkboxMultiMode=s instanceof Array;const n=s instanceof Array?s.includes(t.value):!!s;t.checked!==n&&(t.checked=n)}else"radio"===t.type?t.checked!==(t.value===String(s??""))&&(t.checked=t.value===String(s??"")):"value"in t&&"file"!==t.type?setTimeout(()=>{t.value!==String(s??"")&&(t.value=s)}):t.isContentEditable&&t.innerHTML!==String(s??"")&&(t.innerHTML=s);t.dispatchEvent(new CustomEvent("bind",{bubbles:!1,detail:s}))}else if(["checked","disabled","readonly"].includes(n)&&(s=!!s),"boolean"==typeof s)s?t.setAttribute(n,""):t.removeAttribute(n);else if(void 0!==s)if("string"!=typeof s&&(s=JSON.stringify(s)),"text"===n)t.textContent=s??"";else if("html"===n)t.innerHTML=s??"";else if("IMG"===t.tagName&&"src"===n&&s.includes(".svg"))t.setAttribute("_src",s??"");else if("class"===n){void 0===t._staticClasses&&(t._staticClasses=t.getAttribute("class")||"");const e=t._staticClasses,n=s?e?`${s} ${e}`:s:e;t.setAttribute("class",n.trim().replace(/\s+/g," "))}else if("style"===n){void 0===t._staticStyles&&(t._staticStyles=t.getAttribute("style")||"");const e=t._staticStyles,n=s?e?`${s}; ${e}`:s:e;t.setAttribute("style",n.trim().replace(/;;+/g,";").replace(/^;+\s*|;\s*$/g,""))}else t.setAttribute(n,s??"")}}function L(e){e.node._bindings||(e.node._bindings=[]),e.node._bindings.push({attr:e.attr,prop:e.prop,tpl:e.tpl,exp:e.exp}),w(e)}h(e=>w(e));const k=(e,t={})=>{if(3===e.nodeType){if(e._stTranslated)return;const t=C(e.textContent);return t!==e.textContent&&(e.textContent=t),void(e._stTranslated=!0)}if(1!==e.nodeType)return;if(e._stTranslated||(Array.from(e.attributes).forEach(e=>{if(!e.name.startsWith("$")&&!e.name.startsWith("st-")&&!e.name.startsWith(".")){const t=C(e.value);t!==e.value&&(e.value=t)}}),e._stTranslated=!0),"TEMPLATE"!==e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("$each")||e.hasAttribute("st-if")||e.hasAttribute("st-each")||e.hasAttribute("$$if")||e.hasAttribute("$$each")||e.hasAttribute("st-st-if")||e.hasAttribute("st-st-each"))){const s=document.createElement("TEMPLATE");return Array.from(e.attributes).filter(t=>["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].includes(t.name)||(e.hasAttribute("$each")||e.hasAttribute("st-each")||e.hasAttribute("$$each")||e.hasAttribute("st-st-each"))&&["as","index"].includes(t.name)).forEach(t=>{s.setAttribute(t.name,t.value),e.removeAttribute(t.name)}),e.parentNode.insertBefore(s,e),s.content.appendChild(e),s._ref=e._ref,void k(s,t)}if("TEMPLATE"===e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("st-if")||e.hasAttribute("$$if")||e.hasAttribute("st-st-if"))&&(e.hasAttribute("$each")||e.hasAttribute("st-each")||e.hasAttribute("$$each")||e.hasAttribute("st-st-each"))){const t=document.createElement("TEMPLATE"),s=Array.from(e.attributes).filter(e=>["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].includes(e.name)),n=s[s.length-1];t.setAttribute(n.name,n.value),e.removeAttribute(n.name),["$each","st-each","$$each","st-st-each"].includes(n.name)&&Array.from(e.attributes).filter(e=>["as","index"].includes(e.name)).forEach(s=>{t.setAttribute(s.name,s.value),e.removeAttribute(s.name)}),Array.from(e.content.childNodes).forEach(e=>t.content.appendChild(e)),e.content.appendChild(t),t._ref=e._ref}if("IMG"===e.tagName&&(e.hasAttribute("src")||e.hasAttribute("_src")||e.hasAttribute("$src"))){const t=e;Promise.resolve().then(()=>{const e=t.getAttribute("_src")||t.getAttribute("src");e&&fetch(e,{cache:"force-cache"}).then(e=>e.text()).then(e=>{const s=(new DOMParser).parseFromString(e,"image/svg+xml").querySelector("svg");s&&(Array.from(t.attributes).forEach(e=>s.setAttribute(e.name,e.value)),t.replaceWith(s))})})}if(void 0!==e._thisObj)t.thisObj=e._thisObj||null;else{let s=e;for(;s&&void 0===s._thisObj;)s=s.parentNode;t.thisObj=s?s._thisObj:null}if(void 0===e._ref){let t=e;for(;t&&void 0===t._ref;)t=t.parentNode;e._ref=t?{...t._ref}:{}}t.extendVars&&Object.assign(e._ref,t.extendVars),function(e,t){if(e._bindings)return e._states=new Set,e._bindings.forEach(t=>w({node:e,...t})),void(e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})));T.exists(e.tagName)&&!e._componentInitialized&&(Array.from(e.attributes).forEach(s=>{var n;if(s.name.startsWith("$.")){const a=s.name.slice(2);let i=C(s.value);t.thisObj&&i.includes("this.")&&(i=i.replace(/\bthis\./g,"this.parent."));const r=v(i,{thisNode:e},{parent:t.thisObj||e},e._ref||{});let o=e;const l=a.split(".");for(let e=0;ee.removeAttribute("slot-id")),e._componentInitialized=!0,e._thisObj||(e._thisObj=e)),"TEMPLATE"===e.tagName&&(e._children=[...e.content.childNodes],e._renderedNodes||(e._renderedNodes=[]));let s=[];"TEMPLATE"===e.tagName?["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].forEach(t=>e.hasAttribute(t)&&s.push(e.getAttributeNode(t))):s=Array.from(e.attributes).filter(e=>(e.name.startsWith("$")||e.name.startsWith("st-"))&&!["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].includes(e.name)||e.name.includes(".")),e._thisObj&&t.thisObj&&e._thisObj!==t.thisObj&&(e._thisObj.parent=t.thisObj),e._thisObj||(e._thisObj=t.thisObj||null),e._ref||(e._ref=t.extendVars||{}),e._states=new Set,s.forEach(s=>{let n=0;s.name.startsWith("$$")||s.name.startsWith("st-st-")?n=2:(s.name.startsWith("$")||s.name.startsWith("st-"))&&(n=1);const a=2===n?s.name.startsWith("$$")?s.name.slice(2):s.name.slice(6):1===n?s.name.startsWith("$")?s.name.slice(1):s.name.slice(3):s.name;let i=s.value;if(e.removeAttribute(s.name),a.startsWith("."))L({node:e,prop:a.split("."),tpl:i,exp:n});else if(a.startsWith("on")){const s=a.slice(2);"update"===s&&(e._hasOnUpdate=!0),"load"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnLoad=!0),"unload"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnUnload=!0),e.addEventListener(s,s=>{const n=s.detail&&"object"==typeof s.detail&&!Array.isArray(s.detail)?s.detail:{};y(i,{event:s,thisNode:e,...n},t.thisObj||e,e._ref||{})})}else{if("bind"===a){const s=["INPUT","TEXTAREA"].includes(e.tagName)&&["textarea","text","password","email","number","search","url","tel"].includes(e.type||"text")||e.isContentEditable;e.addEventListener(s?"input":"change",s=>{let n=e.isContentEditable?s.target.innerHTML:"checkbox"===e.type?s.target.checked:"file"===e.type?s.target.files:s.target.value??s.detail;c(e),_(!0),"checkbox"===e.type&&e._checkboxMultiMode?y(`!!checked ? (!${i}.includes(val) && ${i}.push(val)) : (index = ${i}.indexOf(val), index > -1 && ${i}.splice(index, 1))`,{val:e.value,checked:n,thisNode:e},t.thisObj||e,e._ref||{}):y(`${i} = val`,{val:n,thisNode:e},t.thisObj||e,e._ref||{}),_(!1),c(null)})}else"text"!==a||i||(i=e.textContent,e.textContent="");i&&(i=C(i),L({node:e,attr:a,tpl:i,exp:n}))}}),(e._hasOnLoad||e._componentInitialized)&&Promise.resolve().then(()=>e.dispatchEvent(new Event("load",{bubbles:!1}))),e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})),e._thisObj&&(t.thisObj=e._thisObj)}(e,{...t});[...e.childNodes||[]].forEach(s=>k(s,{thisObj:e._thisObj??t.thisObj,extendVars:{...e._ref}}))},P=e=>{1===e.nodeType&&(e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._states&&e._states.forEach(t=>{for(const[s,n]of t)for(const t of n)t.node===e&&n.delete(t)}),e.childNodes&&e.childNodes.forEach(e=>P(e)))};if(globalThis.Component=T,globalThis.SetTranslator=x,globalThis.__unsafeRefreshState=k,"undefined"!=typeof document){const e=()=>{globalThis.Component&&globalThis.Component._initPending&&globalThis.Component._initPending();const e=document.documentElement;e.hasAttribute("$data-bs-theme")||e.hasAttribute("data-bs-theme")||e.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&k(e)}),e.removedNodes.forEach(e=>P(e))})}).observe(document.documentElement,{childList:!0,subtree:!0}),k(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}const U=k;e.$=a,e.$$=i,e.Component=T,e.Hash=b,e.LocalStorage=m,e.NewState=u,e.SetTranslator=x,e.State=p,e.Util=n,e.__unsafeRefreshState=U,e._returnCode=v,e._runCode=y,e.onNotifyUpdate=h,e.setActiveBinding=l,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ApigoState=e.ApigoState||{})}(this,function(e){"use strict";var t,s,n;const a={clone:e=>JSON.parse(JSON.stringify(e)),base64:e=>btoa(String.fromCharCode(...(new TextEncoder).encode(e))),unbase64:e=>(new TextDecoder).decode(Uint8Array.from(atob(e),e=>e.charCodeAt(0))),urlbase64:e=>a.base64(e).replace(/[+/=]/g,e=>({"+":"-","/":"","=":""}[e])),unurlbase64:e=>a.unbase64(e.replace(/[-_.]/g,e=>({"-":"+",_:"/",".":"="}[e])).padEnd(4*Math.ceil(e.length/4),"=")),safeJson:e=>{try{return JSON.parse(e)}catch{return null}},updateDefaults:(e,t)=>{for(const s in t)void 0===e[s]&&(e[s]=t[s])},copyFunction:(e,t,...s)=>{s.forEach(s=>e[s]=t[s].bind(t))},getFunctionBody:e=>{const t=e.toString();return t.slice(t.indexOf("{")+1,t.lastIndexOf("}")).trim()},makeDom:e=>{e.includes(">\n")&&(e=e.replace(/>\s+<").trim());const t=document.createElement("div");return t.innerHTML=e,t.children[0]},newAvg:()=>{let e=0,t=0,s=0;return{add:n=>(e+=n,t++,s=e/t),get:()=>s,clear:()=>{e=0,t=0,s=0}}},newTimeCount:()=>{let e=0,t=0,s=0;return{start:()=>e=(new Date).getTime(),end:()=>{const n=(new Date).getTime(),a=n-e;return e=n,t+=a,s++,a},avg:()=>t/s}}},i=(e,t)=>t?e.querySelector(t):document.querySelector(e),o=(e,t)=>t?e.querySelectorAll(t):document.querySelectorAll(e);globalThis.Util=a,globalThis.$=i,globalThis.$$=o;let r=null,l=null;const c=e=>r=e,d=e=>l=e,h=new Set,u=e=>h.add(e);function f(e={},t=null,s=null){const n={},a=new Map,i=new Map,o=(e,t)=>(i.has(e)||i.set(e,new Set),t?i.get(e).add(t):i.get(e).clear(),()=>i.get(e).delete(t)),c=(e,t)=>{i.has(e)&&i.set(e,new Set),i.get(e).delete(t)},d=t||(e=>n[e]),u=s||((e,t)=>n[e]=t);return Object.assign(n,e),new Proxy(n,{get:(e,t)=>"__watch"===t?o:"__unwatch"===t?c:"__isProxy"===t||(r&&(a.has(t)||a.set(t,new Set),a.get(t).add(r),r.node._states||(r.node._states=new Set),r.node._states.add(a)),d(t)),set(e,t,s){if(d(t)!==s&&u(t,s),i.has(t)&&i.get(t).forEach(n=>{const a=n(s);void 0!==a&&(s=a,e[t]=s)}),i.has(null)&&i.get(null).forEach(e=>e(s)),a.has(t)){const e=a.get(t);for(const t of e)t.node.isConnected?l!==t.node&&h.forEach(e=>e(t)):e.delete(t)}return!0}})}globalThis.NewState=f;let b=new URLSearchParams("undefined"!=typeof globalThis&&(null==(s=null==(t=globalThis.location)?void 0:t.hash)?void 0:s.substring(1))||"");const p=f({},e=>a.safeJson(b.get(e)),(e,t)=>{const s=b.get(e),n=void 0===t?void 0:JSON.stringify(t);s===n||null===s&&void 0===n||(void 0===t?b.delete(e):b.set(e,n),globalThis.location.hash="#"+b.toString())});"undefined"!=typeof globalThis&&globalThis.addEventListener("hashchange",()=>{var e;const t=new URLSearchParams((null==(e=globalThis.location.hash)?void 0:e.substring(1))||""),s=new Set([...b.keys(),...t.keys()]);b=t,s.forEach(e=>p[e]=p[e])});const g=f({},e=>a.safeJson(localStorage.getItem(e)),(e,t)=>{const s=localStorage.getItem(e),n=void 0===t?void 0:JSON.stringify(t);s===n||null===s&&void 0===n||(void 0===t?localStorage.removeItem(e):localStorage.setItem(e,n))}),m=f({},e=>{const t=Object.fromEntries(document.cookie.split(";").map(e=>{const t=e.indexOf("=");return t<0?[e.trim(),""]:[e.slice(0,t).trim(),e.slice(t+1)]}).filter(([e])=>e))[e];if(void 0!==t)try{return a.safeJson(decodeURIComponent(t))}catch(e){return}},(e,t)=>{const s=void 0===t?"":encodeURIComponent(JSON.stringify(t)),n=void 0===t?0:31536e5;document.cookie=`${e}=${s}; Path=/; Max-Age=${n}; SameSite=Lax`}),_=f({});void 0===m.language&&"undefined"!=typeof navigator&&(m.language=(null==(n=navigator.languages)?void 0:n[0])||navigator.language||"en-US");const y=f({exitBlocks:0});globalThis.Hash=p,globalThis.LocalStorage=g,globalThis.Cookie=m,globalThis.Lang=_,globalThis.State=y;let A=!1;const v=e=>{A=e},E=new Map;function $(e,t,s,n){const a={...n||{},...t||{}},i=Object.keys(a),o=Object.values(a),r=e+i.join(",");try{let t=E.get(r);return t||(t=new Function("Hash","LocalStorage","State",...i,e),E.set(r,t)),t.apply(s,[globalThis.Hash,globalThis.LocalStorage,globalThis.State,...o])}catch(a){return A||console.error(a,n,[e,n,t,s]),null}}function T(e,t,s,n){return e.includes("${")?$("return `"+e+"`",t,s,n):$("return "+e,t,s,n)}const N=new Map,O=[],S={getTemplate:e=>document.querySelector(`template[component="${e.toUpperCase()}"]`),register:(e,t,s=null,...n)=>{N.set(e.toUpperCase(),t),"loading"!==document.readyState?S._addTemplate(e,s,n):O.push([e,s,n])},exists:e=>N.has(e.toUpperCase()),getSetupFunction:e=>N.get(e.toUpperCase()),_addTemplate:(e,t,s)=>{if(t){const s=document.createElement("TEMPLATE");s.setAttribute("component",e.toUpperCase()),s.content.appendChild(t),document.body.appendChild(s)}s&&s.forEach(e=>document.body.appendChild(e))},_initPending:()=>{O.forEach(([e,t,s])=>S._addTemplate(e,t,s)),O.length=0}};function j(e,t,s,n={}){e.attributes&&Array.from(e.attributes).forEach(e=>{if("class"!==e.name)if("style"===e.name)t.hasAttribute("style")?t.setAttribute("style",`${e.value}; ${t.getAttribute("style")}`):t.setAttribute("style",e.value);else if(t.hasAttribute(e.name)){const s=["$class","st-class"].includes(e.name),n=["$style","st-style"].includes(e.name);if(s||n){const n=t.getAttribute(e.name),a=e.value,i=s?" ":"; ",o=n.includes("${")?n:`\${${n}}`,r=a.includes("${")?a:`\${${a}}`;t.setAttribute(e.name,`${r}${i}${o}`)}}else t.setAttribute(e.name,e.value)});const a=[...t.classList];t.className="",t.classList.add(...e.classList),t.classList.add(...a);const i="TEMPLATE"===t.tagName?t.content:t,o="TEMPLATE"===e.tagName?e.content.childNodes:e.childNodes;Array.from(o).forEach(e=>i.appendChild(e)),e.tagName&&S.exists(e.tagName)&&C(e.tagName,t,s,n)}const x=e=>{const t=[...e.querySelectorAll("[slot-id]")];return e.querySelectorAll("template").forEach(e=>{t.push(...x(e.content))}),t};function C(e,t,s,n={}){if(n[e])return;n[e]=!0;const a=s.thisObj;s.thisObj&&Array.from(t.attributes).forEach(e=>{(e.name.startsWith("$")||e.name.startsWith("st-"))&&e.value.includes("this.")&&(e.value=e.value.replace(/\bthis\./g,"this.parent."))});const i=S.getSetupFunction(e),o={};Array.from(t.childNodes).forEach(e=>{e.nodeType===Node.ELEMENT_NODE&&e.hasAttribute("slot")&&(o[e.getAttribute("slot")]=e,e.removeAttribute("slot"))}),t.innerHTML="",t.state=f(t.state||{});const r=S.getTemplate(e);if(r){const e=r.content.cloneNode(!0);if(e.childNodes.length){const a=e.children[0];a&&j(a,t,s,n),x(t).forEach(e=>{const t=e.getAttribute("slot-id");o[t]&&(e.removeAttribute("slot-id"),e.innerHTML="",j(o[t],e,s,n))})}}i&&i(t),t._thisObj=t,a&&a!==t&&(t._thisObj.parent=a)}let w=(e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,s)=>t.hasOwnProperty(s)?t[s]:e):e;const M=e=>w=e,L=new Map,k=(e,t)=>{var s;const n=_[e];if(Object.prototype.hasOwnProperty.call(_,e))return((e,t)=>e&&"string"==typeof e?e.replace(/\{(.+?)\}/g,(e,s)=>Object.prototype.hasOwnProperty.call(t,s)?t[s]:e):e)(n,t);const a=null==(s=globalThis.Cookie)?void 0:s.language,i=`${a}\0${e}`;if(L.has(i))return e;let o;try{o=w(e,t)}catch(t){return e}if(!(r=o)||"function"!=typeof r.then)return"string"==typeof o?o:e;var r;const l=Promise.resolve(o).then(t=>{var s;return(null==(s=globalThis.Cookie)?void 0:s.language)===a&&"string"==typeof t&&(_[e]=t),t}).catch(()=>e).finally(()=>L.delete(i));return L.set(i,l),e},P=e=>e&&"string"==typeof e&&e.includes("{#")?e.replace(/\{#(.+?)#\}/g,(e,t)=>{const s=t.split("||").map(e=>e.trim()),n={};if(s.length>1){const e=s[0].match(/\{(.+?)\}/g);e&&e.forEach((e,t)=>n[e.substring(1,e.length-1)]=s[t+1]||"")}return k(s[0],n)}):e;if("undefined"!=typeof document)try{document.createElement("div").setAttribute("$t","1")}catch(e){const t=Element.prototype.setAttribute;Element.prototype.setAttribute=function(e,s){return e.startsWith("$")?t.call(this,"st-"+e.substring(1),s):t.call(this,e,s)}}function U(e){e._renderedNodes&&e._renderedNodes.forEach(e=>e.forEach(e=>{e.remove(),e._renderedNodes&&U(e)}))}function W(e){const t=e.node;if(!t.isConnected&&"TEMPLATE"!==t.tagName)return;c(e);let s=e.exp?e.tpl?T(e.tpl,{thisNode:t},t._thisObj||t,t._ref||null):null:e.tpl;if(2===e.exp&&"string"==typeof s)try{s=T(s,{thisNode:t},t._thisObj||t,t._ref||null)}catch(e){}if("string"==typeof s&&s.includes("{#")&&(s=P(s)),c(null),e.lastResult=s,e.prop){const n=e.prop;let a=t;for(let e=0;e{const s=e.cloneNode(!0);return t.parentNode.insertBefore(s,t),s._ref={...t._ref},s._thisObj=t._thisObj,s});t._renderedNodes=[e]}}else U(t),t._renderedNodes=[];else if("each"===n)if(s&&"object"==typeof s){const e=t.getAttribute("as")||"item",n=t.getAttribute("index")||"index",a=t.getAttribute("key");let i,o;if(s instanceof Map)i=Array.from(s.keys()),o=e=>s.get(e);else if("function"==typeof s[Symbol.iterator]){const e=Array.isArray(s)?s:Array.from(s);i=new Array(e.length);for(let t=0;te[t]}else i=Object.keys(s),o=e=>s[e];t._keyedNodes||(t._keyedNodes=new Map);const r=new Map,l=[];i.forEach((s,i)=>{const c=o(s),d=a?c&&"object"==typeof c?c[a]:c:s,h=null==d||r.has(d)?`st_key_${i}`:d;let u=t._keyedNodes.get(h);u?(t._keyedNodes.delete(h),u.forEach(a=>{t.parentNode.insertBefore(a,t),a._ref[n]=s,a._ref[e]=c,H(a)})):(u=[],t._children.forEach(a=>{const i=a.cloneNode(!0);i._ref={...t._ref,[n]:s,[e]:c},i._thisObj=t._thisObj,t.parentNode.insertBefore(i,t),u.push(i)})),r.set(h,u),l.push(u)}),t._keyedNodes.forEach(e=>e.forEach(e=>{U(e),e.remove()})),t._keyedNodes=r,t._renderedNodes=l}else U(t),t._renderedNodes=[];else if("bind"===n){if(["INPUT","SELECT","TEXTAREA"].includes(t.tagName)&&!t.hasAttribute("autocomplete")&&t.setAttribute("autocomplete","off"),"checkbox"===t.type){"on"===t.value||s||($(`${e.tpl} = []`,{thisNode:t},t._thisObj||t,t._ref||{}),s=[]),t._checkboxMultiMode=s instanceof Array;const n=s instanceof Array?s.includes(t.value):!!s;t.checked!==n&&(t.checked=n)}else"radio"===t.type?t.checked!==(t.value===String(s??""))&&(t.checked=t.value===String(s??"")):"value"in t&&"file"!==t.type?setTimeout(()=>{t.value!==String(s??"")&&(t.value=s)}):t.isContentEditable&&t.innerHTML!==String(s??"")&&(t.innerHTML=s);t.dispatchEvent(new CustomEvent("bind",{bubbles:!1,detail:s}))}else if(["checked","disabled","readonly"].includes(n)&&(s=!!s),"boolean"==typeof s)s?t.setAttribute(n,""):t.removeAttribute(n);else if(void 0!==s)if("string"!=typeof s&&(s=JSON.stringify(s)),"text"===n)t.textContent=s??"";else if("html"===n)t.innerHTML=s??"";else if("IMG"===t.tagName&&"src"===n&&s.includes(".svg"))t.setAttribute("_src",s??"");else if("class"===n){void 0===t._staticClasses&&(t._staticClasses=t.getAttribute("class")||"");const e=t._staticClasses,n=s?e?`${s} ${e}`:s:e;t.setAttribute("class",n.trim().replace(/\s+/g," "))}else if("style"===n){void 0===t._staticStyles&&(t._staticStyles=t.getAttribute("style")||"");const e=t._staticStyles,n=s?e?`${s}; ${e}`:s:e;t.setAttribute("style",n.trim().replace(/;;+/g,";").replace(/^;+\s*|;\s*$/g,""))}else t.setAttribute(n,s??"")}}function I(e){e.node._bindings||(e.node._bindings=[]),e.node._bindings.push({attr:e.attr,prop:e.prop,tpl:e.tpl,exp:e.exp}),W(e)}u(e=>W(e));const H=(e,t={})=>{if(3===e.nodeType){if(e._bindings)return e._states=new Set,void e._bindings.forEach(t=>W({node:e,...t}));if(e._stTranslated)return;return e.textContent.includes("{#")&&I({node:e,attr:"text",tpl:e.textContent,exp:0}),void(e._stTranslated=!0)}if(1!==e.nodeType)return;if(e._stTranslated||(e._stTranslated=!0),"TEMPLATE"!==e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("$each")||e.hasAttribute("st-if")||e.hasAttribute("st-each")||e.hasAttribute("$$if")||e.hasAttribute("$$each")||e.hasAttribute("st-st-if")||e.hasAttribute("st-st-each"))){const s=document.createElement("TEMPLATE");return Array.from(e.attributes).filter(t=>["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].includes(t.name)||(e.hasAttribute("$each")||e.hasAttribute("st-each")||e.hasAttribute("$$each")||e.hasAttribute("st-st-each"))&&["as","index"].includes(t.name)).forEach(t=>{s.setAttribute(t.name,t.value),e.removeAttribute(t.name)}),e.parentNode.insertBefore(s,e),s.content.appendChild(e),s._ref=e._ref,void H(s,t)}if("TEMPLATE"===e.tagName&&(e.hasAttribute("$if")||e.hasAttribute("st-if")||e.hasAttribute("$$if")||e.hasAttribute("st-st-if"))&&(e.hasAttribute("$each")||e.hasAttribute("st-each")||e.hasAttribute("$$each")||e.hasAttribute("st-st-each"))){const t=document.createElement("TEMPLATE"),s=Array.from(e.attributes).filter(e=>["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].includes(e.name)),n=s[s.length-1];t.setAttribute(n.name,n.value),e.removeAttribute(n.name),["$each","st-each","$$each","st-st-each"].includes(n.name)&&Array.from(e.attributes).filter(e=>["as","index"].includes(e.name)).forEach(s=>{t.setAttribute(s.name,s.value),e.removeAttribute(s.name)}),Array.from(e.content.childNodes).forEach(e=>t.content.appendChild(e)),e.content.appendChild(t),t._ref=e._ref}if("IMG"===e.tagName&&(e.hasAttribute("src")||e.hasAttribute("_src")||e.hasAttribute("$src"))){const t=e;Promise.resolve().then(()=>{const e=t.getAttribute("_src")||t.getAttribute("src");e&&fetch(e,{cache:"force-cache"}).then(e=>e.text()).then(e=>{const s=(new DOMParser).parseFromString(e,"image/svg+xml").querySelector("svg");s&&(Array.from(t.attributes).forEach(e=>s.setAttribute(e.name,e.value)),t.replaceWith(s))})})}if(void 0!==e._thisObj)t.thisObj=e._thisObj||null;else{let s=e;for(;s&&void 0===s._thisObj;)s=s.parentNode;t.thisObj=s?s._thisObj:null}if(void 0===e._ref){let t=e;for(;t&&void 0===t._ref;)t=t.parentNode;e._ref=t?{...t._ref}:{}}t.extendVars&&Object.assign(e._ref,t.extendVars),function(e,t){if(e._bindings)return e._states=new Set,e._bindings.forEach(t=>W({node:e,...t})),void(e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})));S.exists(e.tagName)&&!e._componentInitialized&&(Array.from(e.attributes).forEach(s=>{var n;if(s.name.startsWith("$.")){const a=s.name.slice(2);let i=P(s.value);t.thisObj&&i.includes("this.")&&(i=i.replace(/\bthis\./g,"this.parent."));const o=T(i,{thisNode:e},{parent:t.thisObj||e},e._ref||{});let r=e;const l=a.split(".");for(let e=0;ee.removeAttribute("slot-id")),e._componentInitialized=!0,e._thisObj||(e._thisObj=e)),"TEMPLATE"===e.tagName&&(e._children=[...e.content.childNodes],e._renderedNodes||(e._renderedNodes=[]));let s=[];"TEMPLATE"===e.tagName?["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].forEach(t=>e.hasAttribute(t)&&s.push(e.getAttributeNode(t))):s=Array.from(e.attributes).filter(e=>(e.name.startsWith("$")||e.name.startsWith("st-"))&&!["$if","$each","st-if","st-each","$$if","$$each","st-st-if","st-st-each"].includes(e.name)||e.name.includes(".")||!e.name.startsWith("$")&&!e.name.startsWith("st-")&&!e.name.startsWith(".")&&!e.name.startsWith("on")&&"bind"!==e.name&&e.value.includes("{#")),e._thisObj&&t.thisObj&&e._thisObj!==t.thisObj&&(e._thisObj.parent=t.thisObj),e._thisObj||(e._thisObj=t.thisObj||null),e._ref||(e._ref=t.extendVars||{}),e._states=new Set,s.forEach(s=>{let n=0;s.name.startsWith("$$")||s.name.startsWith("st-st-")?n=2:(s.name.startsWith("$")||s.name.startsWith("st-"))&&(n=1);const a=2===n?s.name.startsWith("$$")?s.name.slice(2):s.name.slice(6):1===n?s.name.startsWith("$")?s.name.slice(1):s.name.slice(3):s.name;let i=s.value;if(e.removeAttribute(s.name),a.startsWith("."))I({node:e,prop:a.split("."),tpl:i,exp:n});else if(a.startsWith("on")){const s=a.slice(2);"update"===s&&(e._hasOnUpdate=!0),"load"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnLoad=!0),"unload"!==s||["BODY","IMG","IFRAME"].includes(e.tagName)||(e._hasOnUnload=!0),e.addEventListener(s,s=>{const n=s.detail&&"object"==typeof s.detail&&!Array.isArray(s.detail)?s.detail:{};$(i,{event:s,thisNode:e,...n},t.thisObj||e,e._ref||{})})}else{if("bind"===a){const s=["INPUT","TEXTAREA"].includes(e.tagName)&&["textarea","text","password","email","number","search","url","tel"].includes(e.type||"text")||e.isContentEditable;e.addEventListener(s?"input":"change",s=>{let n=e.isContentEditable?s.target.innerHTML:"checkbox"===e.type?s.target.checked:"file"===e.type?s.target.files:s.target.value??s.detail;d(e),v(!0),"checkbox"===e.type&&e._checkboxMultiMode?$(`!!checked ? (!${i}.includes(val) && ${i}.push(val)) : (index = ${i}.indexOf(val), index > -1 && ${i}.splice(index, 1))`,{val:e.value,checked:n,thisNode:e},t.thisObj||e,e._ref||{}):$(`${i} = val`,{val:n,thisNode:e},t.thisObj||e,e._ref||{}),v(!1),d(null)})}else"text"!==a||i||(i=e.textContent,e.textContent="");i&&I({node:e,attr:a,tpl:i,exp:n})}}),(e._hasOnLoad||e._componentInitialized)&&Promise.resolve().then(()=>e.dispatchEvent(new Event("load",{bubbles:!1}))),e._hasOnUpdate&&e.dispatchEvent(new Event("update",{bubbles:!1})),e._thisObj&&(t.thisObj=e._thisObj)}(e,{...t});[...e.childNodes||[]].forEach(s=>H(s,{thisObj:e._thisObj??t.thisObj,extendVars:{...e._ref}}))},J=e=>{(1===e.nodeType||e._states)&&(1===e.nodeType&&e._hasOnUnload&&e.dispatchEvent(new Event("unload",{bubbles:!1})),e._states&&e._states.forEach(t=>{for(const[s,n]of t)for(const t of n)t.node===e&&n.delete(t)}),e.childNodes&&e.childNodes.forEach(e=>J(e)))};if(globalThis.Component=S,globalThis.SetTranslator=M,globalThis.__unsafeRefreshState=H,"undefined"!=typeof document){const e=()=>{globalThis.Component&&globalThis.Component._initPending&&globalThis.Component._initPending();const e=document.documentElement;e.hasAttribute("$data-bs-theme")||e.hasAttribute("data-bs-theme")||e.setAttribute("$data-bs-theme","LocalStorage.darkMode?'dark':'light'"),new MutationObserver(e=>{e.forEach(e=>{e.addedNodes.forEach(e=>{e.isConnected&&H(e)}),e.removedNodes.forEach(e=>J(e))})}).observe(document.documentElement,{childList:!0,subtree:!0}),H(document.documentElement)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,!0)}const R=H;e.$=i,e.$$=o,e.Component=S,e.Cookie=m,e.Hash=p,e.Lang=_,e.LocalStorage=g,e.NewState=f,e.SetTranslator=M,e.State=y,e.Util=a,e.__unsafeRefreshState=R,e._returnCode=T,e._runCode=$,e.onNotifyUpdate=u,e.setActiveBinding=c,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})}); diff --git a/src/core.js b/src/core.js index d7f36fc..3d70e5e 100644 --- a/src/core.js +++ b/src/core.js @@ -31,6 +31,29 @@ export const LocalStorage = NewState({}, k => Util.safeJson(localStorage.getItem v === undefined ? localStorage.removeItem(k) : localStorage.setItem(k, newStr) }) +const readCookies = () => Object.fromEntries(document.cookie.split(';').map(item => { + const index = item.indexOf('=') + if (index < 0) return [item.trim(), ''] + return [item.slice(0, index).trim(), item.slice(index + 1)] +}).filter(([key]) => key)) + +export const Cookie = NewState({}, k => { + const value = readCookies()[k] + if (value === undefined) return undefined + try { return Util.safeJson(decodeURIComponent(value)) } + catch (_) { return undefined } +}, (k, v) => { + const value = v === undefined ? '' : encodeURIComponent(JSON.stringify(v)) + const maxAge = v === undefined ? 0 : 3153600000 + document.cookie = `${k}=${value}; Path=/; Max-Age=${maxAge}; SameSite=Lax` +}) + +export const Lang = NewState({}) + +if (Cookie.language === undefined && typeof navigator !== 'undefined') { + Cookie.language = navigator.languages?.[0] || navigator.language || 'en-US' +} + export const State = NewState({ exitBlocks: 0 }); @@ -38,6 +61,8 @@ export const State = NewState({ // 全局挂载 globalThis.Hash = Hash; globalThis.LocalStorage = LocalStorage; +globalThis.Cookie = Cookie; +globalThis.Lang = Lang; globalThis.State = State; let _disableRunCodeError = false; diff --git a/src/engine.js b/src/engine.js index 8bca6d0..0db50f2 100644 --- a/src/engine.js +++ b/src/engine.js @@ -5,7 +5,7 @@ import { Util, $, $$ } from './utils.js'; import { NewState, _setActiveBinding, _setNoWriteBack, _onNotifyUpdate } from './observer.js'; -import { _runCode, _returnCode, setDisableRunCodeError } from './core.js'; +import { Lang, _runCode, _returnCode, setDisableRunCodeError } from './core.js'; // --- Component Logic --- const _components = new Map(); @@ -132,6 +132,31 @@ let _translator = (text, args) => { }; export const SetTranslator = (fn) => _translator = fn; +const _translationLoads = new Map(); +const _isPromise = value => value && typeof value.then === 'function'; +const _fillTranslationArgs = (text, args) => { + if (!text || typeof text !== 'string') return text; + return text.replace(/\{(.+?)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(args, key) ? args[key] : match); +}; + +const _getTranslation = (text, args) => { + const cached = Lang[text]; + if (Object.prototype.hasOwnProperty.call(Lang, text)) return _fillTranslationArgs(cached, args); + const language = globalThis.Cookie?.language; + const loadKey = `${language}\0${text}`; + if (_translationLoads.has(loadKey)) return text; + let result; + try { result = _translator(text, args); } + catch (_) { return text; } + if (!_isPromise(result)) return typeof result === 'string' ? result : text; + const loading = Promise.resolve(result).then(translated => { + if (globalThis.Cookie?.language === language && typeof translated === 'string') Lang[text] = translated; + return translated; + }).catch(() => text).finally(() => _translationLoads.delete(loadKey)); + _translationLoads.set(loadKey, loading); + return text; +}; + const _translate = (text) => { if (!text || typeof text !== 'string' || !text.includes('{#')) return text; return text.replace(/\{#(.+?)#\}/g, (m, content) => { @@ -141,7 +166,7 @@ const _translate = (text) => { const matches = parts[0].match(/\{(.+?)\}/g); if (matches) matches.forEach((match, i) => args[match.substring(1, match.length - 1)] = parts[i + 1] || ''); } - return _translator(parts[0], args); + return _getTranslation(parts[0], args); }); }; @@ -174,6 +199,7 @@ export function _updateBinding(binding) { if (binding.exp === 2 && typeof result === 'string') { try { result = _returnCode(result, { thisNode: node }, node._thisObj || node, node._ref || null); } catch (e) { } } + if (typeof result === 'string' && result.includes('{#')) result = _translate(result); _setActiveBinding(null); binding.lastResult = result; @@ -361,7 +387,10 @@ export function _parseNode(node, scanObj) { if (node.tagName === 'TEMPLATE') { ['$if', '$each', 'st-if', 'st-each', '$$if', '$$each', 'st-st-if', 'st-st-each'].forEach(n => node.hasAttribute(n) && attrs.push(node.getAttributeNode(n))); } else { - attrs = Array.from(node.attributes).filter(a => (a.name.startsWith('$') || a.name.startsWith('st-')) && !['$if', '$each', 'st-if', 'st-each', '$$if', '$$each', 'st-st-if', 'st-st-each'].includes(a.name) || a.name.includes('.')); + attrs = Array.from(node.attributes).filter(a => + (a.name.startsWith('$') || a.name.startsWith('st-')) && !['$if', '$each', 'st-if', 'st-each', '$$if', '$$each', 'st-st-if', 'st-st-each'].includes(a.name) || + a.name.includes('.') || + !a.name.startsWith('$') && !a.name.startsWith('st-') && !a.name.startsWith('.') && !a.name.startsWith('on') && a.name !== 'bind' && a.value.includes('{#')); } @@ -399,7 +428,7 @@ export function _parseNode(node, scanObj) { setDisableRunCodeError(false); _setNoWriteBack(null); }); } else if (realAttrName === 'text' && !tpl) { tpl = node.textContent; node.textContent = ''; } - if (tpl) { tpl = _translate(tpl); _initBinding({ node, attr: realAttrName, tpl, exp }); } + if (tpl) _initBinding({ node, attr: realAttrName, tpl, exp }); } }); @@ -410,23 +439,19 @@ export function _parseNode(node, scanObj) { export const _scanTree = (node, scanObj = {}) => { if (node.nodeType === 3) { + if (node._bindings) { + node._states = new Set(); + node._bindings.forEach(binding => _updateBinding({ node, ...binding })); + return; + } if (node._stTranslated) return; - const translated = _translate(node.textContent); - if (translated !== node.textContent) node.textContent = translated; + if (node.textContent.includes('{#')) _initBinding({ node, attr: 'text', tpl: node.textContent, exp: 0 }); node._stTranslated = true; return; } if (node.nodeType !== 1) return; - if (!node._stTranslated) { - Array.from(node.attributes).forEach(attr => { - if (!attr.name.startsWith('$') && !attr.name.startsWith('st-') && !attr.name.startsWith('.')) { - const translated = _translate(attr.value); - if (translated !== attr.value) attr.value = translated; - } - }); - node._stTranslated = true; - } + if (!node._stTranslated) node._stTranslated = true; if (node.tagName !== 'TEMPLATE' && (node.hasAttribute('$if') || node.hasAttribute('$each') || node.hasAttribute('st-if') || node.hasAttribute('st-each') || node.hasAttribute('$$if') || node.hasAttribute('$$each') || node.hasAttribute('st-st-if') || node.hasAttribute('st-st-each'))) { const template = document.createElement('TEMPLATE'); @@ -493,8 +518,8 @@ export const _scanTree = (node, scanObj = {}) => { }; export const _unbindTree = (node) => { - if (node.nodeType !== 1) return; - if (node._hasOnUnload) node.dispatchEvent(new Event('unload', { bubbles: false })); + if (node.nodeType !== 1 && !node._states) return; + if (node.nodeType === 1 && node._hasOnUnload) node.dispatchEvent(new Event('unload', { bubbles: false })); if (node._states) node._states.forEach(mappings => { for (const [key, bindingSet] of mappings) { for (const binding of bindingSet) { if (binding.node === node) bindingSet.delete(binding); } diff --git a/src/index.js b/src/index.js index ad9d747..fb0514d 100644 --- a/src/index.js +++ b/src/index.js @@ -38,5 +38,5 @@ if (typeof document !== 'undefined') { export { NewState, _setActiveBinding as setActiveBinding, _onNotifyUpdate as onNotifyUpdate } from './observer.js'; export { Component, SetTranslator } from './engine.js'; export { $, $$, Util } from './utils.js'; -export { Hash, LocalStorage, State, _runCode, _returnCode } from './core.js'; +export { Cookie, Hash, Lang, LocalStorage, State, _runCode, _returnCode } from './core.js'; export const __unsafeRefreshState = _scanTree; diff --git a/test/core.test.js b/test/core.test.js index 26529ed..f8d73e8 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -1,6 +1,6 @@ // test/core.test.js window.testCore = async function() { - const { _runCode, _returnCode } = ApigoState; + const { Cookie, _runCode, _returnCode } = ApigoState; console.log('Testing core.js...'); const vars = { a: 1, b: 2 }; const extendVars = { c: 3 }; @@ -11,6 +11,12 @@ window.testCore = async function() { // _returnCode if (_returnCode('a + b', vars, {}) !== 3) throw new Error('_returnCode simple failed'); if (_returnCode('${a} + ${b}', vars, {}) !== '1 + 2') throw new Error('_returnCode template failed'); + + Cookie.stateCookieTest = 'works'; + if (Cookie.stateCookieTest !== 'works') throw new Error('Cookie state read/write failed'); + Cookie.stateCookieTest = undefined; + if (Cookie.stateCookieTest !== undefined) throw new Error('Cookie state removal failed'); + if (!Cookie.language) throw new Error('Cookie.language default was not initialized'); console.log('core.js tests passed'); return true; diff --git a/test/dom.test.js b/test/dom.test.js index 1b7a134..c25cc60 100644 --- a/test/dom.test.js +++ b/test/dom.test.js @@ -1,6 +1,6 @@ // test/dom.test.js window.testDom = async function() { - const { __unsafeRefreshState, $, $$, NewState, Component } = ApigoState; + const { __unsafeRefreshState, $, $$, Lang, NewState, SetTranslator, Component } = ApigoState; console.log('Testing dom.js...'); const wait = () => new Promise(r => setTimeout(r, 10)); @@ -145,6 +145,67 @@ window.testDom = async function() { await wait(); if (!$('#nested-inner')) throw new Error('nested $$if failed: should be visible after update'); + // 8. Translation bindings use synchronous source text while loading, then + // update through the reactive Lang state without a second binding system. + const translations = { Hello: '你好', Tooltip: '提示', Yes: '是', No: '否', Result: '结果', Shared: '共享' }; + const translationCalls = {}; + SetTranslator(text => { + translationCalls[text] = (translationCalls[text] || 0) + 1; + return new Promise(resolve => setTimeout(() => resolve(translations[text] || text), 5)); + }); + state.flag = true; + state.label = '{#Result#}'; + window.state = state; + document.documentElement._thisObj = { state }; + document.body.innerHTML = ` + {#Hello#} + +
+
+ {#Shared#}{#Shared#} + `; + __unsafeRefreshState(document.documentElement); + if ($('#translated-text').textContent !== 'Hello') throw new Error('async static translation must initially use source text'); + if ($('#translated-expression').textContent !== 'Yes') throw new Error('async expression translation must initially use source text'); + await wait(); + if ($('#translated-text').textContent !== '你好') throw new Error('static text translation did not react to Lang'); + if ($('#translated-attr').title !== '提示') throw new Error('static attribute translation did not react to Lang'); + if ($('#translated-expression').textContent !== '是') throw new Error('expression translation did not react to Lang'); + if ($('#translated-result').textContent !== '结果') throw new Error('expression result translation did not react to Lang'); + if (translationCalls.Shared !== 1) throw new Error('concurrent translation requests must be deduplicated'); + Lang.Yes = '对'; + await wait(); + if ($('#translated-expression').textContent !== '对') throw new Error('direct Lang updates must refresh existing bindings'); + + // A translated text node must restore its Lang subscription when the same + // node is removed and inserted again. + const translatedTextNode = $('#translated-text').firstChild; + translatedTextNode.remove(); + await wait(); + $('#translated-text').appendChild(translatedTextNode); + await wait(); + Lang.Hello = '您好'; + await wait(); + if ($('#translated-text').textContent !== '您好') throw new Error('reinserted translation text node did not restore its Lang binding'); + + // A synchronous translator failure must fall back to source text and leave + // dependency tracking usable for subsequent bindings. + SetTranslator(() => { throw new Error('translator failure'); }); + document.body.innerHTML = '{#Failure#}{#Tracked#}'; + __unsafeRefreshState(document.documentElement); + if ($('#failed-translation').textContent !== 'Failure') throw new Error('synchronous translator failure must use source text'); + Lang.Tracked = '已跟踪'; + await wait(); + if ($('#tracked-translation').textContent !== '已跟踪') throw new Error('translator failure polluted active binding tracking'); + + // Native event and plain bind attributes are not translation bindings. + document.body.innerHTML = '
'; + __unsafeRefreshState(document.documentElement); + const attrBoundary = $('#translation-attr-boundary'); + if (attrBoundary.getAttribute('oncustom') !== '{#Event#}') throw new Error('plain event attribute was consumed as a translation binding'); + if (attrBoundary.getAttribute('bind') !== '{#Bind#}') throw new Error('plain bind attribute was consumed as a translation binding'); + if (attrBoundary.getAttribute('aria-label') !== 'Label') throw new Error('ordinary static attribute translation failed'); + console.log('dom.js tests passed'); return true; }