import test from 'node:test'; import assert from 'node:assert/strict'; import { tokenize, normalizeRange, pointSelection, adjustRange, occurrences, resolveOverlaps } from './range.mjs'; test('partition preserves original Unicode, whitespace and punctuation exactly', () => { const text = 'Hi, cafe\u0301!\nπŸ™‚ πŸ‘©β€πŸš€ well-known δΈ­ζ–‡'; const tokens = tokenize(text); assert.equal(tokens.map(t => t.text).join(''), text); let end = 0; for (const t of tokens) { assert.equal(t.start, end); assert.equal(text.slice(t.start, t.end), t.text); end = t.end; } assert.deepEqual(tokens.filter(t => t.word).map(t => t.text), ['Hi', 'cafe\u0301', 'πŸ™‚', 'πŸ‘©β€πŸš€', 'well', 'known', 'δΈ­ζ–‡']); }); test('reverse partial selection aligns words and preserves interior punctuation/newlines', () => { const text = ' bright,\nsmall step! '; const tokens = tokenize(text); const range = normalizeRange(text, tokens, 18, 4); assert.equal(range?.text, 'bright,\nsmall step'); assert.equal(range.start, 2); assert.equal(range.end, 20); assert.deepEqual(normalizeRange(text, tokens, 0, text.length), range); }); test('collapsed, whitespace, punctuation and invalid bounds do not select neighbors', () => { const text = 'a, b'; const tokens = tokenize(text); for (const [a, b] of [[1, 1], [1, 3], [-1, 2], [0, 5], [NaN, 3], [0.5, 3]]) { assert.equal(normalizeRange(text, tokens, a, b), null); } }); test('selection cannot cut a surrogate pair, ZWJ or combining grapheme', () => { const text = 'πŸ™‚ e\u0301 πŸ‘©β€πŸš€'; const tokens = tokenize(text); const first = normalizeRange(text, tokens, 1, 2); assert.equal(first?.text, 'πŸ™‚'); const accent = normalizeRange(text, tokens, 4, 5); assert.equal(accent?.text, 'e\u0301'); const astronaut = normalizeRange(text, tokens, 8, 9); assert.equal(astronaut?.text, 'πŸ‘©β€πŸš€'); assert.deepEqual([astronaut.start_cp, astronaut.end_cp, astronaut.start_utf8, astronaut.end_utf8, astronaut.start_utf16, astronaut.end_utf16], [5, 8, 9, 20, 6, 11]); }); test('point selection and boundary adjustment skip separators and never invert', () => { const text = 'one, two\nthree'; const tokens = tokenize(text); const middle = tokens.findIndex(t => t.text === 'two'); const one = pointSelection(text, tokens, middle); assert.equal(one?.text, 'two'); assert.equal(pointSelection(text, tokens, middle - 1), null); const left = adjustRange(text, tokens, one, 'start', -1); assert.equal(left?.text, 'one, two'); const all = adjustRange(text, tokens, left, 'end', 1); assert.equal(all?.text, text); assert.equal(adjustRange(text, tokens, all, 'start', 1)?.text, 'two\nthree'); assert.equal(adjustRange(text, tokens, all, 'end', -1)?.text, 'one, two'); assert.deepEqual(adjustRange(text, tokens, one, 'start', 1), one); assert.deepEqual(adjustRange(text, tokens, one, 'end', -1), one); assert.deepEqual(adjustRange(text, tokens, all, 'start', -1), all); assert.deepEqual(adjustRange(text, tokens, all, 'end', 1), all); }); test('occurrences preserve distinct offsets, case and exact combining form', () => { const text = 'a small step; a small step. A small step. steps e\u0301 Γ©'; assert.deepEqual(occurrences(text, 'a small step').map(r => [r.start, r.end]), [[0, 12], [14, 26]]); assert.equal(occurrences(text, 'step').length, 3); assert.equal(occurrences(text, 'e\u0301').length, 1); assert.equal(occurrences(text, 'Γ©').length, 1); assert.equal(occurrences(text, 'small st').length, 0); assert.deepEqual(occurrences(text, ''), []); }); test('overlapping occurrences remain available; display picks leftmost longest without mutation', () => { assert.deepEqual(occurrences('a a a', 'a a').map(r => r.start), [0, 2]); const ranges = [{ start: 2, end: 9, id: 'later' }, { start: 0, end: 3, id: 'short' }, { start: 0, end: 5, id: 'long' }, { start: 5, end: 7, id: 'next' }]; const before = structuredClone(ranges); assert.deepEqual(resolveOverlaps(ranges).map(r => r.id), ['long', 'next']); assert.deepEqual(ranges, before); });