linegauge

Changelog

Every release of linegauge, newest first, from its CHANGELOG.md — what changed and the pull request it came from.

1.0.0

Major Changes

  • #685 6ef8227 Thanks @ofri-peretz! - linegauge 1.0.0. No API changes from 0.5: this release makes a promise. Every published entry point is now under semver and can change incompatibly only in a new major: linegauge, linegauge/wrap, linegauge/slice, linegauge/truncate, linegauge/widest, linegauge/strip, linegauge/plugin, the schema.json plugin contract and the linegauge bin. Each drop-in path is graded at 100% by its incumbent's own test suite, vendored at the release tag and run unmodified: string-width 8.3.0 by 233 of 233 cases (linegauge), wrap-ansi 10.0.2 by 85 of 85 (linegauge/wrap), strip-ansi 7.2.0 by 8 of 8 (linegauge/strip) and slice-ansi 9.0.1 by 104 of 104 (linegauge/slice). Grading the newest string-width and wrap-ansi before release found two defects, fixed here: width() could hang on a run of combining joiners, and wrap() normalized escape sequences. Out of scope: truncate and widest are linegauge's own API and are not graded against an incumbent's suite, and earlier majors of the four incumbents are not claimed.

Minor Changes

  • #655 4319563 Thanks @ofri-peretz! - linegauge: linegauge/slice is graded against slice-ansi 9.0.1's own suite (104 cases, up from 15 at 7.1.2) and passes all 104; East Asian Width is Unicode 17.

    • slice rounds inward at a wide character, as slice-ansi does: a cluster the range only half covers is left out instead of returned whole. Before, slice('あいう', 0, 3) returned あい (four columns), and truncate('あいう', 4) returned あい… (five columns, over its budget). Both now stay inside the columns asked for.
    • slice reads the escapes slice-ansi 9 reads: OSC 8 links ended by ESC \ or U+009C, the C1 OSC introducer, DCS, SOS, PM and APC strings, a lone ST, and truncated or malformed CSI. A malformed CSI ends at the first byte that cannot belong to it, so the text after it is kept.
    • An escape inside a grapheme cluster (e, a style, then a combining mark) no longer splits the cluster.
    • Hyperlinks don't nest: a second open replaces the first, and the first is closed with its own introducer and terminator. A link around no visible text is removed. A close just past the end of the range is kept as written, and an opener with no text after it is dropped.
    • Where a cut lands, slice gives every cluster at least one position (so CRLF and zero-width characters can start or end a range) and a lone regional indicator two, as slice-ansi does. width is unchanged and still answers as string-width does.
    • The Wide/Fullwidth table is generated from get-east-asian-width 1.7.0 (Unicode 17), like the Ambiguous table next to it. The hand-written table was missing 1,147 code points, which width measured as one column where string-width measures two.

    burgee: burgee migrate serves slice-ansi 9 (its compatibility row moved from 7.1.2 to 9.0.1). slice-ansi 7 is still graded, at 14 of 15, but no longer claimed, so a project on slice-ansi 7 is left on it.

Patch Changes

  • #717 7c77cd2 Thanks @ofri-peretz! - Code no test could reach is gone, and nothing a caller can observe changes.

    • slice(): the fallbacks for a character or code point read at an index that is always inside the string, and the check for a visible token the segmenter could not have skipped, are removed. The hyperlink bookkeeping no longer tracks "no position yet" before the slice starts — nothing has been emitted then, so taking an empty link back out removes nothing — and an empty link at the cut is taken out once, at the end, rather than twice.
    • The SGR reader shared by slice, wrap and truncate drops the same kind of fallback, and a bounds check that a malformed-colour check after it already covered.
    • linegauge check: an unused helper and an unreachable ? in the report are removed.

    The published entries that cut styled text are a little lighter; linegauge's unpacked size goes from 102,495 to 102,004 bytes.

  • #685 6ef8227 Thanks @ofri-peretz! - Security: a string of combining grapheme joiners could hang width(). Deciding whether a cluster occupies no column used the regex ^(?:DI|Control|Format|Mn|Me|Surrogate)+$, and U+034F COMBINING GRAPHEME JOINER belongs to two of those classes, so a run of them followed by one visible character backtracked exponentially: 26 joiners took 2.4 seconds and 1,000 did not finish in ten minutes. Any caller measuring untrusted text, and everything that measures through width() (wrap, slice, truncate, widest, lineCount), could be stalled by a few dozen invisible characters. The check is now a loop over code points, linear in the input: 1,000 joiners measure in under a millisecond and 3,000,000 in about 200 ms, with the answer unchanged on every input the regex finished on. Found by string-width 8.3.0's suite, which added these cases.

  • #685 6ef8227 Thanks @ofri-peretz! - Fixed: wrap() normalized the whole string to NFC, escape sequences included. A combining mark right after a sequence composed with the sequence's last character, so ESC[31m followed by U+0301 became ESC[31ḿ. That is no longer an SGR: the colour was lost and its bytes were wrapped as visible text. OSC payloads such as window titles and hyperlink targets were rewritten as well. Only the text between sequences is normalized now, as wrap-ansi 10.0.2 does. flagstaff/log-update and flagstaff/boxen wrap through it.

  • #674 e9f45d8 Thanks @ofri-peretz! - README: family header, badges, install, migrating, the family table.

    Every package README now opens the same way — lockup, tagline, one badge row in one order (npm version, downloads, Quality Gate, the package's own coverage, OpenSSF Scorecard, unpacked size, dependencies, types, Node, licence, npm provenance), a row of compatibility badges read from the graded baseline — and carries the same sections in the same order: Install for npm, pnpm, yarn and bun, Quick start, Migrating as a before/after diff, Compatibility, Benchmarks, For agents, API, and a generated table of the nine packages. Links are absolute, so they work on npm as well as GitHub.

0.5.4

Patch Changes

  • #604 0e7b1e8 Thanks @ofri-peretz! - Each README links to its migration guides under the docs link: "Migrating from: chalk", "ora · log-update · boxen · cli-table3", and so on. That puts a path from the npm page to the guide for the library you are replacing. No code changes.

0.5.3

Patch Changes

  • #588 073037a Thanks @ofri-peretz! - READMEs and package descriptions now match what each drop-in path is graded at. bellpull names bellpull/node-which as the drop-in for npm which (5 / 5) and no longer lists execa as a drop-in. paratext's ansi-escapes row is 4 / 4, with the CSI half implemented. seniority's rc row is 1 / 1, dotenv's config() defaults to process.env, and lilconfig is 77 / 77. linegauge documents ambiguousIsNarrow and strip as shipped. No code changes.

0.5.2

Patch Changes

  • #474 1955419 Thanks @ofri-peretz! - burgee plugins can hook two more stages. parse runs before the command is resolved: it receives argv and may return a replacement, which is how an alias plugin maps d to deploy. shutdown runs once as the program exits, whether the command succeeded or failed. The family schema.json shipped in every package now describes both stages.

  • #519 77ff1cb Thanks @ofri-peretz! - The drop-ins now export their incumbents' type names, so a TypeScript program migrates by its import alone: roundel/chalk gains chalk's Color, ForegroundColor, BackgroundColor, Modifiers and Options; flagstaff/ora gains Spinner, PrefixTextGenerator and SuffixTextGenerator; flagstaff/boxen gains Options, CustomBorderStyle and Boxes; flagstaff/log-update, linegauge, linegauge/wrap and closeout/exit-hook gain Options; burgee/yargs/parser gains Arguments, Options and Configuration. Types only — no runtime bytes.

0.5.1

Patch Changes

  • #508 1aae1e2 Thanks @ofri-peretz! - <package> --help and --version answer instead of crashing. The bin took its first argument as the plugin file to import, so roundel --help failed with Cannot find module '…/--help' and exit 1. -h/--help now print usage and exit 0, -V/--version print the version and exit 0, and any other flag where the plugin file belongs is a usage error, exit 2.

0.5.0

Minor Changes

  • #507 b8e97dc Thanks @ofri-peretz! - Runs on Node 20 and 22, not just 24+: engines.node is now ^20.19.0 || >=22.13.0. Those are the first releases where require(esm) loads without a warning, so the CommonJS require() path keeps working. Every package's test suite runs on exactly 20.19.0 and 22.13.0, on Linux, macOS and Windows. caique's prompts no longer call Promise.withResolvers, which Node 20 doesn't have.

Patch Changes

  • #505 9800b43 Thanks @ofri-peretz! - Docs: the Benchmarks section's weight ceiling is re-measured against a fresh install of each incumbent's latest release (cosmiconfig 10.0.1, slice-ansi 9.0.1, which 7.0.0, dotenv 18.0.3, …) instead of the copies hoisted in this workspace, and names incumbents that were measured but left out of the ceiling as exactly that.

0.4.4

Patch Changes

  • #494 f7f6d4b Thanks @ofri-peretz! - Each package's homepage and README docs link now point at its own documentation site, https://<package>.interlace.tools, instead of a page on burgee's site. The old burgee.interlace.tools/docs/packages/<package> URLs answer with a 301 to the new host, so nothing already linked breaks. closeout's README override example also resolves to the current release again (npm:closeout@^0.4; the 0.4.0 release left it at ^0.3).

0.4.3

Patch Changes

  • #480 2dc573f Thanks @ofri-peretz! - README corrections: paratext shows terminal-link at its measured 8 / 10 (was the stale 0 / 10 floor); linegauge's and closeout's npm: override examples resolve to the current release instead of 0.2 / 0.1.

0.4.2

Patch Changes

  • #465 acf98f3 Thanks @ofri-peretz! - Each README now opens with the incumbent it replaces and the agent surface it serves (--json, an agent event, or a static projection), so npm shows both above the fold. README text only; no code changed.

0.4.1

Patch Changes

  • #454 b4584e7 Thanks @ofri-peretz! - Every package's npm homepage now points at its page on the docs site, https://burgee.interlace.tools/docs/packages/<name>, and each README links it under the header. The keywords add what people and models search for: burgee gains cli-framework, argument-parser, subcommands, json-schema, mcp-server, model-context-protocol, ai-agent, llm, shell-completion, typescript, zero-dependency, commander-alternative and yargs-alternative; the other eight gain agent, ai-agent, non-tty, json and zero-dependency where the package does that — zero-dependency only on the six that install nothing at all.

    burgee's README gains a short FAQ (commander alternative, agent use, MCP, dependencies) and states the compatibility counts the oracle holds — 1,360 / 1,360 of commander's tests and 804 / 804 of yargs' — where it had said 1,215 and 1,185. caique's README no longer calls a released package pre-release.

  • #442 bdaf364 Thanks @ofri-peretz! - Every package now lists plugin, plugins and extensible in its npm keywords, because every package takes plugins through one shared contract.

    A plugin is a plain object, validated against the schema.json that ships in every package, and checked with the package's own check command. Each package reads its own key and ignores the rest, so one object can extend any subset of the family. The plugins page has a nine-layer example that every package's check accepts in CI.

  • #435 7888524 Thanks @ofri-peretz! - schema.json now describes every plugin host in the family.

    The one schema each package ships as its plugin contract used to cover only four hosts: roundel's tokens, flagstaff's glyphs, spinners, borders and components, paratext's capabilities, and linegauge's widths. Five hosts validated their keys in their own code, but the file an author (or a model) writes against said nothing about them. It now describes all of them:

    • bellpull resolvers, including the absolute-path rule on paths
    • caique widgets
    • closeout handlers, including the phases a plugin may use
    • seniority sources, including the rank bounds
    • burgee commands, hooks and enforce

    Where the schema can express a rule, it gives the same verdict as the host's own validator, and a test holds the two together. Function-valued fields (static, run, read, handler) are described and required, but not typed, because JSON Schema can't say "function".

    flagstaff now validates a plugin against only its own keys, not the whole family schema. It no longer refuses a plugin over another host's key, which lets one plugin object contribute to several hosts. Its entry points are also 4.7–5.9 KB lighter for it.

  • #445 dac303e Thanks @ofri-peretz! - Every package now declares sideEffects truthfully, so bundlers can drop what you don't import.

    Six packages declared nothing, so no bundler could drop any of their modules. A named import from the root now bundles to the same bytes as the same import from its subpath:

    importbeforeafter
    import { explain } from 'seniority'2,939 B1,067 B
    import { decide } from 'caique'1,235 B734 B
    import { strip } from 'linegauge'1,102 B940 B
    import { once } from 'closeout'353 B235 B

    flagstaff and roundel used to declare false, but each ships a check command whose file runs when loaded. Each now lists that file, which is the true statement. paratext also lists the two modules that register its built-in capabilities when they load.

0.4.0

Minor Changes

  • #421 db3c59e Thanks @ofri-peretz! - linegauge hosts plugins — widths, and it is the ninth of nine.

    scripts/extension-surface-lock.test.ts has carried linegauge: { plugin: false } since it was written, and the row was empty honestly: a width function is not obviously extensible, and an extension point invented to fill a table is worse than a gap that says so.

    What makes widths real is that the package already admits the problem. width.ts says ambiguous-width characters are "counted narrow, which is what a terminal does unless it has been told it is rendering an East Asian locale" — and that covers only the ambiguity Unicode sanctions. A Nerd Font putting a two-column icon in the Private Use Area, a code point added by a Unicode release newer than the table compiled into this build, a font drawing U+2500 wide: each is a real, local disagreement with the built-in answer, and until now a user had no way to settle it short of patching the package.

    export default {
      name: "nerd-font",
      widths: {
        icons: {
          ranges: [[0xe000, 0xf8ff]],
          columns: 2,
          why: "Nerd Font patches two-column icons into the PUA; measured in WezTerm",
        },
      },
    };

    Three fields of plain data, so a plugin can arrive as JSON, be diffed, be generated and be printed without running its author's code (R7). why is required, which no other $def in the family does: a width table with no provenance cannot be audited when it turns out to be wrong, and wrong is the normal outcome for ambiguous width.

    A later registration wins over an earlier one and over the built-in tables, which is the point — the built-in answer is right for most terminals and the user is the authority on theirs. An override applies before the zero-width and emoji rules, or it would be decorative. A program with no plugin pays one length === 0 per cluster, and the ASCII fast path never reaches it.

    The family schema gains widthRange, widthOverride and widths, and because it is one byte-identical file across every host, every host that validates against it grows by about 1.3 KB — five flagstaff budgets and two paratext ones moved for a definition only linegauge reads. That trade is the design's and is recorded as D-108 rather than absorbed.

  • #421 db3c59e Thanks @ofri-peretz! - Every plugin host has a check command.

    npx linegauge check ./my-widths.mjs
    npx burgee check ./my-plugin.mjs --json

    PRINCIPLES 7 asks three things of an extension surface: the plugin is data validated against one published schema, there is a check command that shows it every way it can be seen, and the bar is measured. The first was built in all nine hosts; the second existed in flagstaff alone. So an author writing a plugin for any other host found out what it did by shipping it into a program — and a surface nobody can check is a surface nobody outside this repository can write against.

    Each command validates, registers, and shows what the host does with the plugin, in the host's own terms: linegauge measures each code point before and after the override, paratext shows a capability's encode and its fallback, roundel each token and what it replaced, caique each widget's static projection rendered with its own sample. burgee's returns a document rather than printing one, so burgee check --json is the form an agent that just wrote a plugin reads.

    They share one contract with the author, held identically across all nine:

    • a readable report, contribution by contribution, with ok as the last line;
    • a refusal with a code from the family's vocabulary and a fix, exit 1;
    • E_NO_CONTRIBUTION for a plugin that contributes nothing to this host — the schema allows unknown keys so one object registers everywhere, which makes a misspelled key silent, and this is how that typo tells on itself;
    • exit 2 with no file.

    Each host also gains an eval case measuring the one-turn claim, proved to discriminate before it was committed: green against a correct plugin, red against the same plugin with one field broken.

Patch Changes

  • #430 4d1b2b3 Thanks @ofri-peretz! - check now reports every refusal with its code and its fix, wherever it was raised.

    Some plugin files register themselves on import: they call register() at the top of the module and export the result. Until now, when such a file was refused, the error was thrown inside check's import(), before the only try that turns a PluginError into E_PLUGIN_SCHEMA: … plus a fix: line. The author got the bare message on stderr, with no code and no fix. Now the whole of check runs inside that one handler, so every refusal comes out the same way on every host.

0.3.3

Patch Changes

  • #400 5b224e5 Thanks @ofri-peretz! - Thirty fewer bundled bytes per entry point, at no change in behaviour.

    The five Unicode property classes are built on first use and were cached in one object keyed by name. A minifier renames a module-level binding to a single character and cannot touch a property name, so each classes['zeroWidth'] survived minification at full length. Five let bindings hold the same five regexes: linegauge 6,307 → 6,277 bundled bytes, linegauge/wrap 11,278 → 11,248, linegauge/slice 8,938 → 8,908.

0.3.2

Patch Changes

  • #386 5a85175 Thanks @ofri-peretz! - The Unicode segmenter is built on first use rather than at import, and both packages now strip comments from what they publish.

    new Intl.Segmenter() loads ICU's grapheme-break data. Two were constructed at module scope, and almost nothing paid for them: every caller takes the ASCII fast path first, so a run of printable ASCII — a help screen, a flag name, a path — never reaches segment(). segmenter is now a function; the two call sites become segmenter().

    linegauge and seniority were also the two published packages whose build never ran strip-comments at all. Unpacked: linegauge 83,538 → 56,148 and seniority 193,682 → 139,793.

    Together these take import 'burgee' from 56.87 ms to 44.39 ms, medians of seven.

  • #389 88a6996 Thanks @ofri-peretz! - The five Unicode property classes are built on first use, not at import.

    A \p{…} class under the v flag is built when V8 compiles the literal, not when the literal is evaluated — so a module carrying five of them pays for all five at import even if nothing calls them. Wrapping the literals in functions does not help; only constructing from a source string does.

    Measured on Node 24: width.js imports in 5.95 ms against 15.30, linegauge in 11.60 against 19.00, and import 'burgee' in 21.69 against 33.26. A caller that measures a non-ASCII cluster pays the ~10 ms once, on first call.

    string-width 229/229, wrap-ansi 80/80, slice-ansi 15/15 and strip-ansi 8/8 are unchanged.

0.3.1

Patch Changes

  • #373 a1f1d40 Thanks @ofri-peretz! - Stage 2's artifact is now spec.md, the name Anthropic's AI-Native SDLC playbook gives it, so the source comments and README sections that cite a package's own design document point at spec.md rather than design.md.

    No behaviour changes. The published tarballs do move, by two bytes per surviving reference — design.md is nine characters and spec.md is seven — so the four packages carrying a weight band were re-measured against it: linegauge 83,538 to 83,536; paratext 66,343 to 66,341; closeout 84,455 to 84,453; bellpull 86,113 to 86,107.

0.3.0

Minor Changes

  • #303 fc640dd Thanks @ofri-peretz! - Give linegauge/strip, linegauge/wrap and linegauge/slice a default export, each the same function object as the subpath's named export.

    The three packages they replace — strip-ansi, wrap-ansi, slice-ansi — all publish a single function as their default, so import stripAnsi from 'linegauge/strip' now reads exactly like the import it replaces. The root default is untouched and still width: that one is spent on the string-width override recipe and cannot move.

    This is what unblocked grading those three suites. Their tests open with import x from './index.js', and without a default the generated shim does not fail a case, it fails to link — measured at # tests 0 / # pass 0 / # fail 2 on eight cases the implementation already satisfied. All three now grade: strip-ansi 8 / 8, wrap-ansi 80 / 80, slice-ansi 13 / 15.

Patch Changes

  • #332 3ea38c3 Thanks @ofri-peretz! - Lock the two claims linegauge was making with nothing behind them: that its ASCII fast path agrees with the path it short-circuits, and that require('linegauge') works.

    width() has had two implementations of one answer since slice landed — asciiColumns returns s.length for printable ASCII, everything else walks Intl.Segmenter. The design has promised a differential.test.ts locking the two together since 2026-09-09, index.ts still said the fast path was unbuilt, and no test compared them. Every existing case either used an input the fast path rejects or one where both paths are trivially right, so widening the fast path's range by a byte passed the entire suite. A wrong fast path is not a slow program, it is a silently wrong measurement: a box a column short, a help column that stops lining up, and nothing thrown.

    differential.test.ts now asserts width(s) === measure(strip(s)) over the intent's six-row grapheme table, 24 boundary fixtures and 2 000 inputs from a recorded seed, and carries the grapheme table itself as assertions — code units against cluster count against columns. Proven red before green: widening the range to 0x7F fails 2 cases and dropping its floor to 0x00, which lets ESC onto the fast path, fails 8.

    shape.test.ts covers R12. Nothing in the tree had ever called require on this package, and the override recipe it is built for — overrides: { "string-width": "npm:linegauge@^1" } — lands it inside CommonJS trees that have required string-width since 2015. Every published entry is now required from CommonJS, which proves both halves of R12 at once: Node refuses a graph containing a top-level await with ERR_REQUIRE_ASYNC_MODULE, so a require that returns the namespace is also the no-top-level-await check. Proven red by appending a top-level await to widest.ts: 3 of 8 cases fail.

    No behaviour change. 913 tests, up from 867.

  • #316 c8acb28 Thanks @ofri-peretz! - width and the style stack now answer what string-width and slice-ansi answer on twenty-nine cases they did not. The string-width row goes 201 / 229 → 229 / 229; the slice-ansi row goes 13 / 15 → 15 / 15. wrap-ansi holds at 80 / 80 and strip-ansi at 8 / 8 across the change.

    Four defects in width, not twenty-eight cases. Hangul conjoining jamo are additive inside a grapheme cluster: Intl.Segmenter joins a run of them into one cluster, and measuring that cluster by its first code point answered 2 where a terminal draws 12. Modern Hangul composes L + V (+ T) into one two-column syllable and leaves unmatched jamo at their own East Asian Width. Spacing combining marks occupy a column — the zero-width class matched \p{Mark}, which is the spacing marks as well as the non-spacing ones, so Devanagari vowel sign AA measured 0. Prepended concatenation marks (U+0600, U+06DD, U+070F) are Format but not Default_Ignorable, so they missed the zero-width class and were charged a column each — the worst shape of the bug, because a character the cursor never advances past is invisible until a box comes out short. And minimally-qualified emoji sequences — the same ZWJ sequence or keycap without its U+FE0F — are still two columns in every terminal, but \p{RGI_Emoji} matches only the fully-qualified spelling.

    One defect in the style stack, which slice, wrap and truncate share. An SGR parameter with no entry in the close-code table — ESC[20m, ESC[1001m — was dropped at a cut, so the text survived and its styling did not, silently. It is now carried through and reopened like any other style, closed with ESC[0m. The sequence is the caller's, not this library's to vet.

    Measured cost, stated rather than absorbed: the minified bundle grows 939–1 040 bytes per entry that measures or cuts — linegauge itself from 5 241 to 6 180 bytes, 18%. strip is unchanged. packages/linegauge/ceilings.json carries the before, the delta and the after, and a new weight.test.ts ratchets every subpath's dist/ closure so the next growth cannot be silent. That file also records, rather than hides, that R9's weight ceiling is not met: one entry of six is under the bar the design names.

  • #339 f295630 Thanks @ofri-peretz! - schema.json constrains token names, because it was promising something no host honours.

    tokens was described as any name to a #rrggbb colour. roundel's validate() accepts ten semantic names — error, warn, ok, hint, muted, command, flag, value, heading, ground — and throws on everything else. So a plugin author doing exactly what their own E_PLUGIN_SCHEMA error tells them, comparing their object against roundel/schema.json, got a green from the schema and "accent" is not a token from register(). Measured 2026-09-16 with { accent: '[#336699](https://github.com/ofri-peretz/burgee/issues/336699)' }.

    The schema now carries propertyNames.enum, and scripts/plugin-contract-lock.test.ts pins the enum and the runtime set to each other from both sides, so neither can grow a name the other does not know.

    Every host ships a byte-identical copy of this file (plugin-schema-lock.test.ts asserts it), which is why nine packages are listed. Only the key roundel owns is constrained: describing widgets, handlers, sources, resolvers or commands in a file all eight hosts share is what made flagstaff start validating caique's key last time (PluginError: plugin.widgets.later: expected object, got boolean), and those stay in plugin-schema-lock's UNDESCRIBED list with that reason.

    linegauge is in the list for a different change: ceilings.json's R9 block now records the bar as D1's tree-inclusive ceiling — 83,538 against 170,342, a ratio of 0.4904 — and keeps the superseded get-east-asian-width bar beside it with the count of entries that cleared it.

0.2.0

Minor Changes

  • #186 3d744d9 Thanks @ofri-peretz! - slice, truncate and widest, over the style stack extracted from wrap. Cutting a styled string in display columns never splits a grapheme cluster, never loses a combining mark, and closes and reopens whatever styles the cut ran through. truncate measures the ellipsis and keeps it inside the budget. Published as linegauge/slice, linegauge/truncate and linegauge/widest, each isolated from the others.

  • #210 59d910c Thanks @ofri-peretz! - strip (R3), and a width bug it found. util.stripVTControlCharacters leaves the colon form of an extended colour — ESC[38:2::255:0:0m, how every truecolor library writes one — in the output as text, so width() answered 15 for a three-column string. Measured across sixteen sequence shapes: Node is exact on fifteen and wrong on that one. Published as linegauge/strip.

Patch Changes

  • #213 ecedfa2 Thanks @ofri-peretz! - width takes ambiguousIsNarrow. East Asian Ambiguous characters — ±, ×, ÷, the box-drawing set, Greek and Cyrillic — are one column in a Latin terminal and two in a CJK one, and nothing can detect which a terminal is doing, so it is the caller's decision. Default true, matching the incumbent. The 179-range table is generated from Unicode rather than transcribed, with a --check that fails on drift. Graded: string-width 198 / 229 → 201 / 229.

  • #224 ea58e63 Thanks @ofri-peretz! - docs: the README describes the package that shipped

    It opened with "Not yet released. This version reserves the name", under a heading reading "What it will be", while linegauge@0.1.0 was live on npm with a graded string-width row on the public compatibility page. Anyone who installed it was told the package does nothing.

    Rewritten for someone installing it today: what the five functions are, why they are one package rather than twelve, and what "without the edge fraying" actually guarantees — a cluster is atomic, a style that was open gets closed, and the ellipsis is counted inside the budget rather than added on top.

    strip and the ASCII fast path are named as still at the gate, because they are.

  • #205 93114d3 Thanks @ofri-peretz! - width keeps the contract string-width has always kept. A non-string measures 0 instead of throwing — a width function is usually reached with whatever a template produced, which is why the incumbent answers rather than making every caller guard — and a new countAnsiEscapeCodes option counts escape sequences as the characters they are made of. Graded: string-width 194 / 229 → 198 / 229.

0.1.0

Minor Changes

  • #182 28a838f Thanks @ofri-peretz! - linegauge is real: width and wrap move out of flagstaff into the foundation package that was reserved for them (F1, the move only). The default export is width, call-compatible with string-width's default. flagstaff imports them and deletes both files; its 227 tests pass unchanged, and B4's bundled bytes are identical to the byte — the code went to a different file, not away.

Patch Changes

  • #193 8586f58 Thanks @ofri-peretz! - Name the tool the package is named after.

    The npm description opened with "the width of material a saw removes in a cut" — that is a kerf, a different tool from a different trade. A line gauge is the printer's steel rule marked in picas and points, which is what a package that measures typeset width actually does. The description is the first line a reader sees on npm, so it may as well be the one that explains the name.

    No behaviour change.

On this page