release: WRNexusJS 0.8.0
This commit is contained in:
@@ -199,6 +199,17 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
? input[nextIndex]
|
||||
: "";
|
||||
|
||||
var currentLine = input
|
||||
.slice(start, previousIndex + 1)
|
||||
.trim();
|
||||
|
||||
// Postfix updates are complete statements. Treating their trailing + or
|
||||
// - as a multiline operator merges the following assignment into the
|
||||
// same statement (for example count++ followed by message = ...).
|
||||
if (/\+\+$|--$/.test(currentLine)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Newlines are formatting whitespace while either side is an
|
||||
// incomplete expression. This covers multiline assignments and
|
||||
// ternaries emitted by formatted .wrn component functions.
|
||||
@@ -1650,7 +1661,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
});
|
||||
});
|
||||
|
||||
// data-show="expr" — toggle visibility on truthiness.
|
||||
// data-show="expr" — toggle visibility on truthiness. This directive is
|
||||
// intentionally non-destructive because popovers, selects and remote data
|
||||
// controls keep event wiring and state while closed. Use a compiled {#if}
|
||||
// block when the inactive branch must not be rendered.
|
||||
el.querySelectorAll("[data-show]").forEach(function (node) {
|
||||
if (!owns(node)) return;
|
||||
|
||||
@@ -3651,6 +3665,56 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
return special.value;
|
||||
}
|
||||
|
||||
// Template literals are evaluated without eval so they remain compatible
|
||||
// with a strict CSP. Each interpolation uses the same bounded expression
|
||||
// evaluator as every other reactive binding.
|
||||
if (
|
||||
expr.length >= 2 &&
|
||||
expr[0] === "\`" &&
|
||||
expr[expr.length - 1] === "\`"
|
||||
) {
|
||||
var template = expr.slice(1, -1);
|
||||
var rendered = "";
|
||||
var cursor = 0;
|
||||
|
||||
while (cursor < template.length) {
|
||||
if (template[cursor] === "\\") {
|
||||
cursor++;
|
||||
var escaped = template[cursor++];
|
||||
rendered += escaped === "n" ? "\n" : escaped === "t" ? "\t" : escaped || "";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (template[cursor] === "$" && template[cursor + 1] === "{") {
|
||||
var expressionStart = cursor + 2;
|
||||
var expressionEnd = expressionStart;
|
||||
var depth = 1;
|
||||
var quote = "";
|
||||
|
||||
for (; expressionEnd < template.length; expressionEnd++) {
|
||||
var character = template[expressionEnd];
|
||||
if (quote) {
|
||||
if (character === "\\") expressionEnd++;
|
||||
else if (character === quote) quote = "";
|
||||
continue;
|
||||
}
|
||||
if (character === '"' || character === "'") quote = character;
|
||||
else if (character === "{") depth++;
|
||||
else if (character === "}" && --depth === 0) break;
|
||||
}
|
||||
|
||||
if (depth !== 0) throw new Error("Unclosed template interpolation in '" + expr + "'");
|
||||
rendered += String(evaluateExpression(template.slice(expressionStart, expressionEnd), read));
|
||||
cursor = expressionEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
rendered += template[cursor++];
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
var tokens =
|
||||
tokenizeExpression(expr);
|
||||
|
||||
@@ -3959,18 +4023,169 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
}
|
||||
|
||||
var asyncLoads = new Map();
|
||||
function asyncValueAt(value, path) {
|
||||
var current = value;
|
||||
var parts = String(path || "").split(".").filter(Boolean);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (current == null || typeof current !== "object") return "";
|
||||
current = current[parts[i]];
|
||||
}
|
||||
return current == null ? "" : current;
|
||||
}
|
||||
function interpolateAsync(root, model, prefix) {
|
||||
function visit(node) {
|
||||
if (node.nodeType === 3) {
|
||||
node.textContent = String(node.textContent || "").replace(/\{\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}/g, function (whole, path) {
|
||||
var parts = path.split(".");
|
||||
if (parts[0] === prefix) parts.shift();
|
||||
if (prefix === "error" && parts[0] !== "message") return whole;
|
||||
var value = asyncValueAt(model, parts.join("."));
|
||||
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
});
|
||||
}
|
||||
Array.from(node.childNodes || []).forEach(visit);
|
||||
}
|
||||
visit(root);
|
||||
root.querySelectorAll("[data-wrn-async-value]").forEach(function (node) {
|
||||
var path = node.getAttribute("data-wrn-async-value") || "";
|
||||
var value = asyncValueAt(model, path);
|
||||
node.textContent = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
|
||||
});
|
||||
}
|
||||
function showAsync(boundary, state, model) {
|
||||
var template = boundary.querySelector("template[data-wrn-async-" + state + "]");
|
||||
var content = boundary.querySelector("[data-wrn-async-content]");
|
||||
if (!template || !content) return;
|
||||
var fragment = template.content.cloneNode(true);
|
||||
interpolateAsync(fragment, model, state === "error" ? "error" : boundary.getAttribute("data-wrn-async"));
|
||||
content.replaceChildren(fragment);
|
||||
boundary.setAttribute("aria-busy", state === "loading" ? "true" : "false");
|
||||
boundary.setAttribute("data-wrn-async-state", state);
|
||||
}
|
||||
function setupAsyncBoundary(boundary) {
|
||||
if (boundary.__wrnexusAsync) return;
|
||||
boundary.__wrnexusAsync = true;
|
||||
if (boundary.getAttribute("data-wrn-async-resolved") === "true") {
|
||||
boundary.setAttribute("data-wrn-async-state", "success");
|
||||
boundary.setAttribute("aria-busy", "false");
|
||||
return;
|
||||
}
|
||||
var name = boundary.getAttribute("data-wrn-async") || "";
|
||||
if (!/^[A-Za-z_$][\w$]{0,63}$/.test(name)) return showAsync(boundary, "error", { message: "Invalid data source" });
|
||||
var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&name=" + encodeURIComponent(name);
|
||||
var attempts = Math.min(5, Math.max(0, Number(boundary.getAttribute("data-wrn-async-retries")) || 0));
|
||||
var controller = new AbortController();
|
||||
boundary.__wrnexusAsyncAbort = controller;
|
||||
window.addEventListener("wrnexus:navigation-start", function () { controller.abort(); }, { once: true });
|
||||
function request(remaining) {
|
||||
var pending = asyncLoads.get(url);
|
||||
if (!pending) {
|
||||
pending = fetch(url, { headers: { accept: "application/json" }, signal: controller.signal }).then(function (response) {
|
||||
if (!response.ok) throw new Error("Client load returned " + response.status);
|
||||
return response.json();
|
||||
}).finally(function () { asyncLoads.delete(url); });
|
||||
asyncLoads.set(url, pending);
|
||||
}
|
||||
pending.then(function (result) {
|
||||
showAsync(boundary, "success", result.data);
|
||||
}).catch(function (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
if (remaining > 0) return setTimeout(function () { request(remaining - 1); }, 100 * (attempts - remaining + 1));
|
||||
showAsync(boundary, "error", { message: error && error.message ? error.message : "Loading failed" });
|
||||
});
|
||||
}
|
||||
showAsync(boundary, "loading", {});
|
||||
request(attempts);
|
||||
}
|
||||
function hydrateAsyncBoundaries(root) {
|
||||
(root || document).querySelectorAll("[data-wrn-async]").forEach(setupAsyncBoundary);
|
||||
}
|
||||
|
||||
function mountClientRoots(root) {
|
||||
(root || document).querySelectorAll("template[data-wrn-client-template]").forEach(function (template) {
|
||||
var id = template.getAttribute("data-wrn-client-template");
|
||||
var mount = null;
|
||||
(root || document).querySelectorAll("[data-wrn-client-root]").forEach(function (candidate) {
|
||||
if (!mount && candidate.getAttribute("data-wrn-client-root") === id) mount = candidate;
|
||||
});
|
||||
if (!mount || !template.content) return;
|
||||
var fragment = template.content.cloneNode(true);
|
||||
mount.replaceWith(fragment);
|
||||
template.remove();
|
||||
window.dispatchEvent(new window.CustomEvent("wrnexus:client-mounted", { detail: { id: id } }));
|
||||
});
|
||||
}
|
||||
|
||||
function hydrateDeclarativeUi(root) {
|
||||
var host = root || document;
|
||||
host.querySelectorAll("[data-wrn-portal]").forEach(function (portal) {
|
||||
if (portal.__wrnPortalMounted) return;
|
||||
var selector = portal.getAttribute("data-wrn-portal") || "body";
|
||||
var target = null; try { target = document.querySelector(selector); } catch (_) {}
|
||||
if (!target || target === portal || portal.contains(target)) return;
|
||||
portal.__wrnPortalMounted = true;
|
||||
var marker = document.createComment("wrnexus-portal");
|
||||
portal.parentNode && portal.parentNode.insertBefore(marker, portal);
|
||||
target.appendChild(portal);
|
||||
});
|
||||
host.querySelectorAll("[data-wrn-transition]").forEach(function (element) {
|
||||
if (element.__wrnTransitionMounted) return;
|
||||
element.__wrnTransitionMounted = true;
|
||||
var name = element.getAttribute("data-wrn-transition") || "wrn-transition";
|
||||
element.classList.add(name + "-enter", name + "-enter-active");
|
||||
requestAnimationFrame(function () { element.classList.remove(name + "-enter"); element.classList.add(name + "-enter-to"); });
|
||||
element.addEventListener("transitionend", function () { element.classList.remove(name + "-enter-active", name + "-enter-to"); }, { once: true });
|
||||
});
|
||||
host.querySelectorAll("[data-wrn-dynamic-component]").forEach(function (element) {
|
||||
if (element.__wrnDynamicMounted) return;
|
||||
element.__wrnDynamicMounted = true;
|
||||
var cases = Array.prototype.slice.call(element.children).filter(function (candidate) {
|
||||
return candidate.hasAttribute("data-component-case");
|
||||
}).map(function (candidate) {
|
||||
var marker = document.createComment("wrnexus-component-case:" + (candidate.getAttribute("data-component-case") || ""));
|
||||
element.insertBefore(marker, candidate);
|
||||
return { candidate: candidate, marker: marker };
|
||||
});
|
||||
var update = function () {
|
||||
var selected = element.getAttribute("data-wrn-dynamic-component") || "";
|
||||
cases.forEach(function (record) {
|
||||
var candidate = record.candidate;
|
||||
var active = candidate.getAttribute("data-component-case") === selected;
|
||||
candidate.hidden = false;
|
||||
if (active && !candidate.isConnected && record.marker.parentNode) {
|
||||
record.marker.parentNode.insertBefore(candidate, record.marker.nextSibling);
|
||||
} else if (!active && candidate.parentNode) {
|
||||
candidate.parentNode.removeChild(candidate);
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(new window.CustomEvent("wrnexus:dynamic-component", { detail: { component: selected } }));
|
||||
};
|
||||
update();
|
||||
new MutationObserver(update).observe(element, { attributes: true, attributeFilter: ["data-wrn-dynamic-component"] });
|
||||
});
|
||||
}
|
||||
|
||||
window.__wrnexusMountClientRoots = mountClientRoots;
|
||||
window.__wrnexusHydrateAsyncBoundaries = hydrateAsyncBoundaries;
|
||||
window.__wrnexusHydrateScopes = hydrateScopes;
|
||||
window.__wrnexusInvalidateClientModule = function (url) { clientModuleCache.delete(url); };
|
||||
window.__wrnexusHydrateCsrFetches = hydrateCsrFetches;
|
||||
window.__wrnexusDisposeBehaviors = disposeBehaviors;
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
mountClientRoots(document);
|
||||
hydrateScopes(document);
|
||||
hydrateCsrFetches(document);
|
||||
hydrateAsyncBoundaries(document);
|
||||
hydrateDeclarativeUi(document);
|
||||
});
|
||||
} else {
|
||||
mountClientRoots(document);
|
||||
hydrateScopes(document);
|
||||
hydrateCsrFetches(document);
|
||||
hydrateAsyncBoundaries(document);
|
||||
hydrateDeclarativeUi(document);
|
||||
}
|
||||
})();
|
||||
`.trim();
|
||||
|
||||
Reference in New Issue
Block a user