{
  "name": "Selection Calculator",
  "description": "Highlight a math expression, right-click, and get the answer in a notification — evaluated safely, no eval.",
  "tags": [
    "calculator",
    "math",
    "selection",
    "right-click",
    "evaluate"
  ],
  "type": "triggered",
  "nodes": [
    {
      "id": "sticky-intro",
      "position": {
        "x": 20,
        "y": 20
      },
      "data": {
        "color": "#fff266",
        "content": "🧮 Do the math on a selection.\n\nHighlight a simple arithmetic expression (for example 12 * (3 + 4) / 2), right-click, and choose this workflow. It evaluates it safely — parsing only numbers and + - * / % ( ) — and shows the answer in a notification."
      },
      "type": "stickyNote",
      "width": 360,
      "height": 190
    },
    {
      "id": "trigger-context-menu",
      "position": {
        "x": 120,
        "y": 300
      },
      "data": {
        "instance": {
          "id": "af-base-node:trigger:contextMenu"
        },
        "label": "Right-click a selection",
        "inputs": {
          "contexts": [
            "selection"
          ]
        },
        "parameters": {
          "notes": "Adds this workflow to the right-click menu whenever you have text selected."
        }
      },
      "type": "trigger"
    },
    {
      "id": "get-selection",
      "position": {
        "x": 420,
        "y": 470
      },
      "data": {
        "instance": {
          "id": "af-base-node:basic:getSelectedTextNode"
        },
        "label": "Get selected text",
        "inputs": {
          "outputFieldName": "selection",
          "options": {
            "includeInputFields": true
          }
        },
        "parameters": {
          "notes": "Reads the full text you highlighted on the page. The context-menu event only carries a short, truncated copy, so this node gets the complete selection reliably."
        }
      },
      "type": "basic"
    },
    {
      "id": "evaluate",
      "position": {
        "x": 440,
        "y": 300
      },
      "data": {
        "instance": {
          "id": "af-base-node:basic:code"
        },
        "label": "Evaluate the expression",
        "inputs": {
          "code": "import re\nexpr = (input.get('selection', '') if isinstance(input, dict) else str(input)).strip()\ntokens = re.findall(r'\\d+\\.?\\d*|[()+\\-*/%]', expr)\nprec = {'+': 1, '-': 1, '*': 2, '/': 2, '%': 2, 'u-': 3}\nright = {'u-'}\n\ndef to_rpn(tokens):\n    out_q, ops, prev = [], [], None\n    for t in tokens:\n        if re.match(r'^\\d', t):\n            out_q.append(float(t)); prev = 'num'\n        elif t == '(':\n            ops.append(t); prev = '('\n        elif t == ')':\n            while ops and ops[-1] != '(':\n                out_q.append(ops.pop())\n            if ops:\n                ops.pop()\n            prev = 'num'\n        else:\n            op = 'u-' if (t == '-' and prev in (None, '(', 'op')) else t\n            while ops and ops[-1] != '(' and ((op not in right and prec[ops[-1]] >= prec[op]) or (op in right and prec[ops[-1]] > prec[op])):\n                out_q.append(ops.pop())\n            ops.append(op); prev = 'op'\n    while ops:\n        out_q.append(ops.pop())\n    return out_q\n\ndef evaluate(rpn):\n    st = []\n    for t in rpn:\n        if isinstance(t, float):\n            st.append(t)\n        elif t == 'u-':\n            st.append(-st.pop())\n        else:\n            b = st.pop(); a = st.pop()\n            if t == '+': st.append(a + b)\n            elif t == '-': st.append(a - b)\n            elif t == '*': st.append(a * b)\n            elif t == '/': st.append(a / b)\n            elif t == '%': st.append(a % b)\n    return st[-1]\n\ntry:\n    val = evaluate(to_rpn(tokens))\n    if val == int(val):\n        val = int(val)\n    result = str(val)\nexcept Exception:\n    result = 'Could not evaluate'\nout = {'expression': expr or '(empty)', 'result': result}\nout"
        },
        "parameters": {
          "notes": "Python: tokenizes the expression and evaluates it with a shunting-yard parser — only numbers and + - * / % ( ) are allowed, never eval. The last line is the result."
        }
      },
      "type": "basic"
    },
    {
      "id": "notify",
      "position": {
        "x": 760,
        "y": 300
      },
      "data": {
        "instance": {
          "id": "af-base-node:basic:showNotificationNode"
        },
        "label": "Show the answer",
        "inputs": {
          "title": "Calculator",
          "message": "{{ $input.result.expression }} = {{ $input.result.result }}"
        },
        "parameters": {
          "notes": "Shows the expression and its result in a notification."
        }
      },
      "type": "basic"
    }
  ],
  "edges": [
    {
      "id": "e-ctx-get-selection",
      "type": "flowEdge",
      "source": "trigger-context-menu",
      "target": "get-selection"
    },
    {
      "id": "e-trigger-eval",
      "type": "flowEdge",
      "source": "get-selection",
      "target": "evaluate"
    },
    {
      "id": "e-eval-notify",
      "type": "flowEdge",
      "source": "evaluate",
      "target": "notify"
    }
  ]
}