npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

html-string-splitter

v3.0.0

Published

Split, truncate, chunk and stream HTML by character, word, sentence, line, byte, token or tag while preserving valid HTML structure.

Downloads

229

Readme

html-string-splitter

Split, truncate, chunk and stream HTML by character, word, sentence, line, byte, token or tag — while preserving valid HTML structure.

npm version license

Why?

Truncating HTML is hard. String.slice() breaks tags and produces invalid HTML. This library handles all of that:

// Broken HTML
'<p>Hello <strong>world</strong></p>'.slice(0, 18)
// '<p>Hello <strong>w'  — broken tag!

// Valid HTML
clip('<p>Hello <strong>world</strong></p>', { keep: 7, by: 'c' })
// '<p>Hello <strong>w...</strong></p>'  — properly closed

Zero dependencies. TypeScript. ESM + CJS. Emoji-safe. Entity-aware. Streaming.

Counts text the way a browser renders it: un<b>believ</b>able is one word, a sentence can span tags, and escaped text stays escaped.

Installation

npm install html-string-splitter

Common Use Cases

Blog post preview

import { clip } from 'html-string-splitter';

clip(articleHtml, { keep: 200, by: 'c' });
// First 200 characters with "..." and valid HTML

clip(articleHtml, { keep: 200, by: 'c', suffix: '<a href="/post">Read more</a>' });
// With a "Read More" link

Word-based truncation

clip('<p>Hello beautiful world</p>', { keep: 2, by: 'w' });
// '<p>Hello beautiful...</p>'

Conditional "Read More"

import { split } from 'html-string-splitter';

const result = split(html, { keep: 200, by: 'c' });
if (result.truncated) {
  showReadMoreButton();
}
// result = { html, truncated, total, kept }

Count words or characters

import { count } from 'html-string-splitter';

count('<p>Hello world</p>', { by: 'w' });  // 2
count('<p>A &amp; B</p>');                 // 5 (entity = 1 char)

Extract plain text

import { text } from 'html-string-splitter';

text('<p>Hello <strong>world</strong></p>');  // 'Hello world'

Paginate an article

import { chunk } from 'html-string-splitter';

const pages = chunk(articleHtml, { size: 100, by: 'w' });
// pages[0] = first 100 words (valid HTML)
// pages[1] = next 100 words (valid HTML)

Fit an LLM token budget or a DB column

clip(html, { keep: 512, measure: text => encode(text).length });  // tokens
clip(html, { keep: 160, by: 'byte' });                            // UTF-8 bytes
clip(html, { maxOutputLength: 2000 });                            // final string, markup included

Chunk for RAG

chunk(html, { size: 300, by: 'w', breakAt: 'block', overlap: 30, meta: true });
// [{ html, text, index, start, end, headings: ['Guide', 'Install'], anchor: 'install' }, ...]

Natural excerpts and search snippets

import { excerpt, snippet } from 'html-string-splitter';

excerpt(html, { min: 120, max: 200 });     // ends at a paragraph or sentence when possible
snippet(html, 'install', { radius: 8 });   // 8 words each side, match in <mark>

Stream an LLM response

import { splitStream } from 'html-string-splitter';

response.body
  .pipeThrough(new TextDecoderStream())
  .pipeThrough(splitStream({ keep: 200, by: 'w' }));  // stops upstream at the limit

Parse once, run many operations

import { parse } from 'html-string-splitter';

const doc = parse(html);
doc.count({ by: 'w' });
doc.clip({ keep: 50, by: 'w' });
doc.summary().readingTime;

Table of contents and sections

import { outline, section } from 'html-string-splitter';

outline(html);              // [{ level: 2, text: 'Install', id: 'install', slug: 'install', words: 340, children: [...] }]
section(html, 'install');   // balanced HTML of that section only

Apply NLP / LLM results to HTML

import { annotate, text } from 'html-string-splitter';

const entities = findEntities(text(html));  // [{ start, end, className: 'person' }, ...]
annotate(html, entities);                   // ranges wrapped in <mark>, HTML kept valid

Fix broken or partial HTML

import { repair } from 'html-string-splitter';

repair('<p>Hello <b>world</p><div>');  // '<p>Hello <b>world</b></p><div></div>'

Split by HTML tag

// Keep first 3 paragraphs
clip(html, { keep: 3, by: 'p' });

// Keep first 5 list items
clip(html, { keep: 5, by: 'li' });

// Count all images
count(html, { by: 'img' });

Core API

| Function | Returns | Description | |----------|---------|-------------| | clip(html, options) | string | Truncate HTML, return string | | split(html, options) | SplitResult | Truncate with metadata | | count(html, options?) | number | Count units | | text(html, options?) | string | Extract plain text | | splitAt(html, options) | [string, string] | Split into two parts | | slice(html, options?) | string | Extract a range (like String.slice) | | chunk(html, options) | string[] | ChunkResult[] | Split into parts, by size and structure | | excerpt(html, options) | SplitResult | Truncate within a range at a natural boundary | | snippet(html, query) | string | Search-result excerpt around matches |

Advanced API

| Function | Returns | Description | |----------|---------|-------------| | outline(html) | OutlineItem[] | Heading tree with ids, slugs and word counts | | section(html, target) | string | One section's HTML by id, slug, text or index | | summary(html) | SummaryResult | Full statistics in one pass | | pick(html, options) | PickResult[] | Extract pieces by text or tag | | highlight(html, query) | string | Wrap text matches in a tag | | annotate(html, ranges) | string | Wrap plain-text offset ranges in markup | | wrap(html, options) | string | Wrap content at intervals (by chars, words, or tags) | | repair(html) | string | Balance broken or partial HTML | | parse(html) | HtmlDocument | Parse once, call any method without re-tokenizing | | createSplitter(options) | StreamSplitter | Truncate HTML incrementally | | splitStream(options) | TransformStream | Web Streams truncation | | tokenize(html) | Token[] | Low-level HTML tokenizer |


Split Units

The by parameter accepts:

| Unit | Alias | Example | |------|-------|---------| | 'character' | 'c' | clip(html, { keep: 100, by: 'c' }) | | 'word' | 'w' | clip(html, { keep: 20, by: 'w' }) | | 'sentence' | 's' | clip(html, { keep: 3, by: 's' }) | | 'line' | 'l' | clip(html, { keep: 5, by: 'l' }) | | 'byte' | — | clip(html, { keep: 160, by: 'byte' }) | | Any tag name | — | clip(html, { keep: 3, by: 'p' }) |

Any unit can be weighted with measure, and locale enables dictionary word splitting for Chinese, Japanese, Thai and similar languages.


Options

Basic

clip(html, {
  keep: 10,            // units to keep (required)
  by: 'c',             // split unit (default: 'c')
  ellipsis: '...',     // appended at truncation (default: '...')
  suffix: '<a>More</a>', // HTML after ellipsis
  from: 'end',         // 'start' (default) or 'end'
  stripTags: true,     // return plain text
});

Advanced

split(html, {
  keep: 100,
  by: 'c',
  preserveWords: true,         // don't cut mid-word (true | number | 'trim')
  smartEllipsis: true,         // skip "..." at block boundaries
  stripComments: true,         // remove HTML comments
  exclude: ['figcaption'],     // remove elements entirely
  selectiveTags: ['span'],     // only strip these tags (with stripTags)
  imageWeight: 5,              // character cost for <img>, <video>, etc.
  wordPattern: /[\p{Han}]|\w+/gu, // custom word boundaries (CJK)
  locale: 'ja',                // Intl.Segmenter word splitting
  measure: countTokens,        // weight per unit (e.g. LLM tokens)
  maxOutputLength: 500,        // cap the final string, markup included
  output: 'both',              // return html + text in one pass
});

Chunk

chunk(html, {
  size: 100,           // units per chunk (required)
  by: 'w',             // split unit
  overlap: 20,         // shared units between chunks (for RAG/LLM)
  breakAt: 'block',    // end at 'block' | 'sentence' | 'word' boundaries
  meta: true,          // return { html, text, start, end, headings }
});

See Options Reference for detailed explanations with examples.


CommonJS

const { clip, split, count } = require('html-string-splitter');

TypeScript

import type { SplitOptions, SplitResult, ChunkOptions, ChunkResult, HtmlInput, StreamOptions } from 'html-string-splitter';

Documentation

Guides:

Advanced:

Reference:


License

MIT © HrDelwar