md-pen
v1.2.0
Published
Utilities for formatting Markdown
Maintainers
Readme
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)
- Functions compose naturally
Install
npm install md-penUsage
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.
code('hello') // `hello`
code('a `b` c') // `` a `b` c ``bold(text)
bold('important') // __important__italic(text)
italic('emphasis') // *emphasis*strikethrough(text)
strikethrough('no') // ~~no~~link(url, text?, options?)
Markdown by default. Falls back to HTML when options go beyond what markdown supports.
link('https://x.com') // <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' })
// <a target="_blank" href="https://x.com">click</a>image(url, alt?, options?)
Same fallback principle as link.
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
})
// <img width="200" height="100" src="cat.png" alt="A cat" />Block
heading(text, level?) / h1-h6
heading('Title') // # Title
heading('Sub', 2) // ## Sub
h1('Title') // # Title
h3('Section') // ### Sectionblockquote(text)
Prefixes each line with > .
blockquote('line 1\nline 2')
// > line 1
// > line 2codeBlock(code, language?)
Fenced code block. Handles content containing backtick fences.
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.
table([
['Name', 'Age'],
['Alice', 30]
], { align: ['left', 'right'] })
// | Name | Age |
// | :- | -: |
// | Alice | 30 |Also accepts an array of objects (keys become headers):
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:
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.):
table([
['Before', 'After'],
[codeBlock('old()', 'js'), codeBlock('updated()', 'js')]
], { html: true })
// Outputs an HTML <table> with markdown-rendered cellsAlignment: 'left', 'center', 'right', 'none'
[!NOTE] In markdown mode, newlines become
<br>and boundary whitespace becomes , so those literal strings can't be represented in cells. Usehtml: truefor exact content preservation.
ul(items) / ol(items)
Nested arrays become children of the preceding item.
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. thirdhr()
hr() // ---GFM Extras
taskList(items)
taskList([
[true, 'Done'],
[false, 'Todo']
])
// - [x] Done
// - [ ] Todoalert(type, content)
Types: 'note', 'tip', 'important', 'warning', 'caution'
alert('warning', 'Be careful') // eslint-disable-line no-alert
// > [!WARNING]
// > Be carefulfootnoteRef(id) / footnote(id, text)
footnoteRef('1') // [^1]
footnote('1', 'Source') // [^1]: Sourcedetails(summary, content, options?)
Collapsible section. Summary is HTML-escaped, content supports markdown.
details('Click to expand', 'Hidden **markdown** here')
// <details>
// <summary>Click to expand</summary>
//
// Hidden **markdown** here
//
// </details>
details('Expanded', 'Visible', { open: '' })
// <details open="">
// <summary>Expanded</summary>
//
// Visible
//
// </details>Niche
kbd(key, options?)
kbd('Ctrl') // <kbd>Ctrl</kbd>
kbd('Enter', { title: 'Confirm' }) // <kbd title="Confirm">Enter</kbd>sub(text, options?) / sup(text, options?)
sub('2') // <sub>2</sub>
sup('n') // <sup>n</sup>
sub('2', { title: 'subscript' }) // <sub title="subscript">2</sub>math(expression) / mathBlock(expression)
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$). UsemathBlock()instead.
mermaid(code)
Sugar for codeBlock(code, 'mermaid').
mermaid('graph TD;\n A-->B;')
// ```mermaid
// graph TD;
// A-->B;
// ```mention(username) / emoji(name)
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.
el('br') // <br />
el('img', {
src: 'cat.png',
alt: 'A cat'
}) // <img src="cat.png" alt="A cat" />
el('p', { align: 'center' }, 'centered text')
// <p align="center">centered text</p>Escaping
escape(text)
Escapes markdown special characters in untrusted input.
escape('**not bold**') // \*\*not bold\*\*Composition
Functions return plain strings. Bold uses __ and italic uses *, so they compose without delimiter collision:
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<br>link()/image()percent-encodes(,), spaces, and control chars in URLscode()adjusts backtick delimiter length- HTML functions (
kbd,sub,sup,details) escape<,>,&,"
Composition works without double-escaping. Use escape() for untrusted user input.
