From 743c34b4475e0c0bf061d82cb4dd3987cf51bf6e Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 00:40:29 +0300 Subject: [PATCH] parser: fix errors using conditionals in vweb templates (fixes #19475) --- vlib/v/TEMPLATES.md | 12 ++ vlib/v/parser/tmpl.v | 179 +++++++++--------- vlib/v/tests/tmpl/conditional_multi_line.html | 12 ++ .../v/tests/tmpl/conditional_single_line.html | 10 + vlib/v/tests/tmpl_test.v | 40 ++++ 5 files changed, 165 insertions(+), 88 deletions(-) create mode 100644 vlib/v/tests/tmpl/conditional_multi_line.html create mode 100644 vlib/v/tests/tmpl/conditional_single_line.html diff --git a/vlib/v/TEMPLATES.md b/vlib/v/TEMPLATES.md index bd8cd2ea1..2780a0410 100644 --- a/vlib/v/TEMPLATES.md +++ b/vlib/v/TEMPLATES.md @@ -8,6 +8,9 @@ to be used for other kinds of text output also. Each template directive begins with an `@` sign. Block directives are line-based: start with `@if`, `@for`, or `@else` on their own line, then close the block with `@end`, `@endif`, or `@endfor`. +HTML templates also support brace-delimited control blocks, so `@if cond { ... }`, +`@else { ... }`, and `@for item in items { ... }` can be closed with `}` instead. +Inline one-line bodies like `@if cond { shown }` are supported too. Other directives only have `''` (string) parameters. For example: @@ -33,6 +36,7 @@ For example: The if directive consists of the `@if` tag, the condition (using the same syntax as in V), and a block of template content. Close the block with `@end` or `@endif`. +In HTML templates, you can also use a brace-delimited block and close it with `}`. ``` @if @@ -48,6 +52,12 @@ Close the block with `@end` or `@endif`. @end ``` +```html +@if bool_val { + This is shown if bool_val is true +} +``` + You can also use `@else`: ```html @@ -76,6 +86,8 @@ where you can write text, rendered for each iteration of the loop: @end ``` +In HTML templates, you can also use `@for { ... }` and close it with `}`. + ### Example for @for ```html diff --git a/vlib/v/parser/tmpl.v b/vlib/v/parser/tmpl.v index 5bbc964b6..4649c468c 100644 --- a/vlib/v/parser/tmpl.v +++ b/vlib/v/parser/tmpl.v @@ -357,6 +357,7 @@ fn normalize_keyword_template_interpolations(line string) string { struct TmplControlLine { header string inline_body string + prefix string has_inline_body bool opens_brace_block bool closes_inline_block bool @@ -366,28 +367,39 @@ fn parse_tmpl_control_line(line string, directive string) TmplControlLine { pos := line.index(directive) or { return TmplControlLine{} } remainder := line[pos + directive.len..].trim_space() if remainder.len == 0 { - return TmplControlLine{} + return TmplControlLine{ + prefix: line[..pos] + } } if remainder.ends_with('{') { return TmplControlLine{ header: remainder[..remainder.len - 1].trim_space() + prefix: line[..pos] opens_brace_block: true } } if !remainder.ends_with('}') { return TmplControlLine{ header: remainder + prefix: line[..pos] + } + } + close_pos := remainder.last_index('}') or { + return TmplControlLine{ + header: remainder + prefix: line[..pos] + } + } + open_pos := remainder.index('{') or { + return TmplControlLine{ + header: remainder + prefix: line[..pos] } } - close_pos := remainder.last_index('}') or { return TmplControlLine{ - header: remainder - } } - open_pos := remainder.index('{') or { return TmplControlLine{ - header: remainder - } } return TmplControlLine{ header: remainder[..open_pos].trim_space() inline_body: remainder[open_pos + 1..close_pos].trim_space() + prefix: line[..pos] has_inline_body: open_pos + 1 < close_pos opens_brace_block: true closes_inline_block: true @@ -400,40 +412,52 @@ fn parse_tmpl_else_line(line string) TmplControlLine { if remainder.len == 0 { return TmplControlLine{ header: 'else' + prefix: line[..pos] } } if remainder.ends_with('{') { suffix := remainder[..remainder.len - 1].trim_space() return TmplControlLine{ header: if suffix.len == 0 { 'else' } else { 'else ${suffix}' } + prefix: line[..pos] opens_brace_block: true } } if !remainder.ends_with('}') { return TmplControlLine{ header: if remainder.len == 0 { 'else' } else { 'else ${remainder}' } + prefix: line[..pos] } } close_pos := remainder.last_index('}') or { return TmplControlLine{ header: if remainder.len == 0 { 'else' } else { 'else ${remainder}' } + prefix: line[..pos] } } open_pos := remainder.index('{') or { return TmplControlLine{ header: if remainder.len == 0 { 'else' } else { 'else ${remainder}' } + prefix: line[..pos] } } suffix := remainder[..open_pos].trim_space() return TmplControlLine{ header: if suffix.len == 0 { 'else' } else { 'else ${suffix}' } inline_body: remainder[open_pos + 1..close_pos].trim_space() + prefix: line[..pos] has_inline_body: open_pos + 1 < close_pos opens_brace_block: true closes_inline_block: true } } +enum TmplBraceBlockKind { + control + div + span +} + fn (mut p Parser) append_tmpl_line_info(template_file string, tmpl_line int, count int) { for _ in 0 .. count { p.template_line_map << ast.TemplateLineInfo{ @@ -608,9 +632,8 @@ fn veb_tmpl_${fn_name}() string { state = .html } - mut in_span := false mut in_html_comment := false - mut simple_brace_block_depth := 0 + mut brace_block_kinds := []TmplBraceBlockKind{} mut end_of_line_pos := 0 mut start_of_line_pos := 0 mut tline_number := -1 // keep the original line numbers, even after insert/delete ops on lines; `i` changes @@ -725,49 +748,39 @@ fn veb_tmpl_${fn_name}() string { i-- continue } - if state == .simple && simple_brace_block_depth > 0 && trimmed_line == '}' { + if trimmed_line == '}' && brace_block_kinds.len > 0 && brace_block_kinds.last() == .control { source.writeln(tmpl_str_end) // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty p.append_tmpl_line_info(template_file, tline_number, 2) source.writeln('}') p.append_tmpl_line_info(template_file, tline_number, 1) source.write_string(tmpl_str_start) - simple_brace_block_depth-- + brace_block_kinds.delete_last() continue } if line.contains('@if ') { - if state == .simple { - control := parse_tmpl_control_line(line, '@if') + control := parse_tmpl_control_line(line, '@if') + source.writeln(tmpl_str_end) + // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty + p.append_tmpl_line_info(template_file, tline_number, 2) + source.writeln('if ${control.header} {') + p.append_tmpl_line_info(template_file, tline_number, 1) + source.write_string(tmpl_str_start) + if control.has_inline_body { + source.writeln(insert_template_code(fn_name, tmpl_str_start, control.prefix + + control.inline_body)) + p.append_tmpl_line_info(template_file, tline_number, 1) + } + if control.closes_inline_block { source.writeln(tmpl_str_end) // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty p.append_tmpl_line_info(template_file, tline_number, 2) - source.writeln('if ${control.header} {') + source.writeln('}') p.append_tmpl_line_info(template_file, tline_number, 1) source.write_string(tmpl_str_start) - if control.has_inline_body { - source.writeln(insert_template_code(fn_name, tmpl_str_start, - control.inline_body)) - p.append_tmpl_line_info(template_file, tline_number, 1) - } - if control.closes_inline_block { - source.writeln(tmpl_str_end) - // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty - p.append_tmpl_line_info(template_file, tline_number, 2) - source.writeln('}') - p.append_tmpl_line_info(template_file, tline_number, 1) - source.write_string(tmpl_str_start) - } else if control.opens_brace_block { - simple_brace_block_depth++ - } - continue + } else if control.opens_brace_block { + brace_block_kinds << .control } - source.writeln(tmpl_str_end) - // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty - p.append_tmpl_line_info(template_file, tline_number, 2) - pos := line.index('@if') or { continue } - source.writeln('if ' + line[pos + 4..] + '{') - p.append_tmpl_line_info(template_file, tline_number, 1) - source.write_string(tmpl_str_start) continue } if line.contains('@end') { @@ -777,75 +790,60 @@ fn veb_tmpl_${fn_name}() string { source.writeln('}') p.append_tmpl_line_info(template_file, tline_number, 1) source.write_string(tmpl_str_start) + if brace_block_kinds.len > 0 && brace_block_kinds.last() == .control { + brace_block_kinds.delete_last() + } continue } if line.contains('@else') { - if state == .simple { - control := parse_tmpl_else_line(line) + control := parse_tmpl_else_line(line) + source.writeln(tmpl_str_end) + // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty + p.append_tmpl_line_info(template_file, tline_number, 2) + source.writeln('} ${control.header} {') + p.append_tmpl_line_info(template_file, tline_number, 1) + source.write_string(tmpl_str_start) + if control.has_inline_body { + source.writeln(insert_template_code(fn_name, tmpl_str_start, control.prefix + + control.inline_body)) + p.append_tmpl_line_info(template_file, tline_number, 1) + } + if control.closes_inline_block { source.writeln(tmpl_str_end) // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty p.append_tmpl_line_info(template_file, tline_number, 2) - source.writeln('} ${control.header} {') + source.writeln('}') p.append_tmpl_line_info(template_file, tline_number, 1) source.write_string(tmpl_str_start) - if control.has_inline_body { - source.writeln(insert_template_code(fn_name, tmpl_str_start, - control.inline_body)) - p.append_tmpl_line_info(template_file, tline_number, 1) + if brace_block_kinds.len > 0 && brace_block_kinds.last() == .control { + brace_block_kinds.delete_last() } - if control.closes_inline_block { - source.writeln(tmpl_str_end) - // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty - p.append_tmpl_line_info(template_file, tline_number, 2) - source.writeln('}') - p.append_tmpl_line_info(template_file, tline_number, 1) - source.write_string(tmpl_str_start) - } - continue } + continue + } + if line.contains('@for') { + control := parse_tmpl_control_line(line, '@for') source.writeln(tmpl_str_end) // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty p.append_tmpl_line_info(template_file, tline_number, 2) - pos := line.index('@else') or { continue } - source.writeln('}' + line[pos + 1..] + '{') + source.writeln('for ${control.header} {') p.append_tmpl_line_info(template_file, tline_number, 1) - // source.writeln(' } else { ') source.write_string(tmpl_str_start) - continue - } - if line.contains('@for') { - if state == .simple { - control := parse_tmpl_control_line(line, '@for') + if control.has_inline_body { + source.writeln(insert_template_code(fn_name, tmpl_str_start, control.prefix + + control.inline_body)) + p.append_tmpl_line_info(template_file, tline_number, 1) + } + if control.closes_inline_block { source.writeln(tmpl_str_end) // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty p.append_tmpl_line_info(template_file, tline_number, 2) - source.writeln('for ${control.header} {') + source.writeln('}') p.append_tmpl_line_info(template_file, tline_number, 1) source.write_string(tmpl_str_start) - if control.has_inline_body { - source.writeln(insert_template_code(fn_name, tmpl_str_start, - control.inline_body)) - p.append_tmpl_line_info(template_file, tline_number, 1) - } - if control.closes_inline_block { - source.writeln(tmpl_str_end) - // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty - p.append_tmpl_line_info(template_file, tline_number, 2) - source.writeln('}') - p.append_tmpl_line_info(template_file, tline_number, 1) - source.write_string(tmpl_str_start) - } else if control.opens_brace_block { - simple_brace_block_depth++ - } - continue + } else if control.opens_brace_block { + brace_block_kinds << .control } - source.writeln(tmpl_str_end) - // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty - p.append_tmpl_line_info(template_file, tline_number, 2) - pos := line.index('@for') or { continue } - source.writeln('for ' + line[pos + 4..] + '{') - p.append_tmpl_line_info(template_file, tline_number, 1) - source.write_string(tmpl_str_start) continue } if state == .simple { @@ -893,7 +891,7 @@ fn veb_tmpl_${fn_name}() string { tmpl_path: template_file tmpl_line: tline_number } - in_span = true + brace_block_kinds << .span continue } else if line_t.starts_with('.') && line.ends_with('{') { // `.header {` => `
` @@ -905,6 +903,7 @@ fn veb_tmpl_${fn_name}() string { tmpl_path: template_file tmpl_line: tline_number } + brace_block_kinds << .div continue } else if line_t.starts_with('#') && line.ends_with('{') { // `#header {` => `