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
Maintainers
Readme
html-string-splitter
Split, truncate, chunk and stream HTML by character, word, sentence, line, byte, token or tag — while preserving valid HTML structure.
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 closedZero 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-splitterCommon 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" linkWord-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 & 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 includedChunk 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 limitParse 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 onlyApply 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 validFix 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:
- Split & Clip — Truncation, splitAt, slice
- Chunk — Pagination and RAG chunking with overlap, boundaries and metadata
- Excerpt & Snippet — Natural-boundary excerpts and search snippets
- Streaming — Truncate HTML as it arrives
- Outline & Section — Heading tree and section extraction
- Count & Summary — Counting and statistics
- Text Extraction — Plain text and output modes
Advanced:
- Pick, Highlight & Annotate — Extract pieces, highlight matches, apply text ranges
- Wrap — Wrap by chars, words, or tags
- Repair — Balance broken or streamed HTML
- Parsed Documents — Parse once, reuse everywhere
- Tokenize — Low-level and incremental tokenizer API
Reference:
- Options Reference — All options in detail
- Migration — Upgrading from v2 or v1
