Surface parser errors between input and canvas#6
Conversation
Agent-Logs-Url: https://github.com/psaegert/expression-tree-gen/sessions/b76f2e13-c151-4ff7-b4e5-11c4f21e04aa Co-authored-by: psaegert <36567814+psaegert@users.noreply.github.com>
Agent-Logs-Url: https://github.com/psaegert/expression-tree-gen/sessions/b76f2e13-c151-4ff7-b4e5-11c4f21e04aa Co-authored-by: psaegert <36567814+psaegert@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR improves the UX around invalid expressions by surfacing specific parser errors in the UI (instead of silently failing and only showing a generic warning icon), bridging the gap between parsing logic and canvas rendering.
Changes:
- Replace
nullfailure returns intokenize/infixToPostfixwith targetedthrow new Error(...)messages. - Catch parser/tree-construction errors in the canvas renderer and display the error text alongside the warning icon.
- Add and style an
#error-messageelement in the controls UI with live-region semantics.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| infixToPostfix.js | Converts silent parse failures into explicit exceptions with specific messages; adds token validation. |
| canvas.js | Catches parse/build errors and keeps warning + error-message UI state in sync. |
| index.html | Adds the error message container near the expression input with ARIA attributes. |
| app.css | Styles the new error message element (including dark theme adjustments). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!ALLOWED_CHARACTERS_REGEX.test(expression)) { | ||
| var badMatch = expression.match(/[^A-Za-z0-9_#,+\-*/()\s]/) | ||
| throw new Error("Invalid character '" + (badMatch ? badMatch[0] : '?') + "' in expression") | ||
| } | ||
| var tokens = tokenize(expression) | ||
| if (tokens.length === 0) { | ||
| throw new Error('Empty expression') |
There was a problem hiding this comment.
ALLOWED_CHARACTERS_REGEX allows all whitespace via \s, but tokenize() only skips literal spaces (current === ' '). As a result, inputs containing tabs/newlines will pass the regex check and then fail later with an “Invalid character” error. Consider either skipping all whitespace in tokenize() (e.g., \s) or tightening the allowed-character regex to match what the tokenizer actually accepts.
| } else if ("," === token) { | ||
| var commaTop = op_stack.pop() | ||
| while (commaTop !== '(') { | ||
| if (typeof commaTop === 'undefined') { | ||
| return null | ||
| throw new Error("Unexpected ',' outside of a function call") | ||
| } | ||
| postfixList.push(commaTop) | ||
| commaTop = op_stack.pop() | ||
| } | ||
| op_stack.push('(') | ||
| if (customCallStack.length === 0) { | ||
| return null | ||
| throw new Error("Unexpected ',' outside of a function call") | ||
| } | ||
| customCallStack[customCallStack.length - 1].commaCount++ |
There was a problem hiding this comment.
After adding validateTokens(tokens) (which rejects any expression that can’t be fully parsed by parseAddSubtract(..., stopOnComma=false)), a comma outside a custom function call will already throw before the shunting-yard loop runs. That makes these Unexpected ',' outside of a function call branches effectively unreachable and can prevent the intended comma-specific messaging from surfacing. Either move comma validation into validateTokens with a targeted message, or remove the dead/unreachable checks here.
| if (tokens === null || tokens.length === 0) { | ||
| return false; | ||
| if (parsed.nextIndex !== tokens.length) { | ||
| throw new Error('Unexpected token ' + describeToken(tokens[parsed.nextIndex]) + ' at position ' + parsed.nextIndex) |
There was a problem hiding this comment.
The error message ... at position <n> is reporting a token index (not a character index). That can be confusing for users trying to locate the issue in the original input string. Consider renaming this to token <n> / token index <n>, or (if feasible) tracking the character offset during tokenization so the reported position maps to the original expression.
| throw new Error('Unexpected token ' + describeToken(tokens[parsed.nextIndex]) + ' at position ' + parsed.nextIndex) | |
| throw new Error('Unexpected token ' + describeToken(tokens[parsed.nextIndex]) + ' at token index ' + parsed.nextIndex) |
| </button> | ||
| </div> | ||
|
|
||
| <div class="error-message" id="error-message" role="alert" aria-live="polite" hidden></div> |
There was a problem hiding this comment.
role="alert" has an implicit aria-live="assertive" in ARIA; setting aria-live="polite" alongside it is conflicting. If you want polite announcements, prefer role="status" (or drop role="alert" and keep aria-live="polite").
| <div class="error-message" id="error-message" role="alert" aria-live="polite" hidden></div> | |
| <div class="error-message" id="error-message" role="alert" hidden></div> |
The reported expression⚠️ with no indication of what's wrong.
(a * b) - c + z / xin fact parses and renders correctly onmain(verified in Node and headless Chrome); the warning icon is not triggered. The real issue is thatinfixToPostfixsilentlyreturn nulls from ~10 failure sites, so the UI can only show a genericChanges
infixToPostfix.js— replace everyreturn nullfailure path intokenize/infixToPostfixwiththrow new Error(...)carrying a specific message (invalid character, unknown identifier, missing/mismatched), stray,, empty expression, etc.). Added a pre-pass paren-depth check so unbalanced parens produce a targeted message instead of a generic "malformed" one. Removed the now-deadisValidExpression/isValidTokenshelpers.canvas.js—renderExpressioncatches errors frominfixToPostfixandconstructTreeand writeserror.messageinto a new element;clearError/showErrorhelpers keep warning-icon and error-text state in sync.clear-treeand empty-input paths clear both.index.html— new<div id="error-message" role="alert" aria-live="polite" hidden>inserted inside.controls, directly below.input-wrap, so it sits between the input and the canvas.app.css— red outlined pill styling for.error-message, with a dark-theme variant.Example messages
(a+bMissing closing ')'a+b)Mismatched ')' with no matching '('foo(a)Unknown identifier 'foo' (use '#foo' for a custom function)a + $Invalid character '$' in expression#(a)Expected an identifier after '#'