From b9e3d9212f99a973555614ce5510dfc1ade37230 Mon Sep 17 00:00:00 2001 From: Stefan Bilharz Date: Tue, 13 Jan 2026 21:43:11 +0100 Subject: [PATCH 1/3] HTML-48 Properly join MacroFor body expressions and remove hack --- src/instance_template.cr | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/instance_template.cr b/src/instance_template.cr index 69dd2d8..4ebbc84 100644 --- a/src/instance_template.cr +++ b/src/instance_template.cr @@ -125,20 +125,7 @@ module ToHtml {% elsif blk.body.is_a?(MacroFor) %} \{% for {{blk.body.vars.splat}} in {{blk.body.exp}} %} ToHtml.to_html_eval_exps({{io}}, {{indent_level}}) do - # Crystal can turn inline macro expressions (`{{...}}`) inside a macro-for body into - # "line-based" output (newlines around the expansion), which breaks calls without - # parentheses (e.g. `name:\n"value"\n, value:`). We keep real statement newlines, but - # collapse the ones that can't be statement separators. - # - # Upstream: https://github.com/crystal-lang/crystal/issues/16544 - {% - body = blk.body.body.stringify - .gsub(/\s*\n\s*,\s*/, ", ") - .gsub(/\s*\n\s*\./, ".") - .gsub(/\s*\n\s*do\b/, " do") - .gsub(/([:,(\[{=])\s*\n\s*/, "\\1 ") - %} - {{body.id}} + {{blk.body.body.expressions.join("").id}} end \{% end %} {% elsif blk.body.is_a?(MacroExpression) || blk.body.is_a?(MacroLiteral) %} From ddea1a69ebc528d9209645bbdec1e29f7d1add0b Mon Sep 17 00:00:00 2001 From: Stefan Bilharz Date: Tue, 13 Jan 2026 21:51:42 +0100 Subject: [PATCH 2/3] HTML-48 Add MacroIf/MacroFor examples --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index da3489b..132d4d7 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,57 @@ end puts ClassView.to_html ``` +### Compile-time Control Flow (MacroIf / MacroFor) + +In addition to runtime `if`/`#each`, you can also use Crystal's macro control flow (`{% if %}` / `{% for %}`) inside templates. This runs at compile-time and can be used to generate markup based on compile-time information such as compiler flags or `@type`. + +#### `MacroIf` (`{% if %}`) + +```crystal +require "to_html" + +class BuildVariantView + ToHtml.class_template do + head do + {% if flag?(:release) %} + script src: "/assets/app.min.js" + {% else %} + script src: "/assets/app.js" + {% end %} + end + end +end + +puts BuildVariantView.to_html +``` + +#### `MacroFor` (`{% for %}`) + +```crystal +require "to_html" + +class AutoFormView + getter first_name : String + getter last_name : String + + def initialize(@first_name, @last_name); end + + ToHtml.instance_template do + form do + {% for ivar in @type.instance_vars %} + label for: {{ivar.name.stringify}} do + {{ivar.name.stringify}} + end + + input type: :text, name: {{ivar.name.stringify}}, value: {{ivar.name.id}} + {% end %} + end + end +end + +puts AutoFormView.new(first_name: "Ada", last_name: "Lovelace").to_html +``` + ### Attributes #### Named Arguments From b7e142481e167d0671472d03b9e182be0af6192b Mon Sep 17 00:00:00 2001 From: Stefan Bilharz Date: Tue, 13 Jan 2026 21:57:52 +0100 Subject: [PATCH 3/3] Update benchmark --- README.md | 12 ++++++------ shard.lock | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 132d4d7..dcfcefc 100644 --- a/README.md +++ b/README.md @@ -313,12 +313,12 @@ Have a look into the `benchmark/` folder to find out how these numbers were dete Execute `crystal run --release benchmark/benchmark.cr` to reproduce. ``` - ecr 1.55M (643.71ns) (± 3.02%) 4.27kB/op fastest - to_html 884.07k ( 1.13µs) (± 3.45%) 5.52kB/op 1.76× slower - blueprint 457.54k ( 2.19µs) (± 3.70%) 4.9kB/op 3.40× slower - markout 191.89k ( 5.21µs) (± 3.37%) 8.08kB/op 8.10× slower -html_builder 59.03k ( 16.94µs) (± 3.86%) 10.4kB/op 26.32× slower - water 57.48k ( 17.40µs) (± 4.22%) 11.2kB/op 27.03× slower + ecr 999.84k ( 1.00µs) (± 6.97%) 4.27kB/op fastest + to_html 576.96k ( 1.73µs) (± 7.51%) 5.52kB/op 1.73× slower + blueprint 377.61k ( 2.65µs) (± 8.35%) 4.9kB/op 2.65× slower + markout 120.59k ( 8.29µs) (± 8.54%) 8.08kB/op 8.29× slower +html_builder 49.94k ( 20.02µs) (± 7.38%) 10.4kB/op 20.02× slower + water 46.83k ( 21.35µs) (± 8.31%) 11.2kB/op 21.35× slower ``` Compared shards taken from [awesome-crystal](https://github.com/veelenga/awesome-crystal#html-builders) diff --git a/shard.lock b/shard.lock index f60cbb6..cd12f54 100644 --- a/shard.lock +++ b/shard.lock @@ -2,7 +2,7 @@ version: 2.0 shards: blueprint: git: https://github.com/stephannv/blueprint.git - version: 1.0.0 + version: 1.1.0 html_builder: git: https://github.com/crystal-lang/html_builder.git