@@ -7,8 +7,10 @@ import proxyData from 'mustache-validator';
77 * Expand any placeholder strings - {{....}} - in the given template, and return the result.
88 *
99 * The template implementation is currently provided by: [mustache.js](https://github.com/janl/mustache.js).
10+ * This is augmented by also allowing the templates to contain function calls.
1011 *
11- * @param template - A template string, typically with placeholders such as {{query.task.folder}}
12+ * @param template - A template string, typically with placeholders such as {{query.task.folder}} or
13+ * {{query.file.property('task_instruction')}}
1214 * @param view - The property values
1315 *
1416 * @throws Error
@@ -24,8 +26,12 @@ export function expandPlaceholders(template: string, view: any): string {
2426 return text ;
2527 } ;
2628
29+ // Preprocess the template to evaluate any placeholders that involve function calls
30+ const evaluatedTemplate = evaluateAnyFunctionCalls ( template , view ) ;
31+
32+ // Render the preprocessed template
2733 try {
28- return Mustache . render ( template , proxyData ( view ) ) ;
34+ return Mustache . render ( evaluatedTemplate , proxyData ( view ) ) ;
2935 } catch ( error ) {
3036 let message = '' ;
3137 if ( error instanceof Error ) {
@@ -43,3 +49,99 @@ The problem is in:
4349 throw Error ( message ) ;
4450 }
4551}
52+
53+ const ARGUMENTS_REGEX = new RegExp (
54+ [
55+ // Match single-quoted arguments
56+ "'((?:\\\\'|[^'])*)'" ,
57+
58+ // Match double-quoted arguments
59+ '"((?:\\\\"|[^"])*)"' ,
60+
61+ // Match unquoted arguments (non-commas)
62+ '([^,]+)' ,
63+ ] . join ( '|' ) , // Combine all parts with OR (|)
64+ 'g' , // Global flag for multiple matches
65+ ) ;
66+
67+ function parseArgs ( args : string ) : string [ ] {
68+ const parsedArgs : string [ ] = [ ] ;
69+ let match ;
70+
71+ while ( ( match = ARGUMENTS_REGEX . exec ( args ) ) !== null ) {
72+ if ( match [ 1 ] !== undefined ) {
73+ // Single-quoted argument
74+ parsedArgs . push ( match [ 1 ] . replace ( / \\ ' / g, "'" ) ) ;
75+ } else if ( match [ 2 ] !== undefined ) {
76+ // Double-quoted argument
77+ parsedArgs . push ( match [ 2 ] . replace ( / \\ " / g, '"' ) ) ;
78+ } else if ( match [ 3 ] !== undefined ) {
79+ // Unquoted argument
80+ parsedArgs . push ( match [ 3 ] . trim ( ) ) ;
81+ }
82+ }
83+
84+ return parsedArgs ;
85+ }
86+
87+ // Regex to detect function calls in placeholders
88+ const FUNCTION_REGEX = new RegExp (
89+ [
90+ // Match opening double curly braces with optional whitespace
91+ '{{\\s*' ,
92+
93+ // Match and capture the function path (e.g., "object.path.toFunction")
94+ '([\\w.]+)' ,
95+
96+ // Match the opening parenthesis and capture arguments inside
97+ '\\(([^)]*)\\)' ,
98+
99+ // Match optional whitespace followed by closing double curly braces
100+ '\\s*}}' ,
101+ ] . join ( '' ) , // Combine all parts without additional separators
102+ 'g' , // Global flag to match all instances in the template
103+ ) ;
104+
105+ function evaluateAnyFunctionCalls ( template : string , view : any ) {
106+ return template . replace ( FUNCTION_REGEX , ( _match , functionPath , args ) => {
107+ // Split the function path (e.g., "query.file.property") into parts
108+ const pathParts = functionPath . split ( '.' ) ;
109+
110+ // Extract the function name (last part of the path)
111+ const functionName = pathParts . pop ( ) ;
112+
113+ // Traverse the view object to find the object containing the function.
114+ //
115+ // This is needed because JavaScript/TypeScript doesn’t provide a direct way
116+ // to access view['query']['file']['property'] based on such a dynamic path.
117+ //
118+ // So we need the loop to "walk" through the view object step by step,
119+ // accessing each level as specified by the pathParts.
120+ //
121+ // Also, if any part of the path is missing (e.g., view.query.file exists,
122+ // but view.query.file.property does not), the loop ensures the traversal
123+ // stops early, and obj becomes undefined instead of throwing an error.
124+ let obj = view ; // Start at the root of the view object
125+ for ( const part of pathParts ) {
126+ if ( obj == null ) {
127+ // Stop traversal if obj is null or undefined
128+ obj = undefined ;
129+ break ;
130+ }
131+ obj = obj [ part ] ; // Move to the next level of the object
132+ }
133+ // At the end of the loop, obj contains the resolved value or undefined if any part of the path was invalid
134+
135+ // Check if the function exists on the resolved object
136+ if ( obj && typeof obj [ functionName ] === 'function' ) {
137+ // Parse the arguments from the placeholder, stripping quotes and trimming whitespace
138+ const argValues = parseArgs ( args ) ;
139+
140+ // Call the function with the parsed arguments and return the result
141+ return obj [ functionName ] ( ...argValues ) ;
142+ }
143+
144+ // Throw an error if the function does not exist or is invalid
145+ throw new Error ( `Unknown property or invalid function: ${ functionPath } ` ) ;
146+ } ) ;
147+ }
0 commit comments