84 lines
4.0 KiB
JavaScript
84 lines
4.0 KiB
JavaScript
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);
|
|
});
|