Messagelib

quotehtml.cpp
1/*
2 SPDX-FileCopyrightText: 2016 Sandro Knauß <sknauss@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "quotehtml.h"
8
9#include "utils/iconnamecache.h"
10#include "viewer/csshelperbase.h"
11
12#include <MessageViewer/HtmlWriter>
13#include <MessageViewer/MessagePartRendererBase>
14
15#include <KTextToHTML>
16
17#include <QSharedPointer>
18
19/** Check if the newline at position @p newLinePos in string @p s
20 seems to separate two paragraphs (important for correct BiDi
21 behavior, but is heuristic because paragraphs are not
22 well-defined) */
23// Guesstimate if the newline at newLinePos actually separates paragraphs in the text s
24// We use several heuristics:
25// 1. If newLinePos points after or before (=at the very beginning of) text, it is not between paragraphs
26// 2. If the previous line was longer than the wrap size, we want to consider it a paragraph on its own
27// (some clients, notably Outlook, send each para as a line in the plain-text version).
28// 3. Otherwise, we check if the newline could have been inserted for wrapping around; if this
29// was the case, then the previous line will be shorter than the wrap size (which we already
30// know because of item 2 above), but adding the first word from the next line will make it
31// longer than the wrap size.
32bool looksLikeParaBreak(const QString &s, int newLinePos)
33{
34 const int WRAP_COL = 78;
35
36 int length = s.length();
37 // 1. Is newLinePos at an end of the text?
38 if (newLinePos >= length - 1 || newLinePos == 0) {
39 return false;
40 }
41
42 // 2. Is the previous line really a paragraph -- longer than the wrap size?
43
44 // First char of prev line -- works also for first line
45 int prevStart = s.lastIndexOf(QLatin1Char('\n'), newLinePos - 1) + 1;
46 int prevLineLength = newLinePos - prevStart;
47 if (prevLineLength > WRAP_COL) {
48 return true;
49 }
50
51 // find next line to delimit search for first word
52 int nextStart = newLinePos + 1;
53 int nextEnd = s.indexOf(QLatin1Char('\n'), nextStart);
54 if (nextEnd == -1) {
55 nextEnd = length;
56 }
57 QString nextLine = s.mid(nextStart, nextEnd - nextStart);
58 length = nextLine.length();
59 // search for first word in next line
60 int wordStart;
61 bool found = false;
62 for (wordStart = 0; !found && wordStart < length; wordStart++) {
63 switch (nextLine[wordStart].toLatin1()) {
64 case '>':
65 case '|':
66 case ' ': // spaces, tabs and quote markers don't count
67 case '\t':
68 case '\r':
69 break;
70 default:
71 found = true;
72 break;
73 }
74 } /* for() */
75
76 if (!found) {
77 // next line is essentially empty, it seems -- empty lines are
78 // para separators
79 return true;
80 }
81 // Find end of first word.
82 // Note: flowText (in kmmessage.cpp) separates words for wrap by
83 // spaces only. This should be consistent, which calls for some
84 // refactoring.
85 int wordEnd = nextLine.indexOf(QLatin1Char(' '), wordStart);
86 if (wordEnd == (-1)) {
87 wordEnd = length;
88 }
89 int wordLength = wordEnd - wordStart;
90
91 // 3. If adding a space and the first word to the prev line don't
92 // make it reach the wrap column, then the break was probably
93 // meaningful
94 return prevLineLength + wordLength + 1 < WRAP_COL;
95}
96
97void quotedHTML(const QString &s, MessageViewer::RenderContext *context, MessageViewer::HtmlWriter *htmlWriter)
98{
99 const auto cssHelper = context->cssHelper();
100 Q_ASSERT(cssHelper);
101
103 if (context->showEmoticons()) {
104 convertFlags |= KTextToHTML::ReplaceSmileys;
105 }
106
107 const QString normalStartTag = cssHelper->nonQuotedFontTag();
108 QString quoteFontTag[3];
109 QString deepQuoteFontTag[3];
110 for (int i = 0; i < 3; ++i) {
111 quoteFontTag[i] = cssHelper->quoteFontTag(i);
112 deepQuoteFontTag[i] = cssHelper->quoteFontTag(i + 3);
113 }
114 const QString normalEndTag = QStringLiteral("</div>");
115 const QString quoteEnd = QStringLiteral("</div>");
116
117 const int length = s.length();
118 bool paraIsRTL = false;
119 bool startNewPara = true;
120 int pos;
121 int beg;
122
123 // skip leading empty lines
124 for (pos = 0; pos < length && s[pos] <= QLatin1Char(' '); ++pos) { }
125 while (pos > 0 && (s[pos - 1] == QLatin1Char(' ') || s[pos - 1] == QLatin1Char('\t'))) {
126 pos--;
127 }
128 beg = pos;
129
130 int currQuoteLevel = -2; // -2 == no previous lines
131 bool curHidden = false; // no hide any block
132
133 QString collapseIconPath;
134 QString expandIconPath;
135 if (context->showExpandQuotesMark()) {
136 collapseIconPath = MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quotecollapse.png"));
137 expandIconPath = MessageViewer::IconNameCache::instance()->iconPathFromLocal(QStringLiteral("quoteexpand.png"));
138 }
139
140 int previousQuoteDepth = -1;
141 while (beg < length) {
142 /* search next occurrence of '\n' */
143 pos = s.indexOf(QLatin1Char('\n'), beg, Qt::CaseInsensitive);
144 if (pos == -1) {
145 pos = length;
146 }
147
148 QString line(s.mid(beg, pos - beg));
149 beg = pos + 1;
150
151 bool foundQuote = false;
152 /* calculate line's current quoting depth */
153 int actQuoteLevel = -1;
154 const int numberOfCaracters(line.length());
155 int quoteLength = 0;
156 for (int p = 0; p < numberOfCaracters; ++p) {
157 switch (line[p].toLatin1()) {
158 case '>':
159 case '|':
160 if (p == 0 || foundQuote) {
161 actQuoteLevel++;
162 quoteLength = p;
163 foundQuote = true;
164 }
165 break;
166 case ' ': // spaces and tabs are allowed between the quote markers
167 case '\t':
168 case '\r':
169 break;
170 default: // stop quoting depth calculation
171 p = numberOfCaracters;
172 break;
173 }
174 } /* for() */
175 if (!foundQuote) {
176 quoteLength = 0;
177 }
178 bool actHidden = false;
179
180 // This quoted line needs be hidden
181 if (context->showExpandQuotesMark() && context->levelQuote() >= 0 && context->levelQuote() <= actQuoteLevel) {
182 actHidden = true;
183 }
184
185 if (actQuoteLevel != currQuoteLevel) {
186 /* finish last quotelevel */
187 if (currQuoteLevel == -1) {
188 htmlWriter->write(normalEndTag);
189 } else if (currQuoteLevel >= 0 && !curHidden) {
190 htmlWriter->write(quoteEnd);
191 }
192 // Close blockquote
193 if (previousQuoteDepth > actQuoteLevel) {
194 htmlWriter->write(cssHelper->addEndBlockQuote(previousQuoteDepth - actQuoteLevel));
195 }
196
197 /* start new quotelevel */
198 if (actQuoteLevel == -1) {
199 htmlWriter->write(normalStartTag);
200 } else {
201 if (context->showExpandQuotesMark()) {
202 // Add blockquote
203 if (previousQuoteDepth < actQuoteLevel) {
204 htmlWriter->write(cssHelper->addStartBlockQuote(actQuoteLevel - previousQuoteDepth));
205 }
206 if (actHidden) {
207 // only show the QuoteMark when is the first line of the level hidden
208 if (!curHidden) {
209 // Expand all quotes
210 htmlWriter->write(QStringLiteral("<div class=\"quotelevelmark\" >"));
211 htmlWriter->write(QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
212 "<img src=\"%2\"/></a>")
213 .arg(-1)
214 .arg(expandIconPath));
215 htmlWriter->write(QStringLiteral("</div><br/>"));
216 }
217 } else {
218 htmlWriter->write(QStringLiteral("<div class=\"quotelevelmark\" >"));
219 htmlWriter->write(QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
220 "<img src=\"%2\"/></a>")
221 .arg(actQuoteLevel)
222 .arg(collapseIconPath));
223 htmlWriter->write(QStringLiteral("</div>"));
224 if (actQuoteLevel < 3) {
225 htmlWriter->write(quoteFontTag[actQuoteLevel]);
226 } else {
227 htmlWriter->write(deepQuoteFontTag[actQuoteLevel % 3]);
228 }
229 }
230 } else {
231 // Add blockquote
232 if (previousQuoteDepth < actQuoteLevel) {
233 htmlWriter->write(cssHelper->addStartBlockQuote(actQuoteLevel - previousQuoteDepth));
234 }
235
236 if (actQuoteLevel < 3) {
237 htmlWriter->write(quoteFontTag[actQuoteLevel]);
238 } else {
239 htmlWriter->write(deepQuoteFontTag[actQuoteLevel % 3]);
240 }
241 }
242 }
243 currQuoteLevel = actQuoteLevel;
244 }
245 curHidden = actHidden;
246
247 if (!actHidden) {
248 // don't write empty <div ...></div> blocks (they have zero height)
249 // ignore ^M DOS linebreaks
250 if (!line.remove(QLatin1Char('\015')).isEmpty()) {
251 if (startNewPara) {
252 paraIsRTL = line.isRightToLeft();
253 }
254 htmlWriter->write(QStringLiteral("<div dir=\"%1\">").arg(paraIsRTL ? QStringLiteral("rtl") : QStringLiteral("ltr")));
255 // if quoteLengh == 0 && foundQuote => a simple quote
256 if (foundQuote) {
257 quoteLength++;
258 const int rightString = (line.length()) - quoteLength;
259 if (rightString > 0) {
260 htmlWriter->write(QStringLiteral("<span class=\"quotemarks\">%1</span>").arg(line.left(quoteLength)));
261 htmlWriter->write(QStringLiteral("<font color=\"%1\">").arg(cssHelper->quoteColorName(actQuoteLevel)));
262 const QString str = KTextToHTML::convertToHtml(line.right(rightString), convertFlags, 4096, 512);
263 htmlWriter->write(str);
264 htmlWriter->write(QStringLiteral("</font>"));
265 } else {
266 htmlWriter->write(QStringLiteral("<span class=\"quotemarksemptyline\">%1</span>").arg(line.left(quoteLength)));
267 }
268 } else {
269 htmlWriter->write(KTextToHTML::convertToHtml(line, convertFlags, 4096, 512));
270 }
271
272 htmlWriter->write(QStringLiteral("</div>"));
273 startNewPara = looksLikeParaBreak(s, pos);
274 } else {
275 htmlWriter->write(QStringLiteral("<br/>"));
276 // after an empty line, always start a new paragraph
277 startNewPara = true;
278 }
279 }
280 previousQuoteDepth = actQuoteLevel;
281 } /* while() */
282
283 /* really finish the last quotelevel */
284 if (currQuoteLevel == -1) {
285 htmlWriter->write(normalEndTag);
286 } else {
287 htmlWriter->write(quoteEnd + cssHelper->addEndBlockQuote(currQuoteLevel + 1));
288 }
289}
An interface for HTML sinks.
Definition htmlwriter.h:29
void write(const QString &html)
Write out a chunk of text.
KCOREADDONS_EXPORT QString convertToHtml(const QString &plainText, const KTextToHTML::Options &options, int maxUrlLen=4096, int maxAddressLen=255)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.