test: hemar: puh

This commit is contained in:
2025-05-15 23:57:18 +00:00
parent 421cc6e870
commit 725f6eea30
2 changed files with 610 additions and 484 deletions

View File

@@ -168,12 +168,9 @@ template_error_to_string(TemplateErrorCode code, TemplateConfig *config)
strcat(message, "` keyword in section block"); strcat(message, "` keyword in section block");
return message; return message;
case TEMPLATE_ERROR_NO_BEGIN_IN_SECTION: case TEMPLATE_ERROR_NO_BEGIN_IN_SECTION:
message = "Not found `"; return "Missing end tag for section";
strcat(message, config->Syntax.Section.begin);
strcat(message, "` keyword in section block");
return message;
case TEMPLATE_ERROR_UNEXPECTED_SECTION_END: case TEMPLATE_ERROR_UNEXPECTED_SECTION_END:
return "Unexpected section end"; return "Unexpected section end or missing end tag";
case TEMPLATE_ERROR_NESTED_INCLUDE: case TEMPLATE_ERROR_NESTED_INCLUDE:
return "Nested include"; return "Nested include";
case TEMPLATE_ERROR_NESTED_EXECUTE: case TEMPLATE_ERROR_NESTED_EXECUTE:
@@ -201,7 +198,7 @@ template_default_config(MemoryContext context)
config.Syntax.Braces.close = "}}"; config.Syntax.Braces.close = "}}";
config.Syntax.Section.control = "for "; config.Syntax.Section.control = "for ";
config.Syntax.Section.source = "in "; config.Syntax.Section.source = "in ";
config.Syntax.Section.begin = "do "; config.Syntax.Section.begin = ""; /* No longer used, but keep for backward compatibility */
config.Syntax.Interpolate.invoke = ""; config.Syntax.Interpolate.invoke = "";
config.Syntax.Include.invoke = "include "; config.Syntax.Include.invoke = "include ";
config.Syntax.Execute.invoke = "exec "; config.Syntax.Execute.invoke = "exec ";
@@ -336,6 +333,11 @@ template_parse_interpolation(MemoryContext context, const char **s_ptr,
} }
key_len = *s - key_start; key_len = *s - key_start;
/* Trim trailing whitespace if it was terminated by whitespace */
while (key_len > 0 && isspace((unsigned char)key_start[key_len - 1]))
key_len--;
node->value->interpolate.key = MemoryContextStrdup(context, pnstrdup(key_start, key_len)); node->value->interpolate.key = MemoryContextStrdup(context, pnstrdup(key_start, key_len));
elog(DEBUG1, "TPI: Parsing: %s", node->value->interpolate.key); elog(DEBUG1, "TPI: Parsing: %s", node->value->interpolate.key);
@@ -423,7 +425,7 @@ template_parse_section(MemoryContext context, const char **s_ptr,
while (**s != '\0') while (**s != '\0')
{ {
if (isspace((unsigned char)**s) || if (isspace((unsigned char)**s) ||
strncmp(*s, config->Syntax.Section.begin, strlen(config->Syntax.Section.begin)) == 0) strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
break; break;
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
@@ -434,14 +436,6 @@ template_parse_section(MemoryContext context, const char **s_ptr,
return NULL; return NULL;
} }
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
{
if (error_code)
*error_code = TEMPLATE_ERROR_NO_BEGIN_IN_SECTION;
template_free_node(node);
return NULL;
}
(*s)++; (*s)++;
} }
@@ -449,60 +443,72 @@ template_parse_section(MemoryContext context, const char **s_ptr,
node->value->section.collection = MemoryContextStrdup(context, pnstrdup(collection_start, collection_len)); node->value->section.collection = MemoryContextStrdup(context, pnstrdup(collection_start, collection_len));
elog(DEBUG1, "TPS: Parsed section collection: %s", node->value->section.collection); elog(DEBUG1, "TPS: Parsed section collection: %s", node->value->section.collection);
/* Check for 'do' keyword */ /* Skip whitespace before closing brace */
*s = skip_whitespace(*s); *s = skip_whitespace(*s);
// TODO: why check begin second time, first in while
if (strncmp(*s, config->Syntax.Section.begin, strlen(config->Syntax.Section.begin)) != 0) /* Check for closing brace */
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) != 0)
{ {
if (error_code) if (error_code)
*error_code = TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_SOURCE; *error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END;
template_free_node(node); template_free_node(node);
return NULL; return NULL;
} }
*s += strlen(config->Syntax.Section.begin); /* Move past the closing brace */
*s = skip_whitespace(*s); *s += strlen(config->Syntax.Braces.close);
/* Check if there's a closing brace right after 'do' */ /* Start of the body content */
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
{
/* Empty section body */
elog(DEBUG1, "TPS: Parsed empty section body");
*s_ptr = *s + strlen(config->Syntax.Braces.close);
node->value->section.body = NULL;
return node;
}
/* Parse the body as a normal template */
const char *body_start = *s; const char *body_start = *s;
const char *original_s = *s; const char *end_tag_start = NULL;
int nesting_level = 1; /* Start at 1, our current section */
int inner_braces_opened_count = 0;
/* Find the end of the section */ elog(DEBUG1, "TPS: Starting to parse section body at position: %s", body_start);
while (**s) { elog(DEBUG1, "TPS: Looking for end tag");
// s = {% a %} %}
elog(DEBUG2, "TPS: Step, braces opened: %d, s: %s", inner_braces_opened_count, *s); /* Find the matching end tag, accounting for nested sections */
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) { while (**s != '\0')
elog(DEBUG2, "TPS: inner_braces_opened_count++"); {
inner_braces_opened_count++; if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
} {
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) { /* We found an opening brace */
if (inner_braces_opened_count > 0) { const char *tag_start = *s + strlen(config->Syntax.Braces.open);
elog(DEBUG2, "TPS: inner_braces_opened_count--"); const char *tag_ptr = tag_start;
inner_braces_opened_count--;
} /* Skip whitespace after opening brace */
else { while (*tag_ptr && isspace((unsigned char)*tag_ptr))
elog(DEBUG2, "TPS: exit"); tag_ptr++;
break;
} /* Check if this is a new section tag */
} if (strncmp(tag_ptr, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0)
{
/* Found a nested section, increase nesting level */
nesting_level++;
elog(DEBUG1, "TPS: Found nested section, nesting level: %d, at position: %s", nesting_level, *s);
}
/* Check if this is an end tag */
else if (strncmp(tag_ptr, "end", 3) == 0)
{
/* Found an end tag, decrease nesting level */
nesting_level--;
elog(DEBUG1, "TPS: Found end tag, nesting level: %d, at position: %s", nesting_level, *s);
if (nesting_level == 0)
{
/* This is our matching end tag */
end_tag_start = *s;
break;
}
}
}
(*s)++; (*s)++;
} }
if (!**s) /* Check if we found a matching end tag */
if (nesting_level > 0 || !end_tag_start)
{ {
/* Unexpected end of string before closing brace */ elog(WARNING, "TPS: No matching end tag found for section, nesting level: %d", nesting_level);
if (error_code) if (error_code)
*error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END; *error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END;
template_free_node(node); template_free_node(node);
@@ -510,7 +516,7 @@ template_parse_section(MemoryContext context, const char **s_ptr,
} }
/* Extract the body content */ /* Extract the body content */
size_t body_len = *s - body_start; size_t body_len = end_tag_start - body_start;
char *body_content = pnstrdup(body_start, body_len); char *body_content = pnstrdup(body_start, body_len);
elog(DEBUG1, "TPS: Section body content: %s", body_content); elog(DEBUG1, "TPS: Section body content: %s", body_content);
@@ -530,8 +536,26 @@ template_parse_section(MemoryContext context, const char **s_ptr,
pfree(body_content); pfree(body_content);
node->value->section.body = body_node; node->value->section.body = body_node;
/* Skip past the end tag */
*s = end_tag_start;
*s += strlen(config->Syntax.Braces.open);
*s = skip_whitespace(*s);
*s += 3; /* Skip "end" */
*s = skip_whitespace(*s);
/* Check for closing brace of end tag */
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) != 0)
{
elog(WARNING, "TPS: No closing brace for end tag at position: %s", *s);
if (error_code)
*error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END;
template_free_node(node);
return NULL;
}
/* Set the pointer to after the closing brace */ /* Set the pointer to after the closing brace */
*s_ptr = *s + strlen(config->Syntax.Braces.close); *s_ptr = *s + strlen(config->Syntax.Braces.close);
elog(DEBUG1, "TPS: Successfully parsed section, returning at position: %s", *s_ptr);
return node; return node;
} }
@@ -574,6 +598,11 @@ template_parse_include(MemoryContext context, const char **s_ptr,
} }
include_len = *s - include_start; include_len = *s - include_start;
/* Trim trailing whitespace if it was terminated by whitespace */
while (include_len > 0 && isspace((unsigned char)include_start[include_len - 1]))
include_len--;
node->value->include.key = MemoryContextStrdup(context, pnstrdup(include_start, include_len)); node->value->include.key = MemoryContextStrdup(context, pnstrdup(include_start, include_len));
*s = skip_whitespace(*s); *s = skip_whitespace(*s);
@@ -612,23 +641,55 @@ template_parse_execute(MemoryContext context, const char **s_ptr,
*s = skip_whitespace(*s); *s = skip_whitespace(*s);
code_start = *s; code_start = *s;
/* Track quote state to handle SQL content properly */
bool in_single_quote = false;
bool in_double_quote = false;
bool escaped = false;
while (**s != '\0') while (**s != '\0')
{ {
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) /* Handle escaping */
break; if (**s == '\\') {
escaped = !escaped;
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) (*s)++;
{ continue;
if (error_code)
*error_code = TEMPLATE_ERROR_NESTED_EXECUTE;
template_free_node(node);
return NULL;
} }
/* Handle quotes - only toggle quote state if not escaped */
if (!escaped) {
if (**s == '\'') {
in_single_quote = !in_single_quote;
} else if (**s == '"') {
in_double_quote = !in_double_quote;
}
}
/* Only check for closing braces when not inside quotes */
if (!in_single_quote && !in_double_quote) {
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) {
break;
}
/* Check for nested opening braces */
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) {
if (error_code)
*error_code = TEMPLATE_ERROR_NESTED_EXECUTE;
template_free_node(node);
return NULL;
}
}
/* Reset escaped flag after processing a character */
escaped = false;
(*s)++; (*s)++;
} }
code_len = *s - code_start; code_len = *s - code_start;
/* Trim trailing whitespace */
while (code_len > 0 && isspace((unsigned char)code_start[code_len - 1]))
code_len--;
node->value->execute.code = MemoryContextStrdup(context, pnstrdup(code_start, code_len)); node->value->execute.code = MemoryContextStrdup(context, pnstrdup(code_start, code_len));
/* Check for closing brace */ /* Check for closing brace */
@@ -710,25 +771,52 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
tag_prefix = *s + strlen(config->Syntax.Braces.open); tag_prefix = *s + strlen(config->Syntax.Braces.open);
tag_prefix = skip_whitespace(tag_prefix); tag_prefix = skip_whitespace(tag_prefix);
/* Determine tag type by prefix */ /* Find the longest matching prefix to determine tag type */
if (strncmp(tag_prefix, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0) typedef struct {
{ const char *prefix;
int tag_type;
} PrefixMatch;
PrefixMatch matches[] = {
{config->Syntax.Section.control, 1},
{config->Syntax.Include.invoke, 2},
{config->Syntax.Execute.invoke, 3},
{config->Syntax.Interpolate.invoke, 4}
};
int matched_type = 0;
size_t max_length = 0;
/* Find longest match (in case when one prefix is part of another) */
for (int i = 0; i < 4; i++) {
if (strncmp(tag_prefix, matches[i].prefix, strlen(matches[i].prefix)) == 0) {
/* >= because one of the prefixes may be empty */
if (strlen(matches[i].prefix) >= max_length) {
max_length = strlen(matches[i].prefix);
matched_type = matches[i].tag_type;
}
}
}
/* Choose the tag parser based on the matched type */
if (matched_type == 1) {
/* Section tag */
elog(LOG, "TPE: Parsing section tag at position: %.50s", *s);
tag_node = template_parse_section(context, s, config, error_code); tag_node = template_parse_section(context, s, config, error_code);
} } else if (matched_type == 2) {
else if (strncmp(tag_prefix, config->Syntax.Include.invoke, strlen(config->Syntax.Include.invoke)) == 0) /* Include tag */
{ elog(LOG, "TPE: Parsing include tag at position: %.50s", *s);
tag_node = template_parse_include(context, s, config, error_code); tag_node = template_parse_include(context, s, config, error_code);
} } else if (matched_type == 3) {
else if (strncmp(tag_prefix, config->Syntax.Execute.invoke, strlen(config->Syntax.Execute.invoke)) == 0) /* Execute tag */
{ elog(LOG, "TPE: Parsing include tag at position: %.50s", *s);
tag_node = template_parse_execute(context, s, config, error_code); tag_node = template_parse_execute(context, s, config, error_code);
} } else if (matched_type == 4) {
else if (strncmp(tag_prefix, config->Syntax.Interpolate.invoke, strlen(config->Syntax.Interpolate.invoke)) == 0) /* Interpolation tag */
{ elog(LOG, "TPE: Parsing interpolation tag at position: %.50s", *s);
tag_node = template_parse_interpolation(context, s, config, error_code); tag_node = template_parse_interpolation(context, s, config, error_code);
} } else {
else /* Unknown tag type */
{
if (error_code) if (error_code)
*error_code = TEMPLATE_ERROR_UNKNOWN_TAG; *error_code = TEMPLATE_ERROR_UNKNOWN_TAG;
template_free_node(root); template_free_node(root);

View File

@@ -1,405 +1,443 @@
-- Test file for hemar template parser -- Test file for hemar template parser
-- Run with: psql -f test_template_parser.sql -- Run with: psql -f test_template_parser.sql
-- Load extension if not already loaded -- Load extension if not already loaded
-- CREATE EXTENSION IF NOT EXISTS hemar; -- CREATE EXTENSION IF NOT EXISTS hemar;
-- Create test function to validate template parsing -- Create test function to validate template parsing
CREATE OR REPLACE FUNCTION test_template_parse(template_text text, expected_structure text) RETURNS boolean AS $$ CREATE OR REPLACE FUNCTION test_template_parse(template_text text, expected_structure text) RETURNS boolean AS $$
DECLARE DECLARE
parsed_result text; parsed_result text;
passed boolean; passed boolean;
BEGIN BEGIN
BEGIN BEGIN
parsed_result := hemar.parse(template_text); parsed_result := hemar.parse(template_text);
passed := position(expected_structure in parsed_result) > 0;
EXCEPTION IF parsed_result IS NULL THEN
WHEN OTHERS THEN RAISE WARNING 'Parser returned NULL for template: %', template_text;
passed := false; RETURN false;
END; END IF;
IF NOT passed THEN passed := position(expected_structure in parsed_result) > 0;
RAISE WARNING 'Template parsing test failed!';
RAISE WARNING 'Template: %', template_text; IF NOT passed THEN
RAISE WARNING 'Expected to find: %', expected_structure; RAISE WARNING 'Template parsing test failed!';
RAISE WARNING 'Actual result: %', parsed_result; RAISE WARNING 'Template: %', template_text;
END IF; RAISE WARNING 'Expected to find: %', expected_structure;
RAISE WARNING 'Actual result: %', parsed_result;
RETURN passed; END IF;
END;
$$ LANGUAGE plpgsql; RETURN passed;
EXCEPTION WHEN OTHERS THEN
-- Run the tests RAISE WARNING 'Exception during parsing: % (state: %)', SQLERRM, SQLSTATE;
DO $$ RAISE WARNING 'Template: %', template_text;
DECLARE RETURN false;
total_tests integer := 0; END;
passed_tests integer := 0; END;
result boolean; $$ LANGUAGE plpgsql;
BEGIN
RAISE NOTICE 'Starting template parser tests...'; -- Run the tests
DO $$
-- Test 1: Simple interpolation DECLARE
total_tests := total_tests + 1; total_tests integer := 0;
result := test_template_parse( passed_tests integer := 0;
'{{ simple_var }}', result boolean;
'INTERPOLATE: "simple_var"' BEGIN
); PERFORM pg_sleep(2);
IF result THEN RAISE NOTICE 'Starting template parser tests...';
passed_tests := passed_tests + 1;
RAISE NOTICE 'Test %: Simple interpolation - PASSED', total_tests; -- Test 1: Simple interpolation
ELSE total_tests := total_tests + 1;
RAISE WARNING 'Test %: Simple interpolation - FAILED', total_tests; result := test_template_parse(
END IF; $hemar1${{ simple_var }}$hemar1$,
$expected1$INTERPOLATE: "simple_var"$expected1$
-- Test 2: Interpolation with surrounding text );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'Hello, {{ name }}!', RAISE NOTICE 'Test %: Simple interpolation - PASSED', total_tests;
'TEXT: "Hello, " ELSE
INTERPOLATE: "name" RAISE WARNING 'Test %: Simple interpolation - FAILED', total_tests;
TEXT: "!"' END IF;
);
IF result THEN -- Test 2: Interpolation with surrounding text
passed_tests := passed_tests + 1; total_tests := total_tests + 1;
RAISE NOTICE 'Test %: Interpolation with surrounding text - PASSED', total_tests; result := test_template_parse(
ELSE $hemar2$Hello, {{ name }}!$hemar2$,
RAISE WARNING 'Test %: Interpolation with surrounding text - FAILED', total_tests; $expected2$TEXT: "Hello, "
END IF; INTERPOLATE: "name"
TEXT: "!"$expected2$
-- Test 3: Simple section (for loop) );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'{{ for item in items do }}{{ item }}{{ end }}', RAISE NOTICE 'Test %: Interpolation with surrounding text - PASSED', total_tests;
'SECTION: iterator="item", collection="items"' ELSE
); RAISE WARNING 'Test %: Interpolation with surrounding text - FAILED', total_tests;
IF result THEN END IF;
passed_tests := passed_tests + 1;
RAISE NOTICE 'Test %: Simple section (for loop) - PASSED', total_tests; -- Test 3: Simple section (for loop)
ELSE total_tests := total_tests + 1;
RAISE WARNING 'Test %: Simple section (for loop) - FAILED', total_tests; result := test_template_parse(
END IF; $hemar3${{ for item in items }}{{ item }}{{ end }}$hemar3$,
$expected3$SECTION: iterator="item", collection="items"$expected3$
-- Test 4: Section with nested interpolation );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'{{ for item in items do }}Name: {{ item.name }}{{ end }}', RAISE NOTICE 'Test %: Simple section (for loop) - PASSED', total_tests;
'SECTION: iterator="item", collection="items" ELSE
TEXT: "Name: " RAISE WARNING 'Test %: Simple section (for loop) - FAILED', total_tests;
INTERPOLATE: "item.name"' END IF;
);
IF result THEN -- Test 4: Section with nested interpolation
passed_tests := passed_tests + 1; total_tests := total_tests + 1;
RAISE NOTICE 'Test %: Section with nested interpolation - PASSED', total_tests; result := test_template_parse(
ELSE $hemar4${{ for item in items }}Name: {{ item.name }}{{ end }}$hemar4$,
RAISE WARNING 'Test %: Section with nested interpolation - FAILED', total_tests; $expected4$SECTION: iterator="item", collection="items"
END IF; TEXT: "Name: "
INTERPOLATE: "item.name"$expected4$
-- Test 5: Nested sections );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'{{ for item in items do }}{{ for subitem in item.subitems do }}{{ subitem }}{{ end }}{{ end }}', RAISE NOTICE 'Test %: Section with nested interpolation - PASSED', total_tests;
'SECTION: iterator="item", collection="items" ELSE
SECTION: iterator="subitem", collection="item.subitems" RAISE WARNING 'Test %: Section with nested interpolation - FAILED', total_tests;
INTERPOLATE: "subitem"' END IF;
);
IF result THEN -- Test 5: Nested sections
passed_tests := passed_tests + 1; total_tests := total_tests + 1;
RAISE NOTICE 'Test %: Nested sections - PASSED', total_tests; result := test_template_parse(
ELSE $hemar5${{ for item in items }}{{ for subitem in item.subitems }}{{ subitem }}{{ end }}{{ end }}$hemar5$,
RAISE WARNING 'Test %: Nested sections - FAILED', total_tests; $expected5$SECTION: iterator="item", collection="items"
END IF; SECTION: iterator="subitem", collection="item.subitems"
INTERPOLATE: "subitem"$expected5$
-- Test 6: Include tag );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'{{ include template_name }}', RAISE NOTICE 'Test %: Nested sections - PASSED', total_tests;
'INCLUDE: "template_name"' ELSE
); RAISE WARNING 'Test %: Nested sections - FAILED', total_tests;
IF result THEN END IF;
passed_tests := passed_tests + 1;
RAISE NOTICE 'Test %: Include tag - PASSED', total_tests; -- Test 6: Include tag
ELSE total_tests := total_tests + 1;
RAISE WARNING 'Test %: Include tag - FAILED', total_tests; result := test_template_parse(
END IF; $hemar6${{ include template_name }}$hemar6$,
$expected6$INCLUDE: "template_name"$expected6$
-- Test 7: Execute tag );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'{{ exec RETURN my_function(arg1, arg2) }}', RAISE NOTICE 'Test %: Include tag - PASSED', total_tests;
'EXECUTE: "RETURN my_function(arg1, arg2)"' ELSE
); RAISE WARNING 'Test %: Include tag - FAILED', total_tests;
IF result THEN END IF;
passed_tests := passed_tests + 1;
RAISE NOTICE 'Test %: Execute tag - PASSED', total_tests; -- Test 7: Execute tag
ELSE total_tests := total_tests + 1;
RAISE WARNING 'Test %: Execute tag - FAILED', total_tests; result := test_template_parse(
END IF; $hemar7${{ exec RETURN my_function(arg1, arg2) }}$hemar7$,
$expected7$EXECUTE: "RETURN my_function(arg1, arg2)"$expected7$
-- Test 8: Complex mixed template );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'<div>{{ for item in items do }}<p>{{ item.name }}</p>{{ include item.template }}{{ end }}</div>', RAISE NOTICE 'Test %: Execute tag - PASSED', total_tests;
'TEXT: "<div>" ELSE
SECTION: iterator="item", collection="items" RAISE WARNING 'Test %: Execute tag - FAILED', total_tests;
TEXT: "<p>" END IF;
INTERPOLATE: "item.name"
TEXT: "</p>" -- Test 8: Complex mixed template
INCLUDE: "item.template" total_tests := total_tests + 1;
TEXT: "</div>"' result := test_template_parse(
); $hemar8$<div>{{ for item in items }}<p>{{ item.name }}</p>{{ include item.template }}{{ end }}</div>$hemar8$,
IF result THEN $expected8$TEXT: "<div>"
passed_tests := passed_tests + 1; SECTION: iterator="item", collection="items"
RAISE NOTICE 'Test %: Complex mixed template - PASSED', total_tests; TEXT: "<p>"
ELSE INTERPOLATE: "item.name"
RAISE WARNING 'Test %: Complex mixed template - FAILED', total_tests; TEXT: "</p>"
END IF; INCLUDE: "item.template"
TEXT: "</div>"$expected8$
-- Test 9: Execute tag with complex SQL );
total_tests := total_tests + 1; IF result THEN
result := test_template_parse( passed_tests := passed_tests + 1;
'{{ exec RAISE NOTICE 'Test %: Complex mixed template - PASSED', total_tests;
IF condition THEN ELSE
RETURN ''value1''; RAISE WARNING 'Test %: Complex mixed template - FAILED', total_tests;
ELSE END IF;
RETURN ''value2'';
END IF; -- Test 9: Execute tag with complex SQL
}}', total_tests := total_tests + 1;
'EXECUTE: "IF condition THEN' result := test_template_parse(
); $template9${{ exec
IF result THEN IF condition THEN
passed_tests := passed_tests + 1; RETURN 'value1';
RAISE NOTICE 'Test %: Execute tag with complex SQL - PASSED', total_tests; ELSE
ELSE RETURN 'value2';
RAISE WARNING 'Test %: Execute tag with complex SQL - FAILED', total_tests; END IF;
END IF; }}$template9$,
$expected9$EXECUTE: "IF condition THEN
-- Test 10: Whitespace handling RETURN 'value1';
total_tests := total_tests + 1; ELSE
result := test_template_parse( RETURN 'value2';
'{{ spaced_var }}', END IF;"$expected9$
'INTERPOLATE: "spaced_var"' );
); IF result THEN
IF result THEN passed_tests := passed_tests + 1;
passed_tests := passed_tests + 1; RAISE NOTICE 'Test %: Execute tag with complex SQL - PASSED', total_tests;
RAISE NOTICE 'Test %: Whitespace handling - PASSED', total_tests; ELSE
ELSE RAISE WARNING 'Test %: Execute tag with complex SQL - FAILED', total_tests;
RAISE WARNING 'Test %: Whitespace handling - FAILED', total_tests; END IF;
END IF;
-- Test 10: Whitespace handling
-- Test 11: Multiple consecutive tags total_tests := total_tests + 1;
total_tests := total_tests + 1; result := test_template_parse(
result := test_template_parse( $hemar10${{ spaced_var }}$hemar10$,
'{{ var1 }}{{ var2 }}{{ var3 }}', $expected10$INTERPOLATE: "spaced_var"$expected10$
'INTERPOLATE: "var1" );
INTERPOLATE: "var2" IF result THEN
INTERPOLATE: "var3"' passed_tests := passed_tests + 1;
); RAISE NOTICE 'Test %: Whitespace handling - PASSED', total_tests;
IF result THEN ELSE
passed_tests := passed_tests + 1; RAISE WARNING 'Test %: Whitespace handling - FAILED', total_tests;
RAISE NOTICE 'Test %: Multiple consecutive tags - PASSED', total_tests; END IF;
ELSE
RAISE WARNING 'Test %: Multiple consecutive tags - FAILED', total_tests; -- Test 11: Multiple consecutive tags
END IF; total_tests := total_tests + 1;
result := test_template_parse(
-- Test 12: Section with multiple nested elements $hemar11${{ var1 }}{{ var2 }}{{ var3 }}$hemar11$,
total_tests := total_tests + 1; $expected11$INTERPOLATE: "var1"
result := test_template_parse( INTERPOLATE: "var2"
'{{ for item in items do }} INTERPOLATE: "var3"$expected11$
<h2>{{ item.title }}</h2> );
<p>{{ item.description }}</p> IF result THEN
{{ include item.footer }} passed_tests := passed_tests + 1;
{{ end }}', RAISE NOTICE 'Test %: Multiple consecutive tags - PASSED', total_tests;
'SECTION: iterator="item", collection="items" ELSE
TEXT: " RAISE WARNING 'Test %: Multiple consecutive tags - FAILED', total_tests;
<h2>" END IF;
INTERPOLATE: "item.title"
TEXT: "</h2> -- Test 12: Section with multiple nested elements
<p>" total_tests := total_tests + 1;
INTERPOLATE: "item.description" result := test_template_parse(
TEXT: "</p> $hemar12${{ for item in items }}
" <h2>{{ item.title }}</h2>
INCLUDE: "item.footer" <p>{{ item.description }}</p>
TEXT: " {{ include item.footer }}
"' {{ end }}$hemar12$,
); $expected12$SECTION: iterator="item", collection="items"
IF result THEN TEXT: "
passed_tests := passed_tests + 1; <h2>"
RAISE NOTICE 'Test %: Section with multiple nested elements - PASSED', total_tests; INTERPOLATE: "item.title"
ELSE TEXT: "</h2>
RAISE WARNING 'Test %: Section with multiple nested elements - FAILED', total_tests; <p>"
END IF; INTERPOLATE: "item.description"
TEXT: "</p>
-- Test 13: Empty template "
total_tests := total_tests + 1; INCLUDE: "item.footer"
result := test_template_parse( TEXT: "
'', "$expected12$
'TEXT: ""' );
); IF result THEN
IF result THEN passed_tests := passed_tests + 1;
passed_tests := passed_tests + 1; RAISE NOTICE 'Test %: Section with multiple nested elements - PASSED', total_tests;
RAISE NOTICE 'Test %: Empty template - PASSED', total_tests; ELSE
ELSE RAISE WARNING 'Test %: Section with multiple nested elements - FAILED', total_tests;
RAISE WARNING 'Test %: Empty template - FAILED', total_tests; END IF;
END IF;
-- Test 13: Empty template
-- Test 14: Just text, no tags total_tests := total_tests + 1;
total_tests := total_tests + 1; result := test_template_parse(
result := test_template_parse( '',
'Just plain text, no tags here.', 'TEXT: ""'
'TEXT: "Just plain text, no tags here."' );
); IF result THEN
IF result THEN passed_tests := passed_tests + 1;
passed_tests := passed_tests + 1; RAISE NOTICE 'Test %: Empty template - PASSED', total_tests;
RAISE NOTICE 'Test %: Just text, no tags - PASSED', total_tests; ELSE
ELSE RAISE WARNING 'Test %: Empty template - FAILED', total_tests;
RAISE WARNING 'Test %: Just text, no tags - FAILED', total_tests; END IF;
END IF;
-- Test 14: Just text, no tags
-- Test 15: Complex example from documentation total_tests := total_tests + 1;
total_tests := total_tests + 1; result := test_template_parse(
result := test_template_parse( $hemar14$Just plain text, no tags here.$hemar14$,
'<div>text before<div> $expected14$TEXT: "Just plain text, no tags here."$expected14$
);
{{ include inner_template }} IF result THEN
passed_tests := passed_tests + 1;
{{ name }} RAISE NOTICE 'Test %: Just text, no tags - PASSED', total_tests;
ELSE
{{ for item in array do }} RAISE WARNING 'Test %: Just text, no tags - FAILED', total_tests;
some text: {{ name2 }} END IF;
{{ item.name }}
{{ end }} -- Test 15: Complex example from documentation
total_tests := total_tests + 1;
<div>code insertion:</div> result := test_template_parse(
{{ exec $template15$<div>text before<div>
context + ''{"name3": "zalupa"}'';
{{ include inner_template }}
IF context->condition THEN
RAISE INFO ''some log''; {{ name }}
RETURN ''some text''; {{ for item in array }}
END some text: {{ name2 }}
RETURN ''some other text''; {{ item.name }}
}} {{ end }}
<div id="footer">...</div>', <div>code insertion:</div>
'TEXT: "<div>text before<div> // FIXME: IT NEED A SPECE PIZDEZZZZ
{{ exec
" context + '{"name3": "zalupa"}';
INCLUDE: "inner_template"
TEXT: " IF context->condition THEN
RAISE INFO 'some log';
"
INTERPOLATE: "name" RETURN 'some text';
TEXT: " END
RETURN 'some other text';
" }}
SECTION: iterator="item", collection="array"'
); <div id="footer">...</div>$template15$,
IF result THEN $expected15$TEXT: "<div>text before<div>
passed_tests := passed_tests + 1;
RAISE NOTICE 'Test %: Complex example from documentation - PASSED', total_tests; "
ELSE INCLUDE: "inner_template"
RAISE WARNING 'Test %: Complex example from documentation - FAILED', total_tests; TEXT: "
END IF;
"
-- Test 16: Multiple nested sections INTERPOLATE: "name"
total_tests := total_tests + 1; TEXT: "
result := test_template_parse(
'{{ for a in items do }} "
{{ for b in a.items do }} SECTION: iterator="item", collection="array"
{{ for c in b.items do }} TEXT: "
{{ c.name }} some text: "
{{ end }} INTERPOLATE: "name2"
{{ end }} TEXT: "
{{ end }}', "
'SECTION: iterator="a", collection="items" INTERPOLATE: "item.name"
TEXT: " TEXT: "
" "
SECTION: iterator="b", collection="a.items" TEXT: "
TEXT: "
" <div>code insertion:</div>
SECTION: iterator="c", collection="b.items" "
TEXT: " EXECUTE: "context + '{"name3": "zalupa"}';
"
INTERPOLATE: "c.name" IF context->condition THEN
TEXT: " RAISE INFO 'some log';
"'
); RETURN 'some text';
IF result THEN END
passed_tests := passed_tests + 1; RETURN 'some other text';"
RAISE NOTICE 'Test %: Multiple nested sections - PASSED', total_tests; TEXT: "
ELSE
RAISE WARNING 'Test %: Multiple nested sections - FAILED', total_tests; <div id=\"footer\">...</div>"$expected15$
END IF; );
IF result THEN
-- Test 17: Interpolation with special characters passed_tests := passed_tests + 1;
total_tests := total_tests + 1; RAISE NOTICE 'Test %: Complex example from documentation - PASSED', total_tests;
result := test_template_parse( ELSE
'{{ special@field }}', RAISE WARNING 'Test %: Complex example from documentation - FAILED', total_tests;
'INTERPOLATE: "special@field"' END IF;
);
IF result THEN -- Test 16: Multiple nested sections
passed_tests := passed_tests + 1; total_tests := total_tests + 1;
RAISE NOTICE 'Test %: Interpolation with special characters - PASSED', total_tests; result := test_template_parse(
ELSE '{{ for a in items }}
RAISE WARNING 'Test %: Interpolation with special characters - FAILED', total_tests; {{ for b in a.items }}
END IF; {{ for c in b.items }}
{{ c.name }}
-- Test 18: Section with complex iterator and collection names {{ end }}
total_tests := total_tests + 1; {{ end }}
result := test_template_parse( {{ end }}',
'{{ for complex_item.with.dots in complex_collection[0].items do }}{{ end }}', 'SECTION: iterator="a", collection="items"
'SECTION: iterator="complex_item.with.dots", collection="complex_collection[0].items"' TEXT: "
); "
IF result THEN SECTION: iterator="b", collection="a.items"
passed_tests := passed_tests + 1; TEXT: "
RAISE NOTICE 'Test %: Section with complex iterator and collection names - PASSED', total_tests; "
ELSE SECTION: iterator="c", collection="b.items"
RAISE WARNING 'Test %: Section with complex iterator and collection names - FAILED', total_tests; TEXT: "
END IF; "
INTERPOLATE: "c.name"
-- Test 19: Include with complex path TEXT: "
total_tests := total_tests + 1; "'
result := test_template_parse( );
'{{ include templates[0].nested.path }}', IF result THEN
'INCLUDE: "templates[0].nested.path"' passed_tests := passed_tests + 1;
); RAISE NOTICE 'Test %: Multiple nested sections - PASSED', total_tests;
IF result THEN ELSE
passed_tests := passed_tests + 1; RAISE WARNING 'Test %: Multiple nested sections - FAILED', total_tests;
RAISE NOTICE 'Test %: Include with complex path - PASSED', total_tests; END IF;
ELSE
RAISE WARNING 'Test %: Include with complex path - FAILED', total_tests; -- Test 17: Interpolation with special characters
END IF; total_tests := total_tests + 1;
result := test_template_parse(
-- Test 20: Execute with complex SQL and quotes '{{ special@field }}',
total_tests := total_tests + 1; 'INTERPOLATE: "special@field"'
result := test_template_parse( );
'{{ exec SELECT ''text with "double" quotes'' AS result; }}', IF result THEN
'EXECUTE: "SELECT ''text with "double" quotes'' AS result;"' passed_tests := passed_tests + 1;
); RAISE NOTICE 'Test %: Interpolation with special characters - PASSED', total_tests;
IF result THEN ELSE
passed_tests := passed_tests + 1; RAISE WARNING 'Test %: Interpolation with special characters - FAILED', total_tests;
RAISE NOTICE 'Test %: Execute with complex SQL and quotes - PASSED', total_tests; END IF;
ELSE
RAISE WARNING 'Test %: Execute with complex SQL and quotes - FAILED', total_tests; -- Test 18: Section with complex iterator and collection names
END IF; total_tests := total_tests + 1;
result := test_template_parse(
-- Print summary '{{ for complex_item.with.dots in complex_collection[0].items }}{{ end }}',
IF passed_tests = total_tests THEN 'SECTION: iterator="complex_item.with.dots", collection="complex_collection[0].items"'
RAISE NOTICE '------------------------------------'; );
RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)', IF result THEN
passed_tests, total_tests; passed_tests := passed_tests + 1;
RAISE NOTICE '------------------------------------'; RAISE NOTICE 'Test %: Section with complex iterator and collection names - PASSED', total_tests;
ELSE ELSE
RAISE WARNING '------------------------------------'; RAISE WARNING 'Test %: Section with complex iterator and collection names - FAILED', total_tests;
RAISE WARNING 'SUMMARY: % of % tests passed (%)', END IF;
passed_tests,
total_tests, -- Test 19: Include with complex path
round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%'; total_tests := total_tests + 1;
RAISE WARNING '------------------------------------'; result := test_template_parse(
END IF; '{{ include templates[0].nested.path }}',
'INCLUDE: "templates[0].nested.path"'
IF passed_tests != total_tests THEN );
RAISE EXCEPTION 'Tests failed: % of % tests did not pass', (total_tests - passed_tests), total_tests; IF result THEN
END IF; passed_tests := passed_tests + 1;
RAISE NOTICE 'Test %: Include with complex path - PASSED', total_tests;
ELSE
RAISE WARNING 'Test %: Include with complex path - FAILED', total_tests;
END IF;
-- Test 20: Execute with complex SQL and quotes
total_tests := total_tests + 1;
result := test_template_parse(
$template20$
{{ exec SELECT 'text with "double" quotes' AS result; }}
$template20$,
$expected20$EXECUTE: "SELECT 'text with "double" quotes' AS result;"$expected20$
);
IF result THEN
passed_tests := passed_tests + 1;
RAISE NOTICE 'Test %: Execute with complex SQL and quotes - PASSED', total_tests;
ELSE
RAISE WARNING 'Test %: Execute with complex SQL and quotes - FAILED', total_tests;
END IF;
-- Print summary
IF passed_tests = total_tests THEN
RAISE NOTICE '------------------------------------';
RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)',
passed_tests, total_tests;
RAISE NOTICE '------------------------------------';
ELSE
RAISE WARNING '------------------------------------';
RAISE WARNING 'SUMMARY: % of % tests passed (%)',
passed_tests,
total_tests,
round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%';
RAISE WARNING '------------------------------------';
END IF;
IF passed_tests != total_tests THEN
RAISE EXCEPTION 'Tests failed: % of % tests did not pass', (total_tests - passed_tests), total_tests;
END IF;
END $$; END $$;