Messagelib

quotehtml.cpp
1 /*
2  SPDX-FileCopyrightText: 2016 Sandro Knauß <[email protected]>
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.
32 bool 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 
97 void 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  quoteLength = p;
170  break;
171  default: // stop quoting depth calculation
172  p = numberOfCaracters;
173  break;
174  }
175  } /* for() */
176  if (!foundQuote) {
177  quoteLength = 0;
178  }
179  bool actHidden = false;
180 
181  // This quoted line needs be hidden
182  if (context->showExpandQuotesMark() && context->levelQuote() >= 0 && context->levelQuote() <= actQuoteLevel) {
183  actHidden = true;
184  }
185 
186  if (actQuoteLevel != currQuoteLevel) {
187  /* finish last quotelevel */
188  if (currQuoteLevel == -1) {
189  htmlWriter->write(normalEndTag);
190  } else if (currQuoteLevel >= 0 && !curHidden) {
191  htmlWriter->write(quoteEnd);
192  }
193  // Close blockquote
194  if (previousQuoteDepth > actQuoteLevel) {
195  htmlWriter->write(cssHelper->addEndBlockQuote(previousQuoteDepth - actQuoteLevel));
196  }
197 
198  /* start new quotelevel */
199  if (actQuoteLevel == -1) {
200  htmlWriter->write(normalStartTag);
201  } else {
202  if (context->showExpandQuotesMark()) {
203  // Add blockquote
204  if (previousQuoteDepth < actQuoteLevel) {
205  htmlWriter->write(cssHelper->addStartBlockQuote(actQuoteLevel - previousQuoteDepth));
206  }
207  if (actHidden) {
208  // only show the QuoteMark when is the first line of the level hidden
209  if (!curHidden) {
210  // Expand all quotes
211  htmlWriter->write(QStringLiteral("<div class=\"quotelevelmark\" >"));
212  htmlWriter->write(QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
213  "<img src=\"%2\"/></a>")
214  .arg(-1)
215  .arg(expandIconPath));
216  htmlWriter->write(QStringLiteral("</div><br/>"));
217  }
218  } else {
219  htmlWriter->write(QStringLiteral("<div class=\"quotelevelmark\" >"));
220  htmlWriter->write(QStringLiteral("<a href=\"kmail:levelquote?%1 \">"
221  "<img src=\"%2\"/></a>")
222  .arg(actQuoteLevel)
223  .arg(collapseIconPath));
224  htmlWriter->write(QStringLiteral("</div>"));
225  if (actQuoteLevel < 3) {
226  htmlWriter->write(quoteFontTag[actQuoteLevel]);
227  } else {
228  htmlWriter->write(deepQuoteFontTag[actQuoteLevel % 3]);
229  }
230  }
231  } else {
232  // Add blockquote
233  if (previousQuoteDepth < actQuoteLevel) {
234  htmlWriter->write(cssHelper->addStartBlockQuote(actQuoteLevel - previousQuoteDepth));
235  }
236 
237  if (actQuoteLevel < 3) {
238  htmlWriter->write(quoteFontTag[actQuoteLevel]);
239  } else {
240  htmlWriter->write(deepQuoteFontTag[actQuoteLevel % 3]);
241  }
242  }
243  }
244  currQuoteLevel = actQuoteLevel;
245  }
246  curHidden = actHidden;
247 
248  if (!actHidden) {
249  // don't write empty <div ...></div> blocks (they have zero height)
250  // ignore ^M DOS linebreaks
251  if (!line.remove(QLatin1Char('\015')).isEmpty()) {
252  if (startNewPara) {
253  paraIsRTL = line.isRightToLeft();
254  }
255  htmlWriter->write(QStringLiteral("<div dir=\"%1\">").arg(paraIsRTL ? QStringLiteral("rtl") : QStringLiteral("ltr")));
256  // if quoteLengh == 0 && foundQuote => a simple quote
257  if (foundQuote) {
258  quoteLength++;
259  const int rightString = (line.length()) - quoteLength;
260  if (rightString > 0) {
261  htmlWriter->write(QStringLiteral("<span class=\"quotemarks\">%1</span>").arg(line.left(quoteLength)));
262  htmlWriter->write(QStringLiteral("<font color=\"%1\">").arg(cssHelper->quoteColorName(actQuoteLevel)));
263  htmlWriter->write(KTextToHTML::convertToHtml(line.right(rightString), convertFlags, 4096, 512));
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 }
CaseInsensitive
void write(const QString &html)
Write out a chunk of text.
Definition: htmlwriter.cpp:27
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
KCOREADDONS_EXPORT QString convertToHtml(const QString &plainText, const KTextToHTML::Options &options, int maxUrlLen=4096, int maxAddressLen=255)
int length() const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
An interface for HTML sinks.
Definition: htmlwriter.h:28
QString mid(int position, int n) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Fri Mar 24 2023 04:08:32 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.