{
    "exportFormatVersion": 2,
    "exportTime": "2026-05-03 22:42:44",
    "containerVersion": {
        "path": "accounts/6348131723/containers/250151754/versions/0",
        "accountId": "6348131723",
        "containerId": "250151754",
        "containerVersionId": "0",
        "container": {
            "path": "accounts/6348131723/containers/250151754",
            "accountId": "6348131723",
            "containerId": "250151754",
            "name": "www.gtmlowkitexample.com",
            "publicId": "GTM-WLMZJH5V",
            "usageContext": [
                "WEB"
            ],
            "fingerprint": "1776851636485",
            "tagManagerUrl": "https://tagmanager.google.com/#/container/accounts/6348131723/containers/250151754/workspaces?apiLink=container",
            "features": {
                "supportUserPermissions": true,
                "supportEnvironments": true,
                "supportWorkspaces": true,
                "supportGtagConfigs": false,
                "supportBuiltInVariables": true,
                "supportClients": false,
                "supportFolders": true,
                "supportTags": true,
                "supportTemplates": true,
                "supportTriggers": true,
                "supportVariables": true,
                "supportVersions": true,
                "supportZones": true,
                "supportTransformations": false
            },
            "tagIds": [
                "GTM-WLMZJH5V"
            ]
        },
        "builtInVariable": [
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "type": "PAGE_URL",
                "name": "Page URL"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "type": "PAGE_HOSTNAME",
                "name": "Page Hostname"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "type": "PAGE_PATH",
                "name": "Page Path"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "type": "REFERRER",
                "name": "Referrer"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "type": "EVENT",
                "name": "Event"
            }
        ],
        "fingerprint": "1777848164568",
        "tagManagerUrl": "https://tagmanager.google.com/#/versions/accounts/6348131723/containers/250151754/versions/0?apiLink=version",
        "customTemplate": [
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "templateId": "3",
                "name": "(Mock) Ecommerce Items",
                "fingerprint": "1777847758858",
                "templateData": "___INFO___\n\n{\n  \"type\": \"MACRO\",\n  \"id\": \"cvt_temp_public_id\",\n  \"version\": 1,\n  \"securityGroups\": [],\n  \"displayName\": \"(Mock) Ecommerce Items\",\n  \"description\": \"Generates a static array of mock e-commerce items for testing the ggLowCodeGTMKit variable chains.\",\n  \"containerContexts\": [\n    \"WEB\"\n  ]\n}\n\n\n___TEMPLATE_PARAMETERS___\n\n[]\n\n\n___SANDBOXED_JS_FOR_WEB_TEMPLATE___\n\nreturn [\n      { item_id: 'SKU_123', item_name: 'Super Widget', price: 9.97, item_category: 'Widget', quantity:4 },\n      { item_id: 'SKU_456', item_name: 'Mega Widget', price: 19.96, item_category: 'Widget', quantity:1  },\n      { item_id: 'SKU_789', item_name: 'Ultra Widget', price: 29.19, quantity:2  }\n    ];\n\n\n___TESTS___\n\nscenarios: []\n\n\n___NOTES___\n\n\n\n\n"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "templateId": "4",
                "name": "(Mock) Raw DOM Price",
                "fingerprint": "1776852953292",
                "templateData": "___INFO___\n\n{\n  \"type\": \"MACRO\",\n  \"id\": \"cvt_temp_public_id\",\n  \"version\": 1,\n  \"securityGroups\": [],\n  \"displayName\": \"(Mock) Raw DOM Price\",\n  \"description\": \"\",\n  \"containerContexts\": [\n    \"WEB\"\n  ]\n}\n\n\n___TEMPLATE_PARAMETERS___\n\n[]\n\n\n___SANDBOXED_JS_FOR_WEB_TEMPLATE___\n\nreturn \"The price is \\n  <span>€ 1,299.99</span>\\n   \";\n\n\n___TESTS___\n\nscenarios: []\n\n\n___NOTES___\n\n\n\n\n"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "templateId": "6",
                "name": "sumProductOfProperties",
                "fingerprint": "1776852877102",
                "templateData": "___INFO___\n\n{\n  \"type\": \"MACRO\",\n  \"id\": \"cvt_temp_public_id\",\n  \"version\": 1,\n  \"securityGroups\": [],\n  \"displayName\": \"sumProductOfProperties\",\n  \"description\": \"Multiplies two numeric properties for each object in an \\u003cem\\u003earray\\u003c/em\\u003e and sums the results, ignoring non-numeric values. Useful for calculating totals like price × quantity across object collections.\",\n  \"containerContexts\": [\n    \"WEB\"\n  ]\n}\n\n\n___TEMPLATE_PARAMETERS___\n\n[\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"paramSection\",\n    \"displayName\": \"𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯\",\n    \"groupStyle\": \"NO_ZIPPY\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"src\",\n        \"displayName\": \"Input Array\",\n        \"simpleValueType\": true,\n        \"help\": \"💾   The array of objects.\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eArray\\u003c/strong\\u003e\"\n      },\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"pr1\",\n        \"displayName\": \"First Property Name\",\n        \"simpleValueType\": true,\n        \"help\": \"💾   The name of the first numeric property to multiply (e.g., \\\"price\\\").\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eString\\u003c/strong\\u003e\"\n      },\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"pr2\",\n        \"displayName\": \"Second Property Name\",\n        \"simpleValueType\": true,\n        \"help\": \"💾   The name of the second numeric property to multiply (e.g., \\\"quantity\\\").\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eString\\u003c/strong\\u003e\"\n      }\n    ],\n    \"help\": \"Multiplies two numeric properties for each object and sums the results. Useful for price × quantity totals.\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eSum of price × quantity\\u003c/em\\u003e***\\u003cbr\\u003eInput Array: \\u003cstrong\\u003e[\\u003c/strong\\u003e\\u003cbr\\u003e{price: \\u003cstrong\\u003e10, quantity: 2},\\u003c/strong\\u003e\\u003cbr\\u003e{price: \\u003cstrong\\u003e20, quantity: 3},\\u003c/strong\\u003e\\u003cbr\\u003e{price: \\u003cstrong\\u003e30, quantity: 1}\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cstrong\\u003e]\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e110\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eEmpty array returns 0\\u003c/em\\u003e***\\u003cbr\\u003eInput Array: \\u003cstrong\\u003e[]\\u003c/strong\\u003e\\u003cbr\\u003eFirst Property Name: \\u003cstrong\\u003eprice\\u003c/strong\\u003e\\u003cbr\\u003eSecond Property Name: \\u003cstrong\\u003equantity\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e0\\u003c/strong\\u003e\"\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Input Setup\",\n    \"displayName\": \"Input Setup\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"pre\",\n        \"displayName\": \"Input Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional pre-processing function applied to the input before internal logic (e.g., convert object to string, normalize case). Internal transformations such as case handling will still apply afterward.\"\n      }\n    ]\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Result Handling\",\n    \"displayName\": \"Result Handling\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"out\",\n        \"displayName\": \"Output Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional function to apply to the result before returning it (e.g., str \\u003d\\u003e str + \\u0027 €\\u0027, val \\u003d\\u003e val !\\u003d\\u003d undefined for boolean conversion). Useful for chaining transformations on the output.\"\n      }\n    ]\n  }\n]\n\n\n___SANDBOXED_JS_FOR_WEB_TEMPLATE___\n\n/**\n * Sums the product of two numeric properties across all objects in an array.\n *\n * @param {Array} data.src - The array of objects.\n * @param {string} data.pr1 - The first numeric property to multiply (e.g., \"price\").\n * @param {string} data.pr2 - The second numeric property to multiply (e.g., \"quantity\").\n * @param {Function|string} [data.out] - Optional output handler.\n *\n * @returns {number} The sum of (value1 * value2) for each item. Returns 0 if array is invalid.\n *\n * @framework ggLowCodeGTMKit\n */\nconst getType = require('getType');\nconst makeNumber = require('makeNumber');\n\nconst sumProductOfProperties = function(arr, prop1, prop2) {\n  if (getType(arr) !== 'array' || typeof prop1 !== 'string' || typeof prop2 !== 'string') {\n    return 0;\n  }\n\n  let sum = 0;\n  for (let i = 0; i < arr.length; i++) {\n    const v1 = makeNumber(arr[i][prop1]);\n    const v2 = makeNumber(arr[i][prop2]);\n    if (typeof v1 === 'number' && v1 === v1 && typeof v2 === 'number' && v2 === v2) {\n      sum += v1 * v2;\n    }\n  }\n  return sum;\n};\n\nconst safeFunction = fn => typeof fn === 'function' ? fn : x => x;\nconst out = safeFunction(data.out);\n\n// ===============================================================================\n// sumProductOfProperties - Direct mode\n// ===============================================================================\nconst applyCast = (castFn, value) => safeFunction(castFn)(value);\nconst value = applyCast(data.pre, data.src);\nreturn out(sumProductOfProperties(value, data.pr1, data.pr2));\n\n// ===============================================================================\n// sumProductOfProperties - Apply mode\n// ===============================================================================\n/*\nreturn function(arr, prop1, prop2) {\n  prop1 = data.rp1 ? data.pr1 : prop1;\n  prop2 = data.rp2 ? data.pr2 : prop2;\n  return out(sumProductOfProperties(arr, prop1, prop2));\n};\n*/\n\n\n___TESTS___\n\nscenarios:\n- name: '[example] Sum of price × quantity'\n  code: |-\n    /* @display\n    Input Array: [\n        {price: 10, quantity: 2},\n        {price: 20, quantity: 3},\n        {price: 30, quantity: 1}\n    ]\n    @output\n    110\n    */\n    const src = [\n        {price: 10, quantity: 2},\n        {price: 20, quantity: 3},\n        {price: 30, quantity: 1}\n    ];\n    const mockData = {src: src, pr1: 'price', pr2: 'quantity'};\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, 'price', 'quantity');\n    }\n    assertThat(variableResult).isEqualTo(110); // (10*2) + (20*3) + (30*1)\n- name: Skips invalid (non-numeric) values\n  code: |-\n    const src = [\n        {price: 10, quantity: 2},\n        {price: 'invalid', quantity: 3},\n        {price: 20, quantity: null},\n        {price: 30, quantity: 2}\n    ];\n    const mockData = {src: src, pr1: 'price', pr2: 'quantity'};\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, 'price', 'quantity');\n    }\n    assertThat(variableResult).isEqualTo(80); // (10*2) + (30*2)\n- name: Returns 0 for invalid array input\n  code: |-\n    const mockData = {src: null, pr1: 'price', pr2: 'quantity'};\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(null, 'price', 'quantity');\n    }\n    assertThat(variableResult).isEqualTo(0);\n- name: Returns 0 for invalid property names\n  code: |-\n    const src = [\n        {price: 10, quantity: 2},\n        {price: 20, quantity: 3},\n        {price: 30, quantity: 1}\n    ];\n    const mockData = {src: src, pr1: 123, pr2: 'quantity'};\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, 123, 'quantity');\n    }\n    assertThat(variableResult).isEqualTo(0);\n- name: '[example] Empty array returns 0'\n  code: |-\n    /* @display\n    Input Array: []\n    First Property Name: price\n    Second Property Name: quantity\n    @output\n    0\n    */\n    const mockData = {src: [], pr1: 'price', pr2: 'quantity'};\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func([], 'price', 'quantity');\n    }\n    assertThat(variableResult).isEqualTo(0);\nsetup: |-\n  // Change this to switch test mode ('direct', or 'apply')\n  const mode = 'direct';\n  // ===================================================================================================\n  // Derived flags\n  // ===================================================================================================\n  const isDirectMode = mode === 'direct';\n  const isApplyMode = mode === 'apply';\n\n\n___NOTES___\n\nggLowCodeGTMKit - The Composable Variable Framework\nVersion: 0.0.1\nLicense: MIT\n\n📚 Documentation: https://library.youdontknowga.com/\n🐙 GitHub: https://github.com/youdontknowga/ggLowCodeGTMKit\nCreated by Gwennaël Grandmougin\n\n\n"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "templateId": "12",
                "name": "itemPluckProperty",
                "fingerprint": "1776857811423",
                "templateData": "___INFO___\n\n{\n  \"type\": \"MACRO\",\n  \"id\": \"cvt_temp_public_id\",\n  \"version\": 1,\n  \"securityGroups\": [],\n  \"displayName\": \"itemPluckProperty\",\n  \"description\": \"Extracts values from an array of \\u003cem\\u003eobjects\\u003c/em\\u003e (items) by property name, filtering out \\u003cem\\u003enull/undefined\\u003c/em\\u003e values and returning a clean list.\",\n  \"containerContexts\": [\n    \"WEB\"\n  ]\n}\n\n\n___TEMPLATE_PARAMETERS___\n\n[\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"paramSection\",\n    \"displayName\": \"𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯\",\n    \"groupStyle\": \"NO_ZIPPY\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"src\",\n        \"displayName\": \"Items (Array of Objects with shared schema)\",\n        \"simpleValueType\": true,\n        \"help\": \"💾   Array of objects to extract values from.\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eArray\\u003c/strong\\u003e\"\n      },\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"prp\",\n        \"displayName\": \"Property Name\",\n        \"simpleValueType\": true,\n        \"help\": \"💾   Property name to extract from each object.\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eString\\u003c/strong\\u003e\"\n      }\n    ],\n    \"help\": \"Extracts values from an array of objects by property name, filtering out null/undefined values.\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003ePluck property values\\u003c/em\\u003e***\\u003cbr\\u003eItems (Array of Objects with shared schema): \\u003cstrong\\u003e[{name: \\\"John\\\", age: 30}, {name: \\\"Jane\\\", age: 25}, {name: \\\"Bob\\\", age: 35}]\\u003c/strong\\u003e\\u003cbr\\u003eProperty Name: \\u003cstrong\\u003ename\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e[\\\"John\\\", \\\"Jane\\\", \\\"Bob\\\"]\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eEmpty array returns empty\\u003c/em\\u003e***\\u003cbr\\u003eItems (Array of Objects with shared schema): \\u003cstrong\\u003e[]\\u003c/strong\\u003e\\u003cbr\\u003eProperty Name: \\u003cstrong\\u003ename\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e[]\\u003c/strong\\u003e\"\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Input Setup\",\n    \"displayName\": \"Input Setup\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"pre\",\n        \"displayName\": \"Input Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional pre-processing function applied to the input before internal logic (e.g., convert object to string, normalize case). Internal transformations such as case handling will still apply afterward.\"\n      }\n    ]\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Result Handling\",\n    \"displayName\": \"Result Handling\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"out\",\n        \"displayName\": \"Output Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional function to apply to the result before returning it (e.g., str \\u003d\\u003e str + \\u0027 €\\u0027, val \\u003d\\u003e val !\\u003d\\u003d undefined for boolean conversion). Useful for chaining transformations on the output.\"\n      }\n    ]\n  }\n]\n\n\n___SANDBOXED_JS_FOR_WEB_TEMPLATE___\n\n/**\n * Extracts values from an array of objects by property name, filtering out null/undefined values.\n *\n * @param {Array} data.src - Array of objects to extract values from.\n * @param {string} data.prp - Property name to extract from each object.\n * @param {Function|string} [data.out] - Optional output handler: function to transform result or string with format.\n *\n * Direct-mode specific parameters:\n * @param {Function} [data.pre] - Optional pre-processor function to transform src before processing.\n * \n * @returns {Array} Array of extracted values with null/undefined filtered out.\n *\n * @framework ggLowCodeGTMKit\n */\nconst itemPluckProperty = function(items, property) {\n    const values = items.map(function(item) {\n        return item && item[property] !== undefined ? item[property] : null;\n    }).filter(value => value != null); // null == undefined returns true\n    return values;\n};\nconst safeFunction = fn => typeof fn === 'function' ? fn : x => x;\nconst out = safeFunction(data.out);\n// ===============================================================================\n// itemPluckProperty - Direct mode\n// ===============================================================================\nconst applyCast = (castFn, value) => safeFunction(castFn)(value);\nconst value = applyCast(data.pre, data.src);\nreturn out(itemPluckProperty(value, data.prp));\n// ===============================================================================\n// itemPluckProperty(...) – Apply Mode\n// ===============================================================================\n/*\nreturn function(value, property) {\n   property = data.rp1 ? data.prp : property;\n   return out(itemPluckProperty(value, property));\n};\n*/\n\n\n___TESTS___\n\nscenarios:\n- name: '[example] Pluck property values'\n  code: |\n    /* @display\n    Items (Array of Objects with shared schema): [{name: \"John\", age: 30}, {name: \"Jane\", age: 25}, {name: \"Bob\", age: 35}]\n    Property Name: name\n    @output\n    [\"John\", \"Jane\", \"Bob\"]\n    */\n    const src = [{name: \"John\", age: 30}, {name: \"Jane\", age: 25}, {name: \"Bob\", age: 35}];\n    const prp = \"name\";\n    const mockData = {\n        src: src,\n        prp: prp,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, prp);\n    }\n    assertThat(variableResult).isEqualTo([\"John\", \"Jane\", \"Bob\"]);\n- name: Array with some objects missing the property - filters out undefined values\n  code: |-\n    const src = [{id: 1, value: \"A\"}, {id: 2}, {id: 3, value: \"C\"}];\n    const prp = \"value\";\n    const mockData = {\n        src: src,\n        prp: prp,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, prp);\n    }\n    assertThat(variableResult).isEqualTo([\"A\", \"C\"]);\n- name: Array with null values in property - filters out null values\n  code: |-\n    const src = [{name: \"Alice\"}, {name: null}, {name: \"Charlie\"}];\n    const prp = \"name\";\n    const mockData = {\n        src: src,\n        prp: prp,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, prp);\n    }\n    assertThat(variableResult).isEqualTo([\"Alice\", \"Charlie\"]);\n- name: '[example] Empty array returns empty'\n  code: |-\n    /* @display\n    Items (Array of Objects with shared schema): []\n    Property Name: name\n    @output\n    []\n    */\n    const src = [];\n    const prp = \"name\";\n    const mockData = {\n        src: src,\n        prp: prp,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, prp);\n    }\n    assertThat(variableResult).isEqualTo([]);\n- name: Array with nested property access - extracts nested values\n  code: |-\n    const src = [{user: {id: 1}}, {user: {id: 2}}, {user: {id: 3}}];\n    const prp = \"user\";\n    const mockData = {\n        src: src,\n        prp: prp,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, prp);\n    }\n    assertThat(variableResult).isEqualTo([{id: 1}, {id: 2}, {id: 3}]);\nsetup: |-\n  // Change this to switch test mode ('direct', or 'apply')\n  const mode = 'direct';\n  // ===================================================================================================\n  // Derived flags\n  // ===================================================================================================\n  const isDirectMode = mode === 'direct';\n  const isApplyMode = mode === 'apply';\n\n\n___NOTES___\n\nggLowCodeGTMKit - The Composable Variable Framework\nVersion: 0.0.1\nLicense: MIT\n\n📚 Documentation: https://library.youdontknowga.com/\n🐙 GitHub: https://github.com/youdontknowga/ggLowCodeGTMKit\nCreated by Gwennaël Grandmougin\n\n\n"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "templateId": "13",
                "name": "join(...)",
                "fingerprint": "1776857831524",
                "templateData": "___INFO___\n\n{\n  \"type\": \"MACRO\",\n  \"id\": \"cvt_temp_public_id\",\n  \"version\": 1,\n  \"securityGroups\": [],\n  \"displayName\": \"join(...)\",\n  \"description\": \"Joins the elements of an \\u003cem\\u003earray\\u003c/em\\u003e into a single string, with an optional separator between each element.\",\n  \"containerContexts\": [\n    \"WEB\"\n  ]\n}\n\n\n___TEMPLATE_PARAMETERS___\n\n[\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"paramSection\",\n    \"displayName\": \"𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯\",\n    \"groupStyle\": \"NO_ZIPPY\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"sep\",\n        \"displayName\": \"Separator (use {{space}} for space, blank for no separator)\",\n        \"simpleValueType\": true,\n        \"defaultValue\": \",\",\n        \"help\": \"💾 The separator string to place between array elements. Spaces are trimmed in GTM fields, so use \\u003ccode\\u003e{{space}}\\u003c/code\\u003e to represent a space character.\\u003cbr\\u003e\\u003cbr\\u003eSupported formats:\\u003cbr\\u003e\\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eString\\u003c/strong\\u003e (use {{space}} for space)\\u003cbr\\u003e\\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eEmpty string\\u003c/strong\\u003e (no separator)\",\n        \"enablingConditions\": [\n          {\n            \"paramName\": \"rp1\",\n            \"paramValue\": true,\n            \"type\": \"NOT_EQUALS\"\n          }\n        ]\n      },\n      {\n        \"type\": \"GROUP\",\n        \"name\": \"Applied Function Parameters\",\n        \"displayName\": \"𝘈𝘱𝘱𝘭𝘪𝘦𝘥 𝘍𝘶𝘯𝘤𝘵𝘪𝘰𝘯 𝘗𝘢𝘳𝘢𝘮𝘦𝘵𝘦𝘳𝘴\",\n        \"groupStyle\": \"NO_ZIPPY\",\n        \"subParams\": [],\n        \"help\": \"Runtime parameters supplied by the function chain. These fields show what will be passed as arguments, not what is configured in this template.\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eJoin with comma\\u003c/em\\u003e***\\u003cbr\\u003eInput: \\u003cstrong\\u003earr: [\\u0027apple\\u0027, \\u0027banana\\u0027, \\u0027orange\\u0027]\\u003cbr\\u003eSeparator (use {{space}} for space, blank for no separator): ,\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003eapple,banana,orange\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eNon-array returns undefined\\u003c/em\\u003e***\\u003cbr\\u003eInput: \\u003cstrong\\u003earr: not an array\\u003cbr\\u003eSeparator (use {{space}} for space, blank for no separator): ,\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003eundefined\\u003c/strong\\u003e\"\n      },\n      {\n        \"type\": \"LABEL\",\n        \"name\": \"Chained Parameters\",\n        \"displayName\": \"🔗 Array to Join: chained callback parameter\"\n      }\n    ],\n    \"help\": \"Joins the elements of an \\u003cem\\u003earray\\u003c/em\\u003e into a single string, with an optional separator between each element.\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eJoin with comma\\u003c/em\\u003e***\\u003cbr\\u003earr: \\u003cstrong\\u003e[\\u0027apple\\u0027, \\u0027banana\\u0027, \\u0027orange\\u0027]\\u003c/strong\\u003e\\u003cbr\\u003eSeparator (use {{space}} for space, blank for no separator): \\u003cstrong\\u003e,\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003eapple,banana,orange\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eNon-array returns undefined\\u003c/em\\u003e***\\u003cbr\\u003earr: \\u003cstrong\\u003enot an array\\u003c/strong\\u003e\\u003cbr\\u003eSeparator (use {{space}} for space, blank for no separator): \\u003cstrong\\u003e,\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003eundefined\\u003c/strong\\u003e\"\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Result Handling\",\n    \"displayName\": \"Result Handling\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"out\",\n        \"displayName\": \"Output Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional function to apply to the joined string before returning it (e.g., \\u003ccode\\u003estr \\u003d\\u003e str.toUpperCase()\\u003c/code\\u003e, \\u003ccode\\u003estr \\u003d\\u003e str.trim()\\u003c/code\\u003e). Useful for chaining transformations on the output.\"\n      }\n    ]\n  }\n]\n\n\n___SANDBOXED_JS_FOR_WEB_TEMPLATE___\n\n/**\n * Joins the elements of an array into a single string, with an optional separator between each element.\n *\n * @param {Array} data.arr - The array of elements to join.\n * @param {string} data.sep - The separator to use between elements.\n * @param {Function|string} [data.out] - Optional output handler: function to transform result or string with format.\n *\n * Direct-mode specific parameters:\n * @param {Function} [data.pre] - Optional pre-processor function to transform arr before joining.\n * \n * @returns {string|undefined} The joined string if the input is an array, or undefined if the input is not an array.\n *\n * @framework ggLowCodeGTMKit\n */\nconst getType = require('getType');\n\nconst join = function(array, separator) {\n  if (getType(array) === 'array') {\n    return array.join(separator);\n  }\n  return undefined;\n};\nconst safeFunction = fn => typeof fn === 'function' ? fn : x => x;\nconst out = safeFunction(data.out);\n// ===============================================================================\n// join - Direct mode\n// ===============================================================================\n/*\nconst applyCast = (castFn, value) => safeFunction(castFn)(value);\nconst processedArray = applyCast(data.pre, data.arr);\nreturn out(join(processedArray, data.sep));\n*/\n// ===============================================================================\n// join(...) – Apply Mode\n// ===============================================================================\nreturn function(value, separator) {\n   return out(join(value, data.sep));\n};\n\n\n___TESTS___\n\nscenarios:\n- name: '[example] Join with comma'\n  code: |-\n    /* @display\n    Array to Join: ['apple', 'banana', 'orange']\n    Separator (use {{space}} for space, blank for no separator): ,\n    @output\n    apple,banana,orange\n    */\n    const arr = ['apple', 'banana', 'orange'];\n    const sep = ',';\n    const mockData = {\n        arr: arr,\n        sep: sep,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(arr, sep);\n    }\n    assertThat(variableResult).isEqualTo('apple,banana,orange');\n- name: Test joining array with space separator\n  code: |-\n    const arr = ['Hello', 'World'];\n    const sep = ' ';\n    const mockData = {\n        arr: arr,\n        sep: sep,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(arr, sep);\n    }\n    assertThat(variableResult).isEqualTo('Hello World');\n- name: Test joining array with no separator (empty string)\n  code: |\n    const arr = ['a', 'b', 'c'];\n    const sep = '';\n    const mockData = {\n        arr: arr,\n        sep: sep,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(arr, sep);\n    }\n    assertThat(variableResult).isEqualTo('abc');\n- name: '[example] Non-array returns undefined'\n  code: |-\n    /* @display\n    Array to Join: not an array\n    Separator (use {{space}} for space, blank for no separator): ,\n    @output\n    undefined\n    */\n    const arr = 'not an array';\n    const sep = ',';\n    const mockData = {\n        arr: arr,\n        sep: sep,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(arr, sep);\n    }\n    assertThat(variableResult).isUndefined();\n- name: Test joining array with mixed types\n  code: |-\n    const arr = [1, true, 'text', null];\n    const sep = '-';\n    const mockData = {\n        arr: arr,\n        sep: sep,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(arr, sep);\n    }\n    assertThat(variableResult).isEqualTo('1-true-text-');\nsetup: |-\n  // Change this to switch test mode ('direct', 'runtime', or 'configured')\n  const mode = 'apply';\n  // ===================================================================================================\n  // Derived flags\n  // ===================================================================================================\n  const isDirectMode = mode === 'direct';\n  const isApplyMode = mode === 'apply';\n\n\n___NOTES___\n\nggLowCodeGTMKit - The Composable Variable Framework\nVersion: 0.0.1\nLicense: MIT\n\n📚 Documentation: https://library.youdontknowga.com/\n🐙 GitHub: https://github.com/youdontknowga/ggLowCodeGTMKit\nCreated by Gwennaël Grandmougin\n\n\n"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "templateId": "14",
                "name": "clamp(...)",
                "fingerprint": "1777848064068",
                "templateData": "___INFO___\n\n{\n  \"type\": \"MACRO\",\n  \"id\": \"cvt_temp_public_id\",\n  \"version\": 1,\n  \"securityGroups\": [],\n  \"displayName\": \"clamp(...)\",\n  \"description\": \"Clamps a \\u003cem\\u003enumber\\u003c/em\\u003e within a specified range defined by the min and max values, ensuring the result stays within bounds.\",\n  \"containerContexts\": [\n    \"WEB\"\n  ]\n}\n\n\n___TEMPLATE_PARAMETERS___\n\n[\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"paramSection\",\n    \"displayName\": \"𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯\",\n    \"groupStyle\": \"NO_ZIPPY\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"min\",\n        \"displayName\": \"Minimum Value\",\n        \"simpleValueType\": true,\n        \"help\": \"📉   The lower bound of the range.\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eNumber\\u003c/strong\\u003e\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eString\\u003c/strong\\u003e\",\n        \"enablingConditions\": [\n          {\n            \"paramName\": \"rp1\",\n            \"paramValue\": true,\n            \"type\": \"NOT_EQUALS\"\n          }\n        ]\n      },\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"max\",\n        \"displayName\": \"Maximum Value\",\n        \"simpleValueType\": true,\n        \"help\": \"📈   The upper bound of the range.\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eNumber\\u003c/strong\\u003e\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eString\\u003c/strong\\u003e\",\n        \"enablingConditions\": [\n          {\n            \"paramName\": \"rp2\",\n            \"paramValue\": true,\n            \"type\": \"NOT_EQUALS\"\n          }\n        ]\n      },\n      {\n        \"type\": \"GROUP\",\n        \"name\": \"Applied Function Parameters\",\n        \"displayName\": \"𝘈𝘱𝘱𝘭𝘪𝘦𝘥 𝘍𝘶𝘯𝘤𝘵𝘪𝘰𝘯 𝘗𝘢𝘳𝘢𝘮𝘦𝘵𝘦𝘳𝘴\",\n        \"groupStyle\": \"NO_ZIPPY\",\n        \"subParams\": [],\n        \"help\": \"Runtime parameters supplied by the function chain. These fields show what will be passed as arguments, not what is configured in this template.\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eWithin range unchanged\\u003c/em\\u003e***\\u003cbr\\u003eInput: \\u003cstrong\\u003esrc: 5\\u003cbr\\u003eMinimum Value: 0\\u003cbr\\u003eMaximum Value: 10\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e5\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eBelow minimum clamped up\\u003c/em\\u003e***\\u003cbr\\u003eInput: \\u003cstrong\\u003esrc: -5\\u003cbr\\u003eMinimum Value: 0\\u003cbr\\u003eMaximum Value: 10\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e0\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eAbove maximum clamped down\\u003c/em\\u003e***\\u003cbr\\u003eInput: \\u003cstrong\\u003esrc: 15\\u003cbr\\u003eMinimum Value: 0\\u003cbr\\u003eMaximum Value: 10\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e10\\u003c/strong\\u003e\"\n      },\n      {\n        \"type\": \"LABEL\",\n        \"name\": \"Chained Parameters\",\n        \"displayName\": \"🔗 Chained callback parameter: Value To Clamp\"\n      },\n      {\n        \"type\": \"GROUP\",\n        \"name\": \"Advanced Settings\",\n        \"displayName\": \"Advanced Settings\",\n        \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n        \"subParams\": [\n          {\n            \"type\": \"CHECKBOX\",\n            \"name\": \"rp1\",\n            \"checkboxText\": \"⚡Use runtime parameter for: Minimum Value\",\n            \"simpleValueType\": true\n          },\n          {\n            \"type\": \"CHECKBOX\",\n            \"name\": \"rp2\",\n            \"checkboxText\": \"⚡Use runtime parameter for: Maximum Value\",\n            \"simpleValueType\": true\n          }\n        ]\n      }\n    ],\n    \"help\": \"Clamps a \\u003cem\\u003enumber\\u003c/em\\u003e within a specified range defined by min and max values.\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eWithin range unchanged\\u003c/em\\u003e***\\u003cbr\\u003eValue to Process: \\u003cstrong\\u003e5\\u003c/strong\\u003e\\u003cbr\\u003eMinimum Value: \\u003cstrong\\u003e0\\u003c/strong\\u003e\\u003cbr\\u003eMaximum Value: \\u003cstrong\\u003e10\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e5\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eBelow minimum clamped up\\u003c/em\\u003e***\\u003cbr\\u003eValue to Process: \\u003cstrong\\u003e-5\\u003c/strong\\u003e\\u003cbr\\u003eMinimum Value: \\u003cstrong\\u003e0\\u003c/strong\\u003e\\u003cbr\\u003eMaximum Value: \\u003cstrong\\u003e10\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e0\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eAbove maximum clamped down\\u003c/em\\u003e***\\u003cbr\\u003eValue to Process: \\u003cstrong\\u003e15\\u003c/strong\\u003e\\u003cbr\\u003eMinimum Value: \\u003cstrong\\u003e0\\u003c/strong\\u003e\\u003cbr\\u003eMaximum Value: \\u003cstrong\\u003e10\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e10\\u003c/strong\\u003e\"\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Result Handling\",\n    \"displayName\": \"Result Handling\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"out\",\n        \"displayName\": \"Output Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional function to apply to the result before returning it (e.g., str \\u003d\\u003e str + \\u0027 €\\u0027, val \\u003d\\u003e val !\\u003d\\u003d undefined for boolean conversion). Useful for chaining transformations on the output.\"\n      }\n    ]\n  }\n]\n\n\n___SANDBOXED_JS_FOR_WEB_TEMPLATE___\n\n/**\n* Clamps a number within a specified range defined by the min and max values.\n* \n* @param {number} data.src - The number to be clamped.\n* @param {number|string} data.min - The lower bound of the range.\n* @param {number|string} data.max - The upper bound of the range.\n* @param {Function|string} [data.out] - Optional output handler: function to transform result or string with format.\n*\n* Direct-mode specific parameters:\n* @param {Function} [data.pre] - Optional pre-processor function to transform src before clamping.\n* \n* @returns {number} The clamped number, constrained between min and max.\n*\n* @framework ggLowCodeGTMKit\n*/\nconst Math = require('Math');\nconst makeNumber = require('makeNumber');\n\nconst clamp = function(value, min, max) {\n   const val = makeNumber(value);\n   const minVal = makeNumber(min);\n   const maxVal = makeNumber(max);\n   return Math.max(minVal, Math.min(val, maxVal));\n};\n\nconst safeFunction = fn => typeof fn === 'function' ? fn : x => x;\nconst out = safeFunction(data.out);\n\n// ===============================================================================\n// clamp - Direct mode\n// ===============================================================================\n/*\nconst applyCast = (castFn, value) => safeFunction(castFn)(value);\nconst value = applyCast(data.pre, data.src);\nreturn out(clamp(value, data.min, data.max));\n*/\n// ===============================================================================\n// clamp(...) – Apply Mode:\n// ===============================================================================\nreturn function(value, min, max) {\n  min = data.rp1 ? min : data.min;\n  max = data.rp2 ? max : data.max;\n  return out(clamp(value, min, max));\n};\n\n\n___TESTS___\n\nscenarios:\n- name: '[example] Within range unchanged'\n  code: |-\n    /* @display\n    Value to Process: 5\n    Minimum Value: 0\n    Maximum Value: 10\n    @output\n    5\n    */\n    const src = 5;\n    const min = 0;\n    const max = 10;\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(5);\n- name: '[example] Below minimum clamped up'\n  code: |-\n    /* @display\n    Value to Process: -5\n    Minimum Value: 0\n    Maximum Value: 10\n    @output\n    0\n    */\n    const src = -5;\n    const min = 0;\n    const max = 10;\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(0);\n- name: '[example] Above maximum clamped down'\n  code: |-\n    /* @display\n    Value to Process: 15\n    Minimum Value: 0\n    Maximum Value: 10\n    @output\n    10\n    */\n    const src = 15;\n    const min = 0;\n    const max = 10;\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(10);\n- name: Clamp value at minimum boundary\n  code: |-\n    const src = 0;\n    const min = 0;\n    const max = 10;\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(0);\n- name: Clamp value at maximum boundary\n  code: |-\n    const src = 10;\n    const min = 0;\n    const max = 10;\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(10);\n- name: Clamp negative range\n  code: |-\n    const src = -5;\n    const min = -10;\n    const max = -1;\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(-5);\n- name: Clamp decimal value\n  code: |-\n    const src = 5.7;\n    const min = 0;\n    const max = 10;\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(5.7);\n- name: String number values\n  code: |-\n    const src = '7';\n    const min = '0';\n    const max = '10';\n    const mockData = {\n        src: src,\n        min: min,\n        max: max,\n        rp1: false,\n        rp2: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src, min, max);\n    }\n    assertThat(variableResult).isEqualTo(7);\nsetup: |-\n  // Change this to switch test mode ('direct', 'runtime', or 'configured')\n  const mode = 'apply';\n  // ===================================================================================================\n  // Derived flags\n  // ===================================================================================================\n  const isDirectMode = mode === 'direct';\n  const isApplyMode = mode === 'apply';\n\n\n___NOTES___\n\nggLowCodeGTMKit - The Composable Variable Framework\nVersion: 0.0.1\nLicense: MIT\n\n📚 Documentation: https://youdontknowga.com/\n🐙 GitHub: https://github.com/youdontknowga/ggLowCodeGTMKit\nCreated by Gwennaël Grandmougin\n\n\n"
            },
            {
                "accountId": "6348131723",
                "containerId": "250151754",
                "templateId": "15",
                "name": "parseCurrency",
                "fingerprint": "1777848115510",
                "templateData": "___INFO___\n\n{\n  \"type\": \"MACRO\",\n  \"id\": \"cvt_temp_public_id\",\n  \"version\": 1,\n  \"securityGroups\": [],\n  \"displayName\": \"parseCurrency\",\n  \"description\": \"Extracts and parses a valid number from a formatted currency string, handling thousand separators and decimal points.\",\n  \"containerContexts\": [\n    \"WEB\"\n  ]\n}\n\n\n___TEMPLATE_PARAMETERS___\n\n[\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"paramSection\",\n    \"displayName\": \"𝘋𝘦𝘴𝘤𝘳𝘪𝘱𝘵𝘪𝘰𝘯\",\n    \"groupStyle\": \"NO_ZIPPY\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"src\",\n        \"displayName\": \"String To Process\",\n        \"simpleValueType\": true,\n        \"help\": \"💾   The currency string to parse.\\u003cbr\\u003e\\u003cbr\\u003e  Supported formats:\\u003cbr\\u003e \\u0026nbsp;\\u0026nbsp;✓ \\u003cstrong\\u003eString\\u003c/strong\\u003e\"\n      },\n      {\n        \"type\": \"SELECT\",\n        \"name\": \"dec\",\n        \"displayName\": \"Decimal Separator\",\n        \"macrosInSelect\": false,\n        \"selectItems\": [\n          {\n            \"value\": \".\",\n            \"displayValue\": \". (Dot)\"\n          },\n          {\n            \"value\": \",\",\n            \"displayValue\": \", (Comma)\"\n          }\n        ],\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ The decimal separator used in the source string. All other non-digit characters (currency symbols, spaces, thousand separators) will be stripped automatically.\"\n      }\n    ],\n    \"help\": \"Extracts and parses a valid number from a formatted currency string by stripping out currency symbols, thousand separators, and whitespace.\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eUS Format (Dot)\\u003c/em\\u003e***\\u003cbr\\u003eString To Process: \\u003cstrong\\u003e$ 1,234.56 USD\\u003c/strong\\u003e\\u003cbr\\u003eDecimal Separator: \\u003cstrong\\u003e.\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e1234.56\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eEU Format (Comma)\\u003c/em\\u003e***\\u003cbr\\u003eString To Process: \\u003cstrong\\u003e1 234,56 €\\u003c/strong\\u003e\\u003cbr\\u003eDecimal Separator: \\u003cstrong\\u003e,\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e1234.56\\u003c/strong\\u003e\\u003cbr\\u003e\\u003cbr\\u003e*** \\u003cem\\u003eGerman Format (Comma)\\u003c/em\\u003e***\\u003cbr\\u003eString To Process: \\u003cstrong\\u003e1.234,56 €\\u003c/strong\\u003e\\u003cbr\\u003eDecimal Separator: \\u003cstrong\\u003e,\\u003c/strong\\u003e\\u003cbr\\u003e↪️ Output: \\u003cstrong\\u003e1234.56\\u003c/strong\\u003e\"\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Input Setup\",\n    \"displayName\": \"Input Setup\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"pre\",\n        \"displayName\": \"Input Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional pre-processing function applied to the input before internal logic (e.g., convert object to string, normalize case). Internal transformations such as case handling will still apply afterward.\"\n      }\n    ]\n  },\n  {\n    \"type\": \"GROUP\",\n    \"name\": \"Result Handling\",\n    \"displayName\": \"Result Handling\",\n    \"groupStyle\": \"ZIPPY_OPEN_ON_PARAM\",\n    \"subParams\": [\n      {\n        \"type\": \"TEXT\",\n        \"name\": \"out\",\n        \"displayName\": \"Output Function (optional)\",\n        \"simpleValueType\": true,\n        \"help\": \"⚙️ Optional function to apply to the result before returning it (e.g., str \\u003d\\u003e str + \\u0027 €\\u0027, val \\u003d\\u003e val !\\u003d\\u003d undefined for boolean conversion). Useful for chaining transformations on the output.\"\n      }\n    ]\n  }\n]\n\n\n___SANDBOXED_JS_FOR_WEB_TEMPLATE___\n\n/**\n* Extracts and parses a valid number from a formatted currency string.\n* \n* @param {string} data.src - The currency string to parse.\n* @param {string} data.dec - The decimal separator used ('.' or ',').\n* @param {Function|string} [data.out] - Optional output handler.\n*\n* Direct-mode specific parameters:\n* @param {Function} [data.pre] - Optional pre-processor function to transform src.\n* \n* @returns {number|undefined} The parsed number, or undefined if invalid.\n*\n* @framework ggLowCodeGTMKit\n*/\nconst makeNumber = require('makeNumber');\n\nconst parseCurrency = function(input, decimalSeparator) {\n    if (typeof input !== 'string') return undefined;\n    \n    const sep = decimalSeparator === ',' ? ',' : '.';\n    \n    let cleanString = \"\";\n    for (let i = 0; i < input.length; i++) {\n        const char = input.charAt(i);\n        if (\n            (char >= '0' && char <= '9') || \n            char === '-' || \n            char === sep\n        ) {\n            cleanString += char;\n        }\n    }\n    \n    if (cleanString === \"\") return undefined;\n    \n    if (sep === ',') {\n        let dotString = \"\";\n        for (let i = 0; i < cleanString.length; i++) {\n            dotString += cleanString.charAt(i) === ',' ? '.' : cleanString.charAt(i);\n        }\n        cleanString = dotString;\n    }\n    \n    const parsed = makeNumber(cleanString);\n    if (parsed !== parsed) return undefined; // NaN check\n    \n    return parsed;\n};\nconst safeFunction = fn => typeof fn === 'function' ? fn : x => x;\nconst out = safeFunction(data.out);\n// ===============================================================================\n// parseCurrency - Direct mode\n// ===============================================================================\nconst applyCast = (castFn, value) => safeFunction(castFn)(value);\nconst value = applyCast(data.pre, data.src);\nreturn out(parseCurrency(value, data.dec));\n// ===============================================================================\n// parseCurrency(...) – Apply Mode\n// ===============================================================================\n/*\nreturn function(value) {\n   return out(parseCurrency(value, data.dec));\n};\n*/\n\n\n___TESTS___\n\nscenarios:\n- name: '[example] US Format (Dot)'\n  code: |-\n    /* @display\n    String To Process: $ 1,234.56 USD\n    Decimal Separator: .\n    @output\n    1234.56\n    */\n    const src = '$ 1,234.56 USD';\n    const dec = '.';\n    const mockData = {\n        src: src,\n        dec: dec,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src);\n    }\n    assertThat(variableResult).isEqualTo(1234.56);\n- name: '[example] EU Format (Comma)'\n  code: |-\n    /* @display\n    String To Process: 1 234,56 €\n    Decimal Separator: ,\n    @output\n    1234.56\n    */\n    const src = '1 234,56 €';\n    const dec = ',';\n    const mockData = {\n        src: src,\n        dec: dec,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src);\n    }\n    assertThat(variableResult).isEqualTo(1234.56);\n- name: '[example] German Format (Comma)'\n  code: |-\n    /* @display\n    String To Process: 1.234,56 €\n    Decimal Separator: ,\n    @output\n    1234.56\n    */\n    const src = '1.234,56 €';\n    const dec = ',';\n    const mockData = {\n        src: src,\n        dec: dec,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src);\n    }\n    assertThat(variableResult).isEqualTo(1234.56);\n- name: Negative values\n  code: |-\n    const src = '-$45.99';\n    const dec = '.';\n    const mockData = {\n        src: src,\n        dec: dec,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src);\n    }\n    assertThat(variableResult).isEqualTo(-45.99);\n- name: No numbers returns undefined\n  code: |-\n    const src = 'Free';\n    const dec = '.';\n    const mockData = {\n        src: src,\n        dec: dec,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src);\n    }\n    assertThat(variableResult).isUndefined();\n- name: Non-string returns undefined\n  code: |-\n    const src = 1234.56;\n    const dec = '.';\n    const mockData = {\n        src: src,\n        dec: dec,\n        rp1: false\n    };\n    let variableResult;\n    if (isDirectMode) {\n        variableResult = runCode(mockData);\n    } else if (isApplyMode) {\n        const func = runCode(mockData);\n        variableResult = func(src);\n    }\n    assertThat(variableResult).isUndefined();\nsetup: |-\n  // Change this to switch test mode ('direct', or 'apply')\n  const mode = 'direct';\n  // ===================================================================================================\n  // Derived flags\n  // ===================================================================================================\n  const isDirectMode = mode === 'direct';\n  const isApplyMode = mode === 'apply';\n\n\n___NOTES___\n\nggLowCodeGTMKit - The Composable Variable Framework\nVersion: 0.0.1\nLicense: MIT\n\n📚 Documentation: https://youdontknowga.com/\n🐙 GitHub: https://github.com/youdontknowga/ggLowCodeGTMKit\nCreated by Gwennaël Grandmougin\n\n\n"
            }
        ]
    }
}