diff --git a/shared/static/scripts/sx.js b/shared/static/scripts/sx.js index 0085fb5..4f536b3 100644 --- a/shared/static/scripts/sx.js +++ b/shared/static/scripts/sx.js @@ -170,15 +170,18 @@ } function parseExpr(tok) { - var token = tok.next(); - if (token === null) throw parseErr("Unexpected end of input", tok); - if (token === "(") return parseList(tok, ")"); - if (token === "[") return parseList(tok, "]"); - if (token === "{") return parseMap(tok); - if (token === ")" || token === "]" || token === "}") { - throw parseErr("Unexpected " + token, tok); + // Use peek() (raw character) for structural decisions so that string + // values like ")" or "(" don't get confused with actual delimiters. + var raw = tok.peek(); + if (raw === null) throw parseErr("Unexpected end of input", tok); + if (raw === ")" || raw === "]" || raw === "}") { + tok.next(); // consume the delimiter + throw parseErr("Unexpected " + raw, tok); } - return token; + if (raw === "(") { tok.next(); return parseList(tok, ")"); } + if (raw === "[") { tok.next(); return parseList(tok, "]"); } + if (raw === "{") { tok.next(); return parseMap(tok); } + return tok.next(); } function parseList(tok, closer) { @@ -1297,7 +1300,26 @@ var exprs = parseAll(text); for (var i = 0; i < exprs.length; i++) sxEval(exprs[i], _componentEnv); } catch (err) { - console.error("sx.js loadComponents error [v2]:", err, "\ntext first 500:", text ? text.substring(0, 500) : "(empty)"); + // Enhanced error logging: show context around parse failure + var colMatch = err.message && err.message.match(/col (\d+)/); + var lineMatch = err.message && err.message.match(/line (\d+)/); + if (colMatch && text) { + var errLine = lineMatch ? parseInt(lineMatch[1]) : 1; + var errCol = parseInt(colMatch[1]); + var lines = text.split("\n"); + var pos = 0; + for (var li = 0; li < errLine - 1 && li < lines.length; li++) pos += lines[li].length + 1; + pos += errCol; + var start = Math.max(0, pos - 120); + var end = Math.min(text.length, pos + 120); + console.error("sx.js loadComponents PARSE ERROR:", err.message, + "\n total length:", text.length, "lines:", lines.length, + "\n error line " + errLine + ":", lines[errLine - 1] ? lines[errLine - 1].substring(0, 200) : "(no such line)", + "\n around error (pos ~" + pos + "):", + "\n «" + text.substring(start, pos) + "⛔" + text.substring(pos, end) + "»"); + } else { + console.error("sx.js loadComponents error:", err, "\ntext first 500:", text ? text.substring(0, 500) : "(empty)"); + } throw err; } }, @@ -1317,7 +1339,34 @@ mount: function (target, exprOrText, extraEnv) { var el = typeof target === "string" ? document.querySelector(target) : target; if (!el) return; - var node = Sx.render(exprOrText, extraEnv); + var node; + try { + node = Sx.render(exprOrText, extraEnv); + } catch (e) { + if (typeof exprOrText === "string") { + var src = exprOrText; + // Find approx position from error message + var colMatch = e.message && e.message.match(/col (\d+)/); + var lineMatch = e.message && e.message.match(/line (\d+)/); + if (colMatch) { + var errLine = lineMatch ? parseInt(lineMatch[1]) : 1; + var errCol = parseInt(colMatch[1]); + var lines = src.split("\n"); + var pos = 0; + for (var li = 0; li < errLine - 1 && li < lines.length; li++) pos += lines[li].length + 1; + pos += errCol; + var start = Math.max(0, pos - 80); + var end = Math.min(src.length, pos + 80); + console.error("sx.js MOUNT PARSE ERROR:", e.message, + "\n source length:", src.length, + "\n around error (pos ~" + pos + "):", + "\n «" + src.substring(start, pos) + "⛔" + src.substring(pos, end) + "»"); + } else { + console.error("sx.js MOUNT PARSE ERROR:", e.message, "\n first 500:", src.substring(0, 500)); + } + } + throw e; + } el.textContent = ""; el.appendChild(node); // Auto-hoist head elements (meta, title, link, script[ld+json]) to @@ -1354,11 +1403,13 @@ // Server sent full source (cookie was missing/stale) — update cache localStorage.setItem("sx-components-src", text); Sx.loadComponents(text); + console.log("[sx.js] components: downloaded (cookie stale)"); } else { // Server omitted source — load from cache var cached = localStorage.getItem("sx-components-src"); if (cached) { Sx.loadComponents(cached); + console.log("[sx.js] components: cached (" + hash + ")"); } else { // Cache entry missing — clear cookie and reload to get full source _clearSxCompCookie(); @@ -1373,6 +1424,7 @@ localStorage.setItem("sx-components-hash", hash); localStorage.setItem("sx-components-src", text); Sx.loadComponents(text); + console.log("[sx.js] components: downloaded (" + hash + ")"); } else { // Server omitted source but our cache is stale — clear and reload localStorage.removeItem("sx-components-hash"); diff --git a/shared/sx/parser.py b/shared/sx/parser.py index bce9262..f8d9af1 100644 --- a/shared/sx/parser.py +++ b/shared/sx/parser.py @@ -208,17 +208,25 @@ def parse_all(text: str) -> list[Any]: def _parse_expr(tok: Tokenizer) -> Any: - token = tok.next_token() - if token is None: + # Use peek() (raw character) for structural decisions so that string + # values like ")" or "(" don't get confused with actual delimiters. + raw = tok.peek() + if raw is None: raise ParseError("Unexpected end of input", tok.pos, tok.line, tok.col) - if token == "(": + if raw in ")]}": + tok.next_token() # consume the delimiter + raise ParseError(f"Unexpected {raw!r}", tok.pos, tok.line, tok.col) + if raw == "(": + tok.next_token() # consume the '(' return _parse_list(tok, ")") - if token == "[": + if raw == "[": + tok.next_token() # consume the '[' return _parse_list(tok, "]") - if token == "{": + if raw == "{": + tok.next_token() # consume the '{' return _parse_map(tok) - if token in (")", "]", "}"): - raise ParseError(f"Unexpected {token!r}", tok.pos, tok.line, tok.col) + # Everything else: strings, keywords, symbols, numbers + token = tok.next_token() return token