🪵 bbmd

Rendering options

If one of the super-powers of a block-based architecture is that blocks can structurally adjust themselves based on their context, the other is that they can adjust themselves stylistically as well.

You can set rendering options on any block using .setRenderingOptions(). These options propagate through the block's entire subtree, so every nested block renders consistently without any extra effort.

Creating shared options

Use b.renderingOptions() to create a reusable options object. This is particularly useful for organizations that want to define a consistent Markdown style across all documents — create it once, share it via a module, and apply it everywhere.

const orgStyle = b.renderingOptions({
  enforce: {
    bold: { style: "__" },
    italic: { style: "_" },
    unorderedListItem: { style: "*" },
  },
  newlineStrategy: "between_blocks",
})

const doc = b.doc(
  b.h`Style Guide`,
  b.p`${b.b`Bold`} and ${b.i`italic`} text`,
  b.list.ul("First item", "Second item"),
).setRenderingOptions(orgStyle)

String(doc)
# Style Guide

__Bold__ and _italic_ text

* First item
* Second item

You can also inline the options directly on a block. These persist across every render and are used automatically when calling String() or .toString().

const doc = b.doc(
  b.h`Release Notes`,
  b.p("Version ", b.b`2.0`),
  b.p`Bug fixes and improvements.`,
).setRenderingOptions({
  newlineStrategy: "between_blocks",
  enforce: { bold: { style: "__" } },
})

String(doc)
# Release Notes

Version __2.0__

Bug fixes and improvements.

newlineStrategy

Controls how newlines are inserted between blocks in multiline containers like documents and sections.

StrategyBehavior
"none"Single newline between all blocks (default)
"between_blocks"Double newline between all blocks
"before_and_after_heading"Double newline before and after headings only

"none" (default)

b.doc("one", "two", "three")
  .setRenderingOptions({ newlineStrategy: "none" })
one
two
three

"between_blocks"

b.doc("one", "two", "three")
  .setRenderingOptions({ newlineStrategy: "between_blocks" })
one

two

three

"before_and_after_heading"

Adds double newlines around headings while keeping single newlines between other blocks.

b.doc("intro", b.h`Title`, "body")
  .setRenderingOptions({ newlineStrategy: "before_and_after_heading" })
intro

# Title

body

enforce

Style overrides that are applied to every matching block in the subtree, regardless of per-block settings. This is what makes shared rendering options powerful — individual contributors can write blocks however they like, and the organization's style is enforced at render time.

enforce.bold

Override the bold marker style.

StyleOutput
"**"**text** (default)
"__"__text__
b.b`important`
  .setRenderingOptions({ enforce: { bold: { style: "__" } } })
__important__

Even per-block .style() calls are overridden:

b.b`important`.style("__")
  .setRenderingOptions({ enforce: { bold: { style: "**" } } })
**important**

enforce.italic

Override the italic marker style.

StyleOutput
"*"*text* (default)
"_"_text_
b.i`emphasis`
  .setRenderingOptions({ enforce: { italic: { style: "_" } } })
_emphasis_

enforce.unorderedListItem

Override the bullet marker for unordered list items.

StyleOutput
"-"- item (default)
"*"* item
"+"+ item
b.list.unordered("one", "two")
  .setRenderingOptions({ enforce: { unorderedListItem: { style: "*" } } })
* one
* two

enforce.horizontalRule

Override the horizontal rule character.

StyleOutput
"-"--- (default)
"*"***
"_"___
b.hr()
  .setRenderingOptions({ enforce: { horizontalRule: { style: "*" } } })

***

The .count() setting on horizontal rules is still respected:

b.hr().count(5)
  .setRenderingOptions({ enforce: { horizontalRule: { style: "*" } } })

*****

enforce.taskItem

Override the checkbox marker for checked task items. Unchecked tasks always render as [ ].

StyleOutput
"x"- [x] task (default)
"X"- [X] task
b.list.tasks([true, "done"], [false, "pending"])
  .setRenderingOptions({ enforce: { taskItem: { style: "X" } } })
- [X] done
- [ ] pending

enforce.table

Override table column alignment for all columns.

StyleSeparator
undefined--- (default, no alignment)
"left":---
"center":---:
"right"---:
b.table(
  { name: "Name", role: "Role" },
  { name: "Alice", role: "Admin" },
  { name: "Bob", role: "Viewer" },
).setRenderingOptions({ enforce: { table: { align: "center" } } })
| Name  | Role   |
| :---: | :---:  |
| Alice | Admin  |
| Bob   | Viewer |

enforce.list

Override the indentation depth (in spaces) for nested lists.

b.list.unordered(
  "top level",
  b.list.unordered("nested"),
).setRenderingOptions({ enforce: { list: { indent: 4 } } })
- top level
    - nested

Default indentation is 2 spaces.


lineJoinString

Controls the separator used when joining multiple content items within an inline block. Defaults to "" (empty string).

b.p("hello", "world")
  .setRenderingOptions({ lineJoinString: " " })
hello world
b.p("one", "two", "three")
  .setRenderingOptions({ lineJoinString: ", " })
one, two, three

Propagates into nested inline blocks:

b.p("hello ", b.b("foo", "bar"))
  .setRenderingOptions({ lineJoinString: "-" })
hello -**foo-bar**

renderNullish

Controls whether null and undefined values are rendered or silently filtered out. Defaults to false.

When false (default), nullish values are removed:

b.doc("hello", null, "world")
  .setRenderingOptions({ renderNullish: false })
hello
world

When true, nullish values are rendered as empty lines in multiline blocks:

b.doc("hello", null, "world")
  .setRenderingOptions({ renderNullish: true })
hello

world

And as literal strings in inline blocks:

b.p("hello", null, "world")
  .setRenderingOptions({ renderNullish: true })
hellonullworld

Propagation

All rendering options propagate through the entire block subtree — documents, sections, block quotes, lists, and inline blocks all inherit the options from their parent.

const doc = b.doc(
  b.h`Title`,
  b.p(b.b`intro`, " ", b.i`text`),
  b.list.unordered("item 1", "item 2"),
  b.hr(),
).setRenderingOptions({
  enforce: {
    bold: { style: "__" },
    italic: { style: "_" },
    unorderedListItem: { style: "*" },
    horizontalRule: { style: "*" },
  },
  newlineStrategy: "between_blocks",
})

String(doc)
# Title

__intro__ _text_

* item 1
* item 2

***

Priority order

Options are resolved in the following order (highest priority wins):

  1. Options passed to .render()
  2. Block-level options set via .setRenderingOptions()
  3. Default values

On this page