From bad73cbcf31872002cccb9c152e63badb7ba45e0 Mon Sep 17 00:00:00 2001 From: airslice Date: Wed, 18 Mar 2026 17:02:23 +0800 Subject: [PATCH 1/5] feat: support quoted property names in expression evaluator --- .../evaluator/simple/expression/README.md | 376 ++++++++++++++++++ .../simple/expression/expression.test.ts | 65 +++ .../expression/variableReplacer.test.ts | 107 +++++ .../simple/expression/variableReplacer.ts | 21 +- 4 files changed, 568 insertions(+), 1 deletion(-) create mode 100644 src/mantle/evaluator/simple/expression/README.md diff --git a/src/mantle/evaluator/simple/expression/README.md b/src/mantle/evaluator/simple/expression/README.md new file mode 100644 index 0000000..f32e025 --- /dev/null +++ b/src/mantle/evaluator/simple/expression/README.md @@ -0,0 +1,376 @@ +# Expression Evaluator + +A powerful expression evaluator for evaluating dynamic expressions with feature properties, supporting various operators, functions, and property access methods. + +## Table of Contents + +- [Basic Usage](#basic-usage) +- [Property Access](#property-access) +- [Operators](#operators) +- [Functions](#functions) +- [Data Types](#data-types) +- [Advanced Features](#advanced-features) + +## Basic Usage + +```typescript +import { Expression } from "./expression"; + +const feature = { + properties: { + height: 100, + name: "Building A", + }, +}; + +const expr = new Expression("${height} > 50", feature); +const result = expr.evaluate(); // true +``` + +## Property Access + +### 1. Standard Property Names (No Spaces) + +For properties without spaces or special characters, use the simple syntax: + +```typescript +${propertyName} +${height} +${temperature} +``` + +**Example:** +```typescript +const feature = { + properties: { + height: 100, + width: 50, + }, +}; + +const expr = new Expression("${height} * ${width}", feature); +expr.evaluate(); // 5000 +``` + +### 2. Quoted Property Names (With Spaces) + +**✨ NEW:** For properties with spaces or special characters, wrap the property name in quotes: + +```typescript +${"property name"} // Double quotes (recommended) +${'property name'} // Single quotes +``` + +**Example:** +```typescript +const feature = { + properties: { + "user name": "Alice", + "user score": 95, + "email@address": "alice@example.com", + "user-info:age": 25, + }, +}; + +// Access properties with spaces +const expr1 = new Expression('${"user name"}', feature); +expr1.evaluate(); // "Alice" + +// Use in conditionals +const expr2 = new Expression('${"user score"} > 90 ? "Excellent" : "Good"', feature); +expr2.evaluate(); // "Excellent" + +// Arithmetic operations +const expr3 = new Expression('${"user-info:age"} + 5', feature); +expr3.evaluate(); // 30 + +// Properties with special characters +const expr4 = new Expression('${"email@address"} !== ""', feature); +expr4.evaluate(); // true +``` + +**Supported Characters in Quoted Names:** +- Spaces: `${"user name"}` +- Hyphens: `${"user-info"}` +- Colons: `${"category:name"}` +- At signs: `${"email@domain"}` +- Any other special characters + +### 3. Special Variables + +```typescript +${id} // Access feature.id +${rootProperties} // Access entire feature.properties object +``` + +**Example:** +```typescript +const feature = { + id: "feature-123", + properties: { + height: 100, + }, +}; + +const expr = new Expression('${id}', feature); +expr.evaluate(); // "feature-123" +``` + +## Operators + +### Comparison Operators + +```typescript +${height} > 50 // Greater than +${height} >= 50 // Greater than or equal +${height} < 100 // Less than +${height} <= 100 // Less than or equal +${height} === 50 // Strict equality +${height} !== 50 // Strict inequality +${height} == 50 // Loose equality (with type coercion) +${height} != 50 // Loose inequality +``` + +### Arithmetic Operators + +```typescript +${a} + ${b} // Addition +${a} - ${b} // Subtraction +${a} * ${b} // Multiplication +${a} / ${b} // Division +${a} % ${b} // Modulo +``` + +### Logical Operators + +```typescript +${a} && ${b} // Logical AND +${a} || ${b} // Logical OR +!${a} // Logical NOT +``` + +### Conditional (Ternary) Operator + +```typescript +${condition} ? ${trueValue} : ${falseValue} +``` + +**Example:** +```typescript +const expr = new Expression('${height} > 100 ? "Tall" : "Short"', feature); +``` + +## Functions + +### Type Conversion Functions + +```typescript +Boolean(${value}) // Convert to boolean +Number(${value}) // Convert to number +String(${value}) // Convert to string +``` + +### Math Functions + +```typescript +abs(${value}) // Absolute value +sqrt(${value}) // Square root +ceil(${value}) // Round up +floor(${value}) // Round down +round(${value}) // Round to nearest integer +sin(${value}) // Sine +cos(${value}) // Cosine +tan(${value}) // Tangent +``` + +### Utility Functions + +```typescript +isNaN(${value}) // Check if Not a Number +isFinite(${value}) // Check if finite number +``` + +### Color Functions + +```typescript +color("red") // Named color +color("#ff0000") // Hex color +color("#ff0000", 0.5) // Hex color with alpha +rgb(255, 0, 0) // RGB color +rgba(255, 0, 0, 0.5) // RGBA color +hsl(0, 100, 50) // HSL color +hsla(0, 100, 50, 0.5) // HSLA color +``` + +**Example:** +```typescript +const expr = new Expression('${height} > 100 ? color("red") : color("blue")', feature); +``` + +## Data Types + +### Supported Types + +- **Numbers**: `42`, `3.14`, `Infinity`, `NaN` +- **Strings**: `"hello"`, `'world'` +- **Booleans**: `true`, `false` +- **Null**: `null` +- **Undefined**: `undefined` +- **Arrays**: `[1, 2, 3]` +- **Colors**: Result of color functions + +### Constants + +```typescript +Math.PI // 3.141592653589793 +Math.E // 2.718281828459045 +Number.POSITIVE_INFINITY +NaN +Infinity +undefined +``` + +## Advanced Features + +### String Interpolation + +Within string literals, you can interpolate property values: + +```typescript +"Hello, ${name}!" +"Height: ${height}m" +``` + +**Example:** +```typescript +const feature = { + properties: { + name: "Building A", + height: 100, + }, +}; + +const expr = new Expression('"Building: ${name}, Height: ${height}m"', feature); +expr.evaluate(); // "Building: Building A, Height: 100m" +``` + +### Array Comparisons + +The equality operators support checking if a value is in an array: + +```typescript +${value} == [1, 2, 3] // Check if value is in array +${value} != [1, 2, 3] // Check if value is not in array +``` + +**Example:** +```typescript +const feature = { + properties: { + status: "active", + }, +}; + +const expr = new Expression('${status} == ["active", "pending"]', feature); +expr.evaluate(); // true +``` + +### Defines (Variable Substitution) + +You can define placeholder values that get substituted before evaluation: + +```typescript +const defines = { + MAX_HEIGHT: "100", + MIN_HEIGHT: "10", +}; + +const expr = new Expression("${height} > ${MAX_HEIGHT}", feature, defines); +// Becomes: "${height} > 100" +``` + +### Expression Caching + +Expressions are cached automatically for better performance. To clear caches: + +```typescript +import { clearExpressionCaches } from "./expression"; + +clearExpressionCaches(expressionString, feature, defines); +``` + +## Property Name Comparison + +| Syntax | Use Case | Example | +|--------|----------|---------| +| `${name}` | Simple properties (no spaces) | `${height}`, `${temperature}` | +| `${"property name"}` | Properties with spaces/special chars | `${"user name"}`, `${"email@domain"}` | +| `${rootProperties}` | Access entire properties object | `${rootProperties["dynamic-key"]}` | + +## Complete Example + +```typescript +import { Expression } from "./expression"; + +const feature = { + id: "building-001", + properties: { + "building name": "Tower A", + "building height": 150, + "building color": "blue", + floors: 30, + status: "active", + "contact info": { + "email address": "info@tower-a.com", + }, + }, +}; + +// Simple comparison +const expr1 = new Expression('${"building height"} > 100', feature); +console.log(expr1.evaluate()); // true + +// Conditional with color +const expr2 = new Expression( + '${"building height"} > 100 ? color("red") : color("green")', + feature +); +console.log(expr2.evaluate()); // #ff0000 (red) + +// String interpolation +const expr3 = new Expression( + '"${"building name"} has ${floors} floors"', + feature +); +console.log(expr3.evaluate()); // "Tower A has 30 floors" + +// Array membership check +const expr4 = new Expression( + '${status} == ["active", "pending"]', + feature +); +console.log(expr4.evaluate()); // true +``` + +## Migration Guide + +### From Standard Syntax to Quoted Syntax + +If you have properties with spaces, you need to update your expressions: + +**Before (doesn't work):** +```typescript +${user name} // ❌ Fails: interpreted as two separate identifiers +``` + +**After (works):** +```typescript +${"user name"} // ✅ Works: quoted property name +``` + +### Best Practices + +1. **Use simple syntax** for properties without spaces: `${height}` +2. **Use quoted syntax** for properties with spaces: `${"user name"}` +3. **Prefer double quotes** for consistency: `${"property"}` instead of `${'property'}` +4. **Avoid special characters in property names** when possible to keep expressions simple diff --git a/src/mantle/evaluator/simple/expression/expression.test.ts b/src/mantle/evaluator/simple/expression/expression.test.ts index 3244faf..2233cf2 100644 --- a/src/mantle/evaluator/simple/expression/expression.test.ts +++ b/src/mantle/evaluator/simple/expression/expression.test.ts @@ -110,6 +110,71 @@ describe("Expression evaluation", () => { expression.evaluate(); }).toThrow('Unexpected function call "czm_住所"'); }); + + test("should evaluate expression with quoted property names containing spaces", () => { + const expressionString = '${"user name"}'; + const feature = { + properties: { + "user name": "Alice", + "user age": 25, + }, + } as Feature; + + const expression = new Expression(expressionString, feature); + const result = expression.evaluate(); + + expect(result).toBe("Alice"); + }); + + test("should evaluate conditional expression with quoted property names", () => { + const expressionString = '${"user score"} > 50 ? "Pass" : "Fail"'; + const feature1 = { + properties: { + "user score": 75, + }, + } as Feature; + const feature2 = { + properties: { + "user score": 30, + }, + } as Feature; + + const expression1 = new Expression(expressionString, feature1); + const expression2 = new Expression(expressionString, feature2); + + expect(expression1.evaluate()).toBe("Pass"); + expect(expression2.evaluate()).toBe("Fail"); + }); + + test("should evaluate arithmetic expression with quoted property names", () => { + const expressionString = '${"item price"} * ${"item quantity"}'; + const feature = { + properties: { + "item price": 10.5, + "item quantity": 3, + }, + } as Feature; + + const expression = new Expression(expressionString, feature); + const result = expression.evaluate(); + + expect(result).toBe(31.5); + }); + + test("should handle quoted property names with special characters", () => { + const expressionString = '${"user-info:name"} === "Bob Smith"'; + const feature = { + properties: { + "user-info:name": "Bob Smith", + "email@address": "bob@example.com", + }, + } as Feature; + + const expression = new Expression(expressionString, feature); + const result = expression.evaluate(); + + expect(result).toBe(true); + }); }); describe("expression caches", () => { diff --git a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts index acbc9ad..7289e6a 100644 --- a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts +++ b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts @@ -51,4 +51,111 @@ describe("replaceVariables", () => { const [result, _] = replaceVariables("${vari-able}"); expect(result).toBe(`czm_vari$reearth_hyphen_$able`); }); + + test("should handle property names with spaces using bracket notation with double quotes", () => { + const [, res] = replaceVariables('${$["property name"]}', { + "property name": "value with space", + normalProperty: "normal value", + }); + expect(res[0].literalValue).toBe("value with space"); + }); + + test("should handle property names with spaces using bracket notation with single quotes", () => { + const [, res] = replaceVariables("${$['user name']}", { + "user name": "John Doe", + normalProperty: "normal value", + }); + expect(res[0].literalValue).toBe("John Doe"); + }); + + test("should handle nested property names with spaces", () => { + const [, res] = replaceVariables('${$["contact info"]["email address"]}', { + "contact info": { + "email address": "john@example.com", + "phone number": "123-456-7890", + }, + }); + expect(res[0].literalValue).toBe("john@example.com"); + }); + + test("should handle array elements with property names containing spaces", () => { + const [, res] = replaceVariables('${$.items[0]["item name"]}', { + items: [ + { "item name": "Product A", "item price": 100 }, + { "item name": "Product B", "item price": 200 }, + ], + }); + expect(res[0].literalValue).toBe("Product A"); + }); + + test("should handle array slice with property names containing spaces", () => { + const [, res] = replaceVariables('${$.items[:1]["item price"]}', { + items: [ + { "item name": "Product A", "item price": 100 }, + { "item name": "Product B", "item price": 200 }, + ], + }); + expect(res[0].literalValue).toBe(100); + }); + + test("should handle quoted dot notation for property names with spaces", () => { + const [, res] = replaceVariables("${$.'property name'}", { + "property name": "value with space", + }); + expect(res[0].literalValue).toBe("value with space"); + }); + + test("should handle multiple property names with spaces in one expression", () => { + const [result, res] = replaceVariables('${$["user name"]} - ${$["property name"]}', { + "user name": "John Doe", + "property name": "value with space", + }); + expect(res).toHaveLength(2); + expect(res[0].literalValue).toBe("John Doe"); + expect(res[1].literalValue).toBe("value with space"); + expect(result).toContain(res[0].literalName); + expect(result).toContain(res[1].literalName); + }); + + test("should handle quoted property names with double quotes", () => { + const [result, res] = replaceVariables('${"user info"}', { + "user info": "John Doe", + normalProperty: "normal value", + }); + expect(res).toHaveLength(1); + expect(res[0].literalValue).toBe("John Doe"); + expect(result).toBe(res[0].literalName); + }); + + test("should handle quoted property names with single quotes", () => { + const [result, res] = replaceVariables("${'property name'}", { + "property name": "value with space", + normalProperty: "normal value", + }); + expect(res).toHaveLength(1); + expect(res[0].literalValue).toBe("value with space"); + expect(result).toBe(res[0].literalName); + }); + + test("should handle multiple quoted property names in one expression", () => { + const [result, res] = replaceVariables('${"user name"} - ${"user age"}', { + "user name": "John Doe", + "user age": 30, + }); + expect(res).toHaveLength(2); + expect(res[0].literalValue).toBe("John Doe"); + expect(res[1].literalValue).toBe(30); + expect(result).toContain(res[0].literalName); + expect(result).toContain("-"); + expect(result).toContain(res[1].literalName); + }); + + test("should handle quoted property names with special characters", () => { + const [result, res] = replaceVariables('${"user-info:name"}', { + "user-info:name": "Jane Smith", + }); + expect(res).toHaveLength(1); + expect(res[0].literalValue).toBe("Jane Smith"); + expect(result).toBe(res[0].literalName); + }); }); diff --git a/src/mantle/evaluator/simple/expression/variableReplacer.ts b/src/mantle/evaluator/simple/expression/variableReplacer.ts index 6d0cdd2..61f00a6 100644 --- a/src/mantle/evaluator/simple/expression/variableReplacer.ts +++ b/src/mantle/evaluator/simple/expression/variableReplacer.ts @@ -14,6 +14,7 @@ export function replaceVariables(expression: string, feature?: any): [string, JP const featureDefined = typeof feature !== "undefined"; const jsonPathCache: Record = {}; const varExpRegex = /^\$./; + const quotedStringRegex = /^["'](.+)["']$/; while (i >= 0) { if (isInsideQuotes(exp, i)) { const closeQuote = findCloseQuote(exp, i); @@ -24,7 +25,25 @@ export function replaceVariables(expression: string, feature?: any): [string, JP result += exp.slice(0, i); const j = getCloseBracketIndex(exp, i); const varExp = exp.slice(i + 2, j); - if (varExpRegex.test(varExp)) { + const quotedMatch = varExp.match(quotedStringRegex); + if (quotedMatch) { + // Handle quoted property names like ${"user info"} + if (!featureDefined) { + return [result, []]; + } + const propertyName = quotedMatch[1]; + const propertyValue = feature[propertyName]; + if (typeof propertyValue !== "undefined") { + const placeholderLiteral = generateRandomString(10); + literalJP.push({ + literalName: placeholderLiteral, + literalValue: propertyValue, + }); + result += placeholderLiteral; + } else { + return ["false", []]; + } + } else if (varExpRegex.test(varExp)) { if (!featureDefined) { return [result, []]; } From 6d85d2f730817e1e397079fe40b9debd2a987100 Mon Sep 17 00:00:00 2001 From: airslice Date: Thu, 26 Mar 2026 11:10:48 +0800 Subject: [PATCH 2/5] refactor: improve regex --- .../expression/variableReplacer.test.ts | 31 +++++++++++++++++++ .../simple/expression/variableReplacer.ts | 4 +-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts index 7289e6a..f43b976 100644 --- a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts +++ b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts @@ -158,4 +158,35 @@ describe("replaceVariables", () => { expect(res[0].literalValue).toBe("Jane Smith"); expect(result).toBe(res[0].literalName); }); + + test("should reject mismatched quote types (double to single)", () => { + const [result, res] = replaceVariables('${"user info\'}', { + "user info": "John Doe", + }); + // Should not match the quoted pattern, should fall back to variable name + expect(result).toContain('czm_'); + expect(res).toHaveLength(0); + }); + + test("should reject mismatched quote types (single to double)", () => { + const [result, res] = replaceVariables("${\'user info\"}", { + "user info": "Jane Doe", + }); + // Should not match the quoted pattern, should fall back to variable name + expect(result).toContain('czm_'); + expect(res).toHaveLength(0); + }); + + test("should correctly handle consecutive properties with different quote types", () => { + const [result, res] = replaceVariables('${"prop1"} + ${\'prop2\'}', { + "prop1": "value1", + "prop2": "value2", + }); + expect(res).toHaveLength(2); + expect(res[0].literalValue).toBe("value1"); + expect(res[1].literalValue).toBe("value2"); + expect(result).toContain(res[0].literalName); + expect(result).toContain("+"); + expect(result).toContain(res[1].literalName); + }); }); diff --git a/src/mantle/evaluator/simple/expression/variableReplacer.ts b/src/mantle/evaluator/simple/expression/variableReplacer.ts index 61f00a6..72a6bab 100644 --- a/src/mantle/evaluator/simple/expression/variableReplacer.ts +++ b/src/mantle/evaluator/simple/expression/variableReplacer.ts @@ -14,7 +14,7 @@ export function replaceVariables(expression: string, feature?: any): [string, JP const featureDefined = typeof feature !== "undefined"; const jsonPathCache: Record = {}; const varExpRegex = /^\$./; - const quotedStringRegex = /^["'](.+)["']$/; + const quotedStringRegex = /^(["'])(.+)\1$/; while (i >= 0) { if (isInsideQuotes(exp, i)) { const closeQuote = findCloseQuote(exp, i); @@ -31,7 +31,7 @@ export function replaceVariables(expression: string, feature?: any): [string, JP if (!featureDefined) { return [result, []]; } - const propertyName = quotedMatch[1]; + const propertyName = quotedMatch[2]; const propertyValue = feature[propertyName]; if (typeof propertyValue !== "undefined") { const placeholderLiteral = generateRandomString(10); From edda7fd0d1f3e5eaa4bec902867d262ed51b1bcc Mon Sep 17 00:00:00 2001 From: airslice Date: Thu, 26 Mar 2026 11:40:16 +0800 Subject: [PATCH 3/5] refactor: return empty string for missing property --- .../expression/variableReplacer.test.ts | 40 +++++++++++++++++++ .../simple/expression/variableReplacer.ts | 34 +++++++--------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts index f43b976..2c7703a 100644 --- a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts +++ b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts @@ -189,4 +189,44 @@ describe("replaceVariables", () => { expect(result).toContain("+"); expect(result).toContain(res[1].literalName); }); + + test("should return empty string when quoted property is missing (consistent with regular variables)", () => { + const [result, res] = replaceVariables('${"missing"}', { + "existing": "value", + }); + // Returns empty string for missing quoted property (consistent with regular variables) + expect(res).toHaveLength(1); + expect(res[0].literalValue).toBe(""); + expect(result).toBe(res[0].literalName); + }); + + test("should pass through regular variable name when property might be missing", () => { + const [result, res] = replaceVariables('${missing}'); + // Regular variables are passed through as czm_variableName + // They will be evaluated later by Node._evaluateVariable + expect(result).toBe("czm_missing"); + expect(res).toHaveLength(0); + }); + + test("should return empty string for missing JSONPath properties (consistent with regular variables)", () => { + const [result, res] = replaceVariables('${$.missingPath}', { + "existing": "value", + }); + // Returns empty string for missing JSONPath property + expect(res).toHaveLength(1); + expect(res[0].literalValue).toBe(""); + expect(result).toBe(res[0].literalName); + }); + + test("should handle mixed existing and missing properties consistently", () => { + const [result, res] = replaceVariables('${"existing"} - ${"missing"}', { + "existing": "value", + }); + expect(res).toHaveLength(2); + expect(res[0].literalValue).toBe("value"); + expect(res[1].literalValue).toBe(""); // Missing property returns empty string + expect(result).toContain(res[0].literalName); + expect(result).toContain("-"); + expect(result).toContain(res[1].literalName); + }); }); diff --git a/src/mantle/evaluator/simple/expression/variableReplacer.ts b/src/mantle/evaluator/simple/expression/variableReplacer.ts index 72a6bab..6727613 100644 --- a/src/mantle/evaluator/simple/expression/variableReplacer.ts +++ b/src/mantle/evaluator/simple/expression/variableReplacer.ts @@ -33,16 +33,13 @@ export function replaceVariables(expression: string, feature?: any): [string, JP } const propertyName = quotedMatch[2]; const propertyValue = feature[propertyName]; - if (typeof propertyValue !== "undefined") { - const placeholderLiteral = generateRandomString(10); - literalJP.push({ - literalName: placeholderLiteral, - literalValue: propertyValue, - }); - result += placeholderLiteral; - } else { - return ["false", []]; - } + // Return empty string for missing properties to match regular variable behavior + const placeholderLiteral = generateRandomString(10); + literalJP.push({ + literalName: placeholderLiteral, + literalValue: typeof propertyValue !== "undefined" ? propertyValue : "", + }); + result += placeholderLiteral; } else if (varExpRegex.test(varExp)) { if (!featureDefined) { return [result, []]; @@ -56,16 +53,13 @@ export function replaceVariables(expression: string, feature?: any): [string, JP return [result, []]; } } - if (res.length !== 0) { - const placeholderLiteral = generateRandomString(10); - literalJP.push({ - literalName: placeholderLiteral, - literalValue: res[0], - }); - result += placeholderLiteral; - } else { - return ["false", []]; - } + // Return empty string for missing properties to match regular variable behavior + const placeholderLiteral = generateRandomString(10); + literalJP.push({ + literalName: placeholderLiteral, + literalValue: res.length !== 0 ? res[0] : "", + }); + result += placeholderLiteral; } else { const replacedVarExp = replaceReservedWord(varExp); result += `${VARIABLE_PREFIX}${replacedVarExp}`; From ad12e4b89a056c275daf80f4da4cc32b6d931682 Mon Sep 17 00:00:00 2001 From: airslice Date: Thu, 26 Mar 2026 11:55:01 +0800 Subject: [PATCH 4/5] chore: correct readme --- .../evaluator/simple/expression/README.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/mantle/evaluator/simple/expression/README.md b/src/mantle/evaluator/simple/expression/README.md index f32e025..fcaf5e9 100644 --- a/src/mantle/evaluator/simple/expression/README.md +++ b/src/mantle/evaluator/simple/expression/README.md @@ -241,6 +241,8 @@ Within string literals, you can interpolate property values: "Height: ${height}m" ``` +**Note:** String interpolation only supports simple property names (without spaces). For properties with spaces or special characters, use concatenation instead (see below). + **Example:** ```typescript const feature = { @@ -254,6 +256,20 @@ const expr = new Expression('"Building: ${name}, Height: ${height}m"', feature); expr.evaluate(); // "Building: Building A, Height: 100m" ``` +**For properties with spaces, use concatenation:** +```typescript +const feature = { + properties: { + "building name": "Tower A", + floors: 30, + }, +}; + +// Use concatenation with + operator +const expr = new Expression('${"building name"} + " has " + ${floors} + " floors"', feature); +expr.evaluate(); // "Tower A has 30 floors" +``` + ### Array Comparisons The equality operators support checking if a value is in an array: @@ -337,9 +353,9 @@ const expr2 = new Expression( ); console.log(expr2.evaluate()); // #ff0000 (red) -// String interpolation +// String concatenation (for properties with spaces) const expr3 = new Expression( - '"${"building name"} has ${floors} floors"', + '${"building name"} + " has " + ${floors} + " floors"', feature ); console.log(expr3.evaluate()); // "Tower A has 30 floors" From cc02e0d80ceebd6f4f34add0bb6ea44194cd96e5 Mon Sep 17 00:00:00 2001 From: airslice Date: Thu, 26 Mar 2026 11:58:47 +0800 Subject: [PATCH 5/5] test: fix lint issue --- .../expression/variableReplacer.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts index 2c7703a..0e5a457 100644 --- a/src/mantle/evaluator/simple/expression/variableReplacer.test.ts +++ b/src/mantle/evaluator/simple/expression/variableReplacer.test.ts @@ -160,27 +160,27 @@ describe("replaceVariables", () => { }); test("should reject mismatched quote types (double to single)", () => { - const [result, res] = replaceVariables('${"user info\'}', { + const [result, res] = replaceVariables("${\"user info'}", { "user info": "John Doe", }); // Should not match the quoted pattern, should fall back to variable name - expect(result).toContain('czm_'); + expect(result).toContain("czm_"); expect(res).toHaveLength(0); }); test("should reject mismatched quote types (single to double)", () => { - const [result, res] = replaceVariables("${\'user info\"}", { + const [result, res] = replaceVariables("${'user info\"}", { "user info": "Jane Doe", }); // Should not match the quoted pattern, should fall back to variable name - expect(result).toContain('czm_'); + expect(result).toContain("czm_"); expect(res).toHaveLength(0); }); test("should correctly handle consecutive properties with different quote types", () => { - const [result, res] = replaceVariables('${"prop1"} + ${\'prop2\'}', { - "prop1": "value1", - "prop2": "value2", + const [result, res] = replaceVariables("${\"prop1\"} + ${'prop2'}", { + prop1: "value1", + prop2: "value2", }); expect(res).toHaveLength(2); expect(res[0].literalValue).toBe("value1"); @@ -192,7 +192,7 @@ describe("replaceVariables", () => { test("should return empty string when quoted property is missing (consistent with regular variables)", () => { const [result, res] = replaceVariables('${"missing"}', { - "existing": "value", + existing: "value", }); // Returns empty string for missing quoted property (consistent with regular variables) expect(res).toHaveLength(1); @@ -201,7 +201,7 @@ describe("replaceVariables", () => { }); test("should pass through regular variable name when property might be missing", () => { - const [result, res] = replaceVariables('${missing}'); + const [result, res] = replaceVariables("${missing}"); // Regular variables are passed through as czm_variableName // They will be evaluated later by Node._evaluateVariable expect(result).toBe("czm_missing"); @@ -209,8 +209,8 @@ describe("replaceVariables", () => { }); test("should return empty string for missing JSONPath properties (consistent with regular variables)", () => { - const [result, res] = replaceVariables('${$.missingPath}', { - "existing": "value", + const [result, res] = replaceVariables("${$.missingPath}", { + existing: "value", }); // Returns empty string for missing JSONPath property expect(res).toHaveLength(1); @@ -220,7 +220,7 @@ describe("replaceVariables", () => { test("should handle mixed existing and missing properties consistently", () => { const [result, res] = replaceVariables('${"existing"} - ${"missing"}', { - "existing": "value", + existing: "value", }); expect(res).toHaveLength(2); expect(res[0].literalValue).toBe("value");