/* This file is part of the kcalutils library. Copyright (c) 2000,2001 Cornelius Schumacher Copyright (C) 2004 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "htmlexport.h" #include "htmlexportsettings.h" #include "stringify.h" #include using namespace KCalCore; #include #include #include #include #include #include #include #include using namespace KCalUtils; static QString cleanChars(const QString &txt); //@cond PRIVATE class KCalUtils::HtmlExport::Private { public: Private(MemoryCalendar *calendar, HTMLExportSettings *settings) : mCalendar(calendar), mSettings(settings) {} MemoryCalendar *mCalendar; HTMLExportSettings *mSettings; QMap mHolidayMap; }; //@endcond HtmlExport::HtmlExport(MemoryCalendar *calendar, HTMLExportSettings *settings) : d(new Private(calendar, settings)) { } HtmlExport::~HtmlExport() { delete d; } bool HtmlExport::save(const QString &fileName) { QString fn(fileName); if (fn.isEmpty() && d->mSettings) { fn = d->mSettings->outputFile(); } if (!d->mSettings || fn.isEmpty()) { return false; } QFile f(fileName); if (!f.open(QIODevice::WriteOnly)) { return false; } QTextStream ts(&f); bool success = save(&ts); f.close(); return success; } bool HtmlExport::save(QTextStream *ts) { if (!d->mSettings) { return false; } ts->setCodec("UTF-8"); // Write HTML header *ts << "" << endl; *ts << "" << endl; *ts << " " << endl; if (!d->mSettings->pageTitle().isEmpty()) { *ts << " " << d->mSettings->pageTitle() << "" << endl; } *ts << " " << endl; *ts << "" << endl; // FIXME: Write header // (Heading, Calendar-Owner, Calendar-Date, ...) if (d->mSettings->eventView() || d->mSettings->monthView() || d->mSettings->weekView()) { if (!d->mSettings->eventTitle().isEmpty()) { *ts << "

" << d->mSettings->eventTitle() << "

" << endl; } // Write Week View if (d->mSettings->weekView()) { createWeekView(ts); } // Write Month View if (d->mSettings->monthView()) { createMonthView(ts); } // Write Event List if (d->mSettings->eventView()) { createEventList(ts); } } // Write Todo List if (d->mSettings->todoView()) { if (!d->mSettings->todoListTitle().isEmpty()) { *ts << "

" << d->mSettings->todoListTitle() << "

" << endl; } createTodoList(ts); } // Write Journals if (d->mSettings->journalView()) { if (!d->mSettings->journalTitle().isEmpty()) { *ts << "

" << d->mSettings->journalTitle() << "

" << endl; } createJournalView(ts); } // Write Free/Busy if (d->mSettings->freeBusyView()) { if (!d->mSettings->freeBusyTitle().isEmpty()) { *ts << "

" << d->mSettings->freeBusyTitle() << "

" << endl; } createFreeBusyView(ts); } createFooter(ts); // Write HTML trailer *ts << "" << endl; return true; } void HtmlExport::createMonthView(QTextStream *ts) { QDate start = fromDate(); start.setYMD(start.year(), start.month(), 1); // go back to first day in month QDate end(start.year(), start.month(), start.daysInMonth()); int startmonth = start.month(); int startyear = start.year(); while (start < toDate()) { // Write header QDate hDate(start.year(), start.month(), 1); QString hMon = hDate.toString(QLatin1String("MMMM")); QString hYear = hDate.toString(QLatin1String("yyyy")); *ts << "

" << i18nc("@title month and year", "%1 %2", hMon, hYear) << "

" << endl; if (KGlobal::locale()->weekStartDay() == 1) { start = start.addDays(1 - start.dayOfWeek()); } else { if (start.dayOfWeek() != 7) { start = start.addDays(-start.dayOfWeek()); } } *ts << "" << endl; // Write table header *ts << " "; for (int i=0; i < 7; ++i) { *ts << ""; } *ts << "" << endl; // Write days while (start <= end) { *ts << " " << endl; for (int i=0; i < 7; ++i) { *ts << " " << endl; start = start.addDays(1); } *ts << " " << endl; } *ts << "
" << KGlobal::locale()->calendar()->weekDayName(start.addDays(i)) << "
"; *ts << "
mHolidayMap.contains(start) || start.dayOfWeek() == 7) { *ts << "class=\"dateholiday\""; } else { *ts << "class=\"date\""; } *ts << ">" << QString::number(start.day()); if (d->mHolidayMap.contains(start)) { *ts << " " << d->mHolidayMap[start] << ""; } *ts << "
"; // Only print events within the from-to range if (start >= fromDate() && start <= toDate()) { Event::List events = d->mCalendar->events(start, d->mCalendar->timeSpec(), EventSortStartDate, SortDirectionAscending); if (events.count()) { *ts << ""; Event::List::ConstIterator it; for (it = events.constBegin(); it != events.constEnd(); ++it) { if (checkSecrecy(*it)) { createEvent(ts, *it, start, false); } } *ts << "
"; } else { *ts << " "; } } *ts << "
" << endl; startmonth += 1; if (startmonth > 12) { startyear += 1; startmonth = 1; } start.setYMD(startyear, startmonth, 1); end.setYMD(start.year(), start.month(), start.daysInMonth()); } } void HtmlExport::createEventList(QTextStream *ts) { int columns = 3; *ts << "" << endl; *ts << " " << endl; *ts << " " << endl; *ts << " " << endl; *ts << " " << endl; if (d->mSettings->eventLocation()) { *ts << " " << endl; ++columns; } if (d->mSettings->eventCategories()) { *ts << " " << endl; ++columns; } if (d->mSettings->eventAttendees()) { *ts << " " << endl; ++columns; } *ts << " " << endl; for (QDate dt = fromDate(); dt <= toDate(); dt = dt.addDays(1)) { kDebug() << "Getting events for" << dt.toString(); Event::List events = d->mCalendar->events(dt, d->mCalendar->timeSpec(), EventSortStartDate, SortDirectionAscending); if (events.count()) { *ts << " " << endl; Event::List::ConstIterator it; for (it = events.constBegin(); it != events.constEnd(); ++it) { if (checkSecrecy(*it)) { createEvent(ts, *it, dt); } } } } *ts << "
" << i18nc("@title:column event start time", "Start Time") << "" << i18nc("@title:column event end time", "End Time") << "" << i18nc("@title:column event description", "Event") << "" << i18nc("@title:column event locatin", "Location") << "" << i18nc("@title:column event categories", "Categories") << "" << i18nc("@title:column event attendees", "Attendees") << "
" << KGlobal::locale()->formatDate(dt) << "
" << endl; } void HtmlExport::createEvent(QTextStream *ts, const Event::Ptr &event, const QDate &date, bool withDescription) { kDebug() << event->summary(); *ts << " " << endl; if (!event->allDay()) { if (event->isMultiDay(d->mCalendar->timeSpec()) && (event->dtStart().date() != date)) { *ts << "  " << endl; } else { *ts << " " << Stringify::formatTime(event->dtStart(), true, d->mCalendar->timeSpec()) << "" << endl; } if (event->isMultiDay(d->mCalendar->timeSpec()) && (event->dtEnd().date() != date)) { *ts << "  " << endl; } else { *ts << " " << Stringify::formatTime(event->dtEnd(), true, d->mCalendar->timeSpec()) << "" << endl; } } else { *ts << "   " << endl; } *ts << " " << endl; *ts << " " << cleanChars(event->summary()) << "" << endl; if (withDescription && !event->description().isEmpty()) { *ts << "

" << breakString(cleanChars(event->description())) << "

" << endl; } *ts << " " << endl; if (d->mSettings->eventLocation()) { *ts << " " << endl; formatLocation(ts, event); *ts << " " << endl; } if (d->mSettings->eventCategories()) { *ts << " " << endl; formatCategories(ts, event); *ts << " " << endl; } if (d->mSettings->eventAttendees()) { *ts << " " << endl; formatAttendees(ts, event); *ts << " " << endl; } *ts << " " << endl; } void HtmlExport::createTodoList(QTextStream *ts) { Todo::List rawTodoList = d->mCalendar->todos(); int index = 0; while (index < rawTodoList.count()) { Todo::Ptr ev = rawTodoList[ index ]; Todo::Ptr subev = ev; const QString uid = ev->relatedTo(); if (!uid.isEmpty()) { Incidence::Ptr inc = d->mCalendar->incidence(uid); if (inc && inc->type() == Incidence::TypeTodo) { Todo::Ptr todo = inc.staticCast(); if (!rawTodoList.contains(todo)) { rawTodoList.append(todo); } } } index = rawTodoList.indexOf(subev); ++index; } // FIXME: Sort list by priorities. This is brute force and should be // replaced by a real sorting algorithm. Todo::List todoList; Todo::List::ConstIterator it; for (int i = 1; i <= 9; ++i) { for (it = rawTodoList.constBegin(); it != rawTodoList.constEnd(); ++it) { if ((*it)->priority() == i && checkSecrecy(*it)) { todoList.append(*it); } } } for (it = rawTodoList.constBegin(); it != rawTodoList.constEnd(); ++it) { if ((*it)->priority() == 0 && checkSecrecy(*it)) { todoList.append(*it); } } int columns = 3; *ts << "" << endl; *ts << " " << endl; *ts << " " << endl; *ts << " " << endl; *ts << " " << endl; if (d->mSettings->taskDueDate()) { *ts << " " << endl; ++columns; } if (d->mSettings->taskLocation()) { *ts << " " << endl; ++columns; } if (d->mSettings->taskCategories()) { *ts << " " << endl; ++columns; } if (d->mSettings->taskAttendees()) { *ts << " " << endl; ++columns; } *ts << " " << endl; // Create top-level list. for (it = todoList.constBegin(); it != todoList.constEnd(); ++it) { if ((*it)->relatedTo().isEmpty()) { createTodo(ts, *it); } } // Create sub-level lists for (it = todoList.constBegin(); it != todoList.constEnd(); ++it) { Incidence::List relations = d->mCalendar->relations((*it)->uid()); if (relations.count()) { // Generate sub-to-do list *ts << " " << endl; *ts << " " << endl; *ts << " " << endl; Todo::List sortedList; // FIXME: Sort list by priorities. This is brute force and should be // replaced by a real sorting algorithm. for (int i = 1; i <= 9; ++i) { Incidence::List::ConstIterator it2; for (it2 = relations.constBegin(); it2 != relations.constEnd(); ++it2) { Todo::Ptr ev3 = (*it2).staticCast(); if (ev3 && ev3->priority() == i) { sortedList.append(ev3); } } } Incidence::List::ConstIterator it2; for (it2 = relations.constBegin(); it2 != relations.constEnd(); ++it2) { Todo::Ptr ev3 = (*it2).staticCast(); if (ev3 && ev3->priority() == 0) { sortedList.append(ev3); } } Todo::List::ConstIterator it3; for (it3 = sortedList.constBegin(); it3 != sortedList.constEnd(); ++it3) { createTodo(ts, *it3); } } } *ts << "
" << i18nc("@title:column", "To-do") << "" << i18nc("@title:column to-do priority", "Priority") << "" << i18nc("@title:column to-do percent completed", "Completed") << "" << i18nc("@title:column to-do due date", "Due Date") << "" << i18nc("@title:column to-do location", "Location") << "" << i18nc("@title:column to-do categories", "Categories") << "" << i18nc("@title:column to-do attendees", "Attendees") << "
uid() << "\">" << i18nc("@title:column sub-to-dos of the parent to-do", "Sub-To-dos of: ") << "uid() << "\">" << cleanChars((*it)->summary()) << "
" << endl; } void HtmlExport::createTodo(QTextStream *ts, const Todo::Ptr &todo) { kDebug(); const bool completed = todo->isCompleted(); Incidence::List relations = d->mCalendar->relations(todo->uid()); *ts << "" << endl; *ts << " " << endl; *ts << " uid() << "\">" << endl; *ts << " " << cleanChars(todo->summary()) << "" << endl; if (!todo->description().isEmpty()) { *ts << "

" << breakString(cleanChars(todo->description())) << "

" << endl; } if (relations.count()) { *ts << " " << endl; } *ts << " " << endl; *ts << " " << endl; *ts << " " << todo->priority() << endl; *ts << " " << endl; *ts << " " << endl; *ts << " " << i18nc("@info/plain to-do percent complete", "%1 %", todo->percentComplete()) << endl; *ts << " " << endl; if (d->mSettings->taskDueDate()) { *ts << " " << endl; if (todo->hasDueDate()) { *ts << " " << Stringify::formatDate(todo->dtDue(true)) << endl; } else { *ts << "  " << endl; } *ts << " " << endl; } if (d->mSettings->taskLocation()) { *ts << " " << endl; formatLocation(ts, todo); *ts << " " << endl; } if (d->mSettings->taskCategories()) { *ts << " " << endl; formatCategories(ts, todo); *ts << " " << endl; } if (d->mSettings->taskAttendees()) { *ts << " " << endl; formatAttendees(ts, todo); *ts << " " << endl; } *ts << "" << endl; } void HtmlExport::createWeekView(QTextStream *ts) { Q_UNUSED(ts); // FIXME: Implement this! } void HtmlExport::createJournalView(QTextStream *ts) { Q_UNUSED(ts); // Journal::List rawJournalList = d->mCalendar->journals(); // FIXME: Implement this! } void HtmlExport::createFreeBusyView(QTextStream *ts) { Q_UNUSED(ts); // FIXME: Implement this! } bool HtmlExport::checkSecrecy(const Incidence::Ptr &incidence) { int secrecy = incidence->secrecy(); if (secrecy == Incidence::SecrecyPublic) { return true; } if (secrecy == Incidence::SecrecyPrivate && !d->mSettings->excludePrivate()) { return true; } if (secrecy == Incidence::SecrecyConfidential && !d->mSettings->excludeConfidential()) { return true; } return false; } void HtmlExport::formatLocation(QTextStream *ts, const Incidence::Ptr &incidence) { if (!incidence->location().isEmpty()) { *ts << " " << cleanChars(incidence->location()) << endl; } else { *ts << "  " << endl; } } void HtmlExport::formatCategories(QTextStream *ts, const Incidence::Ptr &incidence) { if (!incidence->categoriesStr().isEmpty()) { *ts << " " << cleanChars(incidence->categoriesStr()) << endl; } else { *ts << "  " << endl; } } void HtmlExport::formatAttendees(QTextStream *ts, const Incidence::Ptr &incidence) { Attendee::List attendees = incidence->attendees(); if (attendees.count()) { *ts << ""; *ts << incidence->organizer()->fullName(); *ts << "
"; Attendee::List::ConstIterator it; for (it = attendees.constBegin(); it != attendees.constEnd(); ++it) { Attendee::Ptr a(*it); if (!a->email().isEmpty()) { *ts << "email(); *ts << "\">" << cleanChars(a->name()) << ""; } else { *ts << " " << cleanChars(a->name()); } *ts << "
" << endl; } } else { *ts << "  " << endl; } } QString HtmlExport::breakString(const QString &text) { int number = text.count(QLatin1String("\n")); if (number <= 0) { return text; } else { QString out; QString tmpText = text; int pos = 0; QString tmp; for (int i = 0; i <= number; ++i) { pos = tmpText.indexOf(QLatin1String("\n")); tmp = tmpText.left(pos); tmpText = tmpText.right(tmpText.length() - pos - 1); out += tmp + QLatin1String("
"); } return out; } } void HtmlExport::createFooter(QTextStream *ts) { // FIXME: Implement this in a translatable way! QString trailer = i18nc("@info/plain", "This page was created "); /* bool hasPerson = false; bool hasCredit = false; bool hasCreditURL = false; QString mail, name, credit, creditURL;*/ if (!d->mSettings->eMail().isEmpty()) { if (!d->mSettings->name().isEmpty()) { trailer += i18nc("@info/plain page creator email link with name", "by %2 ", d->mSettings->eMail(), d->mSettings->name()); } else { trailer += i18nc("@info/plain page creator email link", "by %2 ", d->mSettings->eMail(), d->mSettings->eMail()); } } else { if (!d->mSettings->name().isEmpty()) { trailer += i18nc("@info/plain page creator name only", "by %1 ", d->mSettings->name()); } } if (!d->mSettings->creditName().isEmpty()) { if (!d->mSettings->creditURL().isEmpty()) { trailer += i18nc("@info/plain page credit with name and link", "with %2", d->mSettings->creditURL(), d->mSettings->creditName()); } else { trailer += i18nc("@info/plain page credit name only", "with %1", d->mSettings->creditName()); } } *ts << "

" << trailer << "

" << endl; } QString cleanChars(const QString &text) { QString txt = text; txt = txt.replace(QLatin1Char('&'), QLatin1String("&")); txt = txt.replace(QLatin1Char('<'), QLatin1String("<")); txt = txt.replace(QLatin1Char('>'), QLatin1String(">")); txt = txt.replace(QLatin1Char('\"'), QLatin1String(""")); txt = txt.replace(QString::fromUtf8("ä"), QLatin1String("ä")); txt = txt.replace(QString::fromUtf8("Ä"), QLatin1String("Ä")); txt = txt.replace(QString::fromUtf8("ö"), QLatin1String("ö")); txt = txt.replace(QString::fromUtf8("Ö"), QLatin1String("Ö")); txt = txt.replace(QString::fromUtf8("ü"), QLatin1String("ü")); txt = txt.replace(QString::fromUtf8("Ü"), QLatin1String("Ü")); txt = txt.replace(QString::fromUtf8("ß"), QLatin1String("ß")); txt = txt.replace(QString::fromUtf8("€"), QLatin1String("€")); txt = txt.replace(QString::fromUtf8("é"), QLatin1String("é")); return txt; } QString HtmlExport::styleSheet() const { if (!d->mSettings->styleSheet().isEmpty()) { return d->mSettings->styleSheet(); } QString css; if (QApplication::isRightToLeft()) { css += QLatin1String(" body { background-color:white; color:black; direction: rtl }\n"); css += QLatin1String(" td { text-align:center; background-color:#eee }\n"); css += QLatin1String(" th { text-align:center; background-color:#228; color:white }\n"); css += QLatin1String(" td.sumdone { background-color:#ccc }\n"); css += QLatin1String(" td.done { background-color:#ccc }\n"); css += QLatin1String(" td.subhead { text-align:center; background-color:#ccf }\n"); css += QLatin1String(" td.datehead { text-align:center; background-color:#ccf }\n"); css += QLatin1String(" td.space { background-color:white }\n"); css += QLatin1String(" td.dateholiday { color:red }\n"); } else { css += QLatin1String(" body { background-color:white; color:black }\n"); css += QLatin1String(" td { text-align:center; background-color:#eee }\n"); css += QLatin1String(" th { text-align:center; background-color:#228; color:white }\n"); css += QLatin1String(" td.sum { text-align:left }\n"); css += QLatin1String(" td.sumdone { text-align:left; background-color:#ccc }\n"); css += QLatin1String(" td.done { background-color:#ccc }\n"); css += QLatin1String(" td.subhead { text-align:center; background-color:#ccf }\n"); css += QLatin1String(" td.datehead { text-align:center; background-color:#ccf }\n"); css += QLatin1String(" td.space { background-color:white }\n"); css += QLatin1String(" td.date { text-align:left }\n"); css += QLatin1String(" td.dateholiday { text-align:left; color:red }\n"); } return css; } void HtmlExport::addHoliday(const QDate &date, const QString &name) { if (d->mHolidayMap[date].isEmpty()) { d->mHolidayMap[date] = name; } else { d->mHolidayMap[date] = i18nc("@info/plain holiday by date and name", "%1, %2", d->mHolidayMap[date], name); } } QDate HtmlExport::fromDate() const { return d->mSettings->dateStart().date(); } QDate HtmlExport::toDate() const { return d->mSettings->dateEnd().date(); }