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

grapheme-conformance

v0.1.2

Published

Score any JavaScript grapheme segmenter against Unicode's official GraphemeBreakTest.txt. Find out which string splitters are wrong, and about what.

Readme

grapheme-conformance

Your string splitter is probably wrong about Hindi.

Unicode publishes GraphemeBreakTest.txt: a machine-readable answer key that says exactly where a string may be split into user-perceived characters. This package scores any JavaScript grapheme segmenter against it, tells you which cases it got wrong, and gives you an exit code you can put in CI.

It does not ship another segmenter.

The finding

क्षि — Devanagari, four code points, one character a reader sees. Split it with a popular library and you get two clusters. grapheme-splitter and graphemer both break it; runes2 breaks it into three.

| input | splits wrongly in | |---|---| | क्षि Devanagari (0915 094D 0937 093F) | grapheme-splitter (2), graphemer (2), runes2 (3) | | e + combining acute (0065 0301) | runes2 (2) | | Hangul (1112 1161 11AB) | runes2 (2) | | pirate flag (1F3F4 200D 2620 FE0F) | grapheme-splitter (2) |

Family emoji, skin-tone modifiers, regional-indicator flags, tag-sequence flags, ZWJ professions and keycaps pass in every library tested. Emoji is not the story; Indic script is. All seven of graphemer's Unicode 15.1 failures are rule GB9c, added in that release for Indic conjunct clusters.

Scoreboard

See SCOREBOARD.md, regenerated by npm run scoreboard. Cases passed:

| implementation | U15.1.0 (1187) | U16.0.0 (1093) | |---|---|---| | Intl.Segmenter (ICU, Node 22) | 1186 | 1092 | | [email protected] | 1186 | 1092 | | [email protected] | 1180 | 1086 | | [email protected] | 1175 | 1081 | | [email protected] | 730 | 695 |

Intl.Segmenter's single failure is the input 2701 200D 2701, a known ICU deviation — and one that arrived with a later ICU rather than an earlier one. Node 18.20.8 ships an ICU without it and passes every case (1187 and 1093); Node 20 and 22 split that input and score 1186 and 1092.

Install

npm install --save-dev grapheme-conformance

Zero runtime dependencies. Dual ESM/CJS. Node 18+.

CLI

npx grapheme-conformance --module unicode-segmenter/grapheme \
  --export splitGraphemes --version 16.0.0
unicode-segmenter/grapheme (splitGraphemes)  GraphemeBreakTest 16.0.0
  passed  1092/1093  99.91%
  failed  1

  line  input                             want  got   rule
  1105  2701 200D 2701                    1     2     -

| flag | default | meaning | |---|---|---| | --module | required | module to load: a bare name, or a path relative to cwd | | --export | default | the export to score | | --version | 16.0.0 | vendored vectors: 15.0.0, 15.1.0, 16.0.0, 17.0.0 | | --min | 1.0 | minimum pass rate before exiting non-zero | | --limit | 10 | failing cases to print |

Exits 0 when the pass rate is at least --min, 1 when below, 2 on a usage or module-loading error. The exit code is the point: drop it into your own CI. Exports returning a generator or any other iterable are accepted.

API

import { parseBreakTest, score, vectors } from 'grapheme-conformance';

const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const report = score(
  (s) => [...segmenter.segment(s)].map((part) => part.segment),
  vectors['16.0.0'],
);

report.passed; // 1092
report.total; // 1093
report.rate; // 0.999085...
report.failures[0].inputHex; // '2701 200D 2701'
type Segmenter = (input: string) => string[];

interface Vector { input: string; expected: string[]; line: number }

function parseBreakTest(source: string): Vector[];
function score(segmenter: Segmenter, vectors: Vector[]): Report;

interface Report { passed: number; total: number; rate: number; failures: Failure[] }

interface Failure {
  line: number;        // 1-based line number in the source .txt
  input: string;
  inputHex: string;    // 'space-separated UPPERCASE hex code points'
  expected: string[];
  actual: string[];
  rule: string | null; // inferred rule id, or null
}

const vectors: Record<string, Vector[]>; // keyed by Unicode version

parseBreakTest takes a string and never touches the filesystem. score never throws: a segmenter that throws on an input yields a failure with actual: []. failures is ordered by line, and repeated calls return deeply equal reports.

Vectors

15.0.0, 15.1.0, 16.0.0 and 17.0.0 are vendored under vectors/ and committed, fetched once from unicode-org/unicodetools. Nothing is fetched at install, build or test time; the whole suite runs offline.

Rule inference

Each failure carries a best-effort rule: GB9c (Indic conjunct), GB11 (pictographic ZWJ sequence), GB12/GB13 (regional indicators), GB6/GB7/GB8 (Hangul jamo), or null.

This is a convenience for reading the scoreboard, not a correctness claim. It inspects the input for characteristic code points; it does not implement UAX #29 and does not prove which rule an implementation actually got wrong.

Intl.Segmenter and the host ICU

Intl.Segmenter is scored against whatever ICU the host Node ships, so its row moves with the runtime, and not monotonically: Node 18.20.8 scores higher than Node 22 because the 2701 200D 2701 deviation is a newer ICU behaviour. The pure-JS libraries are pinned to exact versions and score identically everywhere.

Verifying this build

See VERIFY.md: one command, ten integers to compare.

Scope

Grapheme clusters only. Word and sentence breaking are out of scope — unicode-segmenter does not implement them, which leaves too few implementations to be worth a scoreboard.

License

MIT