Something went wrong. Try again.
A Bsky-like frontend using the atprotocol natively.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716import { describe, it, expect } from 'vitest';import { toggleStyle, detectStyles, type TextStyle, type DetectedStyles } from '../src/utils/unicodeTextStyles';
// ── helpers ────────────────────────────────────────────────────────────────
function mapStr(s: string, upperBase: number, lowerBase: number, digitsBase?: number): string { let out = ''; for (const ch of s) { const cp = ch.codePointAt(0)!; if (cp >= 65 && cp <= 90) out += String.fromCodePoint(upperBase + (cp - 65)); else if (cp >= 97 && cp <= 122) out += String.fromCodePoint(lowerBase + (cp - 97)); else if (cp >= 48 && cp <= 57 && digitsBase !== undefined) out += String.fromCodePoint(digitsBase + (cp - 48)); else out += ch; } return out;}
/** Bold */function B(s: string): string { return mapStr(s, 0x1D400, 0x1D41A, 0x1D7CE);}
/** Italic (with h → U+210E exception) */const ITALIC_LOWER: readonly number[] = [ 0x1D44E, 0x1D44F, 0x1D450, 0x1D451, 0x1D452, 0x1D453, 0x1D454, 0x210E, 0x1D456, 0x1D457, 0x1D458, 0x1D459, 0x1D45A, 0x1D45B, 0x1D45C, 0x1D45D, 0x1D45E, 0x1D45F, 0x1D460, 0x1D461, 0x1D462, 0x1D463, 0x1D464, 0x1D465, 0x1D466, 0x1D467,];function I(s: string): string { let out = ''; for (const ch of s) { const cp = ch.codePointAt(0)!; if (cp >= 65 && cp <= 90) out += String.fromCodePoint(0x1D434 + (cp - 65)); else if (cp >= 97 && cp <= 122) out += String.fromCodePoint(ITALIC_LOWER[cp - 97]); else out += ch; } return out;}
/** Bold Italic */function BI(s: string): string { return mapStr(s, 0x1D468, 0x1D482);}
// Script uppercase with Letterlike exceptionsconst SCRIPT_UPPER: readonly number[] = [ 0x1D49C, 0x212C, 0x1D49E, 0x1D49F, 0x2130, 0x2131, 0x1D4A2, 0x210B, 0x2110, 0x1D4A5, 0x1D4A6, 0x2112, 0x2133, 0x1D4A9, 0x1D4AA, 0x1D4AB, 0x1D4AC, 0x211B, 0x1D4AE, 0x1D4AF, 0x1D4B0, 0x1D4B1, 0x1D4B2, 0x1D4B3, 0x1D4B4, 0x1D4B5,];/** Script (fancy) */function S(s: string): string { let out = ''; for (const ch of s) { const cp = ch.codePointAt(0)!; if (cp >= 65 && cp <= 90) out += String.fromCodePoint(SCRIPT_UPPER[cp - 65]); else if (cp >= 97 && cp <= 122) out += String.fromCodePoint(0x1D4B6 + (cp - 97)); else out += ch; } return out;}
/** Bold Script */function BS(s: string): string { return mapStr(s, 0x1D4D0, 0x1D4EA);}
// Fraktur uppercase with Letterlike exceptionsconst FRAKTUR_UPPER: readonly number[] = [ 0x1D504, 0x1D505, 0x212D, 0x1D507, 0x1D508, 0x1D509, 0x1D50A, 0x210C, 0x2111, 0x1D50D, 0x1D50E, 0x1D50F, 0x1D510, 0x1D511, 0x1D512, 0x1D513, 0x1D514, 0x211C, 0x1D516, 0x1D517, 0x1D518, 0x1D519, 0x1D51A, 0x1D51B, 0x1D51C, 0x2128,];/** Fraktur */function F(s: string): string { let out = ''; for (const ch of s) { const cp = ch.codePointAt(0)!; if (cp >= 65 && cp <= 90) out += String.fromCodePoint(FRAKTUR_UPPER[cp - 65]); else if (cp >= 97 && cp <= 122) out += String.fromCodePoint(0x1D51E + (cp - 97)); else out += ch; } return out;}
/** Bold Fraktur */function BF(s: string): string { return mapStr(s, 0x1D56C, 0x1D586);}
// Double-struck uppercase with Letterlike exceptionsconst DS_UPPER: readonly number[] = [ 0x1D538, 0x1D539, 0x2102, 0x1D53B, 0x1D53C, 0x1D53D, 0x1D53E, 0x210D, 0x1D540, 0x1D541, 0x1D542, 0x1D543, 0x1D544, 0x2115, 0x1D546, 0x2119, 0x211A, 0x211D, 0x1D54A, 0x1D54B, 0x1D54C, 0x1D54D, 0x1D54E, 0x1D54F, 0x1D550, 0x2124,];/** Double-struck */function DS(s: string): string { let out = ''; for (const ch of s) { const cp = ch.codePointAt(0)!; if (cp >= 65 && cp <= 90) out += String.fromCodePoint(DS_UPPER[cp - 65]); else if (cp >= 97 && cp <= 122) out += String.fromCodePoint(0x1D552 + (cp - 97)); else if (cp >= 48 && cp <= 57) out += String.fromCodePoint(0x1D7D8 + (cp - 48)); else out += ch; } return out;}
/** Sans-serif */function SS(s: string): string { return mapStr(s, 0x1D5A0, 0x1D5BA, 0x1D7E2);}
/** Sans-serif Bold */function SSB(s: string): string { return mapStr(s, 0x1D5D4, 0x1D5EE, 0x1D7EC);}
/** Sans-serif Italic */function SSI(s: string): string { return mapStr(s, 0x1D608, 0x1D622);}
/** Sans-serif Bold Italic */function SSBI(s: string): string { return mapStr(s, 0x1D63C, 0x1D656);}
/** Monospace */function M(s: string): string { return mapStr(s, 0x1D670, 0x1D68A);}
/** Underline (without no-break space handling for simplicity) */function UL(s: string): string { let out = ''; for (const ch of s) { out += ch + '\u0332'; } return out;}
/** Strikethrough */function ST(s: string): string { let out = ''; for (const ch of s) { out += ch + '\u0336'; } return out;}
/** Stroke (enclosing square) */function SK(s: string): string { let out = ''; for (const ch of s) { out += ch + '\u20DE'; } return out;}
// ── tests ──────────────────────────────────────────────────────────────────
describe('unicodeTextStyles', () => { // --- Bold --- describe('bold', () => { it('enables bold on plain text', () => { expect(toggleStyle('Hello', 'bold')).toBe(B('Hello')); });
it('disables bold on bold text', () => { expect(toggleStyle(B('Hello'), 'bold')).toBe('Hello'); });
it('handles digits', () => { expect(toggleStyle('42', 'bold')).toBe(B('42')); });
it('preserves spaces and punctuation', () => { expect(toggleStyle('Hi, world!', 'bold')).toBe(B('Hi') + ', ' + B('world') + '!'); }); });
// --- Italic --- describe('italic', () => { it('enables italic on plain text', () => { expect(toggleStyle('Hello', 'italic')).toBe(I('Hello')); });
it('disables italic on italic text', () => { expect(toggleStyle(I('Hello'), 'italic')).toBe('Hello'); });
it('handles italic h exception (U+210E)', () => { expect(toggleStyle('h', 'italic')).toBe('\u210E'); expect(toggleStyle('\u210E', 'italic')).toBe('h'); });
it('enables italic on bold text → bold+italic', () => { expect(toggleStyle(B('Hi'), 'italic')).toBe(BI('Hi')); });
it('removes italic from bold-italic → bold', () => { expect(toggleStyle(BI('Hi'), 'italic')).toBe(B('Hi')); }); });
// --- Fancy (script) --- describe('fancy (script)', () => { it('enables script on plain text', () => { expect(toggleStyle('ab', 'fancy')).toBe(S('ab')); });
it('enables bold-script on bold text', () => { expect(toggleStyle(B('ab'), 'fancy')).toBe(BS('ab')); });
it('removes fancy from script → plain', () => { expect(toggleStyle(S('ab'), 'fancy')).toBe('ab'); });
it('removes fancy from bold-script → bold', () => { expect(toggleStyle(BS('ab'), 'fancy')).toBe(B('ab')); });
it('handles uppercase script chars including Letterlike Symbols exceptions', () => { const scriptB = S('B'); // ℬ expect(scriptB).toBe('\u212C'); expect(toggleStyle(scriptB, 'fancy')).toBe('B'); }); });
// --- Fraktur (gothic) --- describe('fraktur', () => { it('enables fraktur on plain text', () => { expect(toggleStyle('ab', 'fraktur')).toBe(F('ab')); });
it('enables bold-fraktur on bold text', () => { expect(toggleStyle(B('ab'), 'fraktur')).toBe(BF('ab')); });
it('disables fraktur on fraktur text', () => { expect(toggleStyle(F('ab'), 'fraktur')).toBe('ab'); });
it('disables fraktur on bold-fraktur → bold', () => { expect(toggleStyle(BF('ab'), 'fraktur')).toBe(B('ab')); });
it('handles uppercase fraktur Letterlike exceptions', () => { // C → ℭ (U+212D) expect(toggleStyle('C', 'fraktur')).toBe('\u212D'); expect(toggleStyle('\u212D', 'fraktur')).toBe('C'); // H → ℌ (U+210C) expect(toggleStyle('H', 'fraktur')).toBe('\u210C'); // R → ℜ (U+211C) expect(toggleStyle('R', 'fraktur')).toBe('\u211C'); // Z → ℨ (U+2128) expect(toggleStyle('Z', 'fraktur')).toBe('\u2128'); });
it('replaces script with fraktur', () => { expect(toggleStyle(S('ab'), 'fraktur')).toBe(F('ab')); }); });
// --- Double-struck --- describe('doubleStruck', () => { it('enables double-struck on plain text', () => { expect(toggleStyle('ab', 'doubleStruck')).toBe(DS('ab')); });
it('handles digits', () => { expect(toggleStyle('42', 'doubleStruck')).toBe(DS('42')); });
it('disables double-struck', () => { expect(toggleStyle(DS('ab'), 'doubleStruck')).toBe('ab'); });
it('handles uppercase double-struck Letterlike exceptions', () => { // C → ℂ (U+2102) expect(toggleStyle('C', 'doubleStruck')).toBe('\u2102'); // H → ℍ (U+210D) expect(toggleStyle('H', 'doubleStruck')).toBe('\u210D'); // N → ℕ (U+2115) expect(toggleStyle('N', 'doubleStruck')).toBe('\u2115'); // P → ℙ (U+2119) expect(toggleStyle('P', 'doubleStruck')).toBe('\u2119'); // Q → ℚ (U+211A) expect(toggleStyle('Q', 'doubleStruck')).toBe('\u211A'); // R → ℝ (U+211D) expect(toggleStyle('R', 'doubleStruck')).toBe('\u211D'); // Z → ℤ (U+2124) expect(toggleStyle('Z', 'doubleStruck')).toBe('\u2124'); });
it('preserves bold when enabling double-struck (bold is lost — no bold+DS block)', () => { // No bold double-struck block exists. Enabling DS on bold text replaces bold. expect(toggleStyle(B('ab'), 'doubleStruck')).toBe(DS('ab')); }); });
// --- Sans-serif --- describe('sansSerif', () => { it('enables sans-serif on plain text', () => { expect(toggleStyle('ab', 'sansSerif')).toBe(SS('ab')); });
it('handles digits', () => { expect(toggleStyle('42', 'sansSerif')).toBe(SS('42')); });
it('disables sans-serif', () => { expect(toggleStyle(SS('ab'), 'sansSerif')).toBe('ab'); });
it('enables sans-serif bold on bold text', () => { expect(toggleStyle(B('ab'), 'sansSerif')).toBe(SSB('ab')); });
it('enables sans-serif italic on italic text', () => { expect(toggleStyle(I('ab'), 'sansSerif')).toBe(SSI('ab')); });
it('enables sans-serif bold italic on bold-italic text', () => { expect(toggleStyle(BI('ab'), 'sansSerif')).toBe(SSBI('ab')); });
it('removes sans-serif from sans-serif bold → bold', () => { expect(toggleStyle(SSB('ab'), 'sansSerif')).toBe(B('ab')); }); });
// --- Monospace --- describe('monospace', () => { it('enables monospace on plain text', () => { expect(toggleStyle('ab', 'monospace')).toBe(M('ab')); });
it('disables monospace', () => { expect(toggleStyle(M('ab'), 'monospace')).toBe('ab'); });
it('replaces bold with monospace', () => { // No bold+monospace block; enabling mono on bold replaces bold expect(toggleStyle(B('ab'), 'monospace')).toBe(M('ab')); }); });
// --- Underline --- describe('underline', () => { it('adds combining low lines', () => { const result = toggleStyle('Hi', 'underline'); expect(result).toBe('H\u0332i\u0332'); });
it('removes underlines', () => { expect(toggleStyle('H\u0332i\u0332', 'underline')).toBe('Hi'); });
it('uses no-break space for continuous underline across spaces', () => { const result = toggleStyle('Hi there', 'underline'); expect(result).toContain('\u00A0\u0332'); const removed = toggleStyle(result, 'underline'); expect(removed).toBe('Hi there'); });
it('combines with bold+underline', () => { const boldUL = toggleStyle(toggleStyle('Hi', 'underline'), 'bold'); const stripped = boldUL.replace(/\u0332/g, ''); expect(stripped).toBe(B('Hi')); expect(boldUL.includes('\u0332')).toBe(true); }); });
// --- Strikethrough --- describe('strikethrough', () => { it('adds combining long stroke overlays', () => { const result = toggleStyle('Hi', 'strikethrough'); expect(result).toBe('H\u0336i\u0336'); });
it('removes strikethrough', () => { expect(toggleStyle('H\u0336i\u0336', 'strikethrough')).toBe('Hi'); });
it('uses no-break space for continuous strikethrough across spaces', () => { const result = toggleStyle('Hi there', 'strikethrough'); expect(result).toContain('\u00A0\u0336'); const removed = toggleStyle(result, 'strikethrough'); expect(removed).toBe('Hi there'); });
it('combines with bold+strikethrough', () => { const boldST = toggleStyle(toggleStyle('Hi', 'strikethrough'), 'bold'); const stripped = boldST.replace(/\u0336/g, ''); expect(stripped).toBe(B('Hi')); expect(boldST.includes('\u0336')).toBe(true); });
it('combines with underline+strikethrough', () => { let text = 'Hi'; text = toggleStyle(text, 'underline'); text = toggleStyle(text, 'strikethrough'); // Should have both combining marks expect(text.includes('\u0332')).toBe(true); expect(text.includes('\u0336')).toBe(true); // Remove just strikethrough text = toggleStyle(text, 'strikethrough'); expect(text.includes('\u0332')).toBe(true); expect(text.includes('\u0336')).toBe(false); // Remove underline text = toggleStyle(text, 'underline'); expect(text.includes('\u0332')).toBe(false); expect(text).toBe('Hi'); });
it('bold+underline+strikethrough triple', () => { let text = 'Hi'; text = toggleStyle(text, 'bold'); text = toggleStyle(text, 'underline'); text = toggleStyle(text, 'strikethrough'); const det = detectStyles(text); expect(det.bold).toBe(true); expect(det.underline).toBe(true); expect(det.strikethrough).toBe(true); // Remove bold → keep underline+strikethrough text = toggleStyle(text, 'bold'); const det2 = detectStyles(text); expect(det2.bold).toBe(false); expect(det2.underline).toBe(true); expect(det2.strikethrough).toBe(true); }); });
// --- Stroke (enclosing square) --- describe('stroke', () => { it('adds combining enclosing squares', () => { const result = toggleStyle('Hi', 'stroke'); expect(result).toBe('H\u20DEi\u20DE'); });
it('removes stroke', () => { expect(toggleStyle('H\u20DEi\u20DE', 'stroke')).toBe('Hi'); });
it('uses no-break space for continuous stroke across spaces', () => { const result = toggleStyle('Hi there', 'stroke'); expect(result).toContain('\u00A0\u20DE'); const removed = toggleStyle(result, 'stroke'); expect(removed).toBe('Hi there'); });
it('combines with bold+stroke', () => { const boldSK = toggleStyle(toggleStyle('Hi', 'stroke'), 'bold'); const stripped = boldSK.replace(/\u20DE/g, ''); expect(stripped).toBe(B('Hi')); expect(boldSK.includes('\u20DE')).toBe(true); });
it('combines with underline+stroke', () => { let text = 'Hi'; text = toggleStyle(text, 'underline'); text = toggleStyle(text, 'stroke'); expect(text.includes('\u0332')).toBe(true); expect(text.includes('\u20DE')).toBe(true); // Remove just stroke text = toggleStyle(text, 'stroke'); expect(text.includes('\u0332')).toBe(true); expect(text.includes('\u20DE')).toBe(false); // Remove underline text = toggleStyle(text, 'underline'); expect(text.includes('\u0332')).toBe(false); expect(text).toBe('Hi'); });
it('combines with strikethrough+stroke', () => { let text = 'Hi'; text = toggleStyle(text, 'strikethrough'); text = toggleStyle(text, 'stroke'); expect(text.includes('\u0336')).toBe(true); expect(text.includes('\u20DE')).toBe(true); // Remove strikethrough → keep stroke text = toggleStyle(text, 'strikethrough'); expect(text.includes('\u0336')).toBe(false); expect(text.includes('\u20DE')).toBe(true); });
it('bold+underline+strikethrough+stroke quadruple', () => { let text = 'Hi'; text = toggleStyle(text, 'bold'); text = toggleStyle(text, 'underline'); text = toggleStyle(text, 'strikethrough'); text = toggleStyle(text, 'stroke'); const det = detectStyles(text); expect(det.bold).toBe(true); expect(det.underline).toBe(true); expect(det.strikethrough).toBe(true); expect(det.stroke).toBe(true); // Remove bold → keep others text = toggleStyle(text, 'bold'); const det2 = detectStyles(text); expect(det2.bold).toBe(false); expect(det2.underline).toBe(true); expect(det2.strikethrough).toBe(true); expect(det2.stroke).toBe(true); }); });
// --- Font family mutual exclusivity --- describe('font family exclusivity', () => { it('switching from script to fraktur replaces correctly', () => { const scriptText = toggleStyle('ab', 'fancy'); expect(scriptText).toBe(S('ab')); const frakturText = toggleStyle(scriptText, 'fraktur'); expect(frakturText).toBe(F('ab')); });
it('switching from fraktur to sans-serif replaces correctly', () => { const frakturText = toggleStyle('ab', 'fraktur'); const ssText = toggleStyle(frakturText, 'sansSerif'); expect(ssText).toBe(SS('ab')); });
it('switching from sans-serif to monospace replaces correctly', () => { const ssText = toggleStyle('ab', 'sansSerif'); const monoText = toggleStyle(ssText, 'monospace'); expect(monoText).toBe(M('ab')); });
it('switching from monospace to double-struck replaces correctly', () => { const monoText = toggleStyle('ab', 'monospace'); const dsText = toggleStyle(monoText, 'doubleStruck'); expect(dsText).toBe(DS('ab')); }); });
// --- Combinations --- describe('combinations', () => { it('bold+italic round-trip', () => { const bi = toggleStyle(toggleStyle('Hello', 'bold'), 'italic'); expect(bi).toBe(BI('Hello')); const back = toggleStyle(toggleStyle(bi, 'italic'), 'bold'); expect(back).toBe('Hello'); });
it('bold+underline round-trip', () => { const bu = toggleStyle(toggleStyle('Hello', 'bold'), 'underline'); const noUnderline = toggleStyle(bu, 'underline'); expect(noUnderline).toBe(B('Hello')); const noBold = toggleStyle(noUnderline, 'bold'); expect(noBold).toBe('Hello'); });
it('bold+italic+underline triple combination', () => { const styled = toggleStyle(toggleStyle(toggleStyle('Hi', 'bold'), 'italic'), 'underline'); const noUL = toggleStyle(styled, 'underline'); expect(noUL).toBe(BI('Hi')); const noItalic = toggleStyle(noUL, 'italic'); expect(noItalic).toBe(B('Hi')); const noBold = toggleStyle(noItalic, 'bold'); expect(noBold.replace(/\u0332/g, '')).toBe('Hi'); });
it('bold+italic+underline can be individually removed', () => { let text = 'Hi'; text = toggleStyle(text, 'bold'); text = toggleStyle(text, 'italic'); text = toggleStyle(text, 'underline'); const det = detectStyles(text); expect(det.bold).toBe(true); expect(det.italic).toBe(true); expect(det.underline).toBe(true);
text = toggleStyle(text, 'italic'); const det2 = detectStyles(text); expect(det2.bold).toBe(true); expect(det2.italic).toBe(false); expect(det2.underline).toBe(true);
text = toggleStyle(text, 'bold'); const det3 = detectStyles(text); expect(det3.bold).toBe(false); expect(det3.underline).toBe(true);
text = toggleStyle(text, 'underline'); const det4 = detectStyles(text); expect(det4.underline).toBe(false); expect(text.replace(/\u0332/g, '')).toBe('Hi'); });
it('sans-serif bold + italic + underline', () => { let text = 'ab'; text = toggleStyle(text, 'sansSerif'); text = toggleStyle(text, 'bold'); text = toggleStyle(text, 'italic'); text = toggleStyle(text, 'underline'); const det = detectStyles(text); expect(det.fontFamily).toBe('sansSerif'); expect(det.bold).toBe(true); expect(det.italic).toBe(true); expect(det.underline).toBe(true); // Remove underline → sans-serif bold italic text = toggleStyle(text, 'underline'); expect(text).toBe(SSBI('ab')); }); });
// --- Detect --- describe('detectStyles', () => { it('returns none for plain text', () => { const d = detectStyles('Hello'); expect(d).toEqual({ bold: false, italic: false, underline: false, strikethrough: false, stroke: false, fontFamily: 'none' }); });
it('detects bold', () => { expect(detectStyles(B('x')).bold).toBe(true); });
it('detects italic', () => { expect(detectStyles(I('x')).italic).toBe(true); });
it('detects bold+italic', () => { const d = detectStyles(BI('x')); expect(d.bold).toBe(true); expect(d.italic).toBe(true); });
it('detects fancy (script)', () => { expect(detectStyles(S('x')).fontFamily).toBe('script'); });
it('detects fraktur', () => { expect(detectStyles(F('x')).fontFamily).toBe('fraktur'); });
it('detects double-struck', () => { expect(detectStyles(DS('x')).fontFamily).toBe('doubleStruck'); });
it('detects sans-serif', () => { expect(detectStyles(SS('x')).fontFamily).toBe('sansSerif'); });
it('detects sans-serif bold', () => { const d = detectStyles(SSB('x')); expect(d.fontFamily).toBe('sansSerif'); expect(d.bold).toBe(true); });
it('detects monospace', () => { expect(detectStyles(M('x')).fontFamily).toBe('monospace'); });
it('detects underline', () => { expect(detectStyles('x\u0332').underline).toBe(true); });
it('detects bold+underline', () => { const text = toggleStyle(toggleStyle('x', 'bold'), 'underline'); const d = detectStyles(text); expect(d.bold).toBe(true); expect(d.underline).toBe(true); }); });
// --- Edge cases --- describe('edge cases', () => { it('handles empty string', () => { expect(toggleStyle('', 'bold')).toBe(''); expect(detectStyles('')).toEqual({ bold: false, italic: false, underline: false, strikethrough: false, stroke: false, fontFamily: 'none' }); });
it('preserves emojis and non-ASCII characters', () => { const result = toggleStyle('Hello 🌍!', 'bold'); expect(result).toBe(B('Hello') + ' 🌍!'); });
it('preserves newlines', () => { const result = toggleStyle('Hi\nthere', 'bold'); expect(result).toBe(B('Hi') + '\n' + B('there')); });
it('handles mixed styled text (toggles based on first char)', () => { const mixed = B('Hello') + ' world'; const result = toggleStyle(mixed, 'bold'); expect(result).toBe('Hello world'); });
it('toggling same font family off restores plain', () => { const fText = toggleStyle('abc', 'fraktur'); const plain = toggleStyle(fText, 'fraktur'); expect(plain).toBe('abc'); });
it('full alphabet round-trip for each font family', () => { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const families: TextStyle[] = ['fancy', 'fraktur', 'doubleStruck', 'sansSerif', 'monospace']; for (const fam of families) { const styled = toggleStyle(alphabet, fam); const restored = toggleStyle(styled, fam); // For families without digits, digits pass through unchanged // For double-struck: no bold, so digits are styled expect(restored).toBe(alphabet); } }); });});