diff --git a/doc/templates.md b/doc/templates.md
new file mode 100644
index 0000000..4dc8332
--- /dev/null
+++ b/doc/templates.md
@@ -0,0 +1,156 @@
+# Template Engine
+
+ringhttp includes a compile-time template engine. Templates are `.lt` files compiled to LitaC source before the main build runs, so there is no runtime parsing overhead.
+
+## Template Syntax
+
+Templates use JSP-style tags:
+
+| Tag | Purpose |
+|-----|---------|
+| `<% code %>` | Emit raw LitaC code (statements, variable declarations, etc.) |
+| `<%= expr %>` | Append `expr` to the response body (calls `out.appendString(expr)`) |
+
+### Directives (inside `<% %>`)
+
+| Directive | Example | Effect |
+|-----------|---------|--------|
+| `@type "module" :: TypeName` | `@type "models/user" :: User` | Declares the input type. Generates a typed function signature (`input: *TypeName`) and adds the module import. |
+| `@import "module/path"` | `@import "models/tag"` | Adds a LitaC import to the generated file. |
+| `@load "file.lt"` | `@load "templates/header.html.lt"` | Calls the partial's `_WriteTemplate` function, passing `input` unchanged. |
+| `@load "file.lt" field` | `@load "templates/card.html.lt" input.card` | Calls the partial passing a sub-field as input. |
+
+### Input type
+
+Without `@type`, the generated function signature is:
+
+```litac
+public func page_html_lt_WriteTemplate(resp: *HttpResponse, input: *void) : Status
+```
+
+You cast manually inside the template body:
+
+```html
+<% var data = input as (*PageData) %>
+
<%= data.title %>
+```
+
+With `@type`, the signature is typed and no cast is needed:
+
+```html
+<% @type "models/user" :: User %>
+Hello, <%= input.name %>
+```
+
+## Generated Output
+
+The generator produces one file per template:
+
+```
+src/gen/tmpl_page_html_lt.lita — generated function for page.html.lt
+src/gen/tmpl_greeting_html_lt.lita — generated function for greeting.html.lt
+src/gen/templates.lita — dispatch + named helpers on HttpResponse
+```
+
+`templates.lita` provides two call styles:
+
+```litac
+// String dispatch (runtime name lookup, input is *void)
+resp.template("page_html_lt", &pageData)
+
+// Typed named helper (compile-time type checked for @type templates)
+resp.tmpl_greeting_html_lt(&user)
+```
+
+## Setting Up Template Generation in Your Project
+
+ringhttp is a library — template generation runs in your app's `pre_main.lita` via the LitaC `pre_build_command` feature.
+
+### 1. Create `src/pre_main.lita`
+
+```litac
+import "std/libc"
+import "std/mem"
+import "std/string"
+
+import "ring/template"
+import "ring/http_common"
+
+func main(len: i32, args: **char) : i32 {
+ var templateInputDir = "static/templates"
+ if(len > 1) {
+ templateInputDir = args[1]
+ }
+
+ var genOutputDir = "src/gen"
+ if(len > 2) {
+ genOutputDir = args[2]
+ }
+
+ printf("Generating templates from '%s' -> '%s'\n", templateInputDir, genOutputDir)
+
+ var status = GenerateTemplates(
+ templateInputDir.toString(),
+ genOutputDir,
+ defaultAllocator
+ )
+
+ if(status != Status.OK) {
+ printf("Template generation failed: %s\n", StatusAsStr(status))
+ return 1
+ }
+
+ return 0
+}
+```
+
+### 2. Add `pre_build_command` to `pkg.json`
+
+```json
+"pre_build_command": {
+ "default": {
+ "default": {
+ "default": {
+ "cc": "gcc",
+ "cc_options": "-g -std=gnu99 %input% -o %output%",
+ "lita_options": ["-run"]
+ }
+ }
+ }
+}
+```
+
+### 3. Place your templates
+
+Put `.lt` files under `bin/static/templates/` (or the directory you configured). The directory is scanned recursively; only `.lt` files are processed.
+
+### 4. Build
+
+```sh
+litac build
+```
+
+The pre-build step runs first, writing the generated files into `src/gen/`. The main build then compiles everything including those generated sources.
+
+## Example Template
+
+`bin/static/templates/greeting.html.lt`:
+
+```html
+<% @type "models/user" :: User %>
+
+
+
+ Hello, <%= input.name %>!
+
+
+```
+
+Route handler:
+
+```litac
+func greetingHandler(req: *HttpRequest, resp: *HttpResponse) : Status {
+ var user = User{ .name = $"World" }
+ return resp.tmpl_greeting_html_lt(&user)
+}
+```
diff --git a/pkg.json b/pkg.json
index 892ed8e..0987a41 100644
--- a/pkg.json
+++ b/pkg.json
@@ -4,21 +4,6 @@
"type" : "executable",
"repo" : "",
- "pre_build_command": {
- "default" : {
- "default" : {
- "default" : {
- "cc" : "gcc",
- "cc_options" : "-g -std=gnu99 %input% -o %output% -D_CRT_SECURE_NO_WARNINGS",
- "lita_options" : [
- "-verbose",
- "-run"
- ]
- }
- }
- }
- },
-
"build_command" : {
"default" : {
"linux" : {
diff --git a/src/http_common.lita b/src/http_common.lita
index 4fc53df..5bd4383 100644
--- a/src/http_common.lita
+++ b/src/http_common.lita
@@ -79,6 +79,7 @@ public enum Status {
ERROR_TEMPLATE_NO_END_TAG,
ERROR_TEMPLATE_START_TAG_BEFORE_END_TAG,
ERROR_TEMPLATE_INVALID_COMMAND,
+ ERROR_TEMPLATE_NOT_FOUND,
// HTTP parsing (formerly ParseStatus)
ERROR_HTTP_PARSING_CHUNK_INCOMPLETE,
diff --git a/src/http_server.lita b/src/http_server.lita
index c2206ba..51e588c 100644
--- a/src/http_server.lita
+++ b/src/http_server.lita
@@ -94,6 +94,7 @@ public struct HttpConfig {
// static file directory
fileServerPath: String = $"" // the FileSystem path to look for static assets
+ templatePath: String = $"" // the FileSystem path to look for .lt template files
publicServerPath: String = $"/static/" // the URI path that denotes we're looking for static assets
// the server side keep alive time in seconds
diff --git a/src/pre_main.lita b/src/pre_main.lita
deleted file mode 100644
index 474dbeb..0000000
--- a/src/pre_main.lita
+++ /dev/null
@@ -1,31 +0,0 @@
-import "std/libc"
-import "std/mem"
-import "std/string"
-import "std/string/buffer"
-import "std/string/builder"
-
-import "http_common"
-import "template"
-
-func main(len: i32, args: **char) : i32 {
- var templateInputDir = "static/templates/"
- if(len > 1) {
- templateInputDir = args[1]
- }
- printf("Generating templates from '%s'...\n", templateInputDir)
-
- var allocator = defaultAllocator
-
- var template: Template{}
- template.init(allocator)
- var status = template.build(templateInputDir.toString()) ?: {
- printf("Error building templates: %s\n", StatusAsStr(status))
- return 1
- }
-
- printf("Generated code:\n")
- printf("%s\n", template.generatedCode.cStr())
- // TODO: generate lookup table for each template
- // resp.template(TemplateId.PATH_TO_TEMPLATE], input)
- return 0
-}
\ No newline at end of file
diff --git a/src/template.lita b/src/template.lita
index 15ee6ef..79547c2 100644
--- a/src/template.lita
+++ b/src/template.lita
@@ -5,38 +5,195 @@ import "std/string/buffer"
import "std/system"
import "std/fs"
import "std/io"
-import "std/map"
+import "std/array"
import "std/mem"
import "std/assert"
import "std/ascii"
import "http_common"
+// Scans inputDir for .lt templates, generates .lita source files into outputDir.
+// Writes one tmpl_.lita per template and a templates.lita dispatch file.
+public func GenerateTemplates(
+ inputDir: String,
+ outputDir: *const char,
+ allocator: *const Allocator
+) : Status {
+ var t: Template
+ t.init(allocator)
+
+ var status = t.build(inputDir)
+ ?: return status
+
+ if(t.entries.empty()) {
+ return Status.OK
+ }
+
+ Mkdir(outputDir)
+
+ for(var i = 0; i < t.entries.size(); i += 1) {
+ var entry = t.entries.getPtr(i)
+ var name = entry.name.toString()
+ var content = entry.content.toString()
+
+ var temp:[MAX_PATH]char;
+ var filePath = StringBufferInit(temp, MAX_PATH, 0)
+ filePath.append("%s/tmpl_%.*s.lita", outputDir, name.length, name.buffer)
+
+ var fs = WriteFile(filePath.cStr(), content.buffer, content.length as (usize))
+ if(fs != FileStatus.Ok) {
+ return Status.ERROR_IO_ERROR
+ }
+ }
+
+ var dispatch = StringBuilderInit(4096, allocator)
+ defer dispatch.free()
+ GenerateDispatch(&t, outputDir, &dispatch)
+
+ var temp:[MAX_PATH]char;
+ var dispatchPath = StringBufferInit(temp, MAX_PATH, 0)
+ dispatchPath.append("%s/templates.lita", outputDir)
+
+ var ds = WriteFile(dispatchPath.cStr(), dispatch.cStr(), dispatch.length as (usize))
+ if(ds != FileStatus.Ok) {
+ return Status.ERROR_IO_ERROR
+ }
+
+ return Status.OK
+}
+
+func GenerateDispatch(t: *Template, genOutputDir: *const char, output: *StringBuilder) {
+ BaseImports(output)
+ output.appendStr("\n")
+
+ var typeImports = ArrayInit(8, t.entries.getPtr(0).name.alloc)
+ defer typeImports.free()
+
+ for(var i = 0; i < t.entries.size(); i += 1) {
+ var entry = t.entries.getPtr(i)
+ var name = entry.name.toString()
+ output.append("import \"gen/tmpl_%.*s\"\n", name.length, name.buffer)
+
+ if(entry.typeModule.length > 0) {
+ var mod = entry.typeModule.toString()
+ var seen = false
+ for(var j = 0; j < typeImports.size(); j += 1) {
+ if(typeImports.get(j).equals(mod)) {
+ seen = true
+ break
+ }
+ }
+ if(!seen) {
+ typeImports.add(mod)
+ output.append("import \"%.*s\"\n", mod.length, mod.buffer)
+ }
+ }
+ }
+ output.appendStr("\n")
+
+ // String dispatch — takes *void; typed templates cast input to their concrete type
+ output.appendStr("public func (this: *HttpResponse) template(name: *const char, input: *void) : Status {\n")
+ for(var i = 0; i < t.entries.size(); i += 1) {
+ var entry = t.entries.getPtr(i)
+ var name = entry.name.toString()
+ if(entry.typeName.length > 0) {
+ var typeName = entry.typeName.toString()
+ output.append(
+ " if(StringInit(name).equals($\"%.*s\")) { return %.*s_WriteTemplate(this, input as (*%.*s)) }\n",
+ name.length, name.buffer,
+ name.length, name.buffer,
+ typeName.length, typeName.buffer
+ )
+ } else {
+ output.append(
+ " if(StringInit(name).equals($\"%.*s\")) { return %.*s_WriteTemplate(this, input) }\n",
+ name.length, name.buffer,
+ name.length, name.buffer
+ )
+ }
+ }
+ output.appendStr(" return Status.ERROR_TEMPLATE_NOT_FOUND\n}\n\n")
+
+ // Named helpers per template — typed for @type templates, *void otherwise
+ for(var i = 0; i < t.entries.size(); i += 1) {
+ var entry = t.entries.getPtr(i)
+ var name = entry.name.toString()
+ if(entry.typeName.length > 0) {
+ var typeName = entry.typeName.toString()
+ output.append(
+ "public func (this: *HttpResponse) tmpl_%.*s(input: *%.*s) : Status {\n return %.*s_WriteTemplate(this, input)\n}\n\n",
+ name.length, name.buffer,
+ typeName.length, typeName.buffer,
+ name.length, name.buffer
+ )
+ } else {
+ output.append(
+ "public func (this: *HttpResponse) tmpl_%.*s(input: *void) : Status {\n return %.*s_WriteTemplate(this, input)\n}\n\n",
+ name.length, name.buffer,
+ name.length, name.buffer
+ )
+ }
+ }
+}
+
+// Per-template generation context — tracks imports, @type info, and partial count
+struct GenContext {
+ allocator: *const Allocator
+ imports: Array
+ partialCount: i32
+ typeName: String // from <% @type "module" :: TypeName %>, empty if absent
+ typeModule: String // module path for the declared type
+}
+
+func (ctx: *GenContext) init(allocator: *const Allocator) {
+ ctx.allocator = allocator
+ ctx.imports.init(8, allocator)
+ ctx.partialCount = 0
+ ctx.typeName = String{}
+ ctx.typeModule = String{}
+}
+
+func (ctx: *GenContext) free() {
+ ctx.imports.free()
+}
+
+func (ctx: *GenContext) addImport(path: String) {
+ for(var i = 0; i < ctx.imports.size(); i += 1) {
+ if(ctx.imports.get(i).equals(path)) {
+ return;
+ }
+ }
+ ctx.imports.add(path)
+}
+
+// One generated .lita file per .lt template
+public struct TemplateEntry {
+ name: StringBuilder // sanitized function name prefix, e.g. "greeting_html_lt"
+ content: StringBuilder // complete generated .lita file content
+ typeName: StringBuilder // declared type name (e.g. "User"), empty if no @type
+ typeModule: StringBuilder// declared type module (e.g. "models/user"), empty if no @type
+}
+
public struct Template {
allocator: *const Allocator
fileBuffer: StringBuilder
- generatedCode: StringBuilder
-
- templates: Map
+ entries: Array
}
public func (this: *Template) init(allocator: *const Allocator) {
this.allocator = allocator
- this.fileBuffer = StringBuilderInit(1024, allocator)
- this.generatedCode = StringBuilderInit(1024, allocator)
-
- this.templates = StringMap($"", 16, allocator)
+ this.fileBuffer.init(1024, allocator)
+ this.entries.init(16, allocator)
}
-public func (this: *Template) build(assetPath: String): Status {
+public func (this: *Template) build(assetPath: String) : Status {
return ScanForTemplates(this, assetPath)
}
func ScanForTemplates(template: *Template, assetPath: String) : Status {
var temp:[MAX_PATH]char;
var path = StringBufferInit(temp, MAX_PATH, 0)
- path.append("%s", assetPath)
-
+ path.append("%.*s", assetPath.length, assetPath.buffer)
return ScanDirectory(template, path)
}
@@ -67,144 +224,230 @@ func ScanDirectory(template: *Template, path: *StringBuffer) : Status {
}
template.fileBuffer.clear()
-
var status = ReadFileFully(path.cStr(), template.fileBuffer)
?: return Status.ERROR_FILE_NOT_FOUND
- var templateStatus = GenerateFromTemplate(
+ var entry = TemplateEntry{
+ .name = StringBuilderInit(64, template.allocator),
+ .content = StringBuilderInit(1024, template.allocator)
+ }
+
+ var genStatus = GenerateFromTemplate(
template.allocator,
fileName,
template.fileBuffer.toString(),
- &template.generatedCode
- ) ?: return templateStatus
+ &entry
+ ) ?: return genStatus
+
+ template.entries.add(entry)
}
}
return Status.OK
}
-func LoadTemplate(
- allocator: *const Allocator,
- path: String,
- fileBuffer: *StringBuilder,
- output: *StringBuilder
-) : Status {
-
- var status = ReadFileFullyStr(path, fileBuffer)
- ?: return Status.ERROR_FILE_NOT_FOUND
-
- var templateStatus = GenerateBody(
- allocator,
- fileBuffer.toString(),
- output
- ) ?: return templateStatus
-
- return Status.OK
-}
-
+// Generates a complete .lita source file for one template into entry.
+// GenerateBody runs first (may call SanitizeName for @load), then we
+// sanitize the template name to avoid clobbering the @static buffer.
public func GenerateFromTemplate(
allocator: *const Allocator,
name: String,
template: String,
- output: *StringBuilder
+ entry: *TemplateEntry
) : Status {
- var sname = SanatizeName(name)
- // TODO: Escape function name
- output.append("""
- public func %.*s_WriteTemplate(
- resp: *HttpResponse,
- input: T
- ) : Status {
- var out = resp.body
- """, sname.length, sname.buffer)
-
- var status = GenerateBody(allocator, template, output)
+ var ctx = GenContext{}
+ ctx.init(allocator)
+ defer ctx.free()
+
+ var body = StringBuilderInit(1024, allocator)
+ defer body.free()
+
+ var status = GenerateBody(allocator, &ctx, template, &body)
?: return status
- output.append("""
- return Status.OK
+ // Sanitize after GenerateBody — avoids clobbering the @static buffer mid-body
+ var sname = SanitizeName(name)
+ entry.name.appendString(sname)
+
+ // Copy @type info into entry if declared
+ if(!ctx.typeName.empty()) {
+ entry.typeName.init(ctx.typeName.length + 1, allocator)
+ entry.typeName.appendString(ctx.typeName)
+ entry.typeModule.init(ctx.typeModule.length + 1, allocator)
+ entry.typeModule.appendString(ctx.typeModule)
+ }
+
+ var output = &entry.content
+ BaseImports(output)
+
+ // Imports declared in the template via <% @import "..." %>
+ // (the type module from @type is already added via addImport)
+ for(var i = 0; i < ctx.imports.size(); i += 1) {
+ var imp = ctx.imports.get(i)
+ output.append("import \"%.*s\"\n", imp.length, imp.buffer)
+ }
+ output.appendStr("\n")
+
+ // Function signature: *TypeName when @type declared, *void otherwise
+ if(!ctx.typeName.empty()) {
+ output.append(
+ "public func %.*s_WriteTemplate(\n resp: *HttpResponse,\n input: *%.*s\n) : Status {\n var out = &resp.body\n",
+ sname.length, sname.buffer,
+ ctx.typeName.length, ctx.typeName.buffer
+ )
+ } else {
+ output.append(
+ "public func %.*s_WriteTemplate(\n resp: *HttpResponse,\n input: *void\n) : Status {\n var out = &resp.body\n",
+ sname.length, sname.buffer
+ )
}
- """)
+
+ output.appendString(body.toString())
+ output.appendStr(" return Status.OK\n}\n")
return Status.OK
}
public func GenerateBody(
allocator: *const Allocator,
+ ctx: *GenContext,
template: String,
output: *StringBuilder
) : Status {
var iter = template.split($"<%")
while(iter.hasNext()) {
- var s = iter.next().trim()
+ var s = iter.next()
- output.append(
- "\n output.append(\"%%s\", \"\"\"%.*s\"\"\")\n",
- s.length, s.buffer
- )
+ if(s.length > 0) {
+ // appendStrn avoids format-string injection from raw HTML text
+ output.append(" out.appendStrn(\"\"\"%.*s\"\"\", %d)\n", s.length, s.buffer, s.length)
+ }
var extra = iter.remaining()
- if (!extra.empty()) {
+ if(!extra.empty()) {
+ // <%=expr%> shorthand — expression output
+ var isExpression = extra.startsWith($"=")
+ if(isExpression) {
+ extra = extra.substring(1)
+ }
+
var endIndex = extra.indexOf($"%>")
if(endIndex < 0) {
return Status.ERROR_TEMPLATE_NO_END_TAG
}
- // check if there is an embedded <%
var startIndex = extra.indexOf($"<%")
if(startIndex > -1 && startIndex < endIndex) {
return Status.ERROR_TEMPLATE_START_TAG_BEFORE_END_TAG
}
- // output the escaped code
var code = extra.substring(0, endIndex)
- var status = HandleDirective(allocator, &code, output)
- ?: return status
- output.append("%.*s\n", code.length, code.buffer)
+ if(isExpression) {
+ var expr = code.trim()
+ output.append(" out.appendString(%.*s)\n", expr.length, expr.buffer)
+ } else {
+ var dirStatus = HandleDirective(allocator, ctx, &code, output)
+ ?: return dirStatus
+ if(code.length > 0) {
+ output.append("%.*s\n", code.length, code.buffer)
+ }
+ }
- // move the iterator to the start of the next unescaped section
iter = extra.substring(endIndex + 2).split($"<%")
}
}
return Status.OK
}
-
func HandleDirective(
allocator: *const Allocator,
+ ctx: *GenContext,
code: *String,
output: *StringBuilder
) : Status {
- var block = *code;
- block = block.trim()
+ var block = (*code).trim()
+
+ // <% @import "module/path" %> — add a LitaC import to this template's file
+ if(block.startsWith($"@import ")) {
+ var index = block.endIndexOf($"@import ")
+ assert(index > -1)
- if(!block.startsWith($"$")) {
+ var modulePath = ReadString(block.substring(index))
+ if(modulePath.empty()) {
+ printf("'@import' must be followed by a quoted module path\n")
+ return Status.ERROR_TEMPLATE_INVALID_COMMAND
+ }
+ ctx.addImport(modulePath)
+ ;*code = $""
+ return Status.OK
+ }
+
+ // <% @type "module/path" :: TypeName %> — declare the input type
+ // Generates a typed function signature and adds the module import.
+ if(block.startsWith($"@type ")) {
+ var index = block.endIndexOf($"@type ")
+ assert(index > -1)
+
+ var modulePath = ReadString(block.substring(index))
+ if(modulePath.empty()) {
+ printf("'@type' must be followed by a quoted module path\n")
+ return Status.ERROR_TEMPLATE_INVALID_COMMAND
+ }
+
+ var afterModule = block.substring(index + modulePath.length + 2).trim()
+ if(!afterModule.startsWith($"::")) {
+ printf("'@type' module path must be followed by ':: TypeName'\n")
+ return Status.ERROR_TEMPLATE_INVALID_COMMAND
+ }
+
+ var typeName = afterModule.substring(2).trim()
+ if(typeName.empty()) {
+ printf("'@type' must include a type name after '::'\n")
+ return Status.ERROR_TEMPLATE_INVALID_COMMAND
+ }
+
+ ctx.typeName = typeName
+ ctx.typeModule = modulePath
+ ctx.addImport(modulePath)
+ ;*code = $""
return Status.OK
}
- // load another template
- if(block.startsWith($"$load ")) {
- var index = block.endIndexOf($"$load ")
+
+ // <% @load "file.lt" [field] %> — emit a direct call to the partial's function
+ if(block.startsWith($"@load ")) {
+ var index = block.endIndexOf($"@load ")
assert(index > -1)
var filename = ReadString(block.substring(index))
if(filename.empty()) {
- printf(
- "The template command '$load' must be followed by a string representing the file path to the template to load"
- )
+ printf("'@load' must be followed by a quoted template file path\n")
return Status.ERROR_TEMPLATE_INVALID_COMMAND
}
- var fileBuffer = StringBuilderInit(1024, allocator)
- var status = LoadTemplate(
- allocator,
- filename,
- fileBuffer,
- output
- ) ?: return status
+ var afterFilename = block.substring(index + filename.length + 2).trim()
+ var funcName = SanitizeName(BaseName(filename))
+ var pcount = ctx.partialCount
+ ctx.partialCount += 1
+
+ if(afterFilename.empty()) {
+ output.append(
+ " var _p%d = %.*s_WriteTemplate(resp, input)\n if(_p%d != Status.OK) { return _p%d }\n",
+ pcount, funcName.length, funcName.buffer, pcount, pcount
+ )
+ } else {
+ output.append(
+ " var _p%d = %.*s_WriteTemplate(resp, %.*s)\n if(_p%d != Status.OK) { return _p%d }\n",
+ pcount, funcName.length, funcName.buffer,
+ afterFilename.length, afterFilename.buffer,
+ pcount, pcount
+ )
+ }
- ;*code = block.substring(index + filename.length + 2)
+ ;*code = $""
+ return Status.OK
}
+
return Status.OK
}
@@ -228,15 +471,38 @@ public func ReadString(str: String) : String {
}
}
-// no thread-safe, uses static value
-func SanatizeName(
- name: String
-) : String {
+// Returns the filename portion of a path (after the last path separator)
+func BaseName(path: String) : String {
+ var sep = GetSystemPathSeparator()
+ var lastIdx = -1
+ var i = 0
+ while(true) {
+ var found = path.indexOfAt(sep, i)
+ if(found < 0) { break }
+ lastIdx = found
+ i = found + 1
+ }
+ if(lastIdx < 0) { return path }
+ return path.substring(lastIdx + 1)
+}
+
+func BaseImports(output: *StringBuilder) {
+ // Base imports always required
+ output.appendStr("import \"http_response\"\n")
+ output.appendStr("import \"http_request\"\n")
+ output.appendStr("import \"http_common\"\n")
+ output.appendStr("import \"std/string\"\n")
+ output.appendStr("import \"std/string/builder\"\n")
+ output.appendStr("import \"std/string/buffer\"\n")
+}
+
+// Not thread-safe — uses a @static buffer. Use result before calling again.
+public func SanitizeName(name: String) : String {
@static
var buffer:[MAX_PATH]char;
var str = StringBufferInit(buffer, MAX_PATH, 0)
- for(var i = 0; i < name.length; i+=1) {
+ for(var i = 0; i < name.length; i += 1) {
var c = name.buffer[i]
if(!c.isAlphanumeric()) {
c = '_'
@@ -245,4 +511,3 @@ func SanatizeName(
}
return str.toString()
}
-
diff --git a/test/template_test.lita b/test/template_test.lita
index d00209c..74dfd58 100644
--- a/test/template_test.lita
+++ b/test/template_test.lita
@@ -1,13 +1,12 @@
import "std/assert"
import "std/string"
import "std/string/builder"
+import "std/string/buffer"
import "std/mem"
import "template"
import "http_common"
-// Bug fix: str.buffer[0] accessed without checking length first.
-// An empty string (or whitespace-only that trims to empty) must not crash.
@test
func testReadStringEmptyInput() {
var empty = ReadString(StringInit(""))
@@ -16,40 +15,110 @@ func testReadStringEmptyInput() {
var whitespace = ReadString(StringInit(" "))
assert(whitespace.length == 0)
- // Sanity-check: a well-formed quoted string still works.
var result = ReadString(StringInit("\"hello\""))
assert(result.equals($"hello"))
- // Input that starts with a non-quote character should also return empty.
var noQuote = ReadString(StringInit("hello"))
assert(noQuote.length == 0)
}
@test
-func testTemplate() {
- var output = StringBuilderInit(1024)
+func testSanitizeName() {
+ assert(SanitizeName($"test.html.lt").equals($"test_html_lt"))
+ assert(SanitizeName($"my-template.lt").equals($"my_template_lt"))
+ assert(SanitizeName($"simple").equals($"simple"))
+}
+
+@test
+func testGenerateBodyVoid() {
+ // Without @type: input is *void, user casts manually in template body
+ var entry = TemplateEntry{
+ .name = StringBuilderInit(64, defaultAllocator),
+ .content = StringBuilderInit(1024, defaultAllocator)
+ }
+
+ var status = GenerateFromTemplate(
+ defaultAllocator,
+ $"page.html.lt",
+ """
+
+ <% var data = input as *PageData %>
+ <%= data.title %>
+
+
+""".toString(),
+ &entry
+ )
+
+ assert(status == Status.OK)
+ assert(entry.name.length > 0)
+ assert(entry.typeName.length == 0)
+
+ var content = entry.content.toString()
+ // untyped function takes *void
+ assert(content.contains($"input: *void"))
+ assert(content.contains($"page_html_lt_WriteTemplate"))
+ assert(content.contains($"out.appendString(data.title)"))
+}
+
+@test
+func testGenerateBodyTyped() {
+ // With @type: input is *User, no manual cast needed, named helper gets typed signature
+ var entry = TemplateEntry{
+ .name = StringBuilderInit(64, defaultAllocator),
+ .content = StringBuilderInit(1024, defaultAllocator)
+ }
+
var status = GenerateFromTemplate(
defaultAllocator,
- $"Tony",
- """
-
- <%@import "/title.lt" %>
-
- <%
- for(var i = 0; i < input.length; i+=1) {
- var element = input.getPtr(i)
- %>
-
- <% out.append("%.*s", element.name.length, element.name.buffer) %>
-
- <%
- }
- %>
-
-
- """.toString(),
- output
+ $"greeting.html.lt",
+ """<% @type "models/user" :: User %>
+
+Hello, <%= input.name %>
+
+""".toString(),
+ &entry
)
+
assert(status == Status.OK)
- assert(output.length > 0)
+ assert(entry.name.toString().equals($"greeting_html_lt"))
+ assert(entry.typeName.toString().equals($"User"))
+ assert(entry.typeModule.toString().equals($"models/user"))
+
+ var content = entry.content.toString()
+ // typed function takes *User
+ assert(content.contains($"input: *User"))
+ // type module auto-imported
+ assert(content.contains($"import \"models/user\""))
+ // expression shorthand expanded
+ assert(content.contains($"out.appendString(input.name)"))
+}
+
+@test
+func testGenerateBodyPartial() {
+ var entry = TemplateEntry{
+ .name = StringBuilderInit(64, defaultAllocator),
+ .content = StringBuilderInit(1024, defaultAllocator)
+ }
+
+ var status = GenerateFromTemplate(
+ defaultAllocator,
+ $"page.html.lt",
+ """
+<% @load "templates/header.html.lt" %>
+
+<% @load "templates/card.html.lt" input.card %>
+
+
+""".toString(),
+ &entry
+ )
+
+ assert(status == Status.OK)
+ var content = entry.content.toString()
+
+ // partial without field — passes input unchanged
+ assert(content.contains($"header_html_lt_WriteTemplate(resp, input)"))
+ // partial with field — passes sub-field
+ assert(content.contains($"card_html_lt_WriteTemplate(resp, input.card)"))
}