module dtm2 import hash import json import os import strings // dtm2 is the modern runtime renderer for Dynamic Template Manager. // It keeps DTM dynamic, meaning templates can still be edited on disk without // recompiling the application, while moving the hot path to a parsed-template // cache instead of a rendered-output cache. File extensions are configurable, // but every template ultimately renders in either HTML mode or text mode. // // The intended runtime model is one long-lived Manager per application or // rendering context. Tests and benchmarks may create short-lived managers, but // normal applications should reuse a manager so parsed templates and resolved // paths stay hot. const message_signature = '[Dynamic Template Manager 2]' const internal_server_error = 'Internal Server Error' const include_html_key_suffix = '_#includehtml' const include_directive = '@include ' const max_include_depth = 32 const max_extension_config_size = 64 * 1024 const default_extension_config_filename = 'dtm2_extensions.json' const segment_instruction_size = 9 const segment_kind_text = u8(0) const segment_kind_placeholder = u8(1) // These tags are the compatibility allow-list for the historical `_#includehtml` // placeholder suffix. Any other tag is still escaped. const allowed_html_tags = ['
', '
', '

', '

', '

', '

', '

', '

', '

', '

', '
', '
', '
', '
', '

', '

', '
', '
', '', '', '', '
    ', '
', '
  • ', '
  • ', '
    ', '
    ', '
    ', '
    ', '
    ', '
    ', '', '', '', '
    ', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '
    ', '
    ', '', '
    ', '
    ', '
    ', '
    ', '
    ', '
    ', '', '
    ', '
    ', '', '', '', '', '', '']! pub enum TemplateType { html text } // ExtensionConfig is the JSON representation accepted by // `ManagerParams.extension_config_file`. // // Example: // // ```json // { // "html": [".html", ".htm", ".xml", ".view"], // "text": [".txt", ".mail"] // } // ``` pub struct ExtensionConfig { pub: html []string text []string } // TemplateDependency records the file metadata that decides whether a parsed // template is still fresh. Includes are tracked as dependencies of the parent // template, so editing a partial can invalidate the compiled parent tree. struct TemplateDependency { path string modified_at i64 size u64 content_hash u64 } // CompiledTemplate is the parsed representation stored by Manager. // // It intentionally stores a compact instruction string instead of slices of // segment structs. The instructions contain offsets into the owned `content` // string, which keeps the cache compact and avoids keeping fragile string-slice // graphs alive across many -prod renders. @[heap] struct CompiledTemplate { // Rendering mode inferred from the source file extension. template_type TemplateType // Full template content after static @include expansion. content string // Compact metadata used to decide whether this compiled tree is stale. dependency_signature string // Binary instruction stream: one fixed-size record per text/placeholder segment. instructions string // Number of text and placeholder records in `instructions`. segment_count int // Static-text size hint used to preallocate the render buffer. estimated_size int } // Manager owns the runtime state of a dtm2 renderer. // // It caches resolved template paths and parsed template trees, but never caches // rendered HTML responses. This keeps the new engine deterministic and leaves // legacy rendered-cache compatibility in `x.templating.dtm`. @[heap] pub struct Manager { mut: // Base directory for relative template paths. template_dir string // Enables the lightweight deterministic HTML whitespace compressor. compress_html bool // When true, source and include files are stat-checked before cache reuse. reload_modified_templates bool // Maps caller-provided template paths to canonical source paths. resolved_template_paths map[string]string // Parsed-template cache keyed by canonical source path. compiled_templates map[string]&CompiledTemplate // User-provided extension-to-render-mode overrides. Built-in extensions are // resolved without allocating a per-manager default map. template_extensions map[string]TemplateType } // ManagerParams configure a dtm2 Manager. @[params] pub struct ManagerParams { pub: // Root directory used when `expand()` receives a relative template path. // If empty, `/templates` is used. template_dir string // Compresses HTML output by removing newlines/tabs and redundant spaces. compress_html bool = true // Re-check source files and includes before reusing a parsed template tree. // Disable it for maximum hot-path throughput when templates are immutable. reload_modified_templates bool = true // Additional or overriding extension mappings. // Default mappings are: `.html`, `.htm`, `.xml` => HTML mode and // `.txt`, `.text` => text mode. template_extensions map[string]TemplateType // Optional JSON file containing extension mappings. It is merged after the // default mappings and before `template_extensions`, so explicit code // configuration wins over the file. If empty, DTM2 tries to load // `/dtm2_extensions.json` when that file exists. extension_config_file string } // RenderParams configure one render call. @[params] pub struct RenderParams { pub: // Placeholder values keyed by their template name without the `@` prefix. // Values are escaped by default. placeholders &map[string]string = &map[string]string{} // Prefix written back when a placeholder is missing. The default preserves // the original `@placeholder` text. missing_placeholder_prefix string = '@' } // initialize creates a dynamic template manager rooted at `params.template_dir`. // // The returned manager should normally be kept and reused. Reusing it is what // gives dtm2 its parsed-template and path-resolution cache benefits. pub fn initialize(params ManagerParams) &Manager { raw_template_dir := if params.template_dir == '' { os.join_path(os.dir(os.executable()), 'templates') } else { params.template_dir } template_dir := canonical_template_dir(raw_template_dir) template_extensions := build_template_extensions(params, template_dir) return &Manager{ template_dir: template_dir.clone() compress_html: params.compress_html reload_modified_templates: params.reload_modified_templates resolved_template_paths: map[string]string{} compiled_templates: map[string]&CompiledTemplate{} template_extensions: template_extensions } } fn canonical_template_dir(template_dir string) string { return os.real_path(template_dir) } fn build_template_extensions(params ManagerParams, template_dir string) map[string]TemplateType { mut extensions := map[string]TemplateType{} config_path := extension_config_path(params, template_dir) if config_path != '' { file_extensions := read_extension_config_file(config_path) or { eprintln('${message_signature} ${err.msg()}') map[string]TemplateType{} } merge_template_extensions(mut extensions, file_extensions) } merge_template_extensions(mut extensions, params.template_extensions) return extensions } fn extension_config_path(params ManagerParams, template_dir string) string { if params.extension_config_file != '' { return params.extension_config_file } default_path := os.join_path(template_dir, default_extension_config_filename) if os.exists(default_path) { return default_path } return '' } fn read_extension_config_file(config_path string) !map[string]TemplateType { if !os.exists(config_path) { return error('extension config file "${config_path}" not found') } if os.is_dir(config_path) { return error('extension config path "${config_path}" is a directory') } config_stat := os.stat(config_path)! if config_stat.size > u64(max_extension_config_size) { return error('extension config file "${config_path}" is larger than ${max_extension_config_size} bytes') } raw_config := os.read_file(config_path)! config := json.decode(ExtensionConfig, raw_config)! mut extensions := map[string]TemplateType{} merge_template_extension_list(mut extensions, config.html, .html) merge_template_extension_list(mut extensions, config.text, .text) return extensions } fn merge_template_extensions(mut target map[string]TemplateType, source map[string]TemplateType) { for ext, template_type in source { normalized := validate_template_extension(ext) or { eprintln('${message_signature} ${err.msg()}') continue } target[normalized] = template_type } } fn merge_template_extension_list(mut target map[string]TemplateType, extensions []string, template_type TemplateType) { for ext in extensions { normalized := validate_template_extension(ext) or { eprintln('${message_signature} ${err.msg()}') continue } target[normalized] = template_type } } fn normalize_template_extension(ext string) string { trimmed := ext.trim_space().to_lower() if trimmed == '' { return '' } if trimmed.starts_with('.') { return trimmed } return '.${trimmed}' } fn validate_template_extension(ext string) !string { normalized := normalize_template_extension(ext) if normalized.len < 2 { return error('ignoring invalid template extension "${ext}"') } for i := 1; i < normalized.len; i++ { c := normalized[i] if (c >= `a` && c <= `z`) || (c >= `0` && c <= `9`) || c == `_` || c == `-` || c == `.` { continue } return error('ignoring invalid template extension "${ext}"') } return normalized } // compiled_template_count is intentionally exposed for tests and diagnostics. In // dtm2 it counts parsed template trees currently held by the manager. pub fn (m &Manager) compiled_template_count() int { return m.compiled_templates.len } // expand renders `template_path` with the provided placeholders. // // `template_path` can be absolute or relative to the manager's template // directory. Supported extensions come from the manager extension table. // Errors are reported to stderr and return the legacy-compatible // `Internal Server Error` string. pub fn (mut m Manager) expand(template_path string, params RenderParams) string { source_path := m.cached_template_path(template_path) or { eprintln('${message_signature} ${err.msg()}') return internal_server_error } compiled := m.compiled_template_for_path(source_path) or { eprintln('${message_signature} ${err.msg()}') return internal_server_error } if m.compress_html && compiled.template_type == .html { rendered := render_compiled_template(compiled, params) compressed := compress_html_output(rendered) return compressed.clone() } rendered := render_compiled_template(compiled, params) return rendered.clone() } // cached_template_path reuses canonical paths when reload checks are disabled. // When reload checks are enabled, it revalidates the current real path so a // replaced symlink cannot bypass the template directory boundary. fn (mut m Manager) cached_template_path(template_path string) !string { if source_path := m.resolved_template_paths[template_path] { if m.reload_modified_templates { current_source_path := m.resolve_template_path(template_path)! if current_source_path != source_path { m.resolved_template_paths[template_path] = current_source_path return current_source_path } } return source_path } source_path := m.resolve_template_path(template_path)! m.resolved_template_paths[template_path] = source_path return source_path } // compiled_template_for_path returns the parsed tree for a canonical source // path. When reload checks are enabled, dependency metadata decides whether the // tree can be reused or must be rebuilt from disk. fn (mut m Manager) compiled_template_for_path(source_path string) !&CompiledTemplate { if compiled := m.compiled_templates[source_path] { if !m.reload_modified_templates || compiled.dependencies_are_fresh(m.template_dir) { return compiled } } compiled := compile_template_from_file(source_path, m.template_dir, m.template_extensions, m.reload_modified_templates)! m.compiled_templates[source_path] = compiled return compiled } // compile_template_from_file reads a template, expands static includes, parses // placeholders, and stores dependency metadata for future invalidation. fn compile_template_from_file(source_path string, template_root string, template_extensions map[string]TemplateType, track_dependencies bool) !&CompiledTemplate { canonical_path := os.real_path(source_path) ensure_path_inside_template_dir(canonical_path, template_root)! template_type := template_type_from_path(canonical_path, template_extensions)! content, dependencies := read_template_with_includes(canonical_path, template_root, 0, track_dependencies)! instructions, estimated_size, segment_count := parse_segments(content) dependency_signature := encode_dependencies(dependencies) return &CompiledTemplate{ template_type: template_type content: copy_string(content) dependency_signature: copy_string(dependency_signature) instructions: copy_string(instructions) segment_count: segment_count estimated_size: estimated_size } } // copy_string forces cached strings to own their memory. Cached templates live // beyond the stack frame that built them, so the cache should not retain // accidental views into temporary buffers. fn copy_string(value string) string { mut bytes := []u8{len: value.len} for i := 0; i < value.len; i++ { bytes[i] = value[i] } copied := bytes.bytestr() return copied.clone() } // dependencies_are_fresh checks root and included files. The content hash closes // the same-second, same-size edit window left by second-resolution mtimes. fn (compiled &CompiledTemplate) dependencies_are_fresh(template_root string) bool { signature := compiled.dependency_signature mut offset := 0 for offset < signature.len { first_sep := index_byte_from(signature, `|`, offset) or { return false } second_sep := index_byte_from(signature, `|`, first_sep + 1) or { return false } third_sep := index_byte_from(signature, `|`, second_sep + 1) or { return false } line_end := index_byte_from(signature, `\n`, third_sep + 1) or { signature.len } size := signature[first_sep + 1..second_sep].i64() if size < 0 { return false } content_hash := signature[second_sep + 1..third_sep].u64() path := signature[third_sep + 1..line_end] current_path := os.real_path(path) ensure_path_inside_template_dir(current_path, template_root) or { return false } if current_path != path { return false } stat := os.stat(path) or { return false } if stat.mtime != signature[offset..first_sep].i64() || stat.size != u64(size) { return false } content := os.read_file(path) or { return false } if content_fingerprint(content) != content_hash { return false } offset = line_end + 1 } return true } fn index_byte_from(value string, needle u8, start int) ?int { for i := start; i < value.len; i++ { if value[i] == needle { return i } } return none } fn encode_dependencies(dependencies []TemplateDependency) string { mut out := strings.new_builder(dependencies.len * 64) for dependency in dependencies { out.writeln('${dependency.modified_at}|${dependency.size}|${dependency.content_hash}|${dependency.path}') } encoded := out.str() return encoded.clone() } // resolve_template_path converts absolute or manager-relative template names to // canonical paths. The canonical path is used as the compiled-template cache key. fn (m &Manager) resolve_template_path(template_path string) !string { source_path := if os.is_abs_path(template_path) { template_path } else { os.join_path(m.template_dir, template_path) } if !os.exists(source_path) { return error('template "${source_path}" not found') } canonical_path := os.real_path(source_path) ensure_path_inside_template_dir(canonical_path, m.template_dir)! return canonical_path } fn ensure_path_inside_template_dir(path string, template_root string) ! { root := path_without_trailing_separator(template_root) candidate := path_without_trailing_separator(path) if candidate == root || candidate.starts_with(root + os.path_separator) { return } return error('template "${path}" is outside template directory "${template_root}"') } fn path_without_trailing_separator(path string) string { if path.len > os.path_separator.len && path.ends_with(os.path_separator) { return path[..path.len - os.path_separator.len] } return path } // template_type_from_path keeps rendering rules extension-driven and explicit. fn template_type_from_path(source_path string, template_extensions map[string]TemplateType) !TemplateType { ext := normalize_template_extension(os.file_ext(source_path)) if template_extensions.len > 0 { if template_type := template_extensions[ext] { return template_type } } if template_type := default_template_type_from_extension(ext) { return template_type } return error('template "${source_path}" uses unsupported extension "${ext}"') } fn default_template_type_from_extension(ext string) ?TemplateType { match ext { '.html', '.htm', '.xml' { return .html } '.txt', '.text' { return .text } else { return none } } } // read_template_with_includes expands simple line-level `@include "path"` // directives before parsing placeholders. Includes are part of the parsed tree // and are tracked as dependencies for reload checks. fn read_template_with_includes(source_path string, template_root string, depth int, track_dependencies bool) !(string, []TemplateDependency) { if depth > max_include_depth { return error('maximum @include depth exceeded while reading "${source_path}"') } content := os.read_file(source_path)! mut dependencies := []TemplateDependency{cap: 4} if track_dependencies { dependencies << template_dependency(source_path, content)! } if !content_needs_include_expansion(content) { return content_with_template_newline(content), dependencies } base_dir := os.dir(source_path) mut out := strings.new_builder(content.len) lines := content.split_into_lines() for line in lines { expanded_line, line_dependencies := expand_include_directives(line, base_dir, template_root, depth, track_dependencies)! out.write_string(expanded_line) out.write_u8(`\n`) dependencies << line_dependencies } rendered := out.str() return rendered.clone(), dependencies } fn content_with_template_newline(content string) string { if content == '' || content[content.len - 1] == `\n` { return content.clone() } with_newline := content + '\n' return with_newline.clone() } fn content_needs_include_expansion(content string) bool { for i := 0; i < content.len; i++ { if content[i] == `\r` { return true } if is_include_directive_at(content, i) { return true } } return false } fn is_include_directive_at(content string, pos int) bool { return pos + include_directive.len <= content.len && content[pos] == `@` && content[pos + 1] == `i` && content[pos + 2] == `n` && content[pos + 3] == `c` && content[pos + 4] == `l` && content[pos + 5] == `u` && content[pos + 6] == `d` && content[pos + 7] == `e` && content[pos + 8] == ` ` } fn template_dependency(source_path string, content string) !TemplateDependency { stat := os.stat(source_path)! return TemplateDependency{ path: source_path.clone() modified_at: stat.mtime size: stat.size content_hash: content_fingerprint(content) } } fn content_fingerprint(content string) u64 { return hash.sum64_string(content, 0) } fn expand_include_directives(line string, base_dir string, template_root string, depth int, track_dependencies bool) !(string, []TemplateDependency) { mut out := strings.new_builder(line.len) mut dependencies := []TemplateDependency{} mut offset := 0 for { rel_pos := line[offset..].index(include_directive) or { out.write_string(line[offset..]) break } pos := offset + rel_pos target, end_pos := include_target_from_line_at(line, pos) or { out.write_string(line[offset..pos + include_directive.len]) offset = pos + include_directive.len continue } out.write_string(line[offset..pos]) include_path := resolve_include_path(base_dir, target, template_root)! next_depth := depth + 1 included, include_dependencies := read_template_with_includes(include_path, template_root, next_depth, track_dependencies)! out.write_string(included.trim_right('\n')) dependencies << include_dependencies offset = end_pos } expanded := out.str() return expanded.clone(), dependencies } fn include_target_from_line_at(line string, pos int) ?(string, int) { mut cursor := pos + include_directive.len for cursor < line.len && line[cursor].is_space() { cursor++ } if cursor >= line.len { return none } quote := line[cursor] if quote != `'` && quote != `"` { return none } start := cursor + 1 for cursor = start; cursor < line.len; cursor++ { if line[cursor] == quote { return line[start..cursor], cursor + 1 } } return none } fn resolve_include_path(base_dir string, include_target string, template_root string) !string { mut target := include_target if os.file_ext(target) == '' { target += '.html' } source_path := if os.is_abs_path(target) { target } else { os.join_path(base_dir, target) } if !os.exists(source_path) { return error('included template "${source_path}" not found') } include_path := os.real_path(source_path) ensure_path_inside_template_dir(include_path, template_root)! return include_path } // parse_segments converts template content to a compact instruction stream. // Each instruction stores a segment kind plus offset/length into `content`. // Rendering can then walk the stream without reparsing placeholder syntax. fn parse_segments(content string) (string, int, int) { mut instructions := []u8{cap: segment_instruction_size * 16} mut text_start := 0 mut estimated_size := 0 mut segment_count := 0 mut i := 0 for i < content.len { if content[i] != `@` || i + 1 >= content.len || !is_placeholder_char(content[i + 1]) { i++ continue } if i > text_start { append_segment_instruction(mut instructions, segment_kind_text, text_start, i - text_start) segment_count++ estimated_size += i - text_start } mut end := i + 1 for end < content.len && is_placeholder_char(content[end]) { end++ } append_segment_instruction(mut instructions, segment_kind_placeholder, i + 1, end - i - 1) segment_count++ i = end text_start = end } if text_start < content.len { append_segment_instruction(mut instructions, segment_kind_text, text_start, content.len - text_start) segment_count++ estimated_size += content.len - text_start } encoded := instructions.bytestr() return encoded.clone(), estimated_size, segment_count } fn append_segment_instruction(mut instructions []u8, kind u8, start int, len int) { instructions << kind append_u32(mut instructions, start) append_u32(mut instructions, len) } fn append_u32(mut data []u8, value int) { encoded := u32(value) data << u8(encoded & 0xff) data << u8((encoded >> 8) & 0xff) data << u8((encoded >> 16) & 0xff) data << u8((encoded >> 24) & 0xff) } fn read_u32(data string, offset int) int { b0 := u32(data[offset]) b1 := u32(data[offset + 1]) << 8 b2 := u32(data[offset + 2]) << 16 b3 := u32(data[offset + 3]) << 24 value := b0 | b1 | b2 | b3 return int(value) } fn is_placeholder_char(c u8) bool { return (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || (c >= `0` && c <= `9`) || c == `_` } // render_compiled_template walks the parsed instruction stream and writes the // final output. Placeholder values are rendered only for this call; the manager // never stores caller data after `expand()` returns. fn render_compiled_template(compiled &CompiledTemplate, params RenderParams) string { mut out := strings.new_builder(estimate_render_size(compiled)) mut offset := 0 for offset + segment_instruction_size <= compiled.instructions.len { kind := compiled.instructions[offset] start := read_u32(compiled.instructions, offset + 1) len := read_u32(compiled.instructions, offset + 5) offset += segment_instruction_size if kind == segment_kind_text { text := compiled.segment_text(start, len) out.write_string(text) continue } if kind == segment_kind_placeholder { name := compiled.segment_text(start, len) if write_placeholder_value(mut out, name, params.placeholders, compiled.template_type) { continue } out.write_string(params.missing_placeholder_prefix) out.write_string(name) } } rendered := out.str() return rendered.clone() } fn (compiled &CompiledTemplate) segment_text(start int, len int) string { end := start + len if start < 0 || len < 0 || end > compiled.content.len { return '' } return compiled.content[start..end] } // write_placeholder_value resolves the normal placeholder name first, then the // historical `_#includehtml` alias. The alias is accepted for compatibility but // still passes through the allow-list based HTML escaping path. fn write_placeholder_value(mut out strings.Builder, name string, placeholders &map[string]string, template_type TemplateType) bool { unsafe { if raw_value := placeholders[name] { rendered_value := render_value(raw_value, name.ends_with(include_html_key_suffix), template_type) out.write_string(rendered_value) return true } include_html_name := name + include_html_key_suffix if raw_html := placeholders[include_html_name] { rendered_html := render_value(raw_html, true, template_type) out.write_string(rendered_html) return true } } return false } // estimate_render_size gives strings.Builder enough capacity for common // placeholder expansion without making a second pre-render pass. fn estimate_render_size(compiled &CompiledTemplate) int { return compiled.estimated_size + 1024 + (compiled.segment_count * 64) } // render_value applies DTM's safety default: values are escaped unless the // caller explicitly uses the include-html convention in an HTML template. fn render_value(raw string, allow_html bool, template_type TemplateType) string { if allow_html && template_type == .html { return escape_with_allowed_html(raw) } return escape_html(raw) } // escape_with_allowed_html escapes the full value first, then restores only the // supported tags. This preserves the legacy opt-in behavior without allowing // arbitrary raw HTML through. fn escape_with_allowed_html(value string) string { mut escaped := escape_html(value) for tag in allowed_html_tags { escaped = escaped.replace(escape_html(tag), tag) } return escaped } // escape_html is a local deterministic HTML escape helper. It avoids depending // on heavier generic replacement paths in the hot render loop. fn escape_html(value string) string { mut escaped := strings.new_builder(value.len) for i := 0; i < value.len; i++ { match value[i] { `&` { escaped.write_string('&') } `<` { escaped.write_string('<') } `>` { escaped.write_string('>') } `"` { escaped.write_string('"') } `'` { escaped.write_string(''') } else { escaped.write_u8(value[i]) } } } escaped_value := escaped.str() return escaped_value.clone() } // compress_html_output performs a small, predictable whitespace compression // pass for HTML templates. It intentionally avoids regex work in the hot path. fn compress_html_output(html string) string { mut result := strings.new_builder(html.len) mut pending_space := false mut last_written := u8(0) for i := 0; i < html.len; i++ { c := html[i] if c == `\n` || c == `\t` { continue } if c == ` ` { pending_space = true continue } if pending_space { if !(last_written == `>` && c == `<`) { result.write_u8(` `) last_written = ` ` } pending_space = false } result.write_u8(c) last_written = c } compressed := result.str() return compressed.clone() }