dtm: optimize parser/gen of the template manager (#20751)

This commit is contained in:
GGRei 2024-02-08 18:23:11 +01:00 committed by GitHub
parent b7b47fe130
commit 9782b3c51f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 374 additions and 199 deletions

View file

@ -140,10 +140,13 @@ Three parameters are available:
- `def_cache_path` : ( **String** value ) User can define the path of cache folder.
- `max_size_data_in_mem` : ( **Int** value ) Maximum size of data allowed in memory for each cached
template. The value must be specified in kilobytes. ( Default is: 500KB / Limit max is : 500KB )
- `compress_html` : ( **Bool** value ) Light '**minifier**' of the HTML ouput, to remove all
- `compress_html` : ( **Bool** value ) Light '**minifier**' of the HTML ouput, to remove all
unnecessary spacing. ( Default is true, parameter taken into account only for HTML files )
- `active_cache_server` : ( **Bool** value ) Activate or not the template cache system. ( Default is
true, ***_Highly recommended to keep it enabled for optimal performance_*** )
- `active_cache_server` : ( **Bool** value ) Activate or not the template cache system. ( Default
is true, ***_Highly recommended to keep it enabled for optimal performance_*** )
Regarding the `compress_html` option, it is recommended for performance reasons to disable it
when working directly with raw template generation (i.e., with the cache system disabled).
Use it like this :
@ -163,16 +166,16 @@ initialize(
values in the placeholders map. Templates can dynamically display content.
- `cache_delay_expiration` ( **i64** value ) Specifies the cache expiration time for the concerned
page in seconds. ( Default value is **86400** seconds or one day ). You can add any value you want
in seconds as long as it remains within the indicated range ( see below ).
page in seconds. ( Default value is **86400** seconds or one day ). You can add any value you
want in seconds as long as it remains within the indicated range ( see below ).
Possibility to use already defined cache delay constants like:
- `cache_delay_expiration_at_min` : five minutes
- `cache_delay_expiration_at_max` : one year
- `cache_delay_expiration_by_default` : one day
For specific cases, you can cancel the generation and use of cache file, even if the cache system is
active :
For specific cases, you can cancel the generation and use of cache file, even if the cache system
is active :
- `cache_delay_expiration` : -1
Or set a cache that will never expire:
@ -253,8 +256,8 @@ An example of a template, corresponding to the previous subsection:
You will note that the `'_#includehtml'` directive is not found in the template with
`'@placeholder_name_3'`, and this is entirely normal. Directives are specially handled by the DTM,
and including them in the name of your placeholders within the template will result in the
placeholder not being found because it does not match the key name defined in the map containing the
dynamic content.
placeholder not being found because it does not match the key name defined in the map containing
the dynamic content.
Like the traditional template system in V, inclusions or placeholders start with the '**@**'
@ -268,9 +271,9 @@ character. The traditional inclusion system is still perfectly usable, such as:
## In The Future
As you've understood, the DTM is still under development and optimization. There are functionalities
to be added, such as data compression, managing loops or conditions within the template itself. Able
to be used in contexts other than HTML and raw text.
As you've understood, the DTM is still under development and optimization. There are
functionalities to be added, such as data compression, managing loops or conditions within the
template itself. Able to be used in contexts other than HTML and raw text.
This will come in time.

View file

@ -1,7 +1,6 @@
module dtm
import os
import v.parser
import crypto.md5
import hash.fnv1a
import time
@ -175,6 +174,7 @@ pub struct DynamicTemplateManagerInitialisationParams {
// A cache directory can be created by the user for storage. If it is not defined or encounters issues such as permission problems,
// the DTM will attempt to create it in the OS's temporary area. If this proves impossible, the cache system will be deactivated and the user will be informed if cache system was required.
// Initalisation params are :
//
// - def_cache_path 'type string' User can define the path of cache folder.
// - max_size_data_in_mem 'type int' Maximum size of data allowed in memory for caching. The value must be specified in kilobytes. ( Default is: 500KB / Limit max is : 500KB)
// - compress_html: 'type bool' Light compress of the HTML ouput. ( default is true )
@ -963,17 +963,11 @@ fn (mut tm DynamicTemplateManager) chandler_remaining_cache_template_used(cr Cac
// fn (mut DynamicTemplateManager) parse_tmpl_file(string, string, &map[string]DtmMultiTypeMap, bool, TemplateType) return (string, string)
//
// The V compiler's template parser 'vlib/v/parser/tmpl.v', parses and transforms template file content.
// Parses and generates template file content.
// It ensures template format compatibility necessary for proper compilation and execution in its typical usage outside of DTM like managing various states,
// processing template tags, and supporting string interpolation...
// This function checks for the presence and validity of template directives '@include'
// to prevent runtime errors related to incorrect inclusion paths. Replaces placeholders with their actual values,
// including dynamic content with the possibility of adding HTML code but only for certain specified tags and can also light compress HTML if required ( Removing usless spaces ).
//
// TODO - This function does not perform an in-depth check to ensure the file is indeed a valid template file, which could lead to possible runtime crashes,
// Addressing this by adding a control mechanism is recommended for enhanced stability.
// For now, it is the user's responsibility.
//
const allowed_tags = ['<div>', '</div>', '<h1>', '</h1>', '<h2>', '</h2>', '<h3>', '</h3>', '<h4>',
'</h4>', '<h5>', '</h5>', '<h6>', '</h6>', '<p>', '</p>', '<br>', '<hr>', '<span>', '</span>',
'<ul>', '</ul>', '<ol>', '</ol>', '<li>', '</li>', '<dl>', '</dl>', '<dt>', '</dt>', '<dd>',
@ -987,123 +981,10 @@ const allowed_tags = ['<div>', '</div>', '<h1>', '</h1>', '<h2>', '</h2>', '<h3>
const include_html_key_tag = '_#includehtml'
fn (mut tm DynamicTemplateManager) parse_tmpl_file(file_path string, tmpl_name string, placeholders &map[string]DtmMultiTypeMap, is_compressed bool, tmpl_type TemplateType) string {
// To prevent runtime crashes related to template include directives error,
// this code snippet ensures that the paths in include directives '@include' are correct.
tmpl_content := os.read_file(file_path) or {
eprintln("${dtm.message_signature_error} Unable to read the file: '${file_path}' with parser function.")
return dtm.internat_server_error
}
mut re := regex.regex_opt('.*@include(?P<space_type>[ \t\r\n]+)(?P<quote_type_beg>[\'"])(?P<path>.*)(?P<quote_type_end>[\'"])') or {
tm.stop_cache_handler()
eprintln('${dtm.message_signature_error} with regular expression for template @inclusion in parse_tmpl_file() function. Please check the syntax of the regex pattern : ${err.msg()}')
return dtm.internat_server_error
}
// Find all occurrences of the compiled regular expression within the template content.
matches := re.find_all(tmpl_content)
// Check if any matches were found.
if matches.len > 0 {
mut full_path := ''
// Iterate through the matches. Since each match has a start and end index, increment by 2 for each iteration.
for i := 0; i < matches.len; i += 2 {
// Retrieve the start and end indices of the current match.
start := matches[i]
end := matches[i + 1]
// Extract the substring from the template content that corresponds to the current match.
match_text := tmpl_content[start..end]
// Apply the regex to the extracted substring to enable group capturing.
re.match_string(match_text)
// Extract the path from the current match.
mut path := re.get_group_by_name(match_text, 'path')
// Extract the type of quotation marks.
quote_type_beg := re.get_group_by_name(match_text, 'quote_type_beg')
quote_type_end := re.get_group_by_name(match_text, 'quote_type_end')
// Extract the whitespace characters following the '@include' directive.
space_type := re.get_group_by_name(match_text, 'space_type')
// Check if double quotes are used or if the whitespace sequence contains newline (\n) or carriage return (\r) characters
if quote_type_beg == '"' || quote_type_end == '"' || space_type.contains('\n')
|| space_type.contains('\r') {
eprintln("${dtm.message_signature_error} In the template: '${file_path}', an error occurred in one of the '@include' directives. This could be due to the use of double quotes or unexpected newline/carriage return characters in the whitespace.")
return dtm.internat_server_error
}
// Check if the 'path' string does not end with '.html' or '.txt'. If it doesn't, append '.html'/'.txt' to ensure the path has the correct extension.
match tmpl_type {
.html {
if !path.ends_with('.html') {
path += '.html'
}
}
.text {
if !path.ends_with('.txt') {
path += '.txt'
}
}
}
full_path = os.join_path(tm.template_folder, path)
// Check if the 'path' is empty or if the template path does not exist.
if path.len < 1 || !os.exists(full_path) {
eprintln("${dtm.message_signature_error} In the template: '${file_path}', an error occurred in one of the '@include' directives. This could be due to the use of an invalid path: ${full_path}")
return dtm.internat_server_error
}
}
}
mut p := parser.Parser{}
// Parse/transform the template content, and a subsequent cleaning function restores the parsed content to a usable state for the DTM.
// Refer to the comments in 'clean_parsed_tmpl' for details.
mut tmpl_ := tm.clean_parsed_tmpl(p.compile_template_file(file_path, tmpl_name), tmpl_name,
placeholders)
// If clean_parsed_tmpl() return error
if tmpl_ == dtm.internat_server_error {
return tmpl_
}
// This section completes the processing by replacing any placeholders that were not handled by the compiler template parser.
// If there are placeholders present, it iterates through them, applying necessary filters and substituting their values into the HTML content.
// dtm.filter function used here. 'escape_html_strings_in_templates.v'
// Checks if there are any placeholders to process
if placeholders.len > 0 {
for key, value in placeholders {
mut val := ''
mut key_m := key
match value {
i8, i16, int, i64, u8, u16, u32, u64, f32, f64 {
// Converts value to string
temp_val := value.str()
// Filters the string value for safe insertion
val = filter(temp_val)
}
string {
// Checks if the placeholder allows HTML inclusion
if key.ends_with(dtm.include_html_key_tag) {
if tmpl_type == TemplateType.html {
// Iterates over allowed HTML tags for inclusion
for tag in dtm.allowed_tags {
// Escapes the HTML tag
escaped_tag := filter(tag)
// Replaces the escaped tags with actual HTML tags in the value
val = value.replace(escaped_tag, tag)
}
} else {
val = filter(value)
}
// Adjusts the placeholder key by removing the HTML inclusion tag
key_m = key.all_before_last(dtm.include_html_key_tag)
} else {
// Filters the string value for safe insertion
val = filter(value)
}
}
}
// Forms the actual placeholder to be replaced in the template render.
placeholder := '$${key_m}'
// Check if placeholder exist in the template
if tmpl_.contains(placeholder) {
// Replaces the placeholder if exist in the template with the actual value.
tmpl_ = tmpl_.replace(placeholder, val)
}
}
}
mut tmpl_ := compile_template_file(file_path, tmpl_name, placeholders)
// Performs a light compression of the HTML output by removing usless spaces, newlines, and tabs if user selected this option.
if is_compressed && tmpl_type == TemplateType.html {
if is_compressed && tmpl_type == TemplateType.html && tmpl_ != dtm.internat_server_error {
tmpl_ = tmpl_.replace_each(['\n', '', '\t', '', ' ', ' '])
mut r := regex.regex_opt(r'>(\s+)<') or {
tm.stop_cache_handler()
@ -1119,70 +1000,6 @@ fn (mut tm DynamicTemplateManager) parse_tmpl_file(file_path string, tmpl_name s
return tmpl_
}
// fn (mut DynamicTemplateManager) clean_parsed_tmpl(string, string) return string
//
// Is specifically designed to clean the template output generated by V's compiler template parser.
// It addresses the fact that the parser prepares template content for integration into an executable, rather than for direct use in a web browser.
// The function adjusts markers and escapes specific characters to convert the parser's output into a format suitable for web browsers.
//
// TODO - Any changes to the template output made by the V lang compiler template parser in 'vlib/v/parser/tmpl.v'
// may necessitate corresponding adjustments in this function. Perhaps a more independent function is needed to clean the template rendering?
//
// TODO - This function does not currently handle the cleanup of all template directives typically managed by the Vlang compiler, such as conditional statements or loops....
// Implementation of these features will be necessary.
//
fn (mut tm DynamicTemplateManager) clean_parsed_tmpl(tmpl string, tmpl_name string, provided_placeholders &map[string]DtmMultiTypeMap) string {
// Defines the start marker to encapsulate template content
start_marker := "sb_${tmpl_name}.write_string('"
// Determines the end marker, signaling the end of template content
end_marker := "')\n\n\t_tmpl_res_${tmpl_name} := sb_${tmpl_name}.str()"
// Searches for the start marker in the processed template content. Triggers an error if the start marker is not found.
start := tmpl.index(start_marker) or {
eprintln("${dtm.message_signature_error} Start marker not found for '${tmpl_name}': ${err.msg()}")
// dtm.filter function used here. 'escape_html_strings_in_templates.v'
return filter(tmpl)
}
// Identifies the last occurrence of the end marker. Signals an error if it is missing.
end := tmpl.index_last(end_marker) or {
eprintln("${dtm.message_signature_error} End marker not found for '${tmpl_name}': ${err.msg()}")
// dtm.filter function used here. 'escape_html_strings_in_templates.v'
return filter(tmpl)
}
// Extracts the portion of template content between the start and end markers.
mut tmpl_ := tmpl[start + start_marker.len..end]
// Utilizes a regular expression to identify placeholders within the template output.
// This process checks for placeholders that have no corresponding entries in the provided placeholders map.
// Any unmatched placeholders found are then safely escaped
mut r := regex.regex_opt(r'$([a-zA-Z_][a-zA-Z0-9_]*)') or {
tm.stop_cache_handler()
eprintln('${dtm.message_signature_error} with regular expression for identifying placeholders in the template in clean_parsed_tmpl() function. Please check the syntax of the regex pattern : ${err.msg()}')
return dtm.internat_server_error
}
indices := r.find_all(tmpl_)
// Iterates through each found placeholder.
for i := 0; i < indices.len; i += 2 {
// Retrieves the start and end indices of the current placeholder.
beginning := indices[i]
ending := indices[i + 1]
// Extracts the placeholder from the template using the indices.
placeholder := tmpl_[beginning..ending]
// Removes the '$' symbol to get the placeholder name.
placeholder_name := placeholder[1..]
// Checks if the placeholder or its variant with '_#includehtml' is not in the provided placeholders map.
if !(placeholder_name in provided_placeholders
|| (placeholder_name + dtm.include_html_key_tag) in provided_placeholders) {
// If so, escapes the unresolved placeholder and replaces the original placeholder with the escaped version
escaped_placeholder := filter(placeholder)
tmpl_ = tmpl_.replace(placeholder, escaped_placeholder)
}
}
// Transforms parser-specific escape sequences into their respective characters for proper template
tmpl_ = tmpl_.replace('\\n', '\n').replace("\\'", "'")
return tmpl_
}
// fn check_if_cache_delay_iscorrect(i64, string) return !
//
// Validates the user-specified cache expiration delay for templates.

View file

@ -0,0 +1,355 @@
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
/*
This source code originates from the internal V compiler 'vlib/v/parser/tmpl.v' and
has been heavily modified for the needs of the Dynamic Template Manager. Thanks to its original author, Alexander Medvednikov.
*/
module dtm
import os
import strings
enum State {
simple // default - no special interpretation of tags, *at all*!
// That is suitable for the general case of text template interpolation,
// for example for interpolating arbitrary source code (even V source) templates.
//
html // default, only when the template extension is .html
css // <style>
js // <script>
// span // span.{
}
fn (mut state State) update(line string) {
trimmed_line := line.trim_space()
if is_html_open_tag('style', line) {
state = .css
} else if trimmed_line == '</style>' {
state = .html
} else if is_html_open_tag('script', line) {
state = .js
} else if trimmed_line == '</script>' {
state = .html
}
}
const tmpl_str_end = "')\n"
// check HTML open tag `<name attr="x" >`
fn is_html_open_tag(name string, s string) bool {
trimmed_line := s.trim_space()
mut len := trimmed_line.len
if len < name.len {
return false
}
mut sub := trimmed_line[0..1]
if sub != '<' { // not start with '<'
return false
}
sub = trimmed_line[len - 1..len]
if sub != '>' { // not end with '<'
return false
}
sub = trimmed_line[len - 2..len - 1]
if sub == '/' { // self-closing
return false
}
sub = trimmed_line[1..len - 1]
if sub.contains_any('<>') { // `<name <bad> >`
return false
}
if sub == name { // `<name>`
return true
} else {
len = name.len
if sub.len <= len { // `<nam>` or `<meme>`
return false
}
if sub[..len + 1] != '${name} ' { // not `<name ...>`
return false
}
return true
}
}
fn replace_placeholders_with_data(line string, data &map[string]DtmMultiTypeMap, state State) string {
mut rline := line
mut need_include_html := false
for key, value in data {
mut placeholder := '$${key}'
if placeholder.ends_with(include_html_key_tag) {
placeholder = placeholder.all_before_last(include_html_key_tag)
need_include_html = true
}
if !rline.contains(placeholder) {
need_include_html = false
continue
}
mut val_str := ''
match value {
i8, i16, int, i64, u8, u16, u32, u64, f32, f64 {
// Converts value to string
temp_val := value.str()
// Filters the string value for safe insertion
val_str = filter(temp_val)
}
string {
// Checks if the placeholder allows HTML inclusion
if need_include_html {
if state == State.html {
// Iterates over allowed HTML tags for inclusion
for tag in allowed_tags {
// Escapes the HTML tag
escaped_tag := filter(tag)
// Replaces the escaped tags with actual HTML tags in the value
val_str = value.replace(escaped_tag, tag)
}
} else {
val_str = filter(value)
}
need_include_html = false
} else {
// Filters the string value for safe insertion
val_str = filter(value)
}
}
}
rline = rline.replace(placeholder, val_str)
}
// If no output is found for the placeholder being processed, then the placeholder is escaped
if rline.contains('$') {
rline = filter(rline)
}
return rline
}
fn insert_template_code(fn_name string, tmpl_str_start string, line string, data &map[string]DtmMultiTypeMap, state State) string {
// HTML, may include `@var`
// escaped by cgen, unless it's a `vweb.RawHtml` string
trailing_bs := dtm.tmpl_str_end + 'sb_${fn_name}.write_u8(92)\n' + tmpl_str_start
round1 := ['\\', '\\\\', r"'", "\\'", r'@', r'$']
round2 := [r'$$', r'\@', r'.$', r'.@']
mut rline := line.replace_each(round1).replace_each(round2)
comptime_call_str := rline.find_between('\${', '}')
if comptime_call_str.contains("\\'") {
rline = rline.replace(comptime_call_str, comptime_call_str.replace("\\'", r"'"))
}
if rline.ends_with('\\') {
rline = rline[0..rline.len - 2] + trailing_bs
}
if rline.contains('$') {
rline = replace_placeholders_with_data(rline, data, state)
}
return rline
}
// compile_file compiles the content of a file by the given path as a template
fn compile_template_file(template_file string, fn_name string, data &map[string]DtmMultiTypeMap) string {
mut lines := os.read_lines(template_file) or {
eprintln('${message_signature_error} Template generator can not reading from ${template_file} file')
return internat_server_error
}
basepath := os.dir(template_file)
tmpl_str_start := "\tsb_${fn_name}.write_string('"
mut source := strings.new_builder(1000)
mut state := State.simple
template_ext := os.file_ext(template_file)
if template_ext.to_lower() == '.html' {
state = .html
}
mut in_span := false
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
for i := 0; i < lines.len; i++ {
line := lines[i]
tline_number++
start_of_line_pos = end_of_line_pos
end_of_line_pos += line.len + 1
if state != .simple {
state.update(line)
}
$if trace_tmpl ? {
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}')
}
if line.contains('@header') {
position := line.index('@header') or { 0 }
eprintln("${message_signature_warn} Please use @include 'header' instead of @header (deprecated), position : ${position}")
continue
}
if line.contains('@footer') {
position := line.index('@footer') or { 0 }
eprintln("${message_signature_warn} Please use @include 'footer' instead of @footer (deprecated), position : ${position}")
continue
}
if line.contains('@include ') {
lines.delete(i)
// Allow single or double quoted paths.
mut file_name := if line.contains('"') {
line.split('"')[1]
} else if line.contains("'") {
line.split("'")[1]
} else {
s := '@include '
position := line.index(s) or { 0 }
eprintln("${message_signature_error} path for @include must be quoted with ' or \" without line breaks or extraneous characters between @include and the quotes, position : ${position}")
return internat_server_error
}
mut file_ext := os.file_ext(file_name)
if file_ext == '' {
file_ext = '.html'
}
file_name = file_name.replace(file_ext, '')
// relative path, starting with the current folder
mut templates_folder := os.real_path(basepath)
if file_name.contains('/') && file_name.starts_with('/') {
// an absolute path
templates_folder = ''
}
file_path := os.real_path(os.join_path_single(templates_folder, '${file_name}${file_ext}'))
$if trace_tmpl ? {
eprintln('>>> basepath: "${basepath}" , template_file: "${template_file}" , fn_name: "${fn_name}" , @include line: "${line}" , file_name: "${file_name}" , file_ext: "${file_ext}" , templates_folder: "${templates_folder}" , file_path: "${file_path}"')
}
file_content := os.read_file(file_path) or {
position := line.index('@include ') or { 0 } + '@include '.len
eprintln('${message_signature_error} Reading @include file "${file_name}" from path: ${file_path} failed, position : ${position}')
return internat_server_error
}
file_splitted := file_content.split_into_lines().reverse()
for f in file_splitted {
tline_number--
lines.insert(i, f)
}
i--
continue
}
if line.contains('@if ') {
/* source.writeln(dtm.tmpl_str_end)
pos := line.index('@if') or { continue }
source.writeln('if ' + line[pos + 4..] + '{')
source.writeln(tmpl_str_start) */
continue
}
if line.contains('@end') {
/* // Remove new line byte
source.go_back(1)
source.writeln(dtm.tmpl_str_end)
source.writeln('}')
source.writeln(tmpl_str_start) */
continue
}
if line.contains('@else') {
// Remove new line byte
source.go_back(1)
/* source.writeln(dtm.tmpl_str_end)
source.writeln(' } else { ')
source.writeln(tmpl_str_start) */
continue
}
if line.contains('@for') {
/* source.writeln(dtm.tmpl_str_end)
pos := line.index('@for') or { continue }
source.writeln('for ' + line[pos + 4..] + '{')
source.writeln(tmpl_str_start) */
continue
}
if state == .simple {
// by default, just copy 1:1
source.writeln(insert_template_code(fn_name, tmpl_str_start, line, data, state))
continue
}
// The .simple mode ends here. The rest handles .html/.css/.js state transitions.
if state != .simple {
if line.contains('@js ') {
pos := line.index('@js') or { continue }
source.write_string('<script src="')
source.write_string(line[pos + 5..line.len - 1])
source.writeln('"></script>')
continue
}
if line.contains('@css ') {
pos := line.index('@css') or { continue }
source.write_string('<link href="')
source.write_string(line[pos + 6..line.len - 1])
source.writeln('" rel="stylesheet" type="text/css">')
continue
}
}
match state {
.html {
line_t := line.trim_space()
if line_t.starts_with('span.') && line.ends_with('{') {
//`span.header {` => `<span class='header'>`
class := line.find_between('span.', '{').trim_space()
source.writeln('<span class="${class}">')
in_span = true
continue
} else if line_t.starts_with('.') && line.ends_with('{') {
//`.header {` => `<div class='header'>`
class := line.find_between('.', '{').trim_space()
trimmed := line.trim_space()
source.write_string(strings.repeat(`\t`, line.len - trimmed.len)) // add the necessary indent to keep <div><div><div> code clean
source.writeln('<div class="${class}">')
continue
} else if line_t.starts_with('#') && line.ends_with('{') {
//`#header {` => `<div id='header'>`
class := line.find_between('#', '{').trim_space()
source.writeln('<div id="${class}">')
continue
} else if line_t == '}' {
source.write_string(strings.repeat(`\t`, line.len - line_t.len)) // add the necessary indent to keep <div><div><div> code clean
if in_span {
source.writeln('</span>')
in_span = false
} else {
source.writeln('</div>')
}
continue
}
}
.js {
// if line.contains('//V_TEMPLATE') {
source.writeln(insert_template_code(fn_name, tmpl_str_start, line, data,
state))
//} else {
// replace `$` to `\$` at first to escape JavaScript template literal syntax
// source.writeln(line.replace(r'$', r'\$').replace(r'$$', r'@').replace(r'.$',
// r'.@').replace(r"'", r"\'"))
//}
continue
}
.css {
// disable template variable declaration in inline stylesheet
// because of some CSS rules prefixed with `@`.
source.writeln(line.replace(r'.$', r'.@').replace(r"'", r"\'"))
continue
}
else {}
}
// by default, just copy 1:1
source.writeln(insert_template_code(fn_name, tmpl_str_start, line, data, state))
}
result := source.str()
$if trace_tmpl_expansion ? {
eprintln('>>>>>>> template expanded to:')
eprintln(result)
eprintln('-----------------------------')
}
return result
}