@zio.dev/zio-blocks 0.0.33 → 0.0.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/adr/2026-07-18-data-migration.md +123 -0
  2. package/guides/async-getting-started.md +687 -0
  3. package/guides/compile-time-resource-safety-with-scope.md +21 -16
  4. package/guides/getting-started-with-mux.md +1395 -0
  5. package/guides/query-dsl-extending.md +161 -102
  6. package/guides/query-dsl-fluent-builder.md +217 -157
  7. package/guides/query-dsl-reified-optics.md +12 -10
  8. package/guides/query-dsl-sql.md +640 -165
  9. package/guides/sql-checked-interpolation.md +173 -0
  10. package/guides/sql-transactions.md +286 -0
  11. package/guides/telemetry-guide.md +1130 -0
  12. package/guides/zio-schema-migration.md +29 -22
  13. package/index.md +248 -389
  14. package/package.json +1 -1
  15. package/plans/config-follow-up-prs.md +188 -0
  16. package/plans/config-pr-assessment-roadmap.md +310 -0
  17. package/reference/MuxDataFlow.jsx +250 -0
  18. package/reference/async.md +1499 -0
  19. package/reference/chunk.md +3533 -308
  20. package/reference/codegen/case-class.md +436 -0
  21. package/reference/codegen/emitter-config.md +383 -0
  22. package/reference/codegen/examples.md +664 -0
  23. package/reference/codegen/field.md +316 -0
  24. package/reference/codegen/index.md +317 -0
  25. package/reference/codegen/scala-emitter.md +392 -0
  26. package/reference/codegen/scala-file.md +276 -0
  27. package/reference/codegen/sealed-trait.md +408 -0
  28. package/reference/codegen/type-definition.md +340 -0
  29. package/reference/codegen/type-ref.md +201 -0
  30. package/reference/combinators.md +347 -117
  31. package/reference/config/config-decoder.md +460 -0
  32. package/reference/config/config-source.md +489 -0
  33. package/reference/config/errors.md +278 -0
  34. package/reference/config/flags.md +369 -0
  35. package/reference/config/formats.md +314 -0
  36. package/reference/config/index.md +304 -0
  37. package/reference/config/rollout.md +336 -0
  38. package/reference/context.md +9 -52
  39. package/reference/data-migration.md +269 -0
  40. package/reference/datastar/attributes.md +302 -0
  41. package/reference/datastar/events.md +234 -0
  42. package/reference/datastar/index.md +256 -0
  43. package/reference/datastar/signals.md +230 -0
  44. package/reference/datastar/sse.md +295 -0
  45. package/reference/datastar.md +346 -0
  46. package/reference/docs.md +1461 -345
  47. package/reference/endpoint/auth-type.md +146 -0
  48. package/reference/endpoint/bulk-creation.md +96 -0
  49. package/reference/endpoint/endpoint.md +297 -0
  50. package/reference/endpoint/http-codec.md +249 -0
  51. package/reference/endpoint/index.md +745 -0
  52. package/reference/endpoint/path-codec.md +225 -0
  53. package/reference/endpoint/route-pattern.md +194 -0
  54. package/reference/endpoint/route-tree.md +111 -0
  55. package/reference/endpoint/segment-codec.md +199 -0
  56. package/reference/html.md +1424 -0
  57. package/reference/htmx/attribute-values.md +359 -0
  58. package/reference/htmx/hx-encoding.md +111 -0
  59. package/reference/htmx/hx-params.md +204 -0
  60. package/reference/htmx/hx-swap.md +276 -0
  61. package/reference/htmx/hx-sync.md +251 -0
  62. package/reference/htmx/hx-target.md +314 -0
  63. package/reference/htmx/hx-trigger.md +457 -0
  64. package/reference/htmx/hx-url-update.md +239 -0
  65. package/reference/htmx/index.md +807 -0
  66. package/reference/htmx/response-headers.md +240 -0
  67. package/reference/http-model/headers.md +735 -0
  68. package/reference/http-model/index.md +49 -0
  69. package/reference/http-model/model.md +1517 -0
  70. package/reference/http-model/schema-codecs.md +522 -0
  71. package/reference/http-model/schema.md +750 -0
  72. package/reference/http-model/server-sent-event.md +341 -0
  73. package/reference/jwt.md +195 -0
  74. package/reference/maybe.md +943 -0
  75. package/reference/media-type.md +2 -2
  76. package/reference/mux.md +254 -0
  77. package/reference/mux.mdx +828 -0
  78. package/reference/openapi.md +1351 -0
  79. package/reference/projection.md +654 -0
  80. package/reference/resource-management/defer-handle.md +1 -1
  81. package/reference/resource-management/resource.md +31 -98
  82. package/reference/resource-management/scope.md +28 -220
  83. package/reference/resource-management/wire.md +5 -55
  84. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  85. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  86. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  87. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  88. package/reference/ringbuffer/advanced.mdx +109 -0
  89. package/reference/ringbuffer/index.mdx +145 -0
  90. package/reference/ringbuffer/mpmc.mdx +185 -0
  91. package/reference/ringbuffer/mpsc.mdx +164 -0
  92. package/reference/ringbuffer/spmc.mdx +108 -0
  93. package/reference/ringbuffer/spsc.mdx +416 -0
  94. package/reference/{allows.md → schema/allows.md} +4 -100
  95. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  96. package/reference/{binding.md → schema/binding.md} +3 -4
  97. package/reference/schema/built-in-codecs/avro.md +451 -0
  98. package/reference/schema/built-in-codecs/bson.md +510 -0
  99. package/reference/schema/built-in-codecs/csv.md +564 -0
  100. package/reference/schema/built-in-codecs/index.md +77 -0
  101. package/reference/schema/built-in-codecs/json/index.md +295 -0
  102. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  103. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  104. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  105. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  106. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  107. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  108. package/reference/schema/built-in-codecs/thrift.md +433 -0
  109. package/reference/schema/built-in-codecs/toon.md +1078 -0
  110. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  111. package/reference/schema/built-in-codecs/yaml.md +552 -0
  112. package/reference/{codec.md → schema/codec.md} +11 -11
  113. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
  114. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  115. package/reference/schema/format.md +92 -0
  116. package/reference/schema/index.md +52 -0
  117. package/reference/schema/migration.md +297 -0
  118. package/reference/{modifier.md → schema/modifier.md} +58 -7
  119. package/reference/{optics.md → schema/optics.md} +2 -2
  120. package/reference/{patch.md → schema/patch.md} +1 -1
  121. package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
  122. package/reference/schema/reflect-transformer.md +140 -0
  123. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  124. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  125. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  126. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  127. package/reference/schema/schema-search.md +263 -0
  128. package/reference/{schema.md → schema/schema.md} +22 -2
  129. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  130. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  131. package/reference/smithy.md +1032 -0
  132. package/reference/sql/db-codec-deriver.md +71 -0
  133. package/reference/sql/db-codec.md +687 -0
  134. package/reference/sql/db-con.md +271 -0
  135. package/reference/sql/db-connection.md +153 -0
  136. package/reference/sql/db-param-writer.md +77 -0
  137. package/reference/sql/db-param.md +66 -0
  138. package/reference/sql/db-result-reader.md +148 -0
  139. package/reference/sql/db-tx.md +114 -0
  140. package/reference/sql/db-value.md +41 -0
  141. package/reference/sql/ddl.md +85 -0
  142. package/reference/sql/frag.md +288 -0
  143. package/reference/sql/index.md +341 -0
  144. package/reference/sql/repo.md +600 -0
  145. package/reference/sql/sql-dialect.md +73 -0
  146. package/reference/sql/sql-logger.md +62 -0
  147. package/reference/sql/sql-name-mapper.md +70 -0
  148. package/reference/sql/table-metadata.md +134 -0
  149. package/reference/sql/table.md +448 -0
  150. package/reference/sql/transactor-zio.md +399 -0
  151. package/reference/sql/transactor.md +363 -0
  152. package/reference/sql-zio.md +112 -0
  153. package/reference/streams/core/index.md +32 -0
  154. package/reference/streams/core/pipeline.md +854 -0
  155. package/reference/streams/core/sink.md +1404 -0
  156. package/reference/streams/core/stream.md +3236 -0
  157. package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
  158. package/reference/streams/execution-and-compatibility/index.md +35 -0
  159. package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
  160. package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
  161. package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
  162. package/reference/streams/index.md +726 -0
  163. package/reference/streams/primitives/index.md +30 -0
  164. package/reference/streams/primitives/reader.md +1992 -0
  165. package/reference/streams/primitives/writer.md +1201 -0
  166. package/reference/telemetry/common/any-value.md +90 -0
  167. package/reference/telemetry/common/attribute-key.md +87 -0
  168. package/reference/telemetry/common/attributes.md +118 -0
  169. package/reference/telemetry/common/index.md +39 -0
  170. package/reference/telemetry/common/instrumentation-scope.md +24 -0
  171. package/reference/telemetry/common/resource.md +34 -0
  172. package/reference/telemetry/index.md +311 -0
  173. package/reference/telemetry/logging/index.md +197 -0
  174. package/reference/telemetry/logging/log-enrichment.md +72 -0
  175. package/reference/telemetry/logging/log-formatter.md +100 -0
  176. package/reference/telemetry/logging/log-record-processor.md +56 -0
  177. package/reference/telemetry/logging/log-record.md +44 -0
  178. package/reference/telemetry/logging/log-writer.md +64 -0
  179. package/reference/telemetry/logging/logger-provider.md +142 -0
  180. package/reference/telemetry/logging/logger.md +83 -0
  181. package/reference/telemetry/logging/severity.md +62 -0
  182. package/reference/telemetry/metrics/index.md +150 -0
  183. package/reference/telemetry/metrics/instruments.md +183 -0
  184. package/reference/telemetry/metrics/labeled-instruments.md +74 -0
  185. package/reference/telemetry/metrics/meter-provider.md +76 -0
  186. package/reference/telemetry/metrics/meter.md +98 -0
  187. package/reference/telemetry/metrics/metric-data.md +57 -0
  188. package/reference/telemetry/otel/custom-exporter.md +216 -0
  189. package/reference/telemetry/otel/index.md +212 -0
  190. package/reference/telemetry/tracing/index.md +155 -0
  191. package/reference/telemetry/tracing/sampler.md +89 -0
  192. package/reference/telemetry/tracing/span-builder.md +57 -0
  193. package/reference/telemetry/tracing/span-context.md +39 -0
  194. package/reference/telemetry/tracing/span-data.md +32 -0
  195. package/reference/telemetry/tracing/span-kind.md +55 -0
  196. package/reference/telemetry/tracing/span-processor.md +53 -0
  197. package/reference/telemetry/tracing/span-status.md +47 -0
  198. package/reference/telemetry/tracing/span.md +117 -0
  199. package/reference/telemetry/tracing/tracer-provider.md +91 -0
  200. package/reference/telemetry/tracing/tracer.md +52 -0
  201. package/reference/typeid.md +5 -83
  202. package/sidebars.js +376 -43
  203. package/undocumented-report.md +528 -270
  204. package/reference/formats.md +0 -694
  205. package/reference/http-model.md +0 -1716
  206. package/reference/streams.md +0 -989
  207. package/ringbuffer.md +0 -249
  208. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  209. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  210. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  211. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  212. /package/reference/{registers.md → schema/registers.md} +0 -0
  213. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  214. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  215. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,1424 @@
1
+ ---
2
+ id: html
3
+ title: "HTML"
4
+ ---
5
+
6
+ `zio-blocks-html` is a **type-safe HTML templating library** providing immutable data structures and a fluent DSL for building HTML, CSS, and JavaScript. It offers compile-time safety, automatic XSS protection, and zero-dependency simplicity across Scala 2.13, 3.x, and both JVM and Scala.js platforms.
7
+
8
+ Core types: `Dom` (HTML tree), `CssSelector` (CSS queries), `DomSelection` (DOM navigation), `Css` (stylesheets), `Js` (JavaScript expressions).
9
+
10
+ The module is structured around these core types:
11
+
12
+ ```
13
+ import zio.blocks.html._
14
+ import zio.blocks.chunk.Chunk
15
+
16
+ // Dom variants
17
+ sealed trait Dom
18
+ case class Dom.Text(content: String) extends Dom
19
+ sealed trait Dom.Element extends Dom
20
+ case class Dom.Element.Generic(tag: String, attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
21
+ case class Dom.Element.Script(attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
22
+ case class Dom.Element.Style(attributes: Chunk[Dom.Attribute], children: Chunk[Dom]) extends Dom.Element
23
+ sealed trait Dom.Element.Void extends Dom // void elements (self-closing, no children)
24
+ case class Dom.Element.VoidGeneric(tag: String, attributes: Chunk[Dom.Attribute]) extends Dom.Element.Void
25
+
26
+ // Sealed content-model markers (implemented by dedicated element classes)
27
+ sealed trait Dom.Element.Li extends Dom.Element // <li>, child of ul/ol
28
+ sealed trait Dom.Element.Cell extends Dom.Element // <th> or <td>, child of tr
29
+ sealed trait Dom.Element.Th extends Dom.Element.Cell
30
+ sealed trait Dom.Element.Td extends Dom.Element.Cell
31
+ sealed trait Dom.Element.Tr extends Dom.Element // <tr>, child of table
32
+ sealed trait Dom.Element.SelectChild extends Dom.Element // <option> or <optgroup>, child of select
33
+ sealed trait Dom.Element.Opt extends Dom.Element.SelectChild
34
+ sealed trait Dom.Element.Optgroup extends Dom.Element.SelectChild
35
+
36
+ // Attribute variants
37
+ sealed trait Dom.Attribute
38
+ case class Dom.Attribute.KeyValue(name: String, value: Dom.AttributeValue) extends Dom.Attribute
39
+ case class Dom.Attribute.AppendValue(name: String, value: Dom.AttributeValue, separator: Dom.AttributeSeparator) extends Dom.Attribute
40
+ case class Dom.Attribute.BooleanAttribute(name: String, enabled: Boolean) extends Dom.Attribute
41
+
42
+ // CssSelector variants (additional types omitted for brevity)
43
+ sealed trait CssSelector
44
+ case class CssSelector.Element(tag: String) extends CssSelector
45
+ case class CssSelector.Class(name: String) extends CssSelector
46
+
47
+ // Css variants
48
+ sealed trait Css
49
+ case class Css.Rule(selector: CssSelector, declarations: Chunk[Css.Declaration]) extends Css
50
+ case class Css.Declaration(property: String, value: String) extends Css
51
+ ```
52
+
53
+ ## Motivation
54
+
55
+ Why use `zio-blocks-html`?
56
+
57
+ String concatenation for HTML is error-prone:
58
+ - No type safety — typos in tag names or attributes only surface at runtime
59
+ - Manual escaping is tedious and easy to forget
60
+ - Tightly couples HTML structure to data serialization
61
+
62
+ Template engines with runtime parsing add overhead and complexity:
63
+ - Parsing validation on every render
64
+ - Separate template syntax to learn
65
+ - Difficult to compose programmatically
66
+
67
+ `zio-blocks-html` provides:
68
+ - **Type-safe construction** via DSL functions (`div(id := "main", p("Hello"))`)
69
+ - **Automatic XSS protection** through position-aware typeclasses and HTML escaping
70
+ - **Scala 3 compile-time optimizations** for string interpolators (constant folding, macro validation)
71
+ - **Zero dependencies** — pure Scala, works with any HTTP framework
72
+ - **Cross-platform** — identical API on JVM and Scala.js
73
+ - **Structured querying** — CSS selectors for DOM navigation and testing
74
+
75
+ ## Installation
76
+
77
+ Add to `build.sbt`:
78
+
79
+ ```
80
+ libraryDependencies += "dev.zio" %% "zio-blocks-html" % "0.0.55"
81
+ ```
82
+
83
+ For Scala.js projects, use `%%%`:
84
+
85
+ ```
86
+ libraryDependencies += "dev.zio" %%% "zio-blocks-html" % "0.0.55"
87
+ ```
88
+
89
+ Supported Scala versions: 2.13.x and 3.x
90
+
91
+ :::note
92
+ The `html` module depends transitively on `zio-blocks-schema` and `zio-blocks-chunk`. The schema dependency enables `ToJs` auto-derivation via JSON encoding — if your type has a `Schema` instance, it automatically becomes usable in `js"..."` expressions.
93
+ :::
94
+
95
+ ## Overview: How They Work Together
96
+
97
+ The module is organized around five core subsystems that compose together:
98
+
99
+ ```
100
+ ┌─────────────────────────────────────────────────────┐
101
+ │ DSL Functions (div, p, span, ...) │
102
+ │ + Attribute Builders (id :=, className +=) │
103
+ │ └─> Produces: Generic / Void element nodes │
104
+ │ │
105
+ ├─────────────────────────────────────────────────────┤
106
+ │ String Interpolators (html"", css"", js"") │
107
+ │ └─> Position-aware typeclasses │
108
+ │ └─> html"" → Dom.Element (attribute escaping) │
109
+ │ └─> css"" → Css (CSS value types) │
110
+ │ └─> js"" → Js (JavaScript with </script> guard) │
111
+ │ │
112
+ ├─────────────────────────────────────────────────────┤
113
+ │ Dom ADT (sealed trait Dom) │
114
+ │ ├─> Dom.Text (HTML-escaped text content) │
115
+ │ ├─> Dom.Element (Generic, Script, Style) │
116
+ │ ├─> Dom.Element.Void (void/self-closing elements) │
117
+ │ ├─> Dom.Doctype (<!DOCTYPE html>) │
118
+ │ └─> Dom.Empty (renders to nothing) │
119
+ │ │
120
+ ├─────────────────────────────────────────────────────┤
121
+ │ Rendering │
122
+ │ ├─> dom.render → minified HTML String │
123
+ │ ├─> dom.render(indent=2) → pretty-printed String │
124
+ │ └─> Used in HTTP responses, testing │
125
+ │ │
126
+ ├─────────────────────────────────────────────────────┤
127
+ │ CSS Selectors + DOM Querying │
128
+ │ ├─> CssSelector ADT (Element, Class, Id, etc.) │
129
+ │ ├─> Fluent combinators (>, >>, +, ~, &, |) │
130
+ │ ├─> DomSelection API (select, filter, extract) │
131
+ │ └─> Used for testing, transformation, navigation │
132
+ └─────────────────────────────────────────────────────┘
133
+ ```
134
+
135
+ **Typical workflow:**
136
+
137
+ 1. **Build** — use DSL (`div(className := "card", ...)`) or interpolators (`html"<div>$content</div>"`)
138
+ 2. **Compose** — nest elements and use typeclasses to extend for custom types
139
+ 3. **Query** — use CSS selectors (`page.select(div.hover).texts`) for testing or transformation
140
+ 4. **Render** — call `Dom#render` or `Dom#render(indent)` for HTTP response or testing
141
+ 5. **Style** — use `css"..."` and `Css.Rule` to define or embed stylesheets
142
+
143
+ ## The Dom ADT
144
+
145
+ The `Dom` sealed trait is the core data model. Everything in the module works with or produces `Dom` nodes.
146
+
147
+ ### The Five Node Types
148
+
149
+ A `Dom` tree is composed of five node types:
150
+
151
+ #### Dom.Text
152
+
153
+ A text node containing string content. Text is automatically HTML-escaped when rendered to prevent XSS injection. Escaping happens at render time (not construction time), so `Dom.Text` stores the raw string.
154
+
155
+ To create text nodes, use the DSL (strings are converted via `ToModifier[String]`) or explicitly. Call `Dom#render` to convert a text node to an HTML string:
156
+
157
+ ```scala
158
+ import zio.blocks.html._
159
+
160
+ val text = Dom.Text("Hello, world!")
161
+ // text: Text = Text("Hello, world!")
162
+ text.render
163
+ // res0: String = "Hello, world!"
164
+ ```
165
+
166
+ #### Dom.Element.Generic
167
+
168
+ A standard HTML element. The tag is the element name, attributes is a `Chunk` of key-value pairs and modifiers, and children is a `Chunk` of DOM nodes. Text content in children is HTML-escaped during rendering.
169
+
170
+ The DSL functions (`div`, `p`, `span`, etc.) construct these:
171
+
172
+ ```scala
173
+ import zio.blocks.html._
174
+
175
+ val elem = div(id := "main", p("Content"))
176
+ ```
177
+
178
+ #### Dom.Element.Script
179
+
180
+ A specialized script element with `tag = "script"`. Unlike `Generic`, Script renders its text children **without HTML escaping**, allowing inline JavaScript to be emitted as-is. This is the mechanism that enables safe `js"..."` interpolation — the interpolator escapes the JavaScript, but the Script element does not double-escape.
181
+
182
+ Use `script().inlineJs(js"...")` or `script().externalJs(url)`:
183
+
184
+ ```scala
185
+ import zio.blocks.html._
186
+
187
+ val inlineScript = script().inlineJs(js"console.log('Hello');")
188
+ val externalScript = script().externalJs("/app.js")
189
+ ```
190
+
191
+ #### Dom.Element.Style
192
+
193
+ A specialized style element with `tag = "style"`. Like Script, Style renders text children **without escaping**, allowing raw CSS to be emitted. Use `style().inlineCss(css"...")`:
194
+
195
+ ```scala
196
+ import zio.blocks.html._
197
+
198
+ val inlineStyle = style().inlineCss(css"body { margin: 0; }")
199
+ ```
200
+
201
+ #### Dom.Doctype
202
+
203
+ A DOCTYPE declaration node. Renders as `<!DOCTYPE value>`. The singleton `doctype` value renders as `<!DOCTYPE html>`:
204
+
205
+ ```scala
206
+ import zio.blocks.html._
207
+
208
+ doctype.render
209
+ // res4: String = "<!DOCTYPE html>"
210
+ ```
211
+
212
+ #### Dom.Empty
213
+
214
+ A no-op node that renders to empty string. Useful for conditional rendering (e.g., `if (condition) element else Dom.Empty`).
215
+
216
+ ### Attributes and Values
217
+
218
+ Elements carry a `Chunk[Dom.Attribute]` where each attribute is one of:
219
+
220
+ - **`Dom.Attribute.KeyValue(name: String, value: Dom.AttributeValue)`** — A standard `name="value"` attribute. The value can be a string, multi-value, boolean, or JavaScript expression.
221
+
222
+ - **`Dom.Attribute.AppendValue(name: String, value: Dom.AttributeValue, separator: Dom.AttributeSeparator)`** — Declares a value to be concatenated with any existing value for the same attribute. Provides accumulation of multi-valued attributes like `class` and `rel`. The separator appears between values (Space, Comma, Semicolon, or Custom).
223
+
224
+ - **`Dom.Attribute.BooleanAttribute(name: String, enabled: Boolean)`** — A standalone attribute (like `disabled`, `checked`, `required`) that renders as just the name when enabled, or is omitted when disabled.
225
+
226
+ :::tip
227
+ The DSL provides `id := value` (`:=`) for single-valued attributes and `className += "class"` (`+=`) for appending to multi-valued attributes. Both are easier to use than constructing `Dom.Attribute` directly.
228
+ :::
229
+
230
+ ### Tree Traversal Operations
231
+
232
+ `Dom` provides four pure tree transformation methods:
233
+
234
+ **`Dom#collect(pf: PartialFunction[Dom, Dom]): List[Dom]`** — Applies a partial function to every node in the tree (depth-first) and collects the results. Useful for extracting or transforming specific nodes:
235
+
236
+ ```scala
237
+ import zio.blocks.html._
238
+
239
+ val tree = div(p("A"), span("B"), p("C"))
240
+ val paragraphs = tree.collect { case el: Dom.Element if el.tag == "p" => el }
241
+ ```
242
+
243
+ **`Dom#filter(predicate: Dom => Boolean): Dom`** — Removes any node for which the predicate returns false. Non-matching nodes are replaced with `Dom.Empty`, and their children are lost. Matching elements have their children recursively filtered:
244
+
245
+ ```scala
246
+ import zio.blocks.html._
247
+
248
+ val tree = div(p("A"), span("Keep"), p("B"), span("Also keep"))
249
+ val filtered = tree.filter {
250
+ case el: Dom.Element => el.tag == "div" || el.tag == "span"
251
+ case _ => true
252
+ }
253
+ ```
254
+
255
+ **`Dom#find(predicate: Dom => Boolean): Option[Dom]`** — Returns the first node (depth-first) matching the predicate, or `None`:
256
+
257
+ ```scala
258
+ import zio.blocks.html._
259
+
260
+ val tree = div(p("First"), span("Second"), p("Third"))
261
+ tree.find { case el: Dom.Element => el.tag == "p"; case _ => false }
262
+ ```
263
+
264
+ **`Dom#transform(f: Dom => Dom): Dom`** — Applies a transformation function to every node in pre-order. Each node receives the transformation first, and if it is an Element, its children are recursed on the transformed node, so a transformation that changes the child list affects what gets recursed into:
265
+
266
+ ```scala
267
+ import zio.blocks.html._
268
+
269
+ val tree = div(h3("Old"), p("Content"))
270
+ val upgraded = tree.transform {
271
+ case el: Dom.Element.Generic if el.tag == "h3" => el.copy(tag = "h2")
272
+ case other => other
273
+ }
274
+ ```
275
+
276
+ ## The HTML DSL
277
+
278
+ The DSL provides functions for all HTML5 elements, allowing fluent construction with a modifier pattern.
279
+
280
+ ### Element Functions
281
+
282
+ All standard HTML elements are available as lowercase functions (or backtick-quoted if they shadow Scala keywords):
283
+
284
+ ```scala
285
+ import zio.blocks.html._
286
+
287
+ // Basic elements
288
+ val paragraph = p("Hello, world!")
289
+ val heading = h1("Welcome")
290
+ val container = div("Content here")
291
+
292
+ // Void elements (auto self-closing)
293
+ val image = img(src := "photo.jpg", alt := "A photo")
294
+ val lineBreak = br
295
+ val horizontalRule = hr
296
+
297
+ // Keyword-shadowed elements (use backticks)
298
+ val objElement = `object`("data.bin")
299
+ ```
300
+
301
+ ### Setting Attributes
302
+
303
+ Attributes are set using the `:=` operator on `AttributeKey` builders:
304
+
305
+ ```scala
306
+ import zio.blocks.html._
307
+
308
+ val link = a(
309
+ href := "https://example.com",
310
+ target := "_blank",
311
+ titleAttr := "Visit Example",
312
+ "Visit Example"
313
+ )
314
+ ```
315
+
316
+ ### Multi-Valued Attributes: Override vs Accumulate
317
+
318
+ For attributes that can have multiple values (`class`, `rel`, `accept`), you can override or accumulate:
319
+
320
+ - **`:=` (override)** — Replaces any previous value. Last assignment wins:
321
+
322
+ ```scala
323
+ import zio.blocks.html._
324
+
325
+ val div1 = div(className := "a", className := "b")
326
+ ```
327
+
328
+ - **`+=` (append)** — Concatenates with the previous value using a separator (space for `class`):
329
+
330
+ ```scala
331
+ import zio.blocks.html._
332
+
333
+ val div2 = div(className += "card", className += "active")
334
+ ```
335
+
336
+ - **Mixed** — Set a base value, then append:
337
+
338
+ ```scala
339
+ import zio.blocks.html._
340
+
341
+ val div3 = div(className := "base").when(true)(className += "extra")
342
+ ```
343
+
344
+ ### Boolean Attributes
345
+
346
+ Use `BooleanAttribute.:=` to conditionally include attributes like `disabled`, `required`, or `checked`:
347
+
348
+ ```scala
349
+ import zio.blocks.html._
350
+
351
+ val isDisabled = true
352
+ val submitBtn = button(disabled := isDisabled, "Submit")
353
+ ```
354
+
355
+ ### Data and ARIA Attributes
356
+
357
+ Use `dataAttr(name)` and `aria(name)` builders for HTML5 data attributes and ARIA accessibility attributes:
358
+
359
+ ```scala
360
+ import zio.blocks.html._
361
+
362
+ val userDiv = div(
363
+ dataAttr("user-id") := "42",
364
+ dataAttr("action") := "edit",
365
+ "User Card"
366
+ )
367
+
368
+ val closeBtn = button(
369
+ aria("label") := "Close",
370
+ aria("expanded") := "false",
371
+ "×"
372
+ )
373
+ ```
374
+
375
+ For custom attributes not provided by the DSL, use the generic `attr(name)` builder:
376
+
377
+ ```scala
378
+ import zio.blocks.html._
379
+
380
+ val alpineDiv = div(attr("x-data") := "{open: false}", "Alpine.js component")
381
+ val htmxDiv = div(attr("hx-get") := "/api/data", "HTMX target")
382
+ ```
383
+
384
+ ### Programmatic Multi-Valued Attributes
385
+
386
+ For programmatic construction of multi-valued attributes (when the DSL `+=` operator is not available), use `Dom.multiAttr(name)` or `Dom.multiAttr(name, separator)`:
387
+
388
+ ```scala
389
+ import zio.blocks.html._
390
+
391
+ // Create a custom multi-valued attribute builder (alternative to DSL's className)
392
+ val classBuilder = Dom.multiAttr("class")
393
+ val div1 = div(classBuilder := "base", classBuilder += "active")
394
+
395
+ // Create a multi-valued attribute with explicit separator
396
+ val rel = Dom.multiAttr("rel", Dom.AttributeSeparator.Space)
397
+ val link = a(rel := "prev", rel += "first", href := "/previous")
398
+ ```
399
+
400
+ The `MultiAttributeKey` class handles accumulation of values with configurable separators (Space, Comma, Semicolon, or Custom).
401
+
402
+ For constructing multi-valued attributes directly from collections (without a builder chain), use the `Iterable[String]` overload:
403
+
404
+ ```scala
405
+ import zio.blocks.html._
406
+
407
+ // Directly create a multi-valued attribute from a collection
408
+ val customClasses = Dom.multiAttr("class", List("card", "active", "large"))
409
+ val div1 = div(customClasses)
410
+ ```
411
+
412
+ This approach is useful when programmatically building multi-valued attributes outside the DSL's builder pattern.
413
+
414
+ ### Children
415
+
416
+ Children can be strings, elements, or collections. The DSL uses the `ToModifier` typeclass to convert values into DOM nodes:
417
+
418
+ ```scala
419
+ import zio.blocks.html._
420
+
421
+ // String children are converted to Dom.Text
422
+ val simple = p("Plain text")
423
+
424
+ // Element children are nested
425
+ val nested = div(p("First"), p("Second"))
426
+
427
+ // Lists of children — elements append directly, no wrapper
428
+ val items = List("Apple", "Banana", "Cherry")
429
+ val listEl = ul(items.map(item => li(item)))
430
+ ```
431
+
432
+ ### Conditional Rendering
433
+
434
+ Use `when(condition)` to apply modifiers conditionally:
435
+
436
+ ```scala
437
+ import zio.blocks.html._
438
+
439
+ val isHighlighted = true
440
+ val box = div(
441
+ className := "box"
442
+ ).when(isHighlighted)(
443
+ className += "highlighted",
444
+ titleAttr := "This is highlighted"
445
+ )
446
+ ```
447
+
448
+ Use `whenSome(option)` to apply modifiers based on an `Option`:
449
+
450
+ ```scala
451
+ import zio.blocks.html._
452
+
453
+ val maybeTitle: Option[String] = Some("Important")
454
+ val card = div(
455
+ className := "card",
456
+ p("Content")
457
+ ).whenSome(maybeTitle)(t => Seq(
458
+ titleAttr := t,
459
+ className += "has-title"
460
+ ))
461
+ ```
462
+
463
+ ### Dom.Element.Void
464
+
465
+ Void HTML elements (`br`, `hr`, `img`, `input`, `meta`, `link`, `area`, `base`, `col`, `embed`, `param`, `source`, `track`, `wbr`) are a **separate type** from `Dom.Element` — they extend `Dom.Element.Void` which extends `Dom` directly (not `Dom.Element`). This means they **structurally cannot have children** — passing a modifier like a string or another element to a void element factory is a **compile-time error**:
466
+
467
+ ```scala
468
+ import zio.blocks.html._
469
+
470
+ img("oops") // ❌ does not compile — Void elements don't accept children
471
+ input(div("child")) // ❌ does not compile — Void elements don't accept children
472
+ // error:
473
+ // Found: ("oops" : String)
474
+ // Required: zio.blocks.html.Dom.Attribute
475
+ // error:
476
+ // Found: zio.blocks.html.Dom.Element
477
+ // Required: zio.blocks.html.Dom.Attribute
478
+ // error:
479
+ // Conflicting definitions:
480
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 17 and
481
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 20
482
+ //
483
+ // error:
484
+ // Conflicting definitions:
485
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 20 and
486
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 26
487
+ //
488
+ // error:
489
+ // Conflicting definitions:
490
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 26 and
491
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 29
492
+ //
493
+ // error:
494
+ // Conflicting definitions:
495
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 50 and
496
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 74
497
+ //
498
+ // error:
499
+ // Conflicting definitions:
500
+ // val link: zio.blocks.html.Dom.Element in object MdocApp at line 43 and
501
+ // val link: zio.blocks.html.Dom.Element in object MdocApp at line 76
502
+ //
503
+ // error:
504
+ // Conflicting definitions:
505
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 74 and
506
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 79
507
+ //
508
+ ```
509
+
510
+ Void elements only accept `Dom.Attribute` arguments:
511
+
512
+ ```scala
513
+ import zio.blocks.html._
514
+
515
+ val image = img(src := "photo.jpg", alt := "A photo") // ✅ compiles — only attributes
516
+ val lineBreak = br // ✅ compiles — no args
517
+ val textField = input(`type` := "text", placeholder := "Enter text") // ✅ compiles
518
+ ```
519
+
520
+ Void elements render as self-closing tags:
521
+
522
+ ```scala
523
+ import zio.blocks.html._
524
+
525
+ val voidElements = div(
526
+ br,
527
+ hr,
528
+ img(src := "photo.jpg", alt := "A photo"),
529
+ input(`type` := "text", placeholder := "Enter text")
530
+ )
531
+ ```
532
+
533
+ ## Typed Content Models
534
+
535
+ Several container elements enforce the HTML content model of their children at
536
+ compile time. Instead of accepting arbitrary modifiers, these factories only
537
+ accept attributes plus children whose type matches the content model:
538
+
539
+ | Factory | Accepted element children | Marker type |
540
+ |---|---|---|
541
+ | `ul(...)`, `ol(...)` | `<li>` | `Dom.Element.Li` |
542
+ | `tr(...)` | `<th>`, `<td>` | `Dom.Element.Cell` (`Th \| Td`) |
543
+ | `table(...)` | `<tr>` | `Dom.Element.Tr` |
544
+ | `select(...)` | `<option>`, `<optgroup>` | `Dom.Element.SelectChild` (`Opt \| Optgroup`) |
545
+ | `optgroup(...)` | `<option>` | `Dom.Element.Opt` |
546
+
547
+ Invalid children are rejected by the compiler:
548
+
549
+ ```scala
550
+ import zio.blocks.html._
551
+
552
+ ul(div("not a list item")) // ❌ does not compile — ul only accepts Li children
553
+ tr(p("not a cell")) // ❌ does not compile — tr only accepts Th/Td cells
554
+ select(div("not an option")) // ❌ does not compile — select only accepts option/optgroup
555
+ // error:
556
+ // None of the overloaded alternatives of method ul in trait HtmlElements with types
557
+ // (children: Iterable[zio.blocks.html.Dom.Element.Li]):
558
+ // zio.blocks.html.Dom.Element
559
+ // (effect: zio.blocks.html.Dom.Attribute | zio.blocks.html.Dom.Element.Li, effects
560
+ // : (zio.blocks.html.Dom.Attribute | zio.blocks.html.Dom.Element.Li)*):
561
+ // zio.blocks.html.Dom.Element
562
+ // (): zio.blocks.html.Dom.Element
563
+ // match arguments (zio.blocks.html.Dom.Element)
564
+ // error:
565
+ // None of the overloaded alternatives of method tr in trait HtmlElements with types
566
+ // (children: Iterable[zio.blocks.html.Dom.Element.Cell]):
567
+ // zio.blocks.html.Dom.Element.Tr
568
+ // (effect: zio.blocks.html.Dom.Attribute | zio.blocks.html.Dom.Element.Cell,
569
+ // effects: (zio.blocks.html.Dom.Attribute | zio.blocks.html.Dom.Element.Cell)*)
570
+ // : zio.blocks.html.Dom.Element.Tr
571
+ // (): zio.blocks.html.Dom.Element.Tr
572
+ // match arguments (zio.blocks.html.Dom.Element)
573
+ // error:
574
+ // None of the overloaded alternatives of method select in trait HtmlElements with types
575
+ // (children: Iterable[zio.blocks.html.Dom.Element.SelectChild]):
576
+ // zio.blocks.html.Dom.Element
577
+ // (
578
+ // effect: zio.blocks.html.Dom.Attribute |
579
+ // zio.blocks.html.Dom.Element.SelectChild,
580
+ // effects: (zio.blocks.html.Dom.Attribute |
581
+ // zio.blocks.html.Dom.Element.SelectChild)*): zio.blocks.html.Dom.Element
582
+ // (): zio.blocks.html.Dom.Element
583
+ // match arguments (zio.blocks.html.Dom.Element)
584
+ // select(div("not an option")) // ❌ does not compile — select only accepts option/optgroup
585
+ // ^^^^^^
586
+ // error:
587
+ // Conflicting definitions:
588
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 17 and
589
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 20
590
+ //
591
+ // error:
592
+ // Conflicting definitions:
593
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 20 and
594
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 26
595
+ //
596
+ // error:
597
+ // Conflicting definitions:
598
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 26 and
599
+ // val tree: zio.blocks.html.Dom.Element in object MdocApp at line 29
600
+ //
601
+ // error:
602
+ // Conflicting definitions:
603
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 50 and
604
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 74
605
+ //
606
+ // error:
607
+ // Conflicting definitions:
608
+ // val link: zio.blocks.html.Dom.Element in object MdocApp at line 43 and
609
+ // val link: zio.blocks.html.Dom.Element in object MdocApp at line 76
610
+ //
611
+ // error:
612
+ // Conflicting definitions:
613
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 74 and
614
+ // val div1: zio.blocks.html.Dom.Element in object MdocApp at line 79
615
+ //
616
+ // error:
617
+ // Conflicting definitions:
618
+ // val image: zio.blocks.html.Dom.Element.Void in object MdocApp at line 38 and
619
+ // val image: zio.blocks.html.Dom.Element.Void in object MdocApp at line 103
620
+ //
621
+ // error:
622
+ // Conflicting definitions:
623
+ // val lineBreak: zio.blocks.html.Dom.Element.Void in object MdocApp at line 39 and
624
+ // val lineBreak: zio.blocks.html.Dom.Element.Void in object MdocApp at line 104
625
+ //
626
+ ```
627
+
628
+ Attributes are accepted alongside the constrained children:
629
+
630
+ ```scala
631
+ import zio.blocks.html._
632
+
633
+ val list = ul(id := "menu", className := "nav", li("Home"), li("About"))
634
+ val row = tr(className := "header-row", th("Name"), td("Value"))
635
+ val picker = select(id := "color", name := "color", option("Red"), optgroup(option("Light"), option("Dark")))
636
+ ```
637
+
638
+ Each factory has three forms: empty (`ul()`), mixed attributes and children
639
+ (`ul(id := "x", li("a"))`), and collections of children
640
+ (`ul(items.map(item => li(item)))`).
641
+
642
+ The typed factories produce elements carrying a sealed marker type (`Li`,
643
+ `Cell`, `Tr`, `SelectChild`, ...). The markers are sealed — their
644
+ implementations live inside the module — so a value typed `Dom.Element.Li` is
645
+ guaranteed to render as `<li>`; the DSL factories are the intended
646
+ construction path. Elements with permissive content models (`li`, `th`, `td`,
647
+ `option`) keep accepting any flow content through the regular modifier API.
648
+
649
+ :::note
650
+ `caption`, `colgroup`, `thead`, `tbody`, and `tfoot` do not have typed markers
651
+ yet, so tables containing them cannot be built with the `table(...)`
652
+ factory. Compose those tables with the `html""` interpolator or generic
653
+ elements.
654
+ :::
655
+
656
+ ### Equality Across Element Classes
657
+
658
+ Element equality is structural (tag + attributes + children + text-escaping
659
+ semantics), independent of the concrete class. A factory-produced `li(...)`
660
+ equals the structurally identical `Generic("li", ...)`, and vice versa; equal
661
+ elements also have equal hash codes:
662
+
663
+ ```scala
664
+ import zio.blocks.html._
665
+ import zio.blocks.chunk.Chunk
666
+
667
+ val fromFactory = li("a")
668
+ val generic = Dom.Element.Generic("li", Chunk.empty, Chunk(Dom.Text("a")))
669
+ fromFactory == generic // true — structural equality across classes
670
+ ```
671
+
672
+ Elements that render differently are never equal, even with identical fields:
673
+ `script`/`style` render their children **unescaped** while `Generic` escapes
674
+ them, so a `Script` never equals an equally-shaped `Generic("script", ...)` —
675
+ equal elements always render identically.
676
+
677
+ ## String Interpolators
678
+
679
+ The module provides four string interpolators: `html""`, `css""`, `js""`, and `selector""`. All offer compile-time safety (on Scala 3) and automatic escaping appropriate to their context.
680
+
681
+ ### `html""` Interpolator
682
+
683
+ The `html""` interpolator is **position-aware**: it detects whether each interpolated argument is in an attribute-value position or content position, then summons the appropriate typeclass.
684
+
685
+ The `html""` interpolator determines position based on the preceding string:
686
+
687
+ **Position detection:**
688
+ - **Attribute position**: if the preceding string part ends with `=`, `='`, or `="`, the next argument is treated as an attribute value and uses `ToAttrValue[A]`
689
+ - **Content position**: otherwise, the argument is treated as element content and uses `ToElements[A]`
690
+
691
+ Position-aware interpolation enables safe attribute values and content:
692
+
693
+ ```scala
694
+ import zio.blocks.html._
695
+
696
+ val name = "Alice"
697
+ val age = 30
698
+
699
+ // name is in content position → ToElements[String]
700
+ // age is in attribute position → ToAttrValue[Int]
701
+ val elem = html"""<div id="user-$age" class="profile">User: $name</div>"""
702
+ ```
703
+
704
+ **Single-root requirement**: The `html""` interpolator requires a **single root element**. Multiple top-level nodes cause an exception at runtime (Scala 2) or compile error for static templates (Scala 3):
705
+
706
+ ```scala
707
+ import zio.blocks.html._
708
+
709
+ // This compiles (single root)
710
+ val page = html"<div><p>A</p><p>B</p></div>"
711
+ ```
712
+
713
+ **XSS Protection:**
714
+
715
+ Content interpolated into `html""` is stored as `Dom.Text` nodes, which are HTML-escaped during `Dom#render` to prevent XSS:
716
+
717
+ ```scala
718
+ import zio.blocks.html._
719
+
720
+ val userInput = "<script>alert('XSS')</script>"
721
+ val safe = html"<p>$userInput</p>"
722
+ ```
723
+
724
+ :::danger
725
+ The `html""` interpolator requires a **single root element**. On Scala 3, pure-static templates with multiple roots fail at compile time. On Scala 2 or for dynamic templates, the error occurs at evaluation time. Always wrap multiple elements in a container (e.g., `<div>`).
726
+ :::
727
+
728
+ ### `css""` Interpolator
729
+
730
+ The `css""` interpolator returns a `Css` value with `ToCss` typeclass dispatch for interpolated parts:
731
+
732
+ ```scala
733
+ import zio.blocks.html._
734
+
735
+ val color = "blue"
736
+ val size = 16
737
+
738
+ val styles = css"color: $color; font-size: ${size}px;"
739
+ ```
740
+
741
+ Use `CssLength` and `CssColor` types for type-safe CSS values:
742
+
743
+ ```scala
744
+ import zio.blocks.html._
745
+
746
+ val width = css"width: ${CssLength(300.0, "px")};"
747
+ val background = css"background: ${CssColor.Hex.unsafe("ff0000")};"
748
+ ```
749
+
750
+ :::note
751
+ `CssColor.Hex` returns `Option[CssColor]` because hex validation may fail. Use `CssColor.Hex.apply()` (or just `CssColor.Hex(...)`) for safe validation, or `CssColor.Hex.unsafe()` for known-valid hex strings.
752
+ :::
753
+
754
+ ### `js""` Interpolator
755
+
756
+ The `js""` interpolator returns a `Js` value. Strings are automatically quoted and escaped; numerics and booleans are rendered unquoted:
757
+
758
+ ```scala
759
+ import zio.blocks.html._
760
+
761
+ val message = "Hello, world!"
762
+ val count = 42
763
+
764
+ val code = js"console.log($message); alert($count);"
765
+ ```
766
+
767
+ :::warning
768
+ Never interpolate untrusted user input directly into `js""`. The interpolator escapes string values, but the `Js` value is rendered without additional escaping in script elements. Always use `ToJs[String]` (which automatically quotes and escapes) for any untrusted input.
769
+ :::
770
+
771
+ The interpolator protects against `</script>` injection by escaping `<` and `>` as Unicode escapes:
772
+
773
+ ```scala
774
+ import zio.blocks.html._
775
+
776
+ val userInput = "if (x < y) alert('<script>');"
777
+ val code = js"let check = $userInput"
778
+
779
+ println(code.value)
780
+ // let check = "if (x < y) alert('<script>');"
781
+ // (with < and > characters escaped as < and > in actual output)
782
+ ```
783
+
784
+ ### `selector""` Interpolator
785
+
786
+ The `selector""` interpolator returns a `CssSelector`:
787
+
788
+ ```scala
789
+ import zio.blocks.html._
790
+
791
+ val className = "active"
792
+ val selector = selector".$className"
793
+ ```
794
+
795
+ ## CSS Selectors and the DSL
796
+
797
+ The `CssSelector` ADT provides a fluent DSL for building CSS selectors with combinator methods.
798
+
799
+ ### Basic Selectors
800
+
801
+ Create selectors for elements, classes, IDs, or universal matches:
802
+
803
+ ```scala
804
+ import zio.blocks.html._
805
+
806
+ val divSel = CssSelector.Element("div")
807
+ val classSel = CssSelector.Class("container")
808
+ val idSel = CssSelector.Id("header")
809
+ val universal = CssSelector.Universal
810
+ ```
811
+
812
+ ### Combinators
813
+
814
+ All HTML elements (`div`, `span`, `p`, etc.) implement `CssSelectable`, so you can use them directly as selectors:
815
+
816
+ ```scala
817
+ import zio.blocks.html._
818
+
819
+ // Element selectors via DSL elements
820
+ val divSel = div.selector
821
+ val childSel = div > span // div > span
822
+ val descendantSel = div >> span // div span (descendant)
823
+ val adjacentSel = div + span // div + span
824
+ val siblingSel = div ~ span // div ~ span
825
+ val andSel = div & CssSelector.Class("active") // div.active
826
+ val orSel = div | span // div, span
827
+ ```
828
+
829
+ ### Pseudo-Classes and Pseudo-Elements
830
+
831
+ Pseudo-classes match elements by their state, and pseudo-elements create dynamic content:
832
+
833
+ ```scala
834
+ import zio.blocks.html._
835
+
836
+ val hoverSel = a.hover // a:hover
837
+ val firstChild = li().firstChild // li:first-child
838
+ val nthChild = tr().nthChild(2) // tr:nth-child(2)
839
+ val before = div.before // div::before
840
+ val after = span.after // span::after
841
+ ```
842
+
843
+ ### Attribute Selectors
844
+
845
+ Select elements by their attribute values using built-in matchers:
846
+
847
+ ```scala
848
+ import zio.blocks.html._
849
+
850
+ val input = CssSelector.Element("input")
851
+
852
+ val hasType = input.withAttribute("type") // input[type]
853
+ val exactType = input.withAttribute("type", "text") // input[type="text"]
854
+ val containsClass = input.withAttributeContaining("class", "btn") // input[class*="btn"]
855
+ val startsWithHref = a.withAttributeStarting("href", "https") // a[href^="https"]
856
+ val endsWithPng = img.withAttributeEnding("src", ".png") // img[src$=".png"]
857
+ ```
858
+
859
+ ## DOM Querying with DomSelection
860
+
861
+ The `DomSelection` API lets you query and navigate DOM trees using CSS selectors. It is useful for testing, transforming templates, and extracting information.
862
+
863
+ ### Selecting Elements
864
+
865
+ Call `Dom#select(selector)` to query the tree:
866
+
867
+ ```scala
868
+ import zio.blocks.html._
869
+
870
+ val page = div(
871
+ header(nav(a(href := "/", "Home"), a(href := "/about", "About"))),
872
+ main(p(className += "intro", "Hello"), p("World")),
873
+ footer(p("© 2026"))
874
+ )
875
+
876
+ // Query by tag
877
+ val paragraphs = page.select(CssSelector.Element("p"))
878
+ paragraphs.length
879
+
880
+ // Query by class
881
+ val intros = page.select(CssSelector.Class("intro"))
882
+ intros.texts
883
+ ```
884
+
885
+ ### Navigation
886
+
887
+ The `DomSelection` API returned by `Dom#select` provides navigation methods like `.children` and `.first` to traverse the selection results:
888
+
889
+ ```scala
890
+ import zio.blocks.html._
891
+
892
+ val page = div(
893
+ nav(a(href := "/", "Home"), a(href := "/about", "About")),
894
+ main(p("Content"))
895
+ )
896
+
897
+ // Direct children
898
+ val navLinks = page.select(CssSelector.Element("nav")).children
899
+ ```
900
+
901
+ ### Extraction
902
+
903
+ The `DomSelection` API provides methods like `.attrs` and `.texts` to extract attribute values and text content from selected elements:
904
+
905
+ ```scala
906
+ import zio.blocks.html._
907
+
908
+ val page = div(
909
+ a(href := "/home", "Home"),
910
+ a(href := "/about", "About"),
911
+ a(href := "/contact", "Contact")
912
+ )
913
+
914
+ val hrefs = page.select(CssSelector.Element("a")).attrs("href")
915
+ ```
916
+
917
+ ### Filtering
918
+
919
+ The `DomSelection` API provides `.filter` and `.withClass` methods to filter selections by predicate or attribute:
920
+
921
+ ```scala
922
+ import zio.blocks.html._
923
+
924
+ val page = div(
925
+ p(className += "visible", "A"),
926
+ p(className += "hidden", "B"),
927
+ p("C")
928
+ )
929
+
930
+ // Filter by predicate
931
+ val visible = page.select(CssSelector.Element("p")).filter {
932
+ case el: Dom.Element => el.attributes.exists {
933
+ case attr: Dom.Attribute.KeyValue if attr.name == "class" => true
934
+ case _ => false
935
+ }
936
+ case _ => false
937
+ }
938
+
939
+ // Filter by class
940
+ val withClass = page.select(CssSelector.Element("p")).withClass("visible")
941
+ ```
942
+
943
+ ### Modifying Selections
944
+
945
+ :::warning
946
+ `DomSelection` returns new copies with modifications; the original DOM tree remains unchanged. The `DomSelection` API provides functional transformations. To modify the original DOM tree, use `Dom#transform` with a tree-rewriting function, or rebuild the tree from scratch using the DSL.
947
+ :::
948
+
949
+ The `DomSelection` API provides methods like `.modifyAll`, `.replaceAll`, and `.removeAll` to transform or replace selected nodes:
950
+
951
+ ```scala
952
+ import zio.blocks.html._
953
+
954
+ val page = div(
955
+ p("Old 1"),
956
+ p("Old 2"),
957
+ span("Keep")
958
+ )
959
+
960
+ // Transform all Element nodes in the selection; non-Element nodes pass through unchanged
961
+ val modifiedSelection = page.select(CssSelector.Element("p")).modifyAll {
962
+ case el: Dom.Element.Generic if el.tag == "p" => el.copy(tag = "div")
963
+ case other => other
964
+ }
965
+
966
+ // Replace all selected nodes (returns DomSelection of replacement nodes)
967
+ val replacedSelection = page.select(CssSelector.Element("p")).replaceAll(p("New"))
968
+
969
+ // Remove all selected nodes (returns empty DomSelection)
970
+ val removedSelection = page.select(CssSelector.Element("p")).removeAll
971
+ ```
972
+
973
+ :::note
974
+ Pseudo-class selectors (`:hover`, `:focus`, etc.) match elements structurally by their underlying element selector only — they cannot detect browser interaction state in a static DOM tree. `div.hover` matches the same elements as `CssSelector.Element("div")`.
975
+
976
+ Adjacent sibling (`+`) and general sibling (`~`) selectors are supported in CSS output but not in DOM querying — `DomSelection.select` with these combinators returns empty results.
977
+ :::
978
+
979
+ ## The CSS ADT
980
+
981
+ The `Css` ADT represents structured stylesheets as a typed data structure, separate from strings.
982
+
983
+ All `Css` subtypes support `.render()` for minified output and `.render(indent: Int)` for indented pretty-printing:
984
+ - `Css.Rule` — single CSS rule with selector and declarations
985
+ - `Css.Sheet` — collection of rules
986
+ - `Css.Raw` — raw CSS string
987
+ - `Css.Comment` — CSS comment
988
+
989
+ ### Typed Values: Lengths and Colors
990
+
991
+ A `Css.Declaration` takes its value as a `String`, so nothing stops `"10pixels"` or `"#gggggg"` from reaching a stylesheet. `CssLength` and `CssColor` are the typed alternatives: each validates on construction and renders itself, so an invalid value cannot be built rather than being caught downstream.
992
+
993
+ `CssLength` pairs a numeric value with a unit, and rejects units outside the CSS set — `px`, `em`, `rem`, `%`, `vh`, `vw`, `ch`, `ex`, `vmin`, `vmax`, `cm`, `mm`, `in`, `pt`, `pc`:
994
+
995
+ ```scala
996
+ import zio.blocks.html._
997
+ import zio.blocks.html.CssLength._
998
+ ```
999
+
1000
+ The second import is what brings the numeric extension methods into scope. They live in `object CssLength` rather than the package, so `import zio.blocks.html._` alone gives you the `CssLength` constructor but not `300.px`.
1001
+
1002
+ Writing the constructor directly works, but the extension methods on `Int` and `Double` are shorter and produce the same value:
1003
+
1004
+ ```scala
1005
+ CssLength(300.0, "px").render
1006
+ // res45: String = "300px"
1007
+ 300.px.render
1008
+ // res46: String = "300px"
1009
+ 1.5.rem.render
1010
+ // res47: String = "1.5rem"
1011
+ ```
1012
+
1013
+ `CssLengthIntOps` and `CssLengthDoubleOps` provide `px`, `em`, `rem`, `pct`, `vh`, and `vw` on numeric literals. `pct` is spelled out because `%` is not a legal method name:
1014
+
1015
+ ```scala
1016
+ 50.pct.render
1017
+ // res48: String = "50%"
1018
+ 100.vh.render
1019
+ // res49: String = "100vh"
1020
+ ```
1021
+
1022
+ A whole-number `Double` renders without its decimal point, so `2.0.em` and `2.em` produce identical CSS:
1023
+
1024
+ ```scala
1025
+ 2.0.em.render
1026
+ // res50: String = "2em"
1027
+ 2.em.render
1028
+ // res51: String = "2em"
1029
+ ```
1030
+
1031
+ `CssColor` is a sealed ADT with five cases, covering the notations CSS accepts:
1032
+
1033
+ | Case | Constructor | Renders as |
1034
+ | ---------------------- | --------------------------------- | ----------------------- |
1035
+ | `CssColor.Hex` | `Hex(value)` / `Hex.unsafe(value)` | `#ff0000` |
1036
+ | `CssColor.Rgb` | `Rgb(r, g, b)` | `rgb(255,0,0)` |
1037
+ | `CssColor.Rgba` | `Rgba(r, g, b, a)` | `rgba(255,0,0,0.5)` |
1038
+ | `CssColor.Hsl` | `Hsl(h, s, l)` | `hsl(0,100%,50%)` |
1039
+ | `CssColor.Named` | `Named(name)` | `red` |
1040
+
1041
+ The numeric cases are plain case classes, so they need no validation step:
1042
+
1043
+ ```scala
1044
+ CssColor.Rgb(255, 0, 0).render
1045
+ // res52: String = "rgb(255,0,0)"
1046
+ CssColor.Rgba(255, 0, 0, 0.5).render
1047
+ // res53: String = "rgba(255,0,0,0.5)"
1048
+ CssColor.Hsl(0, 100, 50).render
1049
+ // res54: String = "hsl(0,100%,50%)"
1050
+ ```
1051
+
1052
+ `Hsl` renders its saturation and lightness with percent signs while taking them as bare `Int`s, so pass `100` rather than `1.0` for full saturation.
1053
+
1054
+ `CssColor.Hex` and `CssColor.Named` are the two validated cases, and both return an `Option` from `CssColor.Hex.apply` rather than throwing:
1055
+
1056
+ ```scala
1057
+ CssColor.Hex("ff0000")
1058
+ // res55: Option[CssColor] = Some(Hex("ff0000"))
1059
+ CssColor.Hex("gggggg")
1060
+ // res56: Option[CssColor] = None
1061
+ ```
1062
+
1063
+ A leading `#` is optional and the value is lowercased, so the same colour written four ways yields one representation:
1064
+
1065
+ ```scala
1066
+ CssColor.Hex("#FF0000").map(_.render)
1067
+ // res57: Option[String] = Some("#ff0000")
1068
+ ```
1069
+
1070
+ `CssColor.Hex.unsafe` skips validation for literals you know are well-formed. It returns a `CssColor` directly rather than an `Option`, which is what makes it convenient inside an interpolator — and what makes it unsuitable for anything user-supplied:
1071
+
1072
+ ```scala
1073
+ CssColor.Hex.unsafe("ff0000").render
1074
+ // res58: String = "#ff0000"
1075
+ ```
1076
+
1077
+ :::warning[`unsafe` renders whatever it is given]
1078
+ `CssColor.Hex.unsafe` performs no checking, so `Hex.unsafe("not-a-color")` renders as `#not-a-color` and produces a stylesheet the browser silently ignores. Use it only for compile-time literals; route anything dynamic through `CssColor.Hex.apply` and handle the `None`.
1079
+ :::
1080
+
1081
+ ### Declarations and Rules
1082
+
1083
+ A `Css.Declaration` is a property-value pair:
1084
+
1085
+ ```scala
1086
+ import zio.blocks.html._
1087
+ import zio.blocks.chunk.Chunk
1088
+
1089
+ val marginDecl = Css.Declaration("margin", "10px")
1090
+ val colorDecl = Css.Declaration("color", "blue")
1091
+
1092
+ val rule = Css.Rule(
1093
+ CssSelector.Element("p"),
1094
+ Chunk(marginDecl, colorDecl)
1095
+ )
1096
+
1097
+ rule.render
1098
+ ```
1099
+
1100
+ ### Stylesheets
1101
+
1102
+ A `Css.Sheet` is a collection of rules:
1103
+
1104
+ ```scala
1105
+ import zio.blocks.html._
1106
+ import zio.blocks.chunk.Chunk
1107
+
1108
+ val bodyRule = Css.Rule(
1109
+ CssSelector.Element("body"),
1110
+ Chunk(
1111
+ Css.Declaration("margin", "0"),
1112
+ Css.Declaration("font-family", "sans-serif")
1113
+ )
1114
+ )
1115
+
1116
+ val stylesheet = Css.Sheet(Chunk(bodyRule))
1117
+
1118
+ stylesheet.render(indent = 2)
1119
+ ```
1120
+
1121
+ ### Embedding Stylesheets
1122
+
1123
+ Embed stylesheets in HTML via the `style()` element:
1124
+
1125
+ ```scala
1126
+ import zio.blocks.html._
1127
+ import zio.blocks.chunk.Chunk
1128
+
1129
+ val page = html(
1130
+ head(
1131
+ style().inlineCss(
1132
+ Css.Sheet(Chunk(
1133
+ Css.Rule(
1134
+ CssSelector.Element("body"),
1135
+ Chunk(Css.Declaration("background", "#f0f0f0"))
1136
+ )
1137
+ ))
1138
+ )
1139
+ ),
1140
+ body(p("Styled"))
1141
+ )
1142
+
1143
+ page.render
1144
+ ```
1145
+
1146
+ ### Raw CSS
1147
+
1148
+ For CSS features not expressible via the ADT (media queries, keyframes, at-rules), use `Css.Raw`:
1149
+
1150
+ ```scala
1151
+ import zio.blocks.html._
1152
+
1153
+ val mediaQuery = Css.Raw("""
1154
+ |@media (max-width: 600px) {
1155
+ | body { font-size: 14px; }
1156
+ |}
1157
+ """.stripMargin)
1158
+
1159
+ println(mediaQuery.render)
1160
+ ```
1161
+
1162
+ :::tip
1163
+ Prefer `Css.Rule` and `Css.Sheet` over `Css.Raw` when possible — structured CSS enables future optimization and prevents CSS injection. Use `Css.Raw` only for trusted, hardcoded CSS.
1164
+ :::
1165
+
1166
+ ### CSS Comments
1167
+
1168
+ Add comments to stylesheets using `Css.Comment`:
1169
+
1170
+ ```scala
1171
+ import zio.blocks.html._
1172
+ import zio.blocks.chunk.Chunk
1173
+
1174
+ val stylesheet = Css.Sheet(Chunk(
1175
+ Css.Comment("Mobile-first responsive design"),
1176
+ Css.Rule(
1177
+ CssSelector.Element("body"),
1178
+ Chunk(Css.Declaration("font-size", "16px"))
1179
+ ),
1180
+ Css.Comment("Tablet and desktop breakpoints"),
1181
+ Css.Raw("@media (min-width: 768px) { body { font-size: 18px; } }")
1182
+ ))
1183
+
1184
+ stylesheet.render(indent = 2)
1185
+ ```
1186
+
1187
+ ## Rendering
1188
+
1189
+ All `Dom` and `Css` values support multiple rendering modes.
1190
+
1191
+ ### Minified Rendering
1192
+
1193
+ `Dom#render` produces compact HTML with no extra whitespace. Use `Dom#renderMinified` as an explicit alias for the same operation:
1194
+
1195
+ ```scala
1196
+ import zio.blocks.html._
1197
+
1198
+ val page = div(h1("Title"), p("Content"))
1199
+ page.render
1200
+ ```
1201
+
1202
+ ### Pretty-Printed Rendering
1203
+
1204
+ `render(indent: Int)` produces indented, readable output:
1205
+
1206
+ ```scala
1207
+ import zio.blocks.html._
1208
+
1209
+ val page = div(h1("Title"), p("Content"))
1210
+ page.render(indent = 2)
1211
+ ```
1212
+
1213
+ ### Performance Notes
1214
+
1215
+ - `Dom#render` uses a pre-allocated `StringBuilder` and while-loop rendering — zero allocations for iteration
1216
+ - Indentation strings get cached in an array of pre-built space strings (up to 128 characters) to prevent repeated string allocation
1217
+ - Void elements automatically self-close with no children
1218
+ - Script and Style elements render their children without escaping (with `</` → `<\/` protection for scripts)
1219
+
1220
+ ## Security: XSS Protection
1221
+
1222
+ The module provides multiple layers of automatic XSS protection:
1223
+
1224
+ ### HTML Text Escaping
1225
+
1226
+ All `Dom.Text` nodes are HTML-escaped during rendering:
1227
+ - `&` → `&amp;`
1228
+ - `<` → `&lt;`
1229
+ - `>` → `&gt;`
1230
+ - `"` → `&quot;`
1231
+ - `'` → `&#x27;`
1232
+
1233
+ Untrusted content is always escaped to prevent XSS:
1234
+
1235
+ ```scala
1236
+ import zio.blocks.html._
1237
+
1238
+ val userInput = "<script>alert('XSS')</script>"
1239
+ val safe = div(p(userInput))
1240
+
1241
+ safe.render
1242
+ ```
1243
+
1244
+ ### JavaScript String Escaping
1245
+
1246
+ The `ToJs[String]` typeclass escapes strings to prevent breaking out of script contexts:
1247
+ - `<` → backslash-u-0-0-3-c (the six-character Unicode escape sequence `<`)
1248
+ - `>` → backslash-u-0-0-3-e (the six-character Unicode escape sequence `>`)
1249
+ - `&` → backslash-u-0-0-2-6 (the six-character Unicode escape sequence `&`)
1250
+ - `"` → `\"`, `'` → `\'`, `\` → `\\`
1251
+ - Newlines, carriage returns, and Unicode line/paragraph separators are escaped
1252
+
1253
+ This protects against `</script>` injection:
1254
+
1255
+ ```scala
1256
+ import zio.blocks.html._
1257
+
1258
+ val userInput = "</script><script>alert('XSS');</script>"
1259
+ val code = js"let payload = $userInput"
1260
+
1261
+ println(code.value)
1262
+ // let payload = "</script><script>alert('XSS');</script>"
1263
+ // (with < and > characters escaped as Unicode in actual output)
1264
+ ```
1265
+
1266
+ ### URL Sanitization
1267
+
1268
+ Attributes named `href`, `src`, `action`, or `formaction` are checked for dangerous schemes at render time:
1269
+ - `javascript:`, `vbscript:`, `data:text/html` → prefixed with `unsafe:`
1270
+
1271
+ Dangerous URLs are automatically sanitized in HTML output:
1272
+
1273
+ ```scala
1274
+ import zio.blocks.html._
1275
+
1276
+ val dangerous = a(href := "javascript:alert('XSS')", "Click me")
1277
+ ```
1278
+
1279
+ ### No Raw HTML Escape Hatch
1280
+
1281
+ The module intentionally provides no `Dom.Raw` type for embedding arbitrary HTML. This prevents XSS by ensuring all dynamic content is either constructed via the DSL or interpolated through context-aware interpolators.
1282
+
1283
+ ## Common Patterns
1284
+
1285
+ The module supports several architectural patterns for code organization and reuse:
1286
+
1287
+ ### Building Reusable Components
1288
+
1289
+ Define functions that return `Dom.Element` to create reusable components:
1290
+
1291
+ ```scala
1292
+ import zio.blocks.html._
1293
+
1294
+ def card(title: String, content: String): Dom.Element =
1295
+ div(
1296
+ className := "card",
1297
+ h2(title),
1298
+ p(content)
1299
+ )
1300
+
1301
+ val page = div(
1302
+ card("Card 1", "Content A"),
1303
+ card("Card 2", "Content B")
1304
+ )
1305
+ ```
1306
+
1307
+ ### Conditional Rendering
1308
+
1309
+ Use `when` and `whenSome` for conditional modifiers:
1310
+
1311
+ ```scala
1312
+ import zio.blocks.html._
1313
+
1314
+ def userCard(name: String, isAdmin: Boolean): Dom.Element =
1315
+ div(
1316
+ className := "user-card"
1317
+ ).when(isAdmin)(
1318
+ className += "admin",
1319
+ span(className := "badge", "Admin")
1320
+ )
1321
+ ```
1322
+
1323
+ ### Rendering Collections
1324
+
1325
+ Map over collections to create child elements:
1326
+
1327
+ ```scala
1328
+ import zio.blocks.html._
1329
+
1330
+ def userList(users: List[String]): Dom.Element =
1331
+ ul(users.map(user => li(user)))
1332
+
1333
+ val page = userList(List("Alice", "Bob", "Charlie"))
1334
+ ```
1335
+
1336
+ ### Template Composition
1337
+
1338
+ Combine interpolators and DSL for flexibility:
1339
+
1340
+ ```scala
1341
+ import zio.blocks.html._
1342
+
1343
+ val title = "My Page"
1344
+ val content = "Welcome to my site"
1345
+
1346
+ val page = html"""
1347
+ <html>
1348
+ <head><title>$title</title></head>
1349
+ <body>
1350
+ ${div(p(content))}
1351
+ </body>
1352
+ </html>
1353
+ """
1354
+ ```
1355
+
1356
+ ### Querying for Tests
1357
+
1358
+ Use `DomSelection` to write structural assertions:
1359
+
1360
+ ```scala
1361
+ import zio.blocks.html._
1362
+
1363
+ val page = div(
1364
+ ul(
1365
+ li(a(href := "/home", "Home")),
1366
+ li(a(href := "/about", "About"))
1367
+ )
1368
+ )
1369
+
1370
+ // Test: check number of links
1371
+ val links = page.select(CssSelector.Element("a"))
1372
+ assert(links.length == 2)
1373
+
1374
+ // Test: check specific href
1375
+ val aboutLink = page.select(a.withAttribute("href", "/about"))
1376
+ assert(aboutLink.texts.contains("About"))
1377
+ ```
1378
+
1379
+ ## Complete Example: A Dashboard Page
1380
+
1381
+ Here is a complete, self-contained example building a full dashboard page with navigation, metadata, styling, and content sections:
1382
+
1383
+ ```scala
1384
+ import zio.blocks.html._
1385
+ import zio.blocks.chunk.Chunk
1386
+
1387
+ val userName = "Alice"
1388
+ val items = List("Dashboard", "Settings", "Logout")
1389
+
1390
+ val pieces: Chunk[Dom] = Chunk(
1391
+ doctype,
1392
+ html(
1393
+ head(
1394
+ meta(charset := "utf-8"),
1395
+ meta(name := "viewport", content := "width=device-width, initial-scale=1.0"),
1396
+ title("My App"),
1397
+ link(rel := "stylesheet", href := "/style.css"),
1398
+ style().inlineCss(
1399
+ Css.Sheet(Chunk(
1400
+ Css.Rule(CssSelector.Element("body"), Chunk(
1401
+ Css.Declaration("margin", "0"),
1402
+ Css.Declaration("font-family", "sans-serif")
1403
+ ))
1404
+ ))
1405
+ )
1406
+ ),
1407
+ body(
1408
+ header(
1409
+ nav(items.map(item => a(href := "#", item)))
1410
+ ),
1411
+ main(
1412
+ h1(s"Welcome, $userName!"),
1413
+ p("This is your dashboard.")
1414
+ ),
1415
+ footer(
1416
+ p("© 2026 My App")
1417
+ ),
1418
+ script().inlineJs(js"console.log('Page loaded');")
1419
+ )
1420
+ )
1421
+ )
1422
+
1423
+ pieces.map(_.render(indent = 2)).mkString("\n")
1424
+ ```