{"id":28389018,"url":"https://github.com/abxy/hterl","last_synced_at":"2025-10-05T08:32:21.365Z","repository":{"id":57505489,"uuid":"103306493","full_name":"abxy/hterl","owner":"abxy","description":"An Erlang extension which adds HTML tags to the expression syntax.","archived":false,"fork":false,"pushed_at":"2019-06-29T20:07:52.000Z","size":148,"stargazers_count":19,"open_issues_count":2,"forks_count":3,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-28T18:18:06.380Z","etag":null,"topics":["erlang","web"],"latest_commit_sha":null,"homepage":"","language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/abxy.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}},"created_at":"2017-09-12T18:24:15.000Z","updated_at":"2024-06-27T04:13:14.000Z","dependencies_parsed_at":"2022-09-19T19:50:11.298Z","dependency_job_id":null,"html_url":"https://github.com/abxy/hterl","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abxy%2Fhterl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abxy%2Fhterl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abxy%2Fhterl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abxy%2Fhterl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/abxy","download_url":"https://codeload.github.com/abxy/hterl/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/abxy%2Fhterl/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":257142481,"owners_count":22496037,"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":["erlang","web"],"created_at":"2025-05-31T00:02:38.383Z","updated_at":"2025-10-05T08:32:21.354Z","avatar_url":"https://github.com/abxy.png","language":"Erlang","readme":"Hypertext Erlang\n================\n\n[![Hex.pm](https://img.shields.io/hexpm/v/hterl.svg?style=flat)](https://hex.pm/packages/hterl)\n\nA compiler for Hypertext Erlang.\n\nWhat?\n-----\n\nHypertext Erlang is Erlang extended with HTML-like tags in\nthe expression syntax.\n\nThe intended purpose is to provide a better alternative to text templating\nengines for generating dynamic web pages.\n\nWith Hypertext Erlang you write functions that return HTML.\nThe syntax encourages composition by making it seamless to call other\nfunctions that return HTML themselves; and it works beautifully with\nlist comprehensions and the rest of Erlangs syntax.\n\nHere is a small example of how Hypertext Erlang can be used to render\na table.\n\n```erlang\nresults_table(Results) -\u003e\n    SortedResults = lists:sort(Results),\n    \u003ctable\u003e\n        \u003cthead\u003e\n            \u003ctr\u003e\n                \u003cth\u003e\"Time (mm:ss)\"\u003c/th\u003e\n                \u003cth\u003e\"Player\"\u003c/th\u003e\n            \u003c/tr\u003e\n        \u003c/thead\u003e\n        \u003ctbody\u003e\n            [results_row(Result) || Result \u003c- SortedResults]\n        \u003c/tbody\u003e\n    \u003c/table\u003e.\n\nresults_row({Time, Player}) -\u003e\n    \u003ctr\u003e\n        \u003ctd\u003eTime\u003c/td\u003e\n        \u003ctd\u003ePlayer\u003c/td\u003e\n    \u003c/tr\u003e.\n```\n\nNote the absence of substitution markers such as `\u003c% %\u003e` or `{{ }}` around variable names like `Time` and `Player`.\nThat is because the body of a tag expression can contain any expression, not just other tag expressions.\nThis rule also explains why the header texts `\"Time (mm:ss)\"` and `\"Player\"` are quoted\u0026mdash;that's simply how strings are written in Erlang.\nSee [Syntax](#syntax) for more.\n\nUsage\n-----\n\nInclude `hterl` as a dependency in your `rebar.config`, and add the `rebar3_hterl` plugin (found in a [separate repository](https://github.com/abxy/rebar3_hterl/)).\n\n```erlang\n{deps, [{hterl, \"0.10.0\"}]}.\n\n{plugins, [rebar3_hterl]}.\n```\n\nThe plugin registers as a compiler for `.hterl` files, so `rebar3 compile` and all the rest of the rebar commands should just work.\n\nCreate Hypertext Erlang modules in `src/` or a sub-folder, name the source files something like `example.hterl`.\n\nHere is a tiny example to get you started.\n\n```erlang\n-module(example).\n\n-export([unordered_list/1]).\n\nunordered_list(Xs) -\u003e\n\t\u003cul\u003e\n\t\t[\u003cli\u003e X \u003c/li\u003e || X \u003c- Xs]\n\t\u003c/ul\u003e.\n```\n\nRun `rebar3 shell` to compile the application and start a shell.\nIn the shell, you can use.\n\n```\n1\u003e hterl:render_binary(example:unordered_list([\"One\", \"Two\", \"Three\"])).\n\u003c\u003c\"\u003cul\u003e\u003cli\u003eOne\u003c/li\u003e\u003cli\u003eTwo\u003c/li\u003e\u003cli\u003eThree\u003c/li\u003e\u003c/ul\u003e\"\u003e\u003e\n```\n\n**Note:** Normally you would use `hterl:render/1` which returns `iodata()` to avoid unnecessary copying, but a binary is easier to read so it's used here.\n\nIf you just call `unordered_list` without rendering, you can see that the output is a tagged tuple containing an `iolist()`. The tag is used by `hterl` to recognize that the output should not be escaped if it appears in another tag expression.\nThe `render/1` function will simply extract the `iolist()`.\n\n```\n1\u003e example:unordered_list([\"One\", \"Two\", \"Three\"]).\n{pre_html, [\u003c\u003c\"\u003cul\u003e\"\u003e\u003e, [\n\t[\u003c\u003c\"li\"\u003e\u003e, \u003c\u003c\"One\"\u003e\u003e, \u003c\u003c\"\u003c/li\u003e\"\u003e\u003e],\n\t[\u003c\u003c\"li\"\u003e\u003e, \u003c\u003c\"Two\"\u003e\u003e, \u003c\u003c\"\u003c/li\u003e\"\u003e\u003e],\n\t[\u003c\u003c\"li\"\u003e\u003e, \u003c\u003c\"Three\"\u003e\u003e, \u003c\u003c\"\u003c/li\u003e\"\u003e\u003e]\n], \u003c\u003c\"\u003c/ul\u003e\"\u003e\u003e]}\n```\n\nOutput\n------\n\nTag expressions are compiled to expressions that construct `iodata()` directly,\nliteral strings in tag expressions are encoded and escaped on compile time,\neverything else is escaped on runtime.\n\nThe compiler tries to coalesce contiguous binary fragments on compile time,\nto keep the runtime overhead as small as possible.\n\n```erlang\n% source\ncoalesce_example(Content) -\u003e\n    \u003ch1\u003e\"My Header\"\u003c/h1\u003e\n    \u003cp\u003eContent\u003c/p\u003e.\n```\n\nThe function `coalesce_example/1` above is compiled to the following plain Erlang function.\nNote how the first binary fragment contains `\"\u003ch1\u003eMy Header\u003c/h1\u003e\u003cp\u003e\"`, i.e. it has\ncoalesced the starting entire `h1` element with the starting tag of the `p` element. It can't go any further because the value of `Content` is unknown in compile time.\n\n```erlang\n% compiled\ncoalesce_example(Content) -\u003e\n    {pre_html,\n     [\u003c\u003c\"\u003ch1\u003eMy Header\u003c/h1\u003e\u003cp\u003e\"/utf8\u003e\u003e,\n      hterl_api:interpolate(Content, utf8),\n      \u003c\u003c\"\u003c/p\u003e\"/utf8\u003e\u003e]}.\n```\n\nAs you can see in the compiled version, the `Content` variable is wrapped with a call to `hterl_api:interpolate/2`. This function will escape unsafe values (strings, binaries and characters), encode strings, and unwrap `{pre_html, iodata()}` terms produced by other tag expressions.\n\nImplementation\n--------------\n\nHypertext Erlang is based on the official Erlang toolchain,\nre-using as much as possible.\nTokenization is done using Erlang's scanner, followed by\na post-processing step to rewrite some token sequences. \u003c!-- For instance, a `\u003c` token followed by an `{atom, \"h1\"}` token is fused to a `{tag_start, \"h1\"}` token. --\u003e\nThe parser is generated using `yecc` and a modified copy of the Erlang grammar.\nThe compiler works by transforming syntax tree containing tag-expressions to\na plain Erlang parse tree, which is then compiled using Erlang's compiler.\n\nSyntax\n======\n\nThe syntax is not quite HTML, but it should be familiar enough.\nThis section explains the different syntax rules with examples for each.\n\nTags\n----\n\nTag expressions be written with an opening tag and a _matching_ closing tag `\u003cdiv\u003e [...] \u003c/div\u003e`\nor as a self-closing tag `\u003cbr /\u003e` but never as an opening tag without a closing tag.\nThis is because the parser is not aware of which elements are considered _empty_\naccording to the HTML spec.\n\n\nBody\n----\n\nThe body of an element is a comma separated list of expressions whose results are concatenated in the output.\n\n```erlang\ngreet(Name) -\u003e\n    \u003cspan\u003e \"Hello, \", \u003cu\u003e Name \u003c/u\u003e, \"!\" \u003c/span\u003e.\n```\n\nGiven the above definition `greet(\"Joe\")` renders `\u003cspan\u003eHello, \u003cu\u003eJoe\u003c/u\u003e!\u003c/span\u003e`.\nNote that the text in the body of an element has to be quoted.\nThis is a logical consequence of the rule that says that any expression is allowed in the body of an tag expression.\n\nWhitespace\n----------\nSpaces and line breaks, in and around tags, are ignored. If you want the output to contain space between elements in the body, you have to explicitly include it.\n\n```erlang\nwhitespace(Name) -\u003e\n    \u003cdiv\u003e\n        \u003cspan\u003e\u003c/span\u003e, \" \", \u003cspan\u003e\u003c/span\u003e\n    \u003c/div\u003e.\n```\n\n**Output:** `\u003cdiv\u003e\u003cspan\u003e\u003c/span\u003e \u003cspan\u003e\u003c/span\u003e\u003c/div\u003e`\n\nConcatenation\n-------------\n\nAdjacent tag expressions are concatenated into a single expression, therefore no comma is needed between the `\u003cli\u003e` elements below.\n\n```erlang\nprizes() -\u003e\n    \u003col\u003e\n        \u003cli\u003e\"Gold Medal\"\u003c/li\u003e\n        \u003cli\u003e\"Silver Medal\"\u003c/li\u003e\n        \u003cli\u003e\"Bronze Medal\"\u003c/li\u003e\n    \u003c/ol\u003e.\n```\n\nThe concatenation rule is enabled everywhere, not just within element bodies.\n\n```erlang\ndefinition(Term, Definition) -\u003e\n    \u003cdt\u003eTerm\u003c/dt\u003e\n    \u003cdd\u003eDefinition\u003c/dd\u003e.\n\n```\n\nIf this upsets you, consider that many programming languages, including Erlang, concatenate strings in a similar fashion.\n\n```erlang\nstrings() -\u003e\n    \"These two strings are \"\n    \"concatenated into one.\".\n\n```\n\nAttributes\n----------\n\nAttribute values are also written as bare expressions,\nmeaning literal strings have to be quoted, but variables do not.\n\n```erlang\nfrench_hello() -\u003e\n    \u003ci lang=\"fr\"\u003e\"Bonjour le monde!\"\u003c/i\u003e.\n\nlink(Text, Url) -\u003e\n    \u003ca href=Url\u003eText\u003c/a\u003e.\n```\n\nSome expression forms need to be surrounded by parentheses when they appear as attributes.\nNotably, function calls have this requirement.\n\n```erlang\nmessage(Type, Content) -\u003e\n    \u003cspan class=(message_class(Type))\u003eContent\u003c/span\u003e.\n\nmessage_class(error) -\u003e\n    \"message message-error\";\nmessage_class(_) -\u003e\n    \"message\".\n```\n\nHypertext Erlang requires all attribute names to be valid Erlang atoms.\nIn practice, this means that you have to quote attributes that contain dashes like `aria-labelledby`.\n\n```erlang\ndialog(Header, Content) -\u003e\n\t\u003cdiv role=\"dialog\" 'aria-labelledby'=\"dialogheader\"\u003e\n\t\t\u003ch2 id=\"dialogheader\"\u003eHeader\u003c/h2\u003e,\n\t\tContent\n\t\u003c/div\u003e.\n```\n\nComments\n--------\n\nThere is no support for for HTML style comments `\u003c!-- --\u003e`, but Erlang comments are fully supported.\n\n```erlang\nerlang_comments() -\u003e\n    \u003cp\u003e\n        % Erlang comments may appear inside element bodies, ...\n        \"This is a paragraph of text\"\n        \" which continues on the next line.\"\n    \u003c/p\u003e\n    % between elements, ...\n    \u003cspan\n        % and even in the opening tag.\n        style=\"background: pink;\"\u003e\n    \u003c/span\u003e.\n```\n\nOptions\n=======\n\n* `output` (default: `beam`): Sets the output format, possible values are `beam` and `erl`.\n* `outdir` (Optional): If set to a directory path, the output files will be put in this directory. Only relevant in standalone mode as the `rebar3_hterl` plugin overrides it.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabxy%2Fhterl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fabxy%2Fhterl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fabxy%2Fhterl/lists"}