KCoreAddons

ktexttohtml.cpp
1 /*
2  SPDX-FileCopyrightText: 2002 Dave Corrie <[email protected]>
3  SPDX-FileCopyrightText: 2014 Daniel Vr├ítil <[email protected]>
4 
5  SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "ktexttohtml.h"
9 #include "ktexttohtml_p.h"
10 #include "ktexttohtmlemoticonsinterface.h"
11 
12 #include <QCoreApplication>
13 #include <QFile>
14 #include <QPluginLoader>
15 #include <QRegularExpression>
16 #include <QStringList>
17 
18 #include <limits.h>
19 
20 #include "kcoreaddons_debug.h"
21 
22 static KTextToHTMLEmoticonsInterface *s_emoticonsInterface = nullptr;
23 
24 static void loadEmoticonsPlugin()
25 {
26  static bool triedLoadPlugin = false;
27  if (!triedLoadPlugin) {
28  triedLoadPlugin = true;
29 
30  // Check if QGuiApplication::platformName property exists. This is a
31  // hackish way of determining whether we are running QGuiApplication,
32  // because we cannot load the FrameworkIntegration plugin into a
33  // QCoreApplication, as it would crash immediately
34  if (qApp->metaObject()->indexOfProperty("platformName") > -1) {
35  QPluginLoader lib(QStringLiteral("kf5/KEmoticonsIntegrationPlugin"));
36  QObject *rootObj = lib.instance();
37  if (rootObj) {
38  s_emoticonsInterface = rootObj->property(KTEXTTOHTMLEMOTICONS_PROPERTY).value<KTextToHTMLEmoticonsInterface *>();
39  }
40  }
41  }
42  if (!s_emoticonsInterface) {
43  s_emoticonsInterface = new KTextToHTMLEmoticonsDummy();
44  }
45 }
46 
47 KTextToHTMLHelper::KTextToHTMLHelper(const QString &plainText, int pos, int maxUrlLen, int maxAddressLen)
48  : mText(plainText)
49  , mMaxUrlLen(maxUrlLen)
50  , mMaxAddressLen(maxAddressLen)
51  , mPos(pos)
52 {
53 }
54 
55 KTextToHTMLEmoticonsInterface *KTextToHTMLHelper::emoticonsInterface() const
56 {
57  if (!s_emoticonsInterface) {
58  loadEmoticonsPlugin();
59  }
60  return s_emoticonsInterface;
61 }
62 
63 QString KTextToHTMLHelper::getEmailAddress()
64 {
66 
67  if (mPos < mText.length() && mText.at(mPos) == QLatin1Char('@')) {
68  // the following characters are allowed in a dot-atom (RFC 2822):
69  // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~
70  const QString allowedSpecialChars = QStringLiteral(".!#$%&'*+-/=?^_`{|}~");
71 
72  // determine the local part of the email address
73  int start = mPos - 1;
74  while (start >= 0 && mText.at(start).unicode() < 128
75  && (mText.at(start).isLetterOrNumber() //
76  || mText.at(start) == QLatin1Char('@') // allow @ to find invalid email addresses
77  || allowedSpecialChars.indexOf(mText.at(start)) != -1)) {
78  if (mText.at(start) == QLatin1Char('@')) {
79  return QString(); // local part contains '@' -> no email address
80  }
81  --start;
82  }
83  ++start;
84  // we assume that an email address starts with a letter or a digit
85  while ((start < mPos) && !mText.at(start).isLetterOrNumber()) {
86  ++start;
87  }
88  if (start == mPos) {
89  return QString(); // local part is empty -> no email address
90  }
91 
92  // determine the domain part of the email address
93  int dotPos = INT_MAX;
94  int end = mPos + 1;
95  while (end < mText.length()
96  && (mText.at(end).isLetterOrNumber() //
97  || mText.at(end) == QLatin1Char('@') // allow @ to find invalid email addresses
98  || mText.at(end) == QLatin1Char('.') //
99  || mText.at(end) == QLatin1Char('-'))) {
100  if (mText.at(end) == QLatin1Char('@')) {
101  return QString(); // domain part contains '@' -> no email address
102  }
103  if (mText.at(end) == QLatin1Char('.')) {
104  dotPos = qMin(dotPos, end); // remember index of first dot in domain
105  }
106  ++end;
107  }
108  // we assume that an email address ends with a letter or a digit
109  while ((end > mPos) && !mText.at(end - 1).isLetterOrNumber()) {
110  --end;
111  }
112  if (end == mPos) {
113  return QString(); // domain part is empty -> no email address
114  }
115  if (dotPos >= end) {
116  return QString(); // domain part doesn't contain a dot
117  }
118 
119  if (end - start > mMaxAddressLen) {
120  return QString(); // too long -> most likely no email address
121  }
122  address = mText.mid(start, end - start);
123 
124  mPos = end - 1;
125  }
126  return address;
127 }
128 
129 QString KTextToHTMLHelper::getPhoneNumber()
130 {
131  if (!mText.at(mPos).isDigit() && mText.at(mPos) != QLatin1Char('+')) {
132  return {};
133  }
134 
135  const QString allowedBeginSeparators = QStringLiteral(" \r\t\n:");
136  if (mPos > 0 && !allowedBeginSeparators.contains(mText.at(mPos - 1))) {
137  return {};
138  }
139 
140  // this isn't 100% accurate, we filter stuff below that is too hard to capture with a regexp
141  static const QRegularExpression telPattern(QStringLiteral(R"([+0](( |( ?[/-] ?)?)\(?\d+\)?+){6,30})"));
142  const auto match = telPattern.match(mText, mPos, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption);
143  if (match.hasMatch()) {
144  auto m = match.captured();
145  // check for maximum number of digits (15), see https://en.wikipedia.org/wiki/Telephone_numbering_plan
146  const int count = std::count_if(m.begin(), m.end(), [](const QChar &c) {
147  return c.isDigit();
148  });
149  if (count > 15) {
150  return {};
151  }
152  // only one / is allowed, otherwise we trigger on dates
153  if (std::count(m.begin(), m.end(), QLatin1Char('/')) > 1) {
154  return {};
155  }
156 
157  // parenthesis need to be balanced, and must not be nested
158  int openIdx = -1;
159  for (int i = 0; i < m.size(); ++i) {
160  if ((m[i] == QLatin1Char('(') && openIdx >= 0) || (m[i] == QLatin1Char(')') && openIdx < 0)) {
161  return {};
162  }
163  if (m[i] == QLatin1Char('(')) {
164  openIdx = i;
165  } else if (m[i] == QLatin1Char(')')) {
166  openIdx = -1;
167  }
168  }
169  if (openIdx > 0) {
170  m = m.leftRef(openIdx - 1).trimmed().toString();
171  }
172 
173  // check if there's a plausible separator at the end
174  const QString allowedEndSeparators = QStringLiteral(" \r\t\n,.");
175  const auto l = m.size();
176  if (mText.size() > mPos + l && !allowedEndSeparators.contains(mText.at(mPos + l))) {
177  return {};
178  }
179 
180  mPos += l - 1;
181  return m;
182  }
183  return {};
184 }
185 
186 static QString normalizePhoneNumber(const QString &str)
187 {
188  QString res;
189  res.reserve(str.size());
190  for (const auto c : str) {
191  if (c.isDigit() || c == QLatin1Char('+')) {
192  res.push_back(c);
193  }
194  }
195  return res;
196 }
197 
198 // The following characters are allowed in a dot-atom (RFC 2822):
199 // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~
200 static const char s_allowedSpecialChars[] = ".!#$%&'*+-/=?^_`{|}~";
201 
202 bool KTextToHTMLHelper::atUrl() const
203 {
204  // The character directly before the URL must not be a letter, a number or
205  // any other character allowed in a dot-atom (RFC 2822).
206  if (mPos > 0) {
207  const auto chBefore = mText.at(mPos - 1);
208  if (chBefore.isLetterOrNumber() || QLatin1String(s_allowedSpecialChars).contains(chBefore)) {
209  return false;
210  }
211  }
212 
213  const auto segment = mText.midRef(mPos);
214  /* clang-format off */
215  return segment.startsWith(QLatin1String("http://"))
216  || segment.startsWith(QLatin1String("https://"))
217  || segment.startsWith(QLatin1String("vnc://"))
218  || segment.startsWith(QLatin1String("fish://"))
219  || segment.startsWith(QLatin1String("ftp://"))
220  || segment.startsWith(QLatin1String("ftps://"))
221  || segment.startsWith(QLatin1String("sftp://"))
222  || segment.startsWith(QLatin1String("smb://"))
223  || segment.startsWith(QLatin1String("mailto:"))
224  || segment.startsWith(QLatin1String("www."))
225  || segment.startsWith(QLatin1String("ftp."))
226  || segment.startsWith(QLatin1String("file://"))
227  || segment.startsWith(QLatin1String("news:"))
228  || segment.startsWith(QLatin1String("tel:"))
229  || segment.startsWith(QLatin1String("xmpp:"));
230  /* clang-format on */
231 }
232 
233 bool KTextToHTMLHelper::isEmptyUrl(const QString &url) const
234 {
235  /* clang-format off */
236  return url.isEmpty()
237  || url == QLatin1String("http://")
238  || url == QLatin1String("https://")
239  || url == QLatin1String("fish://")
240  || url == QLatin1String("ftp://")
241  || url == QLatin1String("ftps://")
242  || url == QLatin1String("sftp://")
243  || url == QLatin1String("smb://")
244  || url == QLatin1String("vnc://")
245  || url == QLatin1String("mailto")
246  || url == QLatin1String("mailto:")
247  || url == QLatin1String("www")
248  || url == QLatin1String("ftp")
249  || url == QLatin1String("news:")
250  || url == QLatin1String("news://")
251  || url == QLatin1String("tel")
252  || url == QLatin1String("tel:")
253  || url == QLatin1String("xmpp:");
254  /* clang-format on */
255 }
256 
257 QString KTextToHTMLHelper::getUrl(bool *badurl)
258 {
259  QString url;
260  if (atUrl()) {
261  // NOTE: see http://tools.ietf.org/html/rfc3986#appendix-A and especially appendix-C
262  // Appendix-C mainly says, that when extracting URLs from plain text, line breaks shall
263  // be allowed and should be ignored when the URI is extracted.
264 
265  // This implementation follows this recommendation and
266  // allows the URL to be enclosed within different kind of brackets/quotes
267  // If an URL is enclosed, whitespace characters are allowed and removed, otherwise
268  // the URL ends with the first whitespace
269  // Also, if the URL is enclosed in brackets, the URL itself is not allowed
270  // to contain the closing bracket, as this would be detected as the end of the URL
271 
272  QChar beforeUrl, afterUrl;
273 
274  // detect if the url has been surrounded by brackets or quotes
275  if (mPos > 0) {
276  beforeUrl = mText.at(mPos - 1);
277 
278  /*if ( beforeUrl == '(' ) {
279  afterUrl = ')';
280  } else */
281  if (beforeUrl == QLatin1Char('[')) {
282  afterUrl = QLatin1Char(']');
283  } else if (beforeUrl == QLatin1Char('<')) {
284  afterUrl = QLatin1Char('>');
285  } else if (beforeUrl == QLatin1Char('>')) { // for e.g. <link>http://.....</link>
286  afterUrl = QLatin1Char('<');
287  } else if (beforeUrl == QLatin1Char('"')) {
288  afterUrl = QLatin1Char('"');
289  }
290  }
291  url.reserve(mMaxUrlLen); // avoid allocs
292  int start = mPos;
293  bool previousCharIsSpace = false;
294  bool previousCharIsADoubleQuote = false;
295  bool previousIsAnAnchor = false;
296  /* clang-format off */
297  while (mPos < mText.length() //
298  && (mText.at(mPos).isPrint() || mText.at(mPos).isSpace())
299  && ((afterUrl.isNull() && !mText.at(mPos).isSpace())
300  || (!afterUrl.isNull() && mText.at(mPos) != afterUrl))) {
301  if (!previousCharIsSpace
302  && mText.at(mPos) == QLatin1Char('<')
303  && (mPos + 1) < mText.length()) { /* clang-format on */
304  // Fix Bug #346132: allow "http://www.foo.bar<http://foo.bar/>"
305  // < inside a URL is not allowed, however there is a test which
306  // checks that "http://some<Host>/path" should be allowed
307  // Therefore: check if what follows is another URL and if so, stop here
308  mPos++;
309  if (atUrl()) {
310  mPos--;
311  break;
312  }
313  mPos--;
314  }
315  if (!previousCharIsSpace && (mText.at(mPos) == QLatin1Char(' ')) && ((mPos + 1) < mText.length())) {
316  // Fix kmail bug: allow "http://www.foo.bar http://foo.bar/"
317  // Therefore: check if what follows is another URL and if so, stop here
318  mPos++;
319  if (atUrl()) {
320  mPos--;
321  break;
322  }
323  mPos--;
324  }
325  if (mText.at(mPos).isSpace()) {
326  previousCharIsSpace = true;
327  } else if (!previousIsAnAnchor && mText.at(mPos) == QLatin1Char('[')) {
328  break;
329  } else if (!previousIsAnAnchor && mText.at(mPos) == QLatin1Char(']')) {
330  break;
331  } else { // skip whitespace
332  if (previousCharIsSpace && mText.at(mPos) == QLatin1Char('<')) {
333  url.append(QLatin1Char(' '));
334  break;
335  }
336  previousCharIsSpace = false;
337  if (mText.at(mPos) == QLatin1Char('>') && previousCharIsADoubleQuote) {
338  // it's an invalid url
339  if (badurl) {
340  *badurl = true;
341  }
342  return QString();
343  }
344  if (mText.at(mPos) == QLatin1Char('"')) {
345  previousCharIsADoubleQuote = true;
346  } else {
347  previousCharIsADoubleQuote = false;
348  }
349  if (mText.at(mPos) == QLatin1Char('#')) {
350  previousIsAnAnchor = true;
351  }
352  url.append(mText.at(mPos));
353  if (url.length() > mMaxUrlLen) {
354  break;
355  }
356  }
357 
358  ++mPos;
359  }
360 
361  if (isEmptyUrl(url) || (url.length() > mMaxUrlLen)) {
362  mPos = start;
363  url.clear();
364  return url;
365  } else {
366  --mPos;
367  }
368  }
369 
370  // HACK: This is actually against the RFC. However, most people don't properly escape the URL in
371  // their text with "" or <>. That leads to people writing an url, followed immediately by
372  // a dot to finish the sentence. That would lead the parser to include the dot in the url,
373  // even though that is not wanted. So work around that here.
374  // Most real-life URLs hopefully don't end with dots or commas.
375  const QString wordBoundaries = QStringLiteral(".,:!?)>");
376  if (url.length() > 1) {
377  do {
378  if (wordBoundaries.contains(url.at(url.length() - 1))) {
379  url.chop(1);
380  --mPos;
381  } else {
382  break;
383  }
384  } while (url.length() > 1);
385  }
386  return url;
387 }
388 
389 QString KTextToHTMLHelper::highlightedText()
390 {
391  // formating symbols must be prepended with a whitespace
392  if ((mPos > 0) && !mText.at(mPos - 1).isSpace()) {
393  return QString();
394  }
395 
396  const QChar ch = mText.at(mPos);
397  if (ch != QLatin1Char('/') && ch != QLatin1Char('*') && ch != QLatin1Char('_') && ch != QLatin1Char('-')) {
398  return QString();
399  }
400 
401  QRegularExpression re(QStringLiteral("\\%1([^\\s|^\\%1].*[^\\s|^\\%1])\\%1").arg(ch));
402  re.setPatternOptions(QRegularExpression::InvertedGreedinessOption);
404 
405  if (match.hasMatch()) {
406  if (match.capturedStart() == mPos) {
407  int length = match.capturedLength();
408  // there must be a whitespace after the closing formating symbol
409  if (mPos + length < mText.length() && !mText.at(mPos + length).isSpace()) {
410  return QString();
411  }
412  mPos += length - 1;
413  switch (ch.toLatin1()) {
414  case '*':
415  return QLatin1String("<b>*") + match.capturedRef(1) + QLatin1String("*</b>");
416  case '_':
417  return QLatin1String("<u>_") + match.capturedRef(1) + QLatin1String("_</u>");
418  case '/':
419  return QLatin1String("<i>/") + match.capturedRef(1) + QLatin1String("/</i>");
420  case '-':
421  return QLatin1String("<s>-") + match.capturedRef(1) + QLatin1String("-</s>");
422  }
423  }
424  }
425  return QString();
426 }
427 
428 QString KTextToHTML::convertToHtml(const QString &plainText, const KTextToHTML::Options &flags, int maxUrlLen, int maxAddressLen)
429 {
430  KTextToHTMLHelper helper(plainText, 0, maxUrlLen, maxAddressLen);
431 
432  QString str;
433  QString result(static_cast<QChar *>(nullptr), helper.mText.length() * 2);
434  QChar ch;
435  int x;
436  bool startOfLine = true;
437 
438  for (helper.mPos = 0, x = 0; helper.mPos < helper.mText.length(); ++helper.mPos, ++x) {
439  ch = helper.mText.at(helper.mPos);
440  if (flags & PreserveSpaces) {
441  if (ch == QLatin1Char(' ')) {
442  if (helper.mPos + 1 < helper.mText.length()) {
443  if (helper.mText.at(helper.mPos + 1) != QLatin1Char(' ')) {
444  // A single space, make it breaking if not at the start or end of the line
445  const bool endOfLine = helper.mText.at(helper.mPos + 1) == QLatin1Char('\n');
446  if (!startOfLine && !endOfLine) {
447  result += QLatin1Char(' ');
448  } else {
449  result += QLatin1String("&nbsp;");
450  }
451  } else {
452  // Whitespace of more than one space, make it all non-breaking
453  while (helper.mPos < helper.mText.length() && helper.mText.at(helper.mPos) == QLatin1Char(' ')) {
454  result += QLatin1String("&nbsp;");
455  ++helper.mPos;
456  ++x;
457  }
458 
459  // We incremented once to often, undo that
460  --helper.mPos;
461  --x;
462  }
463  } else {
464  // Last space in the text, it is non-breaking
465  result += QLatin1String("&nbsp;");
466  }
467 
468  if (startOfLine) {
469  startOfLine = false;
470  }
471  continue;
472  } else if (ch == QLatin1Char('\t')) {
473  do {
474  result += QLatin1String("&nbsp;");
475  ++x;
476  } while ((x & 7) != 0);
477  --x;
478  startOfLine = false;
479  continue;
480  }
481  }
482  if (ch == QLatin1Char('\n')) {
483  result += QLatin1String("<br />\n"); // Keep the \n, so apps can figure out the quoting levels correctly.
484  startOfLine = true;
485  x = -1;
486  continue;
487  }
488 
489  startOfLine = false;
490  if (ch == QLatin1Char('&')) {
491  result += QLatin1String("&amp;");
492  } else if (ch == QLatin1Char('"')) {
493  result += QLatin1String("&quot;");
494  } else if (ch == QLatin1Char('<')) {
495  result += QLatin1String("&lt;");
496  } else if (ch == QLatin1Char('>')) {
497  result += QLatin1String("&gt;");
498  } else {
499  const int start = helper.mPos;
500  if (!(flags & IgnoreUrls)) {
501  bool badUrl = false;
502  str = helper.getUrl(&badUrl);
503  if (badUrl) {
504  QString resultBadUrl;
505  const int helperTextSize(helper.mText.count());
506  for (int i = 0; i < helperTextSize; ++i) {
507  const QChar chBadUrl = helper.mText.at(i);
508  if (chBadUrl == QLatin1Char('&')) {
509  resultBadUrl += QLatin1String("&amp;");
510  } else if (chBadUrl == QLatin1Char('"')) {
511  resultBadUrl += QLatin1String("&quot;");
512  } else if (chBadUrl == QLatin1Char('<')) {
513  resultBadUrl += QLatin1String("&lt;");
514  } else if (chBadUrl == QLatin1Char('>')) {
515  resultBadUrl += QLatin1String("&gt;");
516  } else {
517  resultBadUrl += chBadUrl;
518  }
519  }
520  return resultBadUrl;
521  }
522  if (!str.isEmpty()) {
523  QString hyperlink;
524  if (str.startsWith(QLatin1String("www."))) {
525  hyperlink = QLatin1String("http://") + str;
526  } else if (str.startsWith(QLatin1String("ftp."))) {
527  hyperlink = QLatin1String("ftp://") + str;
528  } else {
529  hyperlink = str;
530  }
531  result += QLatin1String("<a href=\"") + hyperlink + QLatin1String("\">") + str.toHtmlEscaped() + QLatin1String("</a>");
532  x += helper.mPos - start;
533  continue;
534  }
535  str = helper.getEmailAddress();
536  if (!str.isEmpty()) {
537  // len is the length of the local part
538  int len = str.indexOf(QLatin1Char('@'));
539  QString localPart = str.left(len);
540 
541  // remove the local part from the result (as '&'s have been expanded to
542  // &amp; we have to take care of the 4 additional characters per '&')
543  result.truncate(result.length() - len - (localPart.count(QLatin1Char('&')) * 4));
544  x -= len;
545 
546  result += QLatin1String("<a href=\"mailto:") + str + QLatin1String("\">") + str + QLatin1String("</a>");
547  x += str.length() - 1;
548  continue;
549  }
550  if (flags & ConvertPhoneNumbers) {
551  str = helper.getPhoneNumber();
552  if (!str.isEmpty()) {
553  result += QLatin1String("<a href=\"tel:") + normalizePhoneNumber(str) + QLatin1String("\">") + str + QLatin1String("</a>");
554  x += str.length() - 1;
555  continue;
556  }
557  }
558  }
559  if (flags & HighlightText) {
560  str = helper.highlightedText();
561  if (!str.isEmpty()) {
562  result += str;
563  x += helper.mPos - start;
564  continue;
565  }
566  }
567  result += ch;
568  }
569  }
570 
571  if (flags & ReplaceSmileys) {
572  const QStringList exclude = {QStringLiteral("(c)"), QStringLiteral("(C)"), QStringLiteral("&gt;:-("), QStringLiteral("&gt;:("), QStringLiteral("(B)"),
573  QStringLiteral("(b)"), QStringLiteral("(P)"), QStringLiteral("(p)"), QStringLiteral("(O)"), QStringLiteral("(o)"),
574  QStringLiteral("(D)"), QStringLiteral("(d)"), QStringLiteral("(E)"), QStringLiteral("(e)"), QStringLiteral("(K)"),
575  QStringLiteral("(k)"), QStringLiteral("(I)"), QStringLiteral("(i)"), QStringLiteral("(L)"), QStringLiteral("(l)"),
576  QStringLiteral("(8)"), QStringLiteral("(T)"), QStringLiteral("(t)"), QStringLiteral("(G)"), QStringLiteral("(g)"),
577  QStringLiteral("(F)"), QStringLiteral("(f)"), QStringLiteral("(H)"), QStringLiteral("8)"), QStringLiteral("(N)"),
578  QStringLiteral("(n)"), QStringLiteral("(Y)"), QStringLiteral("(y)"), QStringLiteral("(U)"), QStringLiteral("(u)"),
579  QStringLiteral("(W)"), QStringLiteral("(w)"), QStringLiteral("(6)")};
580 
581  result = helper.emoticonsInterface()->parseEmoticons(result, true, exclude);
582  }
583 
584  return result;
585 }
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
QString & append(QChar ch)
void truncate(int position)
int size() const const
T value() const const
void chop(int n)
Preserve white-space formatting of the text.
Definition: ktexttohtml.h:28
void clear()
PostalAddress address(const QVariant &location)
QVariant property(const char *name) const const
bool contains(QStringView str, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
Interpret text highlighting markup, like bold, underline and /italic/, and wrap them in corresponding...
Definition: ktexttohtml.h:48
Replace text emoticons smileys by emoticons images.
Definition: ktexttohtml.h:37
KCOREADDONS_EXPORT QString convertToHtml(const QString &plainText, const KTextToHTML::Options &options, int maxUrlLen=4096, int maxAddressLen=255)
Converts plaintext into html.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
This is the main function which does scored fuzzy matching.
Don&#39;t parse and replace any URLs.
Definition: ktexttohtml.h:42
void push_back(QChar ch)
bool isNull() const const
QString toHtmlEscaped() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
char toLatin1() const const
const QList< QKeySequence > & end()
QString mid(int position, int n) const const
int count() const const
const QChar at(int position) const const
int length() const const
void reserve(int size)
QString left(int n) const const
Replace phone numbers with tel: links.
Definition: ktexttohtml.h:54
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sun Apr 18 2021 23:02:02 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.