import { describe, expect, it } from 'vitest';
import {
toPreviewHTML,
toggleLinePrefix,
toggleMarker,
stripFormatting,
isMarkerActive,
WHATSAPP_MARKERS,
} from './whatsapp-markup';
describe('toPreviewHTML — inline formatting', () => {
it('renders bold', () => {
expect(toPreviewHTML('*bold*')).toBe('bold');
});
it('renders italic', () => {
expect(toPreviewHTML('_italic_')).toBe('italic');
});
it('renders strikethrough', () => {
expect(toPreviewHTML('~strike~')).toBe('strike');
});
it('renders inline monospace', () => {
expect(toPreviewHTML('`code`')).toBe('code');
});
it('renders a single-character marker', () => {
expect(toPreviewHTML('*a*')).toBe('a');
});
it('formats markers mid-sentence', () => {
expect(toPreviewHTML('this is *bold* text')).toBe('this is bold text');
});
it('nests bold + italic', () => {
expect(toPreviewHTML('*_x_*')).toBe('x');
});
it('does not format markers padded with spaces', () => {
// Mid-sentence so the leading "* " is not parsed as a bullet list.
expect(toPreviewHTML('a * not bold * b')).toBe('a * not bold * b');
});
it('leaves unmatched markers literal', () => {
expect(toPreviewHTML('a * b')).toBe('a * b');
});
});
describe('toPreviewHTML — code spans are literal', () => {
it('does not format markers inside inline code', () => {
expect(toPreviewHTML('`*not bold*`')).toBe('*not bold*');
});
it('renders fenced code blocks', () => {
expect(toPreviewHTML('```\nline1\nline2\n```')).toBe('
line1\nline2
');
});
it('escapes html inside code', () => {
expect(toPreviewHTML('`&`')).toBe('<b>&</b>');
});
});
describe('toPreviewHTML — blocks', () => {
it('renders a blockquote', () => {
expect(toPreviewHTML('> quoted')).toBe('quoted
');
});
it('renders an unordered list', () => {
expect(toPreviewHTML('- one\n- two')).toBe('');
});
it('renders an ordered list', () => {
expect(toPreviewHTML('1. one\n2. two')).toBe('- one
- two
');
});
it('applies inline formatting within list items', () => {
expect(toPreviewHTML('- *bold*')).toBe('');
});
it('joins plain lines with
', () => {
expect(toPreviewHTML('a\nb')).toBe('a
b');
});
});
describe('toPreviewHTML — safety', () => {
it('escapes html in plain text', () => {
expect(toPreviewHTML('')).toBe(
'<script>alert(1)</script>',
);
});
it('returns empty string for empty input', () => {
expect(toPreviewHTML('')).toBe('');
});
});
describe('toggleMarker', () => {
const bold = WHATSAPP_MARKERS.bold;
it('wraps a selection', () => {
const r = toggleMarker('hello world', 6, 11, bold);
expect(r.value).toBe('hello *world*');
expect(r.value.slice(r.selectionStart, r.selectionEnd)).toBe('world');
});
it('wraps an empty selection and places caret between markers', () => {
const r = toggleMarker('hi ', 3, 3, bold);
expect(r.value).toBe('hi **');
expect(r.selectionStart).toBe(4);
expect(r.selectionEnd).toBe(4);
});
it('unwraps when the selection includes the markers', () => {
const r = toggleMarker('*world*', 0, 7, bold);
expect(r.value).toBe('world');
expect(r.value.slice(r.selectionStart, r.selectionEnd)).toBe('world');
});
it('unwraps when markers sit just outside the selection', () => {
const r = toggleMarker('*world*', 1, 6, bold);
expect(r.value).toBe('world');
expect(r.value.slice(r.selectionStart, r.selectionEnd)).toBe('world');
});
it('round-trips wrap then unwrap', () => {
const wrapped = toggleMarker('abc', 0, 3, bold);
expect(wrapped.value).toBe('*abc*');
const unwrapped = toggleMarker(
wrapped.value,
wrapped.selectionStart,
wrapped.selectionEnd,
bold,
);
expect(unwrapped.value).toBe('abc');
});
});
describe('toggleLinePrefix', () => {
it('adds a blockquote prefix to a single line', () => {
const r = toggleLinePrefix('hello', 0, 0, 'blockquote');
expect(r.value).toBe('> hello');
});
it('removes the blockquote prefix when toggled again', () => {
const r = toggleLinePrefix('> hello', 0, 0, 'blockquote');
expect(r.value).toBe('hello');
});
it('adds a bullet prefix', () => {
const r = toggleLinePrefix('milk', 0, 0, 'bullet');
expect(r.value).toBe('- milk');
});
it('numbers an ordered list across multiple selected lines', () => {
const text = 'one\ntwo\nthree';
const r = toggleLinePrefix(text, 0, text.length, 'ordered');
expect(r.value).toBe('1. one\n2. two\n3. three');
});
it('toggles an ordered list off', () => {
const text = '1. one\n2. two';
const r = toggleLinePrefix(text, 0, text.length, 'ordered');
expect(r.value).toBe('one\ntwo');
});
it('applies bullets to every line spanned by the selection', () => {
const text = 'a\nb';
const r = toggleLinePrefix(text, 0, text.length, 'bullet');
expect(r.value).toBe('- a\n- b');
});
it('only affects the line under the caret, not the whole text', () => {
const text = 'first\nsecond';
// caret inside "second" (index 8)
const r = toggleLinePrefix(text, 8, 8, 'blockquote');
expect(r.value).toBe('first\n> second');
});
it('switches family from bullet to ordered without stacking', () => {
const r = toggleLinePrefix('- a', 0, 3, 'ordered');
expect(r.value).toBe('1. a');
});
});
describe('stripFormatting', () => {
it('removes inline markers', () => {
expect(stripFormatting('*bold* _italic_ ~strike~ `mono`')).toBe('bold italic strike mono');
});
it('removes nested markers', () => {
expect(stripFormatting('*_x_*')).toBe('x');
});
it('unwraps fenced code blocks', () => {
expect(stripFormatting('```\ncode\n```')).toBe('code');
});
it('strips line-prefix blocks', () => {
expect(stripFormatting('> quote\n- item\n1. first')).toBe('quote\nitem\nfirst');
});
it('leaves plain text untouched', () => {
expect(stripFormatting('nothing here')).toBe('nothing here');
});
it('is idempotent', () => {
const once = stripFormatting('*a* _b_');
expect(stripFormatting(once)).toBe(once);
});
});
describe('isMarkerActive', () => {
const BOLD = WHATSAPP_MARKERS.bold;
const ITALIC = WHATSAPP_MARKERS.italic;
it('is false for a collapsed selection', () => {
expect(isMarkerActive('*hi*', 2, 2, BOLD)).toBe(false);
});
it('detects when the selection itself is wrapped (case A)', () => {
expect(isMarkerActive('*hi*', 0, 4, BOLD)).toBe(true);
});
it('detects when markers sit just outside the selection (case B)', () => {
// "hi" selected inside *hi*
expect(isMarkerActive('*hi*', 1, 3, BOLD)).toBe(true);
});
it('is false for an unformatted selection', () => {
expect(isMarkerActive('hi there', 0, 2, BOLD)).toBe(false);
});
it('is marker-specific', () => {
expect(isMarkerActive('_hi_', 0, 4, BOLD)).toBe(false);
expect(isMarkerActive('_hi_', 0, 4, ITALIC)).toBe(true);
});
});