Package Exports
- punctilio
Readme
punctilio (n.): precise observance of formalities.
The best typography package for English.
import { transform } from 'punctilio'
transform('"It\'s a beautiful thing, the destruction of words..." -- 1984')
// → “It’s a beautiful thing, the destruction of words…” — 1984npm install punctilioWhy punctilio?
As far as I can tell, punctilio is the most reliable and feature-complete. I built punctilio for my website. I wrote[^wrote] and sharpened the core regexes sporadically over several months, exhaustively testing edge cases. Eventually, I decided to spin off the functionality into its own package.
[^wrote]: While Claude is the number one contributor to this repository, that’s just because Claude has helped me port my existing code and add minor features. The core regular expressions (e.g. dashes, quotes, multiplication signs) are human-written.
I tested punctilio 0.4 against smartypants 0.2.2, tipograph 0.7.4, and smartquotes 2.3.2.[^python] These other packages have spotty feature coverage and inconsistent impact on text. For example, smartypants mishandles quotes after em dashes:
[^python]: The Python libraries I found were closely related to the JavaScript packages, so I don’t include Python tests.
| Input | smartypants |
punctilio |
|---|---|---|
| She said—"Hi!" | She said—”Hi!” ✗ | She said—“Hi!” ✓ |
By running benchmark.mjs, I basically graded all libraries on a subset of my unit tests, selected to represent a wide range of features.
| Package | Score |
|---|---|
punctilio |
79/82 (96%) |
tipograph |
48/82 (59%) |
smartquotes |
30/82 (37%) |
smartypants |
28/82 (35%) |
| Feature | Example | smartypants |
tipograph |
smartquotes |
punctilio |
|---|---|---|---|---|---|
| Smart quotes | "hello" → “hello” | ✓ | ✓ | ✓ | ✓ |
| Leading apostrophe | 'Twas → ’Twas | ✗ | ✗ | ✓ | ✓ |
| Em dash | -- → — | ✓ | ✗ | ✗ | ✓ |
| En dash (ranges) | 1-5 → 1–5 | ✗ | ✓ | ✗ | ✓ |
| Minus sign | -5 → −5 | ✗ | ✓ | ✗ | ✓ |
| Ellipsis | ... → … | ✓ | ✓ | ✗ | ✓ |
| Multiplication | 5x5 → 5×5 | ✗ | ✗ | ✗ | ✓ |
| Math symbols | != → ≠ | ✗ | ✓ | ✗ | ✓ |
| Legal symbols | (c) → © | ✗ | © only | ✗ | ✓ |
| Arrows | -> → → | ✗ | ✓ | ✗ | ✓ |
| Prime marks | 5'10" → 5′10″ | ✗ | ✓ | ✓ | ✓ |
| Degrees | 20 C → 20 °C | ✗ | ✗ | ✗ | ✓ |
| Fractions | 1/2 → ½ | ✗ | ✗ | ✗ | ✓ |
| Superscripts | 1st → 1ˢᵗ | ✗ | ✗ | ✗ | ✓ |
| Localization | American/British | ✗ | ✗ | ✗ | ✓ |
| Ligatures | ?? → ⁇ | ✗ | ✓ | ✗ | ✓ |
| Non-English quotes | „Hallo" (German) | ✗ | ✓ | ✗ | ✗ |
As far as I can tell, punctilio’s only missing feature is non-English quote support. I don’t have a personal reason to use non-English localization, but feel free to make a pull request!
Works with HTML DOMs via separation boundaries
Other typography libraries either transform plain strings or operate on AST nodes individually (retext-smartypants can’t map changes back to HTML). But real HTML has text spanning multiple elements—if you concatenate text from <em>Wait</em>..., transform it, then try to split it back, you've lost track of where </em> belonged.
punctilio introduces separation boundaries. First, insert a “separator” character (default: U+E000) at each element boundary before transforming (like at the start and end of an <em>). Every regex allows this character mid-pattern without breaking matches. For example, .[SEP].. still becomes …[SEP]. punctilio validates the output by ensuring the separator count remains the same.
import { transform, DEFAULT_SEPARATOR } from 'punctilio'
transform(`"Wait${DEFAULT_SEPARATOR}"`)
// → `“Wait”${DEFAULT_SEPARATOR}`
// The separator doesn’t block the information that this should be an end-quote!Your DOM walker tracks which text node each segment came from, inserts separators between them, transforms the combined string, then splits on separators to update each node. Use the separator option if U+E000 conflicts with your content. For an example of how to integrate this functionality, see my website’s code.
Options
punctilio doesn’t enable all transformations by default. Fractions and degrees tend to match too aggressively (perfectly applying the degree transformation requires semantic meaning). Superscript letters and punctuation ligatures have spotty font support—on GitHub, this README’s font doesn’t even support the example superscript! Furthermore, ligatures = true can change the meaning of text by collapsing question and exclamation marks.
transform(text, {
punctuationStyle: 'american' | 'british' | 'none', // default: 'american'
dashStyle: 'american' | 'british' | 'none', // default: 'american'
symbols: true, // math, legal, arrows
collapseSpaces: true, // normalize whitespace
fractions: false, // 1/2 → ½
degrees: false, // 20 C → 20 °C
superscript: false, // 1st → 1ˢᵗ
ligatures: false, // ??? → ⁇, ?! → ⁈, !? → ⁉, !!! → !
})