Skip to Content
  • Website
Codsen
  • Home
  • Open Source
  • Articles
  • About

prevOpen Source→string-remove-widowsnext

string-remove-widows4.2.6

Helps to prevent widow words in a text

Downloads per monthChangelogMIT Licenselibera manifesto
  • the top
  • Installation
  • Quick Take
  • Examples
  • WHAT IT DOES
  • API — REMOVEWIDOW…
  • API — DEFAULTS
  • API — VERSION
  • EXPORTED TYPE…
  • OUTPUT ENCOD…
  • HTML PRESERVA…
  • IGNORING TEMP…
  • PROTECTING KN…
  • PROGRESS REP…
  • DISTRIBUTION
  • Changelog

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 wordsopens in a new tab 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:

ArgumentTypeRequiredDescription
str
Type: string
strstringYesThe source string to process.
opts
Type: Partial<Opts>
optsPartial<Opts>NoOptions that override the defaults.

Options

OptionTypeDefaultDescription
removeWidowPreventionMeasures
Type: boolean
Default: false
removeWidowPreventionMeasuresbooleanfalseReplaces applicable non-breaking-space forms with a regular space instead of adding prevention measures.
convertEntities
Type: boolean
Default: true
convertEntitiesbooleantrueUses the encoding selected by targetLanguage. Set it to false to insert a raw non-breaking space.
targetLanguage
Type: "html" | "css" | "js"
Default: "html"
targetLanguage"html" | "css" | "js""html"Selects the encoded non-breaking-space form.
UKPostcodes
Type: boolean
Default: false
UKPostcodesbooleanfalseProtects the separator in bounded, valid-looking UK postcodes.
hyphens
Type: boolean
Default: true
hyphensbooleantrueProtects eligible whitespace before hyphens, en dashes, and em dashes without crossing a paragraph boundary.
minWordCount
Type: number | false | null
Default: 4
minWordCountnumber | false | null4Minimum word count required for ordinary last-word protection. A finite non-negative number is accepted; false, null, or 0 disables this threshold.
minCharCount
Type: number | false | null
Default: 5
minCharCountnumber | false | null5Minimum non-whitespace character count required for ordinary last-word protection. A finite non-negative number is accepted; false, null, or 0 disables this threshold.
ignore
Type: IgnorePreset | readonly IgnoreEntry[]
Default: []
ignoreIgnorePreset | readonly IgnoreEntry[][]Skips supported template markers, custom marker pairs, or a mixture of both.
reportProgressFunc
Type: false | null | ((percent) => void)
Default: null
reportProgressFuncfalse | null | ((percent) => void)nullReceives progress updates. false and null disable reporting.
reportProgressFuncFrom
Type: number
Default: 0
reportProgressFuncFromnumber0Inclusive lower progress bound, expressed as an integer from 0 through 100.
reportProgressFuncTo
Type: number
Default: 100
reportProgressFuncTonumber100Inclusive upper progress bound, expressed as an integer from 0 through 100; it must not be below the lower bound.
tagRanges
Type: readonly TagRange[] | null
Default: []
tagRangesreadonly 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

PropertyDescription
res
resThe processed string.
ranges
rangesThe ranges applied to produce res, or null when the output did not change.
log
logBest-effort completion statistics. timeTakenInMilliseconds is elapsed time, not a deterministic result value.
whatWasDone
whatWasDoneReports which operation actually changed the output.
applicableOpts
applicableOptsReports 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&nbsp;amet",
//   ranges: [[21, 22, "&nbsp;"]],
//   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:

TypeDescription
CountThreshold
Type: CountThreshold
CountThresholdA numeric word or character threshold, or false/null to disable it.
HeadsAndTailsObj
Type: HeadsAndTailsObj
HeadsAndTailsObjOne custom template-marker pair. Each side can be one string or a readonly string array.
IgnoreEntry
Type: IgnoreEntry
IgnoreEntryAn IgnorePreset or custom HeadsAndTailsObj.
IgnorePreset
Type: IgnorePreset
IgnorePresetOne of "all", "hexo", "hugo", "jinja", "liquid", or "nunjucks".
Obj
Type: Obj
ObjA general string-keyed object type retained for compatibility.
Opts
Type: Opts
OptsThe complete options object. The function accepts Partial<Opts>.
Res
Type: Res
ResThe complete return object.
TagRange
Type: TagRange
TagRangeA 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:

TargetInserted text
html
html&nbsp;
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&nbsp;four</p><p>five six seven&nbsp;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&nbsp;{{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>&nbsp;<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.

Permalink to changelogChangelog

Open Changelog
↑ back to top
prev next

Copyright

All rights reserved © Roy Revelt 2026
All our open source packages are under MIT licenceopens in a new tab

Activities

🐛 See a bug? Raise an issueopens in a new tab
💘 Check out the Indiewebopens in a new tab and Libera manifestoopens in a new tab