Fix parser bug: string values like ")" were confused with delimiter tokens
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 7s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 7s
Both Python and JS parsers used next_token() which returns plain strings
for both delimiter characters and string values, making them
indistinguishable. A string whose value is ")" or "(" would be
misinterpreted as a structural delimiter, causing parse errors.
Fix: use peek() (raw character) for all structural decisions in
parseExpr before consuming via next_token(). Also add enhanced error
logging to sx.js mount/loadComponents for easier future debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <head>
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user