No 3rd party dependencies. All dependencies and devDependencies, checked recursively, are Codsen packages.
Permalink to InstallationInstallation
Permalink to Quick TakeQuick Take
Permalink to ExamplesExamples
- Encode the inserted non-breaking space for CSS
- Ignore a custom pair of template markers
- Keep a dash with its neighbouring words when hyphen handling is enabled
- Ignore every built-in template syntax preset
- Ignore Hexo template expressions while processing surrounding text
- Ignore Hugo template expressions while processing surrounding text
- Ignore Jinja tags while processing surrounding text
- Ignore Liquid tags while processing surrounding text
- Ignore Nunjucks tags while processing surrounding text
- Encode the inserted non-breaking space for JavaScript
- Apply widow prevention only after the minimum character count is reached
- Apply widow prevention only after the minimum word count is reached
- Inspect which transformations were applied
- Map progress reports into a caller-owned percentage range
- Insert a raw non-breaking space instead of an encoded entity
- Remove an existing widow-prevention entity
- Keep both halves of a UK postcode together
- Widow word removal from text within HTML
What it does
string-remove-widows prevents widow words by replacing eligible whitespace before the last word with a non-breaking space. It can also remove existing prevention measures, normalize their encoding, protect spaces before dashes and inside UK postcodes, and preserve HTML markup, opaque content, template markers, and caller-protected ranges.
The library preserves LF, CRLF, and CR line endings. Its core API is a string-in, result-object-out function with no DOM, file-system, or network access.
API — removeWidows()
The main function removeWidows() is imported like this:
removeWidows() accepts one required string and one optional plain object:
| Argument | Type | Required | Description |
|---|---|---|---|
strType: string | |||
str | string | Yes | The source string to process. |
optsType: Partial<Opts> | |||
opts | Partial<Opts> | No | Options that override the defaults. |
Options
| Option | Type | Default | Description |
|---|---|---|---|
removeWidowPreventionMeasuresType: booleanDefault: false | |||
removeWidowPreventionMeasures | boolean | false | Replaces applicable non-breaking-space forms with a regular space instead of adding prevention measures. |
convertEntitiesType: booleanDefault: true | |||
convertEntities | boolean | true | Uses the encoding selected by targetLanguage. Set it to false to insert a raw non-breaking space. |
targetLanguageType: "html" | "css" | "js"Default: "html" | |||
targetLanguage | "html" | "css" | "js" | "html" | Selects the encoded non-breaking-space form. |
UKPostcodesType: booleanDefault: false | |||
UKPostcodes | boolean | false | Protects the separator in bounded, valid-looking UK postcodes. |
hyphensType: booleanDefault: true | |||
hyphens | boolean | true | Protects eligible whitespace before hyphens, en dashes, and em dashes without crossing a paragraph boundary. |
minWordCountType: number | false | nullDefault: 4 | |||
minWordCount | number | false | null | 4 | Minimum word count required for ordinary last-word protection. A finite non-negative number is accepted; false, null, or 0 disables this threshold. |
minCharCountType: number | false | nullDefault: 5 | |||
minCharCount | number | false | null | 5 | Minimum non-whitespace character count required for ordinary last-word protection. A finite non-negative number is accepted; false, null, or 0 disables this threshold. |
ignoreType: IgnorePreset | readonly IgnoreEntry[]Default: [] | |||
ignore | IgnorePreset | readonly IgnoreEntry[] | [] | Skips supported template markers, custom marker pairs, or a mixture of both. |
reportProgressFuncType: false | null | ((percent) => void)Default: null | |||
reportProgressFunc | false | null | ((percent) => void) | null | Receives progress updates. false and null disable reporting. |
reportProgressFuncFromType: numberDefault: 0 | |||
reportProgressFuncFrom | number | 0 | Inclusive lower progress bound, expressed as an integer from 0 through 100. |
reportProgressFuncToType: numberDefault: 100 | |||
reportProgressFuncTo | number | 100 | Inclusive upper progress bound, expressed as an integer from 0 through 100; it must not be below the lower bound. |
tagRangesType: readonly TagRange[] | nullDefault: [] | |||
tagRanges | readonly TagRange[] | null | [] | Treats half-open source ranges as opaque so their contents are not changed. |
Here are all defaults in one place for copying:
Result
| Property | Description |
|---|---|
res | |
res | The processed string. |
ranges | |
ranges | The ranges applied to produce res, or null when the output did not change. |
log | |
log | Best-effort completion statistics. timeTakenInMilliseconds is elapsed time, not a deterministic result value. |
whatWasDone | |
whatWasDone | Reports which operation actually changed the output. |
applicableOpts | |
applicableOpts | Reports which operations could affect this input, independently of the current option settings. |
For example:
import { removeWidows } from "string-remove-widows";
const result = removeWidows("Lorem ipsum dolor sit amet");
console.log(result);
// {
// res: "Lorem ipsum dolor sit amet",
// ranges: [[21, 22, " "]],
// log: { timeTakenInMilliseconds: 0 },
// whatWasDone: {
// removeWidows: true,
// convertEntities: false,
// },
// applicableOpts: {
// removeWidows: true,
// convertEntities: true,
// },
// }
The elapsed time above is only illustrative and varies between runs. convertEntities is applicable because changing that option would change the inserted representation, even though no existing entity was converted in this call.
API — defaults
You can import defaults:
It's a plain object:
The main function calculates the options to be used by merging the options you passed with these defaults.
The exported defaults and their nested arrays are frozen snapshots. Copy them before making local changes.
API — version
You can import version:
Exported types
This TypeScript package exports these public types:
| Type | Description |
|---|---|
CountThresholdType: CountThreshold | |
CountThreshold | A numeric word or character threshold, or false/null to disable it. |
HeadsAndTailsObjType: HeadsAndTailsObj | |
HeadsAndTailsObj | One custom template-marker pair. Each side can be one string or a readonly string array. |
IgnoreEntryType: IgnoreEntry | |
IgnoreEntry | An IgnorePreset or custom HeadsAndTailsObj. |
IgnorePresetType: IgnorePreset | |
IgnorePreset | One of "all", "hexo", "hugo", "jinja", "liquid", or "nunjucks". |
ObjType: Obj | |
Obj | A general string-keyed object type retained for compatibility. |
OptsType: Opts | |
Opts | The complete options object. The function accepts Partial<Opts>. |
ResType: Res | |
Res | The complete return object. |
TagRangeType: TagRange | |
TagRange | A readonly two- or three-item half-open range used by tagRanges. |
import type {
CountThreshold,
HeadsAndTailsObj,
IgnoreEntry,
IgnorePreset,
Obj,
Opts,
Res,
TagRange,
} from "string-remove-widows";
Output encoding
With convertEntities: true, targetLanguage selects the inserted representation:
| Target | Inserted text |
|---|---|
html | |
html | |
css | |
css | \0000A0 |
js | |
js | \u00A0 |
Set convertEntities to false to insert the raw U+00A0 character.
import { removeWidows } from "string-remove-widows";
const result = removeWidows("Some raw text in a very long line.", {
targetLanguage: "css",
});
console.log(result.res);
// => Some raw text in a very long\0000A0line.
When writing the CSS representation in JavaScript source, escape its backslash as "\\0000A0".
HTML preservation and paragraph boundaries
HTML tags and their attributes are recognized automatically. Comments, declarations,
CDATA sections, and the complete contents of script, style, pre, code,
textarea, and title elements remain unchanged. An unfinished opaque region
protects the rest of the input. Custom elements and mixed-case HTML names are
supported.
Block elements, including paragraphs, list items, headings, and table cells, reset
the word and character thresholds. <br> and <hr> also separate text. Inline
tags remain transparent to surrounding prose; adding a source newline between
HTML paragraphs does not change which final words are protected.
import { removeWidows } from "string-remove-widows";
const source =
'<p class="underline font-bold">one two three four</p><p>five six seven eight</p>';
console.log(removeWidows(source).res);
// => <p class="underline font-bold">one two three four</p><p>five six seven eight</p>
Trailing whitespace and markup do not move an existing final non-breaking space to an earlier word on subsequent calls. Adjacent non-breaking-space tokens retain their count when their encoding changes.
Ignoring template markers
Use ignore when template syntax must remain opaque. A single preset string is accepted, or an array can mix preset names and custom { heads, tails } objects. Custom heads and tails must be non-empty strings or dense arrays of non-empty strings.
import { removeWidows } from "string-remove-widows";
const result = removeWidows("Hi {{ customer.name }} from our support team", {
ignore: [
"nunjucks",
{
heads: "<%",
tails: "%>",
},
],
});
Available presets are all, hexo, hugo, jinja, liquid, and nunjucks. The all preset enables every supported marker set. The Jinja, Nunjucks, and
Liquid presets protect nested if and for blocks through the matching outer
closing marker, including whitespace-control forms such as {%- if ... -%}.
An unfinished outer block remains opaque through the end of input. Custom marker
pairs retain flat matching: the first matching tail ends their protected region.
A template placeholder counts as one word. When it is the final word, its preceding gap receives the non-breaking space, leaving the placeholder unchanged:
removeWidows("one two three {{four}}", { ignore: "hugo" }).res;
// => one two three {{four}}
Protecting known ranges
tagRanges accepts sorted or unsorted half-open ranges in the form [from, to]. A third tuple value is allowed for compatibility but ignored. Ranges are normalized, overlaps are combined, and ends beyond the input length are clipped. The text inside a normalized range is never edited.
Tag recognition does not start inside an explicitly opaque range. If that range
contains an opening tag, include its complete markup in the range; a tag opener
at the range boundary is still recognized normally. Custom ignore markers take
precedence over automatic HTML recognition.
You can also pass tag locations reported by string-strip-html:
import { stripHtml } from "string-strip-html";
import { removeWidows } from "string-remove-widows";
const input =
'something in front here <a style="display: block;">x</a> <b style="display: block;">y</b>';
const { allTagLocations: tagRanges } = stripHtml(input);
console.log(tagRanges);
// => [[24, 51], [52, 56], [57, 84], [85, 89]]
const result = removeWidows(input, { tagRanges });
console.log(result.res);
// => something in front here <a style="display: block;">x</a> <b style="display: block;">y</b>
Progress reporting
When reportProgressFunc is set, it receives finite integer percentages within the inclusive reportProgressFuncFrom and reportProgressFuncTo bounds. Values are deduplicated and strictly increase, and both configured endpoints are reported.
import { removeWidows } from "string-remove-widows";
const progress = [];
removeWidows("A sufficiently long line for widow prevention", {
reportProgressFunc: (percentage) => progress.push(percentage),
reportProgressFuncFrom: 20,
reportProgressFuncTo: 40,
});
console.log(progress.at(0), progress.at(-1));
// => 20 40
Distribution
The package provides an ESM entry point and the historical direct-browser IIFE at dist/string-remove-widows.umd.js. The browser bundle exposes window.stringRemoveWidows and follows the same API contract as the ESM build.