New1.1.0 adds /markdown and /html entries

Your AI writes, typograph polishes

Typograph fixes the straight quotes"Atlas"“Atlas”Curled, open and closed., stray apostrophesit'sit’sThe typewriter mark, retired., and breakable spaces30 min30 minHeld together at line ends. in model output.

Chat streams, email, and every string in your product.

npm install @calebduren/typograph

A streamed reply, before and after

A reply streams into two columns as you scroll: on the left as the model sent it, on the right through Typograph. The finished Typograph version reads: Here’s your daily brief for Thursday, September 24th: “Every launch this quarter shipped on time,” Leland wrote in last night’s update. The Q3 review moves to Monday and it’s down to 30 min. It’s clear and 67 °F by noon. Your run is 5 km, so you’ll be back before Mrs. Osei’s call. “Don’t forget,” Joan added, “the team’s calling Friday’s demo ‘the big one.’”

–µsPer call, measured just now in your browser
4.2KBgzip, spacing engine included. View the measurement
0Network requests. It never phones home.
1Runtime dependency, for no-break spacing

One engine, everywhere your AI writes.

Four entry points share the same rules, so a quote curls the same way in a chat reply, an email, and a push notification.

  • Chat that streams

    The Remark plugin typesets replies as tokens arrive, and holds back until a quote or unit settles.

    remarkPlugins={[[typography, { locale: 'en' }]]}
  • Email that lands

    Finished Markdown becomes email HTML with real quotes and no-break spaces, which every client renders.

    await typeset(brief, { target: 'email' })
  • Every string in your UI

    Titles, toasts, and notifications. Synchronous, parses nothing, and never changes a string’s length.

    typesetText(title)
  • Templates, untouched

    Already HTML? Only typographic characters change, including template-escaped quotes. Markup stays byte for byte.

    await typesetHtml(html, { target: 'email' })

Try it on your own text.

This is the published package, running in your browser.
Paste your text. Nothing leaves the page.

Loading the workbench…

Careful where it counts.

Apart from the optional hanging quote, every edit swaps a character in prose for its typographic form.

  • Literal stays literal

    Code, math, URLs, link destinations, markup, and escaped quotes pass through exactly as they arrived.

  • Your text stays yours

    Typography is presentation only. Store and copy the original message; the package never rewrites content.

  • Private by design

    Pure functions with one small dependency. No network, no telemetry, no model calls. It runs where you render.

Known Limits: English only, with one house style; other languages pass through. No dashes, ellipses, primes, or hyphenation. An inch mark inside an open quotation can read as its closing quote. Email output never hangs punctuation, because mail clients do not render it.

A small addition.
Right where you render.

Works in the browser. No model call, API key, or new service.

npm install @calebduren/typograph

Building with an agent? Copy the prompt into your coding assistant. It will find the right integration for your app.

Read the integration guide
Integrate Typograph into this app's assistant responses. Inspect the framework, package manager, renderer, and installed versions first. Preserve the product's fonts, styling, components, and existing behavior. SOURCE AND INSTALLATION Source: https://github.com/calebduren/typograph Install @calebduren/typograph using this app's package manager (npm install @calebduren/typograph for npm). Read the installed package's README and https://typograph.dev/integration.md. The package is ESM, runs in the browser, and requires Node 22+ for server use. Its source and integration fixture are in the repository above. SELECTED SETTINGS Smart punctuation: on Non-breaking spaces: on Hanging punctuation: on (opening quotes only) Apply this exact configuration: Smart punctuation · Non-breaking spaces · Hanging punctuation. Demo highlighting is not a product feature and must not be installed. INTEGRATION Choose the integration that matches the app's existing renderer. Add the Remark plugin once in the assistant text-part renderer, preserving default and app-specific plugins and sanitization. Typography belongs in the presentation layer; keep transports, tool components, and message storage unchanged. If the app uses AI Elements, find its owned MessageResponse component. With Streamdown, use remarkPlugins and preserve its defaults. If rules can change at runtime, inspect the installed versions and check MessageResponse and Streamdown memoization against examples/chat-integration/src/typography-response.tsx in the source repository. Do not force React key remounts. If the app uses Cloudflare Agents, find the renderer fed by useAgentChat. Keep the Worker, Durable Object, WebSocket transport, persistence, and reconnect behavior unchanged. The same renderer integration applies. With unified, supply the original Markdown to the transformer: processor.runSync(processor.parse(markdown), markdown). This preserves escaped punctuation and unfinished syntax. If the app has no compatible Markdown pipeline, explain the smallest compatible integration before introducing a new renderer. Import typography from '@calebduren/typograph' and register [typography, { locale: 'en', punctuation: true, spacing: true }] only for responses known to be English. This is English-only, with one house style and no language detection. Other-language, mixed-language, or verbatim responses should opt out. If the app has no per-response language signal (a user or tenant language setting, the answer language in the system prompt, or a structured output field), register [typography, { locale: 'en', punctuation: { quotes: false, apostrophes: true } }] instead: the typographic apostrophe is correct across Latin-script languages, and quotation marks stay as written. Interface language alone is not a signal. Enable conservative unit, initial, and abbreviation joins. Leave shortWords and lastWords off. Keep plugin configuration stable between renders and preserve the original Markdown source for the Remark transform. Import hangingPunctuation from '@calebduren/typograph/hanging' and import '@calebduren/typograph/hanging.css'. Add [hangingPunctuation, { locale: 'en' }] to the existing rehype pipeline AFTER sanitization. In Streamdown, preserve ...Object.values(defaultRehypePlugins) before it. In unified, use the existing Remark-to-Rehype conversion before it. This helper hangs opening quotes at the start of English, left-aligned paragraphs and headings outside lists. Quotes in bulleted and numbered lists stay inline. Preserve its generated span classes and data-typograph-hanging attribute in custom components; reserve gutter space and avoid clipping. It uses real text and standard CSS, with no native hanging-punctuation dependency or DOM measurements. It does not hang marks at every wrapped line ending. Check renderer memoization if this setting can change at runtime. Preserve raw SDK messages for storage, tool execution, and Copy original. Transform rendered prose only. Keep code, math, HTML, URLs, link destinations, and escaped punctuation protected. Retain the existing renderer's security settings; this plugin is not a sanitizer. FINISHED TEXT If the app also shows AI text that is complete before display (scheduled briefs, email, summaries, titles, notifications), typeset it once when it is finished. For Markdown, use typeset(markdown, { target, locale: 'en', punctuation: true, spacing: true }) from '@calebduren/typograph/markdown'. Target 'web' returns HTML with hanging markup that needs the same hanging.css, 'email' returns HTML without hanging markup, and 'markdown' returns typeset Markdown with its formatting kept. Install unified, remark-parse, remark-gfm, remark-rehype, and rehype-stringify alongside it. For finished HTML, such as an email template that already contains the text, use typeset(html, { target: 'email', locale: 'en', punctuation: true, spacing: true }) from '@calebduren/typograph/html'; it returns the HTML with only typographic characters changed, curls template-escaped quotes such as ", and needs unified and rehype-parse. For plain strings that are not Markdown, use the synchronous typesetText(text, { locale: 'en', punctuation: true, spacing: true }) from '@calebduren/typograph'. Never typeset a stored string that also feeds SMS: one curly quote or nonbreaking space switches GSM-7 to UCS-2 and cuts a segment from 160 to 70 characters. If email is rendered outside JavaScript, typeset a separate copy with target 'markdown' at generation time and keep the stored original. Do not use @calebduren/typograph/static in a bundled app; it needs all seven parser peers installed. Skip this section if the app has no such text. PARAGRAPH ENDINGS For orphan control, prefer progressive-enhancement CSS text-wrap: pretty on completed assistant prose paragraphs, leaving normal wrapping during streaming. Do not force nonbreaking spaces between the final two words. Keep spacing.lastWords off unless explicitly requested; it requires phase: 'complete' after a verified successful finish for that message, never simply status === 'ready'. Unsupported CSS should fall back to normal wrapping. VERIFY Check English quotes and apostrophes, links and emphasis, literal code and math, streaming prefixes, stop/error/retry, and original-text copying. Verify narrow screens and long replies. Run the app's relevant tests and typecheck, then summarize the changes, checks, and any limitations.

Includes installation, English-only defaults, streaming behavior, and checks for your app.

Specifications

Package
@calebduren/typograph 1.1.0
Size
4.2 KB gzip for the core, including the spacing engine
Runtime
ESM. Node 22+ and modern browsers. No install scripts or telemetry.
Dependencies
One: Typehug. Parsers for typeset are yours, as optional peers.
Language
English, one house style. Other languages pass through.
License
MIT. Source on GitHub