0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Ask how long a piece of text is and there are four correct answers, all different — this page shows all four at once, then takes the text apart character by character. Pick any card and it explodes into its code points: the family emoji turns out to be a woman, a woman, a girl and a boy welded together by three invisible zero-width joiners, 25 bytes for one glyph you cannot half-delete. The clustering is real Intl.Segmenter work with a hand-rolled fallback you can switch to and compare against it, and the encoder decodes every byte back to check its own work. Start with café spelled two ways — two spellings that render identically, one costing an extra code point because its é is really an e with a floating accent, which is why pasted text sometimes refuses to match — then watch the bit-packing panel deal a code point's binary digits out into UTF-8 bytes.
Source — the code you see is the code that just ran 992 lines 52.7 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Character Anatomy — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ---- derived surfaces (theme-paired, same rule as the tokens above) ---- */
:root { --wash: #F1F0EC; --face: #FBFBF9; --glow: rgba(192,86,0,.13); --danger-soft: #FBEBEA; --on-accent: #FFFFFF; --grid-line: rgba(26,29,33,.07); }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --wash: #15181D; --face: #181B21; --glow: rgba(255,180,84,.13); --danger-soft: #2C1A19; --on-accent: #14161A; --grid-line: rgba(232,230,225,.07); }
}
:root[data-theme="dark"] { --wash: #15181D; --face: #181B21; --glow: rgba(255,180,84,.13); --danger-soft: #2C1A19; --on-accent: #14161A; --grid-line: rgba(232,230,225,.07); }
/* ---- shell ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; overflow-x: hidden; -webkit-text-size-adjust: 100%; }
.wrap { max-width: 1060px; margin: 0 auto; padding: 26px 16px 34px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
h2 { font-size: .95rem; margin: 0; font-weight: 600; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 72ch; }
.lede b { color: var(--ink); font-weight: 600; }
.mono { font-family: var(--font-mono); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
footer { margin-top: 22px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }
.k-lab { font-family: var(--font-mono); font-size: .64rem; letter-spacing: .11em; text-transform: uppercase; color: var(--muted); }
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; margin-top: 16px; }
.panelhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; margin-bottom: 11px; }
.panelhead .right { margin-left: auto; }
.hint { color: var(--muted); font-size: .82rem; margin: 0; }
/* ---- controls ---- */
.btn {
font-family: var(--font-mono); font-size: .73rem; letter-spacing: .02em;
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
border-radius: 999px; padding: 6px 11px; min-height: 32px; cursor: pointer;
transition: border-color .12s, color .12s, background .12s;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible, .card:focus-visible, .part:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn[aria-pressed="true"]:hover { color: var(--on-accent); }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn[disabled]:hover { border-color: var(--line); color: var(--ink); }
.btn .glyph-sm { font-family: var(--font-sans); }
.chips { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin-top: 10px; }
.chips .k-lab { margin-right: -2px; }
.sep { width: 1px; align-self: stretch; background: var(--line); margin: 2px 3px; }
textarea {
width: 100%; resize: vertical; min-height: 62px;
font-family: var(--font-sans); font-size: 1.35rem; line-height: 1.5;
color: var(--ink); background: var(--face); border: 1px solid var(--line);
border-radius: var(--radius); padding: 10px 12px;
}
textarea:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--glow); }
/* keep a real, visible ring for keyboard users — the soft glow alone is too faint */
textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; box-shadow: none; }
/* ---- counts ---- */
.counts { display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); gap: 10px; margin-top: 16px; }
.tile { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 11px 12px; }
.tile .num { font-family: var(--font-mono); font-size: 1.85rem; line-height: 1.05; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.tile .nm { font-family: var(--font-mono); font-size: .66rem; letter-spacing: .09em; text-transform: uppercase; color: var(--accent); margin-top: 4px; }
.tile .sub { font-size: .76rem; color: var(--muted); margin-top: 2px; line-height: 1.35; }
.tile.lead { border-color: var(--accent); background: var(--accent-soft); }
/* ---- status ---- */
.statusrow { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; align-items: center; }
.badge {
font-family: var(--font-mono); font-size: .7rem; padding: 4px 9px; border-radius: 999px;
border: 1px solid var(--line); color: var(--muted); background: var(--surface);
}
.badge.ok { color: var(--run); background: var(--run-soft); border-color: var(--run); }
.badge.warn { color: var(--accent); background: var(--accent-soft); border-color: var(--accent); }
.badge.bad { color: var(--danger); background: var(--danger-soft); border-color: var(--danger); }
/* ---- character cards ---- */
.grid { display: flex; flex-wrap: wrap; gap: 8px; }
.card {
position: relative; flex: 0 0 auto; min-width: 82px; max-width: 148px;
background: var(--face); border: 1px solid var(--line); border-radius: var(--radius);
padding: 9px 8px 7px; cursor: pointer; text-align: center; color: inherit;
font-family: var(--font-sans); transition: border-color .12s, background .12s, transform .12s;
}
.card:hover { border-color: var(--accent); }
.card[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.card .idx { position: absolute; top: 5px; left: 7px; font-family: var(--font-mono); font-size: .58rem; color: var(--muted); }
.card .cnt { position: absolute; top: 5px; right: 7px; font-family: var(--font-mono); font-size: .58rem; color: var(--accent); }
.card .gly { font-size: 1.9rem; line-height: 1.25; min-height: 2.35rem; display: flex; align-items: center; justify-content: center; margin-top: 9px; word-break: break-all; }
.card .cp { display: block; font-family: var(--font-mono); font-size: .66rem; color: var(--ink); margin-top: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card .by { display: block; font-family: var(--font-mono); font-size: .6rem; color: var(--muted); margin-top: 1px; white-space: nowrap; }
.ghost {
display: inline-block; font-family: var(--font-mono); font-size: .68rem; color: var(--accent);
border: 1px dashed var(--accent); border-radius: 5px; padding: 3px 5px; background: var(--accent-soft);
}
.more { align-self: center; font-size: .8rem; color: var(--muted); padding: 0 6px; }
/* ---- explosion ---- */
.explode { display: flex; flex-wrap: wrap; align-items: stretch; gap: 6px; }
.whole {
display: flex; align-items: center; gap: 9px; padding: 8px 12px 8px 8px;
border: 1px solid var(--line); border-radius: var(--radius); background: var(--wash);
}
.whole .big { font-size: 2.3rem; line-height: 1.1; }
.eq { align-self: center; font-family: var(--font-mono); color: var(--muted); font-size: 1rem; padding: 0 2px; }
.part {
min-width: 66px; background: var(--face); border: 1px solid var(--line); border-radius: var(--radius);
padding: 7px 6px; text-align: center; cursor: pointer; color: inherit; font-family: var(--font-sans);
transition: border-color .12s, background .12s;
}
.part:hover { border-color: var(--accent); }
.part[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
.part .pg { font-size: 1.5rem; line-height: 1.3; min-height: 1.95rem; display: flex; align-items: center; justify-content: center; }
.part .pc { display: block; font-family: var(--font-mono); font-size: .6rem; color: var(--muted); margin-top: 3px; white-space: nowrap; }
.part.glue { min-width: 52px; border-style: dashed; border-color: var(--accent); background: var(--accent-soft); }
.part.glue .pg { font-family: var(--font-mono); font-size: .68rem; color: var(--accent); letter-spacing: .04em; }
.part.glue .pc { color: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
.pop { animation: pop .26s ease-out both; }
@keyframes pop { from { opacity: 0; transform: translateY(5px) scale(.94); } to { opacity: 1; transform: none; } }
}
/* ---- bit lab ---- */
.lab { margin-top: 13px; border-top: 1px solid var(--line); padding-top: 13px; }
.labname { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px 10px; }
.labname .u { font-family: var(--font-mono); font-size: 1.05rem; color: var(--accent); }
.labname .nmv { font-family: var(--font-mono); font-size: .74rem; color: var(--ink); }
.labname .blk { font-size: .78rem; color: var(--muted); }
.cpbits { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.chunk { border: 1px solid var(--line); border-radius: 7px; padding: 4px 6px 3px; background: var(--wash); }
.chunk .cb { font-family: var(--font-mono); font-size: .8rem; letter-spacing: .09em; color: var(--accent); }
.chunk .ct { font-family: var(--font-mono); font-size: .55rem; letter-spacing: .06em; color: var(--muted); text-align: center; margin-top: 1px; }
.bytes { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 11px; }
.bu { border: 1px solid var(--line); border-radius: 8px; padding: 6px; background: var(--face); }
.cells { display: flex; gap: 2px; }
.bit {
width: 15px; height: 21px; display: flex; align-items: center; justify-content: center;
font-family: var(--font-mono); font-size: .68rem; border-radius: 3px;
}
.bit.mark { background: var(--wash); color: var(--muted); }
.bit.pay { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.bu .blab { font-family: var(--font-mono); font-size: .64rem; color: var(--muted); margin-top: 4px; text-align: center; }
.rows { margin-top: 11px; display: grid; gap: 3px; }
.rows .r { font-family: var(--font-mono); font-size: .74rem; color: var(--ink); }
.rows .r span.t { color: var(--muted); }
.note { font-size: .82rem; color: var(--muted); margin: 10px 0 0; max-width: 72ch; }
.note b { color: var(--ink); font-weight: 600; }
/* ---- dump ---- */
.dump { font-family: var(--font-mono); font-size: .72rem; line-height: 2.05; word-break: break-all; }
.dump .g { border-bottom: 2px solid var(--line); padding-bottom: 1px; }
.dump .g.alt { border-bottom-color: var(--muted); }
.dump .g.hot { border-bottom-color: var(--accent); }
.dump b { font-weight: 400; padding: 1px 3px; border-radius: 3px; background: var(--wash); color: var(--ink);
border: 1px solid transparent; margin-right: 2px; display: inline-block; }
/* the chips carry their own background, so the selected run has to be painted on them, not behind them */
.dump .g.hot b { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.dump .gap { display: inline-block; width: 8px; }
.empty { color: var(--muted); font-size: .88rem; padding: 6px 0; }
@media (max-width: 460px) {
.wrap { padding: 20px 12px 28px; }
.tile .num { font-size: 1.55rem; }
textarea { font-size: 1.15rem; }
.card { min-width: 72px; }
.bit { width: 13px; height: 19px; font-size: .62rem; }
.sep { display: none; } /* the rows wrap here, so the divider just dangles */
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Character Anatomy</h1>
<p class="lede">Type anything — <b>emoji included</b> — and see what the computer actually keeps: every code point, every byte, and the invisible glue that holds a family emoji together as one picture.</p>
</header>
<section class="panel">
<div class="panelhead">
<h2>Your text</h2>
<span class="k-lab right" id="segNote"> </span>
</div>
<label class="sr-only" for="txt">Text to take apart</label>
<textarea id="txt" rows="2" maxlength="600" spellcheck="false" autocomplete="off" autocapitalize="off"></textarea>
<div class="chips" id="presets">
<span class="k-lab">try</span>
</div>
<div class="chips">
<span class="k-lab">rewrite</span>
<button class="btn" id="bNFC" type="button" title="Squash marks into single characters where possible">→ NFC</button>
<button class="btn" id="bNFD" type="button" title="Split accented letters into letter + mark">→ NFD</button>
<button class="btn" id="bClear" type="button">clear</button>
<span class="sep" aria-hidden="true"></span>
<span class="k-lab">split into characters with</span>
<button class="btn seg" id="segAuto" type="button" data-seg="auto" aria-pressed="true">Intl.Segmenter</button>
<button class="btn seg" id="segFb" type="button" data-seg="fallback" aria-pressed="false">fallback rules</button>
</div>
</section>
<section class="counts" id="counts" aria-label="Counts"></section>
<div class="statusrow" id="statusrow" role="status" aria-live="polite"></div>
<section class="panel">
<div class="panelhead">
<h2>Character by character</h2>
<span class="k-lab right" id="gridNote">pick one to open it up</span>
</div>
<div class="grid" id="grid"></div>
</section>
<section class="panel" id="detailPanel">
<div class="panelhead">
<h2 id="detailTitle">Inside one character</h2>
<span class="k-lab right" id="detailKind"> </span>
</div>
<div id="detail"></div>
</section>
<section class="panel">
<div class="panelhead">
<h2>Every byte, in order</h2>
<span class="k-lab">show as</span>
<button class="btn base" type="button" data-base="hex" aria-pressed="true">hex</button>
<button class="btn base" type="button" data-base="bin" aria-pressed="false">binary</button>
<button class="btn base" type="button" data-base="dec" aria-pressed="false">decimal</button>
<button class="btn right" id="bCopy" type="button">copy bytes</button>
</div>
<div class="dump" id="dump"></div>
<p class="note">Underlines mark where one character ends and the next begins. The highlighted run is the character you selected above.</p>
</section>
<footer>demo · sloprun.dev</footer>
</div>
<script>
(function () {
"use strict";
var $ = function (id) { return document.getElementById(id); };
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function hex(n, w) { var s = n.toString(16).toUpperCase(); while (s.length < (w || 2)) s = "0" + s; return s; }
function bin(n, w) { var s = n.toString(2); while (s.length < w) s = "0" + s; return s; }
function chr(cp) { return cp > 0xFFFF ? String.fromCharCode(0xD800 + ((cp - 0x10000) >> 10), 0xDC00 + ((cp - 0x10000) & 1023)) : String.fromCharCode(cp); }
function plural(n, one, many) { return n + " " + (n === 1 ? one : many); }
/* ================= names ================= */
var CTRL = ["NULL", "START OF HEADING", "START OF TEXT", "END OF TEXT", "END OF TRANSMISSION", "ENQUIRY", "ACKNOWLEDGE", "BELL",
"BACKSPACE", "CHARACTER TABULATION (tab)", "LINE FEED (new line)", "LINE TABULATION", "FORM FEED", "CARRIAGE RETURN", "SHIFT OUT", "SHIFT IN",
"DATA LINK ESCAPE", "DEVICE CONTROL ONE", "DEVICE CONTROL TWO", "DEVICE CONTROL THREE", "DEVICE CONTROL FOUR", "NEGATIVE ACKNOWLEDGE",
"SYNCHRONOUS IDLE", "END OF TRANSMISSION BLOCK", "CANCEL", "END OF MEDIUM", "SUBSTITUTE", "ESCAPE", "INFORMATION SEPARATOR FOUR",
"INFORMATION SEPARATOR THREE", "INFORMATION SEPARATOR TWO", "INFORMATION SEPARATOR ONE"];
var DIGITS = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"];
var PUNCT = {
0x20: "SPACE", 0x21: "EXCLAMATION MARK", 0x22: "QUOTATION MARK", 0x23: "NUMBER SIGN", 0x24: "DOLLAR SIGN", 0x25: "PERCENT SIGN",
0x26: "AMPERSAND", 0x27: "APOSTROPHE", 0x28: "LEFT PARENTHESIS", 0x29: "RIGHT PARENTHESIS", 0x2A: "ASTERISK", 0x2B: "PLUS SIGN",
0x2C: "COMMA", 0x2D: "HYPHEN-MINUS", 0x2E: "FULL STOP", 0x2F: "SOLIDUS", 0x3A: "COLON", 0x3B: "SEMICOLON", 0x3C: "LESS-THAN SIGN",
0x3D: "EQUALS SIGN", 0x3E: "GREATER-THAN SIGN", 0x3F: "QUESTION MARK", 0x40: "COMMERCIAL AT", 0x5B: "LEFT SQUARE BRACKET",
0x5C: "REVERSE SOLIDUS", 0x5D: "RIGHT SQUARE BRACKET", 0x5E: "CIRCUMFLEX ACCENT", 0x5F: "LOW LINE", 0x60: "GRAVE ACCENT",
0x7B: "LEFT CURLY BRACKET", 0x7C: "VERTICAL LINE", 0x7D: "RIGHT CURLY BRACKET", 0x7E: "TILDE", 0x7F: "DELETE"
};
var MARKS = {
0x300: ["COMBINING GRAVE ACCENT", "WITH GRAVE"], 0x301: ["COMBINING ACUTE ACCENT", "WITH ACUTE"],
0x302: ["COMBINING CIRCUMFLEX ACCENT", "WITH CIRCUMFLEX"], 0x303: ["COMBINING TILDE", "WITH TILDE"],
0x304: ["COMBINING MACRON", "WITH MACRON"], 0x306: ["COMBINING BREVE", "WITH BREVE"],
0x307: ["COMBINING DOT ABOVE", "WITH DOT ABOVE"], 0x308: ["COMBINING DIAERESIS", "WITH DIAERESIS"],
0x309: ["COMBINING HOOK ABOVE", "WITH HOOK ABOVE"], 0x30A: ["COMBINING RING ABOVE", "WITH RING ABOVE"],
0x30B: ["COMBINING DOUBLE ACUTE ACCENT", "WITH DOUBLE ACUTE"], 0x30C: ["COMBINING CARON", "WITH CARON"],
0x323: ["COMBINING DOT BELOW", "WITH DOT BELOW"], 0x327: ["COMBINING CEDILLA", "WITH CEDILLA"],
0x328: ["COMBINING OGONEK", "WITH OGONEK"], 0x331: ["COMBINING MACRON BELOW", "WITH LINE BELOW"]
};
var EXACT = {
0xA0: "NO-BREAK SPACE", 0xAD: "SOFT HYPHEN", 0xB0: "DEGREE SIGN", 0xB5: "MICRO SIGN", 0xB7: "MIDDLE DOT",
0xBF: "INVERTED QUESTION MARK", 0xC6: "LATIN CAPITAL LETTER AE", 0xE6: "LATIN SMALL LETTER AE",
0xDF: "LATIN SMALL LETTER SHARP S", 0xF8: "LATIN SMALL LETTER O WITH STROKE", 0xD7: "MULTIPLICATION SIGN",
0x2013: "EN DASH", 0x2014: "EM DASH", 0x2018: "LEFT SINGLE QUOTATION MARK", 0x2019: "RIGHT SINGLE QUOTATION MARK",
0x201C: "LEFT DOUBLE QUOTATION MARK", 0x201D: "RIGHT DOUBLE QUOTATION MARK", 0x2026: "HORIZONTAL ELLIPSIS",
0x2022: "BULLET", 0x20AC: "EURO SIGN", 0x00A3: "POUND SIGN", 0x00A5: "YEN SIGN", 0x20B9: "INDIAN RUPEE SIGN",
0x200B: "ZERO WIDTH SPACE", 0x200C: "ZERO WIDTH NON-JOINER", 0x200D: "ZERO WIDTH JOINER",
0x2060: "WORD JOINER", 0xFEFF: "ZERO WIDTH NO-BREAK SPACE (byte order mark)", 0x2009: "THIN SPACE", 0x2003: "EM SPACE",
0x2212: "MINUS SIGN", 0x2260: "NOT EQUAL TO", 0x2248: "ALMOST EQUAL TO", 0x221E: "INFINITY", 0x221A: "SQUARE ROOT",
0x2211: "N-ARY SUMMATION", 0x03A9: "GREEK CAPITAL LETTER OMEGA", 0x2126: "OHM SIGN", 0x03BC: "GREEK SMALL LETTER MU",
0x03C0: "GREEK SMALL LETTER PI", 0x03B1: "GREEK SMALL LETTER ALPHA", 0x03B2: "GREEK SMALL LETTER BETA",
0x20E3: "COMBINING ENCLOSING KEYCAP", 0x2B1B: "BLACK LARGE SQUARE", 0x2B1C: "WHITE LARGE SQUARE",
0x2764: "HEAVY BLACK HEART", 0x2615: "HOT BEVERAGE", 0x26A1: "HIGH VOLTAGE SIGN", 0x2600: "BLACK SUN WITH RAYS",
0x2665: "BLACK HEART SUIT", 0x2708: "AIRPLANE", 0x270D: "WRITING HAND", 0x2728: "SPARKLES",
0x1F600: "GRINNING FACE", 0x1F601: "GRINNING FACE WITH SMILING EYES", 0x1F602: "FACE WITH TEARS OF JOY",
0x1F642: "SLIGHTLY SMILING FACE", 0x1F60A: "SMILING FACE WITH SMILING EYES", 0x1F914: "THINKING FACE",
0x1F44B: "WAVING HAND SIGN", 0x1F44D: "THUMBS UP SIGN", 0x1F44F: "CLAPPING HANDS SIGN", 0x1F64C: "PERSON RAISING BOTH HANDS",
0x1F468: "MAN", 0x1F469: "WOMAN", 0x1F466: "BOY", 0x1F467: "GIRL", 0x1F9D1: "ADULT", 0x1F476: "BABY",
0x1F373: "COOKING", 0x1F355: "SLICE OF PIZZA", 0x1F370: "SHORTCAKE", 0x1F34E: "RED APPLE",
0x1F408: "CAT", 0x1F415: "DOG", 0x1F431: "CAT FACE", 0x1F984: "UNICORN FACE", 0x1F41B: "BUG",
0x1F308: "RAINBOW", 0x1F3F3: "WAVING WHITE FLAG", 0x1F3F4: "WAVING BLACK FLAG", 0x1F30D: "EARTH GLOBE EUROPE-AFRICA",
0x1F680: "ROCKET", 0x1F4A1: "ELECTRIC LIGHT BULB", 0x1F4BB: "PERSONAL COMPUTER", 0x1F4DA: "BOOKS",
0x1F3B8: "GUITAR", 0x1F3A8: "ARTIST PALETTE", 0x1F525: "FIRE", 0x1F389: "PARTY POPPER",
0x1F44C: "OK HAND SIGN", 0x1F441: "EYE", 0x1F5E8: "LEFT SPEECH BUBBLE", 0x1F332: "EVERGREEN TREE"
};
var ABBR = {
0x09: "TAB", 0x0A: "LF", 0x0D: "CR", 0x20: "SP", 0xA0: "NBSP", 0xAD: "SHY", 0x200B: "ZWSP", 0x200C: "ZWNJ",
0x200D: "ZWJ", 0x2060: "WJ", 0xFEFF: "BOM", 0xFE0F: "VS16", 0xFE0E: "VS15", 0x2009: "THIN SP", 0x2003: "EM SP"
};
var BLOCKS = [
[0x0000, 0x007F, "Basic Latin (plain ASCII)"], [0x0080, 0x00FF, "Latin-1 Supplement"],
[0x0100, 0x017F, "Latin Extended-A"], [0x0180, 0x024F, "Latin Extended-B"], [0x0250, 0x02AF, "phonetic letters"],
[0x0300, 0x036F, "combining marks"], [0x0370, 0x03FF, "Greek"], [0x0400, 0x04FF, "Cyrillic"],
[0x0530, 0x058F, "Armenian"], [0x0590, 0x05FF, "Hebrew"], [0x0600, 0x06FF, "Arabic"],
[0x0900, 0x097F, "Devanagari"], [0x0980, 0x09FF, "Bengali"], [0x0A00, 0x0A7F, "Gurmukhi"],
[0x0B80, 0x0BFF, "Tamil"], [0x0C00, 0x0C7F, "Telugu"], [0x0D00, 0x0D7F, "Malayalam"],
[0x0E00, 0x0E7F, "Thai"], [0x1000, 0x109F, "Myanmar"], [0x10A0, 0x10FF, "Georgian"],
[0x1E00, 0x1EFF, "Latin Extended Additional"], [0x2000, 0x206F, "punctuation & spacing"],
[0x20A0, 0x20CF, "currency signs"], [0x2100, 0x214F, "letterlike symbols"], [0x2190, 0x21FF, "arrows"],
[0x2200, 0x22FF, "math operators"], [0x2500, 0x257F, "box drawing"], [0x2600, 0x27BF, "misc symbols & dingbats"],
[0x3040, 0x309F, "Hiragana"], [0x30A0, 0x30FF, "Katakana"], [0x3400, 0x4DBF, "CJK ideographs, extension A"],
[0x4E00, 0x9FFF, "CJK unified ideographs"], [0xAC00, 0xD7AF, "Hangul syllables"],
[0xD800, 0xDFFF, "surrogate half (never valid alone)"], [0xE000, 0xF8FF, "private use"],
[0xFE00, 0xFE0F, "variation selectors"], [0x1F000, 0x1F0FF, "tiles & cards"],
[0x1F300, 0x1F5FF, "misc symbols & pictographs"], [0x1F600, 0x1F64F, "emoticons"],
[0x1F680, 0x1F6FF, "transport & map symbols"], [0x1F900, 0x1F9FF, "supplemental symbols & pictographs"],
[0x1FA70, 0x1FAFF, "symbols & pictographs extended-A"], [0x20000, 0x2A6DF, "CJK ideographs, extension B"]
];
function blockOf(cp) {
for (var i = 0; i < BLOCKS.length; i++) if (cp >= BLOCKS[i][0] && cp <= BLOCKS[i][1]) return BLOCKS[i][2];
return "unassigned or rarely used block";
}
function nameOf(cp, depth) {
if (EXACT[cp]) return EXACT[cp];
if (cp < 0x20) return CTRL[cp];
if (PUNCT[cp]) return PUNCT[cp];
if (cp >= 0x30 && cp <= 0x39) return "DIGIT " + DIGITS[cp - 0x30];
if (cp >= 0x41 && cp <= 0x5A) return "LATIN CAPITAL LETTER " + chr(cp);
if (cp >= 0x61 && cp <= 0x7A) return "LATIN SMALL LETTER " + chr(cp).toUpperCase();
if (MARKS[cp]) return MARKS[cp][0];
if (cp >= 0x1F1E6 && cp <= 0x1F1FF) return "REGIONAL INDICATOR SYMBOL LETTER " + String.fromCharCode(65 + cp - 0x1F1E6);
if (cp >= 0x1F3FB && cp <= 0x1F3FF) return "EMOJI MODIFIER FITZPATRICK TYPE-" + ["1-2", "3", "4", "5", "6"][cp - 0x1F3FB];
if (cp >= 0xFE00 && cp <= 0xFE0F) return "VARIATION SELECTOR-" + (cp - 0xFE00 + 1);
if (cp >= 0xE0100 && cp <= 0xE01EF) return "VARIATION SELECTOR-" + (cp - 0xE0100 + 17);
if (cp >= 0x4E00 && cp <= 0x9FFF) return "CJK UNIFIED IDEOGRAPH-" + hex(cp, 4);
if (cp >= 0x3400 && cp <= 0x4DBF) return "CJK UNIFIED IDEOGRAPH-" + hex(cp, 4);
if (cp >= 0xAC00 && cp <= 0xD7A3) return "HANGUL SYLLABLE";
if (cp >= 0x3041 && cp <= 0x3096) return "HIRAGANA LETTER";
if (cp >= 0x30A1 && cp <= 0x30FA) return "KATAKANA LETTER";
if (isVirama(cp)) return "SIGN VIRAMA (silences the vowel)";
if (cp >= 0x0904 && cp <= 0x0939) return "DEVANAGARI LETTER";
if (cp >= 0x093E && cp <= 0x094C) return "DEVANAGARI VOWEL SIGN";
if ((depth || 0) < 2 && cp > 0x7F && cp < 0x3000) {
try {
var d = String.fromCodePoint(cp).normalize("NFD");
if (d.length > 1) {
var b = d.codePointAt(0), m = d.codePointAt(b > 0xFFFF ? 2 : 1);
var bn = nameOf(b, (depth || 0) + 1);
if (bn && MARKS[m] && bn.indexOf("U+") !== 0) return bn + " " + MARKS[m][1];
}
} catch (e) { /* normalize unsupported: fall through */ }
}
return null;
}
/* ================= encoding (hand-rolled, no TextEncoder) ================= */
function utf8Bytes(cp) {
if (cp < 0x80) return [cp];
if (cp < 0x800) return [0xC0 | (cp >> 6), 0x80 | (cp & 63)];
if (cp < 0x10000) return [0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63)];
return [0xF0 | (cp >> 18), 0x80 | ((cp >> 12) & 63), 0x80 | ((cp >> 6) & 63), 0x80 | (cp & 63)];
}
function utf8Decode(bytes) {
var out = "", i = 0;
while (i < bytes.length) {
var b = bytes[i], cp, n;
if (b < 0x80) { cp = b; n = 0; }
else if ((b & 0xE0) === 0xC0) { cp = b & 0x1F; n = 1; }
else if ((b & 0xF0) === 0xE0) { cp = b & 0x0F; n = 2; }
else if ((b & 0xF8) === 0xF0) { cp = b & 0x07; n = 3; }
else return null;
if (i + n >= bytes.length && n > 0) return null;
for (var k = 1; k <= n; k++) {
var c = bytes[i + k];
if (c === undefined || (c & 0xC0) !== 0x80) return null;
cp = (cp << 6) | (c & 63);
}
out += cp > 0xFFFF ? String.fromCharCode(0xD800 + ((cp - 0x10000) >> 10), 0xDC00 + ((cp - 0x10000) & 1023)) : String.fromCharCode(cp);
i += n + 1;
}
return out;
}
function utf16Units(cp) {
if (cp <= 0xFFFF) return [cp];
var v = cp - 0x10000;
return [0xD800 + (v >> 10), 0xDC00 + (v & 1023)];
}
function cpArray(str) {
var out = [], i = 0;
while (i < str.length) {
var c = str.charCodeAt(i), cp = c, len = 1, lone = false;
if (c >= 0xD800 && c <= 0xDBFF) {
var d = str.charCodeAt(i + 1);
if (d >= 0xDC00 && d <= 0xDFFF) { cp = (c - 0xD800) * 0x400 + (d - 0xDC00) + 0x10000; len = 2; }
else lone = true;
} else if (c >= 0xDC00 && c <= 0xDFFF) lone = true;
out.push({ cp: cp, lone: lone });
i += len;
}
return out;
}
/* ================= grapheme segmentation ================= */
var MARK_RANGES = [[0x0300, 0x036F], [0x0483, 0x0489], [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x0610, 0x061A], [0x064B, 0x065F], [0x0670, 0x0670], [0x06D6, 0x06DC], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0],
/* Indic: signs and vowel-marks only — the consonant/vowel LETTERS in these blocks
are bases, not marks, and folding them in here glued whole words into one cluster. */
[0x0900, 0x0903], [0x093A, 0x093C], [0x093E, 0x094F], [0x0951, 0x0957], [0x0962, 0x0963], /* Devanagari */
[0x0981, 0x0983], [0x09BC, 0x09BC], [0x09BE, 0x09CD], [0x09D7, 0x09D7], [0x09E2, 0x09E3], /* Bengali */
[0x0A01, 0x0A03], [0x0A3C, 0x0A3C], [0x0A3E, 0x0A4D], [0x0A51, 0x0A51], [0x0A70, 0x0A71], /* Gurmukhi */
[0x0A81, 0x0A83], [0x0ABC, 0x0ABC], [0x0ABE, 0x0ACD], [0x0AE2, 0x0AE3], /* Gujarati */
[0x0B01, 0x0B03], [0x0B3C, 0x0B3C], [0x0B3E, 0x0B4D], [0x0B55, 0x0B57], [0x0B62, 0x0B63], /* Oriya */
[0x0B82, 0x0B82], [0x0BBE, 0x0BCD], [0x0BD7, 0x0BD7], /* Tamil */
[0x0C00, 0x0C04], [0x0C3C, 0x0C3C], [0x0C3E, 0x0C4D], [0x0C55, 0x0C56], [0x0C62, 0x0C63], /* Telugu */
[0x0C81, 0x0C83], [0x0CBC, 0x0CBC], [0x0CBE, 0x0CCD], [0x0CD5, 0x0CD6], [0x0CE2, 0x0CE3], /* Kannada */
[0x0D00, 0x0D03], [0x0D3B, 0x0D3C], [0x0D3E, 0x0D4D], [0x0D57, 0x0D57], [0x0D62, 0x0D63], /* Malayalam */
[0x0D81, 0x0D83], [0x0DCA, 0x0DCA], [0x0DCF, 0x0DDF], [0x0DF2, 0x0DF3], /* Sinhala */
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], [0x0EB1, 0x0EBC], [0x0F71, 0x0F84], [0x102B, 0x103E],
[0x1712, 0x1714], [0x17B4, 0x17D3], [0x180B, 0x180D], [0x1A17, 0x1A1B], [0x1AB0, 0x1AFF], [0x1B00, 0x1B04],
[0x1DC0, 0x1DFF], [0x20D0, 0x20F0], [0x2CEF, 0x2CF1], [0xA66F, 0xA672], [0xFE20, 0xFE2F], [0xFE00, 0xFE0F],
[0x101FD, 0x101FD], [0x1E8D0, 0x1E8D6], [0xE0100, 0xE01EF]];
function isMark(cp) {
for (var i = 0; i < MARK_RANGES.length; i++) if (cp >= MARK_RANGES[i][0] && cp <= MARK_RANGES[i][1]) return true;
return false;
}
var VIRAMA = [0x94D, 0x9CD, 0xA4D, 0xACD, 0xB4D, 0xBCD, 0xC4D, 0xCCD, 0xD4D, 0xDCA, 0x0E3A, 0x1039, 0x17D2];
function isVirama(cp) { return VIRAMA.indexOf(cp) >= 0; }
function isRI(cp) { return cp >= 0x1F1E6 && cp <= 0x1F1FF; }
function isSkin(cp) { return cp >= 0x1F3FB && cp <= 0x1F3FF; }
function isInvisible(cp) {
return cp < 0x20 || cp === 0x7F || cp === 0x20 || cp === 0xA0 || cp === 0xAD || (cp >= 0x200B && cp <= 0x200F) ||
(cp >= 0x2028 && cp <= 0x202F) || (cp >= 0x2060 && cp <= 0x2064) || cp === 0xFEFF || (cp >= 0xFE00 && cp <= 0xFE0F) ||
(cp >= 0x2000 && cp <= 0x200A);
}
function fallbackClusters(str) {
var cps = cpArray(str), out = [], cur = [], i = 0;
function flush() { if (cur.length) { out.push(cur.map(function (o) { return chr(o.cp); }).join("")); cur = []; } }
while (i < cps.length) {
var o = cps[i];
if (!cur.length) { cur.push(o); i++; continue; }
var prev = cur[cur.length - 1].cp, cp = o.cp, join = false;
if (isMark(cp) || isSkin(cp) || cp === 0x200D || cp === 0x200C || cp === 0x20E3) join = true;
else if (prev === 0x200D || prev === 0x200C) join = true;
else if (isVirama(prev)) join = true;
else if (isRI(cp) && isRI(prev) && cur.length === 1) join = true;
else if (cp === 0x0A && prev === 0x0D) join = true;
if (join) { cur.push(o); i++; } else flush();
}
flush();
return out;
}
var SEGMENTER = null;
try { if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") SEGMENTER = new Intl.Segmenter("en", { granularity: "grapheme" }); } catch (e) { SEGMENTER = null; }
function intlClusters(str) {
var out = [], it = SEGMENTER.segment(str);
if (typeof Array.from === "function") { Array.from(it).forEach(function (s) { out.push(s.segment); }); }
else { for (var s of it) out.push(s.segment); }
return out;
}
function segment(str, mode) {
if (!str) return [];
if (mode === "auto" && SEGMENTER) { try { return intlClusters(str); } catch (e) { return fallbackClusters(str); } }
return fallbackClusters(str);
}
/* ================= analysis ================= */
function analyze(text, mode) {
var strs = segment(text, mode);
var clusters = strs.map(function (s) {
var cps = cpArray(s).map(function (o) {
return {
cp: o.cp, lone: o.lone, ch: chr(o.cp), bytes: utf8Bytes(o.cp), units: utf16Units(o.cp),
name: nameOf(o.cp), block: blockOf(o.cp)
};
});
var bytes = [], units = 0;
cps.forEach(function (c) { bytes = bytes.concat(c.bytes); units += c.units.length; });
return { s: s, cps: cps, bytes: bytes, units: units };
});
var nBytes = 0, nCps = 0, nInvis = 0, nLone = 0, all = [];
clusters.forEach(function (cl) {
nBytes += cl.bytes.length; nCps += cl.cps.length; all = all.concat(cl.bytes);
cl.cps.forEach(function (c) { if (isInvisible(c.cp)) nInvis++; if (c.lone) nLone++; });
});
var back = utf8Decode(all);
var te = null;
if (!nLone && typeof TextEncoder === "function") {
try {
var arr = new TextEncoder().encode(text), same = arr.length === all.length;
if (same) for (var i = 0; i < arr.length; i++) if (arr[i] !== all[i]) { same = false; break; }
te = same;
} catch (e) { te = null; }
}
return {
text: text, clusters: clusters, bytes: all, nBytes: nBytes, nCps: nCps, nUnits: text.length,
nInvis: nInvis, nLone: nLone, roundTrip: back === text, te: te
};
}
/* ================= state ================= */
var Z = "\u200D";
var FAM = "\u{1F469}"+Z+"\u{1F469}"+Z+"\u{1F467}"+Z+"\u{1F466}";
/* g = optional glyph shown on the chip; omitted where the label already carries it */
var PRESETS = [
{ g: FAM, label: "caf\u00E9 + family", text: "caf\u00E9 " + FAM },
{ label: "caf\u00E9 spelled two ways", text: "caf\u00E9 cafe\u0301" },
{ g: "\u{1F1EF}\u{1F1F5}", label: "flags", text: "\u{1F1EF}\u{1F1F5} \u{1F1E7}\u{1F1F7} \u{1F1FF}\u{1F1E6}" },
{ g: "\u{1F44B}\u{1F3FD}", label: "skin tones", text: "\u{1F44B} \u{1F44B}\u{1F3FD} \u{1F44B}\u{1F3FF}" },
{ g: "\u{1F408}"+Z+"\u2B1B", label: "joiner zoo", text: "\u{1F408}"+Z+"\u2B1B \u{1F468}"+Z+"\u{1F373} \u{1F3F3}\uFE0F"+Z+"\u{1F308}" },
{ g: "\u03A9\u2126", label: "look-alikes", text: "\u03A9 \u2126 \u00B5 \u03BC" },
{ label: "\u0928\u092E\u0938\u094D\u0924\u0947", text: "\u0928\u092E\u0938\u094D\u0924\u0947" },
{ label: "invisible guests", text: "tea\u00A0time\u200Bhere\u00AD!" }
];
var MAXC = 220, MAXB = 900;
var state = { text: PRESETS[0].text, seg: SEGMENTER ? "auto" : "fallback", base: "hex", sel: 0, part: 0 };
var A = null;
function mostInteresting(a) {
var best = 0, bn = -1;
a.clusters.forEach(function (cl, i) {
var score = cl.cps.length * 10 + cl.bytes.length;
if (score > bn) { bn = score; best = i; }
});
return best;
}
/* ================= glyph helpers ================= */
function glyphHTML(cl) {
var cps = cl.cps;
if (cps.length === 1) {
var cp = cps[0].cp;
if (ABBR[cp]) return '<span class="ghost">' + ABBR[cp] + "</span>";
if (isInvisible(cp) || cps[0].lone) return '<span class="ghost">U+' + hex(cp, 4) + "</span>";
if (isMark(cp)) return esc("◌" + cl.s);
}
if (cps.length && isMark(cps[0].cp)) return esc("◌" + cl.s);
return esc(cl.s);
}
function partGlyph(c) {
if (ABBR[c.cp]) return { html: ABBR[c.cp], glue: true };
if (isInvisible(c.cp) || c.lone) return { html: "U+" + hex(c.cp, 4), glue: true };
if (isMark(c.cp)) return { html: esc("◌" + c.ch), glue: false };
return { html: esc(c.ch), glue: false };
}
/* ================= renderers ================= */
function renderCounts() {
var meth = state.seg === "auto" && SEGMENTER ? "Intl.Segmenter" : "fallback rules";
var tiles = [
["lead", A.clusters.length, "characters you see", "grapheme clusters, split by " + meth],
["", A.nCps, "code points", "the numbered slots Unicode defines"],
["", A.nUnits, "UTF-16 units", "exactly what JavaScript .length returns"],
["", A.nBytes, "UTF-8 bytes", "what gets saved to a file or sent over a wire"]
];
$("counts").innerHTML = tiles.map(function (t) {
return '<div class="tile ' + t[0] + '"><div class="num">' + t[1] + '</div><div class="nm">' + esc(t[2]) +
'</div><div class="sub">' + esc(t[3]) + "</div></div>";
}).join("");
}
function renderStatus() {
var b = [];
if (!A.text) {
b.push('<span class="badge">nothing to take apart yet</span>');
} else if (A.roundTrip && A.nLone) {
/* green is only for "it worked" — these bytes come back, but they are not legal UTF-8 */
b.push('<span class="badge warn">these ' + A.nBytes +
" bytes decode back here, but a real UTF-8 encoder would reject them</span>");
} else if (A.roundTrip) {
b.push('<span class="badge ok">✓ round-trip verified — ' + A.nBytes +
" bytes decoded back to the same " + plural(A.clusters.length, "character", "characters") + "</span>");
} else {
b.push('<span class="badge bad">round-trip failed — these bytes do not decode back</span>');
}
if (A.text) {
if (A.te === true) b.push('<span class="badge">byte-for-byte match with TextEncoder</span>');
else if (A.te === null && A.nLone) b.push('<span class="badge warn">unpaired surrogate — real UTF-8 forbids this</span>');
else if (A.te === false) b.push('<span class="badge bad">differs from TextEncoder</span>');
if (A.nInvis) b.push('<span class="badge warn">' + plural(A.nInvis, "invisible code point", "invisible code points") + " in here</span>");
if (state.seg === "fallback" && SEGMENTER) {
var n = segment(A.text, "auto").length;
b.push('<span class="badge' + (n === A.clusters.length ? "" : " warn") + '">fallback rules ' +
(n === A.clusters.length ? "agree with" : "disagree with") + " Intl.Segmenter (" + A.clusters.length + " vs " + n + ")</span>");
}
}
/* this strip is aria-live; only touch it when it really changed, or every
keystroke re-announces the whole row to a screen reader */
var html = b.join("");
if ($("statusrow").innerHTML !== html) $("statusrow").innerHTML = html;
}
function renderGrid() {
var g = $("grid");
if (!A.clusters.length) {
g.innerHTML = '<p class="empty">Empty. <button class="btn" id="bLoad" type="button">load an example</button></p>';
var bl = $("bLoad");
if (bl) bl.addEventListener("click", function () { setText(PRESETS[0].text, true); });
$("gridNote").textContent = "";
return;
}
var shown = A.clusters.slice(0, MAXC), html = "";
shown.forEach(function (cl, i) {
var multi = cl.cps.length > 1;
var cpLabel = multi ? cl.cps.length + " code points" : "U+" + hex(cl.cps[0].cp, 4);
html += '<button class="card" type="button" data-i="' + i + '" aria-pressed="' + (i === state.sel) + '"' +
' tabindex="' + (i === state.sel ? 0 : -1) + '"' +
' aria-label="Character ' + (i + 1) + ": " + esc(describeShort(cl)) + '">' +
'<span class="idx">' + (i + 1) + "</span>" +
(multi ? '<span class="cnt">×' + cl.cps.length + "</span>" : "") +
'<span class="gly">' + glyphHTML(cl) + "</span>" +
'<span class="cp">' + esc(cpLabel) + "</span>" +
'<span class="by">' + cl.bytes.length + " B · " + cl.units + " u16</span>" +
"</button>";
});
g.innerHTML = html;
markSel();
$("gridNote").textContent = A.clusters.length > MAXC
? "showing first " + MAXC + " of " + A.clusters.length + " · arrow keys walk the row"
: "pick one to open it up · arrow keys walk the row";
}
/* One card at a time is in the tab order (roving tabindex): with a few hundred
characters on screen, making every card a tab stop makes the page unusable. */
function markSel() {
var cards = $("grid").querySelectorAll(".card"), any = false;
for (var i = 0; i < cards.length; i++) {
var on = +cards[i].getAttribute("data-i") === state.sel;
cards[i].setAttribute("aria-pressed", String(on));
cards[i].tabIndex = on ? 0 : -1;
if (on) any = true;
}
if (!any && cards.length) cards[0].tabIndex = 0; /* selection scrolled past the cap */
}
function selectCard(i, focus) {
if (i !== state.sel) {
state.sel = i; state.part = 0;
markSel(); renderDetail(true); renderDump();
}
if (focus) {
var c = $("grid").querySelector('.card[data-i="' + i + '"]');
if (c) c.focus();
}
}
function describeShort(cl) {
if (cl.cps.length === 1) {
var c = cl.cps[0];
return (c.name || "code point U+" + hex(c.cp, 4)) + ", " + plural(c.bytes.length, "byte", "bytes");
}
return cl.cps.length + " code points, " + plural(cl.bytes.length, "byte", "bytes");
}
function captionFor(cl) {
var cps = cl.cps, zwj = 0, skin = 0, vs = 0, marks = 0, ri = 0, keycap = 0, pics = 0;
cps.forEach(function (c) {
if (c.cp === 0x200D) zwj++;
else if (isSkin(c.cp)) skin++;
else if (c.cp >= 0xFE00 && c.cp <= 0xFE0F) vs++;
else if (c.cp === 0x20E3) keycap++;
else if (isRI(c.cp)) ri++;
else if (isMark(c.cp)) marks++;
else pics++;
});
if (cps.length === 1) {
var c = cps[0];
return "One code point, " + plural(c.bytes.length, "byte", "bytes") + ". " +
(c.name ? "Its name is <b>" + esc(c.name) + "</b>." : "It lives in the <b>" + esc(c.block) + "</b> block.");
}
if (ri === 2) {
var l1 = String.fromCharCode(65 + cps[0].cp - 0x1F1E6), l2 = String.fromCharCode(65 + cps[1].cp - 0x1F1E6);
return "Two letters, <b>" + l1 + "</b> and <b>" + l2 + "</b>, standing side by side. There is no flag code point — your device sees the pair and draws one.";
}
if (zwj > 0) {
return plural(pics, "picture", "pictures") + " held together by " + plural(zwj, "invisible joiner", "invisible joiners") +
". Remove one joiner and this single image falls apart into separate emoji — that is why deleting it can take " +
plural(cl.bytes.length, "byte", "bytes") + " away at once.";
}
if (skin) return "A base emoji plus a <b>skin-tone modifier</b>. Two code points, drawn as one hand.";
if (keycap) return "A plain character, an invisible <b>emoji-style request</b>, and a keycap sign stacked into one button.";
if (marks) return "A letter with " + plural(marks, "mark", "marks") + " stacked on top. It looks like one character because it <i>is</i> one character — just not one code point.";
if (vs) return "A symbol plus an invisible <b>variation selector</b> asking for the colourful emoji look instead of the plain one.";
return cps.length + " code points that your device draws as a single character.";
}
function planFor(cp) {
if (cp < 0x80) return { w: [7], m: [1] };
if (cp < 0x800) return { w: [5, 6], m: [3, 2] };
if (cp < 0x10000) return { w: [4, 6, 6], m: [4, 2, 2] };
return { w: [3, 6, 6, 6], m: [5, 2, 2, 2] };
}
function bitLab(c) {
var cp = c.cp, p = planFor(cp), total = p.w.reduce(function (a, b) { return a + b; }, 0);
var cpb = bin(cp, total), h = "";
h += '<div class="labname"><span class="u">U+' + hex(cp, 4) + "</span>" +
(c.name ? '<span class="nmv">' + esc(c.name) + "</span>" : '<span class="blk">no stored name here — it lives in the ' + esc(c.block) + " block</span>") +
'<span class="blk">decimal ' + cp + "</span></div>";
h += '<div class="k-lab" style="margin-top:11px">the ' + total + ' bits of the code point, sliced up</div><div class="cpbits">';
var at = 0;
for (var i = 0; i < p.w.length; i++) {
h += '<div class="chunk"><div class="cb">' + cpb.slice(at, at + p.w[i]) + '</div><div class="ct">→ byte ' + (i + 1) + "</div></div>";
at += p.w[i];
}
h += "</div>";
h += '<div class="k-lab" style="margin-top:12px">poured into ' + plural(c.bytes.length, "UTF-8 byte", "UTF-8 bytes") +
" — grey bits are the envelope, amber bits are the code point</div><div class=\"bytes\">";
for (var j = 0; j < c.bytes.length; j++) {
var bits = bin(c.bytes[j], 8), cells = "";
for (var k = 0; k < 8; k++) cells += '<span class="bit ' + (k < p.m[j] ? "mark" : "pay") + '">' + bits[k] + "</span>";
h += '<div class="bu"><div class="cells">' + cells + '</div><div class="blab">0x' + hex(c.bytes[j]) + " · " + c.bytes[j] + "</div></div>";
}
h += "</div>";
var rows = [];
if (c.units.length === 1) {
rows.push(["UTF-16", "one 16-bit unit · 0x" + hex(c.units[0], 4)]);
} else {
var v = cp - 0x10000;
rows.push(["UTF-16", "a surrogate pair · 0x" + hex(c.units[0], 4) + " 0x" + hex(c.units[1], 4)]);
rows.push(["", "0x" + hex(cp, 4) + " − 0x10000 = 0x" + hex(v, 4)]);
rows.push(["", "high = 0xD800 + (0x" + hex(v, 4) + " >> 10) = 0x" + hex(c.units[0], 4)]);
rows.push(["", "low = 0xDC00 + (0x" + hex(v, 4) + " & 0x3FF) = 0x" + hex(c.units[1], 4)]);
}
rows.push(["JavaScript", '"' + (cp > 0xFFFF ? "\\u{" + hex(cp, 4) + "}" : "\\u" + hex(cp, 4)) + '".length is ' + c.units.length]);
rows.push(["HTML", "&#x" + hex(cp, 4) + ";"]);
h += '<div class="rows">' + rows.map(function (r) {
return '<div class="r"><span class="t">' + (r[0] ? esc(r[0]) + " " : " ") + "</span>" + esc(r[1]) + "</div>";
}).join("") + "</div>";
return h;
}
function renderDetail(animate) {
var d = $("detail");
if (!A.clusters.length) {
d.innerHTML = '<p class="empty">Type something above and this panel opens it up, code point by code point.</p>';
$("detailKind").textContent = "";
$("detailTitle").textContent = "Inside one character";
return;
}
var cl = A.clusters[Math.min(state.sel, A.clusters.length - 1)];
if (state.part >= cl.cps.length) state.part = 0;
var h = '<div class="explode"><div class="whole"><span class="big">' + glyphHTML(cl) + "</span>" +
'<span><span class="k-lab">one character</span><br><span class="mono" style="font-size:.74rem">' +
cl.bytes.length + " bytes · " + cl.units + " UTF-16 units</span></span></div>" +
'<span class="eq">=</span>';
cl.cps.forEach(function (c, i) {
var g = partGlyph(c);
h += '<button class="part' + (g.glue ? " glue" : "") + (animate ? " pop" : "") + '" type="button" data-p="' + i +
'" aria-pressed="' + (i === state.part) + '" style="animation-delay:' + (animate ? i * 32 : 0) + 'ms"' +
' aria-label="' + esc((c.name || "U+" + hex(c.cp, 4)) + ", " + plural(c.bytes.length, "byte", "bytes")) + '">' +
'<span class="pg">' + g.html + '</span><span class="pc">U+' + hex(c.cp, 4) + "</span></button>";
});
h += "</div>";
h += '<p class="note">' + captionFor(cl) + "</p>";
h += '<div class="lab">' + bitLab(cl.cps[state.part]) + "</div>";
d.innerHTML = h;
$("detailTitle").textContent = cl.cps.length > 1 ? "Inside one character — " + cl.cps.length + " code points" : "Inside one character";
$("detailKind").textContent = "character " + (state.sel + 1) + " of " + A.clusters.length;
}
function fmtByte(b) { return state.base === "hex" ? hex(b) : state.base === "bin" ? bin(b, 8) : String(b); }
function renderDump() {
var d = $("dump");
if (!A.bytes.length) { d.innerHTML = '<span class="empty">no bytes yet</span>'; return; }
var out = "", count = 0, cut = false;
for (var i = 0; i < A.clusters.length && !cut; i++) {
var cl = A.clusters[i], inner = "";
for (var j = 0; j < cl.bytes.length; j++) {
if (count >= MAXB) { cut = true; break; }
inner += "<b>" + fmtByte(cl.bytes[j]) + "</b>";
count++;
}
if (inner) {
out += '<span class="g' + (i === state.sel ? " hot" : (i % 2 ? " alt" : "")) + '" title="' +
esc("character " + (i + 1) + ": " + describeShort(cl)) + '">' + inner + '</span><span class="gap"></span>';
}
}
if (cut) out += '<span class="empty">… and ' + (A.bytes.length - count) + " more bytes</span>";
d.innerHTML = out;
}
/* ================= drive ================= */
function render(animate) {
A = analyze(state.text, state.seg);
if (state.sel >= A.clusters.length) { state.sel = A.clusters.length ? mostInteresting(A) : 0; state.part = 0; }
renderCounts(); renderStatus(); renderGrid(); renderDetail(animate); renderDump();
}
function setText(t, reselect) {
state.text = t;
if ($("txt").value !== t) $("txt").value = t;
if (reselect) { state.sel = 0; state.part = 0; }
A = analyze(state.text, state.seg);
if (reselect) state.sel = A.clusters.length ? mostInteresting(A) : 0;
render(true);
}
/* presets */
(function () {
var host = $("presets");
PRESETS.forEach(function (p, i) {
var b = document.createElement("button");
b.type = "button"; b.className = "btn";
b.innerHTML = (p.g ? '<span class="glyph-sm">' + esc(p.g) + "</span> " : "") + esc(p.label);
b.title = p.text;
b.addEventListener("click", function () { setText(p.text, true); });
host.appendChild(b);
});
})();
/* text input */
$("txt").addEventListener("input", function () {
state.text = this.value;
render(false);
if (this.value.length >= 600) flash("600 UTF-16 units is the limit here");
});
/* rewrite buttons */
function norm(form) {
if (!state.text) { flash("nothing to rewrite — type something or pick an example"); return; }
try {
var t = state.text.normalize(form);
if (t === state.text) { flash("already in " + form + " — nothing moved"); return; }
setText(t, false);
flash(form === "NFC" ? "squashed into single characters where possible" : "split into letters + marks");
} catch (e) { flash("this browser cannot normalize text"); }
}
var flashT = null;
function flash(msg) {
var n = $("segNote");
n.textContent = msg;
if (flashT) clearTimeout(flashT);
flashT = setTimeout(setSegNote, 2600);
}
function setSegNote() {
$("segNote").textContent = SEGMENTER ? "Intl.Segmenter is available here" : "Intl.Segmenter missing — fallback rules in use";
}
$("bNFC").addEventListener("click", function () { norm("NFC"); });
$("bNFD").addEventListener("click", function () { norm("NFD"); });
$("bClear").addEventListener("click", function () { setText("", true); $("txt").focus(); });
/* segmentation mode */
[$("segAuto"), $("segFb")].forEach(function (b) {
b.addEventListener("click", function () {
if (b.disabled) return;
state.seg = b.getAttribute("data-seg");
$("segAuto").setAttribute("aria-pressed", String(state.seg === "auto"));
$("segFb").setAttribute("aria-pressed", String(state.seg === "fallback"));
state.sel = 0; state.part = 0;
A = analyze(state.text, state.seg);
state.sel = A.clusters.length ? mostInteresting(A) : 0;
render(true);
});
});
/* base toggles */
Array.prototype.forEach.call(document.querySelectorAll(".btn.base"), function (b) {
b.addEventListener("click", function () {
state.base = b.getAttribute("data-base");
Array.prototype.forEach.call(document.querySelectorAll(".btn.base"), function (o) {
o.setAttribute("aria-pressed", String(o === b));
});
renderDump();
});
});
/* copy */
$("bCopy").addEventListener("click", function () {
var btn = this, s = A.bytes.map(fmtByte).join(" ");
if (!s) { btn.textContent = "nothing to copy"; setTimeout(function () { btn.textContent = "copy bytes"; }, 1600); return; }
function done(ok) {
btn.textContent = ok ? "✓ copied" : "select the bytes below";
btn.style.color = ok ? "var(--run)" : "";
btn.style.borderColor = ok ? "var(--run)" : "";
setTimeout(function () { btn.textContent = "copy bytes"; btn.style.color = ""; btn.style.borderColor = ""; }, 1800);
}
function fallbackCopy() {
try {
var ta = document.createElement("textarea");
ta.value = s; ta.setAttribute("readonly", "");
ta.style.cssText = "position:fixed;top:0;left:0;opacity:0";
document.body.appendChild(ta); ta.select();
var ok = document.execCommand && document.execCommand("copy");
document.body.removeChild(ta);
done(!!ok);
} catch (e) { done(false); }
}
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(s).then(function () { done(true); }, fallbackCopy);
return;
}
} catch (e) { /* fall through */ }
fallbackCopy();
});
/* card + part selection (delegated) */
$("grid").addEventListener("click", function (e) {
var c = e.target.closest ? e.target.closest(".card") : null;
if (!c) return;
selectCard(+c.getAttribute("data-i"), true);
});
$("grid").addEventListener("keydown", function (e) {
var cards = $("grid").querySelectorAll(".card");
if (!cards.length) return;
var cur = -1;
for (var i = 0; i < cards.length; i++) if (cards[i] === document.activeElement) cur = i;
if (cur < 0) return;
var next = null;
if (e.key === "ArrowRight") next = Math.min(cur + 1, cards.length - 1);
else if (e.key === "ArrowLeft") next = Math.max(cur - 1, 0);
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = cards.length - 1;
if (next === null) return;
e.preventDefault();
selectCard(+cards[next].getAttribute("data-i"), true);
});
$("detail").addEventListener("click", function (e) {
var p = e.target.closest ? e.target.closest(".part") : null;
if (!p) return;
state.part = +p.getAttribute("data-p");
renderDetail(false);
var again = $("detail").querySelector('.part[data-p="' + state.part + '"]');
if (again) again.focus();
});
/* platform theme */
window.addEventListener("message", function (e) {
var d = e && e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
/* boot */
if (!SEGMENTER) {
$("segAuto").disabled = true;
$("segAuto").setAttribute("aria-pressed", "false");
$("segFb").setAttribute("aria-pressed", "true");
}
setSegNote();
$("txt").value = state.text;
A = analyze(state.text, state.seg);
state.sel = mostInteresting(A);
render(false);
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.