Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ The parser includes comprehensive string manipulation capabilities.
| padRight(str, len, padChar?) | Pads a string on the right with spaces (or optional padding character) to reach the target length. |
| padBoth(str, len, padChar?) | Pads a string on both sides with spaces (or optional padding character) to reach the target length. If an odd number of padding characters is needed, the extra character is added on the right. |

### Slicing and Encoding

| Function | Description |
|:--------------------- |:----------- |
| slice(s, start, end?) | Extracts a portion of a string or array. Supports negative indices (e.g., -1 for last element). |
| urlEncode(str) | URL-encodes a string using `encodeURIComponent`. |
| base64Encode(str) | Base64-encodes a string with proper UTF-8 support. |
| base64Decode(str) | Base64-decodes a string with proper UTF-8 support. |

### Utility Functions

| Function | Description |
|:--------------------- |:----------- |
| coalesce(a, b, ...) | Returns the first non-null and non-empty string value from the arguments. Numbers and booleans (including 0 and false) are considered valid values. |

### String Function Examples

```js
Expand Down Expand Up @@ -203,6 +218,20 @@ parser.evaluate('padRight("5", 3, "0")'); // "500"
parser.evaluate('padBoth("hi", 6)'); // " hi "
parser.evaluate('padBoth("hi", 6, "-")'); // "--hi--"

// Slicing
parser.evaluate('slice("hello world", 0, 5)'); // "hello"
parser.evaluate('slice("hello world", -5)'); // "world"
parser.evaluate('slice([1, 2, 3, 4, 5], -2)'); // [4, 5]

// Encoding
parser.evaluate('urlEncode("foo=bar&baz")'); // "foo%3Dbar%26baz"
parser.evaluate('base64Encode("hello")'); // "aGVsbG8="
parser.evaluate('base64Decode("aGVsbG8=")'); // "hello"

// Coalesce
parser.evaluate('coalesce("", null, "found")'); // "found"
parser.evaluate('coalesce(null, 0, 42)'); // 0

// Complex string operations
parser.evaluate('toUpper(trim(left(" hello world ", 10)))'); // "HELLO WOR"
```
Expand Down
96 changes: 96 additions & 0 deletions src/functions/string/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,99 @@ export function padBoth(str: string | undefined, targetLength: number | undefine

return leftPad + str + rightPad;
}

/**
* Extracts a portion of a string or array
* Supports negative indices (counting from the end)
* @param s - The string or array to slice
* @param start - Start index (negative counts from end)
* @param end - End index (optional, negative counts from end)
*/
export function slice(
s: string | any[] | undefined,
start: number | undefined,
end?: number
): string | any[] | undefined {
if (s === undefined || start === undefined) {
return undefined;
}
if (typeof s !== 'string' && !Array.isArray(s)) {
throw new Error('First argument to slice must be a string or array');
}
if (typeof start !== 'number') {
throw new Error('Second argument to slice must be a number');
}
if (end !== undefined && typeof end !== 'number') {
throw new Error('Third argument to slice must be a number');
}

return s.slice(start, end);
}

/**
* URL-encodes a string
* Uses encodeURIComponent for safe encoding
*/
export function urlEncode(str: string | undefined): string | undefined {
if (str === undefined) {
return undefined;
}
if (typeof str !== 'string') {
throw new Error('Argument to urlEncode must be a string');
}
return encodeURIComponent(str);
}

// Global declarations for btoa/atob (available in Node.js 16+ and browsers)
declare function btoa(data: string): string;
declare function atob(data: string): string;

/**
* Base64-encodes a string
* Handles UTF-8 encoding properly using btoa
*/
export function base64Encode(str: string | undefined): string | undefined {
if (str === undefined) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Simplify this function by using "btoa" instead. Also create and document a base64Decode function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified base64Encode to use btoa and added base64Decode function using atob in c537880. Also added documentation and language server support for both functions, plus 7 new tests for base64Decode.

return undefined;
}
if (typeof str !== 'string') {
throw new Error('Argument to base64Encode must be a string');
}
// Encode UTF-8 string to base64 using btoa
// First encode as UTF-8 bytes, then convert to binary string for btoa
const utf8Str = unescape(encodeURIComponent(str));
return btoa(utf8Str);
}

/**
* Base64-decodes a string
* Handles UTF-8 decoding properly using atob
*/
export function base64Decode(str: string | undefined): string | undefined {
if (str === undefined) {
return undefined;
}
if (typeof str !== 'string') {
throw new Error('Argument to base64Decode must be a string');
}
try {
// Decode base64 to binary string, then decode UTF-8
const binaryStr = atob(str);
return decodeURIComponent(escape(binaryStr));
} catch {
throw new Error('Invalid base64 string');
}
}

/**
* Returns the first non-null and non-empty string value from the arguments
* @param args - Any number of values to check
*/
export function coalesceString(...args: any[]): any {
for (const arg of args) {
if (arg !== undefined && arg !== null && arg !== '') {
return arg;
}
}
return args.length > 0 ? args[args.length - 1] : undefined;
}
37 changes: 37 additions & 0 deletions src/language-service/language-service.documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,43 @@ export const BUILTIN_FUNCTION_DOCS: Record<string, FunctionDoc> = {
{ name: 'length', description: 'Target length.' },
{ name: 'padStr', description: 'Padding string.', optional: true }
]
},
slice: {
name: 'slice',
description: 'Extract a portion of a string or array. Supports negative indices.',
params: [
{ name: 's', description: 'Input string or array.' },
{ name: 'start', description: 'Start index (negative counts from end).' },
{ name: 'end', description: 'End index (negative counts from end).', optional: true }
]
},
urlEncode: {
name: 'urlEncode',
description: 'URL-encode a string using encodeURIComponent.',
params: [
{ name: 'str', description: 'String to encode.' }
]
},
base64Encode: {
name: 'base64Encode',
description: 'Base64-encode a string with UTF-8 support.',
params: [
{ name: 'str', description: 'String to encode.' }
]
},
base64Decode: {
name: 'base64Decode',
description: 'Base64-decode a string with UTF-8 support.',
params: [
{ name: 'str', description: 'Base64 string to decode.' }
]
},
coalesce: {
name: 'coalesce',
description: 'Return the first non-null and non-empty string value from the arguments.',
params: [
{ name: 'values', description: 'Values to check.', isVariadic: true }
]
}
};

Expand Down
9 changes: 7 additions & 2 deletions src/parsing/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Expression } from '../core/expression.js';
import type { Value, VariableResolveResult, Values } from '../types/values.js';
import type { Instruction } from './instruction.js';
import type { OperatorFunction } from '../types/parser.js';
import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth } from '../functions/index.js';
import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth, slice, urlEncode, base64Encode, base64Decode, coalesceString } from '../functions/index.js';
import {
add,
sub,
Expand Down Expand Up @@ -219,7 +219,12 @@ export class Parser {
toBoolean: toBoolean,
padLeft: padLeft,
padRight: padRight,
padBoth: padBoth
padBoth: padBoth,
slice: slice,
urlEncode: urlEncode,
base64Encode: base64Encode,
base64Decode: base64Decode,
coalesce: coalesceString
};

this.numericConstants = {
Expand Down
Loading