# Why linegauge

> linegauge against string-width, wrap-ansi, strip-ansi and slice-ansi, one capability per row, every cell linked to the test, grade or source that proves it.

Source: https://linegauge.interlace.tools/docs/why-linegauge

string-width, wrap-ansi, strip-ansi and slice-ansi are good at what they do, and linegauge
matches them where they are right: each of its four drop-in paths is graded by the incumbent's
own test suite, 100% on all four. What it adds is what four separate packages cannot share —
one package with no dependencies, the pieces that live in yet more packages (truncation with
the ellipsis inside the budget, the widest of many lines), and a way to tell it that your
terminal disagrees with Unicode.

The table below is the whole comparison. Every mark links to its evidence: a test in this
repository for ours, and for theirs the source file of the exact version compat-oracle grades,
or that package's own test suite. `scripts/capabilities-lock.test.ts` fails the build when a
cited test no longer contains the title it is cited for, when a source no longer contains the
line it is quoted for, or when a source we say lacks something has gained it.

✓ yes · ◐ partial (what is missing is said) · ✗ no · — does not apply. Every cell links to its evidence: our test or grade, or the incumbent’s source at the version compat-oracle grades.

### Measuring text

| Capability | **linegauge** | string-width | wrap-ansi | strip-ansi | slice-ansi |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Unicode 17 East Asian Width** — A code point Unicode 17 made wide, such as U+18D80, measures two columns, so a layout built on the width does not drift on new text. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/src/width.test.ts) | [✓ through get-east-asian-width 1.7.0](https://cdn.jsdelivr.net/npm/string-width@8.3.0/index.js) | [✓ measures through string-width](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/index.js) | [— removes escape sequences; measures nothing](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/index.js) | [✓ through is-fullwidth-code-point and get-east-asian-width](https://cdn.jsdelivr.net/npm/is-fullwidth-code-point@5.1.0/index.js) |
| **A ZWJ emoji sequence is one two-column cluster** — Text is measured by grapheme cluster over `Intl.Segmenter`, so a family emoji built from four people and three joiners is two columns, not eight. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/src/width.test.ts) | [✓](https://cdn.jsdelivr.net/npm/string-width@8.3.0/index.js) | [✓](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/index.js) | [— removes escape sequences; measures nothing](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/index.js) | [✓](https://cdn.jsdelivr.net/npm/slice-ansi@9.0.1/tokenize-ansi.js) |

### Cutting styled text

| Capability | **linegauge** | string-width | wrap-ansi | strip-ansi | slice-ansi |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **A slice closes the styles it opened** — A column range re-emits the styles active at its start and closes them at its end, so the fragment neither loses its colour nor leaks it into what follows. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/src/slice.test.ts) | [— measures; its one export is the width, and it does not cut](https://cdn.jsdelivr.net/npm/string-width@8.3.0/index.js) | [— folds text into rows; it takes no column range](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/index.js) | [— removes escape sequences; it does not cut](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/index.js) | [✓](https://cdn.jsdelivr.net/npm/slice-ansi@9.0.1/index.js) |
| **Truncation with the ellipsis inside the budget** — `truncate(text, 10)` occupies ten columns or fewer with its ellipsis counted, cutting from the end, the start or the middle, which is what a table column depends on. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/src/truncate.test.ts) | [✗ measures only; truncation is cli-truncate, another package](https://cdn.jsdelivr.net/npm/string-width@8.3.0/index.js) | [✗ folds text into rows; it never shortens it](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/index.js) | [— removes escape sequences; it does not cut](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/index.js) | [◐ cuts a column range, but adds no ellipsis and keeps no room for one](https://cdn.jsdelivr.net/npm/slice-ansi@9.0.1/index.js) |
| **The widest of many lines, from any iterable** — `widest(lines)` measures an array, a `Set` or a generator in one pass, so the lines never have to be spread into an argument list. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/src/truncate.test.ts) | [✗ measures one string; the widest of many is widest-line, another package](https://cdn.jsdelivr.net/npm/string-width@8.3.0/index.js) | [✗ folds one string](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/index.js) | [— removes escape sequences; measures nothing](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/index.js) | [✗ cuts one string](https://cdn.jsdelivr.net/npm/slice-ansi@9.0.1/index.js) |

### Terminals that disagree with Unicode

| Capability | **linegauge** | string-width | wrap-ansi | strip-ansi | slice-ansi |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Width overrides through a plugin, with provenance** — A plugin registers `{ ranges, columns, why }` for a font that draws Private Use icons two columns wide, and a width table without a `why` is refused. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/src/plugin.test.ts) | [✗ the Unicode table is fixed; only ambiguous width is an option](https://cdn.jsdelivr.net/npm/string-width@8.3.0/index.js) | [✗ measures through string-width, which takes no overrides](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/index.js) | [— removes escape sequences; measures nothing](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/index.js) | [✗ the width of each code point is fixed](https://cdn.jsdelivr.net/npm/slice-ansi@9.0.1/index.js) |
| **A checker for width plugins before they ship** — `npx linegauge check ./widths.mjs` validates a plugin against the family schema and exits 1 with a code and a fix when it is refused. | [✓](https://github.com/ofri-peretz/burgee/blob/main/scripts/plugin-check-lock.test.ts) | [— takes no plugins, so has nothing to check](https://cdn.jsdelivr.net/npm/string-width@8.3.0/package.json) | [— takes no plugins, so has nothing to check](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/package.json) | [— takes no plugins, so has nothing to check](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/package.json) | [— takes no plugins, so has nothing to check](https://cdn.jsdelivr.net/npm/slice-ansi@9.0.1/package.json) |

### Weight

| Capability | **linegauge** | string-width | wrap-ansi | strip-ansi | slice-ansi |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **No runtime dependencies** — Measuring, wrapping, slicing, stripping and truncating install as one package that depends on nothing. | [✓](https://github.com/ofri-peretz/burgee/blob/main/scripts/package-shape-lock.test.ts) | [✗ get-east-asian-width and strip-ansi](https://cdn.jsdelivr.net/npm/string-width@8.3.0/package.json) | [✗ ansi-styles and string-width](https://cdn.jsdelivr.net/npm/wrap-ansi@10.0.2/package.json) | [✗ ansi-regex](https://cdn.jsdelivr.net/npm/strip-ansi@7.2.0/package.json) | [✗ ansi-styles and is-fullwidth-code-point](https://cdn.jsdelivr.net/npm/slice-ansi@9.0.1/package.json) |

### Compatibility

| Capability | **linegauge** | string-width | wrap-ansi | strip-ansi | slice-ansi |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Passes string-width's own test suite** — linegauge's default export is graded by string-width 8.3.0's own tests, unedited, so changing the import keeps string-width's answers. | [✓ 233 / 233 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/string-width.json) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/string-width/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/wrap-ansi/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/strip-ansi/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/slice-ansi/test.js) |
| **Passes wrap-ansi's own test suite** — `linegauge/wrap` is graded by wrap-ansi 10.0.2's own tests, unedited. | [✓ 85 / 85 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/wrap-ansi.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/string-width/test.js) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/wrap-ansi/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/strip-ansi/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/slice-ansi/test.js) |
| **Passes strip-ansi's own test suite** — `linegauge/strip` is graded by strip-ansi 7.2.0's own tests, unedited. | [✓ 8 / 8 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/strip-ansi.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/string-width/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/wrap-ansi/test.js) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/strip-ansi/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/slice-ansi/test.js) |
| **Passes slice-ansi's own test suite** — `linegauge/slice` is graded by slice-ansi 9.0.1's own tests, unedited. | [✓ 104 / 104 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/slice-ansi.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/string-width/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/wrap-ansi/test.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/strip-ansi/test.js) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/slice-ansi/test.js) |

## Reading it

- **Parity rows are here on purpose.** All three measuring incumbents are at Unicode 17 and
  measure a ZWJ sequence as one cluster, and slice-ansi 9 closes the styles it opens. A reader
  would otherwise have to go and check; the cells say they match.
- **— does not apply** is not a soft ✗. strip-ansi removes escape sequences and measures
  nothing, so a measuring row does not apply to it; the cell says why.
- **Truncation and `widest` are not in the four incumbents** — they are cli-truncate and
  widest-line, two more packages. The row shows what you would install on top of the four to
  get them.

## What is not in the table

A row goes in only when every cell of it can be proved. These were left out:

- **`OSC 8` hyperlinks.** `wrap` and `slice` carry a hyperlink across a break and close it at
  a cut, and their differential tests include hyperlink cases — but wrap-ansi and slice-ansi do
  too, and no test of ours names the case on its own.
- **Linear time on adversarial input.** There is no regression test for it in this repository
  yet, so there is no row.
- **Ambiguous width.** `width(text, { ambiguousIsNarrow: false })` counts ambiguous characters
  wide, exactly as string-width's option does. `wrap` and `slice` do not take the option, and
  neither do wrap-ansi and slice-ansi, so the row would say nothing either way
  ([Measuring](/docs/guides/measuring#options)).
- **Weight in bytes.** The per-entry figures are asserted by
  [`weight.test.ts`](https://github.com/ofri-peretz/burgee/blob/main/packages/linegauge/src/weight.test.ts)
  and published on [Benchmarks](https://burgee.interlace.tools/docs/benchmarks), measured the
  same way on both sides. They are a number, not a yes-or-no capability.
