Skip to content

AcceptedOwner sparkles:baseReviewed 2026-10-05

sparkles.base.text — conformance test cases ​

Abstract ​

This page is the curated conformance case table of sparkles:base's terminal width measurement: normative inputs, each with the width in grid cells that kitty's algorithm prescribes, and a runnable report that measures every one and prints the tally.

Introduction ​

The cell-splitting & width specification states the terminalKitty width profile in rules. This case table turns those rules into cases a reader can check at a glance, weighted towards the inputs where terminals and width libraries disagree. Each row is normative: its want width is what kitty's algorithm prescribes.

The executable report measures the implementation via visibleWidth and prints PASS / FAIL per case, then a tally. apps/ci verifies it against its recorded output, so the curated cases stay green or the build fails. The same assertions live as library unittests in width.d and grapheme.d.

The case table is curated, not exhaustive. Every code point, the official GraphemeBreakTest.txt / emoji-test.txt corpora, and cross-checks against real terminals belong to the conformance harness.

Case table ​

want = kitty-normative width (in cells).

CaseInput (code points)wantstatus
ASCII letterU+00411✓
CJK ideograph (EAW W)U+4E162✓
Fullwidth letter (EAW F)U+FF212✓
base + combining mark (Mn)U+0041 U+03011✓
zero-width space (Cf)U+200B0✓
flag (regional-indicator pair)U+1F1FA U+1F1F82✓
emoji + skin-tone modifierU+1F44D U+1F3FE2✓
ZWJ familyU+1F469 U+200D U+1F4672✓
emoji base + VS16U+2764 U+FE0F2✓
emoji base bare (ambiguous)U+27641✓
emoji base + VS15U+2764 U+FE0E1✓
styled text (escapes width 0)ESC[31m h i ESC[0m2✓
base + spacing mark (Mc)U+0915 U+093E1✓
Devanagari syllableU+0915 U+09401✓
lone spacing mark (Mc)U+093E0✓
lone regional indicatorU+1F1FA2✓
lone emoji modifierU+1F3FE2✓
noncharacterU+FDD00✓

Notes on the trickier rows ​

Several rows pin width classes where a naive per-code-point measure goes wrong:

  • Spacing marks (Mc) are zero width (all Marks M* join Cf in the zero-width set), so a Brahmic syllable such as U+0915 U+093E is one 1-cell cluster — not two cells.
  • A lone regional indicator is width 2 (EastAsianWidth.txt marks U+1F1E6..U+1F1FF as neutral, but kitty's flag rule overrides them to 2).
  • Noncharacters (U+FDD0..U+FDEF, U+xxFFFE, U+xxFFFF) are discarded → 0.
  • A lone emoji modifier is width 2, not 0. kitty's prose lists modifiers under zero-width Marks, but U+1F3FB..U+1F3FF have East_Asian_Width = W, and in kitty's priority order Doublewidth outranks Marks, so a modifier in isolation is 2 (it only ever contributes 0 inside a cluster, where the leading emoji already sets the width). sparkles matches this.

Executable report ​

d
#!/usr/bin/env dub
/+ dub.sdl:
    name "spec_text_conformance"
    dependency "sparkles:base" version="*"
+/
import std.stdio : writefln;
import sparkles.base.text.grapheme : visibleWidth;

struct Case { string label; string input; size_t want; }

void main()
{
    // `want` is the kitty-normative width, measured via the top-level visibleWidth.
    static immutable Case[] cases = [
        Case("ASCII 'A'",                   "A", 1),
        Case("CJK U+4E16",                  "世", 2),
        Case("Fullwidth U+FF21",            "A", 2),
        Case("base+Mn (A+U+0301)",          "Á", 1),
        Case("ZWSP U+200B",                 "​", 0),
        Case("flag US",                     "\U0001F1FA\U0001F1F8", 2),
        Case("emoji+tone",                  "\U0001F44D\U0001F3FE", 2),
        Case("ZWJ family",                  "\U0001F469‍\U0001F467", 2),
        Case("heart+VS16",                  "❤️", 2),
        Case("heart bare (ambiguous)",      "❤", 1),
        Case("heart+VS15",                  "❤︎", 1),
        Case("styled (escapes=0)",          "\x1b[31mhi\x1b[0m", 2),
        Case("base+Mc (U+0915 U+093E)",     "का", 1),
        Case("syllable U+0915 U+0940",      "की", 1),
        Case("lone Mc U+093E",              "ा", 0),
        Case("lone RI U+1F1FA",             "\U0001F1FA", 2),
        Case("lone emoji modifier U+1F3FE", "\U0001F3FE", 2),
        Case("noncharacter U+FDD0",         "﷐", 0),
    ];

    size_t pass, fail;
    foreach (c; cases)
    {
        const got = visibleWidth(c.input);
        if (got == c.want) { writefln("PASS  %-32s width=%s", c.label, got); ++pass; }
        else { writefln("FAIL  %-32s got=%s want=%s", c.label, got, c.want); ++fail; }
    }
    writefln("---- %s passed, %s failed (of %s) ----", pass, fail, pass + fail);
}
PASS  ASCII 'A'                        width=1
PASS  CJK U+4E16                       width=2
PASS  Fullwidth U+FF21                 width=2
PASS  base+Mn (A+U+0301)               width=1
PASS  ZWSP U+200B                      width=0
PASS  flag US                          width=2
PASS  emoji+tone                       width=2
PASS  ZWJ family                       width=2
PASS  heart+VS16                       width=2
PASS  heart bare (ambiguous)           width=1
PASS  heart+VS15                       width=1
PASS  styled (escapes=0)               width=2
PASS  base+Mc (U+0915 U+093E)          width=1
PASS  syllable U+0915 U+0940           width=1
PASS  lone Mc U+093E                   width=0
PASS  lone RI U+1F1FA                  width=2
PASS  lone emoji modifier U+1F3FE      width=2
PASS  noncharacter U+FDD0              width=0
---- 18 passed, 0 failed (of 18) ----