Skip to content

Commit 0d4cca5

Browse files
authored
Fix: tolerate CRLF line endings in action-item toggle/count (#21)
1 parent 225a314 commit 0d4cca5

3 files changed

Lines changed: 28 additions & 3 deletions

File tree

src/lib/action-items.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ describe('toggleCheckboxAt', () => {
4646
const out = toggleCheckboxAt(src, 99, true)
4747
expect(out.ok).toBe(false)
4848
})
49+
50+
it('handles CRLF line endings and normalizes to LF', () => {
51+
const crlf = '- [ ] one\r\n- [x] two\r\n- [ ] three'
52+
const out = toggleCheckboxAt(crlf, 0, true)
53+
expect(out.ok).toBe(true)
54+
if (out.ok) expect(out.value).toBe('- [x] one\n- [x] two\n- [ ] three')
55+
})
56+
57+
it('toggles a middle line in a CRLF document', () => {
58+
const crlf = '- [ ] one\r\n- [ ] two\r\n- [ ] three'
59+
const out = toggleCheckboxAt(crlf, 1, true)
60+
expect(out.ok).toBe(true)
61+
if (out.ok) expect(out.value.split('\n')[1]).toBe('- [x] two')
62+
})
4963
})
5064

5165
describe('countActionItems', () => {
@@ -54,6 +68,10 @@ describe('countActionItems', () => {
5468
expect(countActionItems(src)).toEqual({ done: 2, total: 4 })
5569
})
5670

71+
it('counts CRLF documents', () => {
72+
expect(countActionItems('- [x] a\r\n- [ ] b\r\n- [x] c')).toEqual({ done: 2, total: 3 })
73+
})
74+
5775
it('treats null/empty as zero', () => {
5876
expect(countActionItems(null)).toEqual({ done: 0, total: 0 })
5977
expect(countActionItems('')).toEqual({ done: 0, total: 0 })

src/lib/action-items.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
const CHECKBOX_LINE = /^(\s*[-*]\s+)\[( |x|X)\](\s+.*)?$/
22

3+
// Split on any line ending (LF, CRLF, or lone CR). Matters because a value
4+
// saved with CRLF would otherwise leave a trailing \r on each line, which the
5+
// end-anchored CHECKBOX_LINE rejects — making every checkbox un-toggleable.
6+
const LINE_BREAK = /\r\n|\r|\n/
7+
38
export type Validated<T> =
49
| { ok: true; value: T }
510
| { ok: false; error: string }
@@ -10,7 +15,7 @@ export function toggleCheckboxAt(
1015
checked: boolean,
1116
): Validated<string> {
1217
if (!source) return { ok: false, error: 'No action items' }
13-
const lines = source.split('\n')
18+
const lines = source.split(LINE_BREAK)
1419
if (index < 0 || index >= lines.length) {
1520
return { ok: false, error: 'Line index out of range' }
1621
}
@@ -39,7 +44,7 @@ export function countActionItems(source: string | null): { done: number; total:
3944
if (!source) return { done: 0, total: 0 }
4045
let done = 0
4146
let total = 0
42-
for (const line of source.split('\n')) {
47+
for (const line of source.split(LINE_BREAK)) {
4348
const m = line.match(CHECKBOX_LINE)
4449
if (!m) continue
4550
total++

src/lib/actions/meetings.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,9 @@ export async function updateActionItems(
311311
if (!id) return actionError('Missing meeting id')
312312

313313
const raw = formData.get('action_items_md')
314-
const text = raw == null ? '' : String(raw)
314+
// Normalize line endings to LF on the way in — a CRLF value (e.g. pasted
315+
// text) would otherwise leave a trailing \r that breaks checkbox toggling.
316+
const text = (raw == null ? '' : String(raw)).replace(/\r\n?/g, '\n')
315317
if (text.length > 10_000) return actionError('Action items are too long (max 10000 chars)')
316318

317319
const value = text.trim().length === 0 ? null : text

0 commit comments

Comments
 (0)