const { MARKER_REGEX, parseMarker, MAX_MARKER_LEN } = require('../src/citations/markers'); const { createMarkerStripper } = require('../src/citations/markerStripper'); function consume(chunks) { const stripper = createMarkerStripper(); let text = ''; const markers = []; let malformed = 0; for (const chunk of chunks) { const step = stripper.push(chunk); text += step.text; markers.push(...step.markers); malformed += step.malformed; } const tail = stripper.flush(); text += tail.text; markers.push(...tail.markers); malformed += tail.malformed; return { text, markers, malformed }; } function splitRandom(value, random) { const chunks = []; let index = 0; while (index < value.length) { const size = 1 + Math.floor(random() * 5); chunks.push(value.slice(index, index + size)); index += size; } return chunks; } describe('marker grammar', () => { it('parses pages, ranges, and chapters', () => { expect(parseMarker('[[p:47]]')).toEqual({ kind: 'page', page: 47, pageEnd: 47 }); expect(parseMarker('[[P: 47 - 49]]')).toEqual({ kind: 'page', page: 47, pageEnd: 49 }); expect(parseMarker('[[ch:3]]')).toEqual({ kind: 'chapter', chapterNo: 3 }); expect(parseMarker('[[p:abc]]')).toBeNull(); expect(MAX_MARKER_LEN).toBe(24); expect('see [[p:47]][[ch:3]]'.match(MARKER_REGEX)).toHaveLength(2); }); }); describe('marker stripper', () => { it('keeps the same text and markers for any chunk split', () => { const source = 'Start [[p:47]] middle [[p:47-49]][[ch:3]] end.'; const whole = consume([source]); let seed = 7; const random = () => { seed = (seed * 1103515245 + 12345) % 2147483648; return seed / 2147483648; }; for (let trial = 0; trial < 30; trial += 1) { expect(consume(splitRandom(source, random))).toEqual(whole); } expect(whole.text).toBe('Start middle end.'); expect(whole.markers).toEqual([ { kind: 'page', page: 47, pageEnd: 47 }, { kind: 'page', page: 47, pageEnd: 49 }, { kind: 'chapter', chapterNo: 3 }, ]); }); it('handles markers at the start, middle, and end', () => { expect(consume(['[[ch:3]] Hello']).text).toBe('Hello'); expect(consume(['Hello [[p:2]] there']).text).toBe('Hello there'); expect(consume(['Hello [[p:2]].']).text).toBe('Hello.'); }); it('rejoins markers split across chunks', () => { expect(consume(['[[', 'p:', '47]]']).markers).toEqual([{ kind: 'page', page: 47, pageEnd: 47 }]); expect(consume(['[', '[p:47]]']).text).toBe(''); expect(consume(['See [', '[p:1]] now']).text).toBe('See now'); }); it('strips malformed markers and keeps ordinary brackets', () => { const bad = consume(['Note [[p:abc]] next']); expect(bad.text).toBe('Note next'); expect(bad.markers).toEqual([]); expect(bad.malformed).toBe(1); expect(consume(['[[p:4']).text).toBe('[[p:4'); expect(consume(['Read [text](url) please']).text).toBe('Read [text](url) please'); expect(consume(['Just [[ here']).text).toBe('Just [[ here'); expect(consume(['[[ not a marker at all and much longer than the hold']).text).toContain('[['); expect(consume(['Café [[p:8]] résumé']).text).toBe('Café résumé'); }); });