diff --git a/backend/utils/notes-preview.js b/backend/utils/notes-preview.js new file mode 100644 index 0000000..7efaa74 --- /dev/null +++ b/backend/utils/notes-preview.js @@ -0,0 +1,26 @@ +function computeNotesPreview(notes) { + if (!notes || notes.trim() === '') { + return { has_notes: false, notes_preview: null }; + } + + const firstParagraph = notes.split(/\n\n/)[0]; + + const stripped = firstParagraph + .replace(/^#{1,6}\s+/gm, '') // headers + .replace(/\*\*(.+?)\*\*/g, '$1') // bold + .replace(/\*(.+?)\*/g, '$1') // italic with * + .replace(/_(.+?)_/g, '$1') // italic with _ + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links + .replace(/^[-*+]\s+/gm, '') // list markers + .replace(/\n/g, ' ') // collapse remaining newlines + .replace(/\s+/g, ' ') // collapse whitespace + .trim(); + + const truncated = stripped.length > 150 + ? stripped.slice(0, 150) + '...' + : stripped; + + return { has_notes: true, notes_preview: truncated }; +} + +module.exports = { computeNotesPreview }; diff --git a/tests/api/session-notes.test.js b/tests/api/session-notes.test.js new file mode 100644 index 0000000..20f0fc9 --- /dev/null +++ b/tests/api/session-notes.test.js @@ -0,0 +1,64 @@ +const { computeNotesPreview } = require('../../backend/utils/notes-preview'); + +describe('computeNotesPreview', () => { + test('returns has_notes false and null preview for null input', () => { + const result = computeNotesPreview(null); + expect(result).toEqual({ has_notes: false, notes_preview: null }); + }); + + test('returns has_notes false and null preview for empty string', () => { + const result = computeNotesPreview(''); + expect(result).toEqual({ has_notes: false, notes_preview: null }); + }); + + test('returns first paragraph as preview', () => { + const notes = 'First paragraph here.\n\nSecond paragraph here.'; + const result = computeNotesPreview(notes); + expect(result.has_notes).toBe(true); + expect(result.notes_preview).toBe('First paragraph here.'); + }); + + test('strips markdown bold formatting', () => { + const notes = '**Bold text** and more'; + const result = computeNotesPreview(notes); + expect(result.notes_preview).toBe('Bold text and more'); + }); + + test('strips markdown italic formatting', () => { + const notes = '*Italic text* and _also italic_'; + const result = computeNotesPreview(notes); + expect(result.notes_preview).toBe('Italic text and also italic'); + }); + + test('strips markdown links', () => { + const notes = 'Check [this link](http://example.com) out'; + const result = computeNotesPreview(notes); + expect(result.notes_preview).toBe('Check this link out'); + }); + + test('strips markdown headers', () => { + const notes = '## Header text'; + const result = computeNotesPreview(notes); + expect(result.notes_preview).toBe('Header text'); + }); + + test('strips markdown list markers', () => { + const notes = '- Item one\n- Item two'; + const result = computeNotesPreview(notes); + expect(result.notes_preview).toBe('Item one Item two'); + }); + + test('truncates to 150 characters with ellipsis', () => { + const notes = 'A'.repeat(200); + const result = computeNotesPreview(notes); + expect(result.notes_preview).toHaveLength(153); // 150 + '...' + expect(result.notes_preview.endsWith('...')).toBe(true); + }); + + test('does not truncate text at or under 150 characters', () => { + const notes = 'A'.repeat(150); + const result = computeNotesPreview(notes); + expect(result.notes_preview).toHaveLength(150); + expect(result.notes_preview).not.toContain('...'); + }); +});