{"id":34933182,"url":"https://github.com/zigster64/zts","last_synced_at":"2025-12-26T17:31:18.530Z","repository":{"id":199241342,"uuid":"702260845","full_name":"zigster64/zts","owner":"zigster64","description":"Zig Templates made Simple","archived":false,"fork":false,"pushed_at":"2025-11-04T03:09:13.000Z","size":222,"stargazers_count":51,"open_issues_count":3,"forks_count":4,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-11-04T05:19:39.193Z","etag":null,"topics":["http","template","web-template","webdev","zig-package"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zigster64.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.DTT","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-10-09T00:33:19.000Z","updated_at":"2025-10-30T07:46:02.000Z","dependencies_parsed_at":"2023-11-24T14:26:45.251Z","dependency_job_id":"5d1b226b-0022-4374-adc8-90878bd262de","html_url":"https://github.com/zigster64/zts","commit_stats":null,"previous_names":["zigster64/zts"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/zigster64/zts","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zigster64%2Fzts","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zigster64%2Fzts/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zigster64%2Fzts/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zigster64%2Fzts/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zigster64","download_url":"https://codeload.github.com/zigster64/zts/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zigster64%2Fzts/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28057566,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-26T02:00:06.189Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","template","web-template","webdev","zig-package"],"created_at":"2025-12-26T17:31:15.552Z","updated_at":"2025-12-26T17:31:18.523Z","avatar_url":"https://github.com/zigster64.png","language":"Zig","readme":"# ZTS\n\n## Other Zig template management tools to check out\n\n- https://github.com/jetzig-framework/zmpl\n- https://github.com/batiati/mustache-zig\n- https://github.com/haze/etch\n\nalso:\n\n- https://github.com/kristoff-it/ziggy ... static site generator that has some interesting templating possibilities\n\n# Breaking Change - Nov 2025\n\nRefer to this issue for discussion\nhttps://github.com/zigster64/zts/issues/6\n\nBreaking change is that both `write()` and `writeHeader()` are now fully comptime, needing a comptime known \"section\" param.\n\nIf you are using `write()` in existing code to make use of dynamic section names, then please use the new function `writeDynamic()` which fills this niche use case.\n\nIf you are not using dynamic section names, then you shouldnt need any changes to your code.\n\n# Zig Templates made Simple (ZTS)\n\n![zts](https://github.com/zigster64/zts/blob/main/docs/zts.jpg?raw=true)\n\nZTS is a minimalist Zig module that helps you use text templates in a way that is simple, maintainable, and efficient.\n\nSimple:\n- Uses Zig like field tags in your template\n- Uses Zig `fmt.print` for formatting data \n- No funky new templating syntax, no DSL, no new formatting conventions to learn\n\nMaintainable:\n- Control of the template logic is done in your Zig code, not delegated to the template engine\n- Data passed through the template must be explicitly defined\n- There is no magic expansion of data structs that works fine today, and breaks tomorrow as your data model evolves\n- Mismatches between your code, your data, and the template are caught at compile time, not runtime\n\nEfficient:\n- All template parsing is performed at comptime, no runtime overhead\n- Minimal codebase\n- There is just _less_ going on compared to full-featured templating engines\n\nLets have a look ...\n\n\n## ZTS - Very Basic Example\n\nLets say you have a Template file `foobar.txt` that looks like this :\n\n```\n.foo\nI prefer daytime\n.bar\nI like the nighttime\n```\n\nNote the sections `.foo` and `.bar`.  ZTS uses a \"Zig like\" field syntax for defining the section breaks in the text. \n\nThen in your zig code, just embed that file, and then use the `zts.s(template, section_name)` function to return the appropriate section out of the data.\n\n```zig\nconst zts = @import(\"zts\");\n\nconst out = std.io.getStdOut().writer();\n\nconst tmpl = @embedFile(\"foobar.txt\");\ntry out.print(\"foo says - {s}\\n\", zts.s(tmpl, \"foo\"));\ntry out.print(\"bar says - {s}\\n\", zts.s(tmpl, \"bar\"));\n\n```\n\nproduces the output \n```\nfoo says - I prefer daytime\n\nbar says - I like the nighttime\n\n```\n\nThats really all there is to it. Its basically splitting the input into sections delimited by named tags in the input text.\n\n## The contents of data Sections are comptime known\n\nThe data returned from `s(template, section_name)` is comptime known ... which means that it can in turn be passed to Zig standard print functions \nas a formatting string.\n\n```\n.foo\nI like {s}\n.bar\nI prefer {s}\n```\n\n```zig\n\nconst tmpl = @embedFile(\"foobar.txt\");\n\ntry out.print(zts.s(tmpl, \"foo\"), .{\"daytime\"});\ntry out.print(zts.s(tmpl, \"bar\"), .{\"nighttime\"});\n\n```\n\n## ZTS print helper functions\n\nUse of the `s(tmpl, section_name)` function is provided as a low-level utility.\n\nPutting `zts.s(tmpl, section_name)` everywhere is a bit verbose, and gets a bit messy very quickly. \n\nZTS provides helper functions that make it easier to print.\n\n`zts.print(tmpl, section_name, args, writer)` works like `print()` in Zig.\n\n`zts.write(tmpl, section_name, writer)` works like `write()` in Zig.\n\nIn both cases, \"section_name\" must be comptime known, to extract it out of the tmpl as a comptime known string.\n\n\n```\n.foo\nI like {s}\n.bar\nI prefer {s}\n.baz\nI dont really like either\n```\n\n```zig\n\nconst tmpl = @embedFile(\"foobar.txt\");\n\ntry zts.print(tmpl, \"foo\", .{\"daytime\"}, out);\ntry zts.print(tmpl, \"bar\", .{\"nighttime\"}, out);\ntry zts.write(tmpl, \"baz\", out);\n\n```\n\nBecause everything is happening at comptime, if your template file and your Zig code get out of sync due to ongoing changes,\nnothing to fear ... Zig will pick that up at compile time, and throw an error about missing sections in your templates, as \nwell as the standard compile errors about parameters not matching the expected fields in the template.\n\nfor example, if you add this to the code above :\n\n```zig\ntry zts.print(tmpl, \"other\", .{}, out);\nor\ntry zts.write(tmpl, \"other\", out);\n```\n\nThis will throw a compile error saying that there is no section labelled `other` in the template.\n\nIf the template gets modified - say change the label `.foo` to `.fooz` in the text file ... then that will also cause\na compile error in the Zig code, saying that \"foo\" doesnt exist in the template anymore.\n\nIf the template changes again, say ... change the `.foo` contents to `I like {d}` ... then this will also cause a compile\nerror in the Zig code, saying that the string parameter \"daytime\" does not match format \"{d}\" in the template.\n\nThere is no great magic here, its just the power of Zig comptime, as it is actively parsing the text templates at compile time,\nand using the built in Zig `print` formatting which also evaluates at compile time.\n\n## ZTS helper functions for runtime known section names\n\nIf you want to pass data through template segments using the built in Zig `print` functions on the writer, then everything must be comptime.\n\nThere are no exceptions to this, its just the way that Zig `print` works.\n\nIf your template section names are only known at runtime, then its only possible to get the segment contents at runtime, making them\nunusable for `print()` or `write()`.\n\nTo solve this edge case, you can use the `writeDynamic` variant helper functions that ZTS provides to do all the template lookups at runtime.\n\n```zig\ntry zts.writeDynamic(template, section, out);\n```\n\nWhen using `writeDynamic(template, section, out)` ... if the section is null, or cannot be found in the data, then writeDynamice() will\nprint nothing. \n\nThere is also a `lookup()` function that takes runtime / dynamic labels, and returns a non-comptime string of the section ... or `null` if not found.\nIts a runtime version of the `s()` function, that can be used with dynamic labels.\n\nexample:\n```zig\n\n// you can do some fancy dynamic processing here\nconst dynamic_template_section = zts.lookup(tmpl, os.getenv(\"PLANET\").?);\nif (dynamic_template_section == null) {\n   std.debug.print(\"Sorry, cannot find a section for the planet you are on\");\n   return;\n}\ntry out.writeAll(dynamic_template_section);\n\n// or you can do all the above using the writeDynamic helper functions\n// note that if there is no PLANET env, then nothing is printed\ntry zts.writeDynamic(tmpl, os.getenv(\"PLANET\"), out);  \n\n// but you cant do this, because print NEEDS comptime values only, and lookup is a runtime variant\ntry out.print(dynamic_template_section, .{customer_details});  // \u003c\u003c-- compile error ! dynamic_template_section is not comptime known\n\n// or this, because zts.print() \u0026 zts.write() need a comptime known section name\ntry zts.write(tmpl, os.getenv(\"PLANET\"), out)\n\n// and you cant do this either, because s() demands comptime params too\nconst printable_dynamic_section = zts.s(tmpl, os.getenv(\"PLANET\").?);  // \u003c\u003c-- compile error !  unable to resovle comptime value\n```\n\nComptime restrictions can be a pain.\n\nZTS `lookup()`, and `writeDynamic()` might be able to help you out if you need to do some dynamic processing at runtime .. or it might not, \ndepending on how deep a hole of meta programming you are in.\n\n\n## A more common HTML templating example\n\nLets define a typical HTML file, with template segments defined, and add some places where we can print structured data that we pass through the template.\n\nThe HTML template looks like this :\n\n```html\n\u003cdiv\u003e\n  \u003ch1\u003eFinancial Statement Page\u003c/h1\u003e\n\n    .customer_details\n    \u003ch1\u003eCustomer\u003c/h1\u003e\n    \u003cp\u003eName:           {[name]:s}\u003c/p\u003e\n    \u003cp\u003eAddress:        {[address]:s}\u003c/p\u003e\n    \u003cp\u003eCredit Limit: $ {[credit]:.2}\u003c/p\u003e\n\n    .invoice_table\n    \u003ch2\u003eInvoices\u003c/h2\u003e\n    \u003ctable\u003e\n        \u003ctr\u003e\n            \u003cth\u003eDate\u003c/th\u003e\n            \u003cth\u003eDetails\u003c/th\u003e\n            \u003cth\u003eAmount\u003c/th\u003e\n        \u003c/tr\u003e\n\n        .invoice_row\n        \u003ctr\u003e\n            \u003ctd\u003e  {[date]:s}\u003c/td\u003e\n            \u003ctd\u003e  {[details]:s}\u003c/td\u003e\n            \u003ctd\u003e$ {[amount]:.2}\u003c/td\u003e\n        \u003c/tr\u003e\n\n        .invoice_table_total\n        \u003ctr\u003e\n            \u003ctd\u003e\u003c/td\u003e\n            \u003ctd\u003eTotal Due:\u003c/td\u003e\n            \u003ctd\u003e$ {[total]:.2}\u003c/td\u003e\n        \u003c/tr\u003e\n\n    \u003c/table\u003e\n\u003c/div\u003e\n\n```\n\nAnd the Zig code to print data through that template looks like this :\n```zig\nfn printCustomerDetails(out: anytype, cust: *CustomerDetails) !void {\n\n  var tmpl = @embedFile(\"html/financial_statement.html\");\n   \n   try zts.writeHeader(tmpl, out);\n   try zts.print(tmpl, \"customer_details\", .{\n        .name = cust.name,\n        .address = cust.address,\n        .credit = cust.credit,\n   });\n\n   try zts.write(tmpl, \"invoice_table\", out);\n    for (cust.invoices) |invoice|  {\n      try zts.print(tmpl, \"invoice_row\", .{\n          .date = invoice.date,\n          .details = invoice.details,\n          .amount = invoice.amount,\n        },\n      out);\n      total += invoice.amount;\n    }\n\n    try zts.print(tmpl, \"invoice_total\", .{.total = total}, out);\n}\n```\n\nSo thats all pretty explicit.\n\nNote that we cant do this :\n\n```zig\n\n  var tmpl = @embedFile(\"html/financial_statement.html\");\n   \n   try zts.writeHeader(tmpl, out);\n   \n   // explicit parameters defined here\n   // try zts.print(tmpl, \"customer_details\", .{\n        // .name = cust.name,\n        // .address = cust.address,\n        // .credit = cust.credit,\n   // });\n\n   // this alternative will be a compile error instead\n   try zts.print(tmpl, \"customer_details\", cust);\n```\n\nBecause the struct `CustomerDetails` is not an exact match for the parameters that the \"customer_details\" section of the template expects,\nthis will be a compile error.\n\nYes, its more verbose, but its explicit in the Zen of Zig, and easier to maintain.\n\nBy looking at this code only, you can see what parameters the template\nexpects (without reading the template), and you can see what fields of the\nCustomerDetails struct are applied to which template field.\n\nYou cant accidentally miss anything, and any future changes to CustomerDetails struct will not \ncreate any new regressions against the template.\n\n\n## ZTS Templates rely on your Zig code to drive the logic\n\nYou will notice that the pattern used here is that the Zig code is completely driving the flow of logic, and the \"template\" only serves \nto provide a repository of static strings that can be looked up, and delivered at comptime.\n\nAs far as \"template engines\" go - ZTS is just like a fancy table of strings that you have to drive yourself manually.\n\nThis is an inversion of how templating libraries usually work ... where your code passes data to the template engine, which then drives\nthe flow of the logic to produce the output.\n\nThe traditional approach tends to get messy when you want to inject additional logic into the template generation, over and above simple range statements.\n\nOther approaches, such as JSX, employ a variety of character codes enabling you to jump in and out of Javascript inside the template.\n\nor Go templates, which have their own go-like DSL, and the ability to pass a map of function pointers that the template can callback into.\n\nSee https://pkg.go.dev/html/template for details of Go HTML templating.\n\nThere is also the Mustache standard, which offers an array iterator, and lambdas, and rendering of partials amongst other things.\n\nThese are all great of course, but they also delegate the control away from your program, and into a DSL like environment that inevitably employs\nsome magic to get the job done.\n\nIn some instances, it may be more powerful (as well as simpler), to just drive all the logic directly and imperatively from your own code instead.\nIn my subjective opinion, this direct and imperative approach is more in keeping with the Zen of Zig. YMMV.\n\nIf you want the traditional approach, whilst using Zig, have a look at \n\nhttps://github.com/batiati/mustache-zig\n\nwith examples of mustache-zig used in the Zap (web server) project here :\n\nhttps://github.com/zigzap/zap/blob/master/examples/mustache/mustache.zig\n\nThere is also Etch :\nhttps://github.com/haze/etch\n\nThere are some examples of ZTS used with the http.zig library here :\n\nhttps://github.com/zigster64/zig-zag-zoe/blob/master/src/game.zig#L416\n\n... in this case, its rendering a Game board for a multi-player web game\n\n## Selectively print sections from the template\n\nIn the traditional Template-Driven approach, this is normally done by adding syntax to the template such as\n```\nHey There {{customer_title}}\n  You owe us some money !\n\n  Here is the proof ...\n\n{{if language .eq \"de\"}}\n  Geschäftsbedingungen\n  Zahlung 7 Tagen netto\n{{elseif language .eq \"es\"}}\n  Términos y condiciones\n  Pago neto 7 días\n{{elseif }}\n  .... etc etc\n{{else}}\n  Terms and conditons\n  Payment Nett 7 days\n{{endif}}\n```\n\nBut we dont need to add any control flow inside the template in some non-Zig templating language ... we can just do it from the Zig code\nbecause the whole \"template\" is nothing more than a map of section tags to blocks of text.\n\nYou dont even need to print them all !\n\nExample:\n```\nHey There {s}\n  You owe us some money !\n\n  Here is the proof ...\n\n.terms_en\n  Terms and conditons\n  Payment Nett 7 days\n.terms_de\n  Geschäftsbedingungen\n  Zahlung 7 Tagen netto\n.terms_es\n  Términos y condiciones\n  Pago neto 7 días\n.terms_pt\n  Termos e Condições\n  Pagamento líquido em 7 dias\n.terms_fr\n  Termes et conditions\n  Paiement net 7 jours\n.terms_hi\n  नियम और शर्तें\n  भुगतान नेट 7 दिन\n.terms_jp\n  利用規約\n  次の7日でお支払い\n```\n\nLooks a bit cleaner, easier to read, and more Ziggy than the previous example.\n\nCode to process this :\n\n```zig\n\n// dynamically create the label at runtime, based on the LANG env var\n// restriction here is that because the section label is dynamic, it cant be comptime\n// ... and therefore cant be used with the print or write variants\n\ntry terms_section = std.fmt.allocPrint(allocator, \"terms_{s}\", std.os.getenv(\"LANG\").?[0..2]);\ndefer allocator.free(section);\n\ntry zts.printHeader(tmpl, \"Dear Customer\", out);\ntry zts.writeDynamic(tmpl, terms_section, out);\n}\n```\n\n(see example in test cases inside the code in zts.zig)\n\nOr we can even mix up the order of sections in the output depending on some variable :\n\n```zig\nif (is_northern_hemisphere) try zts.printHeader(tmpl, \"Dear Customer\", out);\ntry zts.writeDynamic(tmpl, terms_section, out);\nif (is_southern_hemisphere) try zts.printHeader(tmpl, \"Mate\", out);\n```\n\nSo for our US and EU customers, they get the Notice header followed by the terms and conditions.\n\nFor our AU, NZ, and Sth American customers, because they are upside down, they get the terms and conditions first followed by the Notice header.\n\nDoing the same thing using a traditional template flow would be possible, but likely to be quite ugly, or involve duplicating sections of \nthe template in the original data, wrapped in if statements. \n\nSo again, its not for everyone, but there are definitely some cases where its just simpler and more powerful to keep the control inside your\nZig code rather than a templating engine.\n\n\n## Content that occurs before the first directive\n\nAll content that occurs before the first directive is considered to be the \"header\" of the document.\n\nExample:\n```html\n\u003cdiv\u003e\n   Everything in here is leading content\n\n   .details\n   \u003cdiv\u003e... some details in here\u003c/div\u003e\n   .end_details\n\n   ... more content\n\u003c/div\u003e\n```\n\nYou can use the `printHeader(template, args, out)` to access and print out this header segment.\n```zig\nconst tmpl = @embedFile(\"foobar.txt\");\ntry zts.printHeader(tmpl, .{}, out);\n\n// or the write variant with no extra parameters \ntry zts.writeHeader(tmpl, out);\n```\n\nYou can access this header section using the `s()` function, and passing `null` as the section name.\n\n```zig\nconst tmpl = @embedFile(\"foobar.txt\");\nconst header_content = zts.s(tmpl, null);\n\n// or use lookup for the runtime variant\nconst header_content = zts.lookp(tmpl, null); \n```\n\n# Section Declaration Syntax\n\nIn the template examples above, sections in the template are simply denoted by a line that has a `.directive` and nothing else.\n\nThe syntax for a `.directive` looks a lot like a Zig field declaration\n\nSyntactically, the `.directive` in the template must obey these rules :\n\n- Can start with any amount of leading whitespace\n- Begins with a `.` character\n- Contains just the directive word with no whitespace, and no templated content\n- Directive name cannot contain special characters [] {} - : \\t\n- Is a complete line, terminated by a `\\n` \n\nAny lines that do not obey all of the above rules are considered as content, and not a directive.\n\nExample:\n```\nThings I need to buy this week;\n    - milk\n    - eggs\n    - potatoes\n.car_stuff\n   - indicator fluid\n   - parking meter detector\n.computer_stuff\n    - more ram\n    - more disk\n    - more compilers\n.notes\n   some general notes about things \n   .that need to be purchased\n   by the end of the week\n```\n\nSo that gives us the following sections:\n- header (everything from the start up to car_stuff)\n- car_stuff\n- computer_stuff\n- notes\n\nThe line in notes beginning with `.that` is not seen as a section, rather its part of the notes content\n\n# Future Addition\n\nAs of version 0.12.x of the compiler, Zig comptime can handle getting substrings out of comptime string inputs,\nbut it cant handle returning these slices as comptime values.\n\nIts a bit more complicated than it looks - bascially, comptime strings do not have a usable address until very late \nin the compilation pipeline. This should be addressed in future releases of Zig.\n\nWhen we get there, ZTS will get these shiny new functions :\n\n```\n--- foobar.txt ---\n.foo\nI like daytime {s}\n.bar\nI prefer nighttime {s}\n\n```\n\n```zig\n\nconst MyTemplate = zts.Template(\"templates/foobar.txt\");\n\n// MyTemplate is a comptime generated Type declaration that is equivalent to this :\n\nconst MyTemplate = struct {\n  foo: []const u8 = \"I like daytime {s}\",\n  bar: []const u8 = 'I prefer nighttime {s}\",\n};\n```\n\nWhich you can use in your code like this :\n```\n// note the {} to create an instance of the dynamic type\n\nconst foobar = zts.Template(\"templates/foobar.txt\"){};\n\nzts.print(foobar.foo, \",yes I do !\");\nzts.print(foobar.bar, \"because its quieter\");\n```\n\nto produce the output :\n\n```\nI like daytime, yes I do !\nI prefer nighttime because its quieter\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzigster64%2Fzts","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzigster64%2Fzts","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzigster64%2Fzts/lists"}