Getting started

setwell is one pass over HTML that applies the details of fine typesetting browsers don't do by themselves. It changes characters (curly quotes, dashes, minus signs), joins words that shouldn't be split across lines, and adds a few small spans that its stylesheet finishes (small caps, hanging quotes).

Before

"It's NASA's call," she said -- on pp. 10-20 of the 1st report.

After

“It’s NASA’s call,” she said⁠—on pp. 10–⁠20 of the 1st report.

Install

npm install setwell

No dependencies. ESM and CommonJS, with types.

On a string

For server rendering, static sites and build steps:

import { setwell } from "setwell";

const out = setwell(html);
const out2 = setwell(html, { disable: ["hyphenate"], ignore: ".no-setwell" });

Only text changes. Tags, attributes, comments and any text setwell didn't touch come out byte for byte as they went in, so diffs stay readable. Character references in text are understood (" is a quote) and kept as written where nothing changed.

On an element

For content that's already on the page:

import { setwellElement } from "setwell/dom";

const changes = await setwellElement(document.querySelector("article"), options);

It works on the element in place and returns the changes it made. It's async because the hyphenation patterns (about 30 KB) load only when that rule is on. The result is the same as the string API's for the same HTML.

The stylesheet

import "setwell/setwell.css";
// or, as a string:
import { css } from "setwell/css";

It only styles setwell's own spans (setwell-caps, setwell-ord, the hanging and optical pulls), so it can't change anything else on your page. The amounts are chosen to suit typical text faces. Each is a custom property with a default, so you can change one without copying the file:

.article {
  --setwell-hang-double: 0.42em;   /* how far an opening " hangs (0.46em) */
  --setwell-hang-single: 0.25em;   /* and an opening ' (0.27em) */
  --setwell-caps-size: 0.85em;     /* size of acronyms (0.9em) */
  --setwell-caps-spacing: 0.04em;  /* and their letter-spacing (0.03em) */
  --setwell-ord-size: 0.65em;      /* ordinal suffix size (0.7em) */
}

Options

OptionDefaultMeaning
disable[]Rule or group ids to turn off. See rules. An unknown id throws, so typos don't pass silently.
ignoreSelector(s) for more elements to leave alone, added to the defaults below.
scopeSelector(s): only text inside matching elements is processed.
emDash"closed""hair" puts hair spaces around em dashes.
widows{ maxChars: 16, minWords: 4 }The last two words are joined when together they're at most maxChars long, in blocks of at least minWords words.
output"unicode"String API: "entities" writes new characters as —,   and so on.

In the string API, ignore and scope take type, *, #id, .class and attribute selectors, compounds, commas, and descendant and > combinators. The DOM API takes any selector the browser does.

Always ignored:

head,
title,
script,
style,
template,
noscript,
noembed,
noframes,
iframe,
object,
pre,
xmp,
plaintext,
code,
kbd,
samp,
var,
tt,
textarea,
input,
select,
datalist,
math,
svg,
[contenteditable],
[translate="no"],
[data-setwell="off"],
[class^="setwell-"],
[class*=" setwell-"]

The last two are setwell's own spans, which is what makes a second pass change nothing. Add data-setwell="off" to any element to skip it.

Reports

import { report } from "setwell";

const { html, changes } = report(input);
// changes: [{ rule: "quotes.double", kind: "replace", before: '"', after: "“", offset: 3 }, …]

Each change says which rule made it, what it was before and after, and where it is in the input. The playground lists them next to a before-and-after preview.

What it won't do