mathjson-tree-builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mārtiņš Mednis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # mathjson-tree-builder
2
+
3
+ **mathjson-tree-builder** is the front-end counterpart to
4
+ [mathjson-solver](https://github.com/LongenesisLtd/mathjson-solver): build
5
+ [MathJSON](https://mathlive.io/math-json/) formulas and data-processing
6
+ expressions visually in the browser, evaluate them in your Python backend.
7
+
8
+ Hand-writing MathJSON's array syntax isn't something you can ask a
9
+ non-technical user to do — this framework-agnostic **Custom Element** gives
10
+ them point-and-click building instead, with named external values (form
11
+ fields, survey questions, ...) droppable into any slot.
12
+
13
+ **[Live demo](https://mrtmednis.gitlab.io/mathjson-tree-builder/)**
14
+
15
+ ![Compound interest built as principal times (1 + rate over periods) to the power of periods times years — addition, a fraction, multiplication, and a parenthesized power base all in one formula](https://gitlab.com/mrtmednis/mathjson-tree-builder/-/raw/main/docs/screenshot-compound-interest.png)
16
+
17
+ ## Install & use
18
+
19
+ ```bash
20
+ npm install mathjson-tree-builder
21
+ ```
22
+
23
+ ```html
24
+ <script type="module">
25
+ import "mathjson-tree-builder";
26
+ </script>
27
+
28
+ <mathjson-tree-builder id="builder"></mathjson-tree-builder>
29
+
30
+ <script type="module">
31
+ const el = document.getElementById("builder");
32
+
33
+ el.value = ["Divide", "weight", ["Power", "height", 2]]; // BMI
34
+
35
+ el.symbols = [
36
+ { id: "weight", label: "Weight (kg)", type: "number" },
37
+ { id: "height", label: "Height (m)", type: "number" },
38
+ ];
39
+
40
+ el.addEventListener("change", (e) => {
41
+ console.log(e.detail.value); // the current MathJSON expression,
42
+ // ready to hand to mathjson-solver
43
+ });
44
+ </script>
45
+ ```
46
+
47
+ Works identically in plain HTML, React, Vue, Angular, Svelte — it's a native
48
+ Custom Element with no framework dependency baked into the published bundle
49
+ (Lit is used to *build* it, not required to *use* it).
50
+
51
+ ## What it does
52
+
53
+ - Every one of the ~338 constructs mathjson-solver implements is buildable,
54
+ via a generic `Name(arg1, arg2, ...)` fallback with add/remove-argument and
55
+ wrap-in-parent-construct affordances on every node.
56
+ - The common ones get real notation instead of the generic form: arithmetic
57
+ and comparisons as infix (`a + b`, `age > 18`), `Power`/`Sqrt` as
58
+ superscript/radical, `Sum`/`Product` as Σ/Π, set/list membership as
59
+ `age in (1, 2, 3)`.
60
+ - `Constants` bindings let a piece of a formula be named and collapsed, so a
61
+ large formula reads as a handful of names instead of one long expression.
62
+ - `If` supports both of mathjson-solver's shapes — a single condition, or an
63
+ unlimited elseif chain — and any comparison/membership/boolean operator
64
+ can be swapped for another without rebuilding the condition from scratch.
65
+ - A raw-JSON escape hatch for pasting in an expression built elsewhere, or
66
+ anything the tree view doesn't gracefully cover.
67
+
68
+ See [`docs/API.md`](https://gitlab.com/mrtmednis/mathjson-tree-builder/-/blob/main/docs/API.md) for the full public API and a detailed
69
+ rendering-model reference.
70
+
71
+ ## Known limitations
72
+
73
+ Two things worth knowing before embedding it: the per-slot menu doesn't yet
74
+ use the browser's Popover API, so a host container with
75
+ `overflow: hidden`/`auto`/`scroll` will clip it; and moving focus away from
76
+ an active node without a click can leave its menu visible slightly longer
77
+ than ideal. See [`docs/API.md`](https://gitlab.com/mrtmednis/mathjson-tree-builder/-/blob/main/docs/API.md) for the full details.
78
+
79
+ Not what you're after if you need a real math-notation input — CortexJS's
80
+ [`mathfield`](https://cortexjs.io/mathfield/) is a mature LaTeX editor and
81
+ solves a different problem than this does; see
82
+ [`CONTRIBUTING.md`](https://gitlab.com/mrtmednis/mathjson-tree-builder/-/blob/main/CONTRIBUTING.md)
83
+ for how the two compare.
84
+
85
+ ## Contributing
86
+
87
+ See [`CONTRIBUTING.md`](https://gitlab.com/mrtmednis/mathjson-tree-builder/-/blob/main/CONTRIBUTING.md) for development setup and how the
88
+ construct registry is generated, and
89
+ [`CHANGELOG.md`](https://gitlab.com/mrtmednis/mathjson-tree-builder/-/blob/main/CHANGELOG.md)
90
+ for what's shipped in each release.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Classify a node's current "kind" for rendering/editing purposes. This is
3
+ * the one place that decides literal-vs-symbol-vs-function for a value —
4
+ * see `resolveSymbol` for why a string is ambiguous on its own.
5
+ */
6
+ import type { MathJsonExpression, SymbolReference } from "../types.js";
7
+ export type NodeKind = "number" | "boolean" | "text" | "symbol" | "function";
8
+ export declare function classifyValue(value: MathJsonExpression, symbols: readonly SymbolReference[]): NodeKind;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Generic, path-based tree mutation.
3
+ *
4
+ * This is the *only* place that knows how to read/write a node given its
5
+ * path from the root. Node rendering code calls these, and only these —
6
+ * there is deliberately no per-construct-name mutation logic anywhere else
7
+ * (contrast with prior art, which re-derived "where in
8
+ * the parent array does this go" via a giant if/else keyed on construct
9
+ * name).
10
+ */
11
+ import { type MathJsonExpression, type Path } from "../types.js";
12
+ /** Read the node at `path`, relative to `root`. `path: []` returns `root`. */
13
+ export declare function getAtPath(root: MathJsonExpression, path: Path): MathJsonExpression;
14
+ /**
15
+ * Return a new root with the node at `path` replaced by `value`. Does not
16
+ * mutate `root`. `path: []` replaces the whole root — wrapping the root in
17
+ * a new parent construct (the "add parent node" operation) is therefore
18
+ * the exact same call as wrapping any other node, just with an empty path;
19
+ * callers reassign `el.value` to the result either way.
20
+ *
21
+ * Uses shallow copy-on-the-path-only structural sharing: only the arrays
22
+ * along `path` are cloned, every sibling subtree is reused as-is.
23
+ */
24
+ export declare function setAtPath(root: MathJsonExpression, path: Path, value: MathJsonExpression): MathJsonExpression;
25
+ /**
26
+ * Append a new argument to the compound expression at `path`.
27
+ * `node` must be `[constructName, ...args]`; the new value is added last.
28
+ */
29
+ export declare function withArgAdded(node: MathJsonExpression[], value: MathJsonExpression): MathJsonExpression[];
30
+ /**
31
+ * Remove the argument at `argIndex` (an index into the compound array
32
+ * itself, so `1` is the first argument — index `0` is the construct name
33
+ * and is never a valid `argIndex`).
34
+ */
35
+ export declare function withArgRemoved(node: MathJsonExpression[], argIndex: number): MathJsonExpression[];
36
+ /**
37
+ * Wrap the node currently at `path` in a new parent construct `X`, so it
38
+ * becomes `X`'s first argument: `V` -> `[X, V, ...extraArgs]`. This is the
39
+ * one and only implementation of the "add parent node" operation — it
40
+ * works identically whether `path` addresses the root (`[]`) or any nested
41
+ * node, because it's built directly on `getAtPath`/`setAtPath`.
42
+ */
43
+ export declare function wrapAtPath(root: MathJsonExpression, path: Path, constructName: string, extraArgs?: MathJsonExpression[]): MathJsonExpression;
44
+ /**
45
+ * Swap the construct name of the compound node at `path`, keeping its
46
+ * arguments unchanged: `[Greater, a, b]` -> `[LessEqual, a, b]`. Meant for
47
+ * switching among arity-compatible siblings (the curated comparison/infix
48
+ * groups in `registry/notation.ts`'s `swapTargets`) — this doesn't check
49
+ * compatibility itself, it's a pure rename of index 0.
50
+ *
51
+ * `reverseFirstTwoArgs` additionally swaps the first two arguments —
52
+ * needed for the one pair in that group with a different calling
53
+ * convention (`Contains`'s collection-first order vs. everything else's
54
+ * value-first order; see `registry/notation.ts`'s `swapNeedsArgReversal`),
55
+ * where keeping the args as-is would silently reverse the meaning.
56
+ */
57
+ export declare function swapOperator(root: MathJsonExpression, path: Path, newName: string, reverseFirstTwoArgs?: boolean): MathJsonExpression;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Symbol reference resolution.
3
+ *
4
+ * A MathJSON string leaf is ambiguous on its own — it's either a literal
5
+ * string *value* or a bare symbol reference. This editor doesn't decide
6
+ * that itself; it just checks the string against the host-supplied
7
+ * `symbols` list and renders accordingly (a matching id renders as a
8
+ * labeled chip, a non-matching string renders as a plain literal input).
9
+ */
10
+ import type { SymbolReference } from "../types.js";
11
+ export declare function resolveSymbol(symbols: readonly SymbolReference[], value: string): SymbolReference | undefined;
12
+ /**
13
+ * Filter/sort `symbols` for a given search term and optional expected
14
+ * `type` hint (a soft hint, not a hard validation rule — matching
15
+ * entries sort first, non-matching entries are still included).
16
+ */
17
+ export declare function searchSymbols(symbols: readonly SymbolReference[], query: string, expectedType?: string): SymbolReference[];
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `<mathjson-tree-builder>` — the custom element.
3
+ *
4
+ * Node rendering is implemented as a recursive private template method
5
+ * (`#renderSlot`) rather than as separate nested custom elements — one
6
+ * shadow root for the whole widget, one component recursively renders
7
+ * itself per node at the template level. This keeps the "every mutation
8
+ * goes through one path-based function" contract simple: every
9
+ * rendering call closes directly over its own `path` and calls `#commit`,
10
+ * with no event-bubbling-across-shadow-boundaries plumbing needed.
11
+ *
12
+ * Rendering is also click-to-edit: an unfocused node renders as plain
13
+ * content (no inputs, no "⋯" menu) — only the currently *active* node
14
+ * (the one that has focus, per `activePath`) shows its editable control
15
+ * and its menu. See the `activePath` state and `#onFocusIn` below.
16
+ */
17
+ import { LitElement, type PropertyValues, type TemplateResult } from "lit";
18
+ import { type Evaluator, type MathJsonExpression, type SymbolReference } from "./types.js";
19
+ export declare class MathJsonTreeBuilderElement extends LitElement {
20
+ #private;
21
+ static styles: import("lit").CSSResult;
22
+ value: MathJsonExpression;
23
+ symbols: SymbolReference[];
24
+ blacklist: string[];
25
+ evaluate?: Evaluator;
26
+ /**
27
+ * Operand count above which `And`/`Or` switch from the compact inline
28
+ * `a ∧ b ∧ c` form to a vertical stacked list (one operand per line) —
29
+ * the inline form wraps mid-row once it runs out of width, which reads
30
+ * fine for a couple of conditions but ragged for a long chain. A real
31
+ * HTML attribute (not `attribute: false` like the object-typed
32
+ * properties above) since it's a plain number a host page can set
33
+ * declaratively: `<mathjson-tree-builder inline-to-stacked-threshold="5">`.
34
+ */
35
+ inlineToStackedThreshold: number;
36
+ /**
37
+ * The path (as a `pathKey` string) of the currently focused/active node,
38
+ * or `null` when nothing in this widget has focus. Drives both which
39
+ * node renders its editable control instead of plain text, and which
40
+ * node's "⋯" menu is shown — see the class doc comment.
41
+ */
42
+ private activePath;
43
+ /** Raw-JSON escape hatch: true while showing the textarea instead of the tree. */
44
+ private jsonMode;
45
+ /** The textarea's current (possibly invalid, uncommitted) content while in JSON mode. */
46
+ private jsonDraft;
47
+ /** Parse/shape error for `jsonDraft`, or `null` when it's valid and already committed. */
48
+ private jsonError;
49
+ /**
50
+ * Path keys of explicitly expanded `Constants` bindings — purely
51
+ * a view concern, never touches `value`. Absent = collapsed, so a big
52
+ * formula reads as just its constant names until you open one — the
53
+ * point of naming a piece of it in the first place.
54
+ */
55
+ private expandedConstants;
56
+ connectedCallback(): void;
57
+ disconnectedCallback(): void;
58
+ protected updated(changed: PropertyValues): void;
59
+ protected render(): TemplateResult;
60
+ }
61
+ declare global {
62
+ interface HTMLElementTagNameMap {
63
+ "mathjson-tree-builder": MathJsonTreeBuilderElement;
64
+ }
65
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Public entry point. Importing this module registers
3
+ * `<mathjson-tree-builder>` via `customElements.define` (mirroring
4
+ * MathfieldElement's own integration pattern) as a side effect of
5
+ * importing `./element.js`.
6
+ */
7
+ export { MathJsonTreeBuilderElement, } from "./element.js";
8
+ export type { ArgSpec, ConstructDescriptor, EvalResult, Evaluator, MathJsonExpression, Path, SymbolReference, } from "./types.js";
9
+ export { isCompound, isValidMathJson } from "./types.js";
10
+ export { getAtPath, setAtPath, swapOperator, withArgAdded, withArgRemoved, wrapAtPath, } from "./data-model/path.js";
11
+ export { resolveSymbol, searchSymbols } from "./data-model/symbols.js";
12
+ export { allConstructs, argBounds, categoryLabel, defaultArgsFor, displayName, getConstruct, getNotation, isComparisonLike, operatorGlyph, searchConstructs, swapNeedsArgReversal, swapTargets, CATEGORY_LABELS, } from "./registry/registry.js";
13
+ export type { NotationSpec } from "./registry/notation.js";