Creating blocks
Thinking in blocks
Bbmd uses a block-based approach to building markdown documents. This allows you to create reusable components, express conditions, and keep your types simple.
Although a block-based approach is more convenient for injecting dynamic data and expressing conditions, template literals are more concise for static content and long strings. Bbmd supports both approaches and you can mix and match them freely within the same document.
const doc = b.doc(
b.h`About our company`,
b.p`We are a company that makes widgets.`,
b.br(),
b.h`Our process`.l(2),
b.p`We follow a ${b.i`rigorous`} process to make ${b.u`widgets`}.`,
b.br(),
b.h`Our customers`.l(2),
b.table(
{name: "Name", email: "Email"},
{name: "John Doe", email: "john.doe@example.com"},
{name: "Jane Smith", email: "jane.smith@example.com"}
)
)const doc = `
# About our company
We are a company that makes widgets.
## Our process
We follow a _rigorous_ process to make ==widgets==.
## Our customers
| Name | Email |
| ---------- | ---------------------- |
| John Doe | john.doe@example.com |
| Jane Smith | jane.smith@example.com |
`# About our company
We are a company that makes widgets.
## Our process
We follow a _rigorous_ process to make ==widgets==.
## Our customers
| Name | Email |
| ---------- | ---------------------- |
| John Doe | john.doe@example.com |
| Jane Smith | jane.smith@example.com |The generic template literal b.md`` is also available for parsing existing markdown strings into blocks, which is particularly useful for incrementally adopting Bbmd in an existing codebase.
// 👇 add `b.md` to existing template literals
const doc = b.md`
# About our company
We are a company that makes widgets.
## Our process
We follow a _rigorous_ process to make ==widgets==.
## Our customers
| Name | Email |
| ---------- | ---------------------- |
| John Doe | john.doe@example.com |
| Jane Smith | jane.smith@example.com |
`.parse() // 👈 and then `.parse()` it to convert it automatically# About our company
We are a company that makes widgets.
## Our process
We follow a _rigorous_ process to make ==widgets==.
## Our customers
| Name | Email |
| ---------- | ---------------------- |
| John Doe | john.doe@example.com |
| Jane Smith | jane.smith@example.com |MarkdownDocument
├── MarkdownHeadingBlock [level=1]
│ └── "About our company"
├── MarkdownParagraphBlock
│ └── "We are a company that makes widgets."
├── MarkdownLineBreakBlock
├── MarkdownSectionBlock
│ ├── MarkdownHeadingBlock [level=2]
│ │ └── "Our process"
│ ├── MarkdownParagraphBlock
│ │ ├── "We follow a "
│ │ ├── MarkdownItalicBlock [style=_]
│ │ │ └── "rigorous"
│ │ ├── " process to make "
│ │ ├── MarkdownHighlightBlock
│ │ │ └── "widgets"
│ │ └── "."
│ └── MarkdownLineBreakBlock
└── MarkdownSectionBlock
├── MarkdownHeadingBlock [level=2]
│ └── "Our customers"
└── MarkdownTableBlock [columns=Name,Email, rows=2]
├── columns
│ ├── "Name"
│ └── "Email"
└── rows
├── row 0
│ ├── "John Doe"
│ └── "john.doe@example.com"
└── row 1
├── "Jane Smith"
└── "jane.smith@example.com"Notice that in the example above, we were able to keep our existing markdown string and simply add b.md and .parse() to convert it into a block document. This makes it easy to incrementally adopt Bbmd in your existing codebase without needing to rewrite all of your existing markdown documents.
From there, you can incrementally switch out injected blocks until you're ready to remove .parse.
const doc = b.md`
${b.h`About our company`}
We are a company that makes widgets.
${b.h`Our process`.l(2)}
We follow a ${b.i`rigorous`.style('_')} process to make ${b.hl`widgets`}.
${b.h`Our customers`.l(2)}
${b.table(
{name: "Name", email: "Email"},
{name: "John Doe", email: "john.doe@example.com"},
{name: "Jane Smith", email: "jane.smith@example.com" }
)}
`# About our company
We are a company that makes widgets.
## Our process
We follow a _rigorous_ process to make ==widgets==.
## Our customers
| Name | Email |
| ---------- | ---------------------- |
| John Doe | john.doe@example.com |
| Jane Smith | jane.smith@example.com |MarkdownLiteral [trimmed]
├── MarkdownHeadingBlock [level=1]
│ └── "About our company"
├── "We are a company that makes widgets."
├── MarkdownHeadingBlock [level=2]
│ └── "Our process"
├── "We follow a "
├── MarkdownItalicBlock [style=_]
│ └── "rigorous"
├── " process to make "
├── MarkdownHighlightBlock
│ └── "widgets"
├── "."
├── MarkdownHeadingBlock [level=2]
│ └── "Our customers"
└── MarkdownTableBlock [columns=name,email, rows=2]
├── columns
│ ├── "Name"
│ └── "Email"
└── rows
├── row 0
│ ├── "John Doe"
│ └── "john.doe@example.com"
└── row 1
├── "Jane Smith"
└── "jane.smith@example.com"As you can see, Bbmd exposes a flexible API which allows you to combine the flexibility of functional calls with the conciseness of template literals wherever it makes the most sense.
const companyName = "Acme Inc."
const users = [
{ name: "John Doe", email: "john.doe@example.com" },
{ name: "Jane Smith", email: "jane.smith@example.com" },
]
const doc = b.doc(
b.h("About ", companyName).emptyIf(!companyName).default("About our company"),
b.p`We are a company that makes widgets.`,
b.br(),
b.h`Our process`.l(2),
b.p`We follow a ${b.i`rigorous`} process to make ${b.hl`widgets`}.`,
b.br(),
b.h`Our customers`.l(2),
b.table({name: "Name", email: "Email"}, ...users)
)# About Acme Inc.
We are a company that makes widgets.
## Our process
We follow a *rigorous* process to make ==widgets==.
## Our customers
| Name | Email |
| ---------- | ---------------------- |
| John Doe | john.doe@example.com |
| Jane Smith | jane.smith@example.com |MarkdownDocument
├── MarkdownHeadingBlock [level=1]
│ ├── "About "
│ └── "Acme Inc."
├── MarkdownParagraphBlock
│ └── "We are a company that makes widgets."
├── MarkdownLineBreakBlock
├── MarkdownSectionBlock
│ ├── MarkdownHeadingBlock [level=2]
│ │ └── "Our process"
│ ├── MarkdownParagraphBlock
│ │ ├── "We follow a "
│ │ ├── MarkdownItalicBlock
│ │ │ └── "rigorous"
│ │ ├── " process to make "
│ │ ├── MarkdownHighlightBlock
│ │ │ └── "widgets"
│ │ └── "."
│ └── MarkdownLineBreakBlock
└── MarkdownSectionBlock
├── MarkdownHeadingBlock [level=2]
│ └── "Our customers"
└── MarkdownTableBlock [columns=name,email, rows=2]
├── columns
│ ├── "Name"
│ └── "Email"
└── rows
├── row 0
│ ├── "John Doe"
│ └── "john.doe@example.com"
└── row 1
├── "Jane Smith"
└── "jane.smith@example.com"Dynamically creating blocks
Most documents you create will likely need to be generated dynamically based on data or user input.
Bbmd has been designed to be used in a programmatic way that leans into standard ES6 control flow and syntax. To create documents dynamically, simply create functions that return blocks and use standard JavaScript control flow to generate the content you need.
const createUserDoc = (
userName: string,
userAge: number,
title: string = "User details"
): MarkdownDocument => {
const userDetails = b.p(b.b(userName), b.fn(`Age: ${userAge}`));
return b.doc(b.h(title).id("user-details"), userDetails);
};
createUserDoc("John", 30)# User details {#user-details}
**John**[^1]
[^1]: Age: 30MarkdownDocument
├── MarkdownHeadingBlock [identifier=user-details, level=1]
│ └── "User details"
└── MarkdownParagraphBlock
├── MarkdownBoldBlock
│ └── "John"
└── MarkdownFootnoteBlock [identifier=1]
└── footer
└── "Age: 30"Because blocks are so easy to compose and reuse, you can also accept blocks as arguments to your functions, which allows you to create reusable "slots" that can take in dynamic content from the caller.
const createOrganizationDoc = (
orgName: string,
orgDescription: string,
primaryUserDetailsDoc: MarkdownDocument
): MarkdownDocument => {
return b.doc(
b.h(orgName).id("organization-name"),
b.p(orgDescription),
b.br(),
b.h`User details`.l(2),
primaryUserDetailsDoc,
);
};
const userDetailsDoc = createUserDoc("John", 30, "Main user details")
createOrganizationDoc("Acme Inc.", "We make everything.", userDetailsDoc)# Acme Inc. {#organization-name}
We make everything.
## User details
### Main user details {#user-details}
**John**[^1]
[^1]: Age: 30MarkdownDocument
├── MarkdownHeadingBlock [identifier=organization-name, level=1]
│ └── "Acme Inc."
├── MarkdownParagraphBlock
│ └── "We make everything."
├── MarkdownLineBreakBlock
└── MarkdownSectionBlock
├── MarkdownHeadingBlock [level=2]
│ └── "User details"
└── MarkdownDocument
├── MarkdownHeadingBlock [identifier=user-details, level=3]
│ └── "Main user details"
└── MarkdownParagraphBlock
├── MarkdownBoldBlock
│ └── "John"
└── MarkdownFootnoteBlock [identifier=1]
└── footer
└── "Age: 30"Typing blocks
Types in bbmd have been designed carefully to avoid complexity. There is a three-tier hierarchy of types which will help keep your I/O extremely lean when embedding/returning bbmd blocks.
| Tier | Description | Type(s) |
|---|---|---|
| 1 | All blocks | MarkdownBlock |
| 2 | Structural blocks | MarkdownInlineBlock | MarkdownLineBlock | MarkdownMultilineBlock |
| 3 | Concrete implementations | Specific Markdown blocks (e.g. MarkdownBoldBlock, etc) |
As a general rule, use the highest level of specificity that it is convenient for a function to accept/return. For the most part, bbmd should type to tier 3 for you automatically, however if you need to declare type signatures yourself, it can be more convenient to duck down to the next lowest tier.
const createUserTemplate = (
userName: string,
status: string,
): MarkdownInlineBlock => {
// 👆 the inferred return type is
// MarkdownParagraphBlock | MarkdownBoldBlock | MarkdownStrikethroughBlock
// however, it was more convenient to explicitly type the return as MarkdownInlineBlock
return b.p(userName).change((block) => {
if (status === "active") return block.bold();
if (status === "inactive") return block.strikethrough();
return block;
});
};
createUserTemplate("John", "inactive");~~John~~MarkdownStrikethroughBlock
└── MarkdownParagraphBlock
└── "John"As part of keeping typing simple, remember that Bbmd is smart enough to identify empty blocks and will automatically remove them from the output for you. This means there's no need to write functions that return MarkdownBlock | null or to worry about conditionally including content in your documents — simply return an empty block (b.p() for example) and Bbmd will handle the rest.
// ❌ bad
const createUserTemplateBad = (
userName?: string,
): MarkdownBlock | null => { // this type is more complex than it needs to be
if (!userName) return null; // and results in more complex handling
return b.p(userName);
}
// ✅ good
const createUserTemplateGood = (
userName?: string,
): MarkdownInlineBlock => {
return b.p(userName);
}
const userTemplate = createUserTemplateGood("");
b.doc(b.h`User`.if(userTemplate), userTemplate); // returns emptyMarkdownDocument
├── MarkdownHeadingBlock
└── MarkdownParagraphBlock