fix: hemar: EBUCHIE WHITESPACЫ

This commit is contained in:
2025-05-19 13:59:26 +00:00
parent 6c953563d0
commit 7ce48d75f8
5 changed files with 168 additions and 265 deletions

View File

@@ -25,6 +25,76 @@ static void render_template(TemplateNode *node, Jsonb *define, StringInfo result
static void render_execute_tag(const char *code, Jsonb *define, StringInfo result, MemoryContext context);
static JsonbValue *jsonb_get_by_path_internal(Jsonb *jb, const char *path_str, MemoryContext context);
/* Helper function to find the end of the current line */
static const char *
find_line_end(const char *str)
{
const char *p = str;
while (*p && *p != '\n')
p++;
return p;
}
/* Helper function to check if a string ends with a newline */
static bool
ends_with_newline(const char *str, size_t len)
{
if (len == 0)
return false;
return str[len - 1] == '\n';
}
/* Helper function to find the start of the previous line */
static const char *
find_prev_line_start(const char *str, const char *current)
{
const char *p = current;
while (p > str && *(p - 1) != '\n')
p--;
return p;
}
/* Helper function to check if a tag is on its own line */
static bool
is_tag_on_own_line(const char *start, const char *tag_start, const TemplateConfig *config)
{
const char *line_start = find_prev_line_start(start, tag_start);
const char *p = line_start;
/* Check if there's only whitespace before the tag */
while (p < tag_start && isspace((unsigned char)*p))
p++;
if (p != tag_start)
return false;
/* Find the end of the tag */
p = tag_start;
while (*p && *p != '\n') {
if (strncmp(p, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) {
p += strlen(config->Syntax.Braces.close);
break;
}
p++;
}
/* Check if there's only whitespace or newline after the tag */
while (*p && *p != '\n' && isspace((unsigned char)*p))
p++;
return *p == '\n' || *p == '\0';
}
/* Helper function to trim newline from previous text node */
static void
trim_newline_from_prev_text(TemplateNode *node)
{
if (node && node->type == TEMPLATE_NODE_TEXT && node->value->text.content) {
size_t len = strlen(node->value->text.content);
if (ends_with_newline(node->value->text.content, len)) {
node->value->text.content[len - 1] = '\0';
}
}
}
char *tnt_to_string(TemplateNodeType type) {
switch (type) {
case TEMPLATE_NODE_SECTION: return "SECTION";
@@ -215,6 +285,7 @@ template_default_config(MemoryContext context)
config.Syntax.Section.control = "for";
config.Syntax.Section.source = "in";
config.Syntax.Section.begin = ""; /* No longer used, but keep for backward compatibility */
config.Syntax.Section.end = "end"; /* End tag for sections */
config.Syntax.Interpolate.invoke = "";
config.Syntax.Include.invoke = "include";
config.Syntax.Execute.invoke = "exec";
@@ -274,6 +345,14 @@ template_validate_config(const TemplateConfig *config, TemplateErrorCode *error_
return false;
}
/* Check section end */
if (!config->Syntax.Section.end || strlen(config->Syntax.Section.end) > TEMPLATE_MAX_PREFIX_LEN)
{
if (error_code)
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
return false;
}
/* Check interpolate invoke */
if (!config->Syntax.Interpolate.invoke || strlen(config->Syntax.Interpolate.invoke) > TEMPLATE_MAX_PREFIX_LEN)
{
@@ -376,7 +455,7 @@ template_parse_interpolation(MemoryContext context, const char **s_ptr,
/* Parse section tag */
static TemplateNode *
template_parse_section(MemoryContext context, const char **s_ptr,
const TemplateConfig *config, TemplateErrorCode *error_code)
const TemplateConfig *config, TemplateErrorCode *error_code, bool is_control_on_own_line, bool *is_end_on_own_line)
{
const char **s = s_ptr;
const char *iterator_start, *collection_start;
@@ -500,7 +579,7 @@ template_parse_section(MemoryContext context, const char **s_ptr,
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)
else if (strncmp(tag_ptr, config->Syntax.Section.end, strlen(config->Syntax.Section.end)) == 0)
{
/* Found an end tag, decrease nesting level */
nesting_level--;
@@ -528,14 +607,69 @@ template_parse_section(MemoryContext context, const char **s_ptr,
return NULL;
}
/* Extract the body content */
// FIXME(yukkop): This code is duplicate
char *end_tag_end = end_tag_start;
end_tag_end += strlen(config->Syntax.Braces.open);
end_tag_end = skip_whitespace(end_tag_end);
end_tag_end += strlen(config->Syntax.Section.end); /* Use configured end tag */
end_tag_end = skip_whitespace(end_tag_end);
/* Initialize body_len to the full content length */
size_t body_len = end_tag_start - body_start;
if ((*is_end_on_own_line = is_tag_on_own_line(end_tag_start, end_tag_end, config))) {
/* Find the start of the line containing the end tag */
const char *line_start = find_prev_line_start(end_tag_start, end_tag_end);
/* Update body_len to exclude the line containing the end tag */
body_len = line_start - body_start;
}
/* Extract the body content */
char *body_content = pnstrdup(body_start, body_len);
elog(DEBUG1, "TPS: Section body content: %s", body_content);
if (is_end_on_own_line) {
/* Find last newline by iterating from the end */
const char *p = body_content + body_len - 1;
while (p >= body_content) {
if (*p == '\n') {
/* Check if everything after this newline is whitespace */
const char *check = p + 1;
bool only_whitespace = true;
while (check < body_content + body_len) {
if (!isspace((unsigned char)*check)) {
only_whitespace = false;
break;
}
check++;
}
if (only_whitespace) {
/* Trim everything after the last newline */
body_len = p - body_content + 1; /* Include the newline */
body_content[body_len] = '\0';
}
break;
}
p--;
}
}
/* Parse the body content as a template */
const char *body_ptr = body_content;
if (is_control_on_own_line) {
/* Only trim whitespace and newline if control tag was on its own line */
while (*body_ptr && isspace((unsigned char)*body_ptr)) {
if (*body_ptr == '\n') {
body_ptr++;
break;
}
body_ptr++;
}
}
body_node = template_parse(context, &body_ptr, config, false, error_code);
if (!body_node)
@@ -553,7 +687,7 @@ template_parse_section(MemoryContext context, const char **s_ptr,
*s = end_tag_start;
*s += strlen(config->Syntax.Braces.open);
*s = skip_whitespace(*s);
*s += 3; /* Skip "end" */
*s += strlen(config->Syntax.Section.end); /* Use configured end tag */
*s = skip_whitespace(*s);
/* Check for closing brace of end tag */
@@ -736,203 +870,6 @@ template_parse_execute(MemoryContext context, const char **s_ptr,
return node;
}
/* Helper function to find the end of a tag */
static const char *
find_tag_end(const char *tag_start, const TemplateConfig *config)
{
const char *p = tag_start;
bool in_quotes = false;
char quote_char = 0;
/* Skip opening braces */
p += strlen(config->Syntax.Braces.open);
/* Skip whitespace after opening braces */
while (*p && isspace((unsigned char)*p))
p++;
/* Skip the tag keyword ("for" or "end") */
if (strncmp(p, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0)
{
p += strlen(config->Syntax.Section.control);
}
else if (strncmp(p, "end", 3) == 0)
{
p += 3;
}
else
{
return NULL; /* Not a control tag */
}
/* Skip whitespace after keyword */
while (*p && isspace((unsigned char)*p))
p++;
/* For "for" tags, skip the iterator and "in" parts */
if (strncmp(tag_start + strlen(config->Syntax.Braces.open), config->Syntax.Section.control,
strlen(config->Syntax.Section.control)) == 0)
{
/* Skip iterator name */
while (*p && !isspace((unsigned char)*p))
p++;
/* Skip whitespace */
while (*p && isspace((unsigned char)*p))
p++;
/* Skip "in" keyword */
if (strncmp(p, config->Syntax.Section.source, strlen(config->Syntax.Section.source)) == 0)
{
p += strlen(config->Syntax.Section.source);
/* Skip whitespace after "in" */
while (*p && isspace((unsigned char)*p))
p++;
/* Skip collection name */
while (*p && !isspace((unsigned char)*p))
{
if (*p == '"' || *p == '\'')
{
if (!in_quotes)
{
in_quotes = true;
quote_char = *p;
}
else if (*p == quote_char)
{
in_quotes = false;
}
}
p++;
}
}
}
/* Skip trailing whitespace */
while (*p && isspace((unsigned char)*p))
p++;
/* Find closing braces */
if (strncmp(p, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
{
return p + strlen(config->Syntax.Braces.close);
}
return NULL; /* Invalid tag format */
}
/* Helper function to check if a line contains only whitespace and a control tag */
static bool
is_control_tag_only_line(const char *line_start, const char *line_end, const char *tag_start, const char *tag_end)
{
const char *p;
/* Check whitespace before tag */
for (p = line_start; p < tag_start; p++)
{
if (!isspace((unsigned char)*p))
return false;
}
/* Check whitespace after tag */
for (p = tag_end; p < line_end; p++)
{
if (!isspace((unsigned char)*p))
return false;
}
return true;
}
/* Helper function to find the start of the current line */
static const char *
find_line_start(const char *text, const char *current_pos)
{
const char *p = current_pos;
/* Move backwards until we find a newline or the start of text */
while (p > text && *(p-1) != '\n')
p--;
return p;
}
/* Helper function to find the end of the current line */
static const char *
find_line_end(const char *text)
{
const char *p = text;
/* Move forwards until we find a newline or the end of text */
while (*p && *p != '\n')
p++;
return p;
}
/* Helper function to check if a tag is a control tag */
static bool
is_control_tag(const char *tag_start, const TemplateConfig *config)
{
const char *p = tag_start;
/* Skip opening braces and whitespace */
p += strlen(config->Syntax.Braces.open);
while (*p && isspace((unsigned char)*p))
p++;
/* Check for "for" or "end" */
return (strncmp(p, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0 ||
strncmp(p, "end", 3) == 0);
}
/* Helper function to trim whitespace around control tags */
static void
trim_control_tag_whitespace(const char **s_ptr, const TemplateConfig *config)
{
const char **s = s_ptr;
const char *line_start, *line_end, *tag_start, *tag_end;
/* Find the start of the current line */
line_start = find_line_start(*s - 100, *s); /* Look back up to 100 chars for line start */
if (line_start < *s - 100)
line_start = *s; /* If we couldn't find line start, use current position */
/* Find the end of the current line */
line_end = find_line_end(*s);
/* Find the tag boundaries */
tag_start = *s;
tag_end = find_tag_end(tag_start, config);
if (!tag_end)
return; /* Not a valid control tag */
/* Check if this is a control tag on its own line */
if (is_control_tag_only_line(line_start, line_end, tag_start, tag_end))
{
/* For opening tags, remove whitespace and newline after the tag */
if (strncmp(tag_start + strlen(config->Syntax.Braces.open), config->Syntax.Section.control,
strlen(config->Syntax.Section.control)) == 0)
{
/* Skip to the end of the line */
while (*tag_end && *tag_end != '\n')
tag_end++;
if (*tag_end == '\n')
tag_end++; /* Skip the newline */
*s = tag_end;
}
/* For closing tags, remove whitespace and newline before the tag */
else if (strncmp(tag_start + strlen(config->Syntax.Braces.open), "end", 3) == 0)
{
/* Move back to start of line */
*s = line_start;
}
}
}
/* Main template parser function */
TemplateNode *
template_parse(MemoryContext context, const char **s, const TemplateConfig *config,
@@ -966,7 +903,7 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
while (*s && **s != '\0')
{
/* Check for closing brace in inner parse */
if (inner_parse && strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
if (inner_parse && strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
{
break;
}
@@ -993,9 +930,6 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
current_node_filled = true;
}
/* Check for control tag and trim whitespace if needed */
trim_control_tag_whitespace(s, config);
/* Parse the tag */
tag_node = NULL;
tag_prefix = *s + strlen(config->Syntax.Braces.open);
@@ -1042,7 +976,18 @@ template_parse(MemoryContext context, const char **s, const TemplateConfig *conf
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);
/* Check if this is a section tag on its own line */
bool is_end_on_own_line = false,
is_control_on_own_line = is_tag_on_own_line(start, *s, config);
if (tag_node && is_control_on_own_line) {
/* If we have a previous text node, trim its trailing newline */
trim_newline_from_prev_text(current);
}
/* Parse the section tag */
tag_node = template_parse_section(context, s, config, error_code, is_control_on_own_line, &is_end_on_own_line);
} else if (matched_type == 2) {
/* Include tag */
elog(LOG, "TPE: Parsing include tag at position: %.50s", *s);

View File

@@ -50,6 +50,7 @@ typedef struct {
const char *control; /* default: "for " */
const char *source; /* default: "in " */
const char *begin; /* default: "do " */
const char *end; /* default: "end" */
} Section;
struct {
const char *invoke; /* default: "" */

View File

@@ -16,7 +16,7 @@ BEGIN;
CREATE OR REPLACE FUNCTION pg_temp.test_regexp_replace(string text) RETURNS text AS $$
BEGIN
RETURN regexp_replace(regexp_replace(
RETURN regexp_replace(
regexp_replace(
regexp_replace(
regexp_replace(
@@ -24,8 +24,7 @@ BEGIN;
E'\n', '\\n', 'g'),
E'\r', '\\r', 'g'),
' ', '[S]', 'g'),
'\s', '\\s', 'g'), '\\n', '\\n
');
'\s', '\\s', 'g');
END;
$$ LANGUAGE plpgsql;

View File

@@ -50,7 +50,8 @@ BEGIN
{"id": 3, "value": 300}
]
}'::jsonb,
$template$Items:{{ for item in items }}
$template$Items:
{{ for item in items }}
Item {{ item.id }}: {{ exec RETURN (context->'item'->>'value')::int * 2; }}
{{ end }}$template$
);
@@ -239,48 +240,6 @@ BEGIN
RAISE WARNING 'Test % failed: Error: %', total_tests, SQLERRM;
END;
-- Test 4: Template with execute tag using context from section
total_tests := total_tests + 1;
BEGIN
test_result := hemar.render(
'{
"items": [
{"id": 1, "value": 100},
{"id": 2, "value": 200},
{"id": 3, "value": 300}
]
}'::jsonb,
$template$Items:
{{ for item in items }}
Item {{ item.id }}: {{ exec
DECLARE
v_value INT;
BEGIN
v_value := (context->>'value')::int;
RETURN v_value * 2;
END;
}}
{{ end }}$template$
);
expected := 'Items:
Item 1: 200
Item 2: 400
Item 3: 600
';
passed := test_result = expected;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Template with execute tag using context from section: PASSED', total_tests;
ELSE
RAISE WARNING 'Test %: Template with execute tag using context from section: FAILED. Expected "%", got "%"',
total_tests, expected, test_result;
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'Test % failed: Error: %', total_tests, SQLERRM;
END;
-- Print summary
IF passed_tests = total_tests THEN
RAISE NOTICE '------------------------------------';

View File

@@ -217,7 +217,8 @@ BEGIN
);
expected := ' item
item
item';
item
';
IF test_result = expected THEN
RAISE NOTICE 'Test %: Section whitespaces 2: PASSED', total_tests;
passed_tests := passed_tests + 1;
@@ -238,7 +239,8 @@ BEGIN
);
expected := ' item
item
item';
item
';
IF test_result = expected THEN
RAISE NOTICE 'Test %: Section whitespaces 3: PASSED', total_tests;
passed_tests := passed_tests + 1;
@@ -257,9 +259,7 @@ BEGIN
'{{for item in array}}
item {{end}}'
);
expected := ' item
item
item';
expected := ' item item item ';
IF test_result = expected THEN
RAISE NOTICE 'Test %: Section whitespaces 4: PASSED', total_tests;
passed_tests := passed_tests + 1;
@@ -278,24 +278,23 @@ BEGIN
'{{for item in array}}
item
{{end}}
'
'
);
expected := ' item
item
item
';
IF test_result = expected THEN
RAISE NOTICE 'Test %: Section whitespaces 5: PASSED', total_tests;
passed_tests := passed_tests + 1;
ELSE
RAISE WARNING 'Test %: Section whitespaces 5: FAILED. Expected "%", got "%"', total_tests, expected, test_result;
RAISE WARNING 'Test %: Section whitespaces 5: FAILED. Expected "%", got "%"', total_tests, pg_temp.test_regexp_replace(expected), pg_temp.test_regexp_replace(test_result);
END IF;
EXCEPTION WHEN OTHERS THEN
RAISE WARNING 'Test %: Section whitespaces 5: FAILED with error: %', total_tests, SQLERRM;
END;
-- Print summary
IF passed_tests = total_tests THEN
RAISE NOTICE '------------------------------------';