🚀 Big News:Socket Has Acquired Secure Annex.Learn More
Socket
Book a DemoSign in
Socket

@charlie-labs/format-for

Package Overview
Dependencies
Maintainers
2
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@charlie-labs/format-for

[![CI](https://github.com/charlie-labs/format-for/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/charlie-labs/format-for/actions/workflows/ci.yml) [![Bun](https://img.shields.io/badge/bun-1.x-000)](https://bun.sh)

latest
Source
npmnpm
Version
0.2.0
Version published
Weekly downloads
2.1K
-33.38%
Maintainers
2
Weekly downloads
 
Created
Source

format-for

CI Bun

One Markdown input → clean output for GitHub, Slack, or Linear.

You don’t need to know the input’s dialect. Pass Markdown that might mix Linear fences, Slack <url|label> links/mentions, and GFM. format‑for parses once and renders target‑aware output with predictable, safe degradations and explicit warnings.

Contents

Install

This package is ESM‑first with a CJS fallback and works in Node 22+ and Bun 1.2+.

bun add @charlie-labs/format-for
# or
npm i @charlie-labs/format-for
# or
yarn add @charlie-labs/format-for
# or
pnpm add @charlie-labs/format-for

Quick start

import { formatFor } from '@charlie-labs/format-for';

const md = `
+++ Summary

Collapsible content

+++

See @riley in #dev and <https://example.com|site>.
`;

const gh = await formatFor.github(md);
const slack = await formatFor.slack(md);
const linear = await formatFor.linear(md);

Outputs for the snippet above (exact values):

GitHub

<details>
<summary>Summary</summary>

Collapsible content

</details>

See @riley in #dev and [site](https://example.com).

Slack

*Summary*
> Collapsible content
See @riley in #dev and <https://example.com|site>.

Linear

+++ Summary

Collapsible content

+++

See @riley in #dev and [site](https://example.com).

Prefer to inject live Slack/Linear defaults (real users/channels, org/team autolinks)? Use the factory:

import {
  createFormatFor,
  createEnvDefaultsProvider,
} from '@charlie-labs/format-for';

const ff = createFormatFor({
  defaults: createEnvDefaultsProvider({
    // optional: override cache namespace and token/TTL; when not provided,
    // SLACK_BOT_TOKEN and LINEAR_API_KEY env vars are used by default
    // namespace: 'my-app:format-for:v1',
    // slack: { token: 'xoxb-…', ttlMs: 10 * 60_000 },
    // linear: { apiKey: 'lin_api_…', ttlMs: 60 * 60_000 },
  }),
});

const out = await ff.slack('Ping @riley in #dev');

Example: one input → three outputs

Below is a realistic mixed‑syntax input (taken from our test fixtures), followed by the exact strings returned for each target.

Input (Markdown, mixed Slack/Linear/GFM)
# Project Alpha: Auth flow update (fixture input)

Short summary: We are migrating the auth callback. FYI <!here> see <https://charlie-labs.slack.com/archives/C12345/p1726800000000|auth-discussion>. Ping <@U02AAAAAA> and <#C02OPS|ops>. Old flow ~deprecated~.

+++ Decisions

- Keep email-first login; remove ~magic-link-only~ path.
- Links: Markdown [spec](https://spec.commonmark.org) and Slack form <https://example.com|Docs>.
- Include a bare URL too: <https://example.org>.

+++ Edge cases

- Safari ITP and cookies.
- If user is SSO-only, show a link back.
- Mention special <!channel> to alert during rollout.

+++

- Table and images should still render cross-platform.

+++

## Tasks

1. Backend
   - Add POST /v2/auth/verify
     - Increase rate limit from 30 to 60
2. Frontend
   - Update callback handler
   - Add retry UI

> Note: rollout starts Monday. Coordinate with #release and @riley.

### Code sample

```ts
// `+++` inside code should NOT create a details block.
export function normalizeEmail(s: string): string {
  return s.trim().toLowerCase();
}
```

### Cases

| Case        | Expected |
| ----------- | -------- |
| Valid email | 200      |
| Bad token   | 401      |

![diagram](https://example.com/flow.png)

HTML allowed tags: <u>Important</u> and <sup>2</sup>.<br>
HTML disallowed inline in a paragraph (to exercise Linear's allowlist): <video src="/noop"></video>

<div>Standalone HTML block that should be stripped on Slack/Linear</div>

Footnote ref[^1].

[^1]: This is a footnote with a Slack user <@U0FOOT> and a Slack link <https://ex.com|ex>.
GitHub output (exact value)
# Project Alpha: Auth flow update (fixture input)

Short summary: We are migrating the auth callback. FYI @here see [auth-discussion](https://charlie-labs.slack.com/archives/C12345/p1726800000000). Ping @U02AAAAAA and #ops. Old flow ~~deprecated~~.

<details>
<summary>Decisions</summary>

- Keep email-first login; remove ~~magic-link-only~~ path.
- Links: Markdown [spec](https://spec.commonmark.org) and Slack form [Docs](https://example.com).
- Include a bare URL too: <https://example.org>.

<details>
<summary>Edge cases</summary>

- Safari ITP and cookies.
- If user is SSO-only, show a link back.
- Mention special @channel to alert during rollout.
</details>

- Table and images should still render cross-platform.
</details>

## Tasks

1. Backend
   - Add POST /v2/auth/verify
     - Increase rate limit from 30 to 60
2. Frontend
   - Update callback handler
   - Add retry UI

> Note: rollout starts Monday. Coordinate with #release and @riley.

### Code sample

```ts
// `+++` inside code should NOT create a details block.
export function normalizeEmail(s: string): string {
  return s.trim().toLowerCase();
}
```

### Cases

| Case        | Expected |
| ----------- | -------- |
| Valid email | 200      |
| Bad token   | 401      |

![diagram](https://example.com/flow.png)

HTML allowed tags: <u>Important</u> and <sup>2</sup>.<br>
HTML disallowed inline in a paragraph (to exercise Linear's allowlist): <video src="/noop"></video>

<div>Standalone HTML block that should be stripped on Slack/Linear</div>

Footnote ref[^1].

[^1]: This is a footnote with a Slack user @U0FOOT and a Slack link [ex](https://ex.com).
Linear output (exact value)
# Project Alpha: Auth flow update (fixture input)

Short summary: We are migrating the auth callback. FYI @here see [auth-discussion](https://charlie-labs.slack.com/archives/C12345/p1726800000000). Ping @U02AAAAAA and #ops. Old flow ~~deprecated~~.

+++ Decisions

- Keep email-first login; remove ~~magic-link-only~~ path.
- Links: Markdown [spec](https://spec.commonmark.org) and Slack form [Docs](https://example.com).
- Include a bare URL too: <https://example.org>.

+++ Edge cases

- Safari ITP and cookies.
- If user is SSO-only, show a link back.
- Mention special @channel to alert during rollout.

+++

- Table and images should still render cross-platform.

+++

## Tasks

1. Backend
   - Add POST /v2/auth/verify
     - Increase rate limit from 30 to 60
2. Frontend
   - Update callback handler
   - Add retry UI

> Note: rollout starts Monday. Coordinate with #release and @riley.

### Code sample

```ts
// `+++` inside code should NOT create a details block.
export function normalizeEmail(s: string): string {
  return s.trim().toLowerCase();
}
```

### Cases

| Case        | Expected |
| ----------- | -------- |
| Valid email | 200      |
| Bad token   | 401      |

![diagram](https://example.com/flow.png)

HTML allowed tags: <u>Important</u> and <sup>2</sup>.<br>
HTML disallowed inline in a paragraph (to exercise Linear's allowlist):

Footnote ref[^1].

[^1]: This is a footnote with a Slack user @U0FOOT and a Slack link [ex](https://ex.com).
Slack output (exact value)

_Project Alpha: Auth flow update (fixture input)_

Short summary: We are migrating the auth callback. FYI <!here> see <https://charlie-labs.slack.com/archives/C12345/p1726800000000|auth-discussion>. Ping <@U02AAAAAA> and <#C02OPS|ops>. Old flow ~deprecated~.

_Decisions_

> • Keep email-first login; remove ~magic-link-only~ path.
> • Links: Markdown <https://spec.commonmark.org|spec> and Slack form <https://example.com|Docs>.
> • Include a bare URL too: <https://example.org|https://example.org>.
>
> _Edge cases_
>
> > • Safari ITP and cookies.
> > • If user is SSO-only, show a link back.
> > • Mention special <!channel> to alert during rollout.
> > • Table and images should still render cross-platform.
> > _Tasks_

1. Backend
   • Add POST /v2/auth/verify
   → Increase rate limit from 30 to 60

2. Frontend
   • Update callback handler
   • Add retry UI

> Note: rollout starts Monday. Coordinate with #release and @riley.

_Code sample_

```
// `+++` inside code should NOT create a details block.
export function normalizeEmail(s: string): string {
  return s.trim().toLowerCase();
}
```

_Cases_

```
Case | Expected
Valid email | 200
Bad token | 401
```

<https://example.com/flow.png|diagram>

HTML allowed tags: &lt;u&gt;Important&lt;/u&gt; and &lt;sup&gt;2&lt;/sup&gt;.&lt;br&gt;
HTML disallowed inline in a paragraph (to exercise Linear's allowlist): &lt;video src="/noop"&gt;&lt;/video&gt;

Footnote ref^[1].

Footnotes:
[1] This is a footnote with a Slack user <@U0FOOT> and a Slack link <https://ex.com|ex>.

Concepts

  • Parse once → canonical mdast → render per‑target.
  • Predictable degradations with explicit warnings (e.g., Slack tables → code blocks; math → code; images → links; Linear strips disallowed HTML).
  • Idempotent output per target; code/inline code is never changed by autolinks or formatting.
  • Autolinks and mentions are deterministic and local by default; optional env‑backed defaults provider hydrates Slack users/channels and Linear org/team/user data when tokens are present.

API

formatFor

  • formatFor.github(input, options?)
  • formatFor.slack(input, options?)
  • formatFor.linear(input, options?)

Each returns a Promise<string> with the formatted value for that target.

Factory and env defaults

import {
  createFormatFor,
  createEnvDefaultsProvider,
} from '@charlie-labs/format-for';

const ff = createFormatFor({
  defaults: createEnvDefaultsProvider({
    // optional: override token/TTL/namespace; env fallbacks used by default
    // slack: { token: 'xoxb-…', ttlMs: 10 * 60_000 },
    // linear: { apiKey: 'lin_api_…', ttlMs: 60 * 60_000 },
  }),
});

const out = await ff.github('Ref ENG-123 and say hi to @riley');

FormatOptions (v1)

type FormatOptions = {
  maps?: {
    slack?: {
      users?: Record<string, { id: string; label?: string }>;
      channels?: Record<string, { id: string; label?: string }>;
    };
    linear?: { users?: Record<string, { url: string; label?: string }> };
  };
  autolinks?: Partial<
    Record<
      'github' | 'slack' | 'linear',
      Array<{
        pattern: RegExp;
        urlTemplate: string;
        labelTemplate?: string;
      }>
    >
  >;
  warnings?: {
    mode?: 'console' | 'silent';
    onWarn?: (message: string) => void;
  };
  target?: {
    slack?: {
      lists?: { maxDepth?: number }; // default: 2
      images?: { style?: 'link' | 'url'; emptyAltLabel?: string }; // defaults: style 'link'; emptyAltLabel 'image'
    };
    github?: { breaks?: 'two-spaces' | 'backslash' }; // default: 'two-spaces'
    // Linear options are intentionally not exposed in v1
  };
};

Notes

  • Autolinks are normalized to global regex; when provider + caller rules collide, the caller wins.
  • Linear’s HTML allowlist is fixed internally: details, summary, u, sub, sup, br.

Target behavior highlights

  • GitHub
    • details nodes render as <details><summary>…</summary>…</details> HTML.
    • Hard breaks use two spaces by default (configurable to backslash).
    • Preserves task list state on bare marker lines.
  • Slack
    • Headings become bold lines; quotes have readable spacing.
    • Lists deeper than maxDepth flatten with a single warning per render.
    • Images emit as <url|label> links (or bare URLs) with a warning; style is configurable.
    • Tables → fenced code with a warning; inline/display math → code/code blocks with warnings.
    • Footnotes become ^[n] plus appended refs; all HTML stripped with a warning.
  • Linear
    • +++ Summary → collapsible; disallowed HTML is stripped while keeping surrounding text.
    • Slack/Linear mentions normalize to links/plain text as appropriate.

Recipes

  • Autolink Linear issues (multiple team keys):

    const rules = [
      {
        pattern: /\b(BOT-\d+)\b/g,
        urlTemplate: 'https://linear.app/charlie/issue/$0',
      },
      {
        pattern: /\b(ENG-\d+)\b/g,
        urlTemplate: 'https://linear.app/charlie/issue/$0',
      },
    ];
    await formatFor.github(text, {
      autolinks: { github: rules, linear: rules, slack: rules },
    });
    
  • Route warnings to your logger and silence console output:

    await formatFor.slack(md, {
      warnings: { mode: 'silent', onWarn: (m) => log.warn(m) },
    });
    
  • Preserve GitHub backslash hard breaks:

    await formatFor.github(md, { target: { github: { breaks: 'backslash' } } });
    
  • Use live Slack/Linear defaults (if tokens exist):

    const ff = createFormatFor({ defaults: createEnvDefaultsProvider() });
    const text = await ff.slack('See @riley in #dev');
    

Warnings and safety

  • Slack: HTML stripped; images/tables/math/footnotes degrade with clear warnings; link labels are sanitized.
  • Linear: strict inline HTML allowlist; disallowed tags are removed and paragraphs preserved; warnings are emitted.
  • Code blocks and inline code are never altered by autolinks or formatting passes.

Control warnings with warnings.mode and warnings.onWarn.

Performance and idempotency

  • One parse; lightweight renderers; no network calls unless you opt in via the factory.
  • Formatting is idempotent for a given target (running twice yields the same string).

Contributing

Dev commands:

bun install
bun run typecheck
bun run lint
bun run test

Fixtures live under src/markdown/__tests__/__fixtures__/. To regenerate example outputs locally, run: bun scripts/gen-fixtures.ts.

License

MIT

Keywords

markdown

FAQs

Package last updated on 27 Sep 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts