# Text Inspector

> Exact character, byte, word and line counts; find letters; reveal invisible and look-alike characters.

Category: Text & Data · Page: https://util.caruana.me/tools/text/

## API, MCP and A2A

### Inspect and count text (`text_inspect`)

`GET|POST https://util.caruana.me/api/text/inspect` · MCP tool `text_inspect` · A2A skill `text_inspect` · [reference](https://util.caruana.me/reference/text_inspect/)

Counts text the ways software disagrees about: grapheme clusters (what people see as characters), code points, UTF-16 code units (JavaScript length) and UTF-8 bytes; lines with their line-ending style; words and sentences (Intl.Segmenter); and letters, digits, whitespace and punctuation by Unicode property. With `find`, counts occurrences of a substring or character and returns each position (e.g. `find=r` in `strawberry` is 3). It also reports invisible and deceptive characters with positions: zero-width characters, bidirectional controls (Trojan Source, CVE-2021-42574), unusual spaces such as NBSP, BOM, control characters, Unicode tag characters (hidden-prompt smuggling), and Latin look-alikes in mixed-script words. Use this instead of counting characters yourself.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `input` | string (≤ 20000 chars) | yes | The text, up to 20,000 UTF-16 code units. Sent as-is: no trimming or normalization. |
| `find` | string (≤ 1000 chars) | no | A substring or single character to count and locate. Matched literally (not a regex) and without Unicode normalization. |
| `case_sensitive` | boolean | no | Whether `find` is case-sensitive. When false, Unicode simple case folding is used (`R` matches `r`). |
| `breakdown` | integer (0–2000) | no | Return a per-code-point breakdown (U+XXXX, UTF-8 hex, category, script) of the first N code points. 0 (default) for none; at most 2000. |
| `locale` | string (≤ 64 chars) | no | BCP 47 locale for word and sentence segmentation, e.g. `en`, `th`, `ja`. Default: the runtime default. |

Privacy: The server receives the text (and the search string), analyses it in memory, and returns counts and positions. Your input is not stored or logged; only the per-call metadata row (operation, time, status, sizes) is recorded. For private text use the page, which does the same analysis in your browser.


## User guide

The tool sits in a window that works like a desktop app: menus on the top row and the **full-screen** icon at its right (**Esc** leaves full screen). The set-up options described below are in **Settings → Options…**, an overlay, and the **Settings** menu also lists the common choices directly. To keep the options in view instead, choose **View → Inline config**; the page remembers that choice. **Help → Keyboard shortcuts…** lists the keys.

1. **Paste or type text.** It is analysed as-is: nothing is trimmed or normalized.
2. Read the **Counts** card. _Characters_ are grapheme clusters, what a person would count. The other rows are what programs count:
   - **Code points:** Python's `len()`.
   - **UTF-16 code units:** JavaScript's `.length`, Java's `length()`.
   - **UTF-8 bytes:** what storage and network limits measure.
3. To **count a letter or word**, type it in _Count occurrences of_. Every match is listed with its line, column and index. Tick _Ignore case_ to match `R` and `r` alike.
4. The **banner** turns red if the text contains hidden or deceptive characters, which are listed with their positions:
   - zero-width characters;
   - bidirectional controls;
   - Unicode tag characters;
   - Latin look-alikes from other scripts.
5. Tick **Per-character breakdown** to see each code point's U+ number, UTF-8 bytes, Unicode category and script.

**Why counts differ.** `👍🏽` is one character to a reader, but it is two code points (thumbs up + skin tone), four UTF-16 code units, and eight UTF-8 bytes. `é` can be one code point (U+00E9) or two (`e` + U+0301 combining accent), and both look identical. _Normalization_ tells you whether the text is in NFC, the composed form most systems expect.

**Hidden characters matter.**

- A zero-width space breaks a search, a password or an identifier invisibly.
- Bidirectional overrides can make source code display differently from how it compiles ("Trojan Source").
- Unicode tag characters can hide instructions that language models read but people cannot see.
- A Cyrillic `а` in `pаypal` makes a look-alike domain or username.

## Specification

**Counts.**

| Count            | Definition                                                                                                                                                                                                                 |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `graphemes`      | Extended grapheme clusters, Unicode UAX #29, via `Intl.Segmenter` (granularity `grapheme`)                                                                                                                                 |
| `codePoints`     | Unicode code points. A lone surrogate counts as one                                                                                                                                                                        |
| `utf16CodeUnits` | UTF-16 code units (the JavaScript string length)                                                                                                                                                                           |
| `utf8Bytes`      | Length of `TextEncoder` output. A lone surrogate is encoded as U+FFFD (3 bytes)                                                                                                                                            |
| `words`          | `Intl.Segmenter` word segments with `isWordLike` true. Hyphenated words count as two (`e-mail` → `e`, `mail`); `don't` and `3.14` count as one. Thai, Japanese and Chinese use the runtime's dictionary-based segmentation |
| `sentences`      | `Intl.Segmenter` sentence segments that contain a non-whitespace character                                                                                                                                                 |
| `lines`          | 0 for empty text. Otherwise line breaks + 1, minus 1 if the text ends with a line break. `nonEmptyLines` excludes empty lines                                                                                              |

**Line breaks** are LF, CRLF (counted once) and CR. `lineEndings.style` is `none`, `LF`, `CRLF`, `CR`, or `mixed` when more than one kind occurs. U+0085, U+2028 and U+2029 are not counted as line breaks; they are reported as `line-separator` findings.

**Character classes** count code points by Unicode property: `\p{L}`, `\p{Lu}`, `\p{Ll}`, `\p{M}`, `\p{Nd}`, `\p{N}`, `\p{P}`, `\p{S}`, `\p{White_Space}`, `\p{Zs}`, `\p{Cc}` and `\p{Cf}`. Titlecase letters such as `ǅ` count as letters, but as neither upper nor lower case. `emoji` counts grapheme clusters containing an `\p{Extended_Pictographic}` code point.

**Occurrences** (`find`).

- **Matching:** literal (special characters have no meaning) and on code points.
- **No normalization:** precomposed `é` does not match `e` + U+0301.
- **Case:** case-sensitive by default. With `case_sensitive=false`, Unicode _simple_ case folding is used: `K` (Kelvin sign) matches `k`, but `ß` does not match `ss` and `İ` does not match `i`.
- **`count`:** non-overlapping matches from left to right (`aa` in `aaaa` is 2).
- **`overlappingCount`:** a match may start at every code point (`aa` in `aaaa` is 3).
- **Positions:** each match has `utf16Index`, `codePointIndex` and `graphemeIndex` (0-based), and `line` and `column` (1-based, column in code points). `splitsGrapheme` is true when a match starts or ends inside a grapheme cluster, such as `e` inside `e` + U+0301.
- **Limit:** at most 1,000 positions are listed; `count` is always exact.

**Hidden and deceptive characters.**

| Kind                 | Code points                                                                                                                                                |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bom`                | U+FEFF at the start                                                                                                                                        |
| `zero-width`         | U+200B, U+200C, U+200D, U+2060, U+180E, and U+FEFF elsewhere                                                                                               |
| `bidi-control`       | U+202A–U+202E, U+2066–U+2069 (embeddings, overrides, isolates; CVE-2021-42574)                                                                             |
| `bidi-mark`          | U+200E, U+200F, U+061C                                                                                                                                     |
| `unusual-space`      | Every `\p{Zs}` except U+0020, e.g. U+00A0 NBSP, U+202F, U+2007, U+3000                                                                                     |
| `control`            | C0 and C1 controls and DEL, except tab, LF and CR                                                                                                          |
| `line-separator`     | U+0085, U+2028, U+2029                                                                                                                                     |
| `tag`                | U+E0000–U+E007F                                                                                                                                            |
| `variation-selector` | U+FE00–U+FE0D, U+E0100–U+E01EF                                                                                                                             |
| `invisible-format`   | Other `\p{Cf}` (e.g. U+00AD soft hyphen, U+2061–U+2064) and U+034F                                                                                         |
| `invisible-letter`   | Hangul fillers U+115F, U+1160, U+3164, U+FFA0                                                                                                              |
| `replacement`        | U+FFFD (a sign of an earlier decoding error)                                                                                                               |
| `lone-surrogate`     | An unpaired UTF-16 surrogate                                                                                                                               |
| `private-use`        | `\p{Co}`                                                                                                                                                   |
| `confusable`         | A Latin look-alike from Cyrillic, Greek or Armenian, inside a word that mixes Latin with those scripts; or a fullwidth ASCII form (U+FF01–U+FF5E) anywhere |

**Exempted as normal emoji syntax:**

- ZWJ inside an emoji grapheme cluster (👨‍👩‍👧);
- tag characters in an emoji tag sequence (U+1F3F4, 1–7 tags, U+E007F, such as the flag of England);
- variation selectors 15 and 16 (U+FE0E, U+FE0F).

**Other detection rules.**

- **Look-alikes:** the built-in list covers common look-alikes of ASCII letters. It is a subset of Unicode's `confusables.txt`, not the full UTS #39 algorithm.
- **Unbalanced bidi controls:** `bidiUnterminatedLines` lists lines where an embedding or override is not closed by U+202C, or an isolate by U+2069, before the line ends. This is the Trojan Source pattern.
- **Limit:** at most 1,000 findings are listed, in text order; `total` and `byKind` are exact.

**Per-code-point breakdown** (`breakdown` = N, at most 2,000 through the API, 500 on the page). For each code point:

- its index and `U+XXXX`;
- UTF-8 and UTF-16 bytes in hex;
- its General_Category, tested against all 30 values;
- its Script, checked against 49 widely used scripts plus `Unknown` for unassigned code points (null if it is in none of them);
- a name for control, space and invisible characters (null for others: the full Unicode name list is not bundled).

**Locale.** `locale` must be a valid BCP 47 tag. It affects word and sentence segmentation; grapheme segmentation is locale-independent in practice.

**Versions.** Segmentation and properties come from the ICU and Unicode data in the runtime: V8 in Cloudflare Workers for the API, your browser for the page. Counts for newly added emoji can differ between runtimes on different Unicode versions.

**API limits.** `input` is at most 20,000 UTF-16 code units (about 6 ms of server CPU, inside the Workers per-request budget); split longer text and add the counts. `find` is at most 1,000 code units.
