{"id":21387167,"url":"https://github.com/sshtools/tinytemplate","last_synced_at":"2025-03-16T12:17:35.172Z","repository":{"id":219369555,"uuid":"695830833","full_name":"sshtools/tinytemplate","owner":"sshtools","description":"A small but powerful Java string template processor","archived":false,"fork":false,"pushed_at":"2024-12-20T18:09:35.000Z","size":140,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-23T00:13:29.875Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sshtools.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2023-09-24T11:10:43.000Z","updated_at":"2024-12-20T18:09:39.000Z","dependencies_parsed_at":null,"dependency_job_id":"23db3ccb-6553-4059-a180-33037f59fed0","html_url":"https://github.com/sshtools/tinytemplate","commit_stats":null,"previous_names":["sshtools/tinytemplate"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Ftinytemplate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Ftinytemplate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Ftinytemplate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sshtools%2Ftinytemplate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sshtools","download_url":"https://codeload.github.com/sshtools/tinytemplate/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243864825,"owners_count":20360360,"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","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":[],"created_at":"2024-11-22T12:12:02.420Z","updated_at":"2025-03-16T12:17:35.150Z","avatar_url":"https://github.com/sshtools.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tinytemplate\n\n![Maven Build/Test JDK 17](https://github.com/sshtools/tinytemplate/actions/workflows/maven.yml/badge.svg)\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.sshtools/tinytemplate/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.sshtools/tinytemplate)\n[![Coverage Status](https://coveralls.io/repos/github/sshtools/tinytemplate/badge.svg)](https://coveralls.io/github/sshtools/tinytemplate)\n[![javadoc](https://javadoc.io/badge2/com.sshtools/tinytemplate/javadoc.svg)](https://javadoc.io/doc/com.sshtools/tinytemplate)\n![JPMS](https://img.shields.io/badge/JPMS-com.sshtools.tinytemplate-purple) \n\n\n![TinyTemplate](src/main/web/logo-no-background.png)\n\nA lightweight Java string template engine. While it is intended to be used with HTML. it \nwill work with any text content. While small, it has some unique features and is fast and \nflexible.\n\nIt requires just 6 HTML-like tags, and a bash-like variable expression syntax.\n\n## Status\n\nFeature complete. Just some test coverage to complete and more documentation.\n\n## Features\n\n * No dependencies, JPMS compliant, Graal Native Image friendly\n * Fast. See Design Choices.\n * Simple Java. Public API consists of just 2 main classes, `TemplateModel` and `TemplateProcessor`.\n * Simple Content. Just `\u003ct:if\u003e` (and `\u003ct:else\u003e`), `\u003ct:include\u003e`, `\u003ct:object\u003e`, `\u003ct:list\u003e` and `\u003ct:instruct/\u003e`. Bash like variable such as `${myVar}`.\n * Internationalisation features.  \n \n## Design Choices\n\n * No reflection. Better performance.\n * No expression language. All conditions are *named*, with the condition calculated in Java code.\n * Focus on HTML.\n * Avoids `String.replace()` and friends.\n * Single pass parser, use lambdas to compute template components only when they are actually needed.    \n\n## Quick Start\n\nAdd the library to your project.\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.sshtools\u003c/groupId\u003e\n    \u003cartifactId\u003etinytemplate\u003c/artifactId\u003e\n    \u003cversion\u003e0.9.3\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n### Example\n\nA simple example showing most of the features.\n\n```java\npublic class Example1 {\n\n    public static void main(String[] args) {\n        System.out.println(new TemplateProcessor.Builder().\n                build().process(TemplateModel.ofContent(\"\"\"\n            \u003chtml\u003e\n                \u003chead\u003e\n                    \u003ct:include cssImports/\u003e\n                \u003c/head\u003e\n                \u003cbody\u003e\n                    \u003ch1\u003e${%title}\u003c/h1\u003e\n                    \n                    \u003cp\u003eThe current time is ${time}\u003c/p\u003e\n                    \u003cp\u003eAnd 2 + 2 = ${answer}\u003c/p\u003e\n                    \u003cp\u003eWeather is ${weather}\u003c/p\u003e\n                    \u003cp\u003eI18n Text1: ${i18n1}\u003c/p\u003e\n                    \u003cp\u003eI18n Text2: ${i18n2}\u003c/p\u003e\n                    \n                    \u003ct:if am\u003e\n                        \u003cp\u003eWhich is AM\u003c/p\u003e\n                    \u003ct:else/\u003e\n                        \u003cp\u003eWhich is PM\u003c/p\u003e\n                    \u003c/t:if\u003e\n                    \n                    \u003ct:if menu\u003e\n                        \u003cul\u003e\n                        \u003ct:list menu\u003e\n                            \u003cli\u003e\n                                \u003ca href=\"${link}\"\u003eTime warp to ${day}\n                                    \u003ct:if friday\u003e\n                                        \u003cb\u003e, it's party time!\u003c/b\u003e\n                                    \u003c/t:if\u003e\n                                \u003c/a\u003e\n                            \u003c/li\u003e\n                        \u003c/t:list\u003e\n                        \u003cul\u003e \n                    \u003c/t:if\u003e\n                    \n                    \u003ct:object me\u003e\n                        \u003cp\u003eName: ${name}\u003c/p\u003e\n                        \u003cp\u003eAge: ${age}\u003c/p\u003e\n                        \u003cp\u003eLocation: ${location}\u003c/p\u003e                        \n                    \u003c/t:object\u003e\n                \u003c/body\u003e\n            \u003c/html\u003e\n                \"\"\").\n            bundle(Example1.class).\n            include(\"cssImports\", TemplateModel.ofContent(\"\u003clink src=\\\"styles.css\\\"/\u003e\")).\n                variable(\"time\", Example1::formatTime).\n                variable(\"answer\", () -\u003e 2 + 2).\n                variable(\"weather\", \"Sunny\").\n                i18n(\"i18n1\", \"key1\").\n                i18n(\"i18n2\", \"key2\", Math.random()).\n                condition(\"am\", () -\u003e Calendar.getInstance().get(Calendar.HOUR_OF_DAY) \u003e 11).\n                list(\"menu\", content -\u003e \n                    Arrays.asList(\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\").stream().map(day -\u003e \n                        TemplateModel.ofContent(content).\n                            variable(\"day\", day).\n                            variable(\"link\", () -\u003e \"/warp-to\u003eday=\" + day).\n                            condition(\"friday\", () -\u003e day.equals(\"Fri\"))\n                    ).toList()\n                )).\n                object(\"me\", content -\u003e TemplateModel.ofContent(content).\n                    variable(\"name\", \"Joe B\").\n                    variable(\"age\", 44).\n                    variable(\"location\", \"London\"))\n            );\n    }\n    \n    private static String formatTime() {\n        return DateFormat.getDateTimeInstance().format(new Date());\n    }\n}\n\n```\n\nAnd a corresponding resource file, `Example1.properties`.\n\n```\ntitle=An Example\nkey1=Some Text\nkey2=Some other text with an argument. Random number is {0}\n```\n\n## Usage\n\n### Variable Expansion\n\nTinyTemplate supports a sort-of-like-Bash syntax for variable expansion. The exact\nbehaviour of each *string* replacement depends on a *parameter* and an *operator*.\n\nThe general syntax is `${parameter[\u003coptions\u003e]}`. The *parameter* and any options are evaluated, then all text starting\nwith the `$` and ending with the `}` is substituted with the result.\n\nMost patterns evaluate a named *parameter*. This can be any *condition*, *variable* or other type.\n\n * Evaluates to `true` when a *condition* of the same name evaluates to `true`  \n * Evaluates to `true` when a *variable* of the same name exists and is not an empty string.\n * Evaluates to `true` when any other type exists.\n\n#### ${parameter}\n\nSimplest type. Just always substitute with with value of a *variable* from the model.\n\n```\n${todaysDate}\n```\n\n#### ${parameter:?string:otherString}\n\nIf *parameter* evaluates to false as either a *variable* or *condition*, the expansion of *otherString* \nis substituted. Otherwise, the expansion of *string* is substituted.\n\n```\n${isPM:?Post Meridiem:Ante Meridiem noon}\n``` \n\n#### ${parameter:-string}\n\nIf *parameter* evaluates to false as either a *variable* or *condition*, the expansion of *string* is substituted.\nOtherwise, the value of *parameter* is substituted.\n\n```\n${location:-Unknown Location}\n```\n\n#### ${parameter:+string}\n\nIf *parameter* evaluates to false as either a *variable* or *condition*, an empty string is substituted, otherwise \nthe expansion of *string* is substituted.\n\n```\n\u003cinput type=\"checked\" ${selected:+checked} name=\"selected\"\u003e\n```\n\n#### ${parameter:=string}\n\nIf *parameter* evaluates to false as either a *variable* or *condition*, the expansion of word is substituted, otherwise an empty string is substituted.\n\n```\n \u003cbutton type=\"button\" ${clipboard-empty:=disabled} id=\"paste\"\u003ePaste\u003c/button\u003e\n```\n\n### Internationalisation\n\n#### In A Template\n\nA special form of Variable Expansion is used for internationalisation in a template. This supports arguments, as well as nested variables as arguments.\n\nYou must still set a `ResourceBundle` on the model for I18n keys in a template to work.\n\n```java\nvar model = TemplateModel.ofContent(\n\t\"\"\"\n\t\u003cp\u003e${%someKey}\u003c/p\u003e\n\t\"\"\").\n\tbundle(MyClass.class);\n```\n\nAnd `MyClass.properties` ..\n\n```\nsomeKey=Some internationalised text\n```\n\n##### Basic\n\nThe simplest syntax is `${%someKey}`, which will replace `someKey` with whatever the value is in the supplied `RessourceBundle`. \n\n##### With Arguments\n\nTo supply arguments, a comma separated list is used.  A comma is used by default, but any separator may be configured, and the separator may be escaped using a backslash `\\`. \n\nFor example `${%someKey arg0,arg 1 with space,arg 2 \\, with comma}`\n\n##### With Nested Variables\n\nAn argument can also be a nested variable that is available in the same scope as the replacement. \n\nFixed text arguments and variables can be mixed in an I18N expression. However, you *cannot* currently mix text and variables in the same argument, for example `${%someKey prefix${var}suffix}` will *not* work.\n\nFor example `${%someKey arg0,${var1},${var2}`\n\n#### In Code\n\nAn alternative way to do parameterised i18n messages is in the model. This is particular useful when the calculation of the message is complex or inconvenient to express as either simple strings in the template, or as argument variables.\n\nIn this case, you use the variable pattern `${someName}` instead of the I18n syntext with the `%` prefix.\n\n```java\n\nvar model = TemplateModel.ofContent(\n\t\"\"\"\n\t\u003cp\u003e${someI18NVariable}\u003c/p\u003e\n\t\u003csmall\u003e${someOtherI18NVariable}\u003c/small\u003e\n\t\"\"\").\n\ti18n(\"someI18NVariable\", \"keyInBundle\").\n\ti18n(\"someOtherI18NVariable\", \"keyInBundleWithArgs\", \"arg0\", \"arg1\").\n\tbundle(MyClass.class);\n```\n\nAnd the bundle ..\n\n```\nkeyInBundle=Some internationalised text\nkeyInBundleWithARgs=Some internationalised text {0} {1}\n```\n\nAs with most `TemplateModel` attributes, you can defer calculation of the template text by supplying the appropriate `Supplier\u003c..\u003e` instead of a direct object reference.\n\n### Tags\n\nTinyTemplates primary use is with HTML and fragments of HTML. Tags by default use an *XML* syntax so as to work well with code editors. Each tag starts with `t:`, so we suggest that you start all documents with the following header .. \n\n```html\n\u003chtml lang=\"en\" xmlns:t=\"https://jadaptive.com/t\"\u003e\n```\n\n.. and all *fragments* of HTML with the following.\n\n```html\n\u003chtml lang=\"en\" xmlns:t=\"https://jadaptive.com/t\"\u003e\n\u003ct:instruct reset/\u003e\n```   \n\nIn both cases, the first line introduces the `t` namespace, so subsequent tags that appear in your document will not be marked as syntax errors by your editor.\n\nThe 2nd line used with fragments, will cause TinyTemplate to reset it's buffer, and forget any output so far collected. In effect, it will remove the first line. \n\nDepend on your editor, you may also need to complete the fragment with a closing `\u003chtml\u003e` tag.\n\n```html\n\u003ct:instruct end/\u003e\n\u003c/html\u003e\n```\n\nThis will prevent the template processor from writing any further output within that template, and so that closing tag will not appear in the processed HTML.\n\n#### If / Else\n\nAllows conditional inclusion of one or two blocks on content. Every condition in the template is assigned a *name*, which will be tied to a piece of Java code which produces whether it evaluates to\n`true`.\n\n```html\n\u003ct:if feelingFriendly\u003e\n    \u003cp\u003eHello World!\u003c/p\u003e\n\u003c/t:if\u003e\n``` \n\nAnd the Java.\n\n```java\nmodel.condition(\"feelingFriendly\", true);\n```\n\nYou can also use `\u003ct:else/\u003e` to provide content that will be rendered when the condition evaluates\nto `false`.\n\n```html\n\u003ct:if feelingFriendly\u003e\n    \u003cp\u003eHello World!\u003c/p\u003e\n\u003ct:else/\u003e\n    \u003cp\u003eGo away world!\u003c/p\u003e\n\u003c/t:if\u003e\n``` \n\nAnd the Java.\n\n```java\nmodel.condition(\"feelingFriendly\", false);\n```\n\nConditions can be negated by prefixing the name with either a  `!` or the more XML syntax friendly `not`. \n\n```html\n\u003ct:if !feelingFriendly\u003e\n    \u003cp\u003eHumbug!\u003c/p\u003e\n\u003c/t:if\u003e\n```\n\nIf no such named condition exists, then checks will be made to see if a *Variable* with the same name\nexists. If it doesn't exist, the condition will evaluate as `false`. If it does exist however, then  \nit's result will depend on the value and it's type. \n\n * `null` evaluates as false.\n * Empty string `\"\"` evaluates as false.\n * Any number with a zero value evaluates as false.\n * An empty list evaluates as false, a list with any elements evaluates as true.\n * All other values evaluate as true.\n \nIf there is no such condition, and no such variable, then checks will be made to see if any such \n*Include* or *Object* exists.\n\n```html\n\u003ct:if me\u003e\n    \u003ct:object me\u003e\n        \u003cp\u003eName : ${name}\u003c/p\u003e\n        \u003cp\u003eAge : ${age}\u003c/p\u003e\n        \u003cp\u003eLocation : ${location}\u003c/p\u003e\n    \u003c/t:object\u003e\n\u003ct:else/\u003e\n    \u003cp\u003eI don't know who I am\u003c/p\u003e\n\u003c/t:if\u003e\n```  \n\n```java\nif(me != null) {\n    model.object(\"me\", content -\u003e TemplateModel.ofContent(content).\n                    variable(\"name\", \"Joe B\").\n                    variable(\"age\", 44).\n                    variable(\"location\", \"London\"));\n}\n```\n\n#### Include\n\nIncludes allows templates to be nested. When a `\u003ct:include my_include/\u003e` tag is encountered in the template, a corresponding `my_include` is looked up in the current `TemplateModel`. This include itself, is a new `TemplateModel`, with it's own content (derived from a `String`, a resource, `Path` or whatever) as any other template.\n\nThe include tag would be a key tool if you were to use TinyTemplate to compose pages of lots of smaller parts. \n\n*An include must be completely self contained. It has no direct access to the template it is contained within. Like any other template, all variables (and potentially further nested includes) must be provided specifically to it.*\n\n**main.html**\n\n```html\n\u003chtml lang=\"en\" xmlns:t=\"https://jadaptive.com/t\"\u003e\n\u003cbody\u003e\n\t\u003ct:include nav_menu/\u003e\n\t\u003cp\u003eMy Main Content\u003c/p\u003e\n\u003c/body\u003e\n\u003c/html\u003e\t\n```\n\n**menu.frag.html**\n\n```html\n\u003chtml lang=\"en\" xmlns:t=\"https://jadaptive.com/t\"\u003e\n\u003ct:instruct reset/\u003e\n\u003cul\u003e\n\t\u003ct:list menu\u003e\n\t\t\u003cli\u003e\u003ca href=\"${href}\"\u003e${action}\u003c/a\u003e\u003c/li\u003e\n\t\u003c/t\u003e\n\u003cul\u003e\n\u003ct:instruct end/\u003e\n\u003c/html\u003e\n```\n\n*Note, the use of `\u003ct:instruct reset/\u003e` and `\u003ct:instruct end/\u003e`. This is not strictly required, it is to help your IDE cope with fragments of HTML with custom tags. See above.*\n\n**Main.java**\n\n```java\n\n\npublic record Anchor(String href, String text) {}\n\n// ...\n\nvar links = Set.of(\n\tnew Anchor(\"file.html\", \"File\"),\n\tnew Anchor(\"edit.html\", \"Edit\"),\n\tnew Anchor(\"view.html\", \"View\"),\n\tnew Anchor(\"help.html\", \"Help\")\n);\n\nvar model = TemplateModel.ofResource(Main.class, \"main.html\").\n \tmodel.object(\"nav_menu\", content -\u003e \n\t\tTemplateModel.ofResource(Main.class, \"menu.frag.html\").\n\t\t\tlist(\"menu\", (content) -\u003e\n\t\t\t\tlinks.stream().map(anchor -\u003e TemplateModel.ofContent(content).\n\t\t\t\t\tvariable(\"href\", anchor::href).\n\t\t\t\t\tvariable(\"text\", anchor::text)\n\t\t\t\t).toList()\n\t\t\t)\n    );\n```\n\n#### List\n\nLists allow blocks of content to be repeated, with different values for each row. Each list is assigned a *name*,\nwhich ties it to the Java code that generates this list.\n\nEach row of a list itself is a `TemplateModel`, which should be constructed from the `content` that is passed to \nto it. Every row of course can then contain any other *TinyTemplate* construct such as variables, includes, further\nnested lists and so on.\n\nFor example,\n\n```html\n\u003ch1\u003eA list of ${number} people\u003c/h1\u003e\n\u003cul\u003e\n\t\u003ct:list people\u003e\n\t\t\u003cli\u003e${_number} - ${name}, ${age} - ${locale}\u003c/li\u003e\n\t\u003c/t:list\u003e\n\u003c/ul\u003e\n```\n\n```java\n\npublic record Person(String name, int age, Locale locale) {}\n\n// ...\n\nvar people = Set.of(\n\tnew Person(\"Joe B\", 44, Locale.ENGLISH),\n\tnew Person(\"Maria Z\", 27, Locale.GERMAN),\n\tnew Person(\"Steve P\", 31, Locale.US),\n);\n \nvar model = TemplateModel.ofContent(html).\n\tvariable(\"number\", people.size()).\n\tlist(\"people\", (content) -\u003e\n\t\tpeople.stream().map(person -\u003e TemplateModel.ofContent(content).\n\t\t\tvariable(\"name\", person::name).\n\t\t\tvariable(\"age\", person::age).\n\t\t\tvariable(\"locale\", person.locale()::getDisplayName).\n\t\t).toList()\n\t);\n\n```\n\nLists make some default variables available to each row. \n\n * `_size`, the size of the list.\n * `_index`, the zero-based index of the current row.\n * `_number`, the number of the current row (i.e. `_index + 1`).\n \nAnd some conditions.\n \n * `_first`, if the current row is the first row, will be `true`.\n * `_last`, if the current row is the last row, will be `true`.\n * `_odd`, if the index of the current row is an odd number, will be `true`.\n * `_even`, if the index of the current row is an even number, will be `true`.\n\n#### Object\n\nThe object tag provides scope to a block a template text. The primary use for this would be to allow the same variable name to be used in more than one place in the current template, making it practical to create reusable `TemplateModel` instances, that for example map to a particular Java object. You can of course do this with the `\u003ct:include\u003e` tag, but `\u003ct:object\u003e` does not require a separate template resource.\n\nUnlike `\u003ct:include\u003e`, it also inherits variables and conditions that exist in it's parent template. Any variables or conditions used, if they do not exist in the objects `TemplateModel`, the parent model will also be queried.\n\n#### Instruct\n\nInstructions are generic commands that can be sent to either the `TemplateProcessor` or some user code.\n\nBuilt-in instructions currently consists of `\u003ct:instruct reset/\u003e` and `\u003ct:instruct end/\u003e` that you have seen elsewhere in this document.\n\nCurrently, user supplied instructions may not alter the template or the processors behaviour in any way. So, they can only be used to supply additional functions that affect the template as whole. This makes their use limited.\n\nIn the future, a richer API to create custom tags and processing may be provided.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsshtools%2Ftinytemplate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsshtools%2Ftinytemplate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsshtools%2Ftinytemplate/lists"}