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 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-04-13 19:02:39 +02:00
parent ac17ca2635
commit cf7e138bc9
2 changed files with 24 additions and 8 deletions

View File

@ -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": {

View File

@ -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, {