Replace JSON sx-headers with SX dict expressions, fix blog like component
sx-headers attributes now use native SX dict format {:key val} instead of
JSON strings. Eliminates manual JSON string construction in both .sx files
and Python callers.
- sx.js: parse sx-headers/sx-vals as SX dict ({: prefix) with JSON fallback,
add _serializeDict for dict→attribute serialization, fix verbInfo scope in
_doFetch error handler
- html.py: serialize dict attribute values via SX serialize() not str()
- All .sx files: {:X-CSRFToken csrf} replaces (str "{\"X-CSRFToken\": ...}")
- All Python callers: {"X-CSRFToken": csrf} dict replaces f-string JSON
- Blog like: extract ~blog-like-toggle, fix POST returning wrong component,
fix emoji escapes in .sx (parser has no \U support), fix card :hx-headers
keyword mismatch, wrap sx_content in SxExpr for evaluation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -251,6 +251,18 @@
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Serialize a JS object as SX dict {:key "val" ...} for attribute values. */
|
||||
function _serializeDict(obj) {
|
||||
var parts = [];
|
||||
for (var k in obj) {
|
||||
if (!obj.hasOwnProperty(k)) continue;
|
||||
var v = obj[k];
|
||||
var vs = typeof v === "string" ? '"' + v.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"' : String(v);
|
||||
parts.push(":" + k + " " + vs);
|
||||
}
|
||||
return "{" + parts.join(" ") + "}";
|
||||
}
|
||||
|
||||
// --- Primitives ---
|
||||
|
||||
var PRIMITIVES = {};
|
||||
@@ -1420,7 +1432,7 @@
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
} else {
|
||||
el.setAttribute(attrName, String(attrVal));
|
||||
el.setAttribute(attrName, typeof attrVal === "object" && attrVal !== null && !Array.isArray(attrVal) ? _serializeDict(attrVal) : String(attrVal));
|
||||
}
|
||||
} else {
|
||||
// Child
|
||||
@@ -1851,7 +1863,7 @@
|
||||
cancelButtonText: "Cancel"
|
||||
}).then(function (result) {
|
||||
if (!result.isConfirmed) return;
|
||||
return _doFetch(el, method, url, extraParams);
|
||||
return _doFetch(el, verbInfo, method, url, extraParams);
|
||||
});
|
||||
}
|
||||
if (!window.confirm(confirmMsg)) return Promise.resolve();
|
||||
@@ -1866,10 +1878,10 @@
|
||||
extraParams.promptValue = promptVal;
|
||||
}
|
||||
|
||||
return _doFetch(el, method, url, extraParams);
|
||||
return _doFetch(el, verbInfo, method, url, extraParams);
|
||||
}
|
||||
|
||||
function _doFetch(el, method, url, extraParams) {
|
||||
function _doFetch(el, verbInfo, method, url, extraParams) {
|
||||
// sx-sync: abort previous
|
||||
var sync = el.getAttribute("sx-sync");
|
||||
if (sync && sync.indexOf("replace") >= 0) abortPrevious(el);
|
||||
@@ -1895,12 +1907,12 @@
|
||||
var cssHeader = _getSxCssHeader();
|
||||
if (cssHeader) headers["SX-Css"] = cssHeader;
|
||||
|
||||
// Extra headers from sx-headers
|
||||
// Extra headers from sx-headers (SX dict {:key "val"} or JSON)
|
||||
var extraH = el.getAttribute("sx-headers");
|
||||
if (extraH) {
|
||||
try {
|
||||
var parsed = JSON.parse(extraH);
|
||||
for (var k in parsed) headers[k] = parsed[k];
|
||||
var parsed = extraH.charAt(0) === "{" && extraH.charAt(1) === ":" ? parse(extraH) : JSON.parse(extraH);
|
||||
for (var k in parsed) headers[k] = String(parsed[k]);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -1974,7 +1986,7 @@
|
||||
var valsAttr = el.getAttribute("sx-vals");
|
||||
if (valsAttr) {
|
||||
try {
|
||||
var vals = JSON.parse(valsAttr);
|
||||
var vals = valsAttr.charAt(0) === "{" && valsAttr.charAt(1) === ":" ? parse(valsAttr) : JSON.parse(valsAttr);
|
||||
if (method === "GET") {
|
||||
for (var vk in vals) {
|
||||
url += (url.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
||||
|
||||
@@ -206,7 +206,7 @@ def _render(expr: Any, env: dict[str, Any]) -> str:
|
||||
return ""
|
||||
return _render_list(expr, env)
|
||||
|
||||
# --- dict → skip (data, not renderable) -------------------------------
|
||||
# --- dict → skip (data, not renderable as HTML content) -----------------
|
||||
if isinstance(expr, dict):
|
||||
return ""
|
||||
|
||||
@@ -540,6 +540,9 @@ def _render_element(tag: str, args: list, env: dict[str, Any]) -> str:
|
||||
parts.append(f" {attr_name}")
|
||||
elif attr_val is True:
|
||||
parts.append(f" {attr_name}")
|
||||
elif isinstance(attr_val, dict):
|
||||
from .parser import serialize as _sx_serialize
|
||||
parts.append(f' {attr_name}="{escape_attr(_sx_serialize(attr_val))}"')
|
||||
else:
|
||||
parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
|
||||
parts.append(">")
|
||||
|
||||
@@ -30,8 +30,8 @@ from typing import Any
|
||||
|
||||
from .jinja_bridge import sx
|
||||
|
||||
SEARCH_HEADERS_MOBILE = '{"X-Origin":"search-mobile","X-Search":"true"}'
|
||||
SEARCH_HEADERS_DESKTOP = '{"X-Origin":"search-desktop","X-Search":"true"}'
|
||||
SEARCH_HEADERS_MOBILE = {"X-Origin": "search-mobile", "X-Search": "true"}
|
||||
SEARCH_HEADERS_DESKTOP = {"X-Origin": "search-desktop", "X-Search": "true"}
|
||||
|
||||
|
||||
def render_page(source: str, **kwargs: Any) -> str:
|
||||
|
||||
Reference in New Issue
Block a user