https://github.com/privatenumber/md-pen
Utilities for formatting Markdown
https://github.com/privatenumber/md-pen
format gfm markdown table utilities utils
Last synced: 19 days ago
JSON representation
Utilities for formatting Markdown
- Host: GitHub
- URL: https://github.com/privatenumber/md-pen
- Owner: privatenumber
- License: mit
- Created: 2026-03-30T04:59:40.000Z (3 months ago)
- Default Branch: develop
- Last Pushed: 2026-05-01T07:58:08.000Z (about 2 months ago)
- Last Synced: 2026-05-06T11:49:13.006Z (about 2 months ago)
- Topics: format, gfm, markdown, table, utilities, utils
- Language: TypeScript
- Homepage:
- Size: 846 KB
- Stars: 42
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
md-pen
Typed utilities for formatting Markdown. GFM (GitHub-Flavored Markdown) first.
### Features
- Zero dependencies
- Full TypeScript with exported types
- Every output verified against a CommonMark parser
- GitHub-compatible (audited against [cmark-gfm](https://github.com/github/cmark-gfm))
- Functions compose naturally
## Install
```sh
npm install md-pen
```
## Usage
```ts
import {
bold, code, link, table
} from 'md-pen'
bold('important') // __important__
code('git status') // `git status`
link('https://github.com', 'GitHub') // [GitHub](https://github.com)
table([
['Name', 'Age'],
['Alice', '30']
])
// | Name | Age |
// | - | - |
// | Alice | 30 |
```
A default export is also available: `import md from 'md-pen'` then `md.bold()`, `md.table()`, etc.
## API
### Inline
#### `code(text)`
Wraps in backtick code span. Handles backticks in content automatically.
```ts
code('hello') // `hello`
code('a `b` c') // `` a `b` c ``
```
#### `bold(text)`
```ts
bold('important') // __important__
```
#### `italic(text)`
```ts
italic('emphasis') // *emphasis*
```
#### `strikethrough(text)`
```ts
strikethrough('no') // ~~no~~
```
#### `link(url, text?, options?)`
Markdown by default. Falls back to HTML when options go beyond what markdown supports.
> [!NOTE]
> `link(url, text)` and `image(url, alt)` take URL first — matches HTML's `text`, not Markdown's `[text](url)`.
```ts
link('https://x.com') //
link('/docs/guide') // [/docs/guide](/docs/guide)
link('https://x.com', 'click') // [click](https://x.com)
link('https://x.com', 'click', { title: 'T' }) // [click](https://x.com "T")
// HTML fallback for attributes markdown can't express
link('https://x.com', 'click', { target: '_blank' })
// click
```
#### `image(url, alt?, options?)`
Same fallback principle as `link`.
```ts
image('cat.png') // 
image('cat.png', 'A cat') // 
image('cat.png', 'A cat', { title: 'T' }) // 
// HTML fallback for width/height
image('cat.png', 'A cat', {
width: 200,
height: 100
})
//
```
### Block
#### `heading(text, level?)` / `h1`-`h6`
```ts
heading('Title') // # Title
heading('Sub', 2) // ## Sub
h1('Title') // # Title
h3('Section') // ### Section
```
#### `blockquote(text)`
Prefixes each line with `> `.
```ts
blockquote('line 1\nline 2')
// > line 1
// > line 2
```
#### `codeBlock(code, language?)`
Fenced code block. Handles content containing backtick fences.
```ts
codeBlock('const x = 1', 'ts')
// ```ts
// const x = 1
// ```
```
#### `table(rows, options?)`
First row is the header. Cells auto-stringify numbers and booleans. Ragged rows are padded.
```ts
table([
['Name', 'Age'],
['Alice', 30]
], { align: ['left', 'right'] })
// | Name | Age |
// | :- | -: |
// | Alice | 30 |
```
Also accepts an array of objects (keys become headers):
```ts
table([
{
name: 'Alice',
age: 30
},
{
name: 'Bob',
age: 25
}
])
// | name | age |
// | - | - |
// | Alice | 30 |
// | Bob | 25 |
```
Use `columns` to control order, filter, and rename. Each entry is a key or `[key, header]` tuple:
```ts
table([
{
firstName: 'Alice',
age: 30,
id: 1
}
], {
columns: [['firstName', 'Name'], 'age']
})
// | Name | age |
// | - | - |
// | Alice | 30 |
```
Use `html: true` for block content in cells (code blocks, lists, etc.):
```ts
table([
['Before', 'After'],
[codeBlock('old()', 'js'), codeBlock('updated()', 'js')]
], { html: true })
// Outputs an HTML with markdown-rendered cells
```
Alignment: `'left'`, `'center'`, `'right'`, `'none'`
> [!NOTE]
> In markdown mode, newlines become `
` and boundary whitespace becomes ` `, so those literal strings can't be represented in cells. Use `html: true` for exact content preservation.
#### `ul(items)` / `ol(items)`
Nested arrays become children of the preceding item.
```ts
ul(['a', 'b', ['nested 1', 'nested 2'], 'c'])
// - a
// - b
// - nested 1
// - nested 2
// - c
ol(['first', 'second', ['sub-a'], 'third'])
// 1. first
// 2. second
// 1. sub-a
// 3. third
```
#### `hr()`
```ts
hr() // ---
```
### GFM Extras
#### `taskList(items)`
```ts
taskList([
[true, 'Done'],
[false, 'Todo']
])
// - [x] Done
// - [ ] Todo
```
#### `alert(type, content)`
Types: `'note'`, `'tip'`, `'important'`, `'warning'`, `'caution'`
```ts
alert('warning', 'Be careful') // eslint-disable-line no-alert
// > [!WARNING]
// > Be careful
```
#### `footnoteRef(id)` / `footnote(id, text)`
```ts
footnoteRef('1') // [^1]
footnote('1', 'Source') // [^1]: Source
```
#### `details(summary, content, options?)`
Collapsible section. Summary is HTML-escaped, content supports markdown.
```ts
details('Click to expand', 'Hidden **markdown** here')
//
// Click to expand
//
// Hidden **markdown** here
//
//
details('Expanded', 'Visible', { open: '' })
//
// Expanded
//
// Visible
//
//
```
### Niche
#### `kbd(key, options?)`
```ts
kbd('Ctrl') // Ctrl
kbd('Enter', { title: 'Confirm' }) // Enter
```
#### `sub(text, options?)` / `sup(text, options?)`
```ts
sub('2') // 2
sup('n') // n
sub('2', { title: 'subscript' }) // 2
```
#### `math(expression)` / `mathBlock(expression)`
```ts
math('E = mc^2') // $E = mc^2$
mathBlock(String.raw`\sum_{i=1}^n x_i`)
// $$
// \sum_{i=1}^n x_i
// $$
```
> [!NOTE]
> Inline `math()` cannot render expressions ending with `\` (the backslash escapes the closing `$`). Use `mathBlock()` instead.
#### `mermaid(code)`
Sugar for `codeBlock(code, 'mermaid')`.
```ts
mermaid('graph TD;\n A-->B;')
// ```mermaid
// graph TD;
// A-->B;
// ```
```
#### `mention(username)` / `emoji(name)`
```ts
mention('octocat') // @octocat
emoji('rocket') // :rocket:
```
### Generic
#### `el(tag, attributes?, content?)`
Generic HTML element builder for tags without a dedicated function. Attribute values are HTML-escaped and attribute names are sanitized against injection. Content is raw (not escaped). Without content, produces a self-closing tag.
```ts
el('br') //
el('img', {
src: 'cat.png',
alt: 'A cat'
}) //
el('p', { align: 'center' }, 'centered text')
//
centered text
```
### Escaping
#### `escape(text)`
Escapes markdown special characters in untrusted input.
```ts
escape('**not bold**') // \*\*not bold\*\*
```
## Composition
Functions return plain strings. Bold uses `__` and italic uses `*`, so they compose without delimiter collision:
```ts
bold(italic('text'))
// __*text*__
bold(link('https://x.com', 'click'))
// __[click](https://x.com)__
blockquote(bold('important'))
// > __important__
ul([
link('https://a.com', 'Link A'),
link('https://b.com', 'Link B')
])
// - [Link A](https://a.com)
// - [Link B](https://b.com)
```
## Escaping Strategy
Each function escapes only what would break its own syntax:
- `table()` escapes `|` in cells, newlines become `
`
- `link()` / `image()` percent-encodes `(`, `)`, spaces, and control chars in URLs
- `code()` adjusts backtick delimiter length
- HTML functions (`kbd`, `sub`, `sup`, `details`) escape `<`, `>`, `&`, `"`
Composition works without double-escaping. Use `escape()` for untrusted user input.