Skip to main content

ZIO Blocks Docs (Markdown)

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.

Here are the core type definitions:

import zio.blocks.chunk.Chunk

final case class Doc(blocks: Chunk[Block], metadata: Map[String, String] = Map.empty)

sealed trait Block extends Product with Serializable
final case class Paragraph(content: Chunk[Inline]) extends Block
final case class Heading(level: HeadingLevel, content: Chunk[Inline]) extends Block
final case class CodeBlock(info: Option[String], code: String) extends Block

sealed trait Inline extends Product with Serializable
final case class Text(value: String) extends Inline
final case class Code(value: String) extends Inline
final case class Link(text: Chunk[Inline], url: String, title: Option[String]) extends Inline

Motivation

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:

  • Type-safe ADT: Every markdown element is a Scala case class—no stringly-typed HTML generation
  • Strict parsing: Rejects invalid markdown with precise error locations (line, column)
  • Round-trip semantics: Parse-render-parse cycles preserve document structure
  • Normalized equality: Two documents are equal if their normalized forms are equal (adjacent text nodes merged, empty blocks removed)
  • Multiple output formats: Render to GFM markdown, HTML (full document or fragment), or colorized terminal
  • Compile-time validation: The md"..." string interpolator validates markdown syntax at compile time, catching errors before runtime
  • Type class system: ToMarkdown lets you interpolate custom types into markdown documents

Installation

Add the dependency to your build.sbt:

libraryDependencies += "dev.zio" %% "zio-blocks-markdown" % "0.0.41"

For Scala.js, use the cross-build syntax:

libraryDependencies += "dev.zio" %%% "zio-blocks-markdown" % "0.0.41"

Supported Scala versions: 2.13.x and 3.x

How They Work Together

Markdown documents flow through a parsing → normalization → rendering pipeline:

Markdown String ──> Parser ──> Doc (blocks) ──> Renderer ──> Markdown/HTML/Terminal
│ │
ParseError ToMarkdown
(type class)

Doc = Chunk[Block]
Block = Paragraph | Heading | CodeBlock | List | Table | ... (contains Inlines)
Inline = Text | Code | Emphasis | Strong | Link | Image | ... (leaf nodes)

Typical workflow:

  1. Parse markdownParser.parse("# Hello") returns Either[ParseError, Doc]
  2. Build programmatically — construct Doc with Heading, Paragraph, BulletList, etc.
  3. Normalizedoc.normalize merges adjacent text, removes empty blocks
  4. Render to desired format — GFM (Renderer.render), HTML (HtmlRenderer.render), terminal (TerminalRenderer.render)
  5. Use interpolatormd"# Title with $interpolation" validates markdown at compile time

Here's a complete example of composing a document:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(
Heading(HeadingLevel.H1, Chunk(Text("My Document"))),
Paragraph(Chunk(Text("This is a paragraph with "), Strong(Chunk(Text("bold"))), Text(" text."))),
CodeBlock(Some("scala"), "val x = 42"),
BulletList(Chunk(
ListItem(Chunk(Paragraph(Chunk(Text("Item 1")))), None),
ListItem(Chunk(Paragraph(Chunk(Text("Item 2")))), None)
), tight = true)
))

Rendering to GFM markdown:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Renderer.render(doc)
// res0: String = """# My Document
// This is a paragraph with **bold** text.
//
// ```scala
// val x = 42
// ```
// - Item 1
// - Item 2
// """

Rendering to HTML:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

HtmlRenderer.render(doc)
// 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>"

Rendering to terminal:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

TerminalRenderer.render(doc)
// res2: String = """My Document
//
// This is a paragraph with bold text.
//
// val x = 42
//
// • Item 1
// • Item 2
// """

Parsing markdown strings to create documents:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val markdown = """# My Document

This is a paragraph with **bold** text.

- Item 1
- Item 2
"""

Parser.parse(markdown) match {
case Right(parsedDoc) => println(s"Successfully parsed ${parsedDoc.blocks.size} blocks")
case Left(err) => println(s"Parse error: ${err.message}")
}

Round-trip verification (parse → render → parse preserves structure):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val input = "# Hello\n\nWorld"
// input: String = """# Hello
//
// World"""
val parsed1 = Parser.parse(input).toOption.get
// parsed1: Doc = Doc(
// blocks = IndexedSeq(
// Heading(level = H1, content = IndexedSeq(Text("Hello"))),
// Paragraph(IndexedSeq(Text("World")))
// ),
// metadata = Map()
// )
val rendered = Renderer.render(parsed1)
// rendered: String = """# Hello
// World
//
// """
val parsed2 = Parser.parse(rendered).toOption.get
// parsed2: Doc = Doc(
// blocks = IndexedSeq(
// Heading(level = H1, content = IndexedSeq(Text("Hello"))),
// Paragraph(IndexedSeq(Text("World")))
// ),
// metadata = Map()
// )
parsed1 == parsed2 // Equal after normalization
// res5: Boolean = true

Common Patterns

The markdown module provides several patterns for working with documents and types. Here are the most common usage scenarios:

String Interpolation with Compile-Time Validation

The md"..." interpolator validates markdown at compile time and supports interpolation of any type with a ToMarkdown instance:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val name = "Alice"
val count = 42

Now interpolate these values into markdown:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

md"# Welcome $name\nYou have $count items."
// res6: Doc = Doc(
// blocks = IndexedSeq(
// Heading(
// level = H1,
// content = IndexedSeq(Text("Welcome Alice\\nYou have 42 items."))
// )
// ),
// metadata = Map()
// )

Task Lists (GFM Feature)

Task list items use ListItem with checked: Option[Boolean]:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val tasks = BulletList(Chunk(
ListItem(Chunk(Paragraph(Chunk(Text("Buy groceries")))), Some(true)),
ListItem(Chunk(Paragraph(Chunk(Text("Write docs")))), Some(false)),
ListItem(Chunk(Paragraph(Chunk(Text("Call Alice")))), None) // No checkbox
), tight = true)

Rendering the task list:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Renderer.render(Doc(Chunk(tasks)))
// res7: String = """- [x] Buy groceries
// - [ ] Write docs
// - Call Alice
// """

Tables with Alignment

Tables require a header row, alignment specification, and data rows:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val table = Table(
header = TableRow(Chunk(
Chunk(Text("Name")),
Chunk(Text("Age")),
Chunk(Text("City"))
)),
alignments = Chunk(Alignment.Left, Alignment.Right, Alignment.Center),
rows = Chunk(
TableRow(Chunk(
Chunk(Text("Alice")),
Chunk(Text("30")),
Chunk(Text("NYC"))
)),
TableRow(Chunk(
Chunk(Text("Bob")),
Chunk(Text("25")),
Chunk(Text("LA"))
))
)
)

Rendering the table to markdown:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Renderer.render(Doc(Chunk(table)))
// res8: String = """| Name | Age | City |
// |:---|---:|:--:|
// | Alice | 30 | NYC |
// | Bob | 25 | LA |
// """

Parsing with Error Handling

Parser returns Either[ParseError, Doc] with precise location information:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Parser.parse("# Hello\n[invalid link](") match {
case Right(doc) => println("Parsed successfully")
case Left(err) => println(s"Error at line ${err.line}, column ${err.column}: ${err.message}")
}

Round-Trip Semantics

Parse-render-parse cycles preserve document meaning (normalized forms are equal):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val input = "# Hello\n\nWorld"
val parsed1 = Parser.parse(input).toOption.get
val rendered = Renderer.render(parsed1)
val parsed2 = Parser.parse(rendered).toOption.get
assert(parsed1 == parsed2) // Equal after normalization

Custom Type Interpolation

Implement ToMarkdown for your types to enable interpolation:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

case class User(name: String, role: String)

implicit val userToMarkdown: ToMarkdown[User] = { user =>
Strong(Chunk(Text(user.name), Text(s" – ${user.role}")))
}

val user = User("Alice", "Engineer")
val doc = md"# Team\n$user"

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.

Integration Points

The markdown module integrates seamlessly with other ZIO Blocks components to provide a cohesive ecosystem for working with structured data and documentation.

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.

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.

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.


Doc

A complete GitHub Flavored Markdown document.

Definition

Here is the Doc type definition:

import zio.blocks.chunk.Chunk

final case class Doc(blocks: Chunk[Block], metadata: Map[String, String] = Map.empty)

A parsed or constructed markdown document. The metadata field is reserved for future use and defaults to an empty map.

Construction

Create an empty document:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val empty = Doc.empty

Construct a document from blocks:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(
Heading(HeadingLevel.H1, Chunk(Text("Title"))),
Paragraph(Chunk(Text("Content")))
))

Parse a markdown string to create a document:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Parser.parse("# Title\n\nContent") match {
case Right(doc) => // use doc
case Left(err) => // handle error
}

Core Operations

Merge documents using concatenation with Doc#++:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc1 = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Part 1")))))
val doc2 = Doc(Chunk(Paragraph(Chunk(Text("Part 2")))))
val combined = doc1 ++ doc2

Render a document to GFM markdown:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
// doc: Doc = Doc(
// blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
// metadata = Map()
// )
val markdown: String = doc.toString // Calls Renderer.render internally
// markdown: String = """# Hello
// """

Render to HTML with full document structure including DOCTYPE (returns complete HTML5 document with <!DOCTYPE html> wrapper):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
// doc: Doc = Doc(
// blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
// metadata = Map()
// )
val html = doc.toHtml
// html: String = "<!DOCTYPE html><html><head></head><body><h1>Hello</h1></body></html>"

Render to HTML fragment containing only the content (returns just the rendered HTML blocks without wrapper tags):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
// doc: Doc = Doc(
// blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
// metadata = Map()
// )
val fragment = doc.toHtmlFragment
// fragment: String = "<h1>Hello</h1>"

Render to colorized terminal output (returns ANSI-colored string suitable for terminal display):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Hello")))))
// doc: Doc = Doc(
// blocks = IndexedSeq(Heading(level = H1, content = IndexedSeq(Text("Hello")))),
// metadata = Map()
// )
val terminal = doc.toTerminal
// terminal: String = """Hello
//
// """

Canonicalize document structure with Doc#normalize to merge adjacent text nodes and remove empty blocks:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(
Paragraph(Chunk(Text("A"), Text("B"))), // Adjacent text nodes
Paragraph(Chunk()), // Empty paragraph
Paragraph(Chunk(Text("C")))
))

Call Doc#normalize to see the result:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val normalized = doc.normalize
// normalized: Doc = Doc(
// blocks = IndexedSeq(
// Paragraph(IndexedSeq(Text("AB"))),
// Paragraph(IndexedSeq(Text("C")))
// ),
// metadata = Map()
// )

Equality and Hashing

Two documents are equal if their normalized forms are equal. This means:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc1 = Doc(Chunk(Paragraph(Chunk(Text("Hello"), Text(" "), Text("World")))))
val doc2 = Doc(Chunk(Paragraph(Chunk(Text("Hello World")))))
assert(doc1 == doc2) // Equal after normalization

Hash code computes from the normalized form for consistency with equals.


Block (Sealed Trait)

A block-level markdown element:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

sealed trait Block extends Product with Serializable

Block is a sealed trait with the following concrete subtypes.

Paragraph

A paragraph containing inline content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Paragraph(content: Chunk[Inline]) extends Block

Here's an example of creating a paragraph with mixed content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val para = Paragraph(Chunk(
Text("Hello "),
Strong(Chunk(Text("world")))
))

Heading

An ATX-style heading (# to ######) with a level and inline content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Heading(level: HeadingLevel, content: Chunk[Inline]) extends Block

Here's an example of creating headings with different levels:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val h1 = Heading(HeadingLevel.H1, Chunk(Text("Title")))
val h3 = Heading(HeadingLevel.H3, Chunk(Text("Subsection")))

CodeBlock

A fenced code block with optional language/info string:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class CodeBlock(info: Option[String], code: String) extends Block

Here are examples of creating code blocks with and without language specification:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

// Scala code block
val scalaBlock = CodeBlock(Some("scala"), "val x = 42\nprintln(x)")

// No language specified
val plainBlock = CodeBlock(None, "some code")

ThematicBreak

A thematic break (horizontal rule):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

case object ThematicBreak extends Block

Create a thematic break:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val break = ThematicBreak

Renders as: ---\n (or *** or ___)

BlockQuote

A block quote containing nested blocks:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class BlockQuote(content: Chunk[Block]) extends Block

Here's an example of creating a block quote:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val quote = BlockQuote(Chunk(
Paragraph(Chunk(Text("This is a famous quote.")))
))

BulletList

An unordered list with bullet markers (-, *, +):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class BulletList(items: Chunk[ListItem], tight: Boolean) extends Block

The tight parameter controls spacing: true removes blank lines between items for compact rendering. Here's an example:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val list = BulletList(Chunk(
ListItem(Chunk(Paragraph(Chunk(Text("Item 1")))), None),
ListItem(Chunk(Paragraph(Chunk(Text("Item 2")))), None)
), tight = true)

OrderedList

An ordered list with numeric markers (1., 2., etc.):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class OrderedList(start: Int, items: Chunk[ListItem], tight: Boolean) extends Block

The start parameter specifies the starting number (typically 1). Here's an example:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val list = OrderedList(
start = 1,
items = Chunk(
ListItem(Chunk(Paragraph(Chunk(Text("First")))), None),
ListItem(Chunk(Paragraph(Chunk(Text("Second")))), None)
),
tight = true
)

ListItem

A list item, optionally a task list item:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class ListItem(content: Chunk[Block], checked: Option[Boolean]) extends Block

The checked parameter: Some(true) renders as [x], Some(false) renders as [ ], None for regular list items. Here are examples of each type:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

// Regular list item
val item = ListItem(Chunk(Paragraph(Chunk(Text("Task")))), None)

// Completed task
val completed = ListItem(Chunk(Paragraph(Chunk(Text("Done")))), Some(true))

// Incomplete task
val incomplete = ListItem(Chunk(Paragraph(Chunk(Text("TODO")))), Some(false))

HtmlBlock

Raw HTML block content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class HtmlBlock(content: String) extends Block

Here's an example of creating an HTML block:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val html = HtmlBlock("<div class='alert'>Custom HTML</div>")

Table

A GitHub Flavored Markdown table with aligned columns:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Table(header: TableRow, alignments: Chunk[Alignment], rows: Chunk[TableRow]) extends Block

Here's an example of creating a table:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val table = Table(
header = TableRow(Chunk(Chunk(Text("Name")), Chunk(Text("Age")))),
alignments = Chunk(Alignment.Left, Alignment.Right),
rows = Chunk(
TableRow(Chunk(Chunk(Text("Alice")), Chunk(Text("30")))),
TableRow(Chunk(Chunk(Text("Bob")), Chunk(Text("25"))))
)
)

Inline (Sealed Trait)

An inline-level markdown element:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

sealed trait Inline extends Product with Serializable

Inline is a sealed trait with concrete subtypes, defining both object-level and top-level forms for API compatibility.

Text

Plain text content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Text(value: String) extends Inline

Here's an example of creating text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val text = Text("Hello world")

Code

Inline code span (backtick-delimited):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Code(value: String) extends Inline

Here's an example of creating inline code:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val code = Code("val x = 42")

Emphasis

Emphasized (italic) text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Emphasis(content: Chunk[Inline]) extends Inline

Here's an example of creating emphasized text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val emphasis = Emphasis(Chunk(Text("italic")))
// Renders as: *italic*

Strong

Strong (bold) text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Strong(content: Chunk[Inline]) extends Inline

Here's an example of creating strong text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val strong = Strong(Chunk(Text("bold")))
// Renders as: **bold**

Strikethrough

Strikethrough text (GFM feature):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Strikethrough(content: Chunk[Inline]) extends Inline

Here's an example of creating strikethrough text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val struck = Strikethrough(Chunk(Text("deprecated")))
// Renders as: ~~deprecated~~

A hyperlink:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Link(text: Chunk[Inline], url: String, title: Option[String]) extends Inline

The title parameter is optional link title text. Here are examples of creating links:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

// Simple link
val link = Link(Chunk(Text("Click here")), "https://example.com", None)

// Link with title
val titled = Link(Chunk(Text("Docs")), "/docs", Some("Documentation"))

Image

An image reference:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Image(alt: String, url: String, title: Option[String]) extends Inline

Here are examples of creating images:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val img = Image(alt = "Logo", url = "/logo.png", None)
val imgWithTitle = Image(alt = "Icon", url = "/icon.svg", Some("App Icon"))

HtmlInline

Raw HTML inline content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class HtmlInline(content: String) extends Inline

Here's an example of creating HTML inline content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val html = HtmlInline("<span class='highlight'>custom</span>")

SoftBreak

A soft line break (single newline, rendered as space or newline depending on context):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

case object SoftBreak extends Inline

HardBreak

A hard line break (two spaces or backslash before newline):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

case object HardBreak extends Inline

An autolink (URL or email in angle brackets):

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class Autolink(url: String, isEmail: Boolean) extends Inline

Here are examples of creating autolinks:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val urlLink = Autolink("https://example.com", isEmail = false)
val emailLink = Autolink("user@example.com", isEmail = true)
// Renders as: <https://example.com> and <user@example.com>

HeadingLevel

Heading levels from H1 to H6:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

sealed abstract class HeadingLevel(val value: Int) extends Product with Serializable

object HeadingLevel {
case object H1 extends HeadingLevel(1)
case object H2 extends HeadingLevel(2)
case object H3 extends HeadingLevel(3)
case object H4 extends HeadingLevel(4)
case object H5 extends HeadingLevel(5)
case object H6 extends HeadingLevel(6)
}

Safe Construction

Construct a heading level from an integer, returning an Option:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

HeadingLevel.fromInt(2) == Some(HeadingLevel.H2)
HeadingLevel.fromInt(7) == None // Out of range

Construct a heading level unsafely, throwing if input is invalid:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

HeadingLevel.unsafeFromInt(3) // HeadingLevel.H3
HeadingLevel.unsafeFromInt(7) // Throws IllegalArgumentException

Accessing Value

Access the numeric value of a heading level:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

HeadingLevel.H1.value == 1

Alignment

Table column alignment specification:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

sealed trait Alignment extends Product with Serializable

object Alignment {
case object Left extends Alignment
case object Right extends Alignment
case object Center extends Alignment
case object None extends Alignment
}

Predefined Alignments

Use predefined alignment values for table columns:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Alignment.Left // Renders as :---
Alignment.Right // Renders as ---:
Alignment.Center // Renders as :---:
Alignment.None // Renders as ---

Use in Tables

Create a table with specific column alignments:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._
val header = TableRow(Chunk(
Chunk(Text("Name")),
Chunk(Text("Age")),
Chunk(Text("City"))
))
val rows = Chunk(
TableRow(Chunk(
Chunk(Text("Alice")),
Chunk(Text("30")),
Chunk(Text("NYC"))
)),
TableRow(Chunk(
Chunk(Text("Bob")),
Chunk(Text("25")),
Chunk(Text("LA"))
))
)
val alignments = Chunk(Alignment.Left, Alignment.Center, Alignment.Right)
val table = Table(header, alignments, rows)
val doc = Doc(Chunk(table))

If we render this document, the table will have left-aligned "Name", center-aligned "Age", and right-aligned "City":

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Renderer.render(doc)
// res45: String = """| Name | Age | City |
// |:---|:--:|---:|
// | Alice | 30 | NYC |
// | Bob | 25 | LA |
// """

TableRow

A single row in a table, containing cells as inline content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class TableRow(cells: Chunk[Chunk[Inline]])

Each cell is a Chunk[Inline], allowing formatted content (text, links, emphasis, etc.). Here's an example:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val row = TableRow(Chunk(
Chunk(Text("Alice")),
Chunk(Strong(Chunk(Text("30")))),
Chunk(Link(Chunk(Text("NYC")), "/cities/nyc", None))
))

Parser

A singleton object providing strict GitHub Flavored Markdown parsing with position-aware error reporting.

Parsing

Use the main entry point to parse markdown:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Parser.parse(input: String): Either[ParseError, Doc]

Returns Right(doc) on success or Left(error) on parse failure. Here's an example:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val result = Parser.parse("# Hello\n\nWorld")
result match {
case Right(doc) => println(s"Parsed ${doc.blocks.size} blocks")
case Left(err) => println(s"Parse error at line ${err.line}: ${err.message}")
}

Supported Features

  • ATX headings (# to ######)
  • Fenced code blocks (``` or ~~~)
  • Thematic breaks (---, ***, ___)
  • Block quotes (> prefix)
  • Bullet lists (-, *, +)
  • Ordered lists (1., 2., etc.)
  • Task lists (- [ ] and - [x])
  • Tables with alignment (GFM)
  • Inline formatting (emphasis, strong, strikethrough)
  • Links and images with optional titles
  • Autolinks (<url> or <email>)
  • HTML blocks and inline HTML
  • Soft and hard line breaks

Unsupported Features

  • YAML frontmatter (causes parse error)
  • Setext headings (use ATX style with # instead)
  • Indented code blocks (use fenced with ``` instead)
  • Link reference definitions

ParseError

Error information from parsing, with precise location data:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

final case class ParseError(
message: String, // Human-readable error description
line: Int, // 1-based line number
column: Int, // 1-based column number
input: String // The line that caused the error
)

Here's an example of creating a ParseError:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val err = ParseError("Unexpected token", line = 5, column = 12, input = "[invalid markdown]")

Get the string representation of the error:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

err.toString
// res48: String = """ParseError at line 5, column 12: Unexpected token
// [invalid markdown]"""

Renderer

A singleton object that renders markdown documents back to GitHub Flavored Markdown string format:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

object Renderer

Rendering

Render an entire document to GFM markdown:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Renderer.render(doc: Doc): String

Here's an example of rendering a document:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(
Heading(HeadingLevel.H1, Chunk(Text("Title"))),
Paragraph(Chunk(Text("Content")))
))
val rendered = Renderer.render(doc)

The result:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

rendered
// res50: String = """# Title
// Content
//
// """

Render individual blocks:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Renderer.renderBlock(block: Block): String

Render inline content:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

Renderer.renderInlines(inlines: Chunk[Inline]): String
Renderer.renderInline(inline: Inline): String

Normalization During Rendering

The renderer does not normalize; use doc.normalize before rendering if normalization is needed.

Round-Trip Semantics

Re-parse rendered output to verify round-trip semantics:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val original = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Title")))))
val markdown = Renderer.render(original)
val reparsed = Parser.parse(markdown).toOption.get
assert(original == reparsed) // Equal after normalization

HtmlRenderer

A singleton object that renders markdown documents to HTML5:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

object HtmlRenderer

Full Document Rendering

Render a document as complete HTML with DOCTYPE and html wrapper tags:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

HtmlRenderer.render(doc: Doc): String

Here's an example of rendering a complete HTML document:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Title")))))
val html = HtmlRenderer.render(doc)

The result:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

html
// res54: String = "<!DOCTYPE html><html><head></head><body><h1>Title</h1></body></html>"

Fragment Rendering

Render content-only HTML without wrapper tags:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

HtmlRenderer.renderFragment(doc: Doc): String

Here's an example of rendering an HTML fragment:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val fragment = HtmlRenderer.renderFragment(doc)

Use fragments to embed markdown content into existing HTML templates.

HTML Feature Mapping

  • Headings → <h1> through <h6>
  • Paragraphs → <p>
  • Code blocks → <pre><code class="language-..."> (language from info string)
  • Thematic breaks → <hr>
  • Block quotes → <blockquote>
  • Lists → <ul> and <ol> with <li>
  • Task lists → <li> with checkbox-like rendering
  • Tables → <table>, <tr>, <td> with alignment classes
  • Inline code → <code>
  • Emphasis → <em>
  • Strong → <strong>
  • Strikethrough → <del> or <s>
  • Links → <a href="...">
  • Images → <img alt="..." src="...">
  • HTML blocks and inlines → pass through as-is

TerminalRenderer

A singleton object that renders markdown to ANSI-colored terminal output optimized for console display:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

object TerminalRenderer

Rendering

Render a document to ANSI-colored terminal output:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

TerminalRenderer.render(doc: Doc): String

Here's an example of rendering to terminal:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = Doc(Chunk(Heading(HeadingLevel.H1, Chunk(Text("Title")))))
val terminal = TerminalRenderer.render(doc)
// Returns ANSI-colored string: Title\n\n

Color Scheme

Headings use different colors by level:

  • H1 → Red
  • H2 → Yellow
  • H3 → Green
  • H4 → Cyan
  • H5 → Blue
  • H6 → Magenta

Inline elements use ANSI styles:

  • Strong/Bold → ANSI bold
  • Emphasis/Italic → ANSI italic
  • Code → Gray background
  • Links → Underlined
  • Strikethrough → ANSI strikethrough style

The output is designed to be readable on both light and dark terminal backgrounds.


ToMarkdown

Type class for converting Scala values to markdown inline elements, enabling interpolation in the md"..." string interpolator.

Definition

Define the ToMarkdown type class:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

trait ToMarkdown[-A] {
def toMarkdown(a: A): Inline
}

The type parameter is contravariant (-A), allowing subtypes to use supertype instances.

Summon an Instance

Summon a ToMarkdown instance:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

ToMarkdown[String] // Implicitly summons the ToMarkdown[String] instance

Built-In Instances

Convert primitive types to plain text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

ToMarkdown[String] // a: String => Text(a)
ToMarkdown[Int] // a: Int => Text(a.toString)
ToMarkdown[Long] // a: Long => Text(a.toString)
ToMarkdown[Double] // a: Double => Text(a.toString)
ToMarkdown[Boolean] // a: Boolean => Text(a.toString)

Pass inline elements through unchanged:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

ToMarkdown[Inline] // a: Inline => a (identity)

Convert collections to comma-separated text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

ToMarkdown[List[A]] // as: List[A] => Text(as.map(...).mkString(", "))
ToMarkdown[Chunk[A]] // as: Chunk[A] => Text(as.map(...).mkString(", "))
ToMarkdown[Vector[A]] // as: Vector[A] => Text(as.map(...).mkString(", "))
ToMarkdown[Seq[A]] // as: Seq[A] => Text(as.map(...).mkString(", "))

For collection instances, each element is converted using its ToMarkdown[A] instance, then joined with ", ".

Render blocks to markdown text:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

ToMarkdown[Block] // b: Block => Text(Renderer.render(Doc(Chunk(b))).trim)

Custom Implementations

Define custom ToMarkdown instances for your types:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

case class Person(name: String, age: Int)

implicit val personToMarkdown: ToMarkdown[Person] = { person =>
Strong(Chunk(Text(person.name)))
}

val p = Person("Alice", 30)
val doc = md"## User: $p" // Interpolates as strong text "Alice"

String Interpolator (md"...")

The md string interpolator validates markdown at compile time and supports runtime interpolation of values.

Compile-Time Validation

Markdown syntax inside md"..." is validated at compile time:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val doc = md"# Valid heading" // Compiles

val invalid = md"# [unclosed link](" // Compile error: Invalid markdown

Runtime Interpolation

Values are interpolated and converted to inline markdown using ToMarkdown:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

val name = "Alice"
val count = 42
val doc = md"""
# Welcome $name

You have $count items.
"""
// Equivalent to parsing: "# Welcome Alice\n\nYou have 42 items."

Custom Type Interpolation

Any type with a ToMarkdown instance can be interpolated:

import zio.blocks.chunk.Chunk
import zio.blocks.docs._

case class Tag(label: String)
implicit val tagToMarkdown: ToMarkdown[Tag] = tag =>
Code(tag.label)

val tag = Tag("important")
val doc = md"Please review: $tag" // Renders tag as inline code

Interpolation Mechanics

The interpolator:

  1. Collects string parts and interpolated values (as Inline elements via ToMarkdown)
  2. Combines parts and rendered inlines into a markdown string
  3. Parses the combined string at runtime
  4. Returns a Doc or throws IllegalArgumentException if parsing fails