{"_id":"@charlie-labs/format-for","_rev":"7-a892bd80a8a18f4a85792c0c6dcc19d8","name":"@charlie-labs/format-for","dist-tags":{"latest":"0.2.0"},"versions":{"0.0.4":{"name":"@charlie-labs/format-for","version":"0.0.4","license":"MIT","_id":"@charlie-labs/format-for@0.0.4","maintainers":[{"name":"rileytomasek","email":"riley.tomasek@gmail.com"},{"name":"ryan-charlielabs","email":"ryan@charlielabs.ai"}],"homepage":"https://github.com/charlie-labs/format-for","bugs":{"url":"https://github.com/charlie-labs/format-for/issues"},"dist":{"shasum":"ecbe571bba7f3a741971e5a97bde7602e00b77b3","tarball":"https://registry.npmjs.org/@charlie-labs/format-for/-/format-for-0.0.4.tgz","fileCount":71,"integrity":"sha512-k+gW8S5sxm+qrwC9rPQqr3nwPMATGuHU/76IWih+KhNPgLVX8ijjCJecfDAV2Y3GNBGwxi48zM86bU8OoiC/xQ==","signatures":[{"sig":"MEUCIArZZ3S3rflKHHdTr9gLdTiTIMy0iKrMLDqvKje6beXnAiEAy9y0N5O4VOhCTv2+N3MPLJvCkTbRDpwskVRDAXDTNoU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":180720},"main":"./dist/index.cjs","type":"module","zshy":"./src/index.ts","types":"./dist/index.d.cts","module":"./dist/index.js","engines":{"bun":"1"},"exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"cc1dff9cb1a959e349a2e5fdf8599d58bc9f56f2","scripts":{"ci":"bun run typecheck && bun run lint && bun run test","fix":"eslint --fix --quiet . ; prettier --write --log-level=silent .","lint":"eslint . && prettier --check .","test":"vitest run --reporter=dot --coverage","build":"bun run --bun zshy","clean":"rm -rf node_modules","prepare":"husky","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"bun run build"},"_npmUser":{"name":"rileytomasek","email":"riley.tomasek@gmail.com"},"prettier":"@charlie-labs/config/prettier","repository":{"url":"git+https://github.com/charlie-labs/format-for.git","type":"git"},"_npmVersion":"10.9.3","description":"[![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)","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx}":["eslint --fix","prettier --write"],"*.{json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"22.19.0","dependencies":{"unified":"^11.0.5","remark-gfm":"^4.0.1","remark-parse":"^11.0.0","remark-stringify":"^11.0.0","unist-util-visit":"^5.0.0","mdast-util-to-string":"^4.0.0","unist-util-visit-parents":"^6.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.22","devDependencies":{"knip":"^5.64.0","zshy":"^0.4.2","husky":"^9.1.7","eslint":"^9.36.0","vitest":"^3.2.4","prettier":"^3.6.2","@types/bun":"^1.2.22","fast-check":"^4.3.0","typescript":"^5.9.2","lint-staged":"^16.2.0","@types/mdast":"^4.0.4","@types/unist":"^3.0.3","@vitest/coverage-v8":"^3.2.4","@charlie-labs/config":"^0.0.15"},"_npmOperationalInternal":{"tmp":"tmp/format-for_0.0.4_1758664115670_0.04505904826724283","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@charlie-labs/format-for","version":"0.1.0","keywords":["markdown","formatter","github","slack","linear","unified","remark","mdast","markdown-to-slack","markdown-to-github"],"license":"MIT","_id":"@charlie-labs/format-for@0.1.0","maintainers":[{"name":"rileytomasek","email":"riley.tomasek@gmail.com"},{"name":"ryan-charlielabs","email":"ryan@charlielabs.ai"}],"homepage":"https://github.com/charlie-labs/format-for","bugs":{"url":"https://github.com/charlie-labs/format-for/issues"},"dist":{"shasum":"90684f8e54d9e66b044a5e1fdfac05a13f2fa680","tarball":"https://registry.npmjs.org/@charlie-labs/format-for/-/format-for-0.1.0.tgz","fileCount":71,"integrity":"sha512-Pt6S1pUBMAamHLC6XdX81J3GBmFWcW+i3TcqCtqnafEJOvg4tYxDfyfiGvQg44q6tes4GtTvPSvTXhFSCOoUtw==","signatures":[{"sig":"MEYCIQDhpvd1OiQFtVzYrRUjBw3qE9ODlZIzpqXUO1/yiXBpJgIhAIel2OkLpATHSA92BRkzTOYMaWC5wVZ4TwXIQ+oc+zUS","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@charlie-labs%2fformat-for@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":190121},"main":"./dist/index.cjs","type":"module","zshy":"./src/index.ts","types":"./dist/index.d.cts","module":"./dist/index.js","engines":{"bun":">=1.2","node":">=22"},"exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"00a1b645cd074c416c13bdfcea84e113e7388ddb","scripts":{"ci":"bun run typecheck && bun run lint && bun run test","fix":"eslint --fix --quiet . ; prettier --write --log-level=silent .","lint":"eslint . && prettier --check .","test":"vitest run --reporter=dot --coverage","build":"bun run --bun zshy","clean":"rm -rf node_modules","prepare":"husky","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"bun run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:40ff7faa-f80a-43af-9e41-53a3206a76c7"}},"prettier":"@charlie-labs/config/prettier","repository":{"url":"git+https://github.com/charlie-labs/format-for.git","type":"git"},"_npmVersion":"11.6.0","description":"[![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)","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx}":["eslint --fix","prettier --write"],"*.{json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.8.0","dependencies":{"unified":"^11.0.5","remark-gfm":"^4.0.1","remark-parse":"^11.0.0","remark-stringify":"^11.0.0","unist-util-visit":"^5.0.0","mdast-util-to-string":"^4.0.0","unist-util-visit-parents":"^6.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.22","devDependencies":{"knip":"^5.64.0","zshy":"^0.4.2","husky":"^9.1.7","eslint":"^9.36.0","vitest":"^3.2.4","prettier":"^3.6.2","@types/bun":"^1.2.22","fast-check":"^4.3.0","typescript":"^5.9.2","lint-staged":"^16.2.0","@types/mdast":"^4.0.4","@types/unist":"^3.0.3","@vitest/coverage-v8":"^3.2.4","@charlie-labs/config":"^0.0.15"},"_npmOperationalInternal":{"tmp":"tmp/format-for_0.1.0_1758726955350_0.09019211396559146","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@charlie-labs/format-for","version":"0.2.0","keywords":["markdown","formatter","github","slack","linear","unified","remark","mdast","markdown-to-slack","markdown-to-github"],"license":"MIT","_id":"@charlie-labs/format-for@0.2.0","maintainers":[{"name":"rileytomasek","email":"riley.tomasek@gmail.com"},{"name":"ryan-charlielabs","email":"ryan@charlielabs.ai"}],"homepage":"https://github.com/charlie-labs/format-for","bugs":{"url":"https://github.com/charlie-labs/format-for/issues"},"dist":{"shasum":"529cee9bf61345df2d4237936c27d1b66e60e2eb","tarball":"https://registry.npmjs.org/@charlie-labs/format-for/-/format-for-0.2.0.tgz","fileCount":71,"integrity":"sha512-wm9dsJnKQJf21S6Tj8vAPQm1IrqfjIrW+POB01oBqkgpFt8FS1LMqykmWCm/dlcQhxrF5kSK3gzo+jj2XbNmPA==","signatures":[{"sig":"MEUCIQChBh91qdnXDeR4HAuQ12u0rGK3JLd4icH5YmqyqfOKswIgVOQRPhPqd89Rl+6hOtbzYS8/LdX7c7S0l5YL7SCwGOk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@charlie-labs%2fformat-for@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":221772},"main":"./dist/index.cjs","type":"module","zshy":"./src/index.ts","types":"./dist/index.d.cts","module":"./dist/index.js","engines":{"bun":">=1.2","node":">=22"},"exports":{".":{"types":"./dist/index.d.cts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"1b06df9bfc4f9f08aef0c3841d21a352cdfda44f","scripts":{"ci":"bun run typecheck && bun run lint && bun run test","fix":"eslint --fix --quiet . ; prettier --write --log-level=silent .","lint":"eslint . && prettier --check .","test":"vitest run --reporter=dot --coverage","build":"bun run --bun zshy","clean":"rm -rf node_modules","prepare":"husky","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"bun run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:40ff7faa-f80a-43af-9e41-53a3206a76c7"}},"prettier":"@charlie-labs/config/prettier","repository":{"url":"git+https://github.com/charlie-labs/format-for.git","type":"git"},"_npmVersion":"11.6.0","description":"[![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)","directories":{},"lint-staged":{"*.{ts,tsx,js,jsx}":["eslint --fix","prettier --write"],"*.{json,md,yml,yaml}":["prettier --write"]},"sideEffects":false,"_nodeVersion":"24.9.0","dependencies":{"unified":"^11.0.5","remark-gfm":"^4.0.1","remark-parse":"^11.0.0","remark-stringify":"^11.0.0","unist-util-visit":"^5.0.0","mdast-util-to-string":"^4.0.0","unist-util-visit-parents":"^6.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"bun@1.2.22","devDependencies":{"knip":"^5.64.0","zshy":"^0.4.2","husky":"^9.1.7","eslint":"^9.36.0","vitest":"^3.2.4","prettier":"^3.6.2","@types/bun":"^1.2.22","fast-check":"^4.3.0","typescript":"^5.9.2","lint-staged":"^16.2.0","@types/mdast":"^4.0.4","@types/unist":"^3.0.3","@vitest/coverage-v8":"^3.2.4","@charlie-labs/config":"^0.0.15"},"_npmOperationalInternal":{"tmp":"tmp/format-for_0.2.0_1758991468050_0.7484336466980401","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2025-09-23T21:48:35.267Z","modified":"2026-05-20T03:16:14.390Z","0.0.4":"2025-09-23T21:48:35.876Z","0.1.0":"2025-09-24T15:15:55.575Z","0.2.0":"2025-09-27T16:44:28.260Z"},"bugs":{"url":"https://github.com/charlie-labs/format-for/issues"},"license":"MIT","homepage":"https://github.com/charlie-labs/format-for","keywords":["markdown","formatter","github","slack","linear","unified","remark","mdast","markdown-to-slack","markdown-to-github"],"repository":{"url":"git+https://github.com/charlie-labs/format-for.git","type":"git"},"description":"[![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)","maintainers":[{"email":"riley.tomasek@gmail.com","name":"rileytomasek"},{"email":"ryan@charlielabs.ai","name":"ryan-charlielabs"}],"readme":"# format-for\n\n[![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)\n[![Bun](https://img.shields.io/badge/bun-1.x-000)](https://bun.sh)\n\nOne Markdown input → clean output for GitHub, Slack, or Linear.\n\nYou 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.\n\n## Contents\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Example: one input → three outputs](#example-one-input--three-outputs)\n- [Concepts](#concepts)\n- [API](#api)\n- [Target behavior highlights](#target-behavior-highlights)\n- [Recipes](#recipes)\n- [Warnings and safety](#warnings-and-safety)\n- [Performance and idempotency](#performance-and-idempotency)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Install\n\nThis package is ESM‑first with a CJS fallback and works in Node 22+ and Bun 1.2+.\n\n```bash\nbun add @charlie-labs/format-for\n# or\nnpm i @charlie-labs/format-for\n# or\nyarn add @charlie-labs/format-for\n# or\npnpm add @charlie-labs/format-for\n```\n\n## Quick start\n\n```ts\nimport { formatFor } from '@charlie-labs/format-for';\n\nconst md = `\n+++ Summary\n\nCollapsible content\n\n+++\n\nSee @riley in #dev and <https://example.com|site>.\n`;\n\nconst gh = await formatFor.github(md);\nconst slack = await formatFor.slack(md);\nconst linear = await formatFor.linear(md);\n```\n\nOutputs for the snippet above (exact values):\n\nGitHub\n\n```md\n<details>\n<summary>Summary</summary>\n\nCollapsible content\n\n</details>\n\nSee @riley in #dev and [site](https://example.com).\n```\n\nSlack\n\n```\n*Summary*\n> Collapsible content\nSee @riley in #dev and <https://example.com|site>.\n```\n\nLinear\n\n```md\n+++ Summary\n\nCollapsible content\n\n+++\n\nSee @riley in #dev and [site](https://example.com).\n```\n\nPrefer to inject live Slack/Linear defaults (real users/channels, org/team autolinks)? Use the factory:\n\n```ts\nimport {\n  createFormatFor,\n  createEnvDefaultsProvider,\n} from '@charlie-labs/format-for';\n\nconst ff = createFormatFor({\n  defaults: createEnvDefaultsProvider({\n    // optional: override cache namespace and token/TTL; when not provided,\n    // SLACK_BOT_TOKEN and LINEAR_API_KEY env vars are used by default\n    // namespace: 'my-app:format-for:v1',\n    // slack: { token: 'xoxb-…', ttlMs: 10 * 60_000 },\n    // linear: { apiKey: 'lin_api_…', ttlMs: 60 * 60_000 },\n  }),\n});\n\nconst out = await ff.slack('Ping @riley in #dev');\n```\n\n## Example: one input → three outputs\n\nBelow is a realistic mixed‑syntax input (taken from our test fixtures), followed by the exact strings returned for each target.\n\n<details>\n<summary><strong>Input (Markdown, mixed Slack/Linear/GFM)</strong></summary>\n\n````md\n# Project Alpha: Auth flow update (fixture input)\n\nShort 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~.\n\n+++ Decisions\n\n- Keep email-first login; remove ~magic-link-only~ path.\n- Links: Markdown [spec](https://spec.commonmark.org) and Slack form <https://example.com|Docs>.\n- Include a bare URL too: <https://example.org>.\n\n+++ Edge cases\n\n- Safari ITP and cookies.\n- If user is SSO-only, show a link back.\n- Mention special <!channel> to alert during rollout.\n\n+++\n\n- Table and images should still render cross-platform.\n\n+++\n\n## Tasks\n\n1. Backend\n   - Add POST /v2/auth/verify\n     - Increase rate limit from 30 to 60\n2. Frontend\n   - Update callback handler\n   - Add retry UI\n\n> Note: rollout starts Monday. Coordinate with #release and @riley.\n\n### Code sample\n\n```ts\n// `+++` inside code should NOT create a details block.\nexport function normalizeEmail(s: string): string {\n  return s.trim().toLowerCase();\n}\n```\n\n### Cases\n\n| Case        | Expected |\n| ----------- | -------- |\n| Valid email | 200      |\n| Bad token   | 401      |\n\n![diagram](https://example.com/flow.png)\n\nHTML allowed tags: <u>Important</u> and <sup>2</sup>.<br>\nHTML disallowed inline in a paragraph (to exercise Linear's allowlist): <video src=\"/noop\"></video>\n\n<div>Standalone HTML block that should be stripped on Slack/Linear</div>\n\nFootnote ref[^1].\n\n[^1]: This is a footnote with a Slack user <@U0FOOT> and a Slack link <https://ex.com|ex>.\n````\n\n</details>\n\n<details>\n<summary><strong>GitHub output (exact value)</strong></summary>\n\n````md\n# Project Alpha: Auth flow update (fixture input)\n\nShort 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~~.\n\n<details>\n<summary>Decisions</summary>\n\n- Keep email-first login; remove ~~magic-link-only~~ path.\n- Links: Markdown [spec](https://spec.commonmark.org) and Slack form [Docs](https://example.com).\n- Include a bare URL too: <https://example.org>.\n\n<details>\n<summary>Edge cases</summary>\n\n- Safari ITP and cookies.\n- If user is SSO-only, show a link back.\n- Mention special @channel to alert during rollout.\n</details>\n\n- Table and images should still render cross-platform.\n</details>\n\n## Tasks\n\n1. Backend\n   - Add POST /v2/auth/verify\n     - Increase rate limit from 30 to 60\n2. Frontend\n   - Update callback handler\n   - Add retry UI\n\n> Note: rollout starts Monday. Coordinate with #release and @riley.\n\n### Code sample\n\n```ts\n// `+++` inside code should NOT create a details block.\nexport function normalizeEmail(s: string): string {\n  return s.trim().toLowerCase();\n}\n```\n\n### Cases\n\n| Case        | Expected |\n| ----------- | -------- |\n| Valid email | 200      |\n| Bad token   | 401      |\n\n![diagram](https://example.com/flow.png)\n\nHTML allowed tags: <u>Important</u> and <sup>2</sup>.<br>\nHTML disallowed inline in a paragraph (to exercise Linear's allowlist): <video src=\"/noop\"></video>\n\n<div>Standalone HTML block that should be stripped on Slack/Linear</div>\n\nFootnote ref[^1].\n\n[^1]: This is a footnote with a Slack user @U0FOOT and a Slack link [ex](https://ex.com).\n````\n\n</details>\n\n<details>\n<summary><strong>Linear output (exact value)</strong></summary>\n\n````md\n# Project Alpha: Auth flow update (fixture input)\n\nShort 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~~.\n\n+++ Decisions\n\n- Keep email-first login; remove ~~magic-link-only~~ path.\n- Links: Markdown [spec](https://spec.commonmark.org) and Slack form [Docs](https://example.com).\n- Include a bare URL too: <https://example.org>.\n\n+++ Edge cases\n\n- Safari ITP and cookies.\n- If user is SSO-only, show a link back.\n- Mention special @channel to alert during rollout.\n\n+++\n\n- Table and images should still render cross-platform.\n\n+++\n\n## Tasks\n\n1. Backend\n   - Add POST /v2/auth/verify\n     - Increase rate limit from 30 to 60\n2. Frontend\n   - Update callback handler\n   - Add retry UI\n\n> Note: rollout starts Monday. Coordinate with #release and @riley.\n\n### Code sample\n\n```ts\n// `+++` inside code should NOT create a details block.\nexport function normalizeEmail(s: string): string {\n  return s.trim().toLowerCase();\n}\n```\n\n### Cases\n\n| Case        | Expected |\n| ----------- | -------- |\n| Valid email | 200      |\n| Bad token   | 401      |\n\n![diagram](https://example.com/flow.png)\n\nHTML allowed tags: <u>Important</u> and <sup>2</sup>.<br>\nHTML disallowed inline in a paragraph (to exercise Linear's allowlist):\n\nFootnote ref[^1].\n\n[^1]: This is a footnote with a Slack user @U0FOOT and a Slack link [ex](https://ex.com).\n````\n\n</details>\n\n<details>\n<summary><strong>Slack output (exact value)</strong></summary>\n\n````\n\n_Project Alpha: Auth flow update (fixture input)_\n\nShort 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~.\n\n_Decisions_\n\n> • Keep email-first login; remove ~magic-link-only~ path.\n> • Links: Markdown <https://spec.commonmark.org|spec> and Slack form <https://example.com|Docs>.\n> • Include a bare URL too: <https://example.org|https://example.org>.\n>\n> _Edge cases_\n>\n> > • Safari ITP and cookies.\n> > • If user is SSO-only, show a link back.\n> > • Mention special <!channel> to alert during rollout.\n> > • Table and images should still render cross-platform.\n> > _Tasks_\n\n1. Backend\n   • Add POST /v2/auth/verify\n   → Increase rate limit from 30 to 60\n\n2. Frontend\n   • Update callback handler\n   • Add retry UI\n\n> Note: rollout starts Monday. Coordinate with #release and @riley.\n\n_Code sample_\n\n```\n// `+++` inside code should NOT create a details block.\nexport function normalizeEmail(s: string): string {\n  return s.trim().toLowerCase();\n}\n```\n\n_Cases_\n\n```\nCase | Expected\nValid email | 200\nBad token | 401\n```\n\n<https://example.com/flow.png|diagram>\n\nHTML allowed tags: &lt;u&gt;Important&lt;/u&gt; and &lt;sup&gt;2&lt;/sup&gt;.&lt;br&gt;\nHTML disallowed inline in a paragraph (to exercise Linear's allowlist): &lt;video src=\"/noop\"&gt;&lt;/video&gt;\n\nFootnote ref^[1].\n\nFootnotes:\n[1] This is a footnote with a Slack user <@U0FOOT> and a Slack link <https://ex.com|ex>.\n\n````\n\n</details>\n\n## Concepts\n\n- Parse once → canonical mdast → render per‑target.\n- Predictable degradations with explicit warnings (e.g., Slack tables → code blocks; math → code; images → links; Linear strips disallowed HTML).\n- Idempotent output per target; code/inline code is never changed by autolinks or formatting.\n- 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.\n\n## API\n\n### `formatFor`\n\n- `formatFor.github(input, options?)`\n- `formatFor.slack(input, options?)`\n- `formatFor.linear(input, options?)`\n\nEach returns a `Promise<string>` with the formatted value for that target.\n\n### Factory and env defaults\n\n```ts\nimport {\n  createFormatFor,\n  createEnvDefaultsProvider,\n} from '@charlie-labs/format-for';\n\nconst ff = createFormatFor({\n  defaults: createEnvDefaultsProvider({\n    // optional: override token/TTL/namespace; env fallbacks used by default\n    // slack: { token: 'xoxb-…', ttlMs: 10 * 60_000 },\n    // linear: { apiKey: 'lin_api_…', ttlMs: 60 * 60_000 },\n  }),\n});\n\nconst out = await ff.github('Ref ENG-123 and say hi to @riley');\n```\n\n### FormatOptions (v1)\n\n```ts\ntype FormatOptions = {\n  maps?: {\n    slack?: {\n      users?: Record<string, { id: string; label?: string }>;\n      channels?: Record<string, { id: string; label?: string }>;\n    };\n    linear?: { users?: Record<string, { url: string; label?: string }> };\n  };\n  autolinks?: Partial<\n    Record<\n      'github' | 'slack' | 'linear',\n      Array<{\n        pattern: RegExp;\n        urlTemplate: string;\n        labelTemplate?: string;\n      }>\n    >\n  >;\n  warnings?: {\n    mode?: 'console' | 'silent';\n    onWarn?: (message: string) => void;\n  };\n  target?: {\n    slack?: {\n      lists?: { maxDepth?: number }; // default: 2\n      images?: { style?: 'link' | 'url'; emptyAltLabel?: string }; // defaults: style 'link'; emptyAltLabel 'image'\n    };\n    github?: { breaks?: 'two-spaces' | 'backslash' }; // default: 'two-spaces'\n    // Linear options are intentionally not exposed in v1\n  };\n};\n```\n\nNotes\n\n- Autolinks are normalized to global regex; when provider + caller rules collide, the caller wins.\n- Linear’s HTML allowlist is fixed internally: `details`, `summary`, `u`, `sub`, `sup`, `br`.\n\n## Target behavior highlights\n\n- GitHub\n  - `details` nodes render as `<details><summary>…</summary>…</details>` HTML.\n  - Hard breaks use two spaces by default (configurable to backslash).\n  - Preserves task list state on bare marker lines.\n- Slack\n  - Headings become bold lines; quotes have readable spacing.\n  - Lists deeper than `maxDepth` flatten with a single warning per render.\n  - Images emit as `<url|label>` links (or bare URLs) with a warning; style is configurable.\n  - Tables → fenced code with a warning; inline/display math → code/code blocks with warnings.\n  - Footnotes become `^[n]` plus appended refs; all HTML stripped with a warning.\n- Linear\n  - `+++ Summary` → collapsible; disallowed HTML is stripped while keeping surrounding text.\n  - Slack/Linear mentions normalize to links/plain text as appropriate.\n\n## Recipes\n\n- Autolink Linear issues (multiple team keys):\n\n  ```ts\n  const rules = [\n    {\n      pattern: /\\b(BOT-\\d+)\\b/g,\n      urlTemplate: 'https://linear.app/charlie/issue/$0',\n    },\n    {\n      pattern: /\\b(ENG-\\d+)\\b/g,\n      urlTemplate: 'https://linear.app/charlie/issue/$0',\n    },\n  ];\n  await formatFor.github(text, {\n    autolinks: { github: rules, linear: rules, slack: rules },\n  });\n  ```\n\n- Route warnings to your logger and silence console output:\n\n  ```ts\n  await formatFor.slack(md, {\n    warnings: { mode: 'silent', onWarn: (m) => log.warn(m) },\n  });\n  ```\n\n- Preserve GitHub backslash hard breaks:\n\n  ```ts\n  await formatFor.github(md, { target: { github: { breaks: 'backslash' } } });\n  ```\n\n- Use live Slack/Linear defaults (if tokens exist):\n\n  ```ts\n  const ff = createFormatFor({ defaults: createEnvDefaultsProvider() });\n  const text = await ff.slack('See @riley in #dev');\n  ```\n\n## Warnings and safety\n\n- Slack: HTML stripped; images/tables/math/footnotes degrade with clear warnings; link labels are sanitized.\n- Linear: strict inline HTML allowlist; disallowed tags are removed and paragraphs preserved; warnings are emitted.\n- Code blocks and inline code are never altered by autolinks or formatting passes.\n\nControl warnings with `warnings.mode` and `warnings.onWarn`.\n\n## Performance and idempotency\n\n- One parse; lightweight renderers; no network calls unless you opt in via the factory.\n- Formatting is idempotent for a given target (running twice yields the same string).\n\n## Contributing\n\nDev commands:\n\n```bash\nbun install\nbun run typecheck\nbun run lint\nbun run test\n```\n\nFixtures live under `src/markdown/__tests__/__fixtures__/`. To regenerate example outputs locally, run: `bun scripts/gen-fixtures.ts`.\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md"}