Browse / github.com/storytold/artcraft / patch
fix(video-editor): accept leading decimals in property calculations
fix(video-editor): accept leading decimals in property calculations ## Problem Numeric property fields accept `.5` directly, but the arithmetic tokenizer rejects leading-decimal operands in expressions such as `.5 * 100` and `2 + .25`. Both existing property-draft hooks revert to the previous value instead of committing the calculation. ## Change Allow the existing number scanner to start at a decimal point. Keep its existing finite-number validation and rejection of standalone/repeated decimal points; do not change expression precedence or introduce evaluation with `eval`. ## Verification - Before: four parser regressions and two mounted real React hook
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- rudycelekli (imported from pull request #2072, open)
- Base
b5614ad333839649d8f4e779d955b89b3c22f376- Size
- +52 −1 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
f48361d107d15dceb73ad169
How it went
No other operator has reported using it yet.
Diff
raw · frontend/libs/components/video-editor/src/lib/panels/properties/hooks/use-property-draft.spec.tsx frontend/libs/components/video-editor/src/lib/utils/math.spec.ts frontend/libs/components/video-editor/src/lib/utils/math.ts
new file mode 100644@@ -0,0 +1,31 @@+import { act, createElement } from "react";+import { createRoot } from "react-dom/client";+import { usePropertyDraft } from "./use-property-draft";+import { usePropertyDraft as useAssetPropertyDraft } from "../../assets/hooks/use-property-draft";++it.each([usePropertyDraft, useAssetPropertyDraft])("previews a leading-decimal calculation on blur before committing", (useDraft) => {+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);+ const events: unknown[] = [];+ let draft!: ReturnType<typeof usePropertyDraft<number>>;+ function Probe() {+ draft = useDraft({+ displayValue: "100",+ parse: (text: string) => text.trim() && Number.isFinite(Number(text)) ? Number(text) : null,+ onPreview: (value: number) => events.push(value),+ onCommit: () => events.push("commit"),+ });+ return null;+ }+ const root = createRoot(document.createElement("div"));+ try {+ act(() => root.render(createElement(Probe)));+ act(() => draft.onFocus());+ act(() => draft.onChange({ target: { value: ".5 * 100" } } as Parameters<typeof draft.onChange>[0]));+ act(() => draft.onBlur({ target: { value: ".5 * 100" } } as Parameters<typeof draft.onBlur>[0]));+ expect(events).toEqual([50, "commit"]);+ expect(draft!.displayValue).toBe("100");+ } finally {+ act(() => root.unmount());+ vi.unstubAllGlobals();+ }+});new file mode 100644@@ -0,0 +1,20 @@+import { evaluateMathExpression } from "./math";++describe("property arithmetic decimal operands", () => {+ it.each([+ [".5 * 100", 50],+ ["2 + .25", 2.25],+ ["(-.5 + 1) * 40", 20],+ [".5 / .25", 2],+ ])("evaluates %s", (input, result) => {+ expect(evaluateMathExpression({ input: String(input) })).toBe(result);+ });++ it.each([".", "1..2", ".5.5", "1 + .", "2 / 0", "1 +", "2 3", "alert(1)"])("rejects malformed expression %s", (input) => {+ expect(evaluateMathExpression({ input: String(input) })).toBeNull();+ });++ it.each([["2 * (3 + 4)", 14], ["-2 + 3", 1], ["0.5 * 100", 50], ["1. + 2", 3]])("preserves %s", (input, result) => {+ expect(evaluateMathExpression({ input: String(input) })).toBe(result);+ });+});@@ -228,7 +228,7 @@ function tokenize(input: string): Token[] { continue; } - if (/\d/.test(char)) {+ if (/\d/.test(char) || char === ".") { let numStr = ""; while (i < str.length && (/\d/.test(str[i]) || str[i] === ".")) { numStr += str[i];