@zio.dev/zio-blocks 0.0.1
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.
- package/index.md +426 -0
- package/package.json +6 -0
- package/path-interpolator.md +645 -0
- package/reference/binding.md +364 -0
- package/reference/chunk.md +576 -0
- package/reference/context.md +157 -0
- package/reference/docs.md +524 -0
- package/reference/dynamic-value.md +823 -0
- package/reference/formats.md +640 -0
- package/reference/json-schema.md +626 -0
- package/reference/json.md +979 -0
- package/reference/modifier.md +276 -0
- package/reference/optics.md +1613 -0
- package/reference/patch.md +631 -0
- package/reference/reflect-transform.md +387 -0
- package/reference/reflect.md +521 -0
- package/reference/registers.md +282 -0
- package/reference/schema-evolution.md +540 -0
- package/reference/schema.md +619 -0
- package/reference/syntax.md +409 -0
- package/reference/type-class-derivation-internals.md +632 -0
- package/reference/typeid.md +900 -0
- package/reference/validation.md +458 -0
- package/scope.md +627 -0
- package/sidebars.js +30 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: context
|
|
3
|
+
title: "Context"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Context[+R]` is a type-indexed heterogeneous collection. It stores values of different types, indexed by their types, with compile-time type safety for lookups.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
```scala
|
|
11
|
+
import zio.blocks.context._
|
|
12
|
+
|
|
13
|
+
case class Config(debug: Boolean)
|
|
14
|
+
case class Metrics(count: Int)
|
|
15
|
+
|
|
16
|
+
// Create a context with multiple values
|
|
17
|
+
val ctx: Context[Config & Metrics] = Context(
|
|
18
|
+
Config(debug = true),
|
|
19
|
+
Metrics(count = 42)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// Retrieve values by type
|
|
23
|
+
val config: Config = ctx.get[Config]
|
|
24
|
+
val metrics: Metrics = ctx.get[Metrics]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Construction
|
|
28
|
+
|
|
29
|
+
Create contexts using overloaded `Context.apply` (supports up to 10 values):
|
|
30
|
+
|
|
31
|
+
```scala
|
|
32
|
+
val ctx1 = Context(value1) // Context[Type1]
|
|
33
|
+
val ctx2 = Context(value1, value2) // Context[Type1 & Type2]
|
|
34
|
+
val ctx3 = Context(v1, v2, v3, v4, v5) // Context[T1 & T2 & T3 & T4 & T5]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or build incrementally from empty:
|
|
38
|
+
|
|
39
|
+
```scala
|
|
40
|
+
val ctx = Context.empty
|
|
41
|
+
.add(Config(debug = true))
|
|
42
|
+
.add(Metrics(count = 0))
|
|
43
|
+
// Type: Context[Config & Metrics]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Retrieving Values
|
|
47
|
+
|
|
48
|
+
### get
|
|
49
|
+
|
|
50
|
+
Retrieves a value by type. The type must be in `R`:
|
|
51
|
+
|
|
52
|
+
```scala
|
|
53
|
+
val config: Config = ctx.get[Config]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Supertypes work too:
|
|
57
|
+
|
|
58
|
+
```scala
|
|
59
|
+
trait Named { def name: String }
|
|
60
|
+
case class Person(name: String, age: Int) extends Named
|
|
61
|
+
|
|
62
|
+
val ctx = Context(Person("Alice", 30))
|
|
63
|
+
val named: Named = ctx.get[Named] // Returns the Person
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### getOption
|
|
67
|
+
|
|
68
|
+
Retrieves a value if present, without requiring the type to be in `R`:
|
|
69
|
+
|
|
70
|
+
```scala
|
|
71
|
+
val maybeConfig: Option[Config] = ctx.getOption[Config] // Some(...)
|
|
72
|
+
val maybeOther: Option[Other] = ctx.getOption[Other] // None
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Modifying Contexts
|
|
76
|
+
|
|
77
|
+
### add
|
|
78
|
+
|
|
79
|
+
Adds a value, returning a new context with an expanded type:
|
|
80
|
+
|
|
81
|
+
```scala
|
|
82
|
+
val ctx1 = Context(Config(true)) // Context[Config]
|
|
83
|
+
val ctx2 = ctx1.add(Metrics(0)) // Context[Config & Metrics]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Adding a value of an existing type replaces it:
|
|
87
|
+
|
|
88
|
+
```scala
|
|
89
|
+
val ctx1 = Context(Config(debug = false))
|
|
90
|
+
val ctx2 = ctx1.add(Config(debug = true))
|
|
91
|
+
ctx2.get[Config].debug // true
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### update
|
|
95
|
+
|
|
96
|
+
Transforms an existing value:
|
|
97
|
+
|
|
98
|
+
```scala
|
|
99
|
+
val ctx = Context(Metrics(count = 0))
|
|
100
|
+
val updated = ctx.update[Metrics](m => m.copy(count = m.count + 1))
|
|
101
|
+
updated.get[Metrics].count // 1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### ++ (union)
|
|
105
|
+
|
|
106
|
+
Combines two contexts. Right values override left:
|
|
107
|
+
|
|
108
|
+
```scala
|
|
109
|
+
val ctx1 = Context(Config(debug = false))
|
|
110
|
+
val ctx2 = Context(Config(debug = true), Metrics(0))
|
|
111
|
+
|
|
112
|
+
val merged = ctx1 ++ ctx2
|
|
113
|
+
// Config comes from ctx2 (right wins)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### prune
|
|
117
|
+
|
|
118
|
+
Narrows a context to specific types:
|
|
119
|
+
|
|
120
|
+
```scala
|
|
121
|
+
val ctx: Context[Config & Metrics & Other] = ...
|
|
122
|
+
val pruned: Context[Config] = ctx.prune[Config]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Covariance
|
|
126
|
+
|
|
127
|
+
`Context` is covariant, so `Context[Specific]` is a subtype of `Context[General]`:
|
|
128
|
+
|
|
129
|
+
```scala
|
|
130
|
+
def process(ctx: Context[Named]): Unit = {
|
|
131
|
+
val named = ctx.get[Named]
|
|
132
|
+
println(named.name)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
val ctx: Context[Person] = Context(Person("Bob", 25))
|
|
136
|
+
process(ctx) // Works: Context[Person] <: Context[Named]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Type Safety: IsNominalType
|
|
140
|
+
|
|
141
|
+
Only nominal types can be stored. The `IsNominalType[A]` typeclass is derived automatically for:
|
|
142
|
+
|
|
143
|
+
- Classes, case classes, traits, objects
|
|
144
|
+
- Enums (Scala 3)
|
|
145
|
+
- Applied types (`List[Int]`, `Map[K, V]`)
|
|
146
|
+
|
|
147
|
+
Not supported (compile error):
|
|
148
|
+
|
|
149
|
+
- Intersection types: `A & B`
|
|
150
|
+
- Union types: `A | B`
|
|
151
|
+
- Structural types: `{ def foo: Int }`
|
|
152
|
+
|
|
153
|
+
## Performance
|
|
154
|
+
|
|
155
|
+
- **Caching**: Retrieved values are cached for O(1) subsequent lookups
|
|
156
|
+
- **Subtype matching**: Supertype lookups find matching subtypes and cache results
|
|
157
|
+
- **Platform-optimized**: JVM uses `ConcurrentHashMap`; JS uses efficient mutable maps
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: docs
|
|
3
|
+
title: "Docs Reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docs Module Reference
|
|
7
|
+
|
|
8
|
+
Complete API reference for the zio-blocks-docs module - a zero-dependency GitHub Flavored Markdown library.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```scala
|
|
13
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-docs" % "@VERSION@"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Core Types
|
|
17
|
+
|
|
18
|
+
### Doc
|
|
19
|
+
|
|
20
|
+
The top-level document container. A `Doc` wraps a `Chunk[Block]` representing the document's block-level elements, plus optional metadata.
|
|
21
|
+
|
|
22
|
+
```scala
|
|
23
|
+
final case class Doc(blocks: Chunk[Block], metadata: Map[String, String] = Map.empty)
|
|
24
|
+
```
|
|
25
|
+
|
|
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
|
|
33
|
+
|
|
34
|
+
**Equality:** Two documents are equal if their normalized forms are equal.
|
|
35
|
+
|
|
36
|
+
**Example:**
|
|
37
|
+
```scala
|
|
38
|
+
import zio.blocks.docs._
|
|
39
|
+
|
|
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
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Block
|
|
48
|
+
|
|
49
|
+
Block-level elements that make up a document:
|
|
50
|
+
|
|
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 |
|
|
63
|
+
|
|
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).
|
|
65
|
+
|
|
66
|
+
### Inline
|
|
67
|
+
|
|
68
|
+
Inline elements within blocks:
|
|
69
|
+
|
|
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 |
|
|
83
|
+
|
|
84
|
+
**Note:** Both top-level case classes and `Inline.X` nested variants exist for compatibility. They are treated identically.
|
|
85
|
+
|
|
86
|
+
### HeadingLevel
|
|
87
|
+
|
|
88
|
+
Heading levels H1 through H6:
|
|
89
|
+
|
|
90
|
+
```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
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Example:**
|
|
106
|
+
```scala
|
|
107
|
+
HeadingLevel.fromInt(2) // Some(H2)
|
|
108
|
+
HeadingLevel.fromInt(7) // None
|
|
109
|
+
HeadingLevel.unsafeFromInt(3) // H3
|
|
110
|
+
HeadingLevel.H1.value // 1
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Alignment
|
|
114
|
+
|
|
115
|
+
Table column alignment:
|
|
116
|
+
|
|
117
|
+
```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
|
+
```
|
|
126
|
+
|
|
127
|
+
### TableRow
|
|
128
|
+
|
|
129
|
+
A row in a table:
|
|
130
|
+
|
|
131
|
+
```scala
|
|
132
|
+
final case class TableRow(cells: Chunk[Chunk[Inline]])
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each cell contains a chunk of inline elements, allowing rich formatting within table cells.
|
|
136
|
+
|
|
137
|
+
## Parsing
|
|
138
|
+
|
|
139
|
+
### Parser.parse
|
|
140
|
+
|
|
141
|
+
Parse a Markdown string into a `Doc`:
|
|
142
|
+
|
|
143
|
+
```scala
|
|
144
|
+
object Parser {
|
|
145
|
+
def parse(input: String): Either[ParseError, Doc]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Example:**
|
|
150
|
+
```scala
|
|
151
|
+
import zio.blocks.docs._
|
|
152
|
+
|
|
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
|
+
// )))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Supported Features
|
|
161
|
+
|
|
162
|
+
The parser supports all GitHub Flavored Markdown features:
|
|
163
|
+
|
|
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**
|
|
175
|
+
|
|
176
|
+
### Not Supported
|
|
177
|
+
|
|
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**
|
|
182
|
+
|
|
183
|
+
### ParseError
|
|
184
|
+
|
|
185
|
+
Parsing error with location information:
|
|
186
|
+
|
|
187
|
+
```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
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Example:**
|
|
197
|
+
```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
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Rendering
|
|
207
|
+
|
|
208
|
+
### Markdown Rendering
|
|
209
|
+
|
|
210
|
+
Render a `Doc` back to GFM Markdown:
|
|
211
|
+
|
|
212
|
+
```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
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Example:**
|
|
222
|
+
```scala
|
|
223
|
+
val doc = Parser.parse("# Title\n\nParagraph.").toOption.get
|
|
224
|
+
val markdown = Renderer.render(doc)
|
|
225
|
+
// "# Title\n\nParagraph.\n\n"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The rendered output is GFM-compliant and can be re-parsed to produce an equivalent AST.
|
|
229
|
+
|
|
230
|
+
### HTML Rendering
|
|
231
|
+
|
|
232
|
+
Render to HTML5-compliant HTML:
|
|
233
|
+
|
|
234
|
+
```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
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Example:**
|
|
246
|
+
```scala
|
|
247
|
+
val doc = Parser.parse("# Hello\n\n**Bold**").toOption.get
|
|
248
|
+
|
|
249
|
+
// Full document with <!DOCTYPE html>, <html>, <head>, <body>
|
|
250
|
+
val fullHtml = HtmlRenderer.render(doc)
|
|
251
|
+
|
|
252
|
+
// Just the content: <h1>Hello</h1><p><strong>Bold</strong></p>
|
|
253
|
+
val fragment = HtmlRenderer.renderFragment(doc)
|
|
254
|
+
```
|
|
255
|
+
|
|
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
|
|
261
|
+
|
|
262
|
+
### Terminal Rendering
|
|
263
|
+
|
|
264
|
+
Render with ANSI escape codes for colorful terminal display:
|
|
265
|
+
|
|
266
|
+
```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
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Example:**
|
|
276
|
+
```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
|
|
280
|
+
```
|
|
281
|
+
|
|
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 │
|
|
291
|
+
|
|
292
|
+
## String Interpolator
|
|
293
|
+
|
|
294
|
+
### The md"..." Interpolator
|
|
295
|
+
|
|
296
|
+
Build documents with compile-time validated Markdown syntax:
|
|
297
|
+
|
|
298
|
+
```scala
|
|
299
|
+
import zio.blocks.docs._
|
|
300
|
+
|
|
301
|
+
val name = "World"
|
|
302
|
+
val greeting = md"# Hello $name"
|
|
303
|
+
// Doc(Chunk(Heading(H1, Chunk(Text("Hello World")))))
|
|
304
|
+
|
|
305
|
+
val items = List("one", "two", "three")
|
|
306
|
+
val list = md"""
|
|
307
|
+
# My List
|
|
308
|
+
|
|
309
|
+
${items.map(i => s"- $i").mkString("\n")}
|
|
310
|
+
"""
|
|
311
|
+
```
|
|
312
|
+
|
|
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
|
|
317
|
+
|
|
318
|
+
**Example with validation:**
|
|
319
|
+
```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)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### ToMarkdown Typeclass
|
|
326
|
+
|
|
327
|
+
Make custom types interpolatable:
|
|
328
|
+
|
|
329
|
+
```scala
|
|
330
|
+
trait ToMarkdown[-A] {
|
|
331
|
+
def toMarkdown(a: A): Inline
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
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`)
|
|
340
|
+
|
|
341
|
+
**Custom instance example:**
|
|
342
|
+
```scala
|
|
343
|
+
case class User(name: String, email: String)
|
|
344
|
+
|
|
345
|
+
implicit val userToMarkdown: ToMarkdown[User] = user =>
|
|
346
|
+
Text(s"${user.name} <${user.email}>")
|
|
347
|
+
|
|
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>")))))
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Advanced example - custom formatting:**
|
|
354
|
+
```scala
|
|
355
|
+
case class CodeSnippet(lang: String, code: String)
|
|
356
|
+
|
|
357
|
+
implicit val codeSnippetToMarkdown: ToMarkdown[CodeSnippet] = snippet =>
|
|
358
|
+
Text(s"```${snippet.lang}\n${snippet.code}\n```")
|
|
359
|
+
|
|
360
|
+
val snippet = CodeSnippet("scala", "val x = 42")
|
|
361
|
+
val doc = md"Here's an example:\n\n$snippet"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Working with the AST
|
|
365
|
+
|
|
366
|
+
### Building Documents Programmatically
|
|
367
|
+
|
|
368
|
+
```scala
|
|
369
|
+
import zio.blocks.docs._
|
|
370
|
+
import zio.blocks.chunk.Chunk
|
|
371
|
+
|
|
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
|
+
))
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Concatenation
|
|
389
|
+
|
|
390
|
+
Combine documents with `++`:
|
|
391
|
+
|
|
392
|
+
```scala
|
|
393
|
+
val header = md"# Document Title"
|
|
394
|
+
val body = md"Some content here."
|
|
395
|
+
val footer = md"---\n*Footer*"
|
|
396
|
+
|
|
397
|
+
val full = header ++ body ++ footer
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Metadata merging:**
|
|
401
|
+
```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")
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Normalization
|
|
409
|
+
|
|
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)
|
|
414
|
+
|
|
415
|
+
```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
|
+
))
|
|
423
|
+
|
|
424
|
+
val clean = messy.normalize
|
|
425
|
+
// Doc(Chunk(Paragraph(Chunk(Text("Hello World")))))
|
|
426
|
+
```
|
|
427
|
+
|
|
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
|
|
432
|
+
|
|
433
|
+
**Note:** `Doc.equals` automatically normalizes both sides, so explicit normalization isn't needed for equality checks.
|
|
434
|
+
|
|
435
|
+
## Advanced Usage
|
|
436
|
+
|
|
437
|
+
### Custom Renderers
|
|
438
|
+
|
|
439
|
+
You can traverse the AST to create custom renderers:
|
|
440
|
+
|
|
441
|
+
```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
|
+
```
|
|
453
|
+
|
|
454
|
+
### Extracting Information
|
|
455
|
+
|
|
456
|
+
Pattern match on the AST to extract structured data:
|
|
457
|
+
|
|
458
|
+
```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
|
+
}
|
|
465
|
+
|
|
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
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Transforming Documents
|
|
485
|
+
|
|
486
|
+
Apply transformations to the AST:
|
|
487
|
+
|
|
488
|
+
```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
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Best Practices
|
|
504
|
+
|
|
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)
|
|
509
|
+
|
|
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
|
|
514
|
+
|
|
515
|
+
### 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
|