{"id":50708466,"url":"https://github.com/privatenumber/md-pen","last_synced_at":"2026-06-09T13:31:53.470Z","repository":{"id":347950097,"uuid":"1195815420","full_name":"privatenumber/md-pen","owner":"privatenumber","description":"Utilities for formatting Markdown","archived":false,"fork":false,"pushed_at":"2026-05-01T07:58:08.000Z","size":866,"stargazers_count":42,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2026-05-06T11:49:13.006Z","etag":null,"topics":["format","gfm","markdown","table","utilities","utils"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/privatenumber.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"privatenumber"}},"created_at":"2026-03-30T04:59:40.000Z","updated_at":"2026-05-01T07:58:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/privatenumber/md-pen","commit_stats":null,"previous_names":["privatenumber/md-pen"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/privatenumber/md-pen","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fmd-pen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fmd-pen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fmd-pen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fmd-pen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/privatenumber","download_url":"https://codeload.github.com/privatenumber/md-pen/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/privatenumber%2Fmd-pen/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34110011,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-09T02:00:06.510Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["format","gfm","markdown","table","utilities","utils"],"created_at":"2026-06-09T13:31:52.690Z","updated_at":"2026-06-09T13:31:53.465Z","avatar_url":"https://github.com/privatenumber.png","language":"TypeScript","funding_links":["https://github.com/sponsors/privatenumber"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg width=\"200\" src=\".github/logo.webp\"\u003e\n\u003c/p\u003e\n\u003ch1 align=\"center\"\u003emd-pen\u003c/h1\u003e\n\nTyped utilities for formatting Markdown. GFM (GitHub-Flavored Markdown) first.\n\n### Features\n\n- Zero dependencies\n- Full TypeScript with exported types\n- Every output verified against a CommonMark parser\n- GitHub-compatible (audited against [cmark-gfm](https://github.com/github/cmark-gfm))\n- Functions compose naturally\n\n## Install\n\n```sh\nnpm install md-pen\n```\n\n## Usage\n\n```ts\nimport {\n    bold, code, link, table\n} from 'md-pen'\n\nbold('important') // __important__\ncode('git status') // `git status`\nlink('https://github.com', 'GitHub') // [GitHub](https://github.com)\n\ntable([\n    ['Name', 'Age'],\n    ['Alice', '30']\n])\n// | Name | Age |\n// | - | - |\n// | Alice | 30 |\n```\n\nA default export is also available: `import md from 'md-pen'` then `md.bold()`, `md.table()`, etc.\n\n## API\n\n### Inline\n\n#### `code(text)`\n\nWraps in backtick code span. Handles backticks in content automatically.\n\n```ts\ncode('hello') // `hello`\ncode('a `b` c') // `` a `b` c ``\n```\n\n#### `bold(text)`\n\n```ts\nbold('important') // __important__\n```\n\n#### `italic(text)`\n\n```ts\nitalic('emphasis') // *emphasis*\n```\n\n#### `strikethrough(text)`\n\n```ts\nstrikethrough('no') // ~~no~~\n```\n\n#### `link(url, text?, options?)`\n\nMarkdown by default. Falls back to HTML when options go beyond what markdown supports.\n\n\u003e [!NOTE]\n\u003e `link(url, text)` and `image(url, alt)` take URL first — matches HTML's `\u003ca href=\"url\"\u003etext\u003c/a\u003e`, not Markdown's `[text](url)`.\n\n```ts\nlink('https://x.com') // \u003chttps://x.com\u003e\nlink('/docs/guide') // [/docs/guide](/docs/guide)\nlink('https://x.com', 'click') // [click](https://x.com)\nlink('https://x.com', 'click', { title: 'T' }) // [click](https://x.com \"T\")\n\n// HTML fallback for attributes markdown can't express\nlink('https://x.com', 'click', { target: '_blank' })\n// \u003ca target=\"_blank\" href=\"https://x.com\"\u003eclick\u003c/a\u003e\n```\n\n#### `image(url, alt?, options?)`\n\nSame fallback principle as `link`.\n\n```ts\nimage('cat.png') // ![](cat.png)\nimage('cat.png', 'A cat') // ![A cat](cat.png)\nimage('cat.png', 'A cat', { title: 'T' }) // ![A cat](cat.png \"T\")\n\n// HTML fallback for width/height\nimage('cat.png', 'A cat', {\n    width: 200,\n    height: 100\n})\n// \u003cimg width=\"200\" height=\"100\" src=\"cat.png\" alt=\"A cat\" /\u003e\n```\n\n### Block\n\n#### `heading(text, level?)` / `h1`-`h6`\n\n```ts\nheading('Title') // # Title\nheading('Sub', 2) // ## Sub\nh1('Title') // # Title\nh3('Section') // ### Section\n```\n\n#### `blockquote(text)`\n\nPrefixes each line with `\u003e `.\n\n```ts\nblockquote('line 1\\nline 2')\n// \u003e line 1\n// \u003e line 2\n```\n\n#### `codeBlock(code, language?)`\n\nFenced code block. Handles content containing backtick fences.\n\n```ts\ncodeBlock('const x = 1', 'ts')\n// ```ts\n// const x = 1\n// ```\n```\n\n#### `table(rows, options?)`\n\nFirst row is the header. Cells auto-stringify numbers and booleans. Ragged rows are padded.\n\n```ts\ntable([\n    ['Name', 'Age'],\n    ['Alice', 30]\n], { align: ['left', 'right'] })\n// | Name | Age |\n// | :- | -: |\n// | Alice | 30 |\n```\n\nAlso accepts an array of objects (keys become headers):\n\n```ts\ntable([\n    {\n        name: 'Alice',\n        age: 30\n    },\n    {\n        name: 'Bob',\n        age: 25\n    }\n])\n// | name | age |\n// | - | - |\n// | Alice | 30 |\n// | Bob | 25 |\n```\n\nUse `columns` to control order, filter, and rename. Each entry is a key or `[key, header]` tuple:\n\n```ts\ntable([\n    {\n        firstName: 'Alice',\n        age: 30,\n        id: 1\n    }\n], {\n    columns: [['firstName', 'Name'], 'age']\n})\n// | Name | age |\n// | - | - |\n// | Alice | 30 |\n```\n\nUse `html: true` for block content in cells (code blocks, lists, etc.):\n\n```ts\ntable([\n    ['Before', 'After'],\n    [codeBlock('old()', 'js'), codeBlock('updated()', 'js')]\n], { html: true })\n// Outputs an HTML \u003ctable\u003e with markdown-rendered cells\n```\n\nAlignment: `'left'`, `'center'`, `'right'`, `'none'`\n\n\u003e [!NOTE]\n\u003e In markdown mode, newlines become `\u003cbr\u003e` and boundary whitespace becomes `\u0026nbsp;`, so those literal strings can't be represented in cells. Use `html: true` for exact content preservation.\n\n#### `ul(items)` / `ol(items)`\n\nNested arrays become children of the preceding item.\n\n```ts\nul(['a', 'b', ['nested 1', 'nested 2'], 'c'])\n// - a\n// - b\n//   - nested 1\n//   - nested 2\n// - c\n\nol(['first', 'second', ['sub-a'], 'third'])\n// 1. first\n// 2. second\n//    1. sub-a\n// 3. third\n```\n\n#### `hr()`\n\n```ts\nhr() // ---\n```\n\n### GFM Extras\n\n#### `taskList(items)`\n\n```ts\ntaskList([\n    [true, 'Done'],\n    [false, 'Todo']\n])\n// - [x] Done\n// - [ ] Todo\n```\n\n#### `alert(type, content)`\n\nTypes: `'note'`, `'tip'`, `'important'`, `'warning'`, `'caution'`\n\n```ts\nalert('warning', 'Be careful') // eslint-disable-line no-alert\n// \u003e [!WARNING]\n// \u003e Be careful\n```\n\n#### `footnoteRef(id)` / `footnote(id, text)`\n\n```ts\nfootnoteRef('1') // [^1]\nfootnote('1', 'Source') // [^1]: Source\n```\n\n#### `details(summary, content, options?)`\n\nCollapsible section. Summary is HTML-escaped, content supports markdown.\n\n```ts\ndetails('Click to expand', 'Hidden **markdown** here')\n// \u003cdetails\u003e\n// \u003csummary\u003eClick to expand\u003c/summary\u003e\n//\n// Hidden **markdown** here\n//\n// \u003c/details\u003e\n\ndetails('Expanded', 'Visible', { open: '' })\n// \u003cdetails open=\"\"\u003e\n// \u003csummary\u003eExpanded\u003c/summary\u003e\n//\n// Visible\n//\n// \u003c/details\u003e\n```\n\n### Niche\n\n#### `kbd(key, options?)`\n\n```ts\nkbd('Ctrl') // \u003ckbd\u003eCtrl\u003c/kbd\u003e\nkbd('Enter', { title: 'Confirm' }) // \u003ckbd title=\"Confirm\"\u003eEnter\u003c/kbd\u003e\n```\n\n#### `sub(text, options?)` / `sup(text, options?)`\n\n```ts\nsub('2') // \u003csub\u003e2\u003c/sub\u003e\nsup('n') // \u003csup\u003en\u003c/sup\u003e\nsub('2', { title: 'subscript' }) // \u003csub title=\"subscript\"\u003e2\u003c/sub\u003e\n```\n\n#### `math(expression)` / `mathBlock(expression)`\n\n```ts\nmath('E = mc^2') // $E = mc^2$\n\nmathBlock(String.raw`\\sum_{i=1}^n x_i`)\n// $$\n// \\sum_{i=1}^n x_i\n// $$\n```\n\n\u003e [!NOTE]\n\u003e Inline `math()` cannot render expressions ending with `\\` (the backslash escapes the closing `$`). Use `mathBlock()` instead.\n\n#### `mermaid(code)`\n\nSugar for `codeBlock(code, 'mermaid')`.\n\n```ts\nmermaid('graph TD;\\n  A--\u003eB;')\n// ```mermaid\n// graph TD;\n//   A--\u003eB;\n// ```\n```\n\n#### `mention(username)` / `emoji(name)`\n\n```ts\nmention('octocat') // @octocat\nemoji('rocket') // :rocket:\n```\n\n### Generic\n\n#### `el(tag, attributes?, content?)`\n\nGeneric 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.\n\n```ts\nel('br') // \u003cbr /\u003e\nel('img', {\n    src: 'cat.png',\n    alt: 'A cat'\n}) // \u003cimg src=\"cat.png\" alt=\"A cat\" /\u003e\nel('p', { align: 'center' }, 'centered text')\n// \u003cp align=\"center\"\u003ecentered text\u003c/p\u003e\n```\n\n### Escaping\n\n#### `escape(text)`\n\nEscapes markdown special characters in untrusted input.\n\n```ts\nescape('**not bold**') // \\*\\*not bold\\*\\*\n```\n\n## Composition\n\nFunctions return plain strings. Bold uses `__` and italic uses `*`, so they compose without delimiter collision:\n\n```ts\nbold(italic('text'))\n// __*text*__\n\nbold(link('https://x.com', 'click'))\n// __[click](https://x.com)__\n\nblockquote(bold('important'))\n// \u003e __important__\n\nul([\n    link('https://a.com', 'Link A'),\n    link('https://b.com', 'Link B')\n])\n// - [Link A](https://a.com)\n// - [Link B](https://b.com)\n```\n\n## Escaping Strategy\n\nEach function escapes only what would break its own syntax:\n\n- `table()` escapes `|` in cells, newlines become `\u003cbr\u003e`\n- `link()` / `image()` percent-encodes `(`, `)`, spaces, and control chars in URLs\n- `code()` adjusts backtick delimiter length\n- HTML functions (`kbd`, `sub`, `sup`, `details`) escape `\u003c`, `\u003e`, `\u0026`, `\"`\n\nComposition works without double-escaping. Use `escape()` for untrusted user input.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprivatenumber%2Fmd-pen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprivatenumber%2Fmd-pen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprivatenumber%2Fmd-pen/lists"}