1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include <QStack> |
5 | #include <QVector> |
6 | #include <QPainter> |
7 | #include <QTextLayout> |
8 | #include <QDebug> |
9 | #include <qmath.h> |
10 | #include "qquickstyledtext_p.h" |
11 | #include <QQmlContext> |
12 | #include <QtGui/private/qtexthtmlparser_p.h> |
13 | |
14 | Q_LOGGING_CATEGORY(lcStyledText, "qt.quick.styledtext" ) |
15 | |
16 | /* |
17 | QQuickStyledText supports few tags: |
18 | |
19 | <b></b> - bold |
20 | <del></del> - strike out (removed content) |
21 | <s></s> - strike out (no longer accurate or no longer relevant content) |
22 | <strong></strong> - bold |
23 | <i></i> - italic |
24 | <br> - new line |
25 | <p> - paragraph |
26 | <u> - underlined text |
27 | <font color="color_name" size="1-7"></font> |
28 | <h1> to <h6> - headers |
29 | <a href=""> - anchor |
30 | <ol type="">, <ul type=""> and <li> - ordered and unordered lists |
31 | <pre></pre> - preformated |
32 | <img src=""> - images |
33 | |
34 | The opening and closing tags must be correctly nested. |
35 | */ |
36 | |
37 | QT_BEGIN_NAMESPACE |
38 | |
39 | Q_GUI_EXPORT int qt_defaultDpi(); |
40 | |
41 | class QQuickStyledTextPrivate |
42 | { |
43 | public: |
44 | enum ListType { Ordered, Unordered }; |
45 | enum ListFormat { Bullet, Disc, Square, Decimal, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman }; |
46 | |
47 | struct List { |
48 | int level; |
49 | ListType type; |
50 | ListFormat format; |
51 | }; |
52 | |
53 | QQuickStyledTextPrivate(const QString &t, QTextLayout &l, |
54 | QList<QQuickStyledTextImgTag*> &imgTags, |
55 | const QUrl &baseUrl, |
56 | QQmlContext *context, |
57 | bool preloadImages, |
58 | bool *fontSizeModified) |
59 | : text(t), layout(l), imgTags(&imgTags), baseFont(layout.font()), baseUrl(baseUrl), |
60 | fontSizeModified(fontSizeModified), context(context), preloadImages(preloadImages) |
61 | { |
62 | } |
63 | |
64 | void parse(); |
65 | void appendText(const QString &textIn, int start, int length, QString &textOut); |
66 | bool parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format); |
67 | bool parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut); |
68 | void parseEntity(const QChar *&ch, const QString &textIn, QString &textOut); |
69 | bool parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format); |
70 | bool parseOrderedListAttributes(const QChar *&ch, const QString &textIn); |
71 | bool parseUnorderedListAttributes(const QChar *&ch, const QString &textIn); |
72 | bool parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format); |
73 | void parseImageAttributes(const QChar *&ch, const QString &textIn, QString &textOut); |
74 | QPair<QStringView,QStringView> parseAttribute(const QChar *&ch, const QString &textIn); |
75 | QStringView parseValue(const QChar *&ch, const QString &textIn); |
76 | void setFontSize(int size, QTextCharFormat &format); |
77 | |
78 | inline void skipSpace(const QChar *&ch) { |
79 | while (ch->isSpace() && !ch->isNull()) |
80 | ++ch; |
81 | } |
82 | |
83 | static QString toAlpha(int value, bool upper); |
84 | static QString toRoman(int value, bool upper); |
85 | |
86 | QString text; |
87 | QTextLayout &layout; |
88 | QList<QQuickStyledTextImgTag*> *imgTags; |
89 | QFont baseFont; |
90 | QStack<List> listStack; |
91 | QUrl baseUrl; |
92 | bool *fontSizeModified; |
93 | QQmlContext *context; |
94 | int nbImages = 0; |
95 | bool hasNewLine = true; |
96 | bool updateImagePositions = false; |
97 | bool preFormat = false; |
98 | bool prependSpace = false; |
99 | bool hasSpace = true; |
100 | bool preloadImages; |
101 | |
102 | static const QChar lessThan; |
103 | static const QChar greaterThan; |
104 | static const QChar equals; |
105 | static const QChar singleQuote; |
106 | static const QChar doubleQuote; |
107 | static const QChar slash; |
108 | static const QChar ampersand; |
109 | static const QChar bullet; |
110 | static const QChar disc; |
111 | static const QChar square; |
112 | static const QChar lineFeed; |
113 | static const QChar space; |
114 | static const int tabsize = 6; |
115 | }; |
116 | |
117 | const QChar QQuickStyledTextPrivate::lessThan(QLatin1Char('<')); |
118 | const QChar QQuickStyledTextPrivate::greaterThan(QLatin1Char('>')); |
119 | const QChar QQuickStyledTextPrivate::equals(QLatin1Char('=')); |
120 | const QChar QQuickStyledTextPrivate::singleQuote(QLatin1Char('\'')); |
121 | const QChar QQuickStyledTextPrivate::doubleQuote(QLatin1Char('\"')); |
122 | const QChar QQuickStyledTextPrivate::slash(QLatin1Char('/')); |
123 | const QChar QQuickStyledTextPrivate::ampersand(QLatin1Char('&')); |
124 | const QChar QQuickStyledTextPrivate::bullet(0x2022); |
125 | const QChar QQuickStyledTextPrivate::disc(0x25e6); |
126 | const QChar QQuickStyledTextPrivate::square(0x25a1); |
127 | const QChar QQuickStyledTextPrivate::lineFeed(QLatin1Char('\n')); |
128 | const QChar QQuickStyledTextPrivate::space(QLatin1Char(' ')); |
129 | |
130 | namespace { |
131 | bool is_equal_ignoring_case(QStringView s1, QLatin1StringView s2) noexcept |
132 | { |
133 | return s1.compare(s: s2, cs: Qt::CaseInsensitive) == 0; |
134 | } |
135 | } |
136 | |
137 | QQuickStyledText::QQuickStyledText(const QString &string, QTextLayout &layout, |
138 | QList<QQuickStyledTextImgTag*> &imgTags, |
139 | const QUrl &baseUrl, |
140 | QQmlContext *context, |
141 | bool preloadImages, |
142 | bool *fontSizeModified) |
143 | : d(new QQuickStyledTextPrivate(string, layout, imgTags, baseUrl, context, preloadImages, fontSizeModified)) |
144 | { |
145 | } |
146 | |
147 | QQuickStyledText::~QQuickStyledText() |
148 | { |
149 | delete d; |
150 | } |
151 | |
152 | void QQuickStyledText::parse(const QString &string, QTextLayout &layout, |
153 | QList<QQuickStyledTextImgTag*> &imgTags, |
154 | const QUrl &baseUrl, |
155 | QQmlContext *context, |
156 | bool preloadImages, |
157 | bool *fontSizeModified) |
158 | { |
159 | if (string.isEmpty()) |
160 | return; |
161 | QQuickStyledText styledText(string, layout, imgTags, baseUrl, context, preloadImages, fontSizeModified); |
162 | styledText.d->parse(); |
163 | } |
164 | |
165 | void QQuickStyledTextPrivate::parse() |
166 | { |
167 | QVector<QTextLayout::FormatRange> ranges; |
168 | QStack<QTextCharFormat> formatStack; |
169 | |
170 | QString drawText; |
171 | drawText.reserve(asize: text.size()); |
172 | |
173 | updateImagePositions = !imgTags->isEmpty(); |
174 | |
175 | int textStart = 0; |
176 | int textLength = 0; |
177 | int rangeStart = 0; |
178 | bool formatChanged = false; |
179 | |
180 | const QChar *ch = text.constData(); |
181 | while (!ch->isNull()) { |
182 | if (*ch == lessThan) { |
183 | if (textLength) { |
184 | appendText(textIn: text, start: textStart, length: textLength, textOut&: drawText); |
185 | } else if (prependSpace) { |
186 | drawText.append(c: space); |
187 | prependSpace = false; |
188 | hasSpace = true; |
189 | } |
190 | |
191 | if (rangeStart != drawText.size() && formatStack.size()) { |
192 | if (formatChanged) { |
193 | QTextLayout::FormatRange formatRange; |
194 | formatRange.format = formatStack.top(); |
195 | formatRange.start = rangeStart; |
196 | formatRange.length = drawText.size() - rangeStart; |
197 | ranges.append(t: formatRange); |
198 | formatChanged = false; |
199 | } else if (ranges.size()) { |
200 | ranges.last().length += drawText.size() - rangeStart; |
201 | } |
202 | } |
203 | rangeStart = drawText.size(); |
204 | ++ch; |
205 | if (*ch == slash) { |
206 | ++ch; |
207 | if (parseCloseTag(ch, textIn: text, textOut&: drawText)) { |
208 | if (formatStack.size()) { |
209 | formatChanged = true; |
210 | formatStack.pop(); |
211 | } |
212 | } |
213 | } else { |
214 | QTextCharFormat format; |
215 | if (formatStack.size()) |
216 | format = formatStack.top(); |
217 | if (parseTag(ch, textIn: text, textOut&: drawText, format)) { |
218 | formatChanged = true; |
219 | formatStack.push(t: format); |
220 | } |
221 | } |
222 | textStart = ch - text.constData() + 1; |
223 | textLength = 0; |
224 | } else if (*ch == ampersand) { |
225 | ++ch; |
226 | appendText(textIn: text, start: textStart, length: textLength, textOut&: drawText); |
227 | parseEntity(ch, textIn: text, textOut&: drawText); |
228 | textStart = ch - text.constData() + 1; |
229 | textLength = 0; |
230 | } else if (ch->isSpace()) { |
231 | if (textLength) |
232 | appendText(textIn: text, start: textStart, length: textLength, textOut&: drawText); |
233 | if (!preFormat) { |
234 | prependSpace = !hasSpace; |
235 | for (const QChar *n = ch + 1; !n->isNull() && n->isSpace(); ++n) |
236 | ch = n; |
237 | hasNewLine = false; |
238 | } else if (*ch == lineFeed) { |
239 | drawText.append(c: QChar(QChar::LineSeparator)); |
240 | hasNewLine = true; |
241 | } else { |
242 | drawText.append(c: QChar(QChar::Nbsp)); |
243 | hasNewLine = false; |
244 | } |
245 | textStart = ch - text.constData() + 1; |
246 | textLength = 0; |
247 | } else { |
248 | ++textLength; |
249 | } |
250 | if (!ch->isNull()) |
251 | ++ch; |
252 | } |
253 | if (textLength) |
254 | appendText(textIn: text, start: textStart, length: textLength, textOut&: drawText); |
255 | if (rangeStart != drawText.size() && formatStack.size()) { |
256 | if (formatChanged) { |
257 | QTextLayout::FormatRange formatRange; |
258 | formatRange.format = formatStack.top(); |
259 | formatRange.start = rangeStart; |
260 | formatRange.length = drawText.size() - rangeStart; |
261 | ranges.append(t: formatRange); |
262 | } else if (ranges.size()) { |
263 | ranges.last().length += drawText.size() - rangeStart; |
264 | } |
265 | } |
266 | |
267 | layout.setText(drawText); |
268 | layout.setFormats(ranges); |
269 | } |
270 | |
271 | void QQuickStyledTextPrivate::appendText(const QString &textIn, int start, int length, QString &textOut) |
272 | { |
273 | if (prependSpace) |
274 | textOut.append(c: space); |
275 | textOut.append(v: QStringView(textIn).mid(pos: start, n: length)); |
276 | prependSpace = false; |
277 | hasSpace = false; |
278 | hasNewLine = false; |
279 | } |
280 | |
281 | // |
282 | // Calculates and sets the correct font size in points |
283 | // depending on the size multiplier and base font. |
284 | // |
285 | void QQuickStyledTextPrivate::setFontSize(int size, QTextCharFormat &format) |
286 | { |
287 | static const qreal scaling[] = { 0.7, 0.8, 1.0, 1.2, 1.5, 2.0, 2.4 }; |
288 | if (baseFont.pointSizeF() != -1) |
289 | format.setFontPointSize(baseFont.pointSize() * scaling[size - 1]); |
290 | else |
291 | format.setFontPointSize(baseFont.pixelSize() * qreal(72.) / qreal(qt_defaultDpi()) * scaling[size - 1]); |
292 | *fontSizeModified = true; |
293 | } |
294 | |
295 | bool QQuickStyledTextPrivate::parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format) |
296 | { |
297 | skipSpace(ch); |
298 | |
299 | int tagStart = ch - textIn.constData(); |
300 | int tagLength = 0; |
301 | while (!ch->isNull()) { |
302 | if (*ch == greaterThan) { |
303 | if (tagLength == 0) |
304 | return false; |
305 | auto tag = QStringView(textIn).mid(pos: tagStart, n: tagLength); |
306 | const QChar char0 = tag.at(n: 0).toLower(); |
307 | if (char0 == QLatin1Char('b')) { |
308 | if (tagLength == 1) { |
309 | format.setFontWeight(QFont::Bold); |
310 | return true; |
311 | } else if (tagLength == 2 && tag.at(n: 1).toLower() == QLatin1Char('r')) { |
312 | textOut.append(c: QChar(QChar::LineSeparator)); |
313 | hasSpace = true; |
314 | prependSpace = false; |
315 | return false; |
316 | } |
317 | } else if (char0 == QLatin1Char('i')) { |
318 | if (tagLength == 1) { |
319 | format.setFontItalic(true); |
320 | return true; |
321 | } |
322 | } else if (char0 == QLatin1Char('p')) { |
323 | if (tagLength == 1) { |
324 | if (!hasNewLine) |
325 | textOut.append(c: QChar::LineSeparator); |
326 | hasSpace = true; |
327 | prependSpace = false; |
328 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("pre" ))) { |
329 | preFormat = true; |
330 | if (!hasNewLine) |
331 | textOut.append(c: QChar::LineSeparator); |
332 | format.setFontFamilies(QStringList {QString::fromLatin1(ba: "Courier New" ), QString::fromLatin1(ba: "courier" )}); |
333 | format.setFontFixedPitch(true); |
334 | return true; |
335 | } |
336 | } else if (char0 == QLatin1Char('u')) { |
337 | if (tagLength == 1) { |
338 | format.setFontUnderline(true); |
339 | return true; |
340 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("ul" ))) { |
341 | List listItem; |
342 | listItem.level = 0; |
343 | listItem.type = Unordered; |
344 | listItem.format = Bullet; |
345 | listStack.push(t: listItem); |
346 | } |
347 | } else if (char0 == QLatin1Char('h') && tagLength == 2) { |
348 | int level = tag.at(n: 1).digitValue(); |
349 | if (level >= 1 && level <= 6) { |
350 | if (!hasNewLine) |
351 | textOut.append(c: QChar::LineSeparator); |
352 | hasSpace = true; |
353 | prependSpace = false; |
354 | setFontSize(size: 7 - level, format); |
355 | format.setFontWeight(QFont::Bold); |
356 | return true; |
357 | } |
358 | } else if (char0 == QLatin1Char('s')) { |
359 | if (tagLength == 1) { |
360 | format.setFontStrikeOut(true); |
361 | return true; |
362 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("strong" ))) { |
363 | format.setFontWeight(QFont::Bold); |
364 | return true; |
365 | } |
366 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("del" ))) { |
367 | format.setFontStrikeOut(true); |
368 | return true; |
369 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("ol" ))) { |
370 | List listItem; |
371 | listItem.level = 0; |
372 | listItem.type = Ordered; |
373 | listItem.format = Decimal; |
374 | listStack.push(t: listItem); |
375 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("li" ))) { |
376 | if (!hasNewLine) |
377 | textOut.append(c: QChar(QChar::LineSeparator)); |
378 | if (!listStack.isEmpty()) { |
379 | int count = ++listStack.top().level; |
380 | for (int i = 0; i < listStack.size(); ++i) |
381 | textOut += QString(tabsize, QChar::Nbsp); |
382 | switch (listStack.top().format) { |
383 | case Decimal: |
384 | textOut += QString::number(count) % QLatin1Char('.'); |
385 | break; |
386 | case LowerAlpha: |
387 | textOut += toAlpha(value: count, upper: false) % QLatin1Char('.'); |
388 | break; |
389 | case UpperAlpha: |
390 | textOut += toAlpha(value: count, upper: true) % QLatin1Char('.'); |
391 | break; |
392 | case LowerRoman: |
393 | textOut += toRoman(value: count, upper: false) % QLatin1Char('.'); |
394 | break; |
395 | case UpperRoman: |
396 | textOut += toRoman(value: count, upper: true) % QLatin1Char('.'); |
397 | break; |
398 | case Bullet: |
399 | textOut += bullet; |
400 | break; |
401 | case Disc: |
402 | textOut += disc; |
403 | break; |
404 | case Square: |
405 | textOut += square; |
406 | break; |
407 | } |
408 | textOut += QString(2, QChar::Nbsp); |
409 | } |
410 | } |
411 | return false; |
412 | } else if (ch->isSpace()) { |
413 | // may have params. |
414 | auto tag = QStringView(textIn).mid(pos: tagStart, n: tagLength); |
415 | if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("font" ))) |
416 | return parseFontAttributes(ch, textIn, format); |
417 | if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("ol" ))) { |
418 | parseOrderedListAttributes(ch, textIn); |
419 | return false; // doesn't modify format |
420 | } |
421 | if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("ul" ))) { |
422 | parseUnorderedListAttributes(ch, textIn); |
423 | return false; // doesn't modify format |
424 | } |
425 | if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("a" ))) { |
426 | return parseAnchorAttributes(ch, textIn, format); |
427 | } |
428 | if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("img" ))) { |
429 | parseImageAttributes(ch, textIn, textOut); |
430 | return false; |
431 | } |
432 | if (*ch == greaterThan || ch->isNull()) |
433 | continue; |
434 | } else if (*ch != slash) { |
435 | tagLength++; |
436 | } |
437 | ++ch; |
438 | } |
439 | return false; |
440 | } |
441 | |
442 | bool QQuickStyledTextPrivate::parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut) |
443 | { |
444 | skipSpace(ch); |
445 | |
446 | int tagStart = ch - textIn.constData(); |
447 | int tagLength = 0; |
448 | while (!ch->isNull()) { |
449 | if (*ch == greaterThan) { |
450 | if (tagLength == 0) |
451 | return false; |
452 | auto tag = QStringView(textIn).mid(pos: tagStart, n: tagLength); |
453 | const QChar char0 = tag.at(n: 0).toLower(); |
454 | hasNewLine = false; |
455 | if (char0 == QLatin1Char('b')) { |
456 | if (tagLength == 1) |
457 | return true; |
458 | else if (tag.at(n: 1).toLower() == QLatin1Char('r') && tagLength == 2) |
459 | return false; |
460 | } else if (char0 == QLatin1Char('i')) { |
461 | if (tagLength == 1) |
462 | return true; |
463 | } else if (char0 == QLatin1Char('a')) { |
464 | if (tagLength == 1) |
465 | return true; |
466 | } else if (char0 == QLatin1Char('p')) { |
467 | if (tagLength == 1) { |
468 | textOut.append(c: QChar::LineSeparator); |
469 | hasNewLine = true; |
470 | hasSpace = true; |
471 | return false; |
472 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("pre" ))) { |
473 | preFormat = false; |
474 | if (!hasNewLine) |
475 | textOut.append(c: QChar::LineSeparator); |
476 | hasNewLine = true; |
477 | hasSpace = true; |
478 | return true; |
479 | } |
480 | } else if (char0 == QLatin1Char('u')) { |
481 | if (tagLength == 1) |
482 | return true; |
483 | else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("ul" ))) { |
484 | if (!listStack.isEmpty()) { |
485 | listStack.pop(); |
486 | if (!listStack.size()) |
487 | textOut.append(c: QChar::LineSeparator); |
488 | } |
489 | return false; |
490 | } |
491 | } else if (char0 == QLatin1Char('h') && tagLength == 2) { |
492 | textOut.append(c: QChar::LineSeparator); |
493 | hasNewLine = true; |
494 | hasSpace = true; |
495 | return true; |
496 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("font" ))) { |
497 | return true; |
498 | } else if (char0 == QLatin1Char('s')) { |
499 | if (tagLength == 1) { |
500 | return true; |
501 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("strong" ))) { |
502 | return true; |
503 | } |
504 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("del" ))) { |
505 | return true; |
506 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("ol" ))) { |
507 | if (!listStack.isEmpty()) { |
508 | listStack.pop(); |
509 | if (!listStack.size()) |
510 | textOut.append(c: QChar::LineSeparator); |
511 | } |
512 | return false; |
513 | } else if (is_equal_ignoring_case(s1: tag, s2: QLatin1String("li" ))) { |
514 | return false; |
515 | } |
516 | return false; |
517 | } else if (!ch->isSpace()){ |
518 | tagLength++; |
519 | } |
520 | ++ch; |
521 | } |
522 | |
523 | return false; |
524 | } |
525 | |
526 | void QQuickStyledTextPrivate::parseEntity(const QChar *&ch, const QString &textIn, QString &textOut) |
527 | { |
528 | int entityStart = ch - textIn.constData(); |
529 | int entityLength = 0; |
530 | while (!ch->isNull()) { |
531 | if (*ch == QLatin1Char(';')) { |
532 | auto entity = QStringView(textIn).mid(pos: entityStart, n: entityLength); |
533 | #if QT_CONFIG(texthtmlparser) |
534 | const QString parsedEntity = QTextHtmlParser::parseEntity(entity); |
535 | if (!parsedEntity.isNull()) |
536 | textOut += parsedEntity; |
537 | else |
538 | #endif |
539 | qCWarning(lcStyledText) << "StyledText doesn't support entity" << entity; |
540 | return; |
541 | } else if (*ch == QLatin1Char(' ')) { |
542 | auto entity = QStringView(textIn).mid(pos: entityStart - 1, n: entityLength + 1); |
543 | textOut += entity + *ch; |
544 | return; |
545 | } |
546 | ++entityLength; |
547 | ++ch; |
548 | } |
549 | } |
550 | |
551 | bool QQuickStyledTextPrivate::parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format) |
552 | { |
553 | bool valid = false; |
554 | QPair<QStringView,QStringView> attr; |
555 | do { |
556 | attr = parseAttribute(ch, textIn); |
557 | if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("color" ))) { |
558 | valid = true; |
559 | format.setForeground(QColor::fromString(name: attr.second)); |
560 | } else if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("size" ))) { |
561 | valid = true; |
562 | int size = attr.second.toInt(); |
563 | if (attr.second.at(n: 0) == QLatin1Char('-') || attr.second.at(n: 0) == QLatin1Char('+')) |
564 | size += 3; |
565 | if (size >= 1 && size <= 7) |
566 | setFontSize(size, format); |
567 | } |
568 | } while (!ch->isNull() && !attr.first.isEmpty()); |
569 | |
570 | return valid; |
571 | } |
572 | |
573 | bool QQuickStyledTextPrivate::parseOrderedListAttributes(const QChar *&ch, const QString &textIn) |
574 | { |
575 | bool valid = false; |
576 | |
577 | List listItem; |
578 | listItem.level = 0; |
579 | listItem.type = Ordered; |
580 | listItem.format = Decimal; |
581 | |
582 | QPair<QStringView,QStringView> attr; |
583 | do { |
584 | attr = parseAttribute(ch, textIn); |
585 | if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("type" ))) { |
586 | valid = true; |
587 | if (attr.second == QLatin1String("a" )) |
588 | listItem.format = LowerAlpha; |
589 | else if (attr.second == QLatin1String("A" )) |
590 | listItem.format = UpperAlpha; |
591 | else if (attr.second == QLatin1String("i" )) |
592 | listItem.format = LowerRoman; |
593 | else if (attr.second == QLatin1String("I" )) |
594 | listItem.format = UpperRoman; |
595 | } |
596 | } while (!ch->isNull() && !attr.first.isEmpty()); |
597 | |
598 | listStack.push(t: listItem); |
599 | return valid; |
600 | } |
601 | |
602 | bool QQuickStyledTextPrivate::parseUnorderedListAttributes(const QChar *&ch, const QString &textIn) |
603 | { |
604 | bool valid = false; |
605 | |
606 | List listItem; |
607 | listItem.level = 0; |
608 | listItem.type = Unordered; |
609 | listItem.format = Bullet; |
610 | |
611 | QPair<QStringView,QStringView> attr; |
612 | do { |
613 | attr = parseAttribute(ch, textIn); |
614 | if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("type" ))) { |
615 | valid = true; |
616 | if (is_equal_ignoring_case(s1: attr.second, s2: QLatin1String("disc" ))) |
617 | listItem.format = Disc; |
618 | else if (is_equal_ignoring_case(s1: attr.second, s2: QLatin1String("square" ))) |
619 | listItem.format = Square; |
620 | } |
621 | } while (!ch->isNull() && !attr.first.isEmpty()); |
622 | |
623 | listStack.push(t: listItem); |
624 | return valid; |
625 | } |
626 | |
627 | bool QQuickStyledTextPrivate::parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format) |
628 | { |
629 | bool valid = false; |
630 | |
631 | QPair<QStringView,QStringView> attr; |
632 | do { |
633 | attr = parseAttribute(ch, textIn); |
634 | if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("href" ))) { |
635 | format.setAnchorHref(attr.second.toString()); |
636 | format.setAnchor(true); |
637 | format.setFontUnderline(true); |
638 | valid = true; |
639 | } |
640 | } while (!ch->isNull() && !attr.first.isEmpty()); |
641 | |
642 | return valid; |
643 | } |
644 | |
645 | void QQuickStyledTextPrivate::parseImageAttributes(const QChar *&ch, const QString &textIn, QString &textOut) |
646 | { |
647 | qreal imgWidth = 0.0; |
648 | QFontMetricsF fm(layout.font()); |
649 | const qreal spaceWidth = fm.horizontalAdvance(QChar::Nbsp); |
650 | const bool trailingSpace = textOut.endsWith(c: space); |
651 | |
652 | if (!updateImagePositions) { |
653 | QQuickStyledTextImgTag *image = new QQuickStyledTextImgTag; |
654 | image->position = textOut.size() + (trailingSpace ? 0 : 1); |
655 | |
656 | QPair<QStringView,QStringView> attr; |
657 | do { |
658 | attr = parseAttribute(ch, textIn); |
659 | if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("src" ))) { |
660 | image->url = QUrl(attr.second.toString()); |
661 | } else if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("width" ))) { |
662 | image->size.setWidth(attr.second.toString().toInt()); |
663 | } else if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("height" ))) { |
664 | image->size.setHeight(attr.second.toString().toInt()); |
665 | } else if (is_equal_ignoring_case(s1: attr.first, s2: QLatin1String("align" ))) { |
666 | if (is_equal_ignoring_case(s1: attr.second, s2: QLatin1String("top" ))) { |
667 | image->align = QQuickStyledTextImgTag::Top; |
668 | } else if (is_equal_ignoring_case(s1: attr.second, s2: QLatin1String("middle" ))) { |
669 | image->align = QQuickStyledTextImgTag::Middle; |
670 | } |
671 | } |
672 | } while (!ch->isNull() && !attr.first.isEmpty()); |
673 | |
674 | if (preloadImages && !image->size.isValid()) { |
675 | // if we don't know its size but the image is a local image, |
676 | // we load it in the pixmap cache and save its implicit size |
677 | // to avoid a relayout later on. |
678 | QUrl url = baseUrl.resolved(relative: image->url); |
679 | if (url.isLocalFile()) { |
680 | image->pix = new QQuickPixmap(context->engine(), url, QRect(), image->size); |
681 | if (image->pix && image->pix->isReady()) { |
682 | image->size = image->pix->implicitSize(); |
683 | } else { |
684 | delete image->pix; |
685 | image->pix = nullptr; |
686 | } |
687 | } |
688 | } |
689 | |
690 | imgWidth = image->size.width(); |
691 | image->offset = -std::fmod(x: imgWidth, y: spaceWidth) / 2.0; |
692 | imgTags->append(t: image); |
693 | |
694 | } else { |
695 | // if we already have a list of img tags for this text |
696 | // we only want to update the positions of these tags. |
697 | QQuickStyledTextImgTag *image = imgTags->value(i: nbImages); |
698 | image->position = textOut.size() + (trailingSpace ? 0 : 1); |
699 | imgWidth = image->size.width(); |
700 | image->offset = -std::fmod(x: imgWidth, y: spaceWidth) / 2.0; |
701 | QPair<QStringView,QStringView> attr; |
702 | do { |
703 | attr = parseAttribute(ch, textIn); |
704 | } while (!ch->isNull() && !attr.first.isEmpty()); |
705 | nbImages++; |
706 | } |
707 | |
708 | QString padding(qFloor(v: imgWidth / spaceWidth), QChar::Nbsp); |
709 | if (!trailingSpace) |
710 | textOut += QLatin1Char(' '); |
711 | textOut += padding + QLatin1Char(' '); |
712 | } |
713 | |
714 | QPair<QStringView,QStringView> QQuickStyledTextPrivate::parseAttribute(const QChar *&ch, const QString &textIn) |
715 | { |
716 | skipSpace(ch); |
717 | |
718 | int attrStart = ch - textIn.constData(); |
719 | int attrLength = 0; |
720 | while (!ch->isNull()) { |
721 | if (*ch == greaterThan) { |
722 | break; |
723 | } else if (*ch == equals) { |
724 | ++ch; |
725 | if (*ch != singleQuote && *ch != doubleQuote) { |
726 | while (*ch != greaterThan && !ch->isNull()) |
727 | ++ch; |
728 | break; |
729 | } |
730 | ++ch; |
731 | if (!attrLength) |
732 | break; |
733 | auto attr = QStringView(textIn).mid(pos: attrStart, n: attrLength); |
734 | QStringView val = parseValue(ch, textIn); |
735 | if (!val.isEmpty()) |
736 | return QPair<QStringView,QStringView>(attr,val); |
737 | break; |
738 | } else { |
739 | ++attrLength; |
740 | } |
741 | ++ch; |
742 | } |
743 | |
744 | return QPair<QStringView,QStringView>(); |
745 | } |
746 | |
747 | QStringView QQuickStyledTextPrivate::parseValue(const QChar *&ch, const QString &textIn) |
748 | { |
749 | int valStart = ch - textIn.constData(); |
750 | int valLength = 0; |
751 | while (*ch != singleQuote && *ch != doubleQuote && !ch->isNull()) { |
752 | ++valLength; |
753 | ++ch; |
754 | } |
755 | if (ch->isNull()) |
756 | return QStringView(); |
757 | ++ch; // skip quote |
758 | |
759 | return QStringView(textIn).mid(pos: valStart, n: valLength); |
760 | } |
761 | |
762 | QString QQuickStyledTextPrivate::toAlpha(int value, bool upper) |
763 | { |
764 | const char baseChar = upper ? 'A' : 'a'; |
765 | |
766 | QString result; |
767 | int c = value; |
768 | while (c > 0) { |
769 | c--; |
770 | result.prepend(c: QChar(baseChar + (c % 26))); |
771 | c /= 26; |
772 | } |
773 | return result; |
774 | } |
775 | |
776 | QString QQuickStyledTextPrivate::toRoman(int value, bool upper) |
777 | { |
778 | QString result = QLatin1String("?" ); |
779 | // works for up to 4999 items |
780 | if (value < 5000) { |
781 | QByteArray romanNumeral; |
782 | |
783 | static const char romanSymbolsLower[] = "iiivixxxlxcccdcmmmm" ; |
784 | static const char romanSymbolsUpper[] = "IIIVIXXXLXCCCDCMMMM" ; |
785 | QByteArray romanSymbols; |
786 | if (!upper) |
787 | romanSymbols = QByteArray::fromRawData(data: romanSymbolsLower, size: sizeof(romanSymbolsLower)); |
788 | else |
789 | romanSymbols = QByteArray::fromRawData(data: romanSymbolsUpper, size: sizeof(romanSymbolsUpper)); |
790 | |
791 | int c[] = { 1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000 }; |
792 | int n = value; |
793 | for (int i = 12; i >= 0; n %= c[i], i--) { |
794 | int q = n / c[i]; |
795 | if (q > 0) { |
796 | int startDigit = i + (i + 3) / 4; |
797 | int numDigits; |
798 | if (i % 4) { |
799 | if ((i - 2) % 4) |
800 | numDigits = 2; |
801 | else |
802 | numDigits = 1; |
803 | } |
804 | else |
805 | numDigits = q; |
806 | romanNumeral.append(a: romanSymbols.mid(index: startDigit, len: numDigits)); |
807 | } |
808 | } |
809 | result = QString::fromLatin1(ba: romanNumeral); |
810 | } |
811 | return result; |
812 | } |
813 | |
814 | QT_END_NAMESPACE |
815 | |