@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.
@@ -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