Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/matejchalk/build-md
Markdown builder for JS/TS
https://github.com/matejchalk/build-md
builder-pattern github-flavored-markdown markdown typescript
Last synced: 2 months ago
JSON representation
Markdown builder for JS/TS
- Host: GitHub
- URL: https://github.com/matejchalk/build-md
- Owner: matejchalk
- License: mit
- Created: 2024-05-25T11:58:01.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2024-08-12T15:13:11.000Z (4 months ago)
- Last Synced: 2024-09-14T07:22:52.821Z (3 months ago)
- Topics: builder-pattern, github-flavored-markdown, markdown, typescript
- Language: TypeScript
- Homepage: https://matejchalk.github.io/build-md/
- Size: 1.94 MB
- Stars: 2
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# build-md
[![npm](https://img.shields.io/npm/v/build-md.svg)](https://www.npmjs.com/package/build-md)
[![CI](https://github.com/matejchalk/build-md/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/matejchalk/build-md/actions/workflows/ci.yml?query=branch%3Amain)
[![codecov](https://codecov.io/github/matejchalk/build-md/graph/badge.svg?token=L9BONN6LRT)](https://codecov.io/github/matejchalk/build-md)Comprehensive **Markdown builder** for JavaScript/TypeScript.
![Diagram](./images/diagram.png)
๐ Full documentation is hosted at .
## โญ Key features
- โ๏ธ Its **intuitive syntax** makes it convenient for generating Markdown from JavaScript/TypeScript code.
- [Builder pattern](https://refactoring.guru/design-patterns/builder) used for creating Markdown documents.
- [Tagged template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) used for inline Markdown formatting and nesting Markdown blocks.
- โ Has **comprehensive support** for most commonly used Markdown elements.
- All elements from Markdown's [basic syntax](https://www.markdownguide.org/basic-syntax/) are included.
- Also supports many elements from [extended syntax](https://www.markdownguide.org/extended-syntax/) (e.g. from [GitHub Flavored Markdown](https://github.github.com/gfm/)).
- ๐ See [List of all supported Markdown elements](#-list-of-supported-markdown-elements).
- ๐๏ธ Enables **logical nesting** of Markdown elements and uses **contextual rendering** to ensure output will be rendered correctly.
- Blocks may contain inline elements or even other blocks (e.g. nested lists), inline elements may contain other inline elements, etc.
- Each element may be rendered as HTML instead of Markdown if needed. For example, block elements in Markdown tables will automatically render using equivalent HTML tags. And if a parent element is rendered as HTML, so will all its children.
- ๐งฎ Document builder enables writing **conditional and iterative logic** in a declarative way.
- Falsy values from regular JavaScript expressions are ignored.
- Special methods provided for adding multiple related elements conditionally or in a loop.
- Even for very complex dynamic documents, there should be no need to resort to imperative logic like `if`/`else` branches or `for` loops. But if you prefer this coding style, then its supported in mutable mode (immutable is default).
- ๐ See [Dynamic content](#-dynamic-content).
- ๐ Markdown output is **well-formatted**.
- Automatically inserts line breaks and indentation when appropriate. Even Markdown tables are aligned to be more readable.
- No need to run additional tools like Prettier to have nicely formatted Markdown.
- โป๏ธ Is lightweight with zero dependencies, as well as being completely **runtime agnostic** with regards to browser vs Node, CJS vs ESM, etc.## ๐ Quickstart
Install `build-md` with your package manager in the usual way. E.g. to install as a dev dependency using NPM:
```sh
npm install -D build-md
```Import the [`MarkdownDocument` class](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html), add some basic Markdown blocks and render as string:
```js
import { MarkdownDocument } from 'build-md';new MarkdownDocument()
.heading(1, 'Contributing')
.heading(2, 'Setup')
.paragraph('Install dependencies with:')
.code('sh', 'npm install')
.heading(2, 'Development')
.list([
'npm test - run unit tests with Vitest',
'npm run docs - generate documenation with TypeDoc',
])
.toString();
```To add inline formatting, import the [`md` tagged template literal](https://matejchalk.github.io/build-md/functions/md.html):
```js
import { MarkdownDocument, md } from 'build-md';new MarkdownDocument()
// ...
.list([
md`${md.code('npm test')} - run unit tests with ${md.link(
'https://vitest.dev/',
'Vitest'
)}`,
md`${md.code('npm run docs')} - generate documenation with ${md.link(
'https://typedoc.org/',
'TypeDoc'
)}`,
])
.toString();
```To see it in action, copy/paste this complete example into a `docs.mjs` file and run `node docs.mjs` to generate a `CONTRIBUTING.md` file:
```js
import { MarkdownDocument, md } from 'build-md';
import { writeFile } from 'node:fs/promises';const markdown = new MarkdownDocument()
.heading(1, 'Contributing')
.heading(2, 'Setup')
.paragraph('Install dependencies with:')
.code('sh', 'npm install')
.heading(2, 'Development')
.list([
md`${md.code('npm test')} - run unit tests with ${md.link(
'https://vitest.dev/',
'Vitest'
)}`,
md`${md.code('npm run docs')} - generate documenation with ${md.link(
'https://typedoc.org/',
'TypeDoc'
)}`,
])
.toString();await writeFile('CONTRIBUTING.md', markdown);
```## ๐ List of supported Markdown elements
| Element | Usage | Example |
| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bold | [`md.bold(text)`](https://matejchalk.github.io/build-md/functions/md.html#bold) | **important text** |
| Italic | [`md.italic(text)`](https://matejchalk.github.io/build-md/functions/md.html#italic) | _emphasized text_ |
| Link | [`md.link(href, text?, title?)`](https://matejchalk.github.io/build-md/functions/md.html#link) | [link](#list-of-supported-markdown-elements) |
| Image | [`md.image(src, alt)`](https://matejchalk.github.io/build-md/functions/md.html#image) | ![image](https://img.shields.io/badge/๐ผ๏ธ_image-lightgreen) |
| Code | [`md.code(text)`](https://matejchalk.github.io/build-md/functions/md.html#code) | `source_code` |
| Strikethrough [^1] | [`md.strikethrough(text)`](https://matejchalk.github.io/build-md/functions/md.html#strikethrough) | ~~crossed out~~ |
| Footnote [^1] | [`md.footnote(text, label?)`](https://matejchalk.github.io/build-md/functions/md.html#footnote) | [^2] |
| Heading | [`MarkdownDocument#heading(level, text)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#heading)
[`md.heading(level, text)`](https://matejchalk.github.io/build-md/functions/md.html#heading) |Title
|
| Paragraph | [`MarkdownDocument#paragraph(text)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#paragraph)
[`md.paragraph(text)`](https://matejchalk.github.io/build-md/functions/md.html#paragraph) |Some long text spanning a few sentences.
|
| Code block | [`MarkdownDocument#code(lang?, text)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#code)
[`md.codeBlock(lang?, text)`](https://matejchalk.github.io/build-md/functions/md.html#codeBlock) ||sourceCode({
ย multiLine: true,
ย syntaxHighlighting: true
})
| Horizontal rule | [`MarkdownDocument#rule()`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#rule)
[`md.rule()`](https://matejchalk.github.io/build-md/functions/md.html#rule) |
|
| Blockquote | [`MarkdownDocument#quote(text)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#quote)
[`md.quote(text)`](https://matejchalk.github.io/build-md/functions/md.html#quote) |interesting quote|
| Unordered list | [`MarkdownDocument#list(items)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#list)
[`md.list(items)`](https://matejchalk.github.io/build-md/functions/md.html#list) |
- list item 1
- list item 2
| Ordered list | [`MarkdownDocument#list('ordered', items)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#list)
[`md.list('ordered', items)`](https://matejchalk.github.io/build-md/functions/md.html#list) |
- list item 1
- list item 2
| Task list [^1] | [`MarkdownDocument#list('task', items)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#list)
[`md.list('task', items)`](https://matejchalk.github.io/build-md/functions/md.html#list) | โ list item 1
โ list item 2 |
| Table [^1] | [`MarkdownDocument#table(columns, rows)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#table)
[`md.table(columns, rows)`](https://matejchalk.github.io/build-md/functions/md.html#table) | heading 1heading 2row 1, col. 1row 1, col. 2row 2, col. 1row 2, col. 2 |
| Details [^3] | [`MarkdownDocument#details(summary?, text)`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#details)
[`md.details(summary?, text)`](https://matejchalk.github.io/build-md/functions/md.html#details) | expandable content |
[^1]: Not part of basic Markdown syntax, but supported by some Markdown extensions like GFM.
[^2]: Footnotes render a label in place of insertion, as well as appending a block to the end of the document with the content.
[^3]: Always rendered as HTML.
## ๐ฅฝ Diving in
### ๐งฉ Dynamic content
While the [Quickstart](#-quickstart) example shows how to render static Markdown, the main purpose of a Markdown builder is to generate content dynamically. The `MarkdownDocument` class is designed for writing conditional or iterative logic in a simple and declarative way, without having to break out of the builder chain.
For starters, document blocks with empty content are automatically skipped. So if the expression you write for a top-level block's content evaluates to some empty value (falsy or empty array), then the block won't be appended to the document.
```ts
function createMarkdownComment(
totalCount: number,
passedCount: number,
logsUrl: string | null,
failedChecks?: string[]
): string {
return (
new MarkdownDocument()
.heading(1, `๐ก๏ธ Quality gate - ${passedCount}/${totalCount}`)
// ๐ `false` will skip quote
.quote(passedCount === totalCount && 'โ
Everything in order!')
// ๐ `undefined` or `0` will skip heading
.heading(2, failedChecks?.length && 'โ Failed checks')
// ๐ `undefined` or `[]` will skip list
.list(failedChecks?.map(md.code))
// ๐ `""` or `null` will skip paragraph
.paragraph(logsUrl && md.link(logsUrl, '๐ CI logs'))
.toString()
);
}
```
### ๐งฎ Control flow methods
The conditional expressions approach outlined above is convenient for toggling individual blocks. But if your logic affects multiple blocks at once, you may reach instead for one of the provided control flow methods โ [`$if`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#_if) and [`$foreach`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#_foreach).
The `$if` method is useful for subjecting multiple blocks to a single condition. Provide a callback function which returns the `MarkdownDocument` instance with added blocks. This callback will only be used if the condition is `true`.
```ts
new MarkdownDocument()
.heading(1, `๐ก๏ธ Quality gate - ${passedCount}/${totalCount}`)
.quote(passedCount === totalCount && 'โ
Everything in order!')
// ๐ heading and list added if `passedCount < totalCount`, otherwise both skipped
.$if(passedCount < totalCount, doc =>
doc.heading(2, 'โ Failed checks').list(failedChecks?.map(md.code))
)
.paragraph(logsUrl && md.link(logsUrl, '๐ CI logs'))
.toString();
```
Optionally, you may provide another callback which will be used if the condition is `false` (think of it as the `else`-branch).
```ts
new MarkdownDocument()
.heading(1, `๐ก๏ธ Quality gate - ${passedCount}/${totalCount}`)
.$if(
passedCount === totalCount,
// ๐ quote added if `passedCount === totalCount` is true
doc => doc.quote('โ
Everything in order!'),
// ๐ heading and list added if `passedCount === totalCount` is false
doc => doc.heading(2, 'โ Failed checks').list(failedChecks?.map(md.code))
)
.paragraph(logsUrl && md.link(logsUrl, '๐ CI logs'))
.toString();
```
When it comes to iterative logic, then for individual blocks like lists and tables you can use the usual array methods (`.map`, `.filter`, etc.) to make the content dynamic. But if you need to generate multiple blocks per array item, the `$foreach` method comes in handy.
Provide an array for the 1st argument, and a callback for the 2nd. The callback function is called for each item in the array, and is expected to add blocks to the current `MarkdownDocument` instance.
```ts
function createMarkdownCommentForMonorepo(
projects: {
name: string;
totalCount: number;
passedCount: number;
logsUrl: string | null;
failedChecks?: string[];
}[]
): string {
return new MarkdownDocument()
.heading(1, `๐ก๏ธ Quality gate (${projects.length} projects)`)
.$foreach(
projects,
(doc, { name, totalCount, passedCount, logsUrl, failedChecks }) =>
doc
.heading(2, `๐ผ ${name} - ${passedCount}/${totalCount}`)
.$if(
passedCount === totalCount,
doc => doc.quote('โ
Everything in order!'),
doc =>
doc
.heading(3, 'โ Failed checks')
.list(failedChecks?.map(md.code))
)
.paragraph(logsUrl && md.link(logsUrl, '๐ CI logs'))
)
.toString();
}
```
#### ๐ง Immutable vs mutable
By default, instances of `MarkdownDocument` are immutable. Methods for appending document blocks return a new instance, leaving the original instance unaffected.
```ts
// ๐ `extendedDocument` has additional blocks, `baseDocument` unmodified
const extendedDocument = baseDocument
.rule()
.paragraph(md`Made with โค๏ธ by ${md.link(OWNER_LINK, OWNER_NAME)}`);
```
This is an intentional design decision to encourage building Markdown documents declaratively, instead of an imperative approach using `if`/`else` branches, `for` loops, etc.
However, if you prefer to write your logic imperatively, then you have the option of setting `mutable: true` when instantiating a document.
```ts
function createMarkdownCommentForMonorepo(
projects: {
name: string;
totalCount: number;
passedCount: number;
logsUrl: string | null;
failedChecks?: string[];
}[]
): string {
// ๐ all method calls will mutate document
const doc = new MarkdownDocument({ mutable: true });
// ๐ ignoring return value would have no effect in immutable mode
doc.heading(1, `๐ก๏ธ Quality gate (${projects.length} projects)`);
// ๐ imperative loops work because of side-effects
for (const project of projects) {
const { name, totalCount, passedCount, logsUrl, failedChecks } = project;
doc.heading(2, `๐ผ ${name} - ${passedCount}/${totalCount}`);
// ๐ imperative conditions work because of side-effects
if (passedCount === totalCount) {
doc.quote('โ
Everything in order!');
} else {
doc.heading(3, 'โ Failed checks').list(failedChecks?.map(md.code));
}
if (logsUrl) {
doc.paragraph(md.link(logsUrl, '๐ CI logs'));
}
}
return doc.toString();
}
```
### ๐ช Composing documents
When building complex documents, extracting some sections to other functions helps keep the code more mantainable. This is where the [`$concat`](https://matejchalk.github.io/build-md/classes/MarkdownDocument.html#_concat) method comes in useful. It accepts one or more other documents and appends their blocks to the current document. This makes it convenient to break up pieces of builder logic into functions, as well as making sections of documents easily reusable.
```ts
function createMarkdownComment(
totalCount: number,
passedCount: number,
logsUrl: string | null,
failedChecks?: string[]
): string {
return new MarkdownDocument()
.$concat(
// ๐ adds heading and quote from other document
createMarkdownCommentSummary(totalCount, passedCount),
// ๐ adds heading, list and paragraph from other document
createMarkdownCommentDetails(logsUrl, failedChecks)
)
.toString();
}
function createMarkdownCommentSummary(
totalCount: number,
passedCount: number
): MarkdownDocument {
return new MarkdownDocument()
.heading(1, `๐ก๏ธ Quality gate - ${passedCount}/${totalCount}`)
.quote(passedCount === totalCount && 'โ
Everything in order!');
}
function createMarkdownCommentDetails(
logsUrl: string | null,
failedChecks?: string[]
): MarkdownDocument {
return new MarkdownDocument()
.heading(2, failedChecks?.length && 'โ Failed checks')
.list(failedChecks?.map(md.code))
.paragraph(logsUrl && md.link(logsUrl, '๐ CI logs'));
}
```
### ๐ Inline formatting
The `md` tagged template literal is for composing text which includes Markdown elements.
It provides an intuitive syntax for adding inline formatting, as well as embedding nested blocks within top-level document blocks.
Its output is embeddable into all elements (with a few logical exceptions like code blocks), so it acts as the glue for building documents with a complex hierarchy.
It also comes in handy when you don't want to render a full document, but only need a one-line Markdown string. Just like for the `MarkdownDocument` class, calling `.toString()` returns the converted Markdown text.
```ts
md`${md.bold(severity)} severity vulnerability in ${md.code(name)}`.toString();
```
## ๐ค Contributing
- Prerequisite is having Node.js installed.
- Install dev dependencies with `npm install`.
- Run tests with `npm test` or `npm run test:watch` (uses [Vitest](https://vitest.dev/)).
- Generate documentation with `npm run docs` (uses [TypeDoc](https://typedoc.org/)).
- Compile TypeScript sources with `npm run build` (uses [tsup](https://tsup.egoist.dev/)).
- Use [Conventional Commits](https://www.conventionalcommits.org/) prompts with `npm run commit`.