@zio.dev/zio-blocks 0.0.33 → 0.0.51

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 (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
package/reference/docs.md CHANGED
@@ -1,524 +1,1640 @@
1
1
  ---
2
2
  id: docs
3
- title: "Docs Reference"
3
+ title: "ZIO Blocks Docs (Markdown)"
4
+ sidebar_label: "Docs"
4
5
  ---
5
6
 
6
- # Docs Module Reference
7
+ ZIO Blocks Markdown is a **pure, zero-dependency GitHub Flavored Markdown library** providing an immutable ADT for markdown documents, a strict parser with error handling, multiple renderers (GFM markdown, HTML, terminal), and a compile-time validated string interpolator. Core types: `Doc`, `Block`, `Inline`, `Parser`, `Renderer`, `ToMarkdown`.
7
8
 
8
- Complete API reference for the zio-blocks-docs module - a zero-dependency GitHub Flavored Markdown library.
9
+ Here are the core type definitions:
10
+
11
+ ```scala
12
+ import zio.blocks.chunk.Chunk
13
+
14
+ final case class Doc(blocks: Chunk[Block], metadata: Map[String, String] = Map.empty)
15
+
16
+ sealed trait Block extends Product with Serializable
17
+ final case class Paragraph(content: Chunk[Inline]) extends Block
18
+ final case class Heading(level: HeadingLevel, content: Chunk[Inline]) extends Block
19
+ final case class CodeBlock(info: Option[String], code: String) extends Block
20
+
21
+ sealed trait Inline extends Product with Serializable
22
+ final case class Text(value: String) extends Inline
23
+ final case class Code(value: String) extends Inline
24
+ final case class Link(text: Chunk[Inline], url: String, title: Option[String]) extends Inline
25
+ ```
26
+
27
+ ## Motivation
28
+
29
+ Markdown is the de facto standard for documentation, READMEs, and formatted text in software development. However, parsing and generating markdown at runtime often requires hand-crafted parsers or fragile string concatenation. ZIO Blocks Markdown provides:
30
+
31
+ - **Type-safe ADT**: Every markdown element is a Scala case class—no stringly-typed HTML generation
32
+ - **Strict parsing**: Rejects invalid markdown with precise error locations (line, column)
33
+ - **Round-trip semantics**: Parse-render-parse cycles preserve document structure
34
+ - **Normalized equality**: Two documents are equal if their normalized forms are equal (adjacent text nodes merged, empty blocks removed)
35
+ - **Multiple output formats**: Render to GFM markdown, HTML (full document or fragment), or colorized terminal
36
+ - **Compile-time validation**: The `md"..."` string interpolator validates markdown syntax at compile time, catching errors before runtime
37
+ - **Type class system**: `ToMarkdown` lets you interpolate custom types into markdown documents
9
38
 
10
39
  ## Installation
11
40
 
12
- ```scala
13
- libraryDependencies += "dev.zio" %% "zio-blocks-docs" % "0.0.33"
41
+ Add the dependency to your `build.sbt`:
42
+
43
+ ```sbt
44
+ libraryDependencies += "dev.zio" %% "zio-blocks-markdown" % "0.0.51"
14
45
  ```
15
46
 
16
- ## Core Types
47
+ For Scala.js, use the cross-build syntax:
17
48
 
18
- ### Doc
49
+ ```sbt
50
+ libraryDependencies += "dev.zio" %%% "zio-blocks-markdown" % "0.0.51"
51
+ ```
19
52
 
20
- The top-level document container. A `Doc` wraps a `Chunk[Block]` representing the document's block-level elements, plus optional metadata.
53
+ Supported Scala versions: 2.13.x and 3.x
54
+
55
+ ## How They Work Together
56
+
57
+ Markdown documents flow through a **parsing → normalization → rendering pipeline**:
21
58
 
22
- ```scala
23
- final case class Doc(blocks: Chunk[Block], metadata: Map[String, String] = Map.empty)
24
59
  ```
60
+ Markdown String ──> Parser ──> Doc (blocks) ──> Renderer ──> Markdown/HTML/Terminal
61
+ │ │
62
+ ParseError ToMarkdown
63
+ (type class)
64
+
65
+ Doc = Chunk[Block]
66
+ Block = Paragraph | Heading | CodeBlock | List | Table | ... (contains Inlines)
67
+ Inline = Text | Code | Emphasis | Strong | Link | Image | ... (leaf nodes)
68
+ ```
69
+
70
+ **Typical workflow:**
25
71
 
26
- **Key methods:**
27
- - `++`: Concatenate two documents (merges blocks and metadata, right wins on conflicts)
28
- - `normalize`: Merge adjacent Text nodes and remove empty blocks
29
- - `toHtml`: Render to full HTML5 document (with DOCTYPE, html, head, body tags)
30
- - `toHtmlFragment`: Render to HTML content only (no html/head/body wrapper)
31
- - `toTerminal`: Render with ANSI escape codes for terminal display
32
- - `toString`: Render back to GFM Markdown
72
+ 1. **Parse markdown** — `Parser.parse("# Hello")` returns `Either[ParseError, Doc]`
73
+ 2. **Build programmatically** — construct `Doc` with `Heading`, `Paragraph`, `BulletList`, etc.
74
+ 3. **Normalize** — `doc.normalize` merges adjacent text, removes empty blocks
75
+ 4. **Render to desired format** — GFM (`Renderer.render`), HTML (`HtmlRenderer.render`), terminal (`TerminalRenderer.render`)
76
+ 5. **Use interpolator** — `md"# Title with $interpolation"` validates markdown at compile time
33
77
 
34
- **Equality:** Two documents are equal if their normalized forms are equal.
78
+ Here's a complete example of composing a document:
35
79
 
36
- **Example:**
37
80
  ```scala
81
+ import zio.blocks.chunk.Chunk
38
82
  import zio.blocks.docs._
39
83
 
40
- val doc = Parser.parse("# Hello World").toOption.get
41
- val markdown = doc.toString // "# Hello World\n"
42
- val html = doc.toHtml // Full HTML5 document
43
- val fragment = doc.toHtmlFragment // Just the content
44
- val terminal = doc.toTerminal // ANSI colored output
84
+ val doc = Doc(Chunk(
85
+ Heading(HeadingLevel.H1, Chunk(Text("My Document"))),
86
+ Paragraph(Chunk(Text("This is a paragraph with "), Strong(Chunk(Text("bold"))), Text(" text."))),
87
+ CodeBlock(Some("scala"), "val x = 42"),
88
+ BulletList(Chunk(
89
+ ListItem(Chunk(Paragraph(Chunk(Text("Item 1")))), None),
90
+ ListItem(Chunk(Paragraph(Chunk(Text("Item 2")))), None)
91
+ ), tight = true)
92
+ ))
45
93
  ```
46
94
 
47
- ### Block
95
+ Rendering to GFM markdown:
48
96
 
49
- Block-level elements that make up a document:
97
+ ```scala
98
+ import zio.blocks.chunk.Chunk
99
+ import zio.blocks.docs._
50
100
 
51
- | Variant | Description |
52
- |---------|-------------|
53
- | `Paragraph(content: Chunk[Inline])` | A paragraph of inline content |
54
- | `Heading(level: HeadingLevel, content: Chunk[Inline])` | ATX heading (H1-H6) |
55
- | `CodeBlock(info: Option[String], code: String)` | Fenced code block with optional language |
56
- | `ThematicBreak` | Horizontal rule (`---`, `***`, `___`) |
57
- | `BlockQuote(content: Chunk[Block])` | Quoted block content |
58
- | `BulletList(items: Chunk[ListItem], tight: Boolean)` | Unordered list |
59
- | `OrderedList(start: Int, items: Chunk[ListItem], tight: Boolean)` | Ordered list with start number |
60
- | `ListItem(content: Chunk[Block], checked: Option[Boolean])` | List item, optionally a task item |
61
- | `HtmlBlock(content: String)` | Raw HTML block |
62
- | `Table(header: TableRow, alignments: Chunk[Alignment], rows: Chunk[TableRow])` | GFM table |
101
+ Renderer.render(doc)
102
+ // res0: String = """# My Document
103
+ // This is a paragraph with **bold** text.
104
+ //
105
+ // ```scala
106
+ // val x = 42
107
+ // ```
108
+ // - Item 1
109
+ // - Item 2
110
+ // """
111
+ ```
63
112
 
64
- **Note on Lists:** The `tight` parameter indicates whether the list should be rendered without blank lines between items (tight) or with blank lines (loose).
113
+ Rendering to HTML:
65
114
 
66
- ### Inline
115
+ ```scala
116
+ import zio.blocks.chunk.Chunk
117
+ import zio.blocks.docs._
67
118
 
68
- Inline elements within blocks:
119
+ HtmlRenderer.render(doc)
120
+ // res1: String = "<!DOCTYPE html><html><head></head><body><h1>My Document</h1><p>This is a paragraph with <strong>bold</strong> text.</p><pre><code class=\"language-scala\">val x = 42</code></pre><ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul></body></html>"
121
+ ```
69
122
 
70
- | Variant | Description |
71
- |---------|-------------|
72
- | `Text(value: String)` | Plain text |
73
- | `Code(value: String)` | Inline code (backticks) |
74
- | `Emphasis(content: Chunk[Inline])` | Italic text (`*text*` or `_text_`) |
75
- | `Strong(content: Chunk[Inline])` | Bold text (`**text**` or `__text__`) |
76
- | `Strikethrough(content: Chunk[Inline])` | Strikethrough (`~~text~~`) |
77
- | `Link(text: Chunk[Inline], url: String, title: Option[String])` | Hyperlink |
78
- | `Image(alt: String, url: String, title: Option[String])` | Image |
79
- | `HtmlInline(content: String)` | Raw inline HTML |
80
- | `SoftBreak` | Soft line break (rendered as space in HTML) |
81
- | `HardBreak` | Hard line break (two spaces or backslash before newline) |
82
- | `Autolink(url: String, isEmail: Boolean)` | Auto-detected URL or email |
123
+ Rendering to terminal:
83
124
 
84
- **Note:** Both top-level case classes and `Inline.X` nested variants exist for compatibility. They are treated identically.
125
+ ```scala
126
+ import zio.blocks.chunk.Chunk
127
+ import zio.blocks.docs._
85
128
 
86
- ### HeadingLevel
129
+ TerminalRenderer.render(doc)
130
+ // res2: String = """My Document
131
+ //
132
+ // This is a paragraph with bold text.
133
+ //
134
+ // val x = 42
135
+ //
136
+ // • Item 1
137
+ // • Item 2
138
+ // """
139
+ ```
87
140
 
88
- Heading levels H1 through H6:
141
+ Parsing markdown strings to create documents:
89
142
 
90
143
  ```scala
91
- sealed abstract class HeadingLevel(val value: Int)
92
- object HeadingLevel {
93
- case object H1 extends HeadingLevel(1)
94
- case object H2 extends HeadingLevel(2)
95
- case object H3 extends HeadingLevel(3)
96
- case object H4 extends HeadingLevel(4)
97
- case object H5 extends HeadingLevel(5)
98
- case object H6 extends HeadingLevel(6)
99
-
100
- def fromInt(n: Int): Option[HeadingLevel]
101
- def unsafeFromInt(n: Int): HeadingLevel // Throws on invalid input
144
+ import zio.blocks.chunk.Chunk
145
+ import zio.blocks.docs._
146
+
147
+ val markdown = """# My Document
148
+
149
+ This is a paragraph with **bold** text.
150
+
151
+ - Item 1
152
+ - Item 2
153
+ """
154
+
155
+ Parser.parse(markdown) match {
156
+ case Right(parsedDoc) => println(s"Successfully parsed ${parsedDoc.blocks.size} blocks")
157
+ case Left(err) => println(s"Parse error: ${err.message}")
102
158
  }
103
159
  ```
104
160
 
105
- **Example:**
161
+ Round-trip verification (parse → render → parse preserves structure):
162
+
106
163
  ```scala
107
- HeadingLevel.fromInt(2) // Some(H2)
108
- HeadingLevel.fromInt(7) // None
109
- HeadingLevel.unsafeFromInt(3) // H3
110
- HeadingLevel.H1.value // 1
164
+ import zio.blocks.chunk.Chunk
165
+ import zio.blocks.docs._
166
+
167
+ val input = "# Hello\n\nWorld"
168
+ // input: String = """# Hello
169
+ //
170
+ // World"""
171
+ val parsed1 = Parser.parse(input).toOption.get
172
+ // parsed1: Doc = Doc(
173
+ // blocks = IndexedSeq(
174
+ // Heading(level = H1, content = IndexedSeq(Text("Hello"))),
175
+ // Paragraph(IndexedSeq(Text("World")))
176
+ // ),
177
+ // metadata = Map()
178
+ // )
179
+ val rendered = Renderer.render(parsed1)
180
+ // rendered: String = """# Hello
181
+ // World
182
+ //
183
+ // """
184
+ val parsed2 = Parser.parse(rendered).toOption.get
185
+ // parsed2: Doc = Doc(
186
+ // blocks = IndexedSeq(
187
+ // Heading(level = H1, content = IndexedSeq(Text("Hello"))),
188
+ // Paragraph(IndexedSeq(Text("World")))
189
+ // ),
190
+ // metadata = Map()
191
+ // )
192
+ parsed1 == parsed2 // Equal after normalization
193
+ // res5: Boolean = true
111
194
  ```
112
195
 
113
- ### Alignment
196
+ ## Common Patterns
197
+
198
+ The markdown module provides several patterns for working with documents and types. Here are the most common usage scenarios:
114
199
 
115
- Table column alignment:
200
+ ### String Interpolation with Compile-Time Validation
201
+
202
+ The `md"..."` interpolator validates markdown at compile time and supports interpolation of any type with a `ToMarkdown` instance:
116
203
 
117
204
  ```scala
118
- sealed trait Alignment
119
- object Alignment {
120
- case object None extends Alignment // Default alignment (---)
121
- case object Left extends Alignment // Left aligned (:---)
122
- case object Center extends Alignment // Center aligned (:---:)
123
- case object Right extends Alignment // Right aligned (---:)
124
- }
125
- ```
205
+ import zio.blocks.chunk.Chunk
206
+ import zio.blocks.docs._
126
207
 
127
- ### TableRow
208
+ val name = "Alice"
209
+ val count = 42
210
+ ```
128
211
 
129
- A row in a table:
212
+ Now interpolate these values into markdown:
130
213
 
131
214
  ```scala
132
- final case class TableRow(cells: Chunk[Chunk[Inline]])
215
+ import zio.blocks.chunk.Chunk
216
+ import zio.blocks.docs._
217
+
218
+ md"# Welcome $name\nYou have $count items."
219
+ // res6: Doc = Doc(
220
+ // blocks = IndexedSeq(
221
+ // Heading(
222
+ // level = H1,
223
+ // content = IndexedSeq(Text("Welcome Alice\\nYou have 42 items."))
224
+ // )
225
+ // ),
226
+ // metadata = Map()
227
+ // )
133
228
  ```
134
229
 
135
- Each cell contains a chunk of inline elements, allowing rich formatting within table cells.
230
+ ### Task Lists (GFM Feature)
231
+
232
+ Task list items use `ListItem` with `checked: Option[Boolean]`:
136
233
 
137
- ## Parsing
234
+ ```scala
235
+ import zio.blocks.chunk.Chunk
236
+ import zio.blocks.docs._
138
237
 
139
- ### Parser.parse
238
+ val tasks = BulletList(Chunk(
239
+ ListItem(Chunk(Paragraph(Chunk(Text("Buy groceries")))), Some(true)),
240
+ ListItem(Chunk(Paragraph(Chunk(Text("Write docs")))), Some(false)),
241
+ ListItem(Chunk(Paragraph(Chunk(Text("Call Alice")))), None) // No checkbox
242
+ ), tight = true)
243
+ ```
140
244
 
141
- Parse a Markdown string into a `Doc`:
245
+ Rendering the task list:
142
246
 
143
247
  ```scala
144
- object Parser {
145
- def parse(input: String): Either[ParseError, Doc]
146
- }
248
+ import zio.blocks.chunk.Chunk
249
+ import zio.blocks.docs._
250
+
251
+ Renderer.render(Doc(Chunk(tasks)))
252
+ // res7: String = """- [x] Buy groceries
253
+ // - [ ] Write docs
254
+ // - Call Alice
255
+ // """
147
256
  ```
148
257
 
149
- **Example:**
258
+ ### Tables with Alignment
259
+
260
+ Tables require a header row, alignment specification, and data rows:
261
+
150
262
  ```scala
263
+ import zio.blocks.chunk.Chunk
151
264
  import zio.blocks.docs._
152
265
 
153
- val result = Parser.parse("# Hello\n\nThis is **bold**.")
154
- // Right(Doc(Chunk(
155
- // Heading(H1, Chunk(Text("Hello"))),
156
- // Paragraph(Chunk(Text("This is "), Strong(Chunk(Text("bold"))), Text(".")))
157
- // )))
266
+ val table = Table(
267
+ header = TableRow(Chunk(
268
+ Chunk(Text("Name")),
269
+ Chunk(Text("Age")),
270
+ Chunk(Text("City"))
271
+ )),
272
+ alignments = Chunk(Alignment.Left, Alignment.Right, Alignment.Center),
273
+ rows = Chunk(
274
+ TableRow(Chunk(
275
+ Chunk(Text("Alice")),
276
+ Chunk(Text("30")),
277
+ Chunk(Text("NYC"))
278
+ )),
279
+ TableRow(Chunk(
280
+ Chunk(Text("Bob")),
281
+ Chunk(Text("25")),
282
+ Chunk(Text("LA"))
283
+ ))
284
+ )
285
+ )
158
286
  ```
159
287
 
160
- ### Supported Features
288
+ Rendering the table to markdown:
289
+
290
+ ```scala
291
+ import zio.blocks.chunk.Chunk
292
+ import zio.blocks.docs._
161
293
 
162
- The parser supports all GitHub Flavored Markdown features:
294
+ Renderer.render(Doc(Chunk(table)))
295
+ // res8: String = """| Name | Age | City |
296
+ // |:---|---:|:--:|
297
+ // | Alice | 30 | NYC |
298
+ // | Bob | 25 | LA |
299
+ // """
300
+ ```
301
+
302
+ ### Parsing with Error Handling
163
303
 
164
- - **ATX headings** (# to ######)
165
- - **Fenced code blocks** (``` or ~~~)
166
- - **Thematic breaks** (---, ***, ___)
167
- - **Block quotes** (> prefix)
168
- - **Bullet and ordered lists**
169
- - **Task lists** (- [ ] and - [x])
170
- - **Tables with alignment**
171
- - **Inline formatting** (emphasis, strong, strikethrough, code)
172
- - **Links and images**
173
- - **Autolinks** (`<url>` or plain URLs)
174
- - **HTML blocks and inline HTML**
304
+ Parser returns `Either[ParseError, Doc]` with precise location information:
175
305
 
176
- ### Not Supported
306
+ ```scala
307
+ import zio.blocks.chunk.Chunk
308
+ import zio.blocks.docs._
177
309
 
178
- - **YAML frontmatter** (causes parse error)
179
- - **Setext headings** (use ATX style with #)
180
- - **Indented code blocks** (use fenced code blocks)
181
- - **Link reference definitions**
310
+ Parser.parse("# Hello\n[invalid link](") match {
311
+ case Right(doc) => println("Parsed successfully")
312
+ case Left(err) => println(s"Error at line ${err.line}, column ${err.column}: ${err.message}")
313
+ }
314
+ ```
182
315
 
183
- ### ParseError
316
+ ### Round-Trip Semantics
184
317
 
185
- Parsing error with location information:
318
+ Parse-render-parse cycles preserve document meaning (normalized forms are equal):
186
319
 
187
320
  ```scala
188
- final case class ParseError(
189
- message: String,
190
- line: Int, // 1-based line number
191
- column: Int, // 1-based column number
192
- input: String // The line that caused the error
193
- )
321
+ import zio.blocks.chunk.Chunk
322
+ import zio.blocks.docs._
323
+
324
+ val input = "# Hello\n\nWorld"
325
+ val parsed1 = Parser.parse(input).toOption.get
326
+ val rendered = Renderer.render(parsed1)
327
+ val parsed2 = Parser.parse(rendered).toOption.get
328
+ assert(parsed1 == parsed2) // Equal after normalization
194
329
  ```
195
330
 
196
- **Example:**
331
+ ### Custom Type Interpolation
332
+
333
+ Implement `ToMarkdown` for your types to enable interpolation:
334
+
197
335
  ```scala
198
- Parser.parse("---\ntitle: Test\n---") match {
199
- case Left(err) =>
200
- println(s"Error at line ${err.line}: ${err.message}")
201
- // "Error at line 1: Frontmatter is not supported"
202
- case Right(doc) => // Process doc
336
+ import zio.blocks.chunk.Chunk
337
+ import zio.blocks.docs._
338
+
339
+ case class User(name: String, role: String)
340
+
341
+ implicit val userToMarkdown: ToMarkdown[User] = { user =>
342
+ Strong(Chunk(Text(user.name), Text(s" – ${user.role}")))
203
343
  }
344
+
345
+ val user = User("Alice", "Engineer")
346
+ val doc = md"# Team\n$user"
204
347
  ```
205
348
 
206
- ## Rendering
349
+ Additional use cases for custom type interpolation include building documentation programmatically with rich data types, generating reports with structured business objects, and creating templates that mix markdown formatting with domain-specific data. For example, you could create instances for your API models to automatically format them as markdown tables or code examples, making it easy to generate consistent documentation from live data structures.
350
+
351
+ ## Integration Points
352
+
353
+ The markdown module integrates seamlessly with other ZIO Blocks components to provide a cohesive ecosystem for working with structured data and documentation.
354
+
355
+ **Schema Integration**: The `schema` module can derive codecs for markdown documents, enabling serialization and deserialization of `Doc` and related types. This allows you to persist markdown structures to various formats (JSON, MessagePack, BSON, etc.) while maintaining type safety and schema validation.
356
+
357
+ **Chunk Integration**: Core data structures like `Doc`, `Block`, and `Inline` use `Chunk[T]` throughout for efficient immutable sequences. This provides O(1) concatenation, memory efficiency, and seamless interoperability with other ZIO libraries that also rely on chunks for data streaming and collection manipulation.
358
+
359
+ **HTTP Integration**: Markdown documents can be served directly as documentation endpoints in HTTP servers. Combined with the multiple renderer options (GFM, HTML, terminal), you can build documentation APIs that serve content in multiple formats based on client preferences, making it trivial to expose living documentation alongside your application.
207
360
 
208
- ### Markdown Rendering
361
+ ---
362
+
363
+ ## Doc
364
+
365
+ A complete GitHub Flavored Markdown document.
366
+
367
+ ### Definition
209
368
 
210
- Render a `Doc` back to GFM Markdown:
369
+ Here is the `Doc` type definition:
211
370
 
212
371
  ```scala
213
- object Renderer {
214
- def render(doc: Doc): String
215
- def renderBlock(block: Block): String
216
- def renderInlines(inlines: Chunk[Inline]): String
217
- def renderInline(inline: Inline): String
218
- }
372
+ import zio.blocks.chunk.Chunk
373
+
374
+ final case class Doc(blocks: Chunk[Block], metadata: Map[String, String] = Map.empty)
219
375
  ```
220
376
 
221
- **Example:**
377
+ A parsed or constructed markdown document. The `metadata` field is reserved for future use and defaults to an empty map.
378
+
379
+ ### Construction
380
+
381
+ Create an empty document:
382
+
222
383
  ```scala
223
- val doc = Parser.parse("# Title\n\nParagraph.").toOption.get
224
- val markdown = Renderer.render(doc)
225
- // "# Title\n\nParagraph.\n\n"
384
+ import zio.blocks.chunk.Chunk
385
+ import zio.blocks.docs._
386
+
387
+ val empty = Doc.empty
226
388
  ```
227
389
 
228
- The rendered output is GFM-compliant and can be re-parsed to produce an equivalent AST.
390
+ Construct a document from blocks:
391
+
392
+ ```scala
393
+ import zio.blocks.chunk.Chunk
394
+ import zio.blocks.docs._
229
395
 
230
- ### HTML Rendering
396
+ val doc = Doc(Chunk(
397
+ Heading(HeadingLevel.H1, Chunk(Text("Title"))),
398
+ Paragraph(Chunk(Text("Content")))
399
+ ))
400
+ ```
231
401
 
232
- Render to HTML5-compliant HTML:
402
+ Parse a markdown string to create a document:
233
403
 
234
404
  ```scala
235
- object HtmlRenderer {
236
- def render(doc: Doc): String // Full HTML5 document
237
- def renderFragment(doc: Doc): String // Content only, no wrapper
238
- def renderBlock(block: Block): String
239
- def renderInlines(inlines: Chunk[Inline]): String
240
- def renderInline(inline: Inline): String
241
- def escape(s: String): String // HTML entity escaping
405
+ import zio.blocks.chunk.Chunk
406
+ import zio.blocks.docs._
407
+
408
+ Parser.parse("# Title\n\nContent") match {
409
+ case Right(doc) => // use doc
410
+ case Left(err) => // handle error
242
411
  }
243
412
  ```
244
413
 
245
- **Example:**
246
- ```scala
247
- val doc = Parser.parse("# Hello\n\n**Bold**").toOption.get
414
+ ### Core Operations
248
415
 
249
- // Full document with <!DOCTYPE html>, <html>, <head>, <body>
250
- val fullHtml = HtmlRenderer.render(doc)
416
+ Merge documents using concatenation with `Doc#++`:
251
417
 
252
- // Just the content: <h1>Hello</h1><p><strong>Bold</strong></p>
253
- val fragment = HtmlRenderer.renderFragment(doc)
418
+ ```scala
419
+ import zio.blocks.chunk.Chunk
420
+ import zio.blocks.docs._
421
+
422
+ val doc1 = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Part 1")))))
423
+ val doc2 = Doc(Chunk(Paragraph(Chunk(Text("Part 2")))))
424
+ val combined = doc1 ++ doc2
254
425
  ```
255
426
 
256
- **HTML Features:**
257
- - Code blocks with language classes (`language-scala`, etc.)
258
- - Tables with proper alignment styles
259
- - Task list items with disabled checkboxes
260
- - Proper HTML entity escaping for safety
427
+ Render a document to GFM markdown:
261
428
 
262
- ### Terminal Rendering
429
+ ```scala
430
+ import zio.blocks.chunk.Chunk
431
+ import zio.blocks.docs._
432
+
433
+ val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
434
+ // doc: Doc = Doc(
435
+ // blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
436
+ // metadata = Map()
437
+ // )
438
+ val markdown: String = doc.toString // Calls Renderer.render internally
439
+ // markdown: String = """# Hello
440
+ // """
441
+ ```
263
442
 
264
- Render with ANSI escape codes for colorful terminal display:
443
+ Render to HTML with full document structure including DOCTYPE (returns complete HTML5 document with `<!DOCTYPE html>` wrapper):
265
444
 
266
445
  ```scala
267
- object TerminalRenderer {
268
- def render(doc: Doc): String
269
- def renderBlock(block: Block): String
270
- def renderInlines(inlines: Chunk[Inline]): String
271
- def renderInline(inline: Inline): String
272
- }
446
+ import zio.blocks.chunk.Chunk
447
+ import zio.blocks.docs._
448
+
449
+ val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
450
+ // doc: Doc = Doc(
451
+ // blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
452
+ // metadata = Map()
453
+ // )
454
+ val html = doc.toHtml
455
+ // html: String = "<!DOCTYPE html><html><head></head><body><h1>Hello</h1></body></html>"
273
456
  ```
274
457
 
275
- **Example:**
458
+ Render to HTML fragment containing only the content (returns just the rendered HTML blocks without wrapper tags):
459
+
276
460
  ```scala
277
- val doc = Parser.parse("# Hello\n\nThis is **bold** and *italic*.").toOption.get
278
- val terminal = TerminalRenderer.render(doc)
279
- println(terminal) // Displays with colors and formatting
461
+ import zio.blocks.chunk.Chunk
462
+ import zio.blocks.docs._
463
+
464
+ val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
465
+ // doc: Doc = Doc(
466
+ // blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
467
+ // metadata = Map()
468
+ // )
469
+ val fragment = doc.toHtmlFragment
470
+ // fragment: String = "<h1>Hello</h1>"
280
471
  ```
281
472
 
282
- **ANSI Styling:**
283
- - **Headings:** Bold + colored (H1=red, H2=yellow, H3=green, H4=cyan, H5=blue, H6=magenta)
284
- - **Code blocks:** Gray background
285
- - **Inline code:** Gray background
286
- - **Emphasis:** Italic
287
- - **Strong:** Bold
288
- - **Strikethrough:** Strike-through style
289
- - **Links:** Blue + underlined
290
- - **Block quotes:** Prefixed with │
473
+ Render to colorized terminal output (returns ANSI-colored string suitable for terminal display):
291
474
 
292
- ## String Interpolator
475
+ ```scala
476
+ import zio.blocks.chunk.Chunk
477
+ import zio.blocks.docs._
293
478
 
294
- ### The md"..." Interpolator
479
+ val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
480
+ // doc: Doc = Doc(
481
+ // blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
482
+ // metadata = Map()
483
+ // )
484
+ val terminal = doc.toTerminal
485
+ // terminal: String = """Hello
486
+ //
487
+ // """
488
+ ```
295
489
 
296
- Build documents with compile-time validated Markdown syntax:
490
+ Canonicalize document structure with `Doc#normalize` to merge adjacent text nodes and remove empty blocks:
297
491
 
298
492
  ```scala
493
+ import zio.blocks.chunk.Chunk
299
494
  import zio.blocks.docs._
300
495
 
301
- val name = "World"
302
- val greeting = md"# Hello $name"
303
- // Doc(Chunk(Heading(H1, Chunk(Text("Hello World")))))
496
+ val doc = Doc(Chunk(
497
+ Paragraph(Chunk(Text("A"), Text("B"))), // Adjacent text nodes
498
+ Paragraph(Chunk()), // Empty paragraph
499
+ Paragraph(Chunk(Text("C")))
500
+ ))
501
+ ```
304
502
 
305
- val items = List("one", "two", "three")
306
- val list = md"""
307
- # My List
503
+ Call `Doc#normalize` to see the result:
308
504
 
309
- ${items.map(i => s"- $i").mkString("\n")}
310
- """
505
+ ```scala
506
+ import zio.blocks.chunk.Chunk
507
+ import zio.blocks.docs._
508
+
509
+ val normalized = doc.normalize
510
+ // normalized: Doc = Doc(
511
+ // blocks = IndexedSeq(
512
+ // Paragraph(IndexedSeq(Text("AB"))),
513
+ // Paragraph(IndexedSeq(Text("C")))
514
+ // ),
515
+ // metadata = Map()
516
+ // )
311
517
  ```
312
518
 
313
- The interpolator:
314
- - **Validates syntax at compile time** - invalid markdown causes compilation error
315
- - **Requires ToMarkdown instances** for interpolated values
316
- - **Supports multi-line markdown** with triple quotes
519
+ ### Equality and Hashing
520
+
521
+ Two documents are equal if their **normalized forms** are equal. This means:
317
522
 
318
- **Example with validation:**
319
523
  ```scala
320
- // This won't compile - invalid heading level
321
- val bad = md"####### Too many hashes"
322
- // Error: Invalid markdown: Invalid heading level: 7 (max is 6)
524
+ import zio.blocks.chunk.Chunk
525
+ import zio.blocks.docs._
526
+
527
+ val doc1 = Doc(Chunk(Paragraph(Chunk(Text("Hello"), Text(" "), Text("World")))))
528
+ val doc2 = Doc(Chunk(Paragraph(Chunk(Text("Hello World")))))
529
+ assert(doc1 == doc2) // Equal after normalization
323
530
  ```
324
531
 
325
- ### ToMarkdown Typeclass
532
+ Hash code computes from the normalized form for consistency with `equals`.
326
533
 
327
- Make custom types interpolatable:
534
+ ---
535
+
536
+ ## Block (Sealed Trait)
537
+
538
+ A block-level markdown element:
328
539
 
329
540
  ```scala
330
- trait ToMarkdown[-A] {
331
- def toMarkdown(a: A): Inline
332
- }
541
+ import zio.blocks.chunk.Chunk
542
+ import zio.blocks.docs._
543
+
544
+ sealed trait Block extends Product with Serializable
333
545
  ```
334
546
 
335
- **Built-in instances:**
336
- - `String`, `Int`, `Long`, `Double`, `Boolean` → `Text`
337
- - `Inline` → identity
338
- - `Block` → rendered to markdown then wrapped as `Text`
339
- - `List[A]`, `Vector[A]`, `Seq[A]`, `Chunk[A]` → comma-separated (where `A: ToMarkdown`)
547
+ Block is a sealed trait with the following concrete subtypes.
548
+
549
+ ### Paragraph
550
+
551
+ A paragraph containing inline content:
340
552
 
341
- **Custom instance example:**
342
553
  ```scala
343
- case class User(name: String, email: String)
554
+ import zio.blocks.chunk.Chunk
555
+ import zio.blocks.docs._
556
+
557
+ final case class Paragraph(content: Chunk[Inline]) extends Block
558
+ ```
559
+
560
+ Here's an example of creating a paragraph with mixed content:
344
561
 
345
- implicit val userToMarkdown: ToMarkdown[User] = user =>
346
- Text(s"${user.name} <${user.email}>")
562
+ ```scala
563
+ import zio.blocks.chunk.Chunk
564
+ import zio.blocks.docs._
347
565
 
348
- val user = User("Alice", "alice@example.com")
349
- val doc = md"Contact: $user"
350
- // Doc(Chunk(Paragraph(Chunk(Text("Contact: Alice <alice@example.com>")))))
566
+ val para = Paragraph(Chunk(
567
+ Text("Hello "),
568
+ Strong(Chunk(Text("world")))
569
+ ))
351
570
  ```
352
571
 
353
- **Advanced example - custom formatting:**
572
+ ### Heading
573
+
574
+ An ATX-style heading (# to ######) with a level and inline content:
575
+
354
576
  ```scala
355
- case class CodeSnippet(lang: String, code: String)
577
+ import zio.blocks.chunk.Chunk
578
+ import zio.blocks.docs._
356
579
 
357
- implicit val codeSnippetToMarkdown: ToMarkdown[CodeSnippet] = snippet =>
358
- Text(s"```${snippet.lang}\n${snippet.code}\n```")
580
+ final case class Heading(level: HeadingLevel, content: Chunk[Inline]) extends Block
581
+ ```
582
+
583
+ Here's an example of creating headings with different levels:
584
+
585
+ ```scala
586
+ import zio.blocks.chunk.Chunk
587
+ import zio.blocks.docs._
359
588
 
360
- val snippet = CodeSnippet("scala", "val x = 42")
361
- val doc = md"Here's an example:\n\n$snippet"
589
+ val h1 = Heading(HeadingLevel.H1, Chunk(Text("Title")))
590
+ val h3 = Heading(HeadingLevel.H3, Chunk(Text("Subsection")))
362
591
  ```
363
592
 
364
- ## Working with the AST
593
+ ### CodeBlock
365
594
 
366
- ### Building Documents Programmatically
595
+ A fenced code block with optional language/info string:
367
596
 
368
597
  ```scala
598
+ import zio.blocks.chunk.Chunk
369
599
  import zio.blocks.docs._
600
+
601
+ final case class CodeBlock(info: Option[String], code: String) extends Block
602
+ ```
603
+
604
+ Here are examples of creating code blocks with and without language specification:
605
+
606
+ ```scala
370
607
  import zio.blocks.chunk.Chunk
608
+ import zio.blocks.docs._
371
609
 
372
- val doc = Doc(Chunk(
373
- Heading(HeadingLevel.H1, Chunk(Text("Title"))),
374
- Paragraph(Chunk(
375
- Text("This is "),
376
- Strong(Chunk(Text("important"))),
377
- Text(".")
378
- )),
379
- CodeBlock(Some("scala"), "val x = 42"),
380
- BulletList(Chunk(
381
- ListItem(Chunk(Paragraph(Chunk(Text("Item 1")))), None),
382
- ListItem(Chunk(Paragraph(Chunk(Text("Done")))), Some(true)),
383
- ListItem(Chunk(Paragraph(Chunk(Text("Todo")))), Some(false))
384
- ), tight = true)
385
- ))
610
+ // Scala code block
611
+ val scalaBlock = CodeBlock(Some("scala"), "val x = 42\nprintln(x)")
612
+
613
+ // No language specified
614
+ val plainBlock = CodeBlock(None, "some code")
386
615
  ```
387
616
 
388
- ### Concatenation
617
+ ### ThematicBreak
389
618
 
390
- Combine documents with `++`:
619
+ A thematic break (horizontal rule):
391
620
 
392
621
  ```scala
393
- val header = md"# Document Title"
394
- val body = md"Some content here."
395
- val footer = md"---\n*Footer*"
622
+ import zio.blocks.chunk.Chunk
623
+ import zio.blocks.docs._
396
624
 
397
- val full = header ++ body ++ footer
625
+ case object ThematicBreak extends Block
398
626
  ```
399
627
 
400
- **Metadata merging:**
628
+ Create a thematic break:
629
+
401
630
  ```scala
402
- val doc1 = Doc(Chunk(Paragraph(Chunk(Text("A")))), Map("author" -> "Alice"))
403
- val doc2 = Doc(Chunk(Paragraph(Chunk(Text("B")))), Map("version" -> "1.0"))
404
- val combined = doc1 ++ doc2
405
- // combined.metadata == Map("author" -> "Alice", "version" -> "1.0")
631
+ import zio.blocks.chunk.Chunk
632
+ import zio.blocks.docs._
633
+
634
+ val break = ThematicBreak
406
635
  ```
407
636
 
408
- ### Normalization
637
+ **Renders as:** `---\n` (or `***` or `___`)
638
+
639
+ ### BlockQuote
409
640
 
410
- `normalize` cleans up the AST:
411
- - Merges adjacent `Text` nodes
412
- - Removes empty paragraphs and other empty blocks
413
- - Recursively normalizes nested structures (lists, block quotes, tables)
641
+ A block quote containing nested blocks:
414
642
 
415
643
  ```scala
416
- val messy = Doc(Chunk(
417
- Paragraph(Chunk(
418
- Text("Hello "),
419
- Text("World") // Adjacent Text nodes
420
- )),
421
- Paragraph(Chunk.empty) // Empty paragraph
422
- ))
644
+ import zio.blocks.chunk.Chunk
645
+ import zio.blocks.docs._
423
646
 
424
- val clean = messy.normalize
425
- // Doc(Chunk(Paragraph(Chunk(Text("Hello World")))))
647
+ final case class BlockQuote(content: Chunk[Block]) extends Block
426
648
  ```
427
649
 
428
- **When to normalize:**
429
- - Before comparing documents for equality (equality uses normalized form)
430
- - After programmatic AST construction with potential duplicates
431
- - When cleaning up parsed or generated content
650
+ Here's an example of creating a block quote:
432
651
 
433
- **Note:** `Doc.equals` automatically normalizes both sides, so explicit normalization isn't needed for equality checks.
652
+ ```scala
653
+ import zio.blocks.chunk.Chunk
654
+ import zio.blocks.docs._
434
655
 
435
- ## Advanced Usage
656
+ val quote = BlockQuote(Chunk(
657
+ Paragraph(Chunk(Text("This is a famous quote.")))
658
+ ))
659
+ ```
436
660
 
437
- ### Custom Renderers
661
+ ### BulletList
438
662
 
439
- You can traverse the AST to create custom renderers:
663
+ An unordered list with bullet markers (-, *, +):
440
664
 
441
665
  ```scala
442
- def customRender(doc: Doc): String = {
443
- doc.blocks.map {
444
- case Heading(level, content) =>
445
- s"${"=" * level.value} ${renderInlines(content)}\n"
446
- case Paragraph(content) =>
447
- renderInlines(content) + "\n\n"
448
- case _ =>
449
- Renderer.renderBlock(_)
450
- }.mkString
451
- }
452
- ```
666
+ import zio.blocks.chunk.Chunk
667
+ import zio.blocks.docs._
453
668
 
454
- ### Extracting Information
669
+ final case class BulletList(items: Chunk[ListItem], tight: Boolean) extends Block
670
+ ```
455
671
 
456
- Pattern match on the AST to extract structured data:
672
+ The `tight` parameter controls spacing: `true` removes blank lines between items for compact rendering. Here's an example:
457
673
 
458
674
  ```scala
459
- def extractHeadings(doc: Doc): List[(Int, String)] = {
460
- doc.blocks.collect {
461
- case Heading(level, content) =>
462
- (level.value, Renderer.renderInlines(content))
463
- }.toList
464
- }
675
+ import zio.blocks.chunk.Chunk
676
+ import zio.blocks.docs._
465
677
 
466
- def extractLinks(doc: Doc): List[String] = {
467
- def findLinksInInlines(inlines: Chunk[Inline]): List[String] = {
468
- inlines.toList.flatMap {
469
- case Link(_, url, _) => List(url)
470
- case Strong(content) => findLinksInInlines(content)
471
- case Emphasis(content) => findLinksInInlines(content)
472
- case _ => Nil
473
- }
474
- }
475
-
476
- doc.blocks.flatMap {
477
- case Paragraph(content) => findLinksInInlines(content)
478
- case Heading(_, content) => findLinksInInlines(content)
479
- case _ => Nil
480
- }.toList
481
- }
678
+ val list = BulletList(Chunk(
679
+ ListItem(Chunk(Paragraph(Chunk(Text("Item 1")))), None),
680
+ ListItem(Chunk(Paragraph(Chunk(Text("Item 2")))), None)
681
+ ), tight = true)
482
682
  ```
483
683
 
484
- ### Transforming Documents
684
+ ### OrderedList
485
685
 
486
- Apply transformations to the AST:
686
+ An ordered list with numeric markers (1., 2., etc.):
487
687
 
488
688
  ```scala
489
- def uppercaseHeadings(doc: Doc): Doc = {
490
- val transformedBlocks = doc.blocks.map {
491
- case Heading(level, content) =>
492
- val upperContent = content.map {
493
- case Text(value) => Text(value.toUpperCase)
494
- case other => other
495
- }
496
- Heading(level, upperContent)
497
- case other => other
498
- }
499
- Doc(transformedBlocks, doc.metadata)
500
- }
689
+ import zio.blocks.chunk.Chunk
690
+ import zio.blocks.docs._
691
+
692
+ final case class OrderedList(start: Int, items: Chunk[ListItem], tight: Boolean) extends Block
501
693
  ```
502
694
 
503
- ## Best Practices
695
+ The `start` parameter specifies the starting number (typically 1). Here's an example:
504
696
 
505
- ### Parsing
506
- - Always handle `Either[ParseError, Doc]` - don't assume parsing succeeds
507
- - For user input, display parse errors with line/column information
508
- - Use the interpolator for static markdown (compile-time validation)
697
+ ```scala
698
+ import zio.blocks.chunk.Chunk
699
+ import zio.blocks.docs._
509
700
 
510
- ### Building
511
- - Prefer the `md"..."` interpolator for compile-time safety
512
- - Use programmatic construction for dynamic content
513
- - Call `normalize` after complex programmatic construction
701
+ val list = OrderedList(
702
+ start = 1,
703
+ items = Chunk(
704
+ ListItem(Chunk(Paragraph(Chunk(Text("First")))), None),
705
+ ListItem(Chunk(Paragraph(Chunk(Text("Second")))), None)
706
+ ),
707
+ tight = true
708
+ )
709
+ ```
710
+
711
+ ### ListItem
712
+
713
+ A list item, optionally a task list item:
714
+
715
+ ```scala
716
+ import zio.blocks.chunk.Chunk
717
+ import zio.blocks.docs._
718
+
719
+ final case class ListItem(content: Chunk[Block], checked: Option[Boolean]) extends Block
720
+ ```
721
+
722
+ The `checked` parameter: `Some(true)` renders as `[x]`, `Some(false)` renders as `[ ]`, `None` for regular list items. Here are examples of each type:
723
+
724
+ ```scala
725
+ import zio.blocks.chunk.Chunk
726
+ import zio.blocks.docs._
727
+
728
+ // Regular list item
729
+ val item = ListItem(Chunk(Paragraph(Chunk(Text("Task")))), None)
730
+
731
+ // Completed task
732
+ val completed = ListItem(Chunk(Paragraph(Chunk(Text("Done")))), Some(true))
733
+
734
+ // Incomplete task
735
+ val incomplete = ListItem(Chunk(Paragraph(Chunk(Text("TODO")))), Some(false))
736
+ ```
737
+
738
+ ### HtmlBlock
739
+
740
+ Raw HTML block content:
741
+
742
+ ```scala
743
+ import zio.blocks.chunk.Chunk
744
+ import zio.blocks.docs._
745
+
746
+ final case class HtmlBlock(content: String) extends Block
747
+ ```
748
+
749
+ Here's an example of creating an HTML block:
750
+
751
+ ```scala
752
+ import zio.blocks.chunk.Chunk
753
+ import zio.blocks.docs._
754
+
755
+ val html = HtmlBlock("<div class='alert'>Custom HTML</div>")
756
+ ```
757
+
758
+ ### Table
759
+
760
+ A GitHub Flavored Markdown table with aligned columns:
761
+
762
+ ```scala
763
+ import zio.blocks.chunk.Chunk
764
+ import zio.blocks.docs._
765
+
766
+ final case class Table(header: TableRow, alignments: Chunk[Alignment], rows: Chunk[TableRow]) extends Block
767
+ ```
768
+
769
+ Here's an example of creating a table:
770
+
771
+ ```scala
772
+ import zio.blocks.chunk.Chunk
773
+ import zio.blocks.docs._
774
+
775
+ val table = Table(
776
+ header = TableRow(Chunk(Chunk(Text("Name")), Chunk(Text("Age")))),
777
+ alignments = Chunk(Alignment.Left, Alignment.Right),
778
+ rows = Chunk(
779
+ TableRow(Chunk(Chunk(Text("Alice")), Chunk(Text("30")))),
780
+ TableRow(Chunk(Chunk(Text("Bob")), Chunk(Text("25"))))
781
+ )
782
+ )
783
+ ```
784
+
785
+ ---
786
+
787
+ ## Inline (Sealed Trait)
788
+
789
+ An inline-level markdown element:
790
+
791
+ ```scala
792
+ import zio.blocks.chunk.Chunk
793
+ import zio.blocks.docs._
794
+
795
+ sealed trait Inline extends Product with Serializable
796
+ ```
797
+
798
+ Inline is a sealed trait with concrete subtypes, defining both object-level and top-level forms for API compatibility.
799
+
800
+ ### Text
801
+
802
+ Plain text content:
803
+
804
+ ```scala
805
+ import zio.blocks.chunk.Chunk
806
+ import zio.blocks.docs._
807
+
808
+ final case class Text(value: String) extends Inline
809
+ ```
810
+
811
+ Here's an example of creating text:
812
+
813
+ ```scala
814
+ import zio.blocks.chunk.Chunk
815
+ import zio.blocks.docs._
816
+
817
+ val text = Text("Hello world")
818
+ ```
819
+
820
+ ### Code
821
+
822
+ Inline code span (backtick-delimited):
823
+
824
+ ```scala
825
+ import zio.blocks.chunk.Chunk
826
+ import zio.blocks.docs._
827
+
828
+ final case class Code(value: String) extends Inline
829
+ ```
830
+
831
+ Here's an example of creating inline code:
832
+
833
+ ```scala
834
+ import zio.blocks.chunk.Chunk
835
+ import zio.blocks.docs._
836
+
837
+ val code = Code("val x = 42")
838
+ ```
839
+
840
+ ### Emphasis
841
+
842
+ Emphasized (italic) text:
843
+
844
+ ```scala
845
+ import zio.blocks.chunk.Chunk
846
+ import zio.blocks.docs._
847
+
848
+ final case class Emphasis(content: Chunk[Inline]) extends Inline
849
+ ```
850
+
851
+ Here's an example of creating emphasized text:
852
+
853
+ ```scala
854
+ import zio.blocks.chunk.Chunk
855
+ import zio.blocks.docs._
856
+
857
+ val emphasis = Emphasis(Chunk(Text("italic")))
858
+ // Renders as: *italic*
859
+ ```
860
+
861
+ ### Strong
862
+
863
+ Strong (bold) text:
864
+
865
+ ```scala
866
+ import zio.blocks.chunk.Chunk
867
+ import zio.blocks.docs._
868
+
869
+ final case class Strong(content: Chunk[Inline]) extends Inline
870
+ ```
871
+
872
+ Here's an example of creating strong text:
873
+
874
+ ```scala
875
+ import zio.blocks.chunk.Chunk
876
+ import zio.blocks.docs._
877
+
878
+ val strong = Strong(Chunk(Text("bold")))
879
+ // Renders as: **bold**
880
+ ```
881
+
882
+ ### Strikethrough
883
+
884
+ Strikethrough text (GFM feature):
885
+
886
+ ```scala
887
+ import zio.blocks.chunk.Chunk
888
+ import zio.blocks.docs._
889
+
890
+ final case class Strikethrough(content: Chunk[Inline]) extends Inline
891
+ ```
892
+
893
+ Here's an example of creating strikethrough text:
894
+
895
+ ```scala
896
+ import zio.blocks.chunk.Chunk
897
+ import zio.blocks.docs._
898
+
899
+ val struck = Strikethrough(Chunk(Text("deprecated")))
900
+ // Renders as: ~~deprecated~~
901
+ ```
902
+
903
+ ### Link
904
+
905
+ A hyperlink:
906
+
907
+ ```scala
908
+ import zio.blocks.chunk.Chunk
909
+ import zio.blocks.docs._
910
+
911
+ final case class Link(text: Chunk[Inline], url: String, title: Option[String]) extends Inline
912
+ ```
913
+
914
+ The `title` parameter is optional link title text. Here are examples of creating links:
915
+
916
+ ```scala
917
+ import zio.blocks.chunk.Chunk
918
+ import zio.blocks.docs._
919
+
920
+ // Simple link
921
+ val link = Link(Chunk(Text("Click here")), "https://example.com", None)
922
+
923
+ // Link with title
924
+ val titled = Link(Chunk(Text("Docs")), "/docs", Some("Documentation"))
925
+ ```
926
+
927
+ ### Image
928
+
929
+ An image reference:
930
+
931
+ ```scala
932
+ import zio.blocks.chunk.Chunk
933
+ import zio.blocks.docs._
934
+
935
+ final case class Image(alt: String, url: String, title: Option[String]) extends Inline
936
+ ```
937
+
938
+ Here are examples of creating images:
939
+
940
+ ```scala
941
+ import zio.blocks.chunk.Chunk
942
+ import zio.blocks.docs._
943
+
944
+ val img = Image(alt = "Logo", url = "/logo.png", None)
945
+ val imgWithTitle = Image(alt = "Icon", url = "/icon.svg", Some("App Icon"))
946
+ ```
947
+
948
+ ### HtmlInline
949
+
950
+ Raw HTML inline content:
951
+
952
+ ```scala
953
+ import zio.blocks.chunk.Chunk
954
+ import zio.blocks.docs._
955
+
956
+ final case class HtmlInline(content: String) extends Inline
957
+ ```
958
+
959
+ Here's an example of creating HTML inline content:
960
+
961
+ ```scala
962
+ import zio.blocks.chunk.Chunk
963
+ import zio.blocks.docs._
964
+
965
+ val html = HtmlInline("<span class='highlight'>custom</span>")
966
+ ```
967
+
968
+ ### SoftBreak
969
+
970
+ A soft line break (single newline, rendered as space or newline depending on context):
971
+
972
+ ```scala
973
+ import zio.blocks.chunk.Chunk
974
+ import zio.blocks.docs._
975
+
976
+ case object SoftBreak extends Inline
977
+ ```
978
+
979
+ ### HardBreak
980
+
981
+ A hard line break (two spaces or backslash before newline):
982
+
983
+ ```scala
984
+ import zio.blocks.chunk.Chunk
985
+ import zio.blocks.docs._
986
+
987
+ case object HardBreak extends Inline
988
+ ```
989
+
990
+ ### Autolink
991
+
992
+ An autolink (URL or email in angle brackets):
993
+
994
+ ```scala
995
+ import zio.blocks.chunk.Chunk
996
+ import zio.blocks.docs._
997
+
998
+ final case class Autolink(url: String, isEmail: Boolean) extends Inline
999
+ ```
1000
+
1001
+ Here are examples of creating autolinks:
1002
+
1003
+ ```scala
1004
+ import zio.blocks.chunk.Chunk
1005
+ import zio.blocks.docs._
1006
+
1007
+ val urlLink = Autolink("https://example.com", isEmail = false)
1008
+ val emailLink = Autolink("user@example.com", isEmail = true)
1009
+ // Renders as: <https://example.com> and <user@example.com>
1010
+ ```
1011
+
1012
+ ---
1013
+
1014
+ ## HeadingLevel
1015
+
1016
+ Heading levels from H1 to H6:
1017
+
1018
+ ```scala
1019
+ import zio.blocks.chunk.Chunk
1020
+ import zio.blocks.docs._
1021
+
1022
+ sealed abstract class HeadingLevel(val value: Int) extends Product with Serializable
1023
+
1024
+ object HeadingLevel {
1025
+ case object H1 extends HeadingLevel(1)
1026
+ case object H2 extends HeadingLevel(2)
1027
+ case object H3 extends HeadingLevel(3)
1028
+ case object H4 extends HeadingLevel(4)
1029
+ case object H5 extends HeadingLevel(5)
1030
+ case object H6 extends HeadingLevel(6)
1031
+ }
1032
+ ```
1033
+
1034
+ ### Safe Construction
1035
+
1036
+ Construct a heading level from an integer, returning an `Option`:
1037
+
1038
+ ```scala
1039
+ import zio.blocks.chunk.Chunk
1040
+ import zio.blocks.docs._
1041
+
1042
+ HeadingLevel.fromInt(2) == Some(HeadingLevel.H2)
1043
+ HeadingLevel.fromInt(7) == None // Out of range
1044
+ ```
1045
+
1046
+ Construct a heading level unsafely, throwing if input is invalid:
1047
+
1048
+ ```scala
1049
+ import zio.blocks.chunk.Chunk
1050
+ import zio.blocks.docs._
1051
+
1052
+ HeadingLevel.unsafeFromInt(3) // HeadingLevel.H3
1053
+ HeadingLevel.unsafeFromInt(7) // Throws IllegalArgumentException
1054
+ ```
1055
+
1056
+ ### Accessing Value
1057
+
1058
+ Access the numeric value of a heading level:
1059
+
1060
+ ```scala
1061
+ import zio.blocks.chunk.Chunk
1062
+ import zio.blocks.docs._
1063
+
1064
+ HeadingLevel.H1.value == 1
1065
+ ```
1066
+
1067
+ ---
1068
+
1069
+ ## Alignment
1070
+
1071
+ Table column alignment specification:
1072
+
1073
+ ```scala
1074
+ import zio.blocks.chunk.Chunk
1075
+ import zio.blocks.docs._
1076
+
1077
+ sealed trait Alignment extends Product with Serializable
1078
+
1079
+ object Alignment {
1080
+ case object Left extends Alignment
1081
+ case object Right extends Alignment
1082
+ case object Center extends Alignment
1083
+ case object None extends Alignment
1084
+ }
1085
+ ```
1086
+
1087
+ ### Predefined Alignments
1088
+
1089
+ Use predefined alignment values for table columns:
1090
+
1091
+ ```scala
1092
+ import zio.blocks.chunk.Chunk
1093
+ import zio.blocks.docs._
1094
+
1095
+ Alignment.Left // Renders as :---
1096
+ Alignment.Right // Renders as ---:
1097
+ Alignment.Center // Renders as :---:
1098
+ Alignment.None // Renders as ---
1099
+ ```
1100
+
1101
+ ### Use in Tables
1102
+
1103
+ Create a table with specific column alignments:
1104
+
1105
+ ```scala
1106
+ import zio.blocks.chunk.Chunk
1107
+ import zio.blocks.docs._
1108
+ val header = TableRow(Chunk(
1109
+ Chunk(Text("Name")),
1110
+ Chunk(Text("Age")),
1111
+ Chunk(Text("City"))
1112
+ ))
1113
+ val rows = Chunk(
1114
+ TableRow(Chunk(
1115
+ Chunk(Text("Alice")),
1116
+ Chunk(Text("30")),
1117
+ Chunk(Text("NYC"))
1118
+ )),
1119
+ TableRow(Chunk(
1120
+ Chunk(Text("Bob")),
1121
+ Chunk(Text("25")),
1122
+ Chunk(Text("LA"))
1123
+ ))
1124
+ )
1125
+ val alignments = Chunk(Alignment.Left, Alignment.Center, Alignment.Right)
1126
+ val table = Table(header, alignments, rows)
1127
+ val doc = Doc(Chunk(table))
1128
+ ```
1129
+
1130
+ If we render this document, the table will have left-aligned "Name", center-aligned "Age", and right-aligned "City":
1131
+
1132
+ ```scala
1133
+ import zio.blocks.chunk.Chunk
1134
+ import zio.blocks.docs._
1135
+
1136
+ Renderer.render(doc)
1137
+ // res45: String = """| Name | Age | City |
1138
+ // |:---|:--:|---:|
1139
+ // | Alice | 30 | NYC |
1140
+ // | Bob | 25 | LA |
1141
+ // """
1142
+ ```
1143
+
1144
+ ---
1145
+
1146
+ ## TableRow
1147
+
1148
+ A single row in a table, containing cells as inline content:
1149
+
1150
+ ```scala
1151
+ import zio.blocks.chunk.Chunk
1152
+ import zio.blocks.docs._
1153
+
1154
+ final case class TableRow(cells: Chunk[Chunk[Inline]])
1155
+ ```
1156
+
1157
+ Each cell is a `Chunk[Inline]`, allowing formatted content (text, links, emphasis, etc.). Here's an example:
1158
+
1159
+ ```scala
1160
+ import zio.blocks.chunk.Chunk
1161
+ import zio.blocks.docs._
1162
+
1163
+ val row = TableRow(Chunk(
1164
+ Chunk(Text("Alice")),
1165
+ Chunk(Strong(Chunk(Text("30")))),
1166
+ Chunk(Link(Chunk(Text("NYC")), "/cities/nyc", None))
1167
+ ))
1168
+ ```
1169
+
1170
+ ---
1171
+
1172
+ ## Parser
1173
+
1174
+ A singleton object providing strict GitHub Flavored Markdown parsing with position-aware error reporting.
1175
+
1176
+ ### Parsing
1177
+
1178
+ Use the main entry point to parse markdown:
1179
+
1180
+ ```scala
1181
+ import zio.blocks.chunk.Chunk
1182
+ import zio.blocks.docs._
1183
+
1184
+ Parser.parse(input: String): Either[ParseError, Doc]
1185
+ ```
1186
+
1187
+ Returns `Right(doc)` on success or `Left(error)` on parse failure. Here's an example:
1188
+
1189
+ ```scala
1190
+ import zio.blocks.chunk.Chunk
1191
+ import zio.blocks.docs._
1192
+
1193
+ val result = Parser.parse("# Hello\n\nWorld")
1194
+ result match {
1195
+ case Right(doc) => println(s"Parsed ${doc.blocks.size} blocks")
1196
+ case Left(err) => println(s"Parse error at line ${err.line}: ${err.message}")
1197
+ }
1198
+ ```
1199
+
1200
+ ### Supported Features
1201
+
1202
+ - ATX headings (# to ######)
1203
+ - Fenced code blocks (``` or ~~~)
1204
+ - Thematic breaks (---, ***, ___)
1205
+ - Block quotes (> prefix)
1206
+ - Bullet lists (-, *, +)
1207
+ - Ordered lists (1., 2., etc.)
1208
+ - Task lists (- [ ] and - [x])
1209
+ - Tables with alignment (GFM)
1210
+ - Inline formatting (emphasis, strong, strikethrough)
1211
+ - Links and images with optional titles
1212
+ - Autolinks (`<url>` or `<email>`)
1213
+ - HTML blocks and inline HTML
1214
+ - Soft and hard line breaks
1215
+
1216
+ ### Unsupported Features
1217
+
1218
+ - YAML frontmatter (causes parse error)
1219
+ - Setext headings (use ATX style with # instead)
1220
+ - Indented code blocks (use fenced with ``` instead)
1221
+ - Link reference definitions
1222
+
1223
+ ---
1224
+
1225
+ ## ParseError
1226
+
1227
+ Error information from parsing, with precise location data:
1228
+
1229
+ ```scala
1230
+ import zio.blocks.chunk.Chunk
1231
+ import zio.blocks.docs._
1232
+
1233
+ final case class ParseError(
1234
+ message: String, // Human-readable error description
1235
+ line: Int, // 1-based line number
1236
+ column: Int, // 1-based column number
1237
+ input: String // The line that caused the error
1238
+ )
1239
+ ```
1240
+
1241
+ Here's an example of creating a `ParseError`:
1242
+
1243
+ ```scala
1244
+ import zio.blocks.chunk.Chunk
1245
+ import zio.blocks.docs._
1246
+
1247
+ val err = ParseError("Unexpected token", line = 5, column = 12, input = "[invalid markdown]")
1248
+ ```
1249
+
1250
+ Get the string representation of the error:
1251
+
1252
+ ```scala
1253
+ import zio.blocks.chunk.Chunk
1254
+ import zio.blocks.docs._
1255
+
1256
+ err.toString
1257
+ // res48: String = """ParseError at line 5, column 12: Unexpected token
1258
+ // [invalid markdown]"""
1259
+ ```
1260
+
1261
+ ---
1262
+
1263
+ ## Renderer
1264
+
1265
+ A singleton object that renders markdown documents back to GitHub Flavored Markdown string format:
1266
+
1267
+ ```scala
1268
+ import zio.blocks.chunk.Chunk
1269
+ import zio.blocks.docs._
1270
+
1271
+ object Renderer
1272
+ ```
1273
+
1274
+ ### Rendering
1275
+
1276
+ Render an entire document to GFM markdown:
1277
+
1278
+ ```scala
1279
+ import zio.blocks.chunk.Chunk
1280
+ import zio.blocks.docs._
1281
+
1282
+ Renderer.render(doc: Doc): String
1283
+ ```
1284
+
1285
+ Here's an example of rendering a document:
1286
+
1287
+ ```scala
1288
+ import zio.blocks.chunk.Chunk
1289
+ import zio.blocks.docs._
1290
+
1291
+ val doc = Doc(Chunk(
1292
+ Heading(HeadingLevel.H1, Chunk(Text("Title"))),
1293
+ Paragraph(Chunk(Text("Content")))
1294
+ ))
1295
+ val rendered = Renderer.render(doc)
1296
+ ```
1297
+
1298
+ The result:
1299
+
1300
+ ```scala
1301
+ import zio.blocks.chunk.Chunk
1302
+ import zio.blocks.docs._
1303
+
1304
+ rendered
1305
+ // res50: String = """# Title
1306
+ // Content
1307
+ //
1308
+ // """
1309
+ ```
1310
+
1311
+ Render individual blocks:
1312
+
1313
+ ```scala
1314
+ import zio.blocks.chunk.Chunk
1315
+ import zio.blocks.docs._
1316
+
1317
+ Renderer.renderBlock(block: Block): String
1318
+ ```
1319
+
1320
+ Render inline content:
1321
+
1322
+ ```scala
1323
+ import zio.blocks.chunk.Chunk
1324
+ import zio.blocks.docs._
1325
+
1326
+ Renderer.renderInlines(inlines: Chunk[Inline]): String
1327
+ Renderer.renderInline(inline: Inline): String
1328
+ ```
1329
+
1330
+ ### Normalization During Rendering
1331
+
1332
+ The renderer does not normalize; use `doc.normalize` before rendering if normalization is needed.
1333
+
1334
+ ### Round-Trip Semantics
1335
+
1336
+ Re-parse rendered output to verify round-trip semantics:
1337
+
1338
+ ```scala
1339
+ import zio.blocks.chunk.Chunk
1340
+ import zio.blocks.docs._
1341
+
1342
+ val original = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Title")))))
1343
+ val markdown = Renderer.render(original)
1344
+ val reparsed = Parser.parse(markdown).toOption.get
1345
+ assert(original == reparsed) // Equal after normalization
1346
+ ```
1347
+
1348
+ ---
1349
+
1350
+ ## HtmlRenderer
1351
+
1352
+ A singleton object that renders markdown documents to HTML5:
1353
+
1354
+ ```scala
1355
+ import zio.blocks.chunk.Chunk
1356
+ import zio.blocks.docs._
1357
+
1358
+ object HtmlRenderer
1359
+ ```
1360
+
1361
+ ### Full Document Rendering
1362
+
1363
+ Render a document as complete HTML with DOCTYPE and html wrapper tags:
1364
+
1365
+ ```scala
1366
+ import zio.blocks.chunk.Chunk
1367
+ import zio.blocks.docs._
1368
+
1369
+ HtmlRenderer.render(doc: Doc): String
1370
+ ```
1371
+
1372
+ Here's an example of rendering a complete HTML document:
1373
+
1374
+ ```scala
1375
+ import zio.blocks.chunk.Chunk
1376
+ import zio.blocks.docs._
1377
+
1378
+ val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Title")))))
1379
+ val html = HtmlRenderer.render(doc)
1380
+ ```
1381
+
1382
+ The result:
1383
+
1384
+ ```scala
1385
+ import zio.blocks.chunk.Chunk
1386
+ import zio.blocks.docs._
1387
+
1388
+ html
1389
+ // res54: String = "<!DOCTYPE html><html><head></head><body><h1>Title</h1></body></html>"
1390
+ ```
1391
+
1392
+ ### Fragment Rendering
1393
+
1394
+ Render content-only HTML without wrapper tags:
1395
+
1396
+ ```scala
1397
+ import zio.blocks.chunk.Chunk
1398
+ import zio.blocks.docs._
1399
+
1400
+ HtmlRenderer.renderFragment(doc: Doc): String
1401
+ ```
1402
+
1403
+ Here's an example of rendering an HTML fragment:
1404
+
1405
+ ```scala
1406
+ import zio.blocks.chunk.Chunk
1407
+ import zio.blocks.docs._
1408
+
1409
+ val fragment = HtmlRenderer.renderFragment(doc)
1410
+ ```
1411
+
1412
+ Use fragments to embed markdown content into existing HTML templates.
1413
+
1414
+ ### HTML Feature Mapping
1415
+
1416
+ - Headings → `<h1>` through `<h6>`
1417
+ - Paragraphs → `<p>`
1418
+ - Code blocks → `<pre><code class="language-...">` (language from info string)
1419
+ - Thematic breaks → `<hr>`
1420
+ - Block quotes → `<blockquote>`
1421
+ - Lists → `<ul>` and `<ol>` with `<li>`
1422
+ - Task lists → `<li>` with checkbox-like rendering
1423
+ - Tables → `<table>`, `<tr>`, `<td>` with alignment classes
1424
+ - Inline code → `<code>`
1425
+ - Emphasis → `<em>`
1426
+ - Strong → `<strong>`
1427
+ - Strikethrough → `<del>` or `<s>`
1428
+ - Links → `<a href="...">`
1429
+ - Images → `<img alt="..." src="...">`
1430
+ - HTML blocks and inlines → pass through as-is
1431
+
1432
+ ---
1433
+
1434
+ ## TerminalRenderer
1435
+
1436
+ A singleton object that renders markdown to ANSI-colored terminal output optimized for console display:
1437
+
1438
+ ```scala
1439
+ import zio.blocks.chunk.Chunk
1440
+ import zio.blocks.docs._
1441
+
1442
+ object TerminalRenderer
1443
+ ```
514
1444
 
515
1445
  ### Rendering
516
- - Use `toHtmlFragment` when embedding in existing HTML pages
517
- - Use `render` (full HTML) for standalone documents
518
- - Use `toTerminal` for CLI tools and REPLs
519
- - Use `toString` when you need markdown output
520
-
521
- ### Performance
522
- - Parse once, render multiple times if possible
523
- - Normalization is not free - don't call it unnecessarily
524
- - The AST is immutable - transformations create new instances
1446
+
1447
+ Render a document to ANSI-colored terminal output:
1448
+ ```scala
1449
+ import zio.blocks.chunk.Chunk
1450
+ import zio.blocks.docs._
1451
+
1452
+ TerminalRenderer.render(doc: Doc): String
1453
+ ```
1454
+
1455
+ Here's an example of rendering to terminal:
1456
+ ```scala
1457
+ import zio.blocks.chunk.Chunk
1458
+ import zio.blocks.docs._
1459
+
1460
+ val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Title")))))
1461
+ val terminal = TerminalRenderer.render(doc)
1462
+ // Returns ANSI-colored string: Title\n\n
1463
+ ```
1464
+
1465
+ ### Color Scheme
1466
+
1467
+ Headings use different colors by level:
1468
+ - H1 → Red
1469
+ - H2 → Yellow
1470
+ - H3 → Green
1471
+ - H4 → Cyan
1472
+ - H5 → Blue
1473
+ - H6 → Magenta
1474
+
1475
+ Inline elements use ANSI styles:
1476
+ - Strong/Bold → ANSI bold
1477
+ - Emphasis/Italic → ANSI italic
1478
+ - Code → Gray background
1479
+ - Links → Underlined
1480
+ - Strikethrough → ANSI strikethrough style
1481
+
1482
+ The output is designed to be readable on both light and dark terminal backgrounds.
1483
+
1484
+ ---
1485
+
1486
+ ## ToMarkdown
1487
+
1488
+ Type class for converting Scala values to markdown inline elements, enabling interpolation in the `md"..."` string interpolator.
1489
+
1490
+ ### Definition
1491
+
1492
+ Define the `ToMarkdown` type class:
1493
+
1494
+ ```scala
1495
+ import zio.blocks.chunk.Chunk
1496
+ import zio.blocks.docs._
1497
+
1498
+ trait ToMarkdown[-A] {
1499
+ def toMarkdown(a: A): Inline
1500
+ }
1501
+ ```
1502
+
1503
+ The type parameter is contravariant (`-A`), allowing subtypes to use supertype instances.
1504
+
1505
+ ### Summon an Instance
1506
+
1507
+ Summon a ToMarkdown instance:
1508
+
1509
+ ```scala
1510
+ import zio.blocks.chunk.Chunk
1511
+ import zio.blocks.docs._
1512
+
1513
+ ToMarkdown[String] // Implicitly summons the ToMarkdown[String] instance
1514
+ ```
1515
+
1516
+ ### Built-In Instances
1517
+
1518
+ Convert primitive types to plain text:
1519
+
1520
+ ```scala
1521
+ import zio.blocks.chunk.Chunk
1522
+ import zio.blocks.docs._
1523
+
1524
+ ToMarkdown[String] // a: String => Text(a)
1525
+ ToMarkdown[Int] // a: Int => Text(a.toString)
1526
+ ToMarkdown[Long] // a: Long => Text(a.toString)
1527
+ ToMarkdown[Double] // a: Double => Text(a.toString)
1528
+ ToMarkdown[Boolean] // a: Boolean => Text(a.toString)
1529
+ ```
1530
+
1531
+ Pass inline elements through unchanged:
1532
+
1533
+ ```scala
1534
+ import zio.blocks.chunk.Chunk
1535
+ import zio.blocks.docs._
1536
+
1537
+ ToMarkdown[Inline] // a: Inline => a (identity)
1538
+ ```
1539
+
1540
+ Convert collections to comma-separated text:
1541
+
1542
+ ```scala
1543
+ import zio.blocks.chunk.Chunk
1544
+ import zio.blocks.docs._
1545
+
1546
+ ToMarkdown[List[A]] // as: List[A] => Text(as.map(...).mkString(", "))
1547
+ ToMarkdown[Chunk[A]] // as: Chunk[A] => Text(as.map(...).mkString(", "))
1548
+ ToMarkdown[Vector[A]] // as: Vector[A] => Text(as.map(...).mkString(", "))
1549
+ ToMarkdown[Seq[A]] // as: Seq[A] => Text(as.map(...).mkString(", "))
1550
+ ```
1551
+
1552
+ For collection instances, each element is converted using its `ToMarkdown[A]` instance, then joined with ", ".
1553
+
1554
+ Render blocks to markdown text:
1555
+
1556
+ ```scala
1557
+ import zio.blocks.chunk.Chunk
1558
+ import zio.blocks.docs._
1559
+
1560
+ ToMarkdown[Block] // b: Block => Text(Renderer.render(Doc(Chunk(b))).trim)
1561
+ ```
1562
+
1563
+ ### Custom Implementations
1564
+
1565
+ Define custom `ToMarkdown` instances for your types:
1566
+
1567
+ ```scala
1568
+ import zio.blocks.chunk.Chunk
1569
+ import zio.blocks.docs._
1570
+
1571
+ case class Person(name: String, age: Int)
1572
+
1573
+ implicit val personToMarkdown: ToMarkdown[Person] = { person =>
1574
+ Strong(Chunk(Text(person.name)))
1575
+ }
1576
+
1577
+ val p = Person("Alice", 30)
1578
+ val doc = md"## User: $p" // Interpolates as strong text "Alice"
1579
+ ```
1580
+
1581
+ ---
1582
+
1583
+ ## String Interpolator (`md"..."`)
1584
+
1585
+ The `md` string interpolator validates markdown at compile time and supports runtime interpolation of values.
1586
+
1587
+ ### Compile-Time Validation
1588
+
1589
+ Markdown syntax inside `md"..."` is validated at compile time:
1590
+
1591
+ ```scala
1592
+ import zio.blocks.chunk.Chunk
1593
+ import zio.blocks.docs._
1594
+
1595
+ val doc = md"# Valid heading" // Compiles
1596
+
1597
+ val invalid = md"# [unclosed link](" // Compile error: Invalid markdown
1598
+ ```
1599
+
1600
+ ### Runtime Interpolation
1601
+
1602
+ Values are interpolated and converted to inline markdown using `ToMarkdown`:
1603
+
1604
+ ```scala
1605
+ import zio.blocks.chunk.Chunk
1606
+ import zio.blocks.docs._
1607
+
1608
+ val name = "Alice"
1609
+ val count = 42
1610
+ val doc = md"""
1611
+ # Welcome $name
1612
+
1613
+ You have $count items.
1614
+ """
1615
+ // Equivalent to parsing: "# Welcome Alice\n\nYou have 42 items."
1616
+ ```
1617
+
1618
+ ### Custom Type Interpolation
1619
+
1620
+ Any type with a `ToMarkdown` instance can be interpolated:
1621
+
1622
+ ```scala
1623
+ import zio.blocks.chunk.Chunk
1624
+ import zio.blocks.docs._
1625
+
1626
+ case class Tag(label: String)
1627
+ implicit val tagToMarkdown: ToMarkdown[Tag] = tag =>
1628
+ Code(tag.label)
1629
+
1630
+ val tag = Tag("important")
1631
+ val doc = md"Please review: $tag" // Renders tag as inline code
1632
+ ```
1633
+
1634
+ ### Interpolation Mechanics
1635
+
1636
+ The interpolator:
1637
+ 1. Collects string parts and interpolated values (as `Inline` elements via `ToMarkdown`)
1638
+ 2. Combines parts and rendered inlines into a markdown string
1639
+ 3. Parses the combined string at runtime
1640
+ 4. Returns a `Doc` or throws `IllegalArgumentException` if parsing fails