From cf7e138bc9b0421b7c8742676118f2f0492b16d4 Mon Sep 17 00:00:00 2001 From: mARTin Date: Mon, 13 Apr 2026 19:02:39 +0200 Subject: [PATCH] fix: strip Apple Calendar icon escapes and emoji from PDF event titles Apple Calendar encodes calendar icons as =h=g / =i=g escape sequences in iCalendar SUMMARY fields during CalDAV sync. Helvetica cannot render the underlying glyphs, causing the raw escape bytes to appear verbatim in the PDF. Add pdfTitle() helper that strips these patterns along with emoji and Apple private-use area characters before rendering any title text. The calendar color chip/dot already provides the visual calendar identity. v1.97.3 Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- src/app/api/user/export-week-view/route.ts | 30 +++++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 721c71f..9105703 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.97.2", + "version": "1.97.3", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/api/user/export-week-view/route.ts b/src/app/api/user/export-week-view/route.ts index 45107f5..b0fa81d 100644 --- a/src/app/api/user/export-week-view/route.ts +++ b/src/app/api/user/export-week-view/route.ts @@ -114,6 +114,22 @@ function dayDateLabel(d: Date, de: boolean): string { }); } +// Strip Apple Calendar icon encodings (=h=g, =i=g, etc.) and emoji from +// event titles before PDF rendering. Helvetica cannot render these glyphs and +// they appear as raw escape sequences. The colored dot/chip already identifies +// the calendar visually, so the icon character carries no extra information. +function pdfTitle(s: string | null | undefined, maxLen = 60): string { + if (!s) return ''; + return s + .replace(/(?:=[a-zA-Z])+/g, '') // Apple CalDAV icon escapes: =h=g, =i=g, =k=g … + .replace(/[\u{1F000}-\u{1FFFF}]/gu, '') // emoji (supplementary plane) + .replace(/[\u{2600}-\u{27BF}]/gu, '') // misc symbols & dingbats + .replace(/[\uE000-\uF8FF]/g, '') // Apple private-use area + .replace(/[\uFE00-\uFE0F\u200D]/g, '') // variation selectors / ZWJ + .trim() + .slice(0, maxLen); +} + function fmtHour(h: number): string { return `${String(h).padStart(2, '0')}:00`; } @@ -454,7 +470,7 @@ function WeekCalendarPDF({ return React.createElement(View, { key: t.id, style: { borderRadius: 2, paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2, backgroundColor: DONE_BG }, - }, React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT } }, (t.title || '').slice(0, 36))); + }, React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT } }, pdfTitle(t.title, 36))); } if (t.isExternal) { // Calendar event: solid color chip with white text + dot indicator (matches webapp style) @@ -473,7 +489,7 @@ function WeekCalendarPDF({ React.createElement(View, { style: { width: 5, height: 5, borderRadius: 2.5, backgroundColor: evTextColor, opacity: 0.7, marginRight: 3, flexShrink: 0 }, }), - React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: evTextColor, flex: 1 } }, (t.title || '').slice(0, 36)), + React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: evTextColor, flex: 1 } }, pdfTitle(t.title, 36)), ); } // User-created all-day task: left border only, no background fill @@ -481,7 +497,7 @@ function WeekCalendarPDF({ return React.createElement(View, { key: t.id, style: { borderLeftWidth: 2, borderLeftColor: accent, paddingVertical: 1.5, paddingHorizontal: 4, marginBottom: 2 }, - }, React.createElement(Text, { style: { fontSize: 7.5, color: '#374151' } }, (t.title || '').slice(0, 36))); + }, React.createElement(Text, { style: { fontSize: 7.5, color: '#374151' } }, pdfTitle(t.title, 36))); }), allDayTasks.length > 5 ? React.createElement(Text, { style: { fontSize: 6, color: '#3b82f6', marginTop: 1 }, @@ -575,7 +591,7 @@ function WeekCalendarPDF({ key: t.id, style: { marginBottom: 1, paddingLeft: 1, paddingVertical: 1 }, }, - React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT, lineHeight: 1.3, textDecoration: 'line-through' } }, (t.title || '').slice(0, 60)), + React.createElement(Text, { style: { fontSize: 7.5, color: DONE_TEXT, lineHeight: 1.3, textDecoration: 'line-through' } }, pdfTitle(t.title)), ); } @@ -589,7 +605,7 @@ function WeekCalendarPDF({ paddingLeft: 3, paddingVertical: 1, marginBottom: 1, }, }, - React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, (t.title || '').slice(0, 60)), + React.createElement(Text, { style: { fontFamily: 'Helvetica-Bold', fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, pdfTitle(t.title)), ); } @@ -601,7 +617,7 @@ function WeekCalendarPDF({ paddingVertical: 1, marginBottom: 1, }, }, - React.createElement(Text, { style: { fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, (t.title || '').slice(0, 60)), + React.createElement(Text, { style: { fontSize: 7.5, color: '#1e293b', lineHeight: 1.3 } }, pdfTitle(t.title)), ); }), ); @@ -719,7 +735,7 @@ function WeekCalendarPDF({ }), React.createElement(Text, { style: { fontSize: 7, color: '#374151', lineHeight: 1.3, flex: 1 }, - }, (t.title || '').slice(0, 52)), + }, pdfTitle(t.title)), ) ), list.tasks.length > 18 ? React.createElement(Text, {