v2 / vlib / v / parser / tmpl.v
1074 lines · 1024 sloc · 29.34 KB · e754d70dc085a80ee25618911ed4cdcf9cd6e635
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module parser
5
6import v.ast
7import v.token
8import v.errors
9import os
10import strings
11
12enum State {
13 simple // default - no special interpretation of tags, *at all*!
14 // That is suitable for the general case of text template interpolation,
15 // for example for interpolating arbitrary source code (even V source) templates.
16 //
17 html // default, only when the template extension is .html
18 css // <style>
19 js // <script>
20 // span // span.{
21}
22
23fn (mut state State) update(line string) {
24 trimmed_line := line.trim_space()
25 if is_html_open_tag('style', line) {
26 state = .css
27 } else if trimmed_line == '</style>' {
28 state = .html
29 } else if is_html_open_tag('script', line) {
30 state = .js
31 } else if trimmed_line == '</script>' {
32 state = .html
33 }
34}
35
36const tmpl_str_end = "')\n"
37const tmpl_literal_dollar_marker = '__V_TMPL_LITERAL_DOLLAR__'
38
39// check HTML open tag `<name attr="x" >`
40fn is_html_open_tag(name string, s string) bool {
41 trimmed_line := s.trim_space()
42 mut len := trimmed_line.len
43
44 if len < name.len {
45 return false
46 }
47
48 mut sub := trimmed_line[0..1]
49 if sub != '<' { // not start with '<'
50 return false
51 }
52 sub = trimmed_line[len - 1..len]
53 if sub != '>' { // not end with '<'
54 return false
55 }
56 sub = trimmed_line[len - 2..len - 1]
57 if sub == '/' { // self-closing
58 return false
59 }
60 sub = trimmed_line[1..len - 1]
61 if sub.contains_any('<>') { // `<name <bad> >`
62 return false
63 }
64 if sub == name { // `<name>`
65 return true
66 } else {
67 len = name.len
68 if sub.len <= len { // `<nam>` or `<meme>`
69 return false
70 }
71 if sub[..len + 1] != '${name} ' { // not `<name ...>`
72 return false
73 }
74 return true
75 }
76}
77
78fn is_tmpl_ident_start(c u8) bool {
79 return c.is_letter() || c == `_`
80}
81
82fn is_tmpl_ident_part(c u8) bool {
83 return c.is_letter() || c.is_digit() || c == `_`
84}
85
86fn find_tmpl_balanced_end(line string, start int, open u8, close u8) int {
87 if start >= line.len || line[start] != open {
88 return -1
89 }
90 mut depth := 0
91 mut i := start
92 mut in_single_quote := false
93 mut in_double_quote := false
94 for i < line.len {
95 ch := line[i]
96 if ch == `\\` {
97 i += 2
98 continue
99 }
100 if in_single_quote {
101 if ch == `'` {
102 in_single_quote = false
103 }
104 i++
105 continue
106 }
107 if in_double_quote {
108 if ch == `"` {
109 in_double_quote = false
110 }
111 i++
112 continue
113 }
114 if ch == `'` {
115 in_single_quote = true
116 i++
117 continue
118 }
119 if ch == `"` {
120 in_double_quote = true
121 i++
122 continue
123 }
124 if ch == open {
125 depth++
126 } else if ch == close {
127 depth--
128 if depth == 0 {
129 return i + 1
130 }
131 }
132 i++
133 }
134 return -1
135}
136
137fn find_tmpl_complex_at_expr_end(line string, start int) int {
138 mut i := start
139 if i >= line.len || !is_tmpl_ident_start(line[i]) {
140 return -1
141 }
142 i++
143 for i < line.len && is_tmpl_ident_part(line[i]) {
144 i++
145 }
146 mut has_complex_suffix := false
147 for i + 1 < line.len && line[i] == `.` && is_tmpl_ident_start(line[i + 1]) {
148 i += 2
149 has_complex_suffix = true
150 for i < line.len && is_tmpl_ident_part(line[i]) {
151 i++
152 }
153 }
154 for i < line.len {
155 if line[i] == `[` {
156 expr_end := find_tmpl_balanced_end(line, i, `[`, `]`)
157 if expr_end == -1 {
158 return -1
159 }
160 i = expr_end
161 has_complex_suffix = true
162 continue
163 }
164 if line[i] == `(` {
165 expr_end := find_tmpl_balanced_end(line, i, `(`, `)`)
166 if expr_end == -1 {
167 return -1
168 }
169 i = expr_end
170 has_complex_suffix = true
171 continue
172 }
173 if i + 1 < line.len && line[i] == `.` && is_tmpl_ident_start(line[i + 1]) {
174 i += 2
175 for i < line.len && is_tmpl_ident_part(line[i]) {
176 i++
177 }
178 continue
179 }
180 break
181 }
182 if !has_complex_suffix {
183 return -1
184 }
185 return i
186}
187
188fn rewrite_complex_template_at_expressions(line string) string {
189 mut b := strings.new_builder(line.len + 8)
190 mut i := 0
191 for i < line.len {
192 if line[i] != `@` {
193 b.write_u8(line[i])
194 i++
195 continue
196 }
197 if i > 0 && line[i - 1] == `\\` {
198 b.write_u8(`@`)
199 i++
200 continue
201 }
202 if i + 1 >= line.len {
203 b.write_u8(`@`)
204 i++
205 continue
206 }
207 next := line[i + 1]
208 if next == `@` || next == `{` {
209 b.write_u8(`@`)
210 i++
211 continue
212 }
213 if next == `(` {
214 expr_end := find_tmpl_balanced_end(line, i + 1, `(`, `)`)
215 if expr_end != -1 && expr_end > i + 2 {
216 b.write_string('@{')
217 b.write_string(line[i + 2..expr_end - 1])
218 b.write_u8(`}`)
219 i = expr_end
220 continue
221 }
222 b.write_u8(`@`)
223 i++
224 continue
225 }
226 expr_end := find_tmpl_complex_at_expr_end(line, i + 1)
227 if expr_end != -1 {
228 b.write_string('@{')
229 b.write_string(line[i + 1..expr_end])
230 b.write_u8(`}`)
231 i = expr_end
232 continue
233 }
234 b.write_u8(`@`)
235 i++
236 }
237 return b.str()
238}
239
240fn escape_bare_tmpl_dollar_interpolations(line string) string {
241 mut sb := strings.new_builder(line.len)
242 mut i := 0
243 for i < line.len {
244 if i + 1 < line.len && ((line[i] == `@` && line[i + 1] == `{`)
245 || (line[i] == `$` && line[i + 1] == `{`)) {
246 expr_end := find_tmpl_balanced_end(line, i + 1, `{`, `}`)
247 if expr_end != -1 {
248 sb.write_string(line[i..expr_end])
249 i = expr_end
250 continue
251 }
252 }
253 if line[i] == `$` && i + 1 < line.len && is_tmpl_ident_start(line[i + 1]) {
254 sb.write_string(tmpl_literal_dollar_marker)
255 i++
256 continue
257 }
258 sb.write_u8(line[i])
259 i++
260 }
261 return sb.str()
262}
263
264fn insert_template_code(fn_name string, tmpl_str_start string, line string) string {
265 // HTML, may include `@var`
266 // escaped by cgen, unless it's a `veb.RawHtml` string
267 trailing_bs := tmpl_str_end + 'sb_${fn_name}.write_u8(92)\n' + tmpl_str_start
268 literal_dollar := tmpl_str_end + 'sb_${fn_name}.write_u8(36)\n' + tmpl_str_start
269 rewritten_line :=
270 escape_bare_tmpl_dollar_interpolations(rewrite_complex_template_at_expressions(line))
271 mut sb := strings.new_builder(rewritten_line.len + 16)
272 mut i := 0
273 for i < rewritten_line.len {
274 ch := rewritten_line[i]
275 match ch {
276 `\\` {
277 sb.write_string('\\\\')
278 i++
279 continue
280 }
281 `'` {
282 sb.write_string("\\'")
283 i++
284 continue
285 }
286 `@` {
287 if i + 1 < rewritten_line.len && rewritten_line[i + 1] == `@` {
288 sb.write_u8(`@`)
289 i += 2
290 continue
291 }
292 if i + 1 < rewritten_line.len {
293 next := rewritten_line[i + 1]
294 if next == `{` {
295 sb.write_u8(`$`)
296 i++
297 continue
298 }
299 if is_tmpl_ident_start(next) {
300 // Bare @ident: find the end of the identifier and wrap with ${}
301 mut end := i + 2
302 for end < rewritten_line.len && is_tmpl_ident_part(rewritten_line[end]) {
303 end++
304 }
305 sb.write_string('\${')
306 sb.write_string(rewritten_line[i + 1..end])
307 sb.write_u8(`}`)
308 i = end
309 continue
310 }
311 }
312 sb.write_u8(`@`)
313 i++
314 continue
315 }
316 `$` {
317 if i + 1 < rewritten_line.len && rewritten_line[i + 1] == `$` {
318 sb.write_string(r'\@')
319 i += 2
320 continue
321 }
322 }
323 else {}
324 }
325
326 sb.write_u8(ch)
327 i++
328 }
329 mut rline := sb.str()
330 rline = normalize_keyword_template_interpolations(rline)
331 comptime_call_str := rline.find_between('\${', '}')
332 if comptime_call_str.contains("\\'") {
333 rline = rline.replace(comptime_call_str, comptime_call_str.replace("\\'", r"'"))
334 }
335 rline = rline.replace(tmpl_literal_dollar_marker, literal_dollar)
336 if rline.ends_with('\\') {
337 rline = rline[0..rline.len - 2] + trailing_bs
338 }
339 return rline
340}
341
342fn normalize_keyword_template_interpolations(line string) string {
343 mut sb := strings.new_builder(line.len)
344 mut i := 0
345 for i < line.len {
346 ch := line[i]
347 if ch == `$` && i > 0 && line[i - 1] == `\\` {
348 sb.write_u8(ch)
349 i++
350 continue
351 }
352 if ch == `$` && i + 1 < line.len && (line[i + 1].is_letter() || line[i + 1] == `_`) {
353 mut j := i + 1
354 for j < line.len && (line[j].is_letter() || line[j].is_digit() || line[j] == `_`) {
355 j++
356 }
357 name := line[i + 1..j]
358 if token.is_key(name) {
359 // Force keyword names into the escaped identifier form to avoid parser/scanner issues.
360 sb.write_string('\${@${name}}')
361 i = j
362 continue
363 }
364 }
365 sb.write_u8(ch)
366 i++
367 }
368 return sb.str()
369}
370
371struct TmplControlLine {
372 header string
373 inline_body string
374 prefix string
375 has_inline_body bool
376 opens_brace_block bool
377 closes_inline_block bool
378}
379
380fn parse_tmpl_control_line(line string, directive string) TmplControlLine {
381 pos := line.index(directive) or { return TmplControlLine{} }
382 remainder := line[pos + directive.len..].trim_space()
383 if remainder.len == 0 {
384 return TmplControlLine{
385 prefix: line[..pos]
386 }
387 }
388 if remainder.ends_with('{') {
389 return TmplControlLine{
390 header: remainder[..remainder.len - 1].trim_space()
391 prefix: line[..pos]
392 opens_brace_block: true
393 }
394 }
395 if !remainder.ends_with('}') {
396 return TmplControlLine{
397 header: remainder
398 prefix: line[..pos]
399 }
400 }
401 close_pos := remainder.last_index('}') or {
402 return TmplControlLine{
403 header: remainder
404 prefix: line[..pos]
405 }
406 }
407 open_pos := remainder.index('{') or {
408 return TmplControlLine{
409 header: remainder
410 prefix: line[..pos]
411 }
412 }
413 return TmplControlLine{
414 header: remainder[..open_pos].trim_space()
415 inline_body: remainder[open_pos + 1..close_pos].trim_space()
416 prefix: line[..pos]
417 has_inline_body: open_pos + 1 < close_pos
418 opens_brace_block: true
419 closes_inline_block: true
420 }
421}
422
423fn parse_tmpl_else_line(line string) TmplControlLine {
424 pos := line.index('@else') or { return TmplControlLine{} }
425 remainder := line[pos + '@else'.len..].trim_space()
426 if remainder.len == 0 {
427 return TmplControlLine{
428 header: 'else'
429 prefix: line[..pos]
430 }
431 }
432 if remainder.ends_with('{') {
433 suffix := remainder[..remainder.len - 1].trim_space()
434 return TmplControlLine{
435 header: if suffix.len == 0 { 'else' } else { 'else ${suffix}' }
436 prefix: line[..pos]
437 opens_brace_block: true
438 }
439 }
440 if !remainder.ends_with('}') {
441 return TmplControlLine{
442 header: if remainder.len == 0 { 'else' } else { 'else ${remainder}' }
443 prefix: line[..pos]
444 }
445 }
446 close_pos := remainder.last_index('}') or {
447 return TmplControlLine{
448 header: if remainder.len == 0 { 'else' } else { 'else ${remainder}' }
449 prefix: line[..pos]
450 }
451 }
452 open_pos := remainder.index('{') or {
453 return TmplControlLine{
454 header: if remainder.len == 0 { 'else' } else { 'else ${remainder}' }
455 prefix: line[..pos]
456 }
457 }
458 suffix := remainder[..open_pos].trim_space()
459 return TmplControlLine{
460 header: if suffix.len == 0 { 'else' } else { 'else ${suffix}' }
461 inline_body: remainder[open_pos + 1..close_pos].trim_space()
462 prefix: line[..pos]
463 has_inline_body: open_pos + 1 < close_pos
464 opens_brace_block: true
465 closes_inline_block: true
466 }
467}
468
469enum TmplBraceBlockKind {
470 control
471 div
472 span
473}
474
475fn (mut p Parser) append_tmpl_line_info(template_file string, tmpl_line int, count int) {
476 for _ in 0 .. count {
477 p.template_line_map << ast.TemplateLineInfo{
478 tmpl_path: template_file
479 tmpl_line: tmpl_line
480 }
481 }
482}
483
484// struct to track dependecies and cache templates for reuse without io
485struct DependencyCache {
486pub mut:
487 dependencies map[string][]string
488 cache map[string][]string
489}
490
491// custom error to handle issues when including template files
492struct IncludeError {
493 Error
494pub:
495 calling_file string
496 line_nr int
497 position int
498 col u16
499 message string
500}
501
502fn (err IncludeError) msg() string {
503 return err.message
504}
505
506fn (err IncludeError) line_nr() int {
507 return err.line_nr
508}
509
510fn (err IncludeError) pos() int {
511 return err.position
512}
513
514fn (err IncludeError) calling_file() string {
515 return err.calling_file
516}
517
518fn (err IncludeError) col() u16 {
519 return err.col
520}
521
522struct TmplInclude {
523 path string
524 position int
525}
526
527fn tmpl_include_path_error(calling_file string, line_nr int, position int) IError {
528 return &IncludeError{
529 calling_file: calling_file
530 line_nr: line_nr
531 position: position
532 col: u16(position)
533 message: 'path for @include must be quoted with \' or "'
534 }
535}
536
537fn parse_tmpl_include_path(calling_file string, line_nr int, line string) !TmplInclude {
538 include_pos := line.index('@include ') or { 0 }
539 mut quote_pos := include_pos + '@include '.len
540 for quote_pos < line.len && line[quote_pos].is_space() {
541 quote_pos++
542 }
543 if quote_pos >= line.len || (line[quote_pos] != `'` && line[quote_pos] != `"`) {
544 return tmpl_include_path_error(calling_file, line_nr, quote_pos)
545 }
546 quote := line[quote_pos]
547 mut end_pos := quote_pos + 1
548 for end_pos < line.len && line[end_pos] != quote {
549 end_pos++
550 }
551 if end_pos >= line.len {
552 return tmpl_include_path_error(calling_file, line_nr, quote_pos)
553 }
554 return TmplInclude{
555 path: line[quote_pos + 1..end_pos]
556 position: quote_pos
557 }
558}
559
560fn (mut p Parser) process_includes(calling_file string, line_number int, line string, mut dc DependencyCache) ![]string {
561 base_path := os.dir(calling_file)
562 mut tline_number := line_number
563 include := parse_tmpl_include_path(calling_file, tline_number, line)!
564 mut file_name := include.path
565 mut file_ext := os.file_ext(file_name)
566 if file_ext == '' {
567 file_ext = '.html'
568 }
569 file_name = file_name.replace(file_ext, '')
570 mut file_path := os.real_path(os.join_path_single(base_path, '${file_name}${file_ext}'))
571
572 if !os.exists(file_path) && !file_name.contains('../') {
573 // the calling file is probably original way (relative to calling file) and works from the root folder
574 path_arr := base_path.split_any('/\\')
575 idx := path_arr.index('templates')
576 root_path := path_arr[..idx + 1].join('/')
577 file_name = file_name.rsplit('../')[0]
578 file_path = os.real_path(os.join_path_single(root_path, '${file_name}${file_ext}'))
579 }
580
581 // If file hasnt been called before then add to dependency tree
582 if file_path !in dc.dependencies {
583 dc.dependencies[file_path] = []string{}
584 }
585 if !dc.dependencies[file_path].contains(calling_file) {
586 dc.dependencies[file_path] << calling_file
587 }
588
589 // Circular import detection
590 for callee in dc.dependencies[file_path] {
591 if dc.dependencies[callee].contains(file_path) {
592 return &IncludeError{
593 calling_file: calling_file
594 line_nr: tline_number
595 position: line.index('@include ') or { 0 }
596 message: 'A recursive call is being made on template ${file_name}'
597 }
598 }
599 }
600 mut file_content := []string{}
601 if file_path in dc.cache {
602 file_content = dc.cache[file_path]
603 } else {
604 file_content = os.read_lines(file_path) or {
605 position := include.position
606 return &IncludeError{
607 calling_file: calling_file
608 line_nr: tline_number // line_number
609 position: position
610 message: 'Reading file `${file_name}` from path: ${file_path} failed'
611 }
612 }
613 }
614 // no errors detected in calling file - reset tline_number (error reporting)
615 tline_number = 1
616
617 // loop over the imported file
618 for i, l in file_content {
619 if l.contains('@include ') {
620 processed := p.process_includes(file_path, tline_number, l, mut dc) or { return err }
621 file_content.delete(i) // remove the include line
622 for processed_line in processed.reverse() {
623 file_content.insert(i, processed_line)
624 tline_number--
625 }
626 }
627 }
628 // Add template to parser for reloading
629 p.template_paths << file_path
630 // Add the imported template to the cache
631 dc.cache[file_path] = file_content
632 return file_content
633}
634
635// compile_file compiles the content of a file by the given path as a template
636pub fn (mut p Parser) compile_template_file(template_file string, fn_name string) string {
637 mut lines := os.read_lines(template_file) or {
638 p.error('reading from ${template_file} failed')
639 return ''
640 }
641 p.template_paths << template_file
642 // create a new Dependency tree & cache any templates to avoid further io
643 mut dc := DependencyCache{}
644 lstartlength := lines.len * 30
645 tmpl_str_start := "\tsb_${fn_name}.write_string('"
646 mut source := strings.new_builder(1000)
647
648 // Reset line mapping for this template compilation
649 p.template_line_map = []
650
651 source.writeln('
652import strings
653// === veb html template for file: ${template_file} ===
654fn veb_tmpl_${fn_name}() string {
655 mut sb_${fn_name} := strings.new_builder(${lstartlength})\n
656
657')
658 // Header adds 8 lines (0: empty, 1: import, 2: comment, 3: fn, 4: builder, 5: empty from \n escape, 6: empty from literal, 7: empty from writeln)
659 // Pre-fill the line map with placeholder entries for header lines
660 for _ in 0 .. 8 {
661 p.template_line_map << ast.TemplateLineInfo{
662 tmpl_path: template_file
663 tmpl_line: 0
664 }
665 }
666
667 source.write_string(tmpl_str_start)
668
669 mut state := State.simple
670 template_ext := os.file_ext(template_file)
671 if template_ext.to_lower_ascii() == '.html' {
672 state = .html
673 }
674
675 mut in_html_comment := false
676 mut brace_block_kinds := []TmplBraceBlockKind{}
677 mut end_of_line_pos := 0
678 mut start_of_line_pos := 0
679 mut tline_number := -1 // keep the original line numbers, even after insert/delete ops on lines; `i` changes
680 for i := 0; i < lines.len; i++ {
681 line := lines[i]
682 trimmed_line := line.trim_space()
683 tline_number++
684 start_of_line_pos = end_of_line_pos
685 end_of_line_pos += line.len + 1
686 if state != .simple {
687 state.update(line)
688 }
689 $if trace_tmpl ? {
690 eprintln('>>> tfile: ${template_file}, spos: ${start_of_line_pos:6}, epos:${end_of_line_pos:6}, fi: ${tline_number:5}, i: ${i:5}, state: ${state:10}, line: ${line}')
691 }
692 // Track HTML comments: skip @-interpolation inside <!-- ... -->
693 if state == .html {
694 if in_html_comment {
695 if line.contains('-->') {
696 in_html_comment = false
697 }
698 // Output comment line literally (no @-interpolation)
699 escaped := line.replace('\\', '\\\\').replace("'", "\\'")
700 source.writeln(escaped)
701 p.template_line_map << ast.TemplateLineInfo{
702 tmpl_path: template_file
703 tmpl_line: tline_number
704 }
705 continue
706 }
707 if line.contains('<!--') {
708 if !line.contains('-->') {
709 in_html_comment = true
710 }
711 // Single-line or start of multi-line comment: output literally
712 escaped := line.replace('\\', '\\\\').replace("'", "\\'")
713 source.writeln(escaped)
714 p.template_line_map << ast.TemplateLineInfo{
715 tmpl_path: template_file
716 tmpl_line: tline_number
717 }
718 continue
719 }
720 }
721 if line.contains('@header') {
722 position := line.index('@header') or { 0 }
723 p.error_with_error(errors.Error{
724 message: "Please use @include 'header' instead of @header (deprecated)"
725 file_path: template_file
726 pos: token.Pos{
727 len: '@header'.len
728 line_nr: tline_number
729 pos: start_of_line_pos + position
730 last_line: lines.len
731 }
732 reporter: .parser
733 })
734 continue
735 }
736 if line.contains('@footer') {
737 position := line.index('@footer') or { 0 }
738 p.error_with_error(errors.Error{
739 message: "Please use @include 'footer' instead of @footer (deprecated)"
740 file_path: template_file
741 pos: token.Pos{
742 len: '@footer'.len
743 line_nr: tline_number
744 pos: start_of_line_pos + position
745 last_line: lines.len
746 }
747 reporter: .parser
748 })
749 continue
750 }
751 if line.contains('@include ') {
752 lines.delete(i)
753 resolved := p.process_includes(template_file, tline_number, line, mut &dc) or {
754 if err is IncludeError {
755 p.error_with_error(errors.Error{
756 message: err.msg()
757 file_path: err.calling_file()
758 pos: token.Pos{
759 len: '@include '.len
760 line_nr: err.line_nr()
761 pos: start_of_line_pos + err.pos()
762 col: err.col()
763 last_line: lines.len
764 }
765 reporter: .parser
766 })
767 []string{}
768 } else {
769 p.error_with_error(errors.Error{
770 message: 'An unknown error has occurred'
771 file_path: template_file
772 pos: token.Pos{
773 len: '@include '.len
774 line_nr: tline_number
775 pos: start_of_line_pos
776 last_line: lines.len
777 }
778 reporter: .parser
779 })
780 []string{}
781 }
782 }
783 for resolved_line in resolved.reverse() {
784 tline_number--
785 lines.insert(i, resolved_line)
786 }
787
788 i--
789 continue
790 }
791 if trimmed_line == '}' && brace_block_kinds.len > 0 && brace_block_kinds.last() == .control {
792 source.writeln(tmpl_str_end)
793 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
794 p.append_tmpl_line_info(template_file, tline_number, 2)
795 source.writeln('}')
796 p.append_tmpl_line_info(template_file, tline_number, 1)
797 source.write_string(tmpl_str_start)
798 brace_block_kinds.delete_last()
799 continue
800 }
801 if line.contains('@if ') {
802 control := parse_tmpl_control_line(line, '@if')
803 source.writeln(tmpl_str_end)
804 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
805 p.append_tmpl_line_info(template_file, tline_number, 2)
806 source.writeln('if ${control.header} {')
807 p.append_tmpl_line_info(template_file, tline_number, 1)
808 source.write_string(tmpl_str_start)
809 if control.has_inline_body {
810 source.writeln(insert_template_code(fn_name, tmpl_str_start, control.prefix +
811 control.inline_body))
812 p.append_tmpl_line_info(template_file, tline_number, 1)
813 }
814 if control.closes_inline_block {
815 source.writeln(tmpl_str_end)
816 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
817 p.append_tmpl_line_info(template_file, tline_number, 2)
818 source.writeln('}')
819 p.append_tmpl_line_info(template_file, tline_number, 1)
820 source.write_string(tmpl_str_start)
821 } else if control.opens_brace_block {
822 brace_block_kinds << .control
823 }
824 continue
825 }
826 if line.contains('@end') {
827 source.writeln(tmpl_str_end)
828 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
829 p.append_tmpl_line_info(template_file, tline_number, 2)
830 source.writeln('}')
831 p.append_tmpl_line_info(template_file, tline_number, 1)
832 source.write_string(tmpl_str_start)
833 if brace_block_kinds.len > 0 && brace_block_kinds.last() == .control {
834 brace_block_kinds.delete_last()
835 }
836 continue
837 }
838 if line.contains('@else') {
839 control := parse_tmpl_else_line(line)
840 source.writeln(tmpl_str_end)
841 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
842 p.append_tmpl_line_info(template_file, tline_number, 2)
843 source.writeln('} ${control.header} {')
844 p.append_tmpl_line_info(template_file, tline_number, 1)
845 source.write_string(tmpl_str_start)
846 if control.has_inline_body {
847 source.writeln(insert_template_code(fn_name, tmpl_str_start, control.prefix +
848 control.inline_body))
849 p.append_tmpl_line_info(template_file, tline_number, 1)
850 }
851 if control.closes_inline_block {
852 source.writeln(tmpl_str_end)
853 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
854 p.append_tmpl_line_info(template_file, tline_number, 2)
855 source.writeln('}')
856 p.append_tmpl_line_info(template_file, tline_number, 1)
857 source.write_string(tmpl_str_start)
858 if brace_block_kinds.len > 0 && brace_block_kinds.last() == .control {
859 brace_block_kinds.delete_last()
860 }
861 }
862 continue
863 }
864 if line.contains('@for') {
865 control := parse_tmpl_control_line(line, '@for')
866 source.writeln(tmpl_str_end)
867 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
868 p.append_tmpl_line_info(template_file, tline_number, 2)
869 source.writeln('for ${control.header} {')
870 p.append_tmpl_line_info(template_file, tline_number, 1)
871 source.write_string(tmpl_str_start)
872 if control.has_inline_body {
873 source.writeln(insert_template_code(fn_name, tmpl_str_start, control.prefix +
874 control.inline_body))
875 p.append_tmpl_line_info(template_file, tline_number, 1)
876 }
877 if control.closes_inline_block {
878 source.writeln(tmpl_str_end)
879 // tmpl_str_end contains '\n', so writeln creates 2 lines: ')' and empty
880 p.append_tmpl_line_info(template_file, tline_number, 2)
881 source.writeln('}')
882 p.append_tmpl_line_info(template_file, tline_number, 1)
883 source.write_string(tmpl_str_start)
884 } else if control.opens_brace_block {
885 brace_block_kinds << .control
886 }
887 continue
888 }
889 if state == .simple {
890 // by default, just copy 1:1
891 source.writeln(insert_template_code(fn_name, tmpl_str_start, line))
892 p.append_tmpl_line_info(template_file, tline_number, 1)
893 continue
894 }
895 // in_write = false
896 // The .simple mode ends here. The rest handles .html/.css/.js state transitions.
897
898 if state != .simple {
899 if line.contains('@js ') {
900 pos := line.index('@js') or { continue }
901 source.write_string('<script src="')
902 source.write_string(line[pos + 5..line.len - 1])
903 source.writeln('"></script>')
904 p.template_line_map << ast.TemplateLineInfo{
905 tmpl_path: template_file
906 tmpl_line: tline_number
907 }
908 continue
909 }
910 if line.contains('@css ') {
911 pos := line.index('@css') or { continue }
912 source.write_string('<link href="')
913 source.write_string(line[pos + 6..line.len - 1])
914 source.writeln('" rel="stylesheet" type="text/css">')
915 p.template_line_map << ast.TemplateLineInfo{
916 tmpl_path: template_file
917 tmpl_line: tline_number
918 }
919 continue
920 }
921 }
922
923 match state {
924 .html {
925 line_t := line.trim_space()
926 if line_t.starts_with('span.') && line.ends_with('{') {
927 // `span.header {` => `<span class='header'>`
928 class := line.find_between('span.', '{').trim_space()
929 source.writeln('<span class="${class}">')
930 p.template_line_map << ast.TemplateLineInfo{
931 tmpl_path: template_file
932 tmpl_line: tline_number
933 }
934 brace_block_kinds << .span
935 continue
936 } else if line_t.starts_with('.') && line.ends_with('{') {
937 // `.header {` => `<div class='header'>`
938 class := line.find_between('.', '{').trim_space()
939 trimmed := line.trim_space()
940 source.write_string(strings.repeat(`\t`, line.len - trimmed.len)) // add the necessary indent to keep <div><div><div> code clean
941 source.writeln('<div class="${class}">')
942 p.template_line_map << ast.TemplateLineInfo{
943 tmpl_path: template_file
944 tmpl_line: tline_number
945 }
946 brace_block_kinds << .div
947 continue
948 } else if line_t.starts_with('#') && line.ends_with('{') {
949 // `#header {` => `<div id='header'>`
950 class := line.find_between('#', '{').trim_space()
951 source.writeln('<div id="${class}">')
952 p.template_line_map << ast.TemplateLineInfo{
953 tmpl_path: template_file
954 tmpl_line: tline_number
955 }
956 brace_block_kinds << .div
957 continue
958 } else if line_t == '}' {
959 source.write_string(strings.repeat(`\t`, line.len - line_t.len)) // add the necessary indent to keep <div><div><div> code clean
960 if brace_block_kinds.len > 0 && brace_block_kinds.last() == .span {
961 source.writeln('</span>')
962 brace_block_kinds.delete_last()
963 } else {
964 source.writeln('</div>')
965 if brace_block_kinds.len > 0 && brace_block_kinds.last() == .div {
966 brace_block_kinds.delete_last()
967 }
968 }
969 p.template_line_map << ast.TemplateLineInfo{
970 tmpl_path: template_file
971 tmpl_line: tline_number
972 }
973 continue
974 }
975 }
976 .js {
977 // if line.contains('//V_TEMPLATE') {
978 source.writeln(insert_template_code(fn_name, tmpl_str_start, line))
979 p.template_line_map << ast.TemplateLineInfo{
980 tmpl_path: template_file
981 tmpl_line: tline_number
982 }
983 //} else {
984 // replace `$` to `\$` at first to escape JavaScript template literal syntax
985 // source.writeln(line.replace(r'$', r'\$').replace(r'$$', r'@').replace(r'.$',
986 // r'.@').replace(r"'", r"\'"))
987 //}
988 continue
989 }
990 .css {
991 // disable template variable declaration in inline stylesheet
992 // because of some CSS rules prefixed with `@`.
993 source.writeln(line.replace(r'.$', r'.@').replace(r"'", r"\'"))
994 p.template_line_map << ast.TemplateLineInfo{
995 tmpl_path: template_file
996 tmpl_line: tline_number
997 }
998 continue
999 }
1000 else {}
1001 }
1002
1003 // %translation_key => ${tr('translation_key')}
1004 // Process all %key patterns on this line
1005 mut line_ := line
1006 mut search_start := 0
1007 for {
1008 pos := line_.index_after('%', search_start) or { break }
1009 is_raw := pos + 4 < line_.len && line_[pos..pos + 5] == '%raw '
1010 if is_raw {
1011 // Start reading the key after "raw " (pos + 5)
1012 mut end := pos + 5
1013 // valid variable characters
1014 for end < line_.len && (line_[end].is_letter() || line_[end] == `_`) {
1015 end++
1016 }
1017 // Extract the key
1018 key := line_[pos + 5..end]
1019 if key.len > 0 {
1020 // Replace '%raw key' with just '${key}'
1021 line_ = line_.replace('%raw ${key}',
1022 '\${veb.raw(veb.tr(ctx.lang.str(), "${key}"))}')
1023 }
1024 search_start = pos + 1
1025 } else {
1026 if pos + 1 < line_.len && line_[pos + 1].is_letter() {
1027 mut end := pos + 1
1028 for end < line_.len && (line_[end].is_letter() || line_[end] == `_`) {
1029 end++
1030 }
1031 key := line_[pos + 1..end]
1032 // println('GOT tr key line="${line_}" key="${key}"')
1033 line_ = line_.replace('%${key}', '\${veb.tr(ctx.lang.str(), "${key}")}')
1034 search_start = pos + 1
1035 } else {
1036 // Not a valid translation key, skip this %
1037 search_start = pos + 1
1038 }
1039 }
1040 }
1041 if line_ != line {
1042 source.writeln(insert_template_code(fn_name, tmpl_str_start, line_))
1043 p.template_line_map << ast.TemplateLineInfo{
1044 tmpl_path: template_file
1045 tmpl_line: tline_number
1046 }
1047 } else {
1048 // by default, just copy 1:1
1049 source.writeln(insert_template_code(fn_name, tmpl_str_start, line))
1050 p.template_line_map << ast.TemplateLineInfo{
1051 tmpl_path: template_file
1052 tmpl_line: tline_number
1053 }
1054 }
1055 }
1056
1057 source.writeln(tmpl_str_end)
1058 source.writeln('\t_tmpl_res_${fn_name} := sb_${fn_name}.str() ')
1059 source.writeln('\treturn _tmpl_res_${fn_name}')
1060 source.writeln('}')
1061 source.writeln('// === end of veb html template_file: ${template_file} ===')
1062
1063 mut result := source.str()
1064 if result.contains('veb.') {
1065 result = 'import veb\n' + result
1066 }
1067 $if trace_tmpl_expansion ? {
1068 eprintln('>>>>>>> template expanded to:')
1069 eprintln(result)
1070 eprintln('-----------------------------')
1071 }
1072
1073 return result
1074}
1075