add simple string template function

Sat, 22 Nov 2025 16:44:42 +0100

author
Olaf Wintermann <olaf.wintermann@gmail.com>
date
Sat, 22 Nov 2025 16:44:42 +0100
changeset 634
9728d3a2ac97
parent 633
392ec9026b07
child 635
b85d45fd3b01

add simple string template function

src/server/test/main.c file | annotate | diff | comparison | revisions
src/server/test/objs.mk file | annotate | diff | comparison | revisions
src/server/test/strreplace.c file | annotate | diff | comparison | revisions
src/server/test/strreplace.h file | annotate | diff | comparison | revisions
src/server/util/objs.mk file | annotate | diff | comparison | revisions
src/server/util/strreplace.c file | annotate | diff | comparison | revisions
src/server/util/strreplace.h file | annotate | diff | comparison | revisions
--- a/src/server/test/main.c	Sat Nov 22 14:27:01 2025 +0100
+++ b/src/server/test/main.c	Sat Nov 22 16:44:42 2025 +0100
@@ -50,6 +50,7 @@
 #include "object.h"
 #include "io.h"
 #include "event.h"
+#include "strreplace.h"
 
 void register_pg_tests(int argc, char **argv, CxTestSuite *suite);
 
@@ -140,6 +141,12 @@
     cx_test_register(suite, test_writer_flush);
     cx_test_register(suite, test_writer_put);
     
+    // strreplace tests
+    cx_test_register(suite, test_string_template_compile);
+    cx_test_register(suite, test_string_template_compile_error);
+    cx_test_register(suite, test_string_template_write_to);
+    cx_test_register(suite, test_string_template_build_string);
+    
     // xml tests
     cx_test_register(suite, test_wsxml_iterator);
     cx_test_register(suite, test_wsxml_get_required_namespaces);
--- a/src/server/test/objs.mk	Sat Nov 22 14:27:01 2025 +0100
+++ b/src/server/test/objs.mk	Sat Nov 22 16:44:42 2025 +0100
@@ -41,6 +41,7 @@
 TESTOBJ += object.o
 TESTOBJ += io.o
 TESTOBJ += event.o
+TESTOBJ += strreplace.o
 
 TESTOBJS = $(TESTOBJ:%=$(TEST_OBJPRE)%)
 TESTSOURCE = $(TESTOBJ:%.o=test/%.c)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/test/strreplace.c	Sat Nov 22 16:44:42 2025 +0100
@@ -0,0 +1,278 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "strreplace.h"
+#include "../util/strreplace.h"
+#include <cx/mempool.h>
+
+CX_TEST(test_string_template_compile) {
+    CxMempool *mp = cxMempoolCreate(100, CX_MEMPOOL_TYPE_ADVANCED);
+    const CxAllocator *a = mp->allocator;
+    
+    StringTemplate *tpl = NULL;
+    StringTemplateSegment *s0 = NULL;
+    StringTemplateSegment *s1 = NULL;
+    StringTemplateSegment *s2 = NULL;
+    StringTemplateSegment *s3 = NULL;
+    CX_TEST_DO {
+        // single segment tests
+        
+        tpl = string_template_compile(a, cx_str(""));
+        CX_TEST_ASSERT(tpl); // empty str
+        CX_TEST_ASSERT(!tpl->segments);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("static"));
+        CX_TEST_ASSERT(tpl); // static
+        CX_TEST_ASSERT(tpl->segments);
+        CX_TEST_ASSERT(!tpl->segments->next);
+        CX_TEST_ASSERT(!cx_strcmp(tpl->segments->str, "static"));
+        CX_TEST_ASSERT(tpl->segments->type == STRING_SEGMENT_STR);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$$"));
+        CX_TEST_ASSERT(tpl); // $
+        CX_TEST_ASSERT(tpl->segments);
+        CX_TEST_ASSERT(!tpl->segments->next);
+        CX_TEST_ASSERT(!cx_strcmp(tpl->segments->str, "$"));
+        CX_TEST_ASSERT(tpl->segments->type == STRING_SEGMENT_STR);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$var"));
+        CX_TEST_ASSERT(tpl); // var
+        CX_TEST_ASSERT(tpl->segments);
+        CX_TEST_ASSERT(!tpl->segments->next);
+        CX_TEST_ASSERT(!cx_strcmp(tpl->segments->str, "var"));
+        CX_TEST_ASSERT(tpl->segments->type == STRING_SEGMENT_VAR_PLACEHOLDER);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$12"));
+        CX_TEST_ASSERT(tpl); // 12
+        CX_TEST_ASSERT(tpl->segments);
+        CX_TEST_ASSERT(!tpl->segments->next);
+        CX_TEST_ASSERT(!cx_strcmp(tpl->segments->str, "12"));
+        CX_TEST_ASSERT(tpl->segments->type == STRING_SEGMENT_NUM_PLACEHOLDER);
+        CX_TEST_ASSERT(tpl->segments->num == 12);
+        string_template_free(tpl);
+        
+        // double segment tests
+        tpl = string_template_compile(a, cx_str("test $var"));
+        CX_TEST_ASSERT(tpl);
+        s0 = tpl->segments;
+        CX_TEST_ASSERT(s0);
+        s1 = s0->next;
+        CX_TEST_ASSERT(s1);
+        CX_TEST_ASSERT(!cx_strcmp(s0->str, "test "));
+        CX_TEST_ASSERT(s0->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(!cx_strcmp(s1->str, "var"));
+        CX_TEST_ASSERT(s1->type == STRING_SEGMENT_VAR_PLACEHOLDER);
+        CX_TEST_ASSERT(s1->next == NULL);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("test ${var}"));
+        CX_TEST_ASSERT(tpl);
+        s0 = tpl->segments;
+        CX_TEST_ASSERT(s0);
+        s1 = s0->next;
+        CX_TEST_ASSERT(s1);
+        CX_TEST_ASSERT(!cx_strcmp(s0->str, "test "));
+        CX_TEST_ASSERT(s0->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(!cx_strcmp(s1->str, "var"));
+        CX_TEST_ASSERT(s1->type == STRING_SEGMENT_VAR_PLACEHOLDER);
+        CX_TEST_ASSERT(s1->next == NULL);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$var test"));
+        CX_TEST_ASSERT(tpl);
+        s0 = tpl->segments;
+        CX_TEST_ASSERT(s0);
+        s1 = s0->next;
+        CX_TEST_ASSERT(s1);
+        CX_TEST_ASSERT(!cx_strcmp(s0->str, "var"));
+        CX_TEST_ASSERT(s0->type == STRING_SEGMENT_VAR_PLACEHOLDER);
+        CX_TEST_ASSERT(!cx_strcmp(s1->str, " test"));
+        CX_TEST_ASSERT(s1->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(s1->next == NULL);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$13 test"));
+        CX_TEST_ASSERT(tpl);
+        s0 = tpl->segments;
+        CX_TEST_ASSERT(s0);
+        s1 = s0->next;
+        CX_TEST_ASSERT(s1);
+        CX_TEST_ASSERT(!cx_strcmp(s0->str, "13"));
+        CX_TEST_ASSERT(s0->type == STRING_SEGMENT_NUM_PLACEHOLDER);
+        CX_TEST_ASSERT(!cx_strcmp(s1->str, " test"));
+        CX_TEST_ASSERT(s1->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(s1->next == NULL);
+        string_template_free(tpl);
+        
+        // multi segment tests
+        tpl = string_template_compile(a, cx_str("test$var1$var2"));
+        CX_TEST_ASSERT(tpl);
+        s0 = tpl->segments;
+        CX_TEST_ASSERT(s0);
+        s1 = s0->next;
+        CX_TEST_ASSERT(s1);
+        s2 = s1->next;
+        CX_TEST_ASSERT(2);
+        CX_TEST_ASSERT(!cx_strcmp(s0->str, "test"));
+        CX_TEST_ASSERT(s0->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(!cx_strcmp(s1->str, "var1"));
+        CX_TEST_ASSERT(s1->type == STRING_SEGMENT_VAR_PLACEHOLDER);
+        CX_TEST_ASSERT(!cx_strcmp(s2->str, "var2"));
+        CX_TEST_ASSERT(s2->type == STRING_SEGMENT_VAR_PLACEHOLDER);
+        CX_TEST_ASSERT(s2->next == NULL);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("test/$1/$2"));
+        CX_TEST_ASSERT(tpl);
+        s0 = tpl->segments;
+        CX_TEST_ASSERT(s0);
+        s1 = s0->next;
+        CX_TEST_ASSERT(s1);
+        s2 = s1->next;
+        CX_TEST_ASSERT(2);
+        s3 = s2->next;
+        CX_TEST_ASSERT(s3);
+        CX_TEST_ASSERT(!cx_strcmp(s0->str, "test/"));
+        CX_TEST_ASSERT(s0->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(!cx_strcmp(s1->str, "1"));
+        CX_TEST_ASSERT(s1->type == STRING_SEGMENT_NUM_PLACEHOLDER);
+        CX_TEST_ASSERT(!cx_strcmp(s2->str, "/"));
+        CX_TEST_ASSERT(s2->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(!cx_strcmp(s3->str, "2"));
+        CX_TEST_ASSERT(s3->type == STRING_SEGMENT_NUM_PLACEHOLDER);
+        CX_TEST_ASSERT(s3->next == NULL);
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("ab$$cd/${1}/${2}"));
+        CX_TEST_ASSERT(tpl);
+        s0 = tpl->segments;
+        CX_TEST_ASSERT(s0);
+        s1 = s0->next;
+        CX_TEST_ASSERT(s1);
+        s2 = s1->next;
+        CX_TEST_ASSERT(2);
+        s3 = s2->next;
+        CX_TEST_ASSERT(s3);
+        CX_TEST_ASSERT(!cx_strcmp(s0->str, "ab$cd/"));
+        CX_TEST_ASSERT(s0->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(!cx_strcmp(s1->str, "1"));
+        CX_TEST_ASSERT(s1->type == STRING_SEGMENT_NUM_PLACEHOLDER);
+        CX_TEST_ASSERT(!cx_strcmp(s2->str, "/"));
+        CX_TEST_ASSERT(s2->type == STRING_SEGMENT_STR);
+        CX_TEST_ASSERT(!cx_strcmp(s3->str, "2"));
+        CX_TEST_ASSERT(s3->type == STRING_SEGMENT_NUM_PLACEHOLDER);
+        CX_TEST_ASSERT(s3->next == NULL);
+        string_template_free(tpl);
+    }
+    
+    cxMempoolFree(mp);
+}
+
+CX_TEST(test_string_template_compile_error) {
+    // TODO
+}
+
+static cxmutstr get_var(const CxAllocator *a, StringTemplateSegment *seg, void *userdata, WSBool *free_str) {
+    cxmutstr var_value = cx_strcat_a(a, 3, cx_str("var("), seg->str, cx_str(")"));
+    *free_str = TRUE;
+    return var_value;
+}
+
+CX_TEST(test_string_template_write_to) {
+    CxMempool *mp = cxMempoolCreate(100, CX_MEMPOOL_TYPE_ADVANCED);
+    const CxAllocator *a = mp->allocator;
+    
+    CxBuffer buf;
+    cxBufferInit(&buf, NULL, 1024, a, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS);
+    StringTemplate *tpl = NULL;
+    CX_TEST_DO {
+        tpl = string_template_compile(a, cx_str("hello world"));
+        ssize_t r = string_template_write_to(tpl, a, get_var, NULL, &buf, (cx_write_func)cxBufferWrite);
+        CX_TEST_ASSERT(r > 0);
+        CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space, buf.pos), cx_str("hello world")));
+        buf.pos = 0;
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("insert $var here"));
+        r = string_template_write_to(tpl, a, get_var, NULL, &buf, (cx_write_func)cxBufferWrite);
+        CX_TEST_ASSERT(r > 0);
+        CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space, buf.pos), cx_str("insert var(var) here")));
+        buf.pos = 0;
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$1$2$3$4$5"));
+        r = string_template_write_to(tpl, a, get_var, NULL, &buf, (cx_write_func)cxBufferWrite);
+        CX_TEST_ASSERT(r > 0);
+        CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space, buf.pos), cx_str("var(1)var(2)var(3)var(4)var(5)")));
+        buf.pos = 0;
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$$escape$$$myvar$$end"));
+        r = string_template_write_to(tpl, a, get_var, NULL, &buf, (cx_write_func)cxBufferWrite);
+        CX_TEST_ASSERT(r > 0);
+        CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space, buf.pos), cx_str("$escape$var(myvar)$end")));
+        buf.pos = 0;
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("$$$$${test}"));
+        r = string_template_write_to(tpl, a, get_var, NULL, &buf, (cx_write_func)cxBufferWrite);
+        CX_TEST_ASSERT(r > 0);
+        CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space, buf.pos), cx_str("$$var(test)")));
+        buf.pos = 0;
+        string_template_free(tpl);
+        
+        tpl = string_template_compile(a, cx_str("${123}end"));
+        r = string_template_write_to(tpl, a, get_var, NULL, &buf, (cx_write_func)cxBufferWrite);
+        CX_TEST_ASSERT(r > 0);
+        CX_TEST_ASSERT(!cx_strcmp(cx_strn(buf.space, buf.pos), cx_str("var(123)end")));
+        buf.pos = 0;
+        string_template_free(tpl);
+    }
+    cxBufferDestroy(&buf);
+    cxMempoolFree(mp);
+}
+
+CX_TEST(test_string_template_build_string) {
+    CxMempool *mp = cxMempoolCreate(100, CX_MEMPOOL_TYPE_ADVANCED);
+    const CxAllocator *a = mp->allocator;
+    
+    StringTemplate *tpl = NULL;
+    CX_TEST_DO {
+        tpl = string_template_compile(a, cx_str("insert $var here"));
+        cxmutstr str = string_template_build_string(tpl, a, get_var, NULL);
+        CX_TEST_ASSERT(str.ptr);
+        CX_TEST_ASSERT(!cx_strcmp(str, cx_str("insert var(var) here")));
+        string_template_free(tpl);
+    }
+    
+    cxMempoolFree(mp);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/test/strreplace.h	Sat Nov 22 16:44:42 2025 +0100
@@ -0,0 +1,49 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef TEST_STRREPLACE_H
+#define TEST_STRREPLACE_H
+
+#include "test.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+CX_TEST(test_string_template_compile);
+CX_TEST(test_string_template_compile_error);
+CX_TEST(test_string_template_write_to);
+CX_TEST(test_string_template_build_string);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TEST_STRREPLACE_H */
+
--- a/src/server/util/objs.mk	Sat Nov 22 14:27:01 2025 +0100
+++ b/src/server/util/objs.mk	Sat Nov 22 16:44:42 2025 +0100
@@ -37,6 +37,7 @@
 UTILOBJ += pool.o
 UTILOBJ += shexp.o
 UTILOBJ += strbuf.o
+UTILOBJ += strreplace.o
 UTILOBJ += system.o
 UTILOBJ += systhr.o
 UTILOBJ += thrpool.o
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/server/util/strreplace.c	Sat Nov 22 16:44:42 2025 +0100
@@ -0,0 +1,227 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2025 Olaf Wintermann. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "strreplace.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+
+#include <cx/buffer.h>
+
+
+
+StringTemplate* string_template_compile(const CxAllocator *a, cxstring tpl) {
+    StringTemplateSegment *end = NULL; // segment list end
+    int var = FALSE;
+    int error = FALSE;
+    
+    CxBuffer buf; // tmp buffer
+    if(cxBufferInit(&buf, NULL, 128, NULL, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS)) {
+        return NULL;
+    }
+    
+    StringTemplate *t = cxMalloc(a, sizeof(StringTemplate));
+    if(!t) {
+        cxBufferDestroy(&buf);
+        return NULL;
+    }
+    t->a = a ? a : cxDefaultAllocator;
+    t->segments = NULL;
+
+    StringTemplateSegment *seg = NULL;
+    
+    for(size_t i=0;i<tpl.length;i++) {
+        char c = tpl.ptr[i];
+        int add_char = FALSE; // add current char to the buffer
+        int finish_seg = FALSE; // copy buffer to segment string and start new segment
+        
+        if(!seg) {
+            // start new segment
+            seg = cxMalloc(a, sizeof(StringTemplateSegment));
+            if(seg) {
+                seg->type = var ? STRING_SEGMENT_VAR_PLACEHOLDER : STRING_SEGMENT_STR;
+                seg->str = (cxmutstr){NULL, 0};
+                seg->num = 0;
+                seg->next = NULL;
+                // add segment to segment list
+                if(end) {
+                    end->next = seg;
+                } else {
+                    t->segments = seg;
+                }
+                end = seg;
+            } else {
+                error = TRUE;
+                break;
+            }
+        }
+        
+        if(var) {
+            // current segment is a var
+            if(c == '}') {
+                var = FALSE;
+                finish_seg = TRUE;
+            } else if(c == '{') {
+                // noop
+            } else if(!isalnum(c)) {
+                var = FALSE;
+                finish_seg = TRUE;
+                i--;
+            } else {
+                add_char = TRUE;
+            }
+        } else {
+            if(c == '$') {
+                if(i+1<tpl.length && tpl.ptr[i+1] == '$') {
+                    // $$ -> $
+                    i++;
+                    add_char = TRUE;
+                } else {
+                    var = TRUE;
+                    if(buf.pos == 0) {
+                        // reuse current segment
+                        seg->type = STRING_SEGMENT_VAR_PLACEHOLDER;
+                    } else {
+                        // create new segment
+                        finish_seg = TRUE;
+                    }
+                }
+            } else {
+                add_char = TRUE;
+            }
+        }
+        
+        if(add_char) {
+            if(cxBufferPut(&buf, c) != c) {
+                error = TRUE;
+                break;
+            }
+        } else if(finish_seg) {
+            // copy buffer content
+            cxmutstr seg_str = cx_strdup_a(a, cx_strn(buf.space, buf.pos));
+            if(!seg_str.ptr) {
+                error = TRUE;
+                break;
+            }
+            seg->str = seg_str;
+            if(seg->type == STRING_SEGMENT_VAR_PLACEHOLDER) {
+                // is the var segment an integer reference?
+                if(!cx_strtoi(seg_str, &seg->num, 10)) {
+                    seg->type = STRING_SEGMENT_NUM_PLACEHOLDER;
+                }
+            }
+            buf.pos = 0;
+            seg = NULL;
+        }
+    }
+    
+    // finish last segment
+    if(seg) {
+        cxmutstr seg_str = cx_strdup_a(a, cx_strn(buf.space, buf.pos));
+        if(!seg_str.ptr) {
+            error = TRUE;
+        } else {
+            seg->str = seg_str;
+            if(seg->type == STRING_SEGMENT_VAR_PLACEHOLDER) {
+                if(!cx_strtoi(seg_str, &seg->num, 10)) {
+                    seg->type = STRING_SEGMENT_NUM_PLACEHOLDER;
+                }
+            }
+        }
+    }
+    
+    cxBufferDestroy(&buf);
+    if(error) {
+        string_template_free(t);
+        return NULL;
+    }
+    
+    return t;
+}
+
+void string_template_free(StringTemplate *tpl) {
+    StringTemplateSegment *seg = tpl->segments;
+    while(seg) {
+        StringTemplateSegment *next = seg->next;
+        cxFree(tpl->a, seg->str.ptr);
+        cxFree(tpl->a, seg);
+        seg = next;
+    }
+    cxFree(tpl->a, tpl);
+}
+
+ssize_t string_template_write_to(StringTemplate *tpl, const CxAllocator *a, strtpl_var_func varfunc, void *userdata, void *stream, cx_write_func writef) {
+    if(!tpl) {
+        return -1;
+    }
+    
+    // write each segment to the stream
+    StringTemplateSegment *seg = tpl->segments;
+    ssize_t w = 0;
+    while(seg) {
+        if(seg->type == STRING_SEGMENT_STR) {
+            // just write the segment string
+            if(seg->str.length > 0) {
+                size_t r = writef(seg->str.ptr, 1, seg->str.length, stream);
+                if(r != seg->str.length) {
+                    return -1;
+                }
+                w += r;
+            }
+        } else if(varfunc) {
+            // convert var segment to value
+            WSBool free_str = FALSE;
+            cxmutstr str = varfunc(a, seg, userdata, &free_str);
+            if(str.length > 0) {
+                size_t r = writef(str.ptr, 1, str.length, stream);
+                if(r != str.length) {
+                    return -1;
+                }
+                w += r;
+            }
+            if(free_str) {
+                cxFree(a, str.ptr);
+            }
+        }
+        seg = seg->next;
+    }
+    return w;
+}
+
+cxmutstr string_template_build_string(StringTemplate *tpl, const CxAllocator *a, strtpl_var_func varfunc, void *userdata) {
+    CxBuffer buf;
+    cxBufferInit(&buf, NULL, 1024, a, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS);
+    
+    ssize_t w = string_template_write_to(tpl, a, varfunc, userdata, &buf, (cx_write_func)cxBufferWrite);
+    if(w < 0 || cxBufferTerminate(&buf)) {
+        cxBufferDestroy(&buf);
+        return (cxmutstr){ NULL, 0 };
+    }
+    return (cxmutstr){ buf.space, buf.size };
+}
--- a/src/server/util/strreplace.h	Sat Nov 22 14:27:01 2025 +0100
+++ b/src/server/util/strreplace.h	Sat Nov 22 16:44:42 2025 +0100
@@ -29,6 +29,8 @@
 #ifndef STRINGREPLACE_H
 #define STRINGREPLACE_H
 
+#include "../public/nsapi.h"
+
 #include <cx/string.h>
 #include <cx/buffer.h>
 
@@ -65,7 +67,7 @@
      * STRING_SEGMENT_NUM_PLACEHOLDER: null
      * STRING_SEGMENT_VAR_PLACEHOLDER: variable name
      */
-    cxstring str;
+    cxmutstr str;
     
     /*
      * Segment type
@@ -99,9 +101,8 @@
  * 
  * a: The allocator to use for building the compiled template
  * tpl: Semplate string
- * delim: Delimiter chars for variable names
  */
-StringTemplate string_template_compile(const CxAllocator *a, cxstring tpl, char *delim);
+StringTemplate* string_template_compile(const CxAllocator *a, cxstring tpl);
 
 /*
  * Builds a string using the provided template and writes it to the stream.
@@ -113,14 +114,18 @@
  * userdata: The userdata pointer passed to the strtpl_var_func callback
  * stream: The stream object to which the resulting string should be written
  * writef: Stream write function
+ * 
+ * returns the number of written bytes or -1 on error
  */
-int string_template_write_to(StringTemplate *tpl, const CxAllocator *a, strtpl_var_func varfunc, void *userdata, void *stream, cx_write_func writef);
+ssize_t string_template_write_to(StringTemplate *tpl, const CxAllocator *a, strtpl_var_func varfunc, void *userdata, void *stream, cx_write_func writef);
 
 /*
  * Builds a string, using the provided template and allocator
  */
 cxmutstr string_template_build_string(StringTemplate *tpl, const CxAllocator *a, strtpl_var_func varfunc, void *userdata);
 
+void string_template_free(StringTemplate *tpl);
+
 #ifdef __cplusplus
 }
 #endif

mercurial