Commit 1c3bd593 authored by Sandro Knauß's avatar Sandro Knauß

Move renderingparts to own subdir.

cleanup parsing and rendering from eachother.
parent d8d23702
......@@ -29,7 +29,6 @@ set(libmimetreeparser_main_SRCS
viewer/bodypartformatterbasefactory.cpp
viewer/cryptohelper.cpp
viewer/csshelperbase.cpp
viewer/htmlblock.cpp
viewer/nodehelper.cpp
viewer/objecttreeparser.cpp
viewer/messagepart.cpp
......@@ -49,6 +48,7 @@ set(libmimetreeparser_extra_SRCS
htmlwriter/filehtmlwriter.cpp
htmlwriter/queuehtmlwriter.cpp
themes/default/mailrenderer.cpp
themes/default/htmlblock.cpp
)
set(mimetreeparser_temporaryfile_SRCS
......
......@@ -20,19 +20,10 @@
#include "htmlblock.h"
#include "mimetreeparser_debug.h"
#include "interfaces/objecttreesource.h"
#include "interfaces/htmlwriter.h"
#include "nodehelper.h"
#include <MessageCore/StringUtil>
#include <KMime/Content>
#include <gpgme.h>
#include <Libkleo/CryptoBackendFactory>
#include <KLocalizedString>
#include <KEmailAddress>
#include <QApplication>
using namespace MimeTreeParser;
......@@ -78,45 +69,6 @@ void AttachmentMarkBlock::internalExit()
entered = false;
}
HTMLWarnBlock::HTMLWarnBlock(HtmlWriter *writer, const QString &msg)
: mWriter(writer)
, mMsg(msg)
{
internalEnter();
}
HTMLWarnBlock::~HTMLWarnBlock()
{
internalExit();
}
void HTMLWarnBlock::internalEnter()
{
if (!mWriter || entered) {
return;
}
entered = true;
if (!mMsg.isEmpty()) {
mWriter->queue(QStringLiteral("<div class=\"htmlWarn\">\n"));
mWriter->queue(mMsg);
mWriter->queue(QStringLiteral("</div><br/><br/>"));
}
mWriter->queue(QStringLiteral("<div style=\"position: relative\">\n"));
}
void HTMLWarnBlock::internalExit()
{
if (!entered) {
return;
}
entered = false;
mWriter->queue(QStringLiteral("</div>\n"));
}
RootBlock::RootBlock(HtmlWriter *writer)
: HTMLBlock()
, mWriter(writer)
......
......@@ -20,9 +20,6 @@
#ifndef __MIMETREEPARSER_HTMLBLOCK_H__
#define __MIMETREEPARSER_HTMLBLOCK_H__
#include "partmetadata.h"
#include <Libkleo/CryptoBackend>
#include <QString>
#include <QSharedPointer>
#include <QVector>
......@@ -83,19 +80,6 @@ private:
HtmlWriter *mWriter;
};
class HTMLWarnBlock : public HTMLBlock
{
public:
HTMLWarnBlock(MimeTreeParser::HtmlWriter *writer, const QString &msg);
virtual ~HTMLWarnBlock();
private:
void internalEnter();
void internalExit();
private:
HtmlWriter *mWriter;
const QString &mMsg;
};
// Make sure the whole content is relative, so that nothing is painted over the header
// if a malicious message uses absolute positioning.
// Also force word wrapping, which is useful for printing, see https://issues.kolab.org/issue3992.
......
......@@ -19,10 +19,12 @@
#include "mailrenderer.h"
#include "htmlblock.h"
#include "interfaces/htmlwriter.h"
#include "utils/iconnamecache.h"
#include "viewer/csshelperbase.h"
#include "viewer/converthtmltoplaintext.h"
#include "viewer/htmlblock.h"
#include "viewer/messagepart.h"
#include "viewer/objecttreeparser.h"
......@@ -33,9 +35,11 @@
#include <KEmailAddress>
#include <KLocalizedString>
#include <KTextToHTML>
#include <QApplication>
#include <QDebug>
#include <QFile>
#include <QTextCodec>
#include <QUrl>
......@@ -88,6 +92,96 @@ Grantlee::Template getGrantleeTemplate(QObject *parent, const QString &name)
return m_engine->loadByName(name);
}
static QString iconToDataUrl(const QString &iconPath)
{
QFile f(iconPath);
if (!f.open(QIODevice::ReadOnly)) {
return QString();
}
const QByteArray ba = f.readAll();
return QStringLiteral("data:image/png;base64,%1").arg(QLatin1String(ba.toBase64().constData()));
}
/** Check if the newline at position @p newLinePos in string @p s
seems to separate two paragraphs (important for correct BiDi
behavior, but is heuristic because paragraphs are not
well-defined) */
// Guesstimate if the newline at newLinePos actually separates paragraphs in the text s
// We use several heuristics:
// 1. If newLinePos points after or before (=at the very beginning of) text, it is not between paragraphs
// 2. If the previous line was longer than the wrap size, we want to consider it a paragraph on its own
// (some clients, notably Outlook, send each para as a line in the plain-text version).
// 3. Otherwise, we check if the newline could have been inserted for wrapping around; if this
// was the case, then the previous line will be shorter than the wrap size (which we already
// know because of item 2 above), but adding the first word from the next line will make it
// longer than the wrap size.
bool looksLikeParaBreak(const QString &s, unsigned int newLinePos)
{
const unsigned int WRAP_COL = 78;
unsigned int length = s.length();
// 1. Is newLinePos at an end of the text?
if (newLinePos >= length - 1 || newLinePos == 0) {
return false;
}
// 2. Is the previous line really a paragraph -- longer than the wrap size?
// First char of prev line -- works also for first line
unsigned prevStart = s.lastIndexOf(QLatin1Char('\n'), newLinePos - 1) + 1;
unsigned prevLineLength = newLinePos - prevStart;
if (prevLineLength > WRAP_COL) {
return true;
}
// find next line to delimit search for first word
unsigned int nextStart = newLinePos + 1;
int nextEnd = s.indexOf(QLatin1Char('\n'), nextStart);
if (nextEnd == -1) {
nextEnd = length;
}
QString nextLine = s.mid(nextStart, nextEnd - nextStart);
length = nextLine.length();
// search for first word in next line
unsigned int wordStart;
bool found = false;
for (wordStart = 0; !found && wordStart < length; wordStart++) {
switch (nextLine[wordStart].toLatin1()) {
case '>':
case '|':
case ' ': // spaces, tabs and quote markers don't count
case '\t':
case '\r':
break;
default:
found = true;
break;
}
} /* for() */
if (!found) {
// next line is essentially empty, it seems -- empty lines are
// para separators
return true;
}
//Find end of first word.
//Note: flowText (in kmmessage.cpp) separates words for wrap by
//spaces only. This should be consistent, which calls for some
//refactoring.
int wordEnd = nextLine.indexOf(QLatin1Char(' '), wordStart);
if (wordEnd == (-1)) {
wordEnd = length;
}
int wordLength = wordEnd - wordStart;
// 3. If adding a space and the first word to the prev line don't
// make it reach the wrap column, then the break was probably
// meaningful
return prevLineLength + wordLength + 1 < WRAP_COL;
}
static const int SIG_FRAME_COL_UNDEF = 99;
#define SIG_FRAME_COL_RED -1
#define SIG_FRAME_COL_YELLOW 0
......@@ -355,6 +449,16 @@ public:
mHtml = renderFactory(mMsgPart, QSharedPointer<TestHtmlWriter>());
}
CSSHelperBase *cssHelper() const
{
return mMsgPart->cssHelper();
}
Interface::ObjectTreeSource *source() const
{
return mMsgPart->source();
}
void renderSubParts(MessagePart::Ptr msgPart, const QSharedPointer<TestHtmlWriter> &htmlWriter)
{
foreach (const auto &_m, msgPart->subParts()) {
......@@ -514,6 +618,199 @@ public:
return htmlWriter->html;
}
QString quotedHTML(const QString &s, bool decorate)
{
assert(cssHelper());
KTextToHTML::Options convertFlags = KTextToHTML::PreserveSpaces | KTextToHTML::HighlightText;
if (decorate && source()->showEmoticons()) {
convertFlags |= KTextToHTML::ReplaceSmileys;
}
QString htmlStr;
const QString normalStartTag = cssHelper()->nonQuotedFontTag();
QString quoteFontTag[3];
QString deepQuoteFontTag[3];
for (int i = 0; i < 3; ++i) {
quoteFontTag[i] = cssHelper()->quoteFontTag(i);
deepQuoteFontTag[i] = cssHelper()->quoteFontTag(i + 3);
}
const QString normalEndTag = QStringLiteral("</div>");
const QString quoteEnd = QStringLiteral("</div>");
const unsigned int length = s.length();
bool paraIsRTL = false;
bool startNewPara = true;
unsigned int pos, beg;
// skip leading empty lines
for (pos = 0; pos < length && s[pos] <= QLatin1Char(' '); ++pos)
;
while (pos > 0 && (s[pos - 1] == QLatin1Char(' ') || s[pos - 1] == QLatin1Char('\t'))) {
pos--;
}
beg = pos;
int currQuoteLevel = -2; // -2 == no previous lines
bool curHidden = false; // no hide any block
if (source()->showExpandQuotesMark()) {
// Cache Icons
if (mCollapseIcon.isEmpty()) {
mCollapseIcon = iconToDataUrl(IconNameCache::instance()->iconPath(QStringLiteral("quotecollapse"), 0));
}
if (mExpandIcon.isEmpty()) {
mExpandIcon = iconToDataUrl(IconNameCache::instance()->iconPath(QStringLiteral("quoteexpand"), 0));
}
}
int previousQuoteDepth = -1;
while (beg < length) {
/* search next occurrence of '\n' */
pos = s.indexOf(QLatin1Char('\n'), beg, Qt::CaseInsensitive);
if (pos == (unsigned int)(-1)) {
pos = length;
}
QString line(s.mid(beg, pos - beg));
beg = pos + 1;
bool foundQuote = false;
/* calculate line's current quoting depth */
int actQuoteLevel = -1;
const int numberOfCaracters(line.length());
int quoteLength = 0;
for (int p = 0; p < numberOfCaracters; ++p) {
switch (line[p].toLatin1()) {
case '>':
case '|':
actQuoteLevel++;
quoteLength = p;
foundQuote = true;
break;
case ' ': // spaces and tabs are allowed between the quote markers
case '\t':
case '\r':
quoteLength = p;
break;
default: // stop quoting depth calculation
p = numberOfCaracters;
break;
}
} /* for() */
if (!foundQuote) {
quoteLength = 0;
}
bool actHidden = false;
// This quoted line needs be hidden
if (source()->showExpandQuotesMark() && source()->levelQuote() >= 0
&& source()->levelQuote() <= (actQuoteLevel)) {
actHidden = true;
}
if (actQuoteLevel != currQuoteLevel) {
/* finish last quotelevel */
if (currQuoteLevel == -1) {
htmlStr.append(normalEndTag);
} else if (currQuoteLevel >= 0 && !curHidden) {
htmlStr.append(quoteEnd);
}
//Close blockquote
if (previousQuoteDepth > actQuoteLevel) {
htmlStr += cssHelper()->addEndBlockQuote((previousQuoteDepth - actQuoteLevel));
}
/* start new quotelevel */
if (actQuoteLevel == -1) {
htmlStr += normalStartTag;
} else {
if (source()->showExpandQuotesMark()) {
if (actHidden) {
//only show the QuoteMark when is the first line of the level hidden
if (!curHidden) {
//Expand all quotes
htmlStr += QLatin1String("<div class=\"quotelevelmark\" >");
htmlStr += QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
"<img src=\"%2\"/></a>")
.arg(-1)
.arg(mExpandIcon);
htmlStr += QLatin1String("</div><br/>");
htmlStr += quoteEnd;
}
} else {
htmlStr += QLatin1String("<div class=\"quotelevelmark\" >");
htmlStr += QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
"<img src=\"%2\"/></a>")
.arg(actQuoteLevel)
.arg(mCollapseIcon);
htmlStr += QLatin1String("</div>");
if (actQuoteLevel < 3) {
htmlStr += quoteFontTag[actQuoteLevel];
} else {
htmlStr += deepQuoteFontTag[actQuoteLevel % 3];
}
}
} else {
// Add blockquote
if (previousQuoteDepth < actQuoteLevel) {
htmlStr += cssHelper()->addStartBlockQuote(actQuoteLevel - previousQuoteDepth);
}
if (actQuoteLevel < 3) {
htmlStr += quoteFontTag[actQuoteLevel];
} else {
htmlStr += deepQuoteFontTag[actQuoteLevel % 3];
}
}
}
currQuoteLevel = actQuoteLevel;
}
curHidden = actHidden;
if (!actHidden) {
// don't write empty <div ...></div> blocks (they have zero height)
// ignore ^M DOS linebreaks
if (!line.remove(QLatin1Char('\015')).isEmpty()) {
if (startNewPara) {
paraIsRTL = line.isRightToLeft();
}
htmlStr += QStringLiteral("<div dir=\"%1\">").arg(paraIsRTL ? QStringLiteral("rtl") : QStringLiteral("ltr"));
// if quoteLengh == 0 && foundQuote => a simple quote
if (foundQuote) {
quoteLength++;
htmlStr += QStringLiteral("<span class=\"quotemarks\">%1</span>").arg(line.left(quoteLength));
const int rightString = (line.length()) - quoteLength;
if (rightString > 0) {
htmlStr += QStringLiteral("<font color=\"%1\">").arg(cssHelper()->quoteColorName(actQuoteLevel)) + KTextToHTML::convertToHtml(line.right(rightString), convertFlags) + QStringLiteral("</font>");
}
} else {
htmlStr += KTextToHTML::convertToHtml(line, convertFlags);
}
htmlStr += QLatin1String("</div>");
startNewPara = looksLikeParaBreak(s, pos);
} else {
htmlStr += QLatin1String("<br/>");
// after an empty line, always start a new paragraph
startNewPara = true;
}
}
previousQuoteDepth = actQuoteLevel;
} /* while() */
/* really finish the last quotelevel */
if (currQuoteLevel == -1) {
htmlStr.append(normalEndTag);
} else {
htmlStr += quoteEnd + cssHelper()->addEndBlockQuote(currQuoteLevel + 1);
}
// qCDebug(MIMETREEPARSER_LOG) << "========================================\n"
// << htmlStr
// << "\n======================================\n";
return htmlStr;
}
QString render(MessagePart::Ptr mp)
{
auto htmlWriter = QSharedPointer<TestHtmlWriter>(new TestHtmlWriter(mOldWriter));
......@@ -523,7 +820,7 @@ public:
aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter.data(), mp->mAttachmentNode));
}
htmlWriter->queue(mp->quotedHTML(mp->text(), false));
htmlWriter->queue(quotedHTML(mp->text(), false));
}
return htmlWriter->html;
}
......@@ -1014,6 +1311,9 @@ public:
private:
HtmlRenderer *q;
HtmlWriter *mOldWriter;
QString mCollapseIcon;
QString mExpandIcon;
};
......
......@@ -21,14 +21,12 @@
#include "mimetreeparser_debug.h"
#include "csshelperbase.h"
#include "cryptohelper.h"
#include "htmlblock.h"
#include "objecttreeparser.h"
#include "interfaces/htmlwriter.h"
#include "job/kleojobexecutor.h"
#include "memento/decryptverifybodypartmemento.h"
#include "memento/verifydetachedbodypartmemento.h"
#include "memento/verifyopaquebodypartmemento.h"
#include "utils/iconnamecache.h"
#include <KMime/Content>
......@@ -43,14 +41,12 @@
#include <gpgme++/keylistresult.h>
#include <gpgme.h>
#include <QFile>
#include <QTextCodec>
#include <QWebPage>
#include <QWebElement>
#include <QWebFrame>
#include <KLocalizedString>
#include <KTextToHTML>
using namespace MimeTreeParser;
......@@ -94,14 +90,6 @@ KMime::Content *MessagePart::attachmentNode() const
return mAttachmentNode;
}
HTMLBlock::Ptr MessagePart::attachmentBlock() const
{
if (htmlWriter() && isAttachment()) {
return HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter(), mAttachmentNode));
}
return HTMLBlock::Ptr();
}
void MessagePart::setIsRoot(bool root)
{
mRoot = root;
......@@ -112,14 +100,6 @@ bool MessagePart::isRoot() const
return mRoot;
}
HTMLBlock::Ptr MessagePart::rootBlock() const
{
if (htmlWriter() && isRoot()) {
return HTMLBlock::Ptr(new RootBlock(htmlWriter()));
}
return HTMLBlock::Ptr();
}
QString MessagePart::text() const
{
return mText;
......@@ -153,300 +133,6 @@ void MessagePart::setHtmlWriter(HtmlWriter *htmlWriter) const
mOtp->mHtmlWriter = htmlWriter;
}
static QString iconToDataUrl(const QString &iconPath)
{
QFile f(iconPath);
if (!f.open(QIODevice::ReadOnly)) {
return QString();
}
const QByteArray ba = f.readAll();
return QStringLiteral("data:image/png;base64,%1").arg(QLatin1String(ba.toBase64().constData()));
}
/** Check if the newline at position @p newLinePos in string @p s
seems to separate two paragraphs (important for correct BiDi
behavior, but is heuristic because paragraphs are not
well-defined) */
// Guesstimate if the newline at newLinePos actually separates paragraphs in the text s
// We use several heuristics:
// 1. If newLinePos points after or before (=at the very beginning of) text, it is not between paragraphs
// 2. If the previous line was longer than the wrap size, we want to consider it a paragraph on its own
// (some clients, notably Outlook, send each para as a line in the plain-text version).
// 3. Otherwise, we check if the newline could have been inserted for wrapping around; if this
// was the case, then the previous line will be shorter than the wrap size (which we already
// know because of item 2 above), but adding the first word from the next line will make it
// longer than the wrap size.
bool looksLikeParaBreak(const QString &s, unsigned int newLinePos)
{
const unsigned int WRAP_COL = 78;
unsigned int length = s.length();
// 1. Is newLinePos at an end of the text?
if (newLinePos >= length - 1 || newLinePos == 0) {
return false;
}
// 2. Is the previous line really a paragraph -- longer than the wrap size?
// First char of prev line -- works also for first line
unsigned prevStart = s.lastIndexOf(QLatin1Char('\n'), newLinePos - 1) + 1;
unsigned prevLineLength = newLinePos - prevStart;
if (prevLineLength > WRAP_COL) {
return true;
}
// find next line to delimit search for first word
unsigned int nextStart = newLinePos + 1;
int nextEnd = s.indexOf(QLatin1Char('\n'), nextStart);
if (nextEnd == -1) {
nextEnd = length;
}
QString nextLine = s.mid(nextStart, nextEnd - nextStart);
length = nextLine.length();
// search for first word in next line
unsigned int wordStart;
bool found = false;
for (wordStart = 0; !found && wordStart < length; wordStart++) {
switch (nextLine[wordStart].toLatin1()) {
case '>':
case '|':
case ' ': // spaces, tabs and quote markers don't count
case '\t':
case '\r':
break;
default:
found = true;
break;
}
} /* for() */
if (!found) {
// next line is essentially empty, it seems -- empty lines are
// para separators
return true;
}
//Find end of first word.
//Note: flowText (in kmmessage.cpp) separates words for wrap by
//spaces only. This should be consistent, which calls for some
//refactoring.