Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4bf5d97
Initial plan
Copilot Apr 21, 2026
3ccc55b
feat: add unary function parsing and tree rendering support
Copilot Apr 21, 2026
a4bc34b
chore: address validation feedback nits
Copilot Apr 21, 2026
dc650e9
Merge pull request #1 from psaegert/copilot/extend-expression-tree-un…
psaegert Apr 21, 2026
2a2c951
Initial plan
Copilot Apr 21, 2026
8c97921
Add custom # operators with inferred arity up to 3
Copilot Apr 21, 2026
b9bd520
Address review nits and finalize custom operator support
Copilot Apr 21, 2026
bf0c48c
Hide # prefix in node labels, fix font scaling, improve spacing
Copilot Apr 21, 2026
7be9762
Merge pull request #2 from psaegert/copilot/add-custom-operators-support
psaegert Apr 21, 2026
f1167f8
Initial plan
Copilot Apr 21, 2026
d3dc64b
feat: add export png control enter-to-generate and animation toggle
Copilot Apr 21, 2026
6e08d0f
Merge pull request #3 from psaegert/copilot/add-ux-improvements-expor…
psaegert Apr 21, 2026
555323e
Initial plan
Copilot Apr 21, 2026
7865c16
feat: implement minimal realtime UI with dark mode and sync tree rend…
Copilot Apr 21, 2026
66531e3
chore: add SRI for mathjs CDN script
Copilot Apr 21, 2026
0a10b82
Merge pull request #4 from psaegert/copilot/major-ux-ui-overhaul
psaegert Apr 21, 2026
2cefffe
Initial plan
Copilot Apr 21, 2026
193ddfe
Fix parser validation and add export popover with PNG/SVG options
Copilot Apr 21, 2026
84b0c94
Ensure SVG blob URL persists long enough for download
Copilot Apr 21, 2026
a712d36
Merge pull request #5 from psaegert/copilot/fix-a-plus-b-validation-bug
psaegert Apr 21, 2026
0a74059
Surface parser errors and display them between input and canvas
Copilot Apr 21, 2026
2a9c4d9
Remove now-unused isValidExpression/isValidTokens helpers
Copilot Apr 21, 2026
92a6631
Merge pull request #6 from psaegert/copilot/fix-expression-error-display
psaegert Apr 21, 2026
176a07d
Render node labels as LaTeX via KaTeX with graceful fallback
Copilot Apr 21, 2026
6b5a433
Address review: single-pass LaTeX escape, cleaner warm-cache + a11y
Copilot Apr 21, 2026
d857e5d
Merge pull request #7 from psaegert/copilot/add-latex-display-for-nodes
psaegert Apr 21, 2026
758c8d3
Require `#\name` for LaTeX symbols, sharpen canvas on HiDPI, pin cont…
Copilot Apr 21, 2026
4bc6207
Merge pull request #8 from psaegert/copilot/fix-latex-symbols-and-can…
psaegert Apr 21, 2026
c9f19ce
Fix mobile overlap and node text clipping
Copilot Apr 21, 2026
3d8dcc5
Merge pull request #9 from psaegert/copilot/fix-formatting-issues
psaegert Apr 21, 2026
dcd170b
Support numbers, quoted literals, and copy-to-clipboard actions
Copilot Apr 21, 2026
746f351
Address review: guard PNG fallback, skip all whitespace, note execCom…
Copilot Apr 21, 2026
2aa2f46
Merge pull request #10 from psaegert/copilot/fix-invalid-character-in…
psaegert Apr 21, 2026
5fd6653
Support "..." and $...$ literals as operator (non-leaf) nodes
Copilot Apr 21, 2026
83b7ab1
Consolidate custom/literal arity branch in getTokenArity
Copilot Apr 21, 2026
16597b7
Merge pull request #11 from psaegert/copilot/add-support-for-operator…
psaegert Apr 22, 2026
31bb7d5
Initial plan
Copilot Apr 22, 2026
1bb09bf
fix: crop PNG and SVG exports to tree bounds
Copilot Apr 22, 2026
08dafb1
chore: harden bounds traversal for missing children
Copilot Apr 22, 2026
e3b604a
refactor: share padded bounds size for export cropping
Copilot Apr 22, 2026
3b3ba5e
Merge pull request #12 from psaegert/copilot/crop-exported-image-content
psaegert Apr 22, 2026
266e362
docs: fork-aware README + footer credit to original, with screenshots
psaegert Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 121 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,125 @@
# expression-tree-gen
Building an expression tree of an arithmetic expression is something quite useful and it is one of the steps a compilers must take to generate machine code.
# Expression Tree Generator

With that in mind, and inspired on my lectures on the tree data structure I decided to create a web app that simulates the creation of such a tree given an expression.
A small, dependency-light web app that parses an arithmetic expression and draws its **expression tree** live on a canvas as you type. Node labels are typeset with LaTeX (via KaTeX), and the rendered tree can be exported to PNG or SVG.

Visit this [website](https://lnogueir.github.io/expression-tree-gen/) to simulate an expression yourself.
<p align="center">
<img src="docs/screenshot-light.png" width="49%" alt="Expression tree for sin(#add(a,b))*#\pi, light theme">
<img src="docs/screenshot-dark.png" width="49%" alt="Expression tree for sin(#add(a,b))*#\pi, dark theme">
</p>

## Credits:
**Try it live (this fork): https://psaegert.github.io/expression-tree-gen/**

* This [article](https://llimllib.github.io/pymag-trees/) which helped me a lot introducing me to Knuth's algorithm for the layout of the tree.
* This [article](http://ice-web.cc.gatech.edu/ce21/1/static/audio/static/pythonds/BasicDS/InfixPrefixandPostfixExpressions.html) which introduced me to Dijkstra's Shunting-yard algorithm.
* The [mycodeschool](https://www.youtube.com/user/mycodeschool) youtube channel with great videos explaning the algorithms said above.
> **This is a maintained fork.** The original *Expression Tree Generator* was created by **Lucas Nogueira** ([@lnogueir](https://github.com/lnogueir)). This repository builds on that work with a rewritten real-time UI, LaTeX-rendered node labels, PNG / SVG export, light / dark theming, and a few syntax extensions (numbers, text and LaTeX literals). See [Credits](#credits) and [License and attribution](#license-and-attribution).

| | Live site | Repository |
| ------------------------- | ----------------------------------------------- | ------------------------------------------------ |
| **This fork** (maintained) | https://psaegert.github.io/expression-tree-gen/ | https://github.com/psaegert/expression-tree-gen |
| Original (Lucas Nogueira) | https://lnogueir.github.io/expression-tree-gen/ | https://github.com/lnogueir/expression-tree-gen |

## What it does

Type an expression and the app validates it, converts it to a syntax tree, and renders that tree as labelled circular nodes connected by edges. There is no generate or submit step: the drawing updates automatically as you type (debounced by 150 ms). Invalid input shows an inline error message plus a small warning indicator while keeping the last valid drawing on screen, so the canvas is never abruptly cleared. A random sample expression is pre-filled on first load.

## Features

- **Real-time rendering.** The tree redraws on every keystroke (150 ms debounce); there is no generate button.
- **LaTeX-rendered labels.** Node labels are typeset with [KaTeX](https://katex.org/), so operators, Greek letters, subscripts, radicals, and similar symbols render properly. See [Node label rendering](#node-label-rendering) for the conventions.
- **Export to PNG or SVG.** An export menu offers PNG (at 1x, 2x, 3x, or 4x scale) or SVG. Each export is cropped to the tree's bounding box with a small margin. PNGs are rendered crisp on HiDPI displays; SVGs stay vector by embedding the KaTeX output, so they remain editable.
- **Download or copy.** Either download the export as a file (`expression-tree.png` / `expression-tree.svg`) or copy it straight to the clipboard.
- **Copy the expression.** A one-click button copies the current expression text.
- **Light / dark theme.** A toggle switches themes; the choice is saved in `localStorage` and defaults to your operating system preference on the first visit. Labels and edges re-render to match the active theme.
- **Helpful errors.** Invalid input produces a specific message (for example a mismatched parenthesis or an unknown identifier) rather than failing silently.
- **Responsive and accessible.** The canvas resizes with the window and exposes an `aria-label` describing the current expression.

## Supported syntax

- **Binary operators:** `+`, `-`, `*`, `/` (standard precedence, with parentheses for grouping)
- **Unary functions:** `sin`, `cos`, `tan`, `log`, `ln`, `sqrt`, `exp`, `abs`
- **Variables:** a single lowercase letter, `a` to `z`
- **Numbers:** integer or decimal literals, for example `2` or `3.14`
- **Custom tokens:** `#name`, for example `#pi`, `#add(a, b)` (see below)
- **Text and LaTeX literals:** `"label"` and `$\latex$` (see below)

A run of lowercase letters is only accepted if it is a single variable or an exact match of one of the unary function names above. Anything else (an unknown multi-letter identifier, an uppercase letter, an unexpected character) is reported as an error.

### Custom tokens (`#name`)

A custom token is a leading `#` followed by an identifier (first character a letter or `_`, then letters, digits, or `_`). Arity is **inferred from usage**, not declared:

- A custom token **not** followed by `(` is an arity-0 leaf, for example `#pi`.
- A custom token **followed by** `(` is a call whose arity is the number of comma-separated arguments, for example `#add(a, b)` has arity 2.
- Arity is **capped at 3**: a call with more than three arguments is rejected.

Adding a backslash after the `#` marks the identifier as a LaTeX command, so `#\pi` renders as the symbol π while `#pi` renders as the upright text `pi`. See [Node label rendering](#node-label-rendering).

### Text and LaTeX literals

For labels that are not valid identifiers, you can quote them:

- `"label"` renders the quoted text as a plain-text node label.
- `$...$` renders the quoted content as raw LaTeX, for example `$\frac{a}{b}$`.

Like custom tokens, a literal followed by `(` becomes a function node whose arity is inferred from its arguments (capped at 3), for example `"relu"(x)` or `$\sigma$(a, b)`.

### Examples

```text
sin(#add(a,b))*#\pi
#clamp(x, #min(a,b), c)
(a + b)*c - (x - y)/z
sqrt(a*a + b*b)
2*x + 3.14
$\Sigma$(a, b, c)
"relu"(x)
```

## Node label rendering

Labels are typeset with [KaTeX](https://katex.org/), loaded from a CDN, so no build step is required. A few conventions make common symbols render nicely out of the box:

- **Operators:** `*` renders as `·` (`\cdot`), `/` as `÷` (`\div`); `+` and `-` are left as-is.
- **Unary functions:** `sin`, `cos`, `tan`, `log`, `ln`, `exp` render in upright math style; `sqrt` shows a radical (√) and `abs` shows `|·|`.
- **Custom token symbols:** well-known LaTeX command names can be reached with a leading backslash after `#`, for example `#\pi` to π, `#\theta` to θ, `#\alpha` to α, `#\lambda` to λ, `#\sigma` to σ, `#\omega` to ω, plus uppercase variants such as `#\Gamma`, `#\Sigma`, `#\Omega`, and a handful of operators (`#\infty`, `#\sum`, `#\prod`, `#\int`, `#\nabla`, `#\partial`, `#\emptyset`). Any other `#\name` is forwarded to KaTeX as the raw `\name` command. Without the backslash, `#phi` simply renders as the upright text `phi`.
- **Subscripts:** identifiers ending in digits are rendered with the digits as a subscript, so `x1` becomes *x*₁ and `#\theta12` becomes θ₁₂. An underscore also introduces a subscript, so `#a_foo` becomes *a* with subscript foo.
- **Unknown custom names** without a backslash fall back to upright `\mathrm{name}` so they stay legible.

If KaTeX cannot load or a label cannot be parsed, the node falls back to its raw token text, so expressions are always displayed.

## How it works

1. **Tokenize.** The input is scanned into variables, numbers, the binary operators, unary function names, parentheses, commas, `#`-prefixed custom tokens, and quoted literals. Anything outside this grammar raises a descriptive error.
2. **Validate.** A hand-rolled recursive-descent parser checks that the token stream forms a well-formed expression and that parentheses match. There is no external math library.
3. **Infix to postfix.** A valid expression is converted to postfix using Dijkstra's shunting-yard algorithm, extended to count the arguments of custom and literal function calls so the right arity is attached.
4. **Build the tree.** The postfix stream is consumed with a stack, popping each operator or function's children according to its arity.
5. **Lay out.** Layout is a top-down recursive partition: vertical position is set by depth, and each node's horizontal range is split among its children in proportion to their leaf counts.
6. **Render.** Each node is drawn as a circle whose label is a KaTeX rasterization (with a plain-text fallback); edges are trimmed to the node boundaries. SVG export reuses the same geometry but embeds the KaTeX output as vector content.

## Running locally

The app is a static site with **no build step**. Its only runtime dependency is **KaTeX**, loaded from a CDN, which is used purely to typeset labels: parsing, layout, and export all run offline, and labels gracefully fall back to plain text if KaTeX cannot load.

```bash
git clone https://github.com/psaegert/expression-tree-gen.git
cd expression-tree-gen

# Option A: open index.html directly in your browser
# Option B (recommended): serve the folder, then open http://localhost:8000
python3 -m http.server 8000
```

## Credits

This fork is built on the original **Expression Tree Generator** by **Lucas Nogueira** ([lnogueir/expression-tree-gen](https://github.com/lnogueir/expression-tree-gen)).

Educational references that informed the original project:

- **Tree layout:** [Drawing Presentable Trees](https://llimllib.github.io/pymag-trees/) by Bill Mill, a tour of tree-layout algorithms from Knuth onward.
- **Infix to postfix:** [Infix, Prefix, and Postfix Expressions](https://runestone.academy/ns/books/published/pythonds3/BasicDS/InfixPrefixandPostfixExpressions.html), the chapter on Dijkstra's shunting-yard conversion from *Problem Solving with Algorithms and Data Structures using Python* by Miller and Ranum.
- **[mycodeschool](https://www.youtube.com/user/mycodeschool)** on YouTube, for clear explanations of the algorithms above.

## License and attribution

Neither this fork nor the original repository ships a LICENSE file, so no license is asserted here. If you reuse this code, please credit the original author and link back to the source:

- Original author: **Lucas Nogueira** ([github.com/lnogueir](https://github.com/lnogueir))
- Original repository: [github.com/lnogueir/expression-tree-gen](https://github.com/lnogueir/expression-tree-gen)
Loading